Skip to content

Custom theme support#226

Open
sgruendel wants to merge 3 commits intomodem-dev:mainfrom
sgruendel:custom_theme
Open

Custom theme support#226
sgruendel wants to merge 3 commits intomodem-dev:mainfrom
sgruendel:custom_theme

Conversation

@sgruendel
Copy link
Copy Markdown

  • Add theme = "custom" support so Hunk can read theme colors from config.toml.
  • Let custom themes inherit from any built-in base theme and override palette and syntax colors selectively.
  • Normalize git diff --no-index patch prefixes so colorized pager and patch input still parse correctly.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 7, 2026

Greptile Summary

This PR adds theme = "custom" support to Hunk, letting users define a palette in config.toml that inherits from any built-in base theme and overrides colors selectively. It also normalizes git diff --no-index patch prefixes (1//2/a//b/) so the pager can parse those diffs correctly.

  • Custom theme config (config.ts, types.ts): New CustomThemeConfig / CustomSyntaxColorsConfig types; validation for base IDs and #rrggbb hex colors; two-layer merge (global → repo config) with field-level syntax override merging.
  • UI wiring (themes.ts, App.tsx, AppHost.tsx, appMenus.ts): buildCustomTheme inherits the full palette from a built-in base, availableThemes() appends the custom entry to the cycle/menu list, and resolveTheme dispatches to the custom builder when theme = "custom" and a config is present.
  • No-index normalization (loaders.ts): normalizeGitNoIndexPrefixes rewrites the three header lines that differ between git diff --no-index and standard git diff output, applied before patch parsing.

Confidence Score: 4/5

Safe to merge; the custom theme feature is well-tested and the wiring through config resolution, bootstrap, and UI is consistent.

Two edge cases in the changed paths are worth watching: the greedy regex in normalizeGitNoIndexPrefixes can misparse diff --git headers for filenames that contain 2/ as a literal substring, and a user who sets theme=custom without a [custom_theme] block will silently get graphite with no diagnostic. Neither affects the happy path or existing functionality.

src/core/loaders.ts (regex robustness for --no-index paths) and src/core/config.ts (missing validation when theme=custom has no accompanying custom_theme section).

Important Files Changed

Filename Overview
src/core/config.ts Adds custom theme config parsing, validation, and two-layer merge. Logic is correct; silent fallback when theme=custom has no [custom_theme] section is a minor UX issue.
src/core/loaders.ts Adds normalizeGitNoIndexPrefixes for --no-index patch header rewriting and threads customTheme into AppBootstrap. The greedy regex in the diff --git line can mismatch paths containing 2/.
src/ui/themes.ts Introduces buildCustomTheme, availableThemes, and availableThemeIds; extends resolveTheme to handle custom. The inheritance and lazy syntax style construction look correct.
src/core/types.ts Adds CustomThemeConfig and CustomSyntaxColorsConfig types; extends AppBootstrap with optional customTheme field.
src/ui/App.tsx Switches from static THEMES array to availableThemes() for cycling and menus; passes customTheme into resolveTheme. themeOptions is correctly memoized.
src/ui/AppHost.tsx Properly threads customTheme from resolveConfiguredCliInput into loadAppBootstrap for daemon-driven reloads.
src/ui/lib/appMenus.ts Replaces static THEMES import with an availableThemes parameter, enabling the custom theme entry to appear in the menus.
src/core/startup.ts Passes configured.customTheme into loadAppBootstrap; straightforward wiring change.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[config.toml: theme = custom] --> B[resolveConfiguredCliInput]
    B --> C[readCustomTheme global config]
    B --> D[readCustomTheme repo config]
    C --> E[mergeCustomTheme]
    D --> E
    E -->|CustomThemeConfig| F[loadAppBootstrap]
    F -->|AppBootstrap.customTheme| G[App.tsx]
    G --> H[resolveTheme themeId customTheme]
    H -->|theme=custom + config present| I[buildCustomTheme]
    I --> J[builtInThemeById base]
    J --> K[Merge palette overrides]
    K --> L[withLazySyntaxStyle merged syntax]
    L --> M[activeTheme]
    G --> N[availableThemes customTheme]
    N -->|append custom entry| O[themeOptions for cycle and menus]
    P[git diff --no-index patch] --> Q[normalizeGitNoIndexPrefixes]
    Q -->|1/ to a/ 2/ to b/| R[normalizePatchChangeset]
    R --> S[parsePatchFiles]
Loading

Comments Outside Diff (2)

  1. src/core/loaders.ts, line 452-455 (link)

    P2 Greedy regex may misparse diff --git headers with unusual paths

    The first capture group in /^diff --git 1\/(.+) 2\/(.+)$/gm is greedy, so the regex engine finds the last occurrence of 2/ in the line as the boundary between the two paths. A filename that contains 2/ as a literal substring (e.g. tests/2/case 2/tests/2/case) would be split at the wrong 2/, producing an incorrect a/… / b/… pair and breaking the downstream patch parser. Switching the first group to a lazy quantifier ((.+?)) won't fully eliminate the ambiguity for such names either, but it does resolve the more common scenario where paths contain /2/ as a path component (e.g. feat/2.0/auth).

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/core/loaders.ts
    Line: 452-455
    
    Comment:
    **Greedy regex may misparse `diff --git` headers with unusual paths**
    
    The first capture group in `/^diff --git 1\/(.+) 2\/(.+)$/gm` is greedy, so the regex engine finds the *last* occurrence of ` 2/` in the line as the boundary between the two paths. A filename that contains ` 2/` as a literal substring (e.g. `tests/2/case 2/tests/2/case`) would be split at the wrong `2/`, producing an incorrect `a/…` / `b/…` pair and breaking the downstream patch parser. Switching the first group to a lazy quantifier (`(.+?)`) won't fully eliminate the ambiguity for such names either, but it does resolve the more common scenario where paths contain `/2/` as a path component (e.g. `feat/2.0/auth`).
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. src/core/config.ts, line 386-411 (link)

    P2 theme = "custom" without a [custom_theme] section silently falls back to graphite

    When a user sets theme = "custom" in their config but omits the [custom_theme] table, readCustomTheme returns undefined, so resolvedCustomTheme stays undefined. In resolveTheme, the requested === "custom" && customTheme guard is false, no built-in theme matches "custom", and graphite is returned. The themeId state is therefore initialised to "graphite", silently discarding the user's intent with no warning. A validation check after config layers are merged — e.g., throwing or logging when resolvedOptions.theme === "custom" but resolvedCustomTheme is still undefined — would make the misconfiguration visible.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/core/config.ts
    Line: 386-411
    
    Comment:
    **`theme = "custom"` without a `[custom_theme]` section silently falls back to graphite**
    
    When a user sets `theme = "custom"` in their config but omits the `[custom_theme]` table, `readCustomTheme` returns `undefined`, so `resolvedCustomTheme` stays `undefined`. In `resolveTheme`, the `requested === "custom" && customTheme` guard is false, no built-in theme matches `"custom"`, and graphite is returned. The `themeId` state is therefore initialised to `"graphite"`, silently discarding the user's intent with no warning. A validation check after config layers are merged — e.g., throwing or logging when `resolvedOptions.theme === "custom"` but `resolvedCustomTheme` is still `undefined` — would make the misconfiguration visible.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
src/core/loaders.ts:452-455
**Greedy regex may misparse `diff --git` headers with unusual paths**

The first capture group in `/^diff --git 1\/(.+) 2\/(.+)$/gm` is greedy, so the regex engine finds the *last* occurrence of ` 2/` in the line as the boundary between the two paths. A filename that contains ` 2/` as a literal substring (e.g. `tests/2/case 2/tests/2/case`) would be split at the wrong `2/`, producing an incorrect `a/…` / `b/…` pair and breaking the downstream patch parser. Switching the first group to a lazy quantifier (`(.+?)`) won't fully eliminate the ambiguity for such names either, but it does resolve the more common scenario where paths contain `/2/` as a path component (e.g. `feat/2.0/auth`).

### Issue 2 of 2
src/core/config.ts:386-411
**`theme = "custom"` without a `[custom_theme]` section silently falls back to graphite**

When a user sets `theme = "custom"` in their config but omits the `[custom_theme]` table, `readCustomTheme` returns `undefined`, so `resolvedCustomTheme` stays `undefined`. In `resolveTheme`, the `requested === "custom" && customTheme` guard is false, no built-in theme matches `"custom"`, and graphite is returned. The `themeId` state is therefore initialised to `"graphite"`, silently discarding the user's intent with no warning. A validation check after config layers are merged — e.g., throwing or logging when `resolvedOptions.theme === "custom"` but `resolvedCustomTheme` is still `undefined` — would make the misconfiguration visible.

Reviews (1): Last reviewed commit: "feat: normalize Git no-index prefixes fo..." | Re-trigger Greptile

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