Skip to content

fix(renderer): resolve grapheme clusters by source row when scrolled#6

Merged
bilby91 merged 2 commits into
mainfrom
fix/scrollback-grapheme-row-mismatch
Jun 3, 2026
Merged

fix(renderer): resolve grapheme clusters by source row when scrolled#6
bilby91 merged 2 commits into
mainfrom
fix/scrollback-grapheme-row-mismatch

Conversation

@bilby91

@bilby91 bilby91 commented Jun 3, 2026

Copy link
Copy Markdown
Member

Problem

Multi-codepoint grapheme cells (grapheme_len > 0 — emoji, variation-selector symbols, combining sequences) render the wrong glyph while the viewport is scrolled up, surfacing as stray ? / mismatched glyphs over scrolled content that appear and disappear depending on scroll position. Plain single-codepoint text and box-drawing are unaffected.

Root cause

In lib/renderer.ts, the render loop fetches each line with a scroll-adjusted row index:

  • scrollback portion → getScrollbackLine(scrollbackLength - floor(viewportY) + y)
  • visible portion → buffer.getLine(y - floor(viewportY))

But renderCellText looked the grapheme cluster up via getGraphemeString(y, x) using the raw on-screen row y. getGraphemeString resolves against the live active grid (terminal.ts wires it straight to wasmTerm.getGraphemeString), so once scrolled the two indices disagree by the scroll offset and a cell is painted with a grapheme from a different line. Single-codepoint cells render from cell.codepoint, which is why only special glyphs were affected.

Fix

Thread a graphemeRow through renderLinerenderCellText that tracks the line source:

  • not scrolled / visible-screen portion → the scroll-adjusted screen row → correct grapheme
  • scrollback portion-1; scrollback graphemes are not addressable via the active-grid getGraphemeString, so fall back to the cell's base codepoint (eliminates the wrong-glyph artifact)

Known limitation

For graphemes that have scrolled fully into scrollback, the base codepoint is rendered, so a multi-codepoint cluster loses its combining marks (e.g. ée). This is strictly better than the current wrong-glyph/? behaviour. Full fidelity would require a WASM API to fetch graphemes by absolute scrollback index (e.g. getScrollbackGrapheme) and have getScrollbackLine cells carry their cluster — left as follow-up.

Tests

  • Adds lib/scrollback-grapheme.test.ts, which drives the real CanvasRenderer (mocking only the IRenderable/IScrollbackProvider interfaces), spies on fillText, and asserts a grapheme survives a scroll. It fails on main (draws the wrong-row sentinel) and passes with this change.
  • Full unit suite: bun test lib/ --timeout 30000435 pass / 0 fail.
  • tsc --noEmit and Prettier clean.

🤖 Generated with Claude Code

bilby91 and others added 2 commits June 3, 2026 14:28
Multi-codepoint grapheme cells (grapheme_len > 0) rendered the wrong glyph
while the viewport was scrolled up. The render loop fetches each line with a
scroll-adjusted row index, but renderCellText looked the grapheme cluster up
via getGraphemeString(y, x) using the raw on-screen row. getGraphemeString
resolves against the live active grid (terminal.ts wires it straight to
wasmTerm.getGraphemeString), so once scrolled the two indices disagree by the
scroll offset and a cell gets a glyph from a different line — surfacing as
stray "?" / wrong glyphs over scrolled content that appear and disappear with
scroll position. Single-codepoint cells were unaffected (they render from
cell.codepoint), so plain text and box-drawing stayed correct.

Thread a graphemeRow through renderLine -> renderCellText that tracks the line
source: the scroll-adjusted screen row for the visible portion, or -1 for
scrollback lines (whose graphemes are not addressable via the active-grid
getGraphemeString), in which case we fall back to the cell's base codepoint.

Adds lib/scrollback-grapheme.test.ts reproducing the artifact at the renderer
level.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Satisfies biome's lint/style/useImportType (CI `bun run lint`).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@bilby91 bilby91 merged commit a963a4a into main Jun 3, 2026
5 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