This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139//! 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 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. 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, *apprt.ClipboardRequest) callconv(.c) void,
/// 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, [*:0]const u8, c_int, 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 => {}, .leaf => |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 redrawSurface(self: *App, surface: *Surface) void { _ = self; _ = surface; // No-op, we use a threaded interface so we're constantly drawing. }
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={s} action={} value={}", .{ @tagName(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 => {}, } }};
/// 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, };
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); 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; }
config.@"working-directory" = wd; } }
// 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(); config.input.list.clearRetainingCapacity(); try config.input.list.append( alloc, .{ .raw = try alloc.dupeZ(u8, std.mem.sliceTo( c_input, 0, )) }, ); }
// 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 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, ) !void { // 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;
self.app.opts.read_clipboard( self.userdata, @intCast(@intFromEnum(clipboard_type)), state_ptr, ); }
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 setClipboardString( self: *const Surface, val: [:0]const u8, clipboard_type: apprt.Clipboard, confirm: bool, ) !void { self.app.opts.write_clipboard( self.userdata, val.ptr, @intCast(@intFromEnum(clipboard_type)), confirm, ); }
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 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. 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, 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) 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; };
return .{ .font_size = font_size, }; }
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. switch (self.app.config.@"launched-from".?) { .desktop => env.remove("LANGUAGE"), .dbus, .systemd, .cli => {}, } }
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("cimgui");
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.igCreateContext(null) orelse return error.OutOfMemory; errdefer cimgui.c.igDestroyContext(ig_ctx); cimgui.c.igSetCurrentContext(ig_ctx); const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); 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.igSetCurrentContext(self.ig_ctx); if (self.backend) |v| v.deinit(); cimgui.c.igDestroyContext(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.igSetCurrentContext(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.igNewFrame();
// Build our UI render: { const surface = &self.surface.core_surface; const inspector = surface.inspector orelse break :render; inspector.render(); }
// Render cimgui.c.igRender(); }
// 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.igGetDrawData(), command_buffer.value, encoder.value, ); }
pub fn updateContentScale(self: *Inspector, x: f64, y: f64) void { _ = y; cimgui.c.igSetCurrentContext(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. const style = cimgui.c.ImGuiStyle_ImGuiStyle(); defer cimgui.c.ImGuiStyle_destroy(style); cimgui.c.ImGuiStyle_ScaleAllSizes(style, @floatCast(x)); const active_style = cimgui.c.igGetStyle(); active_style.* = style.*; }
pub fn updateSize(self: *Inspector, width: u32, height: u32) void { cimgui.c.igSetCurrentContext(self.ig_ctx); const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); 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.igSetCurrentContext(self.ig_ctx); const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
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 { _ = mods;
self.queueRender(); cimgui.c.igSetCurrentContext(self.ig_ctx); const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); cimgui.c.ImGuiIO_AddMouseWheelEvent( io, @floatCast(xoff), @floatCast(yoff), ); }
pub fn cursorPosCallback(self: *Inspector, x: f64, y: f64) void { self.queueRender(); cimgui.c.igSetCurrentContext(self.ig_ctx); const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); 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.igSetCurrentContext(self.ig_ctx); const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); cimgui.c.ImGuiIO_AddFocusEvent(io, focused); }
pub fn textCallback(self: *Inspector, text: [:0]const u8) void { self.queueRender(); cimgui.c.igSetCurrentContext(self.ig_ctx); const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); 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.igSetCurrentContext(self.ig_ctx); const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
// 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.igGetIO();
// Determine our delta time const now = try std.time.Instant.now(); io.DeltaTime = if (self.instant) |prev| delta: { const since_ns = now.since(prev); const since_s: f32 = @floatFromInt(since_ns / std.time.ns_per_s); break :delta @max(0.00001, since_s); } else (1 / 60); self.instant = now; }};
// C APIpub 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_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) Surface.Options { return surface.newSurfaceOptions(); }
/// 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.screen.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.screen, ) 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(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))); }
/// Returns the current possible commands for a surface /// in the output parameter. The memory is owned by libghostty /// and doesn't need to be freed. export fn ghostty_surface_commands( surface: *Surface, out: *[*]const input.Command.C, len: *usize, ) void { // In the future we may use this information to filter // some commands. _ = surface;
const commands = input.command.defaultsC; out.* = commands.ptr; len.* = commands.len; }
/// 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, ) bool { const core_event = event.keyEvent().core() orelse { log.warn("error processing key event", .{}); return false; };
return surface.core_surface.keyEventIsBinding(core_event); }
/// 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) 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: 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={} 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 = &surface.renderer_state.terminal.screen; 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.screen.selectWord(pin) 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; } } };};