diff --git a/macos/GhosttyUITests/GhosttyCustomConfigCase.swift b/macos/GhosttyUITests/GhosttyCustomConfigCase.swift index 41993247a..ca3f56677 100644 --- a/macos/GhosttyUITests/GhosttyCustomConfigCase.swift +++ b/macos/GhosttyUITests/GhosttyCustomConfigCase.swift @@ -27,6 +27,8 @@ class GhosttyCustomConfigCase: XCTestCase { true } + static let defaultsSuiteName: String = "GHOSTTY_UI_TESTS" + var configFile: URL? override func setUpWithError() throws { continueAfterFailure = false @@ -47,13 +49,14 @@ class GhosttyCustomConfigCase: XCTestCase { try newConfig.write(to: configFile!, atomically: true, encoding: .utf8) } - func ghosttyApplication() throws -> XCUIApplication { + func ghosttyApplication(defaultsSuite: String = GhosttyCustomConfigCase.defaultsSuiteName) throws -> XCUIApplication { let app = XCUIApplication() app.launchArguments.append(contentsOf: ["-ApplePersistenceIgnoreState", "YES"]) guard let configFile else { return app } app.launchEnvironment["GHOSTTY_CONFIG_PATH"] = configFile.path + app.launchEnvironment["GHOSTTY_USER_DEFAULTS_SUITE"] = defaultsSuite return app } } diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index c8538c9d5..b02337e4b 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -175,7 +175,15 @@ class AppDelegate: NSObject, // MARK: - NSApplicationDelegate func applicationWillFinishLaunching(_ notification: Notification) { - UserDefaults.standard.register(defaults: [ + #if DEBUG + if + let suite = UserDefaults.ghosttySuite, + let clear = ProcessInfo.processInfo.environment["GHOSTTY_CLEAR_USER_DEFAULTS"], + (clear as NSString).boolValue { + UserDefaults.ghostty.removePersistentDomain(forName: suite) + } + #endif + UserDefaults.ghostty.register(defaults: [ // Disable the automatic full screen menu item because we handle // it manually. "NSFullScreenMenuItemEverywhere": false, @@ -194,7 +202,7 @@ class AppDelegate: NSObject, func applicationDidFinishLaunching(_ notification: Notification) { // System settings overrides - UserDefaults.standard.register(defaults: [ + UserDefaults.ghostty.register(defaults: [ // Disable this so that repeated key events make it through to our terminal views. "ApplePressAndHoldEnabled": false, ]) @@ -203,7 +211,7 @@ class AppDelegate: NSObject, applicationLaunchTime = ProcessInfo.processInfo.systemUptime // Check if secure input was enabled when we last quit. - if UserDefaults.standard.bool(forKey: "SecureInput") != SecureInput.shared.enabled { + if UserDefaults.ghostty.bool(forKey: "SecureInput") != SecureInput.shared.enabled { toggleSecureInput(self) } @@ -747,10 +755,10 @@ class AppDelegate: NSObject, // configuration. This is the only way to carefully control whether macOS invokes the // state restoration system. switch config.windowSaveState { - case "never": UserDefaults.standard.setValue(false, forKey: "NSQuitAlwaysKeepsWindows") - case "always": UserDefaults.standard.setValue(true, forKey: "NSQuitAlwaysKeepsWindows") + case "never": UserDefaults.ghostty.setValue(false, forKey: "NSQuitAlwaysKeepsWindows") + case "always": UserDefaults.ghostty.setValue(true, forKey: "NSQuitAlwaysKeepsWindows") case "default": fallthrough - default: UserDefaults.standard.removeObject(forKey: "NSQuitAlwaysKeepsWindows") + default: UserDefaults.ghostty.removeObject(forKey: "NSQuitAlwaysKeepsWindows") } // Sync our auto-update settings. If SUEnableAutomaticChecks (in our Info.plist) is @@ -835,9 +843,9 @@ class AppDelegate: NSObject, private func updateAppIcon(from config: Ghostty.Config) { // Since this is called after `DockTilePlugin` has been running, // clean it up here to trigger a correct update of the current config. - UserDefaults.standard.removeObject(forKey: "CustomGhosttyIcon") + UserDefaults.ghostty.removeObject(forKey: "CustomGhosttyIcon") DispatchQueue.global().async { - UserDefaults.standard.appIcon = AppIcon(config: config) + UserDefaults.ghostty.appIcon = AppIcon(config: config) DistributedNotificationCenter.default() .postNotificationName(.ghosttyIconDidChange, object: nil, userInfo: nil, deliverImmediately: true) } @@ -927,7 +935,7 @@ class AppDelegate: NSObject, input.global.toggle() } self.menuSecureInput?.state = if input.global { .on } else { .off } - UserDefaults.standard.set(input.global, forKey: "SecureInput") + UserDefaults.ghostty.set(input.global, forKey: "SecureInput") } // MARK: - IB Actions @@ -1321,7 +1329,7 @@ extension AppDelegate { } @IBAction func useAsDefault(_ sender: NSMenuItem) { - let ud = UserDefaults.standard + let ud = UserDefaults.ghostty let key = TerminalWindow.defaultLevelKey if menuFloatOnTop?.state == .on { ud.set(NSWindow.Level.floating, forKey: key) diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 560f45207..b9ca1ecc4 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -171,7 +171,7 @@ class TerminalWindow: NSWindow { tab.accessoryView = stackView // Get our saved level - level = UserDefaults.standard.value(forKey: Self.defaultLevelKey) as? NSWindow.Level ?? .normal + level = UserDefaults.ghostty.value(forKey: Self.defaultLevelKey) as? NSWindow.Level ?? .normal } // Both of these must be true for windows without decorations to be able to diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index c4f03b117..3129acfba 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -1070,7 +1070,7 @@ extension Ghostty { // If the user has force click enabled then we do a quick look. There // is no public API for this as far as I can tell. - guard UserDefaults.standard.bool(forKey: "com.apple.trackpad.forceClick") else { return } + guard UserDefaults.ghostty.bool(forKey: "com.apple.trackpad.forceClick") else { return } quickLook(with: event) } diff --git a/macos/Sources/Helpers/Extensions/NSScreen+Extension.swift b/macos/Sources/Helpers/Extensions/NSScreen+Extension.swift index ca338f102..84553ed34 100644 --- a/macos/Sources/Helpers/Extensions/NSScreen+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSScreen+Extension.swift @@ -18,7 +18,7 @@ extension NSScreen { // AND present on this screen. var hasDock: Bool { // If the dock autohides then we don't have a dock ever. - if let dockAutohide = UserDefaults.standard.persistentDomain(forName: "com.apple.dock")?["autohide"] as? Bool { + if let dockAutohide = UserDefaults.ghostty.persistentDomain(forName: "com.apple.dock")?["autohide"] as? Bool { if dockAutohide { return false } } diff --git a/macos/Sources/Helpers/Extensions/UserDefaults+Extension.swift b/macos/Sources/Helpers/Extensions/UserDefaults+Extension.swift new file mode 100644 index 000000000..7cd0e12ed --- /dev/null +++ b/macos/Sources/Helpers/Extensions/UserDefaults+Extension.swift @@ -0,0 +1,15 @@ +import Foundation + +extension UserDefaults { + static var ghosttySuite: String? { + #if DEBUG + ProcessInfo.processInfo.environment["GHOSTTY_USER_DEFAULTS_SUITE"] + #else + nil + #endif + } + + static var ghostty: UserDefaults { + ghosttySuite.flatMap(UserDefaults.init(suiteName:)) ?? .standard + } +} diff --git a/macos/Sources/Helpers/LastWindowPosition.swift b/macos/Sources/Helpers/LastWindowPosition.swift index 933eba394..298367c74 100644 --- a/macos/Sources/Helpers/LastWindowPosition.swift +++ b/macos/Sources/Helpers/LastWindowPosition.swift @@ -15,7 +15,7 @@ class LastWindowPosition { guard let window, window.isVisible else { return false } let frame = window.frame let rect = [frame.origin.x, frame.origin.y, frame.size.width, frame.size.height] - UserDefaults.standard.set(rect, forKey: positionKey) + UserDefaults.ghostty.set(rect, forKey: positionKey) return true } @@ -32,7 +32,7 @@ class LastWindowPosition { func restore(_ window: NSWindow, origin restoreOrigin: Bool = true, size restoreSize: Bool = true) -> Bool { guard restoreOrigin || restoreSize else { return false } - guard let values = UserDefaults.standard.array(forKey: positionKey) as? [Double], + guard let values = UserDefaults.ghostty.array(forKey: positionKey) as? [Double], values.count >= 2 else { return false } let lastPosition = CGPoint(x: values[0], y: values[1]) diff --git a/macos/Sources/Helpers/PermissionRequest.swift b/macos/Sources/Helpers/PermissionRequest.swift index 29d1ab6d3..0308a0204 100644 --- a/macos/Sources/Helpers/PermissionRequest.swift +++ b/macos/Sources/Helpers/PermissionRequest.swift @@ -126,7 +126,7 @@ class PermissionRequest { /// - Parameter key: The UserDefaults key to check /// - Returns: The cached decision, or nil if no valid cached decision exists private static func getStoredResult(for key: String) -> Bool? { - let userDefaults = UserDefaults.standard + let userDefaults = UserDefaults.ghostty guard let data = userDefaults.data(forKey: key), let storedPermission = try? NSKeyedUnarchiver.unarchivedObject( ofClass: StoredPermission.self, from: data) else { @@ -151,7 +151,7 @@ class PermissionRequest { let expiryDate = Date().addingTimeInterval(duration.timeInterval) let storedPermission = StoredPermission(result: result, expiry: expiryDate) if let data = try? NSKeyedArchiver.archivedData(withRootObject: storedPermission, requiringSecureCoding: true) { - let userDefaults = UserDefaults.standard + let userDefaults = UserDefaults.ghostty userDefaults.set(data, forKey: key) } }