Skip to content

Repository files navigation

tt tt logo

A portable touch-typing trainer and speed test. One container, no build step, no network, no account. Everything runs in the browser and is stored locally.

Try it live | About the project

Two tabs:

  • learn: a 62-lesson curriculum from f j to symbols and adaptive weak-key drills, written in terms of key positions so the whole course works on QWERTY, Dvorak, Colemak, Colemak-DH, Workman, Norman, QWERTZ and AZERTY.
  • test: timed / word-count / quote / custom typing tests with a seed, so two people can type byte-identical text and compare scores.

The on-screen keyboard belongs to the learn tab, where it shows you where the keys are. Keys are frosted glass panes that brighten while you hold them, with no jump and no hard shadow. A test gets the whole window for the text instead.

The text area is grey until typed, accent colour once correct, red when wrong.

Six themes: red, green and blue, each in dark and light, plus one you build yourself from six colour wells. Every palette is checked for contrast, and the quiet colours of a custom theme are derived so it cannot end up unreadable.

Run it

docker compose up -d

Then open http://localhost:8080.

Or without compose:

docker build -t tt . && docker run --rm -p 8080:80 tt

Change the port with TT_PORT=9000 docker compose up -d.

There is no build step and no runtime dependency: the image is nginx plus a folder of static files.

Opening app/index.html straight off disk works too. Every script is a plain classic script with relative paths, so nothing needs an origin. The one thing that differs is storage: some browsers refuse localStorage on file://, and when that happens tt falls back to memory for the session and says so in the settings window, so your settings and history last until you close the tab.

Keyboard shortcuts

key action
tab restart the current text
esc new text (new seed unless one is pinned)
enter on the results screen: next run
r on the results screen: repeat the same text
ctrl + k show / hide the on-screen keyboard
ctrl + , settings
ctrl + p the command palette: everything the app can do, by name
ctrl + backspace delete the whole word

Seeded races

Every run has a seed. Leave the seed box empty for a fresh random seed each time, or type one to pin it. copy challenge on the results screen copies a URL that reproduces the exact text:

http://localhost:8080/#m=time&v=30&s=k7q2mp&l=english1k&p=0&n=0

Anyone opening that link gets the same words in the same order. Test recipes deliberately exclude the keyboard layout, so a QWERTY player and a Colemak player race the same text.

Scoring

See docs/SCORING.md for the full derivation. In short:

metric definition
wpm correct characters / 5 / minutes
raw wpm every keystroke / 5 / minutes
accuracy correct keystrokes / all keystrokes, judged at the moment of the press
consistency 100 * (1 - tanh(cv + cv³/3 + cv⁵/5)) over per-second raw speed
effective wpm wpm × accuracy³ × (0.9 + 0.1 × consistency)
tt score effective wpm × text difficulty × 10

Runs shorter than 10 seconds, under 30 keystrokes, or with idle gaps are recorded but never count as a personal best.

Timing

Timing is the part that is easy to get quietly wrong, so it is worth stating exactly what happens:

  • All timestamps come from the monotonic performance timeline; wall-clock time is never used to measure a duration.
  • The preferred stamp is KeyboardEvent.timeStamp, which is when the key event actually occurred, not when JavaScript got round to handling it. Implausible values (legacy epoch stamps, future stamps) fall back to performance.now().
  • t0 is the first counted keystroke, not the render and not a timer.
  • The display clock uses requestAnimationFrame but never accumulates: elapsed is always recomputed as now − t0 − paused.
  • In timed mode nothing past the deadline is counted. A keystroke whose own timestamp is past t0 + duration is discarded and the run ends with exactly the configured duration.
  • Losing window focus pauses the clock; paused time is excluded from the result and reported separately, and a long pause makes the run unranked.
  • Per-second samples are bucketed from the keystroke log after the fact using exact timestamps, not sampled live.

Look

  • themes: red, green and blue in dark and light, plus a custom palette you edit with colour wells and keep. Default is green.
  • paper grain: a fine fixed-position noise layer over the whole page. The keyboard tray paints an opaque ground so the caps stay clean against it.
  • every control is a keycap: tabs, buttons, chips and the on-screen keys are cut from one material, a light pastel face inset from its base with a hard bottom wall and a soft contact shadow. Inputs are recessed instead.
  • typed text colour: a row of pastel accents plus an eyedropper for any colour at all. This is the colour grey text turns once you type it correctly.
  • error colour: 6 pastel reds plus a custom picker.
  • fonts: 16 stacks, marked in the picker if they are not installed on the machine, with a size slider that is capped against the viewport so it can never overflow a narrow screen.
  • caret: line, block, underline or off.
  • keyboard: shown in the learn tab only, and a key lights only while you are pressing or clicking it. Finger colouring and a next-key hint are opt-in teaching aids, and the finger that owns the next key is named under the board.

Layouts

The layout picker changes the on-screen keyboard glyphs and the characters each lesson teaches. Two ways to use it:

  • Your OS is already set to that layout: leave remap input off.
  • You want to practise a layout without changing your OS: turn remap input on. Key events are then read by physical position (event.code) and translated through the selected layout.

Layout of the repository

index.html          the landing page (single self-contained file)
.nojekyll           tells Pages to serve the repository verbatim
app/                the whole application (static)
  index.html
  css/              base, app, keyboard
  js/
    util.js         helpers, namespaced storage
    rng.js          seeded PRNG (xmur3 + sfc32)
    data/           word lists, quotes, layouts, themes, curriculum
    generator.js    deterministic text generation for every drill and mode
    metrics.js      scoring
    engine.js       typing state machine, keystroke log, clock
    keyboard.js     on-screen keyboard
    render.js       text surface and caret
    chart.js        SVG charts
    results.js      results screen
    stats.js        stats tab
    settings.js     settings panel
    store.js        settings, history, personal bests, per-key profile
    app.js          wiring
tools/              headless test harness (node, no browser needed)
docs/               scoring and curriculum notes

Tests

node tools/t-engine.js
node tools/t-gen.js

t-engine.js drives the real engine with synthetic key events on a controlled clock and checks the timing and scoring arithmetic exactly. t-gen.js checks that seeded generation is reproducible and prints a sample of every drill.

Doing everything from the keyboard

ctrl + p opens a command palette holding every action and every setting the app has: tabs, themes, accent and error colours, fonts, layouts, caret styles, test modes and lengths, word lists, all the switches, and all 62 lessons by name. Type a few letters, press enter. Nothing in the app needs a mouse.

Carrying your progress between machines

Settings, history, personal bests, lesson progress and the per-key profile live in localStorage. Nothing leaves the machine.

The settings dialog has a profile section with two ways to move it:

  • export file writes tt-profile.json, and import file reads it back.
  • copy profile code gives you a single line of gzipped, base64 text, small enough to paste into a chat or a terminal. Paste it into another container and press apply. A second button copies the same thing without the run history, for when you only want your settings and progress to follow you.

Publishing

GitHub Pages serves this repository straight from the main branch root, with no build step and no workflow. .nojekyll turns off Jekyll processing so the files are published exactly as they are:

path serves
index.html the landing page at /tt/
app/ the application at /tt/app/

The app is plain static files with relative paths, so the same directory is what nginx serves inside the container and what Pages serves on the web. There is nothing to keep in sync and nothing to configure.

License

GPL-3.0-or-later. See LICENSE.

About

tt - A portable touch-typing trainer and speed test. One container, no build step, no network, no account.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages