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:
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 *);
-