aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/App.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/App.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/App.zig')
-rw-r--r--vendor/libghostty-vt/src/App.zig623
1 files changed, 623 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/App.zig b/vendor/libghostty-vt/src/App.zig
new file mode 100644
index 00000000..5446f4bf
--- /dev/null
+++ b/vendor/libghostty-vt/src/App.zig
@@ -0,0 +1,623 @@
+//! App is the primary GUI application for ghostty. This builds the window,
+//! sets up the renderer, etc. The primary run loop is started by calling
+//! the "run" function.
+const App = @This();
+
+const std = @import("std");
+const builtin = @import("builtin");
+const assert = @import("quirks.zig").inlineAssert;
+const Allocator = std.mem.Allocator;
+const apprt = @import("apprt.zig");
+const Surface = @import("Surface.zig");
+const input = @import("input.zig");
+const configpkg = @import("config.zig");
+const Config = configpkg.Config;
+const BlockingQueue = @import("datastruct/main.zig").BlockingQueue;
+const renderer = @import("renderer.zig");
+const font = @import("font/main.zig");
+
+const log = std.log.scoped(.app);
+
+const SurfaceList = std.ArrayListUnmanaged(*apprt.Surface);
+
+/// General purpose allocator
+alloc: Allocator,
+
+/// The list of surfaces that are currently active.
+surfaces: SurfaceList,
+
+/// This is true if the app that Ghostty is in is focused. This may
+/// mean that no surfaces (terminals) are focused but the app is still
+/// focused, i.e. may an about window. On macOS, this concept is known
+/// as the "active" app while focused windows are known as the
+/// "main" window.
+///
+/// This is used to determine if keyboard shortcuts that are non-global
+/// should be processed. If the app is not focused, then we don't want
+/// to process keyboard shortcuts that are not global.
+///
+/// This defaults to true since we assume that the app is focused when
+/// Ghostty is initialized but a well behaved apprt should call
+/// focusEvent to set this to the correct value right away.
+focused: bool = true,
+
+/// The last focused surface. This surface may not be valid;
+/// you must always call hasSurface to validate it.
+focused_surface: ?*Surface = null,
+
+/// The mailbox that can be used to send this thread messages. Note
+/// this is a blocking queue so if it is full you will get errors (or block).
+mailbox: Mailbox.Queue,
+
+/// The set of font GroupCache instances shared by surfaces with the
+/// same font configuration.
+font_grid_set: font.SharedGridSet,
+
+// Used to rate limit desktop notifications. Some platforms (notably macOS) will
+// run out of resources if desktop notifications are sent too fast and the OS
+// will kill Ghostty.
+last_notification_time: ?std.time.Instant = null,
+last_notification_digest: u64 = 0,
+
+/// The conditional state of the configuration. See the equivalent field
+/// in the Surface struct for more information. In this case, this applies
+/// to the app-level config and as a default for new surfaces.
+config_conditional_state: configpkg.ConditionalState,
+
+/// Set to false once we've created at least one surface. This
+/// never goes true again. This can be used by surfaces to determine
+/// if they are the first surface.
+first: bool = true,
+
+pub const CreateError = Allocator.Error || font.SharedGridSet.InitError;
+
+/// Create a new app instance. This returns a stable pointer to the app
+/// instance which is required for callbacks.
+pub fn create(alloc: Allocator) CreateError!*App {
+ var app = try alloc.create(App);
+ errdefer alloc.destroy(app);
+ try app.init(alloc);
+ return app;
+}
+
+/// Initialize the main app instance. This creates the main window, sets
+/// up the renderer state, compiles the shaders, etc. This is the primary
+/// "startup" logic.
+///
+/// After calling this function, well behaved apprts should then call
+/// `focusEvent` to set the initial focus state of the app.
+pub fn init(
+ self: *App,
+ alloc: Allocator,
+) CreateError!void {
+ var font_grid_set = try font.SharedGridSet.init(alloc);
+ errdefer font_grid_set.deinit();
+
+ self.* = .{
+ .alloc = alloc,
+ .surfaces = .{},
+ .mailbox = .{},
+ .font_grid_set = font_grid_set,
+ .config_conditional_state = .{},
+ };
+}
+
+pub fn deinit(self: *App) void {
+ // Clean up all our surfaces
+ for (self.surfaces.items) |surface| surface.deinit();
+ self.surfaces.deinit(self.alloc);
+
+ // Clean up our font group cache
+ // We should have zero items in the grid set at this point because
+ // destroy only gets called when the app is shutting down and this
+ // should gracefully close all surfaces.
+ assert(self.font_grid_set.count() == 0);
+ self.font_grid_set.deinit();
+}
+
+pub fn destroy(self: *App) void {
+ // Deinitialize the app
+ self.deinit();
+
+ // Free the app memory
+ self.alloc.destroy(self);
+}
+
+/// Tick ticks the app loop. This will drain our mailbox and process those
+/// events. This should be called by the application runtime on every loop
+/// tick.
+pub fn tick(self: *App, rt_app: *apprt.App) !void {
+ // Drain our mailbox
+ try self.drainMailbox(rt_app);
+}
+
+/// Update the configuration associated with the app. This can only be
+/// called from the main thread. The caller owns the config memory. The
+/// memory can be freed immediately when this returns.
+pub fn updateConfig(self: *App, rt_app: *apprt.App, config: *const Config) !void {
+ // Go through and update all of the surface configurations.
+ for (self.surfaces.items) |surface| {
+ try surface.core().handleMessage(.{ .change_config = config });
+ }
+
+ // Apply our conditional state. If we fail to apply the conditional state
+ // then we log and attempt to move forward with the old config.
+ // We only apply this to the app-level config because the surface
+ // config applies its own conditional state.
+ var applied_: ?configpkg.Config = config.changeConditionalState(
+ self.config_conditional_state,
+ ) catch |err| err: {
+ log.warn("failed to apply conditional state to config err={}", .{err});
+ break :err null;
+ };
+ defer if (applied_) |*c| c.deinit();
+ const applied: *const configpkg.Config = if (applied_) |*c| c else config;
+
+ // Notify the apprt that the app has changed configuration.
+ _ = try rt_app.performAction(
+ .app,
+ .config_change,
+ .{ .config = applied },
+ );
+}
+
+/// Add an initialized surface. This is really only for the runtime
+/// implementations to call and should NOT be called by general app users.
+/// The surface must be from the pool.
+pub fn addSurface(
+ self: *App,
+ rt_surface: *apprt.Surface,
+) Allocator.Error!void {
+ try self.surfaces.append(self.alloc, rt_surface);
+
+ // Since we have non-zero surfaces, we can cancel the quit timer.
+ // It is up to the apprt if there is a quit timer at all and if it
+ // should be canceled.
+ _ = rt_surface.rtApp().performAction(
+ .app,
+ .quit_timer,
+ .stop,
+ ) catch |err| {
+ log.warn("error stopping quit timer err={}", .{err});
+ };
+}
+
+/// Delete the surface from the known surface list. This will NOT call the
+/// destructor or free the memory.
+pub fn deleteSurface(self: *App, rt_surface: *apprt.Surface) void {
+ // If this surface is the focused surface then we need to clear it.
+ // There was a bug where we relied on hasSurface to return false and
+ // just let focused surface be but the allocator was reusing addresses
+ // after free and giving false positives, so we must clear it.
+ if (self.focused_surface) |focused| {
+ if (focused == rt_surface.core()) {
+ self.focused_surface = null;
+ }
+ }
+
+ var i: usize = 0;
+ while (i < self.surfaces.items.len) {
+ if (self.surfaces.items[i] == rt_surface) {
+ _ = self.surfaces.swapRemove(i);
+ continue;
+ }
+
+ i += 1;
+ }
+
+ // If we have no surfaces, we can start the quit timer. It is up to the
+ // apprt to determine if this is necessary.
+ if (self.surfaces.items.len == 0) _ = rt_surface.rtApp().performAction(
+ .app,
+ .quit_timer,
+ .start,
+ ) catch |err| {
+ log.warn("error starting quit timer err={}", .{err});
+ };
+}
+
+/// The last focused surface. This is only valid while on the main thread
+/// before tick is called.
+pub fn focusedSurface(self: *const App) ?*Surface {
+ const surface = self.focused_surface orelse return null;
+ if (!self.hasSurface(surface)) return null;
+ return surface;
+}
+
+/// Returns true if confirmation is needed to quit the app. It is up to
+/// the apprt to call this.
+pub fn needsConfirmQuit(self: *const App) bool {
+ for (self.surfaces.items) |v| {
+ if (v.core().needsConfirmQuit()) return true;
+ }
+
+ return false;
+}
+
+/// Drain the mailbox.
+fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
+ while (self.mailbox.pop()) |message| {
+ if (comptime std.log.logEnabled(.debug, .app)) {
+ switch (message) {
+ // these tend to be way too verbose for normal debugging
+ .redraw_surface => {},
+ else => log.debug("mailbox message={t}", .{message}),
+ }
+ }
+ switch (message) {
+ .open_config => try self.performAction(rt_app, .open_config),
+ .new_window => |msg| try self.newWindow(rt_app, msg),
+ .close => |surface| self.closeSurface(surface),
+ .surface_message => |msg| try self.surfaceMessage(msg.surface, msg.message),
+ .redraw_surface => |surface| try self.redrawSurface(rt_app, surface),
+
+ // If we're quitting, then we set the quit flag and stop
+ // draining the mailbox immediately. This lets us defer
+ // mailbox processing to the next tick so that the apprt
+ // can try to quit as quickly as possible.
+ .quit => {
+ log.info("quit message received, short circuiting mailbox drain", .{});
+ try self.performAction(rt_app, .quit);
+ return;
+ },
+ }
+ }
+}
+
+pub fn closeSurface(self: *App, surface: *Surface) void {
+ if (!self.hasSurface(surface)) return;
+ surface.close();
+}
+
+pub fn focusSurface(self: *App, surface: *Surface) void {
+ if (!self.hasSurface(surface)) return;
+ self.focused_surface = surface;
+}
+
+fn redrawSurface(
+ self: *App,
+ rt_app: *apprt.App,
+ surface: *apprt.Surface,
+) !void {
+ if (!self.hasRtSurface(surface)) return;
+
+ _ = try rt_app.performAction(
+ .{ .surface = surface.core() },
+ .render,
+ {},
+ );
+}
+
+/// Create a new window
+pub fn newWindow(self: *App, rt_app: *apprt.App, msg: Message.NewWindow) !void {
+ const target: apprt.Target = target: {
+ const parent = msg.parent orelse break :target .app;
+ if (self.hasSurface(parent)) break :target .{ .surface = parent };
+ break :target .app;
+ };
+
+ _ = try rt_app.performAction(
+ target,
+ .new_window,
+ {},
+ );
+}
+
+/// Handle an app-level focus event. This should be called whenever
+/// the focus state of the entire app containing Ghostty changes.
+/// This is separate from surface focus events. See the `focused`
+/// field for more information.
+pub fn focusEvent(self: *App, focused: bool) void {
+ // Prevent redundant focus events
+ if (self.focused == focused) return;
+
+ log.debug("focus event focused={}", .{focused});
+ self.focused = focused;
+}
+
+/// Returns true if the given key event would trigger a keybinding
+/// if it were to be processed. This is useful for determining if
+/// a key event should be sent to the terminal or not.
+pub fn keyEventIsBinding(
+ self: *App,
+ rt_app: *apprt.App,
+ event: input.KeyEvent,
+) bool {
+ _ = self;
+
+ switch (event.action) {
+ .release => return false,
+ .press, .repeat => {},
+ }
+
+ // If we have a keybinding for this event then we return true.
+ return rt_app.config.keybind.set.getEvent(event) != null;
+}
+
+/// Handle a key event at the app-scope. If this key event is used,
+/// this will return true and the caller shouldn't continue processing
+/// the event. If the event is not used, this will return false.
+///
+/// If the app currently has focus then all key events are processed.
+/// If the app does not have focus then only global key events are
+/// processed.
+pub fn keyEvent(
+ self: *App,
+ rt_app: *apprt.App,
+ event: input.KeyEvent,
+) bool {
+ switch (event.action) {
+ // We don't care about key release events.
+ .release => return false,
+
+ // Continue processing key press events.
+ .press, .repeat => {},
+ }
+
+ // Get the keybind entry for this event. We don't support key sequences
+ // so we can look directly in the top-level set.
+ const entry = rt_app.config.keybind.set.getEvent(event) orelse return false;
+ const leaf: input.Binding.Set.GenericLeaf = switch (entry.value_ptr.*) {
+ // Sequences aren't supported. Our configuration parser verifies
+ // this for global keybinds but we may still get an entry for
+ // a non-global keybind.
+ .leader => return false,
+
+ // Leaf entries are good
+ inline .leaf, .leaf_chained => |leaf| leaf.generic(),
+ };
+ const actions: []const input.Binding.Action = leaf.actionsSlice();
+ assert(actions.len > 0);
+
+ // If we aren't focused, then we only process global keybinds.
+ if (!self.focused and !leaf.flags.global) return false;
+
+ // Global keybinds are done using performAll so that they
+ // can target all surfaces too.
+ if (leaf.flags.global) {
+ self.performAllChainedAction(rt_app, actions);
+ return true;
+ }
+
+ // Must be focused to process non-global keybinds
+ assert(self.focused);
+ assert(!leaf.flags.global);
+
+ // If we are focused, then we process keybinds only if they are
+ // app-scoped. Otherwise, we do nothing. Surface-scoped should
+ // be processed by Surface.keyEvent. For chained actions, all
+ // actions must be app-scoped.
+ for (actions) |action| if (action.scoped(.app) == null) return false;
+ for (actions) |action| {
+ self.performAction(
+ rt_app,
+ action.scoped(.app).?,
+ ) catch |err| {
+ log.warn("error performing app keybind action action={s} err={}", .{
+ @tagName(action),
+ err,
+ });
+ };
+ }
+
+ return true;
+}
+
+/// Call to notify Ghostty that the color scheme for the app has changed.
+/// "Color scheme" in this case refers to system themes such as "light/dark".
+pub fn colorSchemeEvent(
+ self: *App,
+ rt_app: *apprt.App,
+ scheme: apprt.ColorScheme,
+) !void {
+ const new_scheme: configpkg.ConditionalState.Theme = switch (scheme) {
+ .light => .light,
+ .dark => .dark,
+ };
+
+ // If our scheme didn't change, then we don't do anything.
+ if (self.config_conditional_state.theme == new_scheme) return;
+
+ // Setup our conditional state which has the current color theme.
+ self.config_conditional_state.theme = new_scheme;
+
+ // Request our configuration be reloaded because the new scheme may
+ // impact the colors of the app.
+ _ = try rt_app.performAction(
+ .app,
+ .reload_config,
+ .{ .soft = true },
+ );
+}
+
+/// Perform a binding action. This only accepts actions that are scoped
+/// to the app. Callers can use performAllAction to perform any action
+/// and any non-app-scoped actions will be performed on all surfaces.
+pub fn performAction(
+ self: *App,
+ rt_app: *apprt.App,
+ action: input.Binding.Action.Scoped(.app),
+) !void {
+ switch (action) {
+ .unbind => unreachable,
+ .ignore => {},
+ .quit => _ = try rt_app.performAction(.app, .quit, {}),
+ .new_window => _ = try self.newWindow(rt_app, .{ .parent = null }),
+ .open_config => _ = try rt_app.performAction(.app, .open_config, {}),
+ .reload_config => _ = try rt_app.performAction(.app, .reload_config, .{}),
+ .close_all_windows => _ = try rt_app.performAction(.app, .close_all_windows, {}),
+ .toggle_quick_terminal => _ = try rt_app.performAction(.app, .toggle_quick_terminal, {}),
+ .toggle_visibility => _ = try rt_app.performAction(.app, .toggle_visibility, {}),
+ .check_for_updates => _ = try rt_app.performAction(.app, .check_for_updates, {}),
+ .show_gtk_inspector => _ = try rt_app.performAction(.app, .show_gtk_inspector, {}),
+ .undo => _ = try rt_app.performAction(.app, .undo, {}),
+
+ .redo => _ = try rt_app.performAction(.app, .redo, {}),
+ }
+}
+
+/// Performs a chained action. We will continue executing each action
+/// even if there is a failure in a prior action.
+pub fn performAllChainedAction(
+ self: *App,
+ rt_app: *apprt.App,
+ actions: []const input.Binding.Action,
+) void {
+ for (actions) |action| {
+ self.performAllAction(rt_app, action) catch |err| {
+ log.warn("error performing chained action action={s} err={}", .{
+ @tagName(action),
+ err,
+ });
+ };
+ }
+}
+
+/// Perform an app-wide binding action. If the action is surface-specific
+/// then it will be performed on all surfaces. To perform only app-scoped
+/// actions, use performAction.
+pub fn performAllAction(
+ self: *App,
+ rt_app: *apprt.App,
+ action: input.Binding.Action,
+) !void {
+ switch (action.scope()) {
+ // App-scoped actions are handled by the app so that they aren't
+ // repeated for each surface (since each surface forwards
+ // app-scoped actions back up).
+ .app => try self.performAction(
+ rt_app,
+ action.scoped(.app).?, // asserted through the scope match
+ ),
+
+ // Surface-scoped actions are performed on all surfaces. Errors
+ // are logged but processing continues.
+ .surface => for (self.surfaces.items) |surface| {
+ _ = surface.core().performBindingAction(action) catch |err| {
+ log.warn("error performing binding action on surface ptr={X} err={}", .{
+ @intFromPtr(surface),
+ err,
+ });
+ };
+ },
+ }
+}
+
+/// Handle a window message
+fn surfaceMessage(self: *App, surface: *Surface, msg: apprt.surface.Message) !void {
+ // We want to ensure our window is still active. Window messages
+ // are quite rare and we normally don't have many windows so we do
+ // a simple linear search here.
+ if (self.hasSurface(surface)) {
+ try surface.handleMessage(msg);
+ }
+
+ // Window was not found, it probably quit before we handled the message.
+ // Not a problem.
+}
+
+fn hasSurface(self: *const App, surface: *const Surface) bool {
+ for (self.surfaces.items) |v| {
+ if (v.core() == surface) return true;
+ }
+
+ return false;
+}
+
+/// Search for a surface by a 64 bit unique ID.
+pub fn findSurfaceByID(self: *const App, id: u64) ?*Surface {
+ for (self.surfaces.items) |v| {
+ const surface: *Surface = v.core();
+ if (surface.id == id) return surface;
+ }
+
+ return null;
+}
+
+fn hasRtSurface(self: *const App, surface: *apprt.Surface) bool {
+ for (self.surfaces.items) |v| {
+ if (v == surface) return true;
+ }
+
+ return false;
+}
+
+/// The message types that can be sent to the app thread.
+pub const Message = union(enum) {
+ // Open the configuration file
+ open_config: void,
+
+ /// Create a new terminal window.
+ new_window: NewWindow,
+
+ /// Close a surface. This notifies the runtime that a surface
+ /// should close.
+ close: *Surface,
+
+ /// Quit
+ quit: void,
+
+ /// A message for a specific surface.
+ surface_message: struct {
+ surface: *Surface,
+ message: apprt.surface.Message,
+ },
+
+ /// Redraw a surface. This only has an effect for runtimes that
+ /// use single-threaded draws. To redraw a surface for all runtimes,
+ /// wake up the renderer thread. The renderer thread will send this
+ /// message if it needs to.
+ redraw_surface: *apprt.Surface,
+
+ const NewWindow = struct {
+ /// The parent surface
+ parent: ?*Surface = null,
+ };
+};
+
+/// Mailbox is the way that other threads send the app thread messages.
+pub const Mailbox = struct {
+ /// The type used for sending messages to the app thread.
+ pub const Queue = BlockingQueue(Message, 64);
+
+ rt_app: *apprt.App,
+ mailbox: *Queue,
+
+ /// Send a message to the surface.
+ pub fn push(self: Mailbox, msg: Message, timeout: Queue.Timeout) Queue.Size {
+ const result = self.mailbox.push(msg, timeout);
+
+ // Wake up our app loop
+ self.rt_app.wakeup();
+
+ return result;
+ }
+};
+
+// Wasm API.
+pub const Wasm = if (!builtin.target.isWasm()) struct {} else struct {
+ const wasm = @import("os/wasm.zig");
+ const alloc = wasm.alloc;
+
+ // export fn app_new(config: *Config) ?*App {
+ // return app_new_(config) catch |err| { log.err("error initializing app err={}", .{err});
+ // return null;
+ // };
+ // }
+ //
+ // fn app_new_(config: *Config) !*App {
+ // const app = try App.create(alloc, config);
+ // errdefer app.destroy();
+ //
+ // const result = try alloc.create(App);
+ // result.* = app;
+ // return result;
+ // }
+ //
+ // export fn app_free(ptr: ?*App) void {
+ // if (ptr) |v| {
+ // v.destroy();
+ // alloc.destroy(v);
+ // }
+ // }
+};