Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,12 +235,12 @@ tagged. Until then, source builds report the version `dev`.
GitHub issue/PR templates.
- Interactive `/theme` picker: bare `/theme` opens a popup that live-previews each palette as you move
and applies on select (Esc reverts).
- Twelve built-in color themes alongside the `dark`/`light` built-ins — `dracula`, `nord`, `gruvbox`,
- Thirteen built-in color themes alongside the `dark`/`light` built-ins — `dracula`, `nord`, `gruvbox`,
`tokyo-night`, `catppuccin`, `one-dark`, `solarized-dark`, `rose-pine`, `everforest`,
`solarized-light`, `dune`, and `neon` — selectable via `/theme <name>`, `--theme <name>`, or
`solarized-light`, `dune`, `dune-dark`, and `neon` — selectable via `/theme <name>`, `--theme <name>`, or
`ZERO_THEME`. Every palette is contrast-audited to WCAG AA, and the new presets are additionally
audited after xterm-256 downsampling; see [docs/THEMES.md](docs/THEMES.md). The built-in light
theme was reworked for legibility.
audited after xterm-256 downsampling (Dune Dark also after 16-color ANSI conversion); see
[docs/THEMES.md](docs/THEMES.md). The built-in light theme was reworked for legibility.
- `--theme <name>` flag for the TUI, accepting `auto` or any registered theme (previously only the
`ZERO_THEME` env var existed).
- "Accessibility / Appearance" section in the README documenting `NO_COLOR`, `ZERO_THEME`, `/theme`,
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ manifest.
| Control | Effect |
|---|---|
| `NO_COLOR=<anything>` | disables color output |
| `ZERO_THEME=<name>` | selects the startup theme (`auto`, `dark`, `light`, or a color theme like `dracula`, `nord`, `gruvbox`, `tokyo-night`, `catppuccin`, `one-dark`, `solarized-dark`, `rose-pine`, `everforest`, `neon`, `solarized-light`, `dune`) |
| `ZERO_THEME=<name>` | selects the startup theme (`auto`, `dark`, `light`, or a color theme like `dracula`, `nord`, `gruvbox`, `tokyo-night`, `catppuccin`, `one-dark`, `solarized-dark`, `rose-pine`, `everforest`, `neon`, `dune-dark`, `solarized-light`, `dune`) |
| `--theme <name>` | selects the TUI theme from the CLI (same names) |
| `/theme` | opens the theme picker inside the TUI (live preview; `/theme <name>` switches directly) |
| `ZERO_NO_FADE=1` | disables streaming fade animation |
Expand Down
2 changes: 1 addition & 1 deletion README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ zero update 检查更新版本
| 控制 | 效果 |
|---|---|
| `NO_COLOR=<任意值>` | 禁用颜色输出 |
| `ZERO_THEME=<名称>` | 选择启动主题(`auto`、`dark`、`light`,或颜色主题如 `dracula`、`nord`、`gruvbox`、`tokyo-night`、`catppuccin`、`one-dark`、`solarized-dark`、`rose-pine`、`everforest`、`neon`、`solarized-light`、`dune`) |
| `ZERO_THEME=<名称>` | 选择启动主题(`auto`、`dark`、`light`,或颜色主题如 `dracula`、`nord`、`gruvbox`、`tokyo-night`、`catppuccin`、`one-dark`、`solarized-dark`、`rose-pine`、`everforest`、`neon`、`dune-dark`、`solarized-light`、`dune`) |
| `--theme <名称>` | 从 CLI 选择 TUI 主题(相同名称) |
| `/theme` | 在 TUI 中打开主题选择器(实时预览;`/theme <名称>` 直接切换) |
| `ZERO_NO_FADE=1` | 禁用流式淡入动画 |
Expand Down
30 changes: 19 additions & 11 deletions docs/THEMES.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@ the active theme and the registered names without opening the picker.

## Dune (`dune`)

A warm sand-and-cream palette: sand/cream surface, charcoal ink, and a soft
amber accent.
A warm sand-and-cream palette with charcoal ink, soft amber accent, and
quantization-safe diff bands.

## Dune Dark (`dune-dark`)

A dark, colorblind-friendly palette inspired by Claude Code's daltonized dark palette:
near-black surface, white ink, and a deuteranopia-adjusted brand-orange
accent.

## Neon (`neon`)

Expand All @@ -28,12 +34,14 @@ adding a new `palette{...}` literal, a `themeRegistry` entry, and test
coverage for the new palette (see below).

Registry-wide tests in `internal/tui/theme_select_test.go` assert the basic
WCAG AA text tokens, the gray-ramp order, the diff word-span pairs, and the
selected-row band for every entry. The rendered-surface invariants beyond
those (permission surfaces, selected-row secondary text, diff gutters, and
the xterm-256 downsampling checks) are asserted per palette, not against the
whole registry: `TestExtendedThemeContrastInvariants` and
`TestExtendedThemeANSI256Contrast` enumerate the palettes they cover. A new
theme must be added to those tests (or given equivalent palette-specific
assertions), or CI can stay green while its permission, selected-row, and
diff surfaces ship unreadable.
WCAG AA text tokens, the gray-ramp order, the selected-row band, and (via
`TestDiffHighlightWordSpans`) the word-span *contrast* pairs for every
entry. The remaining rendered-surface invariants (permission surfaces,
selected-row secondary text, xterm-256 quantization, and the quantized
add/del band *identity* check that `addBg`/`delBg` stay green/red and
distinct) are asserted per palette, not against the whole registry:
`TestExtendedThemeContrastInvariants` and `TestExtendedThemeANSI256Contrast`
enumerate the palettes they cover. A new theme must be added to those
tests (or given equivalent palette-specific assertions), or CI can stay
green while its permission, selected-row, and quantized-diff surfaces
ship unreadable.
76 changes: 69 additions & 7 deletions internal/tui/theme_palettes.go
Original file line number Diff line number Diff line change
Expand Up @@ -444,17 +444,17 @@ var dunePalette = palette{
muted: "#473e32",
faint: "#554a3a",
faintest: "#655648",
accent: "#724028", // darkened from #8f5215 for AA on selBg (5.46:1) that also survives ANSI-256 downsampling (quantizes to #444444, 6.47:1 on quantized selBg; the previous #7c4712 quantized to #875f00 at 3.81:1)
accent: "#5f4b30", // darkened from #8f5215 for AA on selBg (5.46:1) that also survives ANSI-256 downsampling (quantizes to #444444, 6.47:1 on quantized selBg)
green: "#38572a",
red: "#872d24", // darkened from #963328 so delBg contrast survives ANSI-256 downsampling (true 6.57:1, 256 7.86:1)
red: "#872d24", // darkened from #963328 so delBg contrast survives ANSI-256 downsampling
amber: "#6d4600",
blue: "#2f5680", // darkened from #3d6a9e for AA on selBg (was 3.61:1, now 4.90:1)
blue: "#2f5680",
gitAdd: "#38572a",
gitDel: "#963328",
addBg: "#dcecd0",
delBg: "#f5dbd5",
addBgWord: "#b9dc9e",
delBgWord: "#eebba9",
addBg: "#d0f0c0", // quantizes to xterm green #d7ffd7, keeping add/del rows distinct on 256-color terminals
delBg: "#fcd8d4", // quantizes to xterm red #ffd7d7
addBgWord: "#a5e090",
delBgWord: "#f8b8af",
permBg: "#f0dfae",
selBg: "#e0cf98",
addInk: "#264018",
Expand All @@ -465,6 +465,67 @@ var dunePalette = palette{
cardPerm: "#c2a04a",
}

// duneDarkPalette is a dark theme inspired by Claude Code's colorblind-friendly
// (daltonized) dark palette. The base surface is near-black (Claude Code's true
// dark canvas), with white ink and the brand-orange accent adjusted for
// deuteranopia. Diff signals reuse the proven dark-theme structure so every
// WCAG-AA invariant still holds; the accent and status colors (cool success,
// warm error, amber warning) follow a daltonized palette so additions stay
// distinguishable under color blindness.
//
// 16-color terminals (TERM=xterm) force every token through ansi.Convert16.
// Saturated cyan/blue success and bright red error both collapse onto ANSI
// pairs that fail AA on the green/maroon diff bands (bright-blue-on-green
// 1.67:1, bright-red-on-maroon 2.74:1). Soft periwinkle success and soft pink
// error map to ANSI white, which clears AA on those bands while keeping a
// cool/warm tint in truecolor and 256-color.
var duneDarkPalette = palette{
panel: "#0e0e10", // Claude Code dark canvas — near-black
promptBg: "#262626", // submitted user-prompt bubble
line: "#242429", // borders/separators
line2: "#414147",
ink: "#ececee",
// Gray ramp (ink > muted > faint > faintest > panel) is kept light enough
// that faintest maps to ANSI white / xterm #c6c6c6 and clears WCAG AA on the
// green add gutter under both 16-color and 256-color profiles. Prior cyan
// #55c6cd mapped to bright cyan at only 4.10:1 on ANSI green (short of AA).
muted: "#e0e0e6", // secondary text
faint: "#d4d4dc", // hints/metadata
faintest: "#c8c8d0", // line numbers/separators (and diff gutters)
// Warm sand-gold so TERM=xterm maps to ANSI yellow (brand #ff9628 maps to
// red at only 4.00:1 on the navy selection band under 16-color).
accent: "#fff080",
green: "#c8c8e9", // success — soft periwinkle (cool/daltonized). Maps to ANSI white so add-sign text stays AA (5.14:1) on the green add band under 16-color; saturated cyan/blue collapses to bright blue at 1.67:1
red: "#ffc0c8", // error — soft pink. Maps to ANSI white so del-sign text stays AA (10.95:1) on the maroon del band under 16-color; bright red collapses to 2.74:1 on maroon
amber: "#ffcc00", // warning rgb(255,204,0)
blue: "#cceeff", // permission — light sky. Maps to bright cyan so selected-row local-model dots stay AA on black selBg under 16-color (prior #99ccff -> bright blue at 2.44:1)
gitAdd: "#c8c8e9", // matches green (daltonized cool success)
gitDel: "#ffc0c8", // matches red (daltonized warm error)
addBg: "#003500", // diff added band — quantizes to xterm green #005f00 instead of the same gray as delBg, keeping add/del rows distinct on 256-color terminals (previous #0a1f14/#240a0e both collapsed to #121212); 16-color maps to ANSI green
delBg: "#350000", // diff removed band — quantizes to xterm red #5f0000 (see addBg); 16-color maps to ANSI maroon
addBgWord: "#007400", // word-level added span — quantizes to xterm green #008700, distinct from both addBg's #005f00 and delBgWord's red
delBgWord: "#740000", // word-level removed span — quantizes to xterm red #870000 (see addBgWord)
permBg: "#1c1915",
// Near-black grays collapse to ANSI black with panel under TERM=xterm,
// erasing the full-row highlight. Deep indigo maps to navy (#000080) in
// 16-color and #000087 in 256-color, staying distinct from panel.
selBg: "#25257a",
addInk: "#f0f5d2", // changed-word text — lightened so it still clears AA (4.60:1) against addBgWord's xterm-256 quantized #008700 (the original #bdeed7 fell to 4.06:1); 16-color maps to bright yellow (4.78:1 on ANSI green)
delInk: "#fff0f0", // changed-word text — near-white pink maps to ANSI white so word spans stay AA on maroon under 16-color (prior #f2c4c4 -> bright red at 2.74:1)
onAccent: "#000000",
// Status-card borders are non-text UI (WCAG 1.4.11: >=3:1 against panel).
// Prior cardRun #3399ff mapped to ANSI bright blue at only 2.44:1 on the
// black panel under 16-color. #cceeff (same as blue) maps to bright cyan
// and clears 3:1 in truecolor, xterm-256, and ANSI-16.
// cardPerm must stay distinguishable from cardErr after ANSI conversion:
// #ffcc00 collapsed to the same ANSI red as #ff6666; pure yellow maps to
// ANSI yellow so running (cyan) / error (red) / permission (yellow) stay
// three distinct roles under 16-color and 256-color.
cardRun: "#cceeff", // running tool border: light sky / ANSI bright cyan
cardErr: "#ff6666", // failed tool border: warm red / ANSI red
cardPerm: "#fff200", // permission border: pure yellow / ANSI yellow (was #ffcc00 -> ANSI red)
}

// themeEntry is one registered theme: Name is the /theme value + ZERO_THEME/--theme
// token (lowercase, kebab), Label is the picker display text, and IsDark groups the
// picker (Dark/Light sections) and drives which built-in `auto` resolves to.
Expand All @@ -491,6 +552,7 @@ var themeRegistry = []themeEntry{
{Name: "rose-pine", Label: "Rosé Pine", Palette: rosePinePalette, IsDark: true},
{Name: "everforest", Label: "Everforest", Palette: everforestPalette, IsDark: true},
{Name: "neon", Label: "Neon", Palette: neonPalette, IsDark: true},
{Name: "dune-dark", Label: "Dune Dark", Palette: duneDarkPalette, IsDark: true},
{Name: "light", Label: "light", Palette: lightPalette, IsDark: false},
{Name: "solarized-light", Label: "Solarized Light", Palette: solarizedLightPalette, IsDark: false},
{Name: "dune", Label: "Dune", Palette: dunePalette, IsDark: false},
Expand Down
Loading
Loading