Skip to content

fix(renderer): paint reverse-video cell background with default colors#5

Merged
bilby91 merged 1 commit into
mainfrom
fix/reverse-video-default-color-background
Jun 3, 2026
Merged

fix(renderer): paint reverse-video cell background with default colors#5
bilby91 merged 1 commit into
mainfrom
fix/reverse-video-default-color-background

Conversation

@bilby91

@bilby91 bilby91 commented Jun 3, 2026

Copy link
Copy Markdown
Member

Problem

Reverse-video cells (SGR 7) with a default foreground rendered with no background fill — the cell showed the dark theme.background instead of a solid inverted block.

Most visible as a disappearing/empty cursor: TUIs that draw their own cursor by hiding the hardware cursor (?25l) and inverting a cell — e.g. Claude Code — rendered an empty/dark block in ghostty-web where native ghostty shows a solid one.

Root cause

In renderCellBackground:

const useThemeBg = cell.flags & INVERSE ? cell.fgIsDefault : cell.bgIsDefault;
if (!useThemeBg) { /* paint cell background */ }

For an inverse cell the effective background is the cell's foreground. When that foreground is default, the effective background should resolve to theme.foreground — but the code treated fgIsDefault as "use theme background" and skipped the fill entirely, letting the dark line-level theme.background show through. renderCellText had the mirror-image bug: it fell back to theme.foreground instead of the swapped theme.background.

Fix

  • Inverse cells now always paint their background — theme.foreground when the source foreground is default, otherwise the foreground RGB.
  • Inverse text falls back to theme.background (the swapped theme color), so the glyph stays legible against the inverted block.

Test

  • Adds an e2e regression test in 01-rendering: a default-color reverse-video run must paint a solid light background (sampled via canvas getImageData). The suite had no reverse-video-with-default-colors coverage.
  • Verified locally: renderer unit tests, typecheck, lint (biome) and fmt (prettier) all clean. The full WASM-backed unit suite needs CI — it fetches ghostty-vt.wasm over HTTP, which isn't reachable in my sandbox.

How it was found

Isolated from a downstream app where Claude Code's cursor wasn't rendering a full block. Bisected through the server-side PTY host (passes bytes through unchanged) and Claude's output (emits no DECSCUSR — it relies on the default cursor, then hides it and draws its own reverse-video cell), which localized the defect to ghostty-web's inverse-cell background fill. Confirmed the fix live by patching the built dist into the app.

🤖 Generated with Claude Code

A reverse-video cell (SGR 7) whose foreground is the default color rendered
with no background fill. `renderCellBackground` treated `fgIsDefault` as
"use theme background" and skipped the paint, so the cell showed the dark
theme background instead of the inverted (theme.foreground) block.
`renderCellText` had the mirror-image bug, falling back to theme.foreground
instead of the swapped theme.background.

This is exactly how reverse-video TUIs — and program-drawn block cursors
like Claude Code's, which hides the hardware cursor (DECTCEM) and draws its
own inverted cell — render their cursor. The result was an empty/dark cursor
block in ghostty-web where native ghostty shows a solid block.

Inverse cells now always paint their background (theme.foreground when the
source foreground is default); inverse text falls back to theme.background.

Adds an e2e regression test: a default-color reverse-video run must paint a
solid light background.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@bilby91 bilby91 merged commit 0c4f4ce 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