8000 fix: single event loop, leaner dependencies, unclipped results graph by jrnxf · Pull Request #29 · jrnxf/thokr · GitHub
[go: up one dir, main page]

Skip to content

fix: single event loop, leaner dependencies, unclipped results graph - #29

Merged
jrnxf merged 11 commits into
mainfrom
fm/thokr-rust-refresh
Oct 12, 2026
Merged

jrnxf merged 11 commits into
mainfrom
fm/thokr-rust-refresh

Conversation

@jrnxf
@jrnxf jrnxf commented Oct 12, 2026 •
Copy link
Copy Markdown
Owner

A critique of the Rust in thokr, and the change that carries it out: clearer types and ownership, real error handling, fewer dependencies, a faster and smaller binary. Flags, screens, results numbers and the history log are the same as v0.5.0, with three listed exceptions (see "What changes for a user").

Fixes #22

The critique in brief

Correctness and error handling

  • A failed terminal read panicked in a background thread and left the program running with the terminal already restored.
  • Time was measured with the wall clock, so a clock step during a test could panic at the end of it.
  • A history row that could not be written was dropped with let _ =.

Structure and ownership

  • The event loop was two nested loops, a flag and three levels of matching for six possible actions.
  • cursor_pos was a second copy of input.len() that had to be kept in step.
  • Formatting, locating and writing the history log were one function, so its format could not be tested.

Idiom

  • calc_results cloned the input twice and counted seconds in a HashMap keyed by numbers formatted as text.
  • Options were checked with is_some() and then unwrapped; bools were matched with true and false arms.
  • Layout used float arithmetic and casts where saturating_sub says the same thing.

Performance

  • Three word lists were embedded as JSON and one was parsed into 10,000 heap strings at every start.
  • Every frame built one heap String per prompt character and wrapped the prompt three times.
  • The results screen searched $PATH for a browser on every frame.

Dependencies

  • Six of thirteen direct dependencies did work the standard library does, or none (unicode-width was never imported).
  • The lock file pinned 2022 releases and carried six open advisories.
  • ratatui and chrono were built with default features the program does not use.

Tooling

  • No rust-version, although ratatui 0.30 already requires Rust 1.88.
  • Nothing tested what is drawn on screen.
  • clippy's pedantic group gave 45 warnings; about half pointed at the findings above, the rest were cast lints not worth acting on.

What changes for a user

  1. The tweet capability is gone: the t key on the results screen, the (t)weet entry in the legend and the share link. The legend reads (r)etry / (n)ew / (esc)ape on every machine. Nothing else on the results screen changes.
  2. The results graph draws the whole line. A peak could lie above the top of the wpm axis and be cut off; in 0.4.1 that left a gap in the line (Graph Missing Sections #22). See bugs fixed, 6.
  3. On a prompt that fits one line, the cursor could sit one cell to the left of the next character. It is now on it (see bugs fixed, 7).

Everything else a person sees or types is unchanged.

Changes by theme

Tests first (tests: snapshot every screen)

  • 13 rendering tests against ratatui's TestBackend, recorded from main before anything else changed: characters, style of every cell, cursor position. They pass untouched through every commit of the cleanup; five of them later move the cursor by one cell with the cursor fix below, and nothing else in them changes.

Dependencies (deps: ..., two commits)

  • 13 direct dependencies down to 6. Word lists are text embedded with include_str! instead of JSON parsed at every start.
  • Edition 2024, rust-version = "1.88".
  • webbrowser was first updated from 0.7.1 to 1.2 and then removed together with the tweet capability (remove the tweet capability).

Cleanup (cleanup: ...)

  • One event loop on one thread (event::poll) instead of two threads and a channel. Errors reach main as io::Error.
  • A guard restores the terminal on every return path; the panic hook stays for the panic message.
  • Instant instead of SystemTime; cursor_pos (always equal to input.len()) removed; calc_results rewritten without the text-keyed HashMap; the two screens are two functions; the history log is its own module.

Speed (speed: ...)

  • [profile.release]: lto, codegen-units = 1, strip.
  • Neighbouring prompt characters of one style share a span; the prompt is wrapped once per frame instead of three times. The results screen no longer looks for a browser at all.
  • src/bench.rs: the ignored test behind the frame numbers.

Tweet capability removed (remove the tweet capability)

  • The key, the legend entry, the share link, the browser lookup with its thread, and the webbrowser dependency. With it go url and the ICU crates: 119 to 91 crates, about 4 s off the clean build, 303 KB off the binary.

Issue 22 (fix: the results graph cut off a peak above its top label)

  • See bugs fixed, 6.

Tests (tests: the history log format and the word lists)

  • The log row format against a log written by the v0.5.0 binary, appending to an existing log, a log that cannot be written, the word lists.

Measurements

Release builds of main and of the pushed head, rustc 1.98.1, x86-64 Linux, both binaries measured in alternation, medians of three or four rounds.

measure before (main, v0.5.0) after (this branch) change
launch to first frame, thokr 1.64 ms 1.34 ms -18%
launch to first frame, -l english1k 1.79 ms 1.40 ms -22%
launch to first frame, -l english10k 2.58 ms 1.77 ms -31%
launch to first frame, -f 2 2.21 ms 1.88 ms -15%
launch to first frame, -s 30 1.75 ms 1.37 ms -22%
key press + the frame after it, 100x30 37.0 us 28.5 us -23%
key press + the frame after it, 40x20 20.5 us 11.8 us -42%
results screen frame, redrawn, 100x30 65.2 us 42.2 us -35%
results screen frame, redrawn, 40x20 30.0 us 13.1 us -56%
final key to the first results frame 0.54 ms 0.40 ms -26%
peak memory (RSS) 7.9 MB 7.9 MB none
binary size 3,018,328 bytes 1,393,736 bytes -54%
clean release build 17.2 s 24.7 s +43% (worse)
crates compiled, from the lock file 116 91 -25
crates compiled by cargo install (no lock file) 138 91 -47
direct dependencies 13 6 -7
packages in Cargo.lock, all platforms 261 151 -110
open advisories in Cargo.lock 6 0

One of these is not a gain: the clean build is about 7 s slower. That is the price of lto + codegen-units = 1, which on the same code buy 7% on a key press, 15% on a results frame and a third of the binary. It is paid once per cargo install.

Most of a frame (about 21 of 28 us at 100x30) is ratatui comparing every cell with the previous frame. thokr's own rendering went from about 10 us to about 5 us.

Dependencies

crate decision reason effect
serde 1 drop only derived Deserialize for the word lists with the next two: serde, serde_core, serde_derive, serde_json, include_dir, include_dir_macros leave the build, and nothing is parsed at start (-l english10k: 2.53 ms to 1.69 ms)
serde_json 1.0.79 drop the word lists are now text, one word per line, read with include_str! see serde
include_dir 0.7.2 drop include_str! is in the standard library see serde; the binary no longer carries 164 KB of JSON and a copy of mod.rs
strum_macros 0.24 drop only turned the language enum into a file name a second, older copy of strum_macros leaves the build (ratatui brings its own)
itertools 0.10.3 drop one sorted_by; slice::sort_by does it a second, older copy of itertools leaves the build (ratatui brings its own)
unicode-width 0.1.9 drop never imported a second, older copy of unicode-width leaves the build
ratatui 0.30.1 update to 0.30.2; default features off, crossterm and layout-cache on maintained, current. The unused widgets, the macros and underline colour go. layout-cache must stay: without it a key press + frame costs 74 us instead of 26 its subtree shrinks from 71 to 65 crates (time and its helpers, ratatui-macros); without underline colour ratatui's per-frame cell comparison drops from about 26 us to about 21 us
clap 4 keep; 4.6.1 to 4.6.7 maintained. --help, -V and the error texts are part of what people see, and were checked byte for byte none. It is the largest unit in the build; a smaller parser would change the help text
chrono 0.4 keep; default features off, clock on maintained. It writes the local time stamp in the log (%c). The alternatives are larger or cannot read the local time zone safely none on Linux (4 crates before and after); the wasm packages leave the lock file
directories 4.0 update to 6.0 maintained. config_dir is the same expression on Linux, macOS and Windows in both versions (sources compared), so the log stays where it is none measurable
rand 0.8.5 keep at 0.8 (lock: 0.8.8) maintained; 0.10 is current, but cgisf_lib needs 0.8, so a newer rand here would compile two none; the lock update clears RUSTSEC-2026-0097
cgisf_lib 0.1.3 keep one author; 0.2.1 exists. No other crate generates these sentences. 0.2 changes the API and details of the grammar and adds a dependency, so -f prompts would change none
webbrowser 0.7.1 drop it served only the tweet key, which is removed. On the way it was updated to 1.2, which cleared RUSTSEC-2026-0257 but brought url and the ICU crates with it: 119 to 91 crates, clean build about 29 s to 24.7 s, binary 1,696,848 to 1,393,736 bytes

Minimum Rust is 1.88 because ratatui 0.30 requires it, which is what main needs today; the 2024 edition (1.85) fits under that. Checked with real toolchains: 1.88.0 builds and passes the tests, 1.87.0 is refused by cargo with a clear message.

Bugs fixed

  1. If reading from the terminal failed, the reader thread panicked and the program kept running with the terminal restored, drawing over the shell (in -s and --pace modes). It now exits with the error.
  2. A timed test was up to 0.1 s short: ticks started with the program, not with the first key.
  3. A timed test of 5, 10, 15 or 120 seconds (and others) ran one tick long: 0.1 subtracted repeatedly does not land on zero.
  4. A history row that could not be written was dropped silently. The first failure is now printed on stderr after exit; the exit code is unchanged.
  5. A step of the system clock during a test could panic at the end of it.
  6. The results graph could cut off its own peak (Graph Missing Sections #22). Three separate facts:
    • What starts it: the top of the wpm axis was the highest wpm rounded to the nearest whole number, so a peak with a fraction below one half (98.4 under a top of 98) lay outside the chart.
    • What hides or shows it: the fraction, and the chart library. tui 0.17, which 0.4.1 used, drew nothing for a line segment with an end outside the chart, so one point above the top removed the two segments touching it: the two second gap in the report. Today's ratatui cuts such a segment at the edge instead.
    • What the user sees: in 0.4.1 a gap in the line; on main today a peak shaved flat at the top row. It can look "lower than the max value" because the point that is missing is the maximum itself: what remains either side of the gap are its lower neighbours.
    • The seconds axis had the same defect: a test over in under a second has its only point left of second 1, on an axis that then ran from 1 to 1 and drew nothing.
    • The fix: the top is rounded up, and the seconds axis starts at 0 for a sub-second test, so no point lies outside either axis. Both labels stay whole numbers. fix: results graph clips the wpm peak (#22) #27 proposed the same upper bound; this change rewrites that file, so it carries the fix instead.
  7. On a one-line (centred) prompt the cursor was placed with (width - len) / 2 while ratatui centres the text with width / 2 - len / 2. The two differ by one when the width is even and the length is odd: in an 80-column terminal with a 43-character prompt the text starts at column 19 and the cursor was put at column 18, one cell left of the next character for the whole test. The offset is now worked out once and used for both. Five snapshots change by that one cell; their text and styles do not.

Found by the review step of the validation pipeline, not by me:

  • Bug 7.
  • Two test fixes: the pace-caret snapshots had a 500 ms clock margin (now 5 s), and a test helper existed twice.

Suggestions not taken

They change behaviour on purpose or reach outside this change.

  • Another chart widget or library, more labels or grid lines: not part of this change.
  • panic = "abort": 6% smaller binary, no speed change. Not done: a panic would end in SIGABRT and the terminal restore would rest on the hook alone.
  • opt-level = "s" / "z": 13% / 15% smaller, frames 8-20% / 2.4 times slower.
  • cgisf_lib 0.2: changes what -f generates.
  • Holding ctrl or alt with a letter types the letter (only ctrl-c is special).
  • -w larger than the word list gives the whole list and logs the requested number.
  • A line that is exactly full pushes its space to the start of the next line.
  • Double-width characters in a custom prompt misplace the cursor.
  • CI: cargo clippy --all-targets, and a job on the minimum Rust version. The workflow is unchanged here.
  • README: the examples table says plain thokr gives 50 words; the default is 15. The roadmap items for performance and tests could also be ticked. Left as they are: the first was wrong before this change, the second is the author's own status.

Proof that nothing else changed

  • --help, -h, -V, three argument errors and the not-a-terminal error: byte-identical output and exit codes.
  • The 13 snapshots from main hold: text and styles untouched, the cursor one cell to the right in five of them (bug 7). Four results snapshots lose the (t)weet legend entry and nothing else. New: a snapshot of a 98.4 wpm peak, a test of a sub-second result, and a check over 500 generated results that every point lies inside both axes (the last two fail without the fix).
  • The same key presses were sent to the v0.5.0 binary and to this branch's binary in a real terminal (tmux, 100x30) and the screen was captured after each of 26 steps, with colours and cursor position:
    • A word test with a typo, a backspace and a missed space, through to the results; retry; left and right arrows; resizing while running and on the results screen: text and colours identical cell for cell. On one-line prompts the cursor is one cell to the right (bug 7). Results screens differ only in their numbers, since two runs never type at exactly the same speed.
    • After esc, after ctrl-c while typing and after ctrl-c on the results screen: the shell is back with exit code 0, icanon echo on, the cursor shown and the normal screen active, for both binaries.
    • A timed test from start to results, the pace caret, and a narrow 40x12 terminal: identical.
    • Results screens: captured before the tweet key was removed; the legend entry is the only thing that has changed on them since.
    • Default words, -l english10k -w 30 and -f 2: same shape; the words are random.
    • Test builds in which a key forces a panic or an error, both versions: after a panic on the running screen, a panic on the results screen and an error, the terminal is usable and the message readable. A failed read during a timed test left v0.5.0 running and drawing over the shell; this branch exits with the error. An unwritable history log is silent on v0.5.0 and one line on stderr here.
  • The new binary appended to a log written by v0.5.0: same header, same columns.
  • The 23 existing tests keep their assertions. Setup lines changed mechanically where the API they poke changed: SystemTime to Instant, cursor_pos to input.len(), char_cell(&chars, width, i) to char_cell(&lines, i), has_started() to started_at.is_some().
  • 47 tests. CI on this PR: check, fmt, lint and test are green.

How this was validated

The change went through the validation pipeline twice: once at the head that carried out the critique, and once at the final head, 26c2e35, after the tweet capability was removed and #22 was fixed.

Second run, at the final head

  • Review: no functional defect found. Two notes, neither fixed here, both listed under follow-ups below.

  • Test: clean pass. 13 of 14 scenarios were driven against the release binary in a real terminal (tmux), most of them side by side with v0.5.0, and 67 checks passed with none failing:

    scenario result
    Issue Graph Missing Sections #22: a 10 second test at 94 wpm with a 98.4 wpm peak at second 5 draws the whole line under a top label of 99 pass, driven live
    Boundary: a test finished in under a second (thokr -p hi) still draws its point, on an axis from 0 to 1.00 pass, driven live
    Plain thokr: 15 words appear, typing colours them, the results show graph, stats and legend, and the numbers match what was typed pass, driven live
    Tweet is gone: the legend is (r)etry / (n)ew / (esc)ape and pressing t does nothing and launches no browser pass, driven live
    Keys work as before: backspace, r and left arrow restart the same prompt, n and right arrow give a new one, esc and ctrl-c quit with exit 0 and a usable terminal pass, driven live
    thokr -s 3: the countdown starts at 3.0, matches the clock to the tick and the results appear 3.0 s after the first key pass, driven live
    History log: the target appends to the log.csv that v0.5.0 created, same location, one header, same columns and number formats pass, driven live
    A history log that cannot be written is reported after exit instead of dropped pass, driven live
    Flags are unchanged: help, version and error output are byte identical to v0.5.0, and -w, -l, -f, --pace, -p behave as documented pass, driven live
    Adversarial: stepping the wall clock forward one hour in the middle of a test does not change the results pass, driven live
    Adversarial: resizing down to 1x1 and back, mid test and on the results, never crashes and redraws in full pass, driven live
    One event loop on one thread: a waiting test is a single thread with no wake-ups pass, driven live
    The release binary is smaller and start-up and key presses are faster than v0.5.0 pass, driven live
    A panic in the middle of a test leaves a readable message and a usable terminal not driven in this run

    The panic path was not driven in this run because the shipped binary could not be made to panic through its own surface (an empty prompt, a 1x1 terminal, resize storms and a blocked history log were tried). It was driven in the first run and in the comparison above, with a test build in which a key forces a panic: the terminal came back usable and the message readable.

    Its timing against v0.5.0, medians on one machine through one pty: binary 2.95 MB to 1.35 MB, launch to first frame 5.03 ms to 2.75 ms (3.58 ms to 2.10 ms with -l english10k). A waiting test is one thread with no wake-ups in 2 s; v0.5.0 has two threads. With the wall clock stepped forward one hour in the middle of a test, this branch reports 237 wpm over 2.18 s and v0.5.0 reports 1 wpm over 3602.17 s.

    Screens captured by that run (terminal captures rendered to images):

    Issue #22 before and after, driven live with the same 78 key schedule
    Sub-second test before and after
    Default test: fresh, mid test, results with the new legend
    Timed test: thokr -s 3
    Wall clock stepped one hour mid test: v0.5.0 against the target
    Resizing mid test and on the results

  • Lint: did not run in the pipeline. Its agent had no Rust toolchain on PATH. cargo fmt --all -- --check and cargo clippy --all-features -- -D warnings are the fmt and lint CI jobs on this PR, which are green at 26c2e35.

  • Document: nothing in the repository was stale. One thing it cannot fix from here: the demo gif the README links to lives in a separate assets repository and ends on a results screen whose legend still shows (t)weet. It needs re-recording there.

  • CI: check, fmt, lint and test are green at 26c2e35.

First run, before the tweet removal and the #22 fix

  • Review: found the items listed under bugs fixed; fixed by the pipeline and re-reviewed.
  • Test: approved past a time limit, not a clean pass. The Test agent was stopped at its 30-minute budget. Reason recorded with the approval: "Test agent was cut at its 30 minute budget after its evidence was complete and clean: screens, results, history log and share link match the base build cell for cell; exit paths, timed tests, browser lookup, resize, word lists and timing recorded in the run's evidence folder. fmt, clippy and all 46 tests pass at this head. CI runs check, fmt, clippy and test on the PR and must be green before the PR is reported ready." The second run repeated those checks at the final head and passed without a limit being reached. What it had recorded, base build against that head:
    • Running screens, results screens, the terminal after exit, timed tests and the pace caret, the browser lookup, resizing and the word lists: matching the base build cell for cell, apart from the two listed changes.

    • A history log written by the base build, then this head, then the base build again: one file, same header, same columns throughout.

    • Its timing, measured its own way (rustc 1.99, one byte into a pty until the frame comes back):

      measure base head
      binary 2,951,416 bytes 1,665,856 bytes
      first frame, thokr 1.94 ms 1.78 ms
      first frame, -l english10k -w 50 2.99 ms 2.23 ms
      key to frame, through the pty 132 us 92 us
  • Lint: did not run, for the same reason as above; CI covered it.
  • Document: the step edited the README roadmap; that was reverted, README.md is identical to main.

Follow-ups from the second review, not in this PR

  • The test results_chart_of_a_test_shorter_than_a_second depends on the real clock with a 500 ms margin: on a runner that stalls for half a second between two lines it would fail. It should build its case from fixed coordinates, as peak_with_a_fraction_below_one_half does. The generated-results test already covers sub-second points without the clock.
  • Input.char in src/thok.rs is written and never read. It is dead state of the kind this change removes elsewhere and can go, with its four initialisers.

Open PRs

This will conflict with #18, #26, #27 and #28 (#27 is superseded by the fix for #22 here): each edits src/main.rs, src/thok.rs or src/ui.rs, which are rewritten here, and #26 also edits src/lang/mod.rs, which is now src/lang.rs. #25 only adds docs/usage-guide.md and does not conflict. None of them is folded in.

Notes

  • Not touched, on purpose: the old account name thatvegandev in Cargo.toml (repository, homepage, author email), README.md, and the Docker image name in .github/workflows/deploy.yml.
  • No version bump, tag or release. Publishing a release on this repository publishes the crate.

jrnxf added 11 commits October 12, 2026 10:08
Thirteen rendering tests against ratatui's TestBackend, recorded from the
code as it is before any cleanup: the running screen (fresh, with correct
and incorrect input, wrapped, narrow, too small), the pace caret, the
countdown, and the results screen with its chart.

Each snapshot holds the characters, one style letter per cell and the
cursor position, so a change in colour or cursor placement fails as loudly
as a change in text.

The results snapshots show the (t)weet legend, which today depends on a
browser opener being on PATH of the machine running the tests.
…tion

Dropped:
- serde, serde_json, include_dir: the word lists were JSON embedded as a
  folder and parsed at every start. They are now one word per line,
  embedded with include_str!, so nothing is parsed or copied.
- strum_macros: only turned the language enum into a file name. The enum
  that clap parses now names its list directly.
- itertools: one sorted_by, replaced by slice::sort_by.
- unicode-width: never imported.

Trimmed:
- ratatui 0.30.2 without its default features. layout-cache stays on
  (without it every frame runs the constraint solver); the unused widgets,
  macros and underline colour go.
- chrono with only the clock feature: all it does is stamp the log.

Updated: clap 4.6, directories 6.0 (same config folder on every
platform), and the lock file, which still pinned releases from 2022.
webbrowser is updated in a later commit because that changes what is
shown on machines without a browser.

Kept as they are: rand 0.8 and cgisf_lib 0.1.3. cgisf_lib needs rand 0.8,
so a newer rand here would compile rand twice.

Edition 2024 and rust-version 1.88: ratatui 0.30 already needs Rust 1.88,
so the edition costs nobody a toolchain, and cargo now says so up front.
Event loop (main.rs)
- The reader thread, the tick thread and the channel are gone. One loop
  waits for a key or for the next tick with event::poll, so a failed read
  is an io::Error that reaches main instead of a panic in a thread that
  left the program running with the terminal restored.
- Keys are handled by one match; restart, new test, typing and tweeting
  are methods on App. A test is finished and saved in one place.
- A guard restores the terminal on every return path, and the restore
  shows the cursor itself. The panic hook stays, so the message is
  printed after the alternate screen is left.
- main returns io::Result; Box<dyn Error> had one error type left in it.

Test state (thok.rs)
- Instant instead of SystemTime: a wall-clock step can no longer panic
  at the end of a test or bend its numbers.
- cursor_pos is gone: it was always input.len(). Typing is push,
  backspace is pop.
- calc_results maps each correct key press to its second, sorts and
  counts runs. Same buckets, same numbers, no HashMap keyed by text.
- raw_coords (never read) and the helpers only tests called are removed.

History log (history.rs, new)
- The row format, the append and the path lookup are separate functions.
- A row that cannot be written is no longer dropped in silence: the first
  failure is printed on stderr after the terminal is restored.

Rendering (ui.rs, layout.rs)
- ui::draw replaces the Widget impl plus a second pass for the cursor:
  the prompt is wrapped once per frame instead of three times, and the
  two screens are two functions.
- Styles are constants; layout arithmetic is integer arithmetic.

Bugs fixed
- A timed test ran up to 0.1 s short: the first tick now comes 100 ms
  after the first key instead of at a random point before that.
- A timed test could run one tick long (-s 5, 10, 15 and 120 among
  others): the countdown is derived from the tick count instead of
  subtracting 0.1 again and again.

Existing tests: the 23 keep their assertions. SystemTime became Instant
in their setup, cursor_pos assertions read input.len(), and char_cell
takes the wrapped lines the tests had already computed. The 13 snapshots
are untouched and pass.
webbrowser before 1.2.2 has an open advisory: a BROWSER template with %s
lets a crafted URL add browser arguments. thokr only ever opens its own
fixed https link, so it was not exposed, but the fix is one update away
and 0.7.1 is four years old.

One visible effect. 0.7.1 called a browser available as soon as xdg-open
was on PATH. 1.2 looks for an actual default browser. On a machine that
has xdg-open but no browser configured (a server, a container) the
results legend no longer offers (t)weet, a key that could not open
anything there. With a browser, or with BROWSER set, nothing changes.

A snapshot of the legend without the tweet key is added.
Measured on x86-64 Linux, rustc 1.98, median of four rounds, against
v0.5.0. The whole branch, not this commit alone:

  launch to first frame          1.55 ms -> 1.32 ms
  same, -l english10k            2.53 ms -> 1.69 ms
  key press + frame, 100x30      33.9 us -> 26.1 us
  results frame, 100x30          57.6 us -> 39.8 us
  binary                         3,018,328 -> 1,684,552 bytes
  clean release build            17.1 s -> 28.2 s

In this commit:
- [profile.release] lto, codegen-units = 1, strip. Against the default
  profile on the same code: key press + frame 27.9 -> 26.0 us, results
  frame 45.1 -> 38.5 us, binary 2.55 -> 1.68 MB. The price is the build:
  19.2 s -> 28.8 s, paid once per cargo install.
- Neighbouring prompt chars of one style share a span instead of each
  owning a String: rendering the prompt 6.9 -> 5.0 us.
- Whether a browser can be opened is looked up once, when the first
  results show, instead of on every results frame.
- src/bench.rs: the ignored test behind the frame numbers,
  cargo test --release -- --ignored --nocapture bench

Most of a frame is ratatui comparing every cell with the last frame
(about 21 us at 100x30); thokr's own share is now about 5 us.
History log
- A row has exactly the format of the rows v0.5.0 wrote (the fixture is
  a log produced by the v0.5.0 binary).
- Old rows and a new row read back column by column the same way.
- Appending to an existing log adds a row and leaves the rest alone; a
  first run creates the folder and writes the header once.
- A log that cannot be written is an error, not silence.

Word lists
- Each list holds the expected number of words, one per line.
- A prompt is made of distinct words of the list, stops at the size of
  the list, and sentence mode counts its words.
The t key on the results screen, the (t)weet legend entry, the share
link, the browser lookup with its thread, and the webbrowser dependency
are gone. The results legend is (r)etry / (n)ew / (esc)ape on every
machine, the form machines without a browser already had. Nothing else
on the results screen changes.

Four results snapshots lose the legend entry. The snapshot of the legend
without a browser and the test of the lookup are removed with what they
tested.

With webbrowser go url and the ICU crates it brought in.
The top of the wpm axis was the highest wpm rounded to the nearest whole
number, so a peak with a fraction below one half (98.4 under a top of
98) lay outside the chart. The chart library of 0.4.1 drew nothing for a
line with an end outside the chart, which removed the two segments
touching the peak: the two second gap in the report. Today's ratatui
cuts such a line at the edge instead, so the same peak is drawn shaved.

The top is now rounded up. The seconds axis had the same defect for a
test over in under a second, whose only point lies left of second 1 on
an axis that then ran from 1 to 1 and drew nothing: it starts at 0
there.

Both labels stay whole numbers. A snapshot of a 98.4 peak, a test of the
sub-second case, and a check over 500 generated results that every point
lies inside both axes; the last two fail without the fix.
@jrnxf jrnxf changed the title fix: rework event loop and timing, trim dependencies, speed up rendering fix: single event loop, leaner dependencies, unclipped results graph Oct 12, 2026
@jrnxf
jrnxf merged commit 61f8fad into main Oct 12, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Graph Missing Sections

1 participant

0