Add configurable sidebar tint color with light/dark mode support (#1465)
- Config: sidebar-background supports plain hex (#336699) or light/dark syntax (light:#fbf3db,dark:#103c48) - Config: sidebar-tint-opacity overrides tint opacity - Settings UI: per-scheme color pickers, opacity slider (0-70%), reset - SidebarBackdrop resolves light/dark hex based on @Environment colorScheme - applySidebarAppearanceToUserDefaults guards on rawSidebarBackground presence so UI picks survive appearance toggles when no config is set - Stale light/dark UserDefaults keys cleared when config switches from dual-mode to single or sidebar-background is removed - applyPreset() and Reset Tint clear per-scheme overrides - Debug snapshot (combinedPayload + copySidebarConfig) includes new keys - ColorPicker labels use String(localized:) per localization policy - Opacity slider capped at 0.7 to match debug view vibrancy constraint Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9bb2816e05
commit
a7cb968a55
6 changed files with 1826 additions and 6 deletions
|
|
@ -5,6 +5,7 @@ All notable changes to cmux are documented here.
|
|||
## [0.62.2] - 2026-03-14
|
||||
|
||||
### Added
|
||||
- Configurable sidebar tint color with separate light/dark mode support via Settings and config file (`sidebar-background`, `sidebar-tint-opacity`) ([#1465](https://github.com/manaflow-ai/cmux/pull/1465))
|
||||
- Cmd+P all-surfaces search option ([#1382](https://github.com/manaflow-ai/cmux/pull/1382))
|
||||
- `cmux themes` command with bundled Ghostty themes ([#1334](https://github.com/manaflow-ai/cmux/pull/1334), [#1314](https://github.com/manaflow-ai/cmux/pull/1314))
|
||||
- Sidebar can now shrink to smaller widths ([#1420](https://github.com/manaflow-ai/cmux/pull/1420))
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -12559,19 +12559,30 @@ private struct TitlebarLeadingInsetReader: NSViewRepresentable {
|
|||
}
|
||||
|
||||
private struct SidebarBackdrop: View {
|
||||
@AppStorage("sidebarTintOpacity") private var sidebarTintOpacity = 0.18
|
||||
@AppStorage("sidebarTintHex") private var sidebarTintHex = "#000000"
|
||||
@AppStorage("sidebarTintOpacity") private var sidebarTintOpacity = SidebarTintDefaults.opacity
|
||||
@AppStorage("sidebarTintHex") private var sidebarTintHex = SidebarTintDefaults.hex
|
||||
@AppStorage("sidebarTintHexLight") private var sidebarTintHexLight: String?
|
||||
@AppStorage("sidebarTintHexDark") private var sidebarTintHexDark: String?
|
||||
@AppStorage("sidebarMaterial") private var sidebarMaterial = SidebarMaterialOption.sidebar.rawValue
|
||||
@AppStorage("sidebarBlendMode") private var sidebarBlendMode = SidebarBlendModeOption.withinWindow.rawValue
|
||||
@AppStorage("sidebarState") private var sidebarState = SidebarStateOption.followWindow.rawValue
|
||||
@AppStorage("sidebarCornerRadius") private var sidebarCornerRadius = 0.0
|
||||
@AppStorage("sidebarBlurOpacity") private var sidebarBlurOpacity = 1.0
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
var body: some View {
|
||||
let materialOption = SidebarMaterialOption(rawValue: sidebarMaterial)
|
||||
let blendingMode = SidebarBlendModeOption(rawValue: sidebarBlendMode)?.mode ?? .behindWindow
|
||||
let state = SidebarStateOption(rawValue: sidebarState)?.state ?? .active
|
||||
let tintColor = (NSColor(hex: sidebarTintHex) ?? .black).withAlphaComponent(sidebarTintOpacity)
|
||||
let resolvedHex: String = {
|
||||
if colorScheme == .dark, let dark = sidebarTintHexDark {
|
||||
return dark
|
||||
} else if colorScheme == .light, let light = sidebarTintHexLight {
|
||||
return light
|
||||
}
|
||||
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
|
||||
|
|
@ -12706,6 +12717,11 @@ enum SidebarStateOption: String, CaseIterable, Identifiable {
|
|||
}
|
||||
}
|
||||
|
||||
enum SidebarTintDefaults {
|
||||
static let hex = "#000000"
|
||||
static let opacity = 0.18
|
||||
}
|
||||
|
||||
enum SidebarPresetOption: String, CaseIterable, Identifiable {
|
||||
case nativeSidebar
|
||||
case glassBehind
|
||||
|
|
|
|||
|
|
@ -29,6 +29,13 @@ struct GhosttyConfig {
|
|||
var selectionBackground: NSColor = NSColor(hex: "#57584f")!
|
||||
var selectionForeground: NSColor = NSColor(hex: "#fdfff1")!
|
||||
|
||||
// Sidebar appearance
|
||||
var rawSidebarBackground: String?
|
||||
var sidebarBackground: NSColor?
|
||||
var sidebarBackgroundLight: NSColor?
|
||||
var sidebarBackgroundDark: NSColor?
|
||||
var sidebarTintOpacity: Double?
|
||||
|
||||
// Palette colors (0-15)
|
||||
var palette: [Int: NSColor] = [:]
|
||||
|
||||
|
|
@ -134,6 +141,54 @@ struct GhosttyConfig {
|
|||
return []
|
||||
}
|
||||
|
||||
mutating func resolveSidebarBackground(preferredColorScheme: ColorSchemePreference) {
|
||||
guard let raw = rawSidebarBackground else { return }
|
||||
|
||||
let lightResolved = Self.resolveThemeName(from: raw, preferredColorScheme: .light)
|
||||
let darkResolved = Self.resolveThemeName(from: raw, preferredColorScheme: .dark)
|
||||
let hasDualMode = lightResolved != darkResolved
|
||||
|
||||
if hasDualMode {
|
||||
sidebarBackgroundLight = NSColor(hex: lightResolved)
|
||||
sidebarBackgroundDark = NSColor(hex: darkResolved)
|
||||
}
|
||||
|
||||
let resolved = Self.resolveThemeName(from: raw, preferredColorScheme: preferredColorScheme)
|
||||
if let color = NSColor(hex: resolved) {
|
||||
sidebarBackground = color
|
||||
}
|
||||
}
|
||||
|
||||
func applySidebarAppearanceToUserDefaults() {
|
||||
guard rawSidebarBackground != nil else {
|
||||
if let opacity = sidebarTintOpacity {
|
||||
UserDefaults.standard.set(opacity, forKey: "sidebarTintOpacity")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let defaults = UserDefaults.standard
|
||||
|
||||
if let light = sidebarBackgroundLight {
|
||||
defaults.set(light.hexString(), forKey: "sidebarTintHexLight")
|
||||
} else {
|
||||
defaults.removeObject(forKey: "sidebarTintHexLight")
|
||||
}
|
||||
if let dark = sidebarBackgroundDark {
|
||||
defaults.set(dark.hexString(), forKey: "sidebarTintHexDark")
|
||||
} else {
|
||||
defaults.removeObject(forKey: "sidebarTintHexDark")
|
||||
}
|
||||
if let color = sidebarBackground {
|
||||
defaults.set(color.hexString(), forKey: "sidebarTintHex")
|
||||
} else {
|
||||
defaults.removeObject(forKey: "sidebarTintHex")
|
||||
}
|
||||
if let opacity = sidebarTintOpacity {
|
||||
defaults.set(opacity, forKey: "sidebarTintOpacity")
|
||||
}
|
||||
}
|
||||
|
||||
private static func loadFromDisk(preferredColorScheme: ColorSchemePreference) -> GhosttyConfig {
|
||||
var config = GhosttyConfig()
|
||||
|
||||
|
|
@ -161,6 +216,9 @@ struct GhosttyConfig {
|
|||
)
|
||||
}
|
||||
|
||||
config.resolveSidebarBackground(preferredColorScheme: preferredColorScheme)
|
||||
config.applySidebarAppearanceToUserDefaults()
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
|
|
@ -240,6 +298,12 @@ struct GhosttyConfig {
|
|||
if let color = NSColor(hex: value) {
|
||||
splitDividerColor = color
|
||||
}
|
||||
case "sidebar-background":
|
||||
rawSidebarBackground = value
|
||||
case "sidebar-tint-opacity":
|
||||
if let opacity = Double(value) {
|
||||
sidebarTintOpacity = min(max(opacity, 0), 1)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1526,6 +1526,8 @@ private enum DebugWindowConfigSnapshot {
|
|||
sidebarState=\(stringValue(defaults, key: "sidebarState", fallback: SidebarStateOption.followWindow.rawValue))
|
||||
sidebarBlurOpacity=\(String(format: "%.2f", doubleValue(defaults, key: "sidebarBlurOpacity", fallback: 1.0)))
|
||||
sidebarTintHex=\(stringValue(defaults, key: "sidebarTintHex", fallback: "#000000"))
|
||||
sidebarTintHexLight=\(stringValue(defaults, key: "sidebarTintHexLight", fallback: "(nil)"))
|
||||
sidebarTintHexDark=\(stringValue(defaults, key: "sidebarTintHexDark", fallback: "(nil)"))
|
||||
sidebarTintOpacity=\(String(format: "%.2f", doubleValue(defaults, key: "sidebarTintOpacity", fallback: 0.18)))
|
||||
sidebarCornerRadius=\(String(format: "%.1f", doubleValue(defaults, key: "sidebarCornerRadius", fallback: 0.0)))
|
||||
sidebarBranchVerticalLayout=\(boolValue(defaults, key: SidebarBranchLayoutSettings.key, fallback: SidebarBranchLayoutSettings.defaultVerticalLayout))
|
||||
|
|
@ -2153,8 +2155,10 @@ private struct AboutPanelView: View {
|
|||
|
||||
private struct SidebarDebugView: View {
|
||||
@AppStorage("sidebarPreset") private var sidebarPreset = SidebarPresetOption.nativeSidebar.rawValue
|
||||
@AppStorage("sidebarTintOpacity") private var sidebarTintOpacity = 0.18
|
||||
@AppStorage("sidebarTintHex") private var sidebarTintHex = "#000000"
|
||||
@AppStorage("sidebarTintOpacity") private var sidebarTintOpacity = SidebarTintDefaults.opacity
|
||||
@AppStorage("sidebarTintHex") private var sidebarTintHex = SidebarTintDefaults.hex
|
||||
@AppStorage("sidebarTintHexLight") private var sidebarTintHexLight: String?
|
||||
@AppStorage("sidebarTintHexDark") private var sidebarTintHexDark: String?
|
||||
@AppStorage("sidebarMaterial") private var sidebarMaterial = SidebarMaterialOption.sidebar.rawValue
|
||||
@AppStorage("sidebarBlendMode") private var sidebarBlendMode = SidebarBlendModeOption.withinWindow.rawValue
|
||||
@AppStorage("sidebarState") private var sidebarState = SidebarStateOption.followWindow.rawValue
|
||||
|
|
@ -2308,7 +2312,9 @@ private struct SidebarDebugView: View {
|
|||
HStack(spacing: 12) {
|
||||
Button("Reset Tint") {
|
||||
sidebarTintOpacity = 0.62
|
||||
sidebarTintHex = "#000000"
|
||||
sidebarTintHex = SidebarTintDefaults.hex
|
||||
sidebarTintHexLight = nil
|
||||
sidebarTintHexDark = nil
|
||||
}
|
||||
Button("Reset Blur") {
|
||||
sidebarMaterial = SidebarMaterialOption.hudWindow.rawValue
|
||||
|
|
@ -2389,6 +2395,8 @@ private struct SidebarDebugView: View {
|
|||
sidebarState=\(sidebarState)
|
||||
sidebarBlurOpacity=\(String(format: "%.2f", sidebarBlurOpacity))
|
||||
sidebarTintHex=\(sidebarTintHex)
|
||||
sidebarTintHexLight=\(sidebarTintHexLight ?? "(nil)")
|
||||
sidebarTintHexDark=\(sidebarTintHexDark ?? "(nil)")
|
||||
sidebarTintOpacity=\(String(format: "%.2f", sidebarTintOpacity))
|
||||
sidebarCornerRadius=\(String(format: "%.1f", sidebarCornerRadius))
|
||||
sidebarBranchVerticalLayout=\(sidebarBranchVerticalLayout)
|
||||
|
|
@ -2416,6 +2424,8 @@ private struct SidebarDebugView: View {
|
|||
sidebarTintOpacity = preset.tintOpacity
|
||||
sidebarCornerRadius = preset.cornerRadius
|
||||
sidebarBlurOpacity = preset.blurOpacity
|
||||
sidebarTintHexLight = nil
|
||||
sidebarTintHexDark = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3108,6 +3118,10 @@ struct SettingsView: View {
|
|||
@AppStorage("sidebarShowLog") private var sidebarShowLog = true
|
||||
@AppStorage("sidebarShowProgress") private var sidebarShowProgress = true
|
||||
@AppStorage("sidebarShowStatusPills") private var sidebarShowMetadata = true
|
||||
@AppStorage("sidebarTintHex") private var sidebarTintHex = SidebarTintDefaults.hex
|
||||
@AppStorage("sidebarTintHexLight") private var sidebarTintHexLight: String?
|
||||
@AppStorage("sidebarTintHexDark") private var sidebarTintHexDark: String?
|
||||
@AppStorage("sidebarTintOpacity") private var sidebarTintOpacity = SidebarTintDefaults.opacity
|
||||
@ObservedObject private var notificationStore = TerminalNotificationStore.shared
|
||||
@State private var shortcutResetToken = UUID()
|
||||
@State private var topBlurOpacity: Double = 0
|
||||
|
|
@ -3182,6 +3196,30 @@ struct SettingsView: View {
|
|||
)
|
||||
}
|
||||
|
||||
private var settingsSidebarTintLightBinding: Binding<Color> {
|
||||
Binding(
|
||||
get: {
|
||||
Color(nsColor: NSColor(hex: sidebarTintHexLight ?? sidebarTintHex) ?? .black)
|
||||
},
|
||||
set: { newColor in
|
||||
let nsColor = NSColor(newColor)
|
||||
sidebarTintHexLight = nsColor.hexString()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private var settingsSidebarTintDarkBinding: Binding<Color> {
|
||||
Binding(
|
||||
get: {
|
||||
Color(nsColor: NSColor(hex: sidebarTintHexDark ?? sidebarTintHex) ?? .black)
|
||||
},
|
||||
set: { newColor in
|
||||
let nsColor = NSColor(newColor)
|
||||
sidebarTintHexDark = nsColor.hexString()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private var hasSocketPasswordConfigured: Bool {
|
||||
SocketControlPasswordStore.hasConfiguredPassword()
|
||||
}
|
||||
|
|
@ -3939,6 +3977,83 @@ struct SettingsView: View {
|
|||
}
|
||||
}
|
||||
|
||||
SettingsSectionHeader(title: String(localized: "settings.section.sidebarAppearance", defaultValue: "Sidebar Appearance"))
|
||||
SettingsCard {
|
||||
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.")
|
||||
) {
|
||||
HStack(spacing: 8) {
|
||||
ColorPicker(
|
||||
String(localized: "settings.sidebarAppearance.tintColorLight.picker", defaultValue: "Light tint"),
|
||||
selection: settingsSidebarTintLightBinding,
|
||||
supportsOpacity: false
|
||||
)
|
||||
.labelsHidden()
|
||||
.frame(width: 38)
|
||||
|
||||
Text(sidebarTintHexLight ?? String(localized: "settings.sidebarAppearance.defaultLabel", defaultValue: "Default"))
|
||||
.font(.system(size: 12, weight: .medium, design: .monospaced))
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(width: 76, alignment: .trailing)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCardDivider()
|
||||
|
||||
SettingsCardRow(
|
||||
String(localized: "settings.sidebarAppearance.tintColorDark", defaultValue: "Dark Mode Tint"),
|
||||
subtitle: String(localized: "settings.sidebarAppearance.tintColorDark.subtitle", defaultValue: "Sidebar tint color when using dark appearance.")
|
||||
) {
|
||||
HStack(spacing: 8) {
|
||||
ColorPicker(
|
||||
String(localized: "settings.sidebarAppearance.tintColorDark.picker", defaultValue: "Dark tint"),
|
||||
selection: settingsSidebarTintDarkBinding,
|
||||
supportsOpacity: false
|
||||
)
|
||||
.labelsHidden()
|
||||
.frame(width: 38)
|
||||
|
||||
Text(sidebarTintHexDark ?? String(localized: "settings.sidebarAppearance.defaultLabel", defaultValue: "Default"))
|
||||
.font(.system(size: 12, weight: .medium, design: .monospaced))
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(width: 76, alignment: .trailing)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCardDivider()
|
||||
|
||||
SettingsCardRow(
|
||||
String(localized: "settings.sidebarAppearance.tintOpacity", defaultValue: "Tint Opacity"),
|
||||
subtitle: String(localized: "settings.sidebarAppearance.tintOpacity.subtitle", defaultValue: "How strongly the tint color shows over the sidebar material.")
|
||||
) {
|
||||
HStack(spacing: 8) {
|
||||
Slider(value: $sidebarTintOpacity, in: 0...1)
|
||||
.frame(width: 140)
|
||||
Text(String(format: "%.0f%%", sidebarTintOpacity * 100))
|
||||
.font(.system(size: 12, weight: .medium, design: .monospaced))
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(width: 36, alignment: .trailing)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCardDivider()
|
||||
|
||||
SettingsCardRow(
|
||||
String(localized: "settings.sidebarAppearance.reset", defaultValue: "Reset Sidebar Tint"),
|
||||
subtitle: String(localized: "settings.sidebarAppearance.reset.subtitle", defaultValue: "Restore default sidebar appearance.")
|
||||
) {
|
||||
Button(String(localized: "settings.sidebarAppearance.reset.button", defaultValue: "Reset")) {
|
||||
sidebarTintHexLight = nil
|
||||
sidebarTintHexDark = nil
|
||||
sidebarTintHex = SidebarTintDefaults.hex
|
||||
sidebarTintOpacity = SidebarTintDefaults.opacity
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.small)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSectionHeader(title: String(localized: "settings.section.automation", defaultValue: "Automation"))
|
||||
SettingsCard {
|
||||
SettingsPickerRow(
|
||||
|
|
@ -4503,6 +4618,10 @@ struct SettingsView: View {
|
|||
sidebarShowLog = true
|
||||
sidebarShowProgress = true
|
||||
sidebarShowMetadata = true
|
||||
sidebarTintHex = SidebarTintDefaults.hex
|
||||
sidebarTintHexLight = nil
|
||||
sidebarTintHexDark = nil
|
||||
sidebarTintOpacity = SidebarTintDefaults.opacity
|
||||
showOpenAccessConfirmation = false
|
||||
pendingOpenAccessMode = nil
|
||||
socketPasswordDraft = ""
|
||||
|
|
|
|||
|
|
@ -1614,6 +1614,157 @@ final class GhosttyMouseFocusTests: XCTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
final class SidebarBackgroundConfigTests: XCTestCase {
|
||||
|
||||
func testParseSidebarBackgroundSingleHex() {
|
||||
var config = GhosttyConfig()
|
||||
config.parse("sidebar-background = #336699")
|
||||
XCTAssertEqual(config.rawSidebarBackground, "#336699")
|
||||
}
|
||||
|
||||
func testParseSidebarBackgroundDualMode() {
|
||||
var config = GhosttyConfig()
|
||||
config.parse("sidebar-background = light:#fbf3db,dark:#103c48")
|
||||
XCTAssertEqual(config.rawSidebarBackground, "light:#fbf3db,dark:#103c48")
|
||||
}
|
||||
|
||||
func testParseSidebarTintOpacity() {
|
||||
var config = GhosttyConfig()
|
||||
config.parse("sidebar-tint-opacity = 0.4")
|
||||
XCTAssertEqual(config.sidebarTintOpacity ?? -1, 0.4, accuracy: 0.0001)
|
||||
}
|
||||
|
||||
func testParseSidebarTintOpacityClampedAboveOne() {
|
||||
var config = GhosttyConfig()
|
||||
config.parse("sidebar-tint-opacity = 1.5")
|
||||
XCTAssertEqual(config.sidebarTintOpacity ?? -1, 1.0, accuracy: 0.0001)
|
||||
}
|
||||
|
||||
func testParseSidebarTintOpacityClampedBelowZero() {
|
||||
var config = GhosttyConfig()
|
||||
config.parse("sidebar-tint-opacity = -0.3")
|
||||
XCTAssertEqual(config.sidebarTintOpacity ?? -1, 0.0, accuracy: 0.0001)
|
||||
}
|
||||
|
||||
func testResolveSidebarBackgroundSingleHex() {
|
||||
var config = GhosttyConfig()
|
||||
config.rawSidebarBackground = "#336699"
|
||||
config.resolveSidebarBackground(preferredColorScheme: .light)
|
||||
|
||||
XCTAssertNotNil(config.sidebarBackground)
|
||||
XCTAssertNil(config.sidebarBackgroundLight)
|
||||
XCTAssertNil(config.sidebarBackgroundDark)
|
||||
}
|
||||
|
||||
func testResolveSidebarBackgroundDualModeSetsLightAndDark() {
|
||||
var config = GhosttyConfig()
|
||||
config.rawSidebarBackground = "light:#fbf3db,dark:#103c48"
|
||||
config.resolveSidebarBackground(preferredColorScheme: .light)
|
||||
|
||||
XCTAssertNotNil(config.sidebarBackgroundLight)
|
||||
XCTAssertNotNil(config.sidebarBackgroundDark)
|
||||
XCTAssertNotNil(config.sidebarBackground)
|
||||
}
|
||||
|
||||
func testResolveSidebarBackgroundNilWhenNoRaw() {
|
||||
var config = GhosttyConfig()
|
||||
config.resolveSidebarBackground(preferredColorScheme: .dark)
|
||||
|
||||
XCTAssertNil(config.sidebarBackground)
|
||||
XCTAssertNil(config.sidebarBackgroundLight)
|
||||
XCTAssertNil(config.sidebarBackgroundDark)
|
||||
}
|
||||
|
||||
func testApplyToUserDefaultsSkipsWritesWhenNoConfig() {
|
||||
let defaults = UserDefaults.standard
|
||||
let testKey = "sidebarTintHex"
|
||||
let original = defaults.string(forKey: testKey)
|
||||
defer { restoreDefaultsValue(original, key: testKey, defaults: defaults) }
|
||||
|
||||
defaults.set("#AAAAAA", forKey: testKey)
|
||||
|
||||
var config = GhosttyConfig()
|
||||
config.applySidebarAppearanceToUserDefaults()
|
||||
|
||||
XCTAssertEqual(defaults.string(forKey: testKey), "#AAAAAA",
|
||||
"Should not overwrite UserDefaults when rawSidebarBackground is nil")
|
||||
}
|
||||
|
||||
func testApplyToUserDefaultsWritesHexWhenConfigSet() {
|
||||
let defaults = UserDefaults.standard
|
||||
let keys = ["sidebarTintHex", "sidebarTintHexLight", "sidebarTintHexDark"]
|
||||
let originals = keys.map { defaults.object(forKey: $0) }
|
||||
defer {
|
||||
for (key, original) in zip(keys, originals) {
|
||||
restoreDefaultsValue(original, key: key, defaults: defaults)
|
||||
}
|
||||
}
|
||||
|
||||
var config = GhosttyConfig()
|
||||
config.rawSidebarBackground = "#336699"
|
||||
config.resolveSidebarBackground(preferredColorScheme: .light)
|
||||
config.applySidebarAppearanceToUserDefaults()
|
||||
|
||||
XCTAssertEqual(defaults.string(forKey: "sidebarTintHex"), "#336699")
|
||||
XCTAssertNil(defaults.string(forKey: "sidebarTintHexLight"))
|
||||
XCTAssertNil(defaults.string(forKey: "sidebarTintHexDark"))
|
||||
}
|
||||
|
||||
func testApplyToUserDefaultsClearsStaleKeysOnSwitchFromDualToSingle() {
|
||||
let defaults = UserDefaults.standard
|
||||
let keys = ["sidebarTintHex", "sidebarTintHexLight", "sidebarTintHexDark"]
|
||||
let originals = keys.map { defaults.object(forKey: $0) }
|
||||
defer {
|
||||
for (key, original) in zip(keys, originals) {
|
||||
restoreDefaultsValue(original, key: key, defaults: defaults)
|
||||
}
|
||||
}
|
||||
|
||||
defaults.set("#AAAAAA", forKey: "sidebarTintHexLight")
|
||||
defaults.set("#BBBBBB", forKey: "sidebarTintHexDark")
|
||||
|
||||
var config = GhosttyConfig()
|
||||
config.rawSidebarBackground = "#222222"
|
||||
config.resolveSidebarBackground(preferredColorScheme: .light)
|
||||
config.applySidebarAppearanceToUserDefaults()
|
||||
|
||||
XCTAssertEqual(defaults.string(forKey: "sidebarTintHex"), "#222222")
|
||||
XCTAssertNil(defaults.string(forKey: "sidebarTintHexLight"),
|
||||
"Stale light key should be cleared")
|
||||
XCTAssertNil(defaults.string(forKey: "sidebarTintHexDark"),
|
||||
"Stale dark key should be cleared")
|
||||
}
|
||||
|
||||
func testApplyToUserDefaultsOnlyWritesOpacityWhenExplicit() {
|
||||
let defaults = UserDefaults.standard
|
||||
let keys = ["sidebarTintHex", "sidebarTintHexLight", "sidebarTintHexDark", "sidebarTintOpacity"]
|
||||
let originals = keys.map { defaults.object(forKey: $0) }
|
||||
defer {
|
||||
for (key, original) in zip(keys, originals) {
|
||||
restoreDefaultsValue(original, key: key, defaults: defaults)
|
||||
}
|
||||
}
|
||||
|
||||
defaults.set(0.18, forKey: "sidebarTintOpacity")
|
||||
|
||||
var config = GhosttyConfig()
|
||||
config.rawSidebarBackground = "#336699"
|
||||
config.resolveSidebarBackground(preferredColorScheme: .light)
|
||||
config.applySidebarAppearanceToUserDefaults()
|
||||
|
||||
XCTAssertEqual(defaults.double(forKey: "sidebarTintOpacity"), 0.18, accuracy: 0.0001,
|
||||
"Should not overwrite opacity when config doesn't set sidebar-tint-opacity")
|
||||
}
|
||||
|
||||
private func restoreDefaultsValue(_ value: Any?, key: String, defaults: UserDefaults) {
|
||||
if let value = value {
|
||||
defaults.set(value, forKey: key)
|
||||
} else {
|
||||
defaults.removeObject(forKey: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class ZshShellIntegrationHandoffTests: XCTestCase {
|
||||
func testGhosttyPromptHooksLoadWhenCmuxRequestsZshIntegration() throws {
|
||||
let output = try runInteractiveZsh(cmuxLoadGhosttyIntegration: true)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue