feat: auto-match sidebar background to Ghostty terminal theme#2254
feat: auto-match sidebar background to Ghostty terminal theme#2254christi4nity wants to merge 2 commits intomanaflow-ai:mainfrom
Conversation
Add a "Sidebar Theme" setting (Match Ghostty / Custom) that controls whether the sidebar follows the terminal's Ghostty background or uses custom tint settings. When set to "Match Ghostty" (the default for new installs), SidebarBackdrop reads GhosttyBackgroundTheme.currentColor() directly and renders a solid color via a CALayer-backed NSView, bypassing the glass material that distorts theme colors. Live theme changes are tracked through .ghosttyDefaultBackgroundDidChange and .ghosttyConfigDidReload notifications. Existing users who have customized their sidebar appearance are automatically inferred as "Custom" mode via SidebarThemeSettings so their settings are preserved on upgrade.
|
@christi4nity is attempting to deploy a commit to the Manaflow Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughAdds a sidebar theme selection that persists a mode (match Ghostty or custom), derives sidebar colors from GhosttyConfig or user tint, renders solid-color backgrounds when needed, and adds localization, settings UI, app init, and tests for the new behavior. Notification hooks invalidate cached Ghostty data. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant SettingsView as Settings View
participant AppStorage as `@AppStorage`
participant SidebarBackdrop
participant GhosttyConfig
participant Renderer as Sidebar Renderer
User->>SettingsView: Select sidebar theme
SettingsView->>AppStorage: Write sidebarTheme
AppStorage->>SidebarBackdrop: State update
SidebarBackdrop->>SidebarBackdrop: Resolve selectedSidebarTheme
alt matchGhostty
SidebarBackdrop->>GhosttyConfig: Load config (cached)
GhosttyConfig-->>SidebarBackdrop: backgroundColor, opacity
SidebarBackdrop->>SidebarBackdrop: Compute tint/background
else custom
SidebarBackdrop->>SidebarBackdrop: Use user tint & opacity
end
SidebarBackdrop->>Renderer: Render (SidebarSolidColorBackground or material)
Renderer-->>User: Display updated sidebar
Note over SidebarBackdrop,GhosttyConfig: Ghostty notifications -> invalidate cache -> SidebarBackdrop refresh
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR adds a Sidebar Theme setting that lets the sidebar automatically match the Ghostty terminal's current background colour, bypassing the glass material layer that distorts theme colours. New installs default to "Match Ghostty" mode; existing users with customised sidebar appearance are auto-detected as "Custom" so their settings are preserved.\n\n- P1 – tint overlay defeats the colour match: In "Match Ghostty" mode the code unconditionally layers Confidence Score: 4/5The P1 tint overlay bug prevents Match Ghostty from producing an exact colour match for all default users and should be fixed before merging. One P1 logic bug: Match Ghostty mode unconditionally overlays the 18% black custom tint over GhosttyBackgroundTheme.currentColor(), defeating the feature's stated goal. All other changes are clean and additive. Score becomes 5 once the tint overlay is gated to Custom mode only. Sources/ContentView.swift — the if let baseColor rendering branch inside SidebarBackdrop.body Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[SidebarBackdrop.body] --> B{selectedSidebarTheme}
B -->|matchGhostty| C{hasExplicitSidebarBackground?}
B -->|custom| D[baseColor = nil, materialOption = user pref]
C -->|yes| D
C -->|no| E[baseColor = GhosttyBackgroundTheme.currentColor]
E --> F{if let baseColor}
F -->|yes| G[Render SidebarSolidColorBackground baseColor]
G --> H{tintColor.alpha > 0.0001?}
H -->|yes — P1: always true at default 0.18| I[Overlay tintColor on baseColor]
H -->|no| J[Done]
I --> J
D --> K{materialOption?.material?}
K -->|some| L{useWindowLevelGlass?}
L -->|no| M[SidebarVisualEffectBackground + tint]
L -->|yes| N[Fully transparent]
K -->|nil| O[SidebarSolidColorBackground tintColor]
Reviews (1): Last reviewed commit: "feat: auto-match sidebar background to G..." | Re-trigger Greptile |
| if let baseColor { | ||
| SidebarSolidColorBackground(color: baseColor) | ||
| if tintColor.alphaComponent > 0.0001 { | ||
| SidebarSolidColorBackground(color: tintColor) | ||
| } |
There was a problem hiding this comment.
"Match Ghostty" mode still applies the user's tint on top of the background
In the if let baseColor branch (active when selectedSidebarTheme == .matchGhostty and there is no explicit sidebar-background key), tintColor falls through to customTintColor, which defaults to #000000 at 18% opacity (SidebarTintDefaults.opacity = 0.18). This overlay is unconditionally rendered whenever tintColor.alphaComponent > 0.0001 — which is always true for any fresh install.
The result is that the sidebar is always 18% darker than the actual Ghostty terminal background, so "Match Ghostty" does not produce a pixel-accurate match for new users.
The tint overlay should be gated on selectedSidebarTheme == .custom:
if let baseColor {
SidebarSolidColorBackground(color: baseColor)
if selectedSidebarTheme == .custom, tintColor.alphaComponent > 0.0001 {
SidebarSolidColorBackground(color: tintColor)
}
} else if let material = materialOption?.material {| @AppStorage("sidebarCornerRadius") private var sidebarCornerRadius = 0.0 | ||
| @AppStorage("sidebarBlurOpacity") private var sidebarBlurOpacity = 1.0 | ||
| @Environment(\.colorScheme) private var colorScheme | ||
| @State private var refreshGeneration = 0 |
There was a problem hiding this comment.
refreshGeneration is never read in body
refreshGeneration is incremented in both .onReceive handlers to force a re-render when the Ghostty background changes, but it is never accessed inside the body computed property. Today this still triggers a re-render because any @State mutation invalidates a SwiftUI view regardless of body access, but the intent is entirely implicit.
Add let _ = refreshGeneration at the top of body to make the re-render dependency explicit and linter-clean.
Sources/ContentView.swift
Outdated
| /// Renders a solid color using the same compositing path as the terminal's | ||
| /// CAMetalLayer (bgra8Unorm / sRGB blending). Uses a display link callback | ||
| /// to re-apply layer.backgroundColor after makeViewHierarchyTransparent |
There was a problem hiding this comment.
Docstring mentions a display link that does not exist
The doc comment says "Uses a display link callback to re-apply layer.backgroundColor…" but the implementation uses DispatchQueue.main.async inside viewDidMoveToWindow, not a display link.
| /// Renders a solid color using the same compositing path as the terminal's | |
| /// CAMetalLayer (bgra8Unorm / sRGB blending). Uses a display link callback | |
| /// to re-apply layer.backgroundColor after makeViewHierarchyTransparent | |
| /// Renders a solid color using the same compositing path as the terminal's | |
| /// CAMetalLayer (bgra8Unorm / sRGB blending). Uses `viewDidMoveToWindow` with a | |
| /// deferred main-queue dispatch to re-apply `layer.backgroundColor` after | |
| /// `makeViewHierarchyTransparent` clears it on transparent windows. | |
| private struct SidebarSolidColorBackground: NSViewRepresentable { |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
Sources/cmuxApp.swift (2)
5629-5698:⚠️ Potential issue | 🟠 Major
Reset All Settingsstill leaves stale custom sidebar appearance behind.Line 5694 resets the mode and tint keys, but
sidebarMaterial,sidebarBlendMode,sidebarState,sidebarBlurOpacity,sidebarCornerRadius, andsidebarPresetsurvive. If the user later switches back toCustom, the old appearance comes back instead of a clean reset.🧹 Possible fix
sidebarTheme = SidebarThemeSettings.defaultMode.rawValue sidebarTintHex = SidebarTintDefaults.hex sidebarTintHexLight = nil sidebarTintHexDark = nil sidebarTintOpacity = SidebarTintDefaults.opacity + defaults.removeObject(forKey: "sidebarPreset") + defaults.removeObject(forKey: "sidebarMaterial") + defaults.removeObject(forKey: "sidebarBlendMode") + defaults.removeObject(forKey: "sidebarState") + defaults.removeObject(forKey: "sidebarBlurOpacity") + defaults.removeObject(forKey: "sidebarCornerRadius")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/cmuxApp.swift` around lines 5629 - 5698, The resetAllSettings() routine doesn't clear several custom sidebar appearance properties so old custom values reappear; update resetAllSettings() to explicitly reset sidebarMaterial, sidebarBlendMode, sidebarState, sidebarBlurOpacity, sidebarCornerRadius, and sidebarPreset to their respective defaults (or remove their UserDefaults keys) alongside the existing sidebarTheme/sidebarTint resets so a switch back to "Custom" starts from a clean state; locate these symbols in cmuxApp.swift and set them to SidebarAppearanceSettings.default... (or call the appropriate SidebarAppearanceSettings.reset/remove methods) within resetAllSettings().
4920-5008:⚠️ Potential issue | 🟡 MinorDisable the custom sidebar controls when the theme is not
Custom.With
Match Ghosttyselected, the Light/Dark Tint, Tint Opacity, and Reset rows still look editable even though the sidebar ignores them. That makes the setting read as broken, and the same no-op UX now exists inSidebarDebugViewon fresh installs.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/cmuxApp.swift` around lines 4920 - 5008, The Light/Dark tint controls, Tint Opacity slider and Reset button should be disabled when the sidebar theme is not Custom; wrap or mark the ColorPicker HStacks, the Slider HStack, and the Reset Button with .disabled(sidebarTheme != SidebarThemeOption.custom.rawValue) (or compare to SidebarThemeOption.custom if sidebarTheme is the enum) so the UI reflects they are inert; update the blocks containing settingsSidebarTintLightBinding, settingsSidebarTintDarkBinding, sidebarTintOpacity, and the Reset button that sets sidebarTintHexLight/sidebarTintHexDark to nil to use that same .disabled(...) condition (also consider graying via .opacity if desired) and mirror this behavior in SidebarDebugView if it renders the same controls.
🤖 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/ContentView.swift`:
- Around line 13752-13787: The sidebar tint/background selection is not
enforcing mutual exclusivity between selectedSidebarTheme (.matchGhostty vs
.custom): update the logic around resolvedCustomHex/customTintColor,
explicitSidebarBackground/hasExplicitSidebarBackground, tintColor, baseColor and
materialOption so that when selectedSidebarTheme == .matchGhostty you always use
GhosttyBackgroundTheme/current explicitSidebarBackground (ignore customTintColor
and saved sidebarTintOpacity/sidebarMaterial), and when selectedSidebarTheme ==
.custom you always use customTintColor and saved sidebarMaterial (ignore
ghosttyConfig.sidebarBackground); concretely, change the guard in the tintColor
closure to also check selectedSidebarTheme != .matchGhostty, ensure
hasExplicitSidebarBackground is computed to be false when selectedSidebarTheme
== .custom, and make baseColor/materialOption branches respect
selectedSidebarTheme so the two modes cannot fall back to the other.
- Around line 13829-13832: The .onReceive subscriber for
NotificationCenter.default.publisher(for: .ghosttyConfigDidReload) should
explicitly receive on the main runloop to ensure safe `@State` mutation; update
the chain that currently calls GhosttyConfig.invalidateLoadCache() and
refreshGeneration &+= 1 so that the publisher is scheduled with .receive(on:
RunLoop.main) (before the .onReceive) or otherwise ensure the handler runs on
the main thread when processing .ghosttyConfigDidReload notifications.
---
Outside diff comments:
In `@Sources/cmuxApp.swift`:
- Around line 5629-5698: The resetAllSettings() routine doesn't clear several
custom sidebar appearance properties so old custom values reappear; update
resetAllSettings() to explicitly reset sidebarMaterial, sidebarBlendMode,
sidebarState, sidebarBlurOpacity, sidebarCornerRadius, and sidebarPreset to
their respective defaults (or remove their UserDefaults keys) alongside the
existing sidebarTheme/sidebarTint resets so a switch back to "Custom" starts
from a clean state; locate these symbols in cmuxApp.swift and set them to
SidebarAppearanceSettings.default... (or call the appropriate
SidebarAppearanceSettings.reset/remove methods) within resetAllSettings().
- Around line 4920-5008: The Light/Dark tint controls, Tint Opacity slider and
Reset button should be disabled when the sidebar theme is not Custom; wrap or
mark the ColorPicker HStacks, the Slider HStack, and the Reset Button with
.disabled(sidebarTheme != SidebarThemeOption.custom.rawValue) (or compare to
SidebarThemeOption.custom if sidebarTheme is the enum) so the UI reflects they
are inert; update the blocks containing settingsSidebarTintLightBinding,
settingsSidebarTintDarkBinding, sidebarTintOpacity, and the Reset button that
sets sidebarTintHexLight/sidebarTintHexDark to nil to use that same
.disabled(...) condition (also consider graying via .opacity if desired) and
mirror this behavior in SidebarDebugView if it renders the same controls.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3cf81274-fc4a-4863-928f-04bd12ac4478
📒 Files selected for processing (4)
Resources/Localizable.xcstringsSources/ContentView.swiftSources/cmuxApp.swiftcmuxTests/GhosttyConfigTests.swift
- Make Match Ghostty / Custom modes mutually exclusive: tint overlay no longer applies in Match Ghostty mode, and explicit sidebar-background config is ignored in Custom mode - Add explicit refreshGeneration read in body for linter clarity - Add .receive(on: RunLoop.main) to ghosttyConfigDidReload subscriber - Fix stale docstring referencing display link
There was a problem hiding this comment.
♻️ Duplicate comments (1)
Sources/ContentView.swift (1)
13773-13789:⚠️ Potential issue | 🟠 MajorKeep
matchGhosttyindependent from saved custom material/tint state.When
explicitSidebarBackgroundis present, this path still falls back tosidebarTintOpacityandsidebarMaterial. That means switching back from Custom to Match Ghostty can keep old glass/tint behavior alive and distort the Ghostty-providedsidebar-backgroundinstead of rendering the Ghostty-controlled solid color.Suggested fix
- let hasExplicitSidebarBackground = ghosttyConfig.rawSidebarBackground != nil && explicitSidebarBackground != nil - let tintColor: NSColor = { - guard usesGhosttyTheme else { return customTintColor } - guard let explicitSidebarBackground else { return .clear } - let resolvedOpacity = ghosttyConfig.sidebarTintOpacity ?? sidebarTintOpacity - return explicitSidebarBackground.withAlphaComponent(resolvedOpacity) - }() - let baseColor: NSColor? = - hasExplicitSidebarBackground || selectedSidebarTheme == .custom - ? nil - : GhosttyBackgroundTheme.currentColor() - let materialOption: SidebarMaterialOption? = { - if hasExplicitSidebarBackground || selectedSidebarTheme == .custom { - return SidebarMaterialOption(rawValue: sidebarMaterial) - } - return SidebarMaterialOption.none - }() + let tintColor: NSColor = usesGhosttyTheme ? .clear : customTintColor + let baseColor: NSColor? = { + guard usesGhosttyTheme else { return nil } + if let explicitSidebarBackground { + let opacity = ghosttyConfig.sidebarTintOpacity ?? explicitSidebarBackground.alphaComponent + return explicitSidebarBackground.withAlphaComponent(opacity) + } + return GhosttyBackgroundTheme.currentColor() + }() + let materialOption: SidebarMaterialOption? = + usesGhosttyTheme ? .none : SidebarMaterialOption(rawValue: sidebarMaterial)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/ContentView.swift` around lines 13773 - 13789, When usesGhosttyTheme is true, don't fall back to saved custom state—change the tint and material logic so ghosttyConfig controls opacity and material exclusively: in the tintColor closure (usesGhosttyTheme branch) stop using sidebarTintOpacity as a fallback and use ghosttyConfig.sidebarTintOpacity (or a sensible default like 1.0) instead of sidebarTintOpacity, and in the materialOption closure ensure that when usesGhosttyTheme is true you return .none (ignore sidebarMaterial) so saved SidebarMaterialOption(rawValue: sidebarMaterial) is not applied; update references to usesGhosttyTheme, ghosttyConfig.sidebarTintOpacity, explicitSidebarBackground, sidebarTintOpacity, sidebarMaterial, and SidebarMaterialOption to implement this.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@Sources/ContentView.swift`:
- Around line 13773-13789: When usesGhosttyTheme is true, don't fall back to
saved custom state—change the tint and material logic so ghosttyConfig controls
opacity and material exclusively: in the tintColor closure (usesGhosttyTheme
branch) stop using sidebarTintOpacity as a fallback and use
ghosttyConfig.sidebarTintOpacity (or a sensible default like 1.0) instead of
sidebarTintOpacity, and in the materialOption closure ensure that when
usesGhosttyTheme is true you return .none (ignore sidebarMaterial) so saved
SidebarMaterialOption(rawValue: sidebarMaterial) is not applied; update
references to usesGhosttyTheme, ghosttyConfig.sidebarTintOpacity,
explicitSidebarBackground, sidebarTintOpacity, sidebarMaterial, and
SidebarMaterialOption to implement this.
|
Superseded by #2293 — the team shipped their own implementation. Nice to see the same feature land! 🎉 |
Summary
GhosttyBackgroundTheme.currentColor()directly and renders a solid color via a CALayer-backed NSView — bypassing the glass material that distorts theme colors.ghosttyDefaultBackgroundDidChangeand.ghosttyConfigDidReloadnotificationsSidebarThemeSettings.inferredMode()so their settings are preserved on upgradeTest plan
sidebar-backgroundin Ghostty config still worksxcodebuild -scheme cmux-unit)🤖 Generated with Claude Code
Summary by cubic
Adds a “Sidebar Theme” setting that lets the sidebar auto-match the Ghostty terminal background for accurate colors and live updates. New installs default to “Match Ghostty”; existing customized sidebars stay unchanged.
sidebar-backgroundapplies only in Match Ghostty, while Custom ignores it and restores tint/material controls.Written for commit 00b7834. Summary will update on new commits.
Summary by CodeRabbit
New Features
Bug Fixes
Tests