commit a34ddd823a871565d7ef98f908499d8cf968b6cc
parent 5e6cb3acf0536d3b974889778fcf5a063ca4239a
Author: Emma Weaver <emma@waeaves.com>
Date: Sun, 12 Apr 2026 22:25:35 -0400
post-refactor functioning state
Diffstat:
17 files changed, 453 insertions(+), 252 deletions(-)
diff --git a/Makefile b/Makefile
@@ -1,2 +1,13 @@
-termtype:
- cc termtype.c -o tt
+
+main: words.c words.h
+main: term_controls.c term_controls.h
+main: random.c random.h
+main: typer.c typer.h
+main: main.c
+ cc -o tt main.c term_controls.c typer.c words.c random.c
+
+config.h: config.def.h
+ cp config.def.h config.h
+
+clean:
+ rm -f tt
diff --git a/README.md b/README.md
@@ -0,0 +1,9 @@
+# Kittype
+
+This is a typing practice program designed to improve speed with accuracy for
+things like typing passwords and programming, where you need to correct any
+errors you might make.
+
+It's meant to comply with the suckless style, being minimal, clean, and
+configuration-less. It's my first foray into this particular environment though
+so it's probably a little rough.
diff --git a/config.def.h b/config.def.h
@@ -0,0 +1,4 @@
+
+const char * dictionary_file = "./dicts/20k.txt";
+const int target_word_count = 12;
+
diff --git a/config.h b/config.h
@@ -0,0 +1,4 @@
+
+const char * dictionary_file = "./dicts/20k.txt";
+const int target_word_count = 12;
+
diff --git a/1k.txt b/dicts/1k.txt
diff --git a/20k.txt b/dicts/20k.txt
diff --git a/2k.txt b/dicts/2k.txt
diff --git a/main.c b/main.c
@@ -0,0 +1,92 @@
+#include "config.h"
+#include "typer.h"
+#include "random.h"
+#include "term_controls.h"
+
+#include <time.h>
+#include <termios.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdbool.h>
+
+char word_buffer[1024];
+
+int
+main(int argc, char *argv[])
+{
+ struct termios term_og;
+ struct timespec t1, t2;
+ words prompt;
+ typed counts = {0};
+ type_state state = { 0, -1 };
+ int keypress_kind;
+ bool started = false;
+
+ /* load words */
+ prompt = pick_words(word_buffer, dictionary_file, target_word_count, sizeof word_buffer);
+ if (prompt.len == 0) { /* cppcheck-suppress uninitvar */
+ fprintf(stderr, "Couldn't open dict\n");
+ exit(1);
+ }
+
+ tcgetattr(0, &term_og);
+ setup_termios(term_og);
+
+ start_typing(prompt, &state);
+ fflush(stdout);
+
+ for(;;) {
+ keypress_kind = handle_keypress(prompt, &state);
+ fflush(stdout);
+
+ switch(keypress_kind) {
+ case keypress_exit:
+ restore_termios(term_og);
+ return 1;
+
+ case keypress_correct:
+ counts.correct++;
+ /* fallthrough */
+ case keypress_incorrect:
+ counts.total++;
+ if (!started && counts.total != 0) {
+ started = true;
+ clock_gettime(CLOCK_MONOTONIC, &t1);
+ }
+ break;
+
+ case keypress_next:
+ // TODO clear and repopulate prompt with a new one
+ /* FALLTHROUGH */
+ case keypress_skip:
+ start_typing(prompt, &state);
+ fflush(stdout);
+ break;
+
+ case keypress_none:
+ /* nothing. */
+ break;
+ }
+ }
+
+ clock_gettime(CLOCK_MONOTONIC, &t2);
+
+ {
+ double dif = (
+ (t2.tv_sec - t1.tv_sec) * 1000) +
+ ((t2.tv_nsec - t1.tv_nsec) / 1E6
+ );
+ double min = dif / (60.0 * 1000);
+ int raw = (counts.total / 5.0) / min;
+ int wpm = (counts.correct / 5.0) / min;
+ int acc = ((double) counts.correct / counts.total) * 100;
+
+ printf(COLOR_RESET "\n\rwpm: %d", wpm);
+ printf(COLOR_RESET "\n\racc: %d%%", acc);
+ printf(COLOR_RESET "\n\rraw: %d", raw);
+ }
+
+ restore_termios(term_og);
+ return 0;
+}
diff --git a/random.c b/random.c
@@ -0,0 +1,20 @@
+#include "random.h"
+
+#include <stdint.h>
+
+
+uint64_t
+step_random(uint64_t * state)
+{
+ uint64_t old = * state;
+ *state *= 1111111111111111111;
+ *state += 0x1337;
+ return old ^ (old >> (old >> 59));
+}
+
+uint64_t
+get_random(uint64_t * state, int max)
+{
+ return step_random(state) % max;
+}
+
diff --git a/random.h b/random.h
@@ -0,0 +1,7 @@
+#include <stdint.h>
+
+
+uint64_t step_random(uint64_t *);
+
+uint64_t get_random(uint64_t *, int);
+
diff --git a/term_controls.c b/term_controls.c
@@ -0,0 +1,28 @@
+#include "term_controls.h"
+
+#include "typer.h"
+
+#include <stdio.h>
+
+void
+setup_termios(struct termios term_og)
+{
+ struct termios term_raw;
+ term_raw = term_og; term_raw.c_iflag &= ~(
+ IGNBRK | BRKINT | PARMRK | ISTRIP |
+ INLCR | IGNCR | ICRNL | IXON
+ );
+ term_raw.c_oflag &= ~OPOST;
+ term_raw.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
+ term_raw.c_cflag &= ~(CSIZE | PARENB);
+ term_raw.c_cflag |= CS8;
+ tcsetattr(0, 0, &term_raw);
+}
+
+void
+restore_termios(struct termios term_og)
+{
+ tcsetattr(0, 0, &term_og);
+ printf(COLOR_RESET "\n");
+}
+
diff --git a/term_controls.h b/term_controls.h
@@ -0,0 +1,5 @@
+#include <termios.h>
+
+
+void setup_termios(struct termios);
+void restore_termios(struct termios);
diff --git a/termtype.c b/termtype.c
@@ -1,250 +0,0 @@
-#include <stdio.h>
-#include <string.h>
-#include <time.h>
-#include <termios.h>
-#include <unistd.h>
-#include <stdlib.h>
-#include <stdint.h>
-
-#define COL_GREEN "\033[1;32m"
-#define COL_RED "\033[1;31m"
-#define COL_PALE_RED "\033[0;31m"
-#define COL_BOLD "\033[1;37m"
-#define COL_RESET "\033[0m"
-
-#define CUR_DEL(X) "\033[" #X "D"
-
-#define isblank(X) ((X) == ' ' || (X) == '\t')
-#define isalpha(X) (((X) >= 'a' && (X) <= 'z') || ((X) >= 'A' && (X) <= 'Z'))
-#define isnum(X) ((X) >= '0' && (X) <= '9')
-#define isalnum(X) (isalpha(X) || isnum(X))
-
-#define ARRLEN(X) (sizeof(X) / sizeof((X)[0]))
-
-typedef uint32_t u32;
-typedef uint64_t u64;
-
-enum {
- key_ctrl_c = 3,
- key_ctrl_d = 4,
- key_enter = 13,
- key_escape = 27,
- key_del = 127
-};
-
-typedef struct {
- const char *str;
- size_t len;
-} Words;
-
-/* static globals */
-
-static char wbuf[1024];
-
-/* function implementation */
-
-static void
-cur_move_back(int n)
-{
- printf("\033[%dD", n);
-}
-
-static void
-cur_move_forward(int n)
-{
- printf("\033[%dC", n);
-}
-
-static u32
-rng(u64 *state)
-{
- u64 old = *state;
- *state *= 1111111111111111111;
- *state += 0x1337;
- return old ^ (old >> (old >> 59));
-}
-
-static u64
-rng_bound(u64 *state, u32 bound)
-{
- u32 mask = (u32)-1 >> __builtin_clz(bound | 0x1);
- for (;;) {
- u32 r = rng(state) & mask;
- if (r < bound) {
- return r;
- }
- }
-}
-
-static Words
-get_words(const char *path, int wtarget)
-{
- Words ret = {0};
- FILE *f;
- size_t size;
- int wcnt = 0;
- u64 state;
-
- if (path == NULL)
- return ret;
-
- if ((f = fopen(path, "r")) == NULL)
- return ret;
- fseek(f, 0, SEEK_END);
- size = ftell(f);
- rewind(f);
-
- state = (u64)&state;
- state ^= (u64)&get_words * 11111111111; state ^= state >> 32;
- state ^= (u64)&f * 55555555555; state ^= state >> 32;
- while (wcnt < wtarget && ret.len < sizeof wbuf) {
- int r, c;
-
- r = rng_bound(&state, size);
- fseek(f, r, SEEK_SET);
- while ((c = fgetc(f)) != '\n' && ftell(f) > 1)
- fseek(f, -2, SEEK_CUR);
- if (ftell(f) <= 1)
- rewind(f);
- while ((c = fgetc(f)) != '\n' && isalpha(c) && ret.len < sizeof wbuf - 1)
- wbuf[ret.len++] = c;
- wbuf[ret.len++] = ' ';
- ++wcnt;
- }
- if (wbuf[ret.len-1] == ' ')
- wbuf[--ret.len] = '\0';
-
- ret.str = wbuf;
- return ret;
-}
-
-int
-main(int argc, char *argv[])
-{
- int rc = 0;
- Words input;
- struct termios term_og;
- struct timespec t1, t2;
- int typed, correct;
- int wtarget = 12;
-
- {
- size_t i;
- const char *files[3] = {
- NULL,
- "words",
- "/usr/share/termtype/words"
- };
- files[0] = argc < 2 ? NULL : argv[1];
-
- for (i = 0; i < ARRLEN(files); ++i) {
- input = get_words(files[i], wtarget);
- if (input.len > 0)
- break;
- }
- }
- if (input.len == 0) { /* cppcheck-suppress uninitvar */
- fprintf(stderr, "Couldn't open dict\n");
- exit(1);
- }
-
- {
- struct termios term_raw;
- tcgetattr(0, &term_og);
- term_raw = term_og;
- term_raw.c_iflag &= ~(
- IGNBRK | BRKINT | PARMRK | ISTRIP |
- INLCR | IGNCR | ICRNL | IXON
- );
- term_raw.c_oflag &= ~OPOST;
- term_raw.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
- term_raw.c_cflag &= ~(CSIZE | PARENB);
- term_raw.c_cflag |= CS8;
- tcsetattr(0, 0, &term_raw);
- }
-
- printf("%s\r", input.str);
-
- {
- int i, b;
- for (
- i = b = typed = correct = 0;
- i < (int)input.len;
- /* no-op */
- ) {
- int n, c = getchar();
-
- if (typed == 0 && isalnum(c)) /* count time from the first input */
- clock_gettime(CLOCK_MONOTONIC, &t1);
-
- switch (c) {
- case key_ctrl_c:
- rc = 1;
- /* fallthrough */
- case key_ctrl_d:
- case key_enter:
- case key_escape:
- case EOF:
- goto out;
- break;
- case key_del: /* backspace */
- printf(CUR_DEL(1) COL_RESET);
- n = printf(
- "%s" " ",
- i == 0 ?
- input.str :
- b && isblank(input.str[i]) ?
- input.str+i :
- input.str+i - 1,
- );
- cur_move_back(n);
- i = b ? i : i > 0 ? i - 1 : 0;
- b = b > 0 ? b - 1 : 0;
- break;
- default:
- if (c == input.str[i]) {
- printf(COL_GREEN "%c", input.str[i]);
- ++i;
- ++correct;
- } else {
- if (isblank(input.str[i])) {
- printf(COL_PALE_RED "%c", c);
- printf(COL_RESET "%s", input.str+i);
- cur_move_back(input.len - i);
- ++b;
- } else if (isalnum(c)) {
- printf(COL_RED "%c", input.str[i]);
- ++i;
- } else if (isblank(c)) {
- int p = i;
- printf(COL_RED "%c", input.str[i]);
- while (isalnum(input.str[i]))
- ++i;
- ++i;
- cur_move_forward(i - p - 1);
- }
- }
- ++typed;
- break;
- }
- fflush(stdout);
- }
- }
-
- clock_gettime(CLOCK_MONOTONIC, &t2);
- {
- double dif = ((t2.tv_sec - t1.tv_sec) * 1000) + ((t2.tv_nsec - t1.tv_nsec) / 1E6);
- double min = dif / (60.0 * 1000);
- int raw = (typed / 5.0) / min;
- int wpm = (correct / 5.0) / min;
- int acc = ((double)correct / typed) * 100;
-
- printf(COL_RESET "\n\rwpm: %d", wpm);
- printf(COL_RESET "\n\racc: %d%%", acc);
- printf(COL_RESET "\n\rraw: %d", raw);
- }
-out:
- tcsetattr(0, 0, &term_og);
- printf(COL_RESET "\n");
- return rc;
-}
diff --git a/typer.c b/typer.c
@@ -0,0 +1,131 @@
+#include "typer.h"
+
+#include "random.h"
+#include "term_controls.h"
+
+#include <stdio.h>
+#include <string.h>
+#include <time.h>
+#include <termios.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <stddef.h>
+
+
+void
+cursor_back(int steps)
+{
+ printf("\033[" "%d" "D", steps);
+}
+
+void
+cursor_forward(int steps)
+{
+ printf("\033[" "%d" "C", steps);
+}
+
+void
+on_correct(type_state * state, char c)
+{
+ if(state->error_index == -1)
+ printf(COLOR_GREEN "%c", c);
+ else
+ printf(COLOR_YELLOW "%c", c);
+
+ state->index++;
+}
+
+void
+on_incorrect(words prompt, type_state * state, char c)
+{
+ if(state->error_index == -1)
+ state->error_index = state->index;
+
+ if (is_blank(prompt.str[state->index])) {
+ printf(COLOR_PALE_RED "%c", c);
+ // printf(COLOR_RESET "%s", prompt.str + state->index);
+ // cursor_back(prompt.len - state->index);
+ state->index++;
+ return;
+ }
+
+ if (is_alphanumeric(c)) {
+ printf(COLOR_RED "%c", c); // prompt.str[state->index]);
+ state->index++;
+ return;
+ }
+
+ if (is_blank(c)) {
+ printf(COLOR_PALE_RED "%c", prompt.str[state->index]);
+ state->index++;
+ }
+}
+
+void
+on_backspace(words prompt, type_state * state)
+{
+ if (state->index == 0)
+ return;
+
+ {
+ char to_restore = ' ';
+ if(state->index < prompt.len)
+ to_restore = prompt.str[state->index - 1];
+
+ cursor_back(1);
+ printf(COLOR_RESET);
+ printf("%c", to_restore);
+ cursor_back(1);
+ }
+
+ state->index--;
+
+ if(state->error_index == state->index)
+ state->error_index = -1;
+}
+
+int
+on_enter(words prompt, type_state * state)
+{
+ if(state->error_index >= 0 || state->index != prompt.len) {
+ printf(COLOR_RED "\u274C\n\r" COLOR_RESET); /* cross */
+ return keypress_skip;
+ }
+
+ printf(COLOR_GREEN " \u25CB\n\r" COLOR_RESET); /* circle */
+ return keypress_next;
+}
+
+void
+start_typing(words prompt, type_state * state)
+{
+ state->index = 0;
+ state->error_index = -1;
+ printf("%s" "\r", prompt.str);
+}
+
+int
+handle_keypress(words prompt, type_state * state)
+{
+ char c = getchar();
+
+ if (c == KEY_CTRL_C || c == KEY_CTRL_D || c == KEY_ESCAPE)
+ return keypress_exit;
+
+ if(c == KEY_ENTER)
+ return on_enter(prompt, state);
+
+ if (c == KEY_BACKSPACE) {
+ on_backspace(prompt, state);
+ return keypress_none;
+ }
+
+ if (c == prompt.str[state->index]) {
+ on_correct(state, c);
+ return keypress_correct;
+ }
+
+ on_incorrect(prompt, state, c);
+ return keypress_incorrect;
+}
diff --git a/typer.h b/typer.h
@@ -0,0 +1,43 @@
+#include <stddef.h>
+#include <time.h>
+
+#include "words.h"
+
+
+#define COLOR_GREEN "\033[1;32m"
+#define COLOR_YELLOW "\033[1;33m"
+#define COLOR_RED "\033[1;31m"
+#define COLOR_PALE_RED "\033[0;31m"
+#define COLOR_BOLD "\033[1;37m"
+#define COLOR_RESET "\033[0m"
+
+#define KEY_CTRL_C 3
+#define KEY_CTRL_D 4
+#define KEY_ENTER 13
+#define KEY_ESCAPE 27
+#define KEY_BACKSPACE 127
+
+enum {
+ keypress_correct = 0,
+ keypress_incorrect = 1,
+ keypress_none = 2,
+ keypress_exit = 3,
+ keypress_skip = 4, /* current line incomplete */
+ keypress_next = 5 /* curretn line complete */
+};
+
+typedef struct {
+ int index;
+ int error_index; /* index of the first incorrect character, or -1 */
+} type_state;
+
+typedef struct {
+ int total;
+ int correct;
+} typed;
+
+void cursor_back(int);
+void cursor_forward(int);
+
+void start_typing(words, type_state *);
+int handle_keypress(words, type_state *);
diff --git a/words.c b/words.c
@@ -0,0 +1,79 @@
+#include "words.h"
+
+#include "random.h"
+
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+// load_random_word(char * buffer, File * )
+
+// TODO clean up and rewrite all of this and rename it to prompts.c/h
+
+words
+pick_words(
+ char * ret_buffer,
+ const char * path,
+ int target_word_count,
+ int max_length
+)
+{
+ words ret = {0};
+ FILE * f;
+ size_t size;
+ int word_count = 0;
+ uint64_t state;
+
+ if (path == NULL)
+ return ret;
+
+ f = fopen(path, "r");
+ if (f == NULL)
+ return ret;
+
+ fseek(f, 0, SEEK_END);
+ size = ftell(f);
+ rewind(f);
+
+ state = (uint64_t) &state;
+ state ^= (uint64_t) &pick_words * 11111111111; state ^= state >> 32;
+ state ^= (uint64_t) &f * 55555555555; state ^= state >> 32;
+ while (word_count < target_word_count && ret.len < max_length) {
+ int r, c;
+
+ r = get_random(&state, size);
+ fseek(f, r, SEEK_SET);
+
+ while (
+ (c = fgetc(f)) != '\n' &&
+ ftell(f) > 1
+ )
+ fseek(f, -2, SEEK_CUR);
+
+ if (ftell(f) <= 1)
+ rewind(f);
+
+ while (
+ (c = fgetc(f)) != '\n' &&
+ is_letter(c) &&
+ ret.len < max_length - 1
+ )
+ ret_buffer[ret.len++] = c;
+
+ ret_buffer[ret.len++] = ' ';
+ word_count++;
+ }
+
+ if (ret_buffer[ret.len-1] == ' ')
+ ret_buffer[--ret.len] = '\0';
+
+ ret.str = ret_buffer;
+
+ if (ret.len == 0) {
+ fprintf(stderr, "Couldn't open dict\n");
+ exit(1);
+ }
+
+ return ret;
+}
+
diff --git a/words.h b/words.h
@@ -0,0 +1,18 @@
+#include <stddef.h>
+
+#define is_blank(X) ((X) == ' ' || (X) == '\t')
+#define is_letter(X) (((X) >= 'a' && (X) <= 'z') || ((X) >= 'A' && (X) <= 'Z'))
+#define is_number(X) ((X) >= '0' && (X) <= '9')
+#define is_alphanumeric(X) (is_letter(X) || is_number(X))
+
+#pragma once
+typedef struct {
+ char * str;
+ size_t len;
+} words;
+
+words pick_words(char *, const char *, int, int);
+
+// TODO
+// void load_dictionary(char *);
+