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 67e5183f74a74c4ad41465e9d29f3fefb8507283
parent a34ddd823a871565d7ef98f908499d8cf968b6cc
Author: Emma Weaver <emma@waeaves.com>
Date:   Wed, 15 Apr 2026 10:51:46 -0400

set it up for password-typing practice

Diffstat:
M.gitignore2+-
MMakefile16+++++++---------
MREADME.md12+++++-------
Dconfig.def.h4----
Aconfig.default.h9+++++++++
Mconfig.h7++++++-
Aconfig.mk13+++++++++++++
Mmain.c121+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Aprompt.c102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mrandom.c4----
Drandom.h7-------
Dterm_controls.c28----------------------------
Dterm_controls.h5-----
Mtyper.c89++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Mtyper.h53++++++++++++++++++++++++++++++++++++-----------------
Dwords.c79-------------------------------------------------------------------------------
Dwords.h18------------------
17 files changed, 301 insertions(+), 268 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1,3 +1,3 @@ -tt +typie *.o diff --git a/Makefile b/Makefile @@ -1,13 +1,11 @@ -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 +include config.mk -config.h: config.def.h - cp config.def.h config.h +main: main.c typer.h typer.c prompt.c random.c + $(CC) main.c -o $(PREFIX)/typie + +config.h: config.default.h + cp config.default.h config.h clean: - rm -f tt + rm -f $(PREFIX)/typie diff --git a/README.md b/README.md @@ -1,9 +1,7 @@ -# Kittype +# Typie -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. +This is a suckless-style password typing practice tool. The default +configuration is for practicing typing xkcd passwords (https://xkcd.com/936/). +To change the configuration, edit the code to match your use case and recompile +it. -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 @@ -1,4 +0,0 @@ - -const char * dictionary_file = "./dicts/20k.txt"; -const int target_word_count = 12; - diff --git a/config.default.h b/config.default.h @@ -0,0 +1,9 @@ + +const char * dictionary_file = "./dicts/20k.txt"; + +const int words_per_line = 3; +const int lines_per_run = 5; + +const int display_while_typing = 0; +const char word_delimiter = ' '; + diff --git a/config.h b/config.h @@ -1,4 +1,9 @@ const char * dictionary_file = "./dicts/20k.txt"; -const int target_word_count = 12; + +const int words_per_line = 3; +const int lines_per_run = 5; + +const int display_while_typing = 0; +const char word_delimiter = ' '; diff --git a/config.mk b/config.mk @@ -0,0 +1,13 @@ +# version +VERSION = 0.0.0 + +# Customize below to fit your system + +# paths +PREFIX = . +MANPREFIX = ./man +# PREFIX = /usr/local +# MANPREFIX = $(PREFIX)/share/man + +# compiler and linker +# CC = c99 diff --git a/main.c b/main.c @@ -1,92 +1,115 @@ -#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> + +#include "typer.h" +#include "random.c" +#include "prompt.c" +#include "typer.c" char word_buffer[1024]; int main(int argc, char *argv[]) { - struct termios term_og; + struct termios termios_original; struct timespec t1, t2; - words prompt; - typed counts = {0}; - type_state state = { 0, -1 }; + Words prompt; + State state; int keypress_kind; - bool started = false; + int started = 0; - /* 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); - } + state.index = 0; + state.error_index = -1; - tcgetattr(0, &term_og); - setup_termios(term_og); + prompt.len = 0; + prompt.str = word_buffer; + int chars_total = 0, + chars_correct = 0, + lines_submitted = 0, + lines_failed = 0, + chars_backspace = 0; + + tcgetattr(0, &termios_original); + setup_termios(termios_original); + + prompt = create_prompt( + dictionary_file, + word_buffer, + words_per_line, + word_delimiter + ); start_typing(prompt, &state); fflush(stdout); - for(;;) { + while (lines_submitted < lines_per_run) { keypress_kind = handle_keypress(prompt, &state); - fflush(stdout); + if (display_while_typing) + fflush(stdout); - switch(keypress_kind) { - case keypress_exit: - restore_termios(term_og); + switch (keypress_kind) { + case KEYPRESS_EXIT: + fflush(stdout); + restore_termios(termios_original); return 1; - case keypress_correct: - counts.correct++; - /* fallthrough */ - case keypress_incorrect: - counts.total++; - if (!started && counts.total != 0) { - started = true; + case KEYPRESS_CORRECT: + chars_correct++; + /* FALLTHROUGH */ + case KEYPRESS_INCORRECT: + chars_total++; + if (!started && chars_total != 0) { + started = 1; clock_gettime(CLOCK_MONOTONIC, &t1); } break; - case keypress_next: - // TODO clear and repopulate prompt with a new one - /* FALLTHROUGH */ - case keypress_skip: + case KEYPRESS_NEXT: + lines_submitted++; + if (lines_submitted >= lines_per_run) + break; + prompt = create_prompt( + dictionary_file, + word_buffer, + words_per_line, + word_delimiter + ); + start_typing(prompt, &state); + fflush(stdout); + break; + + case KEYPRESS_SKIP: + lines_failed++; start_typing(prompt, &state); fflush(stdout); break; - case keypress_none: - /* nothing. */ - break; + case KEYPRESS_BACKSPACE: + chars_backspace++; + break; } } clock_gettime(CLOCK_MONOTONIC, &t2); + restore_termios(termios_original); + printf(COLOR_RESET); + { 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; + int words_per_min = (chars_correct / 5.0) / min; + int keys_per_min = (chars_total) / min; + int accuracy = ((double) chars_correct / chars_total) / 100; - printf(COLOR_RESET "\n\rwpm: %d", wpm); - printf(COLOR_RESET "\n\racc: %d%%", acc); - printf(COLOR_RESET "\n\rraw: %d", raw); + printf("retries" "\t\t" "%d" "\n", lines_failed); + printf("backspaces" "\t" "%d" "\n", chars_backspace); + printf("words/min" "\t" "%d" "\n", words_per_min); + printf("keys/min" "\t" "%d" "\n", keys_per_min); + printf("accuracy" "\t" "%d%%" "\n", accuracy); } - restore_termios(term_og); return 0; } diff --git a/prompt.c b/prompt.c @@ -0,0 +1,102 @@ + +uint64_t seed; + +/* returns size of file. */ +FILE * +init_file(const char * filename) { + int size; + FILE * f = fopen(filename, "r"); + + if (f == NULL) { + printf("File %s could not be opened." "\n", filename, stderr); + exit(1); + } + + return f; +} + +size_t +get_file_size(FILE * f) +{ + size_t ret; + + fseek(f, 0, SEEK_END); + ret = ftell(f); + rewind(f); + + return ret; +} + +int +load_word_at(FILE * f, char * buffer, int r) { + int c, len = 0; + + fseek(f, r, SEEK_SET); + + /* step back until start of word */ + for (;;) { + c = fgetc(f); + if (!IS_LETTER(c) || r <= 0) + break; + fseek(f, -2, SEEK_CUR); + r -= 2; + } + + if (ftell(f) <= 1) + rewind(f); + + /* copy word into buffer */ + for (;;) { + c = fgetc(f); + if (!IS_LETTER(c) || len > 20) + break; + buffer[len] = c; + len++; + } + + return len; +} + +Words +create_prompt( + const char * dictionary_filename, + char * ret_buffer, + int target_word_count, + char delimiter +) +{ + Words ret = {0}; + FILE * f; + size_t size; + int word_count = 0; + + ret.str = ret_buffer; + f = init_file(dictionary_filename); + size = get_file_size(f); + + if (seed == 0) + seed = (uint64_t) &seed; + seed ^= (uint64_t) &target_word_count * 11111111111; seed ^= seed >> 32; + seed ^= (uint64_t) &f * 55555555555; seed ^= seed >> 32; + + while (word_count < target_word_count) { + int r = get_random(&seed, size); + ret.len += load_word_at(f, ret.str + ret.len, r); + word_count++; + + ret.str[ret.len] = delimiter; + ret.len++; + } + + if (ret.str[ret.len-1] != delimiter || ret.len == 0) { + printf("Error creating prompt, exiting." "\n", stderr); + exit(1); + } + + /* replace trailing space with terminator */ + ret.str[ret.len-1] = '\0'; + ret.len--; + + return ret; +} + diff --git a/random.c b/random.c @@ -1,7 +1,3 @@ -#include "random.h" - -#include <stdint.h> - uint64_t step_random(uint64_t * state) diff --git a/random.h b/random.h @@ -1,7 +0,0 @@ -#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 @@ -1,28 +0,0 @@ -#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 @@ -1,5 +0,0 @@ -#include <termios.h> - - -void setup_termios(struct termios); -void restore_termios(struct termios); diff --git a/typer.c b/typer.c @@ -1,17 +1,25 @@ -#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 +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"); +} void cursor_back(int steps) @@ -26,9 +34,9 @@ cursor_forward(int steps) } void -on_correct(type_state * state, char c) +on_correct(State * state, char c) { - if(state->error_index == -1) + if (state->error_index == -1) printf(COLOR_GREEN "%c", c); else printf(COLOR_YELLOW "%c", c); @@ -37,40 +45,41 @@ on_correct(type_state * state, char c) } void -on_incorrect(words prompt, type_state * state, char c) +on_incorrect(Words prompt, State * state, char c) { - if(state->error_index == -1) + if (state->error_index == -1) state->error_index = state->index; - if (is_blank(prompt.str[state->index])) { + /* typed char in space */ + 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)) { + /* char in char */ + 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]); + /* space in char */ + if (IS_BLANK(c)) { + printf(COLOR_PALE_RED "\u00B7"); state->index++; } } void -on_backspace(words prompt, type_state * state) +on_backspace(Words prompt, State * state) { if (state->index == 0) return; { char to_restore = ' '; - if(state->index < prompt.len) + if (state->index < prompt.len) to_restore = prompt.str[state->index - 1]; cursor_back(1); @@ -81,51 +90,53 @@ on_backspace(words prompt, type_state * state) state->index--; - if(state->error_index == state->index) + if (state->error_index == state->index) state->error_index = -1; } int -on_enter(words prompt, type_state * state) +on_enter(Words prompt, State * state) { - if(state->error_index >= 0 || state->index != prompt.len) { - printf(COLOR_RED "\u274C\n\r" COLOR_RESET); /* cross */ - return keypress_skip; + if (state->error_index >= 0 || state->index != prompt.len) { + printf(COLOR_RED "\u274C" CLEAR_LINE); /* cross */ + printf("\n\r" COLOR_RESET); + return KEYPRESS_SKIP; } - printf(COLOR_GREEN " \u25CB\n\r" COLOR_RESET); /* circle */ - return keypress_next; + printf(COLOR_GREEN " \u25CB"); /* circle */ + printf("\n\r" COLOR_RESET); + return KEYPRESS_NEXT; } void -start_typing(words prompt, type_state * state) +start_typing(Words prompt, State * state) { state->index = 0; state->error_index = -1; - printf("%s" "\r", prompt.str); + printf("%s" "\n" "\r", prompt.str); } int -handle_keypress(words prompt, type_state * state) +handle_keypress(Words prompt, State * state) { char c = getchar(); if (c == KEY_CTRL_C || c == KEY_CTRL_D || c == KEY_ESCAPE) - return keypress_exit; + return KEYPRESS_EXIT; - if(c == KEY_ENTER) + if (c == KEY_ENTER) return on_enter(prompt, state); if (c == KEY_BACKSPACE) { on_backspace(prompt, state); - return keypress_none; + return KEYPRESS_BACKSPACE; } if (c == prompt.str[state->index]) { on_correct(state, c); - return keypress_correct; + return KEYPRESS_CORRECT; } on_incorrect(prompt, state, c); - return keypress_incorrect; + return KEYPRESS_INCORRECT; } diff --git a/typer.h b/typer.h @@ -1,7 +1,12 @@ #include <stddef.h> #include <time.h> +#include <termios.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> +#include <stdint.h> -#include "words.h" +#include "config.h" #define COLOR_GREEN "\033[1;32m" @@ -10,6 +15,7 @@ #define COLOR_PALE_RED "\033[0;31m" #define COLOR_BOLD "\033[1;37m" #define COLOR_RESET "\033[0m" +#define CLEAR_LINE "\033[0K" #define KEY_CTRL_C 3 #define KEY_CTRL_D 4 @@ -17,27 +23,40 @@ #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 */ -}; +#define IS_BLANK(X) ((X) == ' ' || (X) == '\t') +#define IS_NUMBER(X) ((X) >= '0' && (X) <= '9') +#define IS_LETTER(X) (((X) >= 'a' && (X) <= 'z') || ((X) >= 'A' && (X) <= 'Z')) +#define IS_ALPHANUMERIC(X) (IS_LETTER(X) || IS_NUMBER(X)) -typedef struct { +#define KEYPRESS_CORRECT 0 +#define KEYPRESS_INCORRECT 1 +#define KEYPRESS_BACKSPACE 2 +#define KEYPRESS_EXIT 3 +#define KEYPRESS_SKIP 4 /* current line incomplete */ +#define KEYPRESS_NEXT 5 /* current line complete */ + +; + +typedef struct wrds { + char * str; + size_t len; +} Words; + +typedef struct stt { int index; int error_index; /* index of the first incorrect character, or -1 */ -} type_state; +} State; + +uint64_t step_random(uint64_t *); +uint64_t get_random(uint64_t *, int); + +Words create_prompt(const char *, char *, int, char); -typedef struct { - int total; - int correct; -} typed; +void setup_termios(struct termios); +void restore_termios(struct termios); void cursor_back(int); void cursor_forward(int); +void start_typing(Words, State *); +int handle_keypress(Words, State *); -void start_typing(words, type_state *); -int handle_keypress(words, type_state *); diff --git a/words.c b/words.c @@ -1,79 +0,0 @@ -#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 @@ -1,18 +0,0 @@ -#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.