Merge pull request #2305 from ghostty-org/macos-dispatch

libghostty: unified action dispatch
This commit is contained in:
Mitchell Hashimoto
2024-09-27 06:55:12 -07:00
committed by GitHub
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 }
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: [:]
)
let surfaceView = self.surfaceUserdata(from: userdata)
guard let appState = self.appState(fromView: surfaceView) else { return }
guard appState.config.autoSecureInput else { return }
surfaceView.passwordInput = value
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,