macOS: support injecting temporary defaults when testing

This commit is contained in:
Lukas
2026-03-12 13:11:08 +01:00
parent ab269e2c79
commit d6dfaf28fe
8 changed files with 44 additions and 18 deletions

View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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 }
}

View File

@@ -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
}
}

View File

@@ -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])

View File

@@ -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)
}
}