Skip to content

Add Ghostty terminal theme picker in Settings#699

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

Add Ghostty terminal theme picker in Settings#699
lcamargof wants to merge 1 commit intomanaflow-ai:mainfrom
lcamargof:main

Conversation

@lcamargof
Copy link

@lcamargof lcamargof commented Feb 28, 2026

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 to Settings with a dedicated "Terminal" section
    • Users can select from available themes via a new theme picker
    • Theme selection persists and applies automatically across app sessions
    • Enhanced theme configuration system with proper precedence handling and immediate reload on theme changes
    • Available themes are automatically discovered from system directories
@vercel
Copy link

vercel bot commented Feb 28, 2026

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

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

📝 Walkthrough

Walkthrough

This pull request implements terminal theme selection for Ghostty by adding theme enumeration, CMUX override loading, and a Settings UI picker. A new availableThemeNames() function scans theme directories, while loadCmuxThemeOverrideIfNeeded() applies selected themes via temporary config files. The Settings interface now includes theme selection with cache invalidation on changes.

Changes

Cohort / File(s) Summary
Theme Enumeration
Sources/GhosttyConfig.swift
Adds availableThemeNames() static method that scans multiple theme directories, deduplicates, and returns sorted theme names with configurable environment and bundle resource URL inputs.
CMUX Theme Override Loading
Sources/GhosttyTerminalView.swift
Introduces loadCmuxThemeOverrideIfNeeded() helper that creates a temporary config file with the selected theme and loads it via C API (ghostty_config_load_file), invoked during default config initialization.
Settings UI and State Management
Sources/cmuxApp.swift
Adds TerminalThemeSettings helper with theme key resolution, new Settings section with theme picker wired to availableThemes, cache invalidation on theme changes, and new configuration enums (QuitWarningSettings, CommandPaletteRenameSelectionSettings, ClaudeCodeIntegrationSettings, TelemetrySettings).

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Settings as Settings UI
    participant Config as GhosttyConfig
    participant App as GhosttyApp
    participant C as Ghostty C API
    
    User->>Settings: Select theme in picker
    Settings->>Settings: Update terminalTheme (AppStorage)
    Settings->>Config: Invalidate load cache
    Settings->>App: Reload configuration
    App->>Config: Call availableThemeNames()
    Config-->>App: Return theme list
    App->>App: loadCmuxThemeOverrideIfNeeded()
    App->>App: Create temp config file<br/>(theme = selected_name)
    App->>C: ghostty_config_load_file(temp_path)
    C-->>App: Config loaded
    App->>App: Delete temp file
    App-->>User: Theme applied
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Whiskers twitch with joy today,
Themes now bloom in every way!
From config scans to Settings' grace,
Colors paint each terminal space!

🚥 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 accurately describes the main change: introducing a terminal theme picker UI component in the Settings section.

✏️ 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

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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

Copy link

@coderabbitai coderabbitai bot left a comment

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
Copy link

greptile-apps bot commented Feb 28, 2026

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

Copy link

@greptile-apps greptile-apps bot left a comment

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 on lines +440 to +444
for entry in entries {
guard !entry.hasPrefix("."), !seen.contains(entry) else { continue }
seen.insert(entry)
names.append(entry)
}
Copy link

Choose a reason for hiding this comment

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

Directory entries could appear in the theme picker since contentsOfDirectory returns both files and directories. Consider filtering to only include files:

Suggested change
for entry in entries {
guard !entry.hasPrefix("."), !seen.contains(entry) else { continue }
seen.insert(entry)
names.append(entry)
}
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)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant