mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
Add close_tab
keybinding action for macOS (#4395)
This PR adds support for a dedicated `close_tab` keybinding action, allowing users to bind specific keys for closing tabs. The implementation: - Adds `close_tab` as a new keybinding action - Preserves all existing confirmation dialogs for running processes - Works seamlessly with macOS native tab system ### Testing - [x] Tested with single tabs - [x] Tested with multiple tabs - [x] Tested with running processes (confirmation dialog) - [x] Tested with splits within tabs <img width="797" alt="image" src="https://github.com/user-attachments/assets/8e09eea3-1f71-40a3-a835-76de14013a29" /> https://github.com/user-attachments/assets/155210f7-20fe-4a96-8800-6969df214871 Partially resolved #4331
This commit is contained in:
@ -30,6 +30,7 @@ class AppDelegate: NSObject,
|
|||||||
@IBOutlet private var menuSplitRight: NSMenuItem?
|
@IBOutlet private var menuSplitRight: NSMenuItem?
|
||||||
@IBOutlet private var menuSplitDown: NSMenuItem?
|
@IBOutlet private var menuSplitDown: NSMenuItem?
|
||||||
@IBOutlet private var menuClose: NSMenuItem?
|
@IBOutlet private var menuClose: NSMenuItem?
|
||||||
|
@IBOutlet private var menuCloseTab: NSMenuItem?
|
||||||
@IBOutlet private var menuCloseWindow: NSMenuItem?
|
@IBOutlet private var menuCloseWindow: NSMenuItem?
|
||||||
@IBOutlet private var menuCloseAllWindows: NSMenuItem?
|
@IBOutlet private var menuCloseAllWindows: NSMenuItem?
|
||||||
|
|
||||||
@ -347,6 +348,7 @@ class AppDelegate: NSObject,
|
|||||||
syncMenuShortcut(config, action: "new_window", menuItem: self.menuNewWindow)
|
syncMenuShortcut(config, action: "new_window", menuItem: self.menuNewWindow)
|
||||||
syncMenuShortcut(config, action: "new_tab", menuItem: self.menuNewTab)
|
syncMenuShortcut(config, action: "new_tab", menuItem: self.menuNewTab)
|
||||||
syncMenuShortcut(config, action: "close_surface", menuItem: self.menuClose)
|
syncMenuShortcut(config, action: "close_surface", menuItem: self.menuClose)
|
||||||
|
syncMenuShortcut(config, action: "close_tab", menuItem: self.menuCloseTab)
|
||||||
syncMenuShortcut(config, action: "close_window", menuItem: self.menuCloseWindow)
|
syncMenuShortcut(config, action: "close_window", menuItem: self.menuCloseWindow)
|
||||||
syncMenuShortcut(config, action: "close_all_windows", menuItem: self.menuCloseAllWindows)
|
syncMenuShortcut(config, action: "close_all_windows", menuItem: self.menuCloseAllWindows)
|
||||||
syncMenuShortcut(config, action: "new_split:right", menuItem: self.menuSplitRight)
|
syncMenuShortcut(config, action: "new_split:right", menuItem: self.menuSplitRight)
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
<outlet property="menuCheckForUpdates" destination="GEA-5y-yzH" id="0nV-Tf-nJQ"/>
|
<outlet property="menuCheckForUpdates" destination="GEA-5y-yzH" id="0nV-Tf-nJQ"/>
|
||||||
<outlet property="menuClose" destination="DVo-aG-piG" id="R3t-0C-aSU"/>
|
<outlet property="menuClose" destination="DVo-aG-piG" id="R3t-0C-aSU"/>
|
||||||
<outlet property="menuCloseAllWindows" destination="yKr-Vi-Yqw" id="Zet-Ir-zbm"/>
|
<outlet property="menuCloseAllWindows" destination="yKr-Vi-Yqw" id="Zet-Ir-zbm"/>
|
||||||
|
<outlet property="menuCloseTab" destination="Obb-Mk-j8J" id="Gda-L0-gdz"/>
|
||||||
<outlet property="menuCloseWindow" destination="W5w-UZ-crk" id="6ff-BT-ENV"/>
|
<outlet property="menuCloseWindow" destination="W5w-UZ-crk" id="6ff-BT-ENV"/>
|
||||||
<outlet property="menuCopy" destination="Jqf-pv-Zcu" id="bKd-1C-oy9"/>
|
<outlet property="menuCopy" destination="Jqf-pv-Zcu" id="bKd-1C-oy9"/>
|
||||||
<outlet property="menuDecreaseFontSize" destination="kzb-SZ-dOA" id="Y1B-Vh-6Z2"/>
|
<outlet property="menuDecreaseFontSize" destination="kzb-SZ-dOA" id="Y1B-Vh-6Z2"/>
|
||||||
@ -155,6 +156,12 @@
|
|||||||
<action selector="close:" target="-1" id="tTZ-2b-Mbm"/>
|
<action selector="close:" target="-1" id="tTZ-2b-Mbm"/>
|
||||||
</connections>
|
</connections>
|
||||||
</menuItem>
|
</menuItem>
|
||||||
|
<menuItem title="Close Tab" id="Obb-Mk-j8J">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="closeTab:" target="-1" id="UBb-Bd-nkj"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
<menuItem title="Close Window" id="W5w-UZ-crk">
|
<menuItem title="Close Window" id="W5w-UZ-crk">
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
<connections>
|
<connections>
|
||||||
|
@ -60,6 +60,11 @@ class TerminalController: BaseTerminalController {
|
|||||||
selector: #selector(onGotoTab),
|
selector: #selector(onGotoTab),
|
||||||
name: Ghostty.Notification.ghosttyGotoTab,
|
name: Ghostty.Notification.ghosttyGotoTab,
|
||||||
object: nil)
|
object: nil)
|
||||||
|
center.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(onCloseTab),
|
||||||
|
name: .ghosttyCloseTab,
|
||||||
|
object: nil)
|
||||||
center.addObserver(
|
center.addObserver(
|
||||||
self,
|
self,
|
||||||
selector: #selector(ghosttyConfigDidChange(_:)),
|
selector: #selector(ghosttyConfigDidChange(_:)),
|
||||||
@ -508,7 +513,50 @@ class TerminalController: BaseTerminalController {
|
|||||||
ghostty.newTab(surface: surface)
|
ghostty.newTab(surface: surface)
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction override func closeWindow(_ sender: Any) {
|
private func confirmClose(
|
||||||
|
window: NSWindow,
|
||||||
|
messageText: String,
|
||||||
|
informativeText: String,
|
||||||
|
completion: @escaping () -> Void
|
||||||
|
) {
|
||||||
|
// If we need confirmation by any, show one confirmation for all windows
|
||||||
|
// in the tab group.
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.messageText = messageText
|
||||||
|
alert.informativeText = informativeText
|
||||||
|
alert.addButton(withTitle: "Close")
|
||||||
|
alert.addButton(withTitle: "Cancel")
|
||||||
|
alert.alertStyle = .warning
|
||||||
|
alert.beginSheetModal(for: window) { response in
|
||||||
|
if response == .alertFirstButtonReturn {
|
||||||
|
completion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func closeTab(_ sender: Any?) {
|
||||||
|
guard let window = window else { return }
|
||||||
|
guard window.tabGroup != nil else {
|
||||||
|
// No tabs, no tab group, just perform a normal close.
|
||||||
|
window.performClose(sender)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if surfaceTree?.needsConfirmQuit() ?? false {
|
||||||
|
confirmClose(
|
||||||
|
window: window,
|
||||||
|
messageText: "Close Tab?",
|
||||||
|
informativeText: "The terminal still has a running process. If you close the tab the process will be killed."
|
||||||
|
) {
|
||||||
|
window.close()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
window.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction override func closeWindow(_ sender: Any?) {
|
||||||
guard let window = window else { return }
|
guard let window = window else { return }
|
||||||
guard let tabGroup = window.tabGroup else {
|
guard let tabGroup = window.tabGroup else {
|
||||||
// No tabs, no tab group, just perform a normal close.
|
// No tabs, no tab group, just perform a normal close.
|
||||||
@ -523,47 +571,34 @@ class TerminalController: BaseTerminalController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if any windows require close confirmation.
|
// Check if any windows require close confirmation.
|
||||||
var needsConfirm: Bool = false
|
let needsConfirm = tabGroup.windows.contains { tabWindow in
|
||||||
for tabWindow in tabGroup.windows {
|
guard let controller = tabWindow.windowController as? TerminalController else {
|
||||||
guard let c = tabWindow.windowController as? TerminalController else { continue }
|
return false
|
||||||
if (c.surfaceTree?.needsConfirmQuit() ?? false) {
|
|
||||||
needsConfirm = true
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
return controller.surfaceTree?.needsConfirmQuit() ?? false
|
||||||
}
|
}
|
||||||
|
|
||||||
// If none need confirmation then we can just close all the windows.
|
// If none need confirmation then we can just close all the windows.
|
||||||
if (!needsConfirm) {
|
if !needsConfirm {
|
||||||
for tabWindow in tabGroup.windows {
|
tabGroup.windows.forEach { $0.close() }
|
||||||
tabWindow.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we need confirmation by any, show one confirmation for all windows
|
confirmClose(
|
||||||
// in the tab group.
|
window: window,
|
||||||
let alert = NSAlert()
|
messageText: "Close Window?",
|
||||||
alert.messageText = "Close Window?"
|
informativeText: "All terminal sessions in this window will be terminated."
|
||||||
alert.informativeText = "All terminal sessions in this window will be terminated."
|
) {
|
||||||
alert.addButton(withTitle: "Close Window")
|
tabGroup.windows.forEach { $0.close() }
|
||||||
alert.addButton(withTitle: "Cancel")
|
}
|
||||||
alert.alertStyle = .warning
|
|
||||||
alert.beginSheetModal(for: window, completionHandler: { response in
|
|
||||||
if (response == .alertFirstButtonReturn) {
|
|
||||||
for tabWindow in tabGroup.windows {
|
|
||||||
tabWindow.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func toggleGhosttyFullScreen(_ sender: Any) {
|
@IBAction func toggleGhosttyFullScreen(_ sender: Any?) {
|
||||||
guard let surface = focusedSurface?.surface else { return }
|
guard let surface = focusedSurface?.surface else { return }
|
||||||
ghostty.toggleFullscreen(surface: surface)
|
ghostty.toggleFullscreen(surface: surface)
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func toggleTerminalInspector(_ sender: Any) {
|
@IBAction func toggleTerminalInspector(_ sender: Any?) {
|
||||||
guard let surface = focusedSurface?.surface else { return }
|
guard let surface = focusedSurface?.surface else { return }
|
||||||
ghostty.toggleTerminalInspector(surface: surface)
|
ghostty.toggleTerminalInspector(surface: surface)
|
||||||
}
|
}
|
||||||
@ -720,6 +755,12 @@ class TerminalController: BaseTerminalController {
|
|||||||
targetWindow.makeKeyAndOrderFront(nil)
|
targetWindow.makeKeyAndOrderFront(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func onCloseTab(notification: SwiftUI.Notification) {
|
||||||
|
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
||||||
|
guard surfaceTree?.contains(view: target) ?? false else { return }
|
||||||
|
closeTab(self)
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func onToggleFullscreen(notification: SwiftUI.Notification) {
|
@objc private func onToggleFullscreen(notification: SwiftUI.Notification) {
|
||||||
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
||||||
guard target == self.focusedSurface else { return }
|
guard target == self.focusedSurface else { return }
|
||||||
|
@ -448,6 +448,9 @@ extension Ghostty {
|
|||||||
case GHOSTTY_ACTION_NEW_SPLIT:
|
case GHOSTTY_ACTION_NEW_SPLIT:
|
||||||
newSplit(app, target: target, direction: action.action.new_split)
|
newSplit(app, target: target, direction: action.action.new_split)
|
||||||
|
|
||||||
|
case GHOSTTY_ACTION_CLOSE_TAB:
|
||||||
|
closeTab(app, target: target)
|
||||||
|
|
||||||
case GHOSTTY_ACTION_TOGGLE_FULLSCREEN:
|
case GHOSTTY_ACTION_TOGGLE_FULLSCREEN:
|
||||||
toggleFullscreen(app, target: target, mode: action.action.toggle_fullscreen)
|
toggleFullscreen(app, target: target, mode: action.action.toggle_fullscreen)
|
||||||
|
|
||||||
@ -651,6 +654,27 @@ extension Ghostty {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func closeTab(_ app: ghostty_app_t, target: ghostty_target_s) {
|
||||||
|
switch (target.tag) {
|
||||||
|
case GHOSTTY_TARGET_APP:
|
||||||
|
Ghostty.logger.warning("close tab does nothing with an app target")
|
||||||
|
return
|
||||||
|
|
||||||
|
case GHOSTTY_TARGET_SURFACE:
|
||||||
|
guard let surface = target.target.surface else { return }
|
||||||
|
guard let surfaceView = self.surfaceView(from: surface) else { return }
|
||||||
|
|
||||||
|
NotificationCenter.default.post(
|
||||||
|
name: .ghosttyCloseTab,
|
||||||
|
object: surfaceView
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
default:
|
||||||
|
assertionFailure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static func toggleFullscreen(
|
private static func toggleFullscreen(
|
||||||
_ app: ghostty_app_t,
|
_ app: ghostty_app_t,
|
||||||
target: ghostty_target_s,
|
target: ghostty_target_s,
|
||||||
|
@ -236,6 +236,9 @@ extension Notification.Name {
|
|||||||
/// Goto tab. Has tab index in the userinfo.
|
/// Goto tab. Has tab index in the userinfo.
|
||||||
static let ghosttyMoveTab = Notification.Name("com.mitchellh.ghostty.moveTab")
|
static let ghosttyMoveTab = Notification.Name("com.mitchellh.ghostty.moveTab")
|
||||||
static let GhosttyMoveTabKey = ghosttyMoveTab.rawValue
|
static let GhosttyMoveTabKey = ghosttyMoveTab.rawValue
|
||||||
|
|
||||||
|
/// Close tab
|
||||||
|
static let ghosttyCloseTab = Notification.Name("com.mitchellh.ghostty.closeTab")
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: I am moving all of these to Notification.Name extensions over time. This
|
// NOTE: I am moving all of these to Notification.Name extensions over time. This
|
||||||
|
@ -4266,6 +4266,7 @@ fn closingAction(action: input.Binding.Action) bool {
|
|||||||
return switch (action) {
|
return switch (action) {
|
||||||
.close_surface,
|
.close_surface,
|
||||||
.close_window,
|
.close_window,
|
||||||
|
.close_tab,
|
||||||
=> true,
|
=> true,
|
||||||
|
|
||||||
else => false,
|
else => false,
|
||||||
|
@ -375,6 +375,10 @@ pub const Action = union(enum) {
|
|||||||
/// configured.
|
/// configured.
|
||||||
close_surface: void,
|
close_surface: void,
|
||||||
|
|
||||||
|
/// Close the current tab, regardless of how many splits there may be.
|
||||||
|
/// This will trigger close confirmation as configured.
|
||||||
|
close_tab: void,
|
||||||
|
|
||||||
/// Close the window, regardless of how many tabs or splits there may be.
|
/// Close the window, regardless of how many tabs or splits there may be.
|
||||||
/// This will trigger close confirmation as configured.
|
/// This will trigger close confirmation as configured.
|
||||||
close_window: void,
|
close_window: void,
|
||||||
@ -383,9 +387,6 @@ pub const Action = union(enum) {
|
|||||||
/// This only works for macOS currently.
|
/// This only works for macOS currently.
|
||||||
close_all_windows: void,
|
close_all_windows: void,
|
||||||
|
|
||||||
/// Closes the tab belonging to the currently focused split.
|
|
||||||
close_tab: void,
|
|
||||||
|
|
||||||
/// Toggle fullscreen mode of window.
|
/// Toggle fullscreen mode of window.
|
||||||
toggle_fullscreen: void,
|
toggle_fullscreen: void,
|
||||||
|
|
||||||
@ -729,6 +730,7 @@ pub const Action = union(enum) {
|
|||||||
.write_screen_file,
|
.write_screen_file,
|
||||||
.write_selection_file,
|
.write_selection_file,
|
||||||
.close_surface,
|
.close_surface,
|
||||||
|
.close_tab,
|
||||||
.close_window,
|
.close_window,
|
||||||
.toggle_fullscreen,
|
.toggle_fullscreen,
|
||||||
.toggle_window_decorations,
|
.toggle_window_decorations,
|
||||||
@ -753,7 +755,6 @@ pub const Action = union(enum) {
|
|||||||
.resize_split,
|
.resize_split,
|
||||||
.equalize_splits,
|
.equalize_splits,
|
||||||
.inspector,
|
.inspector,
|
||||||
.close_tab,
|
|
||||||
=> .surface,
|
=> .surface,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user