diff --git a/include/ghostty.h b/include/ghostty.h index c2bdfc975..78c6dfb89 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -49,6 +49,12 @@ typedef enum { GHOSTTY_SPLIT_FOCUS_RIGHT, } ghostty_split_focus_direction_e; +typedef enum { + GHOSTTY_INSPECTOR_TOGGLE, + GHOSTTY_INSPECTOR_SHOW, + GHOSTTY_INSPECTOR_HIDE, +} ghostty_inspector_mode_e; + typedef enum { GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_PRESS, @@ -323,6 +329,7 @@ typedef void (*ghostty_runtime_write_clipboard_cb)(void *, const char *, ghostty typedef void (*ghostty_runtime_new_split_cb)(void *, ghostty_split_direction_e, ghostty_surface_config_s); typedef void (*ghostty_runtime_new_tab_cb)(void *, ghostty_surface_config_s); typedef void (*ghostty_runtime_new_window_cb)(void *, ghostty_surface_config_s); +typedef void (*ghostty_runtime_control_inspector_cb)(void *, ghostty_inspector_mode_e); typedef void (*ghostty_runtime_close_surface_cb)(void *, bool); typedef void (*ghostty_runtime_focus_split_cb)(void *, ghostty_split_focus_direction_e); typedef void (*ghostty_runtime_toggle_split_zoom_cb)(void *); @@ -344,6 +351,7 @@ typedef struct { ghostty_runtime_new_split_cb new_split_cb; ghostty_runtime_new_tab_cb new_tab_cb; ghostty_runtime_new_window_cb new_window_cb; + ghostty_runtime_control_inspector_cb control_inspector_cb; ghostty_runtime_close_surface_cb close_surface_cb; ghostty_runtime_focus_split_cb focus_split_cb; ghostty_runtime_toggle_split_zoom_cb toggle_split_zoom_cb; diff --git a/macos/Sources/AppDelegate.swift b/macos/Sources/AppDelegate.swift index 833fae4c4..cf9b53427 100644 --- a/macos/Sources/AppDelegate.swift +++ b/macos/Sources/AppDelegate.swift @@ -36,6 +36,7 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyApp @IBOutlet private var menuIncreaseFontSize: NSMenuItem? @IBOutlet private var menuDecreaseFontSize: NSMenuItem? @IBOutlet private var menuResetFontSize: NSMenuItem? + @IBOutlet private var menuTerminalInspector: NSMenuItem? /// The dock menu private var dockMenu: NSMenu = NSMenu() @@ -216,6 +217,7 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyApp syncMenuShortcut(action: "increase_font_size:1", menuItem: self.menuIncreaseFontSize) syncMenuShortcut(action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize) syncMenuShortcut(action: "reset_font_size", menuItem: self.menuResetFontSize) + syncMenuShortcut(action: "inspector:toggle", menuItem: self.menuTerminalInspector) // Dock menu reloadDockMenu() @@ -394,4 +396,9 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, GhosttyApp guard let surface = focusedSurface() else { return } ghostty.changeFontSize(surface: surface, .reset) } + + @IBAction func toggleTerminalInspector(_ sender: Any) { + guard let surface = focusedSurface() else { return } + ghostty.toggleTerminalInspector(surface: surface) + } } diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift index fb03c41e2..f81131b32 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -136,6 +136,7 @@ extension Ghostty { new_split_cb: { userdata, direction, surfaceConfig in AppState.newSplit(userdata, direction: direction, config: surfaceConfig) }, new_tab_cb: { userdata, surfaceConfig in AppState.newTab(userdata, config: surfaceConfig) }, new_window_cb: { userdata, surfaceConfig in AppState.newWindow(userdata, config: surfaceConfig) }, + control_inspector_cb: { userdata, mode in AppState.controlInspector(userdata, mode: mode) }, close_surface_cb: { userdata, processAlive in AppState.closeSurface(userdata, processAlive: processAlive) }, focus_split_cb: { userdata, direction in AppState.focusSplit(userdata, direction: direction) }, toggle_split_zoom_cb: { userdata in AppState.toggleSplitZoom(userdata) }, @@ -299,6 +300,13 @@ extension Ghostty { AppDelegate.logger.warning("action failed action=\(action)") } } + + func toggleTerminalInspector(surface: ghostty_surface_t) { + let action = "inspector:toggle" + if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { + AppDelegate.logger.warning("action failed action=\(action)") + } + } // Called when the selected keyboard changes. We have to notify Ghostty so that // it can reload the keyboard mapping for input. @@ -494,6 +502,13 @@ extension Ghostty { ] ) } + + static func controlInspector(_ userdata: UnsafeMutableRawPointer?, mode: ghostty_inspector_mode_e) { + guard let surface = self.surfaceUserdata(from: userdata) else { return } + NotificationCenter.default.post(name: Notification.didControlInspector, object: surface, userInfo: [ + "mode": mode, + ]) + } /// Returns the GhosttyState from the given userdata value. static private func appState(fromView view: SurfaceView) -> AppState? { diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index 051ff4656..bd8db5f12 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -105,6 +105,9 @@ extension Ghostty.Notification { /// Notification to render the inspector for a surface static let inspectorNeedsDisplay = Notification.Name("com.mitchellh.ghostty.inspectorNeedsDisplay") + + /// Notification to show/hide the inspector + static let didControlInspector = Notification.Name("com.mitchellh.ghostty.didControlInspector") } // Make the input enum hashable. diff --git a/macos/Sources/MainMenu.xib b/macos/Sources/MainMenu.xib index fa8beb2f0..910430b95 100644 --- a/macos/Sources/MainMenu.xib +++ b/macos/Sources/MainMenu.xib @@ -1,8 +1,8 @@ - + - + @@ -33,6 +33,7 @@ + @@ -153,6 +154,13 @@ + + + + + + + diff --git a/src/Surface.zig b/src/Surface.zig index 9a4860873..cc3a4a734 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2278,6 +2278,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool } else log.warn("runtime doesn't implement toggleFullscreen", .{}); }, + .inspector => |mode| { + if (@hasDecl(apprt.Surface, "controlInspector")) { + self.rt_surface.controlInspector(mode); + } else log.warn("runtime doesn't implement controlInspector", .{}); + }, + .close_surface => self.close(), .close_window => try self.app.closeSurface(self), diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index a439ffbb4..2289ef9a9 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -73,6 +73,9 @@ pub const App = struct { /// New window with options. new_window: ?*const fn (SurfaceUD, apprt.Surface.Options) callconv(.C) void = null, + /// Control the inspector visibility + control_inspector: ?*const fn (SurfaceUD, input.InspectorMode) callconv(.C) void = null, + /// Close the current surface given by this function. close_surface: ?*const fn (SurfaceUD, bool) callconv(.C) void = null, @@ -326,6 +329,15 @@ pub const Surface = struct { } } + pub fn controlInspector(self: *const Surface, mode: input.InspectorMode) void { + const func = self.app.opts.control_inspector orelse { + log.info("runtime embedder does not support the terminal inspector", .{}); + return; + }; + + func(self.opts.userdata, mode); + } + pub fn newSplit(self: *const Surface, direction: input.SplitDirection) !void { const func = self.app.opts.new_split orelse { log.info("runtime embedder does not support splits", .{}); diff --git a/src/input.zig b/src/input.zig index 21a8702d6..c90226b39 100644 --- a/src/input.zig +++ b/src/input.zig @@ -8,6 +8,7 @@ pub const keycodes = @import("input/keycodes.zig"); pub const kitty = @import("input/kitty.zig"); pub const Binding = @import("input/Binding.zig"); pub const KeyEncoder = @import("input/KeyEncoder.zig"); +pub const InspectorMode = Binding.Action.InspectorMode; pub const SplitDirection = Binding.Action.SplitDirection; pub const SplitFocusDirection = Binding.Action.SplitFocusDirection; diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 9b11a640a..c326b21b0 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -176,6 +176,10 @@ pub const Action = union(enum) { /// zoom/unzoom the current split. toggle_split_zoom: void, + /// Show, hide, or toggle the terminal inspector for the currently + /// focused terminal. + inspector: InspectorMode, + /// Reload the configuration. The exact meaning depends on the app runtime /// in use but this usually involves re-reading the configuration file /// and applying any changes. Note that not all changes can be applied at @@ -220,6 +224,13 @@ pub const Action = union(enum) { right, }; + // Extern because it is used in the embedded runtime ABI. + pub const InspectorMode = enum(c_int) { + toggle, + show, + hide, + }; + /// Parse an action in the format of "key=value" where key is the /// action name and value is the action parameter. The parameter /// is optional depending on the action.