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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/swift-file-length-budget.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
7306 cmuxTests/WorkspaceUnitTests.swift
6943 cmuxTests/WorkspaceRemoteConnectionTests.swift
6555 cmuxTests/GhosttyConfigTests.swift
6330 cmuxTests/SessionPersistenceTests.swift
6332 cmuxTests/SessionPersistenceTests.swift
6153 CLI/cmux_open.swift
6091 Sources/TabManager.swift
6071 Sources/TextBoxInput.swift
Expand Down Expand Up @@ -51,8 +51,8 @@
1949 Sources/Panels/BrowserWebAuthnSupport.swift
1860 cmuxTests/NotificationAndMenuBarTests.swift
1794 Sources/SessionIndexStore.swift
1777 Sources/RestorableAgentSession.swift
1751 Sources/WindowDragHandleView.swift
1744 Sources/RestorableAgentSession.swift
1722 cmuxTests/TerminalControllerSocketSecurityTests.swift
1693 cmuxTests/WorkspacePullRequestSidebarTests.swift
1677 cmuxUITests/BrowserPaneNavigationKeybindUITests.swift
Expand All @@ -70,18 +70,18 @@
1362 Sources/CMUXInstalledExtensionSidebarHostView.swift
1312 cmuxTests/MobileHostAuthorizationTests.swift
1285 cmuxUITests/SidebarHelpMenuUITests.swift
1270 cmuxTests/RestorableAgentSessionIndexTests.swift
1256 Sources/Feed/FeedCoordinator.swift
1205 Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextView.swift
1197 cmuxTests/CodexAppServerSessionTests.swift
1166 Sources/VaultAgentProcessScanner.swift
1157 cmuxTests/SidebarOrderingTests.swift
1144 Sources/VaultAgentProcessScanner.swift
1139 cmuxTests/PiVaultAgentPersistenceTests.swift
1126 cmuxTests/FileExplorerStoreTests.swift
1119 cmuxTests/AgentHibernationTests.swift
1107 Sources/AppDelegate+CmuxSSHURL.swift
1096 Sources/GhosttyConfig.swift
1093 cmuxUITests/BonsplitTabDragUITests.swift
1084 cmuxTests/AgentHibernationTests.swift
1084 cmuxTests/RestorableAgentSessionIndexTests.swift
1021 cmuxUITests/TerminalCmdClickUITests.swift
1006 cmuxTests/CmuxSSHURLRequestTests.swift
1000 cmuxTests/CmuxTopSnapshotScopeTests.swift
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ public protocol SettingsHostActions: AnyObject {
/// window scene so the package can't open it directly.
func openTerminalConfigWindow()

/// Persists an explicit menu-bar-only preference change in the host app.
///
/// The host pairs the visible `app.menuBarOnly` setting with any hidden
/// safety marker it needs before changing the process activation policy.
func setMenuBarOnly(_ enabled: Bool)

/// Opens the iOS pairing window, which shows a scannable QR code for
/// pairing an iPhone with this Mac. The host owns the window so the
/// package can't open it directly.
Expand Down Expand Up @@ -142,6 +148,8 @@ public protocol SettingsHostActions: AnyObject {
public extension SettingsHostActions {
func openMobilePairingWindow() {}

func setMenuBarOnly(_ enabled: Bool) {}

func browserHistoryEntryCount() -> Int? { nil }

/// Default: no status, for hosts without a live mobile service (previews/tests).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ public struct AppSection: View {
String(localized: "settings.app.menuBarOnly", defaultValue: "Menu Bar Only"),
subtitle: String(localized: "settings.app.menuBarOnly.subtitle", defaultValue: "Hide the Dock icon and Cmd+Tab entry. Use the menu bar item to show cmux.")
) {
Toggle("", isOn: Binding(get: { menuBarOnly.current }, set: { menuBarOnly.set($0) }))
Toggle("", isOn: Binding(get: { menuBarOnly.current }, set: { enabled in hostActions.setMenuBarOnly(enabled); menuBarOnly.set(enabled) }))
Comment thread
austinywang marked this conversation as resolved.
Outdated
.labelsHidden()
.controlSize(.small)
.accessibilityIdentifier("SettingsMenuBarOnlyToggle")
Expand Down
10 changes: 5 additions & 5 deletions Sources/App/MenuBarExtraController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -519,15 +519,15 @@ enum MenuBarExtraSettings {

enum MenuBarOnlySettings {
static let menuBarOnlyKey = "menuBarOnly"
static let defaultMenuBarOnly = false
static let explicitEnableKey = "menuBarOnlyExplicitlyEnabled.v1"; static let defaultMenuBarOnly = false
Comment thread
austinywang marked this conversation as resolved.
Outdated

static func isEnabled(defaults: UserDefaults = .standard) -> Bool {
if defaults.object(forKey: menuBarOnlyKey) == nil {
return defaultMenuBarOnly
}
return defaults.bool(forKey: menuBarOnlyKey)
guard defaults.object(forKey: menuBarOnlyKey) != nil, defaults.bool(forKey: menuBarOnlyKey) else { return defaultMenuBarOnly }
return defaults.object(forKey: explicitEnableKey) != nil ? defaults.bool(forKey: explicitEnableKey) : !legacyCommandPaletteToggleWasUsed(defaults: defaults)
Comment thread
austinywang marked this conversation as resolved.
Outdated
}

static func setEnabled(_ enabled: Bool, defaults: UserDefaults = .standard) { defaults.set(enabled, forKey: menuBarOnlyKey); defaults.set(enabled, forKey: explicitEnableKey) }
Comment thread
austinywang marked this conversation as resolved.
Outdated

static func activationPolicy(defaults: UserDefaults = .standard) -> NSApplication.ActivationPolicy {
isEnabled(defaults: defaults) ? .accessory : .regular
}
Expand Down
22 changes: 11 additions & 11 deletions Sources/CommandPalette/CommandPaletteSettingsToggle.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import Foundation
import CmuxSettings

extension MenuBarOnlySettings {
static let legacyCommandPaletteUsageKey = "commandPalette.commandUsage.v1"
static let legacyCommandPaletteMenuBarOnlyCommandId = "palette.toggleSetting.menuBarOnly"

static func legacyCommandPaletteToggleWasUsed(defaults: UserDefaults = .standard) -> Bool {
guard let data = defaults.data(forKey: legacyCommandPaletteUsageKey),
let history = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return false }
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
return history[legacyCommandPaletteMenuBarOnlyCommandId] != nil
}
}

struct CommandPaletteSettingToggleDescriptor: Sendable {
let commandId: String
let settingsKey: String
Expand Down Expand Up @@ -268,17 +279,6 @@ enum CommandPaletteSettingsToggleCommands {
defaultValue: NotificationBadgeSettings.defaultDockBadgeEnabled,
defaultsKey: NotificationBadgeSettings.dockBadgeEnabledKey
),
CommandPaletteSettingToggleDescriptor(
commandId: commandIdPrefix + "menuBarOnly",
settingsKey: "app.menuBarOnly",
title: {
String(localized: "settings.app.menuBarOnly", defaultValue: "Menu Bar Only")
},
sectionTitle: app,
keywords: ["app.menuBarOnly", "menu", "bar", "dock", "cmd-tab", "app", "switcher"],
defaultValue: MenuBarOnlySettings.defaultMenuBarOnly,
defaultsKey: MenuBarOnlySettings.menuBarOnlyKey
),
CommandPaletteSettingToggleDescriptor(
commandId: commandIdPrefix + "showInMenuBar",
settingsKey: "notifications.showInMenuBar",
Expand Down
4 changes: 4 additions & 0 deletions Sources/HostSettingsActions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ final class HostSettingsActions: SettingsHostActions {
window.orderFrontRegardless()
}

func setMenuBarOnly(_ enabled: Bool) {
MenuBarOnlySettings.setEnabled(enabled)
}

func openMobilePairingWindow() {
MobilePairingWindowController.shared.show()
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/KeyboardShortcutSettingsFileStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ final class CmuxSettingsFileStore {
snapshot.managedUserDefaults[AppIconSettings.modeKey] = .string(mode.rawValue)
}
if let value = jsonBool(section["menuBarOnly"]) {
snapshot.managedUserDefaults[MenuBarOnlySettings.menuBarOnlyKey] = .bool(value)
snapshot.managedUserDefaults[MenuBarOnlySettings.menuBarOnlyKey] = .bool(value); snapshot.managedUserDefaults[MenuBarOnlySettings.explicitEnableKey] = .bool(value)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
if let raw = jsonString(section["windowTitleTemplate"]) { snapshot.managedUserDefaults[WindowTitleTemplate.userDefaultsKey] = .string(raw) } else if section.keys.contains("windowTitleTemplate") { logInvalid("app.windowTitleTemplate", sourcePath: sourcePath) }
if let raw = jsonString(section["newWorkspacePlacement"]) {
Expand Down
42 changes: 41 additions & 1 deletion cmuxTests/CommandPaletteSettingsToggleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,51 @@ final class CommandPaletteSettingsToggleTests: XCTestCase {
)

XCTAssertTrue(descriptor.isAvailable(defaults))
defaults.set(true, forKey: MenuBarOnlySettings.menuBarOnlyKey)
MenuBarOnlySettings.setEnabled(true, defaults: defaults)
XCTAssertFalse(descriptor.isAvailable(defaults))
}
}

func testMenuBarOnlyCommandIsNotExposedAsInstantToggle() {
XCTAssertNil(
CommandPaletteSettingsToggleCommands.descriptor(
commandId: "palette.toggleSetting.menuBarOnly"
)
)
XCTAssertFalse(
ContentView.commandPaletteSettingsToggleCommandContributions()
.contains { $0.commandId == "palette.toggleSetting.menuBarOnly" }
)
}

func testMenuBarOnlyCommandHistoryDoesNotEnableAccessoryPolicy() throws {
try withTemporaryDefaults { defaults in
defaults.set(true, forKey: MenuBarOnlySettings.menuBarOnlyKey)
defaults.set(
try JSONSerialization.data(withJSONObject: [
MenuBarOnlySettings.legacyCommandPaletteMenuBarOnlyCommandId: [
"useCount": 1,
"lastUsedAt": 1_700_000_000,
],
]),
forKey: MenuBarOnlySettings.legacyCommandPaletteUsageKey
)

XCTAssertFalse(MenuBarOnlySettings.isEnabled(defaults: defaults))
XCTAssertEqual(MenuBarOnlySettings.activationPolicy(defaults: defaults), .regular)
XCTAssertFalse(MenuBarOnlySettings.shouldShowMainWindowMenuItem(defaults: defaults))
}
}

func testLegacyMenuBarOnlyDefaultWithoutCommandHistoryStillOptsIn() throws {
try withTemporaryDefaults { defaults in
defaults.set(true, forKey: MenuBarOnlySettings.menuBarOnlyKey)

XCTAssertTrue(MenuBarOnlySettings.isEnabled(defaults: defaults))
XCTAssertEqual(MenuBarOnlySettings.activationPolicy(defaults: defaults), .accessory)
}
}

func testInterceptTerminalOpenCommandReadsRawSettingWhenBrowserIsDisabled() throws {
try withTemporaryDefaults { defaults in
let descriptor = try XCTUnwrap(
Expand Down
41 changes: 41 additions & 0 deletions cmuxTests/KeyboardShortcutSettingsFileStoreMigrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,35 @@ private final class ShortcutSettingsLookupRecorder: @unchecked Sendable {
}

final class KeyboardShortcutSettingsFileStoreMigrationTests: XCTestCase {
private let settingsFileBackupsDefaultsKey = "cmux.settingsFile.backups.v1"
private let importedManagedDefaultsKey = "cmux.settingsFile.importedManagedDefaults.v1"

func testSettingsFileStoreMarksMenuBarOnlyAsExplicitOptIn() throws {
let defaults = UserDefaults.standard
let settingKey = MenuBarOnlySettings.menuBarOnlyKey
let explicitKey = MenuBarOnlySettings.explicitEnableKey

try preservingDefaults(keys: [settingKey, explicitKey, settingsFileBackupsDefaultsKey, importedManagedDefaultsKey]) {
let directoryURL = try makeTemporaryDirectory()
defer { try? FileManager.default.removeItem(at: directoryURL) }

let settingsFileURL = directoryURL.appendingPathComponent("cmux.json", isDirectory: false)
try writeSettingsFile(#"{"app":{"menuBarOnly":true}}"#, to: settingsFileURL)

_ = KeyboardShortcutSettingsFileStore(
primaryPath: settingsFileURL.path,
fallbackPath: nil,
additionalFallbackPaths: [],
startWatching: false
)

XCTAssertEqual(defaults.object(forKey: settingKey) as? Bool, true)
XCTAssertEqual(defaults.object(forKey: explicitKey) as? Bool, true)
XCTAssertTrue(MenuBarOnlySettings.isEnabled(defaults: defaults))
XCTAssertEqual(MenuBarOnlySettings.activationPolicy(defaults: defaults), .accessory)
}
}

func testBootstrapMigratesLegacySettingsIntoCanonicalConfig() throws {
let directoryURL = try makeTemporaryDirectory()
defer { try? FileManager.default.removeItem(at: directoryURL) }
Expand Down Expand Up @@ -243,4 +272,16 @@ final class KeyboardShortcutSettingsFileStoreMigrationTests: XCTestCase {
)
try contents.write(to: url, atomically: true, encoding: .utf8)
}

private func preservingDefaults(keys: [String], _ body: () throws -> Void) rethrows {
let defaults = UserDefaults.standard
let previous = Dictionary(uniqueKeysWithValues: keys.map { ($0, defaults.object(forKey: $0)) })
keys.forEach { defaults.removeObject(forKey: $0) }
defer {
for (key, value) in previous {
if let value { defaults.set(value, forKey: key) } else { defaults.removeObject(forKey: key) }
}
}
try body()
}
}
8 changes: 4 additions & 4 deletions cmuxTests/NotificationAndMenuBarTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -805,12 +805,12 @@ final class NotificationDockBadgeTests: XCTestCase {
XCTAssertEqual(MenuBarOnlySettings.activationPolicy(defaults: defaults), .regular)
XCTAssertFalse(MenuBarOnlySettings.shouldShowMainWindowMenuItem(defaults: defaults))

defaults.set(true, forKey: MenuBarOnlySettings.menuBarOnlyKey)
MenuBarOnlySettings.setEnabled(true, defaults: defaults)
XCTAssertTrue(MenuBarOnlySettings.isEnabled(defaults: defaults))
XCTAssertEqual(MenuBarOnlySettings.activationPolicy(defaults: defaults), .accessory)
XCTAssertTrue(MenuBarOnlySettings.shouldShowMainWindowMenuItem(defaults: defaults))

defaults.set(false, forKey: MenuBarOnlySettings.menuBarOnlyKey)
MenuBarOnlySettings.setEnabled(false, defaults: defaults)
XCTAssertFalse(MenuBarOnlySettings.isEnabled(defaults: defaults))
XCTAssertEqual(MenuBarOnlySettings.activationPolicy(defaults: defaults), .regular)
XCTAssertFalse(MenuBarOnlySettings.shouldShowMainWindowMenuItem(defaults: defaults))
Expand All @@ -829,10 +829,10 @@ final class NotificationDockBadgeTests: XCTestCase {
defaults.set(false, forKey: MenuBarExtraSettings.showInMenuBarKey)
XCTAssertFalse(MenuBarExtraSettings.shouldInstallMenuBarExtra(defaults: defaults))

defaults.set(true, forKey: MenuBarOnlySettings.menuBarOnlyKey)
MenuBarOnlySettings.setEnabled(true, defaults: defaults)
XCTAssertTrue(MenuBarExtraSettings.shouldInstallMenuBarExtra(defaults: defaults))

defaults.set(false, forKey: MenuBarOnlySettings.menuBarOnlyKey)
MenuBarOnlySettings.setEnabled(false, defaults: defaults)
defaults.set(true, forKey: MenuBarExtraSettings.showInMenuBarKey)
XCTAssertTrue(MenuBarExtraSettings.shouldInstallMenuBarExtra(defaults: defaults))
}
Expand Down
Loading