mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
Merge pull request #611 from mitchellh/close-confirm
macos: change close confirmations to NSAlert
This commit is contained in:
@ -10,9 +10,6 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyApp
|
|||||||
category: String(describing: AppDelegate.self)
|
category: String(describing: AppDelegate.self)
|
||||||
)
|
)
|
||||||
|
|
||||||
// confirmQuit published so other views can check whether quit needs to be confirmed.
|
|
||||||
@Published var confirmQuit: Bool = false
|
|
||||||
|
|
||||||
/// Various menu items so that we can programmatically sync the keyboard shortcut with the Ghostty config.
|
/// Various menu items so that we can programmatically sync the keyboard shortcut with the Ghostty config.
|
||||||
@IBOutlet private var menuReloadConfig: NSMenuItem?
|
@IBOutlet private var menuReloadConfig: NSMenuItem?
|
||||||
@IBOutlet private var menuQuit: NSMenuItem?
|
@IBOutlet private var menuQuit: NSMenuItem?
|
||||||
@ -113,9 +110,20 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyApp
|
|||||||
// If our app says we don't need to confirm, we can exit now.
|
// If our app says we don't need to confirm, we can exit now.
|
||||||
if (!ghostty.needsConfirmQuit) { return .terminateNow }
|
if (!ghostty.needsConfirmQuit) { return .terminateNow }
|
||||||
|
|
||||||
// We have some visible window, and all our windows will watch the confirmQuit.
|
// We have some visible window. Show an app-wide modal to confirm quitting.
|
||||||
confirmQuit = true
|
let alert = NSAlert()
|
||||||
return .terminateLater
|
alert.messageText = "Quit Ghostty?"
|
||||||
|
alert.informativeText = "All terminal sessions will be terminated."
|
||||||
|
alert.addButton(withTitle: "Close Ghostty")
|
||||||
|
alert.addButton(withTitle: "Cancel")
|
||||||
|
alert.alertStyle = .warning
|
||||||
|
switch (alert.runModal()) {
|
||||||
|
case .alertFirstButtonReturn:
|
||||||
|
return .terminateNow
|
||||||
|
|
||||||
|
default:
|
||||||
|
return .terminateCancel
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This is called when the application is already open and someone double-clicks the icon
|
/// This is called when the application is already open and someone double-clicks the icon
|
||||||
|
@ -28,28 +28,6 @@ struct PrimaryView: View {
|
|||||||
@FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle
|
@FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle
|
||||||
@FocusedValue(\.ghosttySurfaceZoomed) private var zoomedSplit
|
@FocusedValue(\.ghosttySurfaceZoomed) private var zoomedSplit
|
||||||
|
|
||||||
// This is true if this view should be the one to show the quit confirmation.
|
|
||||||
var ownsQuitConfirmation: Bool {
|
|
||||||
// We need to have a window to show a confirmation.
|
|
||||||
guard let window = self.window else { return false }
|
|
||||||
|
|
||||||
// If we are the key window then definitely yes.
|
|
||||||
if (window.isKeyWindow) { return true }
|
|
||||||
|
|
||||||
// If there is some other PrimaryWindow that is key, let it handle it.
|
|
||||||
let windows = NSApplication.shared.windows
|
|
||||||
if (windows.contains {
|
|
||||||
guard let primary = $0 as? PrimaryWindow else { return false }
|
|
||||||
return primary.isKeyWindow
|
|
||||||
}) { return false }
|
|
||||||
|
|
||||||
// We aren't the key window but also there is no key PrimaryWindow.
|
|
||||||
// If we are the FIRST PrimaryWindow in the windows array, then
|
|
||||||
// we take the job.
|
|
||||||
guard let firstWindow = (windows.first { $0 is PrimaryWindow }) else { return false }
|
|
||||||
return window == firstWindow
|
|
||||||
}
|
|
||||||
|
|
||||||
// The title for our window
|
// The title for our window
|
||||||
private var title: String {
|
private var title: String {
|
||||||
var title = "👻"
|
var title = "👻"
|
||||||
@ -73,27 +51,13 @@ struct PrimaryView: View {
|
|||||||
switch ghostty.readiness {
|
switch ghostty.readiness {
|
||||||
case .loading:
|
case .loading:
|
||||||
Text("Loading")
|
Text("Loading")
|
||||||
.onChange(of: appDelegate.confirmQuit) { value in
|
|
||||||
guard value else { return }
|
|
||||||
NSApplication.shared.reply(toApplicationShouldTerminate: true)
|
|
||||||
}
|
|
||||||
case .error:
|
case .error:
|
||||||
ErrorView()
|
ErrorView()
|
||||||
.onChange(of: appDelegate.confirmQuit) { value in
|
|
||||||
guard value else { return }
|
|
||||||
NSApplication.shared.reply(toApplicationShouldTerminate: true)
|
|
||||||
}
|
|
||||||
case .ready:
|
case .ready:
|
||||||
let center = NotificationCenter.default
|
let center = NotificationCenter.default
|
||||||
let gotoTab = center.publisher(for: Ghostty.Notification.ghosttyGotoTab)
|
let gotoTab = center.publisher(for: Ghostty.Notification.ghosttyGotoTab)
|
||||||
let toggleFullscreen = center.publisher(for: Ghostty.Notification.ghosttyToggleFullscreen)
|
let toggleFullscreen = center.publisher(for: Ghostty.Notification.ghosttyToggleFullscreen)
|
||||||
|
|
||||||
let confirmQuitting = Binding<Bool>(get: {
|
|
||||||
self.appDelegate.confirmQuit && self.ownsQuitConfirmation
|
|
||||||
}, set: {
|
|
||||||
self.appDelegate.confirmQuit = $0
|
|
||||||
})
|
|
||||||
|
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
// If we're running in debug mode we show a warning so that users
|
// If we're running in debug mode we show a warning so that users
|
||||||
// know that performance will be degraded.
|
// know that performance will be degraded.
|
||||||
@ -118,21 +82,6 @@ struct PrimaryView: View {
|
|||||||
guard let window = self.window else { return }
|
guard let window = self.window else { return }
|
||||||
window.title = newValue
|
window.title = newValue
|
||||||
}
|
}
|
||||||
.confirmationDialog(
|
|
||||||
"Quit Ghostty?",
|
|
||||||
isPresented: confirmQuitting) {
|
|
||||||
Button("Close Ghostty") {
|
|
||||||
NSApplication.shared.reply(toApplicationShouldTerminate: true)
|
|
||||||
}
|
|
||||||
.keyboardShortcut(.defaultAction)
|
|
||||||
|
|
||||||
Button("Cancel", role: .cancel) {
|
|
||||||
NSApplication.shared.reply(toApplicationShouldTerminate: false)
|
|
||||||
}
|
|
||||||
.keyboardShortcut(.cancelAction)
|
|
||||||
} message: {
|
|
||||||
Text("All terminal sessions will be terminated.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,10 +21,6 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.frame(minWidth: 500, maxWidth: 500, minHeight: 156, maxHeight: 156)
|
.frame(minWidth: 500, maxWidth: 500, minHeight: 156, maxHeight: 156)
|
||||||
.onChange(of: appDelegate.confirmQuit) { value in
|
|
||||||
guard value else { return }
|
|
||||||
NSApplication.shared.reply(toApplicationShouldTerminate: true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -337,9 +337,6 @@ extension Ghostty {
|
|||||||
/// This will be set to true when the split requests that is become closed.
|
/// This will be set to true when the split requests that is become closed.
|
||||||
@Binding var requestClose: Bool
|
@Binding var requestClose: Bool
|
||||||
|
|
||||||
/// This controls whether we're actively confirming if we want to close or not.
|
|
||||||
@State private var confirmClose: Bool = false
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
let center = NotificationCenter.default
|
let center = NotificationCenter.default
|
||||||
let pub = center.publisher(for: Notification.ghosttyNewSplit, object: leaf.surface)
|
let pub = center.publisher(for: Notification.ghosttyNewSplit, object: leaf.surface)
|
||||||
@ -350,18 +347,6 @@ extension Ghostty {
|
|||||||
.onReceive(pub) { onNewSplit(notification: $0) }
|
.onReceive(pub) { onNewSplit(notification: $0) }
|
||||||
.onReceive(pubClose) { onClose(notification: $0) }
|
.onReceive(pubClose) { onClose(notification: $0) }
|
||||||
.onReceive(pubFocus) { onMoveFocus(notification: $0) }
|
.onReceive(pubFocus) { onMoveFocus(notification: $0) }
|
||||||
.confirmationDialog(
|
|
||||||
"Close Terminal?",
|
|
||||||
isPresented: $confirmClose) {
|
|
||||||
Button("Close the Terminal") {
|
|
||||||
confirmClose = false
|
|
||||||
requestClose = true
|
|
||||||
}
|
|
||||||
.keyboardShortcut(.defaultAction)
|
|
||||||
} message: {
|
|
||||||
Text("The terminal still has a running process. If you close the terminal " +
|
|
||||||
"the process will be killed.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func onClose(notification: SwiftUI.Notification) {
|
private func onClose(notification: SwiftUI.Notification) {
|
||||||
@ -378,8 +363,34 @@ extension Ghostty {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Child process is alive, so we want to show a confirmation.
|
// If we don't have a window to attach our modal to, we also exit immediately.
|
||||||
confirmClose = true
|
// This should NOT happen.
|
||||||
|
guard let window = leaf.surface.window else {
|
||||||
|
requestClose = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm close. We use an NSAlert instead of a SwiftUI confirmationDialog
|
||||||
|
// due to SwiftUI bugs (see Ghostty #560). To repeat from #560, the bug is that
|
||||||
|
// confirmationDialog allows the user to Cmd-W close the alert, but when doing
|
||||||
|
// so SwiftUI does not update any of the bindings to note that window is no longer
|
||||||
|
// being shown, and provides no callback to detect this.
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.messageText = "Close Terminal?"
|
||||||
|
alert.informativeText = "The terminal still has a running process. If you close the " +
|
||||||
|
"terminal the process will be killed."
|
||||||
|
alert.addButton(withTitle: "Close the Terminal")
|
||||||
|
alert.addButton(withTitle: "Cancel")
|
||||||
|
alert.alertStyle = .warning
|
||||||
|
alert.beginSheetModal(for: window, completionHandler: { response in
|
||||||
|
switch (response) {
|
||||||
|
case .alertFirstButtonReturn:
|
||||||
|
requestClose = true
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private func onNewSplit(notification: SwiftUI.Notification) {
|
private func onNewSplit(notification: SwiftUI.Notification) {
|
||||||
|
Reference in New Issue
Block a user