fix(renderer): resolve grapheme clusters by source row when scrolled#6
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:getScrollbackLine(scrollbackLength - floor(viewportY) + y)buffer.getLine(y - floor(viewportY))But
renderCellTextlooked the grapheme cluster up viagetGraphemeString(y, x)using the raw on-screen rowy.getGraphemeStringresolves against the live active grid (terminal.tswires it straight towasmTerm.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 fromcell.codepoint, which is why only special glyphs were affected.Fix
Thread a
graphemeRowthroughrenderLine→renderCellTextthat tracks the line source:-1; scrollback graphemes are not addressable via the active-gridgetGraphemeString, 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 havegetScrollbackLinecells carry their cluster — left as follow-up.Tests
lib/scrollback-grapheme.test.ts, which drives the realCanvasRenderer(mocking only theIRenderable/IScrollbackProviderinterfaces), spies onfillText, and asserts a grapheme survives a scroll. It fails onmain(draws the wrong-row sentinel) and passes with this change.bun test lib/ --timeout 30000→ 435 pass / 0 fail.tsc --noEmitand Prettier clean.🤖 Generated with Claude Code