macos: close all windows

Fixes #1052

This implements a `close_all_windows` binding in the core and implements
it for macOS specifically. This will ask for close confirmation if any
surface in any of the windows requires confirmation.

This is bound by default to option+shift+command+w to match Safari. The
binding is generall option+command+w but users may expect this to also
mean "Close All Other Tabs" which is the changed behavior if any tabs
are present in a standard macOS application. So I chose to follow Safari
instead.

This doesn't implement this feature for GTK, that's left as an exercise
for a contributor.
This commit is contained in:
Mitchell Hashimoto
2023-12-17 20:38:52 -08:00
parent b6ed5f7e4a
commit 97433d3aa1
7 changed files with 83 additions and 3 deletions

View File

@ -21,6 +21,7 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, UNUserNoti
@IBOutlet private var menuSplitVertical: NSMenuItem?
@IBOutlet private var menuClose: NSMenuItem?
@IBOutlet private var menuCloseWindow: NSMenuItem?
@IBOutlet private var menuCloseAllWindows: NSMenuItem?
@IBOutlet private var menuCopy: NSMenuItem?
@IBOutlet private var menuPaste: NSMenuItem?
@ -218,6 +219,7 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, UNUserNoti
syncMenuShortcut(action: "new_tab", menuItem: self.menuNewTab)
syncMenuShortcut(action: "close_surface", menuItem: self.menuClose)
syncMenuShortcut(action: "close_window", menuItem: self.menuCloseWindow)
syncMenuShortcut(action: "close_all_windows", menuItem: self.menuCloseAllWindows)
syncMenuShortcut(action: "new_split:right", menuItem: self.menuSplitHorizontal)
syncMenuShortcut(action: "new_split:down", menuItem: self.menuSplitVertical)
@ -360,6 +362,11 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, UNUserNoti
NSApp.activate(ignoringOtherApps: true)
}
@IBAction func closeAllWindows(_ sender: Any?) {
terminalManager.closeAllWindows()
AboutController.shared.hide()
}
@IBAction func showAbout(_ sender: Any?) {
AboutController.shared.show()
}

View File

@ -16,8 +16,11 @@ class AboutController: NSWindowController, NSWindowDelegate {
// MARK: - Functions
func show() {
guard let window = window else { return }
window.makeKeyAndOrderFront(nil)
window?.makeKeyAndOrderFront(nil)
}
func hide() {
window?.close()
}
//MARK: - First Responder

View File

@ -189,6 +189,52 @@ class TerminalManager {
}
}
/// Close all windows, asking for confirmation if necessary.
func closeAllWindows() {
var needsConfirm: Bool = false
for w in self.windows {
if (w.controller.surfaceTree?.needsConfirmQuit() ?? false) {
needsConfirm = true
break
}
}
if (!needsConfirm) {
for w in self.windows {
w.controller.close()
}
return
}
// If we don't have a main window, we just close all windows because
// we have no window to show the modal on top of. I'm sure there's a way
// to do an app-level alert but I don't know how and this case should never
// really happen.
guard let alertWindow = mainWindow?.controller.window else {
for w in self.windows {
w.controller.close()
}
return
}
// If we need confirmation by any, show one confirmation for all windows
let alert = NSAlert()
alert.messageText = "Close All Windows?"
alert.informativeText = "All terminal sessions will be terminated."
alert.addButton(withTitle: "Close All Windows")
alert.addButton(withTitle: "Cancel")
alert.alertStyle = .warning
alert.beginSheetModal(for: alertWindow, completionHandler: { response in
if (response == .alertFirstButtonReturn) {
for w in self.windows {
w.controller.close()
}
}
})
}
/// Relabels all the tabs with the proper keyboard shortcut.
func relabelAllTabs() {
for w in windows {

View File

@ -15,6 +15,7 @@
<customObject id="bbz-4X-AYv" userLabel="AppDelegate" customClass="AppDelegate" customModule="Ghostty" customModuleProvider="target">
<connections>
<outlet property="menuClose" destination="DVo-aG-piG" id="R3t-0C-aSU"/>
<outlet property="menuCloseAllWindows" destination="yKr-Vi-Yqw" id="Zet-Ir-zbm"/>
<outlet property="menuCloseWindow" destination="W5w-UZ-crk" id="6ff-BT-ENV"/>
<outlet property="menuCopy" destination="Jqf-pv-Zcu" id="bKd-1C-oy9"/>
<outlet property="menuDecreaseFontSize" destination="kzb-SZ-dOA" id="Y1B-Vh-6Z2"/>
@ -134,6 +135,12 @@
<action selector="closeWindow:" target="-1" id="ovs-xn-3ju"/>
</connections>
</menuItem>
<menuItem title="Close All Windows" id="yKr-Vi-Yqw">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="closeAllWindows:" target="-1" id="hrz-eb-l5t"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>

View File

@ -2885,6 +2885,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
.close_window => try self.app.closeSurface(self),
.close_all_windows => {
if (@hasDecl(apprt.Surface, "closeAllWindows")) {
self.rt_surface.closeAllWindows();
} else log.warn("runtime doesn't implement closeAllWindows", .{});
},
.quit => try self.app.setQuit(),
}

View File

@ -1183,6 +1183,11 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
.{ .key = .w, .mods = .{ .super = true, .shift = true } },
.{ .close_window = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .w, .mods = .{ .super = true, .shift = true, .alt = true } },
.{ .close_all_windows = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .t, .mods = .{ .super = true } },

View File

@ -221,12 +221,18 @@ pub const Action = union(enum) {
reload_config: void,
/// Close the current "surface", whether that is a window, tab, split,
/// etc. This only closes ONE surface.
/// etc. This only closes ONE surface. This will trigger close confirmation
/// as configured.
close_surface: void,
/// Close the window, regardless of how many tabs or splits there may be.
/// This will trigger close confirmation as configured.
close_window: void,
/// Close all windows. This will trigger close confirmation as configured.
/// This only works for macOS currently.
close_all_windows: void,
/// Toggle fullscreen mode of window.
toggle_fullscreen: void,