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 c0f2af297b772d0f28580c35254171494d0a3c79
parent 42cb383c7faf7a1bdf7b78339349f31356893d3d
Author: Emma Weaver <emma@waeaves.com>
Date:   Thu, 30 Apr 2026 12:07:37 -0400

simplified responsibilities, added flags, added dvorak-learning scripts

Diffstat:
MMakefile26+++++++++++++++++++++-----
AREADME29+++++++++++++++++++++++++++++
DREADME.md7-------
Dconfig.default.h9---------
Dconfig.h9---------
Mconfig.mk16++++++----------
Rdicts/1k.txt -> examples/dicts/1k.txt0
Rdicts/20k.txt -> examples/dicts/20k.txt0
Rdicts/2k.txt -> examples/dicts/2k.txt0
Aexamples/dvorak17+++++++++++++++++
Aexamples/learn.sh51+++++++++++++++++++++++++++++++++++++++++++++++++++
Aexamples/lesson.sh44++++++++++++++++++++++++++++++++++++++++++++
Aexamples/letters.sh20++++++++++++++++++++
Aexamples/qwerty17+++++++++++++++++
Aexamples/words.sh37+++++++++++++++++++++++++++++++++++++
Mmain.c186+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Aopt.c121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aopt.h11+++++++++++
Dprompt.c102-------------------------------------------------------------------------------
Drandom.c16----------------
Mtyper.c205++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mtyper.h60+++++++++++++++++++++++++++++++++---------------------------
Atypie.128++++++++++++++++++++++++++++
23 files changed, 688 insertions(+), 323 deletions(-)

diff --git a/Makefile b/Makefile @@ -1,11 +1,27 @@ include config.mk -main: main.c typer.h typer.c prompt.c random.c - $(CC) main.c -o $(PREFIX)/typie +typie: main.c typer.o opt.o + $(CC) main.c typer.o opt.o -o typie -config.h: config.default.h - cp config.default.h config.h +typer.o: typer.c typer.h + $(CC) -c typer.c -o typer.o + +opt.o: opt.c opt.h + $(CC) -c opt.c -o opt.o clean: - rm -f $(PREFIX)/typie + rm -f typer.o opt.o typie + +install: typie config.mk + mkdir -p $(PREFIX)/bin + cp -f typie $(PREFIX)/bin + chmod 755 $(PREFIX)/bin/typie + mkdir -p $(MANPREFIX)/man1 + sed "s/VERSION/$(VERSION)/g" < typie.1 > $(MANPREFIX)/man1/typie.1 + chmod 644 $(MANPREFIX)/man1/typie.1 + +uninstall: config.mk + rm -f $(PREFIX)/bin/typie + rm -f $(MANPREFIX)/man1/typie.1 + diff --git a/README b/README @@ -0,0 +1,29 @@ +-------------------------------------------------------------------------------- +Typie +-------------------------------------------------------------------------------- +This is a minimal program for practicing typing in the command line. See +examples for some intended use cases. + +Features: + - minimal, only C and standard libs, short and clean code + - skip-to-next-chunk error determination (similar to monkeytype) + - statistics output + +-------------------------------------------------------------------------------- +# Build/installation +-------------------------------------------------------------------------------- +To just build the project, running "make" will compile the program into this +directory. Once you've built it, you can run it with the --help option to see +how it is used. + +To install it, edit config.mk to reflect your system's configuration, then run +"make clean install" to install it. + +-------------------------------------------------------------------------------- +# TODO +-------------------------------------------------------------------------------- + - multiple-line, auto-wrapped-at-80 input + - timed mode + - output file + - mistakes output + diff --git a/README.md b/README.md @@ -1,7 +0,0 @@ -# Typie - -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. - diff --git a/config.default.h b/config.default.h @@ -1,9 +0,0 @@ - -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,9 +0,0 @@ - -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.mk b/config.mk @@ -1,13 +1,9 @@ -# version -VERSION = 0.0.0 +# update this file to match your system's configuration. -# Customize below to fit your system +VERSION = 0.0 -# paths -PREFIX = . -MANPREFIX = ./man -# PREFIX = /usr/local -# MANPREFIX = $(PREFIX)/share/man +PREFIX = /usr/local +MANPREFIX = $(PREFIX)/share/man -# compiler and linker -# CC = c99 +FLAGS = -std=c99 -pedantic -Wall -Wno-deprecated-declarations -Os +CC = c99 $(FLAGS) diff --git a/dicts/1k.txt b/examples/dicts/1k.txt diff --git a/dicts/20k.txt b/examples/dicts/20k.txt diff --git a/dicts/2k.txt b/examples/dicts/2k.txt diff --git a/examples/dvorak b/examples/dvorak @@ -0,0 +1,17 @@ +┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───────┐ +│~ │! │@ │# │$ │% │^ │& │* │( │) │{ │} │ │ +│ ` │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ 0 │ [ │ ] │ │ +├───┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──────┤ +│ │" │< │> │P │Y │F │G │C │R │L │? │+ │ | │ +│ │ ' │ , │ . │ p │ y │ f │ g │ c │ r │ l │ / │ = │ \ │ +├────┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴──────┤ +│ │A │O │E │U │I │D │H │T │N │S │_ │ │ +│ │ a │ o │ e │ u │ i │ d │ h │ t │ n │ s │ - │ │ +├─────┴┬──┴┬──┴┬──┶┯━━┵┬──┴┬──┶┯━━┵┬──┴┬──┴┬──┴┬──┴─────────┤ +│ │: │Q │J │K │X │B │M │W │V │Z │ │ +│ │ ; │ q │ j │ k │ x │ b │ m │ w │ v │ z │ │ +├───┬──┴┬──┴┬──┴┬──┴───┴───┴───┴───┼───┼───┼───┼────────────┘ +│ │ │ │ │ │ │ │ │ +│ │ │ │ │ │ │ │ │ +└───┴───┴───┴───┴──────────────────┴───┴───┴───┘ + diff --git a/examples/learn.sh b/examples/learn.sh @@ -0,0 +1,51 @@ +#!/bin/sh + +# Usage: learn.sh [LESSON_NUMBER] +# Do specific lessons by number. + +set -e + +LESSON_NUMBER=$1 + +LESSONS="uh +et +on +as +id +pg +kb +jm +qw +yf +cr +lx +vz +.>,< +'\";:" + +get_lesson() { + echo "$LESSONS" | sed -n "${1}p" +} + +if [ "$LESSON_NUMBER" = "" ]; then + echo "Usage: ./learn.sh [LESSON_NUMBER]" + + NUMBERS="" + LESSON_COUNT=$(echo "$LESSONS" | wc -l) + for i in $(seq $LESSON_COUNT); do + CUR="$i[$(get_lesson "$i")]" + NUMBERS="$NUMBERS $CUR" + done + echo "LESSON_NUMBER: $NUMBERS" | fold + exit 1 +fi + + +OLD_LETTERS="" +for i in $(seq $(($LESSON_NUMBER-1))); do + CUR="$(get_lesson $i)" + OLD_LETTERS="$OLD_LETTERS$CUR" +done +NEW_LETTERS=$(get_lesson $LESSON_NUMBER) + +./lesson.sh "$OLD_LETTERS" "$NEW_LETTERS" diff --git a/examples/lesson.sh b/examples/lesson.sh @@ -0,0 +1,44 @@ +#!/bin/sh + +# proof-of-concept for what a basic typing lesson might look like. +# Usage: ./lesson.sh OLD_LETTERS NEW_LETTERS + +set -e + +OLD_LETTERS=$1 +NEW_LETTERS=$2 + +cat dvorak + +old_letters() { + echo "$OLD_LETTERS" | fold -b -w 1 | shuf -n $1 | tr -d '\n' +} + +OLD_LETTER_COUNT="$(echo "$OLD_LETTERS" | wc -c)" +EASY=$(($OLD_LETTER_COUNT / 4)) +MEDIUM=$((2 * $OLD_LETTER_COUNT / 4)) +HARD=$((3 * $OLD_LETTER_COUNT / 4)) + +echo "You already know: [$OLD_LETTERS] and will learn [$NEW_LETTERS]." +echo "If it gets too hard, don't be afraid to ctrl+C and redo previous lessons." +echo "" + +echo "Introducing the new letters..." +typie -l "$(./words.sh "$NEW_LETTERS" 5 10)" +typie -l "$(./words.sh "$NEW_LETTERS" 5 10)" + +echo "Integrating with some old letters..." +typie "$(./words.sh "$(old_letters $EASY)$NEW_LETTERS" 5 10)" +typie "$(./words.sh "$(old_letters $EASY)$NEW_LETTERS" 5 10)" +typie "$(./words.sh "$(old_letters $EASY)$NEW_LETTERS" 5 10)" +typie "$(./words.sh "$(old_letters $MEDIUM)$NEW_LETTERS" 5 10)" +typie "$(./words.sh "$(old_letters $MEDIUM)$NEW_LETTERS" 5 10)" +typie "$(./words.sh "$(old_letters $HARD)$NEW_LETTERS" 5 10)" +typie "$(./words.sh "$(old_letters $HARD)$NEW_LETTERS" 5 10)" + +echo "All together now!" +typie "$(./words.sh "$OLD_LETTERS$NEW_LETTERS" 5 10)" +typie "$(./words.sh "$OLD_LETTERS$NEW_LETTERS" 5 10)" +typie "$(./words.sh "$OLD_LETTERS$NEW_LETTERS" 5 10)" + +echo "All done! :3" diff --git a/examples/letters.sh b/examples/letters.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +# this script generates random orders of specific letters. +# Usage: letters.sh CHARSET MIN_LENGTH MAX_LENGTH + +CHARSET=$1 +get_letter() { + LETTER="$(echo $CHARSET | fold -b -w 1 | shuf -n 1)" + echo "$LETTER" +} + +LENGTH=$(seq $2 $3 | shuf -n 1) + +WORD="" +for i in $(seq $LENGTH); do + WORD="$WORD$(get_letter)" +done + +echo "$WORD" + diff --git a/examples/qwerty b/examples/qwerty @@ -0,0 +1,17 @@ +┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───────┐ +│~ │! │@ │# │$ │% │^ │& │* │( │) │_ │+ │ │ +│ ` │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ 0 │ - │ = │ │ +├───┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──────┤ +│ │Q │W │E │R │T │Y │U │I │O │P │{ │} │ | │ +│ │ q │ w │ e │ r │ t │ y │ u │ i │ o │ p │ [ │ ] │ \ │ +├────┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴──────┤ +│ │A │S │D │F │G │H │J │K │L │: │" │ │ +│ │ a │ s │ d │ f │ g │ h │ j │ k │ l │ ; │ ' │ │ +├─────┴┬──┴┬──┴┬──┶┯━━┵┬──┴┬──┶┯━━┵┬──┴┬──┴┬──┴┬──┴─────────┤ +│ │Z │X │C │V │B │N │M │< │> │? │ │ +│ │ z │ x │ c │ v │ b │ n │ m │ , │ . │ / │ │ +├───┬──┴┬──┴┬──┴┬──┴───┴───┴───┴───┼───┼───┼───┼────────────┘ +│ │ │ │ │ │ │ │ │ +│ │ │ │ │ │ │ │ │ +└───┴───┴───┴───┴──────────────────┴───┴───┴───┘ + diff --git a/examples/words.sh b/examples/words.sh @@ -0,0 +1,37 @@ + +CHARSET=$1 +MIN_LENGTH=$2 +MAX_LENGTH=$3 +DICTFILE="dicts/20k.txt" + +if [ $CHARSET = "" ];then + echo "Requires charset argument." + exit 1 +fi + +# filter the dictionary for words only involving the characters, len >= 4. +WORDS="$(cat $DICTFILE | grep -x "[$CHARSET]*" | grep -x ".{5,}")" + +WORD_COUNT="$(echo "$WORDS" | wc -l)" +FEW_WORDS="false" +if [ "$WORD_COUNT" -lt "10" ]; then + FEW_WORDS="true" +fi + +get_word() { + if [ "$(seq 3 | shuf -n 1)" = "1" ] || [ $FEW_WORDS = "true" ]; then + ./letters.sh "$CHARSET" 4 6 + else + echo "$WORDS" | shuf -n 1 + fi +} + +PROMPT="$(get_word)" + +LENGTH="$(seq $MIN_LENGTH $MAX_LENGTH | shuf -n 1)" +for i in $(seq $LENGTH); do + PROMPT="$PROMPT $(get_word)" +done + +echo "$PROMPT" + diff --git a/main.c b/main.c @@ -1,115 +1,133 @@ #include <stdlib.h> +#include <stdio.h> +#include <time.h> #include "typer.h" -#include "random.c" -#include "prompt.c" -#include "typer.c" - -char word_buffer[1024]; +#include "opt.h" + +#define TRUE 1 +#define FALSE 0 +#define MAX_BUFFER_LENGTH 1024 + +char prompt_buffer[MAX_BUFFER_LENGTH]; +int jumps_buffer[MAX_BUFFER_LENGTH]; + +const char * usage =\ +"Usage: typie [-ls] [-f FILE] PROMPT" "\n"\ +"\n"\ +" -l no backspacing, incorrect keys not accepted" "\n"\ +" -s output statistics after completion" "\n"\ +" -f FILE file to write output to (stdout if unspecified)" "\n"\ +/* " -m output mistakes after completion" "\n" */\ +/* " -t TIME terminate after TIME milliseconds" "\n" */\ +"\n"\ +"See the man page for more details." "\n"\ +"\n"; int -main(int argc, char *argv[]) +type(State * state) { - struct termios termios_original; - struct timespec t1, t2; - Words prompt; - State state; int keypress_kind; - int started = 0; - - state.index = 0; - state.error_index = -1; + int started = FALSE; - 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); - - while (lines_submitted < lines_per_run) { - keypress_kind = handle_keypress(prompt, &state); - if (display_while_typing) - fflush(stdout); + for (;;) { + keypress_kind = handle_keypress(state); + fflush(stdout); switch (keypress_kind) { case KEYPRESS_EXIT: - fflush(stdout); - restore_termios(termios_original); return 1; - case KEYPRESS_CORRECT: - chars_correct++; - /* FALLTHROUGH */ + case KEYPRESS_CORRECT: /* FALLTHROUGH */ + state->chars_correct++; case KEYPRESS_INCORRECT: - chars_total++; - if (!started && chars_total != 0) { - started = 1; - clock_gettime(CLOCK_MONOTONIC, &t1); + state->chars_total++; + if (!started) { + started = TRUE; + state->start_millis = time(NULL); } + if (state->prompt[state->index] == '\0') + return 0; break; - 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); + case KEYPRESS_ENTER: + return 0; break; case KEYPRESS_BACKSPACE: - chars_backspace++; break; } } +} + +void +write_output(State state) +{ + FILE * out; + int correct = state.chars_correct, + total = state.chars_total; + double time_millis = state.end_millis - state.start_millis; + double time_seconds = time_millis / 1000; + double time_minutes = time_seconds / 60; + int wpm = (correct / 5.0) / time_minutes; + int accuracy = ((double) correct / total) * 100; + + out = stdout; + if (state.flags & FLAG_FILE) + out = fopen(state.outfile, "w"); + + if (state.flags & FLAG_STATISTICS_OUTPUT) { + fprintf( + out, + "time:" "\t\t" "%f" "\n"\ + "wpm:" "\t\t" "%d" "\n"\ + "accuracy:" "\t" "%d%%" "\n", + time_millis/1000, + wpm, + accuracy + ); + } + + if (state.flags & FLAG_FILE) + fclose(out); +} + +int +main(int argc, const char * argv[]) +{ + struct termios termios_original; + State state; + int result; - clock_gettime(CLOCK_MONOTONIC, &t2); + state.index = 0; + state.jumps.buffer = jumps_buffer; + state.jumps.length = 0; + + result = set_flags(&state, argc, argv); + if (result > 0) { + printf("%s", usage); + exit(1); + } + state.prompt = argv[argc-1]; + + tcgetattr(0, &termios_original); + setup_termios(termios_original); + + start_typing(&state); + fflush(stdout); + + result = type(&state); + if (result > 0) { + restore_termios(termios_original); + exit(1); + } + + state.end_millis = time(NULL); 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 words_per_min = (chars_correct / 5.0) / min; - int keys_per_min = (chars_total) / min; - int accuracy = ((double) chars_correct / chars_total) * 100; - - 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); - } + write_output(state); return 0; } diff --git a/opt.c b/opt.c @@ -0,0 +1,121 @@ + +#include <stdlib.h> +#include <stdio.h> + +#include "opt.h" +#include "typer.h" + +#define ERROR(message) fprintf(stderr, COLOR_RED message COLOR_RESET); +#define ERRORARG(message, arg)\ + fprintf(stderr, COLOR_RED message COLOR_RESET, arg); + +#define PROCESS_GROUP_SUCCESS 0 +#define PROCESS_GROUP_FAILURE 1 +#define PROCESS_GROUP_SKIP 2 + +int +get_flag(char c) +{ + switch (c) { + case 'l': return FLAG_LEARN_MODE; + case 'f': return FLAG_FILE; + case 't': return FLAG_TIME; + case 's': return FLAG_STATISTICS_OUTPUT; + } + + return 0; +} + +int +process_file_flag( + State * state, + const char * group, + const char * next, + int index +) +{ + if (next == NULL || group[index+1] != '\0') { + ERROR("Filename must follow -f." "\n\n"); + return PROCESS_GROUP_FAILURE; + } + + state->outfile = next; + + return PROCESS_GROUP_SKIP; +} + +int +process_flag_group(State * state, const char * group, const char * next) +{ + int index; + char c; + int flag; + + if (group[0] != '-') { + ERROR("Invalid flag format." "\n\n"); + return 1; + } + + index = 1; + for (;;) { + c = group[index]; + + if (c == '\0') + break; + + flag = get_flag(c); + + if (flag == 0) { + ERRORARG("Unrecognized flag: -%c." "\n\n", c); + return PROCESS_GROUP_FAILURE; + } + + if ((state->flags & flag) != 0) { + ERRORARG("Repeated flag: -%c." "\n\n", c); + return PROCESS_GROUP_FAILURE; + } + + if (flag == FLAG_FILE) + return process_file_flag(state, group, next, index); + + state->flags |= flag; + index++; + } + + return 0; +} + +int +set_flags(State * state, int argc, const char * argv[]) +{ + int index = 1; + const char * group; + const char * next; + int result; + + if (argc == 2) + return 0; + + if (argc == 1) { + ERROR("Not enough arguments." "\n\n"); + return 1; + } + + for (;;) { + group = argv[index]; + next = (index+1 < argc-1) ? argv[index+1] : NULL; + + result = process_flag_group(state, group, next); + if (result == PROCESS_GROUP_SKIP) + index++; + if (result == PROCESS_GROUP_FAILURE) + return 1; + + index++; + if(index >= argc-1) /* last arg is PROMPT */ + break; + } + + return 0; +} + diff --git a/opt.h b/opt.h @@ -0,0 +1,11 @@ + +#include "typer.h" + +typedef enum { + FLAG_LEARN_MODE = 1 << 0, + FLAG_FILE = 1 << 1, + FLAG_TIME = 1 << 2, + FLAG_STATISTICS_OUTPUT = 1 << 3 +} Flag; + +int set_flags(State * state, int argc, const char * argv[]); diff --git a/prompt.c b/prompt.c @@ -1,102 +0,0 @@ - -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,16 +0,0 @@ - -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/typer.c b/typer.c @@ -1,16 +1,21 @@ +#include <stdio.h> + +#include "typer.h" +#include "opt.h" void setup_termios(struct termios term_og) { struct termios term_raw; - term_raw = term_og; term_raw.c_iflag &= ~( + 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; + term_raw.c_cflag |= CS8; tcsetattr(0, 0, &term_raw); } @@ -34,109 +39,201 @@ cursor_forward(int steps) } void -on_correct(State * state, char c) +cursor_up(int steps) { - if (state->error_index == -1) - printf(COLOR_GREEN "%c", c); - else - printf(COLOR_YELLOW "%c", c); + printf("\033[" "%d" "A", steps); +} - state->index++; +void +cursor_down(int steps) +{ + printf("\033[" "%d" "B", steps); +} + +void +print_char(char c) +{ + /* replace spaces with interpuncts. */ + if (c == ' ') printf("\u00B7"); + else printf("%c", c); } void -on_incorrect(Words prompt, State * state, char c) +add_jump(State * state, int size) { - if (state->error_index == -1) - state->error_index = state->index; + state->jumps.buffer[state->jumps.length] = size; + state->jumps.length++; +} - /* typed char in space */ - if (IS_BLANK(prompt.str[state->index])) { - printf(COLOR_PALE_RED "%c", c); - state->index++; - return; +int +remove_jump(State * state) +{ + /* returns the size of the most recent jump */ + + state->jumps.length--; + return state->jumps.buffer[state->jumps.length]; +} + +void +show_remaining(State * state) +{ + /* print the rest of the string after index, at the cursor. */ + const char * prompt = state->prompt; + int index = state->index; + int steps = 0; + char c; + + printf(COLOR_RESET "\033[0K"); /* clear to end of line */ + for (;;) { + c = prompt[index]; + if (c == '\0' || c == '\n') + break; + + printf("%c", prompt[index]); + steps++; + index++; } + cursor_back(steps); +} + +void +skip(State * state, char input) +{ + /* jumps to the end of the current whitespace or non-whitespace chunk */ + + int blankness = IS_BLANK(state->prompt[state->index]); + char c; + int steps = 0; - /* char in char */ - if (IS_ALPHANUMERIC(c)) { - printf(COLOR_RED "%c", c); // prompt.str[state->index]); + for (;;) { + c = state->prompt[state->index]; + if (c == '\0' || IS_BLANK(c) != blankness) + break; + + printf("%c" COLOR_YELLOW, c); + steps++; state->index++; + } + printf("%c", input); + steps++; + state->index++; + add_jump(state, steps); +} + +void +on_correct(State * state, char c) +{ + printf(COLOR_GREEN); + printf("%c", c); + add_jump(state, 1); + state->index++; + + show_remaining(state); +} + +void +on_incorrect(State * state, char c) +{ + const char * prompt = state->prompt; + char expected_c = prompt[state->index]; + int input_blank = IS_BLANK(c), prompt_blank = IS_BLANK(expected_c); + + printf(COLOR_RED); + if (state->flags & FLAG_LEARN_MODE) { + print_char(c); + show_remaining(state); + cursor_back(1); return; } - /* space in char */ - if (IS_BLANK(c)) { - printf(COLOR_PALE_RED "\u00B7"); + if ( + (input_blank && prompt_blank) || + (!input_blank && !prompt_blank) + ) { + /* correct character type */ + print_char(c); state->index++; + add_jump(state, 1); + return; + } + + if ( + state->index != 0 && + IS_BLANK(prompt[state->index-1]) == input_blank + ) { + /* extra letters at the end of a chunk */ + print_char(c); + add_jump(state, 0); + show_remaining(state); + return; } + + skip(state, c); + return; + } void -on_backspace(Words prompt, State * state) +on_backspace(State * state) { - if (state->index == 0) + int jump; + + if (state->flags & FLAG_LEARN_MODE) { + show_remaining(state); return; + } - { - char to_restore = ' '; - if (state->index < prompt.len) - to_restore = prompt.str[state->index - 1]; + if (state->jumps.length == 0) + return; + jump = remove_jump(state); + if (jump == 0) { cursor_back(1); - printf(COLOR_RESET); - printf("%c", to_restore); - cursor_back(1); + show_remaining(state); + return; } - state->index--; - - if (state->error_index == state->index) - state->error_index = -1; + cursor_back(jump); + state->index -= jump; + show_remaining(state); } int -on_enter(Words prompt, State * state) +on_enter(State * state) { - 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"); /* circle */ printf("\n\r" COLOR_RESET); - return KEYPRESS_NEXT; + return KEYPRESS_ENTER; } void -start_typing(Words prompt, State * state) +start_typing(State * state) { state->index = 0; - state->error_index = -1; - printf("%s" "\n" "\r", prompt.str); + printf(COLOR_BOLD "%s" COLOR_RESET "\r", state->prompt); + // printf("%s" "\n" "\r", state->prompt); } int -handle_keypress(Words prompt, State * state) +handle_keypress(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); + return on_enter(state); - if (c == KEY_BACKSPACE) { - on_backspace(prompt, state); + if (c == KEY_BACKSPACE || c == KEY_SHIFT_BACKSPACE) { + on_backspace(state); return KEYPRESS_BACKSPACE; } - if (c == prompt.str[state->index]) { + if (c == state->prompt[state->index]) { on_correct(state, c); return KEYPRESS_CORRECT; } - on_incorrect(prompt, state, c); + on_incorrect(state, c); return KEYPRESS_INCORRECT; } diff --git a/typer.h b/typer.h @@ -1,12 +1,8 @@ -#include <stddef.h> -#include <time.h> -#include <termios.h> -#include <stdio.h> -#include <string.h> -#include <unistd.h> -#include <stdint.h> +#ifndef TYPERH +#define TYPERH -#include "config.h" +#include <termios.h> +#include <time.h> #define COLOR_GREEN "\033[1;32m" @@ -22,6 +18,7 @@ #define KEY_ENTER 13 #define KEY_ESCAPE 27 #define KEY_BACKSPACE 127 +#define KEY_SHIFT_BACKSPACE 8 #define IS_BLANK(X) ((X) == ' ' || (X) == '\t') #define IS_NUMBER(X) ((X) >= '0' && (X) <= '9') @@ -32,31 +29,40 @@ #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 */ +#define KEYPRESS_ENTER 4 -; +typedef struct { + int * buffer; + size_t length; +} Jumps; -typedef struct wrds { - char * str; - size_t len; -} Words; +typedef struct { + /* the string being typed */ + const char * prompt; -typedef struct stt { + /* records how many characters are traversed by each keypress */ + Jumps jumps; + + /* sum of jumps, current index in prompt */ int index; - int error_index; /* index of the first incorrect character, or -1 */ -} State; -uint64_t step_random(uint64_t *); -uint64_t get_random(uint64_t *, int); + /* keypress tallies */ + int chars_correct; + int chars_total; -Words create_prompt(const char *, char *, int, char); + /* options */ + int flags; + const char * outfile; + + /* timing */ + long int start_millis; + long int end_millis; +} State; -void setup_termios(struct termios); -void restore_termios(struct termios); +void setup_termios(struct termios termios0); +void restore_termios(struct termios termios0); -void cursor_back(int); -void cursor_forward(int); -void start_typing(Words, State *); -int handle_keypress(Words, State *); +void start_typing(State * state); +int handle_keypress(State * state); +#endif /* TYPERH */ diff --git a/typie.1 b/typie.1 @@ -0,0 +1,28 @@ +.TH TYPIE 1 typie\-VERSION +.SH NAME +typie \- typing practice +.SH SYNOPSIS +.B typie +.RB [ \-ls ] +.RB [ \-f +.IR OUTFILE ] +.IR PROMPT +.SH DESCRIPTION +.B typie +is a simple typing practice program. +.SH OPTIONS +.TP +.B \-l +"learn mode": ignores repeated misinputs, disallows backspacing. +.TP +.B \-s +print statistics upon completion: time in seconds, words per minute, and +accuracy as a percentage. +.TP +.BI \-f " OUTFILE" +write results, if any, to OUTFILE. Defaults to stdout. +.SH AUTHORS +See the LICENSE file for the authors. +.SH LICENSE +See the LICENSE file for the terms of redistribution. +

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