//! 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 = std.debug.assert; const Allocator = std.mem.Allocator; const objc = @import("objc"); const apprt = @import("../apprt.zig"); const input = @import("../input.zig"); const CoreApp = @import("../App.zig"); const CoreSurface = @import("../Surface.zig"); const Config = @import("../config.zig").Config; const log = std.log.scoped(.embedded_window); 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, /// 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, /// Called to set the title of the window. set_title: *const fn (SurfaceUD, [*]const u8) 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. read_clipboard: *const fn (SurfaceUD, c_int) callconv(.C) ?[*:0]const u8, /// Write the clipboard value. write_clipboard: *const fn (SurfaceUD, [*:0]const u8, c_int) 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, input.SplitDirection) 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, input.SplitFocusDirection) callconv(.C) void = null, /// Goto tab goto_tab: ?*const fn (SurfaceUD, usize) callconv(.C) void = null, /// Toggle fullscreen for current window. toggle_fullscreen: ?*const fn (SurfaceUD, bool) callconv(.C) void = null, }; core_app: *CoreApp, config: *const Config, opts: Options, keymap: input.Keymap, pub fn init(core_app: *CoreApp, config: *const Config, opts: Options) !App { return .{ .core_app = core_app, .config = config, .opts = opts, .keymap = try input.Keymap.init(), }; } pub fn terminate(self: App) void { self.keymap.deinit(); } /// This should be called whenever the keyboard layout was changed. pub fn reloadKeymap(self: *App) !void { // Reload the keymap try self.keymap.reload(); // Clear the dead key state since we changed the keymap, any // dead key state is just forgotten. i.e. if you type ' on us-intl // and then switch to us and type a, you'll get a rather than รก. for (self.core_app.surfaces.items) |surface| { surface.keymap_state = .{}; } } pub fn reloadConfig(self: *App) !?*const Config { // Reload if (self.opts.reload_config(self.opts.userdata)) |new| { self.config = new; return self.config; } return null; } pub fn wakeup(self: App) void { self.opts.wakeup(self.opts.userdata); } pub fn wait(self: 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 -- because windows are surfaces for glfw. 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 redrawSurface(self: *App, surface: *Surface) void { _ = self; _ = surface; // No-op, we use a threaded interface so we're constantly drawing. } }; pub const Surface = struct { app: *App, nsview: objc.Object, core_surface: CoreSurface, content_scale: apprt.ContentScale, size: apprt.SurfaceSize, cursor_pos: apprt.CursorPos, opts: Options, keymap_state: input.Keymap.State, pub const Options = extern struct { /// Userdata passed to some of the callbacks. userdata: ?*anyopaque = null, /// The pointer to the backing NSView for the surface. nsview: *anyopaque = undefined, /// The scale factor of the screen. scale_factor: f64 = 1, }; pub fn init(self: *Surface, app: *App, opts: Options) !void { self.* = .{ .app = app, .core_surface = undefined, .nsview = objc.Object.fromId(opts.nsview), .content_scale = .{ .x = @floatCast(opts.scale_factor), .y = @floatCast(opts.scale_factor), }, .size = .{ .width = 800, .height = 600 }, .cursor_pos = .{ .x = 0, .y = 0 }, .opts = opts, .keymap_state = .{}, }; // 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); defer config.deinit(); // 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, .{ .rt_app = app, .mailbox = &app.core_app.mailbox }, app.core_app.resources_dir, self, ); errdefer self.core_surface.deinit(); } pub fn deinit(self: *Surface) void { // 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(); } pub fn newSplit(self: *const Surface, direction: input.SplitDirection) !void { const func = self.app.opts.new_split orelse { log.info("runtime embedder does not support splits", .{}); return; }; func(self.opts.userdata, direction); } 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.opts.userdata, process_alive); } pub fn gotoSplit(self: *const Surface, direction: input.SplitFocusDirection) void { const func = self.app.opts.focus_split orelse { log.info("runtime embedder does not support focus split", .{}); return; }; func(self.opts.userdata, direction); } 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 setSizeLimits(self: *Surface, min: apprt.SurfaceSize, max_: ?apprt.SurfaceSize) !void { _ = self; _ = min; _ = max_; } pub fn setTitle(self: *Surface, slice: [:0]const u8) !void { self.app.opts.set_title( self.opts.userdata, slice.ptr, ); } pub fn supportsClipboard( self: *const Surface, clipboard_type: apprt.Clipboard, ) bool { return switch (clipboard_type) { .standard => true, .selection => self.app.opts.supports_selection_clipboard, }; } pub fn getClipboardString( self: *const Surface, clipboard_type: apprt.Clipboard, ) ![:0]const u8 { const ptr = self.app.opts.read_clipboard( self.opts.userdata, @intCast(@intFromEnum(clipboard_type)), ) orelse return ""; return std.mem.sliceTo(ptr, 0); } pub fn setClipboardString( self: *const Surface, val: [:0]const u8, clipboard_type: apprt.Clipboard, ) !void { self.app.opts.write_clipboard( self.opts.userdata, val.ptr, @intCast(@intFromEnum(clipboard_type)), ); } pub fn setShouldClose(self: *Surface) void { _ = self; } pub fn shouldClose(self: *const Surface) bool { _ = self; return false; } 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 updateContentScale(self: *Surface, x: f64, y: f64) void { self.content_scale = .{ .x = @floatCast(x), .y = @floatCast(y), }; } 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 mouseButtonCallback( self: *Surface, action: input.MouseButtonState, button: input.MouseButton, mods: input.Mods, ) void { self.core_surface.mouseButtonCallback(action, button, mods) catch |err| { log.err("error in mouse button 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) void { // Convert our unscaled x/y to scaled. self.cursor_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; }; self.core_surface.cursorPosCallback(self.cursor_pos) catch |err| { log.err("error in cursor pos callback err={}", .{err}); return; }; } pub fn keyCallback( self: *Surface, action: input.Action, key: input.Key, unmapped_key: input.Key, mods: input.Mods, ) void { // log.warn("key action={} key={} mods={}", .{ action, key, mods }); self.core_surface.keyCallback(action, key, unmapped_key, mods) catch |err| { log.err("error in key callback err={}", .{err}); return; }; } pub fn charCallback(self: *Surface, cp_: u32) void { const cp = std.math.cast(u21, cp_) orelse return; self.core_surface.charCallback(cp) catch |err| { log.err("error in char 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 gotoTab(self: *Surface, n: usize) void { const func = self.app.opts.goto_tab orelse { log.info("runtime embedder does not goto_tab", .{}); return; }; func(self.opts.userdata, n); } pub fn toggleFullscreen(self: *Surface, nonNativeFullscreen: bool) void { const func = self.app.opts.toggle_fullscreen orelse { log.info("runtime embedder does not toggle_fullscreen", .{}); return; }; func(self.opts.userdata, nonNativeFullscreen); } /// 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 }; } }; // C API pub const CAPI = struct { const global = &@import("../main.zig").state; /// 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 { var 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); 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) bool { return v.core_app.tick(v) catch |err| err: { log.err("error app tick err={}", .{err}); break :err false; }; } /// 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(); } /// 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; }; } /// 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 app associated with a surface. export fn ghostty_surface_app(surface: *Surface) *App { return surface.app; } /// Returns true if the surface has transparency set. export fn ghostty_surface_transparent(surface: *Surface) bool { return surface.app.config.@"background-opacity" < 1.0; } /// Tell the surface that it needs to schedule a render export fn ghostty_surface_refresh(surface: *Surface) void { surface.refresh(); } /// 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); } /// 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); } /// Tell the surface that it needs to schedule a render export fn ghostty_surface_key( surface: *Surface, action: input.Action, key: input.Key, unmapped_key: input.Key, mods: c_int, ) void { surface.keyCallback( action, key, unmapped_key, @bitCast(@as(u8, @truncate(@as(c_uint, @bitCast(mods))))), ); } /// TODO: new key processing export fn ghostty_surface_key2( surface: *Surface, action: input.Action, keycode: u32, c_mods: c_int, ) void { if (action != .press) return; var buf: [128]u8 = undefined; const mods: input.Mods = @bitCast(@as(u8, @truncate(@as(c_uint, @bitCast(c_mods))))); const result = surface.app.keymap.translate( &buf, &surface.keymap_state, @intCast(keycode), mods, ) catch |err| { log.err("TRANSLATE error translating key err={}", .{err}); return; }; log.warn("TRANSLATE: action={} keycode={x} dead={} key={any} key_str={s}", .{ action, keycode, result.composing, result.text, result.text, }); } /// Tell the surface that it needs to schedule a render export fn ghostty_surface_char(surface: *Surface, codepoint: u32) void { surface.charCallback(codepoint); } /// 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, ) void { surface.mouseButtonCallback( action, button, @bitCast(@as(u8, @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) void { surface.cursorPosCallback(x, y); } 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_ime_point(surface: *Surface, x: *f64, y: *f64) void { const pos = surface.core_surface.imePoint(); x.* = pos.x; y.* = pos.y; } /// 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: input.SplitDirection) void { ptr.newSplit(direction) catch {}; } /// Focus on the next split (if any). export fn ghostty_surface_split_focus(ptr: *Surface, direction: input.SplitFocusDirection) void { ptr.gotoSplit(direction); } /// Invoke an action on the surface. export fn ghostty_surface_binding_action( ptr: *Surface, key: input.Binding.Key, unused: *anyopaque, ) void { // For future arguments _ = unused; const action: input.Binding.Action = switch (key) { .copy_to_clipboard => .{ .copy_to_clipboard = {} }, .paste_from_clipboard => .{ .paste_from_clipboard = {} }, }; ptr.core_surface.performBindingAction(action) catch |err| { log.err("error performing binding action action={} err={}", .{ action, err }); }; } /// 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( ptr: *Surface, window: *anyopaque, ) void { const config = ptr.app.config; // Do nothing if we don't have background transparency enabled if (config.@"background-opacity" >= 1.0) return; // Do nothing if our blur value is zero if (config.@"background-blur-radius" == 0) return; const nswindow = objc.Object.fromId(window); _ = CGSSetWindowBackgroundBlurRadius( CGSDefaultConnectionForThread(), nswindow.msgSend(usize, objc.sel("windowNumber"), .{}), @intCast(config.@"background-blur-radius"), ); } /// See ghostty_set_window_background_blur extern "c" fn CGSSetWindowBackgroundBlurRadius(*anyopaque, usize, c_int) i32; extern "c" fn CGSDefaultConnectionForThread() *anyopaque; };