libghostty: unified action dispatch

First, this commit modifies libghostty to use a single unified action
dispatch system based on a tagged union versus the one-off callback
system that was previously in place. This change simplifies the code on
both the core and consumer sides of the library. Importantly, as we
introduce new actions, we can now maintain ABI compatibility so long as
our union size does not change (something I don't promise yet).

Second, this moves a lot more of the functions call on a surface into
the action system. This affects all apprts and continues the previous
work of introducing a more unified API for optional surface features.
This commit is contained in:
Mitchell Hashimoto
2024-09-26 16:18:11 -07:00
parent 84e0a05027
commit 4ae20212bf
17 changed files with 1451 additions and 948 deletions

View File

@ -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);

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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<CChar>?) {}
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<CChar>?, body: UnsafePointer<CChar>?) {}
static func updateRendererHealth(_ userdata: UnsafeMutableRawPointer?, health: ghostty_renderer_health_e) {}
static func mouseOverLink(_ userdata: UnsafeMutableRawPointer?, uri: UnsafePointer<CChar>?, 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<CChar>?) {
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<App>.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<SurfaceView>.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<SurfaceView>.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<CChar>?, 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<CChar>?, body: UnsafePointer<CChar>?) {
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<App>.fromOpaque(app_ud).takeUnretainedValue()
}
/// Returns the surface view from the userdata.
static private func surfaceUserdata(from userdata: UnsafeMutableRawPointer?) -> SurfaceView {
return Unmanaged<SurfaceView>.fromOpaque(userdata!).takeUnretainedValue()
}
#endif
}
}

View File

@ -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:

View File

@ -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:

View File

@ -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.

View File

@ -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) {

View File

@ -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 {

View File

@ -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

View File

@ -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;

View File

@ -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,
};
}
};

View File

@ -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();

View File

@ -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,

View File

@ -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,

View File

@ -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;
}

View File

@ -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,