--- /dev/null
+# Makefile based on https://spin.atomicobject.com/makefile-c-projects/
+TARGET_EXEC ?= typetest
+
+BUILD_DIR ?= ./build
+SRC_DIRS ?= ./src
+
+SRCS := $(shell find $(SRC_DIRS) -name *.cpp -or -name *.c -or -name *.s)
+OBJS := $(SRCS:%=$(BUILD_DIR)/%.o)
+DEPS := $(OBJS:.o=.d)
+
+INC_DIRS := $(shell find $(SRC_DIRS) -type d)
+INC_FLAGS := $(addprefix -I,$(INC_DIRS))
+
+# Flags from https://best.openssf.org/Compiler-Hardening-Guides/Compiler-Options-Hardening-Guide-for-C-and-C++.html
+CPPFLAGS ?= $(INC_FLAGS) -MMD -MP \
+ -O2 -Wall -Wformat -Wformat=2 -Wconversion -Wimplicit-fallthrough \
+ -Werror=format-security \
+ -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 \
+ -D_GLIBCXX_ASSERTIONS \
+ -fstack-clash-protection -fstack-protector-strong \
+ -Wl,-z,nodlopen -Wl,-z,noexecstack \
+ -Wl,-z,relro -Wl,-z,now \
+ -fPIE -pie \
+ -fno-delete-null-pointer-checks -fno-strict-overflow -fno-strict-aliasing \
+ -Werror=implicit -Werror=incompatible-pointer-types -Werror=int-conversion \
+ -fexceptions
+
+$(BUILD_DIR)/$(TARGET_EXEC): $(OBJS)
+ $(CC) $(CPPFLAGS) $(OBJS) -o $@ $(LDFLAGS)
+
+# Assembly
+$(BUILD_DIR)/%.s.o: %.s
+ mkdir -p $(dir $@)
+ $(AS) $(ASFLAGS) -c $< -o $@
+
+# C source
+$(BUILD_DIR)/%.c.o: %.c
+ mkdir -p $(dir $@)
+ $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
+
+# C++ source
+$(BUILD_DIR)/%.cpp.o: %.cpp
+ mkdir -p $(dir $@)
+ $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@
+
+.PHONY: debug release tidy format clangdb clean test
+
+debug: CPPFLAGS += -g -fsanitize=address -fsanitize=pointer-compare -fsanitize=pointer-subtract -fsanitize=leak -fno-omit-frame-pointer -fsanitize=undefined -fsanitize=bounds-strict -fsanitize=float-divide-by-zero -fsanitize=float-cast-overflow
+debug: $(BUILD_DIR)/$(TARGET_EXEC)
+
+release: CPPFLAGS += -fno-delete-null-pointer-checks -fno-strict-overflow -fno-strict-aliasing -ftrivial-auto-var-init=zero
+release: $(BUILD_DIR)/$(TARGET_EXEC)
+
+clangdb: clean
+ bear -- make
+
+tidy:
+ clang-tidy $(SRCS)
+
+format:
+ clang-format --style=WebKit $(SRCS)
+
+clean:
+ $(RM) -r $(BUILD_DIR)
+
+test: $(BUILD_DIR)/$(TARGET_EXEC)
+ ./src/test/test.sh
+
+-include $(DEPS)
--- /dev/null
+#ifndef LOG_H
+#define LOG_H
+
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+
+
+/* Logging macros
+Usage:
+- Define int LOGLVL
+- Use logging macros for corresponding level with printf format string
+- Use assert macros to test conditions and log/handle errors
+*/
+
+/* Use to not jump to the error label when using ASSERT:
+ #define GOTO_ERROR
+*/
+
+extern int LOGLVL;
+#define LOGLVL_ERROR 0
+#define LOGLVL_WARN 1
+#define LOGLVL_INFO 2
+#define LOGLVL_DEBUG 3
+
+#define ERRNOMSG(x) (errno ? strerror(errno) : "n/a")
+
+#define LOG_ERROR(...) if (LOGLVL >= LOGLVL_ERROR) {\
+ fprintf (stderr, "[ERROR]\t[%s:%d, %s; ERRNO: %s]\t", __FILE__, __LINE__,\
+ __func__, ERRNOMSG(errno));\
+ fprintf (stderr, __VA_ARGS__);\
+ fprintf (stderr, "\n"); }
+
+#define LOG_WARN(...) if (LOGLVL >= LOGLVL_WARN) {\
+ fprintf (stderr, "[WARN]\t[%s:%d, %s; ERRNO: %s]\t", __FILE__, __LINE__,\
+ __func__, ERRNOMSG(errno));\
+ fprintf (stderr, __VA_ARGS__);\
+ fprintf (stderr, "\n"); }
+
+#define LOG_INFO(...) if (LOGLVL >= LOGLVL_INFO) {\
+ fprintf (stderr, "[INFO]\t[%s:%d, %s; ERRNO: %s]\t", __FILE__, __LINE__,\
+ __func__, ERRNOMSG(errno));\
+ fprintf (stderr, __VA_ARGS__);\
+ fprintf (stderr, "\n"); }
+
+#define LOG_DEBUG(...) if (LOGLVL >= LOGLVL_DEBUG) {\
+ fprintf (stderr, "[DEBUG]\t[%s:%d, %s; ERRNO: %s]\t", __FILE__, __LINE__,\
+ __func__, ERRNOMSG(errno));\
+ fprintf (stderr, __VA_ARGS__);\
+ fprintf (stderr, "\n"); }
+
+#ifndef GOTO_ERROR
+#define GOTO_ERROR goto error
+#endif
+
+#define ASSERT(x, ...) if (!(x)) { LOG_ERROR(__VA_ARGS__); errno = 0; GOTO_ERROR;}
+#define ASSERT_WARN(x, ...) if (!(x)) { LOG_WARN(__VA_ARGS__); errno = 0; GOTO_ERROR;}
+#define ASSERT_INFO(x, ...) if (!(x)) { LOG_INFO(__VA_ARGS__); errno = 0; }
+#define ASSERT_DEBUG(x, ...) if (!(x)) { LOG_DEBUG(__VA_ARGS__); errno = 0; }
+
+#endif
--- /dev/null
+#include <ctype.h>
+#include <errno.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <termios.h>
+#include "log.h"
+
+#define C_BLACK "\x1b[30m"
+#define C_RED "\x1b[31m"
+#define C_GREEN "\x1b[32m"
+#define C_YELLOW "\x1b[33m"
+#define C_BLUE "\x1b[34m"
+#define C_MAGENTA "\x1b[35m"
+#define C_CYAN "\x1b[36m"
+#define C_WHITE "\x1b[37m"
+#define C_NONE "\x1b[m"
+
+#define LOGLVL LOGLVL_WARN
+
+// Limit maximum number of lines and length of lines
+#define LINE_LENGTH 120
+#define LINE_COUNT 40
+
+
+// Disable terminal echo
+struct termios setnoecho(int term) {
+ struct termios t_orig;
+ tcgetattr(term, &t_orig);
+
+ struct termios t = t_orig;
+ t.c_lflag &= (unsigned int) ~ECHO;
+ tcsetattr(term, TCSANOW, &t);
+
+ return t_orig;
+}
+
+// Restore original terminal settings
+void setecho(int term, struct termios t_orig) {
+ tcsetattr(term, TCSANOW, &t_orig);
+}
+
+// Read lines from file into an array of lines
+char **readlines(char **linearray, unsigned long num_lines, char *filename, bool use_stdin) {
+ FILE *file = NULL;
+
+ ASSERT(linearray != NULL, "No line array provided");
+
+ if (use_stdin) {
+ file = stdin;
+ } else {
+ ASSERT(filename != NULL, "No filename provided");
+ file = fopen(filename, "r");
+ ASSERT(file != NULL, "Failed to open file %s", filename);
+ }
+
+ // Read lines from file into linearray
+ char *line = NULL;
+ for (unsigned int i = 0; !feof(file) && i < num_lines; ++i) {
+ line = calloc(1, LINE_LENGTH * sizeof(char));
+
+ char *r = fgets(line, LINE_LENGTH, file);
+ if (r == NULL) {
+ free(line);
+ continue;
+ }
+ ASSERT(ferror(file) == 0, "Failed to read from file");
+
+ // Don't add blank lines
+ bool no_printable_chars = true;
+ for (unsigned int j = 0; j < LINE_LENGTH; ++j) {
+ if (line[j] == '\0') break;
+ if (isprint(line[j])) {
+ no_printable_chars = false;
+ break;
+ }
+ }
+ if (no_printable_chars || line[0] == '\n') {
+ free(line);
+ --i;
+ continue;
+ }
+ linearray[i] = line;
+ line = NULL;
+ }
+
+ return linearray;
+
+error:
+ if (file != NULL) {
+ ASSERT(fclose(file) != EOF, "Failed to close file");
+ }
+ return NULL;
+}
+
+// Read line of input into a string
+char *getinput(FILE *instream, char *line) {
+ if (fgets(line, LINE_LENGTH, instream) == NULL && feof(instream)) {
+ free(line);
+ return NULL;
+ }
+ return line;
+}
+
+// Compare input to a template string and color characters that are the same
+// green and different characters red
+void checkinput(char *template, char *input) {
+ printf("%s", C_GREEN);
+
+ unsigned long template_len = strlen(template);
+ unsigned long input_len = strlen(input);
+
+ if (input_len == 0) {
+ printf("\n");
+ return;
+ }
+
+ // Ignore leading whitespace
+ bool leading_whitespace = true;
+ for (unsigned long i = 0, j = 0; i < template_len && j < input_len; ++i, ++j) {
+ // Advance to at least first non-whitespace character
+ if (leading_whitespace) {
+ while (!isgraph(template[i]) && i < template_len) {
+ ++i;
+ }
+ while (!isgraph(input[j]) && j < input_len) {
+ ++j;
+ }
+ leading_whitespace = false;
+ }
+
+ if (template[i] == input[j]) {
+ printf("%c", template[i]);
+ } else {
+ printf("%s%c%s", C_RED, input[j], C_GREEN);
+ }
+ }
+ printf("\n%s", C_NONE);
+}
+
+// Interactive typing loop
+void loop(char **linearray, FILE *instream) {
+ char *input = malloc(LINE_LENGTH);
+ for (unsigned int i = 0; i < LINE_COUNT; ++i) {
+ if (linearray[i] == NULL || strcmp(linearray[i], "") == 0) {
+ break;
+ }
+ printf("%s>%s %s%s ", C_CYAN, C_YELLOW, linearray[i], C_NONE);
+
+ getinput(instream, input);
+ if (input == NULL) return;
+
+ checkinput(linearray[i], input);
+ }
+ free(input);
+}
+
+int main(int argc, char **argv) {
+ bool use_stdin = false;
+
+ if (argc > 2 || (argc == 1 && isatty(0))) {
+ printf("Usage: %s filename\n", argv[0]);
+ return 1;
+ } else {
+ // Reading input from a pipe
+ if (!isatty(0)) {
+ use_stdin = true;
+ }
+ }
+ FILE *termf = NULL;
+
+ char **linearray = calloc(LINE_COUNT, LINE_LENGTH * sizeof(char *));
+ ASSERT(linearray != NULL, "Failed to allocate linearray");
+
+ char **rl = readlines(linearray, LINE_COUNT, argv[1], use_stdin);
+ if (rl == NULL) return 1;
+
+ // Get path for controlling terminal and open fd
+ char termpath[L_ctermid];
+ ctermid(termpath);
+ termf = fopen(termpath, "r+");
+ int term = fileno(termf);
+
+ // Disable echoing inputted characters
+ struct termios t_orig = setnoecho(term);
+
+ // Run the typing test
+ loop(linearray, termf);
+
+ // Reenable echoing
+ setecho(term, t_orig);
+
+ for (unsigned int i = 0; i < LINE_COUNT; ++i) {
+ free(linearray[i]);
+ }
+ free(linearray);
+ if (termf) {
+ if (fclose(termf) == EOF) return 1;
+ }
+ return 0;
+
+error:
+ for (unsigned int i = 0; i < LINE_COUNT; ++i) {
+ free(linearray[i]);
+ }
+ free(linearray);
+ if (termf) {
+ if (fclose(termf) == EOF) return 1;
+ }
+ return 1;
+}