Skip to content

Add Ghostty terminal theme picker in Settings - #699

Open
lcamargof wants to merge 1 commit into
manaflow-ai:mainfrom
lcamargof:main
Open

lcamargof wants to merge 1 commit into
manaflow-ai:mainfrom
lcamargof:main

Conversation

@lcamargof

@lcamargof lcamargof commented Feb 28, 2026 •

Copy link
Copy Markdown

Summary

  • Adds a "Terminal Theme" picker to Settings that lists all available Ghostty themes
  • Scans bundled themes, Ghostty.app resources, and user theme directories (~/.config/ghostty/themes)
  • Selected theme overrides the ghostty config theme setting; "Default" falls back to user's ghostty config

How it works

TerminalThemeSettings stores a single theme name in UserDefaults. On config load, if a theme is set, it writes a temp theme = <name> config file and loads it via ghostty_config_load_file — overriding whatever the user's ghostty config specifies.

Test plan

  • Open Settings → "Terminal Theme" picker shows available Ghostty themes
  • Pick a theme → terminal colors update immediately
  • Switch app appearance Light↔Dark → terminal keeps the selected theme, only chrome changes
  • Set to "Default (Ghostty Config)" → falls back to ghostty config

Test

Screenshot 2026-02-28 at 2 12 59 p m

Summary by CodeRabbit

  • New Features
    • Added terminal theme customization in Settings with a new "Terminal" section and theme picker.
    • Theme selection persists, is applied immediately, and survives app sessions.
    • Available themes are discovered from multiple locations, deduplicated, sorted, and shown in the picker.
    • Theme list is populated when Settings opens and changes trigger an immediate configuration reload to apply the selected theme.

@vercel

vercel Bot commented Feb 28, 2026

Copy link
Copy Markdown

@lcamargof is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Feb 28, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e9f578d and 2d0cd16.

📒 Files selected for processing (3)
  • Sources/GhosttyConfig.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/cmuxApp.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • Sources/GhosttyTerminalView.swift
  • Sources/cmuxApp.swift

📝 Walkthrough

Walkthrough

Adds terminal theme selection support: enumerates available CMUX theme directories, exposes them to Settings (picker + AppStorage), and applies a CMUX theme override by writing a temporary theme file and loading it via the Ghostty C config loader during startup and when the selected theme changes.

Changes

Cohort / File(s) Summary
Theme enumeration
Sources/GhosttyConfig.swift
Adds GhosttyConfig.availableThemeNames(environment:bundleResourceURL:) to collect theme directory names from resources, bundle, XDG and standard paths; filters out non-directories and hidden entries, deduplicates, and returns a sorted list.
CMUX override application
Sources/GhosttyTerminalView.swift, Sources/GhosttyConfig.swift
Adds loadCmuxThemeOverrideIfNeeded(_:) invoked during default config initialization and when theme changes; writes a temporary CMUX theme override file to /tmp, calls ghostty_config_load_file, and removes the temp file.
Settings UI & settings keys
Sources/cmuxApp.swift
Introduces TerminalThemeSettings (key + effectiveThemeName), terminalTheme AppStorage, availableThemes state, a Settings picker for terminal themes, and logic to invalidate Ghostty load cache and trigger config reloads when the theme changes. Also adds several settings helper enums (QuitWarningSettings, CommandPaletteRenameSelectionSettings, ClaudeCodeIntegrationSettings, TelemetrySettings).

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Settings as Settings UI
    participant App as GhosttyApp
    participant Config as GhosttyConfig
    participant CAPI as Ghostty C API

    User->>Settings: select terminal theme
    Settings->>Settings: update `terminalTheme` (AppStorage)
    Settings->>Config: invalidate load cache
    Settings->>App: request config reload (source: settings.terminalTheme)
    App->>Config: call availableThemeNames()
    Config-->>App: return theme list
    App->>App: loadCmuxThemeOverrideIfNeeded(selectedTheme)
    App->>App: write temp CMUX theme file (/tmp)
    App->>CAPI: ghostty_config_load_file(temp_path)
    CAPI-->>App: config loaded
    App->>App: delete temp file
    App-->>User: theme applied
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped through folders, soft and light,
Collected themes by moon and bright.
A tiny temp file, then colors bloom—
Terminal gardens chase the gloom.
✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add Ghostty terminal theme picker in Settings' directly and clearly describes the main change: introducing a terminal theme picker UI component in the Settings view.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
Sources/GhosttyConfig.swift (1)

412-449: Consider filtering out directories from theme enumeration.

FileManager.contentsOfDirectory(atPath:) returns both files and subdirectories. If a user has a subdirectory in their themes folder (e.g., for organization or backups), it will appear as a selectable theme option but fail to load.

🔧 Optional fix to filter directories
 for dir in themeDirs {
     guard let entries = try? fm.contentsOfDirectory(atPath: dir) else { continue }
     for entry in entries {
         guard !entry.hasPrefix("."), !seen.contains(entry) else { continue }
+        let fullPath = (dir as NSString).appendingPathComponent(entry)
+        var isDirectory: ObjCBool = false
+        guard fm.fileExists(atPath: fullPath, isDirectory: &isDirectory), !isDirectory.boolValue else { continue }
         seen.insert(entry)
         names.append(entry)
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyConfig.swift` around lines 412 - 449, In availableThemeNames,
when iterating entries from FileManager.contentsOfDirectory(atPath:), filter out
non-directory entries so only directories are treated as themes; for each entry
build the full path (e.g., dir + "/" + entry) and use
FileManager.fileExists(atPath:isDirectory:) or attributesOfItem to check
isDirectory, skipping entries that are not directories or are symbolic links to
non-directories; keep the existing deduplication with seen and sorting logic
intact (change only the loop that processes entries).
Sources/GhosttyTerminalView.swift (1)

623-634: Consider improving error visibility and theme name validation.

Two observations:

  1. Silent error swallowing: The empty catch {} makes it difficult to diagnose failures when the theme override doesn't apply. Consider at minimum logging the error in DEBUG builds.

  2. Theme name interpolation: The theme name is directly interpolated into the config content. If a malicious or malformed value is stored in UserDefaults (e.g., containing newlines), it could inject additional config directives.

♻️ Suggested improvements
 private func loadCmuxThemeOverrideIfNeeded(_ config: ghostty_config_t) {
     guard let themeName = TerminalThemeSettings.effectiveThemeName() else { return }
+    // Validate theme name doesn't contain characters that could break config format
+    guard !themeName.contains(where: { $0.isNewline || $0 == "\"" || $0 == "\\" }) else {
+        `#if` DEBUG
+        Self.initLog("loadCmuxThemeOverrideIfNeeded: invalid theme name characters")
+        `#endif`
+        return
+    }
     let tmpPath = "/tmp/cmux-theme-override-\(UUID().uuidString).conf"
     let content = "theme = \(themeName)\n"
     do {
         try content.write(toFile: tmpPath, atomically: true, encoding: .utf8)
         tmpPath.withCString { path in
             ghostty_config_load_file(config, path)
         }
         try? FileManager.default.removeItem(atPath: tmpPath)
-    } catch {}
+    } catch {
+        `#if` DEBUG
+        Self.initLog("loadCmuxThemeOverrideIfNeeded: failed to write temp config: \(error)")
+        `#endif`
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 623 - 634, The
loadCmuxThemeOverrideIfNeeded function silently swallows errors and injects
themeName without validation; update it to validate/sanitize
TerminalThemeSettings.effectiveThemeName() (e.g., reject or escape newlines and
other control characters) before composing content, and replace the empty catch
with a debug-only log that records the caught error and tmpPath (use `#if` DEBUG
and a logger/NSLog) while still attempting to remove the tmp file via
FileManager.default.removeItem(atPath:). Also ensure ghostty_config_load_file is
only called with the sanitized tmpPath/content.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@Sources/GhosttyConfig.swift`:
- Around line 412-449: In availableThemeNames, when iterating entries from
FileManager.contentsOfDirectory(atPath:), filter out non-directory entries so
only directories are treated as themes; for each entry build the full path
(e.g., dir + "/" + entry) and use FileManager.fileExists(atPath:isDirectory:) or
attributesOfItem to check isDirectory, skipping entries that are not directories
or are symbolic links to non-directories; keep the existing deduplication with
seen and sorting logic intact (change only the loop that processes entries).

In `@Sources/GhosttyTerminalView.swift`:
- Around line 623-634: The loadCmuxThemeOverrideIfNeeded function silently
swallows errors and injects themeName without validation; update it to
validate/sanitize TerminalThemeSettings.effectiveThemeName() (e.g., reject or
escape newlines and other control characters) before composing content, and
replace the empty catch with a debug-only log that records the caught error and
tmpPath (use `#if` DEBUG and a logger/NSLog) while still attempting to remove the
tmp file via FileManager.default.removeItem(atPath:). Also ensure
ghostty_config_load_file is only called with the sanitized tmpPath/content.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7143359 and 38c5c1d.

📒 Files selected for processing (3)
  • Sources/GhosttyConfig.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/cmuxApp.swift

@greptile-apps

greptile-apps Bot commented Feb 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds a "Terminal Theme" picker to Settings that overrides the user's Ghostty config theme setting.

  • Scans bundled themes, Ghostty.app resources, and user theme directories (~/.config/ghostty/themes)
  • Theme selection persists in UserDefaults and applies overrides in both Swift config parser (GhosttyConfig) and C API config (ghostty_config_t)
  • Changes take effect immediately via config cache invalidation and reload
  • Properly integrated with "Reset All Settings" functionality

Confidence Score: 5/5

  • Safe to merge - clean implementation with proper separation of concerns
  • Well-structured feature addition with dual config system support, proper state management, and immediate effect handling. Only minor style suggestion about directory filtering.
  • No files require special attention

Important Files Changed

Filename Overview
Sources/GhosttyConfig.swift Added theme override logic and theme discovery function; minor issue: availableThemeNames() doesn't filter directories from file list
Sources/GhosttyTerminalView.swift Added C API theme override using temporary config file; implementation is clean and follows existing patterns
Sources/cmuxApp.swift Added theme picker UI with proper state management, reset handling, and config reload on theme change

Last reviewed commit: 38c5c1d

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment thread Sources/GhosttyConfig.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/cmuxApp.swift`:
- Around line 2661-2665: The effectiveThemeName(defaults: UserDefaults =
.standard) function currently returns any non-empty string stored under
themeKey; change it to validate and sanitize the persisted value before treating
it as an override: read the string for themeKey, trim whitespace and control
characters, then verify it matches the set of allowed theme identifiers (or a
safe pattern such as /^[A-Za-z0-9-_]+$/) and only return it if valid; otherwise
return nil so stale/invalid/unsafe values are ignored. Ensure the validation
happens inside effectiveThemeName and uses themeKey to locate the stored value.

In `@Sources/GhosttyConfig.swift`:
- Around line 417-457: availableThemeNames currently omits XDG_DATA_DIRS so
themes present in those locations never show up; update availableThemeNames to
mirror themeSearchPaths by reading the XDG_DATA_DIRS environment variable,
splitting on ":" and for each non-empty entry appending the "ghostty/themes"
component (and expanding tildes), ensuring each resulting path is appended via
appendDir to preserve deduplication/ordering; reference the availableThemeNames
function and the existing themeSearchPaths behavior when implementing this
parity change.
- Around line 107-110: Guard the unconditional override by verifying the
override actually exists before assigning to config.theme: retrieve the optional
via TerminalThemeSettings.effectiveThemeName(), then only set config.theme =
override if that theme is present in your theme registry (e.g.
ThemeManager.shared.themeExists(named:), ThemeStore.hasTheme(_:), or similar
lookup in your theme list); otherwise leave config.theme untouched (or
explicitly fall back to a default) so a missing previously-selected theme
doesn't overwrite Ghostty's configured theme.

In `@Sources/GhosttyTerminalView.swift`:
- Around line 633-644: In loadCmuxThemeOverrideIfNeeded, validate/sanitize the
persisted theme string from TerminalThemeSettings.effectiveThemeName() before
constructing the override file: reject or normalize values containing newlines,
null bytes, or path/control characters (e.g., if themeName.contains("\n") ||
themeName.contains("\0") ...) and skip creating tmpPath if invalid; if valid,
escape or strictly whitelist allowed characters when building content = "theme =
\(themeName)\n"; also stop swallowing errors—surface or log failures from
writing, ghostty_config_load_file, or file removal (so callers can detect
issues) rather than leaving the catch empty.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 38c5c1d and e9f578d.

📒 Files selected for processing (3)
  • Sources/GhosttyConfig.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/cmuxApp.swift

Comment thread Sources/cmuxApp.swift
Comment thread Sources/GhosttyConfig.swift
Comment thread Sources/GhosttyConfig.swift
Comment thread Sources/GhosttyTerminalView.swift

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: appearance Themes, light and dark mode, window chrome area: settings Settings UI and the config file S3: minor Wrong behavior with a workaround

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants