macos: the approval dialog is now forever

This commit is contained in:
Mitchell Hashimoto
2025-06-21 06:46:33 -07:00
parent 020976bf88
commit 296f340ff4
3 changed files with 75 additions and 24 deletions

View File

@ -46,8 +46,9 @@ func requestIntentPermission() async -> Bool {
PermissionRequest.show( PermissionRequest.show(
"org.mitchellh.ghostty.shortcutsPermission", "org.mitchellh.ghostty.shortcutsPermission",
message: "Allow Shortcuts to interact with Ghostty for the next 10 minutes?", message: "Allow Shortcuts to interact with Ghostty?",
allowDuration: .seconds(600), allowDuration: .forever,
rememberDuration: nil,
) { response in ) { response in
continuation.resume(returning: response) continuation.resume(returning: response)
} }

View File

@ -3,17 +3,25 @@ import Foundation
/// Displays a permission request dialog with optional caching of user decisions /// Displays a permission request dialog with optional caching of user decisions
class PermissionRequest { 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 /// Shows a permission request dialog with customizable caching behavior
/// - Parameters: /// - Parameters:
/// - key: Unique identifier for storing/retrieving cached decisions in UserDefaults /// - key: Unique identifier for storing/retrieving cached decisions in UserDefaults
/// - message: The message to display in the alert dialog /// - message: The message to display in the alert dialog
/// - allowText: Custom text for the allow button (defaults to "Allow") /// - allowText: Custom text for the allow button (defaults to "Allow")
/// - allowDuration: If provided, automatically cache "Allow" responses for this duration /// - 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 /// - 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) /// - completion: Called with the user's decision (true for allow, false for deny)
/// ///
/// Caching behavior: /// 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 /// - 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 /// - Cached decisions are automatically returned without showing the dialog
@MainActor @MainActor
@ -22,7 +30,8 @@ class PermissionRequest {
message: String, message: String,
informative: String = "", informative: String = "",
allowText: String = "Allow", allowText: String = "Allow",
allowDuration: Duration? = nil, allowDuration: AllowDuration = .once,
rememberDuration: Duration? = .seconds(86400),
window: NSWindow? = nil, window: NSWindow? = nil,
completion: @escaping (Bool) -> Void completion: @escaping (Bool) -> Void
) { ) {
@ -41,24 +50,28 @@ class PermissionRequest {
alert.addButton(withTitle: allowText) alert.addButton(withTitle: allowText)
alert.addButton(withTitle: "Don't Allow") alert.addButton(withTitle: "Don't Allow")
// Create checkbox for remembering // Create checkbox for remembering if duration is provided
let checkbox = NSButton( var checkbox: NSButton?
checkboxWithTitle: "Remember my decision for one day", if let rememberDuration = rememberDuration {
target: nil, let checkboxTitle = formatRememberText(for: rememberDuration)
action: nil) checkbox = NSButton(
checkbox.state = .off checkboxWithTitle: checkboxTitle,
target: nil,
action: nil)
checkbox!.state = .off
// Set checkbox as accessory view // Set checkbox as accessory view
alert.accessoryView = checkbox alert.accessoryView = checkbox
}
// Show the alert // Show the alert
if let window = window { if let window = window {
alert.beginSheetModal(for: window) { response in 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 { } else {
let response = alert.runModal() 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 /// - rememberDecision: Whether the remember checkbox was checked
/// - key: The UserDefaults key for caching /// - key: The UserDefaults key for caching
/// - allowDuration: Optional duration for auto-caching allow responses /// - allowDuration: Optional duration for auto-caching allow responses
/// - rememberDuration: Optional duration for the remember checkbox
/// - completion: Completion handler to call with the result /// - completion: Completion handler to call with the result
private static func handleResponse( private static func handleResponse(
_ response: NSApplication.ModalResponse, _ response: NSApplication.ModalResponse,
rememberDecision: Bool, rememberDecision: Bool,
key: String, key: String,
allowDuration: Duration?, allowDuration: AllowDuration,
rememberDuration: Duration?,
completion: @escaping (Bool) -> Void) { completion: @escaping (Bool) -> Void) {
let result: Bool 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 // Store the result if checkbox is checked or if "Allow" was selected and allowDuration is set
if rememberDecision { if rememberDecision, let rememberDuration = rememberDuration {
storeResult(result, for: key, duration: .seconds(86400)) storeResult(result, for: key, duration: rememberDuration)
} else if result, let allowDuration { } else if result {
storeResult(result, for: key, duration: allowDuration) 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) 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 /// Internal class for storing permission decisions with expiration dates in UserDefaults
/// Conforms to NSSecureCoding for safe archiving/unarchiving /// Conforms to NSSecureCoding for safe archiving/unarchiving
@objc(StoredPermission) @objc(StoredPermission)

View File

@ -2368,10 +2368,9 @@ keybind: Keybinds = .{},
/// ///
/// Valid values are: /// Valid values are:
/// ///
/// * `ask` - Ask the user whether for permission. Ghostty will by default /// * `ask` - Ask the user whether for permission. Ghostty will remember
/// cache the user's choice for 10 minutes since we can't determine /// this choice and never ask again. This is similar to other macOS
/// when a single workflow begins or ends. The user also has an option /// permissions such as microphone access, camera access, etc.
/// in the GUI to allow for the remainder of the day.
/// ///
/// * `allow` - Allow Shortcuts to control Ghostty without asking. /// * `allow` - Allow Shortcuts to control Ghostty without asking.
/// ///