Skip to content

fix(grid): keep the wide-glyph wrap filler out of reflowed text - #92

Merged
simota merged 3 commits into
mainfrom
fix/grid-wide-wrap-filler
Sep 30, 2026
Merged

simota merged 3 commits into
mainfrom
fix/grid-wide-wrap-filler

Conversation

@simota

@simota simota commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

When a wide glyph does not fit in the last column, it moves to the next row and leaves one blank cell behind. That cell looked exactly like a real space, so a column-count resize carried it into the reflowed text and left stray blanks mid-word at the old wrap points ("あた り", "1,391,8 31件"). The cell was also left untouched, so stale content could survive under it.

Three commits, each green on its own:

  1. Live screen. Blank that cell whenever a wide glyph wraps. In the last column, also flag it WIDE_PAD. Row::ends_with_wide_pad defines where the flag counts: only in the last column of a soft-wrapped row. A flagged cell anywhere else is an ordinary blank, so a stale flag cannot hide visible text. Reflow drops the filler, and copy and search skip it. A wrap at a narrower right margin only blanks the cell. DCH and rectangle scrolls clear the flag on the cells they move.
  2. Restored history. snapshot::rewrap drops the filler the same way, and flags the column it vacates when it moves a wide glyph down.
  3. Client-mode seed. Plain VT output cannot express the filler, so the seed re-creates it by printing U+3000 in the last column with the filler's background. Grapheme clustering is off for that print. The replica then lays down the filler and wraps by itself. Filler cells are painted with ECH, which keeps their background and never joins a preceding ZWJ cluster. A new seed-only CSI > $ w re-flags a filler that a cursor-latch reprint turned into a plain space.

Reviewer notes:

  • CSI > $ w adds Handler::seed_mark_wide_pad, with a no-op default. It follows the existing seed-only CSI > $ s / CSI > $ t, and the > marker keeps it clear of DECRQPSR (CSI Ps $ w).
  • The expected value in rewrap_never_separates_a_wide_glyph_from_its_spacer changed. The vacated column is now a flagged filler instead of a default cell.
  • A filler on the last row keeps its background and text in the seed but not its flag. The seed never rebuilds a soft wrap on the last row, because there is no next row on screen.

Validation:

  • Regression tests cover reflow in both directions, a real trailing space, copy and search, DCH, rectangle scroll, a narrow-margin wrap, and snapshot restore. For the seed, they cover widened clusters, an overwritten next row, background, ZWJ, the last row, and latched saved, live and origin-relative cursors. Each test was confirmed to fail before the change that fixes it.
  • A 300-trial random multi-step resize fuzz on the reported text went from 228 lines with misplaced blanks to 0. The fuzz was scratch-only and is not committed.
  • cargo test --workspace --exclude noa-pty --exclude noa-ipc and cargo fmt --all -- --check pass on each commit. cargo clippy -p noa-grid -p noa-vt -p noa-core --all-targets is clean on the final commit. noa-pty and noa-ipc need unsandboxed device and socket access and were not run here.

Not addressed:

  • In the same report, some characters were missing outright (for example the 0 in "92,70件"). That could not be reproduced through grid, split-feed, or render paths, and this change is not known to fix it.
  • Visual parity in the GUI was not checked by hand.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-30T03:06:00.406484Z 93993fe PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 93993fef5a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

let blank = self.blank();
let x = self.cursor.x as usize;
if let Some(row) = self.grid.get_mut(self.cursor.y as usize) {
Self::mark_wide_pad(row, x, &blank);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve the wide-pad marker in synthetic seeds

When a client-mode source contains a wide glyph wrapped from the last column (for example, abc界 at width 4), this call adds WIDE_PAD, but terminal/seed.rs can only replay the cell as an ordinary space. The replica therefore differs immediately, and widening both terminals later makes the replica retain that space and shift the following glyph while the source drops it. The seed path needs to recreate or explicitly encode this layout marker.

Useful? React with 👍 / 👎.

Comment thread crates/noa-core/src/attrs.rs Outdated
Comment on lines +23 to +26
/// Filler at the end of a soft-wrapped row where a wide glyph did not
/// fit and moved to the next row. Not content: reflow, copy and search
/// skip it.
const WIDE_PAD = 1 << 15;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle wide pads in persisted-snapshot rewrapping

With scrollback persistence enabled, introducing this new non-default cell state also requires updating snapshot::rewrap in crates/noa-grid/src/snapshot.rs: it currently copies an existing WIDE_PAD into the logical cell stream, and emit_logical_line does not mark fillers when it moves a wide pair to the next row. Restoring at a different width can consequently place later content one column too far right, and a subsequent resize can treat newly created unmarked fillers as real spaces.

Useful? React with 👍 / 👎.

Comment thread crates/noa-grid/src/screen/reflow.rs Outdated
Comment on lines +363 to +366
if row.wrapped
&& len == row.cells.len()
&& len > 0
&& row.cells[len - 1].attrs.contains(CellAttrs::WIDE_PAD)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Drop wide pads created at horizontal margins

When DECSLRM's right margin is before the physical row end, a wide glyph can create WIDE_PAD at that margin rather than at row.cells[len - 1]. After margins are disabled and the terminal is resized, this condition fails, so the filler remains in the reflow stream and inserts a visible blank column even though copy and search skip the same cell. Reflow should remove the marked cell wherever the soft-wrap boundary occurred, not only at the physical row end.

Useful? React with 👍 / 👎.

When a wide glyph does not fit in the last column, it moves to the next row
and leaves one blank cell behind. That cell looked exactly like a real space,
so a column-count resize carried it into the reflowed text and left stray
blanks mid-word at the old wrap points ("あた り", "1,391,8 31件"). The cell
was also left untouched, so stale content could survive under it.

Blank that cell when printing or reflow wraps a wide glyph. In the last
column, also flag it `WIDE_PAD`. `Row::ends_with_wide_pad` defines where the
flag counts: only in the last column of a soft-wrapped row. A flagged cell
anywhere else is an ordinary blank, so a stale flag can never hide visible
text. Reflow drops the filler from the end of a soft-wrapped row, and copy and
search skip it.

A wrap at a narrower right margin just blanks the cell. Flagging it there
would leave fillers mid-row once the margins go away, and a row could carry
more than one of them. DCH and rectangle scrolls clear the flag on the cells
they move.
Restoring saved scrollback at a new width rewraps it with `snapshot::rewrap`,
which kept the filler as a regular cell. As a result, "abc界Z" saved at 4
columns placed 界 at column 4 instead of 3 when restored at 8 columns.

Drop the filler from the end of a soft-wrapped row, as live reflow does. When
rewrap moves a wide glyph down, flag the column it vacates, so a later live
reflow treats that column as filler too.
The seed rebuilds a replica's screen with plain VT output, which has no way
to express the filler. Before this change, the replica received it as an
ordinary space and copied "abc 界Z" where the server copied "abc界Z". Its own
reflow then disagreed with the server as well.

- For a row that ends in a filler, print U+3000 in the last column with the
  filler's own background and grapheme clustering off. The replica then lays
  down the filler and soft-wraps by itself, however the next row now looks,
  and the next row is repainted from the source.
- Paint filler cells with ECH rather than a printed space. This keeps a
  filler's background on the last row, where no wrap can be rebuilt, and never
  joins a preceding cluster ending in a ZWJ.
- Recreating a cursor's deferred-wrap latch reprints the cell under it, which
  turns a filler there into a plain space. That cursor must stay latched, so
  the filler cannot be rebuilt afterwards. The new seed-only `CSI > $ w`
  flags the cell under the cursor without moving it or clearing the latch.
  Both the absolute and origin-relative cursor writers emit it after such a
  reprint.
@simota
simota force-pushed the fix/grid-wide-wrap-filler branch from 93993fe to ef2b848 Compare September 30, 2026 09:27
@simota
simota merged commit 5c136cd into main Sep 30, 2026
1 check passed
@simota
simota deleted the fix/grid-wide-wrap-filler branch September 30, 2026 09:54
@simota simota mentioned this pull request Sep 30, 2026
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