Skip to content

fix: clear wrapped lines when redrawing a prompt - #190

Merged
jdx merged 2 commits into
mainfrom
claude/wrap-aware-redraw
Jul 25, 2026
Merged

fix: clear wrapped lines when redrawing a prompt#190
jdx merged 2 commits into
mainfrom
claude/wrap-aware-redraw

Conversation

@jdx

@jdx jdx commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Any prompt whose rendered frame is wider than the terminal duplicates itself on every keypress.

Widgets clear the previous frame with Term::clear_last_lines(self.height), which works in physical rows, but height was computed as output.lines().count() - 1logical lines. A line wider than the terminal wraps into rows the clear never erases, so the next frame draws below the leftovers and the prompt stacks.

Found via jdx/aube#1107, where a Select of package.json scripts (labels like build: tsc -p tsconfig.build.json && rollup -c …) scrambled on every arrow key. Driving that picker through a pty at 80 columns, two ↓ presses, replayed through a terminal emulator:

 0 |Select a script to run
 1 |package.json scripts
 2 |❯ build: tsc -p tsconfig.build.json && rollup -c rollup.config.mjs --environment
 3 |Select a script to run          ← stale
 4 |package.json scripts
 5 |  build: tsc -p tsconfig.build.json && rollup -c rollup.config.mjs --environment
 6 |Select a script to run          ← stale
 7 |package.json scripts
 8 |  build: …

The same frames at 200 columns are perfectly clean, which is what pins it on wrapping rather than anything terminal-specific.

The fix

A shared height::rendered_height(output, width) counts the physical rows a frame occupies: for each line, measure_text_width(line).div_ceil(width), so ANSI codes don't inflate the count and a line exactly width wide still counts as one row (terminals defer the wrap).

Applied to every widget that redraws via clear_last_lines: Confirm, Dialog, Input, List, Select, Spinner.

One subtlety worth flagging: the helper splits on '\n' and drops the final segment rather than using lines(). What we want is the rows above the cursor, and the cursor rests on whatever follows the last newline — the trailing color reset, or nothing. lines() swallows a trailing empty segment, so it would lose a row for any widget whose output doesn't end in that reset. split('\n') handles both endings identically.

Not in this PR

MultiSelect is untouched. It doesn't use clear_last_linesreposition_and_write rewrites line by line and clears each with clear_line, which erases one physical row. Making it wrap-aware means more than swapping the count (a shrinking wrapped line leaves a tail row that clear_line won't reach), so it deserves its own change rather than a partial one bolted onto this.

Tests

  • Unit tests for rendered_height: the trailing-fragment handling, exact-width lines, multi-row wrapping, ANSI-aware measurement, and zero width.
  • A Select regression test that drives three real redraw cycles through a captured Term, replays the bytes through vt100, and asserts the screen holds one copy of the prompt. It fails with logical-line counting (3 copies) and passes with physical rows.
    • The test applies ONLCR (\n\r\n) before feeding the emulator, since a real TTY does that and the row arithmetic is meaningless without it.
  • The Term capture harness moved out of multiselect.rs into test.rs so both suites share it.
  • Select::run's render-and-record step is now a small draw() method, so the regression test exercises the same code path instead of a copy of it.

cargo test --lib is green (37 passing) and cargo clippy --all-targets -- -D warnings is clean.

Verified end to end by building aube against this branch: the picker is stable at 80 columns and at 34, where the wrapping help footer alone used to retrigger the stacking.

This PR was generated by Claude.


Note

Medium Risk
Touches core TTY redraw/clear paths across multiple widgets; behavior is well-tested but incorrect row math would cause visible glitches or cursor misplacement.

Overview
Fixes prompts duplicating on every keypress when rendered text is wider than the terminal. Redraw logic used logical line count for clear_last_lines, but terminals clear physical rows, so wrapped lines were left on screen.

Adds height::rendered_height (and rendered_rows for frames without a trailing newline) using terminal width and measure_text_width so ANSI and wrapping are counted correctly. Confirm, Dialog, Input, List, Select, and Spinner now record frame height this way; Input also computes input_line_offset in rows for caret placement when titles wrap.

Spinner clears by walking up from the cursor row instead of clear_last_lines, since the cursor sits on the last row of the frame. Select centralizes redraw in draw() for tests.

Adds unit tests for height math, Input offset behavior, a Select vt100 regression for stacked frames, and moves the unix Term capture harness to test.rs for reuse. MultiSelect is unchanged.

Reviewed by Cursor Bugbot for commit d99347b. Bugbot is set up for automated code reviews on this repo. Configure here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@jdx, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 41 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 47c89071-d9d4-4f67-bd05-d4d771de5911

📥 Commits

Reviewing files that changed from the base of the PR and between 0ebf367 and d99347b.

📒 Files selected for processing (10)
  • src/confirm.rs
  • src/dialog.rs
  • src/height.rs
  • src/input.rs
  • src/lib.rs
  • src/list.rs
  • src/multiselect.rs
  • src/select.rs
  • src/spinner.rs
  • src/test.rs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit abd4fe1. Configure here.

Comment thread src/input.rs
@greptile-apps

greptile-apps Bot commented Jul 25, 2026

Copy link
Copy Markdown

Greptile Summary

Adds physical terminal-row accounting for wrapped prompt frames, updates affected widgets to use it, corrects Input cursor-offset and Spinner clearing behavior from previous feedback, and adds shared redraw regression tests.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failures remain in the fixes associated with the previous review threads.

Important Files Changed

Filename Overview
src/height.rs Introduces ANSI-aware helpers for counting physical terminal rows above and including the cursor.
src/input.rs Uses physical-row accounting for frame height and the header offset used during cursor positioning.
src/spinner.rs Counts the cursor-bearing spinner row and clears wrapped frames one physical row at a time.
src/select.rs Centralizes frame drawing and height recording and adds a terminal-emulator regression test for wrapped options.
src/test.rs Provides a shared Unix-only terminal-output capture harness for redraw tests.

Reviews (2): Last reviewed commit: "fix: keep input and spinner row math con..." | Re-trigger Greptile

Comment thread src/input.rs
Comment thread src/spinner.rs Outdated
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jdx
jdx merged commit d01493f into main Jul 25, 2026
8 checks passed
@jdx
jdx deleted the claude/wrap-aware-redraw branch July 25, 2026 02:04
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