Configurable terminal font weight, so bold text stays distinguishable #403
irisitymichaelgrundberg
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
xterm exposes both
fontWeightandfontWeightBold, and Codeman passesneither, so ordinary text renders at 400 and bold at 700. How visible that step
looks depends entirely on the font family, and for a family that ships only a
regular and a bold face there is no way to widen it.
Consolas is the case that prompted this. Windows installs
consola.ttfandconsolab.ttfwith nothing between them and nothing lighter. I measured glyphink coverage for both faces in Firefox on Windows at 150% display scaling, on a
dark skin, and it rises from 14.25% at normal to 16.57% at bold. At 14px my eye
does not reliably catch that: a bold heading reads as body text.
Two things compound the problem for the agent CLIs.
ESC[1mand changes nocolour. I confirmed that by capturing a live tmux pane with escapes intact.
palette index 0 through 7, so the substitution never fires for text sitting
on the theme's default foreground.
Bold text on the default foreground therefore carries exactly one cue, the
weight step, and a two-face family keeps that cue small. For contrast, my
Windows Terminal profile sets
intenseTextStyle: "all", so it answers the sameESC[1mwith a heavier face and a brighter colour, and bold is unmistakablethere with the same font at a similar size.
Why the settings we have do not cover it
The "Terminal font" field changes the family, which is necessary but not
sufficient. A lighter face is only reachable if the normal weight can be
lowered as well, because 400 stays 400 whatever the family. Cascadia is a
variable font and already sits in Codeman's default fallback stack. On the same
machine, six requested weights from 200 to 700 produced five distinct
renderings for Cascadia against two for Consolas, so the range is genuinely
there and nothing exposes it.
The shape I would propose
Two per-device settings beside the existing "Terminal font" field in the Font
group:
normal.bold.CodemanTerminalFont.resolveWeightsresolves both slots from the settingsblob, each falling back to xterm's own default for that slot, so an untouched
install renders exactly as it does today and no caller can hand one slot the
other's fallback. Both values thread into the main terminal and the
teammate-pane terminals, apply on save without a reload, and are stripped from
the settings upload for the same two reasons as
terminalFontFamily: the facehas to exist on the device, and
SettingsUpdateSchemais strict.Setting normal to 300 while bold stays at 700 turns one small step into an
obvious one, and it also makes ordinary text lighter, which was the other half
of what bothered me.
Two details of the surrounding behaviour come with it. A live save reaches open
Agent Teams panes, which read these settings when they are constructed;
applyTerminalSkinalready propagates its own options acrossteammateTerminals, so a pane left at the old weight beside a repaintedterminal would read as a bug. And
i18n.jsgainszh-CNentries for bothlabels and both descriptions, matching the terminal font row directly above
them — option labels stay English, since no settings select in that file
translates its options.
The same live-apply gap exists today for
terminalFontFamily, which onlyupdates the main terminal. I left that alone rather than fold an unrelated fix
into this branch, and I am happy to send it separately.
Two changes land inside the font-settle work
Making a weight settle where it should needs two things, and the second touches
_awaitTerminalFontfrom #396.applyTerminalFontWeightsre-arms_terminalFontReadyand fits again once thewait resolves, because the browser may not have rasterized the new face when
the option is written. On the DOM measurement fallback the weight also moves
the measured cell, since
DomRendererstyles the measure span throughspan:not(.xterm-bold). A browser withOffscreenCanvasmeasures through acanvas whose font string carries no weight, so there the refit is insurance
rather than a correction.
_awaitTerminalFontnow requests two descriptors per family once a weight isset.
CharSizeServicemeasures the cell with${fontSize}px ${fontFamily}(
CharSizeService.ts:122), and the CSSfontshorthand resets the weight, sothe measurement always uses the 400 face while the renderer paints whatever
weight is configured. The wait therefore asks for the unweighted descriptor,
and for the configured one as well when it differs. Because the second is added
only when a weight is set, the default path emits exactly the request it emits
today, and the two descriptor expectations pinned in
test/terminal-font-settle.test.tsare unchanged. Tell me if you would ratherthat method stayed untouched and the wait only ever covered the measured face.
State
I have this working on a branch off 1.26.2: one commit, eight files, 342
insertions and 6 deletions.
npm run typecheck,npm run lint,npm run format:checkandnpm run check:frontend-syntaxall pass, andnpm testreports 355 files and 6816 tests passing with 12 skipped. Theresolvers have unit tests, three cases drive the live-apply path, the
font-settle tests cover the two-descriptor behaviour, and I drove the settings
dialog end to end in a headless browser to confirm that a save reaches the live
terminal and that a stored weight the picker does not list survives it. I also
put the change through two independent code reviewers before posting and acted
on everything they raised. Happy to open the PR if this shape sounds right, or
to change it first.
Open questions
only for a family carrying something heavier than bold. I included it for
symmetry and would happily drop it.
Default entry, and the populate path keeps a stored value the picker does not
list, so a hand-set 350 survives a save rather than being reset by it. A text
field would surface the full range directly instead. I went with the select
because that is what every other enumerated setting here uses.
Written with AI assistance. I have read the change and run it.
All reactions