8000 fix(ui): draw the results graph as a solid line on a zoomed wpm axis by jrnxf · Pull Request #31 · jrnxf/thokr · GitHub
[go: up one dir, main page]

Skip to content 8000

fix(ui): draw the results graph as a solid line on a zoomed wpm axis - #31

Merged
jrnxf merged 5 commits into
mainfrom
fm/thokr-chart-canvas
Oct 12, 2026
Merged

jrnxf merged 5 commits into
mainfrom
fm/thokr-chart-canvas

Conversation

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

Intent

About the results graph of thokr, a typing test that runs in the terminal:

"also, could you see if there is another charting library that might do ab etter job that what we see here?

#22 (or fix this)"

Context for reading that ask:

  • Issue 22 ("Graph Missing Sections") was a results graph cut off at its peak. That bug is fixed and merged in fix: single event loop, leaner dependencies, unclipped results graph #29. This task is the other half: how the graph is drawn.
  • A comparison of ways to draw the graph was made, with real renders and costs. What is wrong with today's graph: the line is a row of dots, the lower two thirds of the plot is empty because the wpm axis starts at 0, and each axis has two labels, one of them reading like 9.62.
  • The options put forward: A, draw the graph by hand with ratatui's own Canvas and quadrant block characters: one solid, continuous line, the wpm axis zoomed to the data on round numbers with a faint rule across at each number, seconds labels placed exactly under the second they name, 0 new crates, about 140 lines in place of today's 38; B, the same Chart widget used better; C, the malevich crate; D, leave it as it is. Two extras were listed as separate calls and were not picked: an opt-in thinner line made of octant characters, and plotting per-second speed.

The answer to those options:

"A is fine."

Separately, about the 14 screen snapshot tests that pull request 29 added:

"are all these snapshot tests in thokr necessary? I'm okay if the answer is yes but be critical"

After being told that 7 of the 14 earn their place, that 2 are duplicates and that 5 test one thing with a whole screen and could be one-line checks, and asked whether to keep the 7 or drop all:

"keep the 7"

What Changed

  • src/ui.rs draws the results graph with ratatui's Canvas and quadrant block characters in place of the Chart widget and its braille dots: one continuous magenta line, and a short dash when the results hold a single point. No crate is added.
  • The wpm axis is zoomed to the data on round numbers (steps of 1, 2 or 5 x 10^n, never a span under 10 wpm), with a label and a faint rule across at each tick. The seconds axis is a row of labels, each centred under the second it names, as many as fit, with a faint sec at the left edge. The old two-labels-per-axis layout, the wpm and seconds titles and labels like 6.00 are gone.
  • Screen snapshots go from 14 to 7. Two duplicate tests are removed (running_fresh, timed_test_in_a_narrow_terminal), and five whole-screen snapshots become plain assertions (pace caret offset, the countdown text, the stats line, the peak's line drawn in every column). The three results-screen snapshots are regenerated for the new graph, and unit tests are added for nice_step, wpm_scale, label placement, and for labels landing on the same cell the canvas draws the line in.

Risk Assessment

✅ Low: The change is confined to how the results graph is drawn in src/ui.rs, both round 3 fixes hold against the canvas source and a simulation of the placement code, and the snapshot set is the 7 the intent keeps.

Testing

No baseline command output was handed to this step. cargo is not on PATH, so I built the release binary of the change and of base 61f8fad with an ephemeral nix shell nixpkgs#cargo nixpkgs#rustc into a temp directory, then drove the real TUI in a private tmux server with its own HOME (so log.csv was a throwaway). I typed tests with timed keystrokes (a default word test with a typo, a 10 s timed test shaped like issue 22, an 8 s test with two spikes, a 0.4 s test, a paste, a test with no correct key, a 45 s test in a 40 column terminal), resized the results screen from 200x60 down to 1x1 and back, and read every screen cell by cell: line continuity at dot level, label values, rows and columns against where the line itself is drawn, colours, and no crash. All 12 live scenarios passed. The four PNGs are redrawn cell by cell from the colour capture of the tmux panes (no headless terminal screenshot tool was available), and each scenario has a text transcript with the raw screens. The snapshot count was checked with the ui:: tests only (26 passed), using insta's own list of snapshots read; that is a test run, not a live drive, so the snapshot scenario is marked untested. All temp files, the tmux server and the browser session were removed; the worktree is clean.

  • Live validation: ✅ go - 12 of 13 scenarios driven live against the product
Scenario Result Live Evidence
Finish a default word test (thokr -w 6) at 80x24: the results graph is one unbroken line of quadrant blocks, with no braille dots ✅ pass live 01-word-test.txt, 01-before-after.png (top row: base vs change, same keys)
Same word test (64 wpm): the wpm axis is zoomed to the data on round numbers (60, 70, 80, 90 - not from 0), one label per row, with a faint full-width rule at each ✅ pass live 01-word-test.txt (labels, rule cells all dim dark gray), 01-before-after.png
Same word test (5.71 s): seconds labels read 1 2 3 4 5 (no 5.71), each in the column the canvas draws that second in ✅ pass live 01-word-test.txt (label columns checked against the end time in the throwaway log.csv)
Issue 22 shape (10 s timed test, peak 98.4 wpm at second 5, final 94 wpm): the whole peak is drawn under a 100 label and the line has a dot in every dot column from second 1 to 10, at 80x24, 100x30, 8… ✅ pass live 02-issue22-peak.txt, 01-before-after.png (bottom row), 02-labels-on-the-line.png
Same issue 22 result at heights 24, 27, 30, 31, 35, 39: the 80 rule is on the row where the line passes 80 wpm at second 3 (the heights the review found one row off) ✅ pass live 02-issue22-peak.txt (rule row vs point row per size), 02-labels-on-the-line.png
8 s timed test with spikes at seconds 3 and 6: the 3 and 6 labels are in the column of each spike's tip (80x24: plot columns 18 and 47; also 100x30, 90x24, 64x20) ✅ pass live 04-seconds-under-spikes.txt, 02-labels-on-the-line.png (top row)
Resize the results screen from 200x60 down to 1x1 and back: never more wpm labels than plot rows, each label on the row of its own value down to two-row and one-row plots, thokr keeps running, and 80x… ✅ pass live 03-resize.txt, 03-resize.png
A test shorter than a second (thokr -p hi, 0.4 s): the one point is drawn as a three-cell dash at the moment the test ended, and the seconds row is labelled (0.5, 1) ✅ pass live 05-one-point.txt, 04-edge-cases.png (top left)
43 keys pasted at once (529390 wpm, six-digit labels): labels stay inside the chart with a full rule each, a dash is drawn, no crash ✅ pass live 05-one-point.txt (second half), 04-edge-cases.png (top right)
Timed test with no correct key (thokr -s 1, one wrong key): empty chart keeps its axes (0, 5, 10 and 0.5, 1), shows 0 wpm, survives resizes, exits 0 ✅ pass live 06-no-correct-keys.txt, 04-edge-cases.png (bottom left)
45.3 s test in a 40 column terminal, then widths 38 down to 17 and up to 100: the seconds row has a label at every width (20 and 40 at 40 columns) ✅ pass live 07-45s-narrow.txt, 04-edge-cases.png (bottom right)
sec starts at the chart's left edge (column 5) for a result under 100 wpm, at 100 and at six digits, and the plot starts in the same column under 100 and at 100 ✅ pass live 08-sec-label-column.txt
The snapshot tests kept are the 7: the ui tests read exactly the 7 files in src/snapshots, none left over ⏸️ untested no Not driven live. The prior run checked this only by running cargo test --locked ui:: (26 passed) and comparing the snapshots insta reported as read with the files in src/snapshots (transcript 09-sna…

Before and after, same keys typed: word test and issue 22 shape at 80x24
Labels on the cell the line is drawn in: spikes under 3 and 6, 80 rule on the 80 wpm point
One results screen resized from 120x40 down to a one-row plot and back
Edge cases: one point, a paste, no correct key, 45 s in a narrow terminal

Evidence: Issue 22 shape on the real binary, 80x24 (after)
100 ───────────────────────────▗▄▄▄────────────────────────▄▄▄▄▄▄▄────
▗▄▄▀▀▀▘ ▀▀▀▀▄▄▄▄▄▖ ▗▄▄▄▞▀▀▀▀▀ ▀▀▀▀
▗▄▀▘ ▝▀▀▀▀▘
▗▄▀▘
80 ─────────────▗▞▘──────────────────────────────────────────────────
▗▞▘
▄▘
▄▀
60 ──────▗▞──────────────────────────────────────────────────────────
▞▘
▄▀
40 ─▗▞───────────────────────────────────────────────────────────────
▞▘


20 ──────────────────────────────────────────────────────────────────
sec 2 4 6 8 10
94 wpm 100% acc 2.60 sd

(r)etry / (n)ew / (esc)ape
Evidence: Same keys on base 61f8fad, 80x24 (before)
84│wpm⠤⢄⣀⡀
│ ⠈⠉⠒⠒⠤⢄⣀⡀
│ ⠈⠉⠑⠒⠒⠤⠤⢄⣀⡀
│ ⠈⠉⠉⠒⠒⠢⠤⠤⠤⠤⠤⠤⠤⠤⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠢⠤⠤⠤⠤⠤⠤⢄⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀
│
│
│
│
│
│
│
│
│
│
0 │ seconds
└───────────────────────────────────────────────────────────────────
1 5.71
64 wpm 97% acc 1.17 sd

(r)etry / (n)ew / (esc)ape
Evidence: Transcript: word test (screens and checks)
# word test at 80x24: solid line, zoomed wpm axis, whole-second labels

  ok    prompt on one line: ['very he change old develop such']
command: thokr -w 6   (prompt drawn: 'very he change old develop such')
typed 31 keys over 5.70 s, key 15 wrong on purpose
  ok    results screen appears when the last key is typed
log.csv row: {'date': 'Mon Oct 12 15:24:18 2026', 'num_words': '6', 'num_secs': '', 'elapsed_secs': '5.71', 'wpm': '64', 'accuracy': '97', 'std_dev': '1.17'}

--- results, 80x24 ---


      90 ──────────────────────────────────────────────────────────────────


         ▀▄
           ▀▄
      80 ────▀▄────────────────────────────────────────────────────────────
               ▀▚▖
                 ▝▚▖
                   ▝▚▖
                     ▝▚▄
      70 ───────────────▀▀▄▖───────────────────────────────────────────────
                           ▝▀▚▄
                               ▀▀▄▖             ▄▄▄▄▄
                                  ▝▀▚▄▄▄▄▀▀▀▀▀▀▀     ▀▀▀▚▄▄▖
                                                           ▝▀▀▀▄▄▄▄▄▄▄▀▀▀▀▀
      60 ──────────────────────────────────────────────────────────────────
     sec 1             2             3             4            5
                           64 wpm   97% acc   1.17 sd

     (r)etry / (n)ew / (esc)ape


---
  ok    line unbroken, quadrant blocks only, inside the plot; one round wpm label per row on the row the canvas draws it; full-width rule at each; seconds labels whole, apart, under their seconds
  ok    no braille dots on screen
wpm labels: ['90', '80', '70', '60']   seconds labels: ['1', '2', '3', '4', '5']
  ok    more than two wpm labels (4)
  ok    wpm axis does not start at 0 (bottom label 60.0)
  ok    no seconds label with decimals
  ok    more than two seconds labels (5)
  ok    stats line wpm 64 equals log.csv wpm 64
  ok    accuracy 97% reflects the typo
  ok    line ends on the row of 64 wpm (rows [14], expected one of [13, 14])
  ok    line ends in the last column of the plot (the end of the test)
  ok    all 258 rule cells are dim dark gray (faint)
  ok    all 72 line cells are magenta, not dim
  ok    `sec` is faint
  ok    wpm labels are in the plain text colour
  ok    esc leaves with exit code 0

--- BEFORE (base 61f8fad), same keys, 80x24 - for comparison only ---


     84│wpm⠤⢄⣀⡀
       │      ⠈⠉⠒⠒⠤⢄⣀⡀
       │             ⠈⠉⠑⠒⠒⠤⠤⢄⣀⡀
       │                      ⠈⠉⠉⠒⠒⠢⠤⠤⠤⠤⠤⠤⠤⠤⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠢⠤⠤⠤⠤⠤⠤⢄⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀
       │
       │
       │
       │
       │
       │
       │
       │
       │
       │
     0 │                                                            seconds
       └───────────────────────────────────────────────────────────────────
       1                                                               5.71
                           64 wpm   97% acc   1.17 sd

     (r)etry / (n)ew / (esc)ape


---
before: 67 braille cells, 0 quadrant cells

RESULT: PASS
Evidence: Transcript: issue 22 shape at six sizes
# issue 22 shape (peak 98.4 wpm at second 5): whole peak drawn under a 100 label, at several sizes

command: thokr -s 10 -p <long prompt>
keys per second [3, 7, 10, 11, 10, 5, 7, 10, 10, 5]: cumulative wpm 36, 60, 80, 93, 98.4, 92, 90.9, 94.5, 97.3, 93.6
  ok    results screen appears when the 10 s run out
log.csv row: {'date': 'Mon Oct 12 15:25:48 2026', 'num_words': '15', 'num_secs': '10.00', 'elapsed_secs': '10.00', 'wpm': '94', 'accuracy': '100', 'std_dev': '2.60'}
  ok    final result 94 wpm as planned (log: 94)

--- results, 80x24 ---


     100 ───────────────────────────▗▄▄▄────────────────────────▄▄▄▄▄▄▄────
                              ▗▄▄▀▀▀▘   ▀▀▀▀▄▄▄▄▄▖    ▗▄▄▄▞▀▀▀▀▀       ▀▀▀▀
                           ▗▄▀▘                  ▝▀▀▀▀▘
                        ▗▄▀▘
      80 ─────────────▗▞▘──────────────────────────────────────────────────
                    ▗▞▘
                   ▄▘
                 ▄▀
      60 ──────▗▞──────────────────────────────────────────────────────────
              ▞▘
            ▄▀
      40 ─▗▞───────────────────────────────────────────────────────────────
         ▞▘


      20 ──────────────────────────────────────────────────────────────────
     sec        2              4             6              8            10
                           94 wpm   100% acc   2.60 sd

     (r)etry / (n)ew / (esc)ape


---
  ok    80x24: all axis and line checks
  ok    80x24: wpm axis 20..100 (['100', '80', '60', '40', '20'])
  ok    80x24: the peak's dot (second 5, 98.4 wpm) is drawn at dot (58, 1)
  ok    80x24: nothing drawn above the peak, peak not flattened (top dot row 1)
  ok    80x24: a dot in every dot column from second 1 to second 10 (132 of 132)
  ok    80x24: the `80` rule is on the row the line has 80 wpm in (rule row 6, point row 6)
  ok    all 312 rule cells are dim dark gray (faint)
  ok    all 76 line cells are magenta, not dim
  ok    `sec` is faint
  ok    wpm labels are in the plain text colour

--- results, 100x30 ---


     100 ────────────────────────────────────▗▄▄▄──────────────────────────────────▄▄▄▄▖───────
                                       ▗▄▄▞▀▀▘   ▀▀▀▄▄▄                  ▗▄▄▄▄▀▀▀▀▀    ▝▀▀▀▀▚▄▄
                                   ▗▄▀▀▘               ▀▀▀▀▀▀▚▄▄▄▄▄▄▞▀▀▀▀▘
                                 ▄▞▘
                              ▗▞▀
      80 ───────────────────▄▀▘────────────────────────────────────────────────────────────────
                          ▄▀
                        ▗▀
                      ▗▞▘
                     ▄▘
                   ▄▀
      60 ────────▗▞────────────────────────────────────────────────────────────────────────────
                ▄▘
              ▗▀
             ▞▘
           ▄▀
      40 ▗▞────────────────────────────────────────────────────────────────────────────────────
         ▘



      20 ──────────────────────────────────────────────────────────────────────────────────────
     sec 1        2         3        4         5        6         7        8         9       10
                                     94 wpm   100% acc   2.60 sd

     (r)etry / (n)ew / (esc)ape


---
  ok    100x30: all axis and line checks
  ok    100x30: wpm axis 20..100 (['100', '80', '60', '40', '20'])
  ok    100x30: the peak's dot (second 5, 98.4 wpm) is drawn at dot (76, 1)
  ok    100x30: nothing drawn above the peak, peak not flattened (top dot row 1)
  ok    100x30: a dot in every dot column from second 1 to second 10 (172 of 172)
  ok    100x30: the `80` rule is on the row the line has 80 wpm in (rule row 7, point row 7)
  ok    100x30: the `5` label is under the peak (label column 47, peak cells [45, 46, 47, 48, 83, 84, 85, 86, 87])

--- results, 80x27 ---


     100 ───────────────────────────▗▄▄▖─────────────────────────▗▄▄▄──────
                              ▗▄▄▀▀▀▘  ▝▀▀▄▄▖           ▗▄▄▄▄▄▀▀▀▘   ▀▀▀▀▄▄
                            ▄▞▘             ▝▀▀▀▀▀▀▀▀▀▀▀▘
                         ▗▄▀
      80 ──────────────▗▞▘─────────────────────────────────────────────────
                      ▞▘
                    ▗▞
                   ▄▘
                  ▞
      60 ───────▗▀─────────────────────────────────────────────────────────
               ▞▘
             ▗▞
            ▗▘
           ▞▘
      40 ▗▞────────────────────────────────────────────────────────────────
         ▘


      20 ──────────────────────────────────────────────────────────────────
     sec        2              4             6              8            10
                           94 wpm   100% acc   2.60 sd

     (r)etry / (n)ew / (esc)ape


---
  ok    80x27: all axis and line checks
  ok    80x27: wpm axis 20..100 (['100', '80', '60', '40', '20'])
  ok    80x27: the peak's dot (second 5, 98.4 wpm) is drawn at dot (58, 1)
  ok    80x27: nothing drawn above the peak, peak not flattened (top dot row 1)
  ok    80x27: a dot in every dot column from second 1 to second 10 (132 of 132)
  ok    80x27: the `80` rule is on the row the line has 80 wpm in (rule row 6, point row 6)
  ok    80x31: all axis and line checks
  ok    80x31: wpm axis 20..100 (['100', '80', '60', '40', '20'])
  ok    80x31: the peak's dot (second 5, 98.4 wpm) is drawn at dot (58, 1)
  ok    80x31: nothing drawn above the peak, peak not flattened (top dot row 1)
  ok    80x31: a dot in every dot column from second 1 to second 10 (132 of 132)
  ok    80x31: the `80` rule is on the row the line has 80 wpm in (rule row 7, point row 7)
  ok    80x35: all axis and line checks
  ok    80x35: wpm axis 20..100 (['100', '80', '60', '40', '20'])
  ok    80x35: the peak's dot (second 5, 98.4 wpm) is drawn at dot (58, 1)
  ok    80x35: nothing drawn above the peak, peak not flattened (top dot row 1)
  ok    80x35: a dot in every dot column from second 1 to second 10 (132 of 132)
  ok    80x35: the `80` rule is on the row the line has 80 wpm in (rule row 8, point row 8)
  ok    80x39: all axis and line checks
  ok    80x39: wpm axis 20..100 (['100', '80', '60', '40', '20'])
  ok    80x39: the peak's dot (second 5, 98.4 wpm) is drawn at dot (58, 1)
  ok    80x39: nothing drawn above the peak, peak not flattened (top dot row 1)
  ok    80x39: a dot in every dot column from second 1 to second 10 (132 of 132)
  ok    80x39: the `80` rule is on the row the line has 80 wpm in (rule row 9, point row 9)

--- BEFORE (base 61f8fad), same keys, 80x24 - for comparison only ---


     99│wpm                    ⣀⣀⠤⠤⠒⠒⠉⠑⠒⠢⠤⢄⣀⡀             ⣀⣀⣀⡠⠤⠤⠤⠔⠒⠒⠒⠤⠤⠤⠤⣀⣀
       │                   ⣀⠤⠒⠉             ⠈⠉⠉⠉⠉⠒⠒⠒⠒⠒⠊⠉⠉⠉
       │               ⣀⠤⠒⠉
       │            ⢀⠔⠊
       │          ⡠⠒⠁
       │       ⢀⠔⠊
       │     ⢀⠔⠁
       │   ⢀⠔⠁
       │ ⢀⠔⠁
       │⠔⠁
       │
       │
       │
       │
     0 │                                                            seconds
       └───────────────────────────────────────────────────────────────────
       1                                                              10.00
                           94 wpm   100% acc   2.60 sd

     (r)etry / (n)ew / (esc)ape


---

RESULT: PASS
Evidence: Transcript: resize sweep
# resizing the results screen: one wpm label per row on its own rule, `sec` in the chart, no crash, down to a one-row plot and smaller

  ok    120x40: thokr still running
  ok    120x40: plot 106x32, wpm labels (plot row, value) [(0, '100'), (8, '80'), (16, '60'), (23, '40'), (31, '20')], seconds ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']
  ok    120x40: no more wpm labels (5) than plot rows (32)
  ok    200x60: thokr still running
  ok    200x60: plot 186x52, wpm labels (plot row, value) [(0, '100'), (13, '80'), (26, '60'), (38, '40'), (51, '20')], seconds ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']
  ok    200x60: no more wpm labels (5) than plot rows (52)
  ok    60x20: thokr still running
  ok    60x20: plot 46x12, wpm labels (plot row, value) [(0, '100'), (6, '50'), (11, '0')], seconds ['2', '4', '6', '8', '10']
  ok    60x20: no more wpm labels (3) than plot rows (12)
  ok    40x14: thokr still running
  ok    40x14: plot 26x6, wpm labels (plot row, value) [(0, '100'), (3, '50'), (5, '0')], seconds ['5', '10']
  ok    40x14: no more wpm labels (3) than plot rows (6)

--- 40x14 ---


     100 ───────▗▄▄▞▀▀▄▄▄▄▄▄▄▄▞▀▀▚▄
             ▄▀▀▘
           ▄▀
      50 ▄▀────────────────────────

       0 ──────────────────────────
     sec            5            10
       94 wpm   100% acc   2.60 sd

     (r)etry / (n)ew / (esc)ape


---
  ok    30x14: thokr still running
  ok    30x14: plot 16x6, wpm labels (plot row, value) [(0, '100'), (3, '50'), (5, '0')], seconds ['5', '10']
  ok    30x14: no more wpm labels (3) than plot rows (6)

--- 30x14 ---


     100 ────▗▄▞▀▄▄▄▄▄▞▀▄
           ▗▞▘
          ▞▘
      50 ▞───────────────

       0 ────────────────
     sec        5      10
     94 wpm   100% acc

     (r)etry / (n)ew / (e


---
  ok    30x12: thokr still running
  ok    30x12: plot 16x4, wpm labels (plot row, value) [(0, '100'), (2, '50'), (3, '0')], seconds ['5', '10']
  ok    30x12: no more wpm labels (3) than plot rows (4)

--- 30x12 ---


     100 ───▗▞▀▀▀▄▄▄▞▀▀▀▀
          ▄▞▘
      50 ▀───────────────
       0 ────────────────
     sec        5      10
     94 wpm   100% acc

     (r)etry / (n)ew / (e


---
  ok    30x11: thokr still running
  ok    30x11: plot 16x3, wpm labels (plot row, value) [(0, '100'), (1, '50'), (2, '0')], seconds ['5', '10']
  ok    30x11: no more wpm labels (3) than plot rows (3)

--- 30x11 ---


     100 ───▄▞▀▀▀▀▀▀▀▀▀▀▀
      50 ▄▀▀─────────────
       0 ────────────────
     sec        5      10
     94 wpm   100% acc

     (r)etry / (n)ew / (e


---
  ok    30x10: thokr still running
  ok    30x10: plot 16x2, wpm labels (plot row, value) [(0, '100'), (1, '0')], seconds ['5', '10']
  ok    30x10: no more wpm labels (2) than plot rows (2)

--- 30x10 ---


     100 ─▄▄▄▞▀▀▀▀▀▀▀▀▀▀▀
       0 ▀───────────────
     sec        5      10
     94 wpm   100% acc

     (r)etry / (n)ew / (e


---
  ok    30x9: thokr still running
  ok    30x9: plot 16x1, wpm labels (plot row, value) [(0, '100')], seconds ['5', '10']
  ok    30x9: no more wpm labels (1) than plot rows (1)

--- 30x9 ---


     100 ▄▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
     sec        5      10
     94 wpm   100% acc

     (r)etry / (n)ew / (e


---
  ok    30x8: thokr still running
30x8: no room for the chart

--- 30x8 ---



     94 wpm   100% acc

     (r)etry / (n)ew / (e


---
  ok    30x6: thokr still running
30x6: no room for the chart

--- 30x6 ---






---
  ok    20x9: thokr still running
  ok    20x9: plot 6x1, wpm labels (plot row, value) [(0, '100')], seconds ['5', '10']
  ok    20x9: no more wpm labels (1) than plot rows (1)

--- 20x9 ---


     100 ▞▀▀▀▀▀
     sec   5 10
     94 wpm   1

     (r)etry /


---
  ok    17x10: thokr still running
  ok    17x10: plot 3x2, wpm labels (plot row, value) [(0, '100'), (1, '0')], seconds ['5']
  ok    17x10: no more wpm labels (2) than plot rows (2)

--- 17x10 ---


     100 ▗▀▀
       0 ▘──
     sec  5
     94 wpm

     (r)etry


---
  ok    12x5: thokr still running
12x5: no room for the chart

--- 12x5 ---





---
  ok    5x3: thokr still running
5x3: no room for the chart

--- 5x3 ---



---
  ok    2x2: thokr still running
2x2: no room for the chart

--- 2x2 ---


---
  ok    1x1: thokr still running
1x1: no room for the chart

--- 1x1 ---

---
  ok    back at 80x24 the screen is drawn exactly as before the resizes

--- 80x24 after all resizes ---


     100 ───────────────────────────▗▄▄▄────────────────────────▄▄▄▄▄▄▄────
                              ▗▄▄▀▀▀▘   ▀▀▀▀▄▄▄▄▄▖    ▗▄▄▄▞▀▀▀▀▀       ▀▀▀▀
                           ▗▄▀▘                  ▝▀▀▀▀▘
                        ▗▄▀▘
      80 ─────────────▗▞▘──────────────────────────────────────────────────
                    ▗▞▘
                   ▄▘
                 ▄▀
      60 ──────▗▞──────────────────────────────────────────────────────────
              ▞▘
            ▄▀
      40 ─▗▞───────────────────────────────────────────────────────────────
         ▞▘


      20 ──────────────────────────────────────────────────────────────────
     sec        2              4             6              8            10
                           94 wpm   100% acc   2.60 sd

     (r)etry / (n)ew / (esc)ape


---
  ok    esc leaves with exit code 0, no panic

RESULT: PASS
Evidence: Transcript: seconds labels under spikes
# 8 s timed test with spikes at seconds 3 and 6: the `3` and `6` labels are in the column of each spike's tip

command: thokr -s 8 -p <long prompt>
keys per second [2, 2, 14, 1, 1, 30, 1, 1]: cumulative wpm 24, 24, 72, 57, 48, 100, 87.4, 78
  ok    results screen appears when the 8 s run out
log.csv row: {'date': 'Mon Oct 12 15:26:45 2026', 'num_words': '15', 'num_secs': '8.00', 'elapsed_secs': '8.00', 'wpm': '78', 'accuracy': '100', 'std_dev': '10.25'}
  ok    final result 78 wpm as planned (log: 78)

--- results, 80x24 ---


     100 ──────────────────────────────────────────────▗▀▄▄────────────────
                                                      ▗▘   ▀▀▄▖
                                                     ▗▘       ▝▀▚▄▖
                                                    ▗▘            ▝▀▀▄▄
      80 ──────────────────────────────────────────▗▘──────────────────▀▀▚▄
                           ▗▖                      ▞
                          ▗▘▝▀▄▖                  ▞
                         ▗▘    ▝▀▚▄              ▞
      60 ───────────────▗▘─────────▀▚▄▄─────────▞──────────────────────────
                       ▗▘              ▀▀▀▄▄▄  ▞
                      ▗▘                     ▀▀
      40 ────────────▗▘────────────────────────────────────────────────────
                    ▗▘
                   ▗▘
         ▄▄▄▄▄▄▄▄▄▄▘
      20 ──────────────────────────────────────────────────────────────────
     sec 1        2        3         4        5         6        7        8
                          78 wpm   100% acc   10.25 sd

     (r)etry / (n)ew / (esc)ape


---
  ok    80x24: all axis and line checks
80x24: seconds labels [('1', 9), ('2', 18), ('3', 27), ('4', 37), ('5', 46), ('6', 56), ('7', 65), ('8', 74)]
  ok    80x24: `3` is in column 27, the tip of the first spike is in column 27
  ok    80x24: `6` is in column 56, the tip of the second spike is in column 56
  ok    80x24: the tips are in plot columns 18 and 47 (18 and 47 on the canvas grid)
  ok    100x30: all axis and line checks
100x30: seconds labels [('1', 9), ('2', 21), ('3', 33), ('4', 45), ('5', 58), ('6', 70), ('7', 82), ('8', 94)]
  ok    100x30: `3` is in column 33, the tip of the first spike is in column 33
  ok    100x30: `6` is in column 70, the tip of the second spike is in column 70
  ok    90x24: all axis and line checks
90x24: seconds labels [('1', 9), ('2', 20), ('3', 30), ('4', 41), ('5', 52), ('6', 63), ('7', 73), ('8', 84)]
  ok    90x24: `3` is in column 30, the tip of the first spike is in column 30
  ok    90x24: `6` is in column 63, the tip of the second spike is in column 63

--- results, 64x20 ---


     100 ───────────────────────────────────▞▀▄▄▖──────────
                                          ▗▞    ▝▀▚▄▄▖
                                         ▗▘          ▝▀▀▀▄▄
                      ▗▀▄▄              ▗▘
                     ▞▘   ▀▚▄▖         ▄▘
                    ▞        ▝▀▀▄▄▄▄  ▞
      50 ─────────▗▀────────────────▀▀─────────────────────
                 ▄▘
         ▄▄▄▄▄▄▄▞


       0 ──────────────────────────────────────────────────
     sec        2             4             6             8
                  78 wpm   100% acc   10.25 sd

     (r)etry / (n)ew / (esc)ape


---
  ok    64x20: all axis and line checks
64x20: seconds labels [('2', 16), ('4', 30), ('6', 44), ('8', 58)]
  --    64x20: no `3` label at this width (labels every 2 s)
  ok    64x20: `6` is in column 44, the tip of the second spike is in column 44

RESULT: PASS
Evidence: Transcript: one point and paste
# a test shorter than a second: a short dash at the one point, seconds still labelled

command: thokr -p hi   (h, then i 0.4 s later)
  ok    results screen appears
log.csv row: {'date': 'Mon Oct 12 15:26:24 2026', 'num_words': '15', 'num_secs': '', 'elapsed_secs': '0.40', 'wpm': '60', 'accuracy': '100', 'std_dev': '0.00'}

--- results, 80x24 ---


      65 ──────────────────────────────────────────────────────────────────




      60 ─────────────────────────▄▄▄──────────────────────────────────────




      55 ──────────────────────────────────────────────────────────────────




      50 ──────────────────────────────────────────────────────────────────
     sec                                 0.5                              1
                           60 wpm   100% acc   0.00 sd

     (r)etry / (n)ew / (esc)ape


---
  ok    all axis and line checks
  ok    the test took 0.4 s
  ok    the line is one dash, 3 cells wide, on one row
  ok    the dash (columns [34, 35, 36]) is at the moment the test ended (column 35)
  ok    seconds labels: ['0.5', '1']
  ok    result 60 wpm lies on the wpm axis 50.0..65.0

command: thokr -p 'the quick brown fox jumps over the lazy dog'   (all 43 keys sent at once, as a paste)
  ok    results screen appears
log.csv row: {'date': 'Mon Oct 12 15:26:24 2026', 'num_words': '15', 'num_secs': '', 'elapsed_secs': '0.00', 'wpm': '529390', 'accuracy': '100', 'std_dev': '0.00'}

--- results, 80x24 ---


     529395 ───────────────────────────────────────────────────────────────




     529390 ▄▄─────────────────────────────────────────────────────────────




     529385 ───────────────────────────────────────────────────────────────




     529380 ───────────────────────────────────────────────────────────────
     sec                                  0.5                             1
                         529390 wpm   100% acc   0.00 sd

     (r)etry / (n)ew / (esc)ape


---
  ok    thokr still running
  ok    `sec` starts at the left edge of the chart (column 5)
  ok    wpm labels ['529395', '529390', '529385', '529380']
  ok    wpm label 529395 is inside the chart and clear of the plot
  ok    rule of 529395 runs the width of the plot
  ok    wpm label 529390 is inside the chart and clear of the plot
  ok    rule of 529390 runs the width of the plot
  ok    wpm label 529385 is inside the chart and clear of the plot
  ok    rule of 529385 runs the width of the plot
  ok    wpm label 529380 is inside the chart and clear of the plot
  ok    rule of 529380 runs the width of the plot
  ok    one dash drawn (2 cells)
  ok    seconds labels: ['0.5', '1']

RESULT: PASS
Evidence: Transcript: no correct key
# a timed test with no correct key: empty chart with its axes, no crash

command: thokr -s 1 -p 'hello world'   (one wrong key, then the second runs out)
  ok    results screen appears when the second runs out

--- results, 80x24 ---


      10 ──────────────────────────────────────────────────────────────────







       5 ──────────────────────────────────────────────────────────────────






       0 ──────────────────────────────────────────────────────────────────
     sec                                 0.5                              1
                            0 wpm   0% acc   0.00 sd

     (r)etry / (n)ew / (esc)ape


---
  ok    all axis checks
  ok    no line, there is nothing to plot
  ok    stats: (0, 0, 0.0)
  ok    wpm axis 0..10: ['10', '5', '0']
  ok    30x10: thokr still running
  ok    12x5: thokr still running
  ok    80x24: thokr still running
  ok    esc leaves with exit code 0, no panic

RESULT: PASS
  • Evidence: Transcript: 45 s test in a narrow terminal (local file: ~/.no-mistakes/evidence/01M4R46D67RX0PXCDGXHSM9S6S/07-45s-narrow.txt)
Evidence: Transcript: `sec` label column
# `sec` starts at the left edge of the chart whatever the width of the wpm labels

word test, labels ['90', '80', '70', '60']: `sec` at column 5, plot from column 9
issue 22 test, labels ['100', '80', '60', '40', '20']: `sec` at column 5, plot from column 9
pasted test, labels ['529395', '529390', '529385', '529380']: `sec` at column 5
  ok    word test result is under 100 wpm (two digit labels)
  ok    issue 22 test reaches 100 (three digit labels)
  ok    `sec` starts at column 5 in all three
  ok    the plot starts in the same column under 100 wpm and at 100

RESULT: PASS
Evidence: Transcript: snapshots read vs files on disk
# snapshot tests kept: the snapshots the ui tests read are exactly the files in src/snapshots

command: INSTA_UPDATE=no INSTA_SNAPSHOT_REFERENCES_FILE=refs.txt cargo test --locked ui::   (26 passed, 0 failed)

snapshots read by the tests (insta's own reference list):
  thokr__ui__tests__results_screen.snap
  thokr__ui__tests__results_screen_for_an_empty_prompt.snap
  thokr__ui__tests__results_screen_in_a_narrow_terminal.snap
  thokr__ui__tests__running_in_a_narrow_terminal.snap
  thokr__ui__tests__running_in_a_terminal_too_small_for_the_prompt.snap
  thokr__ui__tests__running_with_correct_and_incorrect_input.snap
  thokr__ui__tests__running_wraps_and_left_aligns_a_long_prompt.snap

files in src/snapshots:
  thokr__ui__tests__results_screen.snap
  thokr__ui__tests__results_screen_for_an_empty_prompt.snap
  thokr__ui__tests__results_screen_in_a_narrow_terminal.snap
  thokr__ui__tests__running_in_a_narrow_terminal.snap
  thokr__ui__tests__running_in_a_terminal_too_small_for_the_prompt.snap
  thokr__ui__tests__running_with_correct_and_incorrect_input.snap
  thokr__ui__tests__running_wraps_and_left_aligns_a_long_prompt.snap

RESULT: PASS - 7 files, each read by a test, none left over
  • Evidence: Driver scripts used (tmux driver, scenarios, screenshot renderer) (local file: ~/.no-mistakes/evidence/01M4R46D67RX0PXCDGXHSM9S6S/driver)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed (3) ✅
  • ⚠️ src/ui.rs:494 - Four tests were converted from insta::assert_snapshot! to plain assertions, but their .snap files were left in the tree, so src/snapshots/ still holds 11 files for 7 snapshot tests. The commit message says the snapshots were trimmed to seven. Nothing reads these files any more, cargo test will not flag them, and one of them still records the old braille Chart screen (48│wpm, seconds, 5.00), which now misdescribes the results screen. Delete all four: src/snapshots/thokr__ui__tests__pace_caret_ahead_of_the_typist.snap (test at src/ui.rs:489-495), src/snapshots/thokr__ui__tests__pace_caret_behind_the_typist.snap (src/ui.rs:498-504), src/snapshots/thokr__ui__tests__timed_test_shows_the_countdown.snap (src/ui.rs:507-513), src/snapshots/thokr__ui__tests__results_screen_after_the_timer_ran_out.snap (src/ui.rs:771-787). The other three removed snapshot tests (running_fresh, timed_test_in_a_narrow_terminal, results_chart_draws_a_peak_with_a_fraction_below_one_half) had their files deleted correctly.

🔧 Fix applied.
3 issues (2 warnings, 1 info) still open:

  • ⚠️ src/ui.rs:494 - Four tests were converted from insta::assert_snapshot! to plain assertions, but their .snap files were left in the tree, so src/snapshots/ still holds 11 files for 7 snapshot tests. The commit message says the snapshots were trimmed to seven. Nothing reads these files any more, cargo test will not flag them, and one of them still records the old braille Chart screen (48│wpm, seconds, 5.00), which now misdescribes the results screen. Delete all four: src/snapshots/thokr__ui__tests__pace_caret_ahead_of_the_typist.snap (test at src/ui.rs:489-495), src/snapshots/thokr__ui__tests__pace_caret_behind_the_typist.snap (src/ui.rs:498-504), src/snapshots/thokr__ui__tests__timed_test_shows_the_countdown.snap (src/ui.rs:507-513), src/snapshots/thokr__ui__tests__results_screen_after_the_timer_ran_out.snap (src/ui.rs:771-787). The other three removed snapshot tests (running_fresh, timed_test_in_a_narrow_terminal, results_chart_draws_a_peak_with_a_fraction_below_one_half) had their files deleted correctly.
  • ⚠️ src/ui.rs:279 - The wpm rules are mislabelled when the plot is 2 rows tall (a 10-row terminal: chart_area height 3, minus the seconds row). wpm_scale floors intervals at 2 (line 165), so the scale can carry 3 ticks, but row() (line 235) maps them onto 2 rows. Traced with the finished() data at 80x10: scale is 20/40/60, row(60) = 0, row(40) = round(0.5 * 1) = 1, row(20) = 1. axes draws ticks in ascending order, so "40" overwrites "20" and the screen reads 60 on the top rule and 40 on the bottom rule while the canvas y-bounds are 20..60 - the final 36 wpm is plotted in a row labelled 40. Nothing fails, and no test covers it: the_results_chart_is_readable_at_every_size_and_length only asserts no panic below 60x20. Fix in axes: draw the extremes first (hi, then lo, then the middle ticks) and skip any tick whose row already holds a label, so the top and bottom rows always carry hi and lo. Same loop, sibling case: at 1 plot row (9-row terminal) every tick lands on row 0; the same fix leaves hi there. Collisions cannot happen at 3 rows or more (ticks - 1 <= rows - 1 holds from there), and the seconds axis already skips collisions through next_free (line 264), so only this axis is affected.
  • ℹ️ src/ui.rs:297 - The sec label is drawn one column outside the chart area whenever the top wpm tick is under 100. The gutter is sized from the tick labels alone (lines 196-202: 2 chars + 1 = 3), but sec is 3 chars and is placed at area.x - 4, which is chart.x - 1. On screen it starts at column 4 while the wpm labels, the stats and the legend all keep the 5-column margin (visible as ....bbb in the style rows of all three results snapshots). At 100+ wpm the gutter is 4 and sec starts at column 5, so its position also shifts between results. It only works because the single caller leaves a left margin; for a chart at x = 0 the checked_sub(4) silently drops the label. Asking because the remedy changes the layout rather than correcting a computation: size the gutter to at least the 3 chars of sec (the plot moves one column right under 100 wpm), or keep it if the right-aligned overhang is intended.

🔧 Fix applied.
5 issues (4 warnings, 1 info) still open:

  • ⚠️ src/ui.rs:494 - Four tests were converted from insta::assert_snapshot! to plain assertions, but their .snap files were left in the tree, so src/snapshots/ still holds 11 files for 7 snapshot tests. The commit message says the snapshots were trimmed to seven. Nothing reads these files any more, cargo test will not flag them, and one of them still records the old braille Chart screen (48│wpm, seconds, 5.00), which now misdescribes the results screen. Delete all four: src/snapshots/thokr__ui__tests__pace_caret_ahead_of_the_typist.snap (test at src/ui.rs:489-495), src/snapshots/thokr__ui__tests__pace_caret_behind_the_typist.snap (src/ui.rs:498-504), src/snapshots/thokr__ui__tests__timed_test_shows_the_countdown.snap (src/ui.rs:507-513), src/snapshots/thokr__ui__tests__results_screen_after_the_timer_ran_out.snap (src/ui.rs:771-787). The other three removed snapshot tests (running_fresh, timed_test_in_a_narrow_terminal, results_chart_draws_a_peak_with_a_fraction_below_one_half) had their files deleted correctly.
  • ⚠️ src/ui.rs:279 - The wpm rules are mislabelled when the plot is 2 rows tall (a 10-row terminal: chart_area height 3, minus the seconds row). wpm_scale floors intervals at 2 (line 165), so the scale can carry 3 ticks, but row() (line 235) maps them onto 2 rows. Traced with the finished() data at 80x10: scale is 20/40/60, row(60) = 0, row(40) = round(0.5 * 1) = 1, row(20) = 1. axes draws ticks in ascending order, so "40" overwrites "20" and the screen reads 60 on the top rule and 40 on the bottom rule while the canvas y-bounds are 20..60 - the final 36 wpm is plotted in a row labelled 40. Nothing fails, and no test covers it: the_results_chart_is_readable_at_every_size_and_length only asserts no panic below 60x20. Fix in axes: draw the extremes first (hi, then lo, then the middle ticks) and skip any tick whose row already holds a label, so the top and bottom rows always carry hi and lo. Same loop, sibling case: at 1 plot row (9-row terminal) every tick lands on row 0; the same fix leaves hi there. Collisions cannot happen at 3 rows or more (ticks - 1 <= rows - 1 holds from there), and the seconds axis already skips collisions through next_free (line 264), so only this axis is affected.
  • ℹ️ src/ui.rs:297 - The sec label is drawn one column outside the chart area whenever the top wpm tick is under 100. The gutter is sized from the tick labels alone (lines 196-202: 2 chars + 1 = 3), but sec is 3 chars and is placed at area.x - 4, which is chart.x - 1. On screen it starts at column 4 while the wpm labels, the stats and the legend all keep the 5-column margin (visible as ....bbb in the style rows of all three results snapshots). At 100+ wpm the gutter is 4 and sec starts at column 5, so its position also shifts between results. It only works because the single caller leaves a left margin; for a chart at x = 0 the checked_sub(4) silently drops the label. Asking because the remedy changes the layout rather than correcting a computation: size the gutter to at least the 3 chars of sec (the plot moves one column right under 100 wpm), or keep it if the right-aligned overhang is intended.
  • ⚠️ src/ui.rs:254 - The seconds axis is drawn with no labels at all for some test lengths in a narrow terminal. seconds_labels picks step = nice_step(span, intervals) with intervals = (width / 9).clamp(2, 10) (line 249), and with 2 intervals the step can be larger than the last second, so the while secs &lt;= self.x1 loop (line 262) never runs. Traced: terminal 40 columns wide (chart 30, gutter 4, plot 26, so intervals = 2), a test whose last point is at 45 s: x0 = 1, x1 = 45, span = 44, raw = 22, step = 50, first multiple = ceil(1/50)*50 = 50 > 45, so the function returns an empty list and the row shows only sec. A simulation of the function gives an empty axis for every plot width from 3 to 26 when the last point is between 41.1 and 49.9 s, and again between 401 and 499.9 s. Same cause, sibling path: one point alone (x0 = 0, x1 = 2 * last, line 216) with x1 between 4 and 5 or between 40 and 50, for example a single point at 2.2 s. Nothing fails and no test covers it: seconds_labels_sit_under_their_second_and_do_not_touch asserts a non-empty list at widths 24 and 12, but only for a 30 s test. Fix in seconds_labels: when no multiple of step lies in x0..=x1, fall back to the next smaller round step (50 to 20, 5 to 2), for example by raising intervals by one and calling nice_step again until the first label is at or before x1. With 3 intervals the 45 s case gets step 20 and labels 20 and 40. No committed snapshot changes, since each of them already has a label. Add the 45 s case at width 24 to the existing test. This code is from the original commit, not from a fix round.
  • ⚠️ src/ui.rs:236 - Seconds labels and wpm rules are placed with a different rounding than the one the canvas uses for the line, so at many sizes a label sits one cell away from the point it names. col (line 236) rounds t * (width - 1) on the cell grid. The canvas (ratatui-widgets 0.3.2, Painter::get_point, with a 2x2 quadrant grid) rounds t * (2 * width - 1) on the dot grid and then halves it. The two disagree by one cell at some sizes. Traced on a standard 80 column terminal (plot 66 wide) with an 8 second test, x from 1 to 8: second 3 has t = 2/7, col gives round(18.57) = 19, the canvas gives round(37.43) = 37, which is cell 18. The label 3 is drawn one column right of the point of second 3, and 6 one column left of its point (46 against 47). At 80 columns this happens for 18 of the 58 whole-second test lengths from 3 to 60, at 100 columns for 28. The intent asks for "seconds labels placed exactly under the second they name". Same invariant, other axis: row (line 242) rounds t * (height - 1) while the canvas rounds t * (2 * height - 1) and halves. With the issue 22 data (scale 20 to 100, step 20) in a terminal 27 rows high (plot 19 rows), the 80 rule is on plot row 5 while the canvas draws 80 wpm on row 4; the row that carries the 80 label holds 75.1 to 79.5 wpm. The same happens at heights 31, 35 and 39 for that data, and at 32 and 37 for the finished() data (rules 55 and 40). Fix both in one place each by using the canvas's own mapping: self.area.x + (t * (2 * width - 1)).round() / 2 in col, and the same with the height in row (guard the zero size). Ticks stay on separate rows, since neighbouring ticks are more than 2 dots apart whenever the scale has at most rows - 1 intervals, which the round 2 change to wpm_scale already guarantees. None of the three committed results snapshots change: at their sizes both roundings agree. row and col are from the original commit; the round 2 fix changed which ticks are drawn but not where a value lands, and its test every_wpm_label_is_on_the_rule_of_its_own_value compares a label against plot.row itself, so it cannot see this.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 12 of 13 scenarios driven live against the product
Scenario Result Live Evidence
Finish a default word test (thokr -w 6) at 80x24: the results graph is one unbroken line of quadrant blocks, with no braille dots ✅ pass live 01-word-test.txt, 01-before-after.png (top row: base vs change, same keys)
Same word test (64 wpm): the wpm axis is zoomed to the data on round numbers (60, 70, 80, 90 - not from 0), one label per row, with a faint full-width rule at each ✅ pass live 01-word-test.txt (labels, rule cells all dim dark gray), 01-before-after.png
Same word test (5.71 s): seconds labels read 1 2 3 4 5 (no 5.71), each in the column the canvas draws that second in ✅ pass live 01-word-test.txt (label columns checked against the end time in the throwaway log.csv)
Issue 22 shape (10 s timed test, peak 98.4 wpm at second 5, final 94 wpm): the whole peak is drawn under a 100 label and the line has a dot in every dot column from second 1 to 10, at 80x24, 100x30, 8… ✅ pass live 02-issue22-peak.txt, 01-before-after.png (bottom row), 02-labels-on-the-line.png
Same issue 22 result at heights 24, 27, 30, 31, 35, 39: the 80 rule is on the row where the line passes 80 wpm at second 3 (the heights the review found one row off) ✅ pass live 02-issue22-peak.txt (rule row vs point row per size), 02-labels-on-the-line.png
8 s timed test with spikes at seconds 3 and 6: the 3 and 6 labels are in the column of each spike's tip (80x24: plot columns 18 and 47; also 100x30, 90x24, 64x20) ✅ pass live 04-seconds-under-spikes.txt, 02-labels-on-the-line.png (top row)
Resize the results screen from 200x60 down to 1x1 and back: never more wpm labels than plot rows, each label on the row of its own value down to two-row and one-row plots, thokr keeps running, and 80x… ✅ pass live 03-resize.txt, 03-resize.png
A test shorter than a second (thokr -p hi, 0.4 s): the one point is drawn as a three-cell dash at the moment the test ended, and the seconds row is labelled (0.5, 1) ✅ pass live 05-one-point.txt, 04-edge-cases.png (top left)
43 keys pasted at once (529390 wpm, six-digit labels): labels stay inside the chart with a full rule each, a dash is drawn, no crash ✅ pass live 05-one-point.txt (second half), 04-edge-cases.png (top right)
Timed test with no correct key (thokr -s 1, one wrong key): empty chart keeps its axes (0, 5, 10 and 0.5, 1), shows 0 wpm, survives resizes, exits 0 ✅ pass live 06-no-correct-keys.txt, 04-edge-cases.png (bottom left)
45.3 s test in a 40 column terminal, then widths 38 down to 17 and up to 100: the seconds row has a label at every width (20 and 40 at 40 columns) ✅ pass live 07-45s-narrow.txt, 04-edge-cases.png (bottom right)
sec starts at the chart's left edge (column 5) for a result under 100 wpm, at 100 and at six digits, and the plot starts in the same column under 100 and at 100 ✅ pass live 08-sec-label-column.txt
The snapshot tests kept are the 7: the ui tests read exactly the 7 files in src/snapshots, none left over ⏸️ untested no Not driven live. The prior run checked this only by running cargo test --locked ui:: (26 passed) and comparing the snapshots insta reported as read with the files in src/snapshots (transcript 09-sna…
  • nix shell nixpkgs#cargo nixpkgs#rustc -c cargo build --release --locked for the change (ae434ed) and for base 61f8fad, both into a temp target directory
  • thokr -w 6 at 80x24 in tmux: typed the drawn prompt over 5.7 s with one wrong key, checked the results screen, then esc (exit 0); same keys replayed on the base binary for the before picture
  • thokr -s 10 -p &lt;long prompt&gt; with 3,7,10,11,10,5,7,10,10,5 keys per second (peak 98.4 wpm at second 5, final 94 wpm), checked at 80x24, 100x30, 80x27, 80x31, 80x35, 80x39; same keys replayed on the base binary
  • Resized that running results screen through 120x40, 200x60, 60x20, 40x14, 30x14, 30x12, 30x11, 30x10, 30x9, 30x8, 30x6, 20x9, 17x10, 12x5, 5x3, 2x2, 1x1 and back to 80x24 with tmux resize-window, then esc (exit 0)
  • thokr -s 8 -p &lt;long prompt&gt; with 2,2,14,1,1,30,1,1 keys per second (spikes at seconds 3 and 6), checked at 80x24, 100x30, 90x24, 64x20
  • thokr -p hi typed in 0.4 s (one data point)
  • thokr -p &#39;the quick brown fox jumps over the lazy dog&#39; with all 43 keys sent at once (529390 wpm)
  • thokr -s 1 -p &#39;hello world&#39; with one wrong key, then the timer ran out; resized to 30x10, 12x5, 80x24; esc (exit 0)
  • thokr -p &lt;46 chars&gt; in a 40x24 terminal, one key a second, last key at 45.3 s; results checked at widths 40, 38, 36, 34, 32, 30, 28, 26, 24, 22, 20, 19, 18, 17, 60, 80, 100
  • ANSI colour check of the 80x24 results screens: rules and sec dim dark gray, line magenta, labels plain
  • INSTA_UPDATE=no INSTA_SNAPSHOT_REFERENCES_FILE=refs.txt cargo test --locked ui:: (26 passed), then compared insta's list of snapshots read with the files in src/snapshots
  • git status --short after cleanup: clean
⚠️ **Document** - 1 info
  • ℹ️ README.md:10 - The README demo gif still shows the old results graph. I decoded its last frame: a braille dot line, a wpm axis from 0 to 132 with two labels, a seconds axis labelled 1 and 9.00, and the titles wpm and seconds. This change replaces all of that with a solid line, a zoomed wpm axis with faint rules, and seconds labels under their second. Not fixable here: the gif lives in another repository (jrnxf/assets, thokr/demo.gif) and needs a new recording. The same frame also shows a (t)weet legend entry the app no longer has, so the gif was already out of date before this change. Follow-up: record a new demo and replace the file in the assets repository; the README link can stay.
⚠️ **Lint** - 1 warning
  • ⚠️ Neither configured lint check ran: cargo is not on PATH in this environment (command -v cargo rustc rustup rustfmt finds nothing; ~/.cargo/bin is on PATH but holds no cargo) and the repository supplies no local toolchain path. Blocked: cargo fmt --all -- --check and cargo clippy --all-features -- -D warnings, the two commands .github/workflows/build.yml defines. I did not search the machine or fetch a toolchain. Reading the changed code in src/ui.rs by eye shows rustfmt-shaped layout and no obvious default clippy lint, but that is not a substitute for the run. The CI fmt and lint jobs run the same two commands.
✅ **Push** - passed

✅ No issues found.

jrnxf added 5 commits October 12, 2026 14:13
The graph was a Chart of braille dots on a wpm axis that started at 0,
with two labels per axis, one of them reading 9.62.

It is now drawn with ratatui's Canvas and quadrant blocks: one
continuous line, the wpm axis zoomed to the data on round numbers (never
under 10 wpm) with a faint rule at each number, and seconds labels
placed under the second they name. A test with one data point draws a
short dash instead of nothing.

Also trim the screen snapshots to the seven that guard a layout, and
check the other five with a plain assertion on what each is about.
@jrnxf
jrnxf merged commit 55f08ae 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.

1 participant

0