git home / emma home
logo

typie

Minimal typing practice tool.
git clone https://git.y1.nz/archives/typie.tar.gz
README | Files | Log | Refs | LICENSE

commit a34ddd823a871565d7ef98f908499d8cf968b6cc
parent 5e6cb3acf0536d3b974889778fcf5a063ca4239a
Author: Emma Weaver <emma@waeaves.com>
Date:   Sun, 12 Apr 2026 22:25:35 -0400

post-refactor functioning state

Diffstat:
MMakefile15+++++++++++++--
AREADME.md9+++++++++
Aconfig.def.h4++++
Aconfig.h4++++
R1k.txt -> dicts/1k.txt0
R20k.txt -> dicts/20k.txt0
R2k.txt -> dicts/2k.txt0
Amain.c92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Arandom.c20++++++++++++++++++++
Arandom.h7+++++++
Aterm_controls.c28++++++++++++++++++++++++++++
Aterm_controls.h5+++++
Dtermtype.c250-------------------------------------------------------------------------------
Atyper.c131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atyper.h43+++++++++++++++++++++++++++++++++++++++++++
Awords.c79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awords.h18++++++++++++++++++
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 *); +

This webpage is intended to be an accessible preview of this repository. To get a fuller picture, clone it and use the git CLI.