Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -83198,6 +83198,40 @@
}
}
},
"settings.sidebarAppearance.matchTerminalBackground": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Match Terminal Background"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ターミナルの背景に合わせる"
}
}
}
},
"settings.sidebarAppearance.matchTerminalBackground.subtitle": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Use the same background color and transparency as the terminal."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ターミナルと同じ背景色と透明度を使用します。"
}
}
}
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"settings.sidebarAppearance.tintColorLight": {
"extractionState": "manual",
"localizations": {
Expand Down
136 changes: 114 additions & 22 deletions Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2424,6 +2424,7 @@ struct ContentView: View {
}

@AppStorage("sidebarBlendMode") private var sidebarBlendMode = SidebarBlendModeOption.withinWindow.rawValue
@AppStorage("sidebarMatchTerminalBackground") private var sidebarMatchTerminalBackground = false

// Background glass settings
@AppStorage("bgGlassTintHex") private var bgGlassTintHex = "#000000"
Expand Down Expand Up @@ -2601,7 +2602,11 @@ struct ContentView: View {

private var contentAndSidebarLayout: AnyView {
let layout: AnyView
if sidebarBlendMode == SidebarBlendModeOption.withinWindow.rawValue {
// When matching terminal background, use HStack so both sidebar and terminal
// sit directly on the window background with no intermediate layers.
let useWithinWindow = sidebarBlendMode == SidebarBlendModeOption.withinWindow.rawValue
&& !sidebarMatchTerminalBackground
if useWithinWindow {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// Overlay mode: terminal extends full width, sidebar on top
// This allows withinWindow blur to see the terminal content
layout = AnyView(
Expand Down Expand Up @@ -8648,6 +8653,9 @@ struct VerticalTabsSidebar: View {
.accessibilityIdentifier("Sidebar")
.ignoresSafeArea()
.background(SidebarBackdrop().ignoresSafeArea())
.overlay(alignment: .trailing) {
SidebarTrailingBorder()
}
.background(
WindowAccessor { window in
modifierKeyMonitor.setHostWindow(window)
Expand Down Expand Up @@ -13760,7 +13768,70 @@ private struct TitlebarLeadingInsetReader: NSViewRepresentable {
}
}

/// 1px trailing border on the sidebar, derived from the terminal chrome background
/// using the same logic as bonsplit's TabBarColors.nsColorSeparator:
/// dark bg → lighten RGB by 0.16 at 0.36 alpha; light bg → darken by 0.12 at 0.26 alpha.
private struct SidebarTrailingBorder: View {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
@AppStorage("sidebarMatchTerminalBackground") private var matchTerminalBackground = false
@State private var separatorColor: NSColor = chromeSeparatorColor()

var body: some View {
if matchTerminalBackground {
Rectangle()
.fill(Color(nsColor: separatorColor))
.frame(width: 1)
.ignoresSafeArea()
.onAppear {
separatorColor = Self.chromeSeparatorColor()
}
.onReceive(NotificationCenter.default.publisher(for: .ghosttyDefaultBackgroundDidChange)) { _ in
separatorColor = Self.chromeSeparatorColor()
}
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
}
}
Comment on lines +13771 to +13791

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.

P1 SidebarTrailingBorder border color doesn't update on Ghostty theme change

chromeSeparatorColor() is a static call that reads GhosttyBackgroundTheme.currentColor() at render time, but SidebarTrailingBorder has only one reactive dependency: @AppStorage("sidebarMatchTerminalBackground"). SwiftUI will not re-render this view when the Ghostty theme changes, so the border color will become stale after a theme switch.

Compare with SidebarBackdrop: it has @State private var terminalBackgroundColor and an .onReceive(NotificationCenter.default.publisher(for: .ghosttyDefaultBackgroundDidChange)) that mutates that state to force a re-render. SidebarTrailingBorder has no equivalent mechanism.

The test plan item "Change Ghostty themes, verify sidebar updates live" would verify the backdrop updates (because SidebarBackdrop is correctly wired) but would miss the stale border.

The fix is the same pattern used in SidebarBackdrop:

private struct SidebarTrailingBorder: View {
    @AppStorage("sidebarMatchTerminalBackground") private var matchTerminalBackground = false
    @State private var separatorColor: NSColor = chromeSeparatorColor()

    var body: some View {
        if matchTerminalBackground {
            Rectangle()
                .fill(Color(nsColor: separatorColor))
                .frame(width: 1)
                .ignoresSafeArea()
                .onReceive(
                    NotificationCenter.default.publisher(for: .ghosttyDefaultBackgroundDidChange)
                ) { _ in
                    separatorColor = Self.chromeSeparatorColor()
                }
        }
    }
    ...
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Already fixed in 315d670 - SidebarTrailingBorder already has @State separatorColor and .onReceive(.ghosttyDefaultBackgroundDidChange).

— Claude Code


/// Replicates bonsplit TabBarColors.nsColorSeparator derivation from chrome background.
private static func chromeSeparatorColor() -> NSColor {
let chrome = GhosttyBackgroundTheme.currentColor()
let srgb = chrome.usingColorSpace(.sRGB) ?? chrome
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
srgb.getRed(&r, green: &g, blue: &b, alpha: &a)
let luminance = 0.299 * r + 0.587 * g + 0.114 * b
let isLight = luminance > 0.5
let amount: CGFloat = isLight ? -0.12 : 0.16
let alpha: CGFloat = isLight ? 0.26 : 0.36
return NSColor(
red: min(1.0, max(0.0, r + amount)),
green: min(1.0, max(0.0, g + amount)),
blue: min(1.0, max(0.0, b + amount)),
alpha: alpha
)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// Sidebar background that uses the same technique as TitlebarLayerBackground:
/// fully opaque layer color + layer-level opacity. This matches how the terminal's
/// Metal surface composites its background.
private struct SidebarTerminalBackgroundView: NSViewRepresentable {
let backgroundColor: NSColor
let opacity: CGFloat

func makeNSView(context: Context) -> NSView {
let view = NSView()
view.wantsLayer = true
view.layer?.backgroundColor = backgroundColor.withAlphaComponent(1.0).cgColor
view.layer?.opacity = Float(opacity)
return view
}

func updateNSView(_ nsView: NSView, context: Context) {
nsView.layer?.backgroundColor = backgroundColor.withAlphaComponent(1.0).cgColor
nsView.layer?.opacity = Float(opacity)
}
}

private struct SidebarBackdrop: View {
@AppStorage("sidebarMatchTerminalBackground") private var matchTerminalBackground = false
@AppStorage("sidebarTintOpacity") private var sidebarTintOpacity = SidebarTintDefaults.opacity
@AppStorage("sidebarTintHex") private var sidebarTintHex = SidebarTintDefaults.hex
@AppStorage("sidebarTintHexLight") private var sidebarTintHexLight: String?
Expand All @@ -13771,8 +13842,28 @@ private struct SidebarBackdrop: View {
@AppStorage("sidebarCornerRadius") private var sidebarCornerRadius = 0.0
@AppStorage("sidebarBlurOpacity") private var sidebarBlurOpacity = 1.0
@Environment(\.colorScheme) private var colorScheme
@State private var terminalBackgroundColor: NSColor = GhosttyBackgroundTheme.currentColor()

var body: some View {
let cornerRadius = CGFloat(max(0, sidebarCornerRadius))

if matchTerminalBackground {
// The terminal area has two stacked semi-transparent layers (Bonsplit chrome +
// Ghostty Metal background). Compute the effective composited opacity to match.
let alpha = CGFloat(GhosttyApp.shared.defaultBackgroundOpacity)
let effective = alpha >= 0.999 ? alpha : 1.0 - pow(1.0 - alpha, 2)
return AnyView(
SidebarTerminalBackgroundView(
backgroundColor: GhosttyApp.shared.defaultBackgroundColor,
opacity: effective
)
.clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous))
.onReceive(NotificationCenter.default.publisher(for: .ghosttyDefaultBackgroundDidChange)) { _ in
terminalBackgroundColor = GhosttyBackgroundTheme.currentColor()
}
)
}

let materialOption = SidebarMaterialOption(rawValue: sidebarMaterial)
let blendingMode = SidebarBlendModeOption(rawValue: sidebarBlendMode)?.mode ?? .behindWindow
let state = SidebarStateOption(rawValue: sidebarState)?.state ?? .active
Expand All @@ -13785,33 +13876,34 @@ private struct SidebarBackdrop: View {
return sidebarTintHex
}()
let tintColor = (NSColor(hex: resolvedHex) ?? NSColor(hex: sidebarTintHex) ?? .black).withAlphaComponent(sidebarTintOpacity)
let cornerRadius = CGFloat(max(0, sidebarCornerRadius))
let useLiquidGlass = materialOption?.usesLiquidGlass ?? false
let useWindowLevelGlass = useLiquidGlass && blendingMode == .behindWindow

return ZStack {
if let material = materialOption?.material {
// When using liquidGlass + behindWindow, window handles glass + tint
// Sidebar is fully transparent
if !useWindowLevelGlass {
SidebarVisualEffectBackground(
material: material,
blendingMode: blendingMode,
state: state,
opacity: sidebarBlurOpacity,
tintColor: tintColor,
cornerRadius: cornerRadius,
preferLiquidGlass: useLiquidGlass
)
// Tint overlay for NSVisualEffectView fallback
if !useLiquidGlass {
Color(nsColor: tintColor)
return AnyView(
ZStack {
if let material = materialOption?.material {
// When using liquidGlass + behindWindow, window handles glass + tint
// Sidebar is fully transparent
if !useWindowLevelGlass {
SidebarVisualEffectBackground(
material: material,
blendingMode: blendingMode,
state: state,
opacity: sidebarBlurOpacity,
tintColor: tintColor,
cornerRadius: cornerRadius,
preferLiquidGlass: useLiquidGlass
)
// Tint overlay for NSVisualEffectView fallback
if !useLiquidGlass {
Color(nsColor: tintColor)
}
}
}
// When material is none or useWindowLevelGlass, render nothing
}
// When material is none or useWindowLevelGlass, render nothing
}
.clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous))
.clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous))
)
}
}

Expand Down
17 changes: 17 additions & 0 deletions Sources/cmuxApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2863,6 +2863,7 @@ private struct AboutPanelView: View {
}

private struct SidebarDebugView: View {
@AppStorage("sidebarMatchTerminalBackground") private var matchTerminalBackground = false
@AppStorage("sidebarPreset") private var sidebarPreset = SidebarPresetOption.nativeSidebar.rawValue
@AppStorage("sidebarTintOpacity") private var sidebarTintOpacity = SidebarTintDefaults.opacity
@AppStorage("sidebarTintHex") private var sidebarTintHex = SidebarTintDefaults.hex
Expand Down Expand Up @@ -2919,6 +2920,8 @@ private struct SidebarDebugView: View {
Text("Sidebar Appearance")
.font(.headline)

Toggle(String(localized: "settings.sidebarAppearance.matchTerminalBackground", defaultValue: "Match Terminal Background"), isOn: $matchTerminalBackground)

GroupBox("Presets") {
Picker("Preset", selection: $sidebarPreset) {
ForEach(SidebarPresetOption.allCases) { option in
Expand Down Expand Up @@ -3902,6 +3905,7 @@ struct SettingsView: View {
@AppStorage("sidebarTintHexLight") private var sidebarTintHexLight: String?
@AppStorage("sidebarTintHexDark") private var sidebarTintHexDark: String?
@AppStorage("sidebarTintOpacity") private var sidebarTintOpacity = SidebarTintDefaults.opacity
@AppStorage("sidebarMatchTerminalBackground") private var sidebarMatchTerminalBackground = false

@ObservedObject private var notificationStore = TerminalNotificationStore.shared
@State private var shortcutResetToken = UUID()
Expand Down Expand Up @@ -5030,6 +5034,18 @@ struct SettingsView: View {

SettingsSectionHeader(title: String(localized: "settings.section.sidebarAppearance", defaultValue: "Sidebar Appearance"))
SettingsCard {
SettingsCardRow(
String(localized: "settings.sidebarAppearance.matchTerminalBackground", defaultValue: "Match Terminal Background"),
subtitle: String(localized: "settings.sidebarAppearance.matchTerminalBackground.subtitle", defaultValue: "Use the same background color and transparency as the terminal.")
) {
Toggle("", isOn: $sidebarMatchTerminalBackground)
.labelsHidden()
.toggleStyle(.switch)
.controlSize(.small)
}

SettingsCardDivider()

SettingsCardRow(
String(localized: "settings.sidebarAppearance.tintColorLight", defaultValue: "Light Mode Tint"),
subtitle: String(localized: "settings.sidebarAppearance.tintColorLight.subtitle", defaultValue: "Sidebar tint color when using light appearance.")
Expand Down Expand Up @@ -5794,6 +5810,7 @@ struct SettingsView: View {
sidebarTintHexLight = nil
sidebarTintHexDark = nil
sidebarTintOpacity = SidebarTintDefaults.opacity
sidebarMatchTerminalBackground = false
showOpenAccessConfirmation = false
pendingOpenAccessMode = nil
socketPasswordDraft = ""
Expand Down
Loading