diff --git a/macos/Sources/Features/App Intents/IntentPermission.swift b/macos/Sources/Features/App Intents/IntentPermission.swift index 78efb3d5d..2ec4f2bd9 100644 --- a/macos/Sources/Features/App Intents/IntentPermission.swift +++ b/macos/Sources/Features/App Intents/IntentPermission.swift @@ -46,8 +46,9 @@ func requestIntentPermission() async -> Bool { PermissionRequest.show( "org.mitchellh.ghostty.shortcutsPermission", - message: "Allow Shortcuts to interact with Ghostty for the next 10 minutes?", - allowDuration: .seconds(600), + message: "Allow Shortcuts to interact with Ghostty?", + allowDuration: .forever, + rememberDuration: nil, ) { response in continuation.resume(returning: response) } diff --git a/macos/Sources/Helpers/PermissionRequest.swift b/macos/Sources/Helpers/PermissionRequest.swift index 35694081c..9c16c7163 100644 --- a/macos/Sources/Helpers/PermissionRequest.swift +++ b/macos/Sources/Helpers/PermissionRequest.swift @@ -3,17 +3,25 @@ import Foundation /// Displays a permission request dialog with optional caching of user decisions class PermissionRequest { + /// Specifies how long a permission decision should be cached + enum AllowDuration { + case once + case forever + case duration(Duration) + } + /// Shows a permission request dialog with customizable caching behavior /// - Parameters: /// - key: Unique identifier for storing/retrieving cached decisions in UserDefaults /// - message: The message to display in the alert dialog /// - allowText: Custom text for the allow button (defaults to "Allow") /// - allowDuration: If provided, automatically cache "Allow" responses for this duration + /// - rememberDuration: If provided, shows a checkbox to remember the decision for this duration /// - window: If provided, shows the alert as a sheet attached to this window /// - completion: Called with the user's decision (true for allow, false for deny) /// /// Caching behavior: - /// - If user checks "Remember my decision for one day", both allow/deny are cached for 24 hours + /// - If rememberDuration is provided and user checks "Remember my decision", both allow/deny are cached for that duration /// - If allowDuration is provided and user selects allow (without checkbox), decision is cached for that duration /// - Cached decisions are automatically returned without showing the dialog @MainActor @@ -22,7 +30,8 @@ class PermissionRequest { message: String, informative: String = "", allowText: String = "Allow", - allowDuration: Duration? = nil, + allowDuration: AllowDuration = .once, + rememberDuration: Duration? = .seconds(86400), window: NSWindow? = nil, completion: @escaping (Bool) -> Void ) { @@ -41,24 +50,28 @@ class PermissionRequest { alert.addButton(withTitle: allowText) alert.addButton(withTitle: "Don't Allow") - // Create checkbox for remembering - let checkbox = NSButton( - checkboxWithTitle: "Remember my decision for one day", - target: nil, - action: nil) - checkbox.state = .off - - // Set checkbox as accessory view - alert.accessoryView = checkbox + // Create checkbox for remembering if duration is provided + var checkbox: NSButton? + if let rememberDuration = rememberDuration { + let checkboxTitle = formatRememberText(for: rememberDuration) + checkbox = NSButton( + checkboxWithTitle: checkboxTitle, + target: nil, + action: nil) + checkbox!.state = .off + + // Set checkbox as accessory view + alert.accessoryView = checkbox + } // Show the alert if let window = window { alert.beginSheetModal(for: window) { response in - handleResponse(response, rememberDecision: checkbox.state == .on, key: key, allowDuration: allowDuration, completion: completion) + handleResponse(response, rememberDecision: checkbox?.state == .on, key: key, allowDuration: allowDuration, rememberDuration: rememberDuration, completion: completion) } } else { let response = alert.runModal() - handleResponse(response, rememberDecision: checkbox.state == .on, key: key, allowDuration: allowDuration, completion: completion) + handleResponse(response, rememberDecision: checkbox?.state == .on, key: key, allowDuration: allowDuration, rememberDuration: rememberDuration, completion: completion) } } @@ -68,12 +81,14 @@ class PermissionRequest { /// - rememberDecision: Whether the remember checkbox was checked /// - key: The UserDefaults key for caching /// - allowDuration: Optional duration for auto-caching allow responses + /// - rememberDuration: Optional duration for the remember checkbox /// - completion: Completion handler to call with the result private static func handleResponse( _ response: NSApplication.ModalResponse, rememberDecision: Bool, key: String, - allowDuration: Duration?, + allowDuration: AllowDuration, + rememberDuration: Duration?, completion: @escaping (Bool) -> Void) { let result: Bool @@ -87,10 +102,21 @@ class PermissionRequest { } // Store the result if checkbox is checked or if "Allow" was selected and allowDuration is set - if rememberDecision { - storeResult(result, for: key, duration: .seconds(86400)) - } else if result, let allowDuration { - storeResult(result, for: key, duration: allowDuration) + if rememberDecision, let rememberDuration = rememberDuration { + storeResult(result, for: key, duration: rememberDuration) + } else if result { + switch allowDuration { + case .once: + // Don't store anything for once + break + case .forever: + // Store for a very long time (100 years). When the bug comes in that + // 100 years has passed and their forever permission expired I'll be + // dead so it won't be my problem. + storeResult(result, for: key, duration: .seconds(3153600000)) + case .duration(let duration): + storeResult(result, for: key, duration: duration) + } } completion(result) @@ -130,6 +156,31 @@ class PermissionRequest { } } + /// Formats the remember checkbox text based on the duration + /// - Parameter duration: The duration to format + /// - Returns: A human-readable string for the checkbox + private static func formatRememberText(for duration: Duration) -> String { + let seconds = duration.timeInterval + + // Warning: this probably isn't localization friendly at all so we're + // going to have to redo this for that. + switch seconds { + case 0..<60: + return "Remember my decision for \(Int(seconds)) seconds" + case 60..<3600: + let minutes = Int(seconds / 60) + return "Remember my decision for \(minutes) minute\(minutes == 1 ? "" : "s")" + case 3600..<86400: + let hours = Int(seconds / 3600) + return "Remember my decision for \(hours) hour\(hours == 1 ? "" : "s")" + case 86400: + return "Remember my decision for one day" + default: + let days = Int(seconds / 86400) + return "Remember my decision for \(days) day\(days == 1 ? "" : "s")" + } + } + /// Internal class for storing permission decisions with expiration dates in UserDefaults /// Conforms to NSSecureCoding for safe archiving/unarchiving @objc(StoredPermission) diff --git a/src/config/Config.zig b/src/config/Config.zig index aee670213..aabf4f6ba 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2368,10 +2368,9 @@ keybind: Keybinds = .{}, /// /// Valid values are: /// -/// * `ask` - Ask the user whether for permission. Ghostty will by default -/// cache the user's choice for 10 minutes since we can't determine -/// when a single workflow begins or ends. The user also has an option -/// in the GUI to allow for the remainder of the day. +/// * `ask` - Ask the user whether for permission. Ghostty will remember +/// this choice and never ask again. This is similar to other macOS +/// permissions such as microphone access, camera access, etc. /// /// * `allow` - Allow Shortcuts to control Ghostty without asking. ///