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:
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.
+