import AppKit import SwiftUI import Darwin import Bonsplit import UniformTypeIdentifiers @main struct cmuxApp: App { @StateObject private var tabManager: TabManager @StateObject private var notificationStore = TerminalNotificationStore.shared @StateObject private var sidebarState = SidebarState() @StateObject private var sidebarSelectionState = SidebarSelectionState() private let primaryWindowId = UUID() @AppStorage(AppearanceSettings.appearanceModeKey) private var appearanceMode = AppearanceSettings.defaultMode.rawValue @AppStorage("titlebarControlsStyle") private var titlebarControlsStyle = TitlebarControlsStyle.classic.rawValue @AppStorage(ShortcutHintDebugSettings.alwaysShowHintsKey) private var alwaysShowShortcutHints = ShortcutHintDebugSettings.defaultAlwaysShowHints @AppStorage(SocketControlSettings.appStorageKey) private var socketControlMode = SocketControlSettings.defaultMode.rawValue @AppStorage(KeyboardShortcutSettings.Action.toggleSidebar.defaultsKey) private var toggleSidebarShortcutData = Data() @AppStorage(KeyboardShortcutSettings.Action.newTab.defaultsKey) private var newWorkspaceShortcutData = Data() @AppStorage(KeyboardShortcutSettings.Action.newWindow.defaultsKey) private var newWindowShortcutData = Data() @AppStorage(KeyboardShortcutSettings.Action.showNotifications.defaultsKey) private var showNotificationsShortcutData = Data() @AppStorage(KeyboardShortcutSettings.Action.jumpToUnread.defaultsKey) private var jumpToUnreadShortcutData = Data() @AppStorage(KeyboardShortcutSettings.Action.nextSurface.defaultsKey) private var nextSurfaceShortcutData = Data() @AppStorage(KeyboardShortcutSettings.Action.prevSurface.defaultsKey) private var prevSurfaceShortcutData = Data() @AppStorage(KeyboardShortcutSettings.Action.nextSidebarTab.defaultsKey) private var nextWorkspaceShortcutData = Data() @AppStorage(KeyboardShortcutSettings.Action.prevSidebarTab.defaultsKey) private var prevWorkspaceShortcutData = Data() @AppStorage(KeyboardShortcutSettings.Action.splitRight.defaultsKey) private var splitRightShortcutData = Data() @AppStorage(KeyboardShortcutSettings.Action.splitDown.defaultsKey) private var splitDownShortcutData = Data() @AppStorage(KeyboardShortcutSettings.Action.toggleBrowserDeveloperTools.defaultsKey) private var toggleBrowserDeveloperToolsShortcutData = Data() @AppStorage(KeyboardShortcutSettings.Action.showBrowserJavaScriptConsole.defaultsKey) private var showBrowserJavaScriptConsoleShortcutData = Data() @AppStorage(KeyboardShortcutSettings.Action.splitBrowserRight.defaultsKey) private var splitBrowserRightShortcutData = Data() @AppStorage(KeyboardShortcutSettings.Action.splitBrowserDown.defaultsKey) private var splitBrowserDownShortcutData = Data() @AppStorage(KeyboardShortcutSettings.Action.renameWorkspace.defaultsKey) private var renameWorkspaceShortcutData = Data() @AppStorage(KeyboardShortcutSettings.Action.openFolder.defaultsKey) private var openFolderShortcutData = Data() @AppStorage(KeyboardShortcutSettings.Action.closeWorkspace.defaultsKey) private var closeWorkspaceShortcutData = Data() @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate init() { if SocketControlSettings.shouldBlockUntaggedDebugLaunch() { Self.terminateForMissingLaunchTag() } Self.configureGhosttyEnvironment() // Apply saved language preference before any UI loads LanguageSettings.apply(LanguageSettings.languageAtLaunch) let startupAppearance = AppearanceSettings.resolvedMode() Self.applyAppearance(startupAppearance) _tabManager = StateObject(wrappedValue: TabManager()) // Migrate legacy and old-format socket mode values to the new enum. let defaults = UserDefaults.standard if let stored = defaults.string(forKey: SocketControlSettings.appStorageKey) { let migrated = SocketControlSettings.migrateMode(stored) if migrated.rawValue != stored { defaults.set(migrated.rawValue, forKey: SocketControlSettings.appStorageKey) } } else if let legacy = defaults.object(forKey: SocketControlSettings.legacyEnabledKey) as? Bool { defaults.set(legacy ? SocketControlMode.cmuxOnly.rawValue : SocketControlMode.off.rawValue, forKey: SocketControlSettings.appStorageKey) } // Skip keychain migration for DEV/staging builds. Each tagged build gets a // unique bundle ID with its own UserDefaults domain, so migration would run // on every launch and trigger a macOS keychain access prompt (the legacy // keychain item was created by a differently-signed app). let bundleID = Bundle.main.bundleIdentifier if !SocketControlSettings.isDebugLikeBundleIdentifier(bundleID) && !SocketControlSettings.isStagingBundleIdentifier(bundleID) { SocketControlPasswordStore.migrateLegacyKeychainPasswordIfNeeded(defaults: defaults) } migrateSidebarAppearanceDefaultsIfNeeded(defaults: defaults) // UI tests depend on AppDelegate wiring happening even if SwiftUI view appearance // callbacks (e.g. `.onAppear`) are delayed or skipped. appDelegate.configure(tabManager: tabManager, notificationStore: notificationStore, sidebarState: sidebarState) } private static func terminateForMissingLaunchTag() -> Never { let message = "error: refusing to launch untagged cmux DEV; start with ./scripts/reload.sh --tag (or set CMUX_TAG for test harnesses)" fputs("\(message)\n", stderr) fflush(stderr) NSLog("%@", message) Darwin.exit(64) } private static func configureGhosttyEnvironment() { let fileManager = FileManager.default let ghosttyAppResources = "/Applications/Ghostty.app/Contents/Resources/ghostty" let bundledGhosttyURL = Bundle.main.resourceURL?.appendingPathComponent("ghostty") var resolvedResourcesDir: String? if getenv("GHOSTTY_RESOURCES_DIR") == nil { if let bundledGhosttyURL, fileManager.fileExists(atPath: bundledGhosttyURL.path), fileManager.fileExists(atPath: bundledGhosttyURL.appendingPathComponent("themes").path) { resolvedResourcesDir = bundledGhosttyURL.path } else if fileManager.fileExists(atPath: ghosttyAppResources) { resolvedResourcesDir = ghosttyAppResources } else if let bundledGhosttyURL, fileManager.fileExists(atPath: bundledGhosttyURL.path) { resolvedResourcesDir = bundledGhosttyURL.path } if let resolvedResourcesDir { setenv("GHOSTTY_RESOURCES_DIR", resolvedResourcesDir, 1) } } if getenv("TERM") == nil { setenv("TERM", "xterm-ghostty", 1) } if getenv("TERM_PROGRAM") == nil { setenv("TERM_PROGRAM", "ghostty", 1) } if let resourcesDir = getenv("GHOSTTY_RESOURCES_DIR").flatMap({ String(cString: $0) }) { let resourcesURL = URL(fileURLWithPath: resourcesDir) let resourcesParent = resourcesURL.deletingLastPathComponent() let dataDir = resourcesParent.path let manDir = resourcesParent.appendingPathComponent("man").path appendEnvPathIfMissing( "XDG_DATA_DIRS", path: dataDir, defaultValue: "/usr/local/share:/usr/share" ) appendEnvPathIfMissing("MANPATH", path: manDir) } } private static func appendEnvPathIfMissing(_ key: String, path: String, defaultValue: String? = nil) { if path.isEmpty { return } var current = getenv(key).flatMap { String(cString: $0) } ?? "" if current.isEmpty, let defaultValue { current = defaultValue } if current.split(separator: ":").contains(Substring(path)) { return } let updated = current.isEmpty ? path : "\(current):\(path)" setenv(key, updated, 1) } private func migrateSidebarAppearanceDefaultsIfNeeded(defaults: UserDefaults) { let migrationKey = "sidebarAppearanceDefaultsVersion" let targetVersion = 1 guard defaults.integer(forKey: migrationKey) < targetVersion else { return } func normalizeHex(_ value: String) -> String { value .trimmingCharacters(in: .whitespacesAndNewlines) .replacingOccurrences(of: "#", with: "") .uppercased() } func approximatelyEqual(_ lhs: Double, _ rhs: Double, tolerance: Double = 0.0001) -> Bool { abs(lhs - rhs) <= tolerance } let material = defaults.string(forKey: "sidebarMaterial") ?? SidebarMaterialOption.sidebar.rawValue let blendMode = defaults.string(forKey: "sidebarBlendMode") ?? SidebarBlendModeOption.behindWindow.rawValue let state = defaults.string(forKey: "sidebarState") ?? SidebarStateOption.followWindow.rawValue let tintHex = defaults.string(forKey: "sidebarTintHex") ?? "#101010" let tintOpacity = defaults.object(forKey: "sidebarTintOpacity") as? Double ?? 0.54 let blurOpacity = defaults.object(forKey: "sidebarBlurOpacity") as? Double ?? 0.79 let cornerRadius = defaults.object(forKey: "sidebarCornerRadius") as? Double ?? 0.0 let usesLegacyDefaults = material == SidebarMaterialOption.sidebar.rawValue && blendMode == SidebarBlendModeOption.behindWindow.rawValue && state == SidebarStateOption.followWindow.rawValue && normalizeHex(tintHex) == "101010" && approximatelyEqual(tintOpacity, 0.54) && approximatelyEqual(blurOpacity, 0.79) && approximatelyEqual(cornerRadius, 0.0) if usesLegacyDefaults { let preset = SidebarPresetOption.nativeSidebar defaults.set(preset.rawValue, forKey: "sidebarPreset") defaults.set(preset.material.rawValue, forKey: "sidebarMaterial") defaults.set(preset.blendMode.rawValue, forKey: "sidebarBlendMode") defaults.set(preset.state.rawValue, forKey: "sidebarState") defaults.set(preset.tintHex, forKey: "sidebarTintHex") defaults.set(preset.tintOpacity, forKey: "sidebarTintOpacity") defaults.set(preset.blurOpacity, forKey: "sidebarBlurOpacity") defaults.set(preset.cornerRadius, forKey: "sidebarCornerRadius") } defaults.set(targetVersion, forKey: migrationKey) } var body: some Scene { WindowGroup { ContentView(updateViewModel: appDelegate.updateViewModel, windowId: primaryWindowId) .environmentObject(tabManager) .environmentObject(notificationStore) .environmentObject(sidebarState) .environmentObject(sidebarSelectionState) .onAppear { #if DEBUG if ProcessInfo.processInfo.environment["CMUX_UI_TEST_MODE"] == "1" { UpdateLogStore.shared.append("ui test: cmuxApp onAppear") } #endif // Start the Unix socket controller for programmatic access updateSocketController() appDelegate.configure(tabManager: tabManager, notificationStore: notificationStore, sidebarState: sidebarState) applyAppearance() if ProcessInfo.processInfo.environment["CMUX_UI_TEST_SHOW_SETTINGS"] == "1" { DispatchQueue.main.async { appDelegate.openPreferencesWindow(debugSource: "uiTestShowSettings") } } } .onChange(of: appearanceMode) { _ in applyAppearance() } .onChange(of: socketControlMode) { _ in updateSocketController() } } .windowStyle(.hiddenTitleBar) .commands { CommandGroup(replacing: .appSettings) { Button(String(localized: "menu.app.settings", defaultValue: "Settings…")) { appDelegate.openPreferencesWindow(debugSource: "menu.cmdComma") } .keyboardShortcut(",", modifiers: .command) } CommandGroup(replacing: .appInfo) { Button(String(localized: "menu.app.about", defaultValue: "About cmux")) { showAboutPanel() } Button(String(localized: "menu.app.ghosttySettings", defaultValue: "Ghostty Settings…")) { GhosttyApp.shared.openConfigurationInTextEdit() } Button(String(localized: "menu.app.reloadConfiguration", defaultValue: "Reload Configuration")) { GhosttyApp.shared.reloadConfiguration(source: "menu.reload_configuration") } .keyboardShortcut(",", modifiers: [.command, .shift]) Divider() Button(String(localized: "menu.app.checkForUpdates", defaultValue: "Check for Updates…")) { appDelegate.checkForUpdates(nil) } InstallUpdateMenuItem(model: appDelegate.updateViewModel) } #if DEBUG CommandMenu("Update Pill") { Button("Show Update Pill") { appDelegate.showUpdatePill(nil) } Button("Show Long Nightly Pill") { appDelegate.showUpdatePillLongNightly(nil) } Button("Show Loading State") { appDelegate.showUpdatePillLoading(nil) } Button("Hide Update Pill") { appDelegate.hideUpdatePill(nil) } Button("Automatic Update Pill") { appDelegate.clearUpdatePillOverride(nil) } } #endif CommandMenu(String(localized: "menu.updateLogs.title", defaultValue: "Update Logs")) { Button(String(localized: "menu.updateLogs.copyUpdateLogs", defaultValue: "Copy Update Logs")) { appDelegate.copyUpdateLogs(nil) } Button(String(localized: "menu.updateLogs.copyFocusLogs", defaultValue: "Copy Focus Logs")) { appDelegate.copyFocusLogs(nil) } } CommandMenu(String(localized: "menu.notifications.title", defaultValue: "Notifications")) { let snapshot = notificationMenuSnapshot Button(snapshot.stateHintTitle) {} .disabled(true) if !snapshot.recentNotifications.isEmpty { Divider() ForEach(snapshot.recentNotifications) { notification in Button(notificationMenuItemTitle(for: notification)) { openNotificationFromMainMenu(notification) } } Divider() } splitCommandButton(title: String(localized: "menu.notifications.show", defaultValue: "Show Notifications"), shortcut: showNotificationsMenuShortcut) { showNotificationsPopover() } splitCommandButton(title: String(localized: "menu.notifications.jumpToUnread", defaultValue: "Jump to Latest Unread"), shortcut: jumpToUnreadMenuShortcut) { appDelegate.jumpToLatestUnread() } .disabled(!snapshot.hasUnreadNotifications) Button(String(localized: "menu.notifications.markAllRead", defaultValue: "Mark All Read")) { notificationStore.markAllRead() } .disabled(!snapshot.hasUnreadNotifications) Button(String(localized: "menu.notifications.clearAll", defaultValue: "Clear All")) { notificationStore.clearAll() } .disabled(!snapshot.hasNotifications) } #if DEBUG CommandMenu("Debug") { Button("New Tab With Lorem Search Text") { appDelegate.openDebugLoremTab(nil) } Button("New Tab With Large Scrollback") { appDelegate.openDebugScrollbackTab(nil) } Button("Open Workspaces for All Workspace Colors") { appDelegate.openDebugColorComparisonWorkspaces(nil) } Divider() Menu("Debug Windows") { Button("Debug Window Controls…") { DebugWindowControlsWindowController.shared.show() } Button("Settings/About Titlebar Debug…") { SettingsAboutTitlebarDebugWindowController.shared.show() } Divider() Button("Sidebar Debug…") { SidebarDebugWindowController.shared.show() } Button("Background Debug…") { BackgroundDebugWindowController.shared.show() } Button("Menu Bar Extra Debug…") { MenuBarExtraDebugWindowController.shared.show() } Divider() Button("Open All Debug Windows") { openAllDebugWindows() } } Toggle("Always Show Shortcut Hints", isOn: $alwaysShowShortcutHints) Divider() Picker("Titlebar Controls Style", selection: $titlebarControlsStyle) { ForEach(TitlebarControlsStyle.allCases) { style in Text(style.menuTitle).tag(style.rawValue) } } Divider() Button("Trigger Sentry Test Crash") { appDelegate.triggerSentryTestCrash(nil) } } #endif // New tab commands CommandGroup(replacing: .newItem) { splitCommandButton(title: String(localized: "menu.file.newWindow", defaultValue: "New Window"), shortcut: newWindowMenuShortcut) { appDelegate.openNewMainWindow(nil) } splitCommandButton(title: String(localized: "menu.file.newWorkspace", defaultValue: "New Workspace"), shortcut: newWorkspaceMenuShortcut) { if let appDelegate = AppDelegate.shared { if appDelegate.addWorkspaceInPreferredMainWindow(debugSource: "menu.newWorkspace") == nil { #if DEBUG FocusLogStore.shared.append( "cmdn.route phase=fallback_new_window src=menu.newWorkspace reason=workspace_creation_returned_nil" ) #endif appDelegate.openNewMainWindow(nil) } } else { activeTabManager.addTab() } } splitCommandButton(title: String(localized: "menu.file.openFolder", defaultValue: "Open Folder…"), shortcut: openFolderMenuShortcut) { let panel = NSOpenPanel() panel.canChooseFiles = false panel.canChooseDirectories = true panel.allowsMultipleSelection = false panel.title = String(localized: "menu.file.openFolder.panelTitle", defaultValue: "Open Folder") panel.prompt = String(localized: "menu.file.openFolder.panelPrompt", defaultValue: "Open") if panel.runModal() == .OK, let url = panel.url { if let appDelegate = AppDelegate.shared { if appDelegate.addWorkspaceInPreferredMainWindow( workingDirectory: url.path, debugSource: "menu.openFolder" ) == nil { appDelegate.openNewMainWindow(nil) } } else { activeTabManager.addWorkspace(workingDirectory: url.path) } } } } // Close tab/workspace CommandGroup(after: .newItem) { Button(String(localized: "menu.file.goToWorkspace", defaultValue: "Go to Workspace…")) { let targetWindow = NSApp.keyWindow ?? NSApp.mainWindow NotificationCenter.default.post(name: .commandPaletteSwitcherRequested, object: targetWindow) } .keyboardShortcut("p", modifiers: [.command]) Button(String(localized: "menu.file.commandPalette", defaultValue: "Command Palette…")) { let targetWindow = NSApp.keyWindow ?? NSApp.mainWindow NotificationCenter.default.post(name: .commandPaletteRequested, object: targetWindow) } .keyboardShortcut("p", modifiers: [.command, .shift]) Divider() // Terminal semantics: // Cmd+W closes the focused tab (with confirmation if needed). If this is the last // tab in the last workspace, it closes the window. Button(String(localized: "menu.file.closeTab", defaultValue: "Close Tab")) { closePanelOrWindow() } .keyboardShortcut("w", modifiers: .command) Button(String(localized: "menu.file.closeOtherTabs", defaultValue: "Close Other Tabs in Pane")) { closeOtherTabsInFocusedPane() } .keyboardShortcut("t", modifiers: [.command, .option]) .disabled(!activeTabManager.canCloseOtherTabsInFocusedPane()) // Cmd+Shift+W closes the current workspace (with confirmation if needed). If this // is the last workspace, it closes the window. splitCommandButton(title: String(localized: "menu.file.closeWorkspace", defaultValue: "Close Workspace"), shortcut: closeWorkspaceMenuShortcut) { closeTabOrWindow() } Button(String(localized: "menu.file.reopenClosedBrowserPanel", defaultValue: "Reopen Closed Browser Panel")) { _ = activeTabManager.reopenMostRecentlyClosedBrowserPanel() } .keyboardShortcut("t", modifiers: [.command, .shift]) } // Find CommandGroup(after: .textEditing) { Menu(String(localized: "menu.find.title", defaultValue: "Find")) { Button(String(localized: "menu.find.find", defaultValue: "Find…")) { #if DEBUG dlog("find.menu Cmd+F fired") #endif activeTabManager.startSearch() } .keyboardShortcut("f", modifiers: .command) Button(String(localized: "menu.find.findNext", defaultValue: "Find Next")) { activeTabManager.findNext() } .keyboardShortcut("g", modifiers: .command) Button(String(localized: "menu.find.findPrevious", defaultValue: "Find Previous")) { activeTabManager.findPrevious() } .keyboardShortcut("g", modifiers: [.command, .shift]) Divider() Button(String(localized: "menu.find.hideFindBar", defaultValue: "Hide Find Bar")) { activeTabManager.hideFind() } .keyboardShortcut("f", modifiers: [.command, .shift]) .disabled(!(activeTabManager.isFindVisible)) Divider() Button(String(localized: "menu.find.useSelectionForFind", defaultValue: "Use Selection for Find")) { activeTabManager.searchSelection() } .keyboardShortcut("e", modifiers: .command) .disabled(!(activeTabManager.canUseSelectionForFind)) } } // Tab navigation CommandGroup(after: .toolbar) { splitCommandButton(title: String(localized: "menu.view.toggleSidebar", defaultValue: "Toggle Sidebar"), shortcut: toggleSidebarMenuShortcut) { if AppDelegate.shared?.toggleSidebarInActiveMainWindow() != true { sidebarState.toggle() } } Divider() splitCommandButton(title: String(localized: "menu.view.nextSurface", defaultValue: "Next Surface"), shortcut: nextSurfaceMenuShortcut) { activeTabManager.selectNextSurface() } splitCommandButton(title: String(localized: "menu.view.previousSurface", defaultValue: "Previous Surface"), shortcut: prevSurfaceMenuShortcut) { activeTabManager.selectPreviousSurface() } Button(String(localized: "menu.view.back", defaultValue: "Back")) { activeTabManager.focusedBrowserPanel?.goBack() } .keyboardShortcut("[", modifiers: .command) Button(String(localized: "menu.view.forward", defaultValue: "Forward")) { activeTabManager.focusedBrowserPanel?.goForward() } .keyboardShortcut("]", modifiers: .command) Button(String(localized: "menu.view.reloadPage", defaultValue: "Reload Page")) { activeTabManager.focusedBrowserPanel?.reload() } .keyboardShortcut("r", modifiers: .command) splitCommandButton(title: String(localized: "menu.view.toggleDevTools", defaultValue: "Toggle Developer Tools"), shortcut: toggleBrowserDeveloperToolsMenuShortcut) { let manager = activeTabManager if !manager.toggleDeveloperToolsFocusedBrowser() { NSSound.beep() } } splitCommandButton(title: String(localized: "menu.view.showJSConsole", defaultValue: "Show JavaScript Console"), shortcut: showBrowserJavaScriptConsoleMenuShortcut) { let manager = activeTabManager if !manager.showJavaScriptConsoleFocusedBrowser() { NSSound.beep() } } Button(String(localized: "menu.view.zoomIn", defaultValue: "Zoom In")) { _ = activeTabManager.zoomInFocusedBrowser() } .keyboardShortcut("=", modifiers: .command) Button(String(localized: "menu.view.zoomOut", defaultValue: "Zoom Out")) { _ = activeTabManager.zoomOutFocusedBrowser() } .keyboardShortcut("-", modifiers: .command) Button(String(localized: "menu.view.actualSize", defaultValue: "Actual Size")) { _ = activeTabManager.resetZoomFocusedBrowser() } .keyboardShortcut("0", modifiers: .command) Button(String(localized: "menu.view.clearBrowserHistory", defaultValue: "Clear Browser History")) { BrowserHistoryStore.shared.clearHistory() } splitCommandButton(title: String(localized: "menu.view.nextWorkspace", defaultValue: "Next Workspace"), shortcut: nextWorkspaceMenuShortcut) { activeTabManager.selectNextTab() } splitCommandButton(title: String(localized: "menu.view.previousWorkspace", defaultValue: "Previous Workspace"), shortcut: prevWorkspaceMenuShortcut) { activeTabManager.selectPreviousTab() } splitCommandButton(title: String(localized: "menu.view.renameWorkspace", defaultValue: "Rename Workspace…"), shortcut: renameWorkspaceMenuShortcut) { _ = AppDelegate.shared?.requestRenameWorkspaceViaCommandPalette() } Divider() splitCommandButton(title: String(localized: "menu.view.splitRight", defaultValue: "Split Right"), shortcut: splitRightMenuShortcut) { performSplitFromMenu(direction: .right) } splitCommandButton(title: String(localized: "menu.view.splitDown", defaultValue: "Split Down"), shortcut: splitDownMenuShortcut) { performSplitFromMenu(direction: .down) } splitCommandButton(title: String(localized: "menu.view.splitBrowserRight", defaultValue: "Split Browser Right"), shortcut: splitBrowserRightMenuShortcut) { performBrowserSplitFromMenu(direction: .right) } splitCommandButton(title: String(localized: "menu.view.splitBrowserDown", defaultValue: "Split Browser Down"), shortcut: splitBrowserDownMenuShortcut) { performBrowserSplitFromMenu(direction: .down) } Divider() // Cmd+1 through Cmd+9 for workspace selection (9 = last workspace) ForEach(1...9, id: \.self) { number in Button(String(localized: "menu.view.workspace", defaultValue: "Workspace \(number)")) { let manager = activeTabManager if let targetIndex = WorkspaceShortcutMapper.workspaceIndex(forCommandDigit: number, workspaceCount: manager.tabs.count) { manager.selectTab(at: targetIndex) } } .keyboardShortcut(KeyEquivalent(Character("\(number)")), modifiers: .command) } Divider() splitCommandButton(title: String(localized: "menu.view.jumpToUnread", defaultValue: "Jump to Latest Unread"), shortcut: jumpToUnreadMenuShortcut) { AppDelegate.shared?.jumpToLatestUnread() } splitCommandButton(title: String(localized: "menu.view.showNotifications", defaultValue: "Show Notifications"), shortcut: showNotificationsMenuShortcut) { showNotificationsPopover() } } } } private func showAboutPanel() { AboutWindowController.shared.show() NSApp.activate(ignoringOtherApps: true) } private func applyAppearance() { let mode = AppearanceSettings.mode(for: appearanceMode) if appearanceMode != mode.rawValue { appearanceMode = mode.rawValue } Self.applyAppearance(mode) } private static func applyAppearance(_ mode: AppearanceMode) { switch mode { case .system: NSApplication.shared.appearance = nil case .light: NSApplication.shared.appearance = NSAppearance(named: .aqua) case .dark: NSApplication.shared.appearance = NSAppearance(named: .darkAqua) case .auto: NSApplication.shared.appearance = nil } } private func updateSocketController() { let mode = SocketControlSettings.effectiveMode(userMode: currentSocketMode) if mode != .off { TerminalController.shared.start( tabManager: tabManager, socketPath: SocketControlSettings.socketPath(), accessMode: mode ) } else { TerminalController.shared.stop() } } private var currentSocketMode: SocketControlMode { SocketControlSettings.migrateMode(socketControlMode) } private var splitRightMenuShortcut: StoredShortcut { decodeShortcut(from: splitRightShortcutData, fallback: KeyboardShortcutSettings.Action.splitRight.defaultShortcut) } private var toggleSidebarMenuShortcut: StoredShortcut { decodeShortcut(from: toggleSidebarShortcutData, fallback: KeyboardShortcutSettings.Action.toggleSidebar.defaultShortcut) } private var newWorkspaceMenuShortcut: StoredShortcut { decodeShortcut(from: newWorkspaceShortcutData, fallback: KeyboardShortcutSettings.Action.newTab.defaultShortcut) } private var newWindowMenuShortcut: StoredShortcut { decodeShortcut(from: newWindowShortcutData, fallback: KeyboardShortcutSettings.Action.newWindow.defaultShortcut) } private var openFolderMenuShortcut: StoredShortcut { decodeShortcut(from: openFolderShortcutData, fallback: KeyboardShortcutSettings.Action.openFolder.defaultShortcut) } private var showNotificationsMenuShortcut: StoredShortcut { decodeShortcut( from: showNotificationsShortcutData, fallback: KeyboardShortcutSettings.Action.showNotifications.defaultShortcut ) } private var jumpToUnreadMenuShortcut: StoredShortcut { decodeShortcut( from: jumpToUnreadShortcutData, fallback: KeyboardShortcutSettings.Action.jumpToUnread.defaultShortcut ) } private var nextSurfaceMenuShortcut: StoredShortcut { decodeShortcut(from: nextSurfaceShortcutData, fallback: KeyboardShortcutSettings.Action.nextSurface.defaultShortcut) } private var prevSurfaceMenuShortcut: StoredShortcut { decodeShortcut(from: prevSurfaceShortcutData, fallback: KeyboardShortcutSettings.Action.prevSurface.defaultShortcut) } private var nextWorkspaceMenuShortcut: StoredShortcut { decodeShortcut( from: nextWorkspaceShortcutData, fallback: KeyboardShortcutSettings.Action.nextSidebarTab.defaultShortcut ) } private var prevWorkspaceMenuShortcut: StoredShortcut { decodeShortcut( from: prevWorkspaceShortcutData, fallback: KeyboardShortcutSettings.Action.prevSidebarTab.defaultShortcut ) } private var splitDownMenuShortcut: StoredShortcut { decodeShortcut(from: splitDownShortcutData, fallback: KeyboardShortcutSettings.Action.splitDown.defaultShortcut) } private var toggleBrowserDeveloperToolsMenuShortcut: StoredShortcut { decodeShortcut( from: toggleBrowserDeveloperToolsShortcutData, fallback: KeyboardShortcutSettings.Action.toggleBrowserDeveloperTools.defaultShortcut ) } private var showBrowserJavaScriptConsoleMenuShortcut: StoredShortcut { decodeShortcut( from: showBrowserJavaScriptConsoleShortcutData, fallback: KeyboardShortcutSettings.Action.showBrowserJavaScriptConsole.defaultShortcut ) } private var splitBrowserRightMenuShortcut: StoredShortcut { decodeShortcut( from: splitBrowserRightShortcutData, fallback: KeyboardShortcutSettings.Action.splitBrowserRight.defaultShortcut ) } private var splitBrowserDownMenuShortcut: StoredShortcut { decodeShortcut( from: splitBrowserDownShortcutData, fallback: KeyboardShortcutSettings.Action.splitBrowserDown.defaultShortcut ) } private var renameWorkspaceMenuShortcut: StoredShortcut { decodeShortcut( from: renameWorkspaceShortcutData, fallback: KeyboardShortcutSettings.Action.renameWorkspace.defaultShortcut ) } private var closeWorkspaceMenuShortcut: StoredShortcut { decodeShortcut( from: closeWorkspaceShortcutData, fallback: KeyboardShortcutSettings.Action.closeWorkspace.defaultShortcut ) } private var notificationMenuSnapshot: NotificationMenuSnapshot { NotificationMenuSnapshotBuilder.make(notifications: notificationStore.notifications) } private var activeTabManager: TabManager { AppDelegate.shared?.synchronizeActiveMainWindowContext( preferredWindow: NSApp.keyWindow ?? NSApp.mainWindow ) ?? tabManager } private func decodeShortcut(from data: Data, fallback: StoredShortcut) -> StoredShortcut { guard !data.isEmpty, let shortcut = try? JSONDecoder().decode(StoredShortcut.self, from: data) else { return fallback } return shortcut } private func notificationMenuItemTitle(for notification: TerminalNotification) -> String { let tabTitle = appDelegate.tabTitle(for: notification.tabId) return MenuBarNotificationLineFormatter.menuTitle(notification: notification, tabTitle: tabTitle) } private func openNotificationFromMainMenu(_ notification: TerminalNotification) { _ = appDelegate.openNotification( tabId: notification.tabId, surfaceId: notification.surfaceId, notificationId: notification.id ) } private func performSplitFromMenu(direction: SplitDirection) { if AppDelegate.shared?.performSplitShortcut(direction: direction) == true { return } tabManager.createSplit(direction: direction) } private func performBrowserSplitFromMenu(direction: SplitDirection) { if AppDelegate.shared?.performBrowserSplitShortcut(direction: direction) == true { return } _ = tabManager.createBrowserSplit(direction: direction) } @ViewBuilder private func splitCommandButton(title: String, shortcut: StoredShortcut, action: @escaping () -> Void) -> some View { if let key = shortcut.keyEquivalent { Button(title, action: action) .keyboardShortcut(key, modifiers: shortcut.eventModifiers) } else { Button(title, action: action) } } private func closePanelOrWindow() { if let window = NSApp.keyWindow, window.identifier?.rawValue == "cmux.settings" { window.performClose(nil) return } activeTabManager.closeCurrentPanelWithConfirmation() } private func closeOtherTabsInFocusedPane() { activeTabManager.closeOtherTabsInFocusedPaneWithConfirmation() } private func closeTabOrWindow() { activeTabManager.closeCurrentTabWithConfirmation() } private func showNotificationsPopover() { AppDelegate.shared?.toggleNotificationsPopover(animated: false) } private func openAllDebugWindows() { SettingsAboutTitlebarDebugWindowController.shared.show() SidebarDebugWindowController.shared.show() BackgroundDebugWindowController.shared.show() MenuBarExtraDebugWindowController.shared.show() } } private enum SettingsAboutWindowKind: String, CaseIterable, Identifiable { case settings case about var id: String { rawValue } var displayTitle: String { switch self { case .settings: return "Settings Window" case .about: return "About Window" } } var windowIdentifier: String { switch self { case .settings: return "cmux.settings" case .about: return "cmux.about" } } var fallbackTitle: String { switch self { case .settings: return "Settings" case .about: return "About cmux" } } var minimumSize: NSSize { switch self { case .settings: return NSSize(width: 420, height: 360) case .about: return NSSize(width: 360, height: 520) } } } private enum TitlebarVisibilityOption: String, CaseIterable, Identifiable { case hidden case visible var id: String { rawValue } var displayTitle: String { switch self { case .hidden: return "Hidden" case .visible: return "Visible" } } var windowValue: NSWindow.TitleVisibility { switch self { case .hidden: return .hidden case .visible: return .visible } } } private enum TitlebarToolbarStyleOption: String, CaseIterable, Identifiable { case automatic case expanded case preference case unified case unifiedCompact var id: String { rawValue } var displayTitle: String { switch self { case .automatic: return "Automatic" case .expanded: return "Expanded" case .preference: return "Preference" case .unified: return "Unified" case .unifiedCompact: return "Unified Compact" } } var windowValue: NSWindow.ToolbarStyle { switch self { case .automatic: return .automatic case .expanded: return .expanded case .preference: return .preference case .unified: return .unified case .unifiedCompact: return .unifiedCompact } } } private struct SettingsAboutTitlebarDebugOptions: Equatable { var overridesEnabled: Bool var windowTitle: String var titleVisibility: TitlebarVisibilityOption var titlebarAppearsTransparent: Bool var movableByWindowBackground: Bool var titled: Bool var closable: Bool var miniaturizable: Bool var resizable: Bool var fullSizeContentView: Bool var showToolbar: Bool var toolbarStyle: TitlebarToolbarStyleOption static func defaults(for kind: SettingsAboutWindowKind) -> SettingsAboutTitlebarDebugOptions { switch kind { case .settings: return SettingsAboutTitlebarDebugOptions( overridesEnabled: false, windowTitle: "Settings", titleVisibility: .hidden, titlebarAppearsTransparent: true, movableByWindowBackground: true, titled: true, closable: true, miniaturizable: true, resizable: true, fullSizeContentView: true, showToolbar: false, toolbarStyle: .unifiedCompact ) case .about: return SettingsAboutTitlebarDebugOptions( overridesEnabled: false, windowTitle: "About cmux", titleVisibility: .hidden, titlebarAppearsTransparent: true, movableByWindowBackground: false, titled: true, closable: true, miniaturizable: true, resizable: false, fullSizeContentView: false, showToolbar: false, toolbarStyle: .automatic ) } } } @MainActor private final class SettingsAboutTitlebarDebugStore: ObservableObject { static let shared = SettingsAboutTitlebarDebugStore() @Published var settingsOptions = SettingsAboutTitlebarDebugOptions.defaults(for: .settings) { didSet { applyToOpenWindows(for: .settings) } } @Published var aboutOptions = SettingsAboutTitlebarDebugOptions.defaults(for: .about) { didSet { applyToOpenWindows(for: .about) } } private init() {} func options(for kind: SettingsAboutWindowKind) -> SettingsAboutTitlebarDebugOptions { switch kind { case .settings: return settingsOptions case .about: return aboutOptions } } func update(_ newValue: SettingsAboutTitlebarDebugOptions, for kind: SettingsAboutWindowKind) { switch kind { case .settings: settingsOptions = newValue case .about: aboutOptions = newValue } } func reset(_ kind: SettingsAboutWindowKind) { update(SettingsAboutTitlebarDebugOptions.defaults(for: kind), for: kind) } func applyToOpenWindows(for kind: SettingsAboutWindowKind) { for window in NSApp.windows where window.identifier?.rawValue == kind.windowIdentifier { apply(options(for: kind), to: window, for: kind) } } func applyToOpenWindows() { applyToOpenWindows(for: .settings) applyToOpenWindows(for: .about) } func applyCurrentOptions(to window: NSWindow, for kind: SettingsAboutWindowKind) { apply(options(for: kind), to: window, for: kind) } func copyConfigToPasteboard() { let settings = options(for: .settings) let about = options(for: .about) let payload = """ # Settings/About Titlebar Debug settings.overridesEnabled=\(settings.overridesEnabled) settings.title=\(settings.windowTitle) settings.titleVisibility=\(settings.titleVisibility.rawValue) settings.titlebarAppearsTransparent=\(settings.titlebarAppearsTransparent) settings.movableByWindowBackground=\(settings.movableByWindowBackground) settings.titled=\(settings.titled) settings.closable=\(settings.closable) settings.miniaturizable=\(settings.miniaturizable) settings.resizable=\(settings.resizable) settings.fullSizeContentView=\(settings.fullSizeContentView) settings.showToolbar=\(settings.showToolbar) settings.toolbarStyle=\(settings.toolbarStyle.rawValue) about.overridesEnabled=\(about.overridesEnabled) about.title=\(about.windowTitle) about.titleVisibility=\(about.titleVisibility.rawValue) about.titlebarAppearsTransparent=\(about.titlebarAppearsTransparent) about.movableByWindowBackground=\(about.movableByWindowBackground) about.titled=\(about.titled) about.closable=\(about.closable) about.miniaturizable=\(about.miniaturizable) about.resizable=\(about.resizable) about.fullSizeContentView=\(about.fullSizeContentView) about.showToolbar=\(about.showToolbar) about.toolbarStyle=\(about.toolbarStyle.rawValue) """ let pasteboard = NSPasteboard.general pasteboard.clearContents() pasteboard.setString(payload, forType: .string) } private func apply(_ options: SettingsAboutTitlebarDebugOptions, to window: NSWindow, for kind: SettingsAboutWindowKind) { let effective = options.overridesEnabled ? options : SettingsAboutTitlebarDebugOptions.defaults(for: kind) let resolvedTitle = effective.windowTitle.trimmingCharacters(in: .whitespacesAndNewlines) window.title = resolvedTitle.isEmpty ? kind.fallbackTitle : resolvedTitle window.titleVisibility = effective.titleVisibility.windowValue window.titlebarAppearsTransparent = effective.titlebarAppearsTransparent window.isMovableByWindowBackground = effective.movableByWindowBackground window.toolbarStyle = effective.toolbarStyle.windowValue if effective.showToolbar { ensureToolbar(on: window, kind: kind) } else if window.toolbar != nil { window.toolbar = nil } var styleMask = window.styleMask setStyleMaskBit(&styleMask, .titled, enabled: effective.titled) setStyleMaskBit(&styleMask, .closable, enabled: effective.closable) setStyleMaskBit(&styleMask, .miniaturizable, enabled: effective.miniaturizable) setStyleMaskBit(&styleMask, .resizable, enabled: effective.resizable) setStyleMaskBit(&styleMask, .fullSizeContentView, enabled: effective.fullSizeContentView) window.styleMask = styleMask let maxSize = effective.resizable ? NSSize(width: 8192, height: 8192) : kind.minimumSize window.minSize = kind.minimumSize window.maxSize = maxSize window.contentMinSize = kind.minimumSize window.contentMaxSize = maxSize window.invalidateShadow() AppDelegate.shared?.applyWindowDecorations(to: window) } private func ensureToolbar(on window: NSWindow, kind: SettingsAboutWindowKind) { guard window.toolbar == nil else { return } let identifier = NSToolbar.Identifier("cmux.debug.titlebar.\(kind.rawValue)") let toolbar = NSToolbar(identifier: identifier) toolbar.allowsUserCustomization = false toolbar.autosavesConfiguration = false toolbar.displayMode = .iconOnly toolbar.showsBaselineSeparator = false window.toolbar = toolbar } private func setStyleMaskBit( _ styleMask: inout NSWindow.StyleMask, _ bit: NSWindow.StyleMask, enabled: Bool ) { if enabled { styleMask.insert(bit) } else { styleMask.remove(bit) } } } private final class SettingsAboutTitlebarDebugWindowController: NSWindowController, NSWindowDelegate { static let shared = SettingsAboutTitlebarDebugWindowController() private init() { let window = NSPanel( contentRect: NSRect(x: 0, y: 0, width: 470, height: 690), styleMask: [.titled, .closable, .resizable, .utilityWindow], backing: .buffered, defer: false ) window.title = "Settings/About Titlebar Debug" window.titleVisibility = .visible window.titlebarAppearsTransparent = false window.isMovableByWindowBackground = true window.isReleasedWhenClosed = false window.identifier = NSUserInterfaceItemIdentifier("cmux.settingsAboutTitlebarDebug") window.center() window.contentView = NSHostingView(rootView: SettingsAboutTitlebarDebugView()) AppDelegate.shared?.applyWindowDecorations(to: window) super.init(window: window) window.delegate = self } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func show() { window?.center() window?.makeKeyAndOrderFront(nil) SettingsAboutTitlebarDebugStore.shared.applyToOpenWindows() } } private struct SettingsAboutTitlebarDebugView: View { @ObservedObject private var store = SettingsAboutTitlebarDebugStore.shared var body: some View { ScrollView { VStack(alignment: .leading, spacing: 14) { Text("Settings/About Titlebar Debug") .font(.headline) editor(for: .settings) editor(for: .about) GroupBox("Actions") { HStack(spacing: 10) { Button("Reset All") { store.reset(.settings) store.reset(.about) } Button("Reapply to Open Windows") { store.applyToOpenWindows() } Button("Copy Config") { store.copyConfigToPasteboard() } } .frame(maxWidth: .infinity, alignment: .leading) .padding(.top, 2) } Spacer(minLength: 0) } .padding(16) .frame(maxWidth: .infinity, alignment: .topLeading) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } private func editor(for kind: SettingsAboutWindowKind) -> some View { let overridesEnabled = binding(for: kind, keyPath: \.overridesEnabled) return GroupBox(kind.displayTitle) { VStack(alignment: .leading, spacing: 10) { Toggle("Enable Debug Overrides", isOn: overridesEnabled) Text("When disabled, cmux uses normal default titlebar behavior for this window.") .font(.caption) .foregroundColor(.secondary) Divider() VStack(alignment: .leading, spacing: 10) { HStack(spacing: 8) { Text("Window Title") TextField("", text: binding(for: kind, keyPath: \.windowTitle)) } HStack(spacing: 10) { Picker("Title Visibility", selection: binding(for: kind, keyPath: \.titleVisibility)) { ForEach(TitlebarVisibilityOption.allCases) { option in Text(option.displayTitle).tag(option) } } Picker("Toolbar Style", selection: binding(for: kind, keyPath: \.toolbarStyle)) { ForEach(TitlebarToolbarStyleOption.allCases) { option in Text(option.displayTitle).tag(option) } } } Toggle("Show Toolbar", isOn: binding(for: kind, keyPath: \.showToolbar)) Toggle("Transparent Titlebar", isOn: binding(for: kind, keyPath: \.titlebarAppearsTransparent)) Toggle("Movable by Window Background", isOn: binding(for: kind, keyPath: \.movableByWindowBackground)) Divider() Text("Style Mask") .font(.caption) .foregroundColor(.secondary) Toggle("Titled", isOn: binding(for: kind, keyPath: \.titled)) Toggle("Closable", isOn: binding(for: kind, keyPath: \.closable)) Toggle("Miniaturizable", isOn: binding(for: kind, keyPath: \.miniaturizable)) Toggle("Resizable", isOn: binding(for: kind, keyPath: \.resizable)) Toggle("Full Size Content View", isOn: binding(for: kind, keyPath: \.fullSizeContentView)) HStack(spacing: 10) { Button("Reset \(kind == .settings ? "Settings" : "About")") { store.reset(kind) } Button("Apply Now") { store.applyToOpenWindows(for: kind) } } } .disabled(!overridesEnabled.wrappedValue) .opacity(overridesEnabled.wrappedValue ? 1 : 0.75) } .padding(.top, 2) } } private func binding( for kind: SettingsAboutWindowKind, keyPath: WritableKeyPath ) -> Binding { Binding( get: { store.options(for: kind)[keyPath: keyPath] }, set: { newValue in var updated = store.options(for: kind) updated[keyPath: keyPath] = newValue store.update(updated, for: kind) } ) } } private enum DebugWindowConfigSnapshot { static func copyCombinedToPasteboard(defaults: UserDefaults = .standard) { let pasteboard = NSPasteboard.general pasteboard.clearContents() pasteboard.setString(combinedPayload(defaults: defaults), forType: .string) } static func combinedPayload(defaults: UserDefaults = .standard) -> String { let sidebarPayload = """ sidebarPreset=\(stringValue(defaults, key: "sidebarPreset", fallback: SidebarPresetOption.nativeSidebar.rawValue)) sidebarMaterial=\(stringValue(defaults, key: "sidebarMaterial", fallback: SidebarMaterialOption.sidebar.rawValue)) sidebarBlendMode=\(stringValue(defaults, key: "sidebarBlendMode", fallback: SidebarBlendModeOption.withinWindow.rawValue)) 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")) 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)) sidebarActiveTabIndicatorStyle=\(stringValue(defaults, key: SidebarActiveTabIndicatorSettings.styleKey, fallback: SidebarActiveTabIndicatorSettings.defaultStyle.rawValue)) shortcutHintSidebarXOffset=\(String(format: "%.1f", doubleValue(defaults, key: ShortcutHintDebugSettings.sidebarHintXKey, fallback: ShortcutHintDebugSettings.defaultSidebarHintX))) shortcutHintSidebarYOffset=\(String(format: "%.1f", doubleValue(defaults, key: ShortcutHintDebugSettings.sidebarHintYKey, fallback: ShortcutHintDebugSettings.defaultSidebarHintY))) shortcutHintTitlebarXOffset=\(String(format: "%.1f", doubleValue(defaults, key: ShortcutHintDebugSettings.titlebarHintXKey, fallback: ShortcutHintDebugSettings.defaultTitlebarHintX))) shortcutHintTitlebarYOffset=\(String(format: "%.1f", doubleValue(defaults, key: ShortcutHintDebugSettings.titlebarHintYKey, fallback: ShortcutHintDebugSettings.defaultTitlebarHintY))) shortcutHintPaneTabXOffset=\(String(format: "%.1f", doubleValue(defaults, key: ShortcutHintDebugSettings.paneHintXKey, fallback: ShortcutHintDebugSettings.defaultPaneHintX))) shortcutHintPaneTabYOffset=\(String(format: "%.1f", doubleValue(defaults, key: ShortcutHintDebugSettings.paneHintYKey, fallback: ShortcutHintDebugSettings.defaultPaneHintY))) shortcutHintAlwaysShow=\(boolValue(defaults, key: ShortcutHintDebugSettings.alwaysShowHintsKey, fallback: ShortcutHintDebugSettings.defaultAlwaysShowHints)) shortcutHintShowOnCommandHold=\(boolValue(defaults, key: ShortcutHintDebugSettings.showHintsOnCommandHoldKey, fallback: ShortcutHintDebugSettings.defaultShowHintsOnCommandHold)) """ let backgroundPayload = """ bgGlassEnabled=\(boolValue(defaults, key: "bgGlassEnabled", fallback: false)) bgGlassMaterial=\(stringValue(defaults, key: "bgGlassMaterial", fallback: "hudWindow")) bgGlassTintHex=\(stringValue(defaults, key: "bgGlassTintHex", fallback: "#000000")) bgGlassTintOpacity=\(String(format: "%.2f", doubleValue(defaults, key: "bgGlassTintOpacity", fallback: 0.03))) """ let menuBarPayload = MenuBarIconDebugSettings.copyPayload(defaults: defaults) let browserDevToolsPayload = BrowserDevToolsButtonDebugSettings.copyPayload(defaults: defaults) return """ # Sidebar Debug \(sidebarPayload) # Background Debug \(backgroundPayload) # Menu Bar Extra Debug \(menuBarPayload) # Browser DevTools Button \(browserDevToolsPayload) """ } private static func stringValue(_ defaults: UserDefaults, key: String, fallback: String) -> String { defaults.string(forKey: key) ?? fallback } private static func doubleValue(_ defaults: UserDefaults, key: String, fallback: Double) -> Double { if let value = defaults.object(forKey: key) as? NSNumber { return value.doubleValue } if let text = defaults.string(forKey: key), let parsed = Double(text) { return parsed } return fallback } private static func boolValue(_ defaults: UserDefaults, key: String, fallback: Bool) -> Bool { guard defaults.object(forKey: key) != nil else { return fallback } return defaults.bool(forKey: key) } } private final class DebugWindowControlsWindowController: NSWindowController, NSWindowDelegate { static let shared = DebugWindowControlsWindowController() private init() { let window = NSPanel( contentRect: NSRect(x: 0, y: 0, width: 420, height: 560), styleMask: [.titled, .closable, .utilityWindow], backing: .buffered, defer: false ) window.title = "Debug Window Controls" window.titleVisibility = .visible window.titlebarAppearsTransparent = false window.isMovableByWindowBackground = true window.isReleasedWhenClosed = false window.identifier = NSUserInterfaceItemIdentifier("cmux.debugWindowControls") window.center() window.contentView = NSHostingView(rootView: DebugWindowControlsView()) AppDelegate.shared?.applyWindowDecorations(to: window) super.init(window: window) window.delegate = self } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func show() { window?.center() window?.makeKeyAndOrderFront(nil) } } private struct DebugWindowControlsView: View { @AppStorage(ShortcutHintDebugSettings.sidebarHintXKey) private var sidebarShortcutHintXOffset = ShortcutHintDebugSettings.defaultSidebarHintX @AppStorage(ShortcutHintDebugSettings.sidebarHintYKey) private var sidebarShortcutHintYOffset = ShortcutHintDebugSettings.defaultSidebarHintY @AppStorage(ShortcutHintDebugSettings.titlebarHintXKey) private var titlebarShortcutHintXOffset = ShortcutHintDebugSettings.defaultTitlebarHintX @AppStorage(ShortcutHintDebugSettings.titlebarHintYKey) private var titlebarShortcutHintYOffset = ShortcutHintDebugSettings.defaultTitlebarHintY @AppStorage(ShortcutHintDebugSettings.paneHintXKey) private var paneShortcutHintXOffset = ShortcutHintDebugSettings.defaultPaneHintX @AppStorage(ShortcutHintDebugSettings.paneHintYKey) private var paneShortcutHintYOffset = ShortcutHintDebugSettings.defaultPaneHintY @AppStorage(ShortcutHintDebugSettings.alwaysShowHintsKey) private var alwaysShowShortcutHints = ShortcutHintDebugSettings.defaultAlwaysShowHints @AppStorage(SidebarActiveTabIndicatorSettings.styleKey) private var sidebarActiveTabIndicatorStyle = SidebarActiveTabIndicatorSettings.defaultStyle.rawValue @AppStorage("debugTitlebarLeadingExtra") private var titlebarLeadingExtra: Double = 0 @AppStorage(BrowserDevToolsButtonDebugSettings.iconNameKey) private var browserDevToolsIconNameRaw = BrowserDevToolsButtonDebugSettings.defaultIcon.rawValue @AppStorage(BrowserDevToolsButtonDebugSettings.iconColorKey) private var browserDevToolsIconColorRaw = BrowserDevToolsButtonDebugSettings.defaultColor.rawValue private var selectedDevToolsIconOption: BrowserDevToolsIconOption { BrowserDevToolsIconOption(rawValue: browserDevToolsIconNameRaw) ?? BrowserDevToolsButtonDebugSettings.defaultIcon } private var selectedDevToolsColorOption: BrowserDevToolsIconColorOption { BrowserDevToolsIconColorOption(rawValue: browserDevToolsIconColorRaw) ?? BrowserDevToolsButtonDebugSettings.defaultColor } private var selectedSidebarActiveTabIndicatorStyle: SidebarActiveTabIndicatorStyle { SidebarActiveTabIndicatorSettings.resolvedStyle(rawValue: sidebarActiveTabIndicatorStyle) } private var sidebarIndicatorStyleSelection: Binding { Binding( get: { selectedSidebarActiveTabIndicatorStyle.rawValue }, set: { sidebarActiveTabIndicatorStyle = $0 } ) } var body: some View { ScrollView { VStack(alignment: .leading, spacing: 14) { Text("Debug Window Controls") .font(.headline) GroupBox("Open") { VStack(alignment: .leading, spacing: 8) { Button("Settings/About Titlebar Debug…") { SettingsAboutTitlebarDebugWindowController.shared.show() } Button("Sidebar Debug…") { SidebarDebugWindowController.shared.show() } Button("Background Debug…") { BackgroundDebugWindowController.shared.show() } Button("Menu Bar Extra Debug…") { MenuBarExtraDebugWindowController.shared.show() } Button("Open All Debug Windows") { SettingsAboutTitlebarDebugWindowController.shared.show() SidebarDebugWindowController.shared.show() BackgroundDebugWindowController.shared.show() MenuBarExtraDebugWindowController.shared.show() } } .frame(maxWidth: .infinity, alignment: .leading) .padding(.top, 2) } GroupBox("Shortcut Hints") { VStack(alignment: .leading, spacing: 10) { Toggle("Always show shortcut hints", isOn: $alwaysShowShortcutHints) hintOffsetSection( "Sidebar Cmd+1…9", x: $sidebarShortcutHintXOffset, y: $sidebarShortcutHintYOffset ) hintOffsetSection( "Titlebar Buttons", x: $titlebarShortcutHintXOffset, y: $titlebarShortcutHintYOffset ) hintOffsetSection( "Pane Ctrl/Cmd+1…9", x: $paneShortcutHintXOffset, y: $paneShortcutHintYOffset ) HStack(spacing: 12) { Button("Reset Hints") { resetShortcutHintOffsets() } Button("Copy Hint Config") { copyShortcutHintConfig() } } } .padding(.top, 2) } GroupBox("Active Workspace Indicator") { VStack(alignment: .leading, spacing: 8) { Picker("Style", selection: sidebarIndicatorStyleSelection) { ForEach(SidebarActiveTabIndicatorStyle.allCases) { style in Text(style.displayName).tag(style.rawValue) } } .pickerStyle(.menu) Button("Reset Indicator Style") { sidebarActiveTabIndicatorStyle = SidebarActiveTabIndicatorSettings.defaultStyle.rawValue } } .padding(.top, 2) } GroupBox("Titlebar Spacing") { VStack(alignment: .leading, spacing: 6) { HStack(spacing: 8) { Text("Leading extra") Slider(value: $titlebarLeadingExtra, in: 0...40) Text(String(format: "%.0f", titlebarLeadingExtra)) .font(.caption) .monospacedDigit() .frame(width: 30, alignment: .trailing) } Button("Reset (0)") { titlebarLeadingExtra = 0 } } .padding(.top, 2) } GroupBox("Browser DevTools Button") { VStack(alignment: .leading, spacing: 10) { HStack(spacing: 8) { Text("Icon") Picker("Icon", selection: $browserDevToolsIconNameRaw) { ForEach(BrowserDevToolsIconOption.allCases) { option in Text(option.title).tag(option.rawValue) } } .labelsHidden() .pickerStyle(.menu) Spacer() } HStack(spacing: 8) { Text("Color") Picker("Color", selection: $browserDevToolsIconColorRaw) { ForEach(BrowserDevToolsIconColorOption.allCases) { option in Text(option.title).tag(option.rawValue) } } .labelsHidden() .pickerStyle(.menu) Spacer() } HStack(spacing: 8) { Text("Preview") Spacer() Image(systemName: selectedDevToolsIconOption.rawValue) .font(.system(size: 12, weight: .medium)) .foregroundStyle(selectedDevToolsColorOption.color) } HStack(spacing: 12) { Button("Reset Button") { resetBrowserDevToolsButton() } Button("Copy Button Config") { copyBrowserDevToolsButtonConfig() } } } .padding(.top, 2) } GroupBox("Copy") { VStack(alignment: .leading, spacing: 8) { Button("Copy All Debug Config") { DebugWindowConfigSnapshot.copyCombinedToPasteboard() } Text("Copies sidebar, background, menu bar, and browser devtools settings as one payload.") .font(.caption) .foregroundColor(.secondary) } .frame(maxWidth: .infinity, alignment: .leading) .padding(.top, 2) } Spacer(minLength: 0) } .padding(16) .frame(maxWidth: .infinity, alignment: .topLeading) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } private func hintOffsetSection(_ title: String, x: Binding, y: Binding) -> some View { VStack(alignment: .leading, spacing: 6) { Text(title) .font(.caption) .foregroundColor(.secondary) sliderRow("X", value: x) sliderRow("Y", value: y) } } private func sliderRow(_ label: String, value: Binding) -> some View { HStack(spacing: 8) { Text(label) Slider(value: value, in: ShortcutHintDebugSettings.offsetRange) Text(String(format: "%.1f", ShortcutHintDebugSettings.clamped(value.wrappedValue))) .font(.caption) .monospacedDigit() .frame(width: 44, alignment: .trailing) } } private func resetShortcutHintOffsets() { sidebarShortcutHintXOffset = ShortcutHintDebugSettings.defaultSidebarHintX sidebarShortcutHintYOffset = ShortcutHintDebugSettings.defaultSidebarHintY titlebarShortcutHintXOffset = ShortcutHintDebugSettings.defaultTitlebarHintX titlebarShortcutHintYOffset = ShortcutHintDebugSettings.defaultTitlebarHintY paneShortcutHintXOffset = ShortcutHintDebugSettings.defaultPaneHintX paneShortcutHintYOffset = ShortcutHintDebugSettings.defaultPaneHintY alwaysShowShortcutHints = ShortcutHintDebugSettings.defaultAlwaysShowHints } private func copyShortcutHintConfig() { let payload = """ shortcutHintSidebarXOffset=\(String(format: "%.1f", ShortcutHintDebugSettings.clamped(sidebarShortcutHintXOffset))) shortcutHintSidebarYOffset=\(String(format: "%.1f", ShortcutHintDebugSettings.clamped(sidebarShortcutHintYOffset))) shortcutHintTitlebarXOffset=\(String(format: "%.1f", ShortcutHintDebugSettings.clamped(titlebarShortcutHintXOffset))) shortcutHintTitlebarYOffset=\(String(format: "%.1f", ShortcutHintDebugSettings.clamped(titlebarShortcutHintYOffset))) shortcutHintPaneTabXOffset=\(String(format: "%.1f", ShortcutHintDebugSettings.clamped(paneShortcutHintXOffset))) shortcutHintPaneTabYOffset=\(String(format: "%.1f", ShortcutHintDebugSettings.clamped(paneShortcutHintYOffset))) shortcutHintAlwaysShow=\(alwaysShowShortcutHints) """ let pasteboard = NSPasteboard.general pasteboard.clearContents() pasteboard.setString(payload, forType: .string) } private func resetBrowserDevToolsButton() { browserDevToolsIconNameRaw = BrowserDevToolsButtonDebugSettings.defaultIcon.rawValue browserDevToolsIconColorRaw = BrowserDevToolsButtonDebugSettings.defaultColor.rawValue } private func copyBrowserDevToolsButtonConfig() { let payload = BrowserDevToolsButtonDebugSettings.copyPayload(defaults: .standard) let pasteboard = NSPasteboard.general pasteboard.clearContents() pasteboard.setString(payload, forType: .string) } } private final class AboutWindowController: NSWindowController, NSWindowDelegate { static let shared = AboutWindowController() private init() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 360, height: 520), styleMask: [.titled, .closable, .miniaturizable], backing: .buffered, defer: false ) window.isReleasedWhenClosed = false window.identifier = NSUserInterfaceItemIdentifier("cmux.about") window.center() window.contentView = NSHostingView(rootView: AboutPanelView()) SettingsAboutTitlebarDebugStore.shared.applyCurrentOptions(to: window, for: .about) AppDelegate.shared?.applyWindowDecorations(to: window) super.init(window: window) window.delegate = self } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func show() { guard let window else { return } SettingsAboutTitlebarDebugStore.shared.applyCurrentOptions(to: window, for: .about) window.center() window.makeKeyAndOrderFront(nil) } } private final class AcknowledgmentsWindowController: NSWindowController, NSWindowDelegate { static let shared = AcknowledgmentsWindowController() private init() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 500, height: 480), styleMask: [.titled, .closable, .miniaturizable, .resizable], backing: .buffered, defer: false ) window.isReleasedWhenClosed = false window.title = String(localized: "about.licenses.windowTitle", defaultValue: "Third-Party Licenses") window.identifier = NSUserInterfaceItemIdentifier("cmux.licenses") window.center() window.contentView = NSHostingView(rootView: AcknowledgmentsView()) super.init(window: window) window.delegate = self } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func show() { guard let window else { return } window.makeKeyAndOrderFront(nil) } } private struct AcknowledgmentsView: View { private let content: String = { if let url = Bundle.main.url(forResource: "THIRD_PARTY_LICENSES", withExtension: "md"), let text = try? String(contentsOf: url) { return text } return String(localized: "about.licenses.notFound", defaultValue: "Licenses file not found.") }() var body: some View { ScrollView { Text(content) .font(.system(.body, design: .monospaced)) .textSelection(.enabled) .frame(maxWidth: .infinity, alignment: .leading) .padding() } } } final class SettingsWindowController: NSWindowController, NSWindowDelegate { static let shared = SettingsWindowController() private init() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 640, height: 520), styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], backing: .buffered, defer: false ) window.isReleasedWhenClosed = false window.identifier = NSUserInterfaceItemIdentifier("cmux.settings") window.center() window.contentView = NSHostingView(rootView: SettingsRootView()) SettingsAboutTitlebarDebugStore.shared.applyCurrentOptions(to: window, for: .settings) AppDelegate.shared?.applyWindowDecorations(to: window) super.init(window: window) window.delegate = self } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func show() { guard let window else { return } #if DEBUG dlog("settings.window.show requested isVisible=\(window.isVisible ? 1 : 0) isKey=\(window.isKeyWindow ? 1 : 0)") #endif SettingsAboutTitlebarDebugStore.shared.applyCurrentOptions(to: window, for: .settings) if !window.isVisible { window.center() } window.makeKeyAndOrderFront(nil) #if DEBUG dlog("settings.window.show completed isVisible=\(window.isVisible ? 1 : 0) isKey=\(window.isKeyWindow ? 1 : 0)") #endif } } private final class SidebarDebugWindowController: NSWindowController, NSWindowDelegate { static let shared = SidebarDebugWindowController() private init() { let window = NSPanel( contentRect: NSRect(x: 0, y: 0, width: 360, height: 520), styleMask: [.titled, .closable, .utilityWindow], backing: .buffered, defer: false ) window.title = "Sidebar Debug" window.titleVisibility = .visible window.titlebarAppearsTransparent = false window.isMovableByWindowBackground = true window.isReleasedWhenClosed = false window.identifier = NSUserInterfaceItemIdentifier("cmux.sidebarDebug") window.center() window.contentView = NSHostingView(rootView: SidebarDebugView()) AppDelegate.shared?.applyWindowDecorations(to: window) super.init(window: window) window.delegate = self } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func show() { window?.center() window?.makeKeyAndOrderFront(nil) } } private struct AboutPanelView: View { @Environment(\.openURL) private var openURL private let githubURL = URL(string: "https://github.com/manaflow-ai/cmux") private let docsURL = URL(string: "https://cmux.dev/docs") private var version: String? { Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String } private var build: String? { Bundle.main.infoDictionary?["CFBundleVersion"] as? String } private var commit: String? { if let value = Bundle.main.infoDictionary?["CMUXCommit"] as? String, !value.isEmpty { return value } let env = ProcessInfo.processInfo.environment["CMUX_COMMIT"] ?? "" return env.isEmpty ? nil : env } private var copyright: String? { Bundle.main.infoDictionary?["NSHumanReadableCopyright"] as? String } var body: some View { VStack(alignment: .center) { Image(nsImage: NSApplication.shared.applicationIconImage) .resizable() .renderingMode(.original) .frame(width: 96, height: 96) .shadow(color: .black.opacity(0.18), radius: 8, x: 0, y: 3) VStack(alignment: .center, spacing: 32) { VStack(alignment: .center, spacing: 8) { Text(String(localized: "about.appName", defaultValue: "cmux")) .bold() .font(.title) Text(String(localized: "about.description", defaultValue: "A Ghostty-based terminal with vertical tabs\nand a notification panel for macOS.")) .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) .font(.caption) .tint(.secondary) .opacity(0.8) } .textSelection(.enabled) VStack(spacing: 2) { if let version { AboutPropertyRow(label: String(localized: "about.version", defaultValue: "Version"), text: version) } if let build { AboutPropertyRow(label: String(localized: "about.build", defaultValue: "Build"), text: build) } let commitText = commit ?? "—" let commitURL = commit.flatMap { hash in URL(string: "https://github.com/manaflow-ai/cmux/commit/\(hash)") } AboutPropertyRow(label: String(localized: "about.commit", defaultValue: "Commit"), text: commitText, url: commitURL) } .frame(maxWidth: .infinity) HStack(spacing: 8) { if let url = docsURL { Button(String(localized: "about.docs", defaultValue: "Docs")) { openURL(url) } } if let url = githubURL { Button(String(localized: "about.github", defaultValue: "GitHub")) { openURL(url) } } Button(String(localized: "about.licenses", defaultValue: "Licenses")) { AcknowledgmentsWindowController.shared.show() } } if let copy = copyright, !copy.isEmpty { Text(copy) .font(.caption) .textSelection(.enabled) .tint(.secondary) .opacity(0.8) .multilineTextAlignment(.center) .frame(maxWidth: .infinity) } } .frame(maxWidth: .infinity) } .padding(.top, 8) .padding(32) .frame(minWidth: 280) .background(AboutVisualEffectBackground(material: .underWindowBackground).ignoresSafeArea()) } } 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("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 @AppStorage(SidebarBranchLayoutSettings.key) private var sidebarBranchVerticalLayout = SidebarBranchLayoutSettings.defaultVerticalLayout @AppStorage(ShortcutHintDebugSettings.sidebarHintXKey) private var sidebarShortcutHintXOffset = ShortcutHintDebugSettings.defaultSidebarHintX @AppStorage(ShortcutHintDebugSettings.sidebarHintYKey) private var sidebarShortcutHintYOffset = ShortcutHintDebugSettings.defaultSidebarHintY @AppStorage(ShortcutHintDebugSettings.titlebarHintXKey) private var titlebarShortcutHintXOffset = ShortcutHintDebugSettings.defaultTitlebarHintX @AppStorage(ShortcutHintDebugSettings.titlebarHintYKey) private var titlebarShortcutHintYOffset = ShortcutHintDebugSettings.defaultTitlebarHintY @AppStorage(ShortcutHintDebugSettings.paneHintXKey) private var paneShortcutHintXOffset = ShortcutHintDebugSettings.defaultPaneHintX @AppStorage(ShortcutHintDebugSettings.paneHintYKey) private var paneShortcutHintYOffset = ShortcutHintDebugSettings.defaultPaneHintY @AppStorage(ShortcutHintDebugSettings.alwaysShowHintsKey) private var alwaysShowShortcutHints = ShortcutHintDebugSettings.defaultAlwaysShowHints @AppStorage(SidebarActiveTabIndicatorSettings.styleKey) private var sidebarActiveTabIndicatorStyle = SidebarActiveTabIndicatorSettings.defaultStyle.rawValue private var selectedSidebarIndicatorStyle: SidebarActiveTabIndicatorStyle { SidebarActiveTabIndicatorSettings.resolvedStyle(rawValue: sidebarActiveTabIndicatorStyle) } private var sidebarIndicatorStyleSelection: Binding { Binding( get: { selectedSidebarIndicatorStyle.rawValue }, set: { sidebarActiveTabIndicatorStyle = $0 } ) } var body: some View { ScrollView { VStack(alignment: .leading, spacing: 14) { Text("Sidebar Appearance") .font(.headline) GroupBox("Presets") { Picker("Preset", selection: $sidebarPreset) { ForEach(SidebarPresetOption.allCases) { option in Text(option.title).tag(option.rawValue) } } .onChange(of: sidebarPreset) { _ in applyPreset() } .padding(.top, 2) } GroupBox("Blur") { VStack(alignment: .leading, spacing: 8) { Picker("Material", selection: $sidebarMaterial) { ForEach(SidebarMaterialOption.allCases) { option in Text(option.title).tag(option.rawValue) } } Picker("Blending", selection: $sidebarBlendMode) { ForEach(SidebarBlendModeOption.allCases) { option in Text(option.title).tag(option.rawValue) } } Picker("State", selection: $sidebarState) { ForEach(SidebarStateOption.allCases) { option in Text(option.title).tag(option.rawValue) } } HStack(spacing: 8) { Text("Strength") Slider(value: $sidebarBlurOpacity, in: 0...1) Text(String(format: "%.0f%%", sidebarBlurOpacity * 100)) .font(.caption) .frame(width: 44, alignment: .trailing) } } .padding(.top, 2) } GroupBox("Tint") { VStack(alignment: .leading, spacing: 8) { ColorPicker("Tint Color", selection: tintColorBinding, supportsOpacity: false) HStack(spacing: 8) { Text("Opacity") Slider(value: $sidebarTintOpacity, in: 0...0.7) Text(String(format: "%.0f%%", sidebarTintOpacity * 100)) .font(.caption) .frame(width: 44, alignment: .trailing) } } .padding(.top, 2) } GroupBox("Shape") { HStack(spacing: 8) { Text("Corner Radius") Slider(value: $sidebarCornerRadius, in: 0...20) Text(String(format: "%.0f", sidebarCornerRadius)) .font(.caption) .frame(width: 32, alignment: .trailing) } .padding(.top, 2) } GroupBox("Shortcut Hints") { VStack(alignment: .leading, spacing: 10) { Toggle("Always show shortcut hints", isOn: $alwaysShowShortcutHints) hintOffsetSection( "Sidebar Cmd+1…9", x: $sidebarShortcutHintXOffset, y: $sidebarShortcutHintYOffset ) hintOffsetSection( "Titlebar Buttons", x: $titlebarShortcutHintXOffset, y: $titlebarShortcutHintYOffset ) hintOffsetSection( "Pane Ctrl/Cmd+1…9", x: $paneShortcutHintXOffset, y: $paneShortcutHintYOffset ) } .padding(.top, 2) } GroupBox("Active Workspace Indicator") { VStack(alignment: .leading, spacing: 8) { Picker("Style", selection: sidebarIndicatorStyleSelection) { ForEach(SidebarActiveTabIndicatorStyle.allCases) { style in Text(style.displayName).tag(style.rawValue) } } } .padding(.top, 2) } GroupBox("Workspace Metadata") { VStack(alignment: .leading, spacing: 8) { Toggle("Render branch list vertically", isOn: $sidebarBranchVerticalLayout) Text("When enabled, each branch appears on its own line in the sidebar.") .font(.caption) .foregroundColor(.secondary) } .padding(.top, 2) } HStack(spacing: 12) { Button("Reset Tint") { sidebarTintOpacity = 0.62 sidebarTintHex = "#000000" } Button("Reset Blur") { sidebarMaterial = SidebarMaterialOption.hudWindow.rawValue sidebarBlendMode = SidebarBlendModeOption.withinWindow.rawValue sidebarState = SidebarStateOption.active.rawValue sidebarBlurOpacity = 0.98 } Button("Reset Shape") { sidebarCornerRadius = 0.0 } Button("Reset Hints") { resetShortcutHintOffsets() } Button("Reset Active Indicator") { sidebarActiveTabIndicatorStyle = SidebarActiveTabIndicatorSettings.defaultStyle.rawValue } } Button("Copy Config") { copySidebarConfig() } Spacer(minLength: 0) } .padding(16) .frame(maxWidth: .infinity, alignment: .topLeading) } } private var tintColorBinding: Binding { Binding( get: { Color(nsColor: NSColor(hex: sidebarTintHex) ?? .black) }, set: { newColor in let nsColor = NSColor(newColor) sidebarTintHex = nsColor.hexString() } ) } private func hintOffsetSection(_ title: String, x: Binding, y: Binding) -> some View { VStack(alignment: .leading, spacing: 6) { Text(title) .font(.caption) .foregroundColor(.secondary) sliderRow("X", value: x) sliderRow("Y", value: y) } } private func sliderRow(_ label: String, value: Binding) -> some View { HStack(spacing: 8) { Text(label) Slider(value: value, in: ShortcutHintDebugSettings.offsetRange) Text(String(format: "%.1f", ShortcutHintDebugSettings.clamped(value.wrappedValue))) .font(.caption) .monospacedDigit() .frame(width: 44, alignment: .trailing) } } private func resetShortcutHintOffsets() { sidebarShortcutHintXOffset = ShortcutHintDebugSettings.defaultSidebarHintX sidebarShortcutHintYOffset = ShortcutHintDebugSettings.defaultSidebarHintY titlebarShortcutHintXOffset = ShortcutHintDebugSettings.defaultTitlebarHintX titlebarShortcutHintYOffset = ShortcutHintDebugSettings.defaultTitlebarHintY paneShortcutHintXOffset = ShortcutHintDebugSettings.defaultPaneHintX paneShortcutHintYOffset = ShortcutHintDebugSettings.defaultPaneHintY alwaysShowShortcutHints = ShortcutHintDebugSettings.defaultAlwaysShowHints } private func copySidebarConfig() { let payload = """ sidebarPreset=\(sidebarPreset) sidebarMaterial=\(sidebarMaterial) sidebarBlendMode=\(sidebarBlendMode) sidebarState=\(sidebarState) sidebarBlurOpacity=\(String(format: "%.2f", sidebarBlurOpacity)) sidebarTintHex=\(sidebarTintHex) sidebarTintOpacity=\(String(format: "%.2f", sidebarTintOpacity)) sidebarCornerRadius=\(String(format: "%.1f", sidebarCornerRadius)) sidebarBranchVerticalLayout=\(sidebarBranchVerticalLayout) sidebarActiveTabIndicatorStyle=\(sidebarActiveTabIndicatorStyle) shortcutHintSidebarXOffset=\(String(format: "%.1f", ShortcutHintDebugSettings.clamped(sidebarShortcutHintXOffset))) shortcutHintSidebarYOffset=\(String(format: "%.1f", ShortcutHintDebugSettings.clamped(sidebarShortcutHintYOffset))) shortcutHintTitlebarXOffset=\(String(format: "%.1f", ShortcutHintDebugSettings.clamped(titlebarShortcutHintXOffset))) shortcutHintTitlebarYOffset=\(String(format: "%.1f", ShortcutHintDebugSettings.clamped(titlebarShortcutHintYOffset))) shortcutHintPaneTabXOffset=\(String(format: "%.1f", ShortcutHintDebugSettings.clamped(paneShortcutHintXOffset))) shortcutHintPaneTabYOffset=\(String(format: "%.1f", ShortcutHintDebugSettings.clamped(paneShortcutHintYOffset))) shortcutHintAlwaysShow=\(alwaysShowShortcutHints) """ let pasteboard = NSPasteboard.general pasteboard.clearContents() pasteboard.setString(payload, forType: .string) } private func applyPreset() { guard let preset = SidebarPresetOption(rawValue: sidebarPreset) else { return } sidebarMaterial = preset.material.rawValue sidebarBlendMode = preset.blendMode.rawValue sidebarState = preset.state.rawValue sidebarTintHex = preset.tintHex sidebarTintOpacity = preset.tintOpacity sidebarCornerRadius = preset.cornerRadius sidebarBlurOpacity = preset.blurOpacity } } // MARK: - Menu Bar Extra Debug Window private final class MenuBarExtraDebugWindowController: NSWindowController, NSWindowDelegate { static let shared = MenuBarExtraDebugWindowController() private init() { let window = NSPanel( contentRect: NSRect(x: 0, y: 0, width: 420, height: 430), styleMask: [.titled, .closable, .utilityWindow], backing: .buffered, defer: false ) window.title = "Menu Bar Extra Debug" window.titleVisibility = .visible window.titlebarAppearsTransparent = false window.isMovableByWindowBackground = true window.isReleasedWhenClosed = false window.identifier = NSUserInterfaceItemIdentifier("cmux.menubarDebug") window.center() window.contentView = NSHostingView(rootView: MenuBarExtraDebugView()) AppDelegate.shared?.applyWindowDecorations(to: window) super.init(window: window) window.delegate = self } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func show() { window?.center() window?.makeKeyAndOrderFront(nil) } } private struct MenuBarExtraDebugView: View { @AppStorage(MenuBarIconDebugSettings.previewEnabledKey) private var previewEnabled = false @AppStorage(MenuBarIconDebugSettings.previewCountKey) private var previewCount = 1 @AppStorage(MenuBarIconDebugSettings.badgeRectXKey) private var badgeRectX = Double(MenuBarIconDebugSettings.defaultBadgeRect.origin.x) @AppStorage(MenuBarIconDebugSettings.badgeRectYKey) private var badgeRectY = Double(MenuBarIconDebugSettings.defaultBadgeRect.origin.y) @AppStorage(MenuBarIconDebugSettings.badgeRectWidthKey) private var badgeRectWidth = Double(MenuBarIconDebugSettings.defaultBadgeRect.width) @AppStorage(MenuBarIconDebugSettings.badgeRectHeightKey) private var badgeRectHeight = Double(MenuBarIconDebugSettings.defaultBadgeRect.height) @AppStorage(MenuBarIconDebugSettings.singleDigitFontSizeKey) private var singleDigitFontSize = Double(MenuBarIconDebugSettings.defaultSingleDigitFontSize) @AppStorage(MenuBarIconDebugSettings.multiDigitFontSizeKey) private var multiDigitFontSize = Double(MenuBarIconDebugSettings.defaultMultiDigitFontSize) @AppStorage(MenuBarIconDebugSettings.singleDigitYOffsetKey) private var singleDigitYOffset = Double(MenuBarIconDebugSettings.defaultSingleDigitYOffset) @AppStorage(MenuBarIconDebugSettings.multiDigitYOffsetKey) private var multiDigitYOffset = Double(MenuBarIconDebugSettings.defaultMultiDigitYOffset) @AppStorage(MenuBarIconDebugSettings.singleDigitXAdjustKey) private var singleDigitXAdjust = Double(MenuBarIconDebugSettings.defaultSingleDigitXAdjust) @AppStorage(MenuBarIconDebugSettings.multiDigitXAdjustKey) private var multiDigitXAdjust = Double(MenuBarIconDebugSettings.defaultMultiDigitXAdjust) @AppStorage(MenuBarIconDebugSettings.textRectWidthAdjustKey) private var textRectWidthAdjust = Double(MenuBarIconDebugSettings.defaultTextRectWidthAdjust) var body: some View { ScrollView { VStack(alignment: .leading, spacing: 14) { Text("Menu Bar Extra Icon") .font(.headline) GroupBox("Preview Count") { VStack(alignment: .leading, spacing: 8) { Toggle("Override unread count", isOn: $previewEnabled) Stepper(value: $previewCount, in: 0...99) { HStack { Text("Unread Count") Spacer() Text("\(previewCount)") .font(.caption) .monospacedDigit() } } .disabled(!previewEnabled) } .padding(.top, 2) } GroupBox("Badge Rect") { VStack(alignment: .leading, spacing: 8) { sliderRow("X", value: $badgeRectX, range: 0...20, format: "%.2f") sliderRow("Y", value: $badgeRectY, range: 0...20, format: "%.2f") sliderRow("Width", value: $badgeRectWidth, range: 4...14, format: "%.2f") sliderRow("Height", value: $badgeRectHeight, range: 4...14, format: "%.2f") } .padding(.top, 2) } GroupBox("Badge Text") { VStack(alignment: .leading, spacing: 8) { sliderRow("1-digit size", value: $singleDigitFontSize, range: 6...14, format: "%.2f") sliderRow("2-digit size", value: $multiDigitFontSize, range: 6...14, format: "%.2f") sliderRow("1-digit X", value: $singleDigitXAdjust, range: -4...4, format: "%.2f") sliderRow("2-digit X", value: $multiDigitXAdjust, range: -4...4, format: "%.2f") sliderRow("1-digit Y", value: $singleDigitYOffset, range: -3...4, format: "%.2f") sliderRow("2-digit Y", value: $multiDigitYOffset, range: -3...4, format: "%.2f") sliderRow("Text width adjust", value: $textRectWidthAdjust, range: -3...5, format: "%.2f") } .padding(.top, 2) } HStack(spacing: 12) { Button("Reset") { previewEnabled = false previewCount = 1 badgeRectX = Double(MenuBarIconDebugSettings.defaultBadgeRect.origin.x) badgeRectY = Double(MenuBarIconDebugSettings.defaultBadgeRect.origin.y) badgeRectWidth = Double(MenuBarIconDebugSettings.defaultBadgeRect.width) badgeRectHeight = Double(MenuBarIconDebugSettings.defaultBadgeRect.height) singleDigitFontSize = Double(MenuBarIconDebugSettings.defaultSingleDigitFontSize) multiDigitFontSize = Double(MenuBarIconDebugSettings.defaultMultiDigitFontSize) singleDigitYOffset = Double(MenuBarIconDebugSettings.defaultSingleDigitYOffset) multiDigitYOffset = Double(MenuBarIconDebugSettings.defaultMultiDigitYOffset) singleDigitXAdjust = Double(MenuBarIconDebugSettings.defaultSingleDigitXAdjust) multiDigitXAdjust = Double(MenuBarIconDebugSettings.defaultMultiDigitXAdjust) textRectWidthAdjust = Double(MenuBarIconDebugSettings.defaultTextRectWidthAdjust) applyLiveUpdate() } Button("Copy Config") { let payload = MenuBarIconDebugSettings.copyPayload() let pasteboard = NSPasteboard.general pasteboard.clearContents() pasteboard.setString(payload, forType: .string) } } Text("Tip: enable override count, then tune until the menu bar icon looks right.") .font(.caption) .foregroundColor(.secondary) Spacer(minLength: 0) } .padding(16) .frame(maxWidth: .infinity, alignment: .topLeading) } .onAppear { applyLiveUpdate() } .onChange(of: previewEnabled) { _ in applyLiveUpdate() } .onChange(of: previewCount) { _ in applyLiveUpdate() } .onChange(of: badgeRectX) { _ in applyLiveUpdate() } .onChange(of: badgeRectY) { _ in applyLiveUpdate() } .onChange(of: badgeRectWidth) { _ in applyLiveUpdate() } .onChange(of: badgeRectHeight) { _ in applyLiveUpdate() } .onChange(of: singleDigitFontSize) { _ in applyLiveUpdate() } .onChange(of: multiDigitFontSize) { _ in applyLiveUpdate() } .onChange(of: singleDigitXAdjust) { _ in applyLiveUpdate() } .onChange(of: multiDigitXAdjust) { _ in applyLiveUpdate() } .onChange(of: singleDigitYOffset) { _ in applyLiveUpdate() } .onChange(of: multiDigitYOffset) { _ in applyLiveUpdate() } .onChange(of: textRectWidthAdjust) { _ in applyLiveUpdate() } } private func sliderRow( _ label: String, value: Binding, range: ClosedRange, format: String ) -> some View { HStack(spacing: 8) { Text(label) Slider(value: value, in: range) Text(String(format: format, value.wrappedValue)) .font(.caption) .monospacedDigit() .frame(width: 58, alignment: .trailing) } } private func applyLiveUpdate() { AppDelegate.shared?.refreshMenuBarExtraForDebug() } } // MARK: - Background Debug Window private final class BackgroundDebugWindowController: NSWindowController, NSWindowDelegate { static let shared = BackgroundDebugWindowController() private init() { let window = NSPanel( contentRect: NSRect(x: 0, y: 0, width: 360, height: 300), styleMask: [.titled, .closable, .utilityWindow], backing: .buffered, defer: false ) window.title = "Background Debug" window.titleVisibility = .visible window.titlebarAppearsTransparent = false window.isMovableByWindowBackground = true window.isReleasedWhenClosed = false window.identifier = NSUserInterfaceItemIdentifier("cmux.backgroundDebug") window.center() window.contentView = NSHostingView(rootView: BackgroundDebugView()) AppDelegate.shared?.applyWindowDecorations(to: window) super.init(window: window) window.delegate = self } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func show() { window?.center() window?.makeKeyAndOrderFront(nil) } } private struct BackgroundDebugView: View { @AppStorage("bgGlassTintHex") private var bgGlassTintHex = "#000000" @AppStorage("bgGlassTintOpacity") private var bgGlassTintOpacity = 0.03 @AppStorage("bgGlassMaterial") private var bgGlassMaterial = "hudWindow" @AppStorage("bgGlassEnabled") private var bgGlassEnabled = false var body: some View { ScrollView { VStack(alignment: .leading, spacing: 14) { Text("Window Background Glass") .font(.headline) GroupBox("Glass Effect") { VStack(alignment: .leading, spacing: 8) { Toggle("Enable Glass Effect", isOn: $bgGlassEnabled) Picker("Material", selection: $bgGlassMaterial) { Text("HUD Window").tag("hudWindow") Text("Under Window").tag("underWindowBackground") Text("Sidebar").tag("sidebar") Text("Menu").tag("menu") Text("Popover").tag("popover") } .disabled(!bgGlassEnabled) } .padding(.top, 2) } GroupBox("Tint") { VStack(alignment: .leading, spacing: 8) { ColorPicker("Tint Color", selection: tintColorBinding, supportsOpacity: false) .disabled(!bgGlassEnabled) HStack(spacing: 8) { Text("Opacity") Slider(value: $bgGlassTintOpacity, in: 0...0.8) .disabled(!bgGlassEnabled) Text(String(format: "%.0f%%", bgGlassTintOpacity * 100)) .font(.caption) .frame(width: 44, alignment: .trailing) } } .padding(.top, 2) } HStack(spacing: 12) { Button("Reset") { bgGlassTintHex = "#000000" bgGlassTintOpacity = 0.03 bgGlassMaterial = "hudWindow" bgGlassEnabled = false updateWindowGlassTint() } Button("Copy Config") { copyBgConfig() } } Text("Tint changes apply live. Enable/disable requires reload.") .font(.caption) .foregroundColor(.secondary) Spacer(minLength: 0) } .padding(16) .frame(maxWidth: .infinity, alignment: .topLeading) } .onChange(of: bgGlassTintHex) { _ in updateWindowGlassTint() } .onChange(of: bgGlassTintOpacity) { _ in updateWindowGlassTint() } } private func updateWindowGlassTint() { let window: NSWindow? = { if let key = NSApp.keyWindow, let raw = key.identifier?.rawValue, raw == "cmux.main" || raw.hasPrefix("cmux.main.") { return key } return NSApp.windows.first(where: { guard let raw = $0.identifier?.rawValue else { return false } return raw == "cmux.main" || raw.hasPrefix("cmux.main.") }) }() guard let window else { return } let tintColor = (NSColor(hex: bgGlassTintHex) ?? .black).withAlphaComponent(bgGlassTintOpacity) WindowGlassEffect.updateTint(to: window, color: tintColor) } private var tintColorBinding: Binding { Binding( get: { Color(nsColor: NSColor(hex: bgGlassTintHex) ?? .black) }, set: { newColor in let nsColor = NSColor(newColor) bgGlassTintHex = nsColor.hexString() } ) } private func copyBgConfig() { let payload = """ bgGlassEnabled=\(bgGlassEnabled) bgGlassMaterial=\(bgGlassMaterial) bgGlassTintHex=\(bgGlassTintHex) bgGlassTintOpacity=\(String(format: "%.2f", bgGlassTintOpacity)) """ let pasteboard = NSPasteboard.general pasteboard.clearContents() pasteboard.setString(payload, forType: .string) } } private struct AboutPropertyRow: View { private let label: String private let text: String private let url: URL? init(label: String, text: String, url: URL? = nil) { self.label = label self.text = text self.url = url } @ViewBuilder private var textView: some View { Text(text) .frame(width: 140, alignment: .leading) .padding(.leading, 2) .tint(.secondary) .opacity(0.8) .monospaced() } var body: some View { HStack(spacing: 4) { Text(label) .frame(width: 126, alignment: .trailing) .padding(.trailing, 2) if let url { Link(destination: url) { textView } } else { textView } } .font(.callout) .textSelection(.enabled) .frame(maxWidth: .infinity) } } private struct AboutVisualEffectBackground: NSViewRepresentable { let material: NSVisualEffectView.Material let blendingMode: NSVisualEffectView.BlendingMode let isEmphasized: Bool init( material: NSVisualEffectView.Material, blendingMode: NSVisualEffectView.BlendingMode = .behindWindow, isEmphasized: Bool = false ) { self.material = material self.blendingMode = blendingMode self.isEmphasized = isEmphasized } func updateNSView(_ nsView: NSVisualEffectView, context: Context) { nsView.material = material nsView.blendingMode = blendingMode nsView.isEmphasized = isEmphasized } func makeNSView(context: Context) -> NSVisualEffectView { let visualEffect = NSVisualEffectView() visualEffect.autoresizingMask = [.width, .height] return visualEffect } } enum AppearanceMode: String, CaseIterable, Identifiable { case system case light case dark case auto var id: String { rawValue } static var visibleCases: [AppearanceMode] { [.system, .light, .dark] } var displayName: String { switch self { case .system: return String(localized: "appearance.system", defaultValue: "System") case .light: return String(localized: "appearance.light", defaultValue: "Light") case .dark: return String(localized: "appearance.dark", defaultValue: "Dark") case .auto: return String(localized: "appearance.auto", defaultValue: "Auto") } } } enum AppearanceSettings { static let appearanceModeKey = "appearanceMode" static let defaultMode: AppearanceMode = .system static func mode(for rawValue: String?) -> AppearanceMode { guard let rawValue, let mode = AppearanceMode(rawValue: rawValue) else { return defaultMode } if mode == .auto { return .system } return mode } @discardableResult static func resolvedMode(defaults: UserDefaults = .standard) -> AppearanceMode { let stored = defaults.string(forKey: appearanceModeKey) let resolved = mode(for: stored) if stored != resolved.rawValue { defaults.set(resolved.rawValue, forKey: appearanceModeKey) } return resolved } } enum AppLanguage: String, CaseIterable, Identifiable { case system case en case ar case bs case zhHans = "zh-Hans" case zhHant = "zh-Hant" case da case de case es case fr case it case ja case ko case nb case pl case ptBR = "pt-BR" case ru case th case tr var id: String { rawValue } var displayName: String { switch self { case .system: return String(localized: "language.system", defaultValue: "System") case .en: return "English" case .ar: return "\u{200E}العربية (Arabic)" case .bs: return "Bosanski (Bosnian)" case .zhHans: return "简体中文 (Chinese Simplified)" case .zhHant: return "繁體中文 (Chinese Traditional)" case .da: return "Dansk (Danish)" case .de: return "Deutsch (German)" case .es: return "Español (Spanish)" case .fr: return "Français (French)" case .it: return "Italiano (Italian)" case .ja: return "日本語 (Japanese)" case .ko: return "한국어 (Korean)" case .nb: return "Norsk (Norwegian)" case .pl: return "Polski (Polish)" case .ptBR: return "Português (Brasil)" case .ru: return "Русский (Russian)" case .th: return "ไทย (Thai)" case .tr: return "Türkçe (Turkish)" } } } enum LanguageSettings { static let languageKey = "appLanguage" static let defaultLanguage: AppLanguage = .system static func apply(_ language: AppLanguage) { if language == .system { UserDefaults.standard.removeObject(forKey: "AppleLanguages") } else { UserDefaults.standard.set([language.rawValue], forKey: "AppleLanguages") } } static var languageAtLaunch: AppLanguage = { let stored = UserDefaults.standard.string(forKey: languageKey) guard let stored, let lang = AppLanguage(rawValue: stored) else { return .system } return lang }() } enum AppIconMode: String, CaseIterable, Identifiable { case automatic case light case dark var id: String { rawValue } var displayName: String { switch self { case .automatic: return String(localized: "appIcon.automatic", defaultValue: "Automatic") case .light: return String(localized: "appIcon.light", defaultValue: "Light") case .dark: return String(localized: "appIcon.dark", defaultValue: "Dark") } } var imageName: String? { switch self { case .automatic: return nil case .light: return "AppIconLight" case .dark: return "AppIconDark" } } } enum AppIconSettings { static let modeKey = "appIconMode" static let defaultMode: AppIconMode = .automatic static func resolvedMode(defaults: UserDefaults = .standard) -> AppIconMode { guard let raw = defaults.string(forKey: modeKey), let mode = AppIconMode(rawValue: raw) else { return defaultMode } return mode } static func applyIcon(_ mode: AppIconMode) { switch mode { case .automatic: // Let the asset catalog handle appearance-based icon selection (macOS 15+). // Reset to the default bundle icon. NSApplication.shared.applicationIconImage = nil case .light: if let icon = NSImage(named: "AppIconLight") { NSApplication.shared.applicationIconImage = icon } case .dark: if let icon = NSImage(named: "AppIconDark") { NSApplication.shared.applicationIconImage = icon } } } } enum QuitWarningSettings { static let warnBeforeQuitKey = "warnBeforeQuitShortcut" static let defaultWarnBeforeQuit = true static func isEnabled(defaults: UserDefaults = .standard) -> Bool { if defaults.object(forKey: warnBeforeQuitKey) == nil { return defaultWarnBeforeQuit } return defaults.bool(forKey: warnBeforeQuitKey) } static func setEnabled(_ isEnabled: Bool, defaults: UserDefaults = .standard) { defaults.set(isEnabled, forKey: warnBeforeQuitKey) } } enum CommandPaletteRenameSelectionSettings { static let selectAllOnFocusKey = "commandPalette.renameSelectAllOnFocus" static let defaultSelectAllOnFocus = true static func selectAllOnFocusEnabled(defaults: UserDefaults = .standard) -> Bool { if defaults.object(forKey: selectAllOnFocusKey) == nil { return defaultSelectAllOnFocus } return defaults.bool(forKey: selectAllOnFocusKey) } } enum ClaudeCodeIntegrationSettings { static let hooksEnabledKey = "claudeCodeHooksEnabled" static let defaultHooksEnabled = true static func hooksEnabled(defaults: UserDefaults = .standard) -> Bool { if defaults.object(forKey: hooksEnabledKey) == nil { return defaultHooksEnabled } return defaults.bool(forKey: hooksEnabledKey) } } enum TelemetrySettings { static let sendAnonymousTelemetryKey = "sendAnonymousTelemetry" static let defaultSendAnonymousTelemetry = true static func isEnabled(defaults: UserDefaults = .standard) -> Bool { if defaults.object(forKey: sendAnonymousTelemetryKey) == nil { return defaultSendAnonymousTelemetry } return defaults.bool(forKey: sendAnonymousTelemetryKey) } // Freeze telemetry enablement once per launch. Settings changes apply on next restart. static let enabledForCurrentLaunch = isEnabled() } struct SettingsView: View { private let contentTopInset: CGFloat = 8 private let pickerColumnWidth: CGFloat = 196 @AppStorage(LanguageSettings.languageKey) private var appLanguage = LanguageSettings.defaultLanguage.rawValue @AppStorage(AppearanceSettings.appearanceModeKey) private var appearanceMode = AppearanceSettings.defaultMode.rawValue @AppStorage(AppIconSettings.modeKey) private var appIconMode = AppIconSettings.defaultMode.rawValue @AppStorage(SocketControlSettings.appStorageKey) private var socketControlMode = SocketControlSettings.defaultMode.rawValue @AppStorage(ClaudeCodeIntegrationSettings.hooksEnabledKey) private var claudeCodeHooksEnabled = ClaudeCodeIntegrationSettings.defaultHooksEnabled @AppStorage(TelemetrySettings.sendAnonymousTelemetryKey) private var sendAnonymousTelemetry = TelemetrySettings.defaultSendAnonymousTelemetry @AppStorage("cmuxPortBase") private var cmuxPortBase = 9100 @AppStorage("cmuxPortRange") private var cmuxPortRange = 10 @AppStorage(BrowserSearchSettings.searchEngineKey) private var browserSearchEngine = BrowserSearchSettings.defaultSearchEngine.rawValue @AppStorage(BrowserSearchSettings.searchSuggestionsEnabledKey) private var browserSearchSuggestionsEnabled = BrowserSearchSettings.defaultSearchSuggestionsEnabled @AppStorage(BrowserThemeSettings.modeKey) private var browserThemeMode = BrowserThemeSettings.defaultMode.rawValue @AppStorage(BrowserLinkOpenSettings.openTerminalLinksInCmuxBrowserKey) private var openTerminalLinksInCmuxBrowser = BrowserLinkOpenSettings.defaultOpenTerminalLinksInCmuxBrowser @AppStorage(BrowserLinkOpenSettings.interceptTerminalOpenCommandInCmuxBrowserKey) private var interceptTerminalOpenCommandInCmuxBrowser = BrowserLinkOpenSettings.initialInterceptTerminalOpenCommandInCmuxBrowserValue() @AppStorage(BrowserLinkOpenSettings.browserHostWhitelistKey) private var browserHostWhitelist = BrowserLinkOpenSettings.defaultBrowserHostWhitelist @AppStorage(BrowserLinkOpenSettings.browserExternalOpenPatternsKey) private var browserExternalOpenPatterns = BrowserLinkOpenSettings.defaultBrowserExternalOpenPatterns @AppStorage(BrowserInsecureHTTPSettings.allowlistKey) private var browserInsecureHTTPAllowlist = BrowserInsecureHTTPSettings.defaultAllowlistText @AppStorage(NotificationSoundSettings.key) private var notificationSound = NotificationSoundSettings.defaultValue @AppStorage(NotificationSoundSettings.customFilePathKey) private var notificationSoundCustomFilePath = NotificationSoundSettings.defaultCustomFilePath @AppStorage(NotificationSoundSettings.customCommandKey) private var notificationCustomCommand = NotificationSoundSettings.defaultCustomCommand @AppStorage(NotificationBadgeSettings.dockBadgeEnabledKey) private var notificationDockBadgeEnabled = NotificationBadgeSettings.defaultDockBadgeEnabled @AppStorage(QuitWarningSettings.warnBeforeQuitKey) private var warnBeforeQuitShortcut = QuitWarningSettings.defaultWarnBeforeQuit @AppStorage(CommandPaletteRenameSelectionSettings.selectAllOnFocusKey) private var commandPaletteRenameSelectAllOnFocus = CommandPaletteRenameSelectionSettings.defaultSelectAllOnFocus @AppStorage(ShortcutHintDebugSettings.alwaysShowHintsKey) private var alwaysShowShortcutHints = ShortcutHintDebugSettings.defaultAlwaysShowHints @AppStorage(WorkspacePlacementSettings.placementKey) private var newWorkspacePlacement = WorkspacePlacementSettings.defaultPlacement.rawValue @AppStorage(WorkspaceAutoReorderSettings.key) private var workspaceAutoReorder = WorkspaceAutoReorderSettings.defaultValue @AppStorage(SidebarBranchLayoutSettings.key) private var sidebarBranchVerticalLayout = SidebarBranchLayoutSettings.defaultVerticalLayout @AppStorage(SidebarActiveTabIndicatorSettings.styleKey) private var sidebarActiveTabIndicatorStyle = SidebarActiveTabIndicatorSettings.defaultStyle.rawValue @AppStorage("sidebarShowBranchDirectory") private var sidebarShowBranchDirectory = true @AppStorage("sidebarShowPullRequest") private var sidebarShowPullRequest = true @AppStorage(BrowserLinkOpenSettings.openSidebarPullRequestLinksInCmuxBrowserKey) private var openSidebarPullRequestLinksInCmuxBrowser = BrowserLinkOpenSettings.defaultOpenSidebarPullRequestLinksInCmuxBrowser @AppStorage(ShortcutHintDebugSettings.showHintsOnCommandHoldKey) private var showShortcutHintsOnCommandHold = ShortcutHintDebugSettings.defaultShowHintsOnCommandHold @AppStorage("sidebarShowPorts") private var sidebarShowPorts = true @AppStorage("sidebarShowLog") private var sidebarShowLog = true @AppStorage("sidebarShowProgress") private var sidebarShowProgress = true @AppStorage("sidebarShowStatusPills") private var sidebarShowMetadata = true @ObservedObject private var notificationStore = TerminalNotificationStore.shared @State private var shortcutResetToken = UUID() @State private var topBlurOpacity: Double = 0 @State private var topBlurBaselineOffset: CGFloat? @State private var settingsTitleLeadingInset: CGFloat = 92 @State private var showClearBrowserHistoryConfirmation = false @State private var showOpenAccessConfirmation = false @State private var pendingOpenAccessMode: SocketControlMode? @State private var browserHistoryEntryCount: Int = 0 @State private var browserInsecureHTTPAllowlistDraft = BrowserInsecureHTTPSettings.defaultAllowlistText @State private var socketPasswordDraft = "" @State private var socketPasswordStatusMessage: String? @State private var socketPasswordStatusIsError = false @State private var notificationCustomSoundStatusMessage: String? @State private var notificationCustomSoundStatusIsError = false @State private var showNotificationCustomSoundErrorAlert = false @State private var notificationCustomSoundErrorAlertMessage = "" @State private var telemetryValueAtLaunch = TelemetrySettings.enabledForCurrentLaunch @State private var showLanguageRestartAlert = false @State private var isResettingSettings = false @State private var workspaceTabDefaultEntries = WorkspaceTabColorSettings.defaultPaletteWithOverrides() @State private var workspaceTabCustomColors = WorkspaceTabColorSettings.customColors() private var selectedWorkspacePlacement: NewWorkspacePlacement { NewWorkspacePlacement(rawValue: newWorkspacePlacement) ?? WorkspacePlacementSettings.defaultPlacement } private var selectedSidebarActiveTabIndicatorStyle: SidebarActiveTabIndicatorStyle { SidebarActiveTabIndicatorSettings.resolvedStyle(rawValue: sidebarActiveTabIndicatorStyle) } private var sidebarIndicatorStyleSelection: Binding { Binding( get: { selectedSidebarActiveTabIndicatorStyle.rawValue }, set: { sidebarActiveTabIndicatorStyle = $0 } ) } private var selectedSocketControlMode: SocketControlMode { SocketControlSettings.migrateMode(socketControlMode) } private var selectedBrowserThemeMode: BrowserThemeMode { BrowserThemeSettings.mode(for: browserThemeMode) } private var browserThemeModeSelection: Binding { Binding( get: { browserThemeMode }, set: { newValue in browserThemeMode = BrowserThemeSettings.mode(for: newValue).rawValue } ) } private var socketModeSelection: Binding { Binding( get: { socketControlMode }, set: { newValue in let normalized = SocketControlSettings.migrateMode(newValue) if normalized == .allowAll && selectedSocketControlMode != .allowAll { pendingOpenAccessMode = normalized showOpenAccessConfirmation = true return } socketControlMode = normalized.rawValue if normalized != .password { socketPasswordStatusMessage = nil socketPasswordStatusIsError = false } } ) } private var hasSocketPasswordConfigured: Bool { SocketControlPasswordStore.hasConfiguredPassword() } private var browserHistorySubtitle: String { switch browserHistoryEntryCount { case 0: return String(localized: "settings.browser.history.subtitleEmpty", defaultValue: "No saved pages yet.") case 1: return String(localized: "settings.browser.history.subtitleOne", defaultValue: "1 saved page appears in omnibar suggestions.") default: return String(localized: "settings.browser.history.subtitleMany", defaultValue: "\(browserHistoryEntryCount) saved pages appear in omnibar suggestions.") } } private var browserInsecureHTTPAllowlistHasUnsavedChanges: Bool { browserInsecureHTTPAllowlistDraft != browserInsecureHTTPAllowlist } private var hasCustomNotificationSoundFilePath: Bool { !notificationSoundCustomFilePath.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } private var notificationSoundCustomFileDisplayName: String { guard hasCustomNotificationSoundFilePath else { return String( localized: "settings.notifications.sound.custom.file.none", defaultValue: "No file selected" ) } return URL(fileURLWithPath: notificationSoundCustomFilePath).lastPathComponent } private var canPreviewNotificationSound: Bool { switch notificationSound { case "none": return false case NotificationSoundSettings.customFileValue: return hasCustomNotificationSoundFilePath default: return true } } private var notificationPermissionStatusText: String { notificationStore.authorizationState.statusLabel } private var notificationPermissionStatusColor: Color { switch notificationStore.authorizationState { case .authorized, .provisional, .ephemeral: return .green case .denied: return .red case .unknown, .notDetermined: return .secondary } } private var notificationPermissionSubtitle: String { switch notificationStore.authorizationState { case .unknown, .notDetermined: return "Desktop notifications are not enabled yet." case .authorized: return "Desktop notifications are enabled." case .denied: return "Desktop notifications are disabled in System Settings." case .provisional: return "Desktop notifications are enabled with quiet delivery." case .ephemeral: return "Desktop notifications are temporarily enabled." } } private var notificationPermissionActionTitle: String { switch notificationStore.authorizationState { case .unknown, .notDetermined: return "Enable" case .authorized, .denied, .provisional, .ephemeral: return "Open Settings" } } private func blurOpacity(forContentOffset offset: CGFloat) -> Double { guard let baseline = topBlurBaselineOffset else { return 0 } let reveal = (baseline - offset) / 24 return Double(min(max(reveal, 0), 1)) } private func previewNotificationSound() { if notificationSound == NotificationSoundSettings.customFileValue { NotificationSoundSettings.playCustomFileSound(path: notificationSoundCustomFilePath) return } NotificationSoundSettings.previewSound(value: notificationSound) } private func notificationCustomSoundIssueMessage(_ issue: NotificationSoundSettings.CustomSoundPreparationIssue) -> String { switch issue { case .emptyPath: return String( localized: "settings.notifications.sound.custom.status.empty", defaultValue: "Choose a custom audio file first." ) case .missingFile(let path): let fileName = URL(fileURLWithPath: path).lastPathComponent return String( localized: "settings.notifications.sound.custom.status.missingFilePrefix", defaultValue: "File not found: " ) + fileName case .missingFileExtension(let path): let fileName = URL(fileURLWithPath: path).lastPathComponent return String( localized: "settings.notifications.sound.custom.status.missingExtensionPrefix", defaultValue: "File needs an extension: " ) + fileName case .stagingFailed(_, let details): let prefix = String( localized: "settings.notifications.sound.custom.status.prepareFailed", defaultValue: "Could not prepare this file for notifications. Try WAV, AIFF, or CAF." ) return "\(prefix) (\(details))" } } private func notificationCustomSoundReadyStatusMessage(for path: String) -> String { let sourceExtension = URL(fileURLWithPath: path).pathExtension .trimmingCharacters(in: .whitespacesAndNewlines) .lowercased() let stagedExtension = NotificationSoundSettings.stagedCustomSoundFileExtension(forSourceExtension: sourceExtension) if !sourceExtension.isEmpty, stagedExtension != sourceExtension { return String( localized: "settings.notifications.sound.custom.status.readyConverted", defaultValue: "Prepared for notifications (converted to CAF)." ) } return String( localized: "settings.notifications.sound.custom.status.ready", defaultValue: "Ready for notifications." ) } private func refreshNotificationCustomSoundStatus(showAlertOnFailure: Bool = false) { guard notificationSound == NotificationSoundSettings.customFileValue else { notificationCustomSoundStatusMessage = nil notificationCustomSoundStatusIsError = false return } let pathSnapshot = notificationSoundCustomFilePath DispatchQueue.global(qos: .userInitiated).async { let result = NotificationSoundSettings.prepareCustomFileForNotifications(path: pathSnapshot) DispatchQueue.main.async { guard notificationSound == NotificationSoundSettings.customFileValue else { notificationCustomSoundStatusMessage = nil notificationCustomSoundStatusIsError = false return } guard notificationSoundCustomFilePath == pathSnapshot else { return } switch result { case .success: notificationCustomSoundStatusMessage = notificationCustomSoundReadyStatusMessage(for: pathSnapshot) notificationCustomSoundStatusIsError = false case .failure(let issue): let message = notificationCustomSoundIssueMessage(issue) notificationCustomSoundStatusMessage = message notificationCustomSoundStatusIsError = true if showAlertOnFailure { notificationCustomSoundErrorAlertMessage = message showNotificationCustomSoundErrorAlert = true } } } } } private func chooseNotificationSoundFile() { let panel = NSOpenPanel() panel.canChooseFiles = true panel.canChooseDirectories = false panel.allowsMultipleSelection = false panel.allowedContentTypes = [.audio] panel.title = String( localized: "settings.notifications.sound.custom.choose.title", defaultValue: "Choose Notification Sound" ) panel.prompt = String( localized: "settings.notifications.sound.custom.choose.prompt", defaultValue: "Choose" ) guard panel.runModal() == .OK, let url = panel.url else { return } let selectedPath = url.path switch NotificationSoundSettings.prepareCustomFileForNotifications(path: selectedPath) { case .success: notificationSoundCustomFilePath = selectedPath notificationSound = NotificationSoundSettings.customFileValue notificationCustomSoundStatusMessage = notificationCustomSoundReadyStatusMessage(for: selectedPath) notificationCustomSoundStatusIsError = false previewNotificationSound() case .failure(let issue): let message = notificationCustomSoundIssueMessage(issue) notificationCustomSoundErrorAlertMessage = message showNotificationCustomSoundErrorAlert = true refreshNotificationCustomSoundStatus() } } private func handleNotificationPermissionAction() { let state = notificationStore.authorizationState.statusLabel #if DEBUG dlog("notification.ui enableTapped state=\(state)") #endif NSLog("notification.ui enableTapped state=%@", state) switch notificationStore.authorizationState { case .unknown, .notDetermined: notificationStore.requestAuthorizationFromSettings() case .authorized, .denied, .provisional, .ephemeral: notificationStore.openNotificationSettings() } } private func saveSocketPassword() { let trimmed = socketPasswordDraft.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { socketPasswordStatusMessage = String(localized: "settings.automation.socketPassword.enterFirst", defaultValue: "Enter a password first.") socketPasswordStatusIsError = true return } do { try SocketControlPasswordStore.savePassword(trimmed) socketPasswordDraft = "" socketPasswordStatusMessage = String(localized: "settings.automation.socketPassword.saved", defaultValue: "Password saved.") socketPasswordStatusIsError = false } catch { socketPasswordStatusMessage = String(localized: "settings.automation.socketPassword.saveFailed", defaultValue: "Failed to save password (\(error.localizedDescription)).") socketPasswordStatusIsError = true } } private func clearSocketPassword() { do { try SocketControlPasswordStore.clearPassword() socketPasswordDraft = "" socketPasswordStatusMessage = String(localized: "settings.automation.socketPassword.cleared", defaultValue: "Password cleared.") socketPasswordStatusIsError = false } catch { socketPasswordStatusMessage = String(localized: "settings.automation.socketPassword.clearFailed", defaultValue: "Failed to clear password (\(error.localizedDescription)).") socketPasswordStatusIsError = true } } var body: some View { ZStack(alignment: .top) { ScrollView { VStack(alignment: .leading, spacing: 14) { SettingsSectionHeader(title: String(localized: "settings.section.app", defaultValue: "App")) SettingsCard { SettingsPickerRow(String(localized: "settings.app.theme", defaultValue: "Theme"), controlWidth: pickerColumnWidth, selection: $appearanceMode) { ForEach(AppearanceMode.visibleCases) { mode in Text(mode.displayName).tag(mode.rawValue) } } SettingsCardDivider() SettingsCardRow( String(localized: "settings.app.language", defaultValue: "Language"), subtitle: appLanguage != LanguageSettings.languageAtLaunch.rawValue ? String(localized: "settings.app.language.restartSubtitle", defaultValue: "Restart cmux to apply") : nil, controlWidth: pickerColumnWidth ) { Picker("", selection: $appLanguage) { ForEach(AppLanguage.allCases) { lang in Text(lang.displayName).tag(lang.rawValue) } } .labelsHidden() .pickerStyle(.menu) .onChange(of: appLanguage) { newValue in guard !isResettingSettings else { return } DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [self] in // Re-check current value to handle rapid changes let current = appLanguage if let lang = AppLanguage(rawValue: current) { LanguageSettings.apply(lang) } if current != LanguageSettings.languageAtLaunch.rawValue { showLanguageRestartAlert = true } } } } SettingsCardDivider() AppIconPickerRow( selectedMode: appIconMode, onSelect: { mode in appIconMode = mode.rawValue AppIconSettings.applyIcon(mode) } ) SettingsCardDivider() SettingsPickerRow( String(localized: "settings.app.newWorkspacePlacement", defaultValue: "New Workspace Placement"), subtitle: selectedWorkspacePlacement.description, controlWidth: pickerColumnWidth, selection: $newWorkspacePlacement ) { ForEach(NewWorkspacePlacement.allCases) { placement in Text(placement.displayName).tag(placement.rawValue) } } SettingsCardDivider() SettingsCardRow( String(localized: "settings.app.reorderOnNotification", defaultValue: "Reorder on Notification"), subtitle: String(localized: "settings.app.reorderOnNotification.subtitle", defaultValue: "Move workspaces to the top when they receive a notification. Disable for stable shortcut positions.") ) { Toggle("", isOn: $workspaceAutoReorder) .labelsHidden() .controlSize(.small) } SettingsCardDivider() SettingsCardRow( String(localized: "settings.app.dockBadge", defaultValue: "Dock Badge"), subtitle: String(localized: "settings.app.dockBadge.subtitle", defaultValue: "Show unread count on app icon (Dock and Cmd+Tab).") ) { Toggle("", isOn: $notificationDockBadgeEnabled) .labelsHidden() .controlSize(.small) } SettingsCardDivider() SettingsCardRow( "Desktop Notifications", subtitle: notificationPermissionSubtitle ) { HStack(spacing: 6) { Text(notificationPermissionStatusText) .font(.system(size: 11, weight: .semibold)) .foregroundStyle(notificationPermissionStatusColor) .frame(width: 98, alignment: .trailing) Button(notificationPermissionActionTitle) { handleNotificationPermissionAction() } .controlSize(.small) Button("Send Test") { notificationStore.sendSettingsTestNotification() } .controlSize(.small) } } SettingsCardDivider() SettingsCardRow( String(localized: "settings.notifications.sound.title", defaultValue: "Notification Sound"), subtitle: String(localized: "settings.notifications.sound.subtitle", defaultValue: "Sound played when a notification arrives.") ) { VStack(alignment: .trailing, spacing: 6) { HStack(spacing: 6) { Picker("", selection: $notificationSound) { ForEach(NotificationSoundSettings.systemSounds, id: \.value) { sound in Text(sound.label).tag(sound.value) } } .labelsHidden() Button { previewNotificationSound() } label: { Image(systemName: "play.fill") .font(.system(size: 9)) } .buttonStyle(.bordered) .controlSize(.small) .disabled(!canPreviewNotificationSound) } if notificationSound == NotificationSoundSettings.customFileValue { HStack(spacing: 6) { Text(notificationSoundCustomFileDisplayName) .font(.system(size: 11)) .foregroundStyle(.secondary) .lineLimit(1) .truncationMode(.middle) .frame(width: 170, alignment: .trailing) Button( String( localized: "settings.notifications.sound.custom.choose.button", defaultValue: "Choose..." ) ) { chooseNotificationSoundFile() } .controlSize(.small) Button( String( localized: "settings.notifications.sound.custom.clear.button", defaultValue: "Clear" ) ) { notificationSoundCustomFilePath = NotificationSoundSettings.defaultCustomFilePath refreshNotificationCustomSoundStatus() } .controlSize(.small) .disabled(!hasCustomNotificationSoundFilePath) } if let notificationCustomSoundStatusMessage { Text(notificationCustomSoundStatusMessage) .font(.system(size: 11)) .foregroundStyle(notificationCustomSoundStatusIsError ? Color.red : Color.secondary) .lineLimit(2) .multilineTextAlignment(.trailing) .frame(width: 260, alignment: .trailing) } } } } SettingsCardDivider() SettingsCardRow( "Notification Command", subtitle: "Run a shell command when a notification arrives. $CMUX_NOTIFICATION_TITLE, $CMUX_NOTIFICATION_SUBTITLE, $CMUX_NOTIFICATION_BODY are set." ) { TextField("say \"done\"", text: $notificationCustomCommand) .textFieldStyle(.roundedBorder) .frame(width: 200) } SettingsCardDivider() SettingsCardRow( String(localized: "settings.app.telemetry", defaultValue: "Send anonymous telemetry"), subtitle: sendAnonymousTelemetry != telemetryValueAtLaunch ? String(localized: "settings.app.telemetry.subtitleChanged", defaultValue: "Change takes effect on next launch.") : String(localized: "settings.app.telemetry.subtitle", defaultValue: "Share anonymized crash and usage data to help improve cmux.") ) { Toggle("", isOn: $sendAnonymousTelemetry) .labelsHidden() .controlSize(.small) } SettingsCardDivider() SettingsCardRow( String(localized: "settings.app.warnBeforeQuit", defaultValue: "Warn Before Quit"), subtitle: warnBeforeQuitShortcut ? String(localized: "settings.app.warnBeforeQuit.subtitleOn", defaultValue: "Show a confirmation before quitting with Cmd+Q.") : String(localized: "settings.app.warnBeforeQuit.subtitleOff", defaultValue: "Cmd+Q quits immediately without confirmation.") ) { Toggle("", isOn: $warnBeforeQuitShortcut) .labelsHidden() .controlSize(.small) } SettingsCardDivider() SettingsCardRow( String(localized: "settings.app.renameSelectsName", defaultValue: "Rename Selects Existing Name"), subtitle: commandPaletteRenameSelectAllOnFocus ? String(localized: "settings.app.renameSelectsName.subtitleOn", defaultValue: "Command Palette rename starts with all text selected.") : String(localized: "settings.app.renameSelectsName.subtitleOff", defaultValue: "Command Palette rename keeps the caret at the end.") ) { Toggle("", isOn: $commandPaletteRenameSelectAllOnFocus) .labelsHidden() .controlSize(.small) } SettingsCardDivider() SettingsPickerRow( String(localized: "settings.app.sidebarBranchLayout", defaultValue: "Sidebar Branch Layout"), subtitle: sidebarBranchVerticalLayout ? String(localized: "settings.app.sidebarBranchLayout.subtitleVertical", defaultValue: "Vertical: each branch appears on its own line.") : String(localized: "settings.app.sidebarBranchLayout.subtitleInline", defaultValue: "Inline: all branches share one line."), controlWidth: pickerColumnWidth, selection: $sidebarBranchVerticalLayout ) { Text(String(localized: "settings.app.sidebarBranchLayout.vertical", defaultValue: "Vertical")).tag(true) Text(String(localized: "settings.app.sidebarBranchLayout.inline", defaultValue: "Inline")).tag(false) } SettingsCardDivider() SettingsCardRow( String(localized: "settings.app.showBranchDirectory", defaultValue: "Show Branch + Directory in Sidebar"), subtitle: String(localized: "settings.app.showBranchDirectory.subtitle", defaultValue: "Display the built-in git branch and working-directory row.") ) { Toggle("", isOn: $sidebarShowBranchDirectory) .labelsHidden() .controlSize(.small) } SettingsCardDivider() SettingsCardRow( String(localized: "settings.app.showPullRequests", defaultValue: "Show Pull Requests in Sidebar"), subtitle: String(localized: "settings.app.showPullRequests.subtitle", defaultValue: "Display review items (PR/MR/etc.) with status, number, and clickable link.") ) { Toggle("", isOn: $sidebarShowPullRequest) .labelsHidden() .controlSize(.small) } SettingsCardDivider() SettingsCardRow( String(localized: "settings.app.openSidebarPRLinks", defaultValue: "Open Sidebar PR Links in cmux Browser"), subtitle: openSidebarPullRequestLinksInCmuxBrowser ? String(localized: "settings.app.openSidebarPRLinks.subtitleOn", defaultValue: "Clicks open inside cmux browser.") : String(localized: "settings.app.openSidebarPRLinks.subtitleOff", defaultValue: "Clicks open in your default browser.") ) { Toggle("", isOn: $openSidebarPullRequestLinksInCmuxBrowser) .labelsHidden() .controlSize(.small) } SettingsCardDivider() SettingsCardRow( String(localized: "settings.app.showPorts", defaultValue: "Show Listening Ports in Sidebar"), subtitle: String(localized: "settings.app.showPorts.subtitle", defaultValue: "Display detected listening ports for the active workspace.") ) { Toggle("", isOn: $sidebarShowPorts) .labelsHidden() .controlSize(.small) } SettingsCardDivider() SettingsCardRow( String(localized: "settings.app.showLog", defaultValue: "Show Latest Log in Sidebar"), subtitle: String(localized: "settings.app.showLog.subtitle", defaultValue: "Display the latest imperative log/status message.") ) { Toggle("", isOn: $sidebarShowLog) .labelsHidden() .controlSize(.small) } SettingsCardDivider() SettingsCardRow( String(localized: "settings.app.showProgress", defaultValue: "Show Progress in Sidebar"), subtitle: String(localized: "settings.app.showProgress.subtitle", defaultValue: "Display the built-in progress bar from set_progress.") ) { Toggle("", isOn: $sidebarShowProgress) .labelsHidden() .controlSize(.small) } SettingsCardDivider() SettingsCardRow( String(localized: "settings.app.showMetadata", defaultValue: "Show Custom Metadata in Sidebar"), subtitle: String(localized: "settings.app.showMetadata.subtitle", defaultValue: "Display custom metadata from report_meta/set_status and report_meta_block.") ) { Toggle("", isOn: $sidebarShowMetadata) .labelsHidden() .controlSize(.small) } } SettingsSectionHeader(title: String(localized: "settings.section.workspaceColors", defaultValue: "Workspace Colors")) SettingsCard { SettingsPickerRow( String(localized: "settings.workspaceColors.indicator", defaultValue: "Workspace Color Indicator"), controlWidth: pickerColumnWidth, selection: sidebarIndicatorStyleSelection ) { ForEach(SidebarActiveTabIndicatorStyle.allCases) { style in Text(style.displayName).tag(style.rawValue) } } SettingsCardDivider() SettingsCardNote(String(localized: "settings.workspaceColors.paletteNote", defaultValue: "Customize the workspace color palette used by Sidebar > Workspace Color. \"Choose Custom Color...\" entries are persisted below.")) ForEach(Array(workspaceTabDefaultEntries.enumerated()), id: \.element.name) { index, entry in if index > 0 { SettingsCardDivider() } SettingsCardRow( entry.name, subtitle: String(localized: "settings.workspaceColors.base", defaultValue: "Base: \(baseTabColorHex(for: entry.name))") ) { HStack(spacing: 8) { ColorPicker( "", selection: defaultTabColorBinding(for: entry.name), supportsOpacity: false ) .labelsHidden() .frame(width: 38) Text(entry.hex) .font(.system(size: 12, weight: .medium, design: .monospaced)) .foregroundStyle(.secondary) .frame(width: 76, alignment: .trailing) } } } SettingsCardDivider() if workspaceTabCustomColors.isEmpty { SettingsCardNote(String(localized: "settings.workspaceColors.noCustomColors", defaultValue: "Custom colors: none yet. Use \"Choose Custom Color...\" from a workspace context menu.")) } else { VStack(alignment: .leading, spacing: 8) { Text(String(localized: "settings.workspaceColors.customColors", defaultValue: "Custom Colors")) .font(.system(size: 13, weight: .semibold)) ForEach(workspaceTabCustomColors, id: \.self) { hex in HStack(spacing: 8) { Circle() .fill(Color(nsColor: NSColor(hex: hex) ?? .gray)) .frame(width: 11, height: 11) Text(hex) .font(.system(size: 12, weight: .medium, design: .monospaced)) .foregroundStyle(.secondary) Spacer(minLength: 8) Button(String(localized: "settings.workspaceColors.remove", defaultValue: "Remove")) { removeWorkspaceCustomColor(hex) } .buttonStyle(.bordered) .controlSize(.small) } } } .padding(.horizontal, 14) .padding(.vertical, 10) } SettingsCardDivider() SettingsCardRow( String(localized: "settings.workspaceColors.resetPalette", defaultValue: "Reset Palette"), subtitle: String(localized: "settings.workspaceColors.resetPalette.subtitle", defaultValue: "Restore built-in defaults and clear all custom colors.") ) { Button(String(localized: "settings.workspaceColors.resetPalette.button", defaultValue: "Reset")) { resetWorkspaceTabColors() } .buttonStyle(.bordered) .controlSize(.small) } } SettingsSectionHeader(title: String(localized: "settings.section.automation", defaultValue: "Automation")) SettingsCard { SettingsPickerRow( String(localized: "settings.automation.socketMode", defaultValue: "Socket Control Mode"), subtitle: selectedSocketControlMode.description, controlWidth: pickerColumnWidth, selection: socketModeSelection, accessibilityId: "AutomationSocketModePicker" ) { ForEach(SocketControlMode.uiCases) { mode in Text(mode.displayName).tag(mode.rawValue) } } SettingsCardDivider() SettingsCardNote(String(localized: "settings.automation.socketMode.note", defaultValue: "Controls access to the local Unix socket for programmatic control. Choose a mode that matches your threat model.")) if selectedSocketControlMode == .password { SettingsCardDivider() SettingsCardRow( String(localized: "settings.automation.socketPassword", defaultValue: "Socket Password"), subtitle: hasSocketPasswordConfigured ? String(localized: "settings.automation.socketPassword.subtitleSet", defaultValue: "Stored in Application Support.") : String(localized: "settings.automation.socketPassword.subtitleUnset", defaultValue: "No password set. External clients will be blocked until one is configured.") ) { HStack(spacing: 8) { SecureField(String(localized: "settings.automation.socketPassword.placeholder", defaultValue: "Password"), text: $socketPasswordDraft) .textFieldStyle(.roundedBorder) .frame(width: 170) Button(hasSocketPasswordConfigured ? String(localized: "settings.automation.socketPassword.change", defaultValue: "Change") : String(localized: "settings.automation.socketPassword.set", defaultValue: "Set")) { saveSocketPassword() } .buttonStyle(.bordered) .controlSize(.small) .disabled(socketPasswordDraft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) if hasSocketPasswordConfigured { Button(String(localized: "settings.automation.socketPassword.clear", defaultValue: "Clear")) { clearSocketPassword() } .buttonStyle(.bordered) .controlSize(.small) } } } if let message = socketPasswordStatusMessage { Text(message) .font(.caption) .foregroundStyle(socketPasswordStatusIsError ? Color.red : Color.secondary) .padding(.horizontal, 14) .padding(.bottom, 8) } } if selectedSocketControlMode == .allowAll { SettingsCardDivider() Text(String(localized: "settings.automation.openAccessWarning", defaultValue: "Warning: Full open access makes the control socket world-readable/writable on this Mac and disables auth checks. Use only for local debugging.")) .font(.caption) .foregroundStyle(.red) .padding(.horizontal, 14) .padding(.vertical, 8) } SettingsCardNote(String(localized: "settings.automation.socketOverrides.note", defaultValue: "Overrides: CMUX_SOCKET_ENABLE, CMUX_SOCKET_MODE, and CMUX_SOCKET_PATH (set CMUX_ALLOW_SOCKET_OVERRIDE=1 for stable/nightly builds).")) } SettingsCard { SettingsCardRow( String(localized: "settings.automation.claudeCode", defaultValue: "Claude Code Integration"), subtitle: claudeCodeHooksEnabled ? String(localized: "settings.automation.claudeCode.subtitleOn", defaultValue: "Sidebar shows Claude session status and notifications.") : String(localized: "settings.automation.claudeCode.subtitleOff", defaultValue: "Claude Code runs without cmux integration.") ) { Toggle("", isOn: $claudeCodeHooksEnabled) .labelsHidden() .controlSize(.small) .accessibilityIdentifier("SettingsClaudeCodeHooksToggle") } SettingsCardDivider() SettingsCardNote(String(localized: "settings.automation.claudeCode.note", defaultValue: "When enabled, cmux wraps the claude command to inject session tracking and notification hooks. Disable if you prefer to manage Claude Code hooks yourself.")) } SettingsCard { SettingsCardRow(String(localized: "settings.automation.portBase", defaultValue: "Port Base"), subtitle: String(localized: "settings.automation.portBase.subtitle", defaultValue: "Starting port for CMUX_PORT env var."), controlWidth: pickerColumnWidth) { TextField("", value: $cmuxPortBase, format: .number) .textFieldStyle(.roundedBorder) .multilineTextAlignment(.trailing) } SettingsCardDivider() SettingsCardRow(String(localized: "settings.automation.portRange", defaultValue: "Port Range Size"), subtitle: String(localized: "settings.automation.portRange.subtitle", defaultValue: "Number of ports per workspace."), controlWidth: pickerColumnWidth) { TextField("", value: $cmuxPortRange, format: .number) .textFieldStyle(.roundedBorder) .multilineTextAlignment(.trailing) } SettingsCardDivider() SettingsCardNote(String(localized: "settings.automation.port.note", defaultValue: "Each workspace gets CMUX_PORT and CMUX_PORT_END env vars with a dedicated port range. New terminals inherit these values.")) } SettingsSectionHeader(title: String(localized: "settings.section.browser", defaultValue: "Browser")) SettingsCard { SettingsPickerRow( String(localized: "settings.browser.searchEngine", defaultValue: "Default Search Engine"), subtitle: String(localized: "settings.browser.searchEngine.subtitle", defaultValue: "Used by the browser address bar when input is not a URL."), controlWidth: pickerColumnWidth, selection: $browserSearchEngine ) { ForEach(BrowserSearchEngine.allCases) { engine in Text(engine.displayName).tag(engine.rawValue) } } SettingsCardDivider() SettingsCardRow(String(localized: "settings.browser.searchSuggestions", defaultValue: "Show Search Suggestions")) { Toggle("", isOn: $browserSearchSuggestionsEnabled) .labelsHidden() .controlSize(.small) } SettingsCardDivider() SettingsPickerRow( String(localized: "settings.browser.theme", defaultValue: "Browser Theme"), subtitle: selectedBrowserThemeMode == .system ? String(localized: "settings.browser.theme.subtitleSystem", defaultValue: "System follows app and macOS appearance.") : String(localized: "settings.browser.theme.subtitleForced", defaultValue: "\(selectedBrowserThemeMode.displayName) forces that color scheme for compatible pages."), controlWidth: pickerColumnWidth, selection: browserThemeModeSelection ) { ForEach(BrowserThemeMode.allCases) { mode in Text(mode.displayName).tag(mode.rawValue) } } SettingsCardDivider() SettingsCardRow( String(localized: "settings.browser.openTerminalLinks", defaultValue: "Open Terminal Links in cmux Browser"), subtitle: String(localized: "settings.browser.openTerminalLinks.subtitle", defaultValue: "When off, links clicked in terminal output open in your default browser.") ) { Toggle("", isOn: $openTerminalLinksInCmuxBrowser) .labelsHidden() .controlSize(.small) } SettingsCardDivider() SettingsCardRow( String(localized: "settings.browser.interceptOpen", defaultValue: "Intercept open http(s) in Terminal"), subtitle: String(localized: "settings.browser.interceptOpen.subtitle", defaultValue: "When off, `open https://...` and `open http://...` always use your default browser.") ) { Toggle("", isOn: $interceptTerminalOpenCommandInCmuxBrowser) .labelsHidden() .controlSize(.small) } if openTerminalLinksInCmuxBrowser || interceptTerminalOpenCommandInCmuxBrowser { SettingsCardDivider() VStack(alignment: .leading, spacing: 6) { SettingsCardRow( String(localized: "settings.browser.hostWhitelist", defaultValue: "Hosts to Open in Embedded Browser"), subtitle: String(localized: "settings.browser.hostWhitelist.subtitle", defaultValue: "Applies to terminal link clicks and intercepted `open https://...` calls. Only these hosts open in cmux. Others open in your default browser. One host or wildcard per line (for example: example.com, *.internal.example). Leave empty to open all hosts in cmux.") ) { EmptyView() } TextEditor(text: $browserHostWhitelist) .font(.system(.body, design: .monospaced)) .frame(minHeight: 60, maxHeight: 120) .scrollContentBackground(.hidden) .padding(6) .background(Color(nsColor: .controlBackgroundColor)) .cornerRadius(6) .overlay( RoundedRectangle(cornerRadius: 6) .stroke(Color(nsColor: .separatorColor), lineWidth: 0.5) ) .padding(.horizontal, 16) .padding(.bottom, 12) } SettingsCardDivider() VStack(alignment: .leading, spacing: 6) { SettingsCardRow( String(localized: "settings.browser.externalPatterns", defaultValue: "URLs to Always Open Externally"), subtitle: String(localized: "settings.browser.externalPatterns.subtitle", defaultValue: "Applies to terminal link clicks and intercepted `open https://...` calls. One rule per line. Plain text matches any URL substring, or prefix with `re:` for regex (for example: openai.com/usage, re:^https?://[^/]*\\.example\\.com/(billing|usage)).") ) { EmptyView() } TextEditor(text: $browserExternalOpenPatterns) .font(.system(.body, design: .monospaced)) .frame(minHeight: 60, maxHeight: 120) .scrollContentBackground(.hidden) .padding(6) .background(Color(nsColor: .controlBackgroundColor)) .cornerRadius(6) .overlay( RoundedRectangle(cornerRadius: 6) .stroke(Color(nsColor: .separatorColor), lineWidth: 0.5) ) .padding(.horizontal, 16) .padding(.bottom, 12) } } SettingsCardDivider() VStack(alignment: .leading, spacing: 8) { Text(String(localized: "settings.browser.httpAllowlist", defaultValue: "HTTP Hosts Allowed in Embedded Browser")) .font(.system(size: 13, weight: .semibold)) Text(String(localized: "settings.browser.httpAllowlist.description", defaultValue: "Controls which HTTP (non-HTTPS) hosts can open in cmux without a warning prompt. Defaults include localhost, 127.0.0.1, ::1, 0.0.0.0, and *.localtest.me.")) .font(.caption) .foregroundStyle(.secondary) TextEditor(text: $browserInsecureHTTPAllowlistDraft) .font(.system(size: 12, weight: .regular, design: .monospaced)) .frame(minHeight: 86) .padding(6) .background( RoundedRectangle(cornerRadius: 8, style: .continuous) .fill(Color(nsColor: .textBackgroundColor)) ) .overlay( RoundedRectangle(cornerRadius: 8, style: .continuous) .stroke(Color(nsColor: .separatorColor), lineWidth: 1) ) .accessibilityIdentifier("SettingsBrowserHTTPAllowlistField") ViewThatFits(in: .horizontal) { HStack(alignment: .center, spacing: 10) { Text(String(localized: "settings.browser.httpAllowlist.hint", defaultValue: "One host or wildcard per line (for example: localhost, 127.0.0.1, ::1, 0.0.0.0, *.localtest.me).")) .font(.caption) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) Spacer(minLength: 0) Button(String(localized: "settings.browser.httpAllowlist.save", defaultValue: "Save")) { saveBrowserInsecureHTTPAllowlist() } .buttonStyle(.bordered) .controlSize(.small) .disabled(!browserInsecureHTTPAllowlistHasUnsavedChanges) .accessibilityIdentifier("SettingsBrowserHTTPAllowlistSaveButton") } VStack(alignment: .leading, spacing: 8) { Text(String(localized: "settings.browser.httpAllowlist.hint", defaultValue: "One host or wildcard per line (for example: localhost, 127.0.0.1, ::1, 0.0.0.0, *.localtest.me).")) .font(.caption) .foregroundStyle(.secondary) HStack { Spacer(minLength: 0) Button(String(localized: "settings.browser.httpAllowlist.save", defaultValue: "Save")) { saveBrowserInsecureHTTPAllowlist() } .buttonStyle(.bordered) .controlSize(.small) .disabled(!browserInsecureHTTPAllowlistHasUnsavedChanges) .accessibilityIdentifier("SettingsBrowserHTTPAllowlistSaveButton") } } } } .padding(.horizontal, 14) .padding(.vertical, 10) SettingsCardDivider() SettingsCardRow(String(localized: "settings.browser.history", defaultValue: "Browsing History"), subtitle: browserHistorySubtitle) { Button(String(localized: "settings.browser.history.clearButton", defaultValue: "Clear History…")) { showClearBrowserHistoryConfirmation = true } .buttonStyle(.bordered) .controlSize(.small) .disabled(browserHistoryEntryCount == 0) } } SettingsSectionHeader(title: String(localized: "settings.section.keyboardShortcuts", defaultValue: "Keyboard Shortcuts")) SettingsCard { SettingsCardRow( String(localized: "settings.shortcuts.showHints", defaultValue: "Show Cmd/Ctrl-Hold Shortcut Hints"), subtitle: showShortcutHintsOnCommandHold ? String(localized: "settings.shortcuts.showHints.subtitleOn", defaultValue: "Holding Cmd (sidebar/titlebar) or Ctrl/Cmd (pane tabs) shows shortcut hint pills.") : String(localized: "settings.shortcuts.showHints.subtitleOff", defaultValue: "Holding Cmd or Ctrl keeps shortcut hint pills hidden.") ) { Toggle("", isOn: $showShortcutHintsOnCommandHold) .labelsHidden() .controlSize(.small) } SettingsCardDivider() let actions = KeyboardShortcutSettings.Action.allCases ForEach(Array(actions.enumerated()), id: \.element.id) { index, action in ShortcutSettingRow(action: action) .padding(.horizontal, 14) .padding(.vertical, 9) if index < actions.count - 1 { SettingsCardDivider() } } } .id(shortcutResetToken) Text(String(localized: "settings.shortcuts.recordHint", defaultValue: "Click a shortcut value to record a new shortcut.")) .font(.caption) .foregroundColor(.secondary) .padding(.leading, 2) SettingsSectionHeader(title: String(localized: "settings.section.reset", defaultValue: "Reset")) SettingsCard { HStack { Spacer(minLength: 0) Button(String(localized: "settings.reset.resetAll", defaultValue: "Reset All Settings")) { resetAllSettings() } .buttonStyle(.bordered) .controlSize(.regular) Spacer(minLength: 0) } .padding(.horizontal, 14) .padding(.vertical, 10) } } .padding(.horizontal, 20) .padding(.bottom, 20) .padding(.top, contentTopInset) .background( GeometryReader { proxy in Color.clear.preference( key: SettingsTopOffsetPreferenceKey.self, value: proxy.frame(in: .named("SettingsScrollArea")).minY ) } ) } .coordinateSpace(name: "SettingsScrollArea") .onPreferenceChange(SettingsTopOffsetPreferenceKey.self) { value in if topBlurBaselineOffset == nil { topBlurBaselineOffset = value } topBlurOpacity = blurOpacity(forContentOffset: value) } ZStack(alignment: .top) { SettingsTitleLeadingInsetReader(inset: $settingsTitleLeadingInset) .frame(width: 0, height: 0) AboutVisualEffectBackground(material: .underWindowBackground, blendingMode: .withinWindow) .mask( LinearGradient( colors: [ Color.black.opacity(0.9), Color.black.opacity(0.64), Color.black.opacity(0.36), Color.clear ], startPoint: .top, endPoint: .bottom ) ) .opacity(0.52) AboutVisualEffectBackground(material: .underWindowBackground, blendingMode: .withinWindow) .mask( LinearGradient( colors: [ Color.black.opacity(0.98), Color.black.opacity(0.78), Color.black.opacity(0.42), Color.clear ], startPoint: .top, endPoint: .bottom ) ) .opacity(0.14 + (topBlurOpacity * 0.86)) HStack { Text(String(localized: "settings.title", defaultValue: "Settings")) .font(.system(size: 16, weight: .semibold)) .foregroundColor(.primary.opacity(0.92)) Spacer(minLength: 0) } .padding(.leading, settingsTitleLeadingInset) .padding(.top, 12) } .frame(height: 62) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .ignoresSafeArea(.container, edges: .top) .overlay( Rectangle() .fill(Color(nsColor: .separatorColor).opacity(0.07)) .frame(height: 1), alignment: .bottom ) .allowsHitTesting(false) } .background(Color(nsColor: .windowBackgroundColor).ignoresSafeArea()) .toggleStyle(.switch) .onAppear { BrowserHistoryStore.shared.loadIfNeeded() notificationStore.refreshAuthorizationStatus() browserThemeMode = BrowserThemeSettings.mode(defaults: .standard).rawValue browserHistoryEntryCount = BrowserHistoryStore.shared.entries.count browserInsecureHTTPAllowlistDraft = browserInsecureHTTPAllowlist reloadWorkspaceTabColorSettings() refreshNotificationCustomSoundStatus() } .onChange(of: notificationSound) { _, _ in refreshNotificationCustomSoundStatus() } .onChange(of: notificationSoundCustomFilePath) { _, _ in refreshNotificationCustomSoundStatus() } .onChange(of: browserInsecureHTTPAllowlist) { oldValue, newValue in // Keep draft in sync with external changes unless the user has local unsaved edits. if browserInsecureHTTPAllowlistDraft == oldValue { browserInsecureHTTPAllowlistDraft = newValue } } .onReceive(BrowserHistoryStore.shared.$entries) { entries in browserHistoryEntryCount = entries.count } .onReceive(NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification)) { _ in reloadWorkspaceTabColorSettings() } .confirmationDialog( String(localized: "settings.browser.history.clearDialog.title", defaultValue: "Clear browser history?"), isPresented: $showClearBrowserHistoryConfirmation, titleVisibility: .visible ) { Button(String(localized: "settings.browser.history.clearDialog.confirm", defaultValue: "Clear History"), role: .destructive) { BrowserHistoryStore.shared.clearHistory() } Button(String(localized: "settings.browser.history.clearDialog.cancel", defaultValue: "Cancel"), role: .cancel) {} } message: { Text(String(localized: "settings.browser.history.clearDialog.message", defaultValue: "This removes visited-page suggestions from the browser omnibar.")) } .confirmationDialog( String(localized: "settings.automation.openAccess.dialog.title", defaultValue: "Enable full open access?"), isPresented: $showOpenAccessConfirmation, titleVisibility: .visible ) { Button(String(localized: "settings.automation.openAccess.dialog.confirm", defaultValue: "Enable Full Open Access"), role: .destructive) { socketControlMode = (pendingOpenAccessMode ?? .allowAll).rawValue pendingOpenAccessMode = nil } Button(String(localized: "settings.automation.openAccess.dialog.cancel", defaultValue: "Cancel"), role: .cancel) { pendingOpenAccessMode = nil } } message: { Text(String(localized: "settings.automation.openAccess.dialog.message", defaultValue: "This disables ancestry and password checks and opens the socket to all local users. Only enable when you understand the risk.")) } .confirmationDialog( String(localized: "settings.app.language.restartDialog.title", defaultValue: "Restart to apply language change?"), isPresented: $showLanguageRestartAlert, titleVisibility: .visible ) { Button(String(localized: "settings.app.language.restartDialog.confirm", defaultValue: "Restart Now")) { relaunchApp() } Button(String(localized: "settings.app.language.restartDialog.later", defaultValue: "Later"), role: .cancel) {} } .alert( String( localized: "settings.notifications.sound.custom.error.title", defaultValue: "Custom Notification Sound Error" ), isPresented: $showNotificationCustomSoundErrorAlert ) { Button(String(localized: "common.ok", defaultValue: "OK"), role: .cancel) {} } message: { Text(notificationCustomSoundErrorAlertMessage) } } private func relaunchApp() { let bundlePath = Bundle.main.bundlePath let task = Process() task.executableURL = URL(fileURLWithPath: "/bin/sh") task.arguments = ["-c", "sleep 1 && open -n -- \"$RELAUNCH_PATH\""] task.environment = ["RELAUNCH_PATH": bundlePath] do { try task.run() } catch { return } NSApplication.shared.terminate(nil) } private func resetAllSettings() { isResettingSettings = true appLanguage = LanguageSettings.defaultLanguage.rawValue LanguageSettings.apply(.system) if appLanguage != LanguageSettings.languageAtLaunch.rawValue { showLanguageRestartAlert = true } appearanceMode = AppearanceSettings.defaultMode.rawValue appIconMode = AppIconSettings.defaultMode.rawValue AppIconSettings.applyIcon(.automatic) socketControlMode = SocketControlSettings.defaultMode.rawValue claudeCodeHooksEnabled = ClaudeCodeIntegrationSettings.defaultHooksEnabled sendAnonymousTelemetry = TelemetrySettings.defaultSendAnonymousTelemetry browserSearchEngine = BrowserSearchSettings.defaultSearchEngine.rawValue browserSearchSuggestionsEnabled = BrowserSearchSettings.defaultSearchSuggestionsEnabled browserThemeMode = BrowserThemeSettings.defaultMode.rawValue openTerminalLinksInCmuxBrowser = BrowserLinkOpenSettings.defaultOpenTerminalLinksInCmuxBrowser interceptTerminalOpenCommandInCmuxBrowser = BrowserLinkOpenSettings.defaultInterceptTerminalOpenCommandInCmuxBrowser browserHostWhitelist = BrowserLinkOpenSettings.defaultBrowserHostWhitelist browserExternalOpenPatterns = BrowserLinkOpenSettings.defaultBrowserExternalOpenPatterns browserInsecureHTTPAllowlist = BrowserInsecureHTTPSettings.defaultAllowlistText browserInsecureHTTPAllowlistDraft = BrowserInsecureHTTPSettings.defaultAllowlistText notificationSound = NotificationSoundSettings.defaultValue notificationSoundCustomFilePath = NotificationSoundSettings.defaultCustomFilePath notificationCustomSoundStatusMessage = nil notificationCustomSoundStatusIsError = false showNotificationCustomSoundErrorAlert = false notificationCustomSoundErrorAlertMessage = "" notificationCustomCommand = NotificationSoundSettings.defaultCustomCommand notificationDockBadgeEnabled = NotificationBadgeSettings.defaultDockBadgeEnabled warnBeforeQuitShortcut = QuitWarningSettings.defaultWarnBeforeQuit commandPaletteRenameSelectAllOnFocus = CommandPaletteRenameSelectionSettings.defaultSelectAllOnFocus ShortcutHintDebugSettings.resetVisibilityDefaults() alwaysShowShortcutHints = ShortcutHintDebugSettings.defaultAlwaysShowHints newWorkspacePlacement = WorkspacePlacementSettings.defaultPlacement.rawValue workspaceAutoReorder = WorkspaceAutoReorderSettings.defaultValue sidebarBranchVerticalLayout = SidebarBranchLayoutSettings.defaultVerticalLayout sidebarActiveTabIndicatorStyle = SidebarActiveTabIndicatorSettings.defaultStyle.rawValue sidebarShowBranchDirectory = true sidebarShowPullRequest = true openSidebarPullRequestLinksInCmuxBrowser = BrowserLinkOpenSettings.defaultOpenSidebarPullRequestLinksInCmuxBrowser showShortcutHintsOnCommandHold = ShortcutHintDebugSettings.defaultShowHintsOnCommandHold sidebarShowPorts = true sidebarShowLog = true sidebarShowProgress = true sidebarShowMetadata = true showOpenAccessConfirmation = false pendingOpenAccessMode = nil socketPasswordDraft = "" socketPasswordStatusMessage = nil socketPasswordStatusIsError = false KeyboardShortcutSettings.resetAll() WorkspaceTabColorSettings.reset() reloadWorkspaceTabColorSettings() shortcutResetToken = UUID() DispatchQueue.main.async { isResettingSettings = false } } private func defaultTabColorBinding(for name: String) -> Binding { Binding( get: { let hex = WorkspaceTabColorSettings.defaultColorHex(named: name) return Color(nsColor: NSColor(hex: hex) ?? .systemBlue) }, set: { newValue in let hex = NSColor(newValue).hexString() WorkspaceTabColorSettings.setDefaultColor(named: name, hex: hex) reloadWorkspaceTabColorSettings() } ) } private func baseTabColorHex(for name: String) -> String { WorkspaceTabColorSettings.defaultPalette .first(where: { $0.name == name })? .hex ?? "#1565C0" } private func removeWorkspaceCustomColor(_ hex: String) { WorkspaceTabColorSettings.removeCustomColor(hex) reloadWorkspaceTabColorSettings() } private func resetWorkspaceTabColors() { WorkspaceTabColorSettings.reset() reloadWorkspaceTabColorSettings() } private func reloadWorkspaceTabColorSettings() { workspaceTabDefaultEntries = WorkspaceTabColorSettings.defaultPaletteWithOverrides() workspaceTabCustomColors = WorkspaceTabColorSettings.customColors() } private func saveBrowserInsecureHTTPAllowlist() { browserInsecureHTTPAllowlist = browserInsecureHTTPAllowlistDraft } } private struct SettingsTopOffsetPreferenceKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() } } private struct SettingsTitleLeadingInsetReader: NSViewRepresentable { @Binding var inset: CGFloat func makeNSView(context: Context) -> NSView { let view = NSView(frame: .zero) return view } func updateNSView(_ nsView: NSView, context: Context) { DispatchQueue.main.async { guard let window = nsView.window else { return } let buttons: [NSWindow.ButtonType] = [.closeButton, .miniaturizeButton, .zoomButton] let maxX = buttons .compactMap { window.standardWindowButton($0)?.frame.maxX } .max() ?? 78 let nextInset = maxX + 14 if abs(nextInset - inset) > 0.5 { inset = nextInset } } } } private struct SettingsSectionHeader: View { let title: String var body: some View { Text(title) .font(.system(size: 13, weight: .semibold)) .foregroundColor(.secondary) .padding(.leading, 2) .padding(.bottom, -2) } } private struct SettingsCard: View { @ViewBuilder let content: Content init(@ViewBuilder content: () -> Content) { self.content = content() } var body: some View { VStack(alignment: .leading, spacing: 0) { content } .background( RoundedRectangle(cornerRadius: 13, style: .continuous) .fill(Color(nsColor: NSColor.controlBackgroundColor).opacity(0.76)) .overlay( RoundedRectangle(cornerRadius: 13, style: .continuous) .stroke(Color(nsColor: NSColor.separatorColor).opacity(0.5), lineWidth: 1) ) ) } } private struct SettingsCardRow: View { let title: String let subtitle: String? let controlWidth: CGFloat? @ViewBuilder let trailing: Trailing init( _ title: String, subtitle: String? = nil, controlWidth: CGFloat? = nil, @ViewBuilder trailing: () -> Trailing ) { self.title = title self.subtitle = subtitle self.controlWidth = controlWidth self.trailing = trailing() } var body: some View { HStack(alignment: .center, spacing: 12) { VStack(alignment: .leading, spacing: subtitle == nil ? 0 : 3) { Text(title) .font(.system(size: 13, weight: .medium)) if let subtitle { Text(subtitle) .font(.caption) .foregroundColor(.secondary) .lineLimit(2) } } .frame(maxWidth: .infinity, alignment: .leading) Group { if let controlWidth { trailing .frame(width: controlWidth, alignment: .trailing) } else { trailing } } .layoutPriority(1) } .padding(.horizontal, 14) .padding(.vertical, 9) .frame(maxWidth: .infinity, alignment: .leading) } } private struct SettingsPickerRow: View { let title: String let subtitle: String? let controlWidth: CGFloat @Binding var selection: SelectionValue let pickerContent: PickerContent let extraTrailing: ExtraTrailing let accessibilityId: String? init( _ title: String, subtitle: String? = nil, controlWidth: CGFloat, selection: Binding, accessibilityId: String? = nil, @ViewBuilder content: () -> PickerContent, @ViewBuilder extraTrailing: () -> ExtraTrailing ) { self.title = title self.subtitle = subtitle self.controlWidth = controlWidth self._selection = selection self.pickerContent = content() self.extraTrailing = extraTrailing() self.accessibilityId = accessibilityId } var body: some View { SettingsCardRow(title, subtitle: subtitle, controlWidth: controlWidth) { HStack(spacing: 6) { Picker("", selection: $selection) { pickerContent } .labelsHidden() .pickerStyle(.menu) .applyIf(accessibilityId != nil) { $0.accessibilityIdentifier(accessibilityId!) } extraTrailing } } } } extension SettingsPickerRow where ExtraTrailing == EmptyView { init( _ title: String, subtitle: String? = nil, controlWidth: CGFloat, selection: Binding, accessibilityId: String? = nil, @ViewBuilder content: () -> PickerContent ) { self.init(title, subtitle: subtitle, controlWidth: controlWidth, selection: selection, accessibilityId: accessibilityId, content: content) { EmptyView() } } } private extension View { @ViewBuilder func applyIf(_ condition: Bool, transform: (Self) -> some View) -> some View { if condition { transform(self) } else { self } } } private struct SettingsCardDivider: View { var body: some View { Rectangle() .fill(Color(nsColor: NSColor.separatorColor).opacity(0.5)) .frame(height: 1) } } private struct SettingsCardNote: View { let text: String init(_ text: String) { self.text = text } var body: some View { Text(text) .font(.caption) .foregroundColor(.secondary) .padding(.horizontal, 14) .padding(.vertical, 8) .frame(maxWidth: .infinity, alignment: .leading) } } private struct AppIconPickerRow: View { let selectedMode: String let onSelect: (AppIconMode) -> Void private let iconSize: CGFloat = 48 private let autoIconSize: CGFloat = 36 var body: some View { VStack(alignment: .leading, spacing: 10) { Text(String(localized: "settings.app.appIcon", defaultValue: "App Icon")) .font(.system(size: 13, weight: .medium)) HStack(spacing: 12) { ForEach(AppIconMode.allCases) { mode in let isSelected = selectedMode == mode.rawValue Button { onSelect(mode) } label: { VStack(spacing: 6) { Group { if mode == .automatic { // Show both icons overlapping ZStack { Image("AppIconLight") .resizable() .interpolation(.high) .frame(width: autoIconSize, height: autoIconSize) .clipShape(RoundedRectangle(cornerRadius: autoIconSize * 0.22, style: .continuous)) .offset(x: -10) Image("AppIconDark") .resizable() .interpolation(.high) .frame(width: autoIconSize, height: autoIconSize) .clipShape(RoundedRectangle(cornerRadius: autoIconSize * 0.22, style: .continuous)) .offset(x: 10) } .frame(width: iconSize, height: iconSize) } else { Image(mode.imageName ?? "AppIconLight") .resizable() .interpolation(.high) .frame(width: iconSize, height: iconSize) .clipShape(RoundedRectangle(cornerRadius: iconSize * 0.22, style: .continuous)) } } Text(mode.displayName) .font(.system(size: 11)) .foregroundColor(isSelected ? .primary : .secondary) } .padding(.vertical, 8) .padding(.horizontal, 12) .background( RoundedRectangle(cornerRadius: 10, style: .continuous) .fill(isSelected ? Color.accentColor.opacity(0.12) : Color.clear) ) .overlay( RoundedRectangle(cornerRadius: 10, style: .continuous) .stroke(isSelected ? Color.accentColor : Color.clear, lineWidth: 2) ) } .buttonStyle(.plain) } } } .padding(.horizontal, 14) .padding(.vertical, 9) .frame(maxWidth: .infinity, alignment: .leading) } } private struct ShortcutSettingRow: View { let action: KeyboardShortcutSettings.Action @State private var shortcut: StoredShortcut init(action: KeyboardShortcutSettings.Action) { self.action = action _shortcut = State(initialValue: KeyboardShortcutSettings.shortcut(for: action)) } var body: some View { KeyboardShortcutRecorder(label: action.label, shortcut: $shortcut) .onChange(of: shortcut) { newValue in KeyboardShortcutSettings.setShortcut(newValue, for: action) } .onReceive(NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification)) { _ in let latest = KeyboardShortcutSettings.shortcut(for: action) if latest != shortcut { shortcut = latest } } } } private struct SettingsRootView: View { var body: some View { SettingsView() .background(WindowAccessor { window in configureSettingsWindow(window) }) } private func configureSettingsWindow(_ window: NSWindow) { window.identifier = NSUserInterfaceItemIdentifier("cmux.settings") applyCurrentSettingsWindowStyle(to: window) let accessories = window.titlebarAccessoryViewControllers for index in accessories.indices.reversed() { guard let identifier = accessories[index].view.identifier?.rawValue else { continue } guard identifier.hasPrefix("cmux.") else { continue } window.removeTitlebarAccessoryViewController(at: index) } AppDelegate.shared?.applyWindowDecorations(to: window) } private func applyCurrentSettingsWindowStyle(to window: NSWindow) { SettingsAboutTitlebarDebugStore.shared.applyCurrentOptions(to: window, for: .settings) } }