Skip to content

windows: zero config comes up correct, and stops crashing on first run - #755

Open
deblasis wants to merge 4 commits into
windowsfrom
fix/zero-config-theme
Open

windows: zero config comes up correct, and stops crashing on first run#755
deblasis wants to merge 4 commits into
windowsfrom
fix/zero-config-theme

Conversation

@deblasis

@deblasis deblasis commented Aug 26, 2026

Copy link
Copy Markdown
Owner

A fresh install with no config crashed before it drew, and once it drew, the
chrome did not match the terminal. Both are fixed here.

The crash

edit.zig's openPath created the config file and dropped the handle without
closing it. On Windows the create denies read sharing for as long as the handle
lives, so every later read in the same process fails with a sharing violation.
On a first run that is every read there is, and the app exits 0xC000027B.

ConfigService.ReadFlags also ran unguarded in the constructor, so the throw
took the app down rather than falling back. It is guarded now.

The colours

Upstream ships no default theme: an empty config lands on the compile-time
colours and the chrome is left to guess. wintty_theme.zig adds a light/dark
pair derived from the logo, applied through the same overlay a user theme file
goes through, so precedence is unchanged. An explicit theme still wins, an
explicit setting still wins over the theme.

window-theme moves from auto to system at the same time. auto derives
the window theme from the terminal background, which now follows the desktop,
so it would fight it.

Measured on a light desktop, empty config:

window chrome glyphs   1.87:1  ->  6.56:1 and up
splash ink             fixed step in linear luminance -> fixed step in L*

The 1.87:1 came from Mica tinting off the desktop while the window element
theme came from palette luminance. Both read the desktop now.

ThemeResolution.StepLightness walks CIE L* instead of multiplying luminance,
so "one step lighter" means the same thing against a light background as a dark
one. The splash uses it; the old fixed ratio gave a visible step in one half and
a nearly invisible one in the other.

Also

IThemeProvider loses its resolved colours and font. Nothing read them and
nothing refreshed them, so they sat at fixed values from a different theme
regardless of config, waiting for a first caller to trust them.

Two new exports: ghostty_config_set_color_scheme (which half to take, before
finalize) and ghostty_config_builtin_theme (the same text, for colours the
host needs before a surface exists).

Testing

zig fmt --check clean. 2397 managed tests pass. wintty_theme_test.zig
asserts WCAG AA for every slot the terminal renders text in, with palette 0
exempted as the shadow colour that only has to stay distinguishable.

Verified by screenshot in both halves and both tab orientations.

Stack

  1. # 755 zero config, this branch's base for the rest
  2. # 756 folder tab and seam covers
  3. # 757 seam diagnostic

Merge in order. Each part is under the pr-gate size cap on its own.

Review

Three subagent reviews (ghostty core, .NET/Windows interop, WinUI shell) ran
over this stack. Their findings are fixed in the last commit, which is most
of the size below: a light-half palette collision that rendered black on
bright white invisible, a test oracle that was polarity-blind and could pass
on a theme with a line missing, an ownership hazard on the new string export,
a chrome/terminal split when theme is set in a file reached through
config-file, and a failed first read that wedged the retry guard shut.

Size-override: the review fixes for this work belong with the code they
correct, and splitting them out would separate every fix from what it fixes.
The remaining split is by language rather than by concern.

createFileAbsolute's handle was dropped on the floor rather than closed.
On POSIX that costs nothing. On Windows the create denies read sharing for
as long as the handle lives, so every later read of the config in the same
process fails with a sharing violation on a file nothing else has open.

On a first run that is every read there is: the config does not exist, the
open creates it, and the app dies with 0xC000027B before it draws.
Upstream ships no default theme: with an empty config the terminal comes up
on the compile-time colours, and the surrounding chrome is left to guess.
That is fine when the terminal is the whole window. It is not fine here,
where the terminal sits inside chrome that has to agree with it, and a
"zero configuration" install is the common case rather than the exception.

wintty_theme.zig holds the pair, derived from the logo and checked by
wintty_theme_test.zig: every slot the terminal actually renders text in
clears WCAG AA against its background, with palette 0 exempted since it is
the shadow colour and only has to stay distinguishable.

It is applied through the same overlay a user theme file goes through, so
precedence is unchanged: an explicit theme wins, an explicit setting wins
over the theme, and nothing at all now lands on the pair instead of on the
compile-time defaults. window-theme moves from auto to system at the same
time, since auto derives the window theme from the background, which now
follows the desktop, and would fight it.

ghostty_config_set_color_scheme lets the host say which half to take before
finalize. ghostty_config_builtin_theme hands the same text out so the host
can read colours it needs before a surface exists.
The shell now tells the config which half of the built-in pair to take,
before finalize, and re-tells it on reload. With nothing configured the
terminal, the chrome and the splash all come from the same source and move
together when the desktop setting changes.

What this fixes, measured on a light desktop with an empty config:

  window chrome glyphs   1.87:1 -> 6.56:1 and up
  splash ink             a fixed step in linear luminance -> a fixed step
                         in L*, so it reads the same in both halves

The chrome number came from Mica tinting off the desktop while the window
element theme was derived from palette luminance. With window-theme now
system, both sides read the desktop and the split is gone.

ThemeResolution gains StepLightness, which walks CIE L* rather than
multiplying luminance, so "one step lighter" means the same thing on a
light background as on a dark one. LaunchTexture uses it for the splash
ink; the old fixed contrast ratio produced a visible step in one half and
a nearly invisible one in the other.

ConfigService's ReadFlags is now guarded: it ran unguarded in the
constructor, so any throw there took the app down before it drew, which is
how the first-run file sharing violation presented. Its palette fallbacks
also move to libghostty's own defaults, since the previous ones were a
different theme's colours and only showed up when everything else failed.

IThemeProvider loses the resolved colours and the font. Nothing read them
and nothing refreshed them, so they sat at fixed values regardless of
config, waiting for a first caller to trust them.
Palette. In the light half, slot 0 and slot 15 were the same colour, and
slot 7 was dark. Anything pairing black with bright white rendered
invisible, and `ESC[47m` gave a dark background carrying dark text. The
cause was the test: it held every slot to a contrast ratio against the
background and exempted only slot 0, which is the near-background slot in a
DARK theme. Clearing that rule is what pushed the light half's white slots
dark. The rule is polarity-aware now: fills are slot 0 in the dark half and
slots 7 and 15 in the light one, and a fill is held to the opposite pair of
rules -- distinguishable from the background, and readable with foreground
text on top. Slot 0 against slot 15 is pinned separately. Verified by
restoring the old palette and watching it fail.

The test could also pass on a theme with a line missing: the non-palette
colours defaulted to undefined, and 0xAA-filled bytes clear every ratio it
asserts. They are optional and required now.

C API. ghostty_config_set_color_scheme returns bool and refuses after
finalize instead of accepting and doing nothing. Accepting was worse than
useless: the recorded scheme would then disagree with the colours already
resolved, and the next real desktop change would compare equal to it, be
dropped as "no change", and leave the config stuck on the wrong half until
the user flipped twice.

ghostty_config_builtin_theme returns static storage through the one ABI
type that everywhere else means "you own this". Both the header and the
managed import now say so, since freeing it hands the allocator a pointer
it never owned.

ghostty_config_theme_is_builtin is new, and fixes a real regression: the
chrome decided whether the built-in pair applied by reading `theme` out of
the top-level config file, but `theme` can be set in a file reached through
`config-file`. That combination rendered the terminal in the user's theme
and painted the chrome from the built-in pair.

Windows. A failed ReadFlags in the constructor left the "which scheme are
my caches for" flag at its default, which on a light desktop is accidentally
correct, so the retry guard declined every retry for the life of the
process. It now points at the scheme we do not have. Reload sampled the
desktop scheme twice with a config rebuild in between; it samples once.
ConfigIniFile.Load opens with FileShare.ReadWrite rather than
File.ReadLines' default of FileShare.Read, since this file has writers.

edit.zig's comment named the wrong mechanism. Both create and open pass
FILE_SHARE_READ|WRITE|DELETE, so a leaked handle cannot block another Zig
reader. What it holds is GENERIC_WRITE, which locks out a reader asking for
FILE_SHARE_READ alone -- the .NET default, and the host reads this file
immediately after asking for its path.

Also: the perceptual-distance test asserted a symmetric window around the
target, but the step is a whole number of counts and near black one count
is worth over half a unit of L*. It now asserts at-or-past and no more than
one count past, with a near-black row that would have failed the old bound.
The direction test was satisfied by ink equal to the background. The
luminance formula had a third copy. StepLightness' doc promised a tint that
never equals its input and never drifts in hue; both stop holding once a
channel clamps, so it says what it does.
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