import AppKit import SwiftUI import Darwin import Bonsplit @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.closeWorkspace.defaultsKey) private var closeWorkspaceShortcutData = Data() @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate init() { if SocketControlSettings.shouldBlockUntaggedDebugLaunch() { Self.terminateForMissingLaunchTag() } Self.configureGhosttyEnvironment() 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) } 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("Settings…") { appDelegate.openPreferencesWindow(debugSource: "menu.cmdComma") } .keyboardShortcut(",", modifiers: .command) } CommandGroup(replacing: .appInfo) { Button("About cmux") { showAboutPanel() } Button("Ghostty Settings…") { GhosttyApp.shared.openConfigurationInTextEdit() } Button("Reload Configuration") { GhosttyApp.shared.reloadConfiguration(source: "menu.reload_configuration") } .keyboardShortcut(",", modifiers: [.command, .shift]) Divider() Button("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("Update Logs") { Button("Copy Update Logs") { appDelegate.copyUpdateLogs(nil) } Button("Copy Focus Logs") { appDelegate.copyFocusLogs(nil) } } CommandMenu("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: "Show Notifications", shortcut: showNotificationsMenuShortcut) { showNotificationsPopover() } splitCommandButton(title: "Jump to Latest Unread", shortcut: jumpToUnreadMenuShortcut) { appDelegate.jumpToLatestUnread() } .disabled(!snapshot.hasUnreadNotifications) Button("Mark All Read") { notificationStore.markAllRead() } .disabled(!snapshot.hasUnreadNotifications) Button("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 Tab 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: "New Window", shortcut: newWindowMenuShortcut) { appDelegate.openNewMainWindow(nil) } splitCommandButton(title: "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() } } } // Close tab/workspace CommandGroup(after: .newItem) { Button("Go to Workspace or Tab…") { let targetWindow = NSApp.keyWindow ?? NSApp.mainWindow NotificationCenter.default.post(name: .commandPaletteSwitcherRequested, object: targetWindow) } .keyboardShortcut("p", modifiers: [.command]) Button("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("Close Tab") { closePanelOrWindow() } .keyboardShortcut("w", modifiers: .command) Button("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: "Close Workspace", shortcut: closeWorkspaceMenuShortcut) { closeTabOrWindow() } Button("Reopen Closed Browser Panel") { _ = activeTabManager.reopenMostRecentlyClosedBrowserPanel() } .keyboardShortcut("t", modifiers: [.command, .shift]) } // Find CommandGroup(after: .textEditing) { Menu("Find") { Button("Find…") { activeTabManager.startSearch() } .keyboardShortcut("f", modifiers: .command) Button("Find Next") { activeTabManager.findNext() } .keyboardShortcut("g", modifiers: .command) Button("Find Previous") { activeTabManager.findPrevious() } .keyboardShortcut("g", modifiers: [.command, .shift]) Divider() Button("Hide Find Bar") { activeTabManager.hideFind() } .keyboardShortcut("f", modifiers: [.command, .shift]) .disabled(!(activeTabManager.isFindVisible)) Divider() Button("Use Selection for Find") { activeTabManager.searchSelection() } .keyboardShortcut("e", modifiers: .command) .disabled(!(activeTabManager.canUseSelectionForFind)) } } // Tab navigation CommandGroup(after: .toolbar) { splitCommandButton(title: "Toggle Sidebar", shortcut: toggleSidebarMenuShortcut) { if AppDelegate.shared?.toggleSidebarInActiveMainWindow() != true { sidebarState.toggle() } } Divider() splitCommandButton(title: "Next Surface", shortcut: nextSurfaceMenuShortcut) { activeTabManager.selectNextSurface() } splitCommandButton(title: "Previous Surface", shortcut: prevSurfaceMenuShortcut) { activeTabManager.selectPreviousSurface() } Button("Back") { activeTabManager.focusedBrowserPanel?.goBack() } .keyboardShortcut("[", modifiers: .command) Button("Forward") { activeTabManager.focusedBrowserPanel?.goForward() } .keyboardShortcut("]", modifiers: .command) Button("Reload Page") { activeTabManager.focusedBrowserPanel?.reload() } .keyboardShortcut("r", modifiers: .command) splitCommandButton(title: "Toggle Developer Tools", shortcut: toggleBrowserDeveloperToolsMenuShortcut) { let manager = activeTabManager if !manager.toggleDeveloperToolsFocusedBrowser() { NSSound.beep() } } splitCommandButton(title: "Show JavaScript Console", shortcut: showBrowserJavaScriptConsoleMenuShortcut) { let manager = activeTabManager if !manager.showJavaScriptConsoleFocusedBrowser() { NSSound.beep() } } Button("Zoom In") { _ = activeTabManager.zoomInFocusedBrowser() } .keyboardShortcut("=", modifiers: .command) Button("Zoom Out") { _ = activeTabManager.zoomOutFocusedBrowser() } .keyboardShortcut("-", modifiers: .command) Button("Actual Size") { _ = activeTabManager.resetZoomFocusedBrowser() } .keyboardShortcut("0", modifiers: .command) Button("Clear Browser History") { BrowserHistoryStore.shared.clearHistory() } splitCommandButton(title: "Next Workspace", shortcut: nextWorkspaceMenuShortcut) { activeTabManager.selectNextTab() } splitCommandButton(title: "Previous Workspace", shortcut: prevWorkspaceMenuShortcut) { activeTabManager.selectPreviousTab() } splitCommandButton(title: "Rename Workspace…", shortcut: renameWorkspaceMenuShortcut) { _ = AppDelegate.shared?.requestRenameWorkspaceViaCommandPalette() } Divider() splitCommandButton(title: "Split Right", shortcut: splitRightMenuShortcut) { performSplitFromMenu(direction: .right) } splitCommandButton(title: "Split Down", shortcut: splitDownMenuShortcut) { performSplitFromMenu(direction: .down) } splitCommandButton(title: "Split Browser Right", shortcut: splitBrowserRightMenuShortcut) { performBrowserSplitFromMenu(direction: .right) } splitCommandButton(title: "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("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: "Jump to Latest Unread", shortcut: jumpToUnreadMenuShortcut) { AppDelegate.shared?.jumpToLatestUnread() } splitCommandButton(title: "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 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)) """ let backgroundPayload = """ bgGlassEnabled=\(boolValue(defaults, key: "bgGlassEnabled", fallback: true)) 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 = "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 "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("cmux") .bold() .font(.title) Text("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: "Version", text: version) } if let build { AboutPropertyRow(label: "Build", text: build) } let commitText = commit ?? "—" let commitURL = commit.flatMap { hash in URL(string: "https://github.com/manaflow-ai/cmux/commit/\(hash)") } AboutPropertyRow(label: "Commit", text: commitText, url: commitURL) } .frame(maxWidth: .infinity) HStack(spacing: 8) { if let url = docsURL { Button("Docs") { openURL(url) } } if let url = githubURL { Button("GitHub") { openURL(url) } } Button("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 = true 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 = true 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 "System" case .light: return "Light" case .dark: return "Dark" case .auto: return "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 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) } } struct SettingsView: View { private let contentTopInset: CGFloat = 8 private let pickerColumnWidth: CGFloat = 196 @AppStorage(AppearanceSettings.appearanceModeKey) private var appearanceMode = AppearanceSettings.defaultMode.rawValue @AppStorage(SocketControlSettings.appStorageKey) private var socketControlMode = SocketControlSettings.defaultMode.rawValue @AppStorage(ClaudeCodeIntegrationSettings.hooksEnabledKey) private var claudeCodeHooksEnabled = ClaudeCodeIntegrationSettings.defaultHooksEnabled @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(BrowserInsecureHTTPSettings.allowlistKey) private var browserInsecureHTTPAllowlist = BrowserInsecureHTTPSettings.defaultAllowlistText @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(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("sidebarShowPorts") private var sidebarShowPorts = true @AppStorage("sidebarShowLog") private var sidebarShowLog = true @AppStorage("sidebarShowProgress") private var sidebarShowProgress = true @AppStorage("sidebarShowStatusPills") private var sidebarShowMetadata = true @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 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 "No saved pages yet." case 1: return "1 saved page appears in omnibar suggestions." default: return "\(browserHistoryEntryCount) saved pages appear in omnibar suggestions." } } private var browserInsecureHTTPAllowlistHasUnsavedChanges: Bool { browserInsecureHTTPAllowlistDraft != browserInsecureHTTPAllowlist } 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 saveSocketPassword() { let trimmed = socketPasswordDraft.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { socketPasswordStatusMessage = "Enter a password first." socketPasswordStatusIsError = true return } do { try SocketControlPasswordStore.savePassword(trimmed) socketPasswordDraft = "" socketPasswordStatusMessage = "Password saved to keychain." socketPasswordStatusIsError = false } catch { socketPasswordStatusMessage = "Failed to save password (\(error.localizedDescription))." socketPasswordStatusIsError = true } } private func clearSocketPassword() { do { try SocketControlPasswordStore.clearPassword() socketPasswordDraft = "" socketPasswordStatusMessage = "Password cleared." socketPasswordStatusIsError = false } catch { socketPasswordStatusMessage = "Failed to clear password (\(error.localizedDescription))." socketPasswordStatusIsError = true } } var body: some View { ZStack(alignment: .top) { ScrollView { VStack(alignment: .leading, spacing: 14) { SettingsSectionHeader(title: "App") SettingsCard { SettingsCardRow("Theme", controlWidth: pickerColumnWidth) { Picker("", selection: $appearanceMode) { ForEach(AppearanceMode.visibleCases) { mode in Text(mode.displayName).tag(mode.rawValue) } } .labelsHidden() .pickerStyle(.menu) } SettingsCardDivider() SettingsCardRow( "New Workspace Placement", subtitle: selectedWorkspacePlacement.description, controlWidth: pickerColumnWidth ) { Picker("", selection: $newWorkspacePlacement) { ForEach(NewWorkspacePlacement.allCases) { placement in Text(placement.displayName).tag(placement.rawValue) } } .labelsHidden() .pickerStyle(.menu) } SettingsCardDivider() SettingsCardRow( "Reorder on Notification", subtitle: "Move workspaces to the top when they receive a notification. Disable for stable shortcut positions." ) { Toggle("", isOn: $workspaceAutoReorder) .labelsHidden() .controlSize(.small) } SettingsCardDivider() SettingsCardRow( "Dock Badge", subtitle: "Show unread count on app icon (Dock and Cmd+Tab)." ) { Toggle("", isOn: $notificationDockBadgeEnabled) .labelsHidden() .controlSize(.small) } SettingsCardDivider() SettingsCardRow( "Warn Before Quit", subtitle: warnBeforeQuitShortcut ? "Show a confirmation before quitting with Cmd+Q." : "Cmd+Q quits immediately without confirmation." ) { Toggle("", isOn: $warnBeforeQuitShortcut) .labelsHidden() .controlSize(.small) } SettingsCardDivider() SettingsCardRow( "Rename Selects Existing Name", subtitle: commandPaletteRenameSelectAllOnFocus ? "Command Palette rename starts with all text selected." : "Command Palette rename keeps the caret at the end." ) { Toggle("", isOn: $commandPaletteRenameSelectAllOnFocus) .labelsHidden() .controlSize(.small) } SettingsCardDivider() SettingsCardRow( "Sidebar Branch Layout", subtitle: sidebarBranchVerticalLayout ? "Vertical: each branch appears on its own line." : "Inline: all branches share one line." ) { Picker("", selection: $sidebarBranchVerticalLayout) { Text("Vertical").tag(true) Text("Inline").tag(false) } .labelsHidden() .pickerStyle(.menu) } SettingsCardDivider() SettingsCardRow( "Show Branch + Directory in Sidebar", subtitle: "Display the built-in git branch and working-directory row." ) { Toggle("", isOn: $sidebarShowBranchDirectory) .labelsHidden() .controlSize(.small) } SettingsCardDivider() SettingsCardRow( "Show Pull Requests in Sidebar", subtitle: "Display review items (PR/MR/etc.) with status, number, and clickable link." ) { Toggle("", isOn: $sidebarShowPullRequest) .labelsHidden() .controlSize(.small) } SettingsCardDivider() SettingsCardRow( "Open Sidebar PR Links in cmux Browser", subtitle: openSidebarPullRequestLinksInCmuxBrowser ? "Clicks open inside cmux browser." : "Clicks open in your default browser." ) { Toggle("", isOn: $openSidebarPullRequestLinksInCmuxBrowser) .labelsHidden() .controlSize(.small) } SettingsCardDivider() SettingsCardRow( "Show Listening Ports in Sidebar", subtitle: "Display detected listening ports for the active workspace." ) { Toggle("", isOn: $sidebarShowPorts) .labelsHidden() .controlSize(.small) } SettingsCardDivider() SettingsCardRow( "Show Latest Log in Sidebar", subtitle: "Display the latest imperative log/status message." ) { Toggle("", isOn: $sidebarShowLog) .labelsHidden() .controlSize(.small) } SettingsCardDivider() SettingsCardRow( "Show Progress in Sidebar", subtitle: "Display the built-in progress bar from set_progress." ) { Toggle("", isOn: $sidebarShowProgress) .labelsHidden() .controlSize(.small) } SettingsCardDivider() SettingsCardRow( "Show Custom Metadata in Sidebar", subtitle: "Display custom metadata from report_meta/set_status and report_meta_block." ) { Toggle("", isOn: $sidebarShowMetadata) .labelsHidden() .controlSize(.small) } } SettingsSectionHeader(title: "Workspace Colors") SettingsCard { SettingsCardRow( "Workspace Color Indicator", controlWidth: pickerColumnWidth ) { Picker("", selection: sidebarIndicatorStyleSelection) { ForEach(SidebarActiveTabIndicatorStyle.allCases) { style in Text(style.displayName).tag(style.rawValue) } } .labelsHidden() .pickerStyle(.menu) } SettingsCardDivider() SettingsCardNote("Customize the workspace color palette used by Sidebar > Tab 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: "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("Custom colors: none yet. Use \"Choose Custom Color...\" from a workspace context menu.") } else { VStack(alignment: .leading, spacing: 8) { Text("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("Remove") { removeWorkspaceCustomColor(hex) } .buttonStyle(.bordered) .controlSize(.small) } } } .padding(.horizontal, 14) .padding(.vertical, 10) } SettingsCardDivider() SettingsCardRow( "Reset Palette", subtitle: "Restore built-in defaults and clear all custom colors." ) { Button("Reset") { resetWorkspaceTabColors() } .buttonStyle(.bordered) .controlSize(.small) } } SettingsSectionHeader(title: "Automation") SettingsCard { SettingsCardRow( "Socket Control Mode", subtitle: selectedSocketControlMode.description, controlWidth: pickerColumnWidth ) { Picker("", selection: socketModeSelection) { ForEach(SocketControlMode.uiCases) { mode in Text(mode.displayName).tag(mode.rawValue) } } .labelsHidden() .pickerStyle(.menu) .accessibilityIdentifier("AutomationSocketModePicker") } SettingsCardDivider() SettingsCardNote("Controls access to the local Unix socket for programmatic control. Choose a mode that matches your threat model.") if selectedSocketControlMode == .password { SettingsCardDivider() SettingsCardRow( "Socket Password", subtitle: hasSocketPasswordConfigured ? "Stored in login keychain." : "No password set. External clients will be blocked until one is configured." ) { HStack(spacing: 8) { SecureField("Password", text: $socketPasswordDraft) .textFieldStyle(.roundedBorder) .frame(width: 170) Button(hasSocketPasswordConfigured ? "Change" : "Set") { saveSocketPassword() } .buttonStyle(.bordered) .controlSize(.small) .disabled(socketPasswordDraft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) if hasSocketPasswordConfigured { Button("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("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("Overrides: CMUX_SOCKET_ENABLE, CMUX_SOCKET_MODE, and CMUX_SOCKET_PATH (set CMUX_ALLOW_SOCKET_OVERRIDE=1 for stable/nightly builds).") } SettingsCard { SettingsCardRow( "Claude Code Integration", subtitle: claudeCodeHooksEnabled ? "Sidebar shows Claude session status and notifications." : "Claude Code runs without cmux integration." ) { Toggle("", isOn: $claudeCodeHooksEnabled) .labelsHidden() .controlSize(.small) .accessibilityIdentifier("SettingsClaudeCodeHooksToggle") } SettingsCardDivider() SettingsCardNote("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("Port Base", subtitle: "Starting port for CMUX_PORT env var.", controlWidth: pickerColumnWidth) { TextField("", value: $cmuxPortBase, format: .number) .textFieldStyle(.roundedBorder) .multilineTextAlignment(.trailing) } SettingsCardDivider() SettingsCardRow("Port Range Size", subtitle: "Number of ports per workspace.", controlWidth: pickerColumnWidth) { TextField("", value: $cmuxPortRange, format: .number) .textFieldStyle(.roundedBorder) .multilineTextAlignment(.trailing) } SettingsCardDivider() SettingsCardNote("Each workspace gets CMUX_PORT and CMUX_PORT_END env vars with a dedicated port range. New terminals inherit these values.") } SettingsSectionHeader(title: "Browser") SettingsCard { SettingsCardRow( "Default Search Engine", subtitle: "Used by the browser address bar when input is not a URL.", controlWidth: pickerColumnWidth ) { Picker("", selection: $browserSearchEngine) { ForEach(BrowserSearchEngine.allCases) { engine in Text(engine.displayName).tag(engine.rawValue) } } .labelsHidden() .pickerStyle(.menu) } SettingsCardDivider() SettingsCardRow("Show Search Suggestions") { Toggle("", isOn: $browserSearchSuggestionsEnabled) .labelsHidden() .controlSize(.small) } SettingsCardDivider() SettingsCardRow( "Browser Theme", subtitle: selectedBrowserThemeMode == .system ? "System follows app and macOS appearance." : "\(selectedBrowserThemeMode.displayName) forces that color scheme for compatible pages.", controlWidth: pickerColumnWidth ) { Picker("", selection: browserThemeModeSelection) { ForEach(BrowserThemeMode.allCases) { mode in Text(mode.displayName).tag(mode.rawValue) } } .labelsHidden() .pickerStyle(.menu) } SettingsCardDivider() SettingsCardRow( "Open Terminal Links in cmux Browser", subtitle: "When off, links clicked in terminal output open in your default browser." ) { Toggle("", isOn: $openTerminalLinksInCmuxBrowser) .labelsHidden() .controlSize(.small) } SettingsCardDivider() SettingsCardRow( "Intercept open http(s) in Terminal", subtitle: "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( "Hosts to Open in Embedded Browser", subtitle: "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: 8) { Text("HTTP Hosts Allowed in Embedded Browser") .font(.system(size: 13, weight: .semibold)) Text("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("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("Save") { saveBrowserInsecureHTTPAllowlist() } .buttonStyle(.bordered) .controlSize(.small) .disabled(!browserInsecureHTTPAllowlistHasUnsavedChanges) .accessibilityIdentifier("SettingsBrowserHTTPAllowlistSaveButton") } VStack(alignment: .leading, spacing: 8) { Text("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("Save") { saveBrowserInsecureHTTPAllowlist() } .buttonStyle(.bordered) .controlSize(.small) .disabled(!browserInsecureHTTPAllowlistHasUnsavedChanges) .accessibilityIdentifier("SettingsBrowserHTTPAllowlistSaveButton") } } } } .padding(.horizontal, 14) .padding(.vertical, 10) SettingsCardDivider() SettingsCardRow("Browsing History", subtitle: browserHistorySubtitle) { Button("Clear History…") { showClearBrowserHistoryConfirmation = true } .buttonStyle(.bordered) .controlSize(.small) .disabled(browserHistoryEntryCount == 0) } } SettingsSectionHeader(title: "Keyboard Shortcuts") SettingsCard { 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("Click a shortcut value to record a new shortcut.") .font(.caption) .foregroundColor(.secondary) .padding(.leading, 2) SettingsSectionHeader(title: "Reset") SettingsCard { HStack { Spacer(minLength: 0) Button("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("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() browserThemeMode = BrowserThemeSettings.mode(defaults: .standard).rawValue browserHistoryEntryCount = BrowserHistoryStore.shared.entries.count browserInsecureHTTPAllowlistDraft = browserInsecureHTTPAllowlist reloadWorkspaceTabColorSettings() } .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( "Clear browser history?", isPresented: $showClearBrowserHistoryConfirmation, titleVisibility: .visible ) { Button("Clear History", role: .destructive) { BrowserHistoryStore.shared.clearHistory() } Button("Cancel", role: .cancel) {} } message: { Text("This removes visited-page suggestions from the browser omnibar.") } .confirmationDialog( "Enable full open access?", isPresented: $showOpenAccessConfirmation, titleVisibility: .visible ) { Button("Enable Full Open Access", role: .destructive) { socketControlMode = (pendingOpenAccessMode ?? .allowAll).rawValue pendingOpenAccessMode = nil } Button("Cancel", role: .cancel) { pendingOpenAccessMode = nil } } message: { Text("This disables ancestry and password checks and opens the socket to all local users. Only enable when you understand the risk.") } } private func resetAllSettings() { appearanceMode = AppearanceSettings.defaultMode.rawValue socketControlMode = SocketControlSettings.defaultMode.rawValue claudeCodeHooksEnabled = ClaudeCodeIntegrationSettings.defaultHooksEnabled browserSearchEngine = BrowserSearchSettings.defaultSearchEngine.rawValue browserSearchSuggestionsEnabled = BrowserSearchSettings.defaultSearchSuggestionsEnabled browserThemeMode = BrowserThemeSettings.defaultMode.rawValue openTerminalLinksInCmuxBrowser = BrowserLinkOpenSettings.defaultOpenTerminalLinksInCmuxBrowser interceptTerminalOpenCommandInCmuxBrowser = BrowserLinkOpenSettings.defaultInterceptTerminalOpenCommandInCmuxBrowser browserHostWhitelist = BrowserLinkOpenSettings.defaultBrowserHostWhitelist browserInsecureHTTPAllowlist = BrowserInsecureHTTPSettings.defaultAllowlistText browserInsecureHTTPAllowlistDraft = BrowserInsecureHTTPSettings.defaultAllowlistText notificationDockBadgeEnabled = NotificationBadgeSettings.defaultDockBadgeEnabled warnBeforeQuitShortcut = QuitWarningSettings.defaultWarnBeforeQuit commandPaletteRenameSelectAllOnFocus = CommandPaletteRenameSelectionSettings.defaultSelectAllOnFocus newWorkspacePlacement = WorkspacePlacementSettings.defaultPlacement.rawValue workspaceAutoReorder = WorkspaceAutoReorderSettings.defaultValue sidebarBranchVerticalLayout = SidebarBranchLayoutSettings.defaultVerticalLayout sidebarActiveTabIndicatorStyle = SidebarActiveTabIndicatorSettings.defaultStyle.rawValue sidebarShowBranchDirectory = true sidebarShowPullRequest = true openSidebarPullRequestLinksInCmuxBrowser = BrowserLinkOpenSettings.defaultOpenSidebarPullRequestLinksInCmuxBrowser sidebarShowPorts = true sidebarShowLog = true sidebarShowProgress = true sidebarShowMetadata = true showOpenAccessConfirmation = false pendingOpenAccessMode = nil socketPasswordDraft = "" socketPasswordStatusMessage = nil socketPasswordStatusIsError = false KeyboardShortcutSettings.resetAll() WorkspaceTabColorSettings.reset() reloadWorkspaceTabColorSettings() shortcutResetToken = UUID() } 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 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 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) } }