Skip to content

test(painter): assert on a vt100 screen instead of emitted bytes - #1249

Merged
kronberger-droid merged 2 commits into
nushell:mainfrom
kronberger-droid:painter-screen-harness
Oct 18, 2026
Merged

kronberger-droid merged 2 commits into
nushell:mainfrom
kronberger-droid:painter-screen-harness

Conversation

@kronberger-droid

@kronberger-droid kronberger-droid commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Painter tests so far assert on the escape bytes the painter emits, or replay them through the small hand-written emulator in painter.rs (replay).
Bytes say little about what reaches the screen, and they break on any reordering that is correct.
Worse, the expected rows usually come from the same layout math under test, so the painter ends up being its own oracle.

This adds vt100 as a dev-dependency and a thin TestScreen wrapper over it (src/painting/test_screen.rs).
A test paints into the capture writer, feeds the bytes through TestScreen, and asserts on rows, the cursor, cursor visibility and how many rows scrolled into history.
Tests only talk to TestScreen, so the emulator can be swapped by changing that one file.

src/painting/painter/screen_tests.rs has the first tests on it.
They build PromptLines through PromptLines::new the way the engine does, so a new field on PromptLines does not reach them.

No public API change, everything is #[cfg(test)].
The only non-test change is the vt100 dev-dependency in Cargo.toml and Cargo.lock (MSRV 1.70, below ours).

Before

Painter tests check byte strings such as \x1b[?25h\x1b[J\r\n, or a screen rebuilt by replay, which only models the sequences the painter happened to use when it was written.

After

Painter tests can state what the user sees:

session.paint(&FixedPrompt::new("> ", ""), "hello", "");

assert_eq!(session.screen.row(2), "> hello");
assert_eq!(session.screen.cursor(), (7, 2));
assert_eq!(session.screen.scrolled(), 0);

Additional notes

Neither is filed yet, i will open issues for them and fix them separately.

Where vt100 is just one opinion: it keeps a deferred wrap across DECSC/DECRC and wraps a wide grapheme early with one column left.
The module doc says so, tests should assert that the painter stays out of such cases rather than on how vt100 resolves them.

Out of scope:

  • Porting the existing replay based tests and removing replay, follow up.
  • Fixing the two recorded bugs.

The wide grapheme test asserted where `vt100` wraps a grapheme that does
not fit. The painter prints it raw and restores the position the
terminal reached, so both the row and the cursor were the emulator's
choice; the painter's own model of that wrap is covered by
`deferred_wrap_row_on_wide_graphemes`.

The two ignored repros now go green only on a full fix: the large buffer
one tolerates a scroll on the first paint, and the trailing newline one
also checks the cursor column and click mapping against the screen, so
fixing `prompt_height` alone keeps it red.
@kronberger-droid
kronberger-droid marked this pull request as ready for review October 18, 2026 12:20
@kronberger-droid
kronberger-droid merged commit 1b0da36 into nushell:main Oct 18, 2026
9 checks passed
@kronberger-droid
kronberger-droid deleted the painter-screen-harness branch October 18, 2026 12:23
fdncred pushed a commit to nushell/nushell that referenced this pull request Oct 18, 2026
## Description

Bumps reedline to `39db5e3`, six PRs past `d704744`:

- nushell/reedline#1132 `feat(engine): add opt-in paste hooks for
interception and burst detection`
- nushell/reedline#1243 `fix(painter): print external messages from the
prompt's start row`
- nushell/reedline#1249 `test(painter): assert on a vt100 screen instead
of emitted bytes`
- nushell/reedline#1247 `chore: add .claude to gitignore`
- nushell/reedline#1248 `feat(history)!: drop chrono from the public API
with a millisecond HistoryTimestamp`
- nushell/reedline#1254 `Fix large completion menus and synchronized
repainting`

#1248 needs the only change on the nushell side: `repl.rs` now stamps
history with `HistoryTimestamp::now()`, and `history import` converts
its `date` field to millis, so its input type stays the same.

## User-facing changes (Release notes)

- Fixed external messages leaving prompt rows behind or erasing output
above the prompt.
- Fixed completion menus with more entries than fit in terminal
coordinates.
- Repaints are bracketed with synchronized terminal updates, which
reduces flicker on supporting terminals.
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