diff --git a/include/ghostty.h b/include/ghostty.h index e66ce08ea..c64ce0160 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -508,6 +508,7 @@ typedef enum { GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW, GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS, GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL, + GHOSTTY_ACTION_TOGGLE_VISIBILITY, GHOSTTY_ACTION_GOTO_TAB, GHOSTTY_ACTION_GOTO_SPLIT, GHOSTTY_ACTION_RESIZE_SPLIT, diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 5980b8d66..e926e380b 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -86,6 +86,11 @@ class AppDelegate: NSObject, return ProcessInfo.processInfo.systemUptime - applicationLaunchTime } + /// Tracks whether the application is currently visible. This can be gamed, i.e. if a user manually + /// brings each window one by one to the front. But at worst its off by one set of toggles and this + /// makes our logic very easy. + private var isVisible: Bool = true + override init() { terminalManager = TerminalManager(ghostty) updaterController = SPUStandardUpdaterController( @@ -251,6 +256,7 @@ class AppDelegate: NSObject, // Ghostty will validate as well but we can avoid creating an entirely new // surface by doing our own validation here. We can also show a useful error // this way. + var isDirectory = ObjCBool(true) guard FileManager.default.fileExists(atPath: filename, isDirectory: &isDirectory) else { return false } @@ -315,6 +321,7 @@ class AppDelegate: NSObject, syncMenuShortcut(action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize) syncMenuShortcut(action: "reset_font_size", menuItem: self.menuResetFontSize) syncMenuShortcut(action: "toggle_quick_terminal", menuItem: self.menuQuickTerminal) + syncMenuShortcut(action: "toggle_visibility", menuItem: self.menuQuickTerminal) syncMenuShortcut(action: "inspector:toggle", menuItem: self.menuTerminalInspector) syncMenuShortcut(action: "toggle_secure_input", menuItem: self.menuSecureInput) @@ -564,4 +571,23 @@ class AppDelegate: NSObject, self.menuQuickTerminal?.state = if (quickController.visible) { .on } else { .off } } + + /// Toggles visibility of all Ghosty Terminal windows. When hidden, activates Ghostty as the frontmost application + @IBAction func toggleVisibility(_ sender: Any) { + // We only care about terminal windows. + for window in NSApp.windows.filter({ $0.windowController is BaseTerminalController }) { + if isVisible { + window.orderOut(nil) + } else { + window.makeKeyAndOrderFront(nil) + } + } + + // After bringing them all to front we make sure our app is active too. + if !isVisible { + NSApp.activate(ignoringOtherApps: true) + } + + isVisible.toggle() + } } diff --git a/macos/Sources/App/macOS/MainMenu.xib b/macos/Sources/App/macOS/MainMenu.xib index 63aae4c60..2982d6cfc 100644 --- a/macos/Sources/App/macOS/MainMenu.xib +++ b/macos/Sources/App/macOS/MainMenu.xib @@ -217,10 +217,16 @@ - + - + + + + + + + diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 352fb4107..9198e48b6 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -490,6 +490,9 @@ extension Ghostty { case GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL: toggleQuickTerminal(app, target: target) + case GHOSTTY_ACTION_TOGGLE_VISIBILITY: + toggleVisibility(app, target: target) + case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS: fallthrough case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW: @@ -630,6 +633,14 @@ extension Ghostty { } } + private static func toggleVisibility( + _ app: ghostty_app_t, + target: ghostty_target_s + ) { + guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return } + appDelegate.toggleVisibility(self) + } + private static func gotoTab( _ app: ghostty_app_t, target: ghostty_target_s, diff --git a/src/App.zig b/src/App.zig index 5922528ab..7e82bf007 100644 --- a/src/App.zig +++ b/src/App.zig @@ -325,6 +325,7 @@ pub fn performAction( .reload_config => try self.reloadConfig(rt_app), .close_all_windows => try rt_app.performAction(.app, .close_all_windows, {}), .toggle_quick_terminal => try rt_app.performAction(.app, .toggle_quick_terminal, {}), + .toggle_visibility => try rt_app.performAction(.app, .toggle_visibility, {}), } } diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 2f7616bc4..5c8ba6a01 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -96,6 +96,9 @@ pub const Action = union(Key) { /// Toggle the quick terminal in or out. toggle_quick_terminal, + /// Toggle the visibility of all Ghostty terminal windows. + toggle_visibility, + /// Jump to a specific tab. Must handle the scenario that the tab /// value is invalid. goto_tab: GotoTab, @@ -180,6 +183,7 @@ pub const Action = union(Key) { toggle_tab_overview, toggle_window_decorations, toggle_quick_terminal, + toggle_visibility, goto_tab, goto_split, resize_split, diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 87314c0e1..948b38a29 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -197,6 +197,7 @@ pub const App = struct { .toggle_tab_overview, .toggle_window_decorations, .toggle_quick_terminal, + .toggle_visibility, .goto_tab, .inspector, .render_inspector, diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 8e683829c..2e0bbad84 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -389,6 +389,7 @@ pub fn performAction( .close_all_windows, .toggle_split_zoom, .toggle_quick_terminal, + .toggle_visibility, .size_limit, .cell_size, .secure_input, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index da87ac230..9f4ebd626 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -387,6 +387,13 @@ pub const Action = union(enum) { /// configuration file to customize its behavior. toggle_quick_terminal: void, + /// Show/hide all windows. If all windows become shown, we also ensure + /// Ghostty is focused. + /// + /// This currently only works on macOS. When hiding all windows, we do + /// not yield focus to the previous application. + toggle_visibility: void, + /// Quit ghostty. quit: void, @@ -588,6 +595,7 @@ pub const Action = union(enum) { .close_all_windows, .quit, .toggle_quick_terminal, + .toggle_visibility, => .app, // These are app but can be special-cased in a surface context.