From 3ba6eebc7cbb4939a370d3d00afe777be7213802 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Tue, 21 Jul 2026 20:01:32 -0500 Subject: [PATCH] feat(themes): dedicated code-syntax palette keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code highlighting reused brand tokens (accent/text/border/muted), so it couldn't be themed independently. Add syntax_string/number/keyword/comment skin keys → syntax* theme tokens (defaulting to those brand tokens, so defaults are unchanged) and point the highlighter at them. Documented in the element→key map. --- apps/shared/src/skin.ts | 4 ++++ hermes_cli/skin_engine.py | 4 ++++ skills/hermes-themes/SKILL.md | 1 + ui-tui/src/__tests__/theme.test.ts | 17 +++++++++++++++++ ui-tui/src/lib/syntax.ts | 8 ++++---- ui-tui/src/theme.ts | 20 +++++++++++++++++++- 6 files changed, 49 insertions(+), 5 deletions(-) diff --git a/apps/shared/src/skin.ts b/apps/shared/src/skin.ts index cc518817f0b4..d5c600da43f5 100644 --- a/apps/shared/src/skin.ts +++ b/apps/shared/src/skin.ts @@ -44,6 +44,10 @@ export const SKIN_COLOR_TOKENS = [ 'diff_removed', 'diff_added_word', 'diff_removed_word', + 'syntax_string', + 'syntax_number', + 'syntax_keyword', + 'syntax_comment', // CLI / TUI chrome. 'prompt', 'input_rule', diff --git a/hermes_cli/skin_engine.py b/hermes_cli/skin_engine.py index 843f76cc620b..a2a093b7dc38 100644 --- a/hermes_cli/skin_engine.py +++ b/hermes_cli/skin_engine.py @@ -42,6 +42,10 @@ All fields are optional. Missing values inherit from the ``default`` skin. diff_removed: "#ffdcdc" # Diff removed-line background diff_added_word: "#248a3d" # Diff added word-level foreground diff_removed_word: "#cf222e" # Diff removed word-level foreground + syntax_string: "#FFBF00" # Code strings; falls back to ui_accent + syntax_number: "#FFF8DC" # Code numbers; falls back to ui_text + syntax_keyword: "#CD7F32" # Code keywords; falls back to ui_border + syntax_comment: "#CC9B1F" # Code comments; falls back to banner_dim prompt: "#FFF8DC" # Prompt text color input_rule: "#CD7F32" # Input area horizontal rule response_border: "#FFD700" # Response box border (ANSI) diff --git a/skills/hermes-themes/SKILL.md b/skills/hermes-themes/SKILL.md index f95ce613593b..15982af0c8f4 100644 --- a/skills/hermes-themes/SKILL.md +++ b/skills/hermes-themes/SKILL.md @@ -61,6 +61,7 @@ key in its row (element-specific keys fall back to the shared one when unset). | Success / warn / error | `ui_ok` / `ui_warn` / `ui_error` | — | | Status bar text + usage | `status_bar_text`, `status_bar_good/warn/bad/critical` | — | | Diff add/remove (line + word) | `diff_added` / `diff_removed` / `diff_added_word` / `diff_removed_word` | built-in | +| Code syntax (string/number/keyword/comment) | `syntax_string` / `syntax_number` / `syntax_keyword` / `syntax_comment` | accent/text/border/muted | | Completion menu | `completion_menu_bg` / `completion_menu_current_bg` / `…_meta_bg` | — | Note the sharing: `ui_accent` colors tool markers **and** headings/links/chevrons, diff --git a/ui-tui/src/__tests__/theme.test.ts b/ui-tui/src/__tests__/theme.test.ts index 57d03d2eff61..3ba6513e2ab8 100644 --- a/ui-tui/src/__tests__/theme.test.ts +++ b/ui-tui/src/__tests__/theme.test.ts @@ -548,6 +548,23 @@ describe('background-aware adaptation (OSC-11 light terminals)', () => { expect(fallback.color.thinking).toBe('#123456') }) + it('gives code syntax its own keys, defaulting to accent/text/border/muted', async () => { + const { fromSkin } = await importThemeWithCleanEnv() + + const themed = fromSkin( + { syntax_string: '#aa0000', syntax_number: '#00aa00', syntax_keyword: '#0000aa', syntax_comment: '#888888' }, + {} + ) + + expect(themed.color.syntaxString).toBe('#aa0000') + expect(themed.color.syntaxNumber).toBe('#00aa00') + expect(themed.color.syntaxKeyword).toBe('#0000aa') + expect(themed.color.syntaxComment).toBe('#888888') + + const fallback = fromSkin({ ui_accent: '#abcdef' }, {}) + expect(fallback.color.syntaxString).toBe('#abcdef') // string follows accent + }) + it('lets skins override diff colors', async () => { const { fromSkin } = await importThemeWithCleanEnv() diff --git a/ui-tui/src/lib/syntax.ts b/ui-tui/src/lib/syntax.ts index 3b66f6ddc720..519679c1f548 100644 --- a/ui-tui/src/lib/syntax.ts +++ b/ui-tui/src/lib/syntax.ts @@ -80,7 +80,7 @@ export function highlightLine(line: string, lang: string, t: Theme): Token[] { } if (spec.comment && line.trimStart().startsWith(spec.comment)) { - return [[t.color.muted, line]] + return [[t.color.syntaxComment, line]] } const tokens: Token[] = [] @@ -97,11 +97,11 @@ export function highlightLine(line: string, lang: string, t: Theme): Token[] { const ch = tok[0]! if (ch === '"' || ch === "'" || ch === '`') { - tokens.push([t.color.accent, tok]) + tokens.push([t.color.syntaxString, tok]) } else if (ch >= '0' && ch <= '9') { - tokens.push([t.color.text, tok]) + tokens.push([t.color.syntaxNumber, tok]) } else if (spec.keywords.has(tok)) { - tokens.push([t.color.border, tok]) + tokens.push([t.color.syntaxKeyword, tok]) } else { tokens.push(['', tok]) } diff --git a/ui-tui/src/theme.ts b/ui-tui/src/theme.ts index 4901a3124fa6..de18534d6582 100644 --- a/ui-tui/src/theme.ts +++ b/ui-tui/src/theme.ts @@ -23,6 +23,12 @@ export interface ThemeColors { /** Reasoning/thinking body text. Defaults to `muted`. */ thinking: string + /** Code-block syntax highlight. Default to accent/text/border/muted. */ + syntaxString: string + syntaxNumber: string + syntaxKeyword: string + syntaxComment: string + prompt: string sessionLabel: string sessionBorder: string @@ -310,6 +316,13 @@ export function buildPalette(seeds: ThemeSeeds, isLight: boolean): ThemeColors { tool: seeds.accent, thinking: muted, + // Code-syntax tokens default to brand tokens (unchanged highlighting) + // but are independently skinnable. + syntaxString: seeds.accent, + syntaxNumber: seeds.text, + syntaxKeyword: seeds.border ?? tones.border, + syntaxComment: muted, + prompt: seeds.prompt ?? seeds.text, // sessionLabel/sessionBorder track the muted tone — "same role, same // colour" by design (#11300). @@ -855,7 +868,12 @@ export function fromSkin( diffAdded: c('diff_added') ?? derived.diffAdded, diffRemoved: c('diff_removed') ?? derived.diffRemoved, diffAddedWord: c('diff_added_word') ?? derived.diffAddedWord, - diffRemovedWord: c('diff_removed_word') ?? derived.diffRemovedWord + diffRemovedWord: c('diff_removed_word') ?? derived.diffRemovedWord, + // Code-syntax tokens: overridable, else the derived brand-token defaults. + syntaxString: c('syntax_string') ?? derived.syntaxString, + syntaxNumber: c('syntax_number') ?? derived.syntaxNumber, + syntaxKeyword: c('syntax_keyword') ?? derived.syntaxKeyword, + syntaxComment: c('syntax_comment') ?? derived.syntaxComment } // 4. Guard: contrast floors against the real background + fill polarity.