diff --git a/include/ghostty.h b/include/ghostty.h index 77671140f..571cbd904 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -30,7 +30,9 @@ typedef void* ghostty_config_t; typedef void* ghostty_surface_t; typedef void* ghostty_inspector_t; -// Enums are up top so we can reference them later. +// All the types below are fully defined and must be kept in sync with +// their Zig counterparts. Any changes to these types MUST have an associated +// Zig change. typedef enum { GHOSTTY_PLATFORM_INVALID, GHOSTTY_PLATFORM_MACOS, @@ -48,33 +50,6 @@ typedef enum { GHOSTTY_CLIPBOARD_REQUEST_OSC_52_WRITE, } ghostty_clipboard_request_e; -typedef enum { - GHOSTTY_SPLIT_RIGHT, - GHOSTTY_SPLIT_DOWN -} ghostty_split_direction_e; - -typedef enum { - GHOSTTY_SPLIT_FOCUS_PREVIOUS, - GHOSTTY_SPLIT_FOCUS_NEXT, - GHOSTTY_SPLIT_FOCUS_TOP, - GHOSTTY_SPLIT_FOCUS_LEFT, - GHOSTTY_SPLIT_FOCUS_BOTTOM, - GHOSTTY_SPLIT_FOCUS_RIGHT, -} ghostty_split_focus_direction_e; - -typedef enum { - GHOSTTY_SPLIT_RESIZE_UP, - GHOSTTY_SPLIT_RESIZE_DOWN, - GHOSTTY_SPLIT_RESIZE_LEFT, - GHOSTTY_SPLIT_RESIZE_RIGHT, -} ghostty_split_resize_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, @@ -97,55 +72,6 @@ typedef enum { GHOSTTY_MOUSE_MOMENTUM_MAY_BEGIN, } ghostty_input_mouse_momentum_e; -typedef enum { - GHOSTTY_MOUSE_SHAPE_DEFAULT, - GHOSTTY_MOUSE_SHAPE_CONTEXT_MENU, - GHOSTTY_MOUSE_SHAPE_HELP, - GHOSTTY_MOUSE_SHAPE_POINTER, - GHOSTTY_MOUSE_SHAPE_PROGRESS, - GHOSTTY_MOUSE_SHAPE_WAIT, - GHOSTTY_MOUSE_SHAPE_CELL, - GHOSTTY_MOUSE_SHAPE_CROSSHAIR, - GHOSTTY_MOUSE_SHAPE_TEXT, - GHOSTTY_MOUSE_SHAPE_VERTICAL_TEXT, - GHOSTTY_MOUSE_SHAPE_ALIAS, - GHOSTTY_MOUSE_SHAPE_COPY, - GHOSTTY_MOUSE_SHAPE_MOVE, - GHOSTTY_MOUSE_SHAPE_NO_DROP, - GHOSTTY_MOUSE_SHAPE_NOT_ALLOWED, - GHOSTTY_MOUSE_SHAPE_GRAB, - GHOSTTY_MOUSE_SHAPE_GRABBING, - GHOSTTY_MOUSE_SHAPE_ALL_SCROLL, - GHOSTTY_MOUSE_SHAPE_COL_RESIZE, - GHOSTTY_MOUSE_SHAPE_ROW_RESIZE, - GHOSTTY_MOUSE_SHAPE_N_RESIZE, - GHOSTTY_MOUSE_SHAPE_E_RESIZE, - GHOSTTY_MOUSE_SHAPE_S_RESIZE, - GHOSTTY_MOUSE_SHAPE_W_RESIZE, - GHOSTTY_MOUSE_SHAPE_NE_RESIZE, - GHOSTTY_MOUSE_SHAPE_NW_RESIZE, - GHOSTTY_MOUSE_SHAPE_SE_RESIZE, - GHOSTTY_MOUSE_SHAPE_SW_RESIZE, - GHOSTTY_MOUSE_SHAPE_EW_RESIZE, - GHOSTTY_MOUSE_SHAPE_NS_RESIZE, - GHOSTTY_MOUSE_SHAPE_NESW_RESIZE, - GHOSTTY_MOUSE_SHAPE_NWSE_RESIZE, - GHOSTTY_MOUSE_SHAPE_ZOOM_IN, - GHOSTTY_MOUSE_SHAPE_ZOOM_OUT, -} ghostty_mouse_shape_e; - -typedef enum { - GHOSTTY_NON_NATIVE_FULLSCREEN_FALSE, - GHOSTTY_NON_NATIVE_FULLSCREEN_TRUE, - GHOSTTY_NON_NATIVE_FULLSCREEN_VISIBLE_MENU, -} ghostty_non_native_fullscreen_e; - -typedef enum { - GHOSTTY_TAB_PREVIOUS = -1, - GHOSTTY_TAB_NEXT = -2, - GHOSTTY_TAB_LAST = -3, -} ghostty_tab_e; - typedef enum { GHOSTTY_COLOR_SCHEME_LIGHT = 0, GHOSTTY_COLOR_SCHEME_DARK = 1, @@ -357,14 +283,6 @@ typedef enum { GHOSTTY_BUILD_MODE_RELEASE_SMALL, } ghostty_build_mode_e; -typedef enum { - GHOSTTY_RENDERER_HEALTH_OK, - GHOSTTY_RENDERER_HEALTH_UNHEALTHY, -} ghostty_renderer_health_e; - -// Fully defined types. This MUST be kept in sync with equivalent Zig -// structs. To find the Zig struct, grep for this type name. The documentation -// for all of these types is available in the Zig source. typedef struct { ghostty_build_mode_e build_mode; const char* version; @@ -414,13 +332,229 @@ typedef struct { uint32_t cell_height_px; } ghostty_surface_size_s; +// apprt.Target.Key +typedef enum { + GHOSTTY_TARGET_APP, + GHOSTTY_TARGET_SURFACE, +} ghostty_target_tag_e; + +typedef union { + ghostty_surface_t surface; +} ghostty_target_u; + +typedef struct { + ghostty_target_tag_e tag; + ghostty_target_u target; +} ghostty_target_s; + +// apprt.action.SplitDirection +typedef enum { + GHOSTTY_SPLIT_DIRECTION_RIGHT, + GHOSTTY_SPLIT_DIRECTION_DOWN, +} ghostty_action_split_direction_e; + +// apprt.action.GotoSplit +typedef enum { + GHOSTTY_GOTO_SPLIT_PREVIOUS, + GHOSTTY_GOTO_SPLIT_NEXT, + GHOSTTY_GOTO_SPLIT_TOP, + GHOSTTY_GOTO_SPLIT_LEFT, + GHOSTTY_GOTO_SPLIT_BOTTOM, + GHOSTTY_GOTO_SPLIT_RIGHT, +} ghostty_action_goto_split_e; + +// apprt.action.ResizeSplit.Direction +typedef enum { + GHOSTTY_RESIZE_SPLIT_UP, + GHOSTTY_RESIZE_SPLIT_DOWN, + GHOSTTY_RESIZE_SPLIT_LEFT, + GHOSTTY_RESIZE_SPLIT_RIGHT, +} ghostty_action_resize_split_direction_e; + +// apprt.action.ResizeSplit +typedef struct { + uint16_t amount; + ghostty_action_resize_split_direction_e direction; +} ghostty_action_resize_split_s; + +// apprt.action.GotoTab +typedef enum { + GHOSTTY_GOTO_TAB_PREVIOUS, + GHOSTTY_GOTO_TAB_NEXT, + GHOSTTY_GOTO_TAB_LAST, +} ghostty_action_goto_tab_e; + +// apprt.action.Fullscreen +typedef enum { + GHOSTTY_FULLSCREEN_NATIVE, + GHOSTTY_FULLSCREEN_NON_NATIVE, + GHOSTTY_FULLSCREEN_NON_NATIVE_VISIBLE_MENU, +} ghostty_action_fullscreen_e; + +// apprt.action.SecureInput +typedef enum { + GHOSTTY_SECURE_INPUT_ON, + GHOSTTY_SECURE_INPUT_OFF, + GHOSTTY_SECURE_INPUT_TOGGLE, +} ghostty_action_secure_input_e; + +// apprt.action.Inspector +typedef enum { + GHOSTTY_INSPECTOR_TOGGLE, + GHOSTTY_INSPECTOR_SHOW, + GHOSTTY_INSPECTOR_HIDE, +} ghostty_action_inspector_e; + +// apprt.action.QuitTimer +typedef enum { + GHOSTTY_QUIT_TIMER_START, + GHOSTTY_QUIT_TIMER_STOP, +} ghostty_action_quit_timer_e; + +// apprt.action.DesktopNotification.C +typedef struct { + const char* title; + const char* body; +} ghostty_action_desktop_notification_s; + +// apprt.action.SetTitle.C +typedef struct { + const char* title; +} ghostty_action_set_title_s; + +// terminal.MouseShape +typedef enum { + GHOSTTY_MOUSE_SHAPE_DEFAULT, + GHOSTTY_MOUSE_SHAPE_CONTEXT_MENU, + GHOSTTY_MOUSE_SHAPE_HELP, + GHOSTTY_MOUSE_SHAPE_POINTER, + GHOSTTY_MOUSE_SHAPE_PROGRESS, + GHOSTTY_MOUSE_SHAPE_WAIT, + GHOSTTY_MOUSE_SHAPE_CELL, + GHOSTTY_MOUSE_SHAPE_CROSSHAIR, + GHOSTTY_MOUSE_SHAPE_TEXT, + GHOSTTY_MOUSE_SHAPE_VERTICAL_TEXT, + GHOSTTY_MOUSE_SHAPE_ALIAS, + GHOSTTY_MOUSE_SHAPE_COPY, + GHOSTTY_MOUSE_SHAPE_MOVE, + GHOSTTY_MOUSE_SHAPE_NO_DROP, + GHOSTTY_MOUSE_SHAPE_NOT_ALLOWED, + GHOSTTY_MOUSE_SHAPE_GRAB, + GHOSTTY_MOUSE_SHAPE_GRABBING, + GHOSTTY_MOUSE_SHAPE_ALL_SCROLL, + GHOSTTY_MOUSE_SHAPE_COL_RESIZE, + GHOSTTY_MOUSE_SHAPE_ROW_RESIZE, + GHOSTTY_MOUSE_SHAPE_N_RESIZE, + GHOSTTY_MOUSE_SHAPE_E_RESIZE, + GHOSTTY_MOUSE_SHAPE_S_RESIZE, + GHOSTTY_MOUSE_SHAPE_W_RESIZE, + GHOSTTY_MOUSE_SHAPE_NE_RESIZE, + GHOSTTY_MOUSE_SHAPE_NW_RESIZE, + GHOSTTY_MOUSE_SHAPE_SE_RESIZE, + GHOSTTY_MOUSE_SHAPE_SW_RESIZE, + GHOSTTY_MOUSE_SHAPE_EW_RESIZE, + GHOSTTY_MOUSE_SHAPE_NS_RESIZE, + GHOSTTY_MOUSE_SHAPE_NESW_RESIZE, + GHOSTTY_MOUSE_SHAPE_NWSE_RESIZE, + GHOSTTY_MOUSE_SHAPE_ZOOM_IN, + GHOSTTY_MOUSE_SHAPE_ZOOM_OUT, +} ghostty_action_mouse_shape_e; + +// apprt.action.MouseVisibility +typedef enum { + GHOSTTY_MOUSE_VISIBLE, + GHOSTTY_MOUSE_HIDDEN, +} ghostty_action_mouse_visibility_e; + +// apprt.action.MouseOverLink +typedef struct { + const char* url; + size_t len; +} ghostty_action_mouse_over_link_s; + +// apprt.action.SizeLimit +typedef struct { + uint32_t min_width; + uint32_t min_height; + uint32_t max_width; + uint32_t max_height; +} ghostty_action_size_limit_s; + +// apprt.action.InitialSize +typedef struct { + uint32_t width; + uint32_t height; +} ghostty_action_initial_size_s; + +// apprt.action.CellSize +typedef struct { + uint32_t width; + uint32_t height; +} ghostty_action_cell_size_s; + +// renderer.Health +typedef enum { + GHOSTTY_RENDERER_HEALTH_OK, + GHOSTTY_RENDERER_HEALTH_UNHEALTHY, +} ghostty_action_renderer_health_e; + +// apprt.Action.Key +typedef enum { + GHOSTTY_ACTION_NEW_WINDOW, + GHOSTTY_ACTION_NEW_TAB, + GHOSTTY_ACTION_NEW_SPLIT, + GHOSTTY_ACTION_CLOSE_ALL_WINDOWS, + GHOSTTY_ACTION_TOGGLE_FULLSCREEN, + GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS, + GHOSTTY_ACTION_GOTO_TAB, + GHOSTTY_ACTION_GOTO_SPLIT, + GHOSTTY_ACTION_RESIZE_SPLIT, + GHOSTTY_ACTION_EQUALIZE_SPLITS, + GHOSTTY_ACTION_TOGGLE_SPLIT_ZOOM, + GHOSTTY_ACTION_PRESENT_TERMINAL, + GHOSTTY_ACTION_SIZE_LIMIT, + GHOSTTY_ACTION_INITIAL_SIZE, + GHOSTTY_ACTION_CELL_SIZE, + GHOSTTY_ACTION_INSPECTOR, + GHOSTTY_ACTION_RENDER_INSPECTOR, + GHOSTTY_ACTION_DESKTOP_NOTIFICATION, + GHOSTTY_ACTION_SET_TITLE, + GHOSTTY_ACTION_MOUSE_SHAPE, + GHOSTTY_ACTION_MOUSE_VISIBILITY, + GHOSTTY_ACTION_MOUSE_OVER_LINK, + GHOSTTY_ACTION_RENDERER_HEALTH, + GHOSTTY_ACTION_OPEN_CONFIG, + GHOSTTY_ACTION_QUIT_TIMER, + GHOSTTY_ACTION_SECURE_INPUT, +} ghostty_action_tag_e; + +typedef union { + ghostty_action_split_direction_e new_split; + ghostty_action_fullscreen_e toggle_fullscreen; + ghostty_action_goto_tab_e goto_tab; + ghostty_action_goto_split_e goto_split; + ghostty_action_resize_split_s resize_split; + ghostty_action_size_limit_s size_limit; + ghostty_action_initial_size_s initial_size; + ghostty_action_cell_size_s cell_size; + ghostty_action_inspector_e inspector; + ghostty_action_desktop_notification_s desktop_notification; + ghostty_action_set_title_s set_title; + ghostty_action_mouse_shape_e mouse_shape; + ghostty_action_mouse_visibility_e mouse_visibility; + ghostty_action_mouse_over_link_s mouse_over_link; + ghostty_action_renderer_health_e renderer_health; + ghostty_action_quit_timer_e quit_timer; + ghostty_action_secure_input_e secure_input; +} ghostty_action_u; + +typedef struct { + ghostty_action_tag_e tag; + ghostty_action_u action; +} ghostty_action_s; + typedef void (*ghostty_runtime_wakeup_cb)(void*); typedef const ghostty_config_t (*ghostty_runtime_reload_config_cb)(void*); -typedef void (*ghostty_runtime_open_config_cb)(void*); -typedef void (*ghostty_runtime_set_title_cb)(void*, const char*); -typedef void (*ghostty_runtime_set_mouse_shape_cb)(void*, - ghostty_mouse_shape_e); -typedef void (*ghostty_runtime_set_mouse_visibility_cb)(void*, bool); typedef void (*ghostty_runtime_read_clipboard_cb)(void*, ghostty_clipboard_e, void*); @@ -433,71 +567,21 @@ typedef void (*ghostty_runtime_write_clipboard_cb)(void*, const char*, ghostty_clipboard_e, bool); -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_resize_split_cb)( - void*, - ghostty_split_resize_direction_e, - uint16_t); -typedef void (*ghostty_runtime_equalize_splits_cb)(void*); -typedef void (*ghostty_runtime_toggle_split_zoom_cb)(void*); -typedef void (*ghostty_runtime_goto_tab_cb)(void*, int32_t); -typedef void (*ghostty_runtime_toggle_fullscreen_cb)( - void*, - ghostty_non_native_fullscreen_e); -typedef void (*ghostty_runtime_set_initial_window_size_cb)(void*, - uint32_t, - uint32_t); -typedef void (*ghostty_runtime_render_inspector_cb)(void*); -typedef void (*ghostty_runtime_set_cell_size_cb)(void*, uint32_t, uint32_t); -typedef void (*ghostty_runtime_show_desktop_notification_cb)(void*, - const char*, - const char*); -typedef void ( - *ghostty_runtime_update_renderer_health)(void*, ghostty_renderer_health_e); -typedef void (*ghostty_runtime_mouse_over_link_cb)(void*, const char*, size_t); -typedef void (*ghostty_runtime_set_password_input_cb)(void*, bool); -typedef void (*ghostty_runtime_toggle_secure_input_cb)(); +typedef void (*ghostty_runtime_action_cb)(ghostty_app_t, + ghostty_target_s, + ghostty_action_s); typedef struct { void* userdata; bool supports_selection_clipboard; ghostty_runtime_wakeup_cb wakeup_cb; + ghostty_runtime_action_cb action_cb; ghostty_runtime_reload_config_cb reload_config_cb; - ghostty_runtime_open_config_cb open_config_cb; - ghostty_runtime_set_title_cb set_title_cb; - ghostty_runtime_set_mouse_shape_cb set_mouse_shape_cb; - ghostty_runtime_set_mouse_visibility_cb set_mouse_visibility_cb; ghostty_runtime_read_clipboard_cb read_clipboard_cb; ghostty_runtime_confirm_read_clipboard_cb confirm_read_clipboard_cb; ghostty_runtime_write_clipboard_cb write_clipboard_cb; - 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_resize_split_cb resize_split_cb; - ghostty_runtime_equalize_splits_cb equalize_splits_cb; - ghostty_runtime_toggle_split_zoom_cb toggle_split_zoom_cb; - ghostty_runtime_goto_tab_cb goto_tab_cb; - ghostty_runtime_toggle_fullscreen_cb toggle_fullscreen_cb; - ghostty_runtime_set_initial_window_size_cb set_initial_window_size_cb; - ghostty_runtime_render_inspector_cb render_inspector_cb; - ghostty_runtime_set_cell_size_cb set_cell_size_cb; - ghostty_runtime_show_desktop_notification_cb show_desktop_notification_cb; - ghostty_runtime_update_renderer_health update_renderer_health_cb; - ghostty_runtime_mouse_over_link_cb mouse_over_link_cb; - ghostty_runtime_set_password_input_cb set_password_input_cb; - ghostty_runtime_toggle_secure_input_cb toggle_secure_input_cb; } ghostty_runtime_config_s; //------------------------------------------------------------------- @@ -538,7 +622,9 @@ ghostty_surface_config_s ghostty_surface_config_new(); ghostty_surface_t ghostty_surface_new(ghostty_app_t, ghostty_surface_config_s*); void ghostty_surface_free(ghostty_surface_t); +void* ghostty_surface_userdata(ghostty_surface_t); ghostty_app_t ghostty_surface_app(ghostty_surface_t); +ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_surface_t); bool ghostty_surface_needs_confirm_quit(ghostty_surface_t); void ghostty_surface_refresh(ghostty_surface_t); void ghostty_surface_draw(ghostty_surface_t); @@ -569,11 +655,11 @@ void ghostty_surface_mouse_scroll(ghostty_surface_t, void ghostty_surface_mouse_pressure(ghostty_surface_t, uint32_t, double); void ghostty_surface_ime_point(ghostty_surface_t, double*, double*); void ghostty_surface_request_close(ghostty_surface_t); -void ghostty_surface_split(ghostty_surface_t, ghostty_split_direction_e); +void ghostty_surface_split(ghostty_surface_t, ghostty_action_split_direction_e); void ghostty_surface_split_focus(ghostty_surface_t, - ghostty_split_focus_direction_e); + ghostty_action_goto_split_e); void ghostty_surface_split_resize(ghostty_surface_t, - ghostty_split_resize_direction_e, + ghostty_action_resize_split_direction_e, uint16_t); void ghostty_surface_split_equalize(ghostty_surface_t); bool ghostty_surface_binding_action(ghostty_surface_t, const char*, uintptr_t); diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 44f0f0291..fc24345a8 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -484,6 +484,24 @@ class AppDelegate: NSObject, dockMenu.addItem(newTab) } + //MARK: - Global State + + func setSecureInput(_ mode: Ghostty.SetSecureInput) { + let input = SecureInput.shared + switch (mode) { + case .on: + input.global = true + + case .off: + input.global = false + + case .toggle: + input.global.toggle() + } + self.menuSecureInput?.state = if (input.global) { .on } else { .off } + UserDefaults.standard.set(input.global, forKey: "SecureInput") + } + //MARK: - IB Actions @IBAction func openConfig(_ sender: Any?) { @@ -525,9 +543,6 @@ class AppDelegate: NSObject, } @IBAction func toggleSecureInput(_ sender: Any) { - let input = SecureInput.shared - input.global.toggle() - self.menuSecureInput?.state = if (input.global) { .on } else { .off } - UserDefaults.standard.set(input.global, forKey: "SecureInput") + setSecureInput(.toggle) } } diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 74702c621..5bb6dbef6 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -551,12 +551,12 @@ class TerminalController: NSWindowController, NSWindowDelegate, @IBAction func splitRight(_ sender: Any) { guard let surface = focusedSurface?.surface else { return } - ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_RIGHT) + ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DIRECTION_RIGHT) } @IBAction func splitDown(_ sender: Any) { guard let surface = focusedSurface?.surface else { return } - ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DOWN) + ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DIRECTION_DOWN) } @IBAction func splitZoom(_ sender: Any) { @@ -732,8 +732,9 @@ class TerminalController: NSWindowController, NSWindowDelegate, guard let window = self.window else { return } // Get the tab index from the notification - guard let tabIndexAny = notification.userInfo?[Ghostty.Notification.GotoTabKey] else { return } - guard let tabIndex = tabIndexAny as? Int32 else { return } + guard let tabEnumAny = notification.userInfo?[Ghostty.Notification.GotoTabKey] else { return } + guard let tabEnum = tabEnumAny as? ghostty_action_goto_tab_e else { return } + let tabIndex: Int32 = .init(bitPattern: tabEnum.rawValue) guard let windowController = window.windowController else { return } guard let tabGroup = windowController.window?.tabGroup else { return } @@ -747,19 +748,19 @@ class TerminalController: NSWindowController, NSWindowDelegate, guard let selectedWindow = tabGroup.selectedWindow else { return } guard let selectedIndex = tabbedWindows.firstIndex(where: { $0 == selectedWindow }) else { return } - if (tabIndex == GHOSTTY_TAB_PREVIOUS.rawValue) { + if (tabIndex == GHOSTTY_GOTO_TAB_PREVIOUS.rawValue) { if (selectedIndex == 0) { finalIndex = tabbedWindows.count - 1 } else { finalIndex = selectedIndex - 1 } - } else if (tabIndex == GHOSTTY_TAB_NEXT.rawValue) { + } else if (tabIndex == GHOSTTY_GOTO_TAB_NEXT.rawValue) { if (selectedIndex == tabbedWindows.count - 1) { finalIndex = 0 } else { finalIndex = selectedIndex + 1 } - } else if (tabIndex == GHOSTTY_TAB_LAST.rawValue) { + } else if (tabIndex == GHOSTTY_GOTO_TAB_LAST.rawValue) { finalIndex = tabbedWindows.count - 1 } else { return @@ -783,9 +784,9 @@ class TerminalController: NSWindowController, NSWindowDelegate, guard let window = self.window else { return } // Check whether we use non-native fullscreen - guard let useNonNativeFullscreenAny = notification.userInfo?[Ghostty.Notification.NonNativeFullscreenKey] else { return } - guard let useNonNativeFullscreen = useNonNativeFullscreenAny as? ghostty_non_native_fullscreen_e else { return } - self.fullscreenHandler.toggleFullscreen(window: window, nonNativeFullscreen: useNonNativeFullscreen) + guard let fullscreenModeAny = notification.userInfo?[Ghostty.Notification.FullscreenModeKey] else { return } + guard let fullscreenMode = fullscreenModeAny as? ghostty_action_fullscreen_e else { return } + self.fullscreenHandler.toggleFullscreen(window: window, mode: fullscreenMode) // For some reason focus always gets lost when we toggle fullscreen, so we set it back. if let focusedSurface { diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 3b3dd9626..f0128e2f7 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -67,36 +67,12 @@ extension Ghostty { userdata: Unmanaged.passUnretained(self).toOpaque(), supports_selection_clipboard: false, wakeup_cb: { userdata in App.wakeup(userdata) }, + action_cb: { app, target, action in App.action(app!, target: target, action: action) }, reload_config_cb: { userdata in App.reloadConfig(userdata) }, - open_config_cb: { userdata in App.openConfig(userdata) }, - set_title_cb: { userdata, title in App.setTitle(userdata, title: title) }, - set_mouse_shape_cb: { userdata, shape in App.setMouseShape(userdata, shape: shape) }, - set_mouse_visibility_cb: { userdata, visible in App.setMouseVisibility(userdata, visible: visible) }, read_clipboard_cb: { userdata, loc, state in App.readClipboard(userdata, location: loc, state: state) }, confirm_read_clipboard_cb: { userdata, str, state, request in App.confirmReadClipboard(userdata, string: str, state: state, request: request ) }, write_clipboard_cb: { userdata, str, loc, confirm in App.writeClipboard(userdata, string: str, location: loc, confirm: confirm) }, - new_split_cb: { userdata, direction, surfaceConfig in App.newSplit(userdata, direction: direction, config: surfaceConfig) }, - new_tab_cb: { userdata, surfaceConfig in App.newTab(userdata, config: surfaceConfig) }, - new_window_cb: { userdata, surfaceConfig in App.newWindow(userdata, config: surfaceConfig) }, - control_inspector_cb: { userdata, mode in App.controlInspector(userdata, mode: mode) }, - close_surface_cb: { userdata, processAlive in App.closeSurface(userdata, processAlive: processAlive) }, - focus_split_cb: { userdata, direction in App.focusSplit(userdata, direction: direction) }, - resize_split_cb: { userdata, direction, amount in - App.resizeSplit(userdata, direction: direction, amount: amount) }, - equalize_splits_cb: { userdata in - App.equalizeSplits(userdata) }, - toggle_split_zoom_cb: { userdata in App.toggleSplitZoom(userdata) }, - goto_tab_cb: { userdata, n in App.gotoTab(userdata, n: n) }, - toggle_fullscreen_cb: { userdata, nonNativeFullscreen in App.toggleFullscreen(userdata, nonNativeFullscreen: nonNativeFullscreen) }, - set_initial_window_size_cb: { userdata, width, height in App.setInitialWindowSize(userdata, width: width, height: height) }, - render_inspector_cb: { userdata in App.renderInspector(userdata) }, - set_cell_size_cb: { userdata, width, height in App.setCellSize(userdata, width: width, height: height) }, - show_desktop_notification_cb: { userdata, title, body in - App.showUserNotification(userdata, title: title, body: body) }, - update_renderer_health_cb: { userdata, health in App.updateRendererHealth(userdata, health: health) }, - mouse_over_link_cb: { userdata, ptr, len in App.mouseOverLink(userdata, uri: ptr, len: len) }, - set_password_input_cb: { userdata, value in App.setPasswordInput(userdata, value: value) }, - toggle_secure_input_cb: { App.toggleSecureInput() } + close_surface_cb: { userdata, processAlive in App.closeSurface(userdata, processAlive: processAlive) } ) // Create the ghostty app. @@ -185,7 +161,7 @@ extension Ghostty { } } - func split(surface: ghostty_surface_t, direction: ghostty_split_direction_e) { + func split(surface: ghostty_surface_t, direction: ghostty_action_split_direction_e) { ghostty_surface_split(surface, direction) } @@ -254,11 +230,8 @@ extension Ghostty { // MARK: Ghostty Callbacks (iOS) static func wakeup(_ userdata: UnsafeMutableRawPointer?) {} + static func action(_ app: ghostty_app_t, target: ghostty_target_s, action: ghostty_action_s) {} static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? { return nil } - static func openConfig(_ userdata: UnsafeMutableRawPointer?) {} - static func setTitle(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer?) {} - static func setMouseShape(_ userdata: UnsafeMutableRawPointer?, shape: ghostty_mouse_shape_e) {} - static func setMouseVisibility(_ userdata: UnsafeMutableRawPointer?, visible: Bool) {} static func readClipboard( _ userdata: UnsafeMutableRawPointer?, location: ghostty_clipboard_e, @@ -279,30 +252,7 @@ extension Ghostty { confirm: Bool ) {} - static func newSplit( - _ userdata: UnsafeMutableRawPointer?, - direction: ghostty_split_direction_e, - config: ghostty_surface_config_s - ) {} - - static func newTab(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) {} - static func newWindow(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) {} - static func controlInspector(_ userdata: UnsafeMutableRawPointer?, mode: ghostty_inspector_mode_e) {} static func closeSurface(_ userdata: UnsafeMutableRawPointer?, processAlive: Bool) {} - static func focusSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_focus_direction_e) {} - static func resizeSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_resize_direction_e, amount: UInt16) {} - static func equalizeSplits(_ userdata: UnsafeMutableRawPointer?) {} - static func toggleSplitZoom(_ userdata: UnsafeMutableRawPointer?) {} - static func gotoTab(_ userdata: UnsafeMutableRawPointer?, n: Int32) {} - static func toggleFullscreen(_ userdata: UnsafeMutableRawPointer?, nonNativeFullscreen: ghostty_non_native_fullscreen_e) {} - static func setInitialWindowSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) {} - static func renderInspector(_ userdata: UnsafeMutableRawPointer?) {} - static func setCellSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) {} - static func showUserNotification(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer?, body: UnsafePointer?) {} - static func updateRendererHealth(_ userdata: UnsafeMutableRawPointer?, health: ghostty_renderer_health_e) {} - static func mouseOverLink(_ userdata: UnsafeMutableRawPointer?, uri: UnsafePointer?, len: Int) {} - static func setPasswordInput(_ userdata: UnsafeMutableRawPointer?, value: Bool) {} - static func toggleSecureInput() {} #endif #if os(macOS) @@ -318,14 +268,6 @@ extension Ghostty { // MARK: Ghostty Callbacks (macOS) - static func newSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_direction_e, config: ghostty_surface_config_s) { - let surface = self.surfaceUserdata(from: userdata) - NotificationCenter.default.post(name: Notification.ghosttyNewSplit, object: surface, userInfo: [ - "direction": direction, - Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: config), - ]) - } - static func closeSurface(_ userdata: UnsafeMutableRawPointer?, processAlive: Bool) { let surface = self.surfaceUserdata(from: userdata) NotificationCenter.default.post(name: Notification.ghosttyCloseSurface, object: surface, userInfo: [ @@ -333,56 +275,6 @@ extension Ghostty { ]) } - static func focusSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_focus_direction_e) { - let surface = self.surfaceUserdata(from: userdata) - guard let splitDirection = SplitFocusDirection.from(direction: direction) else { return } - NotificationCenter.default.post( - name: Notification.ghosttyFocusSplit, - object: surface, - userInfo: [ - Notification.SplitDirectionKey: splitDirection, - ] - ) - } - - static func resizeSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_resize_direction_e, amount: UInt16) { - let surface = self.surfaceUserdata(from: userdata) - guard let resizeDirection = SplitResizeDirection.from(direction: direction) else { return } - NotificationCenter.default.post( - name: Notification.didResizeSplit, - object: surface, - userInfo: [ - Notification.ResizeSplitDirectionKey: resizeDirection, - Notification.ResizeSplitAmountKey: amount, - ] - ) - } - - static func equalizeSplits(_ userdata: UnsafeMutableRawPointer?) { - let surface = self.surfaceUserdata(from: userdata) - NotificationCenter.default.post(name: Notification.didEqualizeSplits, object: surface) - } - - static func toggleSplitZoom(_ userdata: UnsafeMutableRawPointer?) { - let surface = self.surfaceUserdata(from: userdata) - - NotificationCenter.default.post( - name: Notification.didToggleSplitZoom, - object: surface - ) - } - - static func gotoTab(_ userdata: UnsafeMutableRawPointer?, n: Int32) { - let surface = self.surfaceUserdata(from: userdata) - NotificationCenter.default.post( - name: Notification.ghosttyGotoTab, - object: surface, - userInfo: [ - Notification.GotoTabKey: n, - ] - ) - } - static func readClipboard(_ userdata: UnsafeMutableRawPointer?, location: ghostty_clipboard_e, state: UnsafeMutableRawPointer?) { // If we don't even have a surface, something went terrible wrong so we have // to leak "state". @@ -454,10 +346,6 @@ extension Ghostty { ) } - static func openConfig(_ userdata: UnsafeMutableRawPointer?) { - ghostty_config_open(); - } - static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? { let newConfig = Config() guard newConfig.loaded else { @@ -488,99 +376,648 @@ extension Ghostty { DispatchQueue.main.async { state.appTick() } } - static func renderInspector(_ userdata: UnsafeMutableRawPointer?) { - let surface = self.surfaceUserdata(from: userdata) - NotificationCenter.default.post( - name: Notification.inspectorNeedsDisplay, - object: surface - ) + /// Determine if a given notification should be presented to the user when Ghostty is running in the foreground. + func shouldPresentNotification(notification: UNNotification) -> Bool { + let userInfo = notification.request.content.userInfo + guard let uuidString = userInfo["surface"] as? String, + let uuid = UUID(uuidString: uuidString), + let surface = delegate?.findSurface(forUUID: uuid), + let window = surface.window else { return false } + return !window.isKeyWindow || !surface.focused } - static func setTitle(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer?) { - let surfaceView = self.surfaceUserdata(from: userdata) - guard let titleStr = String(cString: title!, encoding: .utf8) else { return } - DispatchQueue.main.async { - surfaceView.title = titleStr - } + /// Returns the GhosttyState from the given userdata value. + static private func appState(fromView view: SurfaceView) -> App? { + guard let surface = view.surface else { return nil } + guard let app = ghostty_surface_app(surface) else { return nil } + guard let app_ud = ghostty_app_userdata(app) else { return nil } + return Unmanaged.fromOpaque(app_ud).takeUnretainedValue() } - static func setMouseShape(_ userdata: UnsafeMutableRawPointer?, shape: ghostty_mouse_shape_e) { - let surfaceView = self.surfaceUserdata(from: userdata) - surfaceView.setCursorShape(shape) + /// Returns the surface view from the userdata. + static private func surfaceUserdata(from userdata: UnsafeMutableRawPointer?) -> SurfaceView { + return Unmanaged.fromOpaque(userdata!).takeUnretainedValue() } - static func setMouseVisibility(_ userdata: UnsafeMutableRawPointer?, visible: Bool) { - let surfaceView = self.surfaceUserdata(from: userdata) - surfaceView.setCursorVisibility(visible) + static private func surfaceView(from surface: ghostty_surface_t) -> SurfaceView? { + guard let surface_ud = ghostty_surface_userdata(surface) else { return nil } + return Unmanaged.fromOpaque(surface_ud).takeUnretainedValue() } - static func toggleFullscreen(_ userdata: UnsafeMutableRawPointer?, nonNativeFullscreen: ghostty_non_native_fullscreen_e) { - let surface = self.surfaceUserdata(from: userdata) - NotificationCenter.default.post( - name: Notification.ghosttyToggleFullscreen, - object: surface, - userInfo: [ - Notification.NonNativeFullscreenKey: nonNativeFullscreen, - ] - ) - } + // MARK: Actions (macOS) - static func setInitialWindowSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) { - // We need a window to set the frame - let surfaceView = self.surfaceUserdata(from: userdata) - surfaceView.initialSize = NSMakeSize(Double(width), Double(height)) - } + static func action(_ app: ghostty_app_t, target: ghostty_target_s, action: ghostty_action_s) { + // Make sure it a target we understand so all our action handlers can assert + switch (target.tag) { + case GHOSTTY_TARGET_APP, GHOSTTY_TARGET_SURFACE: + break - static func setCellSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) { - let surfaceView = self.surfaceUserdata(from: userdata) - let backingSize = NSSize(width: Double(width), height: Double(height)) - surfaceView.cellSize = surfaceView.convertFromBacking(backingSize) - } - - static func mouseOverLink(_ userdata: UnsafeMutableRawPointer?, uri: UnsafePointer?, len: Int) { - let surfaceView = self.surfaceUserdata(from: userdata) - guard len > 0 else { - surfaceView.hoverUrl = nil + default: + Ghostty.logger.warning("unknown action target=\(target.tag.rawValue)") return } - let buffer = Data(bytes: uri!, count: len) - surfaceView.hoverUrl = String(data: buffer, encoding: .utf8) + // Action dispatch + switch (action.tag) { + case GHOSTTY_ACTION_NEW_WINDOW: + newWindow(app, target: target) + + case GHOSTTY_ACTION_NEW_TAB: + newTab(app, target: target) + + case GHOSTTY_ACTION_NEW_SPLIT: + newSplit(app, target: target, direction: action.action.new_split) + + case GHOSTTY_ACTION_TOGGLE_FULLSCREEN: + toggleFullscreen(app, target: target, mode: action.action.toggle_fullscreen) + + case GHOSTTY_ACTION_GOTO_TAB: + gotoTab(app, target: target, tab: action.action.goto_tab) + + case GHOSTTY_ACTION_GOTO_SPLIT: + gotoSplit(app, target: target, direction: action.action.goto_split) + + case GHOSTTY_ACTION_RESIZE_SPLIT: + resizeSplit(app, target: target, resize: action.action.resize_split) + + case GHOSTTY_ACTION_EQUALIZE_SPLITS: + equalizeSplits(app, target: target) + + case GHOSTTY_ACTION_TOGGLE_SPLIT_ZOOM: + toggleSplitZoom(app, target: target) + + case GHOSTTY_ACTION_INSPECTOR: + controlInspector(app, target: target, mode: action.action.inspector) + + case GHOSTTY_ACTION_RENDER_INSPECTOR: + renderInspector(app, target: target) + + case GHOSTTY_ACTION_DESKTOP_NOTIFICATION: + showDesktopNotification(app, target: target, n: action.action.desktop_notification) + + case GHOSTTY_ACTION_SET_TITLE: + setTitle(app, target: target, v: action.action.set_title) + + case GHOSTTY_ACTION_OPEN_CONFIG: + ghostty_config_open() + + case GHOSTTY_ACTION_SECURE_INPUT: + toggleSecureInput(app, target: target, mode: action.action.secure_input) + + case GHOSTTY_ACTION_MOUSE_SHAPE: + setMouseShape(app, target: target, shape: action.action.mouse_shape) + + case GHOSTTY_ACTION_MOUSE_VISIBILITY: + setMouseVisibility(app, target: target, v: action.action.mouse_visibility) + + case GHOSTTY_ACTION_MOUSE_OVER_LINK: + setMouseOverLink(app, target: target, v: action.action.mouse_over_link) + + case GHOSTTY_ACTION_INITIAL_SIZE: + setInitialSize(app, target: target, v: action.action.initial_size) + + case GHOSTTY_ACTION_CELL_SIZE: + setCellSize(app, target: target, v: action.action.cell_size) + + case GHOSTTY_ACTION_RENDERER_HEALTH: + rendererHealth(app, target: target, v: action.action.renderer_health) + + case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS: + fallthrough + case GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS: + fallthrough + case GHOSTTY_ACTION_PRESENT_TERMINAL: + fallthrough + case GHOSTTY_ACTION_SIZE_LIMIT: + fallthrough + case GHOSTTY_ACTION_QUIT_TIMER: + Ghostty.logger.info("known but unimplemented action action=\(action.tag.rawValue)") + + default: + Ghostty.logger.warning("unknown action action=\(action.tag.rawValue)") + } } - static func setPasswordInput(_ userdata: UnsafeMutableRawPointer?, value: Bool) { - // We don't currently allow global password input being set from this. - guard let userdata else { return } - - let surfaceView = self.surfaceUserdata(from: userdata) - guard let appState = self.appState(fromView: surfaceView) else { return } - guard appState.config.autoSecureInput else { return } - surfaceView.passwordInput = value + private static func newWindow(_ app: ghostty_app_t, target: ghostty_target_s) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + NotificationCenter.default.post( + name: Notification.ghosttyNewWindow, + object: nil, + userInfo: [:] + ) + + 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: Notification.ghosttyNewWindow, + object: surfaceView, + userInfo: [ + Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config(surface)), + ] + ) + + + default: + assertionFailure() + } } - static func toggleSecureInput() { - guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return } - appDelegate.toggleSecureInput(self) - } + private static func newTab(_ app: ghostty_app_t, target: ghostty_target_s) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + NotificationCenter.default.post( + name: Notification.ghosttyNewTab, + object: nil, + userInfo: [:] + ) - static func showUserNotification(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer?, body: UnsafePointer?) { - let surfaceView = self.surfaceUserdata(from: userdata) - guard let title = String(cString: title!, encoding: .utf8) else { return } - guard let body = String(cString: body!, encoding: .utf8) else { return } - - let center = UNUserNotificationCenter.current() - center.requestAuthorization(options: [.alert, .sound]) { _, error in - if let error = error { - AppDelegate.logger.error("Error while requesting notification authorization: \(error)") + case GHOSTTY_TARGET_SURFACE: + guard let surface = target.target.surface else { return } + guard let surfaceView = self.surfaceView(from: surface) else { return } + guard let appState = self.appState(fromView: surfaceView) else { return } + guard appState.config.windowDecorations else { + let alert = NSAlert() + alert.messageText = "Tabs are disabled" + alert.informativeText = "Enable window decorations to use tabs" + alert.addButton(withTitle: "OK") + alert.alertStyle = .warning + _ = alert.runModal() + return } - } - center.getNotificationSettings() { settings in - guard settings.authorizationStatus == .authorized else { return } - surfaceView.showUserNotification(title: title, body: body) + NotificationCenter.default.post( + name: Notification.ghosttyNewTab, + object: surfaceView, + userInfo: [ + Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config(surface)), + ] + ) + + + default: + assertionFailure() } } + private static func newSplit( + _ app: ghostty_app_t, + target: ghostty_target_s, + direction: ghostty_action_split_direction_e) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + // New split does nothing with an app target + Ghostty.logger.warning("new split 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: Notification.ghosttyNewSplit, + object: surfaceView, + userInfo: [ + "direction": direction, + Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config(surface)), + ] + ) + + + default: + assertionFailure() + } + } + + private static func toggleFullscreen( + _ app: ghostty_app_t, + target: ghostty_target_s, + mode: ghostty_action_fullscreen_e) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("toggle fullscreen 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: Notification.ghosttyToggleFullscreen, + object: surfaceView, + userInfo: [ + Notification.FullscreenModeKey: mode, + ] + ) + + + default: + assertionFailure() + } + } + + private static func gotoTab( + _ app: ghostty_app_t, + target: ghostty_target_s, + tab: ghostty_action_goto_tab_e) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("goto 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: Notification.ghosttyGotoTab, + object: surfaceView, + userInfo: [ + Notification.GotoTabKey: tab, + ] + ) + + default: + assertionFailure() + } + } + + private static func gotoSplit( + _ app: ghostty_app_t, + target: ghostty_target_s, + direction: ghostty_action_goto_split_e) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("goto split 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: Notification.ghosttyFocusSplit, + object: surfaceView, + userInfo: [ + Notification.SplitDirectionKey: SplitFocusDirection.from(direction: direction) as Any, + ] + ) + + default: + assertionFailure() + } + } + + private static func resizeSplit( + _ app: ghostty_app_t, + target: ghostty_target_s, + resize: ghostty_action_resize_split_s) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("resize split 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 } + guard let resizeDirection = SplitResizeDirection.from(direction: resize.direction) else { return } + NotificationCenter.default.post( + name: Notification.didResizeSplit, + object: surfaceView, + userInfo: [ + Notification.ResizeSplitDirectionKey: resizeDirection, + Notification.ResizeSplitAmountKey: resize.amount, + ] + ) + + default: + assertionFailure() + } + } + + private static func equalizeSplits( + _ app: ghostty_app_t, + target: ghostty_target_s) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("equalize splits 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: Notification.didEqualizeSplits, + object: surfaceView + ) + + + default: + assertionFailure() + } + } + + private static func toggleSplitZoom( + _ app: ghostty_app_t, + target: ghostty_target_s) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("toggle split zoom 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: Notification.didToggleSplitZoom, + object: surfaceView + ) + + + default: + assertionFailure() + } + } + + private static func controlInspector( + _ app: ghostty_app_t, + target: ghostty_target_s, + mode: ghostty_action_inspector_e) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("toggle split zoom 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: Notification.didControlInspector, + object: surfaceView, + userInfo: ["mode": mode] + ) + + + default: + assertionFailure() + } + } + + private static func showDesktopNotification( + _ app: ghostty_app_t, + target: ghostty_target_s, + n: ghostty_action_desktop_notification_s) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("toggle split zoom 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 } + guard let title = String(cString: n.title!, encoding: .utf8) else { return } + guard let body = String(cString: n.body!, encoding: .utf8) else { return } + + let center = UNUserNotificationCenter.current() + center.requestAuthorization(options: [.alert, .sound]) { _, error in + if let error = error { + Ghostty.logger.error("Error while requesting notification authorization: \(error)") + } + } + + center.getNotificationSettings() { settings in + guard settings.authorizationStatus == .authorized else { return } + surfaceView.showUserNotification(title: title, body: body) + } + + + default: + assertionFailure() + } + } + + private static func toggleSecureInput( + _ app: ghostty_app_t, + target: ghostty_target_s, + mode mode_raw: ghostty_action_secure_input_e + ) { + guard let mode = SetSecureInput.from(mode_raw) else { return } + + switch (target.tag) { + case GHOSTTY_TARGET_APP: + guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return } + appDelegate.setSecureInput(mode) + + case GHOSTTY_TARGET_SURFACE: + guard let surface = target.target.surface else { return } + guard let surfaceView = self.surfaceView(from: surface) else { return } + guard let appState = self.appState(fromView: surfaceView) else { return } + guard appState.config.autoSecureInput else { return } + + switch (mode) { + case .on: + surfaceView.passwordInput = true + + case .off: + surfaceView.passwordInput = false + + case .toggle: + surfaceView.passwordInput = !surfaceView.passwordInput + } + + default: + assertionFailure() + } + } + + private static func setTitle( + _ app: ghostty_app_t, + target: ghostty_target_s, + v: ghostty_action_set_title_s) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("set title 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 } + guard let title = String(cString: v.title!, encoding: .utf8) else { return } + + // We must set this in a dispatchqueue to avoid a deadlock on startup on some + // versions of macOS. I unfortunately didn't document the exact versions so + // I don't know when its safe to remove this. + DispatchQueue.main.async { + surfaceView.title = title + } + + + default: + assertionFailure() + } + } + + private static func setMouseShape( + _ app: ghostty_app_t, + target: ghostty_target_s, + shape: ghostty_action_mouse_shape_e) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("set mouse shapes 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 } + surfaceView.setCursorShape(shape) + + + default: + assertionFailure() + } + } + + private static func setMouseVisibility( + _ app: ghostty_app_t, + target: ghostty_target_s, + v: ghostty_action_mouse_visibility_e) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("set mouse shapes 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 } + switch (v) { + case GHOSTTY_MOUSE_VISIBLE: + surfaceView.setCursorVisibility(true) + + case GHOSTTY_MOUSE_HIDDEN: + surfaceView.setCursorVisibility(false) + + default: + return + } + + + default: + assertionFailure() + } + } + + private static func setMouseOverLink( + _ app: ghostty_app_t, + target: ghostty_target_s, + v: ghostty_action_mouse_over_link_s) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("mouse over link 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 } + guard v.len > 0 else { + surfaceView.hoverUrl = nil + return + } + + let buffer = Data(bytes: v.url!, count: v.len) + surfaceView.hoverUrl = String(data: buffer, encoding: .utf8) + + + default: + assertionFailure() + } + } + + private static func setInitialSize( + _ app: ghostty_app_t, + target: ghostty_target_s, + v: ghostty_action_initial_size_s) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("mouse over link 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 } + surfaceView.initialSize = NSMakeSize(Double(v.width), Double(v.height)) + + + default: + assertionFailure() + } + } + + private static func setCellSize( + _ app: ghostty_app_t, + target: ghostty_target_s, + v: ghostty_action_cell_size_s) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("mouse over link 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 } + let backingSize = NSSize(width: Double(v.width), height: Double(v.height)) + surfaceView.cellSize = surfaceView.convertFromBacking(backingSize) + + default: + assertionFailure() + } + } + + private static func renderInspector( + _ app: ghostty_app_t, + target: ghostty_target_s) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("mouse over link 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: Notification.inspectorNeedsDisplay, + object: surfaceView + ) + + default: + assertionFailure() + } + } + + private static func rendererHealth( + _ app: ghostty_app_t, + target: ghostty_target_s, + v: ghostty_action_renderer_health_e) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("mouse over link 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: Notification.didUpdateRendererHealth, + object: surfaceView, + userInfo: [ + "health": v, + ] + ) + + default: + assertionFailure() + } + } + + // MARK: User Notifications + /// Handle a received user notification. This is called when a user notification is clicked or dismissed by the user func handleUserNotification(response: UNNotificationResponse) { let userInfo = response.notification.request.content.userInfo @@ -600,86 +1037,6 @@ extension Ghostty { } } - /// Determine if a given notification should be presented to the user when Ghostty is running in the foreground. - func shouldPresentNotification(notification: UNNotification) -> Bool { - let userInfo = notification.request.content.userInfo - guard let uuidString = userInfo["surface"] as? String, - let uuid = UUID(uuidString: uuidString), - let surface = delegate?.findSurface(forUUID: uuid), - let window = surface.window else { return false } - return !window.isKeyWindow || !surface.focused - } - - static func newTab(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) { - let surface = self.surfaceUserdata(from: userdata) - - guard let appState = self.appState(fromView: surface) else { return } - guard appState.config.windowDecorations else { - let alert = NSAlert() - alert.messageText = "Tabs are disabled" - alert.informativeText = "Enable window decorations to use tabs" - alert.addButton(withTitle: "OK") - alert.alertStyle = .warning - _ = alert.runModal() - return - } - - NotificationCenter.default.post( - name: Notification.ghosttyNewTab, - object: surface, - userInfo: [ - Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: config), - ] - ) - } - - static func newWindow(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) { - let surface: SurfaceView? = if let userdata { - self.surfaceUserdata(from: userdata) - } else { - nil - } - - NotificationCenter.default.post( - name: Notification.ghosttyNewWindow, - object: surface, - userInfo: [ - Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: config), - ] - ) - } - - static func controlInspector(_ userdata: UnsafeMutableRawPointer?, mode: ghostty_inspector_mode_e) { - let surface = self.surfaceUserdata(from: userdata) - NotificationCenter.default.post(name: Notification.didControlInspector, object: surface, userInfo: [ - "mode": mode, - ]) - } - - static func updateRendererHealth(_ userdata: UnsafeMutableRawPointer?, health: ghostty_renderer_health_e) { - let surface = self.surfaceUserdata(from: userdata) - NotificationCenter.default.post( - name: Notification.didUpdateRendererHealth, - object: surface, - userInfo: [ - "health": health, - ] - ) - } - - /// Returns the GhosttyState from the given userdata value. - static private func appState(fromView view: SurfaceView) -> App? { - guard let surface = view.surface else { return nil } - guard let app = ghostty_surface_app(surface) else { return nil } - guard let app_ud = ghostty_app_userdata(app) else { return nil } - return Unmanaged.fromOpaque(app_ud).takeUnretainedValue() - } - - /// Returns the surface view from the userdata. - static private func surfaceUserdata(from userdata: UnsafeMutableRawPointer?) -> SurfaceView { - return Unmanaged.fromOpaque(userdata!).takeUnretainedValue() - } - #endif } } diff --git a/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift b/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift index c9429ab79..fa8335416 100644 --- a/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift +++ b/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift @@ -219,13 +219,13 @@ extension Ghostty { // Determine our desired direction guard let directionAny = notification.userInfo?["direction"] else { return } - guard let direction = directionAny as? ghostty_split_direction_e else { return } + guard let direction = directionAny as? ghostty_action_split_direction_e else { return } var splitDirection: SplitViewDirection switch (direction) { - case GHOSTTY_SPLIT_RIGHT: + case GHOSTTY_SPLIT_DIRECTION_RIGHT: splitDirection = .horizontal - case GHOSTTY_SPLIT_DOWN: + case GHOSTTY_SPLIT_DIRECTION_DOWN: splitDirection = .vertical default: diff --git a/macos/Sources/Ghostty/InspectorView.swift b/macos/Sources/Ghostty/InspectorView.swift index 2d867e000..b6147647e 100644 --- a/macos/Sources/Ghostty/InspectorView.swift +++ b/macos/Sources/Ghostty/InspectorView.swift @@ -55,7 +55,7 @@ extension Ghostty { private func onControlInspector(_ notification: SwiftUI.Notification) { // Determine our mode guard let modeAny = notification.userInfo?["mode"] else { return } - guard let mode = modeAny as? ghostty_inspector_mode_e else { return } + guard let mode = modeAny as? ghostty_action_inspector_e else { return } switch (mode) { case GHOSTTY_INSPECTOR_TOGGLE: diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index 475d68733..c55af2357 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -39,32 +39,54 @@ extension Ghostty { } } -// MARK: Surface Notifications +// MARK: Swift Types for C Types extension Ghostty { + enum SetSecureInput { + case on + case off + case toggle + + static func from(_ c: ghostty_action_secure_input_e) -> Self? { + switch (c) { + case GHOSTTY_SECURE_INPUT_ON: + return .on + + case GHOSTTY_SECURE_INPUT_OFF: + return .off + + case GHOSTTY_SECURE_INPUT_TOGGLE: + return .toggle + + default: + return nil + } + } + } + /// An enum that is used for the directions that a split focus event can change. enum SplitFocusDirection { case previous, next, top, bottom, left, right /// Initialize from a Ghostty API enum. - static func from(direction: ghostty_split_focus_direction_e) -> Self? { + static func from(direction: ghostty_action_goto_split_e) -> Self? { switch (direction) { - case GHOSTTY_SPLIT_FOCUS_PREVIOUS: + case GHOSTTY_GOTO_SPLIT_PREVIOUS: return .previous - case GHOSTTY_SPLIT_FOCUS_NEXT: + case GHOSTTY_GOTO_SPLIT_NEXT: return .next - case GHOSTTY_SPLIT_FOCUS_TOP: + case GHOSTTY_GOTO_SPLIT_TOP: return .top - case GHOSTTY_SPLIT_FOCUS_BOTTOM: + case GHOSTTY_GOTO_SPLIT_BOTTOM: return .bottom - case GHOSTTY_SPLIT_FOCUS_LEFT: + case GHOSTTY_GOTO_SPLIT_LEFT: return .left - case GHOSTTY_SPLIT_FOCUS_RIGHT: + case GHOSTTY_GOTO_SPLIT_RIGHT: return .right default: @@ -72,25 +94,25 @@ extension Ghostty { } } - func toNative() -> ghostty_split_focus_direction_e { + func toNative() -> ghostty_action_goto_split_e { switch (self) { case .previous: - return GHOSTTY_SPLIT_FOCUS_PREVIOUS + return GHOSTTY_GOTO_SPLIT_PREVIOUS case .next: - return GHOSTTY_SPLIT_FOCUS_NEXT + return GHOSTTY_GOTO_SPLIT_NEXT case .top: - return GHOSTTY_SPLIT_FOCUS_TOP + return GHOSTTY_GOTO_SPLIT_TOP case .bottom: - return GHOSTTY_SPLIT_FOCUS_BOTTOM + return GHOSTTY_GOTO_SPLIT_BOTTOM case .left: - return GHOSTTY_SPLIT_FOCUS_LEFT + return GHOSTTY_GOTO_SPLIT_LEFT case .right: - return GHOSTTY_SPLIT_FOCUS_RIGHT + return GHOSTTY_GOTO_SPLIT_RIGHT } } } @@ -99,31 +121,31 @@ extension Ghostty { enum SplitResizeDirection { case up, down, left, right - static func from(direction: ghostty_split_resize_direction_e) -> Self? { + static func from(direction: ghostty_action_resize_split_direction_e) -> Self? { switch (direction) { - case GHOSTTY_SPLIT_RESIZE_UP: + case GHOSTTY_RESIZE_SPLIT_UP: return .up; - case GHOSTTY_SPLIT_RESIZE_DOWN: + case GHOSTTY_RESIZE_SPLIT_DOWN: return .down; - case GHOSTTY_SPLIT_RESIZE_LEFT: + case GHOSTTY_RESIZE_SPLIT_LEFT: return .left; - case GHOSTTY_SPLIT_RESIZE_RIGHT: + case GHOSTTY_RESIZE_SPLIT_RIGHT: return .right; default: return nil } } - func toNative() -> ghostty_split_resize_direction_e { + func toNative() -> ghostty_action_resize_split_direction_e { switch (self) { case .up: - return GHOSTTY_SPLIT_RESIZE_UP; + return GHOSTTY_RESIZE_SPLIT_UP; case .down: - return GHOSTTY_SPLIT_RESIZE_DOWN; + return GHOSTTY_RESIZE_SPLIT_DOWN; case .left: - return GHOSTTY_SPLIT_RESIZE_LEFT; + return GHOSTTY_RESIZE_SPLIT_LEFT; case .right: - return GHOSTTY_SPLIT_RESIZE_RIGHT; + return GHOSTTY_RESIZE_SPLIT_RIGHT; } } } @@ -174,6 +196,8 @@ extension Ghostty { } } +// MARK: Surface Notifications + extension Ghostty.Notification { /// Used to pass a configuration along when creating a new tab/window/split. static let NewSurfaceConfigKey = "com.mitchellh.ghostty.newSurfaceConfig" @@ -201,7 +225,7 @@ extension Ghostty.Notification { /// Toggle fullscreen of current window static let ghosttyToggleFullscreen = Notification.Name("com.mitchellh.ghostty.toggleFullscreen") - static let NonNativeFullscreenKey = ghosttyToggleFullscreen.rawValue + static let FullscreenModeKey = ghosttyToggleFullscreen.rawValue /// Notification that a surface is becoming focused. This is only sent on macOS 12 to /// work around bugs. macOS 13+ should use the ".focused()" attribute. diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 3c2c17a81..a5e6b2f04 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -251,7 +251,7 @@ extension Ghostty { } } - func setCursorShape(_ shape: ghostty_mouse_shape_e) { + func setCursorShape(_ shape: ghostty_action_mouse_shape_e) { switch (shape) { case GHOSTTY_MOUSE_SHAPE_DEFAULT: pointerStyle = .default @@ -312,7 +312,7 @@ extension Ghostty { @objc private func onUpdateRendererHealth(notification: SwiftUI.Notification) { guard let healthAny = notification.userInfo?["health"] else { return } - guard let health = healthAny as? ghostty_renderer_health_e else { return } + guard let health = healthAny as? ghostty_action_renderer_health_e else { return } healthy = health == GHOSTTY_RENDERER_HEALTH_OK } @@ -926,12 +926,12 @@ extension Ghostty { @IBAction func splitRight(_ sender: Any) { guard let surface = self.surface else { return } - ghostty_surface_split(surface, GHOSTTY_SPLIT_RIGHT) + ghostty_surface_split(surface, GHOSTTY_SPLIT_DIRECTION_RIGHT) } @IBAction func splitDown(_ sender: Any) { guard let surface = self.surface else { return } - ghostty_surface_split(surface, GHOSTTY_SPLIT_DOWN) + ghostty_surface_split(surface, GHOSTTY_SPLIT_DIRECTION_DOWN) } @objc func resetTerminal(_ sender: Any) { diff --git a/macos/Sources/Helpers/FullScreenHandler.swift b/macos/Sources/Helpers/FullScreenHandler.swift index c9d6e594e..d12809d71 100644 --- a/macos/Sources/Helpers/FullScreenHandler.swift +++ b/macos/Sources/Helpers/FullScreenHandler.swift @@ -13,8 +13,18 @@ class FullScreenHandler { var isInNonNativeFullscreen: Bool = false var isInFullscreen: Bool = false - func toggleFullscreen(window: NSWindow, nonNativeFullscreen: ghostty_non_native_fullscreen_e) { - let useNonNativeFullscreen = nonNativeFullscreen != GHOSTTY_NON_NATIVE_FULLSCREEN_FALSE + func toggleFullscreen(window: NSWindow, mode: ghostty_action_fullscreen_e) { + let useNonNativeFullscreen = switch (mode) { + case GHOSTTY_FULLSCREEN_NATIVE: + false + + case GHOSTTY_FULLSCREEN_NON_NATIVE, GHOSTTY_FULLSCREEN_NON_NATIVE_VISIBLE_MENU: + true + + default: + false + } + if isInFullscreen { if useNonNativeFullscreen || isInNonNativeFullscreen { leaveFullscreen(window: window) @@ -27,7 +37,7 @@ class FullScreenHandler { isInFullscreen = false } else { if useNonNativeFullscreen { - let hideMenu = nonNativeFullscreen != GHOSTTY_NON_NATIVE_FULLSCREEN_VISIBLE_MENU + let hideMenu = mode != GHOSTTY_FULLSCREEN_NON_NATIVE_VISIBLE_MENU enterFullscreen(window: window, hideMenu: hideMenu) isInNonNativeFullscreen = true } else { diff --git a/src/Surface.zig b/src/Surface.zig index ed83b2af8..a37f0a7e8 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -515,14 +515,25 @@ pub fn init( errdefer self.io.deinit(); // Report initial cell size on surface creation - try rt_surface.setCellSize(cell_size.width, cell_size.height); + try rt_app.performAction( + .{ .surface = self }, + .cell_size, + .{ .width = cell_size.width, .height = cell_size.height }, + ); // Set a minimum size that is cols=10 h=4. This matches Mac's Terminal.app // but is otherwise somewhat arbitrary. - try rt_surface.setSizeLimits(.{ - .width = cell_size.width * 10, - .height = cell_size.height * 4, - }, null); + try rt_app.performAction( + .{ .surface = self }, + .size_limit, + .{ + .min_width = cell_size.width * 10, + .min_height = cell_size.height * 4, + // No max: + .max_width = 0, + .max_height = 0, + }, + ); // Call our size callback which handles all our retina setup // Note: this shouldn't be necessary and when we clean up the surface @@ -576,13 +587,23 @@ pub fn init( padding.top + padding.bottom; - rt_surface.setInitialWindowSize(final_width, final_height) catch |err| { + rt_app.performAction( + .{ .surface = self }, + .initial_size, + .{ .width = final_width, .height = final_height }, + ) catch |err| { + // We don't treat this as a fatal error because not setting + // an initial size shouldn't stop our terminal from working. log.warn("unable to set initial window size: {s}", .{err}); }; } if (config.title) |title| { - try rt_surface.setTitle(title); + try rt_app.performAction( + .{ .surface = self }, + .set_title, + .{ .title = title }, + ); } else if ((comptime builtin.os.tag == .linux) and config.@"_xdg-terminal-exec") xdg: { @@ -599,7 +620,11 @@ pub fn init( break :xdg; }; defer alloc.free(title); - try rt_surface.setTitle(title); + try rt_app.performAction( + .{ .surface = self }, + .set_title, + .{ .title = title }, + ); } } } @@ -743,7 +768,11 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { // We know that our title should end in 0. const slice = std.mem.sliceTo(@as([*:0]const u8, @ptrCast(v)), 0); log.debug("changing title \"{s}\"", .{slice}); - try self.rt_surface.setTitle(slice); + try self.rt_app.performAction( + .{ .surface = self }, + .set_title, + .{ .title = slice }, + ); }, .report_title => |style| { @@ -769,7 +798,11 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { .set_mouse_shape => |shape| { log.debug("changing mouse shape: {}", .{shape}); - try self.rt_surface.setMouseShape(shape); + try self.rt_app.performAction( + .{ .surface = self }, + .mouse_shape, + shape, + ); }, .clipboard_read => |clipboard| { @@ -897,7 +930,13 @@ fn modsChanged(self: *Surface, mods: input.Mods) void { /// Called when our renderer health state changes. fn updateRendererHealth(self: *Surface, health: renderer.Health) void { log.warn("renderer health status change status={}", .{health}); - self.rt_surface.updateRendererHealth(health); + self.rt_app.performAction( + .{ .surface = self }, + .renderer_health, + health, + ) catch |err| { + log.warn("failed to notify app of renderer health change err={}", .{err}); + }; } /// Update our configuration at runtime. @@ -1194,7 +1233,11 @@ fn setCellSize(self: *Surface, size: renderer.CellSize) !void { }, .unlocked); // Notify the window - try self.rt_surface.setCellSize(size.width, size.height); + try self.rt_app.performAction( + .{ .surface = self }, + .cell_size, + .{ .width = size.width, .height = size.height }, + ); } /// Change the font size. @@ -1214,10 +1257,14 @@ pub fn setFontSize(self: *Surface, size: font.face.DesiredSize) !void { errdefer self.app.font_grid_set.deref(font_grid_key); // Set our cell size - try self.setCellSize(.{ - .width = font_grid.metrics.cell_width, - .height = font_grid.metrics.cell_height, - }); + try self.rt_app.performAction( + .{ .surface = self }, + .cell_size, + .{ + .width = font_grid.metrics.cell_width, + .height = font_grid.metrics.cell_height, + }, + ); // Notify our render thread of the new font stack. The renderer // MUST accept the new font grid and deref the old. @@ -1472,8 +1519,11 @@ pub fn keyCallback( .mods = self.mouse.mods, .over_link = self.mouse.over_link, .hidden = self.mouse.hidden, - }).keyToMouseShape()) |shape| - try self.rt_surface.setMouseShape(shape); + }).keyToMouseShape()) |shape| try self.rt_app.performAction( + .{ .surface = self }, + .mouse_shape, + shape, + ); // We've processed a key event that produced some data so we want to // track the last pressed key. @@ -2975,7 +3025,11 @@ pub fn cursorPosCallback( // We also queue a render so the renderer can undo the rendered link // state. if (over_link) { - self.rt_surface.mouseOverLink(null); + try self.rt_app.performAction( + .{ .surface = self }, + .mouse_over_link, + .{ .url = "" }, + ); try self.queueRender(); } @@ -3061,7 +3115,11 @@ pub fn cursorPosCallback( self.renderer_state.mouse.point = pos_vp; self.mouse.over_link = true; self.renderer_state.terminal.screen.dirty.hyperlink_hover = true; - try self.rt_surface.setMouseShape(.pointer); + try self.rt_app.performAction( + .{ .surface = self }, + .mouse_shape, + .pointer, + ); switch (link[0]) { .open => { @@ -3070,7 +3128,11 @@ pub fn cursorPosCallback( .trim = false, }); defer self.alloc.free(str); - self.rt_surface.mouseOverLink(str); + try self.rt_app.performAction( + .{ .surface = self }, + .mouse_over_link, + .{ .url = str }, + ); }, ._open_osc8 => link: { @@ -3080,14 +3142,26 @@ pub fn cursorPosCallback( log.warn("failed to get URI for OSC8 hyperlink", .{}); break :link; }; - self.rt_surface.mouseOverLink(uri); + try self.rt_app.performAction( + .{ .surface = self }, + .mouse_over_link, + .{ .url = uri }, + ); }, } try self.queueRender(); } else if (over_link) { - try self.rt_surface.setMouseShape(self.io.terminal.mouse_shape); - self.rt_surface.mouseOverLink(null); + try self.rt_app.performAction( + .{ .surface = self }, + .mouse_shape, + self.io.terminal.mouse_shape, + ); + try self.rt_app.performAction( + .{ .surface = self }, + .mouse_over_link, + .{ .url = "" }, + ); try self.queueRender(); } } @@ -3396,13 +3470,25 @@ fn scrollToBottom(self: *Surface) !void { fn hideMouse(self: *Surface) void { if (self.mouse.hidden) return; self.mouse.hidden = true; - self.rt_surface.setMouseVisibility(false); + self.rt_app.performAction( + .{ .surface = self }, + .mouse_visibility, + .hidden, + ) catch |err| { + log.warn("apprt failed to set mouse visibility err={}", .{err}); + }; } fn showMouse(self: *Surface) void { if (!self.mouse.hidden) return; self.mouse.hidden = false; - self.rt_surface.setMouseVisibility(true); + self.rt_app.performAction( + .{ .surface = self }, + .mouse_visibility, + .visible, + ) catch |err| { + log.warn("apprt failed to set mouse visibility err={}", .{err}); + }; } /// Perform a binding action. A binding is a keybinding. This function diff --git a/src/apprt.zig b/src/apprt.zig index 7651ace9b..dd726b3f2 100644 --- a/src/apprt.zig +++ b/src/apprt.zig @@ -31,7 +31,6 @@ pub const ClipboardRequest = structs.ClipboardRequest; pub const ClipboardRequestType = structs.ClipboardRequestType; pub const ColorScheme = structs.ColorScheme; pub const CursorPos = structs.CursorPos; -pub const DesktopNotification = structs.DesktopNotification; pub const IMEPos = structs.IMEPos; pub const Selection = structs.Selection; pub const SurfaceSize = structs.SurfaceSize; diff --git a/src/apprt/action.zig b/src/apprt/action.zig index edeb02d7a..70c189c8f 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -1,13 +1,45 @@ const std = @import("std"); const assert = std.debug.assert; +const apprt = @import("../apprt.zig"); +const renderer = @import("../renderer.zig"); +const terminal = @import("../terminal/main.zig"); const CoreSurface = @import("../Surface.zig"); /// The target for an action. This is generally the thing that had focus /// while the action was made but the concept of "focus" is not guaranteed /// since actions can also be triggered by timers, scripts, etc. -pub const Target = union(enum) { +pub const Target = union(Key) { app, surface: *CoreSurface, + + // Sync with: ghostty_target_tag_e + pub const Key = enum(c_int) { + app, + surface, + }; + + // Sync with: ghostty_target_u + pub const CValue = extern union { + app: void, + surface: *apprt.Surface, + }; + + // Sync with: ghostty_target_s + pub const C = extern struct { + key: Key, + value: CValue, + }; + + /// Convert to ghostty_target_s. + pub fn cval(self: Target) C { + return .{ + .key = @as(Key, self), + .value = switch (self) { + .app => .{ .app = {} }, + .surface => |v| .{ .surface = v.rt_surface }, + }, + }; + } }; /// The possible actions an apprt has to react to. Actions are one-way @@ -19,7 +51,23 @@ pub const Target = union(enum) { /// Importantly, actions are generally OPTIONAL to implement by an apprt. /// Required functionality is called directly on the runtime structure so /// there is a compiler error if an action is not implemented. -pub const Action = union(enum) { +pub const Action = union(Key) { + // A GUIDE TO ADDING NEW ACTIONS: + // + // 1. Add the action to the `Key` enum. The order of the enum matters + // because it maps directly to the libghostty C enum. For ABI + // compatibility, new actions should be added to the end of the enum. + // + // 2. Add the action and optional value to the Action union. + // + // 3. If the value type is not void, ensure the value is C ABI + // compatible (extern). If it is not, add a `C` decl to the value + // and a `cval` function to convert to the C ABI compatible value. + // + // 4. Update `include/ghostty.h`: add the new key, value, and union + // entry. If the value type is void then only the key needs to be + // added. Ensure the order matches exactly with the Zig code. + /// Open a new window. The target determines whether properties such /// as font size should be inherited. new_window, @@ -62,12 +110,42 @@ pub const Action = union(enum) { /// Present the target terminal whether its a tab, split, or window. present_terminal, + /// Sets a size limit (in pixels) for the target terminal. + size_limit: SizeLimit, + + /// Specifies the initial size of the target terminal. This will be + /// sent only during the initialization of a surface. If it is received + /// after the surface is initialized it should be ignored. + initial_size: InitialSize, + + /// The cell size has changed to the given dimensions in pixels. + cell_size: CellSize, + /// Control whether the inspector is shown or hidden. inspector: Inspector, + /// The inspector for the given target has changes and should be + /// rendered at the next opportunity. + render_inspector, + /// Show a desktop notification. desktop_notification: DesktopNotification, + /// Set the title of the target. + set_title: SetTitle, + + /// Set the mouse cursor shape. + mouse_shape: terminal.MouseShape, + + /// Set whether the mouse cursor is visible or not. + mouse_visibility: MouseVisibility, + + /// Called when the mouse is over or recently left a link. + mouse_over_link: MouseOverLink, + + /// The health of the renderer has changed. + renderer_health: renderer.Health, + /// Open the Ghostty configuration. This is platform-specific about /// what it means; it can mean opening a dedicated UI or just opening /// a file in a text editor. @@ -86,8 +164,69 @@ pub const Action = union(enum) { /// system APIs to not log the input, etc. secure_input: SecureInput, - /// The enum of keys in the tagged union. - pub const Key = @typeInfo(Action).Union.tag_type.?; + /// Sync with: ghostty_action_tag_e + pub const Key = enum(c_int) { + new_window, + new_tab, + new_split, + close_all_windows, + toggle_fullscreen, + toggle_window_decorations, + goto_tab, + goto_split, + resize_split, + equalize_splits, + toggle_split_zoom, + present_terminal, + size_limit, + initial_size, + cell_size, + inspector, + render_inspector, + desktop_notification, + set_title, + mouse_shape, + mouse_visibility, + mouse_over_link, + renderer_health, + open_config, + quit_timer, + secure_input, + }; + + /// Sync with: ghostty_action_u + pub const CValue = cvalue: { + const key_fields = @typeInfo(Key).Enum.fields; + var union_fields: [key_fields.len]std.builtin.Type.UnionField = undefined; + for (key_fields, 0..) |field, i| { + const action = @unionInit(Action, field.name, undefined); + const Type = t: { + const Type = @TypeOf(@field(action, field.name)); + // Types can provide custom types for their CValue. + if (Type != void and @hasDecl(Type, "C")) break :t Type.C; + break :t Type; + }; + + union_fields[i] = .{ + .name = field.name, + .type = Type, + .alignment = @alignOf(Type), + }; + } + + break :cvalue @Type(.{ .Union = .{ + .layout = .@"extern", + .tag_type = Key, + .fields = &union_fields, + .decls = &.{}, + } }); + }; + + /// Sync with: ghostty_action_s + pub const C = extern struct { + key: Key, + value: CValue, + }; /// Returns the value type for the given key. pub fn Value(comptime key: Key) type { @@ -98,6 +237,22 @@ pub const Action = union(enum) { unreachable; } + + /// Convert to ghostty_action_s. + pub fn cval(self: Action) C { + const value: CValue = switch (self) { + inline else => |v, tag| @unionInit( + CValue, + @tagName(tag), + if (@TypeOf(v) != void and @hasDecl(@TypeOf(v), "cval")) v.cval() else v, + ), + }; + + return .{ + .key = @as(Key, self), + .value = value, + }; + } }; // This is made extern (c_int) to make interop easier with our embedded @@ -120,7 +275,7 @@ pub const GotoSplit = enum(c_int) { }; /// The amount to resize the split by and the direction to resize it in. -pub const ResizeSplit = struct { +pub const ResizeSplit = extern struct { amount: u16, direction: Direction, @@ -170,8 +325,75 @@ pub const QuitTimer = enum(c_int) { stop, }; +pub const MouseVisibility = enum(c_int) { + visible, + hidden, +}; + +pub const MouseOverLink = struct { + url: []const u8, + + // Sync with: ghostty_action_mouse_over_link_s + pub const C = extern struct { + url: [*]const u8, + len: usize, + }; + + pub fn cval(self: MouseOverLink) C { + return .{ + .url = self.url.ptr, + .len = self.url.len, + }; + } +}; + +pub const SizeLimit = extern struct { + min_width: u32, + min_height: u32, + max_width: u32, + max_height: u32, +}; + +pub const InitialSize = extern struct { + width: u32, + height: u32, +}; + +pub const CellSize = extern struct { + width: u32, + height: u32, +}; + +pub const SetTitle = struct { + title: [:0]const u8, + + // Sync with: ghostty_action_set_title_s + pub const C = extern struct { + title: [*:0]const u8, + }; + + pub fn cval(self: SetTitle) C { + return .{ + .title = self.title.ptr, + }; + } +}; + /// The desktop notification to show. pub const DesktopNotification = struct { title: [:0]const u8, body: [:0]const u8, + + // Sync with: ghostty_action_desktop_notification_s + pub const C = extern struct { + title: [*:0]const u8, + body: [*:0]const u8, + }; + + pub fn cval(self: DesktopNotification) C { + return .{ + .title = self.title.ptr, + .body = self.body.ptr, + }; + } }; diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index ca53f137e..88a69050f 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -44,23 +44,14 @@ pub const App = struct { /// a full tick of the app loop. wakeup: *const fn (AppUD) callconv(.C) void, + /// Callback called to handle an action. + action: *const fn (*App, apprt.Target.C, apprt.Action.C) callconv(.C) void, + /// Reload the configuration and return the new configuration. /// The old configuration can be freed immediately when this is /// called. reload_config: *const fn (AppUD) callconv(.C) ?*const Config, - /// Open the configuration file. - open_config: *const fn (AppUD) callconv(.C) void, - - /// Called to set the title of the window. - set_title: *const fn (SurfaceUD, [*]const u8) callconv(.C) void, - - /// Called to set the cursor shape. - set_mouse_shape: *const fn (SurfaceUD, terminal.MouseShape) callconv(.C) void, - - /// Called to set the mouse visibility. - set_mouse_visibility: *const fn (SurfaceUD, bool) callconv(.C) void, - /// Read the clipboard value. The return value must be preserved /// by the host until the next call. If there is no valid clipboard /// value then this should return null. @@ -79,73 +70,8 @@ pub const App = struct { /// Write the clipboard value. write_clipboard: *const fn (SurfaceUD, [*:0]const u8, c_int, bool) callconv(.C) void, - /// Create a new split view. If the embedder doesn't support split - /// views then this can be null. - new_split: ?*const fn (SurfaceUD, apprt.action.SplitDirection, apprt.Surface.Options) callconv(.C) void = null, - - /// New tab with options. The surface may be null if there is no target - /// surface in which case the apprt is expected to create a new window. - new_tab: ?*const fn (SurfaceUD, apprt.Surface.Options) callconv(.C) void = null, - - /// New window with options. The surface may be null if there is no - /// target surface. - new_window: ?*const fn (SurfaceUD, apprt.Surface.Options) callconv(.C) void = null, - - /// Control the inspector visibility - control_inspector: ?*const fn (SurfaceUD, apprt.action.Inspector) callconv(.C) void = null, - /// Close the current surface given by this function. close_surface: ?*const fn (SurfaceUD, bool) callconv(.C) void = null, - - /// Focus the previous/next split (if any). - focus_split: ?*const fn (SurfaceUD, apprt.action.GotoSplit) callconv(.C) void = null, - - /// Resize the current split. - resize_split: ?*const fn (SurfaceUD, apprt.action.ResizeSplit.Direction, u16) callconv(.C) void = null, - - /// Equalize all splits in the current window - equalize_splits: ?*const fn (SurfaceUD) callconv(.C) void = null, - - /// Zoom the current split. - toggle_split_zoom: ?*const fn (SurfaceUD) callconv(.C) void = null, - - /// Goto tab - goto_tab: ?*const fn (SurfaceUD, apprt.action.GotoTab) callconv(.C) void = null, - - /// Toggle fullscreen for current window. - toggle_fullscreen: ?*const fn (SurfaceUD, configpkg.NonNativeFullscreen) callconv(.C) void = null, - - /// Set the initial window size. It is up to the user of libghostty to - /// determine if it is the initial window and set this appropriately. - set_initial_window_size: ?*const fn (SurfaceUD, u32, u32) callconv(.C) void = null, - - /// Render the inspector for the given surface. - render_inspector: ?*const fn (SurfaceUD) callconv(.C) void = null, - - /// Called when the cell size changes. - set_cell_size: ?*const fn (SurfaceUD, u32, u32) callconv(.C) void = null, - - /// Show a desktop notification to the user. The surface may be null - /// if the notification is global. - show_desktop_notification: ?*const fn (SurfaceUD, [*:0]const u8, [*:0]const u8) void = null, - - /// Called when the health of the renderer changes. - update_renderer_health: ?*const fn (SurfaceUD, renderer.Health) void = null, - - /// Called when the mouse goes over a link. The link target is the - /// parameter. The link target will be null if the mouse is no longer - /// over a link. - mouse_over_link: ?*const fn (SurfaceUD, ?[*]const u8, usize) void = null, - - /// Notifies that a password input has been started for the given - /// surface. The apprt can use this to modify UI, enable features - /// such as macOS secure input, etc. - /// - /// The surface userdata will be null if a surface isn't focused. - set_password_input: ?*const fn (SurfaceUD, bool) callconv(.C) void = null, - - /// Toggle secure input for the application. - toggle_secure_input: ?*const fn () callconv(.C) void = null, }; /// This is the key event sent for ghostty_surface_key and @@ -492,213 +418,6 @@ pub const App = struct { surface.queueInspectorRender(); } - fn newWindow(self: *App, parent: ?*CoreSurface) !void { - // If we have a parent, the surface logic handles it. - if (parent) |surface| { - try surface.rt_surface.newWindow(); - return; - } - - // No parent, call the new window callback. - const func = self.opts.new_window orelse { - log.info("runtime embedder does not support new_window", .{}); - return; - }; - - func(null, .{}); - } - - fn toggleFullscreen( - self: *App, - target: apprt.Target, - fullscreen: apprt.action.Fullscreen, - ) void { - const func = self.opts.toggle_fullscreen orelse { - log.info("runtime embedder does not toggle_fullscreen", .{}); - return; - }; - - switch (target) { - .app => {}, - .surface => |v| func( - v.rt_surface.userdata, - switch (fullscreen) { - .native => .false, - .macos_non_native => .true, - .macos_non_native_visible_menu => .@"visible-menu", - }, - ), - } - } - - fn newTab(self: *const App, target: apprt.Target) void { - const func = self.opts.new_tab orelse { - log.info("runtime embedder does not support new_tab", .{}); - return; - }; - - switch (target) { - .app => func(null, .{}), - .surface => |v| func( - v.rt_surface.userdata, - v.rt_surface.newSurfaceOptions(), - ), - } - } - - fn gotoTab(self: *App, target: apprt.Target, tab: apprt.action.GotoTab) void { - const func = self.opts.goto_tab orelse { - log.info("runtime embedder does not support goto_tab", .{}); - return; - }; - - switch (target) { - .app => {}, - .surface => |v| func(v.rt_surface.userdata, tab), - } - } - - fn newSplit( - self: *const App, - target: apprt.Target, - direction: apprt.action.SplitDirection, - ) void { - const func = self.opts.new_split orelse { - log.info("runtime embedder does not support splits", .{}); - return; - }; - - switch (target) { - .app => func(null, direction, .{}), - .surface => |v| func( - v.rt_surface.userdata, - direction, - v.rt_surface.newSurfaceOptions(), - ), - } - } - - fn gotoSplit( - self: *const App, - target: apprt.Target, - direction: apprt.action.GotoSplit, - ) void { - const func = self.opts.focus_split orelse { - log.info("runtime embedder does not support focus split", .{}); - return; - }; - - switch (target) { - .app => {}, - .surface => |v| func(v.rt_surface.userdata, direction), - } - } - - fn resizeSplit( - self: *const App, - target: apprt.Target, - resize: apprt.action.ResizeSplit, - ) void { - const func = self.opts.resize_split orelse { - log.info("runtime embedder does not support resize split", .{}); - return; - }; - - switch (target) { - .app => {}, - .surface => |v| func( - v.rt_surface.userdata, - resize.direction, - resize.amount, - ), - } - } - - pub fn equalizeSplits(self: *const App, target: apprt.Target) void { - const func = self.opts.equalize_splits orelse { - log.info("runtime embedder does not support equalize splits", .{}); - return; - }; - - switch (target) { - .app => func(null), - .surface => |v| func(v.rt_surface.userdata), - } - } - - fn toggleSplitZoom(self: *const App, target: apprt.Target) void { - const func = self.opts.toggle_split_zoom orelse { - log.info("runtime embedder does not support split zoom", .{}); - return; - }; - - switch (target) { - .app => func(null), - .surface => |v| func(v.rt_surface.userdata), - } - } - - fn controlInspector( - self: *const App, - target: apprt.Target, - value: apprt.action.Inspector, - ) void { - const func = self.opts.control_inspector orelse { - log.info("runtime embedder does not support the terminal inspector", .{}); - return; - }; - - switch (target) { - .app => {}, - .surface => |v| func(v.rt_surface.userdata, value), - } - } - - fn showDesktopNotification( - self: *const App, - target: apprt.Target, - notification: apprt.action.DesktopNotification, - ) void { - const func = self.opts.show_desktop_notification orelse { - log.info("runtime embedder does not support show_desktop_notification", .{}); - return; - }; - - func(switch (target) { - .app => null, - .surface => |v| v.rt_surface.userdata, - }, notification.title, notification.body); - } - - fn setPasswordInput(self: *App, target: apprt.Target, v: apprt.action.SecureInput) void { - switch (v) { - inline .on, .off => |tag| { - const func = self.opts.set_password_input orelse { - log.info("runtime embedder does not support set_password_input", .{}); - return; - }; - - func(switch (target) { - .app => null, - .surface => |surface| surface.rt_surface.userdata, - }, switch (tag) { - .on => true, - .off => false, - else => comptime unreachable, - }); - }, - - .toggle => { - const func = self.opts.toggle_secure_input orelse { - log.info("runtime embedder does not support toggle_secure_input", .{}); - return; - }; - - func(); - }, - } - } - /// Perform a given action. pub fn performAction( self: *App, @@ -706,32 +425,27 @@ pub const App = struct { comptime action: apprt.Action.Key, value: apprt.Action.Value(action), ) !void { + // Special case certain actions before they are sent to the embedder switch (action) { - .new_window => _ = try self.newWindow(switch (target) { - .app => null, - .surface => |v| v, - }), - .toggle_fullscreen => self.toggleFullscreen(target, value), + .set_title => switch (target) { + .app => {}, + .surface => |surface| { + // Dupe the title so that we can store it. If we get an allocation + // error we just ignore it, since this only breaks a few minor things. + const alloc = self.core_app.alloc; + if (surface.rt_surface.title) |v| alloc.free(v); + surface.rt_surface.title = alloc.dupeZ(u8, value.title) catch null; + }, + }, - .new_tab => self.newTab(target), - .goto_tab => self.gotoTab(target, value), - .new_split => self.newSplit(target, value), - .resize_split => self.resizeSplit(target, value), - .equalize_splits => self.equalizeSplits(target), - .toggle_split_zoom => self.toggleSplitZoom(target), - .goto_split => self.gotoSplit(target, value), - .open_config => try configpkg.edit.open(self.core_app.alloc), - .inspector => self.controlInspector(target, value), - .desktop_notification => self.showDesktopNotification(target, value), - .secure_input => self.setPasswordInput(target, value), - - // Unimplemented - .present_terminal, - .close_all_windows, - .toggle_window_decorations, - .quit_timer, - => log.warn("unimplemented action={}", .{action}), + else => {}, } + + self.opts.action( + self, + target.cval(), + @unionInit(apprt.Action, @tagName(action), value).cval(), + ); } }; @@ -966,44 +680,10 @@ pub const Surface = struct { return self.size; } - pub fn setSizeLimits(self: *Surface, min: apprt.SurfaceSize, max_: ?apprt.SurfaceSize) !void { - _ = self; - _ = min; - _ = max_; - } - - pub fn setTitle(self: *Surface, slice: [:0]const u8) !void { - // Dupe the title so that we can store it. If we get an allocation - // error we just ignore it, since this only breaks a few minor things. - const alloc = self.app.core_app.alloc; - if (self.title) |v| alloc.free(v); - self.title = alloc.dupeZ(u8, slice) catch null; - - self.app.opts.set_title( - self.userdata, - slice.ptr, - ); - } - pub fn getTitle(self: *Surface) ?[:0]const u8 { return self.title; } - pub fn setMouseShape(self: *Surface, shape: terminal.MouseShape) !void { - self.app.opts.set_mouse_shape( - self.userdata, - shape, - ); - } - - /// Set the visibility of the mouse cursor. - pub fn setMouseVisibility(self: *Surface, visible: bool) void { - self.app.opts.set_mouse_visibility( - self.userdata, - visible, - ); - } - pub fn supportsClipboard( self: *const Surface, clipboard_type: apprt.Clipboard, @@ -1236,44 +916,18 @@ pub const Surface = struct { }; } - fn newWindow(self: *const Surface) !void { - const func = self.app.opts.new_window orelse { - log.info("runtime embedder does not support new_window", .{}); + fn queueInspectorRender(self: *Surface) void { + self.app.performAction( + .{ .surface = &self.core_surface }, + .render_inspector, + {}, + ) catch |err| { + log.err("error rendering the inspector err={}", .{err}); return; }; - - const options = self.newSurfaceOptions(); - func(self.userdata, options); } - pub fn setInitialWindowSize(self: *const Surface, width: u32, height: u32) !void { - const func = self.app.opts.set_initial_window_size orelse { - log.info("runtime embedder does not set_initial_window_size", .{}); - return; - }; - - func(self.userdata, width, height); - } - - fn queueInspectorRender(self: *const Surface) void { - const func = self.app.opts.render_inspector orelse { - log.info("runtime embedder does not render_inspector", .{}); - return; - }; - - func(self.userdata); - } - - pub fn setCellSize(self: *const Surface, width: u32, height: u32) !void { - const func = self.app.opts.set_cell_size orelse { - log.info("runtime embedder does not support set_cell_size", .{}); - return; - }; - - func(self.userdata, width, height); - } - - fn newSurfaceOptions(self: *const Surface) apprt.Surface.Options { + pub fn newSurfaceOptions(self: *const Surface) apprt.Surface.Options { const font_size: f32 = font_size: { if (!self.app.config.@"window-inherit-font-size") break :font_size 0; break :font_size self.core_surface.font_size.points; @@ -1290,29 +944,6 @@ pub const Surface = struct { const scale = try self.getContentScale(); return .{ .x = pos.x * scale.x, .y = pos.y * scale.y }; } - - /// Update the health of the renderer. - pub fn updateRendererHealth(self: *const Surface, health: renderer.Health) void { - const func = self.app.opts.update_renderer_health orelse { - log.info("runtime embedder does not support update_renderer_health", .{}); - return; - }; - - func(self.userdata, health); - } - - pub fn mouseOverLink(self: *const Surface, uri: ?[]const u8) void { - const func = self.app.opts.mouse_over_link orelse { - log.info("runtime embedder does not support over_link", .{}); - return; - }; - - if (uri) |v| { - func(self.userdata, v.ptr, v.len); - } else { - func(self.userdata, null, 0); - } - } }; /// Inspector is the state required for the terminal inspector. A terminal @@ -1735,11 +1366,21 @@ pub const CAPI = struct { ptr.app.closeSurface(ptr); } + /// Returns the userdata associated with the surface. + export fn ghostty_surface_userdata(surface: *Surface) ?*anyopaque { + return surface.userdata; + } + /// Returns the app associated with a surface. export fn ghostty_surface_app(surface: *Surface) *App { return surface.app; } + /// Returns the config to use for surfaces that inherit from this one. + export fn ghostty_surface_inherited_config(surface: *Surface) Surface.Options { + return surface.newSurfaceOptions(); + } + /// Returns true if the surface needs to confirm quitting. export fn ghostty_surface_needs_confirm_quit(surface: *Surface) bool { return surface.core_surface.needsConfirmQuit(); diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index b73aefced..57667afb1 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -134,8 +134,6 @@ pub const App = struct { comptime action: apprt.Action.Key, value: apprt.Action.Value(action), ) !void { - _ = value; - switch (action) { .new_window => _ = try self.newSurface(switch (target) { .app => null, @@ -147,10 +145,47 @@ pub const App = struct { .surface => |v| v, }), + .size_limit => switch (target) { + .app => {}, + .surface => |surface| try surface.rt_surface.setSizeLimits(.{ + .width = value.min_width, + .height = value.min_height, + }, if (value.max_width > 0) .{ + .width = value.max_width, + .height = value.max_height, + } else null), + }, + + .initial_size => switch (target) { + .app => {}, + .surface => |surface| try surface.rt_surface.setInitialWindowSize( + value.width, + value.height, + ), + }, + .toggle_fullscreen => self.toggleFullscreen(target), .open_config => try configpkg.edit.open(self.app.alloc), + .set_title => switch (target) { + .app => {}, + .surface => |surface| try surface.rt_surface.setTitle(value.title), + }, + + .mouse_shape => switch (target) { + .app => {}, + .surface => |surface| try surface.rt_surface.setMouseShape(value), + }, + + .mouse_visibility => switch (target) { + .app => {}, + .surface => |surface| surface.rt_surface.setMouseVisibility(switch (value) { + .visible => true, + .hidden => false, + }), + }, + // Unimplemented .new_split, .goto_split, @@ -162,9 +197,13 @@ pub const App = struct { .toggle_window_decorations, .goto_tab, .inspector, + .render_inspector, .quit_timer, .secure_input, .desktop_notification, + .mouse_over_link, + .cell_size, + .renderer_health, => log.info("unimplemented action={}", .{action}), } } @@ -581,7 +620,7 @@ pub const Surface = struct { /// Set the initial window size. This is called exactly once at /// surface initialization time. This may be called before "self" /// is fully initialized. - pub fn setInitialWindowSize(self: *const Surface, width: u32, height: u32) !void { + fn setInitialWindowSize(self: *const Surface, width: u32, height: u32) !void { const monitor = self.window.getMonitor() orelse glfw.Monitor.getPrimary() orelse { log.warn("window is not on a monitor, not setting initial size", .{}); return; @@ -594,18 +633,11 @@ pub const Surface = struct { }); } - /// Set the cell size. Unused by GLFW. - pub fn setCellSize(self: *const Surface, width: u32, height: u32) !void { - _ = self; - _ = width; - _ = height; - } - /// Set the size limits of the window. /// Note: this interface is not good, we should redo it if we plan /// to use this more. i.e. you can't set max width but no max height, /// or no mins. - pub fn setSizeLimits(self: *Surface, min: apprt.SurfaceSize, max_: ?apprt.SurfaceSize) !void { + fn setSizeLimits(self: *Surface, min: apprt.SurfaceSize, max_: ?apprt.SurfaceSize) !void { self.window.setSizeLimits(.{ .width = min.width, .height = min.height, @@ -655,7 +687,7 @@ pub const Surface = struct { } /// Set the title of the window. - pub fn setTitle(self: *Surface, slice: [:0]const u8) !void { + fn setTitle(self: *Surface, slice: [:0]const u8) !void { if (self.title_text) |t| self.core_surface.alloc.free(t); self.title_text = try self.core_surface.alloc.dupeZ(u8, slice); self.window.setTitle(self.title_text.?.ptr); @@ -667,7 +699,7 @@ pub const Surface = struct { } /// Set the shape of the cursor. - pub fn setMouseShape(self: *Surface, shape: terminal.MouseShape) !void { + fn setMouseShape(self: *Surface, shape: terminal.MouseShape) !void { if ((comptime builtin.target.isDarwin()) and !internal_os.macosVersionAtLeast(13, 0, 0)) { @@ -703,23 +735,11 @@ pub const Surface = struct { self.cursor = new; } - pub fn mouseOverLink(self: *Surface, uri: ?[]const u8) void { - // We don't do anything in GLFW. - _ = self; - _ = uri; - } - /// Set the visibility of the mouse cursor. - pub fn setMouseVisibility(self: *Surface, visible: bool) void { + fn setMouseVisibility(self: *Surface, visible: bool) void { self.window.setInputModeCursor(if (visible) .normal else .hidden); } - pub fn updateRendererHealth(self: *const Surface, health: renderer.Health) void { - // We don't support this in GLFW. - _ = self; - _ = health; - } - pub fn supportsClipboard( self: *const Surface, clipboard_type: apprt.Clipboard, diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 1d97731fa..45031324a 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -17,6 +17,7 @@ const apprt = @import("../../apprt.zig"); const configpkg = @import("../../config.zig"); const input = @import("../../input.zig"); const internal_os = @import("../../os/main.zig"); +const terminal = @import("../../terminal/main.zig"); const Config = configpkg.Config; const CoreApp = @import("../../App.zig"); const CoreSurface = @import("../../Surface.zig"); @@ -365,14 +366,23 @@ pub fn performAction( .open_config => try configpkg.edit.open(self.core_app.alloc), .inspector => self.controlInspector(target, value), .desktop_notification => self.showDesktopNotification(target, value), + .set_title => try self.setTitle(target, value), .present_terminal => self.presentTerminal(target), + .initial_size => try self.setInitialSize(target, value), + .mouse_visibility => self.setMouseVisibility(target, value), + .mouse_shape => try self.setMouseShape(target, value), + .mouse_over_link => self.setMouseOverLink(target, value), .toggle_window_decorations => self.toggleWindowDecorations(target), .quit_timer => self.quitTimer(value), // Unimplemented .close_all_windows, .toggle_split_zoom, + .size_limit, + .cell_size, .secure_input, + .render_inspector, + .renderer_health, => log.warn("unimplemented action={}", .{action}), } } @@ -551,6 +561,66 @@ fn quitTimer(self: *App, mode: apprt.action.QuitTimer) void { } } +fn setTitle( + _: *App, + target: apprt.Target, + title: apprt.action.SetTitle, +) !void { + switch (target) { + .app => {}, + .surface => |v| try v.rt_surface.setTitle(title.title), + } +} + +fn setMouseVisibility( + _: *App, + target: apprt.Target, + visibility: apprt.action.MouseVisibility, +) void { + switch (target) { + .app => {}, + .surface => |v| v.rt_surface.setMouseVisibility(switch (visibility) { + .visible => true, + .hidden => false, + }), + } +} + +fn setMouseShape( + _: *App, + target: apprt.Target, + shape: terminal.MouseShape, +) !void { + switch (target) { + .app => {}, + .surface => |v| try v.rt_surface.setMouseShape(shape), + } +} + +fn setMouseOverLink( + _: *App, + target: apprt.Target, + value: apprt.action.MouseOverLink, +) void { + switch (target) { + .app => {}, + .surface => |v| v.rt_surface.mouseOverLink(value.url), + } +} + +fn setInitialSize( + _: *App, + target: apprt.Target, + value: apprt.action.InitialSize, +) !void { + switch (target) { + .app => {}, + .surface => |v| try v.rt_surface.setInitialWindowSize( + value.width, + value.height, + ), + } +} fn showDesktopNotification( self: *App, target: apprt.Target, diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 054eb675d..73837d11d 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -784,18 +784,6 @@ pub fn setInitialWindowSize(self: *const Surface, width: u32, height: u32) !void ); } -pub fn setCellSize(self: *const Surface, width: u32, height: u32) !void { - _ = self; - _ = width; - _ = height; -} - -pub fn setSizeLimits(self: *Surface, min: apprt.SurfaceSize, max_: ?apprt.SurfaceSize) !void { - _ = self; - _ = min; - _ = max_; -} - pub fn grabFocus(self: *Surface) void { if (self.container.tab()) |tab| tab.focus_child = self; @@ -1921,9 +1909,3 @@ pub fn present(self: *Surface) void { self.grabFocus(); } - -pub fn updateRendererHealth(self: *const Surface, health: renderer.Health) void { - // We don't support this in GTK. - _ = self; - _ = health; -} diff --git a/src/apprt/structs.zig b/src/apprt/structs.zig index f8bcd72cf..e2e9b913d 100644 --- a/src/apprt/structs.zig +++ b/src/apprt/structs.zig @@ -52,16 +52,6 @@ pub const ClipboardRequest = union(ClipboardRequestType) { osc_52_write: Clipboard, }; -/// A desktop notification. -pub const DesktopNotification = struct { - /// The title of the notification. May be an empty string to not show a - /// title. - title: []const u8, - - /// The body of a notification. This will always be shown. - body: []const u8, -}; - /// The color scheme in use (light vs dark). pub const ColorScheme = enum(u2) { light = 0,