diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-04 19:22:04 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 01:23:06 +0300 |
| commit | 41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch) | |
| tree | 3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/apprt/embedded.zig | |
| parent | 55c138263e190a1e79d4b997bfb9397306c0ec1f (diff) | |
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/apprt/embedded.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/apprt/embedded.zig | 2231 |
1 files changed, 2231 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/apprt/embedded.zig b/vendor/libghostty-vt/src/apprt/embedded.zig new file mode 100644 index 00000000..519a35f2 --- /dev/null +++ b/vendor/libghostty-vt/src/apprt/embedded.zig @@ -0,0 +1,2231 @@ +//! Application runtime for the embedded version of Ghostty. The embedded +//! version is when Ghostty is embedded within a parent host application, +//! rather than owning the application lifecycle itself. This is used for +//! example for the macOS build of Ghostty so that we can use a native +//! Swift+XCode-based application. + +const std = @import("std"); +const builtin = @import("builtin"); +const assert = @import("../quirks.zig").inlineAssert; +const Allocator = std.mem.Allocator; +const objc = @import("objc"); +const apprt = @import("../apprt.zig"); +const font = @import("../font/main.zig"); +const input = @import("../input.zig"); +const internal_os = @import("../os/main.zig"); +const renderer = @import("../renderer.zig"); +const terminal = @import("../terminal/main.zig"); +const CoreApp = @import("../App.zig"); +const CoreInspector = @import("../inspector/main.zig").Inspector; +const CoreSurface = @import("../Surface.zig"); +const configpkg = @import("../config.zig"); +const Config = configpkg.Config; + +const log = std.log.scoped(.embedded_window); + +pub const resourcesDir = internal_os.resourcesDir; + +pub const App = struct { + /// Because we only expect the embedding API to be used in embedded + /// environments, the options are extern so that we can expose it + /// directly to a C callconv and not pay for any translation costs. + /// + /// C type: ghostty_runtime_config_s + pub const Options = extern struct { + /// These are just aliases to make the function signatures below + /// more obvious what values will be sent. + const AppUD = ?*anyopaque; + const SurfaceUD = ?*anyopaque; + + /// Userdata that is passed to all the callbacks. + userdata: AppUD = null, + + /// True if the selection clipboard is supported. + supports_selection_clipboard: bool = false, + + /// Callback called to wakeup the event loop. This should trigger + /// 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) bool, + + /// Read the clipboard value. Returns true if the clipboard request + /// was started and complete_clipboard_request may be called with the + /// given state pointer. Returns false if the clipboard request couldn't + /// be started (such as when no text is available for a paste request). + read_clipboard: *const fn (SurfaceUD, c_int, *apprt.ClipboardRequest) callconv(.c) bool, + + /// This may be called after a read clipboard call to request + /// confirmation that the clipboard value is safe to read. The embedder + /// must call complete_clipboard_request with the given request. + confirm_read_clipboard: *const fn ( + SurfaceUD, + [*:0]const u8, + *apprt.ClipboardRequest, + apprt.ClipboardRequestType, + ) callconv(.c) void, + + /// Write the clipboard value. + write_clipboard: *const fn ( + SurfaceUD, + c_int, + [*]const CAPI.ClipboardContent, + usize, + bool, + ) callconv(.c) void, + + /// Close the current surface given by this function. + close_surface: ?*const fn (SurfaceUD, bool) callconv(.c) void = null, + }; + + /// This is the key event sent for ghostty_surface_key and + /// ghostty_app_key. + pub const KeyEvent = struct { + action: input.Action, + mods: input.Mods, + consumed_mods: input.Mods, + keycode: u32, + text: ?[:0]const u8, + unshifted_codepoint: u32, + composing: bool, + + /// Convert a libghostty key event into a core key event. + fn core(self: KeyEvent) ?input.KeyEvent { + const text: []const u8 = if (self.text) |v| v else ""; + const unshifted_codepoint: u21 = std.math.cast( + u21, + self.unshifted_codepoint, + ) orelse 0; + + // We want to get the physical unmapped key to process keybinds. + const physical_key = keycode: for (input.keycodes.entries) |entry| { + if (entry.native == self.keycode) break :keycode entry.key; + } else .unidentified; + + // Build our final key event + return .{ + .action = self.action, + .key = physical_key, + .mods = self.mods, + .consumed_mods = self.consumed_mods, + .composing = self.composing, + .utf8 = text, + .unshifted_codepoint = unshifted_codepoint, + }; + } + }; + + core_app: *CoreApp, + opts: Options, + keymap: input.Keymap, + + /// The configuration for the app. This is owned by this structure. + config: Config, + + pub fn init( + self: *App, + core_app: *CoreApp, + config: *const Config, + opts: Options, + ) !void { + // We have to clone the config. + const alloc = core_app.alloc; + var config_clone = try config.clone(alloc); + errdefer config_clone.deinit(); + + var keymap = try input.Keymap.init(); + errdefer keymap.deinit(); + + self.* = .{ + .core_app = core_app, + .config = config_clone, + .opts = opts, + .keymap = keymap, + }; + } + + pub fn terminate(self: *App) void { + self.keymap.deinit(); + self.config.deinit(); + } + + /// Returns true if there are any global keybinds in the configuration. + pub fn hasGlobalKeybinds(self: *const App) bool { + var it = self.config.keybind.set.bindings.iterator(); + while (it.next()) |entry| { + switch (entry.value_ptr.*) { + .leader => {}, + inline .leaf, .leaf_chained => |leaf| if (leaf.flags.global) return true, + } + } + + return false; + } + + /// The target of a key event. This is used to determine some subtly + /// different behavior between app and surface key events. + pub const KeyTarget = union(enum) { + app, + surface: *Surface, + }; + + /// See CoreApp.focusEvent + pub fn focusEvent(self: *App, focused: bool) void { + self.core_app.focusEvent(focused); + } + + /// See CoreApp.keyEvent. + pub fn keyEvent( + self: *App, + target: KeyTarget, + event: KeyEvent, + ) !bool { + // Convert our C key event into a Zig one. + const input_event: input.KeyEvent = event.core() orelse + return false; + + // Invoke the core Ghostty logic to handle this input. + const effect: CoreSurface.InputEffect = switch (target) { + .app => if (self.core_app.keyEvent( + self, + input_event, + )) .consumed else .ignored, + + .surface => |surface| try surface.core_surface.keyCallback( + input_event, + ), + }; + + return switch (effect) { + .closed => true, + .ignored => false, + .consumed => true, + }; + } + + /// This should be called whenever the keyboard layout was changed. + pub fn reloadKeymap(self: *App) !void { + // Reload the keymap + try self.keymap.reload(); + } + + /// Loads the keyboard layout. + /// + /// Kind of expensive so this should be avoided if possible. When I say + /// "kind of expensive" I mean that its not something you probably want + /// to run on every keypress. + pub fn keyboardLayout(self: *const App) input.KeyboardLayout { + // We only support keyboard layout detection on macOS. + if (comptime builtin.os.tag != .macos) return .unknown; + + // Any layout larger than this is not something we can handle. + var buf: [256]u8 = undefined; + const id = self.keymap.sourceId(&buf) catch |err| { + comptime assert(@TypeOf(err) == error{OutOfMemory}); + return .unknown; + }; + + return input.KeyboardLayout.mapAppleId(id) orelse .unknown; + } + + pub fn wakeup(self: *const App) void { + self.opts.wakeup(self.opts.userdata); + } + + pub fn wait(self: *const App) !void { + _ = self; + } + + /// Create a new surface for the app. + fn newSurface(self: *App, opts: Surface.Options) !*Surface { + // Grab a surface allocation because we're going to need it. + var surface = try self.core_app.alloc.create(Surface); + errdefer self.core_app.alloc.destroy(surface); + + // Create the surface + try surface.init(self, opts); + errdefer surface.deinit(); + + return surface; + } + + /// Close the given surface. + pub fn closeSurface(self: *App, surface: *Surface) void { + surface.deinit(); + self.core_app.alloc.destroy(surface); + } + + pub fn redrawInspector(self: *App, surface: *Surface) void { + _ = self; + surface.queueInspectorRender(); + } + + /// Perform a given action. Returns `true` if the action was able to be + /// performed, `false` otherwise. + pub fn performAction( + self: *App, + target: apprt.Target, + comptime action: apprt.Action.Key, + value: apprt.Action.Value(action), + ) !bool { + // Special case certain actions before they are sent to the + // embedded apprt. + self.performPreAction(target, action, value); + + log.debug("dispatching action target={t} action={} value={any}", .{ + target, + action, + value, + }); + return self.opts.action( + self, + target.cval(), + @unionInit(apprt.Action, @tagName(action), value).cval(), + ); + } + + fn performPreAction( + self: *App, + target: apprt.Target, + comptime action: apprt.Action.Key, + value: apprt.Action.Value(action), + ) void { + // Special case certain actions before they are sent to the embedder + switch (action) { + .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; + }, + }, + + .config_change => switch (target) { + .surface => {}, + + // For app updates, we update our core config. We need to + // clone it because the caller owns the param. + .app => if (value.config.clone(self.core_app.alloc)) |config| { + self.config.deinit(); + self.config = config; + } else |err| { + log.err("error updating app config err={}", .{err}); + }, + }, + + else => {}, + } + } + + /// Send the given IPC to a running Ghostty. Returns `true` if the action was + /// able to be performed, `false` otherwise. + /// + /// Note that this is a static function. Since this is called from a CLI app (or + /// some other process that is not Ghostty) there is no full-featured apprt App + /// to use. + pub fn performIpc( + _: Allocator, + _: apprt.ipc.Target, + comptime action: apprt.ipc.Action.Key, + _: apprt.ipc.Action.Value(action), + ) (Allocator.Error || std.posix.WriteError || apprt.ipc.Errors)!bool { + switch (action) { + .new_window => return false, + } + } +}; + +/// Platform-specific configuration for libghostty. +pub const Platform = union(PlatformTag) { + macos: MacOS, + ios: IOS, + + // If our build target for libghostty is not darwin then we do + // not include macos support at all. + pub const MacOS = if (builtin.target.os.tag.isDarwin()) struct { + /// The view to render the surface on. + nsview: objc.Object, + } else void; + + pub const IOS = if (builtin.target.os.tag.isDarwin()) struct { + /// The view to render the surface on. + uiview: objc.Object, + } else void; + + // The C ABI compatible version of this union. The tag is expected + // to be stored elsewhere. + pub const C = extern union { + macos: extern struct { + nsview: ?*anyopaque, + }, + + ios: extern struct { + uiview: ?*anyopaque, + }, + }; + + /// Initialize a Platform a tag and configuration from the C ABI. + pub fn init(tag_int: c_int, c_platform: C) !Platform { + const tag = try std.meta.intToEnum(PlatformTag, tag_int); + return switch (tag) { + .macos => if (MacOS != void) macos: { + const config = c_platform.macos; + const nsview = objc.Object.fromId(config.nsview orelse + break :macos error.NSViewMustBeSet); + break :macos .{ .macos = .{ .nsview = nsview } }; + } else error.UnsupportedPlatform, + + .ios => if (IOS != void) ios: { + const config = c_platform.ios; + const uiview = objc.Object.fromId(config.uiview orelse + break :ios error.UIViewMustBeSet); + break :ios .{ .ios = .{ .uiview = uiview } }; + } else error.UnsupportedPlatform, + }; + } +}; + +pub const PlatformTag = enum(c_int) { + // "0" is reserved for invalid so we can detect unset values + // from the C API. + + macos = 1, + ios = 2, +}; + +pub const EnvVar = extern struct { + /// The name of the environment variable. + key: [*:0]const u8, + + /// The value of the environment variable. + value: [*:0]const u8, +}; + +pub const Surface = struct { + app: *App, + platform: Platform, + userdata: ?*anyopaque = null, + core_surface: CoreSurface, + content_scale: apprt.ContentScale, + size: apprt.SurfaceSize, + cursor_pos: apprt.CursorPos, + inspector: ?*Inspector = null, + + /// The current title of the surface. The embedded apprt saves this so + /// that getTitle works without the implementer needing to save it. + title: ?[:0]const u8 = null, + + /// Surface initialization options. + pub const Options = extern struct { + /// The platform that this surface is being initialized for and + /// the associated platform-specific configuration. + platform_tag: c_int = 0, + platform: Platform.C = undefined, + + /// Userdata passed to some of the callbacks. + userdata: ?*anyopaque = null, + + /// The scale factor of the screen. + scale_factor: f64 = 1, + + /// The font size to inherit. If 0, default font size will be used. + font_size: f32 = 0, + + /// The working directory to load into. + working_directory: ?[*:0]const u8 = null, + + /// The command to run in the new surface. If this is set then + /// the "wait-after-command" option is also automatically set to true, + /// since this is used for scripting. + /// + /// This command always run in a shell (e.g. via `/bin/sh -c`), + /// despite Ghostty allowing directly executed commands via config. + /// This is a legacy thing and we should probably change it in the + /// future once we have a concrete use case. + command: ?[*:0]const u8 = null, + + /// Extra environment variables to set for the surface. + env_vars: ?[*]EnvVar = null, + env_var_count: usize = 0, + + /// Input to send to the command after it is started. + initial_input: ?[*:0]const u8 = null, + + /// Wait after the command exits + wait_after_command: bool = false, + + /// Context for the new surface + context: apprt.surface.NewSurfaceContext = .window, + }; + + pub fn init(self: *Surface, app: *App, opts: Options) !void { + self.* = .{ + .app = app, + .platform = try .init(opts.platform_tag, opts.platform), + .userdata = opts.userdata, + .core_surface = undefined, + .content_scale = .{ + .x = @floatCast(opts.scale_factor), + .y = @floatCast(opts.scale_factor), + }, + .size = .{ .width = 800, .height = 600 }, + .cursor_pos = .{ .x = -1, .y = -1 }, + }; + + // Add ourselves to the list of surfaces on the app. + try app.core_app.addSurface(self); + errdefer app.core_app.deleteSurface(self); + + // Shallow copy the config so that we can modify it. + var config = try apprt.surface.newConfig(app.core_app, &app.config, opts.context); + defer config.deinit(); + + // If we have a working directory from the options then we set it. + if (opts.working_directory) |c_wd| { + const wd = std.mem.sliceTo(c_wd, 0); + if (wd.len > 0) wd: { + var dir = std.fs.openDirAbsolute(wd, .{}) catch |err| { + log.warn( + "error opening requested working directory dir={s} err={}", + .{ wd, err }, + ); + break :wd; + }; + defer dir.close(); + + const stat = dir.stat() catch |err| { + log.warn( + "failed to stat requested working directory dir={s} err={}", + .{ wd, err }, + ); + break :wd; + }; + + if (stat.kind != .directory) { + log.warn( + "requested working directory is not a directory dir={s}", + .{wd}, + ); + break :wd; + } + + var wd_val: configpkg.WorkingDirectory = .{ .path = wd }; + if (wd_val.finalize(config.arenaAlloc())) |_| { + config.@"working-directory" = wd_val; + } else |err| { + log.warn( + "error finalizing working directory config dir={s} err={}", + .{ wd_val.path, err }, + ); + } + } + } + + // If we have a command from the options then we set it. + if (opts.command) |c_command| { + const cmd = std.mem.sliceTo(c_command, 0); + if (cmd.len > 0) { + config.command = .{ .shell = cmd }; + config.@"wait-after-command" = true; + } + } + + // Apply any environment variables that were requested. + if (opts.env_var_count > 0) { + const alloc = config.arenaAlloc(); + for (opts.env_vars.?[0..opts.env_var_count]) |env_var| { + const key = std.mem.sliceTo(env_var.key, 0); + const value = std.mem.sliceTo(env_var.value, 0); + try config.env.map.put( + alloc, + try alloc.dupeZ(u8, key), + try alloc.dupeZ(u8, value), + ); + } + } + + // If we have an initial input then we set it. + if (opts.initial_input) |c_input| { + const alloc = config.arenaAlloc(); + + // We need to escape the string because the "raw" field + // expects a Zig string. + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + try std.zig.stringEscape( + std.mem.sliceTo(c_input, 0), + &buf.writer, + ); + + config.input.list.clearRetainingCapacity(); + try config.input.list.append( + alloc, + .{ .raw = try buf.toOwnedSliceSentinel(0) }, + ); + } + + // Wait after command + if (opts.wait_after_command) { + config.@"wait-after-command" = true; + } + + // Initialize our surface right away. We're given a view that is + // ready to use. + try self.core_surface.init( + app.core_app.alloc, + &config, + app.core_app, + app, + self, + ); + errdefer self.core_surface.deinit(); + + // If our options requested a specific font-size, set that. + if (opts.font_size != 0) { + var font_size = self.core_surface.font_size; + font_size.points = opts.font_size; + try self.core_surface.setFontSize(font_size); + } + } + + pub fn deinit(self: *Surface) void { + // Shut down our inspector + self.freeInspector(); + + // Free our title + if (self.title) |v| self.app.core_app.alloc.free(v); + + // Remove ourselves from the list of known surfaces in the app. + self.app.core_app.deleteSurface(self); + + // Clean up our core surface so that all the rendering and IO stop. + self.core_surface.deinit(); + } + + /// Initialize the inspector instance. A surface can only have one + /// inspector at any given time, so this will return the previous inspector + /// if it was already initialized. + pub fn initInspector(self: *Surface) !*Inspector { + if (self.inspector) |v| return v; + + const alloc = self.app.core_app.alloc; + const inspector = try alloc.create(Inspector); + errdefer alloc.destroy(inspector); + inspector.* = try .init(self); + self.inspector = inspector; + return inspector; + } + + pub fn freeInspector(self: *Surface) void { + if (self.inspector) |v| { + v.deinit(); + self.app.core_app.alloc.destroy(v); + self.inspector = null; + } + } + + pub fn core(self: *Surface) *CoreSurface { + return &self.core_surface; + } + + pub fn rtApp(self: *const Surface) *App { + return self.app; + } + + pub fn close(self: *const Surface, process_alive: bool) void { + const func = self.app.opts.close_surface orelse { + log.info("runtime embedder does not support closing a surface", .{}); + return; + }; + + func(self.userdata, process_alive); + } + + pub fn getContentScale(self: *const Surface) !apprt.ContentScale { + return self.content_scale; + } + + pub fn getSize(self: *const Surface) !apprt.SurfaceSize { + return self.size; + } + + pub fn getTitle(self: *Surface) ?[:0]const u8 { + return self.title; + } + + pub fn supportsClipboard( + self: *const Surface, + clipboard_type: apprt.Clipboard, + ) bool { + return switch (clipboard_type) { + .standard => true, + .selection, .primary => self.app.opts.supports_selection_clipboard, + }; + } + + pub fn clipboardRequest( + self: *Surface, + clipboard_type: apprt.Clipboard, + state: apprt.ClipboardRequest, + ) !bool { + // We need to allocate to get a pointer to store our clipboard request + // so that it is stable until the read_clipboard callback and call + // complete_clipboard_request. This sucks but clipboard requests aren't + // high throughput so it's probably fine. + const alloc = self.app.core_app.alloc; + const state_ptr = try alloc.create(apprt.ClipboardRequest); + errdefer alloc.destroy(state_ptr); + state_ptr.* = state; + + const started = self.app.opts.read_clipboard( + self.userdata, + @intCast(@intFromEnum(clipboard_type)), + state_ptr, + ); + if (!started) { + alloc.destroy(state_ptr); + return false; + } + + return true; + } + + fn completeClipboardRequest( + self: *Surface, + str: [:0]const u8, + state: *apprt.ClipboardRequest, + confirmed: bool, + ) void { + const alloc = self.app.core_app.alloc; + + // Attempt to complete the request, but we may request + // confirmation. + self.core_surface.completeClipboardRequest( + state.*, + str, + confirmed, + ) catch |err| switch (err) { + error.UnsafePaste, + error.UnauthorizedPaste, + => { + self.app.opts.confirm_read_clipboard( + self.userdata, + str.ptr, + state, + state.*, + ); + + return; + }, + + else => log.err("error completing clipboard request err={}", .{err}), + }; + + // We don't defer this because the clipboard confirmation route + // preserves the clipboard request. + alloc.destroy(state); + } + + pub fn setClipboard( + self: *const Surface, + clipboard_type: apprt.Clipboard, + contents: []const apprt.ClipboardContent, + confirm: bool, + ) !void { + const alloc = self.app.core_app.alloc; + const array = try alloc.alloc(CAPI.ClipboardContent, contents.len); + defer alloc.free(array); + for (contents, 0..) |content, i| { + array[i] = .{ + .mime = content.mime, + .data = content.data, + }; + } + + self.app.opts.write_clipboard( + self.userdata, + @intCast(@intFromEnum(clipboard_type)), + array.ptr, + array.len, + confirm, + ); + } + + pub fn getCursorPos(self: *const Surface) !apprt.CursorPos { + return self.cursor_pos; + } + + pub fn refresh(self: *Surface) void { + self.core_surface.refreshCallback() catch |err| { + log.err("error in refresh callback err={}", .{err}); + return; + }; + } + + pub fn draw(self: *Surface) void { + self.core_surface.draw() catch |err| { + log.err("error in draw err={}", .{err}); + return; + }; + } + + pub fn updateContentScale(self: *Surface, x: f64, y: f64) void { + // We are an embedded API so the caller can send us all sorts of + // garbage. We want to make sure that the float values are valid + // and we don't want to support fractional scaling below 1. + const x_scaled = @max(1, if (std.math.isNan(x)) 1 else x); + const y_scaled = @max(1, if (std.math.isNan(y)) 1 else y); + + self.content_scale = .{ + .x = @floatCast(x_scaled), + .y = @floatCast(y_scaled), + }; + + self.core_surface.contentScaleCallback(self.content_scale) catch |err| { + log.err("error in content scale callback err={}", .{err}); + return; + }; + } + + pub fn updateSize(self: *Surface, width: u32, height: u32) void { + // Runtimes sometimes generate superfluous resize events even + // if the size did not actually change (SwiftUI). We check + // that the size actually changed from what we last recorded + // since resizes are expensive. + if (self.size.width == width and self.size.height == height) return; + + self.size = .{ + .width = width, + .height = height, + }; + + // Call the primary callback. + self.core_surface.sizeCallback(self.size) catch |err| { + log.err("error in size callback err={}", .{err}); + return; + }; + } + + pub fn colorSchemeCallback(self: *Surface, scheme: apprt.ColorScheme) void { + self.core_surface.colorSchemeCallback(scheme) catch |err| { + log.err("error setting color scheme err={}", .{err}); + return; + }; + } + + pub fn mouseButtonCallback( + self: *Surface, + action: input.MouseButtonState, + button: input.MouseButton, + mods: input.Mods, + ) bool { + return self.core_surface.mouseButtonCallback(action, button, mods) catch |err| { + log.err("error in mouse button callback err={}", .{err}); + return false; + }; + } + + pub fn mousePressureCallback( + self: *Surface, + stage: input.MousePressureStage, + pressure: f64, + ) void { + self.core_surface.mousePressureCallback(stage, pressure) catch |err| { + log.err("error in mouse pressure callback err={}", .{err}); + return; + }; + } + + pub fn scrollCallback( + self: *Surface, + xoff: f64, + yoff: f64, + mods: input.ScrollMods, + ) void { + self.core_surface.scrollCallback(xoff, yoff, mods) catch |err| { + log.err("error in scroll callback err={}", .{err}); + return; + }; + } + + pub fn cursorPosCallback( + self: *Surface, + x: f64, + y: f64, + mods: input.Mods, + ) void { + // Convert our unscaled x/y to scaled. + const pos = self.cursorPosToPixels(.{ + .x = @floatCast(x), + .y = @floatCast(y), + }) catch |err| { + log.err( + "error converting cursor pos to scaled pixels in cursor pos callback err={}", + .{err}, + ); + return; + }; + + // There are cases where the platform reports a mouse motion event + // without the cursor actually moving. For example, on macOS, updating + // the window title can trigger a phantom mouse-move event at the same + // coordinates. This can cause the mouse to incorrectly unhide when + // mouse-hide-while-typing is enabled (commonly seen with TUI apps + // like Zellij that frequently update the title). To prevent incorrect + // behavior, we only continue with callback logic if the cursor has + // actually moved. + if (@abs(self.cursor_pos.x - pos.x) < 1 and + @abs(self.cursor_pos.y - pos.y) < 1) return; + + self.cursor_pos = pos; + + self.core_surface.cursorPosCallback(self.cursor_pos, mods) catch |err| { + log.err("error in cursor pos callback err={}", .{err}); + return; + }; + } + + pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8) void { + _ = self.core_surface.preeditCallback(preedit_) catch |err| { + log.err("error in preedit callback err={}", .{err}); + return; + }; + } + + pub fn textCallback(self: *Surface, text: []const u8) void { + _ = self.core_surface.textCallback(text) catch |err| { + log.err("error in key callback err={}", .{err}); + return; + }; + } + + pub fn focusCallback(self: *Surface, focused: bool) void { + self.core_surface.focusCallback(focused) catch |err| { + log.err("error in focus callback err={}", .{err}); + return; + }; + } + + pub fn occlusionCallback(self: *Surface, visible: bool) void { + self.core_surface.occlusionCallback(visible) catch |err| { + log.err("error in occlusion callback err={}", .{err}); + return; + }; + } + + 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; + }; + } + + pub fn newSurfaceOptions(self: *const Surface, context: apprt.surface.NewSurfaceContext) 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; + }; + + const working_directory: ?[*:0]const u8 = wd: { + if (!apprt.surface.shouldInheritWorkingDirectory(context, &self.app.config)) break :wd null; + const cwd = self.core_surface.pwd(self.app.core_app.alloc) catch null orelse break :wd null; + defer self.app.core_app.alloc.free(cwd); + break :wd self.app.core_app.alloc.dupeZ(u8, cwd) catch null; + }; + + return .{ + .font_size = font_size, + .working_directory = working_directory, + .context = context, + }; + } + + pub fn defaultTermioEnv(self: *const Surface) !std.process.EnvMap { + const alloc = self.app.core_app.alloc; + var env = try internal_os.getEnvMap(alloc); + errdefer env.deinit(); + + if (comptime builtin.target.os.tag.isDarwin()) { + if (env.get("__XCODE_BUILT_PRODUCTS_DIR_PATHS") != null) { + env.remove("__XCODE_BUILT_PRODUCTS_DIR_PATHS"); + env.remove("__XPC_DYLD_LIBRARY_PATH"); + env.remove("DYLD_FRAMEWORK_PATH"); + env.remove("DYLD_INSERT_LIBRARIES"); + env.remove("DYLD_LIBRARY_PATH"); + env.remove("LD_LIBRARY_PATH"); + env.remove("SECURITYSESSIONID"); + env.remove("XPC_SERVICE_NAME"); + } + + // Remove this so that running `ghostty` within Ghostty works. + env.remove("GHOSTTY_MAC_LAUNCH_SOURCE"); + + // If we were launched from the desktop then we want to + // remove the LANGUAGE env var so that we don't inherit + // our translation settings for Ghostty. If we aren't from + // the desktop then we didn't set our LANGUAGE var so we + // don't need to remove it. + if (internal_os.launchedFromDesktop()) env.remove("LANGUAGE"); + } + + return env; + } + + /// The cursor position from the host directly is in screen coordinates but + /// all our interface works in pixels. + fn cursorPosToPixels(self: *const Surface, pos: apprt.CursorPos) !apprt.CursorPos { + const scale = try self.getContentScale(); + return .{ .x = pos.x * scale.x, .y = pos.y * scale.y }; + } +}; + +/// Inspector is the state required for the terminal inspector. A terminal +/// inspector is 1:1 with a Surface. +pub const Inspector = struct { + const cimgui = @import("dcimgui"); + + surface: *Surface, + ig_ctx: *cimgui.c.ImGuiContext, + backend: ?Backend = null, + content_scale: f64 = 1, + + /// Our previous instant used to calculate delta time for animations. + instant: ?std.time.Instant = null, + + const Backend = enum { + metal, + + pub fn deinit(self: Backend) void { + switch (self) { + .metal => if (builtin.target.os.tag.isDarwin()) cimgui.ImGui_ImplMetal_Shutdown(), + } + } + }; + + pub fn init(surface: *Surface) !Inspector { + const ig_ctx = cimgui.c.ImGui_CreateContext(null) orelse return error.OutOfMemory; + errdefer cimgui.c.ImGui_DestroyContext(ig_ctx); + cimgui.c.ImGui_SetCurrentContext(ig_ctx); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); + io.BackendPlatformName = "ghostty_embedded"; + + // Setup our core inspector + CoreInspector.setup(); + surface.core_surface.activateInspector() catch |err| { + log.err("failed to activate inspector err={}", .{err}); + }; + + return .{ + .surface = surface, + .ig_ctx = ig_ctx, + }; + } + + pub fn deinit(self: *Inspector) void { + self.surface.core_surface.deactivateInspector(); + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); + if (self.backend) |v| v.deinit(); + cimgui.c.ImGui_DestroyContext(self.ig_ctx); + } + + /// Queue a render for the next frame. + pub fn queueRender(self: *Inspector) void { + self.surface.queueInspectorRender(); + } + + /// Initialize the inspector for a metal backend. + pub fn initMetal(self: *Inspector, device: objc.Object) bool { + defer device.msgSend(void, objc.sel("release"), .{}); + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); + + if (self.backend) |v| { + v.deinit(); + self.backend = null; + } + + if (!cimgui.ImGui_ImplMetal_Init(device.value)) { + log.warn("failed to initialize metal backend", .{}); + return false; + } + self.backend = .metal; + + log.debug("initialized metal backend", .{}); + return true; + } + + pub fn renderMetal( + self: *Inspector, + command_buffer: objc.Object, + desc: objc.Object, + ) !void { + defer { + command_buffer.msgSend(void, objc.sel("release"), .{}); + desc.msgSend(void, objc.sel("release"), .{}); + } + assert(self.backend == .metal); + //log.debug("render", .{}); + + // Setup our imgui frame. We need to render multiple frames to ensure + // ImGui completes all its state processing. I don't know how to fix + // this. + for (0..2) |_| { + cimgui.ImGui_ImplMetal_NewFrame(desc.value); + try self.newFrame(); + cimgui.c.ImGui_NewFrame(); + + // Build our UI + render: { + const surface = &self.surface.core_surface; + const inspector = surface.inspector orelse break :render; + inspector.render(surface); + } + + // Render + cimgui.c.ImGui_Render(); + } + + // MTLRenderCommandEncoder + const encoder = command_buffer.msgSend( + objc.Object, + objc.sel("renderCommandEncoderWithDescriptor:"), + .{desc.value}, + ); + defer encoder.msgSend(void, objc.sel("endEncoding"), .{}); + cimgui.ImGui_ImplMetal_RenderDrawData( + cimgui.c.ImGui_GetDrawData(), + command_buffer.value, + encoder.value, + ); + } + + pub fn updateContentScale(self: *Inspector, x: f64, y: f64) void { + _ = y; + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); + + // Cache our scale because we use it for cursor position calculations. + self.content_scale = x; + + // Setup a new style and scale it appropriately. We must use the + // ImGuiStyle constructor to get proper default values (e.g., + // CurveTessellationTol) rather than zero-initialized values. + var style: cimgui.c.ImGuiStyle = undefined; + cimgui.ext.ImGuiStyle_ImGuiStyle(&style); + cimgui.c.ImGuiStyle_ScaleAllSizes(&style, @floatCast(x)); + const active_style = cimgui.c.ImGui_GetStyle(); + active_style.* = style; + } + + pub fn updateSize(self: *Inspector, width: u32, height: u32) void { + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); + io.DisplaySize = .{ .x = @floatFromInt(width), .y = @floatFromInt(height) }; + } + + pub fn mouseButtonCallback( + self: *Inspector, + action: input.MouseButtonState, + button: input.MouseButton, + mods: input.Mods, + ) void { + _ = mods; + + self.queueRender(); + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); + + const imgui_button = switch (button) { + .left => cimgui.c.ImGuiMouseButton_Left, + .middle => cimgui.c.ImGuiMouseButton_Middle, + .right => cimgui.c.ImGuiMouseButton_Right, + else => return, // unsupported + }; + + cimgui.c.ImGuiIO_AddMouseButtonEvent(io, imgui_button, action == .press); + } + + pub fn scrollCallback( + self: *Inspector, + xoff: f64, + yoff: f64, + mods: input.ScrollMods, + ) void { + self.queueRender(); + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); + + // For precision scrolling (trackpads), the values are in pixels which + // scroll way too fast. Scale them down to approximate discrete wheel + // notches. imgui expects 1.0 to scroll ~5 lines of text. + const scale: f64 = if (mods.precision) 0.1 else 1.0; + cimgui.c.ImGuiIO_AddMouseWheelEvent( + io, + @floatCast(xoff * scale), + @floatCast(yoff * scale), + ); + } + + pub fn cursorPosCallback(self: *Inspector, x: f64, y: f64) void { + self.queueRender(); + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); + cimgui.c.ImGuiIO_AddMousePosEvent( + io, + @floatCast(x * self.content_scale), + @floatCast(y * self.content_scale), + ); + } + + pub fn focusCallback(self: *Inspector, focused: bool) void { + self.queueRender(); + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); + cimgui.c.ImGuiIO_AddFocusEvent(io, focused); + } + + pub fn textCallback(self: *Inspector, text: [:0]const u8) void { + self.queueRender(); + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); + cimgui.c.ImGuiIO_AddInputCharactersUTF8(io, text.ptr); + } + + pub fn keyCallback( + self: *Inspector, + action: input.Action, + key: input.Key, + mods: input.Mods, + ) !void { + self.queueRender(); + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); + + // Update all our modifiers + cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftShift, mods.shift); + cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftCtrl, mods.ctrl); + cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftAlt, mods.alt); + cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftSuper, mods.super); + + // Send our keypress + if (key.imguiKey()) |imgui_key| { + cimgui.c.ImGuiIO_AddKeyEvent( + io, + imgui_key, + action == .press or action == .repeat, + ); + } + } + + fn newFrame(self: *Inspector) !void { + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); + + // Determine our delta time + const now = try std.time.Instant.now(); + io.DeltaTime = if (self.instant) |prev| delta: { + const since_ns: f64 = @floatFromInt(now.since(prev)); + const ns_per_s: f64 = @floatFromInt(std.time.ns_per_s); + const since_s: f32 = @floatCast(since_ns / ns_per_s); + break :delta @max(0.00001, since_s); + } else (1.0 / 60.0); + self.instant = now; + } +}; + +// C API +pub const CAPI = struct { + const global = &@import("../global.zig").state; + + /// This is the same as Surface.KeyEvent but this is the raw C API version. + const KeyEvent = extern struct { + action: input.Action, + mods: c_int, + consumed_mods: c_int, + keycode: u32, + text: ?[*:0]const u8, + unshifted_codepoint: u32, + composing: bool, + + /// Convert to Zig key event. + fn keyEvent(self: KeyEvent) App.KeyEvent { + return .{ + .action = self.action, + .mods = @bitCast(@as( + input.Mods.Backing, + @truncate(@as(c_uint, @bitCast(self.mods))), + )), + .consumed_mods = @bitCast(@as( + input.Mods.Backing, + @truncate(@as(c_uint, @bitCast(self.consumed_mods))), + )), + .keycode = self.keycode, + .text = if (self.text) |ptr| std.mem.sliceTo(ptr, 0) else null, + .unshifted_codepoint = self.unshifted_codepoint, + .composing = self.composing, + }; + } + }; + + const SurfaceSize = extern struct { + columns: u16, + rows: u16, + width_px: u32, + height_px: u32, + cell_width_px: u32, + cell_height_px: u32, + }; + + // ghostty_clipboard_content_s + const ClipboardContent = extern struct { + mime: [*:0]const u8, + data: [*:0]const u8, + }; + + // ghostty_text_s + const Text = extern struct { + tl_px_x: f64, + tl_px_y: f64, + offset_start: u32, + offset_len: u32, + text: ?[*:0]const u8, + text_len: usize, + + pub fn deinit(self: *Text) void { + if (self.text) |ptr| { + global.alloc.free(ptr[0..self.text_len :0]); + } + } + }; + + // ghostty_point_s + const Point = extern struct { + tag: Tag, + coord_tag: CoordTag, + x: u32, + y: u32, + + const Tag = enum(c_int) { + active = 0, + viewport = 1, + screen = 2, + history = 3, + }; + + const CoordTag = enum(c_int) { + exact = 0, + top_left = 1, + bottom_right = 2, + }; + + fn pin( + self: Point, + screen: *const terminal.Screen, + ) ?terminal.Pin { + // The core point tag. + const tag: terminal.point.Tag = switch (self.tag) { + inline else => |tag| @field( + terminal.point.Tag, + @tagName(tag), + ), + }; + + // Clamp our point to the screen bounds. + const clamped_x = @min(self.x, screen.pages.cols -| 1); + const clamped_y = @min(self.y, screen.pages.rows -| 1); + + return switch (self.coord_tag) { + // Exact coordinates require a specific pin. + .exact => exact: { + const pt_x = std.math.cast( + terminal.size.CellCountInt, + clamped_x, + ) orelse std.math.maxInt(terminal.size.CellCountInt); + + const pt: terminal.Point = switch (tag) { + inline else => |v| @unionInit( + terminal.Point, + @tagName(v), + .{ .x = pt_x, .y = clamped_y }, + ), + }; + + break :exact screen.pages.pin(pt) orelse null; + }, + + .top_left => screen.pages.getTopLeft(tag), + + .bottom_right => screen.pages.getBottomRight(tag), + }; + } + }; + + // ghostty_selection_s + const Selection = extern struct { + tl: Point, + br: Point, + rectangle: bool, + + fn core( + self: Selection, + screen: *const terminal.Screen, + ) ?terminal.Selection { + return .{ + .bounds = .{ .untracked = .{ + .start = self.tl.pin(screen) orelse return null, + .end = self.br.pin(screen) orelse return null, + } }, + .rectangle = self.rectangle, + }; + } + }; + + // Reference the conditional exports based on target platform + // so they're included in the C API. + comptime { + if (builtin.target.os.tag.isDarwin()) { + _ = Darwin; + } + } + + /// Create a new app. + export fn ghostty_app_new( + opts: *const apprt.runtime.App.Options, + config: *const Config, + ) ?*App { + return app_new_(opts, config) catch |err| { + log.err("error initializing app err={}", .{err}); + return null; + }; + } + + fn app_new_( + opts: *const apprt.runtime.App.Options, + config: *const Config, + ) !*App { + const core_app = try CoreApp.create(global.alloc); + errdefer core_app.destroy(); + + // Create our runtime app + var app = try global.alloc.create(App); + errdefer global.alloc.destroy(app); + try app.init(core_app, config, opts.*); + errdefer app.terminate(); + + return app; + } + + /// Tick the event loop. This should be called whenever the "wakeup" + /// callback is invoked for the runtime. + export fn ghostty_app_tick(v: *App) void { + v.core_app.tick(v) catch |err| { + log.err("error app tick err={}", .{err}); + }; + } + + /// Return the userdata associated with the app. + export fn ghostty_app_userdata(v: *App) ?*anyopaque { + return v.opts.userdata; + } + + export fn ghostty_app_free(v: *App) void { + const core_app = v.core_app; + v.terminate(); + global.alloc.destroy(v); + core_app.destroy(); + } + + /// Update the focused state of the app. + export fn ghostty_app_set_focus( + app: *App, + focused: bool, + ) void { + app.focusEvent(focused); + } + + /// Notify the app of a global keypress capture. This will return + /// true if the key was captured by the app, in which case the caller + /// should not process the key. + export fn ghostty_app_key( + app: *App, + event: KeyEvent, + ) bool { + return app.keyEvent(.app, event.keyEvent()) catch |err| { + log.warn("error processing key event err={}", .{err}); + return false; + }; + } + + /// Returns true if the given key event would trigger a binding + /// if it were sent to the surface right now. The "right now" + /// is important because things like trigger sequences are only + /// valid until the next key event. + export fn ghostty_app_key_is_binding( + app: *App, + event: KeyEvent, + ) bool { + const core_event = event.keyEvent().core() orelse { + log.warn("error processing key event", .{}); + return false; + }; + + return app.core_app.keyEventIsBinding(app, core_event); + } + + /// Notify the app that the keyboard was changed. This causes the + /// keyboard layout to be reloaded from the OS. + export fn ghostty_app_keyboard_changed(v: *App) void { + v.reloadKeymap() catch |err| { + log.err("error reloading keyboard map err={}", .{err}); + return; + }; + } + + /// Open the configuration. + export fn ghostty_app_open_config(v: *App) void { + _ = v.performAction(.app, .open_config, {}) catch |err| { + log.err("error reloading config err={}", .{err}); + return; + }; + } + + /// Update the configuration to the provided config. This will propagate + /// to all surfaces as well. + export fn ghostty_app_update_config( + v: *App, + config: *const Config, + ) void { + v.core_app.updateConfig(v, config) catch |err| { + log.err("error updating config err={}", .{err}); + return; + }; + } + + /// Returns true if the app needs to confirm quitting. + export fn ghostty_app_needs_confirm_quit(v: *App) bool { + return v.core_app.needsConfirmQuit(); + } + + /// Returns true if the app has global keybinds. + export fn ghostty_app_has_global_keybinds(v: *App) bool { + return v.hasGlobalKeybinds(); + } + + /// Update the color scheme of the app. + export fn ghostty_app_set_color_scheme(v: *App, scheme_raw: c_int) void { + const scheme = std.meta.intToEnum(apprt.ColorScheme, scheme_raw) catch { + log.warn( + "invalid color scheme to ghostty_surface_set_color_scheme value={}", + .{scheme_raw}, + ); + return; + }; + + v.core_app.colorSchemeEvent(v, scheme) catch |err| { + log.err("error setting color scheme err={}", .{err}); + return; + }; + } + + /// Returns initial surface options. + export fn ghostty_surface_config_new() apprt.Surface.Options { + return .{}; + } + + /// Create a new surface as part of an app. + export fn ghostty_surface_new( + app: *App, + opts: *const apprt.Surface.Options, + ) ?*Surface { + return surface_new_(app, opts) catch |err| { + log.err("error initializing surface err={}", .{err}); + return null; + }; + } + + fn surface_new_( + app: *App, + opts: *const apprt.Surface.Options, + ) !*Surface { + return try app.newSurface(opts.*); + } + + export fn ghostty_surface_free(ptr: *Surface) void { + 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, + source: apprt.surface.NewSurfaceContext, + ) Surface.Options { + return surface.newSurfaceOptions(source); + } + + /// Update the configuration to the provided config for only this surface. + export fn ghostty_surface_update_config( + surface: *Surface, + config: *const Config, + ) void { + surface.core_surface.updateConfig(config) catch |err| { + log.err("error updating config err={}", .{err}); + return; + }; + } + + /// Returns true if the surface needs to confirm quitting. + export fn ghostty_surface_needs_confirm_quit(surface: *Surface) bool { + return surface.core_surface.needsConfirmQuit(); + } + + /// Returns true if the surface process has exited. + export fn ghostty_surface_process_exited(surface: *Surface) bool { + return surface.core_surface.child_exited; + } + + /// Returns true if the surface has a selection. + export fn ghostty_surface_has_selection(surface: *Surface) bool { + return surface.core_surface.hasSelection(); + } + + /// Same as ghostty_surface_read_text but reads from the user selection, + /// if any. + export fn ghostty_surface_read_selection( + surface: *Surface, + result: *Text, + ) bool { + const core_surface = &surface.core_surface; + core_surface.renderer_state.mutex.lock(); + defer core_surface.renderer_state.mutex.unlock(); + + // If we don't have a selection, do nothing. + const core_sel = core_surface.io.terminal.screens.active.selection orelse return false; + + // Read the text from the selection. + return readTextLocked(surface, core_sel, result); + } + + /// Read some arbitrary text from the surface. + /// + /// This is an expensive operation so it shouldn't be called too + /// often. We recommend that callers cache the result and throttle + /// calls to this function. + export fn ghostty_surface_read_text( + surface: *Surface, + sel: Selection, + result: *Text, + ) bool { + surface.core_surface.renderer_state.mutex.lock(); + defer surface.core_surface.renderer_state.mutex.unlock(); + + const core_sel = sel.core( + surface.core_surface.renderer_state.terminal.screens.active, + ) orelse return false; + + return readTextLocked(surface, core_sel, result); + } + + fn readTextLocked( + surface: *Surface, + core_sel: terminal.Selection, + result: *Text, + ) bool { + const core_surface = &surface.core_surface; + + // Get our text directly from the core surface. + const text = core_surface.dumpTextLocked( + global.alloc, + core_sel, + ) catch |err| { + log.warn("error reading text err={}", .{err}); + return false; + }; + + const vp: CoreSurface.Text.Viewport = text.viewport orelse .{ + .tl_px_x = -1, + .tl_px_y = -1, + .offset_start = 0, + .offset_len = 0, + }; + + result.* = .{ + .tl_px_x = vp.tl_px_x, + .tl_px_y = vp.tl_px_y, + .offset_start = vp.offset_start, + .offset_len = vp.offset_len, + .text = text.text.ptr, + .text_len = text.text.len, + }; + + return true; + } + + export fn ghostty_surface_free_text(_: *Surface, ptr: *Text) void { + ptr.deinit(); + } + + /// Tell the surface that it needs to schedule a render + export fn ghostty_surface_refresh(surface: *Surface) void { + surface.refresh(); + } + + /// Tell the surface that it needs to schedule a render + /// call as soon as possible (NOW if possible). + export fn ghostty_surface_draw(surface: *Surface) void { + surface.draw(); + } + + /// Update the size of a surface. This will trigger resize notifications + /// to the pty and the renderer. + export fn ghostty_surface_set_size(surface: *Surface, w: u32, h: u32) void { + surface.updateSize(w, h); + } + + /// Return the size information a surface has. + export fn ghostty_surface_size(surface: *Surface) SurfaceSize { + const grid_size = surface.core_surface.size.grid(); + return .{ + .columns = grid_size.columns, + .rows = grid_size.rows, + .width_px = surface.core_surface.size.screen.width, + .height_px = surface.core_surface.size.screen.height, + .cell_width_px = surface.core_surface.size.cell.width, + .cell_height_px = surface.core_surface.size.cell.height, + }; + } + + /// Update the color scheme of the surface. + export fn ghostty_surface_set_color_scheme(surface: *Surface, scheme_raw: c_int) void { + const scheme = std.meta.intToEnum(apprt.ColorScheme, scheme_raw) catch { + log.warn( + "invalid color scheme to ghostty_surface_set_color_scheme value={}", + .{scheme_raw}, + ); + return; + }; + + surface.colorSchemeCallback(scheme); + } + + /// Update the content scale of the surface. + export fn ghostty_surface_set_content_scale(surface: *Surface, x: f64, y: f64) void { + surface.updateContentScale(x, y); + } + + /// Update the focused state of a surface. + export fn ghostty_surface_set_focus(surface: *Surface, focused: bool) void { + surface.focusCallback(focused); + } + + /// Update the occlusion state of a surface. + export fn ghostty_surface_set_occlusion(surface: *Surface, visible: bool) void { + surface.occlusionCallback(visible); + } + + /// Filter the mods if necessary. This handles settings such as + /// `macos-option-as-alt`. The filtered mods should be used for + /// key translation but should NOT be sent back via the `_key` + /// function -- the original mods should be used for that. + export fn ghostty_surface_key_translation_mods( + surface: *Surface, + mods_raw: c_int, + ) c_int { + const mods: input.Mods = @bitCast(@as( + input.Mods.Backing, + @truncate(@as(c_uint, @bitCast(mods_raw))), + )); + const result = mods.translation( + surface.core_surface.config.macos_option_as_alt orelse + surface.app.keyboardLayout().detectOptionAsAlt(), + ); + return @intCast(@as(input.Mods.Backing, @bitCast(result))); + } + + /// Send this for raw keypresses (i.e. the keyDown event on macOS). + /// This will handle the keymap translation and send the appropriate + /// key and char events. + export fn ghostty_surface_key( + surface: *Surface, + event: KeyEvent, + ) bool { + return surface.app.keyEvent( + .{ .surface = surface }, + event.keyEvent(), + ) catch |err| { + log.warn("error processing key event err={}", .{err}); + return false; + }; + } + + /// Returns true if the given key event would trigger a binding + /// if it were sent to the surface right now. The "right now" + /// is important because things like trigger sequences are only + /// valid until the next key event. + export fn ghostty_surface_key_is_binding( + surface: *Surface, + event: KeyEvent, + c_flags: ?*input.Binding.Flags.C, + ) bool { + const core_event = event.keyEvent().core() orelse { + log.warn("error processing key event", .{}); + return false; + }; + + const flags = surface.core_surface.keyEventIsBinding( + core_event, + ) orelse return false; + if (c_flags) |ptr| ptr.* = flags.cval(); + return true; + } + + /// Send raw text to the terminal. This is treated like a paste + /// so this isn't useful for sending escape sequences. For that, + /// individual key input should be used. + export fn ghostty_surface_text( + surface: *Surface, + ptr: [*]const u8, + len: usize, + ) void { + surface.textCallback(ptr[0..len]); + } + + /// Set the preedit text for the surface. This is used for IME + /// composition. If the length is 0, then the preedit text is cleared. + export fn ghostty_surface_preedit( + surface: *Surface, + ptr: [*]const u8, + len: usize, + ) void { + surface.preeditCallback(if (len == 0) null else ptr[0..len]); + } + + /// Returns true if the surface currently has mouse capturing + /// enabled. + export fn ghostty_surface_mouse_captured(surface: *Surface) bool { + return surface.core_surface.mouseCaptured(); + } + + /// Tell the surface that it needs to schedule a render + export fn ghostty_surface_mouse_button( + surface: *Surface, + action: input.MouseButtonState, + button: input.MouseButton, + mods: c_int, + ) bool { + return surface.mouseButtonCallback( + action, + button, + @bitCast(@as( + input.Mods.Backing, + @truncate(@as(c_uint, @bitCast(mods))), + )), + ); + } + + /// Update the mouse position within the view. + export fn ghostty_surface_mouse_pos( + surface: *Surface, + x: f64, + y: f64, + mods: c_int, + ) void { + surface.cursorPosCallback( + x, + y, + @bitCast(@as( + input.Mods.Backing, + @truncate(@as(c_uint, @bitCast(mods))), + )), + ); + } + + export fn ghostty_surface_mouse_scroll( + surface: *Surface, + x: f64, + y: f64, + scroll_mods: c_int, + ) void { + surface.scrollCallback( + x, + y, + @bitCast(@as(u8, @truncate(@as(c_uint, @bitCast(scroll_mods))))), + ); + } + + export fn ghostty_surface_mouse_pressure( + surface: *Surface, + stage_raw: u32, + pressure: f64, + ) void { + const stage = std.meta.intToEnum( + input.MousePressureStage, + stage_raw, + ) catch { + log.warn( + "invalid mouse pressure stage value={}", + .{stage_raw}, + ); + return; + }; + + surface.mousePressureCallback(stage, pressure); + } + + export fn ghostty_surface_ime_point( + surface: *Surface, + x: *f64, + y: *f64, + width: *f64, + height: *f64, + ) void { + const pos = surface.core_surface.imePoint(); + x.* = pos.x; + y.* = pos.y; + width.* = pos.width; + height.* = pos.height; + } + + /// Request that the surface become closed. This will go through the + /// normal trigger process that a close surface input binding would. + export fn ghostty_surface_request_close(ptr: *Surface) void { + ptr.core_surface.close(); + } + + /// Request that the surface split in the given direction. + export fn ghostty_surface_split(ptr: *Surface, direction: apprt.action.SplitDirection) void { + _ = ptr.app.performAction( + .{ .surface = &ptr.core_surface }, + .new_split, + direction, + ) catch |err| { + log.err("error creating new split err={}", .{err}); + return; + }; + } + + /// Focus on the next split (if any). + export fn ghostty_surface_split_focus( + ptr: *Surface, + direction: apprt.action.GotoSplit, + ) void { + _ = ptr.app.performAction( + .{ .surface = &ptr.core_surface }, + .goto_split, + direction, + ) catch |err| { + log.err("error creating new split err={}", .{err}); + return; + }; + } + + /// Resize the current split by moving the split divider in the given + /// direction. `direction` specifies which direction the split divider will + /// move relative to the focused split. `amount` is a fractional value + /// between 0 and 1 that specifies by how much the divider will move. + export fn ghostty_surface_split_resize( + ptr: *Surface, + direction: apprt.action.ResizeSplit.Direction, + amount: u16, + ) void { + _ = ptr.app.performAction( + .{ .surface = &ptr.core_surface }, + .resize_split, + .{ .direction = direction, .amount = amount }, + ) catch |err| { + log.err("error resizing split err={}", .{err}); + return; + }; + } + + /// Equalize the size of all splits in the current window. + export fn ghostty_surface_split_equalize(ptr: *Surface) void { + _ = ptr.app.performAction( + .{ .surface = &ptr.core_surface }, + .equalize_splits, + {}, + ) catch |err| { + log.err("error equalizing splits err={}", .{err}); + return; + }; + } + + /// Invoke an action on the surface. + export fn ghostty_surface_binding_action( + ptr: *Surface, + action_ptr: [*]const u8, + action_len: usize, + ) bool { + const action_str = action_ptr[0..action_len]; + const action = input.Binding.Action.parse(action_str) catch |err| { + log.err("error parsing binding action action={s} err={}", .{ action_str, err }); + return false; + }; + + return ptr.core_surface.performBindingAction(action) catch |err| { + log.err("error performing binding action action={f} err={}", .{ action, err }); + return false; + }; + } + + /// Complete a clipboard read request started via the read callback. + /// This can only be called once for a given request. Once it is called + /// with a request the request pointer will be invalidated. + export fn ghostty_surface_complete_clipboard_request( + ptr: *Surface, + str: [*:0]const u8, + state: *apprt.ClipboardRequest, + confirmed: bool, + ) void { + ptr.completeClipboardRequest( + std.mem.sliceTo(str, 0), + state, + confirmed, + ); + } + + export fn ghostty_surface_inspector(ptr: *Surface) ?*Inspector { + return ptr.initInspector() catch |err| { + log.err("error initializing inspector err={}", .{err}); + return null; + }; + } + + export fn ghostty_inspector_free(ptr: *Surface) void { + ptr.freeInspector(); + } + + export fn ghostty_inspector_set_size(ptr: *Inspector, w: u32, h: u32) void { + ptr.updateSize(w, h); + } + + export fn ghostty_inspector_set_content_scale(ptr: *Inspector, x: f64, y: f64) void { + ptr.updateContentScale(x, y); + } + + export fn ghostty_inspector_mouse_button( + ptr: *Inspector, + action: input.MouseButtonState, + button: input.MouseButton, + mods: c_int, + ) void { + ptr.mouseButtonCallback( + action, + button, + @bitCast(@as( + input.Mods.Backing, + @truncate(@as(c_uint, @bitCast(mods))), + )), + ); + } + + export fn ghostty_inspector_mouse_pos(ptr: *Inspector, x: f64, y: f64) void { + ptr.cursorPosCallback(x, y); + } + + export fn ghostty_inspector_mouse_scroll( + ptr: *Inspector, + x: f64, + y: f64, + scroll_mods: c_int, + ) void { + ptr.scrollCallback( + x, + y, + @bitCast(@as(u8, @truncate(@as(c_uint, @bitCast(scroll_mods))))), + ); + } + + export fn ghostty_inspector_key( + ptr: *Inspector, + action: input.Action, + key: input.Key, + c_mods: c_int, + ) void { + ptr.keyCallback( + action, + key, + @bitCast(@as( + input.Mods.Backing, + @truncate(@as(c_uint, @bitCast(c_mods))), + )), + ) catch |err| { + log.err("error processing key event err={}", .{err}); + return; + }; + } + + export fn ghostty_inspector_text( + ptr: *Inspector, + str: [*:0]const u8, + ) void { + ptr.textCallback(std.mem.sliceTo(str, 0)); + } + + export fn ghostty_inspector_set_focus(ptr: *Inspector, focused: bool) void { + ptr.focusCallback(focused); + } + + /// Sets the window background blur on macOS to the desired value. + /// I do this in Zig as an extern function because I don't know how to + /// call these functions in Swift. + /// + /// This uses an undocumented, non-public API because this is what + /// every terminal appears to use, including Terminal.app. + export fn ghostty_set_window_background_blur( + app: *App, + window: *anyopaque, + ) void { + // This is only supported on macOS + if (comptime builtin.target.os.tag != .macos) return; + + const config = &app.config; + + // Do nothing if we don't have background transparency enabled + if (config.@"background-opacity" >= 1.0) return; + + const nswindow = objc.Object.fromId(window); + _ = CGSSetWindowBackgroundBlurRadius( + CGSDefaultConnectionForThread(), + nswindow.msgSend(usize, objc.sel("windowNumber"), .{}), + @intCast(config.@"background-blur".cval()), + ); + } + + /// See ghostty_set_window_background_blur + extern "c" fn CGSSetWindowBackgroundBlurRadius(*anyopaque, usize, c_int) i32; + extern "c" fn CGSDefaultConnectionForThread() *anyopaque; + + // Darwin-only C APIs. + const Darwin = struct { + export fn ghostty_surface_set_display_id(ptr: *Surface, display_id: u32) void { + const surface = &ptr.core_surface; + _ = surface.renderer_thread.mailbox.push( + .{ .macos_display_id = display_id }, + .{ .forever = {} }, + ); + surface.renderer_thread.wakeup.notify() catch {}; + } + + /// This returns a CTFontRef that should be used for quicklook + /// highlighted text. This is always the primary font in use + /// regardless of the selected text. If coretext is not in use + /// then this will return nothing. + export fn ghostty_surface_quicklook_font(ptr: *Surface) ?*anyopaque { + // For non-CoreText we just return null. + if (comptime font.options.backend != .coretext) { + return null; + } + + // We'll need content scale so fail early if we can't get it. + const content_scale = ptr.getContentScale() catch return null; + + // Get the shared font grid. We acquire a read lock to + // read the font face. It should not be deferred since + // we're loading the primary face. + const grid = ptr.core_surface.renderer.font_grid; + grid.lock.lockShared(); + defer grid.lock.unlockShared(); + + const collection = &grid.resolver.collection; + const face = collection.getFace(.{}) catch return null; + + // We need to unscale the content scale. We apply the + // content scale to our font stack because we are rendering + // at 1x but callers of this should be using scaled or apply + // scale themselves. + const size: f32 = size: { + const num = face.font.copyAttribute(.size) orelse + break :size 12; + defer num.release(); + var v: f32 = 12; + _ = num.getValue(.float, &v); + break :size v; + }; + + const copy = face.font.copyWithAttributes( + size / content_scale.y, + null, + null, + ) catch return null; + + return copy; + } + + /// This returns the selected word for quicklook. This will populate + /// the buffer with the word under the cursor and the selection + /// info so that quicklook can be rendered. + /// + /// This does not modify the selection active on the surface (if any). + export fn ghostty_surface_quicklook_word( + ptr: *Surface, + result: *Text, + ) bool { + const surface = &ptr.core_surface; + surface.renderer_state.mutex.lock(); + defer surface.renderer_state.mutex.unlock(); + + // Get our word selection + const sel = sel: { + const screen: *terminal.Screen = surface.renderer_state.terminal.screens.active; + const pos = try ptr.getCursorPos(); + const pt_viewport = surface.posToViewport(pos.x, pos.y); + const pin = screen.pages.pin(.{ + .viewport = .{ + .x = pt_viewport.x, + .y = pt_viewport.y, + }, + }) orelse { + if (comptime std.debug.runtime_safety) unreachable; + return false; + }; + break :sel surface.io.terminal.screens.active.selectWord( + pin, + surface.config.selection_word_chars, + ) orelse return false; + }; + + // Read the selection + return readTextLocked(ptr, sel, result); + } + + export fn ghostty_inspector_metal_init(ptr: *Inspector, device: objc.c.id) bool { + return ptr.initMetal(.fromId(device)); + } + + export fn ghostty_inspector_metal_render( + ptr: *Inspector, + command_buffer: objc.c.id, + descriptor: objc.c.id, + ) void { + return ptr.renderMetal( + .fromId(command_buffer), + .fromId(descriptor), + ) catch |err| { + log.err("error rendering inspector err={}", .{err}); + return; + }; + } + + export fn ghostty_inspector_metal_shutdown(ptr: *Inspector) void { + if (ptr.backend) |v| { + v.deinit(); + ptr.backend = null; + } + } + }; +}; |
