fix(terminal): font visibly swaps when selecting text#131
Merged
Conversation
MHohlios
marked this pull request as ready for review
July 22, 2026 16:55
…nts resolve WKWebView cannot resolve user-installed font families on a disconnected canvas, and the bad resolution sticks: assigning an identical ctx.font string is a WebKit no-op, so reattaching never heals it. xterm's WebGL TextureAtlas rasterizes every glyph on one long-lived scratch canvas that draws container-less during the ASCII warm-up and is orphaned when the pane hosting it unmounts, so glyphs could cache with the wrong family and visibly swap under selection (new bg = new atlas key = fresh connected draw). lib/atlasCanvasGuard.ts parks the canvas under document.body at atlas creation/swap and pane dispose, and pokes ctx.font with a sentinel so the next draw re-resolves while connected. Reach-ins optional-chained; xtermInternals.test.ts pins the addon-webgl private names.
MHohlios
force-pushed
the
feature/font-issues
branch
from
July 22, 2026 17:05
e5062b0 to
4718c11
Compare
simion
approved these changes
Jul 22, 2026
simion
left a comment
Owner
There was a problem hiding this comment.
Correct fix. Root cause diagnosis checks out against the installed addon-webgl bundle — the microtask timing is sound (event fires before _charAtlas assignment, warm-up runs on idle after the microtask), the two-part fix targets both halves of the WebKit sticky-resolution bug, and all reach-ins are optional-chained for soft failure. Tests cover the branch matrix and the canary pattern is the right upgrade-safety net. Nice work.
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.
Symptom
Yet another font issue.
With the terminal font set to a user-installed family (e.g. a Nerd Font under
/Library/Fonts), highlighting text in an agent TUI renders the selection in a different family: another sans mono, or Courier (serifed) in the worst case. Deselecting reverts it. Same atlas mechanics as the residual cold-spawn gap in #70.Using obnoxiously loud font here for demonstration don't judge me.
Root cause
Two WKWebView behaviors, verified with a standalone WKWebView probe that pixel-classifies which family actually renders:
monospace(Courier) when no webfont matches.ctx.font = <identical string>, so reattaching the canvas never heals it. Only a different string forces a re-parse.xterm's WebGL
TextureAtlasrasterizes every glyph on one long-lived hidden scratch canvas shared by same-config terminals. Real cell draws attach it to the drawing terminal's element before touchingctx.font, but two windows rasterize detached:Glyphs cache per (char, fg, bg, ext) with no font in the key, so default-colored text keeps wrong-family glyphs indefinitely while a selection (new bg = new key = fresh, connected draw) rasterizes the same characters with the right family. The font swaps under the highlight.
Fix
lib/atlasCanvasGuard.ts: park the scratch canvas underdocument.bodywherever it might be orphaned, and pokectx.fontwith a sentinel so the next draw re-resolves while connected. Wired inloadTerminalRendererat three points:loadAddon(the initial atlas firesonChangeTextureAtlasduring renderer construction, before the addon can forward it);onChangeTextureAtlas+ microtask (font/theme/dpr atlas swaps; the queued warm-up runs later, on idle);dispose()with the dying pane's element (unmount orphaning). Covers TerminalPane and AuxTerminal.Why this is low risk (the #99 lesson)
xtermInternals.test.tsnow pins the addon-webgl private names so an upgrade that renames them fails in CI instead.Testing
npm run buildclean.make betainstall on a machine that hit the bug in daily use. Ran for a day of use and seem to be fine.Also corrects
docs/gotchas.md: the detached-canvas bullet claimed xterm was unaffected; it is affected, and a new bullet documents this failure mode.