A terminal TUI that records the exact timing of your keystrokes as you type, then lets you race a ghost replay of a past session side-by-side — like a racing-game ghost lap, but for typing rhythm.
Recording is built on a small Clock abstraction. The live TUI uses a real
system clock; tests drive a VirtualClock instead, so a recorded session
file is byte-for-byte reproducible given the same sequence of key events and
timings.
This is an early milestone: recording, ghost playback, and a live split-pane race view are implemented.
Requires Python 3.9+. curses ships with the Python standard library on
macOS and Linux (Windows needs a separate curses backend, which isn't
covered here).
git clone <this-repo>
cd ghosttype
pip install -e .
Start a recording session:
ghosttype record session.json
Type normally; every keystroke is captured along with its timestamp relative
to the start of the recording. Press Esc to stop and write the session to
session.json.
The session file is a single line of JSON:
{"duration":0.842,"events":[{"ch":"h","t":0.0},{"ch":"i","t":0.183}],"version":1}Replay a recorded session as a ghost cursor, with each keystroke appearing at exactly the offset it was originally typed at:
ghosttype play session.json
Playback is driven by the same Clock abstraction as recording: a
GhostPlayer is started against a session and, on each poll, releases every
event whose recorded offset has now been reached. The live TUI polls on a
real clock; tests drive a VirtualClock and advance it explicitly, so a
replay's output is exactly reproducible from the same session file.
Race a ghost live: type in a split pane while a past session replays side by side, and see who finishes first.
ghosttype race session.json
The left pane ("YOU") fills in with what you type; the right pane
("GHOST") fills in on its own, at exactly the timing recorded in
session.json. You "finish" once you've typed as many characters as the
ghost session recorded; the ghost finishes once its last event has played.
Whoever finishes first, in elapsed real time from when the race started,
wins — press Esc at any point to abandon the race early. The layout and
race logic (who wins, when each side finishes) are pure functions of the
typed/replayed text and the clock, so they're covered by tests that never
open a real terminal.
When the race ends, a score is printed alongside the result:
you win! 12.40s vs ghost's 13.10s
speed: you 42.3 wpm, ghost 40.1 wpm (+2.2 wpm)
sync: 78/100
Speed is words-per-minute (one "word" = 5 characters), compared as a
straight delta against the ghost. Sync is a 0-100 score of how closely your
keystroke rhythm matched the ghost's, not just your overall speed: it
compares the gap between each of your consecutive keystrokes against the
gap the ghost took at the same point, so typing the right characters at the
wrong rhythm scores lower than matching the ghost's cadence exactly. Both
numbers are pure functions of the two keystroke traces, so they're covered
by tests driven off a VirtualClock with no real typing involved.
None beyond the Python standard library (curses, argparse, json,
dataclasses). Writing a keystroke recorder and a small JSON session format
is well within reach of stdlib Python, so no third-party packages are used.
Built autonomously, one milestone at a time, and gated on a passing test suite before any change ships.