8000 fix: keep the next line left-aligned when a wrapped line fills the width exactly by jrnxf · Pull Request #39 · jrnxf/thokr · GitHub
[go: up one dir, main page]

Skip to content

fix: keep the next line left-aligned when a wrapped line fills the width exactly - #39

Merged
jrnxf merged 4 commits into
mainfrom
fm/thokr-wrap-fix
Oct 25, 2026
Merged

jrnxf merged 4 commits into
mainfrom
fm/thokr-wrap-fix

Conversation

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

Intent

thokr an another pr

The fix referred to: a thokr bug on main where a wrapped line that fills the width exactly pushes the next line one space right (src/layout.rs:33).

What Changed

  • layout::wrap_chars now lets the first space after a full line hang one cell past the edge. Before, that space wrapped onto the next line and pushed the next word one column right. A second space after a full line still wraps. The doc comment and the width/column invariants in the tests now allow this one extra cell.
  • render_running in src/ui.rs makes the prompt's text area one column wider, capped at the terminal's right edge. A hanging space, including a mistyped one shown in red, is drawn instead of clipped. The cursor position is taken from the same x.
  • Adds unit tests for the hanging space and for a second space that wraps. Adds two snapshot tests: a line that is exactly full, and a mistyped hanging space. Updates two existing snapshots that change with the new wrap.

Risk Assessment

✅ Low: The change is small. It lets one space hang past a full line and widens the render area by one margin column so that space can be drawn. The fix-round change draws the hanging cell, shows the mistyped space and keeps the cursor at the same position, and the new snapshots cover it.

Testing

Built thokr at the base and target commits and drove the real TUI in tmux with isolated HOME directories. On base, the bug shows up: lines after an exactly-full line start one column right, at 15 and 80 columns. On the target, every line is left-aligned. The mistyped hanging space shows as a red '·' and the pace caret stays visible on the hanging cell. A second space still wraps, as designed. Narrow terminals and a complete test run to the results screen work. The targeted layout and running-snapshot tests pass. The visual evidence is a screenshot of live before/after tmux captures rendered with their real ANSI colors.

  • Live validation: ✅ go - 8 of 8 scenarios driven live against the product
Scenario Result Live Evidence
Prompt whose first line fills the width exactly: the next lines start at the left edge, not one column in ✅ pass live before-full-line.txt vs after-full-line.txt; before-after.png section 1
80-col terminal, long prompt: third line 'the epoch of incredulity' is aligned with the lines above ✅ pass live before-80col.txt vs after-80col.txt; before-after.png section 4
Typing the full word then the correct space moves the cursor to the start of the next line ✅ pass live after-typed-word (cursor 10,2) and after-typed-space (cursor 5,3) captures
Adversarial: mistyping the hanging space shows a red '·' at the end of the full line (not clipped) ✅ pass live after-mistyped-space.ansi shows ESC[38;5;1m· after the green aaaaa; before-after.png section 2
Pace caret sitting on the hanging space stays visible (reverse-video cell) ✅ pass live after-pace-on-hanging-space.ansi shows ESC[7m on the space at col 10; before-after.png section 3
Adversarial: a second consecutive space after a full line wraps to the next line instead of hanging ✅ pass live after-double-space.txt shows ' bbb' on line 2, which matches the a_second_space_after_a_full_line_wraps intent
Adversarial: very narrow terminals (width-1 prompt area, 8 cols) render without crashing, and the hanging cell stays inside the screen ✅ pass live after-11col.txt shows 'a·' on line 3 with the red dot drawn; after-tiny-term matches base (blank)
A full test still finishes and shows the results screen ✅ pass live after-complete.txt shows the results screen with 100% acc

Before/after screenshot of the live TUI captures

Evidence: Before/after rendered HTML
<html><body style="background:#111;color:#eee;font-family:sans-serif"><h2>thokr wrap fix: before (base 1280cef) vs after (c8a41c8), live tmux captures</h2>
<h3>Exactly-full line: next line no longer indented</h3><div style="display:flex;gap:24px">
<div><b>before</b><pre style="background:#1e1e1e;padding:8px;border:1px solid #444;font-size:15px;line-height:1.2"><span style="color:#ddd;background:#1e1e1e;">

     </span><span style="color:#ddd;background:#1e1e1e;font-weight:bold;opacity:.55;">aaaaa</span><span style="color:#ddd;background:#1e1e1e;">
     </span><span style="color:#ddd;background:#1e1e1e;font-weight:bold;opacity:.55;"> bbb </span><span style="color:#ddd;background:#1e1e1e;">
     </span><span style="color:#ddd;background:#1e1e1e;font-weight:bold;opacity:.55;">ccccc</span><span style="color:#ddd;background:#1e1e1e;">
     </span><span style="color:#ddd;background:#1e1e1e;font-weight:bold;opacity:.55;"> dd</span></pre></div>
<div><b>after</b><pre style="background:#1e1e1e;padding:8px;border:1px solid #444;font-size:15px;line-height:1.2"><span style="color:#ddd;background:#1e1e1e;">

     </span><span style="color:#ddd;background:#1e1e1e;font-weight:bold;opacity:.55;">aaaaa </span><span style="color:#ddd;background:#1e1e1e;">
     </span><span style="color:#ddd;background:#1e1e1e;font-weight:bold;opacity:.55;">bbb </span><span style="color:#ddd;background:#1e1e1e;">
     </span><span style="color:#ddd;background:#1e1e1e;font-weight:bold;opacity:.55;">ccccc </span><span style="color:#ddd;background:#1e1e1e;">
     </span><span style="color:#ddd;background:#1e1e1e;font-weight:bold;opacity:.55;">dd</span></pre></div>
</div>
<h3>Mistyped hanging space shows as red dot at end of full line</h3><div style="display:flex;gap:24px">
<div><b>before</b><pre style="background:#1e1e1e;padding:8px;border:1px solid #444;font-size:15px;line-height:1.2"><span style="color:#ddd;background:#1e1e1e;">

     </span><span style="color:#4fc04f;background:#1e1e1e;font-weight:bold;">aaaaa</span><span style="color:#ddd;background:#1e1e1e;">
     </span><span style="color:#e05252;background:#1e1e1e;font-weight:bold;">·</span><span style="color:#ddd;background:#1e1e1e;font-weight:bold;opacity:.55;">bbb </span><span style="color:#ddd;background:#1e1e1e;">
     </span><span style="color:#ddd;background:#1e1e1e;font-weight:bold;opacity:.55;">ccccc</span><span style="color:#ddd;background:#1e1e1e;">
     </span><span style="color:#ddd;background:#1e1e1e;font-weight:bold;opacity:.55;"> dd</span></pre></div>
<div><b>after</b><pre style="background:#1e1e1e;padding:8px;border:1px solid #444;font-size:15px;line-height:1.2"><span style="color:#ddd;background:#1e1e1e;">

     </span><span style="color:#4fc04f;background:#1e1e1e;font-weight:bold;">aaaaa</span><span style="color:#e05252;background:#1e1e1e;font-weight:bold;">·</span><span style="color:#ddd;background:#1e1e1e;">
     </span><span style="color:#ddd;background:#1e1e1e;font-weight:bold;opacity:.55;">bbb </span><span style="color:#ddd;background:#1e1e1e;">
     </span><span style="color:#ddd;background:#1e1e1e;font-weight:bold;opacity:.55;">ccccc </span><span style="color:#ddd;background:#1e1e1e;">
     </span><span style="color:#ddd;background:#1e1e1e;font-weight:bold;opacity:.55;">dd</span></pre></div>
</div>
<h3>Pace caret on the hanging space stays visible</h3><div style="display:flex;gap:24px">
<div><b>before</b><pre style="background:#1e1e1e;padding:8px;border:1px solid #444;font-size:15px;line-height:1.2"><span style="color:#ddd;background:#1e1e1e;">

     </span><span style="color:#4fc04f;background:#1e1e1e;font-weight:bold;">a</span><span style="color:#ddd;background:#1e1e1e;font-weight:bold;opacity:.55;">aaaa</span><span style="color:#ddd;background:#1e1e1e;">
     </span><span style="color:#1e1e1e;background:#ddd;font-weight:bold;opacity:.55;"> </span><span style="color:#ddd;background:#1e1e1e;font-weight:bold;opacity:.55;">bbb </span><span style="color:#ddd;background:#1e1e1e;">
     </span><span style="color:#ddd;background:#1e1e1e;font-weight:bold;opacity:.55;">ccccc</span><span style="color:#ddd;background:#1e1e1e;">
     </span><span style="color:#ddd;background:#1e1e1e;font-weight:bold;opacity:.55;"> dd</span></pre></div>
<div><b>after</b><pre style="background:#1e1e1e;padding:8px;border:1px solid #444;font-size:15px;line-height:1.2"><span style="color:#ddd;background:#1e1e1e;">

     </span><span style="color:#4fc04f;background:#1e1e1e;font-weight:bold;">a</span><span style="color:#ddd;background:#1e1e1e;font-weight:bold;opacity:.55;">aaaa</span><span style="color:#1e1e1e;background:#ddd;font-weight:bold;opacity:.55;"> </span><span style="color:#ddd;background:#1e1e1e;">
     </span><span style="color:#ddd;background:#1e1e1e;font-weight:bold;opacity:.55;">bbb </span><span style="color:#ddd;background:#1e1e1e;">
     </span><span style="color:#ddd;background:#1e1e1e;font-weight:bold;opacity:.55;">ccccc </span><span style="color:#ddd;background:#1e1e1e;">
     </span><span style="color:#ddd;background:#1e1e1e;font-weight:bold;opacity:.55;">dd</span></pre></div>
</div>
<h3>80 cols, long prompt: third line aligned</h3><div style="display:flex;gap:24px">
<div><b>before</b><pre style="background:#1e1e1e;padding:8px;border:1px solid #444;font-size:15px;line-height:1.2"><span style="color:#ddd;background:#1e1e1e;">









     </span><span style="color:#ddd;background:#1e1e1e;font-weight:bold;opacity:.55;">it was the best of times it was the worst of times it was the age of </span><span style="color:#ddd;background:#1e1e1e;">
     </span><span style="color:#ddd;background:#1e1e1e;font-weight:bold;opacity:.55;">wisdom it was the age of foolishness it was the epoch of belief it was</span><span style="color:#ddd;background:#1e1e1e;">
     </span><span style="color:#ddd;background:#1e1e1e;font-weight:bold;opacity:.55;"> the epoch of incredulity</span></pre></div>
<div><b>after</b><pre style="background:#1e1e1e;padding:8px;border:1px solid #444;font-size:15px;line-height:1.2"><span style="color:#ddd;background:#1e1e1e;">









     </span><span style="color:#ddd;background:#1e1e1e;font-weight:bold;opacity:.55;">it was the best of times it was the worst of times it was the age of </span><span style="color:#ddd;background:#1e1e1e;">
     </span><span style="color:#ddd;background:#1e1e1e;font-weight:bold;opacity:.55;">wisdom it was the age of foolishness it was the epoch of belief it was </span><span style="color:#ddd;background:#1e1e1e;">
     </span><span style="color:#ddd;background:#1e1e1e;font-weight:bold;opacity:.55;">the epoch of incredulity</span></pre></div>
</div></body></html>
Evidence: Exactly-full line, 15 cols (tmux capture)
before:
aaaaa
bbb
ccccc
dd
after:
aaaaa
bbb
ccccc
dd
Evidence: Mistyped hanging space (target build, ANSI)


     ^[[1m^[[38;5;2maaaaa^[[38;5;1m·^[[0m
     ^[[1;2mbbb ^[[0m
     ^[[1;2mccccc ^[[0m
     ^[[1;2mdd^[[0m
Evidence: Pace caret on hanging space (target build, ANSI)


     ^[[1m^[[38;5;2ma^[[2m^[[39maaaa^[[7m ^[[0m
     ^[[1;2mbbb ^[[0m
     ^[[1;2mccccc ^[[0m
     ^[[1;2mdd^[[0m

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • ⚠️ src/ui.rs:73 - The hanging space that wrap_chars now places at col == width is clipped by the renderer, so mistakes on it no longer show. render_running draws the prompt with Paragraph::new(text).render(text_area, buf). It sets no Wrap, and text_area is exactly max_chars_per_line wide (prompt_area.width - x_offset), so ratatui truncates every line to that width. The new 6-char line range (e.g. aaaaa at width 5, src/layout.rs:35) loses its last cell. Concrete case: prompt "aaaaa bbb ccccc dd" at 15 cols, user types "aaaaax". prompt_line emits a red '·' (INCORRECT) for idx 5, but that cell is cut off. The user sees nothing wrong, while the cursor jumps to the margin at x=10 and then to the next line. The pace caret (REVERSED) also disappears whenever it sits on a hanging space. Before this change, that space started the next line and the red '·' was visible. Fix: let the text area extend one column into the right margin when the prompt has more than one line (HORIZONTAL_MARGIN = 5 leaves room), e.g. width: prompt_area.width - x_offset + 1 clamped to area. Then add a snapshot of a mistyped hanging space. The same invariant (a hanging cell must be drawable) applies to the cursor position at src/ui.rs:86-90, which already lands in the margin.

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

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 8 of 8 scenarios driven live against the product
Scenario Result Live Evidence
Prompt whose first line fills the width exactly: the next lines start at the left edge, not one column in ✅ pass live before-full-line.txt vs after-full-line.txt; before-after.png section 1
80-col terminal, long prompt: third line 'the epoch of incredulity' is aligned with the lines above ✅ pass live before-80col.txt vs after-80col.txt; before-after.png section 4
Typing the full word then the correct space moves the cursor to the start of the next line ✅ pass live after-typed-word (cursor 10,2) and after-typed-space (cursor 5,3) captures
Adversarial: mistyping the hanging space shows a red '·' at the end of the full line (not clipped) ✅ pass live after-mistyped-space.ansi shows ESC[38;5;1m· after the green aaaaa; before-after.png section 2
Pace caret sitting on the hanging space stays visible (reverse-video cell) ✅ pass live after-pace-on-hanging-space.ansi shows ESC[7m on the space at col 10; before-after.png section 3
Adversarial: a second consecutive space after a full line wraps to the next line instead of hanging ✅ pass live after-double-space.txt shows ' bbb' on line 2, which matches the a_second_space_after_a_full_line_wraps intent
Adversarial: very narrow terminals (width-1 prompt area, 8 cols) render without crashing, and the hanging cell stays inside the screen ✅ pass live after-11col.txt shows 'a·' on line 3 with the red dot drawn; after-tiny-term matches base (blank)
A full test still finishes and shows the results screen ✅ pass live after-complete.txt shows the results screen with 100% acc
  • Built base 1280cef and target c8a41c8 with nix shell nixpkgs#cargo nixpkgs#rustc nixpkgs#gcc -c cargo build
  • Ran thokr -p &#39;aaaaa bbb ccccc dd&#39; in a detached tmux session at 15x8 with a throwaway HOME, on both builds, and captured the pane with and without ANSI codes
  • Typed aaaaa, then aaaaa&lt;space&gt;, then aaaaax (mistyped space) on the target build, and typed aaaaax on the base build
  • Ran --pace 6, typed a and waited 11s so the pace caret sat on the hanging space (both builds)
  • 80x24 run with the long 'it was the best of times...' prompt on both builds
  • Edge cases: prompt aaaaa bbb ccccc (double space) at 15 cols, aaa bb at 11 cols (width 1) and 8 cols, and a full test run to the results screen at 60x20
  • cargo test layout:: and cargo test running_ (targeted layout and snapshot tests)
  • Rendered the before/after ANSI captures to HTML and took a screenshot with chrome-devtools-axi screenshot
✅ **Document** - passed

✅ No issues found.

⏭️ **Lint** - skipped
  • ⚠️ src/ui.rs - cargo is not on PATH in this environment, so cargo fmt --check and cargo clippy --all-targets did not run. Formatting and lint for src/layout.rs and src/ui.rs are unverified. The doc comment and code comment edits are comment-only and keep lines under 100 columns.
✅ **Push** - passed

✅ No issues found.

jrnxf added 4 commits October 25, 2026 19:01
…ui.rs left one Position::new(...) call split across four lines, and rustfmt wants it on one line. src/ui.rs:90 is now Some(Position::new(x + col, prompt_area.y + line_no as u16)). Nothing else changed. cargo is not on PATH here, so I ran rustfmt through nix with edition 2024 (from Cargo.toml): `nix run nixpkgs#rustfmt -- --edition 2024 --check src/*.rs` passed with no diffs
@jrnxf
jrnxf merged commit 7013a49 into main Oct 25, 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