aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/apprt
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/apprt
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/apprt')
-rw-r--r--vendor/libghostty-vt/src/apprt/action.zig1007
-rw-r--r--vendor/libghostty-vt/src/apprt/browser.zig4
-rw-r--r--vendor/libghostty-vt/src/apprt/embedded.zig2231
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk.zig17
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/App.zig93
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/Surface.zig104
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/adw_version.zig122
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/build/blueprint.zig174
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/build/gresource.zig277
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/build/info.zig18
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/cgroup.zig115
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/class.zig367
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/class/application.zig2945
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/class/clipboard_confirmation_dialog.zig352
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/class/close_confirmation_dialog.zig204
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/class/command_palette.zig789
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/class/config.zig175
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/class/config_errors_dialog.zig161
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/class/debug_warning.zig50
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/class/dialog.zig91
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/class/global_shortcuts.zig633
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/class/imgui_widget.zig551
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/class/inspector_widget.zig245
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/class/inspector_window.zig217
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/class/key_state_overlay.zig342
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/class/resize_overlay.zig338
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/class/search_overlay.zig493
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/class/split_tree.zig1290
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/class/surface.zig4049
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/class/surface_child_exited.zig264
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/class/surface_scrolled_window.zig228
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/class/tab.zig592
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/class/title_dialog.zig234
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/class/window.zig2111
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/css/style-dark.css8
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/css/style-hc-dark.css3
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/css/style-hc.css3
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/css/style.css183
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/ext.zig70
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/ext/actions.zig198
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/ext/slice.zig111
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/flatpak.zig29
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/gsettings.zig101
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/gtk_version.zig140
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/ipc/DBus.zig196
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/ipc/new_window.zig62
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/key.zig535
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/media.zig102
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/portal.zig105
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/portal/OpenURI.zig565
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/post_fork.zig121
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/pre_exec.zig81
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp86
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp12
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/ui/1.2/config-errors-dialog.blp28
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/ui/1.2/debug-warning.blp11
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/ui/1.2/key-state-overlay.blp58
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/ui/1.2/resize-overlay.blp33
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/ui/1.2/search-overlay.blp94
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/ui/1.2/surface.blp371
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/ui/1.3/debug-warning.blp9
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/ui/1.3/surface-child-exited.blp22
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp96
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/ui/1.5/command-palette.blp110
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/ui/1.5/imgui-widget.blp51
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/ui/1.5/inspector-widget.blp10
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/ui/1.5/inspector-window.blp38
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/ui/1.5/split-tree-split.blp20
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/ui/1.5/split-tree.blp25
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/ui/1.5/surface-scrolled-window.blp11
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/ui/1.5/tab.blp18
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/ui/1.5/title-dialog.blp18
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/ui/1.5/window.blp323
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/weak_ref.zig44
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/winproto.zig155
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/winproto/BlurRegion.zig187
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/winproto/noop.zig73
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/winproto/wayland.zig486
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/winproto/wayland/Globals.zig252
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/winproto/x11.zig498
-rw-r--r--vendor/libghostty-vt/src/apprt/ipc.zig192
-rw-r--r--vendor/libghostty-vt/src/apprt/none.zig19
-rw-r--r--vendor/libghostty-vt/src/apprt/runtime.zig29
-rw-r--r--vendor/libghostty-vt/src/apprt/structs.zig114
-rw-r--r--vendor/libghostty-vt/src/apprt/surface.zig197
85 files changed, 27186 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/apprt/action.zig b/vendor/libghostty-vt/src/apprt/action.zig
new file mode 100644
index 00000000..f6865af8
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/action.zig
@@ -0,0 +1,1007 @@
+const std = @import("std");
+const build_config = @import("../build_config.zig");
+const assert = @import("../quirks.zig").inlineAssert;
+const apprt = @import("../apprt.zig");
+const configpkg = @import("../config.zig");
+const input = @import("../input.zig");
+const renderer = @import("../renderer.zig");
+const terminal = @import("../terminal/main.zig");
+const CoreSurface = @import("../Surface.zig");
+const lib = @import("../lib/main.zig");
+
+/// The target for an action. This is generally the thing that had focus
+/// while the action was made but the concept of "focus" is not guaranteed
+/// since actions can also be triggered by timers, scripts, etc.
+pub const Target = union(Key) {
+ app,
+ surface: *CoreSurface,
+
+ // Sync with: ghostty_target_tag_e
+ pub const Key = enum(c_int) {
+ app,
+ surface,
+
+ test "ghostty.h Target.Key" {
+ try lib.checkGhosttyHEnum(Key, "GHOSTTY_TARGET_");
+ }
+ };
+
+ // Sync with: ghostty_target_u
+ pub const CValue = extern union {
+ app: void,
+ surface: *apprt.Surface,
+ };
+
+ // Sync with: ghostty_target_s
+ pub const C = extern struct {
+ key: Key,
+ value: CValue,
+ };
+
+ /// Convert to ghostty_target_s.
+ pub fn cval(self: Target) C {
+ return .{
+ .key = @as(Key, self),
+ .value = switch (self) {
+ .app => .{ .app = {} },
+ .surface => |v| .{ .surface = v.rt_surface },
+ },
+ };
+ }
+};
+
+/// The possible actions an apprt has to react to. Actions are one-way
+/// messages that are sent to the app runtime to trigger some behavior.
+///
+/// Actions are very often key binding actions but can also be triggered
+/// by lifecycle events. For example, the `quit_timer` action is not bindable.
+///
+/// Importantly, actions are generally OPTIONAL to implement by an apprt.
+/// Required functionality is called directly on the runtime structure so
+/// there is a compiler error if an action is not implemented.
+pub const Action = union(Key) {
+ // A GUIDE TO ADDING NEW ACTIONS:
+ //
+ // 1. Add the action to the `Key` enum. The order of the enum matters
+ // because it maps directly to the libghostty C enum. For ABI
+ // compatibility, new actions should be added to the end of the enum.
+ //
+ // 2. Add the action and optional value to the Action union.
+ //
+ // 3. If the value type is not void, ensure the value is C ABI
+ // compatible (extern). If it is not, add a `C` decl to the value
+ // and a `cval` function to convert to the C ABI compatible value.
+ //
+ // 4. Update `include/ghostty.h`: add the new key, value, and union
+ // entry. If the value type is void then only the key needs to be
+ // added. Ensure the order matches exactly with the Zig code.
+
+ /// Quit the application.
+ quit,
+
+ /// Open a new window. The target determines whether properties such
+ /// as font size should be inherited.
+ new_window,
+
+ /// Open a new tab. If the target is a surface it should be opened in
+ /// the same window as the surface. If the target is the app then
+ /// the tab should be opened in a new window.
+ new_tab,
+
+ /// Closes the tab belonging to the currently focused split, or all other
+ /// tabs, depending on the mode.
+ close_tab: CloseTabMode,
+
+ /// Create a new split. The value determines the location of the split
+ /// relative to the target.
+ new_split: SplitDirection,
+
+ /// Close all open windows.
+ close_all_windows,
+
+ /// Toggle maximized window state.
+ toggle_maximize,
+
+ /// Toggle fullscreen mode.
+ toggle_fullscreen: Fullscreen,
+
+ /// Toggle tab overview.
+ toggle_tab_overview,
+
+ /// Toggle whether window directions are shown.
+ toggle_window_decorations,
+
+ /// Toggle the quick terminal in or out.
+ toggle_quick_terminal,
+
+ /// Toggle the command palette.
+ toggle_command_palette,
+
+ /// Toggle the visibility of all Ghostty terminal windows.
+ toggle_visibility,
+
+ /// Toggle the window background opacity. This only has an effect
+ /// if the window started as transparent (non-opaque), and toggles
+ /// it between fully opaque and the configured background opacity.
+ toggle_background_opacity,
+
+ /// Moves a tab by a relative offset.
+ ///
+ /// Adjusts the tab position based on `offset` (e.g., -1 for left, +1
+ /// for right). If the new position is out of bounds, it wraps around
+ /// cyclically within the tab range.
+ move_tab: MoveTab,
+
+ /// Jump to a specific tab. Must handle the scenario that the tab
+ /// value is invalid.
+ goto_tab: GotoTab,
+
+ /// Jump to a specific split.
+ goto_split: GotoSplit,
+
+ /// Jump to next/previous window.
+ goto_window: GotoWindow,
+
+ /// Resize the split in the given direction.
+ resize_split: ResizeSplit,
+
+ /// Equalize all the splits in the target window.
+ equalize_splits,
+
+ /// Toggle whether a split is zoomed or not. A zoomed split is resized
+ /// to take up the entire window.
+ toggle_split_zoom,
+
+ /// Present the target terminal whether its a tab, split, or window.
+ present_terminal,
+
+ /// Sets a size limit (in pixels) for the target terminal.
+ size_limit: SizeLimit,
+
+ /// Resets the window size to the default size. See the
+ /// `reset_window_size` keybinding for more information.
+ reset_window_size,
+
+ /// Specifies the initial size of the target terminal.
+ ///
+ /// This may be sent once during the initialization of a surface
+ /// (as part of the init call) to indicate the initial size requested
+ /// for the window if it is not maximized or fullscreen.
+ ///
+ /// This may also be sent at any time after the surface is initialized
+ /// to note the new "default size" of the window. This should in general
+ /// be ignored, but may be useful if the apprt wants to support
+ /// a "return to default size" action.
+ initial_size: InitialSize,
+
+ /// The cell size has changed to the given dimensions in pixels.
+ cell_size: CellSize,
+
+ /// The scrollbar is updating.
+ scrollbar: terminal.Scrollbar,
+
+ /// The target should be re-rendered. This usually has a specific
+ /// surface target but if the app is targeted then all active
+ /// surfaces should be redrawn.
+ render,
+
+ /// Control whether the inspector is shown or hidden.
+ inspector: Inspector,
+
+ /// Show the GTK inspector.
+ show_gtk_inspector,
+
+ /// The inspector for the given target has changes and should be
+ /// rendered at the next opportunity.
+ render_inspector,
+
+ /// Show a desktop notification.
+ desktop_notification: DesktopNotification,
+
+ /// Set the title of the target to the requested value.
+ set_title: SetTitle,
+
+ /// Set the tab title override for the target's tab.
+ set_tab_title: SetTitle,
+
+ /// Set the title of the target to a prompted value. It is up to
+ /// the apprt to prompt. The value specifies whether to prompt for the
+ /// surface title or the tab title.
+ prompt_title: PromptTitle,
+
+ /// The current working directory has changed for the target terminal.
+ pwd: Pwd,
+
+ /// Set the mouse cursor shape.
+ mouse_shape: terminal.MouseShape,
+
+ /// Set whether the mouse cursor is visible or not.
+ mouse_visibility: MouseVisibility,
+
+ /// Called when the mouse is over or recently left a link.
+ mouse_over_link: MouseOverLink,
+
+ /// The health of the renderer has changed.
+ renderer_health: renderer.Health,
+
+ /// Open the Ghostty configuration. This is platform-specific about
+ /// what it means; it can mean opening a dedicated UI or just opening
+ /// a file in a text editor.
+ open_config,
+
+ /// Called when there are no more surfaces and the app should quit
+ /// after the configured delay.
+ ///
+ /// Despite the name, this is the notification that libghostty sends
+ /// when there are no more surfaces regardless of if the configuration
+ /// wants to quit after close, has any delay set, etc. It's up to the
+ /// apprt to implement the proper logic based on the config.
+ ///
+ /// This can be cancelled by sending another quit_timer action with "stop".
+ /// Multiple "starts" shouldn't happen and can be ignored or cause a
+ /// restart it isn't that important.
+ quit_timer: QuitTimer,
+
+ /// Set the window floating state. A floating window is one that is
+ /// always on top of other windows even when not focused.
+ float_window: FloatWindow,
+
+ /// Set the secure input functionality on or off. "Secure input" means
+ /// that the user is currently at some sort of prompt where they may be
+ /// entering a password or other sensitive information. This can be used
+ /// by the app runtime to change the appearance of the cursor, setup
+ /// system APIs to not log the input, etc.
+ secure_input: SecureInput,
+
+ /// A sequenced key binding has started, continued, or stopped.
+ /// The UI should show some indication that the user is in a sequenced
+ /// key mode because other input may be ignored.
+ key_sequence: KeySequence,
+
+ /// A key table has been activated or deactivated.
+ key_table: KeyTable,
+
+ /// A terminal color was changed programmatically through things
+ /// such as OSC 10/11.
+ color_change: ColorChange,
+
+ /// A request to reload the configuration. The reload request can be
+ /// from a user or for some internal reason. The reload request may
+ /// request it is a soft reload or a full reload. See the struct for
+ /// more documentation.
+ ///
+ /// The configuration should be passed to updateConfig either at the
+ /// app or surface level depending on the target.
+ reload_config: ReloadConfig,
+
+ /// The configuration has changed. The value is a pointer to the new
+ /// configuration. The pointer is only valid for the duration of the
+ /// action and should not be stored.
+ ///
+ /// This should be used by apprts to update any internal state that
+ /// depends on configuration for the given target (i.e. headerbar colors).
+ /// The apprt should copy any data it needs since the memory lifetime
+ /// is only valid for the duration of the action.
+ ///
+ /// This allows an apprt to have config-dependent state reactively
+ /// change without having to store the entire configuration or poll
+ /// for changes.
+ config_change: ConfigChange,
+
+ /// Closes the currently focused window.
+ close_window,
+
+ /// Called when the bell character is seen. The apprt should do whatever
+ /// it needs to ring the bell. This is usually a sound or visual effect.
+ ring_bell,
+
+ /// Undo the last action. See the "undo" keybinding for more
+ /// details on what can and cannot be undone.
+ undo,
+
+ /// Redo the last undone action.
+ redo,
+
+ check_for_updates,
+
+ /// Open a URL using the native OS mechanisms. On macOS this might be `open`
+ /// or on Linux this might be `xdg-open`. The exact mechanism is up to the
+ /// apprt.
+ open_url: OpenUrl,
+
+ /// Show a native GUI notification that the child process has exited.
+ show_child_exited: apprt.surface.Message.ChildExited,
+
+ /// Show a native GUI notification about the progress of some TUI operation.
+ progress_report: terminal.osc.Command.ProgressReport,
+
+ /// Show the on-screen keyboard.
+ show_on_screen_keyboard,
+
+ /// A command has finished,
+ command_finished: CommandFinished,
+
+ /// Start the search overlay with an optional initial needle. If the
+ /// search is already active and the needle is non-empty, update the
+ /// current search needle and focus the search input.
+ start_search: StartSearch,
+
+ /// End the search overlay, clearing the search state and hiding it.
+ end_search,
+
+ /// The total number of matches found by the search.
+ search_total: SearchTotal,
+
+ /// The currently selected search match index (1-based).
+ search_selected: SearchSelected,
+
+ /// The readonly state of the surface has changed.
+ readonly: Readonly,
+
+ /// Copy the effective title of the surface to the clipboard.
+ /// The effective title is the user-overridden title if set,
+ /// otherwise the terminal-set title.
+ copy_title_to_clipboard,
+
+ /// Sync with: ghostty_action_tag_e
+ pub const Key = enum(c_int) {
+ quit,
+ new_window,
+ new_tab,
+ close_tab,
+ new_split,
+ close_all_windows,
+ toggle_maximize,
+ toggle_fullscreen,
+ toggle_tab_overview,
+ toggle_window_decorations,
+ toggle_quick_terminal,
+ toggle_command_palette,
+ toggle_visibility,
+ toggle_background_opacity,
+ move_tab,
+ goto_tab,
+ goto_split,
+ goto_window,
+ resize_split,
+ equalize_splits,
+ toggle_split_zoom,
+ present_terminal,
+ size_limit,
+ reset_window_size,
+ initial_size,
+ cell_size,
+ scrollbar,
+ render,
+ inspector,
+ show_gtk_inspector,
+ render_inspector,
+ desktop_notification,
+ set_title,
+ set_tab_title,
+ prompt_title,
+ pwd,
+ mouse_shape,
+ mouse_visibility,
+ mouse_over_link,
+ renderer_health,
+ open_config,
+ quit_timer,
+ float_window,
+ secure_input,
+ key_sequence,
+ key_table,
+ color_change,
+ reload_config,
+ config_change,
+ close_window,
+ ring_bell,
+ undo,
+ redo,
+ check_for_updates,
+ open_url,
+ show_child_exited,
+ progress_report,
+ show_on_screen_keyboard,
+ command_finished,
+ start_search,
+ end_search,
+ search_total,
+ search_selected,
+ readonly,
+ copy_title_to_clipboard,
+
+ test "ghostty.h Action.Key" {
+ try lib.checkGhosttyHEnum(Key, "GHOSTTY_ACTION_");
+ }
+ };
+
+ /// Sync with: ghostty_action_u
+ pub const CValue = cvalue: {
+ const key_fields = @typeInfo(Key).@"enum".fields;
+ var union_fields: [key_fields.len]std.builtin.Type.UnionField = undefined;
+ for (key_fields, 0..) |field, i| {
+ const action = @unionInit(Action, field.name, undefined);
+ const Type = t: {
+ const Type = @TypeOf(@field(action, field.name));
+ // Types can provide custom types for their CValue.
+ if (Type != void and @hasDecl(Type, "C")) break :t Type.C;
+ break :t Type;
+ };
+
+ union_fields[i] = .{
+ .name = field.name,
+ .type = Type,
+ .alignment = @alignOf(Type),
+ };
+ }
+
+ break :cvalue @Type(.{ .@"union" = .{
+ .layout = .@"extern",
+ .tag_type = null,
+ .fields = &union_fields,
+ .decls = &.{},
+ } });
+ };
+
+ /// Sync with: ghostty_action_s
+ pub const C = extern struct {
+ key: Key,
+ value: CValue,
+ };
+
+ comptime {
+ // For ABI compatibility, we expect that this is our union size.
+ // At the time of writing, we don't promise ABI compatibility
+ // so we can change this but I want to be aware of it.
+ assert(@sizeOf(CValue) == switch (@sizeOf(usize)) {
+ 4 => 16,
+ 8 => 24,
+ else => unreachable,
+ });
+ }
+
+ /// Returns the value type for the given key.
+ pub fn Value(comptime key: Key) type {
+ inline for (@typeInfo(Action).@"union".fields) |field| {
+ const field_key = @field(Key, field.name);
+ if (field_key == key) return field.type;
+ }
+
+ unreachable;
+ }
+
+ /// Convert to ghostty_action_s.
+ pub fn cval(self: Action) C {
+ const value: CValue = switch (self) {
+ inline else => |v, tag| @unionInit(
+ CValue,
+ @tagName(tag),
+ if (@TypeOf(v) != void and @hasDecl(@TypeOf(v), "cval")) v.cval() else v,
+ ),
+ };
+
+ return .{
+ .key = @as(Key, self),
+ .value = value,
+ };
+ }
+};
+
+// This is made extern (c_int) to make interop easier with our embedded
+// runtime. The small size cost doesn't make a difference in our union.
+pub const SplitDirection = enum(c_int) {
+ right,
+ down,
+ left,
+ up,
+
+ test "ghostty.h SplitDirection" {
+ try lib.checkGhosttyHEnum(SplitDirection, "GHOSTTY_SPLIT_DIRECTION_");
+ }
+};
+
+// This is made extern (c_int) to make interop easier with our embedded
+// runtime. The small size cost doesn't make a difference in our union.
+pub const GotoSplit = enum(c_int) {
+ previous,
+ next,
+
+ up,
+ left,
+ down,
+ right,
+
+ test "ghostty.h GotoSplit" {
+ try lib.checkGhosttyHEnum(GotoSplit, "GHOSTTY_GOTO_SPLIT_");
+ }
+};
+
+// This is made extern (c_int) to make interop easier with our embedded
+// runtime. The small size cost doesn't make a difference in our union.
+pub const GotoWindow = enum(c_int) {
+ previous,
+ next,
+
+ test "ghostty.h GotoWindow" {
+ try lib.checkGhosttyHEnum(GotoWindow, "GHOSTTY_GOTO_WINDOW_");
+ }
+};
+
+/// The amount to resize the split by and the direction to resize it in.
+pub const ResizeSplit = extern struct {
+ amount: u16,
+ direction: Direction,
+
+ pub const Direction = enum(c_int) {
+ up,
+ down,
+ left,
+ right,
+
+ test "ghostty.h ResizeSplit.Direction" {
+ try lib.checkGhosttyHEnum(Direction, "GHOSTTY_RESIZE_SPLIT_");
+ }
+ };
+};
+
+pub const MoveTab = extern struct {
+ amount: isize,
+};
+
+/// The tab to jump to. This is non-exhaustive so that integer values represent
+/// the index (zero-based) of the tab to jump to. Negative values are special
+/// values.
+pub const GotoTab = enum(c_int) {
+ previous = -1,
+ next = -2,
+ last = -3,
+ _,
+
+ // TODO: check non-exhaustive enums
+ // test "ghostty.h GotoTab" {
+ // try lib.checkGhosttyHEnum(GotoTab, "GHOSTTY_GOTO_TAB_");
+ // }
+};
+
+/// The fullscreen mode to toggle to if we're moving to fullscreen.
+pub const Fullscreen = enum(c_int) {
+ native,
+
+ /// macOS has a non-native fullscreen mode that is more like a maximized
+ /// window. This is much faster to enter and exit than the native mode.
+ macos_non_native,
+ macos_non_native_visible_menu,
+ macos_non_native_padded_notch,
+
+ test "ghostty.h Fullscreen" {
+ try lib.checkGhosttyHEnum(Fullscreen, "GHOSTTY_FULLSCREEN_");
+ }
+};
+
+pub const FloatWindow = enum(c_int) {
+ on,
+ off,
+ toggle,
+
+ test "ghostty.h FloatWindow" {
+ try lib.checkGhosttyHEnum(FloatWindow, "GHOSTTY_FLOAT_WINDOW_");
+ }
+};
+
+pub const SecureInput = enum(c_int) {
+ on,
+ off,
+ toggle,
+
+ test "ghostty.h SecureInput" {
+ try lib.checkGhosttyHEnum(SecureInput, "GHOSTTY_SECURE_INPUT_");
+ }
+};
+
+/// The inspector mode to toggle to if we're toggling the inspector.
+pub const Inspector = enum(c_int) {
+ toggle,
+ show,
+ hide,
+
+ test "ghostty.h Inspector" {
+ try lib.checkGhosttyHEnum(Inspector, "GHOSTTY_INSPECTOR_");
+ }
+};
+
+pub const QuitTimer = enum(c_int) {
+ start,
+ stop,
+
+ test "ghostty.h QuitTimer" {
+ try lib.checkGhosttyHEnum(QuitTimer, "GHOSTTY_QUIT_TIMER_");
+ }
+};
+
+pub const Readonly = enum(c_int) {
+ off,
+ on,
+
+ test "ghostty.h Readonly" {
+ try lib.checkGhosttyHEnum(Readonly, "GHOSTTY_READONLY_");
+ }
+};
+
+pub const MouseVisibility = enum(c_int) {
+ visible,
+ hidden,
+
+ test "ghostty.h MouseVisibility" {
+ try lib.checkGhosttyHEnum(MouseVisibility, "GHOSTTY_MOUSE_");
+ }
+};
+
+/// Whether to prompt for the surface title or tab title.
+pub const PromptTitle = enum(c_int) {
+ surface,
+ tab,
+
+ test "ghostty.h PromptTitle" {
+ try lib.checkGhosttyHEnum(PromptTitle, "GHOSTTY_PROMPT_TITLE_");
+ }
+};
+
+pub const MouseOverLink = struct {
+ url: [:0]const u8,
+
+ // Sync with: ghostty_action_mouse_over_link_s
+ pub const C = extern struct {
+ url: [*]const u8,
+ len: usize,
+ };
+
+ pub fn cval(self: MouseOverLink) C {
+ return .{
+ .url = self.url.ptr,
+ .len = self.url.len,
+ };
+ }
+};
+
+pub const SizeLimit = extern struct {
+ min_width: u32,
+ min_height: u32,
+ max_width: u32,
+ max_height: u32,
+};
+
+pub const InitialSize = extern struct {
+ width: u32,
+ height: u32,
+
+ /// Make this a valid gobject if we're in a GTK environment.
+ pub const getGObjectType = switch (build_config.app_runtime) {
+ .gtk => @import("gobject").ext.defineBoxed(
+ InitialSize,
+ .{ .name = "GhosttyApprtInitialSize" },
+ ),
+
+ .none => void,
+ };
+};
+
+pub const CellSize = extern struct {
+ width: u32,
+ height: u32,
+};
+
+pub const SetTitle = struct {
+ title: [:0]const u8,
+
+ // Sync with: ghostty_action_set_title_s
+ pub const C = extern struct {
+ title: [*:0]const u8,
+ };
+
+ pub fn cval(self: SetTitle) C {
+ return .{
+ .title = self.title.ptr,
+ };
+ }
+
+ pub fn format(
+ value: @This(),
+ comptime _: []const u8,
+ _: std.fmt.FormatOptions,
+ writer: *std.Io.Writer,
+ ) !void {
+ try writer.print("{s}{{ {s} }}", .{ @typeName(@This()), value.title });
+ }
+};
+
+pub const Pwd = struct {
+ pwd: [:0]const u8,
+
+ // Sync with: ghostty_action_set_pwd_s
+ pub const C = extern struct {
+ pwd: [*:0]const u8,
+ };
+
+ pub fn cval(self: Pwd) C {
+ return .{
+ .pwd = self.pwd.ptr,
+ };
+ }
+
+ pub fn format(
+ value: @This(),
+ comptime _: []const u8,
+ _: std.fmt.FormatOptions,
+ writer: *std.Io.Writer,
+ ) !void {
+ try writer.print("{s}{{ {s} }}", .{ @typeName(@This()), value.pwd });
+ }
+};
+
+/// The desktop notification to show.
+pub const DesktopNotification = struct {
+ title: [:0]const u8,
+ body: [:0]const u8,
+
+ // Sync with: ghostty_action_desktop_notification_s
+ pub const C = extern struct {
+ title: [*:0]const u8,
+ body: [*:0]const u8,
+ };
+
+ pub fn cval(self: DesktopNotification) C {
+ return .{
+ .title = self.title.ptr,
+ .body = self.body.ptr,
+ };
+ }
+
+ pub fn format(
+ value: @This(),
+ comptime _: []const u8,
+ _: std.fmt.FormatOptions,
+ writer: *std.Io.Writer,
+ ) !void {
+ try writer.print("{s}{{ title: {s}, body: {s} }}", .{
+ @typeName(@This()),
+ value.title,
+ value.body,
+ });
+ }
+};
+
+pub const KeySequence = union(enum) {
+ trigger: input.Trigger,
+ end,
+
+ // Sync with: ghostty_action_key_sequence_s
+ pub const C = extern struct {
+ active: bool,
+ trigger: input.Trigger.C,
+ };
+
+ pub fn cval(self: KeySequence) C {
+ return switch (self) {
+ .trigger => |t| .{ .active = true, .trigger = t.cval() },
+ .end => .{ .active = false, .trigger = .{} },
+ };
+ }
+};
+
+pub const KeyTable = union(enum) {
+ activate: []const u8,
+ deactivate,
+ deactivate_all,
+
+ // Sync with: ghostty_action_key_table_tag_e
+ pub const Tag = enum(c_int) {
+ activate,
+ deactivate,
+ deactivate_all,
+ };
+
+ // Sync with: ghostty_action_key_table_u
+ pub const CValue = extern union {
+ activate: extern struct {
+ name: [*]const u8,
+ len: usize,
+ },
+ };
+
+ // Sync with: ghostty_action_key_table_s
+ pub const C = extern struct {
+ tag: Tag,
+ value: CValue,
+ };
+
+ pub fn cval(self: KeyTable) C {
+ return switch (self) {
+ .activate => |name| .{
+ .tag = .activate,
+ .value = .{ .activate = .{ .name = name.ptr, .len = name.len } },
+ },
+ .deactivate => .{
+ .tag = .deactivate,
+ .value = undefined,
+ },
+ .deactivate_all => .{
+ .tag = .deactivate_all,
+ .value = undefined,
+ },
+ };
+ }
+};
+
+pub const ColorChange = extern struct {
+ kind: ColorKind,
+ r: u8,
+ g: u8,
+ b: u8,
+};
+
+pub const ColorKind = enum(c_int) {
+ // Negative numbers indicate some named kind
+ foreground = -1,
+ background = -2,
+ cursor = -3,
+
+ // 0+ values indicate a palette index
+ _,
+
+ // TODO: check non-non-exhaustive enums
+ // test "ghostty.h ColorKind" {
+ // try lib.checkGhosttyHEnum(ColorKind, "GHOSTTY_COLOR_KIND_");
+ // }
+};
+
+pub const ReloadConfig = extern struct {
+ /// A soft reload means that the configuration doesn't need to be
+ /// read off disk, but libghostty needs the full config again so call
+ /// updateConfig with it.
+ soft: bool = false,
+};
+
+pub const ConfigChange = struct {
+ config: *const configpkg.Config,
+
+ // Sync with: ghostty_action_config_change_s
+ pub const C = extern struct {
+ config: *const configpkg.Config,
+ };
+
+ pub fn cval(self: ConfigChange) C {
+ return .{
+ .config = self.config,
+ };
+ }
+};
+
+/// Open a URL
+pub const OpenUrl = struct {
+ /// The type of data that the URL refers to.
+ kind: Kind,
+
+ /// The URL.
+ url: []const u8,
+
+ /// The type of the data at the URL to open. This is used as a hint to
+ /// potentially open the URL in a different way.
+ ///
+ /// Sync with: ghostty_action_open_url_kind_e
+ pub const Kind = enum(c_int) {
+ /// The type is unknown. This is the default and apprts should
+ /// open the URL in the most generic way possible. For example,
+ /// on macOS this would be the equivalent of `open` or on Linux
+ /// this would be `xdg-open`.
+ unknown,
+
+ /// The URL is known to be a text file. In this case, the apprt
+ /// should try to open the URL in a text editor or viewer or
+ /// some equivalent, if possible.
+ text,
+
+ /// The URL is known to contain HTML content.
+ html,
+
+ test "ghostty.h OpenUrl.Kind" {
+ try lib.checkGhosttyHEnum(Kind, "GHOSTTY_ACTION_OPEN_URL_KIND_");
+ }
+ };
+
+ // Sync with: ghostty_action_open_url_s
+ pub const C = extern struct {
+ kind: Kind,
+ url: [*]const u8,
+ len: usize,
+ };
+
+ pub fn cval(self: OpenUrl) C {
+ return .{
+ .kind = self.kind,
+ .url = self.url.ptr,
+ .len = self.url.len,
+ };
+ }
+};
+
+/// sync with ghostty_action_close_tab_mode_e in ghostty.h
+pub const CloseTabMode = enum(c_int) {
+ /// Close the current tab.
+ this,
+ /// Close all other tabs.
+ other,
+ /// Close all tabs to the right of the current tab.
+ right,
+
+ test "ghostty.h CloseTabMode" {
+ try lib.checkGhosttyHEnum(CloseTabMode, "GHOSTTY_ACTION_CLOSE_TAB_MODE_");
+ }
+};
+
+pub const CommandFinished = struct {
+ exit_code: ?u8,
+ duration: configpkg.Config.Duration,
+
+ /// sync with ghostty_action_command_finished_s in ghostty.h
+ pub const C = extern struct {
+ exit_code: i16,
+ duration: u64,
+ };
+
+ pub fn cval(self: CommandFinished) C {
+ return .{
+ .exit_code = self.exit_code orelse -1,
+ .duration = self.duration.duration,
+ };
+ }
+};
+
+pub const StartSearch = struct {
+ needle: [:0]const u8,
+
+ // Sync with: ghostty_action_start_search_s
+ pub const C = extern struct {
+ needle: [*:0]const u8,
+ };
+
+ pub fn cval(self: StartSearch) C {
+ return .{
+ .needle = self.needle.ptr,
+ };
+ }
+};
+
+pub const SearchTotal = struct {
+ total: ?usize,
+
+ // Sync with: ghostty_action_search_total_s
+ pub const C = extern struct {
+ total: isize,
+ };
+
+ pub fn cval(self: SearchTotal) C {
+ return .{
+ .total = if (self.total) |t| @intCast(t) else -1,
+ };
+ }
+};
+
+pub const SearchSelected = struct {
+ selected: ?usize,
+
+ // Sync with: ghostty_action_search_selected_s
+ pub const C = extern struct {
+ selected: isize,
+ };
+
+ pub fn cval(self: SearchSelected) C {
+ return .{
+ .selected = if (self.selected) |s| @intCast(s) else -1,
+ };
+ }
+};
+
+test {
+ _ = std.testing.refAllDeclsRecursive(@This());
+}
diff --git a/vendor/libghostty-vt/src/apprt/browser.zig b/vendor/libghostty-vt/src/apprt/browser.zig
new file mode 100644
index 00000000..3b1aa468
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/browser.zig
@@ -0,0 +1,4 @@
+const internal_os = @import("../os/main.zig");
+pub const resourcesDir = internal_os.resourcesDir;
+pub const App = struct {};
+pub const Window = struct {};
diff --git a/vendor/libghostty-vt/src/apprt/embedded.zig b/vendor/libghostty-vt/src/apprt/embedded.zig
new file mode 100644
index 00000000..519a35f2
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/embedded.zig
@@ -0,0 +1,2231 @@
+//! Application runtime for the embedded version of Ghostty. The embedded
+//! version is when Ghostty is embedded within a parent host application,
+//! rather than owning the application lifecycle itself. This is used for
+//! example for the macOS build of Ghostty so that we can use a native
+//! Swift+XCode-based application.
+
+const std = @import("std");
+const builtin = @import("builtin");
+const assert = @import("../quirks.zig").inlineAssert;
+const Allocator = std.mem.Allocator;
+const objc = @import("objc");
+const apprt = @import("../apprt.zig");
+const font = @import("../font/main.zig");
+const input = @import("../input.zig");
+const internal_os = @import("../os/main.zig");
+const renderer = @import("../renderer.zig");
+const terminal = @import("../terminal/main.zig");
+const CoreApp = @import("../App.zig");
+const CoreInspector = @import("../inspector/main.zig").Inspector;
+const CoreSurface = @import("../Surface.zig");
+const configpkg = @import("../config.zig");
+const Config = configpkg.Config;
+
+const log = std.log.scoped(.embedded_window);
+
+pub const resourcesDir = internal_os.resourcesDir;
+
+pub const App = struct {
+ /// Because we only expect the embedding API to be used in embedded
+ /// environments, the options are extern so that we can expose it
+ /// directly to a C callconv and not pay for any translation costs.
+ ///
+ /// C type: ghostty_runtime_config_s
+ pub const Options = extern struct {
+ /// These are just aliases to make the function signatures below
+ /// more obvious what values will be sent.
+ const AppUD = ?*anyopaque;
+ const SurfaceUD = ?*anyopaque;
+
+ /// Userdata that is passed to all the callbacks.
+ userdata: AppUD = null,
+
+ /// True if the selection clipboard is supported.
+ supports_selection_clipboard: bool = false,
+
+ /// Callback called to wakeup the event loop. This should trigger
+ /// a full tick of the app loop.
+ wakeup: *const fn (AppUD) callconv(.c) void,
+
+ /// Callback called to handle an action.
+ action: *const fn (*App, apprt.Target.C, apprt.Action.C) callconv(.c) bool,
+
+ /// Read the clipboard value. Returns true if the clipboard request
+ /// was started and complete_clipboard_request may be called with the
+ /// given state pointer. Returns false if the clipboard request couldn't
+ /// be started (such as when no text is available for a paste request).
+ read_clipboard: *const fn (SurfaceUD, c_int, *apprt.ClipboardRequest) callconv(.c) bool,
+
+ /// This may be called after a read clipboard call to request
+ /// confirmation that the clipboard value is safe to read. The embedder
+ /// must call complete_clipboard_request with the given request.
+ confirm_read_clipboard: *const fn (
+ SurfaceUD,
+ [*:0]const u8,
+ *apprt.ClipboardRequest,
+ apprt.ClipboardRequestType,
+ ) callconv(.c) void,
+
+ /// Write the clipboard value.
+ write_clipboard: *const fn (
+ SurfaceUD,
+ c_int,
+ [*]const CAPI.ClipboardContent,
+ usize,
+ bool,
+ ) callconv(.c) void,
+
+ /// Close the current surface given by this function.
+ close_surface: ?*const fn (SurfaceUD, bool) callconv(.c) void = null,
+ };
+
+ /// This is the key event sent for ghostty_surface_key and
+ /// ghostty_app_key.
+ pub const KeyEvent = struct {
+ action: input.Action,
+ mods: input.Mods,
+ consumed_mods: input.Mods,
+ keycode: u32,
+ text: ?[:0]const u8,
+ unshifted_codepoint: u32,
+ composing: bool,
+
+ /// Convert a libghostty key event into a core key event.
+ fn core(self: KeyEvent) ?input.KeyEvent {
+ const text: []const u8 = if (self.text) |v| v else "";
+ const unshifted_codepoint: u21 = std.math.cast(
+ u21,
+ self.unshifted_codepoint,
+ ) orelse 0;
+
+ // We want to get the physical unmapped key to process keybinds.
+ const physical_key = keycode: for (input.keycodes.entries) |entry| {
+ if (entry.native == self.keycode) break :keycode entry.key;
+ } else .unidentified;
+
+ // Build our final key event
+ return .{
+ .action = self.action,
+ .key = physical_key,
+ .mods = self.mods,
+ .consumed_mods = self.consumed_mods,
+ .composing = self.composing,
+ .utf8 = text,
+ .unshifted_codepoint = unshifted_codepoint,
+ };
+ }
+ };
+
+ core_app: *CoreApp,
+ opts: Options,
+ keymap: input.Keymap,
+
+ /// The configuration for the app. This is owned by this structure.
+ config: Config,
+
+ pub fn init(
+ self: *App,
+ core_app: *CoreApp,
+ config: *const Config,
+ opts: Options,
+ ) !void {
+ // We have to clone the config.
+ const alloc = core_app.alloc;
+ var config_clone = try config.clone(alloc);
+ errdefer config_clone.deinit();
+
+ var keymap = try input.Keymap.init();
+ errdefer keymap.deinit();
+
+ self.* = .{
+ .core_app = core_app,
+ .config = config_clone,
+ .opts = opts,
+ .keymap = keymap,
+ };
+ }
+
+ pub fn terminate(self: *App) void {
+ self.keymap.deinit();
+ self.config.deinit();
+ }
+
+ /// Returns true if there are any global keybinds in the configuration.
+ pub fn hasGlobalKeybinds(self: *const App) bool {
+ var it = self.config.keybind.set.bindings.iterator();
+ while (it.next()) |entry| {
+ switch (entry.value_ptr.*) {
+ .leader => {},
+ inline .leaf, .leaf_chained => |leaf| if (leaf.flags.global) return true,
+ }
+ }
+
+ return false;
+ }
+
+ /// The target of a key event. This is used to determine some subtly
+ /// different behavior between app and surface key events.
+ pub const KeyTarget = union(enum) {
+ app,
+ surface: *Surface,
+ };
+
+ /// See CoreApp.focusEvent
+ pub fn focusEvent(self: *App, focused: bool) void {
+ self.core_app.focusEvent(focused);
+ }
+
+ /// See CoreApp.keyEvent.
+ pub fn keyEvent(
+ self: *App,
+ target: KeyTarget,
+ event: KeyEvent,
+ ) !bool {
+ // Convert our C key event into a Zig one.
+ const input_event: input.KeyEvent = event.core() orelse
+ return false;
+
+ // Invoke the core Ghostty logic to handle this input.
+ const effect: CoreSurface.InputEffect = switch (target) {
+ .app => if (self.core_app.keyEvent(
+ self,
+ input_event,
+ )) .consumed else .ignored,
+
+ .surface => |surface| try surface.core_surface.keyCallback(
+ input_event,
+ ),
+ };
+
+ return switch (effect) {
+ .closed => true,
+ .ignored => false,
+ .consumed => true,
+ };
+ }
+
+ /// This should be called whenever the keyboard layout was changed.
+ pub fn reloadKeymap(self: *App) !void {
+ // Reload the keymap
+ try self.keymap.reload();
+ }
+
+ /// Loads the keyboard layout.
+ ///
+ /// Kind of expensive so this should be avoided if possible. When I say
+ /// "kind of expensive" I mean that its not something you probably want
+ /// to run on every keypress.
+ pub fn keyboardLayout(self: *const App) input.KeyboardLayout {
+ // We only support keyboard layout detection on macOS.
+ if (comptime builtin.os.tag != .macos) return .unknown;
+
+ // Any layout larger than this is not something we can handle.
+ var buf: [256]u8 = undefined;
+ const id = self.keymap.sourceId(&buf) catch |err| {
+ comptime assert(@TypeOf(err) == error{OutOfMemory});
+ return .unknown;
+ };
+
+ return input.KeyboardLayout.mapAppleId(id) orelse .unknown;
+ }
+
+ pub fn wakeup(self: *const App) void {
+ self.opts.wakeup(self.opts.userdata);
+ }
+
+ pub fn wait(self: *const App) !void {
+ _ = self;
+ }
+
+ /// Create a new surface for the app.
+ fn newSurface(self: *App, opts: Surface.Options) !*Surface {
+ // Grab a surface allocation because we're going to need it.
+ var surface = try self.core_app.alloc.create(Surface);
+ errdefer self.core_app.alloc.destroy(surface);
+
+ // Create the surface
+ try surface.init(self, opts);
+ errdefer surface.deinit();
+
+ return surface;
+ }
+
+ /// Close the given surface.
+ pub fn closeSurface(self: *App, surface: *Surface) void {
+ surface.deinit();
+ self.core_app.alloc.destroy(surface);
+ }
+
+ pub fn redrawInspector(self: *App, surface: *Surface) void {
+ _ = self;
+ surface.queueInspectorRender();
+ }
+
+ /// Perform a given action. Returns `true` if the action was able to be
+ /// performed, `false` otherwise.
+ pub fn performAction(
+ self: *App,
+ target: apprt.Target,
+ comptime action: apprt.Action.Key,
+ value: apprt.Action.Value(action),
+ ) !bool {
+ // Special case certain actions before they are sent to the
+ // embedded apprt.
+ self.performPreAction(target, action, value);
+
+ log.debug("dispatching action target={t} action={} value={any}", .{
+ target,
+ action,
+ value,
+ });
+ return self.opts.action(
+ self,
+ target.cval(),
+ @unionInit(apprt.Action, @tagName(action), value).cval(),
+ );
+ }
+
+ fn performPreAction(
+ self: *App,
+ target: apprt.Target,
+ comptime action: apprt.Action.Key,
+ value: apprt.Action.Value(action),
+ ) void {
+ // Special case certain actions before they are sent to the embedder
+ switch (action) {
+ .set_title => switch (target) {
+ .app => {},
+ .surface => |surface| {
+ // Dupe the title so that we can store it. If we get an allocation
+ // error we just ignore it, since this only breaks a few minor things.
+ const alloc = self.core_app.alloc;
+ if (surface.rt_surface.title) |v| alloc.free(v);
+ surface.rt_surface.title = alloc.dupeZ(u8, value.title) catch null;
+ },
+ },
+
+ .config_change => switch (target) {
+ .surface => {},
+
+ // For app updates, we update our core config. We need to
+ // clone it because the caller owns the param.
+ .app => if (value.config.clone(self.core_app.alloc)) |config| {
+ self.config.deinit();
+ self.config = config;
+ } else |err| {
+ log.err("error updating app config err={}", .{err});
+ },
+ },
+
+ else => {},
+ }
+ }
+
+ /// Send the given IPC to a running Ghostty. Returns `true` if the action was
+ /// able to be performed, `false` otherwise.
+ ///
+ /// Note that this is a static function. Since this is called from a CLI app (or
+ /// some other process that is not Ghostty) there is no full-featured apprt App
+ /// to use.
+ pub fn performIpc(
+ _: Allocator,
+ _: apprt.ipc.Target,
+ comptime action: apprt.ipc.Action.Key,
+ _: apprt.ipc.Action.Value(action),
+ ) (Allocator.Error || std.posix.WriteError || apprt.ipc.Errors)!bool {
+ switch (action) {
+ .new_window => return false,
+ }
+ }
+};
+
+/// Platform-specific configuration for libghostty.
+pub const Platform = union(PlatformTag) {
+ macos: MacOS,
+ ios: IOS,
+
+ // If our build target for libghostty is not darwin then we do
+ // not include macos support at all.
+ pub const MacOS = if (builtin.target.os.tag.isDarwin()) struct {
+ /// The view to render the surface on.
+ nsview: objc.Object,
+ } else void;
+
+ pub const IOS = if (builtin.target.os.tag.isDarwin()) struct {
+ /// The view to render the surface on.
+ uiview: objc.Object,
+ } else void;
+
+ // The C ABI compatible version of this union. The tag is expected
+ // to be stored elsewhere.
+ pub const C = extern union {
+ macos: extern struct {
+ nsview: ?*anyopaque,
+ },
+
+ ios: extern struct {
+ uiview: ?*anyopaque,
+ },
+ };
+
+ /// Initialize a Platform a tag and configuration from the C ABI.
+ pub fn init(tag_int: c_int, c_platform: C) !Platform {
+ const tag = try std.meta.intToEnum(PlatformTag, tag_int);
+ return switch (tag) {
+ .macos => if (MacOS != void) macos: {
+ const config = c_platform.macos;
+ const nsview = objc.Object.fromId(config.nsview orelse
+ break :macos error.NSViewMustBeSet);
+ break :macos .{ .macos = .{ .nsview = nsview } };
+ } else error.UnsupportedPlatform,
+
+ .ios => if (IOS != void) ios: {
+ const config = c_platform.ios;
+ const uiview = objc.Object.fromId(config.uiview orelse
+ break :ios error.UIViewMustBeSet);
+ break :ios .{ .ios = .{ .uiview = uiview } };
+ } else error.UnsupportedPlatform,
+ };
+ }
+};
+
+pub const PlatformTag = enum(c_int) {
+ // "0" is reserved for invalid so we can detect unset values
+ // from the C API.
+
+ macos = 1,
+ ios = 2,
+};
+
+pub const EnvVar = extern struct {
+ /// The name of the environment variable.
+ key: [*:0]const u8,
+
+ /// The value of the environment variable.
+ value: [*:0]const u8,
+};
+
+pub const Surface = struct {
+ app: *App,
+ platform: Platform,
+ userdata: ?*anyopaque = null,
+ core_surface: CoreSurface,
+ content_scale: apprt.ContentScale,
+ size: apprt.SurfaceSize,
+ cursor_pos: apprt.CursorPos,
+ inspector: ?*Inspector = null,
+
+ /// The current title of the surface. The embedded apprt saves this so
+ /// that getTitle works without the implementer needing to save it.
+ title: ?[:0]const u8 = null,
+
+ /// Surface initialization options.
+ pub const Options = extern struct {
+ /// The platform that this surface is being initialized for and
+ /// the associated platform-specific configuration.
+ platform_tag: c_int = 0,
+ platform: Platform.C = undefined,
+
+ /// Userdata passed to some of the callbacks.
+ userdata: ?*anyopaque = null,
+
+ /// The scale factor of the screen.
+ scale_factor: f64 = 1,
+
+ /// The font size to inherit. If 0, default font size will be used.
+ font_size: f32 = 0,
+
+ /// The working directory to load into.
+ working_directory: ?[*:0]const u8 = null,
+
+ /// The command to run in the new surface. If this is set then
+ /// the "wait-after-command" option is also automatically set to true,
+ /// since this is used for scripting.
+ ///
+ /// This command always run in a shell (e.g. via `/bin/sh -c`),
+ /// despite Ghostty allowing directly executed commands via config.
+ /// This is a legacy thing and we should probably change it in the
+ /// future once we have a concrete use case.
+ command: ?[*:0]const u8 = null,
+
+ /// Extra environment variables to set for the surface.
+ env_vars: ?[*]EnvVar = null,
+ env_var_count: usize = 0,
+
+ /// Input to send to the command after it is started.
+ initial_input: ?[*:0]const u8 = null,
+
+ /// Wait after the command exits
+ wait_after_command: bool = false,
+
+ /// Context for the new surface
+ context: apprt.surface.NewSurfaceContext = .window,
+ };
+
+ pub fn init(self: *Surface, app: *App, opts: Options) !void {
+ self.* = .{
+ .app = app,
+ .platform = try .init(opts.platform_tag, opts.platform),
+ .userdata = opts.userdata,
+ .core_surface = undefined,
+ .content_scale = .{
+ .x = @floatCast(opts.scale_factor),
+ .y = @floatCast(opts.scale_factor),
+ },
+ .size = .{ .width = 800, .height = 600 },
+ .cursor_pos = .{ .x = -1, .y = -1 },
+ };
+
+ // Add ourselves to the list of surfaces on the app.
+ try app.core_app.addSurface(self);
+ errdefer app.core_app.deleteSurface(self);
+
+ // Shallow copy the config so that we can modify it.
+ var config = try apprt.surface.newConfig(app.core_app, &app.config, opts.context);
+ defer config.deinit();
+
+ // If we have a working directory from the options then we set it.
+ if (opts.working_directory) |c_wd| {
+ const wd = std.mem.sliceTo(c_wd, 0);
+ if (wd.len > 0) wd: {
+ var dir = std.fs.openDirAbsolute(wd, .{}) catch |err| {
+ log.warn(
+ "error opening requested working directory dir={s} err={}",
+ .{ wd, err },
+ );
+ break :wd;
+ };
+ defer dir.close();
+
+ const stat = dir.stat() catch |err| {
+ log.warn(
+ "failed to stat requested working directory dir={s} err={}",
+ .{ wd, err },
+ );
+ break :wd;
+ };
+
+ if (stat.kind != .directory) {
+ log.warn(
+ "requested working directory is not a directory dir={s}",
+ .{wd},
+ );
+ break :wd;
+ }
+
+ var wd_val: configpkg.WorkingDirectory = .{ .path = wd };
+ if (wd_val.finalize(config.arenaAlloc())) |_| {
+ config.@"working-directory" = wd_val;
+ } else |err| {
+ log.warn(
+ "error finalizing working directory config dir={s} err={}",
+ .{ wd_val.path, err },
+ );
+ }
+ }
+ }
+
+ // If we have a command from the options then we set it.
+ if (opts.command) |c_command| {
+ const cmd = std.mem.sliceTo(c_command, 0);
+ if (cmd.len > 0) {
+ config.command = .{ .shell = cmd };
+ config.@"wait-after-command" = true;
+ }
+ }
+
+ // Apply any environment variables that were requested.
+ if (opts.env_var_count > 0) {
+ const alloc = config.arenaAlloc();
+ for (opts.env_vars.?[0..opts.env_var_count]) |env_var| {
+ const key = std.mem.sliceTo(env_var.key, 0);
+ const value = std.mem.sliceTo(env_var.value, 0);
+ try config.env.map.put(
+ alloc,
+ try alloc.dupeZ(u8, key),
+ try alloc.dupeZ(u8, value),
+ );
+ }
+ }
+
+ // If we have an initial input then we set it.
+ if (opts.initial_input) |c_input| {
+ const alloc = config.arenaAlloc();
+
+ // We need to escape the string because the "raw" field
+ // expects a Zig string.
+ var buf: std.Io.Writer.Allocating = .init(alloc);
+ defer buf.deinit();
+ try std.zig.stringEscape(
+ std.mem.sliceTo(c_input, 0),
+ &buf.writer,
+ );
+
+ config.input.list.clearRetainingCapacity();
+ try config.input.list.append(
+ alloc,
+ .{ .raw = try buf.toOwnedSliceSentinel(0) },
+ );
+ }
+
+ // Wait after command
+ if (opts.wait_after_command) {
+ config.@"wait-after-command" = true;
+ }
+
+ // Initialize our surface right away. We're given a view that is
+ // ready to use.
+ try self.core_surface.init(
+ app.core_app.alloc,
+ &config,
+ app.core_app,
+ app,
+ self,
+ );
+ errdefer self.core_surface.deinit();
+
+ // If our options requested a specific font-size, set that.
+ if (opts.font_size != 0) {
+ var font_size = self.core_surface.font_size;
+ font_size.points = opts.font_size;
+ try self.core_surface.setFontSize(font_size);
+ }
+ }
+
+ pub fn deinit(self: *Surface) void {
+ // Shut down our inspector
+ self.freeInspector();
+
+ // Free our title
+ if (self.title) |v| self.app.core_app.alloc.free(v);
+
+ // Remove ourselves from the list of known surfaces in the app.
+ self.app.core_app.deleteSurface(self);
+
+ // Clean up our core surface so that all the rendering and IO stop.
+ self.core_surface.deinit();
+ }
+
+ /// Initialize the inspector instance. A surface can only have one
+ /// inspector at any given time, so this will return the previous inspector
+ /// if it was already initialized.
+ pub fn initInspector(self: *Surface) !*Inspector {
+ if (self.inspector) |v| return v;
+
+ const alloc = self.app.core_app.alloc;
+ const inspector = try alloc.create(Inspector);
+ errdefer alloc.destroy(inspector);
+ inspector.* = try .init(self);
+ self.inspector = inspector;
+ return inspector;
+ }
+
+ pub fn freeInspector(self: *Surface) void {
+ if (self.inspector) |v| {
+ v.deinit();
+ self.app.core_app.alloc.destroy(v);
+ self.inspector = null;
+ }
+ }
+
+ pub fn core(self: *Surface) *CoreSurface {
+ return &self.core_surface;
+ }
+
+ pub fn rtApp(self: *const Surface) *App {
+ return self.app;
+ }
+
+ pub fn close(self: *const Surface, process_alive: bool) void {
+ const func = self.app.opts.close_surface orelse {
+ log.info("runtime embedder does not support closing a surface", .{});
+ return;
+ };
+
+ func(self.userdata, process_alive);
+ }
+
+ pub fn getContentScale(self: *const Surface) !apprt.ContentScale {
+ return self.content_scale;
+ }
+
+ pub fn getSize(self: *const Surface) !apprt.SurfaceSize {
+ return self.size;
+ }
+
+ pub fn getTitle(self: *Surface) ?[:0]const u8 {
+ return self.title;
+ }
+
+ pub fn supportsClipboard(
+ self: *const Surface,
+ clipboard_type: apprt.Clipboard,
+ ) bool {
+ return switch (clipboard_type) {
+ .standard => true,
+ .selection, .primary => self.app.opts.supports_selection_clipboard,
+ };
+ }
+
+ pub fn clipboardRequest(
+ self: *Surface,
+ clipboard_type: apprt.Clipboard,
+ state: apprt.ClipboardRequest,
+ ) !bool {
+ // We need to allocate to get a pointer to store our clipboard request
+ // so that it is stable until the read_clipboard callback and call
+ // complete_clipboard_request. This sucks but clipboard requests aren't
+ // high throughput so it's probably fine.
+ const alloc = self.app.core_app.alloc;
+ const state_ptr = try alloc.create(apprt.ClipboardRequest);
+ errdefer alloc.destroy(state_ptr);
+ state_ptr.* = state;
+
+ const started = self.app.opts.read_clipboard(
+ self.userdata,
+ @intCast(@intFromEnum(clipboard_type)),
+ state_ptr,
+ );
+ if (!started) {
+ alloc.destroy(state_ptr);
+ return false;
+ }
+
+ return true;
+ }
+
+ fn completeClipboardRequest(
+ self: *Surface,
+ str: [:0]const u8,
+ state: *apprt.ClipboardRequest,
+ confirmed: bool,
+ ) void {
+ const alloc = self.app.core_app.alloc;
+
+ // Attempt to complete the request, but we may request
+ // confirmation.
+ self.core_surface.completeClipboardRequest(
+ state.*,
+ str,
+ confirmed,
+ ) catch |err| switch (err) {
+ error.UnsafePaste,
+ error.UnauthorizedPaste,
+ => {
+ self.app.opts.confirm_read_clipboard(
+ self.userdata,
+ str.ptr,
+ state,
+ state.*,
+ );
+
+ return;
+ },
+
+ else => log.err("error completing clipboard request err={}", .{err}),
+ };
+
+ // We don't defer this because the clipboard confirmation route
+ // preserves the clipboard request.
+ alloc.destroy(state);
+ }
+
+ pub fn setClipboard(
+ self: *const Surface,
+ clipboard_type: apprt.Clipboard,
+ contents: []const apprt.ClipboardContent,
+ confirm: bool,
+ ) !void {
+ const alloc = self.app.core_app.alloc;
+ const array = try alloc.alloc(CAPI.ClipboardContent, contents.len);
+ defer alloc.free(array);
+ for (contents, 0..) |content, i| {
+ array[i] = .{
+ .mime = content.mime,
+ .data = content.data,
+ };
+ }
+
+ self.app.opts.write_clipboard(
+ self.userdata,
+ @intCast(@intFromEnum(clipboard_type)),
+ array.ptr,
+ array.len,
+ confirm,
+ );
+ }
+
+ pub fn getCursorPos(self: *const Surface) !apprt.CursorPos {
+ return self.cursor_pos;
+ }
+
+ pub fn refresh(self: *Surface) void {
+ self.core_surface.refreshCallback() catch |err| {
+ log.err("error in refresh callback err={}", .{err});
+ return;
+ };
+ }
+
+ pub fn draw(self: *Surface) void {
+ self.core_surface.draw() catch |err| {
+ log.err("error in draw err={}", .{err});
+ return;
+ };
+ }
+
+ pub fn updateContentScale(self: *Surface, x: f64, y: f64) void {
+ // We are an embedded API so the caller can send us all sorts of
+ // garbage. We want to make sure that the float values are valid
+ // and we don't want to support fractional scaling below 1.
+ const x_scaled = @max(1, if (std.math.isNan(x)) 1 else x);
+ const y_scaled = @max(1, if (std.math.isNan(y)) 1 else y);
+
+ self.content_scale = .{
+ .x = @floatCast(x_scaled),
+ .y = @floatCast(y_scaled),
+ };
+
+ self.core_surface.contentScaleCallback(self.content_scale) catch |err| {
+ log.err("error in content scale callback err={}", .{err});
+ return;
+ };
+ }
+
+ pub fn updateSize(self: *Surface, width: u32, height: u32) void {
+ // Runtimes sometimes generate superfluous resize events even
+ // if the size did not actually change (SwiftUI). We check
+ // that the size actually changed from what we last recorded
+ // since resizes are expensive.
+ if (self.size.width == width and self.size.height == height) return;
+
+ self.size = .{
+ .width = width,
+ .height = height,
+ };
+
+ // Call the primary callback.
+ self.core_surface.sizeCallback(self.size) catch |err| {
+ log.err("error in size callback err={}", .{err});
+ return;
+ };
+ }
+
+ pub fn colorSchemeCallback(self: *Surface, scheme: apprt.ColorScheme) void {
+ self.core_surface.colorSchemeCallback(scheme) catch |err| {
+ log.err("error setting color scheme err={}", .{err});
+ return;
+ };
+ }
+
+ pub fn mouseButtonCallback(
+ self: *Surface,
+ action: input.MouseButtonState,
+ button: input.MouseButton,
+ mods: input.Mods,
+ ) bool {
+ return self.core_surface.mouseButtonCallback(action, button, mods) catch |err| {
+ log.err("error in mouse button callback err={}", .{err});
+ return false;
+ };
+ }
+
+ pub fn mousePressureCallback(
+ self: *Surface,
+ stage: input.MousePressureStage,
+ pressure: f64,
+ ) void {
+ self.core_surface.mousePressureCallback(stage, pressure) catch |err| {
+ log.err("error in mouse pressure callback err={}", .{err});
+ return;
+ };
+ }
+
+ pub fn scrollCallback(
+ self: *Surface,
+ xoff: f64,
+ yoff: f64,
+ mods: input.ScrollMods,
+ ) void {
+ self.core_surface.scrollCallback(xoff, yoff, mods) catch |err| {
+ log.err("error in scroll callback err={}", .{err});
+ return;
+ };
+ }
+
+ pub fn cursorPosCallback(
+ self: *Surface,
+ x: f64,
+ y: f64,
+ mods: input.Mods,
+ ) void {
+ // Convert our unscaled x/y to scaled.
+ const pos = self.cursorPosToPixels(.{
+ .x = @floatCast(x),
+ .y = @floatCast(y),
+ }) catch |err| {
+ log.err(
+ "error converting cursor pos to scaled pixels in cursor pos callback err={}",
+ .{err},
+ );
+ return;
+ };
+
+ // There are cases where the platform reports a mouse motion event
+ // without the cursor actually moving. For example, on macOS, updating
+ // the window title can trigger a phantom mouse-move event at the same
+ // coordinates. This can cause the mouse to incorrectly unhide when
+ // mouse-hide-while-typing is enabled (commonly seen with TUI apps
+ // like Zellij that frequently update the title). To prevent incorrect
+ // behavior, we only continue with callback logic if the cursor has
+ // actually moved.
+ if (@abs(self.cursor_pos.x - pos.x) < 1 and
+ @abs(self.cursor_pos.y - pos.y) < 1) return;
+
+ self.cursor_pos = pos;
+
+ self.core_surface.cursorPosCallback(self.cursor_pos, mods) catch |err| {
+ log.err("error in cursor pos callback err={}", .{err});
+ return;
+ };
+ }
+
+ pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8) void {
+ _ = self.core_surface.preeditCallback(preedit_) catch |err| {
+ log.err("error in preedit callback err={}", .{err});
+ return;
+ };
+ }
+
+ pub fn textCallback(self: *Surface, text: []const u8) void {
+ _ = self.core_surface.textCallback(text) catch |err| {
+ log.err("error in key callback err={}", .{err});
+ return;
+ };
+ }
+
+ pub fn focusCallback(self: *Surface, focused: bool) void {
+ self.core_surface.focusCallback(focused) catch |err| {
+ log.err("error in focus callback err={}", .{err});
+ return;
+ };
+ }
+
+ pub fn occlusionCallback(self: *Surface, visible: bool) void {
+ self.core_surface.occlusionCallback(visible) catch |err| {
+ log.err("error in occlusion callback err={}", .{err});
+ return;
+ };
+ }
+
+ fn queueInspectorRender(self: *Surface) void {
+ _ = self.app.performAction(
+ .{ .surface = &self.core_surface },
+ .render_inspector,
+ {},
+ ) catch |err| {
+ log.err("error rendering the inspector err={}", .{err});
+ return;
+ };
+ }
+
+ pub fn newSurfaceOptions(self: *const Surface, context: apprt.surface.NewSurfaceContext) apprt.Surface.Options {
+ const font_size: f32 = font_size: {
+ if (!self.app.config.@"window-inherit-font-size") break :font_size 0;
+ break :font_size self.core_surface.font_size.points;
+ };
+
+ const working_directory: ?[*:0]const u8 = wd: {
+ if (!apprt.surface.shouldInheritWorkingDirectory(context, &self.app.config)) break :wd null;
+ const cwd = self.core_surface.pwd(self.app.core_app.alloc) catch null orelse break :wd null;
+ defer self.app.core_app.alloc.free(cwd);
+ break :wd self.app.core_app.alloc.dupeZ(u8, cwd) catch null;
+ };
+
+ return .{
+ .font_size = font_size,
+ .working_directory = working_directory,
+ .context = context,
+ };
+ }
+
+ pub fn defaultTermioEnv(self: *const Surface) !std.process.EnvMap {
+ const alloc = self.app.core_app.alloc;
+ var env = try internal_os.getEnvMap(alloc);
+ errdefer env.deinit();
+
+ if (comptime builtin.target.os.tag.isDarwin()) {
+ if (env.get("__XCODE_BUILT_PRODUCTS_DIR_PATHS") != null) {
+ env.remove("__XCODE_BUILT_PRODUCTS_DIR_PATHS");
+ env.remove("__XPC_DYLD_LIBRARY_PATH");
+ env.remove("DYLD_FRAMEWORK_PATH");
+ env.remove("DYLD_INSERT_LIBRARIES");
+ env.remove("DYLD_LIBRARY_PATH");
+ env.remove("LD_LIBRARY_PATH");
+ env.remove("SECURITYSESSIONID");
+ env.remove("XPC_SERVICE_NAME");
+ }
+
+ // Remove this so that running `ghostty` within Ghostty works.
+ env.remove("GHOSTTY_MAC_LAUNCH_SOURCE");
+
+ // If we were launched from the desktop then we want to
+ // remove the LANGUAGE env var so that we don't inherit
+ // our translation settings for Ghostty. If we aren't from
+ // the desktop then we didn't set our LANGUAGE var so we
+ // don't need to remove it.
+ if (internal_os.launchedFromDesktop()) env.remove("LANGUAGE");
+ }
+
+ return env;
+ }
+
+ /// The cursor position from the host directly is in screen coordinates but
+ /// all our interface works in pixels.
+ fn cursorPosToPixels(self: *const Surface, pos: apprt.CursorPos) !apprt.CursorPos {
+ const scale = try self.getContentScale();
+ return .{ .x = pos.x * scale.x, .y = pos.y * scale.y };
+ }
+};
+
+/// Inspector is the state required for the terminal inspector. A terminal
+/// inspector is 1:1 with a Surface.
+pub const Inspector = struct {
+ const cimgui = @import("dcimgui");
+
+ surface: *Surface,
+ ig_ctx: *cimgui.c.ImGuiContext,
+ backend: ?Backend = null,
+ content_scale: f64 = 1,
+
+ /// Our previous instant used to calculate delta time for animations.
+ instant: ?std.time.Instant = null,
+
+ const Backend = enum {
+ metal,
+
+ pub fn deinit(self: Backend) void {
+ switch (self) {
+ .metal => if (builtin.target.os.tag.isDarwin()) cimgui.ImGui_ImplMetal_Shutdown(),
+ }
+ }
+ };
+
+ pub fn init(surface: *Surface) !Inspector {
+ const ig_ctx = cimgui.c.ImGui_CreateContext(null) orelse return error.OutOfMemory;
+ errdefer cimgui.c.ImGui_DestroyContext(ig_ctx);
+ cimgui.c.ImGui_SetCurrentContext(ig_ctx);
+ const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO();
+ io.BackendPlatformName = "ghostty_embedded";
+
+ // Setup our core inspector
+ CoreInspector.setup();
+ surface.core_surface.activateInspector() catch |err| {
+ log.err("failed to activate inspector err={}", .{err});
+ };
+
+ return .{
+ .surface = surface,
+ .ig_ctx = ig_ctx,
+ };
+ }
+
+ pub fn deinit(self: *Inspector) void {
+ self.surface.core_surface.deactivateInspector();
+ cimgui.c.ImGui_SetCurrentContext(self.ig_ctx);
+ if (self.backend) |v| v.deinit();
+ cimgui.c.ImGui_DestroyContext(self.ig_ctx);
+ }
+
+ /// Queue a render for the next frame.
+ pub fn queueRender(self: *Inspector) void {
+ self.surface.queueInspectorRender();
+ }
+
+ /// Initialize the inspector for a metal backend.
+ pub fn initMetal(self: *Inspector, device: objc.Object) bool {
+ defer device.msgSend(void, objc.sel("release"), .{});
+ cimgui.c.ImGui_SetCurrentContext(self.ig_ctx);
+
+ if (self.backend) |v| {
+ v.deinit();
+ self.backend = null;
+ }
+
+ if (!cimgui.ImGui_ImplMetal_Init(device.value)) {
+ log.warn("failed to initialize metal backend", .{});
+ return false;
+ }
+ self.backend = .metal;
+
+ log.debug("initialized metal backend", .{});
+ return true;
+ }
+
+ pub fn renderMetal(
+ self: *Inspector,
+ command_buffer: objc.Object,
+ desc: objc.Object,
+ ) !void {
+ defer {
+ command_buffer.msgSend(void, objc.sel("release"), .{});
+ desc.msgSend(void, objc.sel("release"), .{});
+ }
+ assert(self.backend == .metal);
+ //log.debug("render", .{});
+
+ // Setup our imgui frame. We need to render multiple frames to ensure
+ // ImGui completes all its state processing. I don't know how to fix
+ // this.
+ for (0..2) |_| {
+ cimgui.ImGui_ImplMetal_NewFrame(desc.value);
+ try self.newFrame();
+ cimgui.c.ImGui_NewFrame();
+
+ // Build our UI
+ render: {
+ const surface = &self.surface.core_surface;
+ const inspector = surface.inspector orelse break :render;
+ inspector.render(surface);
+ }
+
+ // Render
+ cimgui.c.ImGui_Render();
+ }
+
+ // MTLRenderCommandEncoder
+ const encoder = command_buffer.msgSend(
+ objc.Object,
+ objc.sel("renderCommandEncoderWithDescriptor:"),
+ .{desc.value},
+ );
+ defer encoder.msgSend(void, objc.sel("endEncoding"), .{});
+ cimgui.ImGui_ImplMetal_RenderDrawData(
+ cimgui.c.ImGui_GetDrawData(),
+ command_buffer.value,
+ encoder.value,
+ );
+ }
+
+ pub fn updateContentScale(self: *Inspector, x: f64, y: f64) void {
+ _ = y;
+ cimgui.c.ImGui_SetCurrentContext(self.ig_ctx);
+
+ // Cache our scale because we use it for cursor position calculations.
+ self.content_scale = x;
+
+ // Setup a new style and scale it appropriately. We must use the
+ // ImGuiStyle constructor to get proper default values (e.g.,
+ // CurveTessellationTol) rather than zero-initialized values.
+ var style: cimgui.c.ImGuiStyle = undefined;
+ cimgui.ext.ImGuiStyle_ImGuiStyle(&style);
+ cimgui.c.ImGuiStyle_ScaleAllSizes(&style, @floatCast(x));
+ const active_style = cimgui.c.ImGui_GetStyle();
+ active_style.* = style;
+ }
+
+ pub fn updateSize(self: *Inspector, width: u32, height: u32) void {
+ cimgui.c.ImGui_SetCurrentContext(self.ig_ctx);
+ const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO();
+ io.DisplaySize = .{ .x = @floatFromInt(width), .y = @floatFromInt(height) };
+ }
+
+ pub fn mouseButtonCallback(
+ self: *Inspector,
+ action: input.MouseButtonState,
+ button: input.MouseButton,
+ mods: input.Mods,
+ ) void {
+ _ = mods;
+
+ self.queueRender();
+ cimgui.c.ImGui_SetCurrentContext(self.ig_ctx);
+ const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO();
+
+ const imgui_button = switch (button) {
+ .left => cimgui.c.ImGuiMouseButton_Left,
+ .middle => cimgui.c.ImGuiMouseButton_Middle,
+ .right => cimgui.c.ImGuiMouseButton_Right,
+ else => return, // unsupported
+ };
+
+ cimgui.c.ImGuiIO_AddMouseButtonEvent(io, imgui_button, action == .press);
+ }
+
+ pub fn scrollCallback(
+ self: *Inspector,
+ xoff: f64,
+ yoff: f64,
+ mods: input.ScrollMods,
+ ) void {
+ self.queueRender();
+ cimgui.c.ImGui_SetCurrentContext(self.ig_ctx);
+ const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO();
+
+ // For precision scrolling (trackpads), the values are in pixels which
+ // scroll way too fast. Scale them down to approximate discrete wheel
+ // notches. imgui expects 1.0 to scroll ~5 lines of text.
+ const scale: f64 = if (mods.precision) 0.1 else 1.0;
+ cimgui.c.ImGuiIO_AddMouseWheelEvent(
+ io,
+ @floatCast(xoff * scale),
+ @floatCast(yoff * scale),
+ );
+ }
+
+ pub fn cursorPosCallback(self: *Inspector, x: f64, y: f64) void {
+ self.queueRender();
+ cimgui.c.ImGui_SetCurrentContext(self.ig_ctx);
+ const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO();
+ cimgui.c.ImGuiIO_AddMousePosEvent(
+ io,
+ @floatCast(x * self.content_scale),
+ @floatCast(y * self.content_scale),
+ );
+ }
+
+ pub fn focusCallback(self: *Inspector, focused: bool) void {
+ self.queueRender();
+ cimgui.c.ImGui_SetCurrentContext(self.ig_ctx);
+ const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO();
+ cimgui.c.ImGuiIO_AddFocusEvent(io, focused);
+ }
+
+ pub fn textCallback(self: *Inspector, text: [:0]const u8) void {
+ self.queueRender();
+ cimgui.c.ImGui_SetCurrentContext(self.ig_ctx);
+ const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO();
+ cimgui.c.ImGuiIO_AddInputCharactersUTF8(io, text.ptr);
+ }
+
+ pub fn keyCallback(
+ self: *Inspector,
+ action: input.Action,
+ key: input.Key,
+ mods: input.Mods,
+ ) !void {
+ self.queueRender();
+ cimgui.c.ImGui_SetCurrentContext(self.ig_ctx);
+ const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO();
+
+ // Update all our modifiers
+ cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftShift, mods.shift);
+ cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftCtrl, mods.ctrl);
+ cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftAlt, mods.alt);
+ cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftSuper, mods.super);
+
+ // Send our keypress
+ if (key.imguiKey()) |imgui_key| {
+ cimgui.c.ImGuiIO_AddKeyEvent(
+ io,
+ imgui_key,
+ action == .press or action == .repeat,
+ );
+ }
+ }
+
+ fn newFrame(self: *Inspector) !void {
+ const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO();
+
+ // Determine our delta time
+ const now = try std.time.Instant.now();
+ io.DeltaTime = if (self.instant) |prev| delta: {
+ const since_ns: f64 = @floatFromInt(now.since(prev));
+ const ns_per_s: f64 = @floatFromInt(std.time.ns_per_s);
+ const since_s: f32 = @floatCast(since_ns / ns_per_s);
+ break :delta @max(0.00001, since_s);
+ } else (1.0 / 60.0);
+ self.instant = now;
+ }
+};
+
+// C API
+pub const CAPI = struct {
+ const global = &@import("../global.zig").state;
+
+ /// This is the same as Surface.KeyEvent but this is the raw C API version.
+ const KeyEvent = extern struct {
+ action: input.Action,
+ mods: c_int,
+ consumed_mods: c_int,
+ keycode: u32,
+ text: ?[*:0]const u8,
+ unshifted_codepoint: u32,
+ composing: bool,
+
+ /// Convert to Zig key event.
+ fn keyEvent(self: KeyEvent) App.KeyEvent {
+ return .{
+ .action = self.action,
+ .mods = @bitCast(@as(
+ input.Mods.Backing,
+ @truncate(@as(c_uint, @bitCast(self.mods))),
+ )),
+ .consumed_mods = @bitCast(@as(
+ input.Mods.Backing,
+ @truncate(@as(c_uint, @bitCast(self.consumed_mods))),
+ )),
+ .keycode = self.keycode,
+ .text = if (self.text) |ptr| std.mem.sliceTo(ptr, 0) else null,
+ .unshifted_codepoint = self.unshifted_codepoint,
+ .composing = self.composing,
+ };
+ }
+ };
+
+ const SurfaceSize = extern struct {
+ columns: u16,
+ rows: u16,
+ width_px: u32,
+ height_px: u32,
+ cell_width_px: u32,
+ cell_height_px: u32,
+ };
+
+ // ghostty_clipboard_content_s
+ const ClipboardContent = extern struct {
+ mime: [*:0]const u8,
+ data: [*:0]const u8,
+ };
+
+ // ghostty_text_s
+ const Text = extern struct {
+ tl_px_x: f64,
+ tl_px_y: f64,
+ offset_start: u32,
+ offset_len: u32,
+ text: ?[*:0]const u8,
+ text_len: usize,
+
+ pub fn deinit(self: *Text) void {
+ if (self.text) |ptr| {
+ global.alloc.free(ptr[0..self.text_len :0]);
+ }
+ }
+ };
+
+ // ghostty_point_s
+ const Point = extern struct {
+ tag: Tag,
+ coord_tag: CoordTag,
+ x: u32,
+ y: u32,
+
+ const Tag = enum(c_int) {
+ active = 0,
+ viewport = 1,
+ screen = 2,
+ history = 3,
+ };
+
+ const CoordTag = enum(c_int) {
+ exact = 0,
+ top_left = 1,
+ bottom_right = 2,
+ };
+
+ fn pin(
+ self: Point,
+ screen: *const terminal.Screen,
+ ) ?terminal.Pin {
+ // The core point tag.
+ const tag: terminal.point.Tag = switch (self.tag) {
+ inline else => |tag| @field(
+ terminal.point.Tag,
+ @tagName(tag),
+ ),
+ };
+
+ // Clamp our point to the screen bounds.
+ const clamped_x = @min(self.x, screen.pages.cols -| 1);
+ const clamped_y = @min(self.y, screen.pages.rows -| 1);
+
+ return switch (self.coord_tag) {
+ // Exact coordinates require a specific pin.
+ .exact => exact: {
+ const pt_x = std.math.cast(
+ terminal.size.CellCountInt,
+ clamped_x,
+ ) orelse std.math.maxInt(terminal.size.CellCountInt);
+
+ const pt: terminal.Point = switch (tag) {
+ inline else => |v| @unionInit(
+ terminal.Point,
+ @tagName(v),
+ .{ .x = pt_x, .y = clamped_y },
+ ),
+ };
+
+ break :exact screen.pages.pin(pt) orelse null;
+ },
+
+ .top_left => screen.pages.getTopLeft(tag),
+
+ .bottom_right => screen.pages.getBottomRight(tag),
+ };
+ }
+ };
+
+ // ghostty_selection_s
+ const Selection = extern struct {
+ tl: Point,
+ br: Point,
+ rectangle: bool,
+
+ fn core(
+ self: Selection,
+ screen: *const terminal.Screen,
+ ) ?terminal.Selection {
+ return .{
+ .bounds = .{ .untracked = .{
+ .start = self.tl.pin(screen) orelse return null,
+ .end = self.br.pin(screen) orelse return null,
+ } },
+ .rectangle = self.rectangle,
+ };
+ }
+ };
+
+ // Reference the conditional exports based on target platform
+ // so they're included in the C API.
+ comptime {
+ if (builtin.target.os.tag.isDarwin()) {
+ _ = Darwin;
+ }
+ }
+
+ /// Create a new app.
+ export fn ghostty_app_new(
+ opts: *const apprt.runtime.App.Options,
+ config: *const Config,
+ ) ?*App {
+ return app_new_(opts, config) catch |err| {
+ log.err("error initializing app err={}", .{err});
+ return null;
+ };
+ }
+
+ fn app_new_(
+ opts: *const apprt.runtime.App.Options,
+ config: *const Config,
+ ) !*App {
+ const core_app = try CoreApp.create(global.alloc);
+ errdefer core_app.destroy();
+
+ // Create our runtime app
+ var app = try global.alloc.create(App);
+ errdefer global.alloc.destroy(app);
+ try app.init(core_app, config, opts.*);
+ errdefer app.terminate();
+
+ return app;
+ }
+
+ /// Tick the event loop. This should be called whenever the "wakeup"
+ /// callback is invoked for the runtime.
+ export fn ghostty_app_tick(v: *App) void {
+ v.core_app.tick(v) catch |err| {
+ log.err("error app tick err={}", .{err});
+ };
+ }
+
+ /// Return the userdata associated with the app.
+ export fn ghostty_app_userdata(v: *App) ?*anyopaque {
+ return v.opts.userdata;
+ }
+
+ export fn ghostty_app_free(v: *App) void {
+ const core_app = v.core_app;
+ v.terminate();
+ global.alloc.destroy(v);
+ core_app.destroy();
+ }
+
+ /// Update the focused state of the app.
+ export fn ghostty_app_set_focus(
+ app: *App,
+ focused: bool,
+ ) void {
+ app.focusEvent(focused);
+ }
+
+ /// Notify the app of a global keypress capture. This will return
+ /// true if the key was captured by the app, in which case the caller
+ /// should not process the key.
+ export fn ghostty_app_key(
+ app: *App,
+ event: KeyEvent,
+ ) bool {
+ return app.keyEvent(.app, event.keyEvent()) catch |err| {
+ log.warn("error processing key event err={}", .{err});
+ return false;
+ };
+ }
+
+ /// Returns true if the given key event would trigger a binding
+ /// if it were sent to the surface right now. The "right now"
+ /// is important because things like trigger sequences are only
+ /// valid until the next key event.
+ export fn ghostty_app_key_is_binding(
+ app: *App,
+ event: KeyEvent,
+ ) bool {
+ const core_event = event.keyEvent().core() orelse {
+ log.warn("error processing key event", .{});
+ return false;
+ };
+
+ return app.core_app.keyEventIsBinding(app, core_event);
+ }
+
+ /// Notify the app that the keyboard was changed. This causes the
+ /// keyboard layout to be reloaded from the OS.
+ export fn ghostty_app_keyboard_changed(v: *App) void {
+ v.reloadKeymap() catch |err| {
+ log.err("error reloading keyboard map err={}", .{err});
+ return;
+ };
+ }
+
+ /// Open the configuration.
+ export fn ghostty_app_open_config(v: *App) void {
+ _ = v.performAction(.app, .open_config, {}) catch |err| {
+ log.err("error reloading config err={}", .{err});
+ return;
+ };
+ }
+
+ /// Update the configuration to the provided config. This will propagate
+ /// to all surfaces as well.
+ export fn ghostty_app_update_config(
+ v: *App,
+ config: *const Config,
+ ) void {
+ v.core_app.updateConfig(v, config) catch |err| {
+ log.err("error updating config err={}", .{err});
+ return;
+ };
+ }
+
+ /// Returns true if the app needs to confirm quitting.
+ export fn ghostty_app_needs_confirm_quit(v: *App) bool {
+ return v.core_app.needsConfirmQuit();
+ }
+
+ /// Returns true if the app has global keybinds.
+ export fn ghostty_app_has_global_keybinds(v: *App) bool {
+ return v.hasGlobalKeybinds();
+ }
+
+ /// Update the color scheme of the app.
+ export fn ghostty_app_set_color_scheme(v: *App, scheme_raw: c_int) void {
+ const scheme = std.meta.intToEnum(apprt.ColorScheme, scheme_raw) catch {
+ log.warn(
+ "invalid color scheme to ghostty_surface_set_color_scheme value={}",
+ .{scheme_raw},
+ );
+ return;
+ };
+
+ v.core_app.colorSchemeEvent(v, scheme) catch |err| {
+ log.err("error setting color scheme err={}", .{err});
+ return;
+ };
+ }
+
+ /// Returns initial surface options.
+ export fn ghostty_surface_config_new() apprt.Surface.Options {
+ return .{};
+ }
+
+ /// Create a new surface as part of an app.
+ export fn ghostty_surface_new(
+ app: *App,
+ opts: *const apprt.Surface.Options,
+ ) ?*Surface {
+ return surface_new_(app, opts) catch |err| {
+ log.err("error initializing surface err={}", .{err});
+ return null;
+ };
+ }
+
+ fn surface_new_(
+ app: *App,
+ opts: *const apprt.Surface.Options,
+ ) !*Surface {
+ return try app.newSurface(opts.*);
+ }
+
+ export fn ghostty_surface_free(ptr: *Surface) void {
+ ptr.app.closeSurface(ptr);
+ }
+
+ /// Returns the userdata associated with the surface.
+ export fn ghostty_surface_userdata(surface: *Surface) ?*anyopaque {
+ return surface.userdata;
+ }
+
+ /// Returns the app associated with a surface.
+ export fn ghostty_surface_app(surface: *Surface) *App {
+ return surface.app;
+ }
+
+ /// Returns the config to use for surfaces that inherit from this one.
+ export fn ghostty_surface_inherited_config(
+ surface: *Surface,
+ source: apprt.surface.NewSurfaceContext,
+ ) Surface.Options {
+ return surface.newSurfaceOptions(source);
+ }
+
+ /// Update the configuration to the provided config for only this surface.
+ export fn ghostty_surface_update_config(
+ surface: *Surface,
+ config: *const Config,
+ ) void {
+ surface.core_surface.updateConfig(config) catch |err| {
+ log.err("error updating config err={}", .{err});
+ return;
+ };
+ }
+
+ /// Returns true if the surface needs to confirm quitting.
+ export fn ghostty_surface_needs_confirm_quit(surface: *Surface) bool {
+ return surface.core_surface.needsConfirmQuit();
+ }
+
+ /// Returns true if the surface process has exited.
+ export fn ghostty_surface_process_exited(surface: *Surface) bool {
+ return surface.core_surface.child_exited;
+ }
+
+ /// Returns true if the surface has a selection.
+ export fn ghostty_surface_has_selection(surface: *Surface) bool {
+ return surface.core_surface.hasSelection();
+ }
+
+ /// Same as ghostty_surface_read_text but reads from the user selection,
+ /// if any.
+ export fn ghostty_surface_read_selection(
+ surface: *Surface,
+ result: *Text,
+ ) bool {
+ const core_surface = &surface.core_surface;
+ core_surface.renderer_state.mutex.lock();
+ defer core_surface.renderer_state.mutex.unlock();
+
+ // If we don't have a selection, do nothing.
+ const core_sel = core_surface.io.terminal.screens.active.selection orelse return false;
+
+ // Read the text from the selection.
+ return readTextLocked(surface, core_sel, result);
+ }
+
+ /// Read some arbitrary text from the surface.
+ ///
+ /// This is an expensive operation so it shouldn't be called too
+ /// often. We recommend that callers cache the result and throttle
+ /// calls to this function.
+ export fn ghostty_surface_read_text(
+ surface: *Surface,
+ sel: Selection,
+ result: *Text,
+ ) bool {
+ surface.core_surface.renderer_state.mutex.lock();
+ defer surface.core_surface.renderer_state.mutex.unlock();
+
+ const core_sel = sel.core(
+ surface.core_surface.renderer_state.terminal.screens.active,
+ ) orelse return false;
+
+ return readTextLocked(surface, core_sel, result);
+ }
+
+ fn readTextLocked(
+ surface: *Surface,
+ core_sel: terminal.Selection,
+ result: *Text,
+ ) bool {
+ const core_surface = &surface.core_surface;
+
+ // Get our text directly from the core surface.
+ const text = core_surface.dumpTextLocked(
+ global.alloc,
+ core_sel,
+ ) catch |err| {
+ log.warn("error reading text err={}", .{err});
+ return false;
+ };
+
+ const vp: CoreSurface.Text.Viewport = text.viewport orelse .{
+ .tl_px_x = -1,
+ .tl_px_y = -1,
+ .offset_start = 0,
+ .offset_len = 0,
+ };
+
+ result.* = .{
+ .tl_px_x = vp.tl_px_x,
+ .tl_px_y = vp.tl_px_y,
+ .offset_start = vp.offset_start,
+ .offset_len = vp.offset_len,
+ .text = text.text.ptr,
+ .text_len = text.text.len,
+ };
+
+ return true;
+ }
+
+ export fn ghostty_surface_free_text(_: *Surface, ptr: *Text) void {
+ ptr.deinit();
+ }
+
+ /// Tell the surface that it needs to schedule a render
+ export fn ghostty_surface_refresh(surface: *Surface) void {
+ surface.refresh();
+ }
+
+ /// Tell the surface that it needs to schedule a render
+ /// call as soon as possible (NOW if possible).
+ export fn ghostty_surface_draw(surface: *Surface) void {
+ surface.draw();
+ }
+
+ /// Update the size of a surface. This will trigger resize notifications
+ /// to the pty and the renderer.
+ export fn ghostty_surface_set_size(surface: *Surface, w: u32, h: u32) void {
+ surface.updateSize(w, h);
+ }
+
+ /// Return the size information a surface has.
+ export fn ghostty_surface_size(surface: *Surface) SurfaceSize {
+ const grid_size = surface.core_surface.size.grid();
+ return .{
+ .columns = grid_size.columns,
+ .rows = grid_size.rows,
+ .width_px = surface.core_surface.size.screen.width,
+ .height_px = surface.core_surface.size.screen.height,
+ .cell_width_px = surface.core_surface.size.cell.width,
+ .cell_height_px = surface.core_surface.size.cell.height,
+ };
+ }
+
+ /// Update the color scheme of the surface.
+ export fn ghostty_surface_set_color_scheme(surface: *Surface, scheme_raw: c_int) void {
+ const scheme = std.meta.intToEnum(apprt.ColorScheme, scheme_raw) catch {
+ log.warn(
+ "invalid color scheme to ghostty_surface_set_color_scheme value={}",
+ .{scheme_raw},
+ );
+ return;
+ };
+
+ surface.colorSchemeCallback(scheme);
+ }
+
+ /// Update the content scale of the surface.
+ export fn ghostty_surface_set_content_scale(surface: *Surface, x: f64, y: f64) void {
+ surface.updateContentScale(x, y);
+ }
+
+ /// Update the focused state of a surface.
+ export fn ghostty_surface_set_focus(surface: *Surface, focused: bool) void {
+ surface.focusCallback(focused);
+ }
+
+ /// Update the occlusion state of a surface.
+ export fn ghostty_surface_set_occlusion(surface: *Surface, visible: bool) void {
+ surface.occlusionCallback(visible);
+ }
+
+ /// Filter the mods if necessary. This handles settings such as
+ /// `macos-option-as-alt`. The filtered mods should be used for
+ /// key translation but should NOT be sent back via the `_key`
+ /// function -- the original mods should be used for that.
+ export fn ghostty_surface_key_translation_mods(
+ surface: *Surface,
+ mods_raw: c_int,
+ ) c_int {
+ const mods: input.Mods = @bitCast(@as(
+ input.Mods.Backing,
+ @truncate(@as(c_uint, @bitCast(mods_raw))),
+ ));
+ const result = mods.translation(
+ surface.core_surface.config.macos_option_as_alt orelse
+ surface.app.keyboardLayout().detectOptionAsAlt(),
+ );
+ return @intCast(@as(input.Mods.Backing, @bitCast(result)));
+ }
+
+ /// Send this for raw keypresses (i.e. the keyDown event on macOS).
+ /// This will handle the keymap translation and send the appropriate
+ /// key and char events.
+ export fn ghostty_surface_key(
+ surface: *Surface,
+ event: KeyEvent,
+ ) bool {
+ return surface.app.keyEvent(
+ .{ .surface = surface },
+ event.keyEvent(),
+ ) catch |err| {
+ log.warn("error processing key event err={}", .{err});
+ return false;
+ };
+ }
+
+ /// Returns true if the given key event would trigger a binding
+ /// if it were sent to the surface right now. The "right now"
+ /// is important because things like trigger sequences are only
+ /// valid until the next key event.
+ export fn ghostty_surface_key_is_binding(
+ surface: *Surface,
+ event: KeyEvent,
+ c_flags: ?*input.Binding.Flags.C,
+ ) bool {
+ const core_event = event.keyEvent().core() orelse {
+ log.warn("error processing key event", .{});
+ return false;
+ };
+
+ const flags = surface.core_surface.keyEventIsBinding(
+ core_event,
+ ) orelse return false;
+ if (c_flags) |ptr| ptr.* = flags.cval();
+ return true;
+ }
+
+ /// Send raw text to the terminal. This is treated like a paste
+ /// so this isn't useful for sending escape sequences. For that,
+ /// individual key input should be used.
+ export fn ghostty_surface_text(
+ surface: *Surface,
+ ptr: [*]const u8,
+ len: usize,
+ ) void {
+ surface.textCallback(ptr[0..len]);
+ }
+
+ /// Set the preedit text for the surface. This is used for IME
+ /// composition. If the length is 0, then the preedit text is cleared.
+ export fn ghostty_surface_preedit(
+ surface: *Surface,
+ ptr: [*]const u8,
+ len: usize,
+ ) void {
+ surface.preeditCallback(if (len == 0) null else ptr[0..len]);
+ }
+
+ /// Returns true if the surface currently has mouse capturing
+ /// enabled.
+ export fn ghostty_surface_mouse_captured(surface: *Surface) bool {
+ return surface.core_surface.mouseCaptured();
+ }
+
+ /// Tell the surface that it needs to schedule a render
+ export fn ghostty_surface_mouse_button(
+ surface: *Surface,
+ action: input.MouseButtonState,
+ button: input.MouseButton,
+ mods: c_int,
+ ) bool {
+ return surface.mouseButtonCallback(
+ action,
+ button,
+ @bitCast(@as(
+ input.Mods.Backing,
+ @truncate(@as(c_uint, @bitCast(mods))),
+ )),
+ );
+ }
+
+ /// Update the mouse position within the view.
+ export fn ghostty_surface_mouse_pos(
+ surface: *Surface,
+ x: f64,
+ y: f64,
+ mods: c_int,
+ ) void {
+ surface.cursorPosCallback(
+ x,
+ y,
+ @bitCast(@as(
+ input.Mods.Backing,
+ @truncate(@as(c_uint, @bitCast(mods))),
+ )),
+ );
+ }
+
+ export fn ghostty_surface_mouse_scroll(
+ surface: *Surface,
+ x: f64,
+ y: f64,
+ scroll_mods: c_int,
+ ) void {
+ surface.scrollCallback(
+ x,
+ y,
+ @bitCast(@as(u8, @truncate(@as(c_uint, @bitCast(scroll_mods))))),
+ );
+ }
+
+ export fn ghostty_surface_mouse_pressure(
+ surface: *Surface,
+ stage_raw: u32,
+ pressure: f64,
+ ) void {
+ const stage = std.meta.intToEnum(
+ input.MousePressureStage,
+ stage_raw,
+ ) catch {
+ log.warn(
+ "invalid mouse pressure stage value={}",
+ .{stage_raw},
+ );
+ return;
+ };
+
+ surface.mousePressureCallback(stage, pressure);
+ }
+
+ export fn ghostty_surface_ime_point(
+ surface: *Surface,
+ x: *f64,
+ y: *f64,
+ width: *f64,
+ height: *f64,
+ ) void {
+ const pos = surface.core_surface.imePoint();
+ x.* = pos.x;
+ y.* = pos.y;
+ width.* = pos.width;
+ height.* = pos.height;
+ }
+
+ /// Request that the surface become closed. This will go through the
+ /// normal trigger process that a close surface input binding would.
+ export fn ghostty_surface_request_close(ptr: *Surface) void {
+ ptr.core_surface.close();
+ }
+
+ /// Request that the surface split in the given direction.
+ export fn ghostty_surface_split(ptr: *Surface, direction: apprt.action.SplitDirection) void {
+ _ = ptr.app.performAction(
+ .{ .surface = &ptr.core_surface },
+ .new_split,
+ direction,
+ ) catch |err| {
+ log.err("error creating new split err={}", .{err});
+ return;
+ };
+ }
+
+ /// Focus on the next split (if any).
+ export fn ghostty_surface_split_focus(
+ ptr: *Surface,
+ direction: apprt.action.GotoSplit,
+ ) void {
+ _ = ptr.app.performAction(
+ .{ .surface = &ptr.core_surface },
+ .goto_split,
+ direction,
+ ) catch |err| {
+ log.err("error creating new split err={}", .{err});
+ return;
+ };
+ }
+
+ /// Resize the current split by moving the split divider in the given
+ /// direction. `direction` specifies which direction the split divider will
+ /// move relative to the focused split. `amount` is a fractional value
+ /// between 0 and 1 that specifies by how much the divider will move.
+ export fn ghostty_surface_split_resize(
+ ptr: *Surface,
+ direction: apprt.action.ResizeSplit.Direction,
+ amount: u16,
+ ) void {
+ _ = ptr.app.performAction(
+ .{ .surface = &ptr.core_surface },
+ .resize_split,
+ .{ .direction = direction, .amount = amount },
+ ) catch |err| {
+ log.err("error resizing split err={}", .{err});
+ return;
+ };
+ }
+
+ /// Equalize the size of all splits in the current window.
+ export fn ghostty_surface_split_equalize(ptr: *Surface) void {
+ _ = ptr.app.performAction(
+ .{ .surface = &ptr.core_surface },
+ .equalize_splits,
+ {},
+ ) catch |err| {
+ log.err("error equalizing splits err={}", .{err});
+ return;
+ };
+ }
+
+ /// Invoke an action on the surface.
+ export fn ghostty_surface_binding_action(
+ ptr: *Surface,
+ action_ptr: [*]const u8,
+ action_len: usize,
+ ) bool {
+ const action_str = action_ptr[0..action_len];
+ const action = input.Binding.Action.parse(action_str) catch |err| {
+ log.err("error parsing binding action action={s} err={}", .{ action_str, err });
+ return false;
+ };
+
+ return ptr.core_surface.performBindingAction(action) catch |err| {
+ log.err("error performing binding action action={f} err={}", .{ action, err });
+ return false;
+ };
+ }
+
+ /// Complete a clipboard read request started via the read callback.
+ /// This can only be called once for a given request. Once it is called
+ /// with a request the request pointer will be invalidated.
+ export fn ghostty_surface_complete_clipboard_request(
+ ptr: *Surface,
+ str: [*:0]const u8,
+ state: *apprt.ClipboardRequest,
+ confirmed: bool,
+ ) void {
+ ptr.completeClipboardRequest(
+ std.mem.sliceTo(str, 0),
+ state,
+ confirmed,
+ );
+ }
+
+ export fn ghostty_surface_inspector(ptr: *Surface) ?*Inspector {
+ return ptr.initInspector() catch |err| {
+ log.err("error initializing inspector err={}", .{err});
+ return null;
+ };
+ }
+
+ export fn ghostty_inspector_free(ptr: *Surface) void {
+ ptr.freeInspector();
+ }
+
+ export fn ghostty_inspector_set_size(ptr: *Inspector, w: u32, h: u32) void {
+ ptr.updateSize(w, h);
+ }
+
+ export fn ghostty_inspector_set_content_scale(ptr: *Inspector, x: f64, y: f64) void {
+ ptr.updateContentScale(x, y);
+ }
+
+ export fn ghostty_inspector_mouse_button(
+ ptr: *Inspector,
+ action: input.MouseButtonState,
+ button: input.MouseButton,
+ mods: c_int,
+ ) void {
+ ptr.mouseButtonCallback(
+ action,
+ button,
+ @bitCast(@as(
+ input.Mods.Backing,
+ @truncate(@as(c_uint, @bitCast(mods))),
+ )),
+ );
+ }
+
+ export fn ghostty_inspector_mouse_pos(ptr: *Inspector, x: f64, y: f64) void {
+ ptr.cursorPosCallback(x, y);
+ }
+
+ export fn ghostty_inspector_mouse_scroll(
+ ptr: *Inspector,
+ x: f64,
+ y: f64,
+ scroll_mods: c_int,
+ ) void {
+ ptr.scrollCallback(
+ x,
+ y,
+ @bitCast(@as(u8, @truncate(@as(c_uint, @bitCast(scroll_mods))))),
+ );
+ }
+
+ export fn ghostty_inspector_key(
+ ptr: *Inspector,
+ action: input.Action,
+ key: input.Key,
+ c_mods: c_int,
+ ) void {
+ ptr.keyCallback(
+ action,
+ key,
+ @bitCast(@as(
+ input.Mods.Backing,
+ @truncate(@as(c_uint, @bitCast(c_mods))),
+ )),
+ ) catch |err| {
+ log.err("error processing key event err={}", .{err});
+ return;
+ };
+ }
+
+ export fn ghostty_inspector_text(
+ ptr: *Inspector,
+ str: [*:0]const u8,
+ ) void {
+ ptr.textCallback(std.mem.sliceTo(str, 0));
+ }
+
+ export fn ghostty_inspector_set_focus(ptr: *Inspector, focused: bool) void {
+ ptr.focusCallback(focused);
+ }
+
+ /// Sets the window background blur on macOS to the desired value.
+ /// I do this in Zig as an extern function because I don't know how to
+ /// call these functions in Swift.
+ ///
+ /// This uses an undocumented, non-public API because this is what
+ /// every terminal appears to use, including Terminal.app.
+ export fn ghostty_set_window_background_blur(
+ app: *App,
+ window: *anyopaque,
+ ) void {
+ // This is only supported on macOS
+ if (comptime builtin.target.os.tag != .macos) return;
+
+ const config = &app.config;
+
+ // Do nothing if we don't have background transparency enabled
+ if (config.@"background-opacity" >= 1.0) return;
+
+ const nswindow = objc.Object.fromId(window);
+ _ = CGSSetWindowBackgroundBlurRadius(
+ CGSDefaultConnectionForThread(),
+ nswindow.msgSend(usize, objc.sel("windowNumber"), .{}),
+ @intCast(config.@"background-blur".cval()),
+ );
+ }
+
+ /// See ghostty_set_window_background_blur
+ extern "c" fn CGSSetWindowBackgroundBlurRadius(*anyopaque, usize, c_int) i32;
+ extern "c" fn CGSDefaultConnectionForThread() *anyopaque;
+
+ // Darwin-only C APIs.
+ const Darwin = struct {
+ export fn ghostty_surface_set_display_id(ptr: *Surface, display_id: u32) void {
+ const surface = &ptr.core_surface;
+ _ = surface.renderer_thread.mailbox.push(
+ .{ .macos_display_id = display_id },
+ .{ .forever = {} },
+ );
+ surface.renderer_thread.wakeup.notify() catch {};
+ }
+
+ /// This returns a CTFontRef that should be used for quicklook
+ /// highlighted text. This is always the primary font in use
+ /// regardless of the selected text. If coretext is not in use
+ /// then this will return nothing.
+ export fn ghostty_surface_quicklook_font(ptr: *Surface) ?*anyopaque {
+ // For non-CoreText we just return null.
+ if (comptime font.options.backend != .coretext) {
+ return null;
+ }
+
+ // We'll need content scale so fail early if we can't get it.
+ const content_scale = ptr.getContentScale() catch return null;
+
+ // Get the shared font grid. We acquire a read lock to
+ // read the font face. It should not be deferred since
+ // we're loading the primary face.
+ const grid = ptr.core_surface.renderer.font_grid;
+ grid.lock.lockShared();
+ defer grid.lock.unlockShared();
+
+ const collection = &grid.resolver.collection;
+ const face = collection.getFace(.{}) catch return null;
+
+ // We need to unscale the content scale. We apply the
+ // content scale to our font stack because we are rendering
+ // at 1x but callers of this should be using scaled or apply
+ // scale themselves.
+ const size: f32 = size: {
+ const num = face.font.copyAttribute(.size) orelse
+ break :size 12;
+ defer num.release();
+ var v: f32 = 12;
+ _ = num.getValue(.float, &v);
+ break :size v;
+ };
+
+ const copy = face.font.copyWithAttributes(
+ size / content_scale.y,
+ null,
+ null,
+ ) catch return null;
+
+ return copy;
+ }
+
+ /// This returns the selected word for quicklook. This will populate
+ /// the buffer with the word under the cursor and the selection
+ /// info so that quicklook can be rendered.
+ ///
+ /// This does not modify the selection active on the surface (if any).
+ export fn ghostty_surface_quicklook_word(
+ ptr: *Surface,
+ result: *Text,
+ ) bool {
+ const surface = &ptr.core_surface;
+ surface.renderer_state.mutex.lock();
+ defer surface.renderer_state.mutex.unlock();
+
+ // Get our word selection
+ const sel = sel: {
+ const screen: *terminal.Screen = surface.renderer_state.terminal.screens.active;
+ const pos = try ptr.getCursorPos();
+ const pt_viewport = surface.posToViewport(pos.x, pos.y);
+ const pin = screen.pages.pin(.{
+ .viewport = .{
+ .x = pt_viewport.x,
+ .y = pt_viewport.y,
+ },
+ }) orelse {
+ if (comptime std.debug.runtime_safety) unreachable;
+ return false;
+ };
+ break :sel surface.io.terminal.screens.active.selectWord(
+ pin,
+ surface.config.selection_word_chars,
+ ) orelse return false;
+ };
+
+ // Read the selection
+ return readTextLocked(ptr, sel, result);
+ }
+
+ export fn ghostty_inspector_metal_init(ptr: *Inspector, device: objc.c.id) bool {
+ return ptr.initMetal(.fromId(device));
+ }
+
+ export fn ghostty_inspector_metal_render(
+ ptr: *Inspector,
+ command_buffer: objc.c.id,
+ descriptor: objc.c.id,
+ ) void {
+ return ptr.renderMetal(
+ .fromId(command_buffer),
+ .fromId(descriptor),
+ ) catch |err| {
+ log.err("error rendering inspector err={}", .{err});
+ return;
+ };
+ }
+
+ export fn ghostty_inspector_metal_shutdown(ptr: *Inspector) void {
+ if (ptr.backend) |v| {
+ v.deinit();
+ ptr.backend = null;
+ }
+ }
+ };
+};
diff --git a/vendor/libghostty-vt/src/apprt/gtk.zig b/vendor/libghostty-vt/src/apprt/gtk.zig
new file mode 100644
index 00000000..eb33c4e4
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk.zig
@@ -0,0 +1,17 @@
+// The required comptime API for any apprt.
+pub const App = @import("gtk/App.zig");
+pub const Surface = @import("gtk/Surface.zig");
+pub const resourcesDir = @import("gtk/flatpak.zig").resourcesDir;
+
+// The exported API, custom for the apprt.
+pub const class = @import("gtk/class.zig");
+pub const WeakRef = @import("gtk/weak_ref.zig").WeakRef;
+pub const pre_exec = @import("gtk/pre_exec.zig");
+pub const post_fork = @import("gtk/post_fork.zig");
+
+test {
+ @import("std").testing.refAllDecls(@This());
+ _ = @import("gtk/ext.zig");
+ _ = @import("gtk/key.zig");
+ _ = @import("gtk/portal.zig");
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/App.zig b/vendor/libghostty-vt/src/apprt/gtk/App.zig
new file mode 100644
index 00000000..39c13c19
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/App.zig
@@ -0,0 +1,93 @@
+/// This is the main entrypoint to the apprt for Ghostty. Ghostty will
+/// initialize this in main to start the application..
+const App = @This();
+
+const std = @import("std");
+const builtin = @import("builtin");
+const Allocator = std.mem.Allocator;
+const apprt = @import("../../apprt.zig");
+const configpkg = @import("../../config.zig");
+const Config = configpkg.Config;
+const CoreApp = @import("../../App.zig");
+
+const Application = @import("class/application.zig").Application;
+const Surface = @import("Surface.zig");
+const ipcNewWindow = @import("ipc/new_window.zig").newWindow;
+
+const log = std.log.scoped(.gtk);
+
+/// This is detected by the Renderer, in which case it sends a `redraw_surface`
+/// message so that we can call `drawFrame` ourselves from the app thread,
+/// because GTK's `GLArea` does not support drawing from a different thread.
+pub const must_draw_from_app_thread = true;
+
+/// GTK application ID
+pub const application_id = @import("build/info.zig").application_id;
+
+/// GTK object path
+pub const object_path = @import("build/info.zig").object_path;
+
+/// The GObject Application instance
+app: *Application,
+
+pub fn init(
+ self: *App,
+ core_app: *CoreApp,
+
+ // Required by the apprt interface but we don't use it.
+ opts: struct {},
+) !void {
+ _ = opts;
+
+ const app: *Application = try .new(self, core_app);
+ errdefer app.unref();
+ self.* = .{ .app = app };
+ return;
+}
+
+pub fn run(self: *App) !void {
+ try self.app.run();
+}
+
+pub fn terminate(self: *App) void {
+ // We force deinitialize the app. We don't unref because other things
+ // tend to have a reference at this point, so this just forces the
+ // disposal now.
+ self.app.deinit();
+}
+
+/// Called by CoreApp to wake up the event loop.
+pub fn wakeup(self: *App) void {
+ self.app.wakeup();
+}
+
+pub fn performAction(
+ self: *App,
+ target: apprt.Target,
+ comptime action: apprt.Action.Key,
+ value: apprt.Action.Value(action),
+) !bool {
+ return try self.app.performAction(target, action, value);
+}
+
+/// Send the given IPC to a running Ghostty. Returns `true` if the action was
+/// able to be performed, `false` otherwise.
+///
+/// Note that this is a static function. Since this is called from a CLI app (or
+/// some other process that is not Ghostty) there is no full-featured apprt App
+/// to use.
+pub fn performIpc(
+ alloc: Allocator,
+ target: apprt.ipc.Target,
+ comptime action: apprt.ipc.Action.Key,
+ value: apprt.ipc.Action.Value(action),
+) !bool {
+ switch (action) {
+ .new_window => return try ipcNewWindow(alloc, target, value),
+ }
+}
+
+/// Redraw the inspector for the given surface.
+pub fn redrawInspector(_: *App, surface: *Surface) void {
+ surface.redrawInspector();
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/Surface.zig b/vendor/libghostty-vt/src/apprt/gtk/Surface.zig
new file mode 100644
index 00000000..71597367
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/Surface.zig
@@ -0,0 +1,104 @@
+const Self = @This();
+
+const std = @import("std");
+const apprt = @import("../../apprt.zig");
+const configpkg = @import("../../config.zig");
+const CoreSurface = @import("../../Surface.zig");
+const ApprtApp = @import("App.zig");
+const Application = @import("class/application.zig").Application;
+const Surface = @import("class/surface.zig").Surface;
+
+/// The GObject Surface
+surface: *Surface,
+
+pub fn deinit(self: *Self) void {
+ _ = self;
+}
+
+/// Returns the GObject surface for this apprt surface. This is a function
+/// so we can add some extra logic if we ever have to here.
+pub fn gobj(self: *Self) *Surface {
+ return self.surface;
+}
+
+pub fn core(self: *Self) *CoreSurface {
+ // This asserts the non-optional because libghostty should only
+ // be calling this for initialized surfaces.
+ return self.surface.core().?;
+}
+
+pub fn rtApp(self: *Self) *ApprtApp {
+ _ = self;
+ return Application.default().rt();
+}
+
+pub fn close(self: *Self, process_active: bool) void {
+ _ = process_active;
+ self.surface.close();
+}
+
+pub fn cgroup(self: *Self) ?[]const u8 {
+ return self.surface.cgroupPath();
+}
+
+pub fn getTitle(self: *Self) ?[:0]const u8 {
+ return self.surface.getTitle();
+}
+
+pub fn getContentScale(self: *const Self) !apprt.ContentScale {
+ return self.surface.getContentScale();
+}
+
+pub fn getSize(self: *const Self) !apprt.SurfaceSize {
+ return self.surface.getSize();
+}
+
+pub fn getCursorPos(self: *const Self) !apprt.CursorPos {
+ return self.surface.getCursorPos();
+}
+
+pub fn supportsClipboard(
+ self: *const Self,
+ clipboard_type: apprt.Clipboard,
+) bool {
+ _ = self;
+ return switch (clipboard_type) {
+ .standard,
+ .selection,
+ .primary,
+ => true,
+ };
+}
+
+pub fn clipboardRequest(
+ self: *Self,
+ clipboard_type: apprt.Clipboard,
+ state: apprt.ClipboardRequest,
+) !bool {
+ return try self.surface.clipboardRequest(
+ clipboard_type,
+ state,
+ );
+}
+
+pub fn setClipboard(
+ self: *Self,
+ clipboard_type: apprt.Clipboard,
+ contents: []const apprt.ClipboardContent,
+ confirm: bool,
+) !void {
+ self.surface.setClipboard(
+ clipboard_type,
+ contents,
+ confirm,
+ );
+}
+
+pub fn defaultTermioEnv(self: *Self) !std.process.EnvMap {
+ return try self.surface.defaultTermioEnv();
+}
+
+/// Redraw the inspector for our surface.
+pub fn redrawInspector(self: *Self) void {
+ self.surface.redrawInspector();
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/adw_version.zig b/vendor/libghostty-vt/src/apprt/gtk/adw_version.zig
new file mode 100644
index 00000000..6f7be52d
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/adw_version.zig
@@ -0,0 +1,122 @@
+const std = @import("std");
+
+// Until the gobject bindings are built at the same time we are building
+// Ghostty, we need to import `adwaita.h` directly to ensure that the version
+// macros match the version of `libadwaita` that we are building/linking
+// against.
+const c = @cImport({
+ @cInclude("adwaita.h");
+});
+
+const adw = @import("adw");
+
+const log = std.log.scoped(.gtk);
+
+pub const comptime_version: std.SemanticVersion = .{
+ .major = c.ADW_MAJOR_VERSION,
+ .minor = c.ADW_MINOR_VERSION,
+ .patch = c.ADW_MICRO_VERSION,
+};
+
+pub fn getRuntimeVersion() std.SemanticVersion {
+ return .{
+ .major = adw.getMajorVersion(),
+ .minor = adw.getMinorVersion(),
+ .patch = adw.getMicroVersion(),
+ };
+}
+
+pub fn logVersion() void {
+ log.info("libadwaita version build={f} runtime={f}", .{
+ comptime_version,
+ getRuntimeVersion(),
+ });
+}
+
+/// Verifies that the running libadwaita version is at least the given
+/// version. This will return false if Ghostty is configured to not build with
+/// libadwaita.
+///
+/// This can be run in both a comptime and runtime context. If it is run in a
+/// comptime context, it will only check the version in the headers. If it is
+/// run in a runtime context, it will check the actual version of the library we
+/// are linked against. So generally you probably want to do both checks!
+///
+/// This is inlined so that the comptime checks will disable the runtime checks
+/// if the comptime checks fail.
+pub inline fn atLeast(
+ comptime major: u16,
+ comptime minor: u16,
+ comptime micro: u16,
+) bool {
+ // If our header has lower versions than the given version, we can return
+ // false immediately. This prevents us from compiling against unknown
+ // symbols and makes runtime checks very slightly faster.
+ if (comptime comptime_version.order(.{
+ .major = major,
+ .minor = minor,
+ .patch = micro,
+ }) == .lt) return false;
+
+ // If we're in comptime then we can't check the runtime version.
+ if (@inComptime()) return true;
+
+ return runtimeAtLeast(major, minor, micro);
+}
+
+/// Verifies that the libadwaita version at runtime is at least the given version.
+///
+/// This function should be used in cases where the only the runtime behavior
+/// is affected by the version check. For checks which would affect code
+/// generation, use `atLeast`.
+pub inline fn runtimeAtLeast(
+ comptime major: u16,
+ comptime minor: u16,
+ comptime micro: u16,
+) bool {
+ // We use the functions instead of the constants such as c.GTK_MINOR_VERSION
+ // because the function gets the actual runtime version.
+ const runtime_version = getRuntimeVersion();
+ return runtime_version.order(.{
+ .major = major,
+ .minor = minor,
+ .patch = micro,
+ }) != .lt;
+}
+
+test "versionAtLeast" {
+ const testing = std.testing;
+
+ const funs = &.{ atLeast, runtimeAtLeast };
+ inline for (funs) |fun| {
+ try testing.expect(fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION));
+ try testing.expect(!fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION + 1));
+ try testing.expect(!fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION + 1, c.ADW_MICRO_VERSION));
+ try testing.expect(!fun(c.ADW_MAJOR_VERSION + 1, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION));
+ try testing.expect(fun(c.ADW_MAJOR_VERSION - 1, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION));
+ try testing.expect(fun(c.ADW_MAJOR_VERSION - 1, c.ADW_MINOR_VERSION + 1, c.ADW_MICRO_VERSION));
+ try testing.expect(fun(c.ADW_MAJOR_VERSION - 1, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION + 1));
+ try testing.expect(fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION - 1, c.ADW_MICRO_VERSION + 1));
+ }
+}
+
+// Whether AdwDialog, AdwAlertDialog, etc. are supported (1.5+)
+pub inline fn supportsDialogs() bool {
+ return atLeast(1, 5, 0);
+}
+
+pub inline fn supportsTabOverview() bool {
+ return atLeast(1, 4, 0);
+}
+
+pub inline fn supportsSwitchRow() bool {
+ return atLeast(1, 4, 0);
+}
+
+pub inline fn supportsToolbarView() bool {
+ return atLeast(1, 4, 0);
+}
+
+pub inline fn supportsBanner() bool {
+ return atLeast(1, 3, 0);
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/build/blueprint.zig b/vendor/libghostty-vt/src/apprt/gtk/build/blueprint.zig
new file mode 100644
index 00000000..4920ce6f
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/build/blueprint.zig
@@ -0,0 +1,174 @@
+//! Compiles a blueprint file using `blueprint-compiler`. This performs
+//! additional checks to ensure that various minimum versions are met.
+//!
+//! Usage: blueprint.zig <major> <minor> <output> <input>
+//!
+//! Example: blueprint.zig 1 5 output.ui input.blp
+
+const std = @import("std");
+
+pub const c = @cImport({
+ @cInclude("adwaita.h");
+});
+
+pub const blueprint_compiler_help =
+ \\
+ \\When building from a Git checkout, Ghostty requires
+ \\version {f} or newer of `blueprint-compiler` as a
+ \\build-time dependency. Please install it, ensure that it
+ \\is available on your PATH, and then retry building Ghostty.
+ \\See `HACKING.md` for more details.
+ \\
+ \\This message should *not* appear for normal users, who
+ \\should build Ghostty from official release tarballs instead.
+ \\Please consult https://ghostty.org/docs/install/build for
+ \\more information on the recommended build instructions.
+;
+
+const adwaita_version = std.SemanticVersion{
+ .major = c.ADW_MAJOR_VERSION,
+ .minor = c.ADW_MINOR_VERSION,
+ .patch = c.ADW_MICRO_VERSION,
+};
+const required_blueprint_version = std.SemanticVersion{
+ .major = 0,
+ .minor = 16,
+ .patch = 0,
+};
+
+pub fn main() !void {
+ var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
+ defer _ = debug_allocator.deinit();
+ const alloc = debug_allocator.allocator();
+
+ // Get our args
+ var it = try std.process.argsWithAllocator(alloc);
+ defer it.deinit();
+ _ = it.next(); // Skip argv0
+ const arg_major = it.next() orelse return error.NoMajorVersion;
+ const arg_minor = it.next() orelse return error.NoMinorVersion;
+ const output = it.next() orelse return error.NoOutput;
+ const input = it.next() orelse return error.NoInput;
+
+ const required_adwaita_version = std.SemanticVersion{
+ .major = try std.fmt.parseUnsigned(u8, arg_major, 10),
+ .minor = try std.fmt.parseUnsigned(u8, arg_minor, 10),
+ .patch = 0,
+ };
+ if (adwaita_version.order(required_adwaita_version) == .lt) {
+ std.debug.print(
+ \\`libadwaita` is too old.
+ \\
+ \\Ghostty requires a version {f} or newer of `libadwaita` to
+ \\compile this blueprint. Please install it, ensure that it is
+ \\available on your PATH, and then retry building Ghostty.
+ , .{required_adwaita_version});
+ std.posix.exit(1);
+ }
+
+ // Version checks
+ {
+ var stdout: std.ArrayListUnmanaged(u8) = .empty;
+ defer stdout.deinit(alloc);
+ var stderr: std.ArrayListUnmanaged(u8) = .empty;
+ defer stderr.deinit(alloc);
+
+ var blueprint_compiler = std.process.Child.init(
+ &.{
+ "blueprint-compiler",
+ "--version",
+ },
+ alloc,
+ );
+ blueprint_compiler.stdout_behavior = .Pipe;
+ blueprint_compiler.stderr_behavior = .Pipe;
+ try blueprint_compiler.spawn();
+ try blueprint_compiler.collectOutput(
+ alloc,
+ &stdout,
+ &stderr,
+ std.math.maxInt(u16),
+ );
+ const term = blueprint_compiler.wait() catch |err| switch (err) {
+ error.FileNotFound => {
+ std.debug.print(
+ \\`blueprint-compiler` not found.
+ ++ blueprint_compiler_help,
+ .{required_blueprint_version},
+ );
+ std.posix.exit(1);
+ },
+ else => return err,
+ };
+ switch (term) {
+ .Exited => |rc| if (rc != 0) std.process.exit(1),
+ else => std.process.exit(1),
+ }
+
+ const version = try std.SemanticVersion.parse(std.mem.trim(
+ u8,
+ stdout.items,
+ &std.ascii.whitespace,
+ ));
+ if (version.order(required_blueprint_version) == .lt) {
+ std.debug.print(
+ \\`blueprint-compiler` is the wrong version.
+ ++ blueprint_compiler_help,
+ .{required_blueprint_version},
+ );
+ std.posix.exit(1);
+ }
+ }
+
+ // Compilation
+ {
+ var stdout: std.ArrayListUnmanaged(u8) = .empty;
+ defer stdout.deinit(alloc);
+ var stderr: std.ArrayListUnmanaged(u8) = .empty;
+ defer stderr.deinit(alloc);
+
+ var blueprint_compiler = std.process.Child.init(
+ &.{
+ "blueprint-compiler",
+ "compile",
+ "--output",
+ output,
+ input,
+ },
+ alloc,
+ );
+ blueprint_compiler.stdout_behavior = .Pipe;
+ blueprint_compiler.stderr_behavior = .Pipe;
+ try blueprint_compiler.spawn();
+ try blueprint_compiler.collectOutput(
+ alloc,
+ &stdout,
+ &stderr,
+ std.math.maxInt(u16),
+ );
+ const term = blueprint_compiler.wait() catch |err| switch (err) {
+ error.FileNotFound => {
+ std.debug.print(
+ \\`blueprint-compiler` not found.
+ ++ blueprint_compiler_help,
+ .{required_blueprint_version},
+ );
+ std.posix.exit(1);
+ },
+ else => return err,
+ };
+
+ switch (term) {
+ .Exited => |rc| {
+ if (rc != 0) {
+ std.debug.print("{s}", .{stderr.items});
+ std.process.exit(1);
+ }
+ },
+ else => {
+ std.debug.print("{s}", .{stderr.items});
+ std.process.exit(1);
+ },
+ }
+ }
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/build/gresource.zig b/vendor/libghostty-vt/src/apprt/gtk/build/gresource.zig
new file mode 100644
index 00000000..c50ea8cd
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/build/gresource.zig
@@ -0,0 +1,277 @@
+//! This file contains a binary helper that builds our gresource XML
+//! file that we can then use with `glib-compile-resources`.
+//!
+//! This binary is expected to be run from the Ghostty source root.
+//! Litmus test: `src/apprt/gtk` should exist relative to the pwd.
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+const build_info = @import("info.zig");
+
+/// The path to the Blueprint files. The folder structure is expected to be
+/// `{version}/{name}.blp` where `version` is the major and minor
+/// minimum adwaita version.
+pub const ui_path = "src/apprt/gtk/ui";
+
+/// The path to the CSS files.
+pub const css_path = "src/apprt/gtk/css";
+
+/// The possible icon sizes we'll embed into the gresource file.
+/// If any size doesn't exist then it will be an error. We could
+/// infer this completely from available files but we wouldn't be
+/// able to error when they don't exist that way.
+pub const icon_sizes: []const comptime_int = &.{ 16, 32, 128, 256, 512, 1024 };
+
+/// The blueprint files that we will embed into the gresource file.
+/// We can't look these up at runtime [easily] because we require the
+/// compiled UI files as input. We can refactor this lator to maybe do
+/// all of this automatically and ensure we have the right dependencies
+/// setup in the build system.
+///
+/// These will be asserted to exist at runtime.
+pub const blueprints: []const Blueprint = &.{
+ .{ .major = 1, .minor = 0, .name = "clipboard-confirmation-dialog" },
+ .{ .major = 1, .minor = 4, .name = "clipboard-confirmation-dialog" },
+ .{ .major = 1, .minor = 2, .name = "close-confirmation-dialog" },
+ .{ .major = 1, .minor = 2, .name = "config-errors-dialog" },
+ .{ .major = 1, .minor = 2, .name = "debug-warning" },
+ .{ .major = 1, .minor = 3, .name = "debug-warning" },
+ .{ .major = 1, .minor = 5, .name = "imgui-widget" },
+ .{ .major = 1, .minor = 5, .name = "inspector-widget" },
+ .{ .major = 1, .minor = 5, .name = "inspector-window" },
+ .{ .major = 1, .minor = 2, .name = "resize-overlay" },
+ .{ .major = 1, .minor = 2, .name = "search-overlay" },
+ .{ .major = 1, .minor = 2, .name = "key-state-overlay" },
+ .{ .major = 1, .minor = 5, .name = "split-tree" },
+ .{ .major = 1, .minor = 5, .name = "split-tree-split" },
+ .{ .major = 1, .minor = 2, .name = "surface" },
+ .{ .major = 1, .minor = 5, .name = "surface-scrolled-window" },
+ .{ .major = 1, .minor = 3, .name = "surface-child-exited" },
+ .{ .major = 1, .minor = 5, .name = "tab" },
+ .{ .major = 1, .minor = 5, .name = "title-dialog" },
+ .{ .major = 1, .minor = 5, .name = "window" },
+ .{ .major = 1, .minor = 5, .name = "command-palette" },
+};
+
+/// CSS files in css_path
+pub const css = [_][]const u8{
+ "style.css",
+ "style-dark.css",
+ "style-hc.css",
+ "style-hc-dark.css",
+};
+
+pub const Blueprint = struct {
+ major: u16,
+ minor: u16,
+ name: []const u8,
+};
+
+/// The list of filepaths that we depend on. Used for the build
+/// system to have proper caching.
+pub const file_inputs = deps: {
+ const total = (icon_sizes.len * 2) + blueprints.len + css.len;
+ var deps: [total][]const u8 = undefined;
+ var index: usize = 0;
+ for (icon_sizes) |size| {
+ deps[index] = std.fmt.comptimePrint("images/gnome/{d}.png", .{size});
+ deps[index + 1] = std.fmt.comptimePrint("images/gnome/{d}.png", .{size * 2});
+ index += 2;
+ }
+ for (blueprints) |bp| {
+ deps[index] = std.fmt.comptimePrint("{s}/{d}.{d}/{s}.blp", .{
+ ui_path,
+ bp.major,
+ bp.minor,
+ bp.name,
+ });
+ index += 1;
+ }
+ for (css) |name| {
+ deps[index] = std.fmt.comptimePrint("{s}/{s}", .{ css_path, name });
+ index += 1;
+ }
+ break :deps deps;
+};
+
+/// Returns the matching blueprint resource path for the given blueprint
+/// definition. This will fail at compile time if the blueprint is not
+/// found.
+///
+/// Must be called at comptime.
+pub fn blueprint(comptime bp: Blueprint) [:0]const u8 {
+ // The comptime block around this whole thing forces an error if
+ // the caller attempts to call this function at runtime.
+ comptime {
+ for (blueprints) |candidate| {
+ if (candidate.major == bp.major and
+ candidate.minor == bp.minor and
+ std.mem.eql(u8, candidate.name, bp.name))
+ {
+ return std.fmt.comptimePrint("{s}/ui/{d}.{d}/{s}.ui", .{
+ build_info.resource_path,
+ candidate.major,
+ candidate.minor,
+ candidate.name,
+ });
+ }
+ }
+
+ @compileError("invalid blueprint");
+ }
+}
+
+pub fn main() !void {
+ var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
+ defer _ = debug_allocator.deinit();
+ const alloc = debug_allocator.allocator();
+
+ // Collect the UI files that are passed in as arguments.
+ var ui_files: std.ArrayListUnmanaged([]const u8) = .empty;
+ defer {
+ for (ui_files.items) |item| alloc.free(item);
+ ui_files.deinit(alloc);
+ }
+ var it = try std.process.argsWithAllocator(alloc);
+ defer it.deinit();
+ while (it.next()) |arg| {
+ if (!std.mem.endsWith(u8, arg, ".ui")) continue;
+ try ui_files.append(
+ alloc,
+ try alloc.dupe(u8, arg),
+ );
+ }
+
+ var buf: [4096]u8 = undefined;
+ var stdout = std.fs.File.stdout().writer(&buf);
+ const writer = &stdout.interface;
+ try writer.writeAll(
+ \\<?xml version="1.0" encoding="UTF-8"?>
+ \\<gresources>
+ \\
+ );
+
+ try genRoot(writer);
+ try genIcons(writer);
+ try genUi(alloc, writer, &ui_files);
+
+ try writer.writeAll(
+ \\</gresources>
+ \\
+ );
+
+ try stdout.end();
+}
+
+/// Generate the icon resources. This works by looking up all the icons
+/// specified by `icon_sizes` in `images/icons/`. They are asserted to exist
+/// by trying to access the file.
+fn genIcons(writer: *std.Io.Writer) !void {
+ try writer.print(
+ \\ <gresource prefix="{s}/icons">
+ \\
+ , .{build_info.resource_path});
+
+ const cwd = std.fs.cwd();
+ inline for (icon_sizes) |size| {
+ // 1x
+ {
+ const alias = std.fmt.comptimePrint("{d}x{d}", .{ size, size });
+ const source = std.fmt.comptimePrint("images/gnome/{d}.png", .{size});
+ try cwd.access(source, .{});
+ try writer.print(
+ \\ <file alias="{s}/apps/{s}.png">{s}</file>
+ \\
+ ,
+ .{ alias, build_info.base_application_id, source },
+ );
+ }
+
+ // 2x
+ {
+ const alias = std.fmt.comptimePrint("{d}x{d}@2", .{ size, size });
+ const source = std.fmt.comptimePrint("images/gnome/{d}.png", .{size * 2});
+ try cwd.access(source, .{});
+ try writer.print(
+ \\ <file alias="{s}/apps/{s}.png">{s}</file>
+ \\
+ ,
+ .{ alias, build_info.base_application_id, source },
+ );
+ }
+ }
+
+ try writer.writeAll(
+ \\ </gresource>
+ \\
+ );
+}
+
+/// Generate the resources at the root prefix.
+fn genRoot(writer: *std.Io.Writer) !void {
+ try writer.print(
+ \\ <gresource prefix="{s}">
+ \\
+ , .{build_info.resource_path});
+
+ const cwd = std.fs.cwd();
+ inline for (css) |name| {
+ const source = std.fmt.comptimePrint(
+ "{s}/{s}",
+ .{ css_path, name },
+ );
+ try cwd.access(source, .{});
+ try writer.print(
+ \\ <file compressed="true" alias="{s}">{s}</file>
+ \\
+ ,
+ .{ name, source },
+ );
+ }
+
+ try writer.writeAll(
+ \\ </gresource>
+ \\
+ );
+}
+
+/// Generate all the UI resources. This works by looking up all the
+/// blueprint files in `${ui_path}/{major}.{minor}/{name}.blp` and
+/// assuming these will be
+fn genUi(
+ alloc: Allocator,
+ writer: *std.Io.Writer,
+ files: *const std.ArrayListUnmanaged([]const u8),
+) !void {
+ try writer.print(
+ \\ <gresource prefix="{s}/ui">
+ \\
+ , .{build_info.resource_path});
+
+ for (files.items) |ui_file| {
+ for (blueprints) |bp| {
+ const expected = try std.fmt.allocPrint(
+ alloc,
+ "/{d}.{d}/{s}.ui",
+ .{ bp.major, bp.minor, bp.name },
+ );
+ defer alloc.free(expected);
+ if (!std.mem.endsWith(u8, ui_file, expected)) continue;
+ try writer.print(
+ " <file compressed=\"true\" preprocess=\"xml-stripblanks\" alias=\"{d}.{d}/{s}.ui\">{s}</file>\n",
+ .{ bp.major, bp.minor, bp.name, ui_file },
+ );
+ break;
+ } else {
+ // The for loop never broke which means it didn't find
+ // a matching blueprint for this input.
+ return error.BlueprintNotFound;
+ }
+ }
+
+ try writer.writeAll(
+ \\ </gresource>
+ \\
+ );
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/build/info.zig b/vendor/libghostty-vt/src/apprt/gtk/build/info.zig
new file mode 100644
index 00000000..fc6478d8
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/build/info.zig
@@ -0,0 +1,18 @@
+const builtin = @import("builtin");
+
+/// Base application ID
+pub const base_application_id = "com.mitchellh.ghostty";
+
+/// GTK application ID
+pub const application_id = switch (builtin.mode) {
+ .Debug, .ReleaseSafe => base_application_id ++ "-debug",
+ .ReleaseFast, .ReleaseSmall => base_application_id,
+};
+
+pub const resource_path = "/com/mitchellh/ghostty";
+
+/// GTK object path
+pub const object_path = switch (builtin.mode) {
+ .Debug, .ReleaseSafe => resource_path ++ "_debug",
+ .ReleaseFast, .ReleaseSmall => resource_path,
+};
diff --git a/vendor/libghostty-vt/src/apprt/gtk/cgroup.zig b/vendor/libghostty-vt/src/apprt/gtk/cgroup.zig
new file mode 100644
index 00000000..868aa268
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/cgroup.zig
@@ -0,0 +1,115 @@
+/// Contains all the logic for putting individual surfaces into
+/// transient systemd scopes.
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const assert = @import("../../quirks.zig").inlineAssert;
+
+const gio = @import("gio");
+const glib = @import("glib");
+
+const internal_os = @import("../../os/main.zig");
+
+const log = std.log.scoped(.gtk_systemd_cgroup);
+
+pub const Options = struct {
+ memory_high: ?u64 = null,
+ tasks_max: ?u64 = null,
+};
+
+pub fn fmtScope(buf: []u8, pid: u32) [:0]const u8 {
+ const fmt = "app-ghostty-surface-transient-{}.scope";
+
+ assert(buf.len >= fmt.len - 2 + std.math.log10_int(@as(usize, std.math.maxInt(@TypeOf(pid)))) + 1);
+
+ return std.fmt.bufPrintZ(buf, fmt, .{pid}) catch unreachable;
+}
+
+/// Create a transient systemd scope unit for the given process and
+/// move the process into it.
+pub fn createScope(
+ dbus: *gio.DBusConnection,
+ pid: u32,
+ options: Options,
+) error{DbusCallFailed}!void {
+ // The unit name needs to be unique. We use the PID for this.
+ var name_buf: [256]u8 = undefined;
+ const name = fmtScope(&name_buf, pid);
+
+ const builder_type = glib.VariantType.new("(ssa(sv)a(sa(sv)))");
+ defer glib.free(builder_type);
+
+ // Initialize our builder to build up our parameters
+ var builder: glib.VariantBuilder = undefined;
+ builder.init(builder_type);
+
+ builder.add("s", name.ptr);
+ builder.add("s", "fail");
+
+ {
+ // Properties
+ const properties_type = glib.VariantType.new("a(sv)");
+ defer glib.free(properties_type);
+
+ builder.open(properties_type);
+ defer builder.close();
+
+ if (options.memory_high) |value| {
+ builder.add("(sv)", "MemoryHigh", glib.Variant.newUint64(value));
+ }
+
+ if (options.tasks_max) |value| {
+ builder.add("(sv)", "TasksMax", glib.Variant.newUint64(value));
+ }
+
+ // https://www.freedesktop.org/software/systemd/man/latest/systemd-oomd.service.html
+ builder.add("(sv)", "ManagedOOMMemoryPressure", glib.Variant.newString("kill"));
+
+ // PID to move into the unit
+ const pids_value_type = glib.VariantType.new("u");
+ defer glib.free(pids_value_type);
+
+ const pids_value = glib.Variant.newFixedArray(pids_value_type, &pid, 1, @sizeOf(u32));
+
+ builder.add("(sv)", "PIDs", pids_value);
+ }
+
+ {
+ // Aux - unused but must be present
+ const aux_type = glib.VariantType.new("a(sa(sv))");
+ defer glib.free(aux_type);
+
+ builder.open(aux_type);
+ defer builder.close();
+ }
+
+ var err: ?*glib.Error = null;
+ defer if (err) |e| e.free();
+
+ const reply_type = glib.VariantType.new("(o)");
+ defer glib.free(reply_type);
+
+ const value = builder.end();
+
+ const reply = dbus.callSync(
+ "org.freedesktop.systemd1",
+ "/org/freedesktop/systemd1",
+ "org.freedesktop.systemd1.Manager",
+ "StartTransientUnit",
+ value,
+ reply_type,
+ .{},
+ -1,
+ null,
+ &err,
+ ) orelse {
+ if (err) |e| log.err(
+ "creating transient cgroup scope failed code={} err={s}",
+ .{
+ e.f_code,
+ if (e.f_message) |msg| msg else "(no message)",
+ },
+ );
+ return error.DbusCallFailed;
+ };
+ defer reply.unref();
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/class.zig b/vendor/libghostty-vt/src/apprt/gtk/class.zig
new file mode 100644
index 00000000..942666cf
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/class.zig
@@ -0,0 +1,367 @@
+//! This files contains all the GObject classes for the GTK apprt
+//! along with helpers to work with them.
+
+const std = @import("std");
+const glib = @import("glib");
+const gobject = @import("gobject");
+const gtk = @import("gtk");
+
+const ext = @import("ext.zig");
+pub const Application = @import("class/application.zig").Application;
+pub const Window = @import("class/window.zig").Window;
+pub const Config = @import("class/config.zig").Config;
+pub const Surface = @import("class/surface.zig").Surface;
+
+/// Common methods for all GObject classes we create.
+pub fn Common(
+ comptime Self: type,
+ comptime Private: ?type,
+) type {
+ return struct {
+ /// Upcast our type to a parent type or interface. This will fail at
+ /// compile time if the cast isn't 100% safe. For unsafe casts,
+ /// use `gobject.ext.cast` instead. We don't have a helper for that
+ /// because its uncommon and unsafe behavior should be noisier.
+ pub fn as(self: *Self, comptime T: type) *T {
+ return gobject.ext.as(T, self);
+ }
+
+ /// Increase the reference count of the object.
+ pub fn ref(self: *Self) *Self {
+ return @ptrCast(@alignCast(gobject.Object.ref(self.as(gobject.Object))));
+ }
+
+ /// If the reference count is 1 and the object is floating, clear the
+ /// floating attribute. Otherwise, increase the reference count by 1.
+ pub fn refSink(self: *Self) *Self {
+ return @ptrCast(@alignCast(gobject.Object.refSink(self.as(gobject.Object))));
+ }
+
+ /// Decrease the reference count of the object.
+ pub fn unref(self: *Self) void {
+ gobject.Object.unref(self.as(gobject.Object));
+ }
+
+ /// Access the private data of the object. This should be forwarded
+ /// via a non-pub const usually.
+ pub const private = if (Private) |P| (struct {
+ fn private(self: *Self) *P {
+ return gobject.ext.impl_helpers.getPrivate(
+ self,
+ P,
+ P.offset,
+ );
+ }
+ }).private else {};
+
+ /// Get the class for the object.
+ ///
+ /// This _seems_ ugly and unsafe but this is how GObject
+ /// works under the hood. From the [GObject Type System
+ /// Concepts](https://docs.gtk.org/gobject/concepts.html) documentation:
+ ///
+ /// Every object must define two structures: its class structure
+ /// and its instance structure. All class structures must contain
+ /// as first member a GTypeClass structure. All instance structures
+ /// must contain as first member a GTypeInstance structure.
+ /// …
+ /// These constraints allow the type system to make sure that
+ /// every object instance (identified by a pointer to the object’s
+ /// instance structure) contains in its first bytes a pointer to the
+ /// object’s class structure.
+ /// …
+ /// The C standard mandates that the first field of a C structure is
+ /// stored starting in the first byte of the buffer used to hold the
+ /// structure’s fields in memory. This means that the first field of
+ /// an instance of an object B is A’s first field which in turn is
+ /// GTypeInstance‘s first field which in turn is g_class, a pointer
+ /// to B’s class structure.
+ ///
+ /// This means that to access the class structure for an object you cast it
+ /// to `*gobject.TypeInstance` and then access the `f_g_class` field.
+ ///
+ /// https://gitlab.gnome.org/GNOME/glib/-/blob/2c08654b62d52a31c4e4d13d7d85e12b989e72be/gobject/gtype.h#L555-571
+ /// https://gitlab.gnome.org/GNOME/glib/-/blob/2c08654b62d52a31c4e4d13d7d85e12b989e72be/gobject/gtype.h#L2673
+ ///
+ pub fn getClass(self: *Self) ?*Self.Class {
+ const type_instance: *gobject.TypeInstance = @ptrCast(self);
+ return @ptrCast(type_instance.f_g_class orelse return null);
+ }
+
+ /// Define a virtual method. The `Self.Class` type must have a field
+ /// named `name` which is a function pointer in the following form:
+ ///
+ /// ?*const fn (*Self) callconv(.c) void
+ ///
+ /// The virtual method may take additional parameters and specify
+ /// a non-void return type. The parameters and return type must be
+ /// valid for the C calling convention.
+ pub fn defineVirtualMethod(
+ comptime name: [:0]const u8,
+ ) type {
+ return struct {
+ pub fn call(
+ class: anytype,
+ object: *ClassInstance(@TypeOf(class)),
+ params: anytype,
+ ) (fn_info.return_type orelse void) {
+ const func = @field(
+ gobject.ext.as(Self.Class, class),
+ name,
+ ).?;
+ @call(.auto, func, .{
+ gobject.ext.as(Self, object),
+ } ++ params);
+ }
+
+ pub fn implement(
+ class: anytype,
+ implementation: *const ImplementFunc(@TypeOf(class)),
+ ) void {
+ @field(gobject.ext.as(
+ Self.Class,
+ class,
+ ), name) = @ptrCast(implementation);
+ }
+
+ /// The type info of the virtual method.
+ const fn_info = fn_info: {
+ // This is broken down like this so its slightly more
+ // readable. We expect a field named "name" on the Class
+ // with the rough type of `?*const fn` and we need the
+ // function info.
+ const Field = @FieldType(Self.Class, name);
+ const opt = @typeInfo(Field).optional;
+ const ptr = @typeInfo(opt.child).pointer;
+ break :fn_info @typeInfo(ptr.child).@"fn";
+ };
+
+ /// The instance type for a class.
+ fn ClassInstance(comptime T: type) type {
+ return @typeInfo(T).pointer.child.Instance;
+ }
+
+ /// The function type for implementations. This is the same type
+ /// as the virtual method but the self parameter points to the
+ /// target instead of the original class.
+ fn ImplementFunc(comptime T: type) type {
+ var params: [fn_info.params.len]std.builtin.Type.Fn.Param = undefined;
+ @memcpy(&params, fn_info.params);
+ params[0].type = *ClassInstance(T);
+ return @Type(.{ .@"fn" = .{
+ .calling_convention = fn_info.calling_convention,
+ .is_generic = fn_info.is_generic,
+ .is_var_args = fn_info.is_var_args,
+ .return_type = fn_info.return_type,
+ .params = &params,
+ } });
+ }
+ };
+ }
+
+ /// A helper that creates a property that reads and writes a
+ /// private field with only shallow copies. This is good for primitives
+ /// such as bools, numbers, etc.
+ pub fn privateShallowFieldAccessor(
+ comptime name: []const u8,
+ ) gobject.ext.Accessor(
+ Self,
+ @FieldType(Private.?, name),
+ ) {
+ return gobject.ext.privateFieldAccessor(
+ Self,
+ Private.?,
+ &Private.?.offset,
+ name,
+ );
+ }
+
+ /// A helper that can be used to create a property that reads and
+ /// writes a private boxed gobject field type.
+ ///
+ /// Reading the property will result in allocating a pointer and
+ /// setting it will free the previous pointer.
+ ///
+ /// The object class (Self) must still free the private field
+ /// in finalize!
+ pub fn privateBoxedFieldAccessor(
+ comptime name: []const u8,
+ ) gobject.ext.Accessor(
+ Self,
+ @FieldType(Private.?, name),
+ ) {
+ return .{
+ .getter = &struct {
+ fn get(self: *Self, value: *gobject.Value) void {
+ gobject.ext.Value.set(
+ value,
+ @field(private(self), name),
+ );
+ }
+ }.get,
+ .setter = &struct {
+ fn set(self: *Self, value: *const gobject.Value) void {
+ const priv = private(self);
+ if (@field(priv, name)) |v| {
+ ext.boxedFree(
+ @typeInfo(@TypeOf(v)).pointer.child,
+ v,
+ );
+ }
+
+ const T = @TypeOf(@field(priv, name));
+ @field(
+ priv,
+ name,
+ ) = gobject.ext.Value.dup(value, T);
+ }
+ }.set,
+ };
+ }
+
+ /// A helper that can be used to create a property that reads and
+ /// writes a private field gobject field type (reference counted).
+ ///
+ /// Reading the property will result in taking a reference to the
+ /// value and writing the property will unref the previous value.
+ ///
+ /// The object class (Self) must still free the private field
+ /// in finalize!
+ pub fn privateObjFieldAccessor(
+ comptime name: []const u8,
+ ) gobject.ext.Accessor(
+ Self,
+ @FieldType(Private.?, name),
+ ) {
+ return .{
+ .getter = &struct {
+ fn get(self: *Self, value: *gobject.Value) void {
+ gobject.ext.Value.set(
+ value,
+ @field(private(self), name),
+ );
+ }
+ }.get,
+ .setter = &struct {
+ fn set(self: *Self, value: *const gobject.Value) void {
+ const priv = private(self);
+ if (@field(priv, name)) |v| v.unref();
+
+ const T = @TypeOf(@field(priv, name));
+ @field(
+ priv,
+ name,
+ ) = gobject.ext.Value.dup(value, T);
+ }
+ }.set,
+ };
+ }
+
+ /// A helper that can be used to create a property that reads and
+ /// writes a private `?[:0]const u8` field type.
+ ///
+ /// Reading the property will result in a copy of the string
+ /// and callers are responsible for freeing it.
+ ///
+ /// Writing the property will free the previous value and copy
+ /// the new value into the private field.
+ ///
+ /// The object class (Self) must still free the private field
+ /// in finalize!
+ pub fn privateStringFieldAccessor(
+ comptime name: []const u8,
+ ) gobject.ext.Accessor(
+ Self,
+ @FieldType(Private.?, name),
+ ) {
+ const S = struct {
+ fn getter(self: *Self) ?[:0]const u8 {
+ return @field(private(self), name);
+ }
+
+ fn setter(self: *Self, value: ?[:0]const u8) void {
+ const priv = private(self);
+ if (@field(priv, name)) |v| {
+ glib.free(@ptrCast(@constCast(v)));
+ }
+
+ // We don't need to copy this because it was already
+ // copied by the typedAccessor.
+ @field(priv, name) = value;
+ }
+ };
+
+ return gobject.ext.typedAccessor(
+ Self,
+ ?[:0]const u8,
+ .{
+ .getter = S.getter,
+ .getter_transfer = .none,
+ .setter = S.setter,
+ .setter_transfer = .full,
+ },
+ );
+ }
+
+ /// Common class functions.
+ pub const Class = struct {
+ pub fn as(class: *Self.Class, comptime T: type) *T {
+ return gobject.ext.as(T, class);
+ }
+
+ /// Bind a template child to a private entry in the class.
+ pub const bindTemplateChildPrivate = if (Private) |P| (struct {
+ pub fn bindTemplateChildPrivate(
+ class: *Self.Class,
+ comptime name: [:0]const u8,
+ comptime options: gtk.ext.BindTemplateChildOptions,
+ ) void {
+ gtk.ext.impl_helpers.bindTemplateChildPrivate(
+ class,
+ name,
+ P,
+ P.offset,
+ options,
+ );
+ }
+ }).bindTemplateChildPrivate else {};
+
+ /// Bind a function pointer to a template callback symbol.
+ pub fn bindTemplateCallback(
+ class: *Self.Class,
+ comptime name: [:0]const u8,
+ comptime func: anytype,
+ ) void {
+ {
+ const ptr_ti = @typeInfo(@TypeOf(func));
+ if (ptr_ti != .pointer) {
+ @compileError("bound function must be a pointer type");
+ }
+ if (ptr_ti.pointer.size != .one) {
+ @compileError("bound function must be a pointer to a function");
+ }
+
+ const func_ti = @typeInfo(ptr_ti.pointer.child);
+ if (func_ti != .@"fn") {
+ @compileError("bound function must be a function pointer");
+ }
+ if (func_ti.@"fn".return_type == bool) {
+ // glib booleans are ints and returning a Zig bool type
+ // I think uses a byte and causes ABI issues.
+ @compileError("bound function must return c_int instead of bool");
+ }
+ }
+
+ gtk.Widget.Class.bindTemplateCallbackFull(
+ class.as(gtk.Widget.Class),
+ name,
+ @ptrCast(func),
+ );
+ }
+ };
+ };
+}
+
+test {
+ @import("std").testing.refAllDecls(@This());
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/class/application.zig b/vendor/libghostty-vt/src/apprt/gtk/class/application.zig
new file mode 100644
index 00000000..873674ce
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/class/application.zig
@@ -0,0 +1,2945 @@
+const std = @import("std");
+const assert = @import("../../../quirks.zig").inlineAssert;
+const Allocator = std.mem.Allocator;
+const adw = @import("adw");
+const gdk = @import("gdk");
+const gio = @import("gio");
+const glib = @import("glib");
+const gobject = @import("gobject");
+const gtk = @import("gtk");
+
+const build_config = @import("../../../build_config.zig");
+const build_info = @import("../build/info.zig");
+const state = &@import("../../../global.zig").state;
+const i18n = @import("../../../os/main.zig").i18n;
+const apprt = @import("../../../apprt.zig");
+const CoreApp = @import("../../../App.zig");
+const configpkg = @import("../../../config.zig");
+const input = @import("../../../input.zig");
+const internal_os = @import("../../../os/main.zig");
+const systemd = @import("../../../os/systemd.zig");
+const terminal = @import("../../../terminal/main.zig");
+const xev = @import("../../../global.zig").xev;
+const Binding = @import("../../../input.zig").Binding;
+const CoreConfig = configpkg.Config;
+const CoreSurface = @import("../../../Surface.zig");
+const lib = @import("../../../lib/main.zig");
+
+const ext = @import("../ext.zig");
+const key = @import("../key.zig");
+const adw_version = @import("../adw_version.zig");
+const gtk_version = @import("../gtk_version.zig");
+const winprotopkg = @import("../winproto.zig");
+const ApprtApp = @import("../App.zig");
+const Common = @import("../class.zig").Common;
+const WeakRef = @import("../weak_ref.zig").WeakRef;
+const Config = @import("config.zig").Config;
+const Surface = @import("surface.zig").Surface;
+const SplitTree = @import("split_tree.zig").SplitTree;
+const Window = @import("window.zig").Window;
+const Tab = @import("tab.zig").Tab;
+const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog;
+const ConfigErrorsDialog = @import("config_errors_dialog.zig").ConfigErrorsDialog;
+const GlobalShortcuts = @import("global_shortcuts.zig").GlobalShortcuts;
+const OpenURI = @import("../portal.zig").OpenURI;
+
+const log = std.log.scoped(.gtk_ghostty_application);
+
+/// Function used to funnel GLib/GObject/GTK log messages into Zig's logging
+/// system rather than just getting dumped directly to stderr.
+fn glibLogWriterFunction(
+ level: glib.LogLevelFlags,
+ fields: [*]const glib.LogField,
+ n_fields: usize,
+ _: ?*anyopaque,
+) callconv(.c) glib.LogWriterOutput {
+ const glib_log = std.log.scoped(.glib);
+
+ var message_: ?[]const u8 = null;
+ var domain_: ?[]const u8 = null;
+ for (0..n_fields) |i| {
+ const field = fields[i];
+ const k = std.mem.span(field.f_key orelse continue);
+ const v: []const u8 = v: {
+ if (field.f_length >= 0) {
+ const v: [*]const u8 = @ptrCast(field.f_value orelse continue);
+ break :v v[0..@intCast(field.f_length)];
+ }
+ const v: [*:0]const u8 = @ptrCast(field.f_value orelse continue);
+ break :v std.mem.span(v);
+ };
+ if (std.mem.eql(u8, k, "MESSAGE")) {
+ message_ = v;
+ continue;
+ }
+ if (std.mem.eql(u8, k, "GLIB_DOMAIN")) {
+ domain_ = v;
+ continue;
+ }
+ }
+
+ const message = message_ orelse return .unhandled;
+ const domain = domain_ orelse "«unknown»";
+
+ if (level.level_error) {
+ glib_log.err("ERROR: {s}: {s}", .{ domain, message });
+ return .handled;
+ }
+ if (level.level_critical) {
+ glib_log.err("CRITICAL: {s}: {s}", .{ domain, message });
+ return .handled;
+ }
+ if (level.level_warning) {
+ glib_log.warn("WARNING: {s}: {s}", .{ domain, message });
+ return .handled;
+ }
+ if (level.level_message) {
+ glib_log.info("MESSAGE: {s}: {s}", .{ domain, message });
+ return .handled;
+ }
+ if (level.level_info) {
+ glib_log.info("INFO: {s}: {s}", .{ domain, message });
+ return .handled;
+ }
+ if (level.level_debug) {
+ glib_log.debug("DEBUG: {s}: {s}", .{ domain, message });
+ return .handled;
+ }
+ glib_log.debug("UNKNOWN: {s}: {s}", .{ domain, message });
+ return .handled;
+}
+
+/// The primary entrypoint for the Ghostty GTK application.
+///
+/// This requires a `ghostty.App` and `ghostty.Config` and takes
+/// care of the rest. Call `run` to run the application to completion.
+pub const Application = extern struct {
+ /// This type creates a new GObject class. Since the Application is
+ /// the primary entrypoint I'm going to use this as a place to document
+ /// how this all works and where you can find resources for it, but
+ /// this applies to any other GObject class within this apprt.
+ ///
+ /// The various fields (parent_instance) and constants (Parent,
+ /// getGObjectType, etc.) are mandatory "interfaces" for zig-gobject
+ /// to create a GObject class.
+ ///
+ /// I found these to be the best resources:
+ ///
+ /// * https://github.com/ianprime0509/zig-gobject/blob/d7f1edaf50193d49b56c60568dfaa9f23195565b/extensions/gobject2.zig
+ /// * https://github.com/ianprime0509/zig-gobject/blob/d7f1edaf50193d49b56c60568dfaa9f23195565b/example/src/custom_class.zig
+ ///
+ const Self = @This();
+
+ parent_instance: Parent,
+ pub const Parent = adw.Application;
+ pub const getGObjectType = gobject.ext.defineClass(Self, .{
+ .name = "GhosttyApplication",
+ .classInit = &Class.init,
+ .parent_class = &Class.parent,
+ .private = .{ .Type = Private, .offset = &Private.offset },
+ });
+
+ pub const properties = struct {
+ pub const config = struct {
+ pub const name = "config";
+ const impl = gobject.ext.defineProperty(
+ "config",
+ Self,
+ ?*Config,
+ .{
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ ?*Config,
+ .{
+ .getter = Self.getConfig,
+ .getter_transfer = .full,
+ },
+ ),
+ },
+ );
+ };
+ };
+
+ const Private = struct {
+ /// The apprt App. This is annoying that we need this it'd be
+ /// nicer to just make THIS the apprt app but the current libghostty
+ /// API doesn't allow that.
+ rt_app: *ApprtApp,
+
+ /// The libghostty App instance.
+ core_app: *CoreApp,
+
+ /// The configuration for the application.
+ config: *Config,
+
+ /// State and logic for the underlying windowing protocol.
+ winproto: winprotopkg.App,
+
+ /// The global shortcut logic.
+ global_shortcuts: *GlobalShortcuts,
+
+ /// This is set to true so long as we request a window exactly
+ /// once. This prevents quitting the app before we've shown one
+ /// window.
+ requested_window: bool = false,
+
+ /// This is set to false internally when the event loop
+ /// should exit and the application should quit. This must
+ /// only be set by the main loop thread.
+ running: bool = false,
+
+ /// The timer used to quit the application after the last window is
+ /// closed. Even if there is no quit delay set, this is the state
+ /// used to determine to close the app.
+ quit_timer: union(enum) {
+ off,
+ active: c_uint,
+ expired,
+ } = .off,
+
+ /// If non-null, we're currently showing a config errors dialog.
+ /// This is a WeakRef because the dialog can close on its own
+ /// outside of our own lifecycle and that's okay.
+ config_errors_dialog: WeakRef(ConfigErrorsDialog) = .empty,
+
+ /// glib source for our signal handler.
+ signal_source: ?c_uint = null,
+
+ /// CSS Provider for any styles based on Ghostty configuration values.
+ css_provider: *gtk.CssProvider,
+
+ /// Providers for loading custom stylesheets defined by user
+ custom_css_providers: std.ArrayListUnmanaged(*gtk.CssProvider) = .empty,
+
+ /// A copy of the LANG environment variable that was provided to Ghostty
+ /// by the system. If this is null, the LANG environment variable did
+ /// not exist in Ghostty's environment variable.
+ saved_language: ?[:0]const u8 = null,
+
+ open_uri: OpenURI = undefined,
+
+ pub var offset: c_int = 0;
+ };
+
+ /// Get this application as the default, allowing access to its
+ /// properties globally.
+ ///
+ /// This asserts that there is a default application and that the
+ /// default application is a GhosttyApplication. The program would have
+ /// to be in a very bad state for this to be violated.
+ pub fn default() *Self {
+ const app = gio.Application.getDefault().?;
+ return gobject.ext.cast(Self, app).?;
+ }
+
+ /// Creates a new Application instance.
+ ///
+ /// This does a lot more work than a typical class instantiation,
+ /// because we expect that this is the main program entrypoint.
+ ///
+ /// The only failure mode of initializing the application is early OOM.
+ /// Early OOM can't be recovered from. Every other error is mapped to
+ /// some degraded state where we can at least show a window with an error.
+ pub fn new(
+ rt_app: *ApprtApp,
+ core_app: *CoreApp,
+ ) Allocator.Error!*Self {
+ const alloc = core_app.alloc;
+
+ // Capture GLib/GObject/GTK log messages and funnel them through Zig's
+ // logging system rather than just getting dumped directly to stderr.
+ _ = glib.logSetWriterFunc(glibLogWriterFunction, null, null);
+
+ // Log our GTK versions
+ gtk_version.logVersion();
+ adw_version.logVersion();
+
+ // Load our configuration.
+ var config = CoreConfig.load(alloc) catch |err| err: {
+ // If we fail to load the configuration, then we should log
+ // the error in the diagnostics so it can be shown to the user.
+ // We can still load a default which only fails for OOM, allowing
+ // us to startup.
+ var def: CoreConfig = try .default(alloc);
+ errdefer def.deinit();
+ try def.addDiagnosticFmt(
+ "error loading user configuration: {}",
+ .{err},
+ );
+
+ break :err def;
+ };
+ defer config.deinit();
+
+ const saved_language: ?[:0]const u8 = saved_language: {
+ const old_language = old_language: {
+ const result = (internal_os.getenv(alloc, "LANG") catch break :old_language null) orelse break :old_language null;
+ defer result.deinit(alloc);
+ break :old_language alloc.dupeZ(u8, result.value) catch break :old_language null;
+ };
+
+ if (config.language) |language| _ = internal_os.setenv("LANG", language);
+
+ break :saved_language old_language;
+ };
+
+ // Set gettext global domain to be our app so that our unqualified
+ // translations map to our translations.
+ internal_os.i18n.initGlobalDomain() catch |err| {
+ // Failures shuldn't stop application startup. Our app may
+ // not translate correctly but it should still work. In the
+ // future we may want to add this to the GUI to show.
+ log.warn("i18n initialization failed error={}", .{err});
+ };
+
+ // Setup our GTK init env vars
+ setGtkEnv(&config) catch |err| switch (err) {
+ error.NoSpaceLeft => {
+ // If we fail to set GTK environment variables then we still
+ // try to start the application...
+ log.warn(
+ "error setting GTK environment variables err={}",
+ .{err},
+ );
+ },
+ };
+ adw.init();
+
+ const single_instance = switch (config.@"gtk-single-instance") {
+ .true => true,
+ .false => false,
+ // This should have been resolved to true/false during config loading.
+ .detect => unreachable,
+ };
+
+ // Setup the flags for our application.
+ const app_flags: gio.ApplicationFlags = app_flags: {
+ var flags: gio.ApplicationFlags = .flags_default_flags;
+ if (!single_instance) flags.non_unique = true;
+ break :app_flags flags;
+ };
+
+ // Our app ID determines uniqueness and maps to our desktop file.
+ // We append "-debug" to the ID if we're in debug mode so that we
+ // can develop Ghostty in Ghostty.
+ const app_id: [:0]const u8 = app_id: {
+ if (config.class) |class| {
+ if (gio.Application.idIsValid(class) != 0) {
+ break :app_id class;
+ } else {
+ log.warn("invalid 'class' in config, ignoring", .{});
+ }
+ }
+
+ break :app_id build_info.application_id;
+ };
+
+ const display: *gdk.Display = gdk.Display.getDefault() orelse {
+ // I'm unsure of any scenario where this happens. Because we don't
+ // want to litter null checks everywhere, we just exit here.
+ log.warn("gdk display is null, exiting", .{});
+ std.posix.exit(1);
+ };
+
+ // Setup our windowing protocol logic
+ var wp: winprotopkg.App = winprotopkg.App.init(
+ alloc,
+ display,
+ app_id,
+ &config,
+ ) catch |err| wp: {
+ // If we fail to detect or setup the windowing protocol
+ // specifies, we fallback to a noop implementation so we can
+ // still launch.
+ log.warn("error initializing windowing protocol err={}", .{err});
+ break :wp .{ .none = .{} };
+ };
+ errdefer wp.deinit();
+ log.debug("windowing protocol={s}", .{@tagName(wp)});
+
+ // Create our GTK Application which encapsulates our process.
+ log.debug("creating GTK application id={s} single-instance={}", .{
+ app_id,
+ single_instance,
+ });
+
+ // Wrap our configuration in a GObject.
+ const config_obj: *Config = try .new(alloc, &config);
+ errdefer config_obj.unref();
+
+ // Internally, GTK ensures that only one instance of this provider
+ // exists in the provider list for the display.
+ const css_provider = gtk.CssProvider.new();
+ gtk.StyleContext.addProviderForDisplay(
+ display,
+ css_provider.as(gtk.StyleProvider),
+ gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + 3,
+ );
+ errdefer css_provider.unref();
+
+ // Initialize the app.
+ const self = gobject.ext.newInstance(Self, .{
+ .application_id = app_id.ptr,
+ .flags = app_flags,
+
+ // Force the resource path to a known value so it doesn't depend
+ // on the app id (which changes between debug/release and can be
+ // user-configured) and force it to load in compiled resources.
+ .resource_base_path = build_info.resource_path,
+ });
+
+ // Setup our private state. More setup is done in the init
+ // callback that GObject calls, but we can't pass this data through
+ // to there (and we don't need it there directly) so this is here.
+ const priv: *Private = self.private();
+ priv.* = .{
+ .rt_app = rt_app,
+ .core_app = core_app,
+ .config = config_obj,
+ .winproto = wp,
+ .css_provider = css_provider,
+ .custom_css_providers = .empty,
+ .global_shortcuts = gobject.ext.newInstance(GlobalShortcuts, .{}),
+ .saved_language = saved_language,
+ .open_uri = .init(rt_app),
+ };
+
+ // Signals
+ _ = gobject.Object.signals.notify.connect(
+ self,
+ *Self,
+ propConfig,
+ self,
+ .{ .detail = "config" },
+ );
+
+ _ = gtk.CssProvider.signals.parsing_error.connect(
+ css_provider,
+ *Self,
+ signalCssParsingError,
+ self,
+ .{},
+ );
+
+ // Trigger initial config changes
+ self.as(gobject.Object).notifyByPspec(properties.config.impl.param_spec);
+
+ return self;
+ }
+
+ /// Force deinitialize the application.
+ ///
+ /// Normally in a GObject lifecycle, this would be called by the
+ /// finalizer. But applications are never fully unreferenced so this
+ /// ensures that our memory is cleaned up properly.
+ pub fn deinit(self: *Self) void {
+ const alloc = self.allocator();
+ const priv: *Private = self.private();
+ priv.config.unref();
+ priv.winproto.deinit();
+ priv.open_uri.deinit();
+ priv.global_shortcuts.unref();
+ if (priv.saved_language) |language| alloc.free(language);
+ if (gdk.Display.getDefault()) |display| {
+ gtk.StyleContext.removeProviderForDisplay(
+ display,
+ priv.css_provider.as(gtk.StyleProvider),
+ );
+
+ for (priv.custom_css_providers.items) |provider| {
+ gtk.StyleContext.removeProviderForDisplay(
+ display,
+ provider.as(gtk.StyleProvider),
+ );
+ }
+ }
+ priv.css_provider.unref();
+ for (priv.custom_css_providers.items) |provider| provider.unref();
+ priv.custom_css_providers.deinit(alloc);
+ }
+
+ /// The global allocator that all other classes should use by
+ /// calling `Application.default().allocator()`. Zig code should prefer
+ /// this wherever possible so we get leak detection in debug/tests.
+ pub fn allocator(self: *Self) std.mem.Allocator {
+ return self.private().core_app.alloc;
+ }
+
+ /// Get the original language that Ghostty was launched with. This returns a
+ /// pointer to internal memory so it must be copied by callers.
+ pub fn savedLanguage(self: *Self) ?[:0]const u8 {
+ return self.private().saved_language;
+ }
+
+ /// Run the application. This is a replacement for `gio.Application.run`
+ /// because we want more tight control over our event loop so we can
+ /// integrate it with libghostty.
+ pub fn run(self: *Self) !void {
+ // Based on the actual `gio.Application.run` implementation:
+ // https://github.com/GNOME/glib/blob/a8e8b742e7926e33eb635a8edceac74cf239d6ed/gio/gapplication.c#L2533
+
+ // Acquire the default context for the application
+ const ctx = glib.MainContext.default();
+ if (glib.MainContext.acquire(ctx) == 0) return error.ContextAcquireFailed;
+
+ // The final cleanup that is always required at the end of running.
+ defer {
+ // Ensure our timer source is removed
+ self.stopQuitTimer();
+
+ // Sync any remaining settings
+ gio.Settings.sync();
+
+ // Clear out the event loop, don't block.
+ while (glib.MainContext.iteration(ctx, 0) != 0) {}
+
+ // Release the context so something else can use it.
+ defer glib.MainContext.release(ctx);
+ }
+
+ // Register the application
+ var err_: ?*glib.Error = null;
+ if (self.as(gio.Application).register(
+ null,
+ &err_,
+ ) == 0) {
+ if (err_) |err| {
+ defer err.free();
+ log.warn(
+ "error registering application: {s}",
+ .{err.f_message orelse "(unknown)"},
+ );
+ }
+
+ return error.ApplicationRegisterFailed;
+ }
+ assert(err_ == null);
+
+ // This just calls the `activate` signal but its part of the normal startup
+ // routine so we just call it, but only if the config allows it (this allows
+ // for launching Ghostty in the "background" without immediately opening
+ // a window).
+ //
+ // https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302
+ const priv = self.private();
+ {
+ // We need to scope any config access because once we run our
+ // event loop, this can change out from underneath us.
+ const config = priv.config.get();
+ if (config.@"initial-window") self.as(gio.Application).activate();
+ }
+
+ // If we are NOT the primary instance, then we never want to run.
+ // This means that another instance of the GTK app is running.
+ if (self.as(gio.Application).getIsRemote() != 0) {
+ log.debug(
+ "application is remote, exiting run loop after activation",
+ .{},
+ );
+ return;
+ }
+
+ // Tell systemd that we are ready.
+ systemd.notify.ready();
+
+ log.debug("entering runloop", .{});
+ defer log.debug("exiting runloop", .{});
+ priv.running = true;
+ while (priv.running) {
+ _ = glib.MainContext.iteration(ctx, 1);
+
+ // Tick the core Ghostty terminal app
+ try priv.core_app.tick(priv.rt_app);
+
+ // Check if we must quit based on the current state.
+ const must_quit = q: {
+ // If we are configured to always stay running, don't quit.
+ const config = priv.config.get();
+ if (!config.@"quit-after-last-window-closed") break :q false;
+
+ // If the quit timer has expired, quit.
+ if (priv.quit_timer == .expired) {
+ log.debug("must_quit due to quit timer expired", .{});
+ break :q true;
+ }
+
+ // If we have no windows attached to our app, also quit.
+ // We only do this if we don't have the closed delay set,
+ // because with the closed delay set we'll exit eventually.
+ if (config.@"quit-after-last-window-closed-delay" == null) {
+ if (priv.requested_window and @as(
+ ?*glib.List,
+ self.as(gtk.Application).getWindows(),
+ ) == null) {
+ log.debug("must_quit due to no app windows", .{});
+ break :q true;
+ }
+ }
+
+ // No quit conditions met
+ break :q false;
+ };
+
+ if (must_quit) {
+ // All must quit scenarios do not need confirmation.
+ // Furthermore, must quit scenarios may result in a situation
+ // where its unsafe to even access the app/surface memory
+ // since its in the process of being freed. We must simply
+ // begin our exit immediately.
+ self.quitNow();
+ }
+ }
+ }
+
+ /// Quit the application. This will start the process to stop the
+ /// run loop. It will not `posix.exit`.
+ pub fn quit(self: *Self) void {
+ const priv = self.private();
+
+ // If our run loop has already exited then we are done.
+ if (!priv.running) return;
+
+ // If our core app doesn't need to confirm quit then we
+ // can exit immediately.
+ if (!priv.core_app.needsConfirmQuit()) {
+ self.quitNow();
+ return;
+ }
+
+ // Get the parent for our dialog
+ const parent: ?*gtk.Widget = parent: {
+ const list = gtk.Window.listToplevels();
+ defer list.free();
+ const focused = @as(?*glib.List, list.findCustom(
+ null,
+ findActiveWindow,
+ )) orelse {
+ // If we have an active surface then we should have
+ // a window available but in the rare case we don't we
+ // should exit so we don't crash.
+ break :parent null;
+ };
+ break :parent @ptrCast(@alignCast(focused.f_data));
+ };
+
+ // Show a confirmation dialog
+ const dialog: *CloseConfirmationDialog = .new(.app);
+ _ = CloseConfirmationDialog.signals.@"close-request".connect(
+ dialog,
+ *Application,
+ handleCloseConfirmation,
+ self,
+ .{},
+ );
+
+ // Show it
+ dialog.present(parent);
+ }
+
+ fn quitNow(self: *Self) void {
+ // Get all our windows and destroy them, forcing them to free.
+ const list = gtk.Window.listToplevels();
+ defer list.free();
+ list.foreach(struct {
+ fn callback(data: ?*anyopaque, _: ?*anyopaque) callconv(.c) void {
+ const ptr = data orelse return;
+ const window: *gtk.Window = @ptrCast(@alignCast(ptr));
+
+ // We only want to destroy our windows. These windows own
+ // every other type of window that is possible so this will
+ // trigger a proper shutdown sequence.
+ //
+ // We previously just destroyed ALL windows but this leads to
+ // a double-free with the fcitx ime, because it has a nested
+ // gtk.Window as a property that we don't own and it later
+ // tries to free on its own. I think this is probably a bug in
+ // the fcitx ime widget but still, we don't want a double free!
+ if (gobject.ext.isA(window, Window)) {
+ window.destroy();
+ }
+ }
+ }.callback, null);
+
+ // Trigger our runloop exit.
+ self.private().running = false;
+ }
+
+ /// apprt API to perform an action.
+ pub fn performAction(
+ self: *Self,
+ target: apprt.Target,
+ comptime action: apprt.Action.Key,
+ value: apprt.Action.Value(action),
+ ) !bool {
+ switch (action) {
+ .close_tab => return Action.closeTab(target, value),
+ .close_window => return Action.closeWindow(target),
+
+ .copy_title_to_clipboard => return Action.copyTitleToClipboard(target),
+
+ .config_change => try Action.configChange(
+ self,
+ target,
+ value.config,
+ ),
+
+ .desktop_notification => Action.desktopNotification(self, target, value),
+
+ .equalize_splits => return Action.equalizeSplits(target),
+
+ .goto_split => return Action.gotoSplit(target, value),
+
+ .goto_window => return Action.gotoWindow(value),
+
+ .goto_tab => return Action.gotoTab(target, value),
+
+ .initial_size => return Action.initialSize(target, value),
+
+ .inspector => return Action.controlInspector(target, value),
+
+ .key_sequence => return Action.keySequence(target, value),
+ .key_table => return Action.keyTable(target, value),
+
+ .mouse_over_link => Action.mouseOverLink(target, value),
+ .mouse_shape => Action.mouseShape(target, value),
+ .mouse_visibility => Action.mouseVisibility(target, value),
+
+ .move_tab => return Action.moveTab(target, value),
+
+ .new_split => return Action.newSplit(target, value),
+
+ .new_tab => return Action.newTab(target),
+
+ .new_window => try Action.newWindow(
+ self,
+ switch (target) {
+ .app => null,
+ .surface => |v| v,
+ },
+ .none,
+ ),
+
+ .open_config => return Action.openConfig(self),
+
+ .open_url => Action.openUrl(self, value),
+
+ .pwd => Action.pwd(target, value),
+
+ .present_terminal => return Action.presentTerminal(target),
+
+ .progress_report => return Action.progressReport(target, value),
+
+ .prompt_title => return Action.promptTitle(target, value),
+
+ .quit => self.quit(),
+
+ .quit_timer => try Action.quitTimer(self, value),
+
+ .reload_config => try Action.reloadConfig(self, target, value),
+
+ .render => Action.render(target),
+
+ .resize_split => return Action.resizeSplit(target, value),
+
+ .ring_bell => Action.ringBell(target),
+
+ .scrollbar => Action.scrollbar(target, value),
+
+ .set_title => Action.setTitle(target, value),
+ .set_tab_title => return Action.setTabTitle(target, value),
+
+ .show_child_exited => return Action.showChildExited(target, value),
+
+ .show_gtk_inspector => Action.showGtkInspector(),
+
+ .size_limit => return Action.sizeLimit(target, value),
+
+ .toggle_maximize => Action.toggleMaximize(target),
+ .toggle_fullscreen => Action.toggleFullscreen(target),
+ .toggle_quick_terminal => return Action.toggleQuickTerminal(self),
+ .toggle_tab_overview => return Action.toggleTabOverview(target),
+ .toggle_window_decorations => return Action.toggleWindowDecorations(target),
+ .toggle_command_palette => return Action.toggleCommandPalette(target),
+ .toggle_split_zoom => return Action.toggleSplitZoom(target),
+ .show_on_screen_keyboard => return Action.showOnScreenKeyboard(target),
+ .command_finished => return Action.commandFinished(target, value),
+ .readonly => return Action.setReadonly(target, value),
+
+ .start_search => Action.startSearch(target, value),
+ .end_search => Action.endSearch(target),
+ .search_total => Action.searchTotal(target, value),
+ .search_selected => Action.searchSelected(target, value),
+
+ // Unimplemented
+ .secure_input,
+ .close_all_windows,
+ .float_window,
+ .toggle_visibility,
+ .toggle_background_opacity,
+ .cell_size,
+ .render_inspector,
+ .renderer_health,
+ .color_change,
+ .reset_window_size,
+ .check_for_updates,
+ .undo,
+ .redo,
+ => {
+ log.warn("unimplemented action={}", .{action});
+ return false;
+ },
+ }
+
+ // Assume it was handled. The unhandled case must be explicit
+ // in the switch above.
+ return true;
+ }
+
+ /// Returns the core app associated with this application. This is
+ /// not a reference-counted type so you should not store this.
+ pub fn core(self: *Self) *CoreApp {
+ return self.private().core_app;
+ }
+
+ /// Returns the apprt application associated with this application.
+ pub fn rt(self: *Self) *ApprtApp {
+ return self.private().rt_app;
+ }
+
+ /// Returns the app winproto implementation.
+ pub fn winproto(self: *Self) *winprotopkg.App {
+ return &self.private().winproto;
+ }
+
+ /// Returns the open URI portal implementation.
+ pub fn openUri(self: *Self) *OpenURI {
+ return &self.private().open_uri;
+ }
+
+ /// This will get called when there are no more open surfaces.
+ fn startQuitTimer(self: *Self) void {
+ const priv = self.private();
+ const config = priv.config.get();
+
+ // Cancel any previous timer.
+ self.stopQuitTimer();
+
+ // This is a no-op unless we are configured to quit after last window is closed.
+ if (!config.@"quit-after-last-window-closed") return;
+
+ // If a delay is configured, set a timeout function to quit after the delay.
+ if (config.@"quit-after-last-window-closed-delay") |v| {
+ priv.quit_timer = .{
+ .active = glib.timeoutAdd(
+ v.asMilliseconds(),
+ handleQuitTimerExpired,
+ self,
+ ),
+ };
+ } else {
+ // If no delay is configured, treat it as expired.
+ priv.quit_timer = .expired;
+ }
+ }
+
+ /// This will get called when a new surface gets opened.
+ fn stopQuitTimer(self: *Self) void {
+ const priv = self.private();
+ switch (priv.quit_timer) {
+ .off => {},
+ .expired => priv.quit_timer = .off,
+ .active => |source| {
+ if (glib.Source.remove(source) == 0) {
+ log.warn(
+ "unable to remove quit timer source={d}",
+ .{source},
+ );
+ }
+
+ priv.quit_timer = .off;
+ },
+ }
+ }
+
+ fn loadRuntimeCss(self: *Self) (Allocator.Error || std.Io.Writer.Error)!void {
+ const alloc = self.allocator();
+ const priv: *Private = self.private();
+ const config = priv.config.get();
+
+ var buf: std.Io.Writer.Allocating = try .initCapacity(alloc, 2048);
+ defer buf.deinit();
+
+ const writer = &buf.writer;
+
+ // Load standard css first as it can override some of the user configured styling.
+ try loadRuntimeCss414(config, writer);
+ try loadRuntimeCss416(config, writer);
+
+ const unfocused_fill: CoreConfig.Color = config.@"unfocused-split-fill" orelse config.background;
+
+ try writer.print(
+ \\widget.unfocused-split {{
+ \\ opacity: {d:.2};
+ \\ background-color: rgb({d},{d},{d});
+ \\}}
+ \\
+ , .{
+ 1.0 - config.@"unfocused-split-opacity",
+ unfocused_fill.r,
+ unfocused_fill.g,
+ unfocused_fill.b,
+ });
+
+ if (config.@"split-divider-color") |color| {
+ try writer.print(
+ \\.window .split paned > separator {{
+ \\ color: rgb({[r]d},{[g]d},{[b]d});
+ \\ background: rgb({[r]d},{[g]d},{[b]d});
+ \\}}
+ \\
+ , .{
+ .r = color.r,
+ .g = color.g,
+ .b = color.b,
+ });
+ }
+
+ if (config.@"window-title-font-family") |font_family| {
+ try writer.print(
+ \\.window headerbar {{
+ \\ font-family: "{[font_family]s}";
+ \\}}
+ \\
+ , .{ .font_family = font_family });
+ }
+
+ const contents = buf.written();
+
+ log.debug("runtime CSS is {d} bytes", .{contents.len});
+
+ const bytes = glib.Bytes.new(contents.ptr, contents.len);
+ defer bytes.unref();
+
+ // Clears any previously loaded CSS from this provider
+ priv.css_provider.loadFromBytes(bytes);
+ }
+
+ /// Load runtime CSS for older than GTK 4.16
+ fn loadRuntimeCss414(
+ config: *const CoreConfig,
+ writer: *std.Io.Writer,
+ ) std.Io.Writer.Error!void {
+ if (gtk_version.runtimeAtLeast(4, 16, 0)) return;
+
+ const window_theme = config.@"window-theme";
+ const headerbar_background = config.@"window-titlebar-background" orelse config.background;
+ const headerbar_foreground = config.@"window-titlebar-foreground" orelse config.foreground;
+
+ switch (window_theme) {
+ .ghostty => try writer.print(
+ \\windowhandle {{
+ \\ background-color: rgb({d},{d},{d});
+ \\ color: rgb({d},{d},{d});
+ \\}}
+ \\windowhandle:backdrop {{
+ \\ background-color: oklab(from rgb({d},{d},{d}) calc(l * 0.9) a b / alpha);
+ \\}}
+ \\
+ , .{
+ headerbar_background.r,
+ headerbar_background.g,
+ headerbar_background.b,
+ headerbar_foreground.r,
+ headerbar_foreground.g,
+ headerbar_foreground.b,
+ headerbar_background.r,
+ headerbar_background.g,
+ headerbar_background.b,
+ }),
+ else => {},
+ }
+ }
+
+ /// Load runtime for GTK 4.16 and newer
+ fn loadRuntimeCss416(
+ config: *const CoreConfig,
+ writer: *std.Io.Writer,
+ ) std.Io.Writer.Error!void {
+ if (gtk_version.runtimeUntil(4, 16, 0)) return;
+
+ const window_theme = config.@"window-theme";
+ const headerbar_background = config.@"window-titlebar-background" orelse config.background;
+ const headerbar_foreground = config.@"window-titlebar-foreground" orelse config.foreground;
+
+ try writer.writeAll(
+ \\/*
+ \\ * Child Exited Overlay
+ \\ */
+ \\
+ \\.child-exited.normal revealer widget {
+ \\ background-color: color-mix(
+ \\ in srgb,
+ \\ var(--success-bg-color),
+ \\ transparent 50%
+ \\ );
+ \\}
+ \\
+ \\.child-exited.abnormal revealer widget {
+ \\ background-color: color-mix(
+ \\ in srgb,
+ \\ var(--error-bg-color),
+ \\ transparent 50%
+ \\ );
+ \\}
+ \\
+ \\/*
+ \\ * Surface
+ \\ */
+ \\
+ \\.surface progressbar.error trough progress {
+ \\ background-color: color-mix(
+ \\ in srgb,
+ \\ var(--error-bg-color),
+ \\ transparent 50%
+ \\ );
+ \\}
+ \\
+ \\.surface .bell-overlay {
+ \\ border-color: color-mix(
+ \\ in srgb,
+ \\ var(--accent-color),
+ \\ transparent 50%
+ \\ );
+ \\}
+ \\
+ \\/*
+ \\ * Splits
+ \\ */
+ \\
+ \\.window .split paned > separator {
+ \\ background-color: color-mix(
+ \\ in srgb,
+ \\ var(--window-bg-color),
+ \\ transparent 0%
+ \\ );
+ \\}
+ \\
+ );
+
+ switch (window_theme) {
+ .ghostty => try writer.print(
+ \\:root {{
+ \\ --ghostty-fg: rgb({d},{d},{d});
+ \\ --ghostty-bg: rgb({d},{d},{d});
+ \\ --headerbar-fg-color: var(--ghostty-fg);
+ \\ --headerbar-bg-color: var(--ghostty-bg);
+ \\ --headerbar-backdrop-color: oklab(from var(--headerbar-bg-color) calc(l * 0.9) a b / alpha);
+ \\ --overview-fg-color: var(--ghostty-fg);
+ \\ --overview-bg-color: var(--ghostty-bg);
+ \\ --popover-fg-color: var(--ghostty-fg);
+ \\ --popover-bg-color: var(--ghostty-bg);
+ \\ --window-fg-color: var(--ghostty-fg);
+ \\ --window-bg-color: var(--ghostty-bg);
+ \\}}
+ \\windowhandle {{
+ \\ background-color: var(--headerbar-bg-color);
+ \\ color: var(--headerbar-fg-color);
+ \\}}
+ \\windowhandle:backdrop {{
+ \\ background-color: var(--headerbar-backdrop-color);
+ \\}}
+ , .{
+ headerbar_foreground.r,
+ headerbar_foreground.g,
+ headerbar_foreground.b,
+ headerbar_background.r,
+ headerbar_background.g,
+ headerbar_background.b,
+ }),
+ else => {},
+ }
+ }
+
+ fn loadCustomCss(self: *Self) (std.fs.File.ReadError || Allocator.Error)!void {
+ const priv: *Private = self.private();
+ const alloc = self.allocator();
+ const display = gdk.Display.getDefault() orelse {
+ log.warn("unable to get display", .{});
+ return;
+ };
+
+ // unload the previously loaded style providers
+ for (priv.custom_css_providers.items) |provider| {
+ gtk.StyleContext.removeProviderForDisplay(
+ display,
+ provider.as(gtk.StyleProvider),
+ );
+ provider.unref();
+ }
+ priv.custom_css_providers.clearRetainingCapacity();
+
+ const config = priv.config.get();
+ for (config.@"gtk-custom-css".value.items) |p| {
+ const path, const optional = switch (p) {
+ .optional => |path| .{ path, true },
+ .required => |path| .{ path, false },
+ };
+ const file = std.fs.openFileAbsolute(path, .{}) catch |err| {
+ if (err != error.FileNotFound or !optional) {
+ log.warn(
+ "error opening gtk-custom-css file {s}: {}",
+ .{ path, err },
+ );
+ }
+ continue;
+ };
+ defer file.close();
+
+ const css_file_size_limit = 5 * 1024 * 1024; // 5MB
+
+ log.info("loading gtk-custom-css path={s}", .{path});
+ const contents = file.readToEndAlloc(
+ alloc,
+ css_file_size_limit,
+ ) catch |err| switch (err) {
+ error.FileTooBig => {
+ log.warn("gtk-custom-css file {s} was larger than {Bi}", .{ path, css_file_size_limit });
+ continue;
+ },
+ else => |e| return e,
+ };
+ defer alloc.free(contents);
+
+ const bytes = glib.Bytes.new(contents.ptr, contents.len);
+ defer bytes.unref();
+
+ const css_provider = gtk.CssProvider.new();
+ errdefer css_provider.unref();
+
+ _ = gtk.CssProvider.signals.parsing_error.connect(
+ css_provider,
+ *Self,
+ signalCssParsingError,
+ self,
+ .{},
+ );
+
+ try priv.custom_css_providers.append(alloc, css_provider);
+
+ css_provider.loadFromBytes(bytes);
+
+ gtk.StyleContext.addProviderForDisplay(
+ display,
+ css_provider.as(gtk.StyleProvider),
+ gtk.STYLE_PROVIDER_PRIORITY_USER,
+ );
+ }
+ }
+
+ fn syncActionAccelerators(self: *Self) void {
+ self.syncActionAccelerator("app.quit", .{ .quit = {} });
+ self.syncActionAccelerator("app.open-config", .{ .open_config = {} });
+ self.syncActionAccelerator("app.reload-config", .{ .reload_config = {} });
+ self.syncActionAccelerator("win.toggle-inspector", .{ .inspector = .toggle });
+ self.syncActionAccelerator("app.show-gtk-inspector", .show_gtk_inspector);
+ self.syncActionAccelerator("win.toggle-command-palette", .toggle_command_palette);
+ self.syncActionAccelerator("win.close", .{ .close_window = {} });
+ self.syncActionAccelerator("win.new-window", .{ .new_window = {} });
+ self.syncActionAccelerator("win.new-tab", .{ .new_tab = {} });
+ self.syncActionAccelerator("win.close-tab::this", .{ .close_tab = .this });
+ self.syncActionAccelerator("tab.close::this", .{ .close_tab = .this });
+ self.syncActionAccelerator("win.split-right", .{ .new_split = .right });
+ self.syncActionAccelerator("win.split-down", .{ .new_split = .down });
+ self.syncActionAccelerator("win.split-left", .{ .new_split = .left });
+ self.syncActionAccelerator("win.split-up", .{ .new_split = .up });
+ self.syncActionAccelerator("win.copy", .{ .copy_to_clipboard = .mixed });
+ self.syncActionAccelerator("win.paste", .{ .paste_from_clipboard = {} });
+ self.syncActionAccelerator("win.reset", .{ .reset = {} });
+ self.syncActionAccelerator("win.clear", .{ .clear_screen = {} });
+ self.syncActionAccelerator("win.prompt-title", .{ .prompt_surface_title = {} });
+ self.syncActionAccelerator("split-tree.new-split::left", .{ .new_split = .left });
+ self.syncActionAccelerator("split-tree.new-split::right", .{ .new_split = .right });
+ self.syncActionAccelerator("split-tree.new-split::up", .{ .new_split = .up });
+ self.syncActionAccelerator("split-tree.new-split::down", .{ .new_split = .down });
+ }
+
+ fn syncActionAccelerator(
+ self: *Self,
+ gtk_action: [:0]const u8,
+ action: input.Binding.Action,
+ ) void {
+ const gtk_app = self.as(gtk.Application);
+
+ // Reset it initially
+ const zero = [_:null]?[*:0]const u8{};
+ gtk_app.setAccelsForAction(gtk_action, &zero);
+
+ const config = self.private().config.get();
+ const trigger = config.keybind.set.getTrigger(action) orelse return;
+ var buf: [1024]u8 = undefined;
+ const accel = if (key.accelFromTrigger(
+ &buf,
+ trigger,
+ )) |accel_|
+ accel_ orelse return
+ else |err| switch (err) {
+ // This should really never, never happen. Its not critical enough
+ // to actually crash, but this is a bug somewhere. An accelerator
+ // for a trigger can't possibly be more than 1024 bytes.
+ error.WriteFailed => {
+ log.warn("accelerator somehow longer than 1024 bytes: {f}", .{trigger});
+ return;
+ },
+ };
+ const accels = [_:null]?[*:0]const u8{accel};
+
+ gtk_app.setAccelsForAction(gtk_action, &accels);
+ }
+
+ //---------------------------------------------------------------
+ // Properties
+
+ /// Returns the configuration for this application.
+ ///
+ /// The reference count is increased.
+ pub fn getConfig(self: *Self) *Config {
+ return self.private().config.ref();
+ }
+
+ /// Set the configuration for this application. The reference count
+ /// is increased on the new configuration and the old one is
+ /// unreferenced.
+ ///
+ /// If the config has errors this may show the config errors dialog.
+ fn setConfig(self: *Self, config: *Config) void {
+ const priv = self.private();
+ priv.config.unref();
+ priv.config = config.ref();
+ self.as(gobject.Object).notifyByPspec(properties.config.impl.param_spec);
+
+ // Show our errors if we have any
+ self.showConfigErrorsDialog();
+ }
+
+ fn propConfig(
+ _: *Application,
+ _: *gobject.ParamSpec,
+ self: *Self,
+ ) callconv(.c) void {
+ // Sync our accelerators for menu items.
+ self.syncActionAccelerators();
+
+ // Load our runtime and custom CSS. If this fails then our window is
+ // just stuck with the old CSS but we don't want to fail the entire
+ // config change operation.
+ self.loadRuntimeCss() catch |err| switch (err) {
+ error.WriteFailed, error.OutOfMemory => log.warn(
+ "out of memory loading runtime CSS, no runtime CSS applied",
+ .{},
+ ),
+ };
+ self.loadCustomCss() catch |err| {
+ log.warn(
+ "failed to load custom CSS, no custom CSS applied, err={}",
+ .{err},
+ );
+ };
+ }
+
+ /// Log CSS parsing error
+ fn signalCssParsingError(
+ _: *gtk.CssProvider,
+ css_section: *gtk.CssSection,
+ err: *glib.Error,
+ _: *Self,
+ ) callconv(.c) void {
+ const location = css_section.toString();
+ defer glib.free(location);
+ if (comptime gtk_version.atLeast(4, 16, 0)) bytes: {
+ const bytes = css_section.getBytes() orelse break :bytes;
+ var len: usize = undefined;
+ const ptr = bytes.getData(&len) orelse break :bytes;
+ const data = ptr[0..len];
+ log.warn("css parsing failed at {s}: {s} {d} {s}\n{s}", .{
+ location,
+ glib.quarkToString(err.f_domain),
+ err.f_code,
+ err.f_message orelse "«unknown»",
+ data,
+ });
+ return;
+ }
+ log.warn("css parsing failed at {s}: {s} {d} {s}", .{
+ location,
+ glib.quarkToString(err.f_domain),
+ err.f_code,
+ err.f_message orelse "«unknown»",
+ });
+ }
+
+ //---------------------------------------------------------------
+ // Libghostty Callbacks
+
+ pub fn wakeup(self: *Self) void {
+ _ = self;
+ glib.MainContext.wakeup(null);
+ }
+
+ //---------------------------------------------------------------
+ // Virtual Methods
+
+ fn startup(self: *Self) callconv(.c) void {
+ log.debug("startup", .{});
+
+ gio.Application.virtual_methods.startup.call(
+ Class.parent,
+ self.as(Parent),
+ );
+
+ // Set ourselves as the default application.
+ gio.Application.setDefault(self.as(gio.Application));
+
+ // The D-Bus connection is only valid after GApplication startup.
+ self.openUri().setDbusConnection(
+ self.as(gio.Application).getDbusConnection(),
+ );
+
+ // Setup our event loop
+ self.startupXev();
+
+ // Setup our style manager (light/dark mode)
+ self.startupStyleManager();
+
+ // Setup some signal handlers
+ self.startupSignals();
+
+ // Setup our action map
+ self.startupActionMap();
+
+ // Setup our global shortcuts
+ self.startupGlobalShortcuts();
+
+ // If we have any config diagnostics from loading, then we
+ // show the diagnostics dialog. We show this one as a general
+ // modal (not to any specific window) because we don't even
+ // know if the window will load.
+ self.showConfigErrorsDialog();
+ }
+
+ /// Configure libxev to use a specific backend.
+ ///
+ /// This must be called before any other xev APIs are used.
+ fn startupXev(self: *Self) void {
+ const priv = self.private();
+ const config = priv.config.get();
+
+ // If our backend is auto then we have no setup to do.
+ if (config.@"async-backend" == .auto) return;
+
+ // Setup our event loop backend to the preferred method
+ const result: bool = switch (config.@"async-backend") {
+ .auto => unreachable,
+ .epoll => if (comptime xev.dynamic) xev.prefer(.epoll) else false,
+ .io_uring => if (comptime xev.dynamic) xev.prefer(.io_uring) else false,
+ };
+
+ if (result) {
+ log.info(
+ "libxev manual backend={s}",
+ .{@tagName(xev.backend)},
+ );
+ } else {
+ log.warn(
+ "libxev manual backend failed, using default={s}",
+ .{@tagName(xev.backend)},
+ );
+ }
+ }
+
+ /// Setup the style manager on startup. The primary task here is to
+ /// setup our initial light/dark mode based on the configuration and
+ /// setup listeners for changes to the style manager.
+ fn startupStyleManager(self: *Self) void {
+ const priv = self.private();
+ const config = priv.config.get();
+
+ // Setup our initial light/dark
+ const style = self.as(adw.Application).getStyleManager();
+ style.setColorScheme(switch (config.@"window-theme") {
+ .auto, .ghostty => auto: {
+ const lum = config.background.toTerminalRGB().perceivedLuminance();
+ break :auto if (lum > 0.5)
+ .prefer_light
+ else
+ .prefer_dark;
+ },
+ .system => .prefer_light,
+ .dark => .force_dark,
+ .light => .force_light,
+ });
+
+ // Setup color change notifications
+ _ = gobject.Object.signals.notify.connect(
+ style,
+ *Self,
+ handleStyleManagerDark,
+ self,
+ .{ .detail = "dark" },
+ );
+
+ // Do an initial color scheme sync. This is idempotent and does nothing
+ // if our current theme matches what libghostty has so its safe to
+ // call.
+ handleStyleManagerDark(style, undefined, self);
+ }
+
+ /// Setup signal handlers
+ fn startupSignals(self: *Self) void {
+ const priv = self.private();
+ assert(priv.signal_source == null);
+ priv.signal_source = glib.unixSignalAdd(
+ std.posix.SIG.USR2,
+ handleSigusr2,
+ self,
+ );
+ }
+
+ /// Setup our action map.
+ fn startupActionMap(self: *Self) void {
+ const t_variant_type = glib.ext.VariantType.newFor(u64);
+ defer t_variant_type.free();
+
+ const as_variant_type = glib.VariantType.new("as");
+ defer as_variant_type.free();
+
+ const actions = [_]ext.actions.Action(Self){
+ .init("new-window", actionNewWindow, null),
+ .init("new-window-command", actionNewWindow, as_variant_type),
+ .init("open-config", actionOpenConfig, null),
+ .init("present-surface", actionPresentSurface, t_variant_type),
+ .init("quit", actionQuit, null),
+ .init("reload-config", actionReloadConfig, null),
+ };
+
+ ext.actions.add(Self, self, &actions);
+ }
+
+ /// Setup our global shortcuts.
+ fn startupGlobalShortcuts(self: *Self) void {
+ const priv = self.private();
+
+ // On startup, our dbus connection should be available.
+ priv.global_shortcuts.setDbusConnection(
+ self.as(gio.Application).getDbusConnection(),
+ );
+
+ // Setup a binding so that the shortcut config always matches the app.
+ _ = gobject.Object.bindProperty(
+ self.as(gobject.Object),
+ "config",
+ priv.global_shortcuts.as(gobject.Object),
+ "config",
+ .{ .sync_create = true },
+ );
+
+ // Setup the signal handler for global shortcut triggers
+ _ = GlobalShortcuts.signals.trigger.connect(
+ priv.global_shortcuts,
+ *Application,
+ globalShortcutTrigger,
+ self,
+ .{},
+ );
+ }
+
+ fn activate(self: *Self) callconv(.c) void {
+ log.debug("activate", .{});
+
+ // Queue a new window
+ const priv = self.private();
+ _ = priv.core_app.mailbox.push(.{
+ .new_window = .{},
+ }, .{ .forever = {} });
+
+ // Call the parent activate method.
+ gio.Application.virtual_methods.activate.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ fn dispose(self: *Self) callconv(.c) void {
+ const priv = self.private();
+ if (priv.config_errors_dialog.get()) |diag| {
+ diag.close();
+ diag.unref(); // strong ref from get()
+ }
+ priv.config_errors_dialog.set(null);
+ if (priv.signal_source) |v| {
+ if (glib.Source.remove(v) == 0) {
+ log.warn("unable to remove signal source", .{});
+ }
+ priv.signal_source = null;
+ }
+
+ gobject.Object.virtual_methods.dispose.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ fn finalize(self: *Self) callconv(.c) void {
+ self.deinit();
+ gobject.Object.virtual_methods.finalize.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ //---------------------------------------------------------------
+ // Signal Handlers
+
+ /// SIGUSR2 signal handler via g_unix_signal_add
+ fn handleSigusr2(ud: ?*anyopaque) callconv(.c) c_int {
+ const self: *Self = @ptrCast(@alignCast(ud orelse
+ return @intFromBool(glib.SOURCE_CONTINUE)));
+
+ log.info("received SIGUSR2, reloading configuration", .{});
+ Action.reloadConfig(
+ self,
+ .app,
+ .{},
+ ) catch |err| {
+ // If we fail to reload the configuration, then we want the
+ // user to know it. For now we log but we should show another
+ // GUI.
+ log.warn("error reloading config: {}", .{err});
+ };
+
+ return @intFromBool(glib.SOURCE_CONTINUE);
+ }
+
+ fn handleCloseConfirmation(
+ _: *CloseConfirmationDialog,
+ self: *Self,
+ ) callconv(.c) void {
+ self.quitNow();
+ }
+
+ fn handleQuitTimerExpired(ud: ?*anyopaque) callconv(.c) c_int {
+ const self: *Self = @ptrCast(@alignCast(ud));
+ const priv = self.private();
+ priv.quit_timer = .expired;
+ return 0;
+ }
+
+ fn handleStyleManagerDark(
+ style: *adw.StyleManager,
+ _: *gobject.ParamSpec,
+ self: *Self,
+ ) callconv(.c) void {
+ const scheme: apprt.ColorScheme = if (style.getDark() == 0)
+ .light
+ else
+ .dark;
+ log.debug("style manager changed scheme={}", .{scheme});
+
+ const priv: *Private = self.private();
+ const core_app = priv.core_app;
+ core_app.colorSchemeEvent(self.rt(), scheme) catch |err| {
+ log.warn("error updating app color scheme err={}", .{err});
+ };
+ for (core_app.surfaces.items) |surface| {
+ surface.core().colorSchemeCallback(scheme) catch |err| {
+ log.warn(
+ "unable to tell surface about color scheme change err={}",
+ .{err},
+ );
+ };
+ }
+
+ if (gtk_version.atLeast(4, 20, 0)) {
+ const gtk_scheme: gtk.InterfaceColorScheme = switch (scheme) {
+ .light => gtk.InterfaceColorScheme.light,
+ .dark => gtk.InterfaceColorScheme.dark,
+ };
+ var value = gobject.ext.Value.newFrom(gtk_scheme);
+ gobject.Object.setProperty(
+ priv.css_provider.as(gobject.Object),
+ "prefers-color-scheme",
+ &value,
+ );
+ for (priv.custom_css_providers.items) |css_provider| {
+ gobject.Object.setProperty(
+ css_provider.as(gobject.Object),
+ "prefers-color-scheme",
+ &value,
+ );
+ }
+ }
+ }
+
+ fn handleReloadConfig(
+ _: *ConfigErrorsDialog,
+ self: *Self,
+ ) callconv(.c) void {
+ // We clear our dialog reference because its going to close
+ // after response handling and we don't want to reuse it.
+ const priv = self.private();
+ priv.config_errors_dialog.set(null);
+
+ // Reload our config as if the app reloaded.
+ Action.reloadConfig(
+ self,
+ .app,
+ .{},
+ ) catch |err| {
+ // If we fail to reload the configuration, then we want the
+ // user to know it. For now we log but we should show another
+ // GUI.
+ log.warn("error reloading config: {}", .{err});
+ };
+ }
+
+ /// Show the config errors dialog if the config on our application
+ /// has diagnostics.
+ fn showConfigErrorsDialog(self: *Self) void {
+ const priv = self.private();
+
+ // If we already have a dialog, just update the config.
+ if (priv.config_errors_dialog.get()) |diag| {
+ defer diag.unref(); // get gets a strong ref
+
+ var value = gobject.ext.Value.newFrom(priv.config);
+ defer value.unset();
+ gobject.Object.setProperty(
+ diag.as(gobject.Object),
+ "config",
+ &value,
+ );
+
+ if (!priv.config.hasDiagnostics()) {
+ diag.close();
+ } else {
+ diag.present(null);
+ }
+
+ return;
+ }
+
+ // No diagnostics, do nothing.
+ if (!priv.config.hasDiagnostics()) return;
+
+ // No dialog yet, initialize a new one. There's no need to unref
+ // here because the widget that it becomes a part of takes ownership.
+ const dialog: *ConfigErrorsDialog = .new(priv.config);
+ priv.config_errors_dialog.set(dialog);
+
+ // Connect to the reload signal so we know to reload our config.
+ _ = ConfigErrorsDialog.signals.@"reload-config".connect(
+ dialog,
+ *Application,
+ handleReloadConfig,
+ self,
+ .{},
+ );
+
+ // Show it
+ dialog.present(null);
+ }
+
+ fn globalShortcutTrigger(
+ _: *GlobalShortcuts,
+ action: *const Binding.Action,
+ self: *Self,
+ ) callconv(.c) void {
+ self.core().performAllAction(self.rt(), action.*) catch |err| {
+ log.warn("failed to perform action={}", .{err});
+ };
+ }
+
+ fn actionReloadConfig(
+ _: *gio.SimpleAction,
+ _: ?*glib.Variant,
+ self: *Self,
+ ) callconv(.c) void {
+ const priv = self.private();
+ priv.core_app.performAction(self.rt(), .reload_config) catch |err| {
+ log.warn("error reloading config err={}", .{err});
+ };
+ }
+
+ fn actionQuit(
+ _: *gio.SimpleAction,
+ _: ?*glib.Variant,
+ self: *Self,
+ ) callconv(.c) void {
+ const priv = self.private();
+ priv.core_app.performAction(self.rt(), .quit) catch |err| {
+ log.warn("error quitting err={}", .{err});
+ };
+ }
+
+ /// Handle `app.new-window` and `app.new-window-command` GTK actions
+ pub fn actionNewWindow(
+ _: *gio.SimpleAction,
+ parameter_: ?*glib.Variant,
+ self: *Self,
+ ) callconv(.c) void {
+ log.debug("received new window action", .{});
+
+ var arena: std.heap.ArenaAllocator = .init(Application.default().allocator());
+ defer arena.deinit();
+
+ const alloc = arena.allocator();
+
+ var working_directory: ?[:0]const u8 = null;
+ var title: ?[:0]const u8 = null;
+ var command: ?configpkg.Command = null;
+ var args: std.ArrayList([:0]const u8) = .empty;
+
+ overrides: {
+ // were we given a parameter?
+ const parameter = parameter_ orelse break :overrides;
+
+ const as_variant_type = glib.VariantType.new("as");
+ defer as_variant_type.free();
+
+ // ensure that the supplied parameter is an array of strings
+ if (glib.Variant.isOfType(parameter, as_variant_type) == 0) {
+ log.warn("parameter is of type '{s}', not '{s}'", .{
+ parameter.getTypeString(),
+ as_variant_type.peekString()[0..as_variant_type.getStringLength()],
+ });
+ break :overrides;
+ }
+
+ const s_variant_type = glib.VariantType.new("s");
+ defer s_variant_type.free();
+
+ var it: glib.VariantIter = undefined;
+ _ = it.init(parameter);
+
+ var e_seen: bool = false;
+ var i: usize = 0;
+
+ while (it.nextValue()) |value| : (i += 1) {
+ defer value.unref();
+
+ // just to be sure
+ if (value.isOfType(s_variant_type) == 0) continue;
+
+ var len: usize = undefined;
+ const buf = value.getString(&len);
+ const str = buf[0..len];
+
+ log.debug("new-window argument: {d} {s}", .{ i, str });
+
+ if (e_seen) {
+ const duplicated = alloc.dupeZ(u8, str) catch |err| {
+ log.warn("unable to duplicate argument {d} {s}: {t}", .{ i, str, err });
+ break :overrides;
+ };
+ args.append(alloc, duplicated) catch |err| {
+ log.warn("unable to append argument {d} {s}: {t}", .{ i, str, err });
+ break :overrides;
+ };
+ continue;
+ }
+
+ if (std.mem.eql(u8, str, "-e")) {
+ e_seen = true;
+ continue;
+ }
+
+ if (lib.cutPrefix(u8, str, "--command=")) |v| {
+ var cmd: configpkg.Command = undefined;
+ cmd.parseCLI(alloc, v) catch |err| {
+ log.warn("unable to parse command: {t}", .{err});
+ continue;
+ };
+ command = cmd;
+ continue;
+ }
+ if (lib.cutPrefix(u8, str, "--working-directory=")) |v| {
+ working_directory = alloc.dupeZ(u8, std.mem.trim(u8, v, &std.ascii.whitespace)) catch |err| wd: {
+ log.warn("unable to duplicate working directory: {t}", .{err});
+ break :wd null;
+ };
+ continue;
+ }
+ if (lib.cutPrefix(u8, str, "--title=")) |v| {
+ title = alloc.dupeZ(u8, std.mem.trim(u8, v, &std.ascii.whitespace)) catch |err| t: {
+ log.warn("unable to duplicate title: {t}", .{err});
+ break :t null;
+ };
+ continue;
+ }
+ }
+ }
+
+ if (args.items.len > 0) {
+ command = .{
+ .direct = args.items,
+ };
+ }
+
+ Action.newWindow(self, null, .{
+ .command = command,
+ .working_directory = working_directory,
+ .title = title,
+ }) catch |err| {
+ log.warn("unable to create new window: {t}", .{err});
+ };
+ }
+
+ pub fn actionOpenConfig(
+ _: *gio.SimpleAction,
+ _: ?*glib.Variant,
+ self: *Self,
+ ) callconv(.c) void {
+ _ = self.core().mailbox.push(.open_config, .forever);
+ }
+
+ fn actionPresentSurface(
+ _: *gio.SimpleAction,
+ parameter_: ?*glib.Variant,
+ self: *Self,
+ ) callconv(.c) void {
+ const parameter = parameter_ orelse return;
+
+ const t = glib.ext.VariantType.newFor(u64);
+ defer glib.VariantType.free(t);
+
+ // Make sure that we've received a u64 from the system.
+ if (glib.Variant.isOfType(parameter, t) == 0) {
+ return;
+ }
+
+ // Convert the u64 to a core surface by using it as a surface ID.
+ // A value of zero means that there was no target surface for the
+ // notification so we don't focus any surface.
+ const surface_id = parameter.getUint64();
+ if (surface_id == 0) return;
+ const surface = self.core().findSurfaceByID(surface_id) orelse return;
+
+ _ = self.core().mailbox.push(
+ .{
+ .surface_message = .{
+ .surface = surface,
+ .message = .present_surface,
+ },
+ },
+ .forever,
+ );
+ }
+
+ //----------------------------------------------------------------
+ // Boilerplate/Noise
+
+ const C = Common(Self, Private);
+ pub const as = C.as;
+ pub const ref = C.ref;
+ pub const unref = C.unref;
+ const private = C.private;
+
+ pub const Class = extern struct {
+ parent_class: Parent.Class,
+ var parent: *Parent.Class = undefined;
+ pub const Instance = Self;
+
+ fn init(class: *Class) callconv(.c) void {
+ // Register our compiled resources exactly once.
+ {
+ const c = @cImport({
+ // generated header files
+ @cInclude("ghostty_resources.h");
+ });
+ if (c.ghostty_get_resource()) |ptr| {
+ gio.resourcesRegister(@ptrCast(@alignCast(ptr)));
+ } else {
+ // If we fail to load resources then things will
+ // probably look really bad but it shouldn't stop our
+ // app from loading.
+ log.warn("unable to load resources", .{});
+ }
+ }
+
+ // Properties
+ gobject.ext.registerProperties(class, &.{
+ properties.config.impl,
+ });
+
+ // Virtual methods
+ gio.Application.virtual_methods.activate.implement(class, &activate);
+ gio.Application.virtual_methods.startup.implement(class, &startup);
+ gobject.Object.virtual_methods.dispose.implement(class, &dispose);
+ gobject.Object.virtual_methods.finalize.implement(class, &finalize);
+ }
+ };
+
+ pub fn openUrlFallback(self: *Application, kind: apprt.action.OpenUrl.Kind, url: []const u8) void {
+ // Fallback to the minimal cross-platform way of opening a URL.
+ // This is always a safe fallback and enables for example Windows
+ // to open URLs (GTK on Windows via WSL is a thing).
+ internal_os.open(
+ self.allocator(),
+ kind,
+ url,
+ ) catch |err| log.warn("unable to open url: {}", .{err});
+ }
+};
+
+/// All apprt action handlers
+const Action = struct {
+ pub fn closeTab(target: apprt.Target, value: apprt.Action.Value(.close_tab)) bool {
+ switch (target) {
+ .app => return false,
+ .surface => |core| {
+ const surface = core.rt_surface.surface;
+ return surface.as(gtk.Widget).activateAction(
+ "tab.close",
+ glib.ext.VariantType.stringFor([:0]const u8),
+ @as([*:0]const u8, @tagName(value)),
+ ) != 0;
+ },
+ }
+ }
+
+ pub fn closeWindow(target: apprt.Target) bool {
+ switch (target) {
+ .app => return false,
+ .surface => |core| {
+ const surface = core.rt_surface.surface;
+ return surface.as(gtk.Widget).activateAction("win.close", null) != 0;
+ },
+ }
+ }
+
+ pub fn copyTitleToClipboard(target: apprt.Target) bool {
+ return switch (target) {
+ .app => false,
+ .surface => |v| v.rt_surface.gobj().copyTitleToClipboard(),
+ };
+ }
+
+ pub fn configChange(
+ self: *Application,
+ target: apprt.Target,
+ new_config: *const CoreConfig,
+ ) !void {
+ // Wrap our config in a GObject. This will clone it.
+ const alloc = self.allocator();
+ const config_obj: *Config = try .new(alloc, new_config);
+ defer config_obj.unref();
+
+ switch (target) {
+ .surface => |core| core.rt_surface.surface.setConfig(config_obj),
+ .app => self.setConfig(config_obj),
+ }
+ }
+
+ pub fn desktopNotification(
+ self: *Application,
+ target: apprt.Target,
+ n: apprt.action.DesktopNotification,
+ ) void {
+ switch (target) {
+ .app => {},
+ .surface => |v| {
+ v.rt_surface.gobj().sendDesktopNotification(n.title, n.body);
+ return;
+ },
+ }
+
+ // Set a default title if we don't already have one
+ const t = switch (n.title.len) {
+ 0 => "Ghostty",
+ else => n.title,
+ };
+
+ const notification = gio.Notification.new(t);
+ defer notification.unref();
+ notification.setBody(n.body);
+
+ const icon = gio.ThemedIcon.new("com.mitchellh.ghostty");
+ defer icon.unref();
+ notification.setIcon(icon.as(gio.Icon));
+ notification.setDefaultActionAndTargetValue(
+ "app.present-surface",
+ glib.Variant.newUint64(0),
+ );
+
+ // We set the notification ID to the body content. If the content is the
+ // same, this notification may replace a previous notification
+ const gio_app = self.as(gio.Application);
+ gio_app.sendNotification(n.body, notification);
+ }
+
+ pub fn equalizeSplits(target: apprt.Target) bool {
+ switch (target) {
+ .app => {
+ log.warn("equalize splits to app is unexpected", .{});
+ return false;
+ },
+
+ .surface => |core| {
+ const surface = core.rt_surface.surface;
+ return surface.as(gtk.Widget).activateAction("split-tree.equalize", null) != 0;
+ },
+ }
+ }
+
+ pub fn gotoSplit(
+ target: apprt.Target,
+ to: apprt.action.GotoSplit,
+ ) bool {
+ switch (target) {
+ .app => return false,
+ .surface => |core| {
+ // Design note: we can't use widget actions here because
+ // we need to know whether there is a goto target for returning
+ // the proper perform result (boolean).
+
+ const surface = core.rt_surface.surface;
+ const tree = ext.getAncestor(
+ SplitTree,
+ surface.as(gtk.Widget),
+ ) orelse {
+ log.warn("surface is not in a split tree, ignoring goto_split", .{});
+ return false;
+ };
+
+ return tree.goto(switch (to) {
+ .previous => .previous_wrapped,
+ .next => .next_wrapped,
+ .up => .{ .spatial = .up },
+ .down => .{ .spatial = .down },
+ .left => .{ .spatial = .left },
+ .right => .{ .spatial = .right },
+ });
+ },
+ }
+ }
+
+ pub fn gotoTab(
+ target: apprt.Target,
+ tab: apprt.action.GotoTab,
+ ) bool {
+ switch (target) {
+ .app => return false,
+ .surface => |core| {
+ const surface = core.rt_surface.surface;
+ const window = ext.getAncestor(
+ Window,
+ surface.as(gtk.Widget),
+ ) orelse {
+ log.warn("surface is not in a window, ignoring new_tab", .{});
+ return false;
+ };
+
+ return window.selectTab(switch (tab) {
+ .previous => .previous,
+ .next => .next,
+ .last => .last,
+ else => .{ .n = @intCast(@intFromEnum(tab)) },
+ });
+ },
+ }
+ }
+
+ pub fn gotoWindow(direction: apprt.action.GotoWindow) bool {
+ const glist = gtk.Window.listToplevels();
+ defer glist.free();
+
+ // The window we're starting from is typically our active window.
+ const starting: *glib.List = @as(?*glib.List, glist.findCustom(
+ null,
+ findActiveWindow,
+ )) orelse glist;
+
+ // Go forward or backwards in the list until we find a valid
+ // window that is visible.
+ var current_: ?*glib.List = starting;
+ while (current_) |node| : (current_ = switch (direction) {
+ .next => node.f_next,
+ .previous => node.f_prev,
+ }) {
+ const data = node.f_data orelse continue;
+ const gtk_window: *gtk.Window = @ptrCast(@alignCast(data));
+ if (gotoWindowMaybe(gtk_window)) return true;
+ }
+
+ // If we reached here, we didn't find a valid window to focus.
+ // Wrap around.
+ current_ = switch (direction) {
+ .next => glist,
+ .previous => last: {
+ var end: *glib.List = glist;
+ while (end.f_next) |next| end = next;
+ break :last end;
+ },
+ };
+ while (current_) |node| : (current_ = switch (direction) {
+ .next => node.f_next,
+ .previous => node.f_prev,
+ }) {
+ if (current_ == starting) break;
+ const data = node.f_data orelse continue;
+ const gtk_window: *gtk.Window = @ptrCast(@alignCast(data));
+ if (gotoWindowMaybe(gtk_window)) return true;
+ }
+
+ return false;
+ }
+
+ fn gotoWindowMaybe(gtk_window: *gtk.Window) bool {
+ // If it is already active skip it.
+ if (gtk_window.isActive() != 0) return false;
+ // If it is hidden, skip it.
+ if (gtk_window.as(gtk.Widget).isVisible() == 0) return false;
+ // If it isn't a Ghostty window, skip it.
+ const window = gobject.ext.cast(
+ Window,
+ gtk_window,
+ ) orelse return false;
+
+ // Focus our active surface
+ const surface = window.getActiveSurface() orelse return false;
+ gtk.Window.present(gtk_window);
+ surface.grabFocus();
+ return true;
+ }
+
+ pub fn initialSize(
+ target: apprt.Target,
+ value: apprt.action.InitialSize,
+ ) bool {
+ switch (target) {
+ .app => return false,
+ .surface => |core| {
+ const surface = core.rt_surface.surface;
+ surface.setDefaultSize(.{
+ .width = value.width,
+ .height = value.height,
+ });
+ return true;
+ },
+ }
+ }
+
+ pub fn mouseOverLink(
+ target: apprt.Target,
+ value: apprt.action.MouseOverLink,
+ ) void {
+ switch (target) {
+ .app => log.warn("mouse over link to app is unexpected", .{}),
+ .surface => |surface| surface.rt_surface.gobj().setMouseHoverUrl(
+ if (value.url.len > 0) value.url else null,
+ ),
+ }
+ }
+
+ pub fn mouseShape(
+ target: apprt.Target,
+ shape: terminal.MouseShape,
+ ) void {
+ switch (target) {
+ .app => log.warn("mouse shape to app is unexpected", .{}),
+ .surface => |surface| surface.rt_surface.gobj().setMouseShape(shape),
+ }
+ }
+
+ pub fn mouseVisibility(
+ target: apprt.Target,
+ visibility: apprt.action.MouseVisibility,
+ ) void {
+ switch (target) {
+ .app => log.warn("mouse visibility to app is unexpected", .{}),
+ .surface => |surface| surface.rt_surface.gobj().setMouseHidden(switch (visibility) {
+ .visible => false,
+ .hidden => true,
+ }),
+ }
+ }
+
+ pub fn moveTab(
+ target: apprt.Target,
+ value: apprt.action.MoveTab,
+ ) bool {
+ switch (target) {
+ .app => return false,
+ .surface => |core| {
+ const surface = core.rt_surface.surface;
+ const window = ext.getAncestor(
+ Window,
+ surface.as(gtk.Widget),
+ ) orelse {
+ log.warn("surface is not in a window, ignoring new_tab", .{});
+ return false;
+ };
+
+ return window.moveTab(
+ surface,
+ @intCast(value.amount),
+ );
+ },
+ }
+ }
+
+ pub fn newSplit(
+ target: apprt.Target,
+ direction: apprt.action.SplitDirection,
+ ) bool {
+ switch (target) {
+ .app => {
+ log.warn("new split to app is unexpected", .{});
+ return false;
+ },
+
+ .surface => |core| {
+ const surface = core.rt_surface.surface;
+
+ return surface.as(gtk.Widget).activateAction(
+ "split-tree.new-split",
+ "&s",
+ @tagName(direction).ptr,
+ ) != 0;
+ },
+ }
+ }
+
+ pub fn newTab(target: apprt.Target) bool {
+ switch (target) {
+ .app => {
+ log.warn("new tab to app is unexpected", .{});
+ return false;
+ },
+
+ .surface => |core| {
+ // Get the window ancestor of the surface. Surfaces shouldn't
+ // be aware they might be in windows but at the app level we
+ // can do this.
+ const surface = core.rt_surface.surface;
+ const window = ext.getAncestor(
+ Window,
+ surface.as(gtk.Widget),
+ ) orelse {
+ log.warn("surface is not in a window, ignoring new_tab", .{});
+ return false;
+ };
+ window.newTab(core);
+ return true;
+ },
+ }
+ }
+
+ pub fn newWindow(
+ self: *Application,
+ parent: ?*CoreSurface,
+ overrides: struct {
+ command: ?configpkg.Command = null,
+ working_directory: ?[:0]const u8 = null,
+ title: ?[:0]const u8 = null,
+
+ pub const none: @This() = .{};
+ },
+ ) !void {
+ // Note that we've requested a window at least once. This is used
+ // to trigger quit on no windows. Note I'm not sure if this is REALLY
+ // necessary, but I don't want to risk a bug where on a slow machine
+ // or something we quit immediately after starting up because there
+ // was a delay in the event loop before we created a Window.
+ self.private().requested_window = true;
+
+ const win = Window.new(self, .{
+ .title = overrides.title,
+ });
+ initAndShowWindow(
+ self,
+ win,
+ parent,
+ .{
+ .command = overrides.command,
+ .working_directory = overrides.working_directory,
+ .title = overrides.title,
+ },
+ );
+ }
+
+ fn initAndShowWindow(
+ self: *Application,
+ win: *Window,
+ parent: ?*CoreSurface,
+ overrides: struct {
+ command: ?configpkg.Command = null,
+ working_directory: ?[:0]const u8 = null,
+ title: ?[:0]const u8 = null,
+
+ pub const none: @This() = .{};
+ },
+ ) void {
+ // Setup a binding so that whenever our config changes so does the
+ // window. There's never a time when the window config should be out
+ // of sync with the application config.
+ _ = gobject.Object.bindProperty(
+ self.as(gobject.Object),
+ "config",
+ win.as(gobject.Object),
+ "config",
+ .{},
+ );
+
+ // Create a new tab with window context (first tab in new window)
+ win.newTabForWindow(parent, .{
+ .command = overrides.command,
+ .working_directory = overrides.working_directory,
+ .title = overrides.title,
+ });
+
+ // Estimate the initial window size before presenting so the window
+ // manager can position it correctly.
+ if (win.getActiveSurface()) |surface| {
+ surface.estimateInitialSize();
+ if (surface.getDefaultSize()) |size| {
+ win.as(gtk.Window).setDefaultSize(
+ @intCast(size.width),
+ @intCast(size.height),
+ );
+ }
+ }
+
+ // Show the window
+ gtk.Window.present(win.as(gtk.Window));
+ }
+
+ pub fn openConfig(self: *Application) bool {
+ // Get the config file path
+ const alloc = self.allocator();
+ const path = configpkg.edit.openPath(alloc) catch |err| {
+ log.warn("error getting config file path: {}", .{err});
+ return false;
+ };
+ defer alloc.free(path);
+
+ // Open it using openURL. "path" isn't actually a URL but
+ // at the time of writing that works just fine for GTK.
+ openUrl(self, .{ .kind = .text, .url = path });
+ return true;
+ }
+
+ pub fn openUrl(
+ self: *Application,
+ value: apprt.action.OpenUrl,
+ ) void {
+ if (std.mem.startsWith(u8, value.url, "/")) {
+ self.openUrlFallback(value.kind, value.url);
+ return;
+ }
+ if (std.mem.startsWith(u8, value.url, "file://")) {
+ self.openUrlFallback(value.kind, value.url);
+ return;
+ }
+
+ self.openUri().start(value) catch |err| {
+ log.err("unable to open uri err={}", .{err});
+ self.openUrlFallback(value.kind, value.url);
+ return;
+ };
+ }
+
+ pub fn pwd(
+ target: apprt.Target,
+ value: apprt.action.Pwd,
+ ) void {
+ switch (target) {
+ .app => log.warn("pwd to app is unexpected", .{}),
+ .surface => |surface| surface.rt_surface.gobj().setPwd(value.pwd),
+ }
+ }
+
+ pub fn quitTimer(
+ self: *Application,
+ mode: apprt.action.QuitTimer,
+ ) !void {
+ switch (mode) {
+ .start => self.startQuitTimer(),
+ .stop => self.stopQuitTimer(),
+ }
+ }
+
+ pub fn presentTerminal(
+ target: apprt.Target,
+ ) bool {
+ return switch (target) {
+ .app => false,
+ .surface => |v| surface: {
+ v.rt_surface.surface.present();
+ break :surface true;
+ },
+ };
+ }
+
+ pub fn progressReport(
+ target: apprt.Target,
+ value: terminal.osc.Command.ProgressReport,
+ ) bool {
+ return switch (target) {
+ .app => false,
+ .surface => |v| surface: {
+ v.rt_surface.surface.setProgressReport(value);
+ break :surface true;
+ },
+ };
+ }
+
+ pub fn promptTitle(target: apprt.Target, value: apprt.action.PromptTitle) bool {
+ switch (value) {
+ .surface => switch (target) {
+ .app => return false,
+ .surface => |v| {
+ v.rt_surface.surface.promptTitle();
+ return true;
+ },
+ },
+ .tab => {
+ switch (target) {
+ .app => return false,
+ .surface => |v| {
+ const surface = v.rt_surface.surface;
+ const tab = ext.getAncestor(
+ Tab,
+ surface.as(gtk.Widget),
+ ) orelse {
+ log.warn("surface is not in a tab, ignoring prompt_tab_title", .{});
+ return false;
+ };
+ tab.promptTabTitle();
+ return true;
+ },
+ }
+ },
+ }
+ }
+
+ /// Reload the configuration for the application and propagate it
+ /// across the entire application and all terminals.
+ pub fn reloadConfig(
+ self: *Application,
+ target: apprt.Target,
+ opts: apprt.action.ReloadConfig,
+ ) !void {
+ // Tell systemd that reloading has started.
+ systemd.notify.reloading();
+
+ // When we exit this function tell systemd that reloading has finished.
+ defer systemd.notify.ready();
+
+ // Get our config object.
+ const config: *Config = config: {
+ // Soft-reloading applies conditional logic to the existing loaded
+ // config so we return that as-is (but take a reference).
+ if (opts.soft) {
+ break :config self.private().config.ref();
+ }
+
+ // Hard reload, load a new config completely.
+ const alloc = self.allocator();
+ var config = try CoreConfig.load(alloc);
+ defer config.deinit();
+ break :config try .new(alloc, &config);
+ };
+ defer config.unref();
+
+ // Update the proper target. This will trigger a `config_change`
+ // apprt action which will propagate the config properly to our
+ // property system.
+ switch (target) {
+ .app => try self.core().updateConfig(
+ self.rt(),
+ config.get(),
+ ),
+ .surface => |core| try core.updateConfig(config.get()),
+ }
+ }
+
+ pub fn render(target: apprt.Target) void {
+ switch (target) {
+ .app => {},
+ .surface => |v| v.rt_surface.surface.redraw(),
+ }
+ }
+
+ pub fn resizeSplit(
+ target: apprt.Target,
+ value: apprt.action.ResizeSplit,
+ ) bool {
+ switch (target) {
+ .app => {
+ log.warn("resize_split to app is unexpected", .{});
+ return false;
+ },
+ .surface => |core| {
+ const surface = core.rt_surface.surface;
+ const tree = ext.getAncestor(
+ SplitTree,
+ surface.as(gtk.Widget),
+ ) orelse {
+ log.warn("surface is not in a split tree, ignoring resize_split", .{});
+ return false;
+ };
+
+ // If the tree has no splits (only one leaf), this action is not performable.
+ // This allows the key event to pass through to the terminal.
+ if (!tree.getIsSplit()) return false;
+
+ return tree.resize(
+ switch (value.direction) {
+ .up => .up,
+ .down => .down,
+ .left => .left,
+ .right => .right,
+ },
+ value.amount,
+ ) catch |err| switch (err) {
+ error.OutOfMemory => {
+ log.warn("unable to resize split, out of memory", .{});
+ return false;
+ },
+ };
+ },
+ }
+ }
+
+ pub fn ringBell(target: apprt.Target) void {
+ switch (target) {
+ .app => {},
+ .surface => |v| v.rt_surface.surface.setBellRinging(true),
+ }
+ }
+
+ pub fn scrollbar(
+ target: apprt.Target,
+ value: apprt.Action.Value(.scrollbar),
+ ) void {
+ switch (target) {
+ .app => {},
+ .surface => |v| v.rt_surface.surface.setScrollbar(value),
+ }
+ }
+
+ pub fn startSearch(target: apprt.Target, value: apprt.action.StartSearch) void {
+ switch (target) {
+ .app => {},
+ .surface => |v| v.rt_surface.surface.setSearchActive(true, value.needle),
+ }
+ }
+
+ pub fn endSearch(target: apprt.Target) void {
+ switch (target) {
+ .app => {},
+ .surface => |v| v.rt_surface.surface.setSearchActive(false, ""),
+ }
+ }
+
+ pub fn searchTotal(target: apprt.Target, value: apprt.action.SearchTotal) void {
+ switch (target) {
+ .app => {},
+ .surface => |v| v.rt_surface.surface.setSearchTotal(value.total),
+ }
+ }
+
+ pub fn searchSelected(target: apprt.Target, value: apprt.action.SearchSelected) void {
+ switch (target) {
+ .app => {},
+ .surface => |v| v.rt_surface.surface.setSearchSelected(value.selected),
+ }
+ }
+
+ pub fn setTitle(
+ target: apprt.Target,
+ value: apprt.action.SetTitle,
+ ) void {
+ switch (target) {
+ .app => log.warn("set_title to app is unexpected", .{}),
+ .surface => |surface| surface.rt_surface.gobj().setTitle(value.title),
+ }
+ }
+
+ pub fn setTabTitle(
+ target: apprt.Target,
+ value: apprt.action.SetTitle,
+ ) bool {
+ switch (target) {
+ .app => {
+ log.warn("set_tab_title to app is unexpected", .{});
+ return false;
+ },
+ .surface => |core| {
+ const surface = core.rt_surface.surface;
+ const tab = ext.getAncestor(
+ Tab,
+ surface.as(gtk.Widget),
+ ) orelse {
+ log.warn("surface is not in a tab, ignoring set_tab_title", .{});
+ return false;
+ };
+ tab.setTitleOverride(if (value.title.len == 0) null else value.title);
+ return true;
+ },
+ }
+ }
+
+ pub fn showChildExited(
+ target: apprt.Target,
+ value: apprt.surface.Message.ChildExited,
+ ) bool {
+ return switch (target) {
+ .app => false,
+ .surface => |v| v.rt_surface.surface.childExited(value),
+ };
+ }
+
+ pub fn showGtkInspector() void {
+ gtk.Window.setInteractiveDebugging(@intFromBool(true));
+ }
+
+ pub fn sizeLimit(
+ target: apprt.Target,
+ value: apprt.action.SizeLimit,
+ ) bool {
+ switch (target) {
+ .app => return false,
+ .surface => |core| {
+ // Note: we ignore the max size currently because we have
+ // no mechanism to enforce it.
+ const surface = core.rt_surface.surface;
+ surface.setMinSize(.{
+ .width = value.min_width,
+ .height = value.min_height,
+ });
+
+ return true;
+ },
+ }
+ }
+
+ pub fn toggleFullscreen(target: apprt.Target) void {
+ switch (target) {
+ .app => {},
+ .surface => |v| v.rt_surface.surface.toggleFullscreen(),
+ }
+ }
+
+ pub fn toggleQuickTerminal(self: *Application) bool {
+ // If we already have a quick terminal window, we just toggle the
+ // visibility of it.
+ if (getQuickTerminalWindow()) |win| {
+ win.toggleVisibility();
+ return true;
+ }
+
+ // If we don't support quick terminals then we do nothing.
+ const priv = self.private();
+ if (!priv.winproto.supportsQuickTerminal()) return false;
+
+ // Create our new window as a quick terminal
+ const win = gobject.ext.newInstance(Window, .{
+ .application = self,
+ .@"quick-terminal" = true,
+ });
+ assert(win.isQuickTerminal());
+ initAndShowWindow(self, win, null, .none);
+ return true;
+ }
+
+ pub fn toggleSplitZoom(target: apprt.Target) bool {
+ switch (target) {
+ .app => {
+ log.warn("toggle_split_zoom to app is unexpected", .{});
+ return false;
+ },
+
+ .surface => |core| {
+ // TODO: pass surface ID when we have that
+ const surface = core.rt_surface.surface;
+ const tree = ext.getAncestor(
+ SplitTree,
+ surface.as(gtk.Widget),
+ ) orelse {
+ log.warn("surface is not in a split tree, ignoring toggle_split_zoom", .{});
+ return false;
+ };
+
+ // If the tree has no splits (only one leaf), this action is not performable.
+ // This allows the key event to pass through to the terminal.
+ if (!tree.getIsSplit()) return false;
+
+ return surface.as(gtk.Widget).activateAction("split-tree.zoom", null) != 0;
+ },
+ }
+ }
+
+ pub fn showOnScreenKeyboard(target: apprt.Target) bool {
+ switch (target) {
+ .app => {
+ log.warn("show_on_screen_keyboard to app is unexpected", .{});
+ return false;
+ },
+ // NOTE: Even though `activateOsk` takes a gdk.Event, it's currently
+ // unused by all implementations of `activateOsk` as of GTK 4.18.
+ // The commit that introduced the method (ce6aa73c) clarifies that
+ // the event *may* be used by other IM backends, but for Linux desktop
+ // environments this doesn't matter.
+ .surface => |v| return v.rt_surface.surface.showOnScreenKeyboard(null),
+ }
+ }
+
+ fn getQuickTerminalWindow() ?*Window {
+ // Find a quick terminal window.
+ const list = gtk.Window.listToplevels();
+ defer list.free();
+ if (ext.listFind(gtk.Window, list, struct {
+ fn find(gtk_win: *gtk.Window) bool {
+ const win = gobject.ext.cast(
+ Window,
+ gtk_win,
+ ) orelse return false;
+ return win.isQuickTerminal();
+ }
+ }.find)) |w| return gobject.ext.cast(
+ Window,
+ w,
+ ).?;
+
+ return null;
+ }
+
+ pub fn toggleMaximize(target: apprt.Target) void {
+ switch (target) {
+ .app => {},
+ .surface => |v| v.rt_surface.surface.toggleMaximize(),
+ }
+ }
+
+ pub fn toggleTabOverview(target: apprt.Target) bool {
+ switch (target) {
+ .app => return false,
+ .surface => |core| {
+ const surface = core.rt_surface.surface;
+ const window = ext.getAncestor(
+ Window,
+ surface.as(gtk.Widget),
+ ) orelse {
+ log.warn("surface is not in a window, ignoring new_tab", .{});
+ return false;
+ };
+
+ window.toggleTabOverview();
+ return true;
+ },
+ }
+ }
+
+ pub fn toggleWindowDecorations(target: apprt.Target) bool {
+ switch (target) {
+ .app => return false,
+ .surface => |core| {
+ const surface = core.rt_surface.surface;
+ const window = ext.getAncestor(
+ Window,
+ surface.as(gtk.Widget),
+ ) orelse {
+ log.warn("surface is not in a window, ignoring toggle_window_decorations", .{});
+ return false;
+ };
+
+ window.toggleWindowDecorations();
+ return true;
+ },
+ }
+ }
+
+ pub fn toggleCommandPalette(target: apprt.Target) bool {
+ switch (target) {
+ .app => return false,
+ .surface => |surface| {
+ return surface.rt_surface.gobj().toggleCommandPalette();
+ },
+ }
+ }
+
+ pub fn controlInspector(target: apprt.Target, value: apprt.Action.Value(.inspector)) bool {
+ switch (target) {
+ .app => return false,
+ .surface => |surface| {
+ return surface.rt_surface.gobj().controlInspector(value);
+ },
+ }
+ }
+
+ pub fn commandFinished(target: apprt.Target, value: apprt.Action.Value(.command_finished)) bool {
+ switch (target) {
+ .app => return false,
+ .surface => |surface| {
+ return surface.rt_surface.gobj().commandFinished(value);
+ },
+ }
+ }
+
+ pub fn setReadonly(target: apprt.Target, value: apprt.Action.Value(.readonly)) bool {
+ switch (target) {
+ .app => return false,
+ .surface => |surface| {
+ return surface.rt_surface.gobj().setReadonly(value);
+ },
+ }
+ }
+
+ pub fn keySequence(target: apprt.Target, value: apprt.Action.Value(.key_sequence)) bool {
+ switch (target) {
+ .app => {
+ log.warn("key_sequence action to app is unexpected", .{});
+ return false;
+ },
+ .surface => |core| {
+ core.rt_surface.gobj().keySequenceAction(value) catch |err| {
+ log.warn("error handling key_sequence action: {}", .{err});
+ };
+ return true;
+ },
+ }
+ }
+
+ pub fn keyTable(target: apprt.Target, value: apprt.Action.Value(.key_table)) bool {
+ switch (target) {
+ .app => {
+ log.warn("key_table action to app is unexpected", .{});
+ return false;
+ },
+ .surface => |core| {
+ core.rt_surface.gobj().keyTableAction(value) catch |err| {
+ log.warn("error handling key_table action: {}", .{err});
+ };
+ return true;
+ },
+ }
+ }
+};
+
+/// This sets various GTK-related environment variables as necessary
+/// given the runtime environment or configuration.
+///
+/// This must be called BEFORE GTK initialization.
+fn setGtkEnv(config: *const CoreConfig) error{NoSpaceLeft}!void {
+ assert(gtk.isInitialized() == 0);
+
+ var gdk_debug: struct {
+ /// output OpenGL debug information
+ opengl: bool = false,
+ /// disable GLES, Ghostty can't use GLES
+ @"gl-disable-gles": bool = false,
+ // GTK's new renderer can cause blurry font when using fractional scaling.
+ @"gl-no-fractional": bool = false,
+ /// Disabling Vulkan can improve startup times by hundreds of
+ /// milliseconds on some systems. We don't use Vulkan so we can just
+ /// disable it.
+ @"vulkan-disable": bool = false,
+ } = .{
+ // `gtk-opengl-debug` dumps logs directly to stderr so both must be true
+ // to enable OpenGL debugging.
+ .opengl = state.logging.stderr and config.@"gtk-opengl-debug",
+ };
+
+ var gdk_disable: struct {
+ @"gles-api": bool = false,
+ /// current gtk implementation for color management is not good enough.
+ /// see: https://bugs.kde.org/show_bug.cgi?id=495647
+ /// gtk issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/6864
+ @"color-mgmt": bool = true,
+ /// Disabling Vulkan can improve startup times by hundreds of
+ /// milliseconds on some systems. We don't use Vulkan so we can just
+ /// disable it.
+ vulkan: bool = false,
+ } = .{};
+
+ environment: {
+ if (gtk_version.runtimeAtLeast(4, 18, 0)) {
+ gdk_disable.@"color-mgmt" = false;
+ }
+
+ if (gtk_version.runtimeAtLeast(4, 16, 0)) {
+ // From gtk 4.16, GDK_DEBUG is split into GDK_DEBUG and GDK_DISABLE.
+ // For the remainder of "why" see the 4.14 comment below.
+ gdk_disable.@"gles-api" = true;
+ gdk_disable.vulkan = true;
+ break :environment;
+ }
+ if (gtk_version.runtimeAtLeast(4, 14, 0)) {
+ // We need to export GDK_DEBUG to run on Wayland after GTK 4.14.
+ // Older versions of GTK do not support these values so it is safe
+ // to always set this. Forwards versions are uncertain so we'll have
+ // to reassess...
+ //
+ // Upstream issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/6589
+ gdk_debug.@"gl-disable-gles" = true;
+ gdk_debug.@"vulkan-disable" = true;
+
+ if (gtk_version.runtimeUntil(4, 17, 5)) {
+ // Removed at GTK v4.17.5
+ gdk_debug.@"gl-no-fractional" = true;
+ }
+ break :environment;
+ }
+
+ // Versions prior to 4.14 are a bit of an unknown for Ghostty. It
+ // is an environment that isn't tested well and we don't have a
+ // good understanding of what we may need to do.
+ gdk_debug.@"vulkan-disable" = true;
+ }
+
+ {
+ var buf: [1024]u8 = undefined;
+ var fmt = std.io.fixedBufferStream(&buf);
+ const writer = fmt.writer();
+ var first: bool = true;
+ inline for (@typeInfo(@TypeOf(gdk_debug)).@"struct".fields) |field| {
+ if (@field(gdk_debug, field.name)) {
+ if (!first) try writer.writeAll(",");
+ try writer.writeAll(field.name);
+ first = false;
+ }
+ }
+ try writer.writeByte(0);
+ const value = fmt.getWritten();
+ log.warn("setting GDK_DEBUG={s}", .{value[0 .. value.len - 1]});
+ _ = internal_os.setenv("GDK_DEBUG", value[0 .. value.len - 1 :0]);
+ }
+
+ {
+ var buf: [1024]u8 = undefined;
+ var fmt = std.io.fixedBufferStream(&buf);
+ const writer = fmt.writer();
+ var first: bool = true;
+ inline for (@typeInfo(@TypeOf(gdk_disable)).@"struct".fields) |field| {
+ if (@field(gdk_disable, field.name)) {
+ if (!first) try writer.writeAll(",");
+ try writer.writeAll(field.name);
+ first = false;
+ }
+ }
+ try writer.writeByte(0);
+ const value = fmt.getWritten();
+ log.warn("setting GDK_DISABLE={s}", .{value[0 .. value.len - 1]});
+ _ = internal_os.setenv("GDK_DISABLE", value[0 .. value.len - 1 :0]);
+ }
+}
+
+fn findActiveWindow(data: ?*const anyopaque, _: ?*const anyopaque) callconv(.c) c_int {
+ const window: *gtk.Window = @ptrCast(@alignCast(@constCast(data orelse return -1)));
+
+ // Confusingly, `isActive` returns 1 when active,
+ // but we want to return 0 to indicate equality.
+ // Abusing integers to be enums and booleans is a terrible idea, C.
+ return if (window.isActive() != 0) 0 else -1;
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/class/clipboard_confirmation_dialog.zig b/vendor/libghostty-vt/src/apprt/gtk/class/clipboard_confirmation_dialog.zig
new file mode 100644
index 00000000..d44d38a3
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/class/clipboard_confirmation_dialog.zig
@@ -0,0 +1,352 @@
+const std = @import("std");
+const adw = @import("adw");
+const glib = @import("glib");
+const gobject = @import("gobject");
+const gtk = @import("gtk");
+
+const apprt = @import("../../../apprt.zig");
+const gresource = @import("../build/gresource.zig");
+const i18n = @import("../../../os/main.zig").i18n;
+const adw_version = @import("../adw_version.zig");
+const Common = @import("../class.zig").Common;
+const Dialog = @import("dialog.zig").Dialog;
+
+const log = std.log.scoped(.gtk_ghostty_clipboard_confirmation);
+
+/// Whether we're able to have the remember switch
+const can_remember = adw_version.supportsSwitchRow();
+
+pub const ClipboardConfirmationDialog = extern struct {
+ const Self = @This();
+ parent_instance: Parent,
+ pub const Parent = Dialog;
+ pub const getGObjectType = gobject.ext.defineClass(Self, .{
+ .name = "GhosttyClipboardConfirmationDialog",
+ .instanceInit = &init,
+ .classInit = &Class.init,
+ .parent_class = &Class.parent,
+ .private = .{ .Type = Private, .offset = &Private.offset },
+ });
+
+ pub const properties = struct {
+ pub const @"can-remember" = struct {
+ pub const name = "can-remember";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ bool,
+ .{
+ .default = false,
+ .accessor = gobject.ext.privateFieldAccessor(
+ Self,
+ Private,
+ &Private.offset,
+ "can_remember",
+ ),
+ },
+ );
+ };
+
+ pub const request = struct {
+ pub const name = "request";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?*apprt.ClipboardRequest,
+ .{
+ .accessor = C.privateBoxedFieldAccessor("request"),
+ },
+ );
+ };
+
+ pub const @"clipboard-contents" = struct {
+ pub const name = "clipboard-contents";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?*gtk.TextBuffer,
+ .{
+ .accessor = C.privateObjFieldAccessor("clipboard_contents"),
+ },
+ );
+ };
+
+ pub const blur = struct {
+ pub const name = "blur";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ bool,
+ .{
+ .default = false,
+ .accessor = gobject.ext.privateFieldAccessor(
+ Self,
+ Private,
+ &Private.offset,
+ "blur",
+ ),
+ },
+ );
+ };
+ };
+
+ pub const signals = struct {
+ pub const deny = struct {
+ pub const name = "deny";
+ pub const connect = impl.connect;
+ const impl = gobject.ext.defineSignal(
+ name,
+ Self,
+ &.{bool},
+ void,
+ );
+ };
+
+ pub const confirm = struct {
+ pub const name = "confirm";
+ pub const connect = impl.connect;
+ const impl = gobject.ext.defineSignal(
+ name,
+ Self,
+ &.{bool},
+ void,
+ );
+ };
+ };
+
+ const Private = struct {
+ /// The request that this dialog is for.
+ request: ?*apprt.ClipboardRequest = null,
+
+ /// The clipboard contents being read/written.
+ clipboard_contents: ?*gtk.TextBuffer = null,
+
+ /// Whether the contents should be blurred.
+ blur: bool = false,
+
+ /// Whether the user can remember the choice.
+ can_remember: bool = false,
+
+ // Template bindings
+ text_view_scroll: *gtk.ScrolledWindow,
+ text_view: *gtk.TextView,
+ reveal_button: *gtk.Button,
+ hide_button: *gtk.Button,
+ remember_choice: if (can_remember) *adw.SwitchRow else void,
+
+ pub var offset: c_int = 0;
+ };
+
+ pub fn new() *Self {
+ return gobject.ext.newInstance(Self, .{});
+ }
+
+ fn init(self: *Self, _: *Class) callconv(.c) void {
+ gtk.Widget.initTemplate(self.as(gtk.Widget));
+
+ // Trigger initial values
+ self.propBlur(undefined, null);
+ self.propRequest(undefined, null);
+ }
+
+ pub fn present(self: *Self, parent: ?*gtk.Widget) void {
+ self.as(Dialog).present(parent);
+ }
+
+ /// Get the clipboard request without copying.
+ pub fn getRequest(self: *Self) ?*apprt.ClipboardRequest {
+ return self.private().request;
+ }
+
+ /// Get the clipboard contents without copying.
+ pub fn getClipboardContents(self: *Self) ?*gtk.TextBuffer {
+ return self.private().clipboard_contents;
+ }
+
+ //---------------------------------------------------------------
+ // Signal Handlers
+
+ fn propBlur(
+ self: *Self,
+ _: *gobject.ParamSpec,
+ _: ?*anyopaque,
+ ) callconv(.c) void {
+ const priv = self.private();
+ if (priv.blur) {
+ priv.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(false));
+ priv.text_view.as(gtk.Widget).addCssClass("blurred");
+ priv.reveal_button.as(gtk.Widget).setVisible(@intFromBool(true));
+ priv.hide_button.as(gtk.Widget).setVisible(@intFromBool(false));
+ } else {
+ priv.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(true));
+ priv.text_view.as(gtk.Widget).removeCssClass("blurred");
+ priv.reveal_button.as(gtk.Widget).setVisible(@intFromBool(false));
+ priv.hide_button.as(gtk.Widget).setVisible(@intFromBool(false));
+ }
+ }
+
+ fn propRequest(
+ self: *Self,
+ _: *gobject.ParamSpec,
+ _: ?*anyopaque,
+ ) callconv(.c) void {
+ const priv = self.private();
+ const req = priv.request orelse return;
+ switch (req.*) {
+ .osc_52_write => {
+ self.as(Dialog.Parent).setHeading(i18n._("Authorize Clipboard Access"));
+ self.as(Dialog.Parent).setBody(i18n._("An application is attempting to write to the clipboard. The current clipboard contents are shown below."));
+ },
+ .osc_52_read => {
+ self.as(Dialog.Parent).setHeading(i18n._("Authorize Clipboard Access"));
+ self.as(Dialog.Parent).setBody(i18n._("An application is attempting to read from the clipboard. The current clipboard contents are shown below."));
+ },
+ .paste => {
+ self.as(Dialog.Parent).setHeading(i18n._("Warning: Potentially Unsafe Paste"));
+ self.as(Dialog.Parent).setBody(i18n._("Pasting this text into the terminal may be dangerous as it looks like some commands may be executed."));
+ },
+ }
+ }
+
+ fn revealButtonClicked(_: *gtk.Button, self: *Self) callconv(.c) void {
+ const priv = self.private();
+ priv.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(true));
+ priv.text_view.as(gtk.Widget).removeCssClass("blurred");
+ priv.hide_button.as(gtk.Widget).setVisible(@intFromBool(true));
+ priv.reveal_button.as(gtk.Widget).setVisible(@intFromBool(false));
+ }
+
+ fn hideButtonClicked(_: *gtk.Button, self: *Self) callconv(.c) void {
+ const priv = self.private();
+ priv.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(false));
+ priv.text_view.as(gtk.Widget).addCssClass("blurred");
+ priv.hide_button.as(gtk.Widget).setVisible(@intFromBool(false));
+ priv.reveal_button.as(gtk.Widget).setVisible(@intFromBool(true));
+ }
+
+ //---------------------------------------------------------------
+ // Virtual methods
+
+ fn response(
+ self: *Self,
+ response_id: [*:0]const u8,
+ ) callconv(.c) void {
+ const remember: bool = if (comptime can_remember) remember: {
+ const priv = self.private();
+ break :remember priv.remember_choice.getActive() != 0;
+ } else false;
+
+ if (std.mem.orderZ(u8, response_id, "cancel") == .eq) {
+ signals.deny.impl.emit(
+ self,
+ null,
+ .{remember},
+ null,
+ );
+ } else if (std.mem.orderZ(u8, response_id, "ok") == .eq) {
+ signals.confirm.impl.emit(
+ self,
+ null,
+ .{remember},
+ null,
+ );
+ }
+ }
+
+ fn dispose(self: *Self) callconv(.c) void {
+ const priv = self.private();
+ if (priv.clipboard_contents) |v| {
+ v.unref();
+ priv.clipboard_contents = null;
+ }
+
+ gtk.Widget.disposeTemplate(
+ self.as(gtk.Widget),
+ getGObjectType(),
+ );
+
+ gobject.Object.virtual_methods.dispose.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ fn finalize(self: *Self) callconv(.c) void {
+ const priv = self.private();
+ if (priv.request) |v| {
+ glib.ext.destroy(v);
+ priv.request = null;
+ }
+
+ gobject.Object.virtual_methods.finalize.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ const C = Common(Self, Private);
+ pub const as = C.as;
+ pub const ref = C.ref;
+ pub const unref = C.unref;
+ const private = C.private;
+
+ pub const Class = extern struct {
+ parent_class: Parent.Class,
+ var parent: *Parent.Class = undefined;
+ pub const Instance = Self;
+
+ fn init(class: *Class) callconv(.c) void {
+ gtk.Widget.Class.setTemplateFromResource(
+ class.as(gtk.Widget.Class),
+ if (comptime adw_version.atLeast(1, 4, 0))
+ comptime gresource.blueprint(.{
+ .major = 1,
+ .minor = 4,
+ .name = "clipboard-confirmation-dialog",
+ })
+ else
+ comptime gresource.blueprint(.{
+ .major = 1,
+ .minor = 0,
+ .name = "clipboard-confirmation-dialog",
+ }),
+ );
+
+ // Bindings
+ class.bindTemplateChildPrivate("text_view_scroll", .{});
+ class.bindTemplateChildPrivate("text_view", .{});
+ class.bindTemplateChildPrivate("hide_button", .{});
+ class.bindTemplateChildPrivate("reveal_button", .{});
+ if (comptime can_remember) {
+ class.bindTemplateChildPrivate("remember_choice", .{});
+ }
+
+ // Template Callbacks
+ class.bindTemplateCallback("reveal_clicked", &revealButtonClicked);
+ class.bindTemplateCallback("hide_clicked", &hideButtonClicked);
+ class.bindTemplateCallback("notify_blur", &propBlur);
+ class.bindTemplateCallback("notify_request", &propRequest);
+
+ // Properties
+ gobject.ext.registerProperties(class, &.{
+ properties.blur.impl,
+ properties.@"can-remember".impl,
+ properties.@"clipboard-contents".impl,
+ properties.request.impl,
+ });
+
+ // Signals
+ signals.confirm.impl.register(.{});
+ signals.deny.impl.register(.{});
+
+ // Virtual methods
+ gobject.Object.virtual_methods.dispose.implement(class, &dispose);
+ gobject.Object.virtual_methods.finalize.implement(class, &finalize);
+ Dialog.virtual_methods.response.implement(class, &response);
+ }
+
+ pub const as = C.Class.as;
+ pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
+ pub const bindTemplateCallback = C.Class.bindTemplateCallback;
+ };
+};
diff --git a/vendor/libghostty-vt/src/apprt/gtk/class/close_confirmation_dialog.zig b/vendor/libghostty-vt/src/apprt/gtk/class/close_confirmation_dialog.zig
new file mode 100644
index 00000000..5919f9c9
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/class/close_confirmation_dialog.zig
@@ -0,0 +1,204 @@
+const std = @import("std");
+const gobject = @import("gobject");
+const gtk = @import("gtk");
+
+const gresource = @import("../build/gresource.zig");
+const i18n = @import("../../../os/main.zig").i18n;
+const Common = @import("../class.zig").Common;
+const Dialog = @import("dialog.zig").Dialog;
+
+const log = std.log.scoped(.gtk_ghostty_close_confirmation_dialog);
+
+pub const CloseConfirmationDialog = extern struct {
+ const Self = @This();
+ parent_instance: Parent,
+ pub const Parent = Dialog;
+ pub const getGObjectType = gobject.ext.defineClass(Self, .{
+ .name = "GhosttyCloseConfirmationDialog",
+ .instanceInit = &init,
+ .classInit = &Class.init,
+ .parent_class = &Class.parent,
+ .private = .{ .Type = Private, .offset = &Private.offset },
+ });
+
+ pub const properties = struct {
+ pub const target = struct {
+ pub const name = "target";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ Target,
+ .{
+ .default = .app,
+ .accessor = gobject.ext.privateFieldAccessor(
+ Self,
+ Private,
+ &Private.offset,
+ "target",
+ ),
+ },
+ );
+ };
+ };
+
+ pub const signals = struct {
+ pub const @"close-request" = struct {
+ pub const name = "close-request";
+ pub const connect = impl.connect;
+ const impl = gobject.ext.defineSignal(
+ name,
+ Self,
+ &.{},
+ void,
+ );
+ };
+
+ pub const cancel = struct {
+ pub const name = "cancel";
+ pub const connect = impl.connect;
+ const impl = gobject.ext.defineSignal(
+ name,
+ Self,
+ &.{},
+ void,
+ );
+ };
+ };
+
+ const Private = struct {
+ target: Target,
+ pub var offset: c_int = 0;
+ };
+
+ pub fn new(target: Target) *Self {
+ return gobject.ext.newInstance(Self, .{
+ .target = target,
+ });
+ }
+
+ fn init(self: *Self, _: *Class) callconv(.c) void {
+ gtk.Widget.initTemplate(self.as(gtk.Widget));
+ }
+
+ pub fn present(self: *Self, parent: ?*gtk.Widget) void {
+ // Setup our title/body text.
+ const priv = self.private();
+ self.as(Dialog.Parent).setHeading(priv.target.title());
+ self.as(Dialog.Parent).setBody(priv.target.body());
+
+ // Show it
+ self.as(Dialog).present(parent);
+ }
+
+ pub fn close(self: *Self) void {
+ self.as(Dialog).close();
+ }
+
+ fn response(
+ self: *Self,
+ response_id: [*:0]const u8,
+ ) callconv(.c) void {
+ if (std.mem.orderZ(u8, response_id, "close") == .eq) {
+ signals.@"close-request".impl.emit(
+ self,
+ null,
+ .{},
+ null,
+ );
+ } else {
+ signals.cancel.impl.emit(
+ self,
+ null,
+ .{},
+ null,
+ );
+ }
+ }
+
+ fn dispose(self: *Self) callconv(.c) void {
+ gtk.Widget.disposeTemplate(
+ self.as(gtk.Widget),
+ getGObjectType(),
+ );
+
+ gobject.Object.virtual_methods.dispose.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ const C = Common(Self, Private);
+ pub const as = C.as;
+ pub const ref = C.ref;
+ pub const refSink = C.refSink;
+ pub const unref = C.unref;
+ const private = C.private;
+
+ pub const Class = extern struct {
+ parent_class: Parent.Class,
+ var parent: *Parent.Class = undefined;
+ pub const Instance = Self;
+
+ fn init(class: *Class) callconv(.c) void {
+ gobject.ext.ensureType(Dialog);
+ gtk.Widget.Class.setTemplateFromResource(
+ class.as(gtk.Widget.Class),
+ comptime gresource.blueprint(.{
+ .major = 1,
+ .minor = 2,
+ .name = "close-confirmation-dialog",
+ }),
+ );
+
+ // Properties
+ gobject.ext.registerProperties(class, &.{
+ properties.target.impl,
+ });
+
+ // Signals
+ signals.@"close-request".impl.register(.{});
+ signals.cancel.impl.register(.{});
+
+ // Virtual methods
+ gobject.Object.virtual_methods.dispose.implement(class, &dispose);
+ Dialog.virtual_methods.response.implement(class, &response);
+ }
+
+ pub const as = C.Class.as;
+ };
+};
+
+/// The target of a close dialog.
+///
+/// This is here so that we can consolidate all logic related to
+/// prompting the user and closing windows/tabs/surfaces/etc.
+/// together into one struct that is the sole source of truth.
+pub const Target = enum(c_int) {
+ app,
+ tab,
+ window,
+ surface,
+
+ pub fn title(self: Target) [*:0]const u8 {
+ return switch (self) {
+ .app => i18n._("Quit Ghostty?"),
+ .tab => i18n._("Close Tab?"),
+ .window => i18n._("Close Window?"),
+ .surface => i18n._("Close Split?"),
+ };
+ }
+
+ pub fn body(self: Target) [*:0]const u8 {
+ return switch (self) {
+ .app => i18n._("All terminal sessions will be terminated."),
+ .tab => i18n._("All terminal sessions in this tab will be terminated."),
+ .window => i18n._("All terminal sessions in this window will be terminated."),
+ .surface => i18n._("The currently running process in this split will be terminated."),
+ };
+ }
+
+ pub const getGObjectType = gobject.ext.defineEnum(
+ Target,
+ .{ .name = "GhosttyCloseConfirmationDialogTarget" },
+ );
+};
diff --git a/vendor/libghostty-vt/src/apprt/gtk/class/command_palette.zig b/vendor/libghostty-vt/src/apprt/gtk/class/command_palette.zig
new file mode 100644
index 00000000..6101e82e
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/class/command_palette.zig
@@ -0,0 +1,789 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const ArenaAllocator = std.heap.ArenaAllocator;
+
+const adw = @import("adw");
+const gio = @import("gio");
+const gobject = @import("gobject");
+const gtk = @import("gtk");
+
+const input = @import("../../../input.zig");
+const gresource = @import("../build/gresource.zig");
+const key = @import("../key.zig");
+const WeakRef = @import("../weak_ref.zig").WeakRef;
+const Common = @import("../class.zig").Common;
+const Application = @import("application.zig").Application;
+const Window = @import("window.zig").Window;
+const Surface = @import("surface.zig").Surface;
+const Tab = @import("tab.zig").Tab;
+const Config = @import("config.zig").Config;
+
+const log = std.log.scoped(.gtk_ghostty_command_palette);
+
+pub const CommandPalette = extern struct {
+ const Self = @This();
+ parent_instance: Parent,
+ pub const Parent = adw.Bin;
+ pub const getGObjectType = gobject.ext.defineClass(Self, .{
+ .name = "GhosttyCommandPalette",
+ .instanceInit = &init,
+ .classInit = &Class.init,
+ .parent_class = &Class.parent,
+ .private = .{ .Type = Private, .offset = &Private.offset },
+ });
+
+ pub const properties = struct {
+ pub const config = struct {
+ pub const name = "config";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?*Config,
+ .{
+ .accessor = C.privateObjFieldAccessor("config"),
+ },
+ );
+ };
+ };
+
+ pub const signals = struct {
+ /// Emitted when a command from the command palette is activated. The
+ /// action contains pointers to allocated data so if a receiver of this
+ /// signal needs to keep the action around it will need to clone the
+ /// action or there may be use-after-free errors.
+ pub const trigger = struct {
+ pub const name = "trigger";
+ pub const connect = impl.connect;
+ const impl = gobject.ext.defineSignal(
+ name,
+ Self,
+ &.{*const input.Binding.Action},
+ void,
+ );
+ };
+ };
+
+ const Private = struct {
+ /// The configuration that this command palette is using.
+ config: ?*Config = null,
+
+ /// The dialog object containing the palette UI.
+ dialog: *adw.Dialog,
+
+ /// The search input text field.
+ search: *gtk.SearchEntry,
+
+ /// The view containing each result row.
+ view: *gtk.ListView,
+
+ /// The model that provides filtered data for the view to display.
+ model: *gtk.SingleSelection,
+
+ /// The list that serves as the data source of the model.
+ /// This is where all command data is ultimately stored.
+ source: *gio.ListStore,
+
+ pub var offset: c_int = 0;
+ };
+
+ /// Create a new instance of the command palette. The caller will own a
+ /// reference to the object.
+ pub fn new() *Self {
+ const self = gobject.ext.newInstance(Self, .{});
+
+ // Sink ourselves so that we aren't floating anymore. We'll unref
+ // ourselves when the palette is closed or an action is activated.
+ _ = self.refSink();
+
+ // Bump the ref so that the caller has a reference.
+ return self.ref();
+ }
+
+ //---------------------------------------------------------------
+ // Virtual Methods
+
+ fn init(self: *Self, _: *Class) callconv(.c) void {
+ gtk.Widget.initTemplate(self.as(gtk.Widget));
+
+ // Listen for any changes to our config.
+ _ = gobject.Object.signals.notify.connect(
+ self,
+ ?*anyopaque,
+ propConfig,
+ null,
+ .{
+ .detail = "config",
+ },
+ );
+ }
+
+ fn dispose(self: *Self) callconv(.c) void {
+ const priv = self.private();
+
+ priv.source.removeAll();
+
+ if (priv.config) |config| {
+ config.unref();
+ priv.config = null;
+ }
+
+ gtk.Widget.disposeTemplate(
+ self.as(gtk.Widget),
+ getGObjectType(),
+ );
+
+ gobject.Object.virtual_methods.dispose.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ //---------------------------------------------------------------
+ // Signal Handlers
+
+ fn propConfig(self: *CommandPalette, _: *gobject.ParamSpec, _: ?*anyopaque) callconv(.c) void {
+ const priv = self.private();
+
+ const config = priv.config orelse {
+ log.warn("command palette does not have a config!", .{});
+ return;
+ };
+
+ // Clear existing binds
+ priv.source.removeAll();
+
+ const alloc = Application.default().allocator();
+ var commands: std.ArrayList(*Command) = .{};
+ defer {
+ for (commands.items) |cmd| cmd.unref();
+ commands.deinit(alloc);
+ }
+
+ self.collectJumpCommands(config, &commands) catch |err| {
+ log.warn("failed to collect jump commands: {}", .{err});
+ };
+
+ self.collectRegularCommands(config, &commands, alloc);
+
+ // Sort commands
+ std.mem.sort(*Command, commands.items, {}, struct {
+ fn lessThan(_: void, a: *Command, b: *Command) bool {
+ return compareCommands(a, b);
+ }
+ }.lessThan);
+
+ for (commands.items) |cmd| {
+ const cmd_ref = cmd.as(gobject.Object);
+ priv.source.append(cmd_ref);
+ }
+ }
+
+ /// Collect regular commands from configuration, filtering out unsupported actions.
+ fn collectRegularCommands(
+ self: *CommandPalette,
+ config: *Config,
+ commands: *std.ArrayList(*Command),
+ alloc: std.mem.Allocator,
+ ) void {
+ _ = self;
+ const cfg = config.get();
+
+ for (cfg.@"command-palette-entry".value.items) |command| {
+ // Filter out actions that are not implemented or don't make sense
+ // for GTK.
+ if (!isActionSupportedOnGtk(command.action)) continue;
+
+ const cmd = Command.new(config, command) catch |err| {
+ log.warn("failed to create command: {}", .{err});
+ continue;
+ };
+ errdefer cmd.unref();
+
+ commands.append(alloc, cmd) catch |err| {
+ log.warn("failed to add command to list: {}", .{err});
+ continue;
+ };
+ }
+ }
+
+ /// Check if an action is supported on GTK.
+ fn isActionSupportedOnGtk(action: input.Binding.Action) bool {
+ return switch (action) {
+ .close_all_windows,
+ .toggle_secure_input,
+ .check_for_updates,
+ .redo,
+ .undo,
+ .reset_window_size,
+ .toggle_window_float_on_top,
+ => false,
+
+ else => true,
+ };
+ }
+
+ /// Collect jump commands for all surfaces across all windows.
+ fn collectJumpCommands(
+ self: *CommandPalette,
+ config: *Config,
+ commands: *std.ArrayList(*Command),
+ ) !void {
+ _ = self;
+ const app = Application.default();
+ const alloc = app.allocator();
+
+ // Get all surfaces from the core app
+ const core_app = app.core();
+ for (core_app.surfaces.items) |apprt_surface| {
+ const surface = apprt_surface.gobj();
+ const cmd = Command.newJump(config, surface);
+ errdefer cmd.unref();
+ try commands.append(alloc, cmd);
+ }
+ }
+
+ /// Compare two commands for sorting.
+ /// Sorts alphabetically by title (case-insensitive), with colon normalization
+ /// so "Foo:" sorts before "Foo Bar:". Uses sort_key as tie-breaker.
+ fn compareCommands(a: *Command, b: *Command) bool {
+ const a_title = a.propGetTitle() orelse return false;
+ const b_title = b.propGetTitle() orelse return true;
+
+ // Compare case-insensitively with colon normalization
+ for (0..@min(a_title.len, b_title.len)) |i| {
+ // Get characters, replacing ':' with '\t'
+ const a_char = if (a_title[i] == ':') '\t' else a_title[i];
+ const b_char = if (b_title[i] == ':') '\t' else b_title[i];
+
+ const a_lower = std.ascii.toLower(a_char);
+ const b_lower = std.ascii.toLower(b_char);
+
+ if (a_lower != b_lower) {
+ return a_lower < b_lower;
+ }
+ }
+
+ // If one title is a prefix of the other, shorter one comes first
+ if (a_title.len != b_title.len) {
+ return a_title.len < b_title.len;
+ }
+
+ // Titles are equal - use sort_key as tie-breaker if both are jump commands
+ const a_sort_key = switch (a.private().data) {
+ .regular => return false,
+ .jump => |*ja| ja.sort_key,
+ };
+ const b_sort_key = switch (b.private().data) {
+ .regular => return false,
+ .jump => |*jb| jb.sort_key,
+ };
+
+ return a_sort_key < b_sort_key;
+ }
+
+ fn close(self: *CommandPalette) void {
+ const priv = self.private();
+ _ = priv.dialog.close();
+ }
+
+ fn dialogClosed(_: *adw.Dialog, self: *CommandPalette) callconv(.c) void {
+ self.unref();
+ }
+
+ fn searchStopped(_: *gtk.SearchEntry, self: *CommandPalette) callconv(.c) void {
+ // ESC was pressed - close the palette
+ self.close();
+ }
+
+ fn searchActivated(_: *gtk.SearchEntry, self: *CommandPalette) callconv(.c) void {
+ // If Enter is pressed, activate the selected entry
+ const priv = self.private();
+ self.activated(priv.model.getSelected());
+ }
+
+ fn rowActivated(_: *gtk.ListView, pos: c_uint, self: *CommandPalette) callconv(.c) void {
+ self.activated(pos);
+ }
+
+ //---------------------------------------------------------------
+
+ /// Show or hide the command palette dialog. If the dialog is shown it will
+ /// be modal over the given window.
+ pub fn toggle(self: *CommandPalette, window: *Window) void {
+ const priv = self.private();
+
+ // If the dialog has been shown, close it.
+ if (priv.dialog.as(gtk.Widget).getRealized() != 0) {
+ self.close();
+ return;
+ }
+
+ // Show the dialog
+ priv.dialog.present(window.as(gtk.Widget));
+
+ // Focus on the search bar when opening the dialog
+ _ = priv.search.as(gtk.Widget).grabFocus();
+ }
+
+ /// Helper function to send a signal containing the action that should be
+ /// performed.
+ fn activated(self: *CommandPalette, pos: c_uint) void {
+ const priv = self.private();
+
+ // Use priv.model and not priv.source here to use the list of *visible* results
+ const object_ = priv.model.as(gio.ListModel).getObject(pos);
+ defer if (object_) |object| object.unref();
+
+ // Close before running the action in order to avoid being replaced by
+ // another dialog (such as the change title dialog). If that occurs then
+ // the command palette dialog won't be counted as having closed properly
+ // and cannot receive focus when reopened.
+ self.close();
+
+ const cmd = gobject.ext.cast(Command, object_ orelse return) orelse return;
+
+ // Handle jump commands differently
+ if (cmd.isJump()) {
+ const surface = cmd.getJumpSurface() orelse return;
+ defer surface.unref();
+ surface.present();
+ return;
+ }
+
+ // Regular command - emit trigger signal
+ const action = cmd.getAction() orelse return;
+
+ // Signal that an action has been selected. Signals are synchronous
+ // so we shouldn't need to worry about cloning the action.
+ signals.trigger.impl.emit(
+ self,
+ null,
+ .{&action},
+ null,
+ );
+ }
+
+ const C = Common(Self, Private);
+ pub const as = C.as;
+ pub const ref = C.ref;
+ pub const refSink = C.refSink;
+ pub const unref = C.unref;
+ const private = C.private;
+
+ pub const Class = extern struct {
+ parent_class: Parent.Class,
+ var parent: *Parent.Class = undefined;
+ pub const Instance = Self;
+
+ fn init(class: *Class) callconv(.c) void {
+ gobject.ext.ensureType(Command);
+ gtk.Widget.Class.setTemplateFromResource(
+ class.as(gtk.Widget.Class),
+ comptime gresource.blueprint(.{
+ .major = 1,
+ .minor = 5,
+ .name = "command-palette",
+ }),
+ );
+
+ // Bindings
+ class.bindTemplateChildPrivate("dialog", .{});
+ class.bindTemplateChildPrivate("search", .{});
+ class.bindTemplateChildPrivate("view", .{});
+ class.bindTemplateChildPrivate("model", .{});
+ class.bindTemplateChildPrivate("source", .{});
+
+ // Template Callbacks
+ class.bindTemplateCallback("closed", &dialogClosed);
+ class.bindTemplateCallback("notify_config", &propConfig);
+ class.bindTemplateCallback("search_stopped", &searchStopped);
+ class.bindTemplateCallback("search_activated", &searchActivated);
+ class.bindTemplateCallback("row_activated", &rowActivated);
+
+ // Properties
+ gobject.ext.registerProperties(class, &.{
+ properties.config.impl,
+ });
+
+ // Signals
+ signals.trigger.impl.register(.{});
+
+ // Virtual methods
+ gobject.Object.virtual_methods.dispose.implement(class, &dispose);
+ }
+
+ pub const as = C.Class.as;
+ pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
+ pub const bindTemplateCallback = C.Class.bindTemplateCallback;
+ };
+};
+
+/// Object that wraps around a command.
+///
+/// As GTK list models only accept objects that are within the GObject hierarchy,
+/// we have to construct a wrapper to be easily consumed by the list model.
+const Command = extern struct {
+ pub const Self = @This();
+ pub const Parent = gobject.Object;
+ parent: Parent,
+
+ pub const getGObjectType = gobject.ext.defineClass(Self, .{
+ .name = "GhosttyCommand",
+ .instanceInit = &init,
+ .classInit = Class.init,
+ .parent_class = &Class.parent,
+ .private = .{ .Type = Private, .offset = &Private.offset },
+ });
+
+ const properties = struct {
+ pub const config = struct {
+ pub const name = "config";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?*Config,
+ .{
+ .accessor = C.privateObjFieldAccessor("config"),
+ },
+ );
+ };
+
+ pub const action_key = struct {
+ pub const name = "action-key";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?[:0]const u8,
+ .{
+ .default = null,
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ ?[:0]const u8,
+ .{
+ .getter = propGetActionKey,
+ .getter_transfer = .none,
+ },
+ ),
+ },
+ );
+ };
+
+ pub const action = struct {
+ pub const name = "action";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?[:0]const u8,
+ .{
+ .default = null,
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ ?[:0]const u8,
+ .{
+ .getter = propGetAction,
+ .getter_transfer = .none,
+ },
+ ),
+ },
+ );
+ };
+
+ pub const title = struct {
+ pub const name = "title";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?[:0]const u8,
+ .{
+ .default = null,
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ ?[:0]const u8,
+ .{
+ .getter = propGetTitle,
+ .getter_transfer = .none,
+ },
+ ),
+ },
+ );
+ };
+
+ pub const description = struct {
+ pub const name = "description";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?[:0]const u8,
+ .{
+ .default = null,
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ ?[:0]const u8,
+ .{
+ .getter = propGetDescription,
+ .getter_transfer = .none,
+ },
+ ),
+ },
+ );
+ };
+ };
+
+ pub const Private = struct {
+ config: ?*Config = null,
+ arena: ArenaAllocator,
+ data: CommandData,
+
+ pub var offset: c_int = 0;
+
+ pub const CommandData = union(enum) {
+ regular: RegularData,
+ jump: JumpData,
+ };
+
+ pub const RegularData = struct {
+ command: input.Command,
+ action: ?[:0]const u8 = null,
+ action_key: ?[:0]const u8 = null,
+ };
+
+ pub const JumpData = struct {
+ surface: WeakRef(Surface) = .empty,
+ title: ?[:0]const u8 = null,
+ description: ?[:0]const u8 = null,
+ sort_key: usize,
+ };
+ };
+
+ pub fn new(config: *Config, command: input.Command) Allocator.Error!*Self {
+ const self = gobject.ext.newInstance(Self, .{
+ .config = config,
+ });
+ errdefer self.unref();
+
+ const priv = self.private();
+ const cloned = try command.clone(priv.arena.allocator());
+
+ priv.data = .{
+ .regular = .{
+ .command = cloned,
+ },
+ };
+
+ return self;
+ }
+
+ /// Create a new jump command that focuses a specific surface.
+ pub fn newJump(config: *Config, surface: *Surface) *Self {
+ const self = gobject.ext.newInstance(Self, .{
+ .config = config,
+ });
+
+ const priv = self.private();
+ priv.data = .{
+ .jump = .{
+ // TODO: Replace with surface id whenever Ghostty adds one
+ .sort_key = @intFromPtr(surface),
+ },
+ };
+ priv.data.jump.surface.set(surface);
+
+ return self;
+ }
+
+ fn init(self: *Self, _: *Class) callconv(.c) void {
+ // NOTE: we do not watch for changes to the config here as the command
+ // palette will destroy and recreate this object if/when the config
+ // changes.
+
+ const priv = self.private();
+ priv.arena = .init(Application.default().allocator());
+ }
+
+ fn dispose(self: *Self) callconv(.c) void {
+ const priv = self.private();
+
+ if (priv.config) |config| {
+ config.unref();
+ priv.config = null;
+ }
+
+ switch (priv.data) {
+ .regular => {},
+ .jump => |*j| {
+ j.surface.set(null);
+ },
+ }
+
+ gobject.Object.virtual_methods.dispose.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ fn finalize(self: *Self) callconv(.c) void {
+ const priv = self.private();
+
+ priv.arena.deinit();
+
+ gobject.Object.virtual_methods.finalize.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ //---------------------------------------------------------------
+
+ fn propGetActionKey(self: *Self) ?[:0]const u8 {
+ const priv = self.private();
+
+ const regular = switch (priv.data) {
+ .regular => |*r| r,
+ .jump => return null,
+ };
+
+ if (regular.action_key) |action_key| return action_key;
+
+ regular.action_key = std.fmt.allocPrintSentinel(
+ priv.arena.allocator(),
+ "{f}",
+ .{regular.command.action},
+ 0,
+ ) catch null;
+
+ return regular.action_key;
+ }
+
+ fn propGetAction(self: *Self) ?[:0]const u8 {
+ const priv = self.private();
+
+ const regular = switch (priv.data) {
+ .regular => |*r| r,
+ .jump => return null,
+ };
+
+ if (regular.action) |action| return action;
+
+ const cfg = if (priv.config) |config| config.get() else return null;
+ const keybinds = cfg.keybind.set;
+
+ const alloc = priv.arena.allocator();
+
+ regular.action = action: {
+ var buf: [64]u8 = undefined;
+ const trigger = keybinds.getTrigger(regular.command.action) orelse break :action null;
+ const accel = (key.accelFromTrigger(&buf, trigger) catch break :action null) orelse break :action null;
+ break :action alloc.dupeZ(u8, accel) catch return null;
+ };
+
+ return regular.action;
+ }
+
+ fn propGetTitle(self: *Self) ?[:0]const u8 {
+ const priv = self.private();
+
+ switch (priv.data) {
+ .regular => |*r| return r.command.title,
+ .jump => |*j| {
+ if (j.title) |title| return title;
+
+ const surface = j.surface.get() orelse return null;
+ defer surface.unref();
+
+ const alloc = priv.arena.allocator();
+ const effective_title = surface.getEffectiveTitle() orelse "Untitled";
+
+ j.title = std.fmt.allocPrintSentinel(
+ alloc,
+ "Focus: {s}",
+ .{effective_title},
+ 0,
+ ) catch null;
+
+ return j.title;
+ },
+ }
+ }
+
+ fn propGetDescription(self: *Self) ?[:0]const u8 {
+ const priv = self.private();
+
+ switch (priv.data) {
+ .regular => |*r| return r.command.description,
+ .jump => |*j| {
+ if (j.description) |desc| return desc;
+
+ const surface = j.surface.get() orelse return null;
+ defer surface.unref();
+
+ const alloc = priv.arena.allocator();
+ const title = surface.getEffectiveTitle() orelse "Untitled";
+ const pwd = surface.getPwd();
+
+ if (pwd) |p| {
+ if (std.mem.indexOf(u8, title, p) == null) {
+ j.description = alloc.dupeZ(u8, p) catch null;
+ }
+ }
+
+ return j.description;
+ },
+ }
+ }
+
+ //---------------------------------------------------------------
+
+ /// Return a copy of the action. Callers must ensure that they do not use
+ /// the action beyond the lifetime of this object because it has internally
+ /// allocated data that will be freed when this object is.
+ pub fn getAction(self: *Self) ?input.Binding.Action {
+ const priv = self.private();
+ return switch (priv.data) {
+ .regular => |*r| r.command.action,
+ .jump => null,
+ };
+ }
+
+ /// Check if this is a jump command.
+ pub fn isJump(self: *Self) bool {
+ const priv = self.private();
+ return priv.data == .jump;
+ }
+
+ /// Get the jump surface. Returns a strong reference that the caller
+ /// must unref when done, or null if the surface has been destroyed.
+ pub fn getJumpSurface(self: *Self) ?*Surface {
+ const priv = self.private();
+ return switch (priv.data) {
+ .regular => null,
+ .jump => |*j| j.surface.get(),
+ };
+ }
+
+ //---------------------------------------------------------------
+
+ const C = Common(Self, Private);
+ pub const as = C.as;
+ pub const ref = C.ref;
+ pub const unref = C.unref;
+ const private = C.private;
+
+ pub const Class = extern struct {
+ parent_class: Parent.Class,
+ var parent: *Parent.Class = undefined;
+ pub const Instance = Self;
+
+ fn init(class: *Class) callconv(.c) void {
+ gobject.ext.registerProperties(class, &.{
+ properties.config.impl,
+ properties.action_key.impl,
+ properties.action.impl,
+ properties.title.impl,
+ properties.description.impl,
+ });
+
+ gobject.Object.virtual_methods.dispose.implement(class, &dispose);
+ gobject.Object.virtual_methods.finalize.implement(class, &finalize);
+ }
+ };
+};
diff --git a/vendor/libghostty-vt/src/apprt/gtk/class/config.zig b/vendor/libghostty-vt/src/apprt/gtk/class/config.zig
new file mode 100644
index 00000000..9a705d35
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/class/config.zig
@@ -0,0 +1,175 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const gobject = @import("gobject");
+const gtk = @import("gtk");
+
+const configpkg = @import("../../../config.zig");
+const CoreConfig = configpkg.Config;
+
+const Common = @import("../class.zig").Common;
+
+const log = std.log.scoped(.gtk_ghostty_config);
+
+/// Wraps a `Ghostty.Config` object in a GObject so it can be reference
+/// counted. When this object is freed, the underlying config is also freed.
+///
+/// It is highly recommended to NOT take a reference to this object,
+/// since configuration takes up a lot of memory (relatively). Instead,
+/// receivers of this should usually create a `DerivedConfig` struct from
+/// this, copy any memory they require, and own that structure instead.
+///
+/// This can also expose helpers to access configuration in ways that
+/// may be more ergonomic to GTK primitives.
+pub const Config = extern struct {
+ const Self = @This();
+ parent_instance: Parent,
+ pub const Parent = gobject.Object;
+ pub const getGObjectType = gobject.ext.defineClass(Self, .{
+ .name = "GhosttyConfig",
+ .classInit = &Class.init,
+ .parent_class = &Class.parent,
+ .private = .{ .Type = Private, .offset = &Private.offset },
+ });
+
+ pub const properties = struct {
+ pub const @"diagnostics-buffer" = gobject.ext.defineProperty(
+ "diagnostics-buffer",
+ Self,
+ ?*gtk.TextBuffer,
+ .{
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ ?*gtk.TextBuffer,
+ .{
+ .getter = Self.diagnosticsBuffer,
+ .getter_transfer = .full,
+ },
+ ),
+ },
+ );
+
+ pub const @"has-diagnostics" = gobject.ext.defineProperty(
+ "has-diagnostics",
+ Self,
+ bool,
+ .{
+ .default = false,
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ bool,
+ .{
+ .getter = Self.hasDiagnostics,
+ },
+ ),
+ },
+ );
+ };
+
+ const Private = struct {
+ config: CoreConfig,
+
+ pub var offset: c_int = 0;
+ };
+
+ /// Create a new GhosttyConfig from a loaded configuration.
+ ///
+ /// This clones the given configuration, so it is safe for the
+ /// caller to free the original configuration after this call.
+ pub fn new(alloc: Allocator, config: *const CoreConfig) Allocator.Error!*Self {
+ const self = gobject.ext.newInstance(Self, .{});
+ errdefer self.unref();
+
+ const priv = self.private();
+ priv.config = try config.clone(alloc);
+
+ return self;
+ }
+
+ /// Get the wrapped configuration. It's unsafe to store this or access
+ /// it in any way that may live beyond the lifetime of this object.
+ pub fn get(self: *Self) *const CoreConfig {
+ return &self.private().config;
+ }
+
+ /// Get the mutable configuration. This is usually NOT recommended
+ /// because any changes to the config won't be propagated to anyone
+ /// with a reference to this object. If you know what you're doing, then
+ /// you can use this.
+ pub fn getMut(self: *Self) *CoreConfig {
+ return &self.private().config;
+ }
+
+ /// Returns whether this configuration has any diagnostics.
+ pub fn hasDiagnostics(self: *Self) bool {
+ const config = self.get();
+ return !config._diagnostics.empty();
+ }
+
+ /// Reads the diagnostics of this configuration as a TextBuffer,
+ /// or returns null if there are no diagnostics.
+ pub fn diagnosticsBuffer(self: *Self) ?*gtk.TextBuffer {
+ const config = self.get();
+ if (config._diagnostics.empty()) return null;
+
+ const text_buf: *gtk.TextBuffer = .new(null);
+ errdefer text_buf.unref();
+
+ var buf: [4095:0]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&buf);
+ for (config._diagnostics.items()) |diag| {
+ writer.end = 0;
+ diag.format(&writer) catch |err| {
+ log.warn(
+ "error writing diagnostic to buffer err={}",
+ .{err},
+ );
+ continue;
+ };
+
+ text_buf.insertAtCursor(&buf, @intCast(writer.end));
+ text_buf.insertAtCursor("\n", 1);
+ }
+
+ return text_buf;
+ }
+
+ fn finalize(self: *Self) callconv(.c) void {
+ self.private().config.deinit();
+
+ gobject.Object.virtual_methods.finalize.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ const C = Common(Self, Private);
+ pub const as = C.as;
+ pub const ref = C.ref;
+ pub const unref = C.unref;
+ const private = C.private;
+
+ pub const Class = extern struct {
+ parent_class: Parent.Class,
+ var parent: *Parent.Class = undefined;
+ pub const Instance = Self;
+
+ fn init(class: *Class) callconv(.c) void {
+ gobject.Object.virtual_methods.finalize.implement(class, &finalize);
+ gobject.ext.registerProperties(class, &.{
+ properties.@"diagnostics-buffer",
+ properties.@"has-diagnostics",
+ });
+ }
+ };
+};
+
+// This test verifies our memory management works as expected. Since
+// we use the testing allocator any leaks are detected.
+test "GhosttyConfig" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ var config: CoreConfig = try .default(alloc);
+ defer config.deinit();
+ const obj: *Config = try .new(alloc, &config);
+ obj.unref();
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/class/config_errors_dialog.zig b/vendor/libghostty-vt/src/apprt/gtk/class/config_errors_dialog.zig
new file mode 100644
index 00000000..46d5fe62
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/class/config_errors_dialog.zig
@@ -0,0 +1,161 @@
+const std = @import("std");
+const gobject = @import("gobject");
+const gtk = @import("gtk");
+
+const gresource = @import("../build/gresource.zig");
+const Common = @import("../class.zig").Common;
+const Config = @import("config.zig").Config;
+const Dialog = @import("dialog.zig").Dialog;
+
+const log = std.log.scoped(.gtk_ghostty_config_errors_dialog);
+
+pub const ConfigErrorsDialog = extern struct {
+ const Self = @This();
+ parent_instance: Parent,
+ pub const Parent = Dialog;
+ pub const getGObjectType = gobject.ext.defineClass(Self, .{
+ .name = "GhosttyConfigErrorsDialog",
+ .instanceInit = &init,
+ .classInit = &Class.init,
+ .parent_class = &Class.parent,
+ .private = .{ .Type = Private, .offset = &Private.offset },
+ });
+
+ pub const properties = struct {
+ pub const config = gobject.ext.defineProperty(
+ "config",
+ Self,
+ ?*Config,
+ .{
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ ?*Config,
+ .{
+ .getter = Self.getConfig,
+ .setter = Self.setConfig,
+ },
+ ),
+ },
+ );
+ };
+
+ pub const signals = struct {
+ pub const @"reload-config" = struct {
+ pub const name = "reload-config";
+ pub const connect = impl.connect;
+ const impl = gobject.ext.defineSignal(
+ name,
+ Self,
+ &.{},
+ void,
+ );
+ };
+ };
+
+ const Private = struct {
+ config: ?*Config,
+ pub var offset: c_int = 0;
+ };
+
+ pub fn new(config: *Config) *Self {
+ return gobject.ext.newInstance(Self, .{
+ .config = config,
+ });
+ }
+
+ fn init(self: *Self, _: *Class) callconv(.c) void {
+ gtk.Widget.initTemplate(self.as(gtk.Widget));
+ }
+
+ pub fn present(self: *Self, parent: ?*gtk.Widget) void {
+ self.as(Dialog).present(parent);
+ }
+
+ pub fn close(self: *Self) void {
+ self.as(Dialog).close();
+ }
+
+ fn response(
+ self: *Self,
+ response_id: [*:0]const u8,
+ ) callconv(.c) void {
+ if (std.mem.orderZ(u8, response_id, "reload") != .eq) return;
+ signals.@"reload-config".impl.emit(
+ self,
+ null,
+ .{},
+ null,
+ );
+ }
+
+ fn dispose(self: *Self) callconv(.c) void {
+ const priv = self.private();
+ if (priv.config) |v| {
+ v.unref();
+ priv.config = null;
+ }
+
+ gtk.Widget.disposeTemplate(
+ self.as(gtk.Widget),
+ getGObjectType(),
+ );
+
+ gobject.Object.virtual_methods.dispose.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ fn getConfig(self: *Self) ?*Config {
+ return self.private().config;
+ }
+
+ fn setConfig(self: *Self, config: ?*Config) void {
+ const priv = self.private();
+ if (priv.config) |old| old.unref();
+
+ // We don't need to increase the reference count because
+ // the property setter handles it (uses GValue.get vs. take)
+ priv.config = config;
+ }
+
+ const C = Common(Self, Private);
+ pub const as = C.as;
+ pub const ref = C.ref;
+ pub const unref = C.unref;
+ const private = C.private;
+
+ pub const Class = extern struct {
+ parent_class: Parent.Class,
+ var parent: *Parent.Class = undefined;
+ pub const Instance = Self;
+
+ fn init(class: *Class) callconv(.c) void {
+ gobject.ext.ensureType(Dialog);
+ gtk.Widget.Class.setTemplateFromResource(
+ class.as(gtk.Widget.Class),
+ comptime gresource.blueprint(.{
+ .major = 1,
+ .minor = 2,
+ .name = "config-errors-dialog",
+ }),
+ );
+
+ // Properties
+ gobject.ext.registerProperties(class, &.{
+ properties.config,
+ });
+
+ // Signals
+ signals.@"reload-config".impl.register(.{});
+
+ // Virtual methods
+ gobject.Object.virtual_methods.dispose.implement(class, &dispose);
+ Dialog.virtual_methods.response.implement(class, &response);
+ }
+
+ pub fn as(class: *Class, comptime T: type) *T {
+ return gobject.ext.as(T, class);
+ }
+ };
+};
diff --git a/vendor/libghostty-vt/src/apprt/gtk/class/debug_warning.zig b/vendor/libghostty-vt/src/apprt/gtk/class/debug_warning.zig
new file mode 100644
index 00000000..0ad32033
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/class/debug_warning.zig
@@ -0,0 +1,50 @@
+const adw = @import("adw");
+const gobject = @import("gobject");
+const gtk = @import("gtk");
+
+const adw_version = @import("../adw_version.zig");
+const gresource = @import("../build/gresource.zig");
+const Common = @import("../class.zig").Common;
+
+/// Debug warning banner. It will be based on adw.Banner if we're using Adwaita
+/// 1.3 or newer. Otherwise it will use a gtk.Label.
+pub const DebugWarning = extern struct {
+ const Self = @This();
+ parent_instance: Parent,
+ pub const Parent = if (adw_version.supportsBanner()) adw.Bin else gtk.Box;
+ pub const getGObjectType = gobject.ext.defineClass(Self, .{
+ .name = "GhosttyDebugWarning",
+ .instanceInit = &init,
+ .classInit = &Class.init,
+ .parent_class = &Class.parent,
+ });
+
+ fn init(self: *Self, _: *Class) callconv(.c) void {
+ gtk.Widget.initTemplate(self.as(gtk.Widget));
+ }
+
+ const C = Common(Self, null);
+ pub const as = C.as;
+ pub const ref = C.ref;
+ pub const unref = C.unref;
+ const private = C.private;
+
+ pub const Class = extern struct {
+ parent_class: Parent.Class,
+ var parent: *Parent.Class = undefined;
+ pub const Instance = Self;
+
+ fn init(class: *Class) callconv(.c) void {
+ gtk.Widget.Class.setTemplateFromResource(
+ class.as(gtk.Widget.Class),
+ comptime gresource.blueprint(.{
+ .major = 1,
+ .minor = if (adw_version.supportsBanner()) 3 else 2,
+ .name = "debug-warning",
+ }),
+ );
+ }
+
+ pub const as = C.Class.as;
+ };
+};
diff --git a/vendor/libghostty-vt/src/apprt/gtk/class/dialog.zig b/vendor/libghostty-vt/src/apprt/gtk/class/dialog.zig
new file mode 100644
index 00000000..5bc3cdfa
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/class/dialog.zig
@@ -0,0 +1,91 @@
+const std = @import("std");
+const adw = @import("adw");
+const gobject = @import("gobject");
+const gtk = @import("gtk");
+
+const adw_version = @import("../adw_version.zig");
+const Common = @import("../class.zig").Common;
+
+const log = std.log.scoped(.gtk_ghostty_dialog);
+
+/// Dialog is a simple abstraction over the `adw.AlertDialog` and
+/// `adw.MessageDialog` widgets, chosen at comptime based on the linked
+/// Adwaita version.
+///
+/// Once we drop support for Adwaita <= 1.2, this can be fully removed
+/// and we can use `adw.AlertDialog` directly.
+pub const Dialog = extern struct {
+ const Self = @This();
+ parent_instance: Parent,
+
+ pub const Parent = if (adw_version.supportsDialogs())
+ adw.AlertDialog
+ else
+ adw.MessageDialog;
+
+ pub const getGObjectType = gobject.ext.defineClass(Self, .{
+ .name = "GhosttyDialog",
+ .classInit = &Class.init,
+ .parent_class = &Class.parent,
+ });
+
+ pub const virtual_methods = struct {
+ /// Forwarded from parent so subclasses can reference this
+ /// directly. This will make it easier to remove Dialog in the future.
+ pub const response = Parent.virtual_methods.response;
+ };
+
+ pub fn present(self: *Self, parent: ?*gtk.Widget) void {
+ switch (Parent) {
+ adw.AlertDialog => self.as(adw.Dialog).present(parent),
+
+ adw.MessageDialog => {
+ // Reset the previous parent window
+ self.as(gtk.Window).setTransientFor(null);
+
+ // We need to get the window for the parent in order
+ // to set the transient-for property on the MessageDialog.
+ if (parent) |widget| parent: {
+ const root = gtk.Widget.getRoot(widget) orelse break :parent;
+ const window = gobject.ext.cast(
+ gtk.Window,
+ root,
+ ) orelse break :parent;
+ self.as(gtk.Window).setTransientFor(window);
+ }
+
+ self.as(gtk.Window).present();
+ },
+
+ else => comptime unreachable,
+ }
+ }
+
+ pub fn close(self: *Self) void {
+ switch (Parent) {
+ adw.AlertDialog => self.as(adw.Dialog).forceClose(),
+ adw.MessageDialog => self.as(gtk.Window).close(),
+ else => comptime unreachable,
+ }
+ }
+
+ const C = Common(Self, null);
+ pub const as = C.as;
+ pub const ref = C.ref;
+ pub const unref = C.unref;
+ const private = C.private;
+
+ pub const Class = extern struct {
+ parent_class: Parent.Class,
+ var parent: *Parent.Class = undefined;
+ pub const Instance = Self;
+
+ fn init(class: *Class) callconv(.c) void {
+ _ = class;
+ }
+
+ pub fn as(class: *Class, comptime T: type) *T {
+ return gobject.ext.as(T, class);
+ }
+ };
+};
diff --git a/vendor/libghostty-vt/src/apprt/gtk/class/global_shortcuts.zig b/vendor/libghostty-vt/src/apprt/gtk/class/global_shortcuts.zig
new file mode 100644
index 00000000..cf0f31a6
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/class/global_shortcuts.zig
@@ -0,0 +1,633 @@
+const std = @import("std");
+const assert = @import("../../../quirks.zig").inlineAssert;
+const Allocator = std.mem.Allocator;
+const gio = @import("gio");
+const glib = @import("glib");
+const gobject = @import("gobject");
+
+const Binding = @import("../../../input.zig").Binding;
+const key = @import("../key.zig");
+const Common = @import("../class.zig").Common;
+const Application = @import("application.zig").Application;
+const Config = @import("config.zig").Config;
+
+const log = std.log.scoped(.gtk_ghostty_global_shortcuts);
+
+pub const GlobalShortcuts = extern struct {
+ const Self = @This();
+ parent_instance: Parent,
+ pub const Parent = gobject.Object;
+ pub const getGObjectType = gobject.ext.defineClass(Self, .{
+ .name = "GhosttyGlobalShortcuts",
+ .instanceInit = &init,
+ .classInit = &Class.init,
+ .parent_class = &Class.parent,
+ .private = .{ .Type = Private, .offset = &Private.offset },
+ });
+
+ pub const properties = struct {
+ pub const config = struct {
+ pub const name = "config";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?*Config,
+ .{
+ .accessor = C.privateObjFieldAccessor("config"),
+ },
+ );
+ };
+
+ pub const @"dbus-connection" = struct {
+ pub const name = "dbus-connection";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?*gio.DBusConnection,
+ .{
+ .accessor = C.privateObjFieldAccessor("dbus_connection"),
+ },
+ );
+ };
+ };
+
+ const Private = struct {
+ /// The configuration that this is using.
+ config: ?*Config = null,
+
+ /// The dbus connection.
+ dbus_connection: ?*gio.DBusConnection = null,
+
+ /// An arena allocator that is present for each refresh.
+ arena: ?std.heap.ArenaAllocator = null,
+
+ /// A mapping from a unique ID to an action.
+ /// Currently the unique ID is simply the serialized representation of the
+ /// trigger that was used for the action as triggers are unique in the keymap,
+ /// but this may change in the future.
+ map: std.StringArrayHashMapUnmanaged(Binding.Action) = .{},
+
+ /// The handle of the current global shortcuts portal session,
+ /// as a D-Bus object path.
+ handle: ?[:0]const u8 = null,
+
+ /// The D-Bus signal subscription for the response signal on requests.
+ /// The ID is guaranteed to be non-zero, so we can use 0 to indicate null.
+ response_subscription: c_uint = 0,
+
+ /// The D-Bus signal subscription for the keybind activate signal.
+ /// The ID is guaranteed to be non-zero, so we can use 0 to indicate null.
+ activate_subscription: c_uint = 0,
+
+ pub var offset: c_int = 0;
+ };
+
+ pub const signals = struct {
+ /// Emitted whenever a global shortcut is triggered.
+ pub const trigger = struct {
+ pub const name = "trigger";
+ pub const connect = impl.connect;
+ const impl = gobject.ext.defineSignal(
+ name,
+ Self,
+ &.{*const Binding.Action},
+ void,
+ );
+ };
+ };
+
+ fn init(self: *Self, _: *Class) callconv(.c) void {
+ _ = gobject.Object.signals.notify.connect(
+ self,
+ *Self,
+ propConfig,
+ self,
+ .{ .detail = "config" },
+ );
+ }
+
+ fn close(self: *Self) void {
+ const priv = self.private();
+ const dbus = priv.dbus_connection orelse return;
+
+ if (priv.response_subscription != 0) {
+ dbus.signalUnsubscribe(priv.response_subscription);
+ priv.response_subscription = 0;
+ }
+
+ if (priv.activate_subscription != 0) {
+ dbus.signalUnsubscribe(priv.activate_subscription);
+ priv.activate_subscription = 0;
+ }
+
+ if (priv.handle) |handle| {
+ // Close existing session
+ dbus.call(
+ "org.freedesktop.portal.Desktop",
+ handle,
+ "org.freedesktop.portal.Session",
+ "Close",
+ null,
+ null,
+ .{},
+ -1,
+ null,
+ null,
+ null,
+ );
+ priv.handle = null;
+ }
+
+ if (priv.arena) |*arena| {
+ arena.deinit();
+ priv.arena = null;
+ priv.map = .{}; // Uses arena memory
+ }
+ }
+
+ fn refresh(self: *Self) Allocator.Error!void {
+ // Always close our previous state first.
+ self.close();
+
+ const priv = self.private();
+
+ // We need configuration to proceed.
+ const config = if (priv.config) |v| v.get() else return;
+
+ // Setup our new arena that we'll use for memory allocations.
+ assert(priv.arena == null);
+ var arena: std.heap.ArenaAllocator = .init(Application.default().allocator());
+ errdefer arena.deinit();
+ const alloc = arena.allocator();
+
+ // Our map starts out empty again. We don't need to worry about
+ // memory because its part of the arena we clear.
+ priv.map = .{};
+ errdefer priv.map = .{};
+
+ // Update map
+ var trigger_buf: [1024]u8 = undefined;
+ var it = config.keybind.set.bindings.iterator();
+ while (it.next()) |entry| {
+ const leaf: Binding.Set.GenericLeaf = switch (entry.value_ptr.*) {
+ .leader => continue,
+ inline .leaf, .leaf_chained => |leaf| leaf.generic(),
+ };
+ if (!leaf.flags.global) continue;
+
+ // We only allow global keybinds that map to exactly a single
+ // action for now. TODO: remove this restriction
+ const actions = leaf.actionsSlice();
+ if (actions.len != 1) continue;
+
+ const trigger = if (key.xdgShortcutFromTrigger(
+ &trigger_buf,
+ entry.key_ptr.*,
+ )) |shortcut_|
+ shortcut_ orelse continue
+ else |err| switch (err) {
+ // If there isn't space to translate the trigger, then our
+ // buffer might be too small (but 1024 is insane!). In any case
+ // we don't want to stop registering globals.
+ error.WriteFailed => {
+ log.warn(
+ "buffer too small to translate trigger, ignoring={f}",
+ .{entry.key_ptr.*},
+ );
+ continue;
+ },
+ };
+
+ try priv.map.put(
+ alloc,
+ try alloc.dupeZ(u8, trigger),
+ actions[0],
+ );
+ }
+
+ // Store our arena
+ priv.arena = arena;
+
+ // Create our session if we have global shortcuts.
+ if (priv.map.count() > 0) try self.request(.create_session);
+ }
+
+ const Method = enum {
+ create_session,
+ bind_shortcuts,
+
+ fn name(self: Method) [:0]const u8 {
+ return switch (self) {
+ .create_session => "CreateSession",
+ .bind_shortcuts => "BindShortcuts",
+ };
+ }
+
+ /// Construct the payload expected by the XDG portal call.
+ fn makePayload(
+ self: Method,
+ shortcuts: *GlobalShortcuts,
+ request_token: [:0]const u8,
+ ) ?*glib.Variant {
+ switch (self) {
+ // See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-createsession
+ .create_session => {
+ var session_token: Token = undefined;
+ return glib.Variant.newParsed(
+ "({'handle_token': <%s>, 'session_handle_token': <%s>},)",
+ request_token.ptr,
+ generateToken(&session_token).ptr,
+ );
+ },
+
+ // See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-bindshortcuts
+ .bind_shortcuts => {
+ const priv = shortcuts.private();
+ const handle = priv.handle orelse return null;
+
+ const bind_type = glib.VariantType.new("a(sa{sv})");
+ defer glib.free(bind_type);
+
+ var binds: glib.VariantBuilder = undefined;
+ glib.VariantBuilder.init(&binds, bind_type);
+
+ var action_buf: [256]u8 = undefined;
+
+ var it = priv.map.iterator();
+ while (it.next()) |entry| {
+ const trigger = entry.key_ptr.*.ptr;
+ const action = std.fmt.bufPrintZ(
+ &action_buf,
+ "{f}",
+ .{entry.value_ptr.*},
+ ) catch continue;
+
+ binds.addParsed(
+ "(%s, {'description': <%s>, 'preferred_trigger': <%s>})",
+ trigger,
+ action.ptr,
+ trigger,
+ );
+ }
+
+ return glib.Variant.newParsed(
+ "(%o, %*, '', {'handle_token': <%s>})",
+ handle.ptr,
+ binds.end(),
+ request_token.ptr,
+ );
+ },
+ }
+ }
+
+ fn onResponse(self: Method, shortcuts: *GlobalShortcuts, vardict: *glib.Variant) void {
+ switch (self) {
+ .create_session => {
+ var handle: ?[*:0]u8 = null;
+ if (vardict.lookup("session_handle", "&s", &handle) == 0) {
+ log.warn(
+ "session handle not found in response={s}",
+ .{vardict.print(@intFromBool(true))},
+ );
+ return;
+ }
+
+ const priv = shortcuts.private();
+ const dbus = priv.dbus_connection.?;
+ const alloc = priv.arena.?.allocator();
+ priv.handle = alloc.dupeZ(u8, std.mem.span(handle.?)) catch {
+ log.warn("out of memory: failed to clone session handle", .{});
+ return;
+ };
+ log.debug("session_handle={?s}", .{handle});
+
+ // Subscribe to keybind activations
+ assert(priv.activate_subscription == 0);
+ priv.activate_subscription = dbus.signalSubscribe(
+ null,
+ "org.freedesktop.portal.GlobalShortcuts",
+ "Activated",
+ "/org/freedesktop/portal/desktop",
+ handle,
+ .{ .match_arg0_path = true },
+ shortcutActivated,
+ shortcuts,
+ null,
+ );
+
+ shortcuts.request(.bind_shortcuts) catch |err| {
+ log.warn("failed to bind shortcuts={}", .{err});
+ return;
+ };
+ },
+ .bind_shortcuts => {},
+ }
+ }
+ };
+
+ /// Submit a request to the global shortcuts portal.
+ fn request(
+ self: *Self,
+ comptime method: Method,
+ ) Allocator.Error!void {
+ // NOTE(pluiedev):
+ // XDG Portals are really, really poorly-designed pieces of hot garbage.
+ // How the protocol is _initially_ designed to work is as follows:
+ //
+ // 1. The client calls a method which returns the path of a Request object;
+ // 2. The client waits for the Response signal under said object path;
+ // 3. When the signal arrives, the actual return value and status code
+ // become available for the client for further processing.
+ //
+ // THIS DOES NOT WORK. Once the first two steps are complete, the client
+ // needs to immediately start listening for the third step, but an overeager
+ // server implementation could easily send the Response signal before the
+ // client is even ready, causing communications to break down over a simple
+ // race condition/two generals' problem that even _TCP_ had figured out
+ // decades ago. Worse yet, you get exactly _one_ chance to listen for the
+ // signal, or else your communication attempt so far has all been in vain.
+ //
+ // And they know this. Instead of fixing their freaking protocol, they just
+ // ask clients to manually construct the expected object path and subscribe
+ // to the request signal beforehand, making the whole response value of
+ // the original call COMPLETELY MEANINGLESS.
+ //
+ // Furthermore, this is _entirely undocumented_ aside from one tiny
+ // paragraph under the documentation for the Request interface, and
+ // anyone would be forgiven for missing it without reading the libportal
+ // source code.
+ //
+ // When in Rome, do as the Romans do, I guess...?
+
+ const callbacks = struct {
+ fn gotResponseHandle(
+ source: ?*gobject.Object,
+ res: *gio.AsyncResult,
+ _: ?*anyopaque,
+ ) callconv(.c) void {
+ const dbus_ = gobject.ext.cast(gio.DBusConnection, source.?).?;
+
+ var err: ?*glib.Error = null;
+ defer if (err) |err_| err_.free();
+
+ const params_ = dbus_.callFinish(res, &err) orelse {
+ if (err) |err_| log.warn("request failed={s} ({})", .{
+ err_.f_message orelse "(unknown)",
+ err_.f_code,
+ });
+ return;
+ };
+ defer params_.unref();
+
+ // TODO: XDG recommends updating the signal subscription if the actual
+ // returned request path is not the same as the expected request
+ // path, to retain compatibility with older versions of XDG portals.
+ // Although it suffers from the race condition outlined above,
+ // we should still implement this at some point.
+ }
+
+ // See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html#org-freedesktop-portal-request-response
+ fn responded(
+ dbus: *gio.DBusConnection,
+ _: ?[*:0]const u8,
+ _: [*:0]const u8,
+ _: [*:0]const u8,
+ _: [*:0]const u8,
+ params_: *glib.Variant,
+ ud: ?*anyopaque,
+ ) callconv(.c) void {
+ const self_cb: *GlobalShortcuts = @ptrCast(@alignCast(ud));
+ const priv = self_cb.private();
+
+ // Unsubscribe from the response signal
+ if (priv.response_subscription != 0) {
+ dbus.signalUnsubscribe(priv.response_subscription);
+ priv.response_subscription = 0;
+ }
+
+ var response: u32 = 0;
+ var vardict: ?*glib.Variant = null;
+ defer if (vardict) |v| v.unref();
+ params_.get("(u@a{sv})", &response, &vardict);
+
+ switch (response) {
+ 0 => {
+ log.debug("request successful", .{});
+ method.onResponse(self_cb, vardict.?);
+ },
+ 1 => log.debug("request was cancelled by user", .{}),
+ 2 => log.warn("request ended unexpectedly", .{}),
+ else => log.warn("unrecognized response code={}", .{response}),
+ }
+ }
+ };
+
+ var request_token_buf: Token = undefined;
+ const request_token = generateToken(&request_token_buf);
+
+ const payload = method.makePayload(self, request_token) orelse return;
+ const request_path = try self.getRequestPath(request_token);
+
+ const priv = self.private();
+ const dbus = priv.dbus_connection.?;
+
+ assert(priv.response_subscription == 0);
+ priv.response_subscription = dbus.signalSubscribe(
+ null,
+ "org.freedesktop.portal.Request",
+ "Response",
+ request_path,
+ null,
+ .{},
+ callbacks.responded,
+ self,
+ null,
+ );
+
+ dbus.call(
+ "org.freedesktop.portal.Desktop",
+ "/org/freedesktop/portal/desktop",
+ "org.freedesktop.portal.GlobalShortcuts",
+ method.name(),
+ payload,
+ null,
+ .{},
+ -1,
+ null,
+ callbacks.gotResponseHandle,
+ null,
+ );
+ }
+
+ /// Get the XDG portal request path for the current Ghostty instance.
+ ///
+ /// If this sounds like nonsense, see `request` for an explanation as to
+ /// why we need to do this.
+ ///
+ /// Precondition: dbus connection exists, arena setup
+ fn getRequestPath(self: *Self, token: [:0]const u8) Allocator.Error![:0]const u8 {
+ const priv = self.private();
+ const dbus = priv.dbus_connection.?;
+ const alloc = priv.arena.?.allocator();
+
+ // See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html
+ // for the syntax XDG portals expect.
+
+ // `getUniqueName` should never return null here as we're using an ordinary
+ // message bus connection. If it doesn't, something is very wrong
+ const unique_name = std.mem.span(dbus.getUniqueName().?);
+
+ const object_path = try std.mem.joinZ(
+ alloc,
+ "/",
+ &.{
+ "/org/freedesktop/portal/desktop/request",
+ unique_name[1..], // Remove leading `:`
+ token,
+ },
+ );
+
+ // Sanitize the unique name by replacing every `.` with `_`.
+ // In effect, this will turn a unique name like `:1.192` into `1_192`.
+ // Valid D-Bus object path components never contain `.`s anyway, so we're
+ // free to replace all instances of `.` here and avoid extra allocation.
+ std.mem.replaceScalar(u8, object_path, '.', '_');
+
+ return object_path;
+ }
+
+ //---------------------------------------------------------------
+ // Property Handlers
+
+ pub fn setDbusConnection(
+ self: *Self,
+ dbus_connection: ?*gio.DBusConnection,
+ ) void {
+ const priv = self.private();
+
+ // If we have a prior dbus connection we need to close our prior
+ // registrations first.
+ if (priv.dbus_connection) |v| {
+ self.close();
+ v.unref();
+ priv.dbus_connection = null;
+ }
+
+ priv.dbus_connection = null;
+ if (dbus_connection) |v| {
+ v.ref(); // Weird this doesn't return self
+ priv.dbus_connection = v;
+ self.refresh() catch |err| {
+ log.warn("error refreshing global shortcuts: {}", .{err});
+ };
+ }
+
+ self.as(gobject.Object).notifyByPspec(properties.@"dbus-connection".impl.param_spec);
+ }
+
+ fn propConfig(
+ _: *Self,
+ _: *gobject.ParamSpec,
+ self: *Self,
+ ) callconv(.c) void {
+ self.refresh() catch |err| {
+ log.warn("error refreshing global shortcuts: {}", .{err});
+ };
+ }
+
+ //---------------------------------------------------------------
+ // Signal Handlers
+
+ fn shortcutActivated(
+ _: *gio.DBusConnection,
+ _: ?[*:0]const u8,
+ _: [*:0]const u8,
+ _: [*:0]const u8,
+ _: [*:0]const u8,
+ params: *glib.Variant,
+ ud: ?*anyopaque,
+ ) callconv(.c) void {
+ const self: *Self = @ptrCast(@alignCast(ud));
+
+ // 2nd value in the tuple is the activated shortcut ID
+ // See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-activated
+ var shortcut_id: [*:0]const u8 = undefined;
+ params.getChild(1, "&s", &shortcut_id);
+ log.debug("activated={s}", .{shortcut_id});
+
+ const action = self.private().map.get(std.mem.span(shortcut_id)) orelse return;
+ signals.trigger.impl.emit(
+ self,
+ null,
+ .{&action},
+ null,
+ );
+ }
+
+ //---------------------------------------------------------------
+ // Virtual methods
+
+ fn dispose(self: *Self) callconv(.c) void {
+ // Since we drop references here we may lose access to things like
+ // dbus connections, so we need to close all our connections right
+ // away instead of in finalize.
+ self.close();
+
+ const priv = self.private();
+ if (priv.config) |v| {
+ v.unref();
+ priv.config = null;
+ }
+ if (priv.dbus_connection) |v| {
+ v.unref();
+ priv.dbus_connection = null;
+ }
+
+ gobject.Object.virtual_methods.dispose.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ const C = Common(Self, Private);
+ pub const as = C.as;
+ pub const ref = C.ref;
+ pub const unref = C.unref;
+ const private = C.private;
+
+ pub const Class = extern struct {
+ parent_class: Parent.Class,
+ var parent: *Parent.Class = undefined;
+ pub const Instance = Self;
+
+ fn init(class: *Class) callconv(.c) void {
+ // Properties
+ gobject.ext.registerProperties(class, &.{
+ properties.config.impl,
+ properties.@"dbus-connection".impl,
+ });
+
+ // Signals
+ signals.trigger.impl.register(.{});
+
+ // Virtual methods
+ gobject.Object.virtual_methods.dispose.implement(class, &dispose);
+ }
+
+ pub const as = C.Class.as;
+ pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
+ };
+};
+
+const Token = [16]u8;
+
+/// Generate a random token suitable for use in requests.
+fn generateToken(buf: *Token) [:0]const u8 {
+ // u28 takes up 7 bytes in hex, 8 bytes for "ghostty_" and 1 byte for NUL
+ // 7 + 8 + 1 = 16
+ return std.fmt.bufPrintZ(
+ buf,
+ "ghostty_{x:0<7}",
+ .{std.crypto.random.int(u28)},
+ ) catch unreachable;
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/class/imgui_widget.zig b/vendor/libghostty-vt/src/apprt/gtk/class/imgui_widget.zig
new file mode 100644
index 00000000..0ef753a8
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/class/imgui_widget.zig
@@ -0,0 +1,551 @@
+const std = @import("std");
+const assert = @import("../../../quirks.zig").inlineAssert;
+
+const cimgui = @import("dcimgui");
+const gl = @import("opengl");
+const adw = @import("adw");
+const gdk = @import("gdk");
+const gobject = @import("gobject");
+const gtk = @import("gtk");
+
+const input = @import("../../../input.zig");
+const gresource = @import("../build/gresource.zig");
+
+const key = @import("../key.zig");
+const Common = @import("../class.zig").Common;
+
+const log = std.log.scoped(.gtk_ghostty_imgui_widget);
+
+/// A widget for embedding a Dear ImGui application.
+///
+/// It'd be a lot cleaner to use inheritance here but zig-gobject doesn't
+/// currently have a way to define virtual methods, so we have to use
+/// composition and signals instead.
+pub const ImguiWidget = extern struct {
+ const Self = @This();
+ parent_instance: Parent,
+ pub const Parent = adw.Bin;
+ pub const getGObjectType = gobject.ext.defineClass(Self, .{
+ .name = "GhosttyImguiWidget",
+ .instanceInit = &init,
+ .classInit = &Class.init,
+ .parent_class = &Class.parent,
+ .private = .{ .Type = Private, .offset = &Private.offset },
+ });
+
+ pub const properties = struct {};
+
+ pub const signals = struct {};
+
+ pub const virtual_methods = struct {
+ /// This virtual method will be called to allow the Dear ImGui
+ /// application to do one-time setup of the context. The correct context
+ /// will be current when the virtual method is called.
+ pub const setup = C.defineVirtualMethod("setup");
+
+ /// This virtual method will be called at each frame to allow the Dear
+ /// ImGui application to draw the application. The correct context will
+ /// be current when the virtual method is called.
+ pub const render = C.defineVirtualMethod("render");
+ };
+
+ const Private = struct {
+ /// GL area where we display the Dear ImGui application.
+ gl_area: *gtk.GLArea,
+
+ /// GTK input method context
+ im_context: *gtk.IMMulticontext,
+
+ /// Dear ImGui context. We create a context per widget so that we can
+ /// have multiple active imgui views in the same application.
+ ig_context: ?*cimgui.c.ImGuiContext = null,
+
+ /// Our previous instant used to calculate delta time for animations.
+ instant: ?std.time.Instant = null,
+
+ /// Tick callback ID for timed updates.
+ tick_callback_id: c_uint = 0,
+
+ /// Last render time for throttling to 30 FPS.
+ last_render_time: ?std.time.Instant = null,
+
+ pub var offset: c_int = 0;
+ };
+
+ //---------------------------------------------------------------
+ // Virtual Methods
+
+ fn init(self: *Self, _: *Class) callconv(.c) void {
+ gtk.Widget.initTemplate(self.as(gtk.Widget));
+ }
+
+ fn dispose(self: *Self) callconv(.c) void {
+ gtk.Widget.disposeTemplate(
+ self.as(gtk.Widget),
+ getGObjectType(),
+ );
+
+ gobject.Object.virtual_methods.dispose.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ //---------------------------------------------------------------
+ // Public methods
+
+ /// This should be called anytime the underlying data for the UI changes
+ /// so that the UI can be refreshed.
+ pub fn queueRender(self: *ImguiWidget) void {
+ const priv = self.private();
+ priv.gl_area.queueRender();
+ }
+
+ //---------------------------------------------------------------
+ // Public wrappers for virtual methods
+
+ /// This virtual method will be called to allow the Dear ImGui application
+ /// to do one-time setup of the context. The correct context will be current
+ /// when the virtual method is called.
+ pub fn setup(self: *Self) callconv(.c) void {
+ const class = self.getClass() orelse return;
+ virtual_methods.setup.call(class, self, .{});
+ }
+
+ /// This virtual method will be called at each frame to allow the Dear ImGui
+ /// application to draw the application. The correct context will be current
+ /// when the virtual method is called.
+ pub fn render(self: *Self) callconv(.c) void {
+ const class = self.getClass() orelse return;
+ virtual_methods.render.call(class, self, .{});
+ }
+
+ //---------------------------------------------------------------
+ // Private Methods
+
+ /// Set our imgui context to be current, or return an error. This must be
+ /// called before any Dear ImGui API calls so that they're made against
+ /// the proper context.
+ fn setCurrentContext(self: *Self) error{ContextNotInitialized}!void {
+ const priv = self.private();
+ const ig_context = priv.ig_context orelse {
+ log.warn("Dear ImGui context not initialized", .{});
+ return error.ContextNotInitialized;
+ };
+ cimgui.c.ImGui_SetCurrentContext(ig_context);
+ }
+
+ /// Initialize the frame. Expects that the context is already current.
+ fn newFrame(self: *Self) void {
+ const priv = self.private();
+ const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO();
+
+ // Determine our delta time
+ const now = std.time.Instant.now() catch unreachable;
+ io.DeltaTime = if (priv.instant) |prev| delta: {
+ const since_ns: f64 = @floatFromInt(now.since(prev));
+ const ns_per_s: f64 = @floatFromInt(std.time.ns_per_s);
+ const since_s: f32 = @floatCast(since_ns / ns_per_s);
+ break :delta @max(0.00001, since_s);
+ } else (1.0 / 60.0);
+
+ priv.instant = now;
+ }
+
+ /// Handle key press/release events.
+ fn keyEvent(
+ self: *ImguiWidget,
+ action: input.Action,
+ ec_key: *gtk.EventControllerKey,
+ keyval: c_uint,
+ _: c_uint,
+ gtk_mods: gdk.ModifierType,
+ ) bool {
+ self.queueRender();
+
+ self.setCurrentContext() catch return false;
+
+ const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO();
+
+ const mods = key.translateMods(gtk_mods);
+ 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);
+
+ // If our keyval has a key, then we send that key event
+ if (key.keyFromKeyval(keyval)) |inputkey| {
+ if (inputkey.imguiKey()) |imgui_key| {
+ cimgui.c.ImGuiIO_AddKeyEvent(io, imgui_key, action == .press);
+ }
+ }
+
+ // Try to process the event as text
+ if (ec_key.as(gtk.EventController).getCurrentEvent()) |event| {
+ const priv = self.private();
+ _ = priv.im_context.as(gtk.IMContext).filterKeypress(event);
+ }
+
+ return true;
+ }
+
+ /// Translate a GTK mouse button to a Dear ImGui mouse button.
+ fn translateMouseButton(button: c_uint) ?c_int {
+ return switch (button) {
+ 1 => cimgui.c.ImGuiMouseButton_Left,
+ 2 => cimgui.c.ImGuiMouseButton_Middle,
+ 3 => cimgui.c.ImGuiMouseButton_Right,
+ else => null,
+ };
+ }
+
+ /// Get the scale factor that the display is operating at.
+ fn getScaleFactor(self: *Self) f64 {
+ const priv = self.private();
+ return @floatFromInt(priv.gl_area.as(gtk.Widget).getScaleFactor());
+ }
+
+ //---------------------------------------------------------------
+ // Properties
+
+ //---------------------------------------------------------------
+ // Signal Handlers
+
+ fn glAreaRealize(_: *gtk.GLArea, self: *Self) callconv(.c) void {
+ const priv = self.private();
+ assert(priv.ig_context == null);
+
+ priv.gl_area.makeCurrent();
+ if (priv.gl_area.getError()) |err| {
+ log.warn("GLArea for Dear ImGui widget failed to realize: {s}", .{err.f_message orelse "(unknown)"});
+ return;
+ }
+
+ priv.ig_context = cimgui.c.ImGui_CreateContext(null) orelse {
+ log.warn("unable to initialize Dear ImGui context", .{});
+ return;
+ };
+ self.setCurrentContext() catch return;
+
+ // Setup some basic config
+ const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO();
+ io.BackendPlatformName = "ghostty_gtk";
+
+ // Realize means that our OpenGL context is ready, so we can now
+ // initialize the ImgUI OpenGL backend for our context.
+ _ = cimgui.ImGui_ImplOpenGL3_Init(null);
+
+ // Call the virtual method to setup the UI.
+ self.setup();
+
+ // Add a tick callback to drive timed updates via the frame clock.
+ priv.tick_callback_id = self.as(gtk.Widget).addTickCallback(
+ tickCallback,
+ null,
+ null,
+ );
+ }
+
+ /// Handle a request to unrealize the GLArea
+ fn glAreaUnrealize(_: *gtk.GLArea, self: *ImguiWidget) callconv(.c) void {
+ const priv = self.private();
+ assert(priv.ig_context != null);
+
+ // Remove the tick callback if it was registered.
+ if (priv.tick_callback_id != 0) {
+ self.as(gtk.Widget).removeTickCallback(priv.tick_callback_id);
+ priv.tick_callback_id = 0;
+ }
+
+ // Unrealize is not guaranteed to be called with a current GL context,
+ // so we make it current for ImGui cleanup.
+ priv.gl_area.makeCurrent();
+ if (priv.gl_area.getError()) |err| {
+ log.warn("GLArea for Dear ImGui widget failed to realize: {s}", .{err.f_message orelse "(unknown)"});
+ return;
+ }
+
+ self.setCurrentContext() catch return;
+ cimgui.ImGui_ImplOpenGL3_ShutdownWithLoaderCleanup();
+ cimgui.c.ImGui_DestroyContext(priv.ig_context);
+ priv.ig_context = null;
+ }
+
+ /// Handle a request to resize the GLArea
+ fn glAreaResize(area: *gtk.GLArea, width: c_int, height: c_int, self: *Self) callconv(.c) void {
+ self.setCurrentContext() catch return;
+ const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO();
+ const scale_factor = area.as(gtk.Widget).getScaleFactor();
+
+ // Our display size is always unscaled. We'll do the scaling in the
+ // style instead. This creates crisper looking fonts.
+ io.DisplaySize = .{ .x = @floatFromInt(width), .y = @floatFromInt(height) };
+ io.DisplayFramebufferScale = .{ .x = 1, .y = 1 };
+
+ // Setup a new style and scale it appropriately. We must use the
+ // ImGuiStyle constructor to get proper default values (e.g.,
+ // CurveTessellationTol) rather than zero-initialized values.
+ var style: cimgui.c.ImGuiStyle = undefined;
+ cimgui.ext.ImGuiStyle_ImGuiStyle(&style);
+ cimgui.c.ImGuiStyle_ScaleAllSizes(&style, @floatFromInt(scale_factor));
+ const active_style = cimgui.c.ImGui_GetStyle();
+ active_style.* = style;
+ }
+
+ /// Handle a request to render the contents of our GLArea
+ fn glAreaRender(_: *gtk.GLArea, _: *gdk.GLContext, self: *Self) callconv(.c) c_int {
+ self.setCurrentContext() catch return @intFromBool(false);
+
+ // Update last render time for tick callback throttling.
+ const priv = self.private();
+ priv.last_render_time = std.time.Instant.now() catch null;
+
+ // Setup our frame. We render twice because some ImGui behaviors
+ // take multiple renders to process. I don't know how to make this
+ // more efficient.
+ for (0..2) |_| {
+ cimgui.ImGui_ImplOpenGL3_NewFrame();
+ self.newFrame();
+ cimgui.c.ImGui_NewFrame();
+
+ // Call the virtual method to draw the UI.
+ self.render();
+
+ // Render
+ cimgui.c.ImGui_Render();
+ }
+
+ // OpenGL final render
+ gl.clearColor(0x28 / 0xFF, 0x2C / 0xFF, 0x34 / 0xFF, 1.0);
+ gl.clear(gl.c.GL_COLOR_BUFFER_BIT);
+ cimgui.ImGui_ImplOpenGL3_RenderDrawData(cimgui.c.ImGui_GetDrawData());
+
+ return @intFromBool(true);
+ }
+
+ fn ecFocusEnter(_: *gtk.EventControllerFocus, self: *Self) callconv(.c) void {
+ self.setCurrentContext() catch return;
+ const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO();
+ cimgui.c.ImGuiIO_AddFocusEvent(io, true);
+ self.queueRender();
+ }
+
+ fn ecFocusLeave(_: *gtk.EventControllerFocus, self: *Self) callconv(.c) void {
+ self.setCurrentContext() catch return;
+ const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO();
+ cimgui.c.ImGuiIO_AddFocusEvent(io, false);
+ self.queueRender();
+ }
+
+ fn ecKeyPressed(
+ ec_key: *gtk.EventControllerKey,
+ keyval: c_uint,
+ keycode: c_uint,
+ gtk_mods: gdk.ModifierType,
+ self: *ImguiWidget,
+ ) callconv(.c) c_int {
+ return @intFromBool(self.keyEvent(
+ .press,
+ ec_key,
+ keyval,
+ keycode,
+ gtk_mods,
+ ));
+ }
+
+ fn ecKeyReleased(
+ ec_key: *gtk.EventControllerKey,
+ keyval: c_uint,
+ keycode: c_uint,
+ gtk_mods: gdk.ModifierType,
+ self: *ImguiWidget,
+ ) callconv(.c) void {
+ _ = self.keyEvent(
+ .release,
+ ec_key,
+ keyval,
+ keycode,
+ gtk_mods,
+ );
+ }
+
+ fn ecMousePressed(
+ gesture: *gtk.GestureClick,
+ _: c_int,
+ _: f64,
+ _: f64,
+ self: *ImguiWidget,
+ ) callconv(.c) void {
+ self.queueRender();
+ self.setCurrentContext() catch return;
+ const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO();
+ const gdk_button = gesture.as(gtk.GestureSingle).getCurrentButton();
+ if (translateMouseButton(gdk_button)) |button| {
+ cimgui.c.ImGuiIO_AddMouseButtonEvent(io, button, true);
+ }
+ }
+
+ fn ecMouseReleased(
+ gesture: *gtk.GestureClick,
+ _: c_int,
+ _: f64,
+ _: f64,
+ self: *ImguiWidget,
+ ) callconv(.c) void {
+ self.queueRender();
+ self.setCurrentContext() catch return;
+ const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO();
+ const gdk_button = gesture.as(gtk.GestureSingle).getCurrentButton();
+ if (translateMouseButton(gdk_button)) |button| {
+ cimgui.c.ImGuiIO_AddMouseButtonEvent(io, button, false);
+ }
+ }
+
+ fn ecMouseMotion(
+ _: *gtk.EventControllerMotion,
+ x: f64,
+ y: f64,
+ self: *ImguiWidget,
+ ) callconv(.c) void {
+ self.queueRender();
+ self.setCurrentContext() catch return;
+ const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO();
+ const scale_factor = self.getScaleFactor();
+ cimgui.c.ImGuiIO_AddMousePosEvent(
+ io,
+ @floatCast(x * scale_factor),
+ @floatCast(y * scale_factor),
+ );
+ }
+
+ fn ecMouseScroll(
+ _: *gtk.EventControllerScroll,
+ x: f64,
+ y: f64,
+ self: *ImguiWidget,
+ ) callconv(.c) c_int {
+ self.queueRender();
+ self.setCurrentContext() catch return @intFromBool(false);
+ const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO();
+ cimgui.c.ImGuiIO_AddMouseWheelEvent(
+ io,
+ @floatCast(x),
+ @floatCast(-y),
+ );
+ return @intFromBool(true);
+ }
+
+ fn imCommit(
+ _: *gtk.IMMulticontext,
+ bytes: [*:0]u8,
+ self: *ImguiWidget,
+ ) callconv(.c) void {
+ self.queueRender();
+ self.setCurrentContext() catch return;
+ const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO();
+ cimgui.c.ImGuiIO_AddInputCharactersUTF8(io, bytes);
+ }
+
+ /// Tick callback for timed updates. This drives periodic redraws.
+ /// Redraws are limited to 30 FPS max since our imgui widgets don't
+ /// usually need higher frame rates than that.
+ fn tickCallback(
+ widget: *gtk.Widget,
+ _: *gdk.FrameClock,
+ _: ?*anyopaque,
+ ) callconv(.c) c_int {
+ const self: *Self = gobject.ext.cast(Self, widget) orelse return 0;
+ const priv = self.private();
+
+ const now = std.time.Instant.now() catch {
+ self.queueRender();
+ return 1;
+ };
+
+ // Throttle to 30 FPS (~33ms between frames)
+ const frame_time_ns: u64 = std.time.ns_per_s / 30;
+ const should_render = if (priv.last_render_time) |last|
+ now.since(last) >= frame_time_ns
+ else
+ true;
+
+ if (should_render) self.queueRender();
+
+ return 1; // Continue the tick callback
+ }
+
+ //---------------------------------------------------------------
+ // Default virtual method handlers
+
+ /// Default setup function. Does nothing but log a warning.
+ fn defaultSetup(_: *Self) callconv(.c) void {
+ log.warn("default Dear ImGui setup called, this is a bug.", .{});
+ }
+
+ /// Default render function. Does nothing but log a warning.
+ fn defaultRender(_: *Self) callconv(.c) void {
+ log.warn("default Dear ImGui render called, this is a bug.", .{});
+ }
+
+ const C = Common(Self, Private);
+ pub const as = C.as;
+ pub const ref = C.ref;
+ pub const refSink = C.refSink;
+ pub const unref = C.unref;
+ pub const getClass = C.getClass;
+ const private = C.private;
+
+ pub const Class = extern struct {
+ parent_class: Parent.Class,
+
+ /// Function pointers for virtual methods.
+ setup: ?*const fn (*Self) callconv(.c) void,
+ render: ?*const fn (*Self) callconv(.c) void,
+
+ var parent: *Parent.Class = undefined;
+ pub const Instance = Self;
+
+ fn init(class: *Class) callconv(.c) void {
+ gtk.Widget.Class.setTemplateFromResource(
+ class.as(gtk.Widget.Class),
+ comptime gresource.blueprint(.{
+ .major = 1,
+ .minor = 5,
+ .name = "imgui-widget",
+ }),
+ );
+
+ // Initialize our virtual methods with default functions.
+ class.setup = defaultSetup;
+ class.render = defaultRender;
+
+ // Bindings
+ class.bindTemplateChildPrivate("gl_area", .{});
+ class.bindTemplateChildPrivate("im_context", .{});
+
+ // Template Callbacks
+ class.bindTemplateCallback("realize", &glAreaRealize);
+ class.bindTemplateCallback("unrealize", &glAreaUnrealize);
+ class.bindTemplateCallback("resize", &glAreaResize);
+ class.bindTemplateCallback("render", &glAreaRender);
+ class.bindTemplateCallback("focus_enter", &ecFocusEnter);
+ class.bindTemplateCallback("focus_leave", &ecFocusLeave);
+ class.bindTemplateCallback("key_pressed", &ecKeyPressed);
+ class.bindTemplateCallback("key_released", &ecKeyReleased);
+ class.bindTemplateCallback("mouse_pressed", &ecMousePressed);
+ class.bindTemplateCallback("mouse_released", &ecMouseReleased);
+ class.bindTemplateCallback("mouse_motion", &ecMouseMotion);
+ class.bindTemplateCallback("scroll", &ecMouseScroll);
+ class.bindTemplateCallback("im_commit", &imCommit);
+
+ // Signals
+
+ // Virtual methods
+ gobject.Object.virtual_methods.dispose.implement(class, &dispose);
+ }
+
+ pub const as = C.Class.as;
+ pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
+ pub const bindTemplateCallback = C.Class.bindTemplateCallback;
+ };
+};
diff --git a/vendor/libghostty-vt/src/apprt/gtk/class/inspector_widget.zig b/vendor/libghostty-vt/src/apprt/gtk/class/inspector_widget.zig
new file mode 100644
index 00000000..80ac7fc3
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/class/inspector_widget.zig
@@ -0,0 +1,245 @@
+const std = @import("std");
+
+const gobject = @import("gobject");
+const gtk = @import("gtk");
+
+const gresource = @import("../build/gresource.zig");
+const Inspector = @import("../../../inspector/Inspector.zig");
+
+const Common = @import("../class.zig").Common;
+const Surface = @import("surface.zig").Surface;
+const ImguiWidget = @import("imgui_widget.zig").ImguiWidget;
+
+const log = std.log.scoped(.gtk_ghostty_inspector_widget);
+
+/// Widget for displaying the Ghostty inspector.
+pub const InspectorWidget = extern struct {
+ const Self = @This();
+ parent_instance: Parent,
+ pub const Parent = ImguiWidget;
+ pub const getGObjectType = gobject.ext.defineClass(Self, .{
+ .name = "GhosttyInspectorWidget",
+ .instanceInit = &init,
+ .classInit = &Class.init,
+ .parent_class = &Class.parent,
+ .private = .{ .Type = Private, .offset = &Private.offset },
+ });
+
+ pub const properties = struct {
+ pub const surface = struct {
+ pub const name = "surface";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?*Surface,
+ .{
+ .accessor = .{
+ .getter = getSurfaceValue,
+ .setter = setSurfaceValue,
+ },
+ },
+ );
+ };
+ };
+
+ pub const signals = struct {};
+
+ const Private = struct {
+ /// The surface that we are attached to. This is NOT referenced.
+ /// We attach a weak notify to the object.
+ surface: ?*Surface = null,
+
+ pub var offset: c_int = 0;
+ };
+
+ //---------------------------------------------------------------
+ // Virtual Methods
+
+ fn init(self: *Self, _: *Class) callconv(.c) void {
+ gtk.Widget.initTemplate(self.as(gtk.Widget));
+ }
+
+ fn dispose(self: *Self) callconv(.c) void {
+ // Clear our surface so it deactivates the inspector.
+ self.setSurface(null);
+
+ gtk.Widget.disposeTemplate(
+ self.as(gtk.Widget),
+ getGObjectType(),
+ );
+
+ gobject.Object.virtual_methods.dispose.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ /// Called to do initial setup of the UI.
+ fn imguiSetup(
+ _: *Self,
+ ) callconv(.c) void {
+ Inspector.setup();
+ }
+
+ /// Called for every frame to draw the UI.
+ fn imguiRender(
+ self: *Self,
+ ) callconv(.c) void {
+ const priv = self.private();
+ const surface = priv.surface orelse return;
+ const core_surface = surface.core() orelse return;
+ const inspector = core_surface.inspector orelse return;
+ inspector.render(core_surface);
+ }
+
+ //---------------------------------------------------------------
+ // Public methods
+
+ /// Queue a render of the Dear ImGui widget.
+ pub fn queueRender(self: *Self) void {
+ self.as(ImguiWidget).queueRender();
+ }
+
+ //---------------------------------------------------------------
+ // Properties
+
+ fn getSurfaceValue(self: *Self, value: *gobject.Value) void {
+ gobject.ext.Value.set(
+ value,
+ self.private().surface,
+ );
+ }
+
+ fn setSurfaceValue(self: *Self, value: *const gobject.Value) void {
+ self.setSurface(gobject.ext.Value.get(
+ value,
+ ?*Surface,
+ ));
+ }
+
+ pub fn getSurface(self: *Self) ?*Surface {
+ return self.private().surface;
+ }
+
+ pub fn setSurface(self: *Self, surface_: ?*Surface) void {
+ const priv = self.private();
+
+ // Do nothing if we're not changing the value.
+ if (surface_ == priv.surface) return;
+
+ // Setup our notification to happen at the end because we're
+ // changing values no matter what.
+ self.as(gobject.Object).freezeNotify();
+ defer self.as(gobject.Object).thawNotify();
+ self.as(gobject.Object).notifyByPspec(properties.surface.impl.param_spec);
+
+ // Deactivate the inspector on the old surface if it exists
+ // and set our value to null.
+ if (priv.surface) |old| old: {
+ priv.surface = null;
+
+ // Remove our weak ref
+ old.as(gobject.Object).weakUnref(
+ surfaceWeakNotify,
+ self,
+ );
+
+ // Deactivate the inspector
+ const core_surface = old.core() orelse break :old;
+ core_surface.deactivateInspector();
+ }
+
+ // Activate the inspector on the new surface.
+ const surface = surface_ orelse return;
+ const core_surface = surface.core() orelse return;
+ core_surface.activateInspector() catch |err| {
+ log.warn("failed to activate inspector err={}", .{err});
+ return;
+ };
+
+ // We use a weak reference on surface to determine if the surface
+ // was closed while our inspector was active.
+ surface.as(gobject.Object).weakRef(
+ surfaceWeakNotify,
+ self,
+ );
+
+ // Store our surface. We don't need to ref this because we setup
+ // the weak notify above.
+ priv.surface = surface;
+
+ self.queueRender();
+ }
+
+ //---------------------------------------------------------------
+ // Signal Handlers
+
+ fn surfaceWeakNotify(
+ ud: ?*anyopaque,
+ surface: *gobject.Object,
+ ) callconv(.c) void {
+ const self: *Self = @ptrCast(@alignCast(ud orelse return));
+ const priv = self.private();
+
+ // The weak notify docs call out that we can specifically use the
+ // pointer values for comparison, but the objects themselves are unsafe.
+ if (@intFromPtr(priv.surface) != @intFromPtr(surface)) return;
+
+ // According to weak notify docs, "surface" is in the "dispose" state.
+ // Our surface doesn't clear the core surface until the "finalize"
+ // state so we should be able to safely access it here. We need to
+ // be really careful though.
+ const old = priv.surface orelse return;
+ const core_surface = old.core() orelse return;
+ core_surface.deactivateInspector();
+ priv.surface = null;
+ self.as(gobject.Object).notifyByPspec(properties.surface.impl.param_spec);
+
+ // Note: in the future we should probably show some content on our
+ // window to note that the surface went away in case our embedding
+ // widget doesn't close itself. As I type this, our window closes
+ // immediately when the surface goes away so you don't see this, but
+ // for completeness sake we should clean this up.
+ }
+
+ const C = Common(Self, Private);
+ pub const as = C.as;
+ pub const ref = C.ref;
+ pub const refSink = C.refSink;
+ pub const unref = C.unref;
+ const private = C.private;
+
+ pub const Class = extern struct {
+ parent_class: Parent.Class,
+ var parent: *Parent.Class = undefined;
+ pub const Instance = Self;
+
+ fn init(class: *Class) callconv(.c) void {
+ gobject.ext.ensureType(ImguiWidget);
+ gtk.Widget.Class.setTemplateFromResource(
+ class.as(gtk.Widget.Class),
+ comptime gresource.blueprint(.{
+ .major = 1,
+ .minor = 5,
+ .name = "inspector-widget",
+ }),
+ );
+
+ // Properties
+ gobject.ext.registerProperties(class, &.{
+ properties.surface.impl,
+ });
+
+ // Signals
+
+ // Virtual methods
+ ImguiWidget.virtual_methods.setup.implement(class, imguiSetup);
+ ImguiWidget.virtual_methods.render.implement(class, imguiRender);
+ gobject.Object.virtual_methods.dispose.implement(class, &dispose);
+ }
+
+ pub const as = C.Class.as;
+ pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
+ pub const bindTemplateCallback = C.Class.bindTemplateCallback;
+ };
+};
diff --git a/vendor/libghostty-vt/src/apprt/gtk/class/inspector_window.zig b/vendor/libghostty-vt/src/apprt/gtk/class/inspector_window.zig
new file mode 100644
index 00000000..739e7569
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/class/inspector_window.zig
@@ -0,0 +1,217 @@
+const std = @import("std");
+const build_config = @import("../../../build_config.zig");
+
+const adw = @import("adw");
+const gobject = @import("gobject");
+const gtk = @import("gtk");
+
+const gresource = @import("../build/gresource.zig");
+
+const Common = @import("../class.zig").Common;
+const Surface = @import("surface.zig").Surface;
+const DebugWarning = @import("debug_warning.zig").DebugWarning;
+const InspectorWidget = @import("inspector_widget.zig").InspectorWidget;
+const WeakRef = @import("../weak_ref.zig").WeakRef;
+
+const log = std.log.scoped(.gtk_ghostty_inspector_window);
+
+/// Window for displaying the Ghostty inspector.
+pub const InspectorWindow = extern struct {
+ const Self = @This();
+ parent_instance: Parent,
+ pub const Parent = adw.ApplicationWindow;
+ pub const getGObjectType = gobject.ext.defineClass(Self, .{
+ .name = "GhosttyInspectorWindow",
+ .instanceInit = &init,
+ .classInit = &Class.init,
+ .parent_class = &Class.parent,
+ .private = .{ .Type = Private, .offset = &Private.offset },
+ });
+
+ pub const properties = struct {
+ pub const surface = struct {
+ pub const name = "surface";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?*Surface,
+ .{
+ .accessor = .{
+ .getter = getSurfaceValue,
+ .setter = setSurfaceValue,
+ },
+ },
+ );
+ };
+
+ pub const debug = struct {
+ pub const name = "debug";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ bool,
+ .{
+ .default = build_config.is_debug,
+ .accessor = gobject.ext.typedAccessor(Self, bool, .{
+ .getter = struct {
+ pub fn getter(_: *Self) bool {
+ return build_config.is_debug;
+ }
+ }.getter,
+ }),
+ },
+ );
+ };
+ };
+
+ pub const signals = struct {};
+
+ const Private = struct {
+ /// The surface that we are attached to
+ surface: WeakRef(Surface) = .empty,
+
+ /// The embedded inspector widget.
+ inspector_widget: *InspectorWidget,
+
+ pub var offset: c_int = 0;
+ };
+
+ //---------------------------------------------------------------
+ // Virtual Methods
+
+ fn init(self: *Self, _: *Class) callconv(.c) void {
+ gtk.Widget.initTemplate(self.as(gtk.Widget));
+
+ // Add our dev CSS class if we're in debug mode.
+ if (comptime build_config.is_debug) {
+ self.as(gtk.Widget).addCssClass("devel");
+ }
+
+ // Set our window icon. We can't set this in the blueprint file
+ // because its dependent on the build config.
+ self.as(gtk.Window).setIconName(build_config.bundle_id);
+ }
+
+ fn dispose(self: *Self) callconv(.c) void {
+ // You MUST clear all weak refs in dispose, otherwise it causes
+ // memory corruption on dispose on the TARGET (weak referenced)
+ // object. The only way we caught this is via Valgrind. Its not a leak,
+ // its an invalid memory read. In practice, I found this sometimes
+ // caused hanging!
+ self.setSurface(null);
+
+ gtk.Widget.disposeTemplate(
+ self.as(gtk.Widget),
+ getGObjectType(),
+ );
+
+ gobject.Object.virtual_methods.dispose.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ //---------------------------------------------------------------
+ // Public methods
+
+ pub fn new(surface: *Surface) *Self {
+ return gobject.ext.newInstance(Self, .{
+ .surface = surface,
+ });
+ }
+
+ /// Present the window.
+ pub fn present(self: *Self) void {
+ self.as(gtk.Window).present();
+ }
+
+ /// Queue a render of the embedded widget.
+ pub fn queueRender(self: *Self) void {
+ const priv = self.private();
+ priv.inspector_widget.queueRender();
+ }
+
+ //---------------------------------------------------------------
+ // Properties
+
+ fn setSurface(self: *Self, newvalue: ?*Surface) void {
+ const priv = self.private();
+ priv.surface.set(newvalue);
+ }
+
+ fn getSurfaceValue(self: *Self, value: *gobject.Value) void {
+ // Important: get() refs, so we take to not increase ref twice
+ gobject.ext.Value.take(
+ value,
+ self.private().surface.get(),
+ );
+ }
+
+ fn setSurfaceValue(self: *Self, value: *const gobject.Value) void {
+ self.setSurface(gobject.ext.Value.get(
+ value,
+ ?*Surface,
+ ));
+ }
+
+ //---------------------------------------------------------------
+ // Signal Handlers
+
+ fn propInspectorSurface(
+ inspector: *InspectorWidget,
+ _: *gobject.ParamSpec,
+ self: *Self,
+ ) callconv(.c) void {
+ // If the inspector's surface went away, we destroy the window.
+ // The inspector has a weak notify on the surface so it knows
+ // if it goes nil.
+ if (inspector.getSurface() == null) {
+ self.as(gtk.Window).destroy();
+ }
+ }
+
+ const C = Common(Self, Private);
+ pub const as = C.as;
+ pub const ref = C.ref;
+ pub const refSink = C.refSink;
+ pub const unref = C.unref;
+ const private = C.private;
+
+ pub const Class = extern struct {
+ parent_class: Parent.Class,
+ var parent: *Parent.Class = undefined;
+ pub const Instance = Self;
+
+ fn init(class: *Class) callconv(.c) void {
+ gobject.ext.ensureType(DebugWarning);
+ gobject.ext.ensureType(InspectorWidget);
+ gtk.Widget.Class.setTemplateFromResource(
+ class.as(gtk.Widget.Class),
+ comptime gresource.blueprint(.{
+ .major = 1,
+ .minor = 5,
+ .name = "inspector-window",
+ }),
+ );
+
+ // Template Bindings
+ class.bindTemplateChildPrivate("inspector_widget", .{});
+
+ // Template callbacks
+ class.bindTemplateCallback("notify_inspector_surface", &propInspectorSurface);
+
+ // Properties
+ gobject.ext.registerProperties(class, &.{
+ properties.surface.impl,
+ properties.debug.impl,
+ });
+
+ // Virtual methods
+ gobject.Object.virtual_methods.dispose.implement(class, &dispose);
+ }
+
+ pub const as = C.Class.as;
+ pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
+ pub const bindTemplateCallback = C.Class.bindTemplateCallback;
+ };
+};
diff --git a/vendor/libghostty-vt/src/apprt/gtk/class/key_state_overlay.zig b/vendor/libghostty-vt/src/apprt/gtk/class/key_state_overlay.zig
new file mode 100644
index 00000000..7aca8f01
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/class/key_state_overlay.zig
@@ -0,0 +1,342 @@
+const std = @import("std");
+const adw = @import("adw");
+const glib = @import("glib");
+const gobject = @import("gobject");
+const gtk = @import("gtk");
+
+const ext = @import("../ext.zig");
+const gresource = @import("../build/gresource.zig");
+const Application = @import("application.zig").Application;
+const Common = @import("../class.zig").Common;
+
+const log = std.log.scoped(.gtk_ghostty_key_state_overlay);
+
+/// An overlay that displays the current key table stack and pending key sequence.
+/// This helps users understand what key bindings are active and what keys they've
+/// pressed in a multi-key sequence.
+pub const KeyStateOverlay = extern struct {
+ const Self = @This();
+ parent_instance: Parent,
+ pub const Parent = adw.Bin;
+ pub const getGObjectType = gobject.ext.defineClass(Self, .{
+ .name = "GhosttyKeyStateOverlay",
+ .instanceInit = &init,
+ .classInit = &Class.init,
+ .parent_class = &Class.parent,
+ .private = .{ .Type = Private, .offset = &Private.offset },
+ });
+
+ pub const properties = struct {
+ pub const tables = struct {
+ pub const name = "tables";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?*ext.StringList,
+ .{
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ ?*ext.StringList,
+ .{
+ .getter = getTables,
+ .getter_transfer = .none,
+ .setter = setTables,
+ .setter_transfer = .full,
+ },
+ ),
+ },
+ );
+ };
+
+ pub const @"has-tables" = struct {
+ pub const name = "has-tables";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ bool,
+ .{
+ .default = false,
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ bool,
+ .{ .getter = getHasTables },
+ ),
+ },
+ );
+ };
+
+ pub const sequence = struct {
+ pub const name = "sequence";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?*ext.StringList,
+ .{
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ ?*ext.StringList,
+ .{
+ .getter = getSequence,
+ .getter_transfer = .none,
+ .setter = setSequence,
+ .setter_transfer = .full,
+ },
+ ),
+ },
+ );
+ };
+
+ pub const @"has-sequence" = struct {
+ pub const name = "has-sequence";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ bool,
+ .{
+ .default = false,
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ bool,
+ .{ .getter = getHasSequence },
+ ),
+ },
+ );
+ };
+
+ pub const @"valign-target" = struct {
+ pub const name = "valign-target";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ gtk.Align,
+ .{
+ .default = .end,
+ .accessor = C.privateShallowFieldAccessor("valign_target"),
+ },
+ );
+ };
+ };
+
+ const Private = struct {
+ /// The key table stack.
+ tables: ?*ext.StringList = null,
+
+ /// The key sequence.
+ sequence: ?*ext.StringList = null,
+
+ /// Target vertical alignment for the overlay.
+ valign_target: gtk.Align = .end,
+
+ pub var offset: c_int = 0;
+ };
+
+ fn init(self: *Self, _: *Class) callconv(.c) void {
+ gtk.Widget.initTemplate(self.as(gtk.Widget));
+ }
+
+ fn getTables(self: *Self) ?*ext.StringList {
+ return self.private().tables;
+ }
+
+ fn getSequence(self: *Self) ?*ext.StringList {
+ return self.private().sequence;
+ }
+
+ fn setTables(self: *Self, value: ?*ext.StringList) void {
+ const priv = self.private();
+ if (priv.tables) |old| {
+ old.destroy();
+ priv.tables = null;
+ }
+ if (value) |v| {
+ priv.tables = v;
+ }
+
+ self.as(gobject.Object).notifyByPspec(properties.tables.impl.param_spec);
+ self.as(gobject.Object).notifyByPspec(properties.@"has-tables".impl.param_spec);
+ }
+
+ fn setSequence(self: *Self, value: ?*ext.StringList) void {
+ const priv = self.private();
+ if (priv.sequence) |old| {
+ old.destroy();
+ priv.sequence = null;
+ }
+ if (value) |v| {
+ priv.sequence = v;
+ }
+
+ self.as(gobject.Object).notifyByPspec(properties.sequence.impl.param_spec);
+ self.as(gobject.Object).notifyByPspec(properties.@"has-sequence".impl.param_spec);
+ }
+
+ fn getHasTables(self: *Self) bool {
+ const v = self.private().tables orelse return false;
+ return v.strings.len > 0;
+ }
+
+ fn getHasSequence(self: *Self) bool {
+ const v = self.private().sequence orelse return false;
+ return v.strings.len > 0;
+ }
+
+ fn closureShowChevron(
+ _: *Self,
+ has_tables: bool,
+ has_sequence: bool,
+ ) callconv(.c) c_int {
+ return if (has_tables and has_sequence) 1 else 0;
+ }
+
+ fn closureHasState(
+ _: *Self,
+ has_tables: bool,
+ has_sequence: bool,
+ ) callconv(.c) c_int {
+ return if (has_tables or has_sequence) 1 else 0;
+ }
+
+ fn closureTablesText(
+ _: *Self,
+ tables: ?*ext.StringList,
+ ) callconv(.c) ?[*:0]const u8 {
+ const list = tables orelse return null;
+ if (list.strings.len == 0) return null;
+
+ var buf: std.Io.Writer.Allocating = .init(Application.default().allocator());
+ defer buf.deinit();
+
+ for (list.strings, 0..) |s, i| {
+ if (i > 0) buf.writer.writeAll(" > ") catch return null;
+ buf.writer.writeAll(s) catch return null;
+ }
+
+ return glib.ext.dupeZ(u8, buf.written());
+ }
+
+ fn closureSequenceText(
+ _: *Self,
+ sequence: ?*ext.StringList,
+ ) callconv(.c) ?[*:0]const u8 {
+ const list = sequence orelse return null;
+ if (list.strings.len == 0) return null;
+
+ var buf: std.Io.Writer.Allocating = .init(Application.default().allocator());
+ defer buf.deinit();
+
+ for (list.strings, 0..) |s, i| {
+ if (i > 0) buf.writer.writeAll(" ") catch return null;
+ buf.writer.writeAll(s) catch return null;
+ }
+
+ return glib.ext.dupeZ(u8, buf.written());
+ }
+
+ //---------------------------------------------------------------
+ // Template callbacks
+
+ fn onDragEnd(
+ _: *gtk.GestureDrag,
+ _: f64,
+ offset_y: f64,
+ self: *Self,
+ ) callconv(.c) void {
+ // Key state overlay only moves between top-center and bottom-center.
+ // Horizontal alignment is always center.
+ const priv = self.private();
+ const widget = self.as(gtk.Widget);
+ const parent = widget.getParent() orelse return;
+
+ const parent_height: f64 = @floatFromInt(parent.getAllocatedHeight());
+ const self_height: f64 = @floatFromInt(widget.getAllocatedHeight());
+
+ const self_y: f64 = if (priv.valign_target == .start) 0 else parent_height - self_height;
+ const new_y = self_y + offset_y + (self_height / 2);
+
+ const new_valign: gtk.Align = if (new_y > parent_height / 2) .end else .start;
+
+ if (new_valign != priv.valign_target) {
+ priv.valign_target = new_valign;
+ self.as(gobject.Object).notifyByPspec(properties.@"valign-target".impl.param_spec);
+ self.as(gtk.Widget).queueResize();
+ }
+ }
+
+ //---------------------------------------------------------------
+ // Virtual methods
+
+ fn dispose(self: *Self) callconv(.c) void {
+ gtk.Widget.disposeTemplate(
+ self.as(gtk.Widget),
+ getGObjectType(),
+ );
+
+ gobject.Object.virtual_methods.dispose.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ fn finalize(self: *Self) callconv(.c) void {
+ const priv = self.private();
+
+ if (priv.tables) |v| {
+ v.destroy();
+ }
+ if (priv.sequence) |v| {
+ v.destroy();
+ }
+
+ gobject.Object.virtual_methods.finalize.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ const C = Common(Self, Private);
+ pub const as = C.as;
+ pub const ref = C.ref;
+ pub const unref = C.unref;
+ const private = C.private;
+
+ pub const Class = extern struct {
+ parent_class: Parent.Class,
+ var parent: *Parent.Class = undefined;
+ pub const Instance = Self;
+
+ fn init(class: *Class) callconv(.c) void {
+ gtk.Widget.Class.setTemplateFromResource(
+ class.as(gtk.Widget.Class),
+ comptime gresource.blueprint(.{
+ .major = 1,
+ .minor = 2,
+ .name = "key-state-overlay",
+ }),
+ );
+
+ // Template Callbacks
+ class.bindTemplateCallback("on_drag_end", &onDragEnd);
+ class.bindTemplateCallback("show_chevron", &closureShowChevron);
+ class.bindTemplateCallback("has_state", &closureHasState);
+ class.bindTemplateCallback("tables_text", &closureTablesText);
+ class.bindTemplateCallback("sequence_text", &closureSequenceText);
+
+ // Properties
+ gobject.ext.registerProperties(class, &.{
+ properties.tables.impl,
+ properties.@"has-tables".impl,
+ properties.sequence.impl,
+ properties.@"has-sequence".impl,
+ properties.@"valign-target".impl,
+ });
+
+ // Virtual methods
+ gobject.Object.virtual_methods.dispose.implement(class, &dispose);
+ gobject.Object.virtual_methods.finalize.implement(class, &finalize);
+ }
+
+ pub const as = C.Class.as;
+ pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
+ pub const bindTemplateCallback = C.Class.bindTemplateCallback;
+ };
+};
diff --git a/vendor/libghostty-vt/src/apprt/gtk/class/resize_overlay.zig b/vendor/libghostty-vt/src/apprt/gtk/class/resize_overlay.zig
new file mode 100644
index 00000000..e14f1563
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/class/resize_overlay.zig
@@ -0,0 +1,338 @@
+const std = @import("std");
+const adw = @import("adw");
+const glib = @import("glib");
+const gobject = @import("gobject");
+const gtk = @import("gtk");
+
+const gresource = @import("../build/gresource.zig");
+const Common = @import("../class.zig").Common;
+
+const log = std.log.scoped(.gtk_ghostty_resize_overlay);
+
+/// The overlay that shows the current size while a surface is resizing.
+/// This can be used generically to show pretty much anything with a
+/// disappearing overlay, but we have no other use at this point so it
+/// is named specifically for what it does.
+///
+/// General usage:
+///
+/// 1. Add it to an overlay
+/// 2. Set the label with `setLabel`
+/// 3. Schedule to show it with `schedule`
+///
+/// Set any properties to change the behavior.
+pub const ResizeOverlay = extern struct {
+ const Self = @This();
+ parent_instance: Parent,
+ pub const Parent = adw.Bin;
+ pub const getGObjectType = gobject.ext.defineClass(Self, .{
+ .name = "GhosttyResizeOverlay",
+ .instanceInit = &init,
+ .classInit = &Class.init,
+ .parent_class = &Class.parent,
+ .private = .{ .Type = Private, .offset = &Private.offset },
+ });
+
+ pub const properties = struct {
+ pub const duration = struct {
+ pub const name = "duration";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ c_uint,
+ .{
+ .default = 750,
+ .minimum = 250,
+ .maximum = std.math.maxInt(c_uint),
+ .accessor = gobject.ext.privateFieldAccessor(
+ Self,
+ Private,
+ &Private.offset,
+ "duration",
+ ),
+ },
+ );
+ };
+
+ pub const @"first-delay" = struct {
+ pub const name = "first-delay";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ c_uint,
+ .{
+ .default = 250,
+ .minimum = 250,
+ .maximum = std.math.maxInt(c_uint),
+ .accessor = gobject.ext.privateFieldAccessor(
+ Self,
+ Private,
+ &Private.offset,
+ "first_delay",
+ ),
+ },
+ );
+ };
+
+ pub const label = struct {
+ pub const name = "label";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?[:0]const u8,
+ .{
+ .default = null,
+ .accessor = C.privateStringFieldAccessor("label_text"),
+ },
+ );
+ };
+
+ pub const @"overlay-halign" = struct {
+ pub const name = "overlay-halign";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ gtk.Align,
+ .{
+ .default = .center,
+ .accessor = gobject.ext.privateFieldAccessor(
+ Self,
+ Private,
+ &Private.offset,
+ "halign",
+ ),
+ },
+ );
+ };
+
+ pub const @"overlay-valign" = struct {
+ pub const name = "overlay-valign";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ gtk.Align,
+ .{
+ .default = .center,
+ .accessor = gobject.ext.privateFieldAccessor(
+ Self,
+ Private,
+ &Private.offset,
+ "valign",
+ ),
+ },
+ );
+ };
+ };
+
+ const Private = struct {
+ /// The label with the text
+ label: *gtk.Label,
+
+ /// The text to set on the label when scheduled.
+ label_text: ?[:0]const u8,
+
+ /// The time that the overlay appears.
+ duration: c_uint,
+
+ /// The first delay before any overlay is shown. Must be specified
+ /// during construction otherwise it has no effect.
+ first_delay: c_uint,
+
+ /// The idle source that we use to update the label.
+ idler: ?c_uint = null,
+
+ /// The timer for dismissing the overlay.
+ timer: ?c_uint = null,
+
+ /// The first delay timer.
+ delay_timer: ?c_uint = null,
+
+ /// The alignment of the label
+ halign: gtk.Align,
+ valign: gtk.Align,
+
+ pub var offset: c_int = 0;
+ };
+
+ fn init(self: *Self, _: *Class) callconv(.c) void {
+ gtk.Widget.initTemplate(self.as(gtk.Widget));
+
+ const priv = self.private();
+ if (priv.first_delay > 0) {
+ priv.delay_timer = glib.timeoutAdd(
+ priv.first_delay,
+ onDelayTimer,
+ self,
+ );
+ }
+ }
+
+ /// Set the label for the overlay. This will not show the
+ /// overlay if it is currently hidden; you must call schedule.
+ pub fn setLabel(self: *Self, label: ?[:0]const u8) void {
+ const priv = self.private();
+ if (priv.label_text) |v| glib.free(@ptrCast(@constCast(v)));
+ priv.label_text = null;
+ if (label) |v| priv.label_text = glib.ext.dupeZ(u8, v);
+ self.as(gobject.Object).notifyByPspec(properties.label.impl.param_spec);
+ }
+
+ /// Schedule the overlay to be shown. To avoid flickering during
+ /// resizes we schedule the overlay to be shown on the next idle tick.
+ pub fn schedule(self: *Self) void {
+ const priv = self.private();
+
+ // If we have a delay timer then we're not showing anything
+ // yet so do nothing.
+ if (priv.delay_timer != null) return;
+
+ // When updating a widget, wait until GTK is "idle", i.e. not in the middle
+ // of doing any other updates. Since we are called in the middle of resizing
+ // GTK is doing a lot of work rearranging all of the widgets. Not doing this
+ // results in a lot of warnings from GTK and _horrible_ flickering of the
+ // resize overlay.
+ if (priv.idler != null) return;
+ priv.idler = glib.idleAdd(onIdle, self);
+ }
+
+ fn onIdle(ud: ?*anyopaque) callconv(.c) c_int {
+ const self: *Self = @ptrCast(@alignCast(ud orelse return 0));
+ const priv = self.private();
+
+ // No matter what our idler is complete with this callback
+ priv.idler = null;
+
+ // Cancel our previous show timer no matter what.
+ if (priv.timer) |timer| {
+ if (glib.Source.remove(timer) == 0) {
+ log.warn("unable to remove size overlay timer", .{});
+ }
+ priv.timer = null;
+ }
+
+ // If we have a label to show, show ourselves. If we don't have
+ // label text, then hide our label.
+ const text = priv.label_text orelse {
+ self.as(gtk.Widget).setVisible(0);
+ return 0;
+ };
+
+ // Set our label and show it.
+ priv.label.setLabel(text);
+ self.as(gtk.Widget).setVisible(1);
+
+ // Setup the new timer to hide ourselves after the delay.
+ priv.timer = glib.timeoutAdd(
+ priv.duration,
+ onTimer,
+ self,
+ );
+
+ return 0;
+ }
+
+ fn onTimer(ud: ?*anyopaque) callconv(.c) c_int {
+ const self: *Self = @ptrCast(@alignCast(ud orelse return 0));
+ const priv = self.private();
+ priv.timer = null;
+ self.as(gtk.Widget).setVisible(0);
+ return 0;
+ }
+
+ fn onDelayTimer(ud: ?*anyopaque) callconv(.c) c_int {
+ const self: *Self = @ptrCast(@alignCast(ud orelse return 0));
+ const priv = self.private();
+ priv.delay_timer = null;
+ return 0;
+ }
+
+ //---------------------------------------------------------------
+ // Virtual methods
+
+ fn dispose(self: *Self) callconv(.c) void {
+ const priv = self.private();
+ if (priv.idler) |v| {
+ if (glib.Source.remove(v) == 0) {
+ log.warn("unable to remove resize overlay idler", .{});
+ }
+ priv.idler = null;
+ }
+ if (priv.timer) |v| {
+ if (glib.Source.remove(v) == 0) {
+ log.warn("unable to remove resize overlay timer", .{});
+ }
+ priv.timer = null;
+ }
+ if (priv.delay_timer) |v| {
+ if (glib.Source.remove(v) == 0) {
+ log.warn("unable to remove resize overlay delay timer", .{});
+ }
+ priv.delay_timer = null;
+ }
+
+ gtk.Widget.disposeTemplate(
+ self.as(gtk.Widget),
+ getGObjectType(),
+ );
+
+ gobject.Object.virtual_methods.dispose.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ fn finalize(self: *Self) callconv(.c) void {
+ const priv = self.private();
+ if (priv.label_text) |v| {
+ glib.free(@ptrCast(@constCast(v)));
+ priv.label_text = null;
+ }
+
+ gobject.Object.virtual_methods.finalize.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ const C = Common(Self, Private);
+ pub const as = C.as;
+ pub const ref = C.ref;
+ pub const unref = C.unref;
+ const private = C.private;
+
+ pub const Class = extern struct {
+ parent_class: Parent.Class,
+ var parent: *Parent.Class = undefined;
+ pub const Instance = Self;
+
+ fn init(class: *Class) callconv(.c) void {
+ gtk.Widget.Class.setTemplateFromResource(
+ class.as(gtk.Widget.Class),
+ comptime gresource.blueprint(.{
+ .major = 1,
+ .minor = 2,
+ .name = "resize-overlay",
+ }),
+ );
+
+ // Bindings
+ class.bindTemplateChildPrivate("label", .{});
+
+ // Properties
+ gobject.ext.registerProperties(class, &.{
+ properties.duration.impl,
+ properties.label.impl,
+ properties.@"first-delay".impl,
+ properties.@"overlay-halign".impl,
+ properties.@"overlay-valign".impl,
+ });
+
+ // Virtual methods
+ gobject.Object.virtual_methods.dispose.implement(class, &dispose);
+ gobject.Object.virtual_methods.finalize.implement(class, &finalize);
+ }
+
+ pub const as = C.Class.as;
+ pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
+ };
+};
diff --git a/vendor/libghostty-vt/src/apprt/gtk/class/search_overlay.zig b/vendor/libghostty-vt/src/apprt/gtk/class/search_overlay.zig
new file mode 100644
index 00000000..a8111546
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/class/search_overlay.zig
@@ -0,0 +1,493 @@
+const std = @import("std");
+const adw = @import("adw");
+const glib = @import("glib");
+const gobject = @import("gobject");
+const gdk = @import("gdk");
+const gtk = @import("gtk");
+
+const gresource = @import("../build/gresource.zig");
+const Common = @import("../class.zig").Common;
+
+const log = std.log.scoped(.gtk_ghostty_search_overlay);
+
+/// The overlay that shows the current size while a surface is resizing.
+/// This can be used generically to show pretty much anything with a
+/// disappearing overlay, but we have no other use at this point so it
+/// is named specifically for what it does.
+///
+/// General usage:
+///
+/// 1. Add it to an overlay
+/// 2. Set the label with `setLabel`
+/// 3. Schedule to show it with `schedule`
+///
+/// Set any properties to change the behavior.
+pub const SearchOverlay = extern struct {
+ const Self = @This();
+ parent_instance: Parent,
+ pub const Parent = adw.Bin;
+ pub const getGObjectType = gobject.ext.defineClass(Self, .{
+ .name = "GhosttySearchOverlay",
+ .instanceInit = &init,
+ .classInit = &Class.init,
+ .parent_class = &Class.parent,
+ .private = .{ .Type = Private, .offset = &Private.offset },
+ });
+
+ pub const properties = struct {
+ pub const active = struct {
+ pub const name = "active";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ bool,
+ .{
+ .default = false,
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ bool,
+ .{
+ .getter = getSearchActive,
+ .setter = setSearchActive,
+ },
+ ),
+ },
+ );
+ };
+
+ pub const @"search-total" = struct {
+ pub const name = "search-total";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ u64,
+ .{
+ .default = 0,
+ .minimum = 0,
+ .maximum = std.math.maxInt(u64),
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ u64,
+ .{ .getter = getSearchTotal },
+ ),
+ },
+ );
+ };
+
+ pub const @"has-search-total" = struct {
+ pub const name = "has-search-total";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ bool,
+ .{
+ .default = false,
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ bool,
+ .{ .getter = getHasSearchTotal },
+ ),
+ },
+ );
+ };
+
+ pub const @"search-selected" = struct {
+ pub const name = "search-selected";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ u64,
+ .{
+ .default = 0,
+ .minimum = 0,
+ .maximum = std.math.maxInt(u64),
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ u64,
+ .{ .getter = getSearchSelected },
+ ),
+ },
+ );
+ };
+
+ pub const @"has-search-selected" = struct {
+ pub const name = "has-search-selected";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ bool,
+ .{
+ .default = false,
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ bool,
+ .{ .getter = getHasSearchSelected },
+ ),
+ },
+ );
+ };
+
+ pub const @"halign-target" = struct {
+ pub const name = "halign-target";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ gtk.Align,
+ .{
+ .default = .end,
+ .accessor = C.privateShallowFieldAccessor("halign_target"),
+ },
+ );
+ };
+
+ pub const @"valign-target" = struct {
+ pub const name = "valign-target";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ gtk.Align,
+ .{
+ .default = .start,
+ .accessor = C.privateShallowFieldAccessor("valign_target"),
+ },
+ );
+ };
+ };
+
+ pub const signals = struct {
+ /// Emitted when the search is stopped (e.g., Escape pressed).
+ pub const @"stop-search" = struct {
+ pub const name = "stop-search";
+ pub const connect = impl.connect;
+ const impl = gobject.ext.defineSignal(
+ name,
+ Self,
+ &.{},
+ void,
+ );
+ };
+
+ /// Emitted when the search text changes (debounced).
+ pub const @"search-changed" = struct {
+ pub const name = "search-changed";
+ pub const connect = impl.connect;
+ const impl = gobject.ext.defineSignal(
+ name,
+ Self,
+ &.{?[*:0]const u8},
+ void,
+ );
+ };
+
+ /// Emitted when navigating to the next match.
+ pub const @"next-match" = struct {
+ pub const name = "next-match";
+ pub const connect = impl.connect;
+ const impl = gobject.ext.defineSignal(
+ name,
+ Self,
+ &.{},
+ void,
+ );
+ };
+
+ /// Emitted when navigating to the previous match.
+ pub const @"previous-match" = struct {
+ pub const name = "previous-match";
+ pub const connect = impl.connect;
+ const impl = gobject.ext.defineSignal(
+ name,
+ Self,
+ &.{},
+ void,
+ );
+ };
+ };
+
+ const Private = struct {
+ /// The search entry widget.
+ search_entry: *gtk.SearchEntry,
+
+ /// True when a search is active, meaning we should show the overlay.
+ active: bool = false,
+
+ /// Total number of search matches (null means unknown/none).
+ search_total: ?usize = null,
+
+ /// Currently selected match index (null means none selected).
+ search_selected: ?usize = null,
+
+ /// Target horizontal alignment for the overlay.
+ halign_target: gtk.Align = .end,
+
+ /// Target vertical alignment for the overlay.
+ valign_target: gtk.Align = .start,
+
+ pub var offset: c_int = 0;
+ };
+
+ fn init(self: *Self, _: *Class) callconv(.c) void {
+ gtk.Widget.initTemplate(self.as(gtk.Widget));
+ }
+
+ /// Grab focus on the search entry and select all text.
+ pub fn grabFocus(self: *Self) void {
+ const priv = self.private();
+ _ = priv.search_entry.as(gtk.Widget).grabFocus();
+
+ // Select all text in the search entry field. -1 is distance from
+ // the end, causing the entire text to be selected.
+ priv.search_entry.as(gtk.Editable).selectRegion(0, -1);
+ }
+
+ // Set active status, and update search on activation
+ fn setSearchActive(self: *Self, active: bool) void {
+ const priv = self.private();
+ if (!priv.active and active) {
+ const text = priv.search_entry.as(gtk.Editable).getText();
+ signals.@"search-changed".impl.emit(self, null, .{text}, null);
+ }
+ priv.active = active;
+ }
+
+ // Set contents of search
+ pub fn setSearchContents(self: *Self, content: [:0]const u8) void {
+ const priv = self.private();
+ priv.search_entry.as(gtk.Editable).setText(content);
+ signals.@"search-changed".impl.emit(self, null, .{content}, null);
+ }
+
+ /// Set the total number of search matches.
+ pub fn setSearchTotal(self: *Self, total: ?usize) void {
+ const priv = self.private();
+ const had_total = priv.search_total != null;
+ if (priv.search_total == total) return;
+ priv.search_total = total;
+ self.as(gobject.Object).notifyByPspec(properties.@"search-total".impl.param_spec);
+ if (had_total != (total != null)) {
+ self.as(gobject.Object).notifyByPspec(properties.@"has-search-total".impl.param_spec);
+ }
+ }
+
+ /// Set the currently selected match index.
+ pub fn setSearchSelected(self: *Self, selected: ?usize) void {
+ const priv = self.private();
+ const had_selected = priv.search_selected != null;
+ if (priv.search_selected == selected) return;
+ priv.search_selected = selected;
+ self.as(gobject.Object).notifyByPspec(properties.@"search-selected".impl.param_spec);
+ if (had_selected != (selected != null)) {
+ self.as(gobject.Object).notifyByPspec(properties.@"has-search-selected".impl.param_spec);
+ }
+ }
+
+ fn getSearchActive(self: *Self) bool {
+ return self.private().active;
+ }
+
+ fn getSearchTotal(self: *Self) u64 {
+ return self.private().search_total orelse 0;
+ }
+
+ fn getHasSearchTotal(self: *Self) bool {
+ return self.private().search_total != null;
+ }
+
+ fn getSearchSelected(self: *Self) u64 {
+ return self.private().search_selected orelse 0;
+ }
+
+ fn getHasSearchSelected(self: *Self) bool {
+ return self.private().search_selected != null;
+ }
+
+ fn closureMatchLabel(
+ _: *Self,
+ has_selected: bool,
+ selected: u64,
+ has_total: bool,
+ total: u64,
+ ) callconv(.c) ?[*:0]const u8 {
+ if (!has_total or total == 0) return glib.ext.dupeZ(u8, "0/0");
+ var buf: [32]u8 = undefined;
+ const label = std.fmt.bufPrintZ(&buf, "{}/{}", .{
+ if (has_selected) selected + 1 else 0,
+ total,
+ }) catch return null;
+ return glib.ext.dupeZ(u8, label);
+ }
+
+ //---------------------------------------------------------------
+ // Template callbacks
+
+ fn searchChanged(entry: *gtk.SearchEntry, self: *Self) callconv(.c) void {
+ const text = entry.as(gtk.Editable).getText();
+ signals.@"search-changed".impl.emit(self, null, .{text}, null);
+ }
+
+ // NOTE: The callbacks below use anyopaque for the first parameter
+ // because they're shared with multiple widgets in the template.
+
+ fn stopSearch(_: *anyopaque, self: *Self) callconv(.c) void {
+ signals.@"stop-search".impl.emit(self, null, .{}, null);
+ }
+
+ fn nextMatch(_: *anyopaque, self: *Self) callconv(.c) void {
+ signals.@"next-match".impl.emit(self, null, .{}, null);
+ }
+
+ fn previousMatch(_: *anyopaque, self: *Self) callconv(.c) void {
+ signals.@"previous-match".impl.emit(self, null, .{}, null);
+ }
+
+ fn searchEntryKeyPressed(
+ _: *gtk.EventControllerKey,
+ keyval: c_uint,
+ _: c_uint,
+ gtk_mods: gdk.ModifierType,
+ self: *Self,
+ ) callconv(.c) c_int {
+ if (keyval == gdk.KEY_Return or keyval == gdk.KEY_KP_Enter) {
+ if (gtk_mods.shift_mask) {
+ signals.@"previous-match".impl.emit(self, null, .{}, null);
+ } else {
+ signals.@"next-match".impl.emit(self, null, .{}, null);
+ }
+
+ return 1;
+ }
+
+ return 0;
+ }
+
+ fn onDragEnd(
+ _: *gtk.GestureDrag,
+ offset_x: f64,
+ offset_y: f64,
+ self: *Self,
+ ) callconv(.c) void {
+ // On drag end, we want to move our halign/valign if we crossed
+ // the midpoint on either axis. This lets the search overlay be
+ // moved to different corners of the parent container.
+
+ const priv = self.private();
+ const widget = self.as(gtk.Widget);
+ const parent = widget.getParent() orelse return;
+
+ const parent_width: f64 = @floatFromInt(parent.getAllocatedWidth());
+ const parent_height: f64 = @floatFromInt(parent.getAllocatedHeight());
+ const self_width: f64 = @floatFromInt(widget.getAllocatedWidth());
+ const self_height: f64 = @floatFromInt(widget.getAllocatedHeight());
+
+ const self_x: f64 = if (priv.halign_target == .start) 0 else parent_width - self_width;
+ const self_y: f64 = if (priv.valign_target == .start) 0 else parent_height - self_height;
+
+ const new_x = self_x + offset_x + (self_width / 2);
+ const new_y = self_y + offset_y + (self_height / 2);
+
+ const new_halign: gtk.Align = if (new_x > parent_width / 2) .end else .start;
+ const new_valign: gtk.Align = if (new_y > parent_height / 2) .end else .start;
+
+ var changed = false;
+ if (new_halign != priv.halign_target) {
+ priv.halign_target = new_halign;
+ self.as(gobject.Object).notifyByPspec(properties.@"halign-target".impl.param_spec);
+ changed = true;
+ }
+ if (new_valign != priv.valign_target) {
+ priv.valign_target = new_valign;
+ self.as(gobject.Object).notifyByPspec(properties.@"valign-target".impl.param_spec);
+ changed = true;
+ }
+
+ if (changed) self.as(gtk.Widget).queueResize();
+ }
+
+ //---------------------------------------------------------------
+ // Virtual methods
+
+ fn dispose(self: *Self) callconv(.c) void {
+ const priv = self.private();
+ _ = priv;
+
+ gtk.Widget.disposeTemplate(
+ self.as(gtk.Widget),
+ getGObjectType(),
+ );
+
+ gobject.Object.virtual_methods.dispose.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ fn finalize(self: *Self) callconv(.c) void {
+ const priv = self.private();
+ _ = priv;
+
+ gobject.Object.virtual_methods.finalize.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ const C = Common(Self, Private);
+ pub const as = C.as;
+ pub const ref = C.ref;
+ pub const unref = C.unref;
+ const private = C.private;
+
+ pub const Class = extern struct {
+ parent_class: Parent.Class,
+ var parent: *Parent.Class = undefined;
+ pub const Instance = Self;
+
+ fn init(class: *Class) callconv(.c) void {
+ gtk.Widget.Class.setTemplateFromResource(
+ class.as(gtk.Widget.Class),
+ comptime gresource.blueprint(.{
+ .major = 1,
+ .minor = 2,
+ .name = "search-overlay",
+ }),
+ );
+
+ // Bindings
+ class.bindTemplateChildPrivate("search_entry", .{});
+
+ // Template Callbacks
+ class.bindTemplateCallback("stop_search", &stopSearch);
+ class.bindTemplateCallback("search_changed", &searchChanged);
+ class.bindTemplateCallback("match_label_closure", &closureMatchLabel);
+ class.bindTemplateCallback("next_match", &nextMatch);
+ class.bindTemplateCallback("previous_match", &previousMatch);
+ class.bindTemplateCallback("search_entry_key_pressed", &searchEntryKeyPressed);
+ class.bindTemplateCallback("on_drag_end", &onDragEnd);
+
+ // Properties
+ gobject.ext.registerProperties(class, &.{
+ properties.active.impl,
+ properties.@"search-total".impl,
+ properties.@"has-search-total".impl,
+ properties.@"search-selected".impl,
+ properties.@"has-search-selected".impl,
+ properties.@"halign-target".impl,
+ properties.@"valign-target".impl,
+ });
+
+ // Signals
+ signals.@"stop-search".impl.register(.{});
+ signals.@"search-changed".impl.register(.{});
+ signals.@"next-match".impl.register(.{});
+ signals.@"previous-match".impl.register(.{});
+
+ // Virtual methods
+ gobject.Object.virtual_methods.dispose.implement(class, &dispose);
+ gobject.Object.virtual_methods.finalize.implement(class, &finalize);
+ }
+
+ pub const as = C.Class.as;
+ pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
+ pub const bindTemplateCallback = C.Class.bindTemplateCallback;
+ };
+};
diff --git a/vendor/libghostty-vt/src/apprt/gtk/class/split_tree.zig b/vendor/libghostty-vt/src/apprt/gtk/class/split_tree.zig
new file mode 100644
index 00000000..311fbd8a
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/class/split_tree.zig
@@ -0,0 +1,1290 @@
+const std = @import("std");
+const assert = @import("../../../quirks.zig").inlineAssert;
+const Allocator = std.mem.Allocator;
+const adw = @import("adw");
+const gio = @import("gio");
+const glib = @import("glib");
+const gobject = @import("gobject");
+const gtk = @import("gtk");
+
+const configpkg = @import("../../../config.zig");
+const apprt = @import("../../../apprt.zig");
+const ext = @import("../ext.zig");
+const gresource = @import("../build/gresource.zig");
+const Common = @import("../class.zig").Common;
+const WeakRef = @import("../weak_ref.zig").WeakRef;
+const Application = @import("application.zig").Application;
+const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog;
+const Surface = @import("surface.zig").Surface;
+const SurfaceScrolledWindow = @import("surface_scrolled_window.zig").SurfaceScrolledWindow;
+
+const log = std.log.scoped(.gtk_ghostty_split_tree);
+
+pub const SplitTree = extern struct {
+ const Self = @This();
+ parent_instance: Parent,
+ pub const Parent = gtk.Box;
+ pub const getGObjectType = gobject.ext.defineClass(Self, .{
+ .name = "GhosttySplitTree",
+ .instanceInit = &init,
+ .classInit = &Class.init,
+ .parent_class = &Class.parent,
+ .private = .{ .Type = Private, .offset = &Private.offset },
+ });
+
+ pub const properties = struct {
+ /// The active surface is the surface that should be receiving all
+ /// surface-targeted actions. This is usually the focused surface,
+ /// but may also not be focused if the user has selected a non-surface
+ /// widget.
+ pub const @"active-surface" = struct {
+ pub const name = "active-surface";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?*Surface,
+ .{
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ ?*Surface,
+ .{
+ .getter = getActiveSurface,
+ },
+ ),
+ },
+ );
+ };
+
+ pub const @"has-surfaces" = struct {
+ pub const name = "has-surfaces";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ bool,
+ .{
+ .default = false,
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ bool,
+ .{
+ .getter = getHasSurfaces,
+ },
+ ),
+ },
+ );
+ };
+
+ pub const @"is-zoomed" = struct {
+ pub const name = "is-zoomed";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ bool,
+ .{
+ .default = false,
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ bool,
+ .{
+ .getter = getIsZoomed,
+ },
+ ),
+ },
+ );
+ };
+
+ pub const tree = struct {
+ pub const name = "tree";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?*Surface.Tree,
+ .{
+ .accessor = .{
+ .getter = getTreeValue,
+ .setter = setTreeValue,
+ },
+ },
+ );
+ };
+
+ pub const @"is-split" = struct {
+ pub const name = "is-split";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ bool,
+ .{
+ .default = false,
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ bool,
+ .{
+ .getter = getIsSplit,
+ },
+ ),
+ },
+ );
+ };
+ };
+
+ pub const signals = struct {
+ /// Emitted whenever the tree property has changed, with access
+ /// to the previous and new values.
+ pub const changed = struct {
+ pub const name = "changed";
+ pub const connect = impl.connect;
+ const impl = gobject.ext.defineSignal(
+ name,
+ Self,
+ &.{ ?*const Surface.Tree, ?*const Surface.Tree },
+ void,
+ );
+ };
+ };
+
+ const Private = struct {
+ /// The tree datastructure containing all of our surface views.
+ tree: ?*Surface.Tree,
+
+ // Template bindings
+ tree_bin: *adw.Bin,
+
+ /// Last focused surface in the tree. We need this to handle various
+ /// tree change states.
+ last_focused: WeakRef(Surface) = .empty,
+
+ /// The source that we use to rebuild the tree. This is also
+ /// used to debounce updates.
+ rebuild_source: ?c_uint = null,
+
+ /// Used to store state about a pending surface close for the
+ /// close dialog.
+ pending_close: ?Surface.Tree.Node.Handle,
+
+ pub var offset: c_int = 0;
+ };
+
+ fn init(self: *Self, _: *Class) callconv(.c) void {
+ gtk.Widget.initTemplate(self.as(gtk.Widget));
+
+ // Initialize our actions
+ self.initActionMap();
+
+ // Initialize some basic state
+ const priv = self.private();
+ priv.pending_close = null;
+ }
+
+ fn initActionMap(self: *Self) void {
+ const s_variant_type = glib.ext.VariantType.newFor([:0]const u8);
+ defer s_variant_type.free();
+
+ const actions = [_]ext.actions.Action(Self){
+ // All of these will eventually take a target surface parameter.
+ // For now all our targets originate from the focused surface.
+ .init("new-split", actionNewSplit, s_variant_type),
+ .init("equalize", actionEqualize, null),
+ .init("zoom", actionZoom, null),
+ };
+
+ _ = ext.actions.addAsGroup(Self, self, "split-tree", &actions);
+ }
+
+ /// Create a new split in the given direction from the currently
+ /// active surface.
+ ///
+ /// If the tree is empty this will create a new tree with a new surface
+ /// and ignore the direction.
+ ///
+ /// The parent will be used as the parent of the surface regardless of
+ /// if that parent is in this split tree or not. This allows inheriting
+ /// surface properties from anywhere.
+ pub fn newSplit(
+ self: *Self,
+ direction: Surface.Tree.Split.Direction,
+ parent_: ?*Surface,
+ overrides: struct {
+ command: ?configpkg.Command = null,
+ working_directory: ?[:0]const u8 = null,
+ title: ?[:0]const u8 = null,
+
+ pub const none: @This() = .{};
+ },
+ ) Allocator.Error!void {
+ const alloc = Application.default().allocator();
+
+ // Create our new surface.
+ const surface: *Surface = .new(.{
+ .command = overrides.command,
+ .working_directory = overrides.working_directory,
+ .title = overrides.title,
+ });
+ defer surface.unref();
+ _ = surface.refSink();
+
+ // Inherit properly if we were asked to.
+ if (parent_) |p| {
+ if (p.core()) |core| {
+ surface.setParent(core, .split);
+ }
+ }
+
+ // Bind is-split property for new surface
+ _ = self.as(gobject.Object).bindProperty(
+ "is-split",
+ surface.as(gobject.Object),
+ "is-split",
+ .{ .sync_create = true },
+ );
+
+ // Create our tree
+ var single_tree = try Surface.Tree.init(alloc, surface);
+ defer single_tree.deinit();
+
+ // We want to move our focus to the new surface no matter what.
+ // But we need to be careful to restore state if we fail.
+ const old_last_focused = self.private().last_focused.get();
+ defer if (old_last_focused) |v| v.unref(); // unref strong ref from get
+ self.private().last_focused.set(surface);
+ errdefer self.private().last_focused.set(old_last_focused);
+
+ // If we have no tree yet, then this becomes our tree and we're done.
+ const old_tree = self.getTree() orelse {
+ self.setTree(&single_tree);
+ return;
+ };
+
+ // The handle we create the split relative to. Today this is the active
+ // surface but this might be the handle of the given parent if we want.
+ const handle = self.getActiveSurfaceHandle() orelse .root;
+
+ // Create our split!
+ var new_tree = try old_tree.split(
+ alloc,
+ handle,
+ direction,
+ 0.5, // Always split equally for new splits
+ &single_tree,
+ );
+ defer new_tree.deinit();
+ log.debug(
+ "new split at={} direction={} old_tree={f} new_tree={f}",
+ .{ handle, direction, old_tree, &new_tree },
+ );
+
+ // Replace our tree
+ self.setTree(&new_tree);
+ }
+
+ pub fn resize(
+ self: *Self,
+ direction: Surface.Tree.Split.Direction,
+ amount: u16,
+ ) Allocator.Error!bool {
+ // Avoid useless work
+ if (amount == 0) return false;
+
+ const old_tree = self.getTree() orelse return false;
+ const active = self.getActiveSurfaceHandle() orelse return false;
+
+ // Get all our dimensions we're going to need to turn our
+ // amount into a percentage.
+ const priv = self.private();
+ const width = priv.tree_bin.as(gtk.Widget).getWidth();
+ const height = priv.tree_bin.as(gtk.Widget).getHeight();
+ if (width == 0 or height == 0) return false;
+ const width_f64: f64 = @floatFromInt(width);
+ const height_f64: f64 = @floatFromInt(height);
+ const amount_f64: f64 = @floatFromInt(amount);
+
+ // Get our ratio and use positive/neg for directions.
+ const ratio: f64 = switch (direction) {
+ .right => amount_f64 / width_f64,
+ .left => -(amount_f64 / width_f64),
+ .down => amount_f64 / height_f64,
+ .up => -(amount_f64 / height_f64),
+ };
+
+ const layout: Surface.Tree.Split.Layout = switch (direction) {
+ .left, .right => .horizontal,
+ .up, .down => .vertical,
+ };
+
+ var new_tree = try old_tree.resize(
+ Application.default().allocator(),
+ active,
+ layout,
+ @floatCast(ratio),
+ );
+ defer new_tree.deinit();
+ self.setTree(&new_tree);
+ return true;
+ }
+
+ /// Move focus from the currently focused surface to the given
+ /// direction. Returns true if focus switched to a new surface.
+ pub fn goto(self: *Self, to: Surface.Tree.Goto) bool {
+ const tree = self.getTree() orelse return false;
+ const active = self.getActiveSurfaceHandle() orelse return false;
+ const target = if (tree.goto(
+ Application.default().allocator(),
+ active,
+ to,
+ )) |handle_|
+ handle_ orelse return false
+ else |err| switch (err) {
+ // Nothing we can do in this scenario. This is highly unlikely
+ // since split trees don't use that much memory. The application
+ // is probably about to crash in other ways.
+ error.OutOfMemory => return false,
+ };
+
+ // If we aren't changing targets then we did nothing.
+ if (active == target) return false;
+
+ // Get the surface at the target location and grab focus.
+ const surface = tree.nodes[target.idx()].leaf;
+ surface.grabFocus();
+
+ // We also need to setup our last_focused to this because if we
+ // trigger a tree change like below, the grab focus above never
+ // actually triggers in time to set this and this ensures we
+ // grab focus to the right thing.
+ const old_last_focused = self.private().last_focused.get();
+ defer if (old_last_focused) |v| v.unref(); // unref strong ref from get
+ self.private().last_focused.set(surface);
+ errdefer self.private().last_focused.set(old_last_focused);
+
+ if (tree.zoomed != null) {
+ const app = Application.default();
+ const config_obj = app.getConfig();
+ defer config_obj.unref();
+ const config = config_obj.get();
+
+ if (!config.@"split-preserve-zoom".navigation) {
+ tree.zoomed = null;
+ } else {
+ tree.zoom(target);
+ }
+
+ // When the zoom state changes our tree state changes and
+ // we need to send the proper notifications to trigger
+ // relayout.
+ const object = self.as(gobject.Object);
+ object.notifyByPspec(properties.tree.impl.param_spec);
+ object.notifyByPspec(properties.@"is-zoomed".impl.param_spec);
+ }
+
+ return true;
+ }
+
+ fn disconnectSurfaceHandlers(self: *Self) void {
+ const tree = self.getTree() orelse return;
+ var it = tree.iterator();
+ while (it.next()) |entry| {
+ const surface = entry.view;
+ _ = gobject.signalHandlersDisconnectMatched(
+ surface.as(gobject.Object),
+ .{ .data = true },
+ 0,
+ 0,
+ null,
+ null,
+ self,
+ );
+ }
+ }
+
+ fn connectSurfaceHandlers(self: *Self) void {
+ const tree = self.getTree() orelse return;
+ var it = tree.iterator();
+ while (it.next()) |entry| {
+ const surface = entry.view;
+ _ = Surface.signals.@"close-request".connect(
+ surface,
+ *Self,
+ surfaceCloseRequest,
+ self,
+ .{},
+ );
+ _ = gobject.Object.signals.notify.connect(
+ surface,
+ *Self,
+ propSurfaceFocused,
+ self,
+ .{ .detail = "focused" },
+ );
+ }
+ }
+
+ //---------------------------------------------------------------
+ // Properties
+
+ /// Returns true if this split tree needs confirmation before quitting based
+ /// on the various Ghostty configurations.
+ pub fn getNeedsConfirmQuit(self: *Self) bool {
+ const tree = self.getTree() orelse return false;
+ var it = tree.iterator();
+ while (it.next()) |entry| {
+ if (entry.view.core()) |core| {
+ if (core.needsConfirmQuit()) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /// Get the currently active surface. See the "active-surface" property.
+ /// This does not ref the value.
+ pub fn getActiveSurface(self: *Self) ?*Surface {
+ const tree = self.getTree() orelse return null;
+ const handle = self.getActiveSurfaceHandle() orelse return null;
+ return tree.nodes[handle.idx()].leaf;
+ }
+
+ fn getActiveSurfaceHandle(self: *Self) ?Surface.Tree.Node.Handle {
+ const tree = self.getTree() orelse return null;
+ var it = tree.iterator();
+ while (it.next()) |entry| {
+ if (entry.view.getFocused()) return entry.handle;
+ }
+
+ // If none are currently focused, the most previously focused
+ // surface (if it exists) is our active surface. This lets things
+ // like apprt actions and bell ringing continue to work in the
+ // background.
+ if (self.private().last_focused.get()) |v| {
+ defer v.unref();
+
+ // We need to find the handle of the last focused surface.
+ it = tree.iterator();
+ while (it.next()) |entry| {
+ if (entry.view == v) return entry.handle;
+ }
+ }
+
+ return null;
+ }
+
+ /// Returns the last focused surface in the tree.
+ pub fn getLastFocusedSurface(self: *Self) ?*Surface {
+ const surface = self.private().last_focused.get() orelse return null;
+ // We unref because get() refs the surface. We don't use the weakref
+ // in a multi-threaded context so this is safe.
+ surface.unref();
+ return surface;
+ }
+
+ pub fn getHasSurfaces(self: *Self) bool {
+ const tree: *const Surface.Tree = self.private().tree orelse &.empty;
+ return !tree.isEmpty();
+ }
+
+ pub fn getIsZoomed(self: *Self) bool {
+ const tree: *const Surface.Tree = self.private().tree orelse &.empty;
+ return tree.zoomed != null;
+ }
+
+ /// Get the tree data model that we're showing in this widget. This
+ /// does not clone the tree.
+ pub fn getTree(self: *Self) ?*Surface.Tree {
+ return self.private().tree;
+ }
+
+ /// Set the tree data model that we're showing in this widget. This
+ /// will clone the given tree.
+ pub fn setTree(self: *Self, tree_: ?*const Surface.Tree) void {
+ const priv = self.private();
+
+ // We always normalize our tree parameter so that empty trees
+ // become null so that we don't have to deal with callers being
+ // confused about that.
+ const tree: ?*const Surface.Tree = tree: {
+ const tree = tree_ orelse break :tree null;
+ if (tree.isEmpty()) break :tree null;
+ break :tree tree;
+ };
+
+ // Emit the signal so that handlers can witness both the before and
+ // after values of the tree.
+ signals.changed.impl.emit(
+ self,
+ null,
+ .{ priv.tree, tree },
+ null,
+ );
+
+ if (priv.tree) |old_tree| {
+ self.disconnectSurfaceHandlers();
+ ext.boxedFree(Surface.Tree, old_tree);
+ priv.tree = null;
+ }
+
+ if (tree) |new_tree| {
+ assert(priv.tree == null);
+ assert(!new_tree.isEmpty());
+ priv.tree = ext.boxedCopy(Surface.Tree, new_tree);
+ self.connectSurfaceHandlers();
+ }
+
+ self.as(gobject.Object).notifyByPspec(properties.tree.impl.param_spec);
+ }
+
+ fn getTreeValue(self: *Self, value: *gobject.Value) void {
+ gobject.ext.Value.set(
+ value,
+ self.private().tree,
+ );
+ }
+
+ fn setTreeValue(self: *Self, value: *const gobject.Value) void {
+ self.setTree(gobject.ext.Value.get(
+ value,
+ ?*Surface.Tree,
+ ));
+ }
+
+ pub fn getIsSplit(self: *Self) bool {
+ const tree: *const Surface.Tree = self.private().tree orelse &.empty;
+ if (tree.isEmpty()) return false;
+
+ const root_handle: Surface.Tree.Node.Handle = .root;
+ const root = tree.nodes[root_handle.idx()];
+ return switch (root) {
+ .leaf => false,
+ .split => true,
+ };
+ }
+
+ //---------------------------------------------------------------
+ // Virtual methods
+
+ fn dispose(self: *Self) callconv(.c) void {
+ const priv = self.private();
+ priv.last_focused.set(null);
+ if (priv.rebuild_source) |v| {
+ if (glib.Source.remove(v) == 0) {
+ log.warn("unable to remove rebuild source", .{});
+ }
+ priv.rebuild_source = null;
+ }
+
+ gtk.Widget.disposeTemplate(
+ self.as(gtk.Widget),
+ getGObjectType(),
+ );
+
+ gobject.Object.virtual_methods.dispose.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ fn finalize(self: *Self) callconv(.c) void {
+ const priv = self.private();
+ if (priv.tree) |tree| {
+ ext.boxedFree(Surface.Tree, tree);
+ priv.tree = null;
+ }
+
+ gobject.Object.virtual_methods.finalize.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ //---------------------------------------------------------------
+ // Signal handlers
+
+ pub fn actionNewSplit(
+ _: *gio.SimpleAction,
+ args_: ?*glib.Variant,
+ self: *Self,
+ ) callconv(.c) void {
+ const args = args_ orelse {
+ log.warn("split-tree.new-split called without a parameter", .{});
+ return;
+ };
+
+ var dir: ?[*:0]const u8 = null;
+ args.get("&s", &dir);
+
+ const direction = std.meta.stringToEnum(
+ Surface.Tree.Split.Direction,
+ std.mem.span(dir) orelse return,
+ ) orelse {
+ // Need to be defensive here since actions can be triggered externally.
+ log.warn("invalid split direction for split-tree.new-split: {s}", .{dir.?});
+ return;
+ };
+
+ self.newSplit(
+ direction,
+ self.getActiveSurface(),
+ .none,
+ ) catch |err| {
+ log.warn("new split failed error={}", .{err});
+ };
+ }
+
+ pub fn actionEqualize(
+ _: *gio.SimpleAction,
+ parameter_: ?*glib.Variant,
+ self: *Self,
+ ) callconv(.c) void {
+ _ = parameter_;
+
+ const old_tree = self.getTree() orelse return;
+ var new_tree = old_tree.equalize(Application.default().allocator()) catch |err| {
+ log.warn("unable to equalize tree: {}", .{err});
+ return;
+ };
+ defer new_tree.deinit();
+ self.setTree(&new_tree);
+ }
+
+ pub fn actionZoom(
+ _: *gio.SimpleAction,
+ _: ?*glib.Variant,
+ self: *Self,
+ ) callconv(.c) void {
+ const tree = self.getTree() orelse return;
+ if (tree.zoomed != null) {
+ tree.zoomed = null;
+ } else {
+ const active = self.getActiveSurfaceHandle() orelse return;
+ if (tree.zoomed == active) return;
+ tree.zoom(active);
+ }
+
+ self.as(gobject.Object).notifyByPspec(properties.tree.impl.param_spec);
+ }
+
+ fn surfaceCloseRequest(
+ surface: *Surface,
+ self: *Self,
+ ) callconv(.c) void {
+ const core = surface.core() orelse return;
+
+ // Reset our pending close state
+ const priv = self.private();
+ priv.pending_close = null;
+
+ // Find the surface in the tree to verify this is valid and
+ // set our pending close handle.
+ priv.pending_close = handle: {
+ const tree = self.getTree() orelse return;
+ var it = tree.iterator();
+ while (it.next()) |entry| {
+ if (entry.view == surface) {
+ break :handle entry.handle;
+ }
+ }
+
+ return;
+ };
+
+ // If we don't need to confirm then just close immediately.
+ if (!core.needsConfirmQuit()) {
+ closeConfirmationClose(
+ null,
+ self,
+ );
+ return;
+ }
+
+ // Show a confirmation dialog
+ const dialog: *CloseConfirmationDialog = .new(.surface);
+ _ = CloseConfirmationDialog.signals.@"close-request".connect(
+ dialog,
+ *Self,
+ closeConfirmationClose,
+ self,
+ .{},
+ );
+ dialog.present(self.as(gtk.Widget));
+ }
+
+ fn closeConfirmationClose(
+ _: ?*CloseConfirmationDialog,
+ self: *Self,
+ ) callconv(.c) void {
+ // Get the handle we're closing
+ const priv = self.private();
+ const handle = priv.pending_close orelse return;
+ priv.pending_close = null;
+
+ // Figure out our next focus target. The next focus target is
+ // always the "previous" surface unless we're the leftmost then
+ // its the next.
+ const old_tree = self.getTree() orelse return;
+ const next_focus: ?*Surface = next_focus: {
+ const alloc = Application.default().allocator();
+ const next_handle: Surface.Tree.Node.Handle =
+ (old_tree.goto(alloc, handle, .previous) catch null) orelse
+ (old_tree.goto(alloc, handle, .next) catch null) orelse
+ break :next_focus null;
+ if (next_handle == handle) break :next_focus null;
+
+ // Note: we don't need to ref this or anything because its
+ // guaranteed to remain in the new tree since its not part
+ // of the handle we're removing.
+ break :next_focus old_tree.nodes[next_handle.idx()].leaf;
+ };
+
+ // Remove it from the tree.
+ var new_tree = old_tree.remove(
+ Application.default().allocator(),
+ handle,
+ ) catch |err| {
+ log.warn("unable to remove surface from tree: {}", .{err});
+ return;
+ };
+ defer new_tree.deinit();
+ self.setTree(&new_tree);
+
+ // Grab focus. We have to set this on the "last focused" because our
+ // focus will be set when the tree is redrawn.
+ if (next_focus) |v| priv.last_focused.set(v);
+ }
+
+ fn propSurfaceFocused(
+ surface: *Surface,
+ _: *gobject.ParamSpec,
+ self: *Self,
+ ) callconv(.c) void {
+ // We never CLEAR our last_focused because the property is specifically
+ // the last focused surface. We let the weakref clear itself when
+ // the surface is destroyed.
+ if (!surface.getFocused()) return;
+ self.private().last_focused.set(surface);
+
+ // Our active surface probably changed
+ self.as(gobject.Object).notifyByPspec(properties.@"active-surface".impl.param_spec);
+ }
+
+ fn propTree(
+ self: *Self,
+ _: *gobject.ParamSpec,
+ _: ?*anyopaque,
+ ) callconv(.c) void {
+ const priv = self.private();
+
+ // No matter what we notify
+ self.as(gobject.Object).freezeNotify();
+ defer self.as(gobject.Object).thawNotify();
+ self.as(gobject.Object).notifyByPspec(properties.@"has-surfaces".impl.param_spec);
+ self.as(gobject.Object).notifyByPspec(properties.@"is-zoomed".impl.param_spec);
+
+ // If we were planning a rebuild, always remove that so we can
+ // start from a clean slate.
+ if (priv.rebuild_source) |v| {
+ if (glib.Source.remove(v) == 0) {
+ log.warn("unable to remove rebuild source", .{});
+ }
+ priv.rebuild_source = null;
+ }
+
+ // If we transitioned to an empty tree, clear immediately instead of
+ // waiting for an idle callback. Delaying teardown can keep the last
+ // surface alive during shutdown if the main loop exits first.
+ if (priv.tree == null) {
+ priv.tree_bin.setChild(null);
+ return;
+ }
+
+ // Build on an idle callback so rapid tree changes are debounced.
+ // We keep the existing tree attached until the rebuild runs,
+ // which avoids transient empty frames.
+ assert(priv.rebuild_source == null);
+ priv.rebuild_source = glib.idleAdd(
+ onRebuild,
+ self,
+ );
+ }
+
+ fn onRebuild(ud: ?*anyopaque) callconv(.c) c_int {
+ const self: *Self = @ptrCast(@alignCast(ud orelse return 0));
+
+ // Always mark our rebuild source as null since we're done.
+ const priv = self.private();
+ priv.rebuild_source = null;
+
+ // Rebuild our tree
+ const tree: *const Surface.Tree = self.private().tree orelse &.empty;
+ if (tree.isEmpty()) {
+ priv.tree_bin.setChild(null);
+ } else {
+ const built = self.buildTree(
+ tree,
+ tree.zoomed orelse .root,
+ );
+ defer built.deinit();
+ priv.tree_bin.setChild(built.widget);
+ }
+
+ // Replacing our tree widget hierarchy can reset focus state.
+ // If we have a last-focused surface, restore focus to it.
+ if (priv.last_focused.get()) |v| {
+ defer v.unref();
+ v.grabFocus();
+ }
+
+ // Our split status may have changed
+ self.as(gobject.Object).notifyByPspec(properties.@"is-split".impl.param_spec);
+
+ // Our active surface may have changed
+ self.as(gobject.Object).notifyByPspec(properties.@"active-surface".impl.param_spec);
+
+ return 0;
+ }
+
+ /// Builds the widget tree associated with a surface split tree.
+ ///
+ /// Returned widgets are expected to be attached to a parent by the caller.
+ ///
+ /// If `release_ref` is true then `widget` has an extra temporary
+ /// reference that must be released once it is parented in the rebuilt
+ /// tree.
+ const BuildTreeResult = struct {
+ widget: *gtk.Widget,
+ release_ref: bool,
+
+ pub fn initNew(widget: *gtk.Widget) BuildTreeResult {
+ return .{ .widget = widget, .release_ref = false };
+ }
+
+ pub fn initReused(widget: *gtk.Widget) BuildTreeResult {
+ // We add a temporary ref to the widget to ensure it doesn't
+ // get destroyed while we're rebuilding the tree and detaching
+ // it from its old parent. The caller is expected to release
+ // this ref once the widget is attached to its new parent.
+ _ = widget.as(gobject.Object).ref();
+
+ // Detach after we ref it so that this doesn't mark the
+ // widget for destruction.
+ detachWidget(widget);
+
+ return .{ .widget = widget, .release_ref = true };
+ }
+
+ pub fn deinit(self: BuildTreeResult) void {
+ // If we have to release a ref, do it.
+ if (self.release_ref) self.widget.as(gobject.Object).unref();
+ }
+ };
+
+ fn buildTree(
+ self: *Self,
+ tree: *const Surface.Tree,
+ current: Surface.Tree.Node.Handle,
+ ) BuildTreeResult {
+ return switch (tree.nodes[current.idx()]) {
+ .leaf => |v| leaf: {
+ const window = ext.getAncestor(
+ SurfaceScrolledWindow,
+ v.as(gtk.Widget),
+ ) orelse {
+ // The surface isn't in a window already so we don't
+ // have to worry about reuse.
+ break :leaf .initNew(gobject.ext.newInstance(
+ SurfaceScrolledWindow,
+ .{ .surface = v },
+ ).as(gtk.Widget));
+ };
+
+ // Keep this widget alive while we detach it from the
+ // old tree and adopt it into the new one.
+ break :leaf .initReused(window.as(gtk.Widget));
+ },
+ .split => |s| split: {
+ const left = self.buildTree(tree, s.left);
+ defer left.deinit();
+ const right = self.buildTree(tree, s.right);
+ defer right.deinit();
+
+ break :split .initNew(SplitTreeSplit.new(
+ current,
+ &s,
+ left.widget,
+ right.widget,
+ ).as(gtk.Widget));
+ },
+ };
+ }
+
+ /// Detach a split widget from its current parent.
+ ///
+ /// We intentionally use parent-specific child APIs when possible
+ /// (`GtkPaned.setStartChild/setEndChild`, `AdwBin.setChild`) instead of
+ /// calling `gtk.Widget.unparent` directly. Container implementations track
+ /// child pointers/properties internally, and those setters are the path
+ /// that keeps container state and notifications in sync.
+ fn detachWidget(widget: *gtk.Widget) void {
+ const parent = widget.getParent() orelse return;
+
+ // Surface will be in a paned when it is split.
+ if (gobject.ext.cast(gtk.Paned, parent)) |paned| {
+ if (paned.getStartChild()) |child| {
+ if (child == widget) {
+ paned.setStartChild(null);
+ return;
+ }
+ }
+
+ if (paned.getEndChild()) |child| {
+ if (child == widget) {
+ paned.setEndChild(null);
+ return;
+ }
+ }
+ }
+
+ // Surface will be in a bin when it is not split.
+ if (gobject.ext.cast(adw.Bin, parent)) |bin| {
+ if (bin.getChild()) |child| {
+ if (child == widget) {
+ bin.setChild(null);
+ return;
+ }
+ }
+ }
+
+ // Fallback for unexpected parents where we don't have a typed
+ // container API available.
+ widget.unparent();
+ }
+
+ //---------------------------------------------------------------
+ // Class
+
+ const C = Common(Self, Private);
+ pub const as = C.as;
+ pub const ref = C.ref;
+ pub const unref = C.unref;
+ const private = C.private;
+
+ pub const Class = extern struct {
+ parent_class: Parent.Class,
+ var parent: *Parent.Class = undefined;
+ pub const Instance = Self;
+
+ fn init(class: *Class) callconv(.c) void {
+ gobject.ext.ensureType(Surface);
+ gtk.Widget.Class.setTemplateFromResource(
+ class.as(gtk.Widget.Class),
+ comptime gresource.blueprint(.{
+ .major = 1,
+ .minor = 5,
+ .name = "split-tree",
+ }),
+ );
+
+ // Properties
+ gobject.ext.registerProperties(class, &.{
+ properties.@"active-surface".impl,
+ properties.@"has-surfaces".impl,
+ properties.@"is-zoomed".impl,
+ properties.tree.impl,
+ properties.@"is-split".impl,
+ });
+
+ // Bindings
+ class.bindTemplateChildPrivate("tree_bin", .{});
+
+ // Template Callbacks
+ class.bindTemplateCallback("notify_tree", &propTree);
+
+ // Signals
+ signals.changed.impl.register(.{});
+
+ // Virtual methods
+ gobject.Object.virtual_methods.dispose.implement(class, &dispose);
+ gobject.Object.virtual_methods.finalize.implement(class, &finalize);
+ }
+
+ pub const as = C.Class.as;
+ pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
+ pub const bindTemplateCallback = C.Class.bindTemplateCallback;
+ };
+};
+
+/// This is an internal-only widget that represents a split in the
+/// split tree. This is a wrapper around gtk.Paned that allows us to handle
+/// ratio (0 to 1) based positioning of the split, and also allows us to
+/// write back the updated ratio to the split tree when the user manually
+/// adjusts the split position.
+///
+/// Since this is internal, it expects to be nested within a SplitTree and
+/// will use `getAncestor` to find the SplitTree it belongs to.
+///
+/// This is an _immutable_ widget. It isn't meant to be updated after
+/// creation. As such, there are no properties or APIs to change the split,
+/// access the paned, etc.
+const SplitTreeSplit = extern struct {
+ const Self = @This();
+ parent_instance: Parent,
+ pub const Parent = adw.Bin;
+ pub const getGObjectType = gobject.ext.defineClass(Self, .{
+ .name = "GhosttySplitTreeSplit",
+ .instanceInit = &init,
+ .classInit = &Class.init,
+ .parent_class = &Class.parent,
+ .private = .{ .Type = Private, .offset = &Private.offset },
+ });
+
+ const Private = struct {
+ /// The handle of the node in the tree that this split represents.
+ /// Assumed to be correct.
+ handle: Surface.Tree.Node.Handle,
+
+ /// Source to handle repositioning the split when properties change.
+ idle: ?c_uint = null,
+
+ // Template bindings
+ paned: *gtk.Paned,
+
+ pub var offset: c_int = 0;
+ };
+
+ /// Create a new split.
+ ///
+ /// The reason we don't use GObject properties here is because this is
+ /// an immutable widget and we don't want to deal with the overhead of
+ /// all the boilerplate for properties, signals, bindings, etc.
+ pub fn new(
+ handle: Surface.Tree.Node.Handle,
+ split: *const Surface.Tree.Split,
+ start_child: *gtk.Widget,
+ end_child: *gtk.Widget,
+ ) *Self {
+ const self = gobject.ext.newInstance(Self, .{});
+ const priv = self.private();
+ priv.handle = handle;
+
+ // Setup our paned fields
+ const paned = priv.paned;
+ paned.setStartChild(start_child);
+ paned.setEndChild(end_child);
+ paned.as(gtk.Orientable).setOrientation(switch (split.layout) {
+ .horizontal => .horizontal,
+ .vertical => .vertical,
+ });
+
+ // Signals and so on are setup in the template.
+
+ return self;
+ }
+
+ fn init(self: *Self, _: *Class) callconv(.c) void {
+ gtk.Widget.initTemplate(self.as(gtk.Widget));
+ }
+
+ fn refresh(self: *Self) void {
+ const priv = self.private();
+ if (priv.idle == null) priv.idle = glib.idleAdd(
+ onIdle,
+ self,
+ );
+ }
+
+ fn onIdle(ud: ?*anyopaque) callconv(.c) c_int {
+ const self: *Self = @ptrCast(@alignCast(ud orelse return 0));
+ const priv = self.private();
+ const paned = priv.paned;
+
+ // Our idle source is always over
+ priv.idle = null;
+
+ // Get our split. This is the most dangerous part of this entire
+ // widget. We assume that this widget is always a child of a
+ // SplitTree, we assume that our handle is valid, and we assume
+ // the handle is always a split node.
+ const split_tree = ext.getAncestor(
+ SplitTree,
+ self.as(gtk.Widget),
+ ) orelse return 0;
+ const tree = split_tree.getTree() orelse return 0;
+ const split: *const Surface.Tree.Split = &tree.nodes[priv.handle.idx()].split;
+
+ // Current, min, and max positions as pixels.
+ const pos = paned.getPosition();
+ const min = min: {
+ var val = gobject.ext.Value.new(c_int);
+ defer val.unset();
+ gobject.Object.getProperty(
+ paned.as(gobject.Object),
+ "min-position",
+ &val,
+ );
+ break :min gobject.ext.Value.get(&val, c_int);
+ };
+ const max = max: {
+ var val = gobject.ext.Value.new(c_int);
+ defer val.unset();
+ gobject.Object.getProperty(
+ paned.as(gobject.Object),
+ "max-position",
+ &val,
+ );
+ break :max gobject.ext.Value.get(&val, c_int);
+ };
+ const pos_set: bool = max: {
+ var val = gobject.ext.Value.new(c_int);
+ defer val.unset();
+ gobject.Object.getProperty(
+ paned.as(gobject.Object),
+ "position-set",
+ &val,
+ );
+ break :max gobject.ext.Value.get(&val, c_int) != 0;
+ };
+
+ // We don't actually use min, but we don't expect this to ever
+ // be non-zero, so let's add an assert to ensure that.
+ assert(min == 0);
+
+ // If our max is zero then we can't do any math. I don't know
+ // if this is possible but I suspect it can be if you make a nested
+ // split completely minimized.
+ if (max == 0) return 0;
+
+ // Determine our current ratio.
+ const current_ratio: f64 = ratio: {
+ const pos_f64: f64 = @floatFromInt(pos);
+ const max_f64: f64 = @floatFromInt(max);
+ break :ratio pos_f64 / max_f64;
+ };
+ const desired_ratio: f64 = @floatCast(split.ratio);
+
+ // If our ratio is close enough to our desired ratio, then
+ // we ignore the update. This is to avoid constant split updates
+ // for lossy floating point math.
+ if (std.math.approxEqAbs(
+ f64,
+ current_ratio,
+ desired_ratio,
+ 0.001,
+ )) {
+ return 0;
+ }
+
+ // If we're out of bounds, then we need to either set the position
+ // to what we expect OR update our expected ratio.
+
+ // If we've never set the position, then we set it to the desired.
+ if (!pos_set) {
+ const desired_pos: c_int = desired_pos: {
+ const max_f64: f64 = @floatFromInt(max);
+ break :desired_pos @intFromFloat(@round(max_f64 * desired_ratio));
+ };
+ paned.setPosition(desired_pos);
+ return 0;
+ }
+
+ // If we've set the position, then this is a manual human update
+ // and we need to write our update back to the tree.
+ tree.resizeInPlace(priv.handle, @floatCast(current_ratio));
+
+ return 0;
+ }
+
+ //---------------------------------------------------------------
+ // Signal handlers
+
+ fn propPosition(
+ _: *gtk.Paned,
+ _: *gobject.ParamSpec,
+ self: *Self,
+ ) callconv(.c) void {
+ self.refresh();
+ }
+
+ fn propMaxPosition(
+ _: *gtk.Paned,
+ _: *gobject.ParamSpec,
+ self: *Self,
+ ) callconv(.c) void {
+ self.refresh();
+ }
+
+ fn propMinPosition(
+ _: *gtk.Paned,
+ _: *gobject.ParamSpec,
+ self: *Self,
+ ) callconv(.c) void {
+ self.refresh();
+ }
+
+ //---------------------------------------------------------------
+ // Virtual methods
+
+ fn dispose(self: *Self) callconv(.c) void {
+ const priv = self.private();
+ if (priv.idle) |v| {
+ if (glib.Source.remove(v) == 0) {
+ log.warn("unable to remove idle source", .{});
+ }
+ priv.idle = null;
+ }
+
+ gtk.Widget.disposeTemplate(
+ self.as(gtk.Widget),
+ getGObjectType(),
+ );
+
+ gobject.Object.virtual_methods.dispose.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ fn finalize(self: *Self) callconv(.c) void {
+ gobject.Object.virtual_methods.finalize.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ const C = Common(Self, Private);
+ pub const as = C.as;
+ pub const ref = C.ref;
+ pub const unref = C.unref;
+ const private = C.private;
+
+ pub const Class = extern struct {
+ parent_class: Parent.Class,
+ var parent: *Parent.Class = undefined;
+ pub const Instance = Self;
+
+ fn init(class: *Class) callconv(.c) void {
+ gtk.Widget.Class.setTemplateFromResource(
+ class.as(gtk.Widget.Class),
+ comptime gresource.blueprint(.{
+ .major = 1,
+ .minor = 5,
+ .name = "split-tree-split",
+ }),
+ );
+
+ // Bindings
+ class.bindTemplateChildPrivate("paned", .{});
+
+ // Template Callbacks
+ class.bindTemplateCallback("notify_max_position", &propMaxPosition);
+ class.bindTemplateCallback("notify_min_position", &propMinPosition);
+ class.bindTemplateCallback("notify_position", &propPosition);
+
+ // Virtual methods
+ gobject.Object.virtual_methods.dispose.implement(class, &dispose);
+ gobject.Object.virtual_methods.finalize.implement(class, &finalize);
+ }
+
+ pub const as = C.Class.as;
+ pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
+ pub const bindTemplateCallback = C.Class.bindTemplateCallback;
+ };
+};
diff --git a/vendor/libghostty-vt/src/apprt/gtk/class/surface.zig b/vendor/libghostty-vt/src/apprt/gtk/class/surface.zig
new file mode 100644
index 00000000..179c779d
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/class/surface.zig
@@ -0,0 +1,4049 @@
+const std = @import("std");
+const assert = @import("../../../quirks.zig").inlineAssert;
+const Allocator = std.mem.Allocator;
+const adw = @import("adw");
+const gdk = @import("gdk");
+const gio = @import("gio");
+const glib = @import("glib");
+const gobject = @import("gobject");
+const gtk = @import("gtk");
+
+const apprt = @import("../../../apprt.zig");
+const build_config = @import("../../../build_config.zig");
+const configpkg = @import("../../../config.zig");
+const datastruct = @import("../../../datastruct/main.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 CoreSurface = @import("../../../Surface.zig");
+const gresource = @import("../build/gresource.zig");
+const ext = @import("../ext.zig");
+const gsettings = @import("../gsettings.zig");
+const gtk_key = @import("../key.zig");
+const ApprtSurface = @import("../Surface.zig");
+const Common = @import("../class.zig").Common;
+const Application = @import("application.zig").Application;
+const Config = @import("config.zig").Config;
+const ResizeOverlay = @import("resize_overlay.zig").ResizeOverlay;
+const SearchOverlay = @import("search_overlay.zig").SearchOverlay;
+const KeyStateOverlay = @import("key_state_overlay.zig").KeyStateOverlay;
+const ChildExited = @import("surface_child_exited.zig").SurfaceChildExited;
+const ClipboardConfirmationDialog = @import("clipboard_confirmation_dialog.zig").ClipboardConfirmationDialog;
+const TitleDialog = @import("title_dialog.zig").TitleDialog;
+const Window = @import("window.zig").Window;
+const InspectorWindow = @import("inspector_window.zig").InspectorWindow;
+const i18n = @import("../../../os/i18n.zig");
+const media = @import("../media.zig");
+
+const log = std.log.scoped(.gtk_ghostty_surface);
+
+pub const Surface = extern struct {
+ const Self = @This();
+ parent_instance: Parent,
+ pub const Parent = adw.Bin;
+ pub const Implements = [_]type{gtk.Scrollable};
+ pub const getGObjectType = gobject.ext.defineClass(Self, .{
+ .name = "GhosttySurface",
+ .instanceInit = &init,
+ .classInit = &Class.init,
+ .parent_class = &Class.parent,
+ .private = .{ .Type = Private, .offset = &Private.offset },
+ .implements = &.{
+ gobject.ext.implement(gtk.Scrollable, .{}),
+ },
+ });
+
+ /// A SplitTree implementation that stores surfaces.
+ pub const Tree = datastruct.SplitTree(Self);
+
+ pub const properties = struct {
+ /// This property is set to true when the bell is ringing. Note that
+ /// this property will only emit a changed signal when there is a
+ /// full state change. If a bell is ringing and another bell event
+ /// comes through, the change notification will NOT be emitted.
+ ///
+ /// If you need to know every scenario the bell is triggered,
+ /// listen to the `bell` signal instead.
+ pub const @"bell-ringing" = struct {
+ pub const name = "bell-ringing";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ bool,
+ .{
+ .default = false,
+ .accessor = C.privateShallowFieldAccessor("bell_ringing"),
+ },
+ );
+ };
+
+ pub const config = struct {
+ pub const name = "config";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?*Config,
+ .{
+ .accessor = C.privateObjFieldAccessor("config"),
+ },
+ );
+ };
+
+ pub const @"child-exited" = struct {
+ pub const name = "child-exited";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ bool,
+ .{
+ .default = false,
+ .accessor = gobject.ext.privateFieldAccessor(
+ Self,
+ Private,
+ &Private.offset,
+ "child_exited",
+ ),
+ },
+ );
+ };
+
+ pub const @"default-size" = struct {
+ pub const name = "default-size";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?*Size,
+ .{
+ .accessor = C.privateBoxedFieldAccessor("default_size"),
+ },
+ );
+ };
+
+ pub const @"error" = struct {
+ pub const name = "error";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ bool,
+ .{
+ .default = false,
+ .accessor = gobject.ext.privateFieldAccessor(
+ Self,
+ Private,
+ &Private.offset,
+ "error",
+ ),
+ },
+ );
+ };
+
+ pub const @"font-size-request" = struct {
+ pub const name = "font-size-request";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?*font.face.DesiredSize,
+ .{
+ .accessor = C.privateBoxedFieldAccessor("font_size_request"),
+ },
+ );
+ };
+
+ pub const focused = struct {
+ pub const name = "focused";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ bool,
+ .{
+ .default = false,
+ .accessor = gobject.ext.privateFieldAccessor(
+ Self,
+ Private,
+ &Private.offset,
+ "focused",
+ ),
+ },
+ );
+ };
+
+ pub const @"min-size" = struct {
+ pub const name = "min-size";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?*Size,
+ .{
+ .accessor = C.privateBoxedFieldAccessor("min_size"),
+ },
+ );
+ };
+
+ pub const @"mouse-hidden" = struct {
+ pub const name = "mouse-hidden";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ bool,
+ .{
+ .default = false,
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ bool,
+ .{
+ .getter = getMouseHidden,
+ .setter = setMouseHidden,
+ },
+ ),
+ },
+ );
+ };
+
+ pub const @"mouse-shape" = struct {
+ pub const name = "mouse-shape";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ terminal.MouseShape,
+ .{
+ .default = .text,
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ terminal.MouseShape,
+ .{
+ .getter = getMouseShape,
+ .setter = setMouseShape,
+ },
+ ),
+ },
+ );
+ };
+
+ pub const @"mouse-hover-url" = struct {
+ pub const name = "mouse-hover-url";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?[:0]const u8,
+ .{
+ .default = null,
+ .accessor = C.privateStringFieldAccessor("mouse_hover_url"),
+ },
+ );
+ };
+
+ pub const pwd = struct {
+ pub const name = "pwd";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?[:0]const u8,
+ .{
+ .default = null,
+ .accessor = C.privateStringFieldAccessor("pwd"),
+ },
+ );
+ };
+
+ pub const title = struct {
+ pub const name = "title";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?[:0]const u8,
+ .{
+ .default = null,
+ .accessor = C.privateStringFieldAccessor("title"),
+ },
+ );
+ };
+
+ pub const @"title-override" = struct {
+ pub const name = "title-override";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?[:0]const u8,
+ .{
+ .default = null,
+ .accessor = C.privateStringFieldAccessor("title_override"),
+ },
+ );
+ };
+
+ pub const zoom = struct {
+ pub const name = "zoom";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ bool,
+ .{
+ .default = false,
+ .accessor = gobject.ext.privateFieldAccessor(
+ Self,
+ Private,
+ &Private.offset,
+ "zoom",
+ ),
+ },
+ );
+ };
+
+ pub const @"is-split" = struct {
+ pub const name = "is-split";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ bool,
+ .{
+ .default = false,
+ .accessor = gobject.ext.privateFieldAccessor(
+ Self,
+ Private,
+ &Private.offset,
+ "is_split",
+ ),
+ },
+ );
+ };
+
+ pub const hadjustment = struct {
+ pub const name = "hadjustment";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?*gtk.Adjustment,
+ .{
+ .accessor = .{
+ .getter = getHAdjustmentValue,
+ .setter = setHAdjustmentValue,
+ },
+ },
+ );
+ };
+
+ pub const vadjustment = struct {
+ pub const name = "vadjustment";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?*gtk.Adjustment,
+ .{
+ .accessor = .{
+ .getter = getVAdjustmentValue,
+ .setter = setVAdjustmentValue,
+ },
+ },
+ );
+ };
+
+ pub const @"hscroll-policy" = struct {
+ pub const name = "hscroll-policy";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ gtk.ScrollablePolicy,
+ .{
+ .default = .natural,
+ .accessor = C.privateShallowFieldAccessor("hscroll_policy"),
+ },
+ );
+ };
+
+ pub const @"vscroll-policy" = struct {
+ pub const name = "vscroll-policy";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ gtk.ScrollablePolicy,
+ .{
+ .default = .natural,
+ .accessor = C.privateShallowFieldAccessor("vscroll_policy"),
+ },
+ );
+ };
+
+ pub const @"key-sequence" = struct {
+ pub const name = "key-sequence";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?*ext.StringList,
+ .{
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ ?*ext.StringList,
+ .{
+ .getter = getKeySequence,
+ .getter_transfer = .full,
+ },
+ ),
+ },
+ );
+ };
+
+ pub const @"key-table" = struct {
+ pub const name = "key-table";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?*ext.StringList,
+ .{
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ ?*ext.StringList,
+ .{
+ .getter = getKeyTable,
+ .getter_transfer = .full,
+ },
+ ),
+ },
+ );
+ };
+
+ pub const readonly = struct {
+ pub const name = "readonly";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ bool,
+ .{
+ .default = false,
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ bool,
+ .{
+ .getter = getReadonly,
+ },
+ ),
+ },
+ );
+ };
+ };
+
+ pub const signals = struct {
+ /// Emitted whenever the bell event is received. Unlike the
+ /// `bell-ringing` property, this is emitted every time the event
+ /// is received and not just on state changes.
+ pub const bell = struct {
+ pub const name = "bell";
+ pub const connect = impl.connect;
+ const impl = gobject.ext.defineSignal(
+ name,
+ Self,
+ &.{},
+ void,
+ );
+ };
+ /// Emitted whenever the surface would like to be closed for any
+ /// reason.
+ ///
+ /// The surface view does NOT handle its own close confirmation.
+ /// If there is a process alive then the boolean parameter will
+ /// specify it and the parent widget should handle this request.
+ ///
+ /// This signal lets the containing widget decide how closure works.
+ /// This lets this Surface widget be used as a split, tab, etc.
+ /// without it having to be aware of its own semantics.
+ pub const @"close-request" = struct {
+ pub const name = "close-request";
+ pub const connect = impl.connect;
+ const impl = gobject.ext.defineSignal(
+ name,
+ Self,
+ &.{},
+ void,
+ );
+ };
+
+ /// Emitted whenever the clipboard has been written.
+ pub const @"clipboard-write" = struct {
+ pub const name = "clipboard-write";
+ pub const connect = impl.connect;
+ const impl = gobject.ext.defineSignal(
+ name,
+ Self,
+ &.{
+ apprt.Clipboard,
+ [*:0]const u8,
+ },
+ void,
+ );
+ };
+
+ /// Emitted whenever the surface reads the clipboard.
+ pub const @"clipboard-read" = struct {
+ pub const name = "clipboard-read";
+ pub const connect = impl.connect;
+ const impl = gobject.ext.defineSignal(
+ name,
+ Self,
+ &.{},
+ void,
+ );
+ };
+
+ /// Emitted after the surface is initialized.
+ pub const init = struct {
+ pub const name = "init";
+ pub const connect = impl.connect;
+ const impl = gobject.ext.defineSignal(
+ name,
+ Self,
+ &.{},
+ void,
+ );
+ };
+
+ /// Emitted just prior to the context menu appearing.
+ pub const menu = struct {
+ pub const name = "menu";
+ pub const connect = impl.connect;
+ const impl = gobject.ext.defineSignal(
+ name,
+ Self,
+ &.{},
+ void,
+ );
+ };
+
+ /// Emitted when the focus wants to be brought to the top and
+ /// focused.
+ pub const @"present-request" = struct {
+ pub const name = "present-request";
+ pub const connect = impl.connect;
+ const impl = gobject.ext.defineSignal(
+ name,
+ Self,
+ &.{},
+ void,
+ );
+ };
+
+ /// Emitted when this surface requests its container to toggle its
+ /// fullscreen state.
+ pub const @"toggle-fullscreen" = struct {
+ pub const name = "toggle-fullscreen";
+ pub const connect = impl.connect;
+ const impl = gobject.ext.defineSignal(
+ name,
+ Self,
+ &.{},
+ void,
+ );
+ };
+
+ /// Emitted when this surface requests its container to toggle its
+ /// maximized state.
+ pub const @"toggle-maximize" = struct {
+ pub const name = "toggle-maximize";
+ pub const connect = impl.connect;
+ const impl = gobject.ext.defineSignal(
+ name,
+ Self,
+ &.{},
+ void,
+ );
+ };
+ };
+
+ const Private = struct {
+ /// The configuration that this surface is using.
+ config: ?*Config = null,
+
+ /// The default size for a window that embeds this surface.
+ default_size: ?*Size = null,
+
+ /// The minimum size for this surface. Embedders enforce this,
+ /// not the surface itself.
+ min_size: ?*Size = null,
+
+ /// The requested font size. This only applies to initialization
+ /// and has no effect later.
+ font_size_request: ?*font.face.DesiredSize = null,
+
+ /// The mouse shape to show for the surface.
+ mouse_shape: terminal.MouseShape = .default,
+
+ /// Whether the mouse should be hidden or not as requested externally.
+ mouse_hidden: bool = false,
+
+ /// The URL that the mouse is currently hovering over.
+ mouse_hover_url: ?[:0]const u8 = null,
+
+ /// The current working directory. This has to be reported externally,
+ /// usually by shell integration which then talks to libghostty
+ /// which triggers this property.
+ ///
+ /// If this is set prior to initialization then the surface will
+ /// start in this pwd. If it is set after, it has no impact on the
+ /// core surface.
+ pwd: ?[:0]const u8 = null,
+
+ /// The title of this surface, if any has been set.
+ title: ?[:0]const u8 = null,
+
+ /// The manually overridden title of this surface from `promptTitle`.
+ title_override: ?[:0]const u8 = null,
+
+ /// The current focus state of the terminal based on the
+ /// focus events.
+ focused: bool = true,
+
+ /// Whether this surface is "zoomed" or not. A zoomed surface
+ /// shows up taking the full bounds of a split view.
+ zoom: bool = false,
+
+ /// The GLAarea that renders the actual surface. This is a binding
+ /// to the template so it doesn't have to be unrefed manually.
+ gl_area: *gtk.GLArea,
+
+ /// The labels for the left/right sides of the URL hover tooltip.
+ url_left: *gtk.Label,
+ url_right: *gtk.Label,
+
+ /// The resize overlay
+ resize_overlay: *ResizeOverlay,
+
+ /// The search overlay
+ search_overlay: *SearchOverlay,
+
+ /// The key state overlay
+ key_state_overlay: *KeyStateOverlay,
+
+ /// The apprt Surface.
+ rt_surface: ApprtSurface = undefined,
+
+ /// The core surface backing this GTK surface. This starts out
+ /// null because it can't be initialized until there is an available
+ /// GLArea that is realized.
+ //
+ // NOTE(mitchellh): This is a limitation we should definitely remove
+ // at some point by modifying our OpenGL renderer for GTK to
+ // start in an unrealized state. There are other benefits to being
+ // able to initialize the surface early so we should aim for that,
+ // eventually.
+ core_surface: ?*CoreSurface = null,
+
+ /// Cached metrics for libghostty callbacks
+ size: apprt.SurfaceSize,
+ cursor_pos: apprt.CursorPos,
+
+ /// Various input method state. All related to key input.
+ in_keyevent: IMKeyEvent = .false,
+ im_context: *gtk.IMMulticontext,
+ im_composing: bool = false,
+ im_buf: [128]u8 = undefined,
+ im_len: u7 = 0,
+
+ /// True when we have a precision scroll in progress
+ precision_scroll: bool = false,
+
+ /// True when the child has exited.
+ child_exited: bool = false,
+
+ // Progress bar
+ progress_bar_timer: ?c_uint = null,
+
+ // True while the bell is ringing. This will be set to false (after
+ // true) under various scenarios, but can also manually be set to
+ // false by a parent widget.
+ bell_ringing: bool = false,
+
+ /// True if this surface is in an error state. This is currently
+ /// a simple boolean with no additional information on WHAT the
+ /// error state is, because we don't yet need it or use it. For now,
+ /// if this is true, then it means the terminal is non-functional.
+ @"error": bool = false,
+
+ /// The source that handles setting our child property.
+ idle_rechild: ?c_uint = null,
+
+ /// A weak reference to an inspector window.
+ inspector: ?*InspectorWindow = null,
+
+ // True if the current surface is a split, this is used to apply
+ // unfocused-split-* options
+ is_split: bool = false,
+
+ action_group: ?*gio.SimpleActionGroup = null,
+
+ // Gtk.Scrollable interface adjustments
+ hadj: ?*gtk.Adjustment = null,
+ vadj: ?*gtk.Adjustment = null,
+ hscroll_policy: gtk.ScrollablePolicy = .natural,
+ vscroll_policy: gtk.ScrollablePolicy = .natural,
+ vadj_signal_group: ?*gobject.SignalGroup = null,
+
+ // Key state tracking for key sequences and tables
+ key_sequence: std.ArrayListUnmanaged([:0]const u8) = .empty,
+ key_tables: std.ArrayListUnmanaged([:0]const u8) = .empty,
+
+ // Template binds
+ child_exited_overlay: *ChildExited,
+ context_menu: *gtk.PopoverMenu,
+ drop_target: *gtk.DropTarget,
+ progress_bar_overlay: *gtk.ProgressBar,
+ error_page: *adw.StatusPage,
+ terminal_page: *gtk.Overlay,
+
+ /// The context for this surface (window, tab, or split)
+ context: apprt.surface.NewSurfaceContext = .window,
+
+ /// Whether primary paste (middle-click paste) is enabled.
+ gtk_enable_primary_paste: bool = true,
+
+ /// True when a left mouse down was consumed purely for a focus change,
+ /// and the matching left mouse release should also be suppressed.
+ suppress_left_mouse_release: bool = false,
+
+ /// How much pending horizontal scroll do we have?
+ pending_horizontal_scroll: f64 = 0.0,
+
+ /// Timer to reset the amount of horizontal scroll if the user
+ /// stops scrolling.
+ pending_horizontal_scroll_reset: ?c_uint = null,
+
+ overrides: struct {
+ command: ?configpkg.Command = null,
+ working_directory: ?[:0]const u8 = null,
+
+ pub const none: @This() = .{};
+ } = .none,
+
+ pub var offset: c_int = 0;
+ };
+
+ pub fn new(overrides: struct {
+ command: ?configpkg.Command = null,
+ working_directory: ?[:0]const u8 = null,
+ title: ?[:0]const u8 = null,
+
+ pub const none: @This() = .{};
+ }) *Self {
+ const self = gobject.ext.newInstance(Self, .{
+ .@"title-override" = overrides.title,
+ });
+ const alloc = Application.default().allocator();
+ const priv: *Private = self.private();
+ priv.overrides = .{
+ .command = if (overrides.command) |c| c.clone(alloc) catch null else null,
+ .working_directory = if (overrides.working_directory) |wd| alloc.dupeZ(u8, wd) catch null else null,
+ };
+ return self;
+ }
+
+ pub fn core(self: *Self) ?*CoreSurface {
+ const priv = self.private();
+ return priv.core_surface;
+ }
+
+ pub fn rt(self: *Self) *ApprtSurface {
+ const priv = self.private();
+ return &priv.rt_surface;
+ }
+
+ /// Set the parent of this surface. This will extract the information
+ /// required to initialize this surface with the proper values but doesn't
+ /// retain any memory.
+ ///
+ /// If the surface is already realized this does nothing.
+ pub fn setParent(
+ self: *Self,
+ parent: *CoreSurface,
+ context: apprt.surface.NewSurfaceContext,
+ ) void {
+ const priv = self.private();
+
+ // This is a mistake! We can only set a parent before surface
+ // realization. We log this because this is probably a logic error.
+ if (priv.core_surface != null) {
+ log.warn("setParent called after surface is already realized", .{});
+ return;
+ }
+
+ // Store the context so initSurface can use it
+ priv.context = context;
+
+ // Setup our font size
+ const font_size_ptr = glib.ext.create(font.face.DesiredSize);
+ errdefer glib.ext.destroy(font_size_ptr);
+ font_size_ptr.* = parent.font_size;
+ priv.font_size_request = font_size_ptr;
+ self.as(gobject.Object).notifyByPspec(properties.@"font-size-request".impl.param_spec);
+
+ // Remainder needs a config. If there is no config we just assume
+ // we aren't inheriting any of these values.
+ if (priv.config) |config_obj| {
+ // Setup our cwd if configured to inherit
+ if (apprt.surface.shouldInheritWorkingDirectory(context, config_obj.get())) {
+ if (parent.rt_surface.surface.getPwd()) |pwd| {
+ priv.pwd = glib.ext.dupeZ(u8, pwd);
+ self.as(gobject.Object).notifyByPspec(properties.pwd.impl.param_spec);
+ }
+ }
+ }
+ }
+
+ /// Force the surface to redraw itself. Ghostty often will only redraw
+ /// the terminal in reaction to internal changes. If there are external
+ /// events that invalidate the surface, such as the widget moving parents,
+ /// then we should force a redraw.
+ pub fn redraw(self: *Self) void {
+ const priv = self.private();
+ priv.gl_area.queueRender();
+ }
+
+ /// Callback used to determine whether border should be shown around the
+ /// surface.
+ fn closureShouldBorderBeShown(
+ _: *Self,
+ config_: ?*Config,
+ bell_ringing_: c_int,
+ ) callconv(.c) c_int {
+ const bell_ringing = bell_ringing_ != 0;
+
+ // If the bell isn't ringing exit early because when the surface is
+ // first created there's a race between this code being run and the
+ // config being set on the surface. That way we don't overwhelm people
+ // with the warning that we issue if the config isn't set and overwhelm
+ // ourselves with large numbers of bug reports.
+ if (!bell_ringing) return @intFromBool(false);
+
+ const config = if (config_) |v| v.get() else {
+ log.warn("config unavailable for computing whether border should be shown, likely bug", .{});
+ return @intFromBool(false);
+ };
+
+ return @intFromBool(config.@"bell-features".border);
+ }
+
+ /// Callback used to determine whether unfocused-split-fill / unfocused-split-opacity
+ /// should be applied to the surface
+ fn closureShouldUnfocusedSplitBeShown(
+ _: *Self,
+ search_active: c_int,
+ focused: c_int,
+ is_split: c_int,
+ ) callconv(.c) c_int {
+ return @intFromBool(search_active == 0 and focused == 0 and is_split != 0);
+ }
+
+ pub fn toggleFullscreen(self: *Self) void {
+ signals.@"toggle-fullscreen".impl.emit(
+ self,
+ null,
+ .{},
+ null,
+ );
+ }
+
+ pub fn toggleMaximize(self: *Self) void {
+ signals.@"toggle-maximize".impl.emit(
+ self,
+ null,
+ .{},
+ null,
+ );
+ }
+
+ pub fn toggleCommandPalette(self: *Self) bool {
+ // TODO: pass the surface with the action
+ return self.as(gtk.Widget).activateAction("win.toggle-command-palette", null) != 0;
+ }
+
+ pub fn controlInspector(
+ self: *Self,
+ value: apprt.Action.Value(.inspector),
+ ) bool {
+ // Let's see if we have an inspector already.
+ const priv = self.private();
+ if (priv.inspector) |inspector| switch (value) {
+ .show => {},
+ // Our weak ref will set our private value to null
+ .toggle, .hide => inspector.as(gtk.Window).destroy(),
+ } else switch (value) {
+ .toggle, .show => {
+ const inspector = InspectorWindow.new(self);
+ inspector.present();
+ inspector.as(gobject.Object).weakRef(inspectorWeakNotify, self);
+ priv.inspector = inspector;
+ },
+
+ .hide => {},
+ }
+
+ return true;
+ }
+
+ /// Redraw our inspector, if there is one associated with this surface.
+ pub fn redrawInspector(self: *Self) void {
+ const priv = self.private();
+ if (priv.inspector) |v| v.queueRender();
+ }
+
+ /// Handle a key sequence action from the apprt.
+ pub fn keySequenceAction(
+ self: *Self,
+ value: apprt.action.KeySequence,
+ ) Allocator.Error!void {
+ const priv = self.private();
+ const alloc = Application.default().allocator();
+
+ self.as(gobject.Object).freezeNotify();
+ defer self.as(gobject.Object).thawNotify();
+ self.as(gobject.Object).notifyByPspec(properties.@"key-sequence".impl.param_spec);
+
+ switch (value) {
+ .trigger => |trigger| {
+ // Convert the trigger to a human-readable label
+ var buf: std.Io.Writer.Allocating = .init(alloc);
+ defer buf.deinit();
+ if (gtk_key.labelFromTrigger(&buf.writer, trigger)) |success| {
+ if (!success) return;
+ } else |_| return error.OutOfMemory;
+
+ // Make space
+ try priv.key_sequence.ensureUnusedCapacity(alloc, 1);
+
+ // Copy and append
+ const duped = try buf.toOwnedSliceSentinel(0);
+ errdefer alloc.free(duped);
+ priv.key_sequence.appendAssumeCapacity(duped);
+ },
+ .end => {
+ // Free all the stored strings and clear
+ for (priv.key_sequence.items) |s| alloc.free(s);
+ priv.key_sequence.clearAndFree(alloc);
+ },
+ }
+ }
+
+ /// Handle a key table action from the apprt.
+ pub fn keyTableAction(
+ self: *Self,
+ value: apprt.action.KeyTable,
+ ) Allocator.Error!void {
+ const priv = self.private();
+ const alloc = Application.default().allocator();
+
+ self.as(gobject.Object).freezeNotify();
+ defer self.as(gobject.Object).thawNotify();
+ self.as(gobject.Object).notifyByPspec(properties.@"key-table".impl.param_spec);
+
+ switch (value) {
+ .activate => |name| {
+ // Duplicate the name string and push onto stack
+ const duped = try alloc.dupeZ(u8, name);
+ errdefer alloc.free(duped);
+ try priv.key_tables.append(alloc, duped);
+ },
+ .deactivate => {
+ // Pop and free the top table
+ if (priv.key_tables.pop()) |s| alloc.free(s);
+ },
+ .deactivate_all => {
+ // Free all tables and clear
+ for (priv.key_tables.items) |s| alloc.free(s);
+ priv.key_tables.clearAndFree(alloc);
+ },
+ }
+ }
+
+ pub fn showOnScreenKeyboard(self: *Self, event: ?*gdk.Event) bool {
+ const priv = self.private();
+ return priv.im_context.as(gtk.IMContext).activateOsk(event) != 0;
+ }
+
+ /// Set the scrollbar state for this surface. This will setup the
+ /// properties for our Gtk.Scrollable interface properly.
+ pub fn setScrollbar(self: *Self, scrollbar: terminal.Scrollbar) void {
+ // Update existing adjustment in-place. If we don't have an
+ // adjustment then we do nothing because we're not part of a
+ // scrolled window.
+ const vadj = self.getVAdjustment() orelse return;
+
+ // Check if values match existing adjustment and skip update if so
+ const value: f64 = @floatFromInt(scrollbar.offset);
+ const upper: f64 = @floatFromInt(scrollbar.total);
+ const page_size: f64 = @floatFromInt(scrollbar.len);
+
+ if (std.math.approxEqAbs(f64, vadj.getValue(), value, 0.001) and
+ std.math.approxEqAbs(f64, vadj.getUpper(), upper, 0.001) and
+ std.math.approxEqAbs(f64, vadj.getPageSize(), page_size, 0.001))
+ {
+ return;
+ }
+
+ // If we have a vadjustment we MUST have the signal group since
+ // it is setup in the prop handler.
+ const priv = self.private();
+ const group = priv.vadj_signal_group.?;
+
+ // During manual scrollbar changes from Ghostty core we don't
+ // want to emit value-changed signals so we block them. This would
+ // cause a waste of resources at best and infinite loops at worst.
+ group.block();
+ defer group.unblock();
+
+ vadj.configure(
+ value, // value: current scroll position
+ 0, // lower: minimum value
+ upper, // upper: maximum value (total scrollable area)
+ 1, // step_increment: amount to scroll on arrow click
+ page_size, // page_increment: amount to scroll on page up/down
+ page_size, // page_size: size of visible area
+ );
+ }
+
+ /// Set the current progress report state.
+ pub fn setProgressReport(
+ self: *Self,
+ value: terminal.osc.Command.ProgressReport,
+ ) void {
+ const priv = self.private();
+
+ // No matter what, we stop the timer because if we're removing
+ // then we're done and otherwise we restart it.
+ if (priv.progress_bar_timer) |timer| {
+ if (glib.Source.remove(timer) == 0) {
+ log.warn("unable to remove progress bar timer", .{});
+ }
+ priv.progress_bar_timer = null;
+ }
+
+ if (priv.config) |config| {
+ if (!config.get().@"progress-style") {
+ log.debug("progress_report action blocked by config", .{});
+ priv.progress_bar_overlay.as(gtk.Widget).setVisible(@intFromBool(false));
+ return;
+ }
+ }
+
+ const progress_bar = priv.progress_bar_overlay;
+ switch (value.state) {
+ // Remove the progress bar
+ .remove => {
+ progress_bar.as(gtk.Widget).setVisible(@intFromBool(false));
+ return;
+ },
+
+ // Set the progress bar to a fixed value if one was provided, otherwise pulse.
+ // Remove the `error` CSS class so that the progress bar shows as normal.
+ .set => {
+ progress_bar.as(gtk.Widget).removeCssClass("error");
+ if (value.progress) |progress| {
+ progress_bar.setFraction(computeFraction(progress));
+ } else {
+ progress_bar.pulse();
+ }
+ },
+
+ // Set the progress bar to a fixed value if one was provided, otherwise pulse.
+ // Set the `error` CSS class so that the progress bar shows as an error color.
+ .@"error" => {
+ progress_bar.as(gtk.Widget).addCssClass("error");
+ if (value.progress) |progress| {
+ progress_bar.setFraction(computeFraction(progress));
+ } else {
+ progress_bar.pulse();
+ }
+ },
+
+ // The state of progress is unknown, so pulse the progress bar to
+ // indicate that things are still happening.
+ .indeterminate => {
+ progress_bar.pulse();
+ },
+
+ // If a progress value was provided, set the progress bar to that value.
+ // Don't pulse the progress bar as that would indicate that things were
+ // happening. Otherwise this is mainly used to keep the progress bar on
+ // screen instead of timing out.
+ .pause => {
+ if (value.progress) |progress| {
+ progress_bar.setFraction(computeFraction(progress));
+ }
+ },
+ }
+
+ // Assume all states lead to visibility
+ assert(value.state != .remove);
+ progress_bar.as(gtk.Widget).setVisible(@intFromBool(true));
+
+ // Start our timer to remove bad actor programs that stall
+ // the progress bar.
+ const progress_bar_timeout_seconds = 15;
+ assert(priv.progress_bar_timer == null);
+ priv.progress_bar_timer = glib.timeoutAdd(
+ progress_bar_timeout_seconds * std.time.ms_per_s,
+ progressBarTimer,
+ self,
+ );
+ }
+
+ /// The progress bar hasn't been updated by the TUI recently, remove it.
+ fn progressBarTimer(ud: ?*anyopaque) callconv(.c) c_int {
+ const self: *Self = @ptrCast(@alignCast(ud.?));
+ const priv = self.private();
+ priv.progress_bar_timer = null;
+ self.setProgressReport(.{ .state = .remove });
+ return @intFromBool(glib.SOURCE_REMOVE);
+ }
+
+ /// Request that this terminal come to the front and become focused.
+ /// It is up to the embedding widget to react to this.
+ pub fn present(self: *Self) void {
+ signals.@"present-request".impl.emit(
+ self,
+ null,
+ .{},
+ null,
+ );
+ }
+
+ pub fn commandFinished(self: *Self, value: apprt.Action.Value(.command_finished)) bool {
+ const app = Application.default();
+ const alloc = app.allocator();
+ const priv: *Private = self.private();
+
+ const notify_next_command_finish = notify: {
+ const simple_action_group = priv.action_group orelse break :notify false;
+ const action_group = simple_action_group.as(gio.ActionGroup);
+ const state = action_group.getActionState("notify-on-next-command-finish") orelse break :notify false;
+ const bool_variant_type = glib.ext.VariantType.newFor(bool);
+ defer bool_variant_type.free();
+ if (state.isOfType(bool_variant_type) == 0) break :notify false;
+ const notify = state.getBoolean() != 0;
+ action_group.changeActionState("notify-on-next-command-finish", glib.Variant.newBoolean(@intFromBool(false)));
+ break :notify notify;
+ };
+
+ const config = priv.config orelse return false;
+
+ const cfg = config.get();
+
+ if (!notify_next_command_finish) {
+ if (cfg.@"notify-on-command-finish" == .never) return true;
+ if (cfg.@"notify-on-command-finish" == .unfocused and self.getFocused()) return true;
+ }
+
+ if (value.duration.lte(cfg.@"notify-on-command-finish-after")) return true;
+
+ const action = cfg.@"notify-on-command-finish-action";
+
+ if (action.bell) self.setBellRinging(true);
+
+ if (action.notify) notify: {
+ const title_ = title: {
+ const exit_code = value.exit_code orelse break :title i18n._("Command Finished");
+ if (exit_code == 0) break :title i18n._("Command Succeeded");
+ break :title i18n._("Command Failed");
+ };
+ const title = std.mem.span(title_);
+ const body = body: {
+ const exit_code = value.exit_code orelse break :body std.fmt.allocPrintSentinel(
+ alloc,
+ "Command took {f}.",
+ .{value.duration.round(std.time.ns_per_ms)},
+ 0,
+ ) catch break :notify;
+ break :body std.fmt.allocPrintSentinel(
+ alloc,
+ "Command took {f} and exited with code {d}.",
+ .{ value.duration.round(std.time.ns_per_ms), exit_code },
+ 0,
+ ) catch break :notify;
+ };
+ defer alloc.free(body);
+
+ self.sendDesktopNotification(title, body);
+ }
+
+ return true;
+ }
+
+ /// Get the readonly state from the core surface.
+ pub fn getReadonly(self: *Self) bool {
+ const priv: *Private = self.private();
+ const surface = priv.core_surface orelse return false;
+ return surface.readonly;
+ }
+
+ /// Notify anyone interested that the readonly status has changed.
+ pub fn setReadonly(self: *Self, _: apprt.Action.Value(.readonly)) bool {
+ self.as(gobject.Object).notifyByPspec(properties.readonly.impl.param_spec);
+
+ return true;
+ }
+
+ /// Key press event (press or release).
+ ///
+ /// At a high level, we want to construct an `input.KeyEvent` and
+ /// pass that to `keyCallback`. At a low level, this is more complicated
+ /// than it appears because we need to construct all of this information
+ /// and its not given to us.
+ ///
+ /// For all events, we run the GdkEvent through the input method context.
+ /// This allows the input method to capture the event and trigger
+ /// callbacks such as preedit, commit, etc.
+ ///
+ /// There are a couple important aspects to the prior paragraph: we must
+ /// send ALL events through the input method context. This is because
+ /// input methods use both key press and key release events to determine
+ /// the state of the input method. For example, fcitx uses key release
+ /// events on modifiers (i.e. ctrl+shift) to switch the input method.
+ ///
+ /// We set some state to note we're in a key event (self.in_keyevent)
+ /// because some of the input method callbacks change behavior based on
+ /// this state. For example, we don't want to send character events
+ /// like "a" via the input "commit" event if we're actively processing
+ /// a keypress because we'd lose access to the keycode information.
+ /// However, a "commit" event may still happen outside of a keypress
+ /// event from e.g. a tablet or on-screen keyboard.
+ ///
+ /// Finally, we take all of the information in order to determine if we have
+ /// a unicode character or if we have to map the keyval to a code to
+ /// get the underlying logical key, etc.
+ ///
+ /// Then we can emit the keyCallback.
+ pub fn keyEvent(
+ self: *Surface,
+ action: input.Action,
+ ec_key: *gtk.EventControllerKey,
+ keyval: c_uint,
+ keycode: c_uint,
+ gtk_mods: gdk.ModifierType,
+ ) bool {
+ //log.warn("keyEvent action={}", .{action});
+ const event = ec_key.as(gtk.EventController).getCurrentEvent() orelse return false;
+ const key_event = gobject.ext.cast(gdk.KeyEvent, event) orelse return false;
+ const priv = self.private();
+
+ // The block below is all related to input method handling. See the function
+ // comment for some high level details and then the comments within
+ // the block for more specifics.
+ {
+ // This can trigger an input method so we need to notify the im context
+ // where the cursor is so it can render the dropdowns in the correct
+ // place.
+ if (priv.core_surface) |surface| {
+ const ime_point = surface.imePoint();
+ priv.im_context.as(gtk.IMContext).setCursorLocation(&.{
+ .f_x = @intFromFloat(ime_point.x),
+ .f_y = @intFromFloat(ime_point.y),
+ .f_width = 1,
+ .f_height = 1,
+ });
+ }
+
+ // We note that we're in a keypress because we want some logic to
+ // depend on this. For example, we don't want to send character events
+ // like "a" via the input "commit" event if we're actively processing
+ // a keypress because we'd lose access to the keycode information.
+ //
+ // We have to maintain some additional state here of whether we
+ // were composing because different input methods call the callbacks
+ // in different orders. For example, ibus calls commit THEN preedit
+ // end but simple calls preedit end THEN commit.
+ priv.in_keyevent = if (priv.im_composing) .composing else .not_composing;
+ defer priv.in_keyevent = .false;
+
+ // Pass the event through the input method which returns true if handled.
+ // Confusingly, not all events handled by the input method result
+ // in this returning true so we have to maintain some additional
+ // state about whether we were composing or not to determine if
+ // we should proceed with key encoding.
+ //
+ // Cases where the input method does not mark the event as handled:
+ //
+ // - If we change the input method via keypress while we have preedit
+ // text, the input method will commit the pending text but will not
+ // mark it as handled. We use the `.composing` state to detect
+ // this case.
+ //
+ // - If we switch input methods (i.e. via ctrl+shift with fcitx),
+ // the input method will handle the key release event but will not
+ // mark it as handled. I don't know any way to detect this case so
+ // it will result in a key event being sent to the key callback.
+ // For Kitty text encoding, this will result in modifiers being
+ // triggered despite being technically consumed. At the time of
+ // writing, both Kitty and Alacritty have the same behavior. I
+ // know of no way to fix this.
+ const im_handled = priv.im_context.as(gtk.IMContext).filterKeypress(event) != 0;
+ // log.warn("GTKIM: im_handled={} im_len={} im_composing={}", .{
+ // im_handled,
+ // self.im_len,
+ // self.im_composing,
+ // });
+
+ // If the input method handled the event, you would think we would
+ // never proceed with key encoding for Ghostty but that is not the
+ // case. Input methods will handle basic character encoding like
+ // typing "a" and we want to associate that with the key event.
+ // So we have to check additional state to determine if we exit.
+ if (im_handled) {
+ // If we are composing then we're in a preedit state and do
+ // not want to encode any keys. For example: type a deadkey
+ // such as single quote on a US international keyboard layout.
+ if (priv.im_composing) return true;
+
+ // If we were composing and now we're not, it means that we committed
+ // the text. We also don't want to encode a key event for this.
+ // Example: enable Japanese input method, press "konn" and then
+ // press enter. The final enter should not be encoded and "konn"
+ // (in hiragana) should be written as "こん".
+ if (priv.in_keyevent == .composing) return true;
+
+ // Not composing and our input method buffer is empty. This could
+ // mean that the input method reacted to this event by activating
+ // an onscreen keyboard or something equivalent. We don't know.
+ // But the input method handled it and didn't give us text so
+ // we will just assume we should not encode this. This handles a
+ // real scenario when ibus starts the emoji input method
+ // (super+.).
+ if (priv.im_len == 0) return true;
+ }
+
+ // At this point, for the sake of explanation of internal state:
+ // it is possible that im_len > 0 and im_composing == false. This
+ // means that we received a commit event from the input method that
+ // we want associated with the key event. This is common: its how
+ // basic character translation for simple inputs like "a" work.
+ }
+
+ // We always reset the length of the im buffer. There's only one scenario
+ // we reach this point with im_len > 0 and that's if we received a commit
+ // event from the input method. We don't want to keep that state around
+ // since we've handled it here.
+ defer priv.im_len = 0;
+
+ // Get the keyvals for this event.
+ const keyval_unicode = gdk.keyvalToUnicode(keyval);
+ const keyval_unicode_unshifted: u21 = gtk_key.keyvalUnicodeUnshifted(
+ priv.gl_area.as(gtk.Widget),
+ key_event,
+ keycode,
+ );
+
+ // We want to get the physical unmapped key to process physical keybinds.
+ // (These are keybinds explicitly marked as requesting physical mapping).
+ const physical_key = keycode: {
+ const w3c_key: input.Key = w3c: for (input.keycodes.entries) |entry| {
+ if (entry.native == keycode) break :w3c entry.key;
+ } else .unidentified;
+
+ // Consult the pre-remapped XKB keyval/keysym to get the (possibly)
+ // remapped key. If the W3C key or the remapped key
+ // is eligible for remapping, we use it.
+ //
+ // See the docs for `shouldBeRemappable` for why we even have to
+ // do this in the first place.
+ if (gtk_key.keyFromKeyval(keyval)) |remapped| {
+ if (w3c_key.shouldBeRemappable() or remapped.shouldBeRemappable())
+ break :keycode remapped;
+ }
+
+ // Return the original physical key
+ break :keycode w3c_key;
+ };
+
+ // Get our modifier for the event
+ const mods: input.Mods = gtk_key.eventMods(
+ event,
+ physical_key,
+ gtk_mods,
+ action,
+ Application.default().winproto(),
+ );
+
+ // Get our consumed modifiers
+ const consumed_mods: input.Mods = consumed: {
+ const T = @typeInfo(gdk.ModifierType);
+ std.debug.assert(T.@"struct".layout == .@"packed");
+ const I = T.@"struct".backing_integer.?;
+
+ const masked = @as(I, @bitCast(key_event.getConsumedModifiers())) & @as(I, gdk.MODIFIER_MASK);
+ break :consumed gtk_key.translateMods(@bitCast(masked));
+ };
+
+ // log.debug("key pressed key={} keyval={x} physical_key={} composing={} text_len={} mods={}", .{
+ // key,
+ // keyval,
+ // physical_key,
+ // priv.im_composing,
+ // priv.im_len,
+ // mods,
+ // });
+
+ // If we have no UTF-8 text, we try to convert our keyval to
+ // a text value. We have to do this because GTK will not process
+ // "Ctrl+Shift+1" (on US keyboards) as "Ctrl+!" but instead as "".
+ // But the keyval is set correctly so we can at least extract that.
+ if (priv.im_len == 0 and keyval_unicode > 0) im: {
+ if (std.math.cast(u21, keyval_unicode)) |cp| {
+ // We don't want to send control characters as IM
+ // text. Control characters are handled already by
+ // the encoder directly.
+ if (cp < 0x20) break :im;
+
+ if (std.unicode.utf8Encode(cp, &priv.im_buf)) |len| {
+ priv.im_len = len;
+ } else |_| {}
+ }
+ }
+
+ // Invoke the core Ghostty logic to handle this input.
+ const surface = priv.core_surface orelse return false;
+ const effect = surface.keyCallback(.{
+ .action = action,
+ .key = physical_key,
+ .mods = mods,
+ .consumed_mods = consumed_mods,
+ .composing = priv.im_composing,
+ .utf8 = priv.im_buf[0..priv.im_len],
+ .unshifted_codepoint = keyval_unicode_unshifted,
+ }) catch |err| {
+ log.err("error in key callback err={}", .{err});
+ return false;
+ };
+
+ switch (effect) {
+ .closed => return true,
+ .ignored => {},
+ .consumed => if (action == .press or action == .repeat) {
+ // If we were in the composing state then we reset our context.
+ // We do NOT want to reset if we're not in the composing state
+ // because there is other IME state that we want to preserve,
+ // such as quotation mark ordering for Chinese input.
+ if (priv.im_composing) {
+ priv.im_context.as(gtk.IMContext).reset();
+ surface.preeditCallback(null) catch {};
+ }
+
+ // Bell stops ringing when any key is pressed that is used by
+ // the core in any way.
+ self.setBellRinging(false);
+
+ return true;
+ },
+ }
+
+ return false;
+ }
+
+ /// Prompt for a manual title change for the surface.
+ pub fn promptTitle(self: *Self) void {
+ const priv = self.private();
+ const dialog = TitleDialog.new(.surface, priv.title_override orelse priv.title);
+ _ = TitleDialog.signals.set.connect(
+ dialog,
+ *Self,
+ titleDialogSet,
+ self,
+ .{},
+ );
+
+ dialog.present(self.as(gtk.Widget));
+ }
+
+ /// Scale x/y by the GDK device scale.
+ fn scaledCoordinates(
+ self: *Self,
+ x: f64,
+ y: f64,
+ ) struct { x: f64, y: f64 } {
+ const gl_area = self.private().gl_area;
+ const scale_factor: f64 = @floatFromInt(
+ gl_area.as(gtk.Widget).getScaleFactor(),
+ );
+
+ return .{
+ .x = x * scale_factor,
+ .y = y * scale_factor,
+ };
+ }
+
+ //---------------------------------------------------------------
+ // Libghostty Callbacks
+
+ pub fn close(self: *Self) void {
+ signals.@"close-request".impl.emit(
+ self,
+ null,
+ .{},
+ null,
+ );
+ }
+
+ pub fn childExited(
+ self: *Self,
+ data: apprt.surface.Message.ChildExited,
+ ) bool {
+ // Even if we don't support the overlay, we still keep our property
+ // up to date for anyone listening.
+ const priv = self.private();
+ priv.child_exited = true;
+ self.as(gobject.Object).notifyByPspec(
+ properties.@"child-exited".impl.param_spec,
+ );
+
+ // If we have the noop child exited overlay then we don't do anything
+ // for child exited. The false return will force libghostty to show
+ // the normal text-based message.
+ if (comptime @hasDecl(ChildExited, "noop")) {
+ return false;
+ }
+
+ priv.child_exited_overlay.setData(&data);
+ return true;
+ }
+
+ pub fn getContentScale(self: *Self) apprt.ContentScale {
+ const priv = self.private();
+ const gl_area = priv.gl_area;
+
+ const gtk_scale: f32 = scale: {
+ const widget = gl_area.as(gtk.Widget);
+ // Future: detect GTK version 4.12+ and use gdk_surface_get_scale so we
+ // can support fractional scaling.
+ const scale = widget.getScaleFactor();
+ if (scale <= 0) {
+ log.warn("gtk_widget_get_scale_factor returned a non-positive number: {}", .{scale});
+ break :scale 1.0;
+ }
+ break :scale @floatFromInt(scale);
+ };
+
+ // Also scale using font-specific DPI, which is often exposed to the user
+ // via DE accessibility settings (see https://docs.gtk.org/gtk4/class.Settings.html).
+ const xft_dpi_scale = xft_scale: {
+ // gtk-xft-dpi is font DPI multiplied by 1024. See
+ // https://docs.gtk.org/gtk4/property.Settings.gtk-xft-dpi.html
+ const gtk_xft_dpi = gsettings.get(.@"gtk-xft-dpi") orelse {
+ log.warn("gtk-xft-dpi was not set, using default value", .{});
+ break :xft_scale 1.0;
+ };
+
+ // Use a value of 1.0 for the XFT DPI scale if the setting is <= 0
+ // See:
+ // https://gitlab.gnome.org/GNOME/libadwaita/-/commit/a7738a4d269bfdf4d8d5429ca73ccdd9b2450421
+ // https://gitlab.gnome.org/GNOME/libadwaita/-/commit/9759d3fd81129608dd78116001928f2aed974ead
+ if (gtk_xft_dpi <= 0) {
+ log.warn("gtk-xft-dpi has invalid value ({}), using default", .{gtk_xft_dpi});
+ break :xft_scale 1.0;
+ }
+
+ // As noted above gtk-xft-dpi is multiplied by 1024, so we divide by
+ // 1024, then divide by the default value (96) to derive a scale. Note
+ // gtk-xft-dpi can be fractional, so we use floating point math here.
+ const xft_dpi: f32 = @as(f32, @floatFromInt(gtk_xft_dpi)) / 1024.0;
+ break :xft_scale xft_dpi / 96.0;
+ };
+
+ const scale = gtk_scale * xft_dpi_scale;
+ return .{ .x = scale, .y = scale };
+ }
+
+ pub fn getSize(self: *Self) apprt.SurfaceSize {
+ const priv = self.private();
+ // By the time this is called, we should be in a widget tree.
+ // This should not be called before that. We ensure this by initializing
+ // the surface in `glareaResize`. This is VERY important because it
+ // avoids the pty having an incorrect initial size.
+ assert(priv.size.width >= 0 and priv.size.height >= 0);
+ return priv.size;
+ }
+
+ pub fn getCursorPos(self: *Self) apprt.CursorPos {
+ return self.private().cursor_pos;
+ }
+
+ pub fn defaultTermioEnv(self: *Self) !std.process.EnvMap {
+ const app = Application.default();
+ const alloc = app.allocator();
+ var env = try internal_os.getEnvMap(alloc);
+ errdefer env.deinit();
+
+ if (app.savedLanguage()) |language| {
+ try env.put("LANG", language);
+ } else {
+ env.remove("LANG");
+ }
+
+ // Don't leak these GTK environment variables to child processes.
+ env.remove("GDK_DEBUG");
+ env.remove("GDK_DISABLE");
+ env.remove("GSK_RENDERER");
+
+ // Remove some environment variables that are set when Ghostty is launched
+ // from a `.desktop` file, by D-Bus activation, or systemd.
+ env.remove("GIO_LAUNCHED_DESKTOP_FILE");
+ env.remove("GIO_LAUNCHED_DESKTOP_FILE_PID");
+ env.remove("DBUS_STARTER_ADDRESS");
+ env.remove("DBUS_STARTER_BUS_TYPE");
+ env.remove("INVOCATION_ID");
+ env.remove("JOURNAL_STREAM");
+ env.remove("NOTIFY_SOCKET");
+
+ // Unset environment varies set by snaps if we're running in a snap.
+ // This allows Ghostty to further launch additional snaps.
+ if (comptime build_config.snap) {
+ if (env.get("SNAP") != null) try filterSnapPaths(
+ alloc,
+ &env,
+ );
+ }
+
+ // This is a hack because it ties ourselves (optionally) to the
+ // Window class. The right solution we should do is emit a signal
+ // here where the handler can modify our EnvMap, but boxing the
+ // EnvMap is a bit annoying so I'm punting it.
+ if (ext.getAncestor(Window, self.as(gtk.Widget))) |window| {
+ try window.winproto().addSubprocessEnv(&env);
+
+ if (window.isQuickTerminal()) {
+ try env.put("GHOSTTY_QUICK_TERMINAL", "1");
+ }
+ }
+
+ return env;
+ }
+
+ /// Filter out environment variables that start with forbidden prefixes.
+ fn filterSnapPaths(gpa: std.mem.Allocator, env_map: *std.process.EnvMap) !void {
+ comptime assert(build_config.snap);
+
+ const snap_vars = [_][]const u8{
+ "SNAP",
+ "SNAP_USER_COMMON",
+ "SNAP_USER_DATA",
+ "SNAP_DATA",
+ "SNAP_COMMON",
+ };
+
+ // Use an arena because everything in this function is temporary.
+ var arena = std.heap.ArenaAllocator.init(gpa);
+ defer arena.deinit();
+ const alloc = arena.allocator();
+
+ var env_to_remove: std.ArrayList([]const u8) = .empty;
+ var env_to_update: std.ArrayList(struct {
+ key: []const u8,
+ value: []const u8,
+ }) = .empty;
+
+ var it = env_map.iterator();
+ while (it.next()) |entry| {
+ const key = entry.key_ptr.*;
+ const value = entry.value_ptr.*;
+
+ // Ignore fields we set ourself
+ if (std.mem.eql(u8, key, "TERMINFO")) continue;
+ if (std.mem.startsWith(u8, key, "GHOSTTY")) continue;
+
+ // Any env var starting with SNAP must be removed
+ if (std.mem.startsWith(u8, key, "SNAP_")) {
+ try env_to_remove.append(alloc, key);
+ continue;
+ }
+
+ var filtered_paths: std.ArrayList([]const u8) = .empty;
+ var modified = false;
+ var paths = std.mem.splitAny(u8, value, ":");
+ while (paths.next()) |path| {
+ var include = true;
+ for (snap_vars) |k| if (env_map.get(k)) |snap_path| {
+ if (snap_path.len == 0) continue;
+ if (std.mem.startsWith(u8, path, snap_path)) {
+ include = false;
+ modified = true;
+ break;
+ }
+ };
+ if (include) try filtered_paths.append(alloc, path);
+ }
+
+ if (modified) {
+ if (filtered_paths.items.len > 0) {
+ const new_value = try std.mem.join(alloc, ":", filtered_paths.items);
+ try env_to_update.append(alloc, .{ .key = key, .value = new_value });
+ } else {
+ try env_to_remove.append(alloc, key);
+ }
+ }
+ }
+
+ for (env_to_update.items) |item| try env_map.put(
+ item.key,
+ item.value,
+ );
+ for (env_to_remove.items) |key| _ = env_map.remove(key);
+ }
+
+ pub fn clipboardRequest(
+ self: *Self,
+ clipboard_type: apprt.Clipboard,
+ state: apprt.ClipboardRequest,
+ ) !bool {
+ return try Clipboard.request(
+ self,
+ clipboard_type,
+ state,
+ );
+ }
+
+ pub fn setClipboard(
+ self: *Self,
+ clipboard_type: apprt.Clipboard,
+ contents: []const apprt.ClipboardContent,
+ confirm: bool,
+ ) void {
+ Clipboard.set(
+ self,
+ clipboard_type,
+ contents,
+ confirm,
+ );
+ }
+
+ /// Focus this surface. This properly focuses the input part of
+ /// our surface.
+ pub fn grabFocus(self: *Self) void {
+ const priv = self.private();
+ _ = priv.gl_area.as(gtk.Widget).grabFocus();
+ }
+
+ pub fn sendDesktopNotification(self: *Self, title: [:0]const u8, body: [:0]const u8) void {
+ const app = Application.default();
+ const priv: *Private = self.private();
+
+ const core_surface = priv.core_surface orelse {
+ log.warn("can't send notification because there is no core surface", .{});
+ return;
+ };
+
+ const t = switch (title.len) {
+ 0 => "Ghostty",
+ else => title,
+ };
+
+ const notification = gio.Notification.new(t);
+ defer notification.unref();
+ notification.setBody(body);
+
+ const icon = gio.ThemedIcon.new("com.mitchellh.ghostty");
+ defer icon.unref();
+ notification.setIcon(icon.as(gio.Icon));
+
+ const pointer = glib.Variant.newUint64(core_surface.id);
+ notification.setDefaultActionAndTargetValue(
+ "app.present-surface",
+ pointer,
+ );
+
+ // We set the notification ID to the body content. If the content is the
+ // same, this notification may replace a previous notification
+ const gio_app = app.as(gio.Application);
+ gio_app.sendNotification(body, notification);
+ }
+
+ //---------------------------------------------------------------
+ // Virtual Methods
+
+ fn init(self: *Self, _: *Class) callconv(.c) void {
+ gtk.Widget.initTemplate(self.as(gtk.Widget));
+
+ // Initialize our actions
+ self.initActionMap();
+
+ const priv = self.private();
+
+ // Initialize some private fields so they aren't undefined
+ priv.rt_surface = .{ .surface = self };
+ priv.precision_scroll = false;
+ priv.cursor_pos = .{ .x = 0, .y = 0 };
+ priv.mouse_shape = .text;
+ priv.mouse_hidden = false;
+ priv.focused = true;
+ priv.size = .{ .width = 0, .height = 0 };
+ priv.vadj_signal_group = null;
+
+ // If our configuration is null then we get the configuration
+ // from the application.
+ if (priv.config == null) {
+ const app = Application.default();
+ priv.config = app.getConfig();
+ }
+
+ // Setup our input method state
+ priv.in_keyevent = .false;
+ priv.im_composing = false;
+ priv.im_len = 0;
+
+ // Read GTK primary paste setting
+ priv.gtk_enable_primary_paste = gsettings.get(.@"gtk-enable-primary-paste") orelse true;
+
+ // Set up to handle items being dropped on our surface. Files can be dropped
+ // from Nautilus and strings can be dropped from many programs. The order
+ // of these types matter.
+ var drop_target_types = [_]gobject.Type{
+ gdk.FileList.getGObjectType(),
+ gio.File.getGObjectType(),
+ gobject.ext.types.string,
+ };
+ priv.drop_target.setGtypes(&drop_target_types, drop_target_types.len);
+
+ // Setup properties we can't set from our Blueprint file.
+ self.as(gtk.Widget).setCursorFromName("text");
+
+ // Initialize our config
+ self.propConfig(undefined, null);
+ }
+
+ fn initActionMap(self: *Self) void {
+ const priv: *Private = self.private();
+
+ const actions = [_]ext.actions.Action(Self){
+ .init(
+ "prompt-title",
+ actionPromptTitle,
+ null,
+ ),
+ .initStateful(
+ "notify-on-next-command-finish",
+ actionNotifyOnNextCommandFinish,
+ null,
+ glib.Variant.newBoolean(@intFromBool(false)),
+ ),
+ };
+
+ priv.action_group = ext.actions.addAsGroup(Self, self, "surface", &actions);
+ }
+
+ fn dispose(self: *Self) callconv(.c) void {
+ const priv = self.private();
+
+ if (priv.config) |v| {
+ v.unref();
+ priv.config = null;
+ }
+
+ if (priv.vadj_signal_group) |group| {
+ group.setTarget(null);
+ group.as(gobject.Object).unref();
+ priv.vadj_signal_group = null;
+ }
+
+ if (priv.hadj) |v| {
+ v.as(gobject.Object).unref();
+ priv.hadj = null;
+ }
+
+ if (priv.vadj) |v| {
+ v.as(gobject.Object).unref();
+ priv.vadj = null;
+ }
+
+ if (priv.progress_bar_timer) |timer| {
+ if (glib.Source.remove(timer) == 0) {
+ log.warn("unable to remove progress bar timer", .{});
+ }
+ priv.progress_bar_timer = null;
+ }
+
+ if (priv.idle_rechild) |v| {
+ if (glib.Source.remove(v) == 0) {
+ log.warn("unable to remove idle source", .{});
+ }
+ priv.idle_rechild = null;
+ }
+
+ if (priv.pending_horizontal_scroll_reset) |v| {
+ if (glib.Source.remove(v) == 0) {
+ log.warn("unable to remove pending horizontal scroll reset source", .{});
+ }
+ priv.pending_horizontal_scroll_reset = null;
+ }
+
+ // This works around a GTK double-free bug where if you bind
+ // to a top-level template child, it frees twice if the widget is
+ // also the root child of the template. By unsetting the child here,
+ // we avoid the double-free.
+ self.as(adw.Bin).setChild(null);
+
+ gtk.Widget.disposeTemplate(
+ self.as(gtk.Widget),
+ getGObjectType(),
+ );
+
+ gobject.Object.virtual_methods.dispose.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ fn finalize(self: *Self) callconv(.c) void {
+ const alloc = Application.default().allocator();
+ const priv = self.private();
+ if (priv.core_surface) |v| {
+ // Remove ourselves from the list of known surfaces in the app.
+ // We do this before deinit in case a callback triggers
+ // searching for this surface.
+ Application.default().core().deleteSurface(self.rt());
+
+ // NOTE: We must deinit the surface in the finalize call and NOT
+ // the dispose call because the inspector widget relies on this
+ // behavior with a weakRef to properly deactivate.
+
+ // Deinit the surface
+ v.deinit();
+ alloc.destroy(v);
+
+ priv.core_surface = null;
+ }
+ if (priv.mouse_hover_url) |v| {
+ glib.free(@ptrCast(@constCast(v)));
+ priv.mouse_hover_url = null;
+ }
+ if (priv.default_size) |v| {
+ ext.boxedFree(Size, v);
+ priv.default_size = null;
+ }
+ if (priv.font_size_request) |v| {
+ glib.ext.destroy(v);
+ priv.font_size_request = null;
+ }
+ if (priv.min_size) |v| {
+ ext.boxedFree(Size, v);
+ priv.min_size = null;
+ }
+ if (priv.pwd) |v| {
+ glib.free(@ptrCast(@constCast(v)));
+ priv.pwd = null;
+ }
+ if (priv.title) |v| {
+ glib.free(@ptrCast(@constCast(v)));
+ priv.title = null;
+ }
+ if (priv.title_override) |v| {
+ glib.free(@ptrCast(@constCast(v)));
+ priv.title_override = null;
+ }
+ if (priv.overrides.command) |c| {
+ c.deinit(alloc);
+ priv.overrides.command = null;
+ }
+ if (priv.overrides.working_directory) |wd| {
+ alloc.free(wd);
+ priv.overrides.working_directory = null;
+ }
+
+ // Clean up key sequence and key table state
+ for (priv.key_sequence.items) |s| alloc.free(s);
+ priv.key_sequence.deinit(alloc);
+ for (priv.key_tables.items) |s| alloc.free(s);
+ priv.key_tables.deinit(alloc);
+
+ gobject.Object.virtual_methods.finalize.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ //---------------------------------------------------------------
+ // Properties
+
+ /// Returns the title property without a copy.
+ pub fn getTitle(self: *Self) ?[:0]const u8 {
+ return self.private().title;
+ }
+
+ /// Returns the effective title: the user-overridden title if set,
+ /// otherwise the terminal-set title.
+ pub fn getEffectiveTitle(self: *Self) ?[:0]const u8 {
+ const priv = self.private();
+ return priv.title_override orelse priv.title;
+ }
+
+ /// Copies the effective title to the clipboard.
+ pub fn copyTitleToClipboard(self: *Self) bool {
+ const title = self.getEffectiveTitle() orelse return false;
+ if (title.len == 0) return false;
+ self.setClipboard(.standard, &.{.{
+ .mime = "text/plain",
+ .data = title,
+ }}, false);
+ return true;
+ }
+
+ /// Set the title for this surface, copies the value. This should always
+ /// be the title as set by the terminal program, not any manually set
+ /// title. For manually set titles see `setTitleOverride`.
+ pub fn setTitle(self: *Self, title: ?[:0]const u8) void {
+ const priv = self.private();
+ if (priv.title) |v| glib.free(@ptrCast(@constCast(v)));
+ priv.title = null;
+ if (title) |v| priv.title = glib.ext.dupeZ(u8, v);
+ self.as(gobject.Object).notifyByPspec(properties.title.impl.param_spec);
+ }
+
+ /// Overridden title. This will be generally be shown over the title
+ /// unless this is unset (null).
+ pub fn setTitleOverride(self: *Self, title: ?[:0]const u8) void {
+ const priv = self.private();
+ if (priv.title_override) |v| glib.free(@ptrCast(@constCast(v)));
+ priv.title_override = null;
+ if (title) |v| priv.title_override = glib.ext.dupeZ(u8, v);
+ self.as(gobject.Object).notifyByPspec(properties.@"title-override".impl.param_spec);
+ }
+
+ /// Returns the pwd property without a copy.
+ pub fn getPwd(self: *Self) ?[:0]const u8 {
+ return self.private().pwd;
+ }
+
+ /// Set the pwd for this surface, copies the value.
+ pub fn setPwd(self: *Self, pwd: ?[:0]const u8) void {
+ const priv = self.private();
+ if (priv.pwd) |v| glib.free(@ptrCast(@constCast(v)));
+ priv.pwd = null;
+ if (pwd) |v| priv.pwd = glib.ext.dupeZ(u8, v);
+ self.as(gobject.Object).notifyByPspec(properties.pwd.impl.param_spec);
+ }
+
+ /// Returns the focus state of this surface.
+ pub fn getFocused(self: *Self) bool {
+ return self.private().focused;
+ }
+
+ /// Change the configuration for this surface.
+ pub fn setConfig(self: *Self, config: *Config) void {
+ const priv = self.private();
+ if (priv.config) |c| c.unref();
+ priv.config = config.ref();
+ self.as(gobject.Object).notifyByPspec(properties.config.impl.param_spec);
+ }
+
+ /// Return the default size, if set.
+ pub fn getDefaultSize(self: *Self) ?*Size {
+ const priv = self.private();
+ return priv.default_size;
+ }
+
+ /// Set the default size for a window that contains this surface.
+ /// This is up to the embedding widget to respect this. Generally, only
+ /// the first surface in a window respects this.
+ pub fn setDefaultSize(self: *Self, size: Size) void {
+ const priv = self.private();
+ if (priv.default_size) |v| ext.boxedFree(
+ Size,
+ v,
+ );
+ priv.default_size = ext.boxedCopy(
+ Size,
+ &size,
+ );
+ self.as(gobject.Object).notifyByPspec(properties.@"default-size".impl.param_spec);
+ }
+
+ /// Estimate and set the initial window size from config and font metrics.
+ /// This can be called before the core surface exists to set up the window
+ /// size before presenting. This is an estimate because it does not take
+ /// into account any padding that may need to be added to the window.
+ pub fn estimateInitialSize(self: *Self) void {
+ const priv: *Private = self.private();
+ const config_obj = priv.config orelse return;
+ const config = config_obj.get();
+
+ // Both dimensions must be configured
+ if (config.@"window-height" <= 0 or config.@"window-width" <= 0) return;
+
+ const app = Application.default();
+ const alloc = app.allocator();
+
+ // Get content scale and compute DPI
+ const content_scale = self.getContentScale();
+ const x_dpi = content_scale.x * font.face.default_dpi;
+ const y_dpi = content_scale.y * font.face.default_dpi;
+
+ const font_size: font.face.DesiredSize = .{
+ .points = config.@"font-size",
+ .xdpi = @intFromFloat(x_dpi),
+ .ydpi = @intFromFloat(y_dpi),
+ };
+
+ // Get font grid for cell metrics
+ var derived_config = font.SharedGridSet.DerivedConfig.init(alloc, config) catch return;
+ defer derived_config.deinit();
+
+ const font_grid_key, const font_grid = app.core().font_grid_set.ref(
+ &derived_config,
+ font_size,
+ ) catch return;
+ defer app.core().font_grid_set.deref(font_grid_key);
+
+ const cell = font_grid.cellSize();
+
+ const width = @max(CoreSurface.min_window_width_cells, config.@"window-width") * cell.width;
+ const height = @max(CoreSurface.min_window_height_cells, config.@"window-height") * cell.height;
+ const width_f32: f32 = @floatFromInt(width);
+ const height_f32: f32 = @floatFromInt(height);
+
+ const final_width: u32 = @intFromFloat(@ceil(width_f32 / content_scale.x));
+ const final_height: u32 = @intFromFloat(@ceil(height_f32 / content_scale.y));
+
+ self.setDefaultSize(.{ .width = final_width, .height = final_height });
+ }
+
+ /// Get the key sequence list. Full transfer.
+ fn getKeySequence(self: *Self) ?*ext.StringList {
+ const priv = self.private();
+ const alloc = Application.default().allocator();
+ return ext.StringList.create(alloc, priv.key_sequence.items) catch null;
+ }
+
+ /// Get the key table list. Full transfer.
+ fn getKeyTable(self: *Self) ?*ext.StringList {
+ const priv = self.private();
+ const alloc = Application.default().allocator();
+ return ext.StringList.create(alloc, priv.key_tables.items) catch null;
+ }
+
+ /// Return the min size, if set.
+ pub fn getMinSize(self: *Self) ?*Size {
+ const priv = self.private();
+ return priv.min_size;
+ }
+
+ /// Set the min size for a window that contains this surface.
+ /// This is up to the embedding widget to respect this. Generally, only
+ /// the first surface in a window respects this.
+ pub fn setMinSize(self: *Self, size: Size) void {
+ const priv = self.private();
+ if (priv.min_size) |v| ext.boxedFree(
+ Size,
+ v,
+ );
+ priv.min_size = ext.boxedCopy(
+ Size,
+ &size,
+ );
+ self.as(gobject.Object).notifyByPspec(properties.@"min-size".impl.param_spec);
+ }
+
+ pub fn getMouseShape(self: *Self) terminal.MouseShape {
+ return self.private().mouse_shape;
+ }
+
+ pub fn setMouseShape(self: *Self, shape: terminal.MouseShape) void {
+ const priv = self.private();
+ priv.mouse_shape = shape;
+ self.as(gobject.Object).notifyByPspec(properties.@"mouse-shape".impl.param_spec);
+ }
+
+ pub fn getMouseHidden(self: *Self) bool {
+ return self.private().mouse_hidden;
+ }
+
+ pub fn setMouseHidden(self: *Self, hidden: bool) void {
+ const priv = self.private();
+ priv.mouse_hidden = hidden;
+ self.as(gobject.Object).notifyByPspec(properties.@"mouse-hidden".impl.param_spec);
+ }
+
+ pub fn setMouseHoverUrl(self: *Self, url: ?[:0]const u8) void {
+ const priv = self.private();
+ if (priv.mouse_hover_url) |v| glib.free(@ptrCast(@constCast(v)));
+ priv.mouse_hover_url = null;
+ if (url) |v| priv.mouse_hover_url = glib.ext.dupeZ(u8, v);
+ self.as(gobject.Object).notifyByPspec(properties.@"mouse-hover-url".impl.param_spec);
+ }
+
+ pub fn getBellRinging(self: *Self) bool {
+ return self.private().bell_ringing;
+ }
+
+ pub fn setBellRinging(self: *Self, ringing: bool) void {
+ // Prevent duplicate change notifications if the signals we emit
+ // in this function cause this state to change again.
+ self.as(gobject.Object).freezeNotify();
+ defer self.as(gobject.Object).thawNotify();
+
+ // Logic around bell reaction happens on every event even if we're
+ // already in the ringing state.
+ if (ringing) self.ringBell();
+
+ // Property change only happens on actual state change
+ const priv = self.private();
+ if (priv.bell_ringing == ringing) return;
+ priv.bell_ringing = ringing;
+ self.as(gobject.Object).notifyByPspec(properties.@"bell-ringing".impl.param_spec);
+ }
+
+ pub fn setError(self: *Self, v: bool) void {
+ const priv = self.private();
+ priv.@"error" = v;
+ self.as(gobject.Object).notifyByPspec(properties.@"error".impl.param_spec);
+ }
+
+ pub fn setSearchActive(self: *Self, active: bool, needle: [:0]const u8) void {
+ const priv = self.private();
+ var value = gobject.ext.Value.newFrom(active);
+ defer value.unset();
+ gobject.Object.setProperty(
+ priv.search_overlay.as(gobject.Object),
+ SearchOverlay.properties.active.name,
+ &value,
+ );
+
+ if (!std.mem.eql(u8, needle, "")) {
+ priv.search_overlay.setSearchContents(needle);
+ }
+
+ if (active) {
+ priv.search_overlay.grabFocus();
+ }
+ }
+
+ pub fn setSearchTotal(self: *Self, total: ?usize) void {
+ self.private().search_overlay.setSearchTotal(total);
+ }
+
+ pub fn setSearchSelected(self: *Self, selected: ?usize) void {
+ self.private().search_overlay.setSearchSelected(selected);
+ }
+
+ fn propConfig(
+ self: *Self,
+ _: *gobject.ParamSpec,
+ _: ?*anyopaque,
+ ) callconv(.c) void {
+ const priv = self.private();
+ const config = if (priv.config) |c| c.get() else return;
+
+ // resize-overlay-duration
+ {
+ const ms = config.@"resize-overlay-duration".asMilliseconds();
+ var value = gobject.ext.Value.newFrom(ms);
+ defer value.unset();
+ gobject.Object.setProperty(
+ priv.resize_overlay.as(gobject.Object),
+ "duration",
+ &value,
+ );
+ }
+
+ // resize-overlay-position
+ {
+ const hv: struct {
+ gtk.Align, // halign
+ gtk.Align, // valign
+ } = switch (config.@"resize-overlay-position") {
+ .center => .{ .center, .center },
+ .@"top-left" => .{ .start, .start },
+ .@"top-right" => .{ .end, .start },
+ .@"top-center" => .{ .center, .start },
+ .@"bottom-left" => .{ .start, .end },
+ .@"bottom-right" => .{ .end, .end },
+ .@"bottom-center" => .{ .center, .end },
+ };
+
+ var halign = gobject.ext.Value.newFrom(hv[0]);
+ defer halign.unset();
+ var valign = gobject.ext.Value.newFrom(hv[1]);
+ defer valign.unset();
+ gobject.Object.setProperty(
+ priv.resize_overlay.as(gobject.Object),
+ "overlay-halign",
+ &halign,
+ );
+ gobject.Object.setProperty(
+ priv.resize_overlay.as(gobject.Object),
+ "overlay-valign",
+ &valign,
+ );
+ }
+ }
+
+ fn propError(
+ self: *Self,
+ _: *gobject.ParamSpec,
+ _: ?*anyopaque,
+ ) callconv(.c) void {
+ const priv = self.private();
+ if (priv.@"error") {
+ // Ensure we have an opaque background. The window will NOT set
+ // this if we have transparency set and we need an opaque
+ // background for the error message to be readable.
+ self.as(gtk.Widget).addCssClass("background");
+ } else {
+ // Regardless of transparency setting, we remove the background
+ // CSS class from this widget. Parent widgets will set it
+ // appropriately (see window.zig for example).
+ self.as(gtk.Widget).removeCssClass("background");
+ }
+
+ // We need to set our child property on an idle tick, because the
+ // error property can be triggered by signals that are in the middle
+ // of widget mapping and changing our child during that time
+ // results in a hard gtk crash.
+ if (priv.idle_rechild == null) priv.idle_rechild = glib.idleAdd(
+ onIdleRechild,
+ self,
+ );
+ }
+
+ fn onIdleRechild(ud: ?*anyopaque) callconv(.c) c_int {
+ const self: *Self = @ptrCast(@alignCast(ud orelse return 0));
+ const priv = self.private();
+ priv.idle_rechild = null;
+ if (priv.@"error") {
+ self.as(adw.Bin).setChild(priv.error_page.as(gtk.Widget));
+ } else {
+ self.as(adw.Bin).setChild(priv.terminal_page.as(gtk.Widget));
+ }
+ return 0;
+ }
+
+ fn propMouseHoverUrl(
+ self: *Self,
+ _: *gobject.ParamSpec,
+ _: ?*anyopaque,
+ ) callconv(.c) void {
+ const priv = self.private();
+ const visible = if (priv.mouse_hover_url) |v| v.len > 0 else false;
+ priv.url_left.as(gtk.Widget).setVisible(if (visible) 1 else 0);
+ }
+
+ fn propMouseHidden(
+ self: *Self,
+ _: *gobject.ParamSpec,
+ _: ?*anyopaque,
+ ) callconv(.c) void {
+ const priv = self.private();
+
+ // If we're hidden we set it to "none"
+ if (priv.mouse_hidden) {
+ self.as(gtk.Widget).setCursorFromName("none");
+ return;
+ }
+
+ // If we're not hidden we just trigger the mouse shape
+ // prop notification to handle setting the proper mouse shape.
+ self.propMouseShape(undefined, null);
+ }
+
+ fn propMouseShape(
+ self: *Self,
+ _: *gobject.ParamSpec,
+ _: ?*anyopaque,
+ ) callconv(.c) void {
+ const priv = self.private();
+
+ // If our mouse should be hidden currently then we don't
+ // do anything.
+ if (priv.mouse_hidden) return;
+
+ const name: [:0]const u8 = switch (priv.mouse_shape) {
+ .default => "default",
+ .help => "help",
+ .pointer => "pointer",
+ .context_menu => "context-menu",
+ .progress => "progress",
+ .wait => "wait",
+ .cell => "cell",
+ .crosshair => "crosshair",
+ .text => "text",
+ .vertical_text => "vertical-text",
+ .alias => "alias",
+ .copy => "copy",
+ .no_drop => "no-drop",
+ .move => "move",
+ .not_allowed => "not-allowed",
+ .grab => "grab",
+ .grabbing => "grabbing",
+ .all_scroll => "all-scroll",
+ .col_resize => "col-resize",
+ .row_resize => "row-resize",
+ .n_resize => "n-resize",
+ .e_resize => "e-resize",
+ .s_resize => "s-resize",
+ .w_resize => "w-resize",
+ .ne_resize => "ne-resize",
+ .nw_resize => "nw-resize",
+ .se_resize => "se-resize",
+ .sw_resize => "sw-resize",
+ .ew_resize => "ew-resize",
+ .ns_resize => "ns-resize",
+ .nesw_resize => "nesw-resize",
+ .nwse_resize => "nwse-resize",
+ .zoom_in => "zoom-in",
+ .zoom_out => "zoom-out",
+ };
+
+ // Set our new cursor.
+ self.as(gtk.Widget).setCursorFromName(name.ptr);
+ }
+
+ fn vadjValueChanged(adj: *gtk.Adjustment, self: *Self) callconv(.c) void {
+ // This will trigger for every single pixel change in the adjustment,
+ // but our core surface handles the noise from this so that identical
+ // rows are cheap.
+ const core_surface = self.core() orelse return;
+ const row: usize = @intFromFloat(@round(adj.getValue()));
+ _ = core_surface.performBindingAction(.{ .scroll_to_row = row }) catch |err| {
+ log.err("error performing scroll_to_row action err={}", .{err});
+ };
+ }
+
+ fn propVAdjustment(
+ self: *Self,
+ _: *gobject.ParamSpec,
+ _: ?*anyopaque,
+ ) callconv(.c) void {
+ const priv = self.private();
+
+ // When vadjustment is first set, we setup the signal group lazily.
+ // This makes it so that if we don't use scrollbars, we never
+ // pay the memory cost of this.
+ const group: *gobject.SignalGroup = priv.vadj_signal_group orelse group: {
+ const group = gobject.SignalGroup.new(gtk.Adjustment.getGObjectType());
+ group.connect(
+ "value-changed",
+ @ptrCast(&vadjValueChanged),
+ self,
+ );
+
+ priv.vadj_signal_group = group;
+ break :group group;
+ };
+
+ // Setup our signal group target
+ group.setTarget(if (priv.vadj) |v| v.as(gobject.Object) else null);
+ }
+
+ /// Handle bell features that need to happen every time a BEL is received
+ /// Currently this is audio and system but this could change in the future.
+ fn ringBell(self: *Self) void {
+ const priv = self.private();
+
+ // Emit the signal
+ signals.bell.impl.emit(
+ self,
+ null,
+ .{},
+ null,
+ );
+
+ // Activate actions if they exist
+ _ = self.as(gtk.Widget).activateAction("tab.ring-bell", null);
+ _ = self.as(gtk.Widget).activateAction("win.ring-bell", null);
+
+ const config = if (priv.config) |c| c.get() else return;
+
+ // Do our sound
+ if (config.@"bell-features".audio) audio: {
+ const config_path = config.@"bell-audio-path" orelse break :audio;
+ const path, const required = switch (config_path) {
+ .optional => |path| .{ path, false },
+ .required => |path| .{ path, true },
+ };
+
+ const volume = std.math.clamp(
+ config.@"bell-audio-volume",
+ 0.0,
+ 1.0,
+ );
+
+ const media_file = media.fromFilename(path) orelse break :audio;
+ media.playMediaFile(media_file, volume, required);
+ }
+ }
+
+ //---------------------------------------------------------------
+ // Gtk.Scrollable interface implementation
+
+ pub fn getHAdjustment(self: *Self) ?*gtk.Adjustment {
+ return self.private().hadj;
+ }
+
+ pub fn setHAdjustment(self: *Self, adj_: ?*gtk.Adjustment) void {
+ self.as(gobject.Object).freezeNotify();
+ defer self.as(gobject.Object).thawNotify();
+ self.as(gobject.Object).notifyByPspec(properties.hadjustment.impl.param_spec);
+
+ const priv = self.private();
+ if (priv.hadj) |old| {
+ old.as(gobject.Object).unref();
+ priv.hadj = null;
+ }
+
+ const adj = adj_ orelse return;
+ _ = adj.as(gobject.Object).ref();
+ priv.hadj = adj;
+ }
+
+ fn getHAdjustmentValue(self: *Self, value: *gobject.Value) void {
+ gobject.ext.Value.set(value, self.getHAdjustment());
+ }
+
+ fn setHAdjustmentValue(self: *Self, value: *const gobject.Value) void {
+ self.setHAdjustment(gobject.ext.Value.get(value, ?*gtk.Adjustment));
+ }
+
+ pub fn getVAdjustment(self: *Self) ?*gtk.Adjustment {
+ return self.private().vadj;
+ }
+
+ pub fn setVAdjustment(self: *Self, adj_: ?*gtk.Adjustment) void {
+ self.as(gobject.Object).freezeNotify();
+ defer self.as(gobject.Object).thawNotify();
+ self.as(gobject.Object).notifyByPspec(properties.vadjustment.impl.param_spec);
+
+ const priv = self.private();
+
+ if (priv.vadj) |old| {
+ old.as(gobject.Object).unref();
+ priv.vadj = null;
+ }
+
+ const adj = adj_ orelse return;
+ _ = adj.as(gobject.Object).ref();
+ priv.vadj = adj;
+ }
+
+ fn getVAdjustmentValue(self: *Self, value: *gobject.Value) void {
+ gobject.ext.Value.set(value, self.getVAdjustment());
+ }
+
+ fn setVAdjustmentValue(self: *Self, value: *const gobject.Value) void {
+ self.setVAdjustment(gobject.ext.Value.get(value, ?*gtk.Adjustment));
+ }
+
+ //---------------------------------------------------------------
+ // Signal Handlers
+
+ pub fn actionPromptTitle(
+ _: *gio.SimpleAction,
+ _: ?*glib.Variant,
+ self: *Self,
+ ) callconv(.c) void {
+ const surface = self.core() orelse return;
+ _ = surface.performBindingAction(.prompt_surface_title) catch |err| {
+ log.warn("unable to perform prompt title action err={}", .{err});
+ };
+ }
+
+ pub fn actionNotifyOnNextCommandFinish(
+ action: *gio.SimpleAction,
+ _: ?*glib.Variant,
+ _: *Self,
+ ) callconv(.c) void {
+ const state = action.as(gio.Action).getState() orelse glib.Variant.newBoolean(@intFromBool(false));
+ defer state.unref();
+ const bool_variant_type = glib.ext.VariantType.newFor(bool);
+ defer bool_variant_type.free();
+ if (state.isOfType(bool_variant_type) == 0) return;
+ const value = state.getBoolean() != 0;
+ action.setState(glib.Variant.newBoolean(@intFromBool(!value)));
+ }
+
+ fn childExitedClose(
+ _: *ChildExited,
+ self: *Self,
+ ) callconv(.c) void {
+ // This closes the surface with no confirmation.
+ self.close();
+ }
+
+ fn contextMenuClosed(
+ _: *gtk.PopoverMenu,
+ self: *Self,
+ ) callconv(.c) void {
+ // When the context menu closes, it moves focus back to the tab
+ // bar if there are tabs. That's not correct. We need to grab it
+ // on the surface.
+ self.grabFocus();
+ }
+
+ fn inspectorWeakNotify(
+ ud: ?*anyopaque,
+ _: *gobject.Object,
+ ) callconv(.c) void {
+ const self: *Self = @ptrCast(@alignCast(ud orelse return));
+ const priv = self.private();
+ priv.inspector = null;
+ }
+
+ fn dtDrop(
+ _: *gtk.DropTarget,
+ value: *gobject.Value,
+ _: f64,
+ _: f64,
+ self: *Self,
+ ) callconv(.c) c_int {
+ const alloc = Application.default().allocator();
+
+ if (ext.gValueHolds(value, gdk.FileList.getGObjectType())) {
+ var stream: std.Io.Writer.Allocating = .init(alloc);
+ defer stream.deinit();
+
+ var shell_escape_writer: internal_os.ShellEscapeWriter = .init(&stream.writer);
+ const writer = &shell_escape_writer.writer;
+
+ const list: ?*glib.SList = list: {
+ const unboxed = value.getBoxed() orelse return 0;
+ const fl: *gdk.FileList = @ptrCast(@alignCast(unboxed));
+ break :list fl.getFiles();
+ };
+ defer if (list) |v| v.free();
+
+ {
+ var current: ?*glib.SList = list;
+ while (current) |item| : (current = item.f_next) {
+ const file: *gio.File = @ptrCast(@alignCast(item.f_data orelse continue));
+ const path = file.getPath() orelse continue;
+ const slice = std.mem.span(path);
+ defer glib.free(path);
+
+ writer.writeAll(slice) catch |err| {
+ log.err("unable to write path to buffer: {}", .{err});
+ continue;
+ };
+ writer.writeAll("\n") catch |err| {
+ log.err("unable to write to buffer: {}", .{err});
+ continue;
+ };
+ }
+ }
+
+ const string = stream.toOwnedSliceSentinel(0) catch |err| {
+ log.err("unable to convert to a slice: {}", .{err});
+ return 0;
+ };
+ defer alloc.free(string);
+ Clipboard.paste(self, string);
+ return 1;
+ }
+
+ if (ext.gValueHolds(value, gio.File.getGObjectType())) {
+ const object = value.getObject() orelse return 0;
+ const file = gobject.ext.cast(gio.File, object) orelse return 0;
+ const path = file.getPath() orelse return 0;
+ var stream: std.Io.Writer.Allocating = .init(alloc);
+ defer stream.deinit();
+
+ var shell_escape_writer: internal_os.ShellEscapeWriter = .init(&stream.writer);
+ const writer = &shell_escape_writer.writer;
+ writer.writeAll(std.mem.span(path)) catch |err| {
+ log.err("unable to write path to buffer: {}", .{err});
+ return 0;
+ };
+ writer.writeAll("\n") catch |err| {
+ log.err("unable to write to buffer: {}", .{err});
+ return 0;
+ };
+
+ const string = stream.toOwnedSliceSentinel(0) catch |err| {
+ log.err("unable to convert to a slice: {}", .{err});
+ return 0;
+ };
+ defer alloc.free(string);
+ return 1;
+ }
+
+ if (ext.gValueHolds(value, gobject.ext.types.string)) {
+ if (value.getString()) |string| {
+ Clipboard.paste(self, std.mem.span(string));
+ }
+ return 1;
+ }
+
+ return 1;
+ }
+
+ fn ecKeyPressed(
+ ec_key: *gtk.EventControllerKey,
+ keyval: c_uint,
+ keycode: c_uint,
+ gtk_mods: gdk.ModifierType,
+ self: *Self,
+ ) callconv(.c) c_int {
+ return @intFromBool(self.keyEvent(
+ .press,
+ ec_key,
+ keyval,
+ keycode,
+ gtk_mods,
+ ));
+ }
+
+ fn ecKeyReleased(
+ ec_key: *gtk.EventControllerKey,
+ keyval: c_uint,
+ keycode: c_uint,
+ state: gdk.ModifierType,
+ self: *Self,
+ ) callconv(.c) void {
+ _ = self.keyEvent(
+ .release,
+ ec_key,
+ keyval,
+ keycode,
+ state,
+ );
+ }
+
+ fn ecFocusEnter(_: *gtk.EventControllerFocus, self: *Self) callconv(.c) void {
+ self.updateFocus(true);
+ }
+
+ fn ecFocusLeave(_: *gtk.EventControllerFocus, self: *Self) callconv(.c) void {
+ self.updateFocus(false);
+ }
+
+ fn updateFocus(self: *Self, focused: bool) void {
+ const priv = self.private();
+ priv.focused = focused;
+
+ const ctx = priv.im_context.as(gtk.IMContext);
+ if (focused) ctx.focusIn() else ctx.focusOut();
+
+ _ = glib.idleAddOnce(idleFocus, self.ref());
+ self.as(gobject.Object).notifyByPspec(properties.focused.impl.param_spec);
+
+ // Bell stops ringing as soon as we gain focus
+ if (focused) self.setBellRinging(false);
+ }
+
+ /// The focus callback must be triggered on an idle loop source because
+ /// there are actions within libghostty callbacks (such as showing close
+ /// confirmation dialogs) that can trigger focus loss and cause a deadlock
+ /// because the lock may be held during the callback.
+ ///
+ /// Userdata should be a `*Surface`. This will unref once.
+ fn idleFocus(ud: ?*anyopaque) callconv(.c) void {
+ const self: *Self = @ptrCast(@alignCast(ud orelse return));
+ defer self.unref();
+
+ const priv = self.private();
+ const surface = priv.core_surface orelse return;
+ surface.focusCallback(priv.focused) catch |err| {
+ log.warn("error in focus callback err={}", .{err});
+ };
+ }
+
+ fn gcMouseDown(
+ gesture: *gtk.GestureClick,
+ _: c_int,
+ x: f64,
+ y: f64,
+ self: *Self,
+ ) callconv(.c) void {
+ const event = gesture.as(gtk.EventController).getCurrentEvent() orelse return;
+
+ // Bell stops ringing if any mouse button is pressed.
+ self.setBellRinging(false);
+
+ // Get our surface. If we don't have one, ignore this.
+ const priv = self.private();
+ const core_surface = priv.core_surface orelse return;
+
+ // If we don't have focus, grab it.
+ const gl_area_widget = priv.gl_area.as(gtk.Widget);
+ const had_focus = gl_area_widget.hasFocus() != 0;
+ if (!had_focus) {
+ _ = gl_area_widget.grabFocus();
+ }
+
+ // Report the event
+ const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton());
+
+ // If this click is only transitioning split focus, suppress it so
+ // it doesn't get forwarded to the terminal as a mouse event.
+ if (!had_focus and button == .left) {
+ priv.suppress_left_mouse_release = true;
+ return;
+ }
+
+ if (button == .middle and !priv.gtk_enable_primary_paste) {
+ return;
+ }
+
+ const consumed = consumed: {
+ const gtk_mods = event.getModifierState();
+ const mods = gtk_key.translateMods(gtk_mods);
+ break :consumed core_surface.mouseButtonCallback(
+ .press,
+ button,
+ mods,
+ ) catch |err| err: {
+ log.warn("error in key callback err={}", .{err});
+ break :err false;
+ };
+ };
+
+ // If a right click isn't consumed, mouseButtonCallback selects the hovered
+ // word and returns false. We can use this to handle the context menu
+ // opening under normal scenarios.
+ if (!consumed and button == .right) {
+ signals.menu.impl.emit(
+ self,
+ null,
+ .{},
+ null,
+ );
+
+ const rect: gdk.Rectangle = .{
+ .f_x = @intFromFloat(x),
+ .f_y = @intFromFloat(y),
+ .f_width = 1,
+ .f_height = 1,
+ };
+
+ const popover = priv.context_menu.as(gtk.Popover);
+ popover.setPointingTo(&rect);
+ popover.popup();
+ }
+ }
+
+ fn gcMouseUp(
+ gesture: *gtk.GestureClick,
+ _: c_int,
+ _: f64,
+ _: f64,
+ self: *Self,
+ ) callconv(.c) void {
+ const event = gesture.as(gtk.EventController).getCurrentEvent() orelse return;
+
+ const priv = self.private();
+ const surface = priv.core_surface orelse return;
+ const gtk_mods = event.getModifierState();
+ const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton());
+
+ if (button == .left and priv.suppress_left_mouse_release) {
+ priv.suppress_left_mouse_release = false;
+ return;
+ }
+
+ if (button == .middle and !priv.gtk_enable_primary_paste) {
+ return;
+ }
+
+ const mods = gtk_key.translateMods(gtk_mods);
+ const consumed = surface.mouseButtonCallback(
+ .release,
+ button,
+ mods,
+ ) catch |err| {
+ log.warn("error in key callback err={}", .{err});
+ return;
+ };
+
+ // Trigger the on-screen keyboard if we have no selection,
+ // and that the mouse event hasn't been intercepted by the callback.
+ //
+ // It's better to do this here rather than within the core callback
+ // since we have direct access to the underlying gdk.Event here.
+ if (!consumed and button == .left and !surface.hasSelection()) {
+ if (!self.showOnScreenKeyboard(event)) {
+ log.warn("failed to activate the on-screen keyboard", .{});
+ }
+ }
+ }
+
+ fn ecMouseMotion(
+ ec: *gtk.EventControllerMotion,
+ x: f64,
+ y: f64,
+ self: *Self,
+ ) callconv(.c) void {
+ const event = ec.as(gtk.EventController).getCurrentEvent() orelse return;
+ const priv = self.private();
+
+ const scaled = self.scaledCoordinates(x, y);
+ const pos: apprt.CursorPos = .{
+ .x = @floatCast(scaled.x),
+ .y = @floatCast(scaled.y),
+ };
+
+ // There seem to be at least two cases where GTK issues a mouse motion
+ // event without the cursor actually moving:
+ // 1. GLArea is resized under the mouse. This has the unfortunate
+ // side effect of causing focus to potentially change when
+ // `focus-follows-mouse` is enabled.
+ // 2. The window title is updated. This can cause the mouse to unhide
+ // incorrectly when hide-mouse-when-typing is enabled.
+ // To prevent incorrect behavior, we'll only grab focus and
+ // continue with callback logic if the cursor has actually moved.
+ const is_cursor_still = @abs(priv.cursor_pos.x - pos.x) < 1 and
+ @abs(priv.cursor_pos.y - pos.y) < 1;
+ if (is_cursor_still) return;
+
+ // If we don't have focus, and we want it, grab it.
+ if (priv.config) |config| {
+ const gl_area_widget = priv.gl_area.as(gtk.Widget);
+ if (gl_area_widget.hasFocus() == 0 and
+ config.get().@"focus-follows-mouse")
+ {
+ _ = gl_area_widget.grabFocus();
+ }
+ }
+
+ // Our pos changed, update
+ priv.cursor_pos = pos;
+
+ // Notify the callback
+ if (priv.core_surface) |surface| {
+ const gtk_mods = event.getModifierState();
+ const mods = gtk_key.translateMods(gtk_mods);
+ surface.cursorPosCallback(priv.cursor_pos, mods) catch |err| {
+ log.warn("error in cursor pos callback err={}", .{err});
+ };
+ }
+ }
+
+ fn ecMouseLeave(
+ ec_motion: *gtk.EventControllerMotion,
+ self: *Self,
+ ) callconv(.c) void {
+ const event = ec_motion.as(gtk.EventController).getCurrentEvent() orelse return;
+
+ // Get our modifiers
+ const priv = self.private();
+ if (priv.core_surface) |surface| {
+ // If we have a core surface then we can send the cursor pos
+ // callback with an invalid position to indicate the mouse left.
+ const gtk_mods = event.getModifierState();
+ const mods = gtk_key.translateMods(gtk_mods);
+ surface.cursorPosCallback(
+ .{ .x = -1, .y = -1 },
+ mods,
+ ) catch |err| {
+ log.warn("error in cursor pos callback err={}", .{err});
+ return;
+ };
+ }
+ }
+
+ fn ecMouseScrollVerticalPrecisionBegin(
+ _: *gtk.EventControllerScroll,
+ self: *Self,
+ ) callconv(.c) void {
+ self.private().precision_scroll = true;
+ }
+
+ fn ecMouseScrollVerticalPrecisionEnd(
+ _: *gtk.EventControllerScroll,
+ self: *Self,
+ ) callconv(.c) void {
+ self.private().precision_scroll = false;
+ }
+
+ fn ecMouseScrollVertical(
+ _: *gtk.EventControllerScroll,
+ x: f64,
+ y: f64,
+ self: *Self,
+ ) callconv(.c) c_int {
+ const priv: *Private = self.private();
+ const surface = priv.core_surface orelse return 0;
+
+ // Multiply precision scrolls by 10 to get a better response from
+ // touchpad scrolling
+ const multiplier: f64 = if (priv.precision_scroll) 10.0 else 1.0;
+ const scroll_mods: input.ScrollMods = .{
+ .precision = priv.precision_scroll,
+ };
+
+ const scaled = self.scaledCoordinates(x, y);
+ surface.scrollCallback(
+ // We invert because we apply natural scrolling to the values.
+ // This behavior has existed for years without Linux users complaining
+ // but I suspect we'll have to make this configurable in the future
+ // or read a system setting.
+ scaled.x * -1 * multiplier,
+ scaled.y * -1 * multiplier,
+ scroll_mods,
+ ) catch |err| {
+ log.warn("error in scroll callback err={}", .{err});
+ return 0;
+ };
+
+ return 1;
+ }
+
+ fn ecMouseScrollHorizontal(
+ ec: *gtk.EventControllerScroll,
+ x: f64,
+ _: f64,
+ self: *Self,
+ ) callconv(.c) c_int {
+ const priv: *Private = self.private();
+
+ switch (ec.getUnit()) {
+ .surface => {},
+ .wheel => return @intFromBool(false),
+ else => return @intFromBool(false),
+ }
+
+ priv.pending_horizontal_scroll += x;
+
+ if (@abs(priv.pending_horizontal_scroll) < 120) {
+ if (priv.pending_horizontal_scroll_reset) |v| {
+ _ = glib.Source.remove(v);
+ priv.pending_horizontal_scroll_reset = null;
+ }
+ priv.pending_horizontal_scroll_reset = glib.timeoutAdd(500, ecMouseScrollHorizontalReset, self);
+ return @intFromBool(true);
+ }
+
+ _ = self.as(gtk.Widget).activateAction(
+ if (priv.pending_horizontal_scroll < 0.0)
+ "tab.next-page"
+ else
+ "tab.previous-page",
+ null,
+ );
+
+ if (priv.pending_horizontal_scroll_reset) |v| {
+ _ = glib.Source.remove(v);
+ priv.pending_horizontal_scroll_reset = null;
+ }
+
+ priv.pending_horizontal_scroll = 0.0;
+
+ return @intFromBool(true);
+ }
+
+ fn ecMouseScrollHorizontalReset(ud: ?*anyopaque) callconv(.c) c_int {
+ const self: *Self = @ptrCast(@alignCast(ud orelse return @intFromBool(glib.SOURCE_REMOVE)));
+ const priv: *Private = self.private();
+ priv.pending_horizontal_scroll = 0.0;
+ priv.pending_horizontal_scroll_reset = null;
+ return @intFromBool(glib.SOURCE_REMOVE);
+ }
+
+ fn imPreeditStart(
+ _: *gtk.IMMulticontext,
+ self: *Self,
+ ) callconv(.c) void {
+ // log.warn("GTKIM: preedit start", .{});
+
+ // Start our composing state for the input method and reset our
+ // input buffer to empty.
+ const priv = self.private();
+ priv.im_composing = true;
+ priv.im_len = 0;
+ }
+
+ fn imPreeditChanged(
+ ctx: *gtk.IMMulticontext,
+ self: *Self,
+ ) callconv(.c) void {
+ const priv = self.private();
+
+ // Any preedit change should mark that we're composing. Its possible this
+ // is false using fcitx5-hangul and typing "dkssud<space>" ("안녕"). The
+ // second "s" results in a "commit" for "안" which sets composing to false,
+ // but then immediately sends a preedit change for the next symbol. With
+ // composing set to false we won't commit this text. Therefore, we must
+ // ensure it is set here.
+ priv.im_composing = true;
+
+ // We can't set our preedit on our surface unless we're realized.
+ // We do this now because we want to still keep our input method
+ // state coherent.
+ const surface = priv.core_surface orelse return;
+
+ // Get our pre-edit string that we'll use to show the user.
+ var buf: [*:0]u8 = undefined;
+ ctx.as(gtk.IMContext).getPreeditString(
+ &buf,
+ null,
+ null,
+ );
+ defer glib.free(buf);
+ const str = std.mem.sliceTo(buf, 0);
+
+ // Update our preedit state in Ghostty core
+ // log.warn("GTKIM: preedit change str={s}", .{str});
+ surface.preeditCallback(str) catch |err| {
+ log.warn(
+ "error in preedit callback err={}",
+ .{err},
+ );
+ };
+ }
+
+ fn imPreeditEnd(
+ _: *gtk.IMMulticontext,
+ self: *Self,
+ ) callconv(.c) void {
+ // log.warn("GTKIM: preedit end", .{});
+
+ // End our composing state for GTK, allowing us to commit the text.
+ const priv = self.private();
+ priv.im_composing = false;
+
+ // End our preedit state in Ghostty core
+ const surface = priv.core_surface orelse return;
+ surface.preeditCallback(null) catch |err| {
+ log.warn("error in preedit callback err={}", .{err});
+ };
+ }
+
+ fn imCommit(
+ _: *gtk.IMMulticontext,
+ bytes: [*:0]u8,
+ self: *Self,
+ ) callconv(.c) void {
+ const priv = self.private();
+ const str = std.mem.sliceTo(bytes, 0);
+
+ // log.debug("GTKIM: input commit composing={} keyevent={} str={s}", .{
+ // self.im_composing,
+ // self.in_keyevent,
+ // str,
+ // });
+
+ // We need to handle commit specially if we're in a key event.
+ // Specifically, GTK will send us a commit event for basic key
+ // encodings like "a" (on a US layout keyboard). We don't want
+ // to treat this as IME committed text because we want to associate
+ // it with a key event (i.e. "a" key press).
+ switch (priv.in_keyevent) {
+ // If we're not in a key event then this commit is from
+ // some other source (i.e. on-screen keyboard, tablet, etc.)
+ // and we want to commit the text to the core surface.
+ .false => {},
+
+ // If we're in a composing state and in a key event then this
+ // key event is resulting in a commit of multiple keypresses
+ // and we don't want to encode it alongside the keypress.
+ .composing => {},
+
+ // If we're not composing then this commit is just a normal
+ // key encoding and we want our key event to handle it so
+ // that Ghostty can be aware of the key event alongside
+ // the text.
+ .not_composing => {
+ if (str.len > priv.im_buf.len) {
+ log.warn("not enough buffer space for input method commit", .{});
+ return;
+ }
+
+ // Copy our committed text to the buffer
+ @memcpy(priv.im_buf[0..str.len], str);
+ priv.im_len = @intCast(str.len);
+
+ // log.debug("input commit len={}", .{priv.im_len});
+ return;
+ },
+ }
+
+ // If we reach this point from above it means we're composing OR
+ // not in a keypress. In either case, we want to commit the text
+ // given to us because that's what GTK is asking us to do. If we're
+ // not in a keypress it means that this commit came via a non-keyboard
+ // event (i.e. on-screen keyboard, tablet of some kind, etc.).
+
+ // Committing ends composing state
+ priv.im_composing = false;
+
+ // We can't set our preedit on our surface unless we're realized.
+ // We do this now because we want to still keep our input method
+ // state coherent.
+ if (priv.core_surface) |surface| {
+ // End our preedit state. Well-behaved input methods do this for us
+ // by triggering a preedit-end event but some do not (ibus 1.5.29).
+ surface.preeditCallback(null) catch |err| {
+ log.warn("error in preedit callback err={}", .{err});
+ };
+
+ // Send the text to the core surface, associated with no key (an
+ // invalid key, which should produce no PTY encoding).
+ _ = surface.keyCallback(.{
+ .action = .press,
+ .key = .unidentified,
+ .mods = .{},
+ .consumed_mods = .{},
+ .composing = false,
+ .utf8 = str,
+ }) catch |err| {
+ log.warn("error in key callback err={}", .{err});
+ };
+ }
+ }
+
+ fn glareaRealize(
+ _: *gtk.GLArea,
+ self: *Self,
+ ) callconv(.c) void {
+ log.debug("realize", .{});
+
+ // Make the GL area current so we can detect any OpenGL errors. If
+ // we have errors here we can't render and we switch to the error
+ // state.
+ const priv = self.private();
+ priv.gl_area.makeCurrent();
+ if (priv.gl_area.getError()) |err| {
+ log.warn("failed to make GL context current: {s}", .{err.f_message orelse "(no message)"});
+ log.warn("this error is almost always due to a library, driver, or GTK issue", .{});
+ log.warn("this is a common cause of this issue: https://ghostty.org/docs/help/gtk-opengl-context", .{});
+ self.setError(true);
+ return;
+ }
+
+ // If we already have an initialized surface then we notify it.
+ // If we don't, we'll initialize it on the first resize so we have
+ // our proper initial dimensions.
+ if (priv.core_surface) |v| realize: {
+ v.renderer.displayRealized() catch |err| {
+ log.warn("core displayRealized failed err={}", .{err});
+ break :realize;
+ };
+
+ self.redraw();
+ }
+
+ // Setup our input method. We do this here because this will
+ // create a strong reference back to ourself and we want to be
+ // able to release that in unrealize.
+ priv.im_context.as(gtk.IMContext).setClientWidget(self.as(gtk.Widget));
+ }
+
+ fn glareaUnrealize(
+ gl_area: *gtk.GLArea,
+ self: *Self,
+ ) callconv(.c) void {
+ log.debug("unrealize", .{});
+
+ // Notify our core surface
+ const priv = self.private();
+ if (priv.core_surface) |surface| {
+ // There is no guarantee that our GLArea context is current
+ // when unrealize is emitted, so we need to make it current.
+ gl_area.makeCurrent();
+ if (gl_area.getError()) |err| {
+ // I don't know a scenario this can happen, but it means
+ // we probably leaked memory because displayUnrealized
+ // below frees resources that aren't specifically OpenGL
+ // related. I didn't make the OpenGL renderer handle this
+ // scenario because I don't know if its even possible
+ // under valid circumstances, so let's log.
+ log.warn(
+ "gl_area_make_current failed in unrealize msg={s}",
+ .{err.f_message orelse "(no message)"},
+ );
+ log.warn("OpenGL resources and memory likely leaked", .{});
+ return;
+ }
+
+ surface.renderer.displayUnrealized();
+ }
+
+ // Unset our input method
+ priv.im_context.as(gtk.IMContext).setClientWidget(null);
+ }
+
+ fn glareaRender(
+ _: *gtk.GLArea,
+ _: *gdk.GLContext,
+ self: *Self,
+ ) callconv(.c) c_int {
+ // If we don't have a surface then we failed to initialize for
+ // some reason and there's nothing to draw to the GLArea.
+ const priv = self.private();
+ const surface = priv.core_surface orelse return 1;
+
+ surface.renderer.drawFrame(true) catch |err| {
+ log.warn("failed to draw frame err={}", .{err});
+ return 0;
+ };
+
+ return 1;
+ }
+
+ fn glareaResize(
+ gl_area: *gtk.GLArea,
+ width: c_int,
+ height: c_int,
+ self: *Self,
+ ) callconv(.c) void {
+ // Some debug output to help understand what GTK is telling us.
+ {
+ const widget = gl_area.as(gtk.Widget);
+ const scale_factor = widget.getScaleFactor();
+ const window_scale_factor = scale: {
+ const root = widget.getRoot() orelse break :scale 0;
+ const gtk_native = root.as(gtk.Native);
+ const gdk_surface = gtk_native.getSurface() orelse break :scale 0;
+ break :scale gdk_surface.getScaleFactor();
+ };
+
+ log.debug("gl resize width={} height={} scale={} window_scale={}", .{
+ width,
+ height,
+ scale_factor,
+ window_scale_factor,
+ });
+ }
+
+ // Store our cached size
+ const priv = self.private();
+
+ const new_size: apprt.SurfaceSize = .{
+ .width = @intCast(width),
+ .height = @intCast(height),
+ };
+ const changed = !priv.size.eql(&new_size);
+ priv.size = new_size;
+
+ // If our surface is realize, we send callbacks.
+ if (priv.core_surface) |surface| {
+ // We also update the content scale because there is no signal for
+ // content scale change and it seems to trigger a resize event.
+ surface.contentScaleCallback(self.getContentScale()) catch |err| {
+ log.warn("error in content scale callback err={}", .{err});
+ };
+
+ if (changed) {
+ surface.sizeCallback(new_size) catch |err| {
+ log.warn("error in size callback err={}", .{err});
+ };
+ // Setup our resize overlay if configured
+ self.resizeOverlaySchedule();
+ }
+
+ return;
+ }
+
+ // If we don't have a surface, then we initialize it.
+ self.initSurface() catch |err| {
+ log.warn("surface failed to initialize err={}", .{err});
+ };
+ }
+
+ const InitError = Allocator.Error || error{
+ GLAreaError,
+ SurfaceError,
+ };
+
+ fn initSurface(self: *Self) InitError!void {
+ const priv: *Private = self.private();
+ assert(priv.core_surface == null);
+ const gl_area = priv.gl_area;
+
+ // We need to make the context current so we can call GL functions.
+ // This is required for all surface operations.
+ gl_area.makeCurrent();
+ if (gl_area.getError()) |err| {
+ log.warn("failed to make GL context current: {s}", .{err.f_message orelse "(no message)"});
+ log.warn("this error is usually due to a driver or gtk bug", .{});
+ log.warn("this is a common cause of this issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/4950", .{});
+ return error.GLAreaError;
+ }
+
+ const app = Application.default();
+ const alloc = app.allocator();
+
+ // Make our pointer to store our surface
+ const surface = try alloc.create(CoreSurface);
+ errdefer alloc.destroy(surface);
+
+ // Add ourselves to the list of surfaces on the app.
+ try app.core().addSurface(self.rt());
+ errdefer app.core().deleteSurface(self.rt());
+
+ // Initialize our surface configuration.
+ var config = try apprt.surface.newConfig(
+ app.core(),
+ priv.config.?.get(),
+ priv.context,
+ );
+ defer config.deinit();
+
+ if (priv.overrides.command) |c| {
+ config.command = try c.clone(config._arena.?.allocator());
+ }
+ if (priv.overrides.working_directory) |wd| {
+ const config_alloc = config.arenaAlloc();
+ var wd_val: configpkg.WorkingDirectory = .{ .path = try config_alloc.dupe(u8, wd) };
+ try wd_val.finalize(config_alloc);
+ config.@"working-directory" = wd_val;
+ }
+
+ // Properties that can impact surface init
+ if (priv.font_size_request) |size| config.@"font-size" = size.points;
+ if (priv.pwd) |pwd| {
+ const config_alloc = config.arenaAlloc();
+ var wd_val: configpkg.WorkingDirectory = .{ .path = try config_alloc.dupe(u8, pwd) };
+ try wd_val.finalize(config_alloc);
+ config.@"working-directory" = wd_val;
+ }
+
+ // Initialize the surface
+ surface.init(
+ alloc,
+ &config,
+ app.core(),
+ app.rt(),
+ &priv.rt_surface,
+ ) catch |err| {
+ log.warn("failed to initialize surface err={}", .{err});
+ return error.SurfaceError;
+ };
+ errdefer surface.deinit();
+
+ // Store it!
+ priv.core_surface = surface;
+
+ // Emit the signal that we initialized the surface.
+ Surface.signals.init.impl.emit(
+ self,
+ null,
+ .{},
+ null,
+ );
+
+ self.updateFocus(priv.focused);
+ }
+
+ fn resizeOverlaySchedule(self: *Self) void {
+ const priv = self.private();
+ const surface = priv.core_surface orelse return;
+
+ // Only show the resize overlay if its enabled
+ const config = if (priv.config) |c| c.get() else return;
+ switch (config.@"resize-overlay") {
+ .always, .@"after-first" => {},
+ .never => return,
+ }
+
+ // If we have resize overlays enabled, setup an idler
+ // to show that. We do this in an idle tick because doing it
+ // during the resize results in flickering.
+ var buf: [32]u8 = undefined;
+ priv.resize_overlay.setLabel(text: {
+ const grid_size = surface.size.grid();
+ break :text std.fmt.bufPrintZ(
+ &buf,
+ "{d} x {d}",
+ .{
+ grid_size.columns,
+ grid_size.rows,
+ },
+ ) catch |err| err: {
+ log.warn("unable to format text: {}", .{err});
+ break :err "";
+ };
+ });
+ priv.resize_overlay.schedule();
+ }
+
+ fn ecUrlMouseEnter(
+ _: *gtk.EventControllerMotion,
+ _: f64,
+ _: f64,
+ self: *Self,
+ ) callconv(.c) void {
+ const priv = self.private();
+ const right = priv.url_right.as(gtk.Widget);
+ right.setVisible(1);
+ }
+
+ fn ecUrlMouseLeave(
+ _: *gtk.EventControllerMotion,
+ self: *Self,
+ ) callconv(.c) void {
+ const priv = self.private();
+ const right = priv.url_right.as(gtk.Widget);
+ right.setVisible(0);
+ }
+
+ fn titleDialogSet(
+ _: *TitleDialog,
+ title_ptr: [*:0]const u8,
+ self: *Self,
+ ) callconv(.c) void {
+ const title = std.mem.span(title_ptr);
+ self.setTitleOverride(if (title.len == 0) null else title);
+ }
+
+ fn searchStop(_: *SearchOverlay, self: *Self) callconv(.c) void {
+ const surface = self.core() orelse return;
+ _ = surface.performBindingAction(.end_search) catch |err| {
+ log.warn("unable to perform end_search action err={}", .{err});
+ };
+ _ = self.private().gl_area.as(gtk.Widget).grabFocus();
+ }
+
+ fn searchChanged(_: *SearchOverlay, needle: ?[*:0]const u8, self: *Self) callconv(.c) void {
+ const surface = self.core() orelse return;
+ _ = surface.performBindingAction(.{ .search = std.mem.sliceTo(needle orelse "", 0) }) catch |err| {
+ log.warn("unable to perform search action err={}", .{err});
+ };
+ }
+
+ fn searchNextMatch(_: *SearchOverlay, self: *Self) callconv(.c) void {
+ const surface = self.core() orelse return;
+ _ = surface.performBindingAction(.{ .navigate_search = .next }) catch |err| {
+ log.warn("unable to perform navigate_search action err={}", .{err});
+ };
+ }
+
+ fn searchPreviousMatch(_: *SearchOverlay, self: *Self) callconv(.c) void {
+ const surface = self.core() orelse return;
+ _ = surface.performBindingAction(.{ .navigate_search = .previous }) catch |err| {
+ log.warn("unable to perform navigate_search action err={}", .{err});
+ };
+ }
+
+ const C = Common(Self, Private);
+ pub const as = C.as;
+ pub const ref = C.ref;
+ pub const refSink = C.refSink;
+ pub const unref = C.unref;
+ const private = C.private;
+
+ pub const Class = extern struct {
+ parent_class: Parent.Class,
+ var parent: *Parent.Class = undefined;
+ pub const Instance = Self;
+
+ fn init(class: *Class) callconv(.c) void {
+ gobject.ext.ensureType(ResizeOverlay);
+ gobject.ext.ensureType(SearchOverlay);
+ gobject.ext.ensureType(KeyStateOverlay);
+ gobject.ext.ensureType(ChildExited);
+ gtk.Widget.Class.setTemplateFromResource(
+ class.as(gtk.Widget.Class),
+ comptime gresource.blueprint(.{
+ .major = 1,
+ .minor = 2,
+ .name = "surface",
+ }),
+ );
+
+ // Bindings
+ class.bindTemplateChildPrivate("gl_area", .{});
+ class.bindTemplateChildPrivate("url_left", .{});
+ class.bindTemplateChildPrivate("url_right", .{});
+ class.bindTemplateChildPrivate("child_exited_overlay", .{});
+ class.bindTemplateChildPrivate("context_menu", .{});
+ class.bindTemplateChildPrivate("error_page", .{});
+ class.bindTemplateChildPrivate("progress_bar_overlay", .{});
+ class.bindTemplateChildPrivate("resize_overlay", .{});
+ class.bindTemplateChildPrivate("search_overlay", .{});
+ class.bindTemplateChildPrivate("key_state_overlay", .{});
+ class.bindTemplateChildPrivate("terminal_page", .{});
+ class.bindTemplateChildPrivate("drop_target", .{});
+ class.bindTemplateChildPrivate("im_context", .{});
+
+ // Template Callbacks
+ class.bindTemplateCallback("focus_enter", &ecFocusEnter);
+ class.bindTemplateCallback("focus_leave", &ecFocusLeave);
+ class.bindTemplateCallback("key_pressed", &ecKeyPressed);
+ class.bindTemplateCallback("key_released", &ecKeyReleased);
+ class.bindTemplateCallback("mouse_down", &gcMouseDown);
+ class.bindTemplateCallback("mouse_up", &gcMouseUp);
+ class.bindTemplateCallback("mouse_motion", &ecMouseMotion);
+ class.bindTemplateCallback("mouse_leave", &ecMouseLeave);
+ class.bindTemplateCallback("scroll_vertical", &ecMouseScrollVertical);
+ class.bindTemplateCallback("scroll_vertical_begin", &ecMouseScrollVerticalPrecisionBegin);
+ class.bindTemplateCallback("scroll_vertical_end", &ecMouseScrollVerticalPrecisionEnd);
+ class.bindTemplateCallback("scroll_horizontal", &ecMouseScrollHorizontal);
+ class.bindTemplateCallback("drop", &dtDrop);
+ class.bindTemplateCallback("gl_realize", &glareaRealize);
+ class.bindTemplateCallback("gl_unrealize", &glareaUnrealize);
+ class.bindTemplateCallback("gl_render", &glareaRender);
+ class.bindTemplateCallback("gl_resize", &glareaResize);
+ class.bindTemplateCallback("im_preedit_start", &imPreeditStart);
+ class.bindTemplateCallback("im_preedit_changed", &imPreeditChanged);
+ class.bindTemplateCallback("im_preedit_end", &imPreeditEnd);
+ class.bindTemplateCallback("im_commit", &imCommit);
+ class.bindTemplateCallback("url_mouse_enter", &ecUrlMouseEnter);
+ class.bindTemplateCallback("url_mouse_leave", &ecUrlMouseLeave);
+ class.bindTemplateCallback("child_exited_close", &childExitedClose);
+ class.bindTemplateCallback("context_menu_closed", &contextMenuClosed);
+ class.bindTemplateCallback("notify_config", &propConfig);
+ class.bindTemplateCallback("notify_error", &propError);
+ class.bindTemplateCallback("notify_mouse_hover_url", &propMouseHoverUrl);
+ class.bindTemplateCallback("notify_mouse_hidden", &propMouseHidden);
+ class.bindTemplateCallback("notify_mouse_shape", &propMouseShape);
+ class.bindTemplateCallback("notify_vadjustment", &propVAdjustment);
+ class.bindTemplateCallback("should_border_be_shown", &closureShouldBorderBeShown);
+ class.bindTemplateCallback("should_unfocused_split_be_shown", &closureShouldUnfocusedSplitBeShown);
+ class.bindTemplateCallback("search_stop", &searchStop);
+ class.bindTemplateCallback("search_changed", &searchChanged);
+ class.bindTemplateCallback("search_next_match", &searchNextMatch);
+ class.bindTemplateCallback("search_previous_match", &searchPreviousMatch);
+
+ // Properties
+ gobject.ext.registerProperties(class, &.{
+ properties.@"bell-ringing".impl,
+ properties.config.impl,
+ properties.@"child-exited".impl,
+ properties.@"default-size".impl,
+ properties.@"error".impl,
+ properties.@"font-size-request".impl,
+ properties.focused.impl,
+ properties.@"key-sequence".impl,
+ properties.@"key-table".impl,
+ properties.@"min-size".impl,
+ properties.@"mouse-shape".impl,
+ properties.@"mouse-hidden".impl,
+ properties.@"mouse-hover-url".impl,
+ properties.pwd.impl,
+ properties.title.impl,
+ properties.@"title-override".impl,
+ properties.zoom.impl,
+ properties.@"is-split".impl,
+ properties.readonly.impl,
+
+ // For Gtk.Scrollable
+ properties.hadjustment.impl,
+ properties.vadjustment.impl,
+ properties.@"hscroll-policy".impl,
+ properties.@"vscroll-policy".impl,
+ });
+
+ // Signals
+ signals.bell.impl.register(.{});
+ signals.@"close-request".impl.register(.{});
+ signals.@"clipboard-read".impl.register(.{});
+ signals.@"clipboard-write".impl.register(.{});
+ signals.init.impl.register(.{});
+ signals.menu.impl.register(.{});
+ signals.@"present-request".impl.register(.{});
+ signals.@"toggle-fullscreen".impl.register(.{});
+ signals.@"toggle-maximize".impl.register(.{});
+
+ // Virtual methods
+ gobject.Object.virtual_methods.dispose.implement(class, &dispose);
+ gobject.Object.virtual_methods.finalize.implement(class, &finalize);
+ }
+
+ pub const as = C.Class.as;
+ pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
+ pub const bindTemplateCallback = C.Class.bindTemplateCallback;
+ };
+
+ /// Simple dimensions struct for the surface used by various properties.
+ pub const Size = extern struct {
+ width: u32,
+ height: u32,
+
+ pub const getGObjectType = gobject.ext.defineBoxed(
+ Size,
+ .{ .name = "GhosttySurfaceSize" },
+ );
+ };
+};
+
+/// The state of the key event while we're doing IM composition.
+/// See gtkKeyPressed for detailed descriptions.
+pub const IMKeyEvent = enum {
+ /// Not in a key event.
+ false,
+
+ /// In a key event but im_composing was either true or false
+ /// prior to the calling IME processing. This is important to
+ /// work around different input methods calling commit and
+ /// preedit end in a different order.
+ composing,
+ not_composing,
+};
+
+fn translateMouseButton(button: c_uint) input.MouseButton {
+ return switch (button) {
+ 1 => .left,
+ 2 => .middle,
+ 3 => .right,
+ 4 => .four,
+ 5 => .five,
+ 6 => .six,
+ 7 => .seven,
+ 8 => .eight,
+ 9 => .nine,
+ 10 => .ten,
+ 11 => .eleven,
+ else => .unknown,
+ };
+}
+
+/// A namespace for our clipboard-related functions so Surface isn't SO large.
+const Clipboard = struct {
+ /// Set the clipboard contents.
+ pub fn set(
+ self: *Surface,
+ clipboard_type: apprt.Clipboard,
+ contents: []const apprt.ClipboardContent,
+ confirm: bool,
+ ) void {
+ const priv = self.private();
+
+ // Grab our plaintext content for use in confirmation dialogs
+ // and signals. We always expect one to exist.
+ const text: [:0]const u8 = for (contents) |content| {
+ if (std.mem.eql(u8, content.mime, "text/plain")) {
+ break content.data;
+ }
+ } else return;
+
+ // If no confirmation is necessary, set the clipboard.
+ if (!confirm) {
+ const clipboard = get(
+ priv.gl_area.as(gtk.Widget),
+ clipboard_type,
+ ) orelse return;
+
+ const alloc = Application.default().allocator();
+ if (alloc.alloc(*gdk.ContentProvider, contents.len)) |providers| {
+ // Note: we don't need to unref the individual providers
+ // because new_union takes ownership of them.
+ defer alloc.free(providers);
+
+ for (contents, 0..) |content, i| {
+ const bytes = glib.Bytes.new(content.data.ptr, content.data.len);
+ defer bytes.unref();
+ if (std.mem.eql(u8, content.mime, "text/plain")) {
+ // Add an explicit UTF-8 encoding parameter to the
+ // text/plain type. The default charset when there is
+ // none is ASCII, and lots of things look for UTF-8
+ // specifically.
+ // The specs are not clear about the order here, but
+ // some clients apparently pick the first match in the
+ // order we set here then garble up bare 'text/plain'
+ // with non-ASCII UTF-8 content, so offer UTF-8 first.
+ //
+ // Note that under X11, GTK automatically adds the
+ // UTF8_STRING atom when this is present.
+ const text_provider_atoms = [_][:0]const u8{
+ "text/plain;charset=utf-8",
+ "text/plain",
+ };
+ var text_providers: [text_provider_atoms.len]*gdk.ContentProvider = undefined;
+ for (text_provider_atoms, 0..) |atom, j| {
+ const provider = gdk.ContentProvider.newForBytes(atom, bytes);
+ text_providers[j] = provider;
+ }
+ const text_union = gdk.ContentProvider.newUnion(
+ &text_providers,
+ text_providers.len,
+ );
+ providers[i] = text_union;
+ } else {
+ const provider = gdk.ContentProvider.newForBytes(content.mime, bytes);
+ providers[i] = provider;
+ }
+ }
+
+ const all = gdk.ContentProvider.newUnion(providers.ptr, providers.len);
+ defer all.unref();
+ _ = clipboard.setContent(all);
+ } else |_| {
+ // If we fail to alloc, we can at least set the text content.
+ clipboard.setText(text);
+ }
+
+ Surface.signals.@"clipboard-write".impl.emit(
+ self,
+ null,
+ .{ clipboard_type, text.ptr },
+ null,
+ );
+
+ return;
+ }
+
+ showClipboardConfirmation(
+ self,
+ .{ .osc_52_write = clipboard_type },
+ text,
+ );
+ }
+
+ /// Request data from the clipboard (read the clipboard). This
+ /// completes asynchronously and will call the `completeClipboardRequest`
+ /// core surface API when done.
+ ///
+ /// Returns true if the request was started, false if the clipboard
+ /// doesn't contain text (allowing performable keybinds to pass through).
+ pub fn request(
+ self: *Surface,
+ clipboard_type: apprt.Clipboard,
+ state: apprt.ClipboardRequest,
+ ) Allocator.Error!bool {
+ // Get our requested clipboard
+ const clipboard = get(
+ self.private().gl_area.as(gtk.Widget),
+ clipboard_type,
+ ) orelse return false;
+
+ // For paste requests, check if clipboard has text format available.
+ // This is a synchronous check that allows performable keybinds to
+ // pass through when the clipboard contains non-text content (e.g., images).
+ if (state == .paste) {
+ const formats = clipboard.getFormats();
+ if (formats.containGtype(gobject.ext.types.string) == 0) {
+ log.debug("clipboard has no text format, not starting paste request", .{});
+ return false;
+ }
+ }
+
+ // Allocate our userdata
+ const alloc = Application.default().allocator();
+ const ud = try alloc.create(Request);
+ errdefer alloc.destroy(ud);
+ ud.* = .{
+ // Important: we ref self here so that we can't free memory
+ // while we have an outstanding clipboard read.
+ .self = self.ref(),
+ .state = state,
+ };
+ errdefer self.unref();
+
+ // Read
+ clipboard.readTextAsync(
+ null,
+ clipboardReadText,
+ ud,
+ );
+
+ return true;
+ }
+
+ /// Paste explicit text directly into the surface, regardless of the
+ /// actual clipboard contents.
+ pub fn paste(
+ self: *Surface,
+ text: [:0]const u8,
+ ) void {
+ if (text.len == 0) return;
+
+ const surface = self.private().core_surface orelse return;
+ surface.completeClipboardRequest(
+ .paste,
+ text,
+ false,
+ ) catch |err| switch (err) {
+ error.UnsafePaste,
+ error.UnauthorizedPaste,
+ => {
+ showClipboardConfirmation(
+ self,
+ .paste,
+ text,
+ );
+ return;
+ },
+
+ else => {
+ log.warn(
+ "failed to complete clipboard request err={}",
+ .{err},
+ );
+ return;
+ },
+ };
+ }
+
+ /// Get the specific type of clipboard for a widget.
+ fn get(
+ widget: *gtk.Widget,
+ clipboard: apprt.Clipboard,
+ ) ?*gdk.Clipboard {
+ return switch (clipboard) {
+ .standard => widget.getClipboard(),
+ .selection, .primary => widget.getPrimaryClipboard(),
+ };
+ }
+
+ fn showClipboardConfirmation(
+ self: *Surface,
+ req: apprt.ClipboardRequest,
+ str: [:0]const u8,
+ ) void {
+ // Build a text buffer for our contents
+ const contents_buf: *gtk.TextBuffer = .new(null);
+ defer contents_buf.unref();
+ contents_buf.insertAtCursor(str, @intCast(str.len));
+
+ // Confirm
+ const dialog = gobject.ext.newInstance(
+ ClipboardConfirmationDialog,
+ .{
+ .request = &req,
+ .@"can-remember" = switch (req) {
+ .osc_52_read, .osc_52_write => true,
+ .paste => false,
+ },
+ .@"clipboard-contents" = contents_buf,
+ },
+ );
+
+ _ = ClipboardConfirmationDialog.signals.confirm.connect(
+ dialog,
+ *Surface,
+ clipboardConfirmationConfirm,
+ self,
+ .{},
+ );
+ _ = ClipboardConfirmationDialog.signals.deny.connect(
+ dialog,
+ *Surface,
+ clipboardConfirmationDeny,
+ self,
+ .{},
+ );
+
+ dialog.present(self.as(gtk.Widget));
+ }
+
+ fn clipboardConfirmationConfirm(
+ dialog: *ClipboardConfirmationDialog,
+ remember: bool,
+ self: *Surface,
+ ) callconv(.c) void {
+ const priv = self.private();
+ const surface = priv.core_surface orelse return;
+ const req = dialog.getRequest() orelse return;
+
+ // Handle remember
+ if (remember) switch (req.*) {
+ .osc_52_read => surface.config.clipboard_read = .allow,
+ .osc_52_write => surface.config.clipboard_write = .allow,
+ .paste => {},
+ };
+
+ // Get our text
+ const text_buf = dialog.getClipboardContents() orelse return;
+ var text_val = gobject.ext.Value.new(?[:0]const u8);
+ defer text_val.unset();
+ gobject.Object.getProperty(
+ text_buf.as(gobject.Object),
+ "text",
+ &text_val,
+ );
+ const text = gobject.ext.Value.get(
+ &text_val,
+ ?[:0]const u8,
+ ) orelse return;
+
+ surface.completeClipboardRequest(
+ req.*,
+ text,
+ true,
+ ) catch |err| {
+ log.warn("failed to complete clipboard request: {}", .{err});
+ };
+ }
+
+ fn clipboardConfirmationDeny(
+ dialog: *ClipboardConfirmationDialog,
+ remember: bool,
+ self: *Surface,
+ ) callconv(.c) void {
+ const priv = self.private();
+ const surface = priv.core_surface orelse return;
+ const req = dialog.getRequest() orelse return;
+
+ // Handle remember
+ if (remember) switch (req.*) {
+ .osc_52_read => surface.config.clipboard_read = .deny,
+ .osc_52_write => surface.config.clipboard_write = .deny,
+ .paste => @panic("paste should not be able to be remembered"),
+ };
+ }
+
+ fn clipboardReadText(
+ source: ?*gobject.Object,
+ res: *gio.AsyncResult,
+ ud: ?*anyopaque,
+ ) callconv(.c) void {
+ const clipboard = gobject.ext.cast(
+ gdk.Clipboard,
+ source orelse return,
+ ) orelse return;
+ const req: *Request = @ptrCast(@alignCast(ud orelse return));
+
+ const alloc = Application.default().allocator();
+ defer alloc.destroy(req);
+
+ const self = req.self;
+ defer self.unref();
+
+ var gerr: ?*glib.Error = null;
+ const cstr_ = clipboard.readTextFinish(res, &gerr);
+ if (gerr) |err| {
+ defer err.free();
+ log.warn(
+ "failed to read clipboard err={s}",
+ .{err.f_message orelse "(no message)"},
+ );
+ return;
+ }
+ const cstr = cstr_ orelse return;
+ defer glib.free(cstr);
+ const str = std.mem.sliceTo(cstr, 0);
+
+ const surface = self.private().core_surface orelse return;
+ surface.completeClipboardRequest(
+ req.state,
+ str,
+ false,
+ ) catch |err| switch (err) {
+ error.UnsafePaste,
+ error.UnauthorizedPaste,
+ => {
+ showClipboardConfirmation(
+ self,
+ req.state,
+ str,
+ );
+ return;
+ },
+
+ else => {
+ log.warn(
+ "failed to complete clipboard request err={}",
+ .{err},
+ );
+ return;
+ },
+ };
+
+ Surface.signals.@"clipboard-read".impl.emit(
+ self,
+ null,
+ .{},
+ null,
+ );
+ }
+
+ /// The request we send as userdata to the clipboard read.
+ const Request = struct {
+ /// "Self" is reffed so we can't dispose it until the clipboard
+ /// read is complete. Callers must unref when done.
+ self: *Surface,
+ state: apprt.ClipboardRequest,
+ };
+};
+
+/// Compute a fraction [0.0, 1.0] from the supplied progress, which is clamped
+/// to [0, 100].
+fn computeFraction(progress: u8) f64 {
+ return @as(f64, @floatFromInt(std.math.clamp(progress, 0, 100))) / 100.0;
+}
+
+test "computeFraction" {
+ try std.testing.expectEqual(1.0, computeFraction(100));
+ try std.testing.expectEqual(1.0, computeFraction(255));
+ try std.testing.expectEqual(0.0, computeFraction(0));
+ try std.testing.expectEqual(0.5, computeFraction(50));
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/class/surface_child_exited.zig b/vendor/libghostty-vt/src/apprt/gtk/class/surface_child_exited.zig
new file mode 100644
index 00000000..d7dd41bc
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/class/surface_child_exited.zig
@@ -0,0 +1,264 @@
+const std = @import("std");
+const adw = @import("adw");
+const glib = @import("glib");
+const gobject = @import("gobject");
+const gtk = @import("gtk");
+
+const adw_version = @import("../adw_version.zig");
+const apprt = @import("../../../apprt.zig");
+const gresource = @import("../build/gresource.zig");
+const i18n = @import("../../../os/main.zig").i18n;
+const Common = @import("../class.zig").Common;
+
+const log = std.log.scoped(.gtk_ghostty_surface_child_exited);
+
+pub const SurfaceChildExited = if (adw_version.supportsBanner())
+ SurfaceChildExitedBanner
+else
+ SurfaceChildExitedNoop;
+
+/// Child exited overlay based on adw.Banner introduced in
+/// Adwaita 1.3.
+const SurfaceChildExitedBanner = extern struct {
+ const Self = @This();
+ parent_instance: Parent,
+ pub const Parent = adw.Bin;
+ pub const getGObjectType = gobject.ext.defineClass(Self, .{
+ .name = "GhosttySurfaceChildExited",
+ .instanceInit = &init,
+ .classInit = &Class.init,
+ .parent_class = &Class.parent,
+ .private = .{ .Type = Private, .offset = &Private.offset },
+ });
+
+ pub const properties = struct {
+ pub const data = struct {
+ pub const name = "data";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?*apprt.surface.Message.ChildExited,
+ .{
+ .accessor = C.privateBoxedFieldAccessor("data"),
+ },
+ );
+ };
+ };
+
+ pub const signals = struct {
+ /// Emitted when the banner would like to be closed.
+ pub const @"close-request" = struct {
+ pub const name = "close-request";
+ pub const connect = impl.connect;
+ const impl = gobject.ext.defineSignal(
+ name,
+ Self,
+ &.{},
+ void,
+ );
+ };
+ };
+
+ const Private = struct {
+ /// The child exited data sent by the apprt.
+ data: ?*apprt.surface.Message.ChildExited = null,
+
+ // Template bindings
+ banner: *adw.Banner,
+
+ pub var offset: c_int = 0;
+ };
+
+ fn init(self: *Self, _: *Class) callconv(.c) void {
+ gtk.Widget.initTemplate(self.as(gtk.Widget));
+ }
+
+ pub fn setData(
+ self: *Self,
+ data_: ?*const apprt.surface.Message.ChildExited,
+ ) void {
+ const priv = self.private();
+ if (priv.data) |v| glib.ext.destroy(v);
+ const data = data_ orelse {
+ priv.data = null;
+ return;
+ };
+
+ const ptr = glib.ext.create(apprt.surface.Message.ChildExited);
+ ptr.* = data.*;
+ priv.data = ptr;
+ self.as(gobject.Object).notifyByPspec(properties.data.impl.param_spec);
+ }
+
+ //---------------------------------------------------------------
+ // Signal handlers
+
+ fn propData(
+ self: *Self,
+ _: *gobject.ParamSpec,
+ _: ?*anyopaque,
+ ) callconv(.c) void {
+ const priv = self.private();
+ const banner = priv.banner;
+ const data = priv.data orelse {
+ // Not localized on purpose.
+ banner.as(adw.Banner).setTitle("This is a bug in Ghostty. Please report it.");
+ return;
+ };
+ if (data.exit_code == 0) {
+ banner.as(adw.Banner).setTitle(i18n._("Command succeeded"));
+ self.as(gtk.Widget).addCssClass("normal");
+ self.as(gtk.Widget).removeCssClass("abnormal");
+ } else {
+ banner.as(adw.Banner).setTitle(i18n._("Command failed"));
+ self.as(gtk.Widget).removeCssClass("normal");
+ self.as(gtk.Widget).addCssClass("abnormal");
+ }
+ }
+
+ fn closeButtonClicked(
+ _: *adw.Banner,
+ self: *Self,
+ ) callconv(.c) void {
+ signals.@"close-request".impl.emit(
+ self,
+ null,
+ .{},
+ null,
+ );
+ }
+
+ //---------------------------------------------------------------
+ // Virtual methods
+
+ fn dispose(self: *Self) callconv(.c) void {
+ gtk.Widget.disposeTemplate(
+ self.as(gtk.Widget),
+ getGObjectType(),
+ );
+
+ gobject.Object.virtual_methods.dispose.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ fn finalize(self: *Self) callconv(.c) void {
+ const priv = self.private();
+ if (priv.data) |v| {
+ glib.ext.destroy(v);
+ priv.data = null;
+ }
+
+ gobject.Object.virtual_methods.finalize.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ const C = Common(Self, Private);
+ pub const as = C.as;
+ pub const ref = C.ref;
+ pub const unref = C.unref;
+ const private = C.private;
+
+ pub const Class = extern struct {
+ parent_class: Parent.Class,
+ var parent: *Parent.Class = undefined;
+ pub const Instance = Self;
+
+ fn init(class: *Class) callconv(.c) void {
+ gtk.Widget.Class.setTemplateFromResource(
+ class.as(gtk.Widget.Class),
+ comptime gresource.blueprint(.{
+ .major = 1,
+ .minor = 3,
+ .name = "surface-child-exited",
+ }),
+ );
+
+ // Template bindings
+ class.bindTemplateChildPrivate("banner", .{});
+ class.bindTemplateCallback("clicked", &closeButtonClicked);
+ class.bindTemplateCallback("notify_data", &propData);
+
+ // Properties
+ gobject.ext.registerProperties(class, &.{
+ properties.data.impl,
+ });
+
+ // Signals
+ signals.@"close-request".impl.register(.{});
+
+ // Virtual methods
+ gobject.Object.virtual_methods.dispose.implement(class, &dispose);
+ gobject.Object.virtual_methods.finalize.implement(class, &finalize);
+ }
+
+ pub const as = C.Class.as;
+ pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
+ pub const bindTemplateCallback = C.Class.bindTemplateCallback;
+ };
+};
+
+/// Empty widget that does nothing if we don't have a new enough
+/// Adwaita version to support the child exited banner.
+const SurfaceChildExitedNoop = extern struct {
+ /// Can be detected at comptime
+ pub const noop = true;
+
+ const Self = @This();
+ parent_instance: Parent,
+ pub const Parent = gtk.Widget;
+ pub const getGObjectType = gobject.ext.defineClass(Self, .{
+ .name = "GhosttySurfaceChildExited",
+ .classInit = &Class.init,
+ .parent_class = &Class.parent,
+ });
+
+ pub const signals = struct {
+ pub const @"close-request" = struct {
+ pub const name = "close-request";
+ pub const connect = impl.connect;
+ const impl = gobject.ext.defineSignal(
+ name,
+ Self,
+ &.{},
+ void,
+ );
+ };
+ };
+
+ pub fn setData(
+ self: *Self,
+ _: ?*const apprt.surface.Message.ChildExited,
+ ) void {
+ signals.@"close-request".impl.emit(
+ self,
+ null,
+ .{},
+ null,
+ );
+ }
+
+ const C = Common(Self, null);
+ pub const as = C.as;
+ pub const ref = C.ref;
+ pub const unref = C.unref;
+ const private = C.private;
+
+ pub const Class = extern struct {
+ parent_class: Parent.Class,
+ var parent: *Parent.Class = undefined;
+ pub const Instance = Self;
+
+ fn init(class: *Class) callconv(.c) void {
+ _ = class;
+ signals.@"close-request".impl.register(.{});
+ }
+
+ pub const as = C.Class.as;
+ pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
+ pub const bindTemplateCallback = C.Class.bindTemplateCallback;
+ };
+};
diff --git a/vendor/libghostty-vt/src/apprt/gtk/class/surface_scrolled_window.zig b/vendor/libghostty-vt/src/apprt/gtk/class/surface_scrolled_window.zig
new file mode 100644
index 00000000..dd4f17d1
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/class/surface_scrolled_window.zig
@@ -0,0 +1,228 @@
+const std = @import("std");
+const adw = @import("adw");
+const gobject = @import("gobject");
+const gtk = @import("gtk");
+const gtk_version = @import("../gtk_version.zig");
+
+const gresource = @import("../build/gresource.zig");
+const Common = @import("../class.zig").Common;
+const Surface = @import("surface.zig").Surface;
+const Config = @import("config.zig").Config;
+
+const log = std.log.scoped(.gtk_ghostty_surface_scrolled_window);
+
+/// A wrapper widget that embeds a Surface inside a GtkScrolledWindow.
+/// This provides scrollbar functionality for the terminal surface.
+/// The surface property can be set during initialization or changed
+/// dynamically via the surface property.
+pub const SurfaceScrolledWindow = extern struct {
+ const Self = @This();
+ parent_instance: Parent,
+ pub const Parent = adw.Bin;
+ pub const getGObjectType = gobject.ext.defineClass(Self, .{
+ .name = "GhostttySurfaceScrolledWindow",
+ .instanceInit = &init,
+ .classInit = &Class.init,
+ .parent_class = &Class.parent,
+ .private = .{ .Type = Private, .offset = &Private.offset },
+ });
+
+ pub const properties = struct {
+ pub const config = struct {
+ pub const name = "config";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?*Config,
+ .{
+ .accessor = C.privateObjFieldAccessor("config"),
+ },
+ );
+ };
+
+ pub const surface = struct {
+ pub const name = "surface";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?*Surface,
+ .{
+ .accessor = .{
+ .getter = getSurfaceValue,
+ .setter = setSurfaceValue,
+ },
+ },
+ );
+ };
+ };
+
+ const Private = struct {
+ config: ?*Config = null,
+ config_binding: ?*gobject.Binding = null,
+ surface: ?*Surface = null,
+ scrolled_window: *gtk.ScrolledWindow,
+ pub var offset: c_int = 0;
+ };
+
+ fn init(self: *Self, _: *Class) callconv(.c) void {
+ gtk.Widget.initTemplate(self.as(gtk.Widget));
+ if (gtk_version.runtimeUntil(4, 20, 1)) self.disableKineticScroll();
+ }
+
+ fn disableKineticScroll(self: *Self) void {
+ // Until gtk 4.20.1 trackpads have kinetic scrolling behavior regardless
+ // of `Gtk.ScrolledWindow.kinetic_scrolling`. As a workaround, disable
+ // EventControllerScroll.kinetic
+ const controllers = self.private().scrolled_window.as(gtk.Widget).observeControllers();
+ defer controllers.unref();
+ var i: c_uint = 0;
+ while (controllers.getObject(i)) |obj| : (i += 1) {
+ defer obj.unref();
+ const controller = gobject.ext.cast(gtk.EventControllerScroll, obj) orelse continue;
+ var flags = controller.getFlags();
+ flags.kinetic = false;
+ controller.setFlags(flags);
+ }
+ }
+
+ fn dispose(self: *Self) callconv(.c) void {
+ const priv = self.private();
+
+ if (priv.config_binding) |binding| {
+ binding.as(gobject.Object).unref();
+ priv.config_binding = null;
+ }
+
+ if (priv.config) |v| {
+ v.unref();
+ priv.config = null;
+ }
+
+ gtk.Widget.disposeTemplate(
+ self.as(gtk.Widget),
+ getGObjectType(),
+ );
+
+ gobject.Object.virtual_methods.dispose.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ fn finalize(self: *Self) callconv(.c) void {
+ gobject.Object.virtual_methods.finalize.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ fn getSurfaceValue(self: *Self, value: *gobject.Value) void {
+ gobject.ext.Value.set(
+ value,
+ self.private().surface,
+ );
+ }
+
+ fn setSurfaceValue(self: *Self, value: *const gobject.Value) void {
+ self.setSurface(gobject.ext.Value.get(
+ value,
+ ?*Surface,
+ ));
+ }
+
+ pub fn getSurface(self: *Self) ?*Surface {
+ return self.private().surface;
+ }
+
+ pub fn setSurface(self: *Self, surface_: ?*Surface) void {
+ const priv = self.private();
+
+ if (surface_ == priv.surface) return;
+
+ self.as(gobject.Object).freezeNotify();
+ defer self.as(gobject.Object).thawNotify();
+ self.as(gobject.Object).notifyByPspec(properties.surface.impl.param_spec);
+
+ priv.surface = surface_;
+ }
+
+ fn closureScrollbarPolicy(
+ _: *Self,
+ config_: ?*Config,
+ ) callconv(.c) gtk.PolicyType {
+ const config = if (config_) |c| c.get() else return .automatic;
+ return switch (config.scrollbar) {
+ .never => .never,
+ .system => .automatic,
+ };
+ }
+
+ fn propSurface(
+ self: *Self,
+ _: *gobject.ParamSpec,
+ _: ?*anyopaque,
+ ) callconv(.c) void {
+ const priv = self.private();
+ const child: *gtk.Widget = self.as(Parent).getChild().?;
+ const scrolled_window = gobject.ext.cast(gtk.ScrolledWindow, child).?;
+ scrolled_window.setChild(if (priv.surface) |s| s.as(gtk.Widget) else null);
+
+ // Unbind old config binding if it exists
+ if (priv.config_binding) |binding| {
+ binding.as(gobject.Object).unref();
+ priv.config_binding = null;
+ }
+
+ // Bind config from surface to our config property
+ if (priv.surface) |surface| {
+ priv.config_binding = surface.as(gobject.Object).bindProperty(
+ properties.config.name,
+ self.as(gobject.Object),
+ properties.config.name,
+ .{ .sync_create = true },
+ );
+ }
+ }
+
+ const C = Common(Self, Private);
+ pub const as = C.as;
+ pub const ref = C.ref;
+ pub const unref = C.unref;
+ const private = C.private;
+
+ pub const Class = extern struct {
+ parent_class: Parent.Class,
+ var parent: *Parent.Class = undefined;
+ pub const Instance = Self;
+
+ fn init(class: *Class) callconv(.c) void {
+ gtk.Widget.Class.setTemplateFromResource(
+ class.as(gtk.Widget.Class),
+ comptime gresource.blueprint(.{
+ .major = 1,
+ .minor = 5,
+ .name = "surface-scrolled-window",
+ }),
+ );
+
+ // Bindings
+ class.bindTemplateCallback("scrollbar_policy", &closureScrollbarPolicy);
+ class.bindTemplateCallback("notify_surface", &propSurface);
+ class.bindTemplateChildPrivate("scrolled_window", .{});
+
+ // Properties
+ gobject.ext.registerProperties(class, &.{
+ properties.config.impl,
+ properties.surface.impl,
+ });
+
+ // Virtual methods
+ gobject.Object.virtual_methods.dispose.implement(class, &dispose);
+ gobject.Object.virtual_methods.finalize.implement(class, &finalize);
+ }
+
+ pub const as = C.Class.as;
+ pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
+ pub const bindTemplateCallback = C.Class.bindTemplateCallback;
+ };
+};
diff --git a/vendor/libghostty-vt/src/apprt/gtk/class/tab.zig b/vendor/libghostty-vt/src/apprt/gtk/class/tab.zig
new file mode 100644
index 00000000..0c60c8cc
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/class/tab.zig
@@ -0,0 +1,592 @@
+const std = @import("std");
+const adw = @import("adw");
+const gio = @import("gio");
+const glib = @import("glib");
+const gobject = @import("gobject");
+const gtk = @import("gtk");
+
+const configpkg = @import("../../../config.zig");
+const apprt = @import("../../../apprt.zig");
+const CoreSurface = @import("../../../Surface.zig");
+const ext = @import("../ext.zig");
+const gresource = @import("../build/gresource.zig");
+const Common = @import("../class.zig").Common;
+const Config = @import("config.zig").Config;
+const Application = @import("application.zig").Application;
+const SplitTree = @import("split_tree.zig").SplitTree;
+const Surface = @import("surface.zig").Surface;
+const TitleDialog = @import("title_dialog.zig").TitleDialog;
+
+const log = std.log.scoped(.gtk_ghostty_window);
+
+pub const Tab = extern struct {
+ const Self = @This();
+ parent_instance: Parent,
+ pub const Parent = gtk.Box;
+ pub const getGObjectType = gobject.ext.defineClass(Self, .{
+ .name = "GhosttyTab",
+ .instanceInit = &init,
+ .classInit = &Class.init,
+ .parent_class = &Class.parent,
+ .private = .{ .Type = Private, .offset = &Private.offset },
+ });
+
+ pub const properties = struct {
+ /// The active surface is the surface that should be receiving all
+ /// surface-targeted actions. This is usually the focused surface,
+ /// but may also not be focused if the user has selected a non-surface
+ /// widget.
+ pub const @"active-surface" = struct {
+ pub const name = "active-surface";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?*Surface,
+ .{
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ ?*Surface,
+ .{
+ .getter = Self.getActiveSurface,
+ },
+ ),
+ },
+ );
+ };
+
+ pub const config = struct {
+ pub const name = "config";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?*Config,
+ .{
+ .accessor = C.privateObjFieldAccessor("config"),
+ },
+ );
+ };
+
+ pub const @"split-tree" = struct {
+ pub const name = "split-tree";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?*SplitTree,
+ .{
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ ?*SplitTree,
+ .{
+ .getter = getSplitTree,
+ },
+ ),
+ },
+ );
+ };
+
+ pub const @"surface-tree" = struct {
+ pub const name = "surface-tree";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?*Surface.Tree,
+ .{
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ ?*Surface.Tree,
+ .{
+ .getter = getSurfaceTree,
+ },
+ ),
+ },
+ );
+ };
+
+ pub const tooltip = struct {
+ pub const name = "tooltip";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?[:0]const u8,
+ .{
+ .default = null,
+ .accessor = C.privateStringFieldAccessor("tooltip"),
+ },
+ );
+ };
+
+ pub const title = struct {
+ pub const name = "title";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?[:0]const u8,
+ .{
+ .default = null,
+ .accessor = C.privateStringFieldAccessor("title"),
+ },
+ );
+ };
+ pub const @"title-override" = struct {
+ pub const name = "title-override";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?[:0]const u8,
+ .{
+ .default = null,
+ .accessor = C.privateStringFieldAccessor("title_override"),
+ },
+ );
+ };
+ };
+
+ pub const signals = struct {
+ /// Emitted whenever the tab would like to be closed.
+ pub const @"close-request" = struct {
+ pub const name = "close-request";
+ pub const connect = impl.connect;
+ const impl = gobject.ext.defineSignal(
+ name,
+ Self,
+ &.{},
+ void,
+ );
+ };
+ };
+
+ const Private = struct {
+ /// The configuration that this surface is using.
+ config: ?*Config = null,
+
+ /// The title of this tab. This is usually bound to the active surface.
+ title: ?[:0]const u8 = null,
+
+ /// The manually overridden title from `promptTabTitle`.
+ title_override: ?[:0]const u8 = null,
+
+ /// The tooltip of this tab. This is usually bound to the active surface.
+ tooltip: ?[:0]const u8 = null,
+
+ // Template bindings
+ split_tree: *SplitTree,
+
+ pub var offset: c_int = 0;
+ };
+
+ /// Set the parent of this tab page. This only affects the first surface
+ /// ever created for a tab. If a surface was already created this does
+ /// nothing.
+ pub fn setParent(self: *Self, parent: *CoreSurface) void {
+ self.setParentWithContext(parent, .tab);
+ }
+
+ pub fn setParentWithContext(self: *Self, parent: *CoreSurface, context: apprt.surface.NewSurfaceContext) void {
+ if (self.getActiveSurface()) |surface| {
+ surface.setParent(parent, context);
+ }
+ }
+
+ pub fn new(config: ?*Config, overrides: struct {
+ command: ?configpkg.Command = null,
+ working_directory: ?[:0]const u8 = null,
+ title: ?[:0]const u8 = null,
+
+ pub const none: @This() = .{};
+ }) *Self {
+ const tab = gobject.ext.newInstance(Tab, .{});
+
+ const priv: *Private = tab.private();
+
+ if (config) |c| priv.config = c.ref();
+
+ // If our configuration is null then we get the configuration
+ // from the application.
+ if (priv.config == null) {
+ const app = Application.default();
+ priv.config = app.getConfig();
+ }
+
+ tab.as(gobject.Object).notifyByPspec(properties.config.impl.param_spec);
+
+ // Create our initial surface in the split tree.
+ priv.split_tree.newSplit(.right, null, .{
+ .command = overrides.command,
+ .working_directory = overrides.working_directory,
+ .title = overrides.title,
+ }) catch |err| switch (err) {
+ error.OutOfMemory => {
+ // TODO: We should make our "no surfaces" state more aesthetically
+ // pleasing and show something like an "Oops, something went wrong"
+ // message. For now, this is incredibly unlikely.
+ @panic("oom");
+ },
+ };
+
+ return tab;
+ }
+
+ fn init(self: *Self, _: *Class) callconv(.c) void {
+ gtk.Widget.initTemplate(self.as(gtk.Widget));
+
+ // Init our actions
+ self.initActionMap();
+ }
+
+ fn initActionMap(self: *Self) void {
+ const s_param_type = glib.ext.VariantType.newFor([:0]const u8);
+ defer s_param_type.free();
+
+ const actions = [_]ext.actions.Action(Self){
+ .init("close", actionClose, s_param_type),
+ .init("ring-bell", actionRingBell, null),
+ .init("next-page", actionNextPage, null),
+ .init("previous-page", actionPreviousPage, null),
+ .init("prompt-tab-title", actionPromptTabTitle, null),
+ };
+
+ _ = ext.actions.addAsGroup(Self, self, "tab", &actions);
+ }
+
+ //---------------------------------------------------------------
+ // Properties
+
+ /// Overridden title. This will be generally be shown over the title
+ /// unless this is unset (null).
+ pub fn setTitleOverride(self: *Self, title: ?[:0]const u8) void {
+ const priv = self.private();
+ if (priv.title_override) |v| glib.free(@ptrCast(@constCast(v)));
+ priv.title_override = null;
+ if (title) |v| priv.title_override = glib.ext.dupeZ(u8, v);
+ self.as(gobject.Object).notifyByPspec(properties.@"title-override".impl.param_spec);
+ }
+ fn titleDialogSet(
+ _: *TitleDialog,
+ title_ptr: [*:0]const u8,
+ self: *Self,
+ ) callconv(.c) void {
+ const title = std.mem.span(title_ptr);
+ self.setTitleOverride(if (title.len == 0) null else title);
+ }
+ pub fn promptTabTitle(self: *Self) void {
+ const priv = self.private();
+ const dialog = TitleDialog.new(.tab, priv.title_override orelse priv.title);
+ _ = TitleDialog.signals.set.connect(
+ dialog,
+ *Self,
+ titleDialogSet,
+ self,
+ .{},
+ );
+
+ dialog.present(self.as(gtk.Widget));
+ }
+
+ /// Get the currently active surface. See the "active-surface" property.
+ /// This does not ref the value.
+ pub fn getActiveSurface(self: *Self) ?*Surface {
+ return self.getSplitTree().getActiveSurface();
+ }
+
+ /// Get the surface tree of this tab.
+ pub fn getSurfaceTree(self: *Self) ?*Surface.Tree {
+ const priv = self.private();
+ return priv.split_tree.getTree();
+ }
+
+ /// Get the split tree widget that is in this tab.
+ pub fn getSplitTree(self: *Self) *SplitTree {
+ const priv = self.private();
+ return priv.split_tree;
+ }
+
+ /// Returns true if this tab needs confirmation before quitting based
+ /// on the various Ghostty configurations.
+ pub fn getNeedsConfirmQuit(self: *Self) bool {
+ const tree = self.getSplitTree();
+ return tree.getNeedsConfirmQuit();
+ }
+
+ /// Get the tab view holding this tab, if any.
+ fn getTabView(self: *Self) ?*adw.TabView {
+ return ext.getAncestor(
+ adw.TabView,
+ self.as(gtk.Widget),
+ );
+ }
+
+ /// Get the tab page holding this tab, if any.
+ fn getTabPage(self: *Self) ?*adw.TabPage {
+ const tab_view = self.getTabView() orelse return null;
+ return tab_view.getPage(self.as(gtk.Widget));
+ }
+
+ //---------------------------------------------------------------
+ // Virtual methods
+
+ fn dispose(self: *Self) callconv(.c) void {
+ const priv = self.private();
+ if (priv.config) |v| {
+ v.unref();
+ priv.config = null;
+ }
+
+ gtk.Widget.disposeTemplate(
+ self.as(gtk.Widget),
+ getGObjectType(),
+ );
+
+ gobject.Object.virtual_methods.dispose.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ fn finalize(self: *Self) callconv(.c) void {
+ const priv = self.private();
+ if (priv.tooltip) |v| {
+ glib.free(@ptrCast(@constCast(v)));
+ priv.tooltip = null;
+ }
+ if (priv.title) |v| {
+ glib.free(@ptrCast(@constCast(v)));
+ priv.title = null;
+ }
+ if (priv.title_override) |v| {
+ glib.free(@ptrCast(@constCast(v)));
+ priv.title_override = null;
+ }
+
+ gobject.Object.virtual_methods.finalize.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+ //---------------------------------------------------------------
+ // Signal handlers
+
+ fn propSplitTree(
+ _: *SplitTree,
+ _: *gobject.ParamSpec,
+ self: *Self,
+ ) callconv(.c) void {
+ self.as(gobject.Object).notifyByPspec(properties.@"surface-tree".impl.param_spec);
+
+ // If our tree is empty we close the tab.
+ const tree: *const Surface.Tree = self.getSurfaceTree() orelse &.empty;
+ if (tree.isEmpty()) {
+ signals.@"close-request".impl.emit(
+ self,
+ null,
+ .{},
+ null,
+ );
+ return;
+ }
+ }
+
+ fn propActiveSurface(
+ _: *SplitTree,
+ _: *gobject.ParamSpec,
+ self: *Self,
+ ) callconv(.c) void {
+ self.as(gobject.Object).notifyByPspec(properties.@"active-surface".impl.param_spec);
+ }
+
+ fn actionClose(
+ _: *gio.SimpleAction,
+ param_: ?*glib.Variant,
+ self: *Self,
+ ) callconv(.c) void {
+ const param = param_ orelse {
+ log.warn("tab.close-tab called without a parameter", .{});
+ return;
+ };
+
+ var str: ?[*:0]const u8 = null;
+ param.get("&s", &str);
+
+ const tab_view = self.getTabView() orelse return;
+ const page = tab_view.getPage(self.as(gtk.Widget));
+
+ const mode = std.meta.stringToEnum(
+ apprt.action.CloseTabMode,
+ std.mem.span(
+ str orelse {
+ log.warn("invalid mode provided to tab.close-tab", .{});
+ return;
+ },
+ ),
+ ) orelse {
+ // Need to be defensive here since actions can be triggered externally.
+ log.warn("invalid mode provided to tab.close-tab: {s}", .{str.?});
+ return;
+ };
+
+ // Delegate to our parent to handle this, since this will emit
+ // a close-page signal that the parent can intercept.
+ switch (mode) {
+ .this => tab_view.closePage(page),
+ .other => tab_view.closeOtherPages(page),
+ .right => tab_view.closePagesAfter(page),
+ }
+ }
+
+ fn actionPromptTabTitle(
+ _: *gio.SimpleAction,
+ _: ?*glib.Variant,
+ self: *Self,
+ ) callconv(.c) void {
+ self.promptTabTitle();
+ }
+
+ fn actionRingBell(
+ _: *gio.SimpleAction,
+ _: ?*glib.Variant,
+ self: *Self,
+ ) callconv(.c) void {
+ // Future note: I actually don't like this logic living here at all.
+ // I think a better approach will be for the ring bell action to
+ // specify its sending surface and then do all this in the window.
+
+ // If the page is selected already we don't mark it as needing
+ // attention. We only want to mark unfocused pages. This will then
+ // clear when the page is selected.
+ const page = self.getTabPage() orelse return;
+ if (page.getSelected() != 0) return;
+ page.setNeedsAttention(@intFromBool(true));
+ }
+
+ /// Select the next tab page.
+ fn actionNextPage(
+ _: *gio.SimpleAction,
+ _: ?*glib.Variant,
+ self: *Self,
+ ) callconv(.c) void {
+ const tab_view = self.getTabView() orelse return;
+ _ = tab_view.selectNextPage();
+ }
+
+ /// Select the previous tab page.
+ fn actionPreviousPage(
+ _: *gio.SimpleAction,
+ _: ?*glib.Variant,
+ self: *Self,
+ ) callconv(.c) void {
+ const tab_view = self.getTabView() orelse return;
+ _ = tab_view.selectPreviousPage();
+ }
+
+ fn closureComputedTitle(
+ _: *Self,
+ config_: ?*Config,
+ terminal_: ?[*:0]const u8,
+ surface_override_: ?[*:0]const u8,
+ tab_override_: ?[*:0]const u8,
+ zoomed_: c_int,
+ bell_ringing_: c_int,
+ _: *gobject.ParamSpec,
+ ) callconv(.c) ?[*:0]const u8 {
+ const zoomed = zoomed_ != 0;
+ const bell_ringing = bell_ringing_ != 0;
+
+ // Our plain title is the manually tab overridden title if it exists,
+ // otherwise the overridden title if it exists, otherwise
+ // the terminal title if it exists, otherwise a default string.
+ const plain = plain: {
+ const default = "Ghostty";
+ const config_title: ?[*:0]const u8 = title: {
+ const config = config_ orelse break :title null;
+ break :title config.get().title orelse null;
+ };
+
+ const plain = tab_override_ orelse
+ surface_override_ orelse
+ terminal_ orelse
+ config_title orelse
+ break :plain default;
+ break :plain std.mem.span(plain);
+ };
+
+ // We don't need a config in every case, but if we don't have a config
+ // let's just assume something went terribly wrong and use our
+ // default title. Its easier then guarding on the config existing
+ // in every case for something so unlikely.
+ const config = if (config_) |v| v.get() else {
+ log.warn("config unavailable for computed title, likely bug", .{});
+ return glib.ext.dupeZ(u8, plain);
+ };
+
+ // Use an allocator to build up our string as we write it.
+ var buf: std.Io.Writer.Allocating = .init(Application.default().allocator());
+ defer buf.deinit();
+
+ // If our bell is ringing, then we prefix the bell icon to the title.
+ if (bell_ringing and config.@"bell-features".title) {
+ buf.writer.writeAll("🔔 ") catch {};
+ }
+
+ // If we're zoomed, prefix with the magnifying glass emoji.
+ if (zoomed) {
+ buf.writer.writeAll("🔍 ") catch {};
+ }
+
+ buf.writer.writeAll(plain) catch return glib.ext.dupeZ(u8, plain);
+ return glib.ext.dupeZ(u8, buf.written());
+ }
+
+ const C = Common(Self, Private);
+ pub const as = C.as;
+ pub const ref = C.ref;
+ pub const unref = C.unref;
+ const private = C.private;
+
+ pub const Class = extern struct {
+ parent_class: Parent.Class,
+ var parent: *Parent.Class = undefined;
+ pub const Instance = Self;
+
+ fn init(class: *Class) callconv(.c) void {
+ gobject.ext.ensureType(SplitTree);
+ gobject.ext.ensureType(Surface);
+ gtk.Widget.Class.setTemplateFromResource(
+ class.as(gtk.Widget.Class),
+ comptime gresource.blueprint(.{
+ .major = 1,
+ .minor = 5,
+ .name = "tab",
+ }),
+ );
+
+ // Properties
+ gobject.ext.registerProperties(class, &.{
+ properties.@"active-surface".impl,
+ properties.config.impl,
+ properties.@"split-tree".impl,
+ properties.@"surface-tree".impl,
+ properties.title.impl,
+ properties.@"title-override".impl,
+ properties.tooltip.impl,
+ });
+
+ // Bindings
+ class.bindTemplateChildPrivate("split_tree", .{});
+
+ // Template Callbacks
+ class.bindTemplateCallback("computed_title", &closureComputedTitle);
+ class.bindTemplateCallback("notify_active_surface", &propActiveSurface);
+ class.bindTemplateCallback("notify_tree", &propSplitTree);
+
+ // Signals
+ signals.@"close-request".impl.register(.{});
+
+ // Virtual methods
+ gobject.Object.virtual_methods.dispose.implement(class, &dispose);
+ gobject.Object.virtual_methods.finalize.implement(class, &finalize);
+ }
+
+ pub const as = C.Class.as;
+ pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
+ pub const bindTemplateCallback = C.Class.bindTemplateCallback;
+ };
+};
diff --git a/vendor/libghostty-vt/src/apprt/gtk/class/title_dialog.zig b/vendor/libghostty-vt/src/apprt/gtk/class/title_dialog.zig
new file mode 100644
index 00000000..ac95ae4b
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/class/title_dialog.zig
@@ -0,0 +1,234 @@
+const std = @import("std");
+const adw = @import("adw");
+const gio = @import("gio");
+const glib = @import("glib");
+const gobject = @import("gobject");
+const gtk = @import("gtk");
+
+const gresource = @import("../build/gresource.zig");
+const i18n = @import("../../../os/main.zig").i18n;
+const ext = @import("../ext.zig");
+const Common = @import("../class.zig").Common;
+const Dialog = @import("dialog.zig").Dialog;
+
+const log = std.log.scoped(.gtk_ghostty_title_dialog);
+
+pub const TitleDialog = extern struct {
+ const Self = @This();
+ parent_instance: Parent,
+ pub const Parent = adw.AlertDialog;
+ pub const getGObjectType = gobject.ext.defineClass(Self, .{
+ .name = "GhosttyTitleDialog",
+ .instanceInit = &init,
+ .classInit = &Class.init,
+ .parent_class = &Class.parent,
+ .private = .{ .Type = Private, .offset = &Private.offset },
+ });
+
+ pub const properties = struct {
+ pub const target = struct {
+ pub const name = "target";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ Target,
+ .{
+ .default = .surface,
+ .accessor = gobject.ext
+ .privateFieldAccessor(
+ Self,
+ Private,
+ &Private.offset,
+ "target",
+ ),
+ },
+ );
+ };
+ pub const @"initial-value" = struct {
+ pub const name = "initial-value";
+ pub const get = impl.get;
+ pub const set = impl.set;
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?[:0]const u8,
+ .{
+ .default = null,
+ .accessor = C.privateStringFieldAccessor("initial_value"),
+ },
+ );
+ };
+ };
+
+ pub const signals = struct {
+ /// Set the title to the given value.
+ pub const set = struct {
+ pub const name = "set";
+ pub const connect = impl.connect;
+ const impl = gobject.ext.defineSignal(
+ name,
+ Self,
+ &.{[*:0]const u8},
+ void,
+ );
+ };
+ };
+
+ const Private = struct {
+ /// The initial value of the entry field.
+ initial_value: ?[:0]const u8 = null,
+
+ // Template bindings
+ target: Target,
+ entry: *gtk.Entry,
+
+ pub var offset: c_int = 0;
+ };
+
+ fn init(self: *Self, _: *Class) callconv(.c) void {
+ gtk.Widget.initTemplate(self.as(gtk.Widget));
+ }
+
+ pub fn new(target: Target, initial_value: ?[:0]const u8) *Self {
+ return gobject.ext.newInstance(Self, .{ .target = target, .@"initial-value" = initial_value });
+ }
+
+ pub fn present(self: *Self, parent_: *gtk.Widget) void {
+ // If we have a window we can attach to, we prefer that.
+ const parent: *gtk.Widget = if (ext.getAncestor(
+ adw.ApplicationWindow,
+ parent_,
+ )) |window|
+ window.as(gtk.Widget)
+ else if (ext.getAncestor(
+ adw.Window,
+ parent_,
+ )) |window|
+ window.as(gtk.Widget)
+ else
+ parent_;
+
+ // Set our initial value
+ const priv = self.private();
+ if (priv.initial_value) |v| {
+ priv.entry.getBuffer().setText(v, -1);
+ }
+
+ // Set the title for the dialog
+ self.as(Dialog.Parent).setHeading(priv.target.title());
+
+ // Show it. We could also just use virtual methods to bind to
+ // response but this is pretty simple.
+ self.as(adw.AlertDialog).choose(
+ parent,
+ null,
+ alertDialogReady,
+ self,
+ );
+ }
+
+ fn alertDialogReady(
+ _: ?*gobject.Object,
+ result: *gio.AsyncResult,
+ ud: ?*anyopaque,
+ ) callconv(.c) void {
+ const self: *Self = @ptrCast(@alignCast(ud));
+ const response = self.as(adw.AlertDialog).chooseFinish(result);
+
+ // If we didn't hit "okay" then we do nothing.
+ if (std.mem.orderZ(u8, "ok", response) != .eq) return;
+
+ // Emit our signal with the new title.
+ const title = std.mem.span(self.private().entry.getBuffer().getText());
+ signals.set.impl.emit(
+ self,
+ null,
+ .{title.ptr},
+ null,
+ );
+ }
+
+ fn dispose(self: *Self) callconv(.c) void {
+ gtk.Widget.disposeTemplate(
+ self.as(gtk.Widget),
+ getGObjectType(),
+ );
+
+ gobject.Object.virtual_methods.dispose.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ fn finalize(self: *Self) callconv(.c) void {
+ const priv = self.private();
+ if (priv.initial_value) |v| {
+ glib.free(@ptrCast(@constCast(v)));
+ priv.initial_value = null;
+ }
+
+ gobject.Object.virtual_methods.finalize.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ const C = Common(Self, Private);
+ pub const as = C.as;
+ pub const ref = C.ref;
+ pub const unref = C.unref;
+ const private = C.private;
+
+ pub const Class = extern struct {
+ parent_class: Parent.Class,
+ var parent: *Parent.Class = undefined;
+ pub const Instance = Self;
+
+ fn init(class: *Class) callconv(.c) void {
+ gtk.Widget.Class.setTemplateFromResource(
+ class.as(gtk.Widget.Class),
+ comptime gresource.blueprint(.{
+ .major = 1,
+ .minor = 5,
+ .name = "title-dialog",
+ }),
+ );
+
+ // Signals
+ signals.set.impl.register(.{});
+
+ // Bindings
+ class.bindTemplateChildPrivate("entry", .{});
+
+ // Properties
+ gobject.ext.registerProperties(class, &.{
+ properties.@"initial-value".impl,
+ properties.target.impl,
+ });
+
+ // Virtual methods
+ gobject.Object.virtual_methods.dispose.implement(class, &dispose);
+ gobject.Object.virtual_methods.finalize.implement(class, &finalize);
+ }
+
+ pub const as = C.Class.as;
+ pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
+ pub const bindTemplateCallback = C.Class.bindTemplateCallback;
+ };
+};
+
+pub const Target = enum(c_int) {
+ surface,
+ tab,
+ pub fn title(self: Target) [*:0]const u8 {
+ return switch (self) {
+ .surface => i18n._("Change Terminal Title"),
+ .tab => i18n._("Change Tab Title"),
+ };
+ }
+
+ pub const getGObjectType = gobject.ext.defineEnum(
+ Target,
+ .{ .name = "GhosttyTitleDialogTarget" },
+ );
+};
diff --git a/vendor/libghostty-vt/src/apprt/gtk/class/window.zig b/vendor/libghostty-vt/src/apprt/gtk/class/window.zig
new file mode 100644
index 00000000..bf2a2fe7
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/class/window.zig
@@ -0,0 +1,2111 @@
+const std = @import("std");
+const build_config = @import("../../../build_config.zig");
+const assert = @import("../../../quirks.zig").inlineAssert;
+const adw = @import("adw");
+const gdk = @import("gdk");
+const gio = @import("gio");
+const glib = @import("glib");
+const gobject = @import("gobject");
+const gtk = @import("gtk");
+
+const i18n = @import("../../../os/main.zig").i18n;
+const apprt = @import("../../../apprt.zig");
+const configpkg = @import("../../../config.zig");
+const TitlebarStyle = configpkg.Config.GtkTitlebarStyle;
+const input = @import("../../../input.zig");
+const CoreSurface = @import("../../../Surface.zig");
+const ext = @import("../ext.zig");
+const gtk_version = @import("../gtk_version.zig");
+const adw_version = @import("../adw_version.zig");
+const gresource = @import("../build/gresource.zig");
+const winprotopkg = @import("../winproto.zig");
+const Common = @import("../class.zig").Common;
+const Config = @import("config.zig").Config;
+const Application = @import("application.zig").Application;
+const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog;
+const SplitTree = @import("split_tree.zig").SplitTree;
+const Surface = @import("surface.zig").Surface;
+const Tab = @import("tab.zig").Tab;
+const DebugWarning = @import("debug_warning.zig").DebugWarning;
+const CommandPalette = @import("command_palette.zig").CommandPalette;
+const WeakRef = @import("../weak_ref.zig").WeakRef;
+
+const log = std.log.scoped(.gtk_ghostty_window);
+
+pub const Window = extern struct {
+ const Self = @This();
+ parent_instance: Parent,
+ pub const Parent = adw.ApplicationWindow;
+ pub const getGObjectType = gobject.ext.defineClass(Self, .{
+ .name = "GhosttyWindow",
+ .instanceInit = &init,
+ .classInit = &Class.init,
+ .parent_class = &Class.parent,
+ .private = .{ .Type = Private, .offset = &Private.offset },
+ });
+
+ pub const properties = struct {
+ /// The active surface is the focus that should be receiving all
+ /// surface-targeted actions. This is usually the focused surface,
+ /// but may also not be focused if the user has selected a non-surface
+ /// widget.
+ pub const @"active-surface" = struct {
+ pub const name = "active-surface";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?*Surface,
+ .{
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ ?*Surface,
+ .{
+ .getter = Self.getActiveSurface,
+ },
+ ),
+ },
+ );
+ };
+
+ pub const config = struct {
+ pub const name = "config";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ ?*Config,
+ .{
+ .accessor = C.privateObjFieldAccessor("config"),
+ },
+ );
+ };
+
+ pub const debug = struct {
+ pub const name = "debug";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ bool,
+ .{
+ .default = build_config.is_debug,
+ .accessor = gobject.ext.typedAccessor(Self, bool, .{
+ .getter = struct {
+ pub fn getter(_: *Self) bool {
+ return build_config.is_debug;
+ }
+ }.getter,
+ }),
+ },
+ );
+ };
+
+ pub const @"titlebar-style" = struct {
+ pub const name = "titlebar-style";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ TitlebarStyle,
+ .{
+ .default = .native,
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ TitlebarStyle,
+ .{
+ .getter = Self.getTitlebarStyle,
+ },
+ ),
+ },
+ );
+ };
+
+ pub const @"headerbar-visible" = struct {
+ pub const name = "headerbar-visible";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ bool,
+ .{
+ .default = true,
+ .accessor = gobject.ext.typedAccessor(Self, bool, .{
+ .getter = Self.getHeaderbarVisible,
+ }),
+ },
+ );
+ };
+
+ pub const @"quick-terminal" = struct {
+ pub const name = "quick-terminal";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ bool,
+ .{
+ .default = true,
+ .accessor = gobject.ext.privateFieldAccessor(
+ Self,
+ Private,
+ &Private.offset,
+ "quick_terminal",
+ ),
+ },
+ );
+ };
+
+ pub const @"tabs-autohide" = struct {
+ pub const name = "tabs-autohide";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ bool,
+ .{
+ .default = true,
+ .accessor = gobject.ext.typedAccessor(Self, bool, .{
+ .getter = Self.getTabsAutohide,
+ }),
+ },
+ );
+ };
+
+ pub const @"tabs-wide" = struct {
+ pub const name = "tabs-wide";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ bool,
+ .{
+ .default = true,
+ .accessor = gobject.ext.typedAccessor(Self, bool, .{
+ .getter = Self.getTabsWide,
+ }),
+ },
+ );
+ };
+
+ pub const @"tabs-visible" = struct {
+ pub const name = "tabs-visible";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ bool,
+ .{
+ .default = true,
+ .accessor = gobject.ext.typedAccessor(Self, bool, .{
+ .getter = Self.getTabsVisible,
+ }),
+ },
+ );
+ };
+
+ pub const @"toolbar-style" = struct {
+ pub const name = "toolbar-style";
+ const impl = gobject.ext.defineProperty(
+ name,
+ Self,
+ adw.ToolbarStyle,
+ .{
+ .default = .raised,
+ .accessor = gobject.ext.typedAccessor(
+ Self,
+ adw.ToolbarStyle,
+ .{
+ .getter = Self.getToolbarStyle,
+ },
+ ),
+ },
+ );
+ };
+ };
+
+ const Private = struct {
+ /// Whether this window is a quick terminal. If it is then it
+ /// behaves slightly differently under certain scenarios.
+ quick_terminal: bool = false,
+
+ /// The window decoration override. If this is not set then we'll
+ /// inherit whatever the config has. This allows overriding the
+ /// config on a per-window basis.
+ window_decoration: ?configpkg.WindowDecoration = null,
+
+ /// Binding group for our active tab.
+ tab_bindings: *gobject.BindingGroup,
+
+ /// The configuration that this surface is using.
+ config: ?*Config = null,
+
+ /// State and logic for windowing protocol for a window.
+ winproto: winprotopkg.Window,
+
+ /// Kind of hacky to have this but this lets us know if we've
+ /// initialized any single surface yet. We need this because we
+ /// gate default size on this so that we don't resize the window
+ /// after surfaces already exist.
+ ///
+ /// I think long term we can probably get rid of this by implementing
+ /// a property or method that gets us all the surfaces in all the
+ /// tabs and checking if we have zero or one that isn't initialized.
+ ///
+ /// For now, this logic is more similar to our legacy GTK side.
+ surface_init: bool = false,
+
+ /// See tabOverviewOpen for why we have this.
+ tab_overview_focus_timer: ?c_uint = null,
+
+ /// A weak reference to a command palette.
+ command_palette: WeakRef(CommandPalette) = .empty,
+
+ /// Tab page that the context menu was opened for.
+ /// setup by `setup-menu`.
+ context_menu_page: ?*adw.TabPage = null,
+
+ // Template bindings
+ tab_overview: *adw.TabOverview,
+ tab_bar: *adw.TabBar,
+ tab_view: *adw.TabView,
+ toolbar: *adw.ToolbarView,
+ toast_overlay: *adw.ToastOverlay,
+
+ pub var offset: c_int = 0;
+ };
+
+ pub fn new(
+ app: *Application,
+ overrides: struct {
+ title: ?[:0]const u8 = null,
+
+ pub const none: @This() = .{};
+ },
+ ) *Self {
+ const win = gobject.ext.newInstance(Self, .{
+ .application = app,
+ });
+
+ if (overrides.title) |title| {
+ // If the overrides have a title set, we set that immediately
+ // so that any applications inspecting the window states see an
+ // immediate title set when the window appears, rather than waiting
+ // possibly a few event loop ticks for it to sync from the surface.
+ win.as(gtk.Window).setTitle(title);
+ }
+
+ return win;
+ }
+
+ fn init(self: *Self, _: *Class) callconv(.c) void {
+ gtk.Widget.initTemplate(self.as(gtk.Widget));
+
+ // If our configuration is null then we get the configuration
+ // from the application.
+ const priv = self.private();
+
+ const config = config: {
+ if (priv.config) |config| break :config config.get();
+ const app = Application.default();
+ const config = app.getConfig();
+ priv.config = config;
+ break :config config.get();
+ };
+
+ // We initialize our windowing protocol to none because we can't
+ // actually initialize this until we get realized.
+ priv.winproto = .none;
+
+ // Add our dev CSS class if we're in debug mode.
+ if (comptime build_config.is_debug) {
+ self.as(gtk.Widget).addCssClass("devel");
+ }
+
+ // Setup our tab binding group. This ensures certain properties
+ // are only synced from the currently active tab.
+ priv.tab_bindings = gobject.BindingGroup.new();
+ priv.tab_bindings.bind("title", self.as(gobject.Object), "title", .{});
+
+ // Set our window icon. We can't set this in the blueprint file
+ // because its dependent on the build config.
+ self.as(gtk.Window).setIconName(build_config.bundle_id);
+
+ // Initialize our actions
+ self.initActionMap();
+
+ // Start states based on config.
+ if (config.maximize) self.as(gtk.Window).maximize();
+ if (config.fullscreen != .false) self.as(gtk.Window).fullscreen();
+
+ // If we have an explicit title set, we set that immediately
+ // so that any applications inspecting the window states see
+ // an immediate title set when the window appears, rather than
+ // waiting possibly a few event loop ticks for it to sync from
+ // the surface.
+ if (config.title) |title| {
+ self.as(gtk.Window).setTitle(title);
+ }
+
+ // We always sync our appearance at the end because loading our
+ // config and such can affect our bindings which are setup initially
+ // in initTemplate.
+ self.syncAppearance();
+
+ // We need to do this so that the title initializes properly,
+ // I think because its a dynamic getter.
+ self.as(gobject.Object).notifyByPspec(properties.@"active-surface".impl.param_spec);
+ }
+
+ /// Setup our action map.
+ fn initActionMap(self: *Self) void {
+ const s_variant_type = glib.ext.VariantType.newFor([:0]const u8);
+ defer s_variant_type.free();
+
+ const actions = [_]ext.actions.Action(Self){
+ .init("about", actionAbout, null),
+ .init("close", actionClose, null),
+ .init("close-tab", actionCloseTab, s_variant_type),
+ .init("new-tab", actionNewTab, null),
+ .init("new-window", actionNewWindow, null),
+ .init("prompt-surface-title", actionPromptSurfaceTitle, null),
+ .init("prompt-tab-title", actionPromptTabTitle, null),
+ .init("prompt-context-tab-title", actionPromptContextTabTitle, null),
+ .init("ring-bell", actionRingBell, null),
+ .init("split-right", actionSplitRight, null),
+ .init("split-left", actionSplitLeft, null),
+ .init("split-up", actionSplitUp, null),
+ .init("split-down", actionSplitDown, null),
+ .init("copy", actionCopy, null),
+ .init("paste", actionPaste, null),
+ .init("reset", actionReset, null),
+ .init("clear", actionClear, null),
+ // TODO: accept the surface that toggled the command palette
+ .init("toggle-command-palette", actionToggleCommandPalette, null),
+ .init("toggle-inspector", actionToggleInspector, null),
+ };
+
+ ext.actions.add(Self, self, &actions);
+ }
+
+ /// Winproto backend for this window.
+ pub fn winproto(self: *Self) *winprotopkg.Window {
+ return &self.private().winproto;
+ }
+
+ /// Create a new tab with the given parent. The tab will be inserted
+ /// at the position dictated by the `window-new-tab-position` config.
+ /// The new tab will be selected.
+ pub fn newTab(self: *Self, parent_: ?*CoreSurface) void {
+ _ = self.newTabPage(parent_, .tab, .none);
+ }
+
+ pub fn newTabForWindow(
+ self: *Self,
+ parent_: ?*CoreSurface,
+ overrides: struct {
+ command: ?configpkg.Command = null,
+ working_directory: ?[:0]const u8 = null,
+ title: ?[:0]const u8 = null,
+
+ pub const none: @This() = .{};
+ },
+ ) void {
+ _ = self.newTabPage(
+ parent_,
+ .window,
+ .{
+ .command = overrides.command,
+ .working_directory = overrides.working_directory,
+ .title = overrides.title,
+ },
+ );
+ }
+
+ fn newTabPage(
+ self: *Self,
+ parent_: ?*CoreSurface,
+ context: apprt.surface.NewSurfaceContext,
+ overrides: struct {
+ command: ?configpkg.Command = null,
+ working_directory: ?[:0]const u8 = null,
+ title: ?[:0]const u8 = null,
+
+ pub const none: @This() = .{};
+ },
+ ) *adw.TabPage {
+ const priv: *Private = self.private();
+ const tab_view = priv.tab_view;
+
+ // Create our new tab object
+ const tab = Tab.new(
+ priv.config,
+ .{
+ .command = overrides.command,
+ .working_directory = overrides.working_directory,
+ .title = overrides.title,
+ },
+ );
+
+ if (parent_) |p| {
+ // For a new window's first tab, inherit the parent's initial size hints.
+ if (context == .window) {
+ surfaceInit(p.rt_surface.gobj(), self);
+ }
+ tab.setParentWithContext(p, context);
+ }
+
+ // Get the position that we should insert the new tab at.
+ const config = if (priv.config) |v| v.get() else {
+ // If we don't have a config we just append it at the end.
+ // This should never happen.
+ return tab_view.append(tab.as(gtk.Widget));
+ };
+ const position = switch (config.@"window-new-tab-position") {
+ .current => current: {
+ const selected = tab_view.getSelectedPage() orelse
+ break :current tab_view.getNPages();
+ const current = tab_view.getPagePosition(selected);
+ break :current current + 1;
+ },
+
+ .end => tab_view.getNPages(),
+ };
+
+ // Add the page and select it
+ const page = tab_view.insert(tab.as(gtk.Widget), position);
+ tab_view.setSelectedPage(page);
+
+ // Create some property bindings
+ _ = tab.as(gobject.Object).bindProperty(
+ "title",
+ page.as(gobject.Object),
+ "title",
+ .{ .sync_create = true },
+ );
+ _ = tab.as(gobject.Object).bindProperty(
+ "tooltip",
+ page.as(gobject.Object),
+ "tooltip",
+ .{ .sync_create = true },
+ );
+
+ // Bind signals
+ const split_tree = tab.getSplitTree();
+ _ = SplitTree.signals.changed.connect(
+ split_tree,
+ *Self,
+ tabSplitTreeChanged,
+ self,
+ .{},
+ );
+
+ // Run an initial notification for the surface tree so we can setup
+ // initial state.
+ tabSplitTreeChanged(
+ split_tree,
+ null,
+ split_tree.getTree(),
+ self,
+ );
+
+ return page;
+ }
+
+ pub const SelectTab = union(enum) {
+ previous,
+ next,
+ last,
+ n: usize,
+ };
+
+ /// Select the tab as requested. Returns true if the tab selection
+ /// changed.
+ pub fn selectTab(self: *Self, n: SelectTab) bool {
+ const priv = self.private();
+ const tab_view = priv.tab_view;
+
+ // Get our current tab numeric position
+ const selected = tab_view.getSelectedPage() orelse return false;
+ const current = tab_view.getPagePosition(selected);
+
+ // Get our total
+ const total = tab_view.getNPages();
+
+ const goto: c_int = switch (n) {
+ .previous => if (current > 0)
+ current - 1
+ else
+ total - 1,
+
+ .next => if (current < total - 1)
+ current + 1
+ else
+ 0,
+
+ .last => total - 1,
+
+ .n => |v| n: {
+ // 1-indexed
+ if (v == 0) return false;
+
+ const n_int = std.math.cast(
+ c_int,
+ v,
+ ) orelse return false;
+ break :n @min(n_int - 1, total - 1);
+ },
+ };
+ assert(goto >= 0);
+ assert(goto < total);
+
+ // If our target is the same as our current then we do nothing.
+ if (goto == current) return false;
+
+ // Add the page and select it
+ const page = tab_view.getNthPage(goto);
+ tab_view.setSelectedPage(page);
+
+ return true;
+ }
+
+ /// Move the tab containing the given surface by the given amount.
+ /// Returns if this affected any tab positioning.
+ pub fn moveTab(
+ self: *Self,
+ surface: *Surface,
+ amount: isize,
+ ) bool {
+ const priv = self.private();
+ const tab_view = priv.tab_view;
+
+ // If we have one tab we never move.
+ const total = tab_view.getNPages();
+ if (total == 1) return false;
+
+ // Get the tab that contains the given surface.
+ const tab = ext.getAncestor(
+ Tab,
+ surface.as(gtk.Widget),
+ ) orelse return false;
+
+ // Get the page position that contains the tab.
+ const page = tab_view.getPage(tab.as(gtk.Widget));
+ const pos = tab_view.getPagePosition(page);
+
+ // Move it
+ const desired_pos: c_int = desired: {
+ const initial: c_int = @intCast(pos + amount);
+ const max = total - 1;
+ break :desired if (initial < 0)
+ max + initial + 1
+ else if (initial > max)
+ initial - max - 1
+ else
+ initial;
+ };
+ assert(desired_pos >= 0);
+ assert(desired_pos < total);
+
+ return tab_view.reorderPage(page, desired_pos) != 0;
+ }
+
+ pub fn toggleTabOverview(self: *Self) void {
+ const priv = self.private();
+ const tab_overview = priv.tab_overview;
+ const is_open = tab_overview.getOpen() != 0;
+ tab_overview.setOpen(@intFromBool(!is_open));
+ }
+
+ /// Toggle the visible property.
+ pub fn toggleVisibility(self: *Self) void {
+ const widget = self.as(gtk.Widget);
+ widget.setVisible(@intFromBool(widget.isVisible() == 0));
+ }
+
+ /// Updates various appearance properties. This should always be safe
+ /// to call multiple times. This should be called whenever a change
+ /// happens that might affect how the window appears (config change,
+ /// fullscreen, etc.).
+ fn syncAppearance(self: *Self) void {
+ const priv = self.private();
+ const widget = self.as(gtk.Widget);
+
+ // Toggle style classes based on whether we're using CSDs or SSDs.
+ //
+ // These classes are defined in the gtk.Window documentation:
+ // https://docs.gtk.org/gtk4/class.Window.html#css-nodes.
+ {
+ // Reset all style classes first
+ inline for (&.{
+ "ssd",
+ "csd",
+ "solid-csd",
+ "no-border-radius",
+ }) |class|
+ widget.removeCssClass(class);
+
+ const csd_enabled = priv.winproto.clientSideDecorationEnabled();
+ self.as(gtk.Window).setDecorated(@intFromBool(csd_enabled));
+
+ if (csd_enabled) {
+ const display = widget.getDisplay();
+
+ // We do the exact same check GTK is doing internally and toggle
+ // either the `csd` or `solid-csd` style, based on whether the user's
+ // window manager is deemed _non-compositing_.
+ //
+ // In practice this only impacts users of traditional X11 window
+ // managers (e.g. i3, dwm, awesomewm, etc.) and not X11 desktop
+ // environments or Wayland compositors/DEs.
+ if (display.isRgba() != 0 and display.isComposited() != 0) {
+ widget.addCssClass("csd");
+ } else {
+ widget.addCssClass("solid-csd");
+ }
+ } else {
+ widget.addCssClass("ssd");
+ // Fix any artifacting that may occur in window corners.
+ widget.addCssClass("no-border-radius");
+ }
+ }
+
+ // Trigger all our dynamic properties that depend on the config.
+ inline for (&.{
+ "headerbar-visible",
+ "tabs-autohide",
+ "tabs-visible",
+ "tabs-wide",
+ "toolbar-style",
+ "titlebar-style",
+ }) |key| {
+ self.as(gobject.Object).notifyByPspec(
+ @field(properties, key).impl.param_spec,
+ );
+ }
+
+ // Remainder uses the config
+ const config = if (priv.config) |v| v.get() else return;
+
+ // Only add a solid background if we're opaque.
+ self.toggleCssClass(
+ "background",
+ config.@"background-opacity" >= 1,
+ );
+
+ // Apply class to color headerbar if window-theme is set to `ghostty` and
+ // GTK version is before 4.16. The conditional is because above 4.16
+ // we use GTK CSS color variables.
+ self.toggleCssClass(
+ "window-theme-ghostty",
+ !gtk_version.atLeast(4, 16, 0) and
+ config.@"window-theme" == .ghostty,
+ );
+
+ // Move the tab bar to the proper location.
+ priv.toolbar.remove(priv.tab_bar.as(gtk.Widget));
+ switch (config.@"gtk-tabs-location") {
+ .top => priv.toolbar.addTopBar(priv.tab_bar.as(gtk.Widget)),
+ .bottom => priv.toolbar.addBottomBar(priv.tab_bar.as(gtk.Widget)),
+ }
+
+ // Do our window-protocol specific appearance sync.
+ priv.winproto.syncAppearance() catch |err| {
+ log.warn("failed to sync winproto appearance error={}", .{err});
+ };
+ }
+
+ /// Sync the state of any actions on this window.
+ fn syncActions(self: *Self) void {
+ const has_selection = selection: {
+ const surface = self.getActiveSurface() orelse
+ break :selection false;
+ const core_surface = surface.core() orelse
+ break :selection false;
+ break :selection core_surface.hasSelection();
+ };
+
+ const action_map: *gio.ActionMap = gobject.ext.cast(
+ gio.ActionMap,
+ self,
+ ) orelse return;
+ const action: *gio.SimpleAction = gobject.ext.cast(
+ gio.SimpleAction,
+ action_map.lookupAction("copy") orelse return,
+ ) orelse return;
+ action.setEnabled(@intFromBool(has_selection));
+ }
+
+ fn toggleCssClass(self: *Self, class: [:0]const u8, value: bool) void {
+ const widget = self.as(gtk.Widget);
+ if (value)
+ widget.addCssClass(class.ptr)
+ else
+ widget.removeCssClass(class.ptr);
+ }
+
+ /// Perform a binding action on the window's active surface.
+ fn performBindingAction(
+ self: *Self,
+ action: input.Binding.Action,
+ ) void {
+ const surface = self.getActiveSurface() orelse return;
+ const core_surface = surface.core() orelse return;
+ _ = core_surface.performBindingAction(action) catch |err| {
+ log.warn("error performing binding action error={}", .{err});
+ return;
+ };
+ }
+
+ /// Queue a simple text-based toast. All text-based toasts share the
+ /// same timeout for consistency.
+ ///
+ // This is not `pub` because we should be using signals emitted by
+ // other widgets to trigger our toasts. Other objects should not
+ // trigger toasts directly.
+ fn addToast(self: *Self, title: [*:0]const u8) void {
+ const toast = adw.Toast.new(title);
+ toast.setTimeout(3);
+ self.private().toast_overlay.addToast(toast);
+ }
+
+ fn connectSurfaceHandlers(
+ self: *Self,
+ tree: *const Surface.Tree,
+ ) void {
+ const priv = self.private();
+ var it = tree.iterator();
+ while (it.next()) |entry| {
+ const surface = entry.view;
+ // Before adding any new signal handlers, disconnect any that we may
+ // have added before. Otherwise we may get multiple handlers for the
+ // same signal.
+ _ = gobject.signalHandlersDisconnectMatched(
+ surface.as(gobject.Object),
+ .{ .data = true },
+ 0,
+ 0,
+ null,
+ null,
+ self,
+ );
+
+ _ = Surface.signals.@"present-request".connect(
+ surface,
+ *Self,
+ surfacePresentRequest,
+ self,
+ .{},
+ );
+ _ = Surface.signals.@"clipboard-write".connect(
+ surface,
+ *Self,
+ surfaceClipboardWrite,
+ self,
+ .{},
+ );
+ _ = Surface.signals.menu.connect(
+ surface,
+ *Self,
+ surfaceMenu,
+ self,
+ .{},
+ );
+ _ = Surface.signals.@"toggle-fullscreen".connect(
+ surface,
+ *Self,
+ surfaceToggleFullscreen,
+ self,
+ .{},
+ );
+ _ = Surface.signals.@"toggle-maximize".connect(
+ surface,
+ *Self,
+ surfaceToggleMaximize,
+ self,
+ .{},
+ );
+
+ // If we've never had a surface initialize yet, then we register
+ // this signal. Its theoretically possible to launch multiple surfaces
+ // before init so we could register this on multiple and that is not
+ // a problem because we'll check the flag again in each handler.
+ if (!priv.surface_init) {
+ _ = Surface.signals.init.connect(
+ surface,
+ *Self,
+ surfaceInit,
+ self,
+ .{},
+ );
+ }
+ }
+ }
+
+ /// Disconnect all the surface handlers for the given tree. This should
+ /// be called whenever a tree is no longer present in the window, e.g.
+ /// when a tab is detached or the tree changes.
+ fn disconnectSurfaceHandlers(
+ self: *Self,
+ tree: *const Surface.Tree,
+ ) void {
+ var it = tree.iterator();
+ while (it.next()) |entry| {
+ const surface = entry.view;
+ _ = gobject.signalHandlersDisconnectMatched(
+ surface.as(gobject.Object),
+ .{ .data = true },
+ 0,
+ 0,
+ null,
+ null,
+ self,
+ );
+ }
+ }
+
+ //---------------------------------------------------------------
+ // Properties
+
+ /// Whether this terminal is a quick terminal or not.
+ pub fn isQuickTerminal(self: *Self) bool {
+ return self.private().quick_terminal;
+ }
+
+ /// Get the currently active surface. See the "active-surface" property.
+ /// This does not ref the value.
+ pub fn getActiveSurface(self: *Self) ?*Surface {
+ const tab = self.getSelectedTab() orelse return null;
+ return tab.getActiveSurface();
+ }
+
+ /// Returns the configuration for this window. The reference count
+ /// is not increased.
+ pub fn getConfig(self: *Self) ?*Config {
+ return self.private().config;
+ }
+
+ /// Get the tab view for this window.
+ pub fn getTabView(self: *Self) *adw.TabView {
+ return self.private().tab_view;
+ }
+
+ /// Get the current window decoration value for this window.
+ pub fn getWindowDecoration(self: *Self) configpkg.WindowDecoration {
+ const priv = self.private();
+ if (priv.window_decoration) |v| return v;
+ if (priv.config) |v| return v.get().@"window-decoration";
+ return .auto;
+ }
+
+ /// Toggle the window decorations for this window.
+ pub fn toggleWindowDecorations(self: *Self) void {
+ const priv = self.private();
+
+ if (priv.window_decoration) |_| {
+ // Unset any previously set window decoration settings
+ self.setWindowDecoration(null);
+ return;
+ }
+
+ const config = if (priv.config) |v| v.get() else return;
+ self.setWindowDecoration(switch (config.@"window-decoration") {
+ // Use auto when the decoration is initially none
+ .none => .auto,
+
+ // Anything non-none to none
+ .auto, .client, .server => .none,
+ });
+ }
+
+ /// Set the window decoration override for this window. If this is null,
+ /// then we'll revert back to the configuration's default.
+ fn setWindowDecoration(
+ self: *Self,
+ new_: ?configpkg.WindowDecoration,
+ ) void {
+ const priv = self.private();
+ priv.window_decoration = new_;
+ self.syncAppearance();
+ }
+
+ /// Get the currently selected tab as a Tab object.
+ fn getSelectedTab(self: *Self) ?*Tab {
+ const priv = self.private();
+ const page = priv.tab_view.getSelectedPage() orelse return null;
+ const child = page.getChild();
+ assert(gobject.ext.isA(child, Tab));
+ return gobject.ext.cast(Tab, child);
+ }
+
+ /// Returns true if this window needs confirmation before quitting.
+ fn getNeedsConfirmQuit(self: *Self) bool {
+ const priv = self.private();
+ const n = priv.tab_view.getNPages();
+ assert(n >= 0);
+
+ for (0..@intCast(n)) |i| {
+ const page = priv.tab_view.getNthPage(@intCast(i));
+ const child = page.getChild();
+ const tab = gobject.ext.cast(Tab, child) orelse {
+ log.warn("unexpected non-Tab child in tab view", .{});
+ continue;
+ };
+ if (tab.getNeedsConfirmQuit()) return true;
+ }
+
+ return false;
+ }
+
+ fn isFullscreen(self: *Window) bool {
+ return self.as(gtk.Window).isFullscreen() != 0;
+ }
+
+ fn isMaximized(self: *Window) bool {
+ return self.as(gtk.Window).isMaximized() != 0;
+ }
+
+ fn getHeaderbarVisible(self: *Self) bool {
+ const priv = self.private();
+
+ // Never display the header bar when CSDs are disabled.
+ const csd_enabled = priv.winproto.clientSideDecorationEnabled();
+ if (!csd_enabled) return false;
+
+ // Never display the header bar as a quick terminal.
+ if (priv.quick_terminal) return false;
+
+ // If we're fullscreen we never show the header bar.
+ if (self.isFullscreen()) return false;
+
+ // The remainder needs a config
+ const config_obj = self.private().config orelse return true;
+ const config = config_obj.get();
+
+ // *Conditionally* disable the header bar when maximized, and
+ // gtk-titlebar-hide-when-maximized is set
+ if (self.isMaximized() and config.@"gtk-titlebar-hide-when-maximized") {
+ return false;
+ }
+
+ return switch (config.@"gtk-titlebar-style") {
+ // If the titlebar style is tabs never show the titlebar.
+ .tabs => false,
+
+ // If the titlebar style is native show the titlebar if configured
+ // to do so.
+ .native => config.@"gtk-titlebar",
+ };
+ }
+
+ fn getTabsAutohide(self: *Self) bool {
+ const priv = self.private();
+ const config = if (priv.config) |v| v.get() else return true;
+
+ return switch (config.@"gtk-titlebar-style") {
+ // If the titlebar style is tabs we cannot autohide.
+ .tabs => false,
+
+ .native => switch (config.@"window-show-tab-bar") {
+ // Auto we always autohide... obviously.
+ .auto => true,
+
+ // Always we never autohide because we always show the tab bar.
+ .always => false,
+
+ // Never we autohide because it doesn't actually matter,
+ // since getTabsVisible will return false.
+ .never => true,
+ },
+ };
+ }
+
+ fn getTabsVisible(self: *Self) bool {
+ const priv = self.private();
+ const config = if (priv.config) |v| v.get() else return true;
+
+ switch (config.@"gtk-titlebar-style") {
+ .tabs => {
+ // *Conditionally* disable the tab bar when maximized, the titlebar
+ // style is tabs, and gtk-titlebar-hide-when-maximized is set.
+ if (self.isMaximized() and config.@"gtk-titlebar-hide-when-maximized") return false;
+
+ // If the titlebar style is tabs the tab bar must always be visible.
+ return true;
+ },
+ .native => {
+ return switch (config.@"window-show-tab-bar") {
+ .always, .auto => true,
+ .never => false,
+ };
+ },
+ }
+ }
+
+ fn getTabsWide(self: *Self) bool {
+ const priv = self.private();
+ const config = if (priv.config) |v| v.get() else return true;
+ return config.@"gtk-wide-tabs";
+ }
+
+ fn getToolbarStyle(self: *Self) adw.ToolbarStyle {
+ const priv = self.private();
+ const config = if (priv.config) |v| v.get() else return .raised;
+ return switch (config.@"gtk-toolbar-style") {
+ .flat => .flat,
+ .raised => .raised,
+ .@"raised-border" => .raised_border,
+ };
+ }
+
+ fn getTitlebarStyle(self: *Self) TitlebarStyle {
+ const priv = self.private();
+ const config = if (priv.config) |v| v.get() else return .native;
+ return config.@"gtk-titlebar-style";
+ }
+
+ fn propConfig(
+ _: *adw.ApplicationWindow,
+ _: *gobject.ParamSpec,
+ self: *Self,
+ ) callconv(.c) void {
+ const priv = self.private();
+ if (priv.config) |config_obj| {
+ const config = config_obj.get();
+ if (config.@"app-notifications".@"config-reload") {
+ self.addToast(i18n._("Reloaded the configuration"));
+ }
+ }
+
+ self.syncAppearance();
+ }
+
+ fn propIsActive(
+ _: *gtk.Window,
+ _: *gobject.ParamSpec,
+ self: *Self,
+ ) callconv(.c) void {
+ // Hide quick-terminal if set to autohide
+ if (self.isQuickTerminal()) {
+ if (self.getConfig()) |cfg| {
+ if (cfg.get().@"quick-terminal-autohide" and self.as(gtk.Window).isActive() == 0) {
+ self.toggleVisibility();
+ }
+ }
+ }
+
+ // Don't change urgency if we're not the active window.
+ if (self.as(gtk.Window).isActive() == 0) return;
+
+ self.winproto().setUrgent(false) catch |err| {
+ log.warn(
+ "winproto failed to reset urgency={}",
+ .{err},
+ );
+ };
+ }
+
+ fn propGdkSurfaceDims(
+ _: *gdk.Surface,
+ _: *gobject.ParamSpec,
+ self: *Self,
+ ) callconv(.c) void {
+ // X11 needs to fix blurring on resize, but winproto implementations
+ // could do anything.
+ self.private().winproto.resizeEvent() catch |err| {
+ log.warn(
+ "winproto resize event failed error={}",
+ .{err},
+ );
+ };
+ }
+
+ fn propFullscreened(
+ _: *adw.ApplicationWindow,
+ _: *gobject.ParamSpec,
+ self: *Self,
+ ) callconv(.c) void {
+ self.syncAppearance();
+ }
+
+ fn propMaximized(
+ _: *adw.ApplicationWindow,
+ _: *gobject.ParamSpec,
+ self: *Self,
+ ) callconv(.c) void {
+ self.syncAppearance();
+ }
+
+ fn propMenuActive(
+ button: *gtk.MenuButton,
+ _: *gobject.ParamSpec,
+ self: *Self,
+ ) callconv(.c) void {
+ // Debian 12 is stuck on GTK 4.8
+ if (!gtk_version.atLeast(4, 10, 0)) return;
+
+ // We only care if we're activating. If we're activating then
+ // we need to check the validity of our menu items.
+ const active = button.getActive() != 0;
+ if (!active) return;
+
+ self.syncActions();
+ }
+
+ fn propQuickTerminal(
+ _: *adw.ApplicationWindow,
+ _: *gobject.ParamSpec,
+ self: *Self,
+ ) callconv(.c) void {
+ const priv = self.private();
+ if (priv.surface_init) {
+ log.warn("quick terminal property can't be changed after surfaces have been initialized", .{});
+ return;
+ }
+
+ if (priv.quick_terminal) {
+ // Initialize the quick terminal at the app-layer
+ Application.default().winproto().initQuickTerminal(self) catch |err| {
+ log.warn("failed to initialize quick terminal error={}", .{err});
+ return;
+ };
+ }
+ }
+
+ fn propScaleFactor(
+ _: *adw.ApplicationWindow,
+ _: *gobject.ParamSpec,
+ self: *Self,
+ ) callconv(.c) void {
+ // On some platforms (namely X11) we need to refresh our appearance when
+ // the scale factor changes. In theory this could be more fine-grained as
+ // a full refresh could be expensive, but a) this *should* be rare, and
+ // b) quite noticeable visual bugs would occur if this is not present.
+ self.private().winproto.syncAppearance() catch |err| {
+ log.warn(
+ "failed to sync appearance after scale factor has been updated={}",
+ .{err},
+ );
+ return;
+ };
+ }
+
+ fn closureTitlebarStyleIsTab(
+ _: *Self,
+ value: TitlebarStyle,
+ ) callconv(.c) c_int {
+ return @intFromBool(switch (value) {
+ .native => false,
+ .tabs => true,
+ });
+ }
+
+ fn closureSubtitle(
+ _: *Self,
+ config_: ?*Config,
+ pwd_: ?[*:0]const u8,
+ ) callconv(.c) ?[*:0]const u8 {
+ const config = if (config_) |v| v.get() else return null;
+ return switch (config.@"window-subtitle") {
+ .false => null,
+ .@"working-directory" => pwd: {
+ const pwd = pwd_ orelse return null;
+ break :pwd glib.ext.dupeZ(u8, std.mem.span(pwd));
+ },
+ };
+ }
+
+ //---------------------------------------------------------------
+ // Virtual methods
+
+ fn dispose(self: *Self) callconv(.c) void {
+ const priv = self.private();
+
+ priv.command_palette.set(null);
+
+ if (priv.config) |v| {
+ v.unref();
+ priv.config = null;
+ }
+
+ priv.tab_bindings.setSource(null);
+
+ gtk.Widget.disposeTemplate(
+ self.as(gtk.Widget),
+ getGObjectType(),
+ );
+
+ gobject.Object.virtual_methods.dispose.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ fn finalize(self: *Self) callconv(.c) void {
+ const priv = self.private();
+ priv.tab_bindings.unref();
+ priv.winproto.deinit();
+
+ gobject.Object.virtual_methods.finalize.call(
+ Class.parent,
+ self.as(Parent),
+ );
+ }
+
+ //---------------------------------------------------------------
+ // Signal handlers
+
+ fn windowRealize(_: *gtk.Widget, self: *Window) callconv(.c) void {
+ const app = Application.default();
+
+ // Initialize our window protocol logic
+ if (winprotopkg.Window.init(
+ app.allocator(),
+ app.winproto(),
+ self,
+ )) |wp| {
+ self.private().winproto = wp;
+ } else |err| {
+ log.warn("failed to initialize window protocol error={}", .{err});
+ return;
+ }
+
+ // We need to setup resize notifications on our surface,
+ // which is only available after the window had been realized.
+ if (self.as(gtk.Native).getSurface()) |gdk_surface| {
+ _ = gobject.Object.signals.notify.connect(
+ gdk_surface,
+ *Self,
+ propGdkSurfaceDims,
+ self,
+ .{ .detail = "width" },
+ );
+ _ = gobject.Object.signals.notify.connect(
+ gdk_surface,
+ *Self,
+ propGdkSurfaceDims,
+ self,
+ .{ .detail = "height" },
+ );
+ }
+
+ // When we are realized we always setup our appearance since this
+ // calls some winproto functions.
+ self.syncAppearance();
+ }
+
+ fn btnNewTab(_: *adw.SplitButton, self: *Self) callconv(.c) void {
+ self.performBindingAction(.new_tab);
+ }
+
+ fn tabOverviewCreateTab(
+ _: *adw.TabOverview,
+ self: *Self,
+ ) callconv(.c) *adw.TabPage {
+ return self.newTabPage(if (self.getActiveSurface()) |v| v.core() else null, .tab, .none);
+ }
+
+ fn tabOverviewOpen(
+ tab_overview: *adw.TabOverview,
+ _: *gobject.ParamSpec,
+ self: *Self,
+ ) callconv(.c) void {
+ // We only care about when the tab overview is closed.
+ if (tab_overview.getOpen() != 0) return;
+
+ // On tab overview close, focus is sometimes lost. This is an
+ // upstream issue in libadwaita[1]. When this is resolved we
+ // can put a runtime version check here to avoid this workaround.
+ //
+ // Our workaround is to start a timer after 500ms to refocus
+ // the currently selected tab. We choose 500ms because the adw
+ // animation is 400ms.
+ //
+ // [1]: https://gitlab.gnome.org/GNOME/libadwaita/-/issues/670
+
+ // If we have an old timer remove it
+ const priv = self.private();
+ if (priv.tab_overview_focus_timer) |timer| {
+ _ = glib.Source.remove(timer);
+ }
+
+ // Restart our timer
+ priv.tab_overview_focus_timer = glib.timeoutAdd(
+ 500,
+ tabOverviewFocusTimer,
+ self,
+ );
+ }
+
+ fn tabOverviewFocusTimer(
+ ud: ?*anyopaque,
+ ) callconv(.c) c_int {
+ const self: *Self = @ptrCast(@alignCast(ud orelse return 0));
+
+ // Always note our timer is removed
+ self.private().tab_overview_focus_timer = null;
+
+ // Get our currently active surface which should respect the newly
+ // selected tab. Grab focus.
+ const surface = self.getActiveSurface() orelse return 0;
+ surface.grabFocus();
+
+ // Remove the timer
+ return 0;
+ }
+
+ fn windowCloseRequest(
+ _: *gtk.Window,
+ self: *Self,
+ ) callconv(.c) c_int {
+ if (self.getNeedsConfirmQuit()) {
+ // Show a confirmation dialog
+ const dialog: *CloseConfirmationDialog = .new(.window);
+ _ = CloseConfirmationDialog.signals.@"close-request".connect(
+ dialog,
+ *Self,
+ closeConfirmationClose,
+ self,
+ .{},
+ );
+
+ // Show it
+ dialog.present(self.as(gtk.Widget));
+ return @intFromBool(true);
+ }
+
+ self.as(gtk.Window).destroy();
+ return @intFromBool(false);
+ }
+
+ fn closeConfirmationClose(
+ _: *CloseConfirmationDialog,
+ self: *Self,
+ ) callconv(.c) void {
+ self.as(gtk.Window).destroy();
+ }
+
+ fn closeConfirmationCloseTab(
+ _: *CloseConfirmationDialog,
+ page: *adw.TabPage,
+ ) callconv(.c) void {
+ const tab_view = ext.getAncestor(
+ adw.TabView,
+ page.getChild().as(gtk.Widget),
+ ) orelse {
+ log.warn("close confirmation called for non-existent page", .{});
+ return;
+ };
+ tab_view.closePageFinish(page, @intFromBool(true));
+ }
+
+ fn closeConfirmationCancelTab(
+ _: *CloseConfirmationDialog,
+ page: *adw.TabPage,
+ ) callconv(.c) void {
+ const tab_view = ext.getAncestor(
+ adw.TabView,
+ page.getChild().as(gtk.Widget),
+ ) orelse {
+ log.warn("close confirmation called for non-existent page", .{});
+ return;
+ };
+ tab_view.closePageFinish(page, @intFromBool(false));
+ }
+
+ fn tabViewClosePage(
+ _: *adw.TabView,
+ page: *adw.TabPage,
+ self: *Self,
+ ) callconv(.c) c_int {
+ const priv = self.private();
+ const child = page.getChild();
+ const tab = gobject.ext.cast(Tab, child) orelse
+ return @intFromBool(false);
+
+ // If the tab says it doesn't need confirmation then we go ahead
+ // and close immediately.
+ if (!tab.getNeedsConfirmQuit()) {
+ priv.tab_view.closePageFinish(page, @intFromBool(true));
+ return @intFromBool(true);
+ }
+
+ // Show a confirmation dialog
+ const dialog: *CloseConfirmationDialog = .new(.tab);
+ _ = CloseConfirmationDialog.signals.@"close-request".connect(
+ dialog,
+ *adw.TabPage,
+ closeConfirmationCloseTab,
+ page,
+ .{},
+ );
+ _ = CloseConfirmationDialog.signals.cancel.connect(
+ dialog,
+ *adw.TabPage,
+ closeConfirmationCancelTab,
+ page,
+ .{},
+ );
+
+ // Show it
+ dialog.present(child);
+ return @intFromBool(true);
+ }
+
+ fn tabViewSelectedPage(
+ _: *adw.TabView,
+ _: *gobject.ParamSpec,
+ self: *Self,
+ ) callconv(.c) void {
+ const priv = self.private();
+
+ // Always reset our binding source in case we have no pages.
+ priv.tab_bindings.setSource(null);
+
+ // Get our current page which MUST be a Tab object.
+ const page = priv.tab_view.getSelectedPage() orelse return;
+ const child = page.getChild();
+ assert(gobject.ext.isA(child, Tab));
+
+ // Setup our binding group. This ensures things like the title
+ // are synced from the active tab.
+ priv.tab_bindings.setSource(child.as(gobject.Object));
+
+ // If the tab was previously marked as needing attention
+ // (e.g. due to a bell character), we now unmark that
+ page.setNeedsAttention(@intFromBool(false));
+ }
+
+ fn tabViewPageAttached(
+ _: *adw.TabView,
+ page: *adw.TabPage,
+ _: c_int,
+ self: *Self,
+ ) callconv(.c) void {
+ // Get the attached page which must be a Tab object.
+ const child = page.getChild();
+ const tab = gobject.ext.cast(Tab, child) orelse return;
+
+ // Attach listeners for the tab.
+ _ = Tab.signals.@"close-request".connect(
+ tab,
+ *Self,
+ tabCloseRequest,
+ self,
+ .{},
+ );
+
+ // Attach listeners for the surface.
+ //
+ // Interesting behavior here that was previously undocumented but
+ // I'm going to make it explicit here: we accept all the signals here
+ // (like toggle-fullscreen) regardless of whether the surface or tab
+ // is focused. At the time of writing this we have no API that could
+ // really trigger these that way but its theoretically possible.
+ //
+ // What is DEFINITELY possible is something like OSC52 triggering
+ // a clipboard-write signal on an unfocused tab/surface. We definitely
+ // want to show the user a notification about that but our notification
+ // right now is a toast that doesn't make it clear WHO used the
+ // clipboard. We probably want to change that in the future.
+ //
+ // I'm not sure how desirable all the above is, and we probably
+ // should be thoughtful about future signals here. But all of this
+ // behavior is consistent with macOS and the previous GTK apprt,
+ // but that behavior was all implicit and not documented, so here
+ // I am.
+ if (tab.getSurfaceTree()) |tree| {
+ self.connectSurfaceHandlers(tree);
+ }
+ }
+
+ fn tabViewPageDetached(
+ _: *adw.TabView,
+ page: *adw.TabPage,
+ _: c_int,
+ self: *Self,
+ ) callconv(.c) void {
+ // We need to get the tab to disconnect the signals.
+ const child = page.getChild();
+ const tab = gobject.ext.cast(Tab, child) orelse return;
+ _ = gobject.signalHandlersDisconnectMatched(
+ tab.as(gobject.Object),
+ .{ .data = true },
+ 0,
+ 0,
+ null,
+ null,
+ self,
+ );
+
+ // Remove the tree handlers
+ if (tab.getSurfaceTree()) |tree| {
+ self.disconnectSurfaceHandlers(tree);
+ }
+ }
+
+ fn tabViewCreateWindow(
+ _: *adw.TabView,
+ _: *Self,
+ ) callconv(.c) *adw.TabView {
+ // Create a new window without creating a new tab.
+ const win = gobject.ext.newInstance(
+ Self,
+ .{
+ .application = Application.default(),
+ },
+ );
+
+ // We have to show it otherwise it'll just be hidden.
+ gtk.Window.present(win.as(gtk.Window));
+
+ // Get our tab view
+ return win.private().tab_view;
+ }
+
+ fn tabCloseRequest(
+ tab: *Tab,
+ self: *Self,
+ ) callconv(.c) void {
+ const priv = self.private();
+ const page = priv.tab_view.getPage(tab.as(gtk.Widget));
+ // TODO: connect close page handler to tab to check for confirmation
+ priv.tab_view.closePage(page);
+ }
+
+ fn tabViewNPages(
+ _: *adw.TabView,
+ _: *gobject.ParamSpec,
+ self: *Self,
+ ) callconv(.c) void {
+ const priv = self.private();
+ if (priv.tab_view.getNPages() == 0) {
+ // If we have no pages left then we want to close window.
+
+ // If the tab overview is open, then we don't close the window
+ // because its a rather abrupt experience. This also fixes an
+ // issue where dragging out the last tab in the tab overview
+ // won't cause Ghostty to exit.
+ if (priv.tab_overview.getOpen() != 0) return;
+
+ self.as(gtk.Window).close();
+ }
+ }
+ fn setupTabMenu(
+ _: *adw.TabView,
+ page: ?*adw.TabPage,
+ self: *Self,
+ ) callconv(.c) void {
+ self.private().context_menu_page = page;
+ }
+
+ fn surfaceClipboardWrite(
+ _: *Surface,
+ clipboard_type: apprt.Clipboard,
+ text: [*:0]const u8,
+ self: *Self,
+ ) callconv(.c) void {
+ // We only toast for the standard clipboard.
+ if (clipboard_type != .standard) return;
+
+ // We only toast if configured to
+ const priv = self.private();
+ const config_obj = priv.config orelse return;
+ const config = config_obj.get();
+ if (!config.@"app-notifications".@"clipboard-copy") {
+ return;
+ }
+
+ if (text[0] != 0)
+ self.addToast(i18n._("Copied to clipboard"))
+ else
+ self.addToast(i18n._("Cleared clipboard"));
+ }
+
+ fn surfaceMenu(
+ _: *Surface,
+ self: *Self,
+ ) callconv(.c) void {
+ self.syncActions();
+ }
+
+ fn surfacePresentRequest(
+ surface: *Surface,
+ self: *Self,
+ ) callconv(.c) void {
+ // Verify that this surface is actually in this window.
+ {
+ const surface_window = ext.getAncestor(
+ Self,
+ surface.as(gtk.Widget),
+ ) orelse {
+ log.warn(
+ "present request called for non-existent surface",
+ .{},
+ );
+ return;
+ };
+ if (surface_window != self) {
+ log.warn(
+ "present request called for surface in different window",
+ .{},
+ );
+ return;
+ }
+ }
+
+ // Get the tab for this surface.
+ const tab = ext.getAncestor(
+ Tab,
+ surface.as(gtk.Widget),
+ ) orelse {
+ log.warn("present request surface not found", .{});
+ return;
+ };
+
+ // Get the page that contains this tab
+ const priv = self.private();
+ const tab_view = priv.tab_view;
+ const page = tab_view.getPage(tab.as(gtk.Widget));
+ tab_view.setSelectedPage(page);
+
+ // Grab focus
+ surface.grabFocus();
+
+ // Bring the window to the front.
+ self.as(gtk.Window).present();
+ }
+
+ fn surfaceToggleFullscreen(
+ surface: *Surface,
+ self: *Self,
+ ) callconv(.c) void {
+ _ = surface;
+ if (self.as(gtk.Window).isFullscreen() != 0) {
+ self.as(gtk.Window).unfullscreen();
+ } else {
+ self.as(gtk.Window).fullscreen();
+ }
+
+ // We react to the changes in the propFullscreen callback
+ }
+
+ fn surfaceToggleMaximize(
+ surface: *Surface,
+ self: *Self,
+ ) callconv(.c) void {
+ _ = surface;
+ if (self.as(gtk.Window).isMaximized() != 0) {
+ self.as(gtk.Window).unmaximize();
+ } else {
+ self.as(gtk.Window).maximize();
+ }
+
+ // We react to the changes in the propMaximized callback
+ }
+
+ fn surfaceInit(
+ surface: *Surface,
+ self: *Self,
+ ) callconv(.c) void {
+ const priv = self.private();
+
+ // Make sure we init only once
+ if (priv.surface_init) return;
+ priv.surface_init = true;
+
+ // Setup our default and minimum size.
+ if (surface.getDefaultSize()) |size| {
+ self.as(gtk.Window).setDefaultSize(
+ @intCast(size.width),
+ @intCast(size.height),
+ );
+ }
+ if (surface.getMinSize()) |size| {
+ self.as(gtk.Widget).setSizeRequest(
+ @intCast(size.width),
+ @intCast(size.height),
+ );
+ }
+ }
+
+ fn tabSplitTreeChanged(
+ _: *SplitTree,
+ old_tree: ?*const Surface.Tree,
+ new_tree: ?*const Surface.Tree,
+ self: *Self,
+ ) callconv(.c) void {
+ if (old_tree) |tree| {
+ self.disconnectSurfaceHandlers(tree);
+ }
+
+ if (new_tree) |tree| {
+ self.connectSurfaceHandlers(tree);
+ }
+ }
+
+ fn actionAbout(
+ _: *gio.SimpleAction,
+ _: ?*glib.Variant,
+ self: *Self,
+ ) callconv(.c) void {
+ const name = "Ghostty";
+ const icon = "com.mitchellh.ghostty";
+ const website = "https://ghostty.org";
+
+ if (adw_version.supportsDialogs()) {
+ adw.showAboutDialog(
+ self.as(gtk.Widget),
+ "application-name",
+ name,
+ "developer-name",
+ i18n._("Ghostty Developers"),
+ "application-icon",
+ icon,
+ "version",
+ build_config.version_string.ptr,
+ "issue-url",
+ "https://github.com/ghostty-org/ghostty/issues",
+ "website",
+ website,
+ @as(?*anyopaque, null),
+ );
+ } else {
+ gtk.showAboutDialog(
+ self.as(gtk.Window),
+ "program-name",
+ name,
+ "logo-icon-name",
+ icon,
+ "title",
+ i18n._("About Ghostty"),
+ "version",
+ build_config.version_string.ptr,
+ "website",
+ website,
+ @as(?*anyopaque, null),
+ );
+ }
+ }
+
+ fn actionClose(
+ _: *gio.SimpleAction,
+ _: ?*glib.Variant,
+ self: *Self,
+ ) callconv(.c) void {
+ self.as(gtk.Window).close();
+ }
+
+ fn actionCloseTab(
+ _: *gio.SimpleAction,
+ param_: ?*glib.Variant,
+ self: *Window,
+ ) callconv(.c) void {
+ const param = param_ orelse {
+ log.warn("win.close-tab called without a parameter", .{});
+ return;
+ };
+
+ var str: ?[*:0]const u8 = null;
+ param.get("&s", &str);
+
+ const mode = std.meta.stringToEnum(
+ input.Binding.Action.CloseTabMode,
+ std.mem.span(
+ str orelse {
+ log.warn("invalid mode provided to win.close-tab", .{});
+ return;
+ },
+ ),
+ ) orelse {
+ log.warn("invalid mode provided to win.close-tab: {s}", .{str.?});
+ return;
+ };
+
+ self.performBindingAction(.{ .close_tab = mode });
+ }
+
+ fn actionNewWindow(
+ _: *gio.SimpleAction,
+ _: ?*glib.Variant,
+ self: *Window,
+ ) callconv(.c) void {
+ self.performBindingAction(.new_window);
+ }
+
+ fn actionNewTab(
+ _: *gio.SimpleAction,
+ _: ?*glib.Variant,
+ self: *Window,
+ ) callconv(.c) void {
+ self.performBindingAction(.new_tab);
+ }
+
+ fn actionPromptContextTabTitle(
+ _: *gio.SimpleAction,
+ _: ?*glib.Variant,
+ self: *Self,
+ ) callconv(.c) void {
+ const priv = self.private();
+ const page = priv.context_menu_page orelse return;
+ const child = page.getChild();
+ const tab = gobject.ext.cast(Tab, child) orelse return;
+ tab.promptTabTitle();
+ }
+
+ fn actionPromptSurfaceTitle(
+ _: *gio.SimpleAction,
+ _: ?*glib.Variant,
+ self: *Window,
+ ) callconv(.c) void {
+ self.performBindingAction(.prompt_surface_title);
+ }
+
+ fn actionPromptTabTitle(
+ _: *gio.SimpleAction,
+ _: ?*glib.Variant,
+ self: *Window,
+ ) callconv(.c) void {
+ self.performBindingAction(.prompt_tab_title);
+ }
+
+ fn actionSplitRight(
+ _: *gio.SimpleAction,
+ _: ?*glib.Variant,
+ self: *Window,
+ ) callconv(.c) void {
+ self.performBindingAction(.{ .new_split = .right });
+ }
+
+ fn actionSplitLeft(
+ _: *gio.SimpleAction,
+ _: ?*glib.Variant,
+ self: *Window,
+ ) callconv(.c) void {
+ self.performBindingAction(.{ .new_split = .left });
+ }
+
+ fn actionSplitUp(
+ _: *gio.SimpleAction,
+ _: ?*glib.Variant,
+ self: *Window,
+ ) callconv(.c) void {
+ self.performBindingAction(.{ .new_split = .up });
+ }
+
+ fn actionSplitDown(
+ _: *gio.SimpleAction,
+ _: ?*glib.Variant,
+ self: *Window,
+ ) callconv(.c) void {
+ self.performBindingAction(.{ .new_split = .down });
+ }
+
+ fn actionCopy(
+ _: *gio.SimpleAction,
+ _: ?*glib.Variant,
+ self: *Window,
+ ) callconv(.c) void {
+ self.performBindingAction(.{ .copy_to_clipboard = .mixed });
+ }
+
+ fn actionPaste(
+ _: *gio.SimpleAction,
+ _: ?*glib.Variant,
+ self: *Window,
+ ) callconv(.c) void {
+ self.performBindingAction(.paste_from_clipboard);
+ }
+
+ fn actionReset(
+ _: *gio.SimpleAction,
+ _: ?*glib.Variant,
+ self: *Window,
+ ) callconv(.c) void {
+ self.performBindingAction(.reset);
+ }
+
+ fn actionClear(
+ _: *gio.SimpleAction,
+ _: ?*glib.Variant,
+ self: *Window,
+ ) callconv(.c) void {
+ self.performBindingAction(.clear_screen);
+ }
+
+ fn actionRingBell(
+ _: *gio.SimpleAction,
+ _: ?*glib.Variant,
+ self: *Window,
+ ) callconv(.c) void {
+ const priv = self.private();
+ const config = if (priv.config) |v| v.get() else return;
+
+ if (config.@"bell-features".system) system: {
+ const native = self.as(gtk.Native).getSurface() orelse {
+ log.warn("unable to get native surface from window", .{});
+ break :system;
+ };
+ native.beep();
+ }
+
+ if (config.@"bell-features".attention) attention: {
+ // Dont set urgency if the window is already active.
+ if (self.as(gtk.Window).isActive() != 0) break :attention;
+
+ // Request user attention
+ self.winproto().setUrgent(true) catch |err| {
+ log.warn("winproto failed to set urgency={}", .{err});
+ };
+ }
+ }
+
+ /// Toggle the command palette.
+ ///
+ /// TODO: accept the surface that toggled the command palette as a parameter
+ fn toggleCommandPalette(self: *Window) void {
+ const priv = self.private();
+
+ // Get a reference to a command palette. First check the weak reference
+ // that we save to see if we already have one stored. If we don't then
+ // create a new one.
+ const command_palette = priv.command_palette.get() orelse command_palette: {
+ // Create a fresh command palette.
+ const command_palette = CommandPalette.new();
+
+ // Synchronize our config to the command palette's config.
+ _ = gobject.Object.bindProperty(
+ self.as(gobject.Object),
+ "config",
+ command_palette.as(gobject.Object),
+ "config",
+ .{ .sync_create = true },
+ );
+
+ // Listen to the activate signal to know if the user selected an option in
+ // the command palette.
+ _ = CommandPalette.signals.trigger.connect(
+ command_palette,
+ *Window,
+ signalCommandPaletteTrigger,
+ self,
+ .{},
+ );
+
+ // Save a weak reference to the command palette. We use a weak reference to avoid
+ // reference counting cycles that might cause problems later.
+ priv.command_palette.set(command_palette);
+
+ break :command_palette command_palette;
+ };
+ defer command_palette.unref();
+
+ // Tell the command palette to toggle itself. If the dialog gets
+ // presented (instead of hidden) it will be modal over our window.
+ command_palette.toggle(self);
+ }
+
+ // React to a signal from a command palette asking an action to be performed.
+ fn signalCommandPaletteTrigger(_: *CommandPalette, action: *const input.Binding.Action, self: *Self) callconv(.c) void {
+ // If the activation actually has an action, perform it.
+ self.performBindingAction(action.*);
+ }
+
+ /// React to a GTK action requesting that the command palette be toggled.
+ fn actionToggleCommandPalette(
+ _: *gio.SimpleAction,
+ _: ?*glib.Variant,
+ self: *Window,
+ ) callconv(.c) void {
+ // TODO: accept the surface that toggled the command palette as a
+ // parameter
+ self.toggleCommandPalette();
+ }
+
+ /// Toggle the Ghostty inspector for the active surface.
+ fn toggleInspector(self: *Self) void {
+ const surface = self.getActiveSurface() orelse return;
+ _ = surface.controlInspector(.toggle);
+ }
+
+ /// React to a GTK action requesting that the Ghostty inspector be toggled.
+ fn actionToggleInspector(
+ _: *gio.SimpleAction,
+ _: ?*glib.Variant,
+ self: *Window,
+ ) callconv(.c) void {
+ // TODO: accept the surface that toggled the command palette as a
+ // parameter
+ self.toggleInspector();
+ }
+
+ const C = Common(Self, Private);
+ pub const as = C.as;
+ pub const ref = C.ref;
+ pub const unref = C.unref;
+ const private = C.private;
+
+ pub const Class = extern struct {
+ parent_class: Parent.Class,
+ var parent: *Parent.Class = undefined;
+ pub const Instance = Self;
+
+ fn init(class: *Class) callconv(.c) void {
+ gobject.ext.ensureType(DebugWarning);
+ gobject.ext.ensureType(SplitTree);
+ gobject.ext.ensureType(Surface);
+ gobject.ext.ensureType(Tab);
+ gtk.Widget.Class.setTemplateFromResource(
+ class.as(gtk.Widget.Class),
+ comptime gresource.blueprint(.{
+ .major = 1,
+ .minor = 5,
+ .name = "window",
+ }),
+ );
+
+ // Properties
+ gobject.ext.registerProperties(class, &.{
+ properties.@"active-surface".impl,
+ properties.config.impl,
+ properties.debug.impl,
+ properties.@"headerbar-visible".impl,
+ properties.@"quick-terminal".impl,
+ properties.@"tabs-autohide".impl,
+ properties.@"tabs-visible".impl,
+ properties.@"tabs-wide".impl,
+ properties.@"toolbar-style".impl,
+ properties.@"titlebar-style".impl,
+ });
+
+ // Bindings
+ class.bindTemplateChildPrivate("tab_overview", .{});
+ class.bindTemplateChildPrivate("tab_bar", .{});
+ class.bindTemplateChildPrivate("tab_view", .{});
+ class.bindTemplateChildPrivate("toolbar", .{});
+ class.bindTemplateChildPrivate("toast_overlay", .{});
+
+ // Template Callbacks
+ class.bindTemplateCallback("realize", &windowRealize);
+ class.bindTemplateCallback("new_tab", &btnNewTab);
+ class.bindTemplateCallback("overview_create_tab", &tabOverviewCreateTab);
+ class.bindTemplateCallback("overview_notify_open", &tabOverviewOpen);
+ class.bindTemplateCallback("close_request", &windowCloseRequest);
+ class.bindTemplateCallback("close_page", &tabViewClosePage);
+ class.bindTemplateCallback("page_attached", &tabViewPageAttached);
+ class.bindTemplateCallback("page_detached", &tabViewPageDetached);
+ class.bindTemplateCallback("setup_tab_menu", &setupTabMenu);
+ class.bindTemplateCallback("tab_create_window", &tabViewCreateWindow);
+ class.bindTemplateCallback("notify_n_pages", &tabViewNPages);
+ class.bindTemplateCallback("notify_selected_page", &tabViewSelectedPage);
+ class.bindTemplateCallback("notify_config", &propConfig);
+ class.bindTemplateCallback("notify_fullscreened", &propFullscreened);
+ class.bindTemplateCallback("notify_is_active", &propIsActive);
+ class.bindTemplateCallback("notify_maximized", &propMaximized);
+ class.bindTemplateCallback("notify_menu_active", &propMenuActive);
+ class.bindTemplateCallback("notify_quick_terminal", &propQuickTerminal);
+ class.bindTemplateCallback("notify_scale_factor", &propScaleFactor);
+ class.bindTemplateCallback("titlebar_style_is_tabs", &closureTitlebarStyleIsTab);
+ class.bindTemplateCallback("computed_subtitle", &closureSubtitle);
+
+ // Virtual methods
+ gobject.Object.virtual_methods.dispose.implement(class, &dispose);
+ gobject.Object.virtual_methods.finalize.implement(class, &finalize);
+ }
+
+ pub const as = C.Class.as;
+ pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
+ pub const bindTemplateCallback = C.Class.bindTemplateCallback;
+ };
+};
diff --git a/vendor/libghostty-vt/src/apprt/gtk/css/style-dark.css b/vendor/libghostty-vt/src/apprt/gtk/css/style-dark.css
new file mode 100644
index 00000000..f13b4f4f
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/css/style-dark.css
@@ -0,0 +1,8 @@
+.transparent {
+ background-color: transparent;
+}
+
+.window .split paned > separator {
+ background-color: rgba(36, 36, 36, 1);
+ background-clip: content-box;
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/css/style-hc-dark.css b/vendor/libghostty-vt/src/apprt/gtk/css/style-hc-dark.css
new file mode 100644
index 00000000..a9aa2dcc
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/css/style-hc-dark.css
@@ -0,0 +1,3 @@
+.transparent {
+ background-color: transparent;
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/css/style-hc.css b/vendor/libghostty-vt/src/apprt/gtk/css/style-hc.css
new file mode 100644
index 00000000..a9aa2dcc
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/css/style-hc.css
@@ -0,0 +1,3 @@
+.transparent {
+ background-color: transparent;
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/css/style.css b/vendor/libghostty-vt/src/apprt/gtk/css/style.css
new file mode 100644
index 00000000..9c0f115f
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/css/style.css
@@ -0,0 +1,183 @@
+/* Application CSS that applies to the entire application.
+ *
+ * This is automatically loaded by AdwApplication:
+ * https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1.3/styles-and-appearance.html#custom-styles
+ */
+
+window.ssd.no-border-radius {
+ /* Without clearing the border radius, at least on Mutter with
+ * gtk-titlebar=true and gtk-adwaita=false, there is some window artifacting
+ * that this will mitigate.
+ */
+ border-radius: 0 0;
+}
+
+/*
+ * GhosttySurface URL overlay
+ */
+label.url-overlay {
+ padding: 4px 8px 4px 8px;
+ outline-style: solid;
+ outline-color: #555555;
+ outline-width: 1px;
+}
+
+label.url-overlay:hover {
+ opacity: 0;
+}
+
+label.url-overlay.left {
+ border-radius: 0px 6px 0px 0px;
+}
+
+label.url-overlay.right {
+ border-radius: 6px 0px 0px 0px;
+}
+
+/*
+ * GhosttySurface search overlay
+ */
+.search-overlay {
+ padding: 6px 8px;
+ margin: 8px;
+ border-radius: 8px;
+ outline-style: solid;
+ outline-color: #555555;
+ outline-width: 1px;
+}
+
+/*
+ * GhosttySurface key state overlay
+ */
+.key-state-overlay {
+ padding: 6px 10px;
+ margin: 8px;
+ border-radius: 8px;
+ outline-style: solid;
+ outline-color: #555555;
+ outline-width: 1px;
+}
+
+/*
+ * GhosttySurface resize overlay
+ */
+label.resize-overlay {
+ padding: 4px 8px 4px 8px;
+ border-radius: 6px 6px 6px 6px;
+ outline-style: solid;
+ outline-color: #555555;
+ outline-width: 1px;
+}
+
+/*
+ * GhosttyClipboardConfirmationDialog
+ *
+ * Based on boxed-list-separate:
+ * https://gitlab.gnome.org/GNOME/libadwaita/-/blob/ad446167acf3e6d1ee693f98ca636268be8592a1/src/stylesheet/widgets/_lists.scss#L548
+ */
+.clipboard-confirmation-dialog list {
+ background: none;
+}
+
+.clipboard-confirmation-dialog list > row {
+ border: none;
+ margin-bottom: 12px;
+}
+
+.clipboard-confirmation-dialog list > row:last-child {
+ margin-bottom: 0;
+}
+
+.clipboard-confirmation-dialog .clipboard-overlay {
+ border-radius: 10px;
+}
+
+.clipboard-confirmation-dialog .clipboard-contents {
+ filter: blur(0px);
+ transition: filter 0.3s ease;
+ border-radius: 10px;
+}
+
+.clipboard-confirmation-dialog .clipboard-contents.blurred {
+ filter: blur(5px);
+}
+
+/*
+ * Child Exited Overlay
+ */
+.child-exited.normal revealer widget {
+ background-color: rgba(38, 162, 105, 0.5);
+ /* after GTK 4.16 is a requirement, switch to the following: */
+ /* background-color: color-mix(in srgb, var(--success-bg-color), transparent 50%); */
+}
+
+.child-exited.abnormal revealer widget {
+ background-color: rgba(192, 28, 40, 0.5);
+ /* after GTK 4.16 is a requirement, switch to the following: */
+ /* background-color: color-mix(in srgb, var(--error-bg-color), transparent 50%); */
+}
+
+/*
+ * Surface
+ */
+.surface progressbar.error trough progress {
+ background-color: rgba(192, 28, 40, 0.5);
+ /* after GTK 4.16 is a requirement, switch to the following: */
+ /* background-color: color-mix(in srgb, var(--error-bg-color), transparent 50%); */
+}
+
+.surface .bell-overlay {
+ border-color: rgba(58, 148, 74, 0.5);
+ /* after GTK 4.16 is a requirement, switch to the following: */
+ /* background-color: color-mix(in srgb, var(--accent-color), transparent 50%); */
+ border-width: 3px;
+ border-style: solid;
+}
+
+.surface .readonly_overlay {
+ /* Should be the equivalent of the following SwiftUI color: */
+ /* Color(hue: 0.08, saturation: 0.5, brightness: 0.8) */
+ color: hsl(25 50 75);
+ padding: 8px 8px 8px 8px;
+ margin: 8px 8px 8px 8px;
+ border-radius: 6px 6px 6px 6px;
+ outline-style: solid;
+ outline-width: 1px;
+}
+/*
+ * Command Palette
+ */
+.command-palette-search > image:first-child {
+ margin-left: 8px;
+ margin-right: 4px;
+}
+
+.command-palette-search > image:last-child {
+ margin-left: 4px;
+ margin-right: 8px;
+}
+
+/*
+ * Splits
+ */
+
+.window .split paned > separator {
+ background-color: rgba(250, 250, 250, 1);
+ /* after GTK 4.16 is a requirement, switch to the following: */
+ /* background-color: color-mix(in srgb, var(--window-bg-color), transparent 0%); */
+ background-clip: content-box;
+
+ /* This works around the oversized drag area for the right side of GtkPaned.
+ *
+ * Upstream Gtk issue:
+ * https://gitlab.gnome.org/GNOME/gtk/-/issues/4484#note_2362002
+ *
+ * Ghostty issue:
+ * https://github.com/ghostty-org/ghostty/issues/3020
+ *
+ * Without this, it's not possible to select the first character on the
+ * right-hand side of a split.
+ */
+ margin: 0;
+ padding: 0;
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/ext.zig b/vendor/libghostty-vt/src/apprt/gtk/ext.zig
new file mode 100644
index 00000000..df9ab4ea
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/ext.zig
@@ -0,0 +1,70 @@
+//! Extensions/helpers for GTK objects, following a similar naming
+//! style to zig-gobject. These should, wherever possible, be Zig-friendly
+//! wrappers around existing GTK functionality, rather than complex new
+//! helpers.
+
+const std = @import("std");
+const assert = @import("../../quirks.zig").inlineAssert;
+const testing = std.testing;
+
+const glib = @import("glib");
+const gobject = @import("gobject");
+const gtk = @import("gtk");
+
+pub const actions = @import("ext/actions.zig");
+const slice = @import("ext/slice.zig");
+pub const StringList = slice.StringList;
+
+/// Wrapper around `gobject.boxedCopy` to copy a boxed type `T`.
+pub fn boxedCopy(comptime T: type, ptr: *const T) *T {
+ const copy = gobject.boxedCopy(T.getGObjectType(), ptr);
+ return @ptrCast(@alignCast(copy));
+}
+
+/// Wrapper around `gobject.boxedFree` to free a boxed type `T`.
+pub fn boxedFree(comptime T: type, ptr: ?*T) void {
+ if (ptr) |p| gobject.boxedFree(
+ T.getGObjectType(),
+ p,
+ );
+}
+
+/// A wrapper around `glib.List.findCustom` to find an element in the list.
+/// The type `T` must be the guaranteed type of every list element.
+pub fn listFind(
+ comptime T: type,
+ list: *glib.List,
+ comptime func: *const fn (*T) bool,
+) ?*T {
+ const elem_: ?*glib.List = list.findCustom(null, struct {
+ fn callback(data: ?*const anyopaque, _: ?*const anyopaque) callconv(.c) c_int {
+ const ptr = data orelse return 1;
+ const v: *T = @ptrCast(@alignCast(@constCast(ptr)));
+ return if (func(v)) 0 else 1;
+ }
+ }.callback);
+ const elem = elem_ orelse return null;
+ return @ptrCast(@alignCast(elem.f_data));
+}
+
+/// Wrapper around `gtk.Widget.getAncestor` to get the widget ancestor
+/// of the given type `T`, or null if it doesn't exist.
+pub fn getAncestor(comptime T: type, widget: *gtk.Widget) ?*T {
+ const ancestor_ = widget.getAncestor(gobject.ext.typeFor(T));
+ const ancestor = ancestor_ orelse return null;
+ // We can assert the unwrap because getAncestor above
+ return gobject.ext.cast(T, ancestor).?;
+}
+
+/// Check a gobject.Value to see what type it is wrapping. This is equivalent to GTK's
+/// `G_VALUE_HOLDS()` macro but Zig's C translator does not like it.
+pub fn gValueHolds(value_: ?*const gobject.Value, g_type: gobject.Type) bool {
+ const value = value_ orelse return false;
+ if (value.f_g_type == g_type) return true;
+ return gobject.typeCheckValueHolds(value, g_type) != 0;
+}
+
+test {
+ _ = actions;
+ _ = slice;
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/ext/actions.zig b/vendor/libghostty-vt/src/apprt/gtk/ext/actions.zig
new file mode 100644
index 00000000..3232bc18
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/ext/actions.zig
@@ -0,0 +1,198 @@
+const std = @import("std");
+
+const assert = @import("../../../quirks.zig").inlineAssert;
+const testing = std.testing;
+
+const gio = @import("gio");
+const glib = @import("glib");
+const gobject = @import("gobject");
+const gtk = @import("gtk");
+
+const gValueHolds = @import("../ext.zig").gValueHolds;
+
+/// Check that an action name is valid.
+///
+/// Reimplementation of `g_action_name_is_valid()` so that it can be
+/// used at comptime.
+///
+/// See:
+/// https://docs.gtk.org/gio/type_func.Action.name_is_valid.html
+fn gActionNameIsValid(name: [:0]const u8) bool {
+ if (name.len == 0) return false;
+
+ for (name) |c| switch (c) {
+ '-' => continue,
+ '.' => continue,
+ '0'...'9' => continue,
+ 'a'...'z' => continue,
+ 'A'...'Z' => continue,
+ else => return false,
+ };
+
+ return true;
+}
+
+test "gActionNameIsValid" {
+ try testing.expect(gActionNameIsValid("ring-bell"));
+ try testing.expect(!gActionNameIsValid("ring_bell"));
+}
+
+/// Function to create a structure for describing an action.
+pub fn Action(comptime T: type) type {
+ return struct {
+ const Self = @This();
+ pub const Callback = *const fn (*gio.SimpleAction, ?*glib.Variant, *T) callconv(.c) void;
+
+ name: [:0]const u8,
+ callback: Callback,
+ parameter_type: ?*const glib.VariantType,
+ state: ?*glib.Variant = null,
+
+ /// Function to initialize a new action so that we can comptime check
+ /// the name.
+ pub fn init(
+ comptime name: [:0]const u8,
+ callback: Callback,
+ parameter_type: ?*const glib.VariantType,
+ ) Self {
+ comptime assert(gActionNameIsValid(name));
+
+ return .{
+ .name = name,
+ .callback = callback,
+ .parameter_type = parameter_type,
+ };
+ }
+
+ /// Function to initialize a new stateful action so that we can comptime
+ /// check the name.
+ pub fn initStateful(
+ comptime name: [:0]const u8,
+ callback: Callback,
+ parameter_type: ?*const glib.VariantType,
+ state: *glib.Variant,
+ ) Self {
+ comptime assert(gActionNameIsValid(name));
+ return .{
+ .name = name,
+ .callback = callback,
+ .parameter_type = parameter_type,
+ .state = state,
+ };
+ }
+ };
+}
+
+/// Add actions to a widget that implements gio.ActionMap.
+pub fn add(comptime T: type, self: *T, actions: []const Action(T)) void {
+ addToMap(T, self, self.as(gio.ActionMap), actions);
+}
+
+/// Add actions to the given map.
+pub fn addToMap(comptime T: type, self: *T, map: *gio.ActionMap, actions: []const Action(T)) void {
+ for (actions) |entry| {
+ assert(gActionNameIsValid(entry.name));
+ const action = action: {
+ if (entry.state) |state| {
+ break :action gio.SimpleAction.newStateful(
+ entry.name,
+ entry.parameter_type,
+ state,
+ );
+ }
+ break :action gio.SimpleAction.new(
+ entry.name,
+ entry.parameter_type,
+ );
+ };
+ defer action.unref();
+ _ = gio.SimpleAction.signals.activate.connect(
+ action,
+ *T,
+ entry.callback,
+ self,
+ .{},
+ );
+ map.addAction(action.as(gio.Action));
+ }
+}
+
+/// Add actions to a widget that doesn't implement ActionGroup directly.
+pub fn addAsGroup(comptime T: type, self: *T, comptime name: [:0]const u8, actions: []const Action(T)) *gio.SimpleActionGroup {
+ comptime assert(gActionNameIsValid(name));
+
+ // Collect our actions into a group since we're just a plain widget that
+ // doesn't implement ActionGroup directly.
+ const group = gio.SimpleActionGroup.new();
+ errdefer group.unref();
+
+ addToMap(T, self, group.as(gio.ActionMap), actions);
+
+ self.as(gtk.Widget).insertActionGroup(
+ name,
+ group.as(gio.ActionGroup),
+ );
+
+ return group;
+}
+
+test "adding actions to an object" {
+ // This test requires a connection to an active display environment.
+ if (gtk.initCheck() == 0) return error.SkipZigTest;
+
+ _ = glib.MainContext.acquire(null);
+ defer glib.MainContext.release(null);
+
+ const callbacks = struct {
+ fn callback(_: *gio.SimpleAction, variant_: ?*glib.Variant, self: *gtk.Box) callconv(.c) void {
+ const i32_variant_type = glib.ext.VariantType.newFor(i32);
+ defer i32_variant_type.free();
+
+ const variant = variant_ orelse return;
+ assert(variant.isOfType(i32_variant_type) != 0);
+
+ var value = std.mem.zeroes(gobject.Value);
+ _ = value.init(gobject.ext.types.int);
+ defer value.unset();
+
+ value.setInt(variant.getInt32());
+
+ self.as(gobject.Object).setProperty("spacing", &value);
+ }
+ };
+
+ const box = gtk.Box.new(.vertical, 0);
+ _ = box.as(gobject.Object).refSink();
+ defer box.unref();
+
+ {
+ const i32_variant_type = glib.ext.VariantType.newFor(i32);
+ defer i32_variant_type.free();
+
+ const actions = [_]Action(gtk.Box){
+ .init("test", callbacks.callback, i32_variant_type),
+ };
+
+ _ = addAsGroup(gtk.Box, box, "test", &actions);
+ }
+
+ const expected = std.crypto.random.intRangeAtMost(i32, 1, std.math.maxInt(u31));
+ const parameter = glib.Variant.newInt32(expected);
+
+ try testing.expect(box.as(gtk.Widget).activateActionVariant("test.test", parameter) != 0);
+
+ _ = glib.MainContext.iteration(null, @intFromBool(true));
+
+ var value = std.mem.zeroes(gobject.Value);
+ _ = value.init(gobject.ext.types.int);
+ defer value.unset();
+
+ box.as(gobject.Object).getProperty("spacing", &value);
+
+ try testing.expect(gValueHolds(&value, gobject.ext.types.int));
+
+ const actual = value.getInt();
+ try testing.expectEqual(expected, actual);
+
+ while (glib.MainContext.iteration(null, 0) != 0) {}
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/ext/slice.zig b/vendor/libghostty-vt/src/apprt/gtk/ext/slice.zig
new file mode 100644
index 00000000..49ad63d8
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/ext/slice.zig
@@ -0,0 +1,111 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const ArenaAllocator = std.heap.ArenaAllocator;
+const glib = @import("glib");
+const gobject = @import("gobject");
+
+/// A boxed type that holds a list of string slices.
+pub const StringList = struct {
+ arena: ArenaAllocator,
+ strings: []const [:0]const u8,
+
+ pub fn create(
+ alloc: Allocator,
+ strings: []const [:0]const u8,
+ ) Allocator.Error!*StringList {
+ var arena: ArenaAllocator = .init(alloc);
+ errdefer arena.deinit();
+ const arena_alloc = arena.allocator();
+ var stored = try arena_alloc.alloc([:0]const u8, strings.len);
+ for (strings, 0..) |s, i| stored[i] = try arena_alloc.dupeZ(u8, s);
+
+ const ptr = try alloc.create(StringList);
+ errdefer alloc.destroy(ptr);
+ ptr.* = .{ .arena = arena, .strings = stored };
+
+ return ptr;
+ }
+
+ pub fn deinit(self: *StringList) void {
+ self.arena.deinit();
+ }
+
+ pub fn destroy(self: *StringList) void {
+ const alloc = self.arena.child_allocator;
+ self.deinit();
+ alloc.destroy(self);
+ }
+
+ /// Returns the general-purpose allocator used by this StringList.
+ pub fn allocator(self: *const StringList) Allocator {
+ return self.arena.child_allocator;
+ }
+
+ pub const getGObjectType = gobject.ext.defineBoxed(
+ StringList,
+ .{
+ .name = "GhosttyStringList",
+ .funcs = .{
+ .copy = &struct {
+ fn copy(self: *StringList) callconv(.c) *StringList {
+ return StringList.create(
+ self.arena.child_allocator,
+ self.strings,
+ ) catch @panic("OOM");
+ }
+ }.copy,
+ .free = &struct {
+ fn free(self: *StringList) callconv(.c) void {
+ self.destroy();
+ }
+ }.free,
+ },
+ },
+ );
+};
+
+test "StringList create and destroy" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const input: []const [:0]const u8 = &.{ "hello", "world" };
+ const list = try StringList.create(alloc, input);
+ defer list.destroy();
+
+ try testing.expectEqual(@as(usize, 2), list.strings.len);
+ try testing.expectEqualStrings("hello", list.strings[0]);
+ try testing.expectEqualStrings("world", list.strings[1]);
+}
+
+test "StringList create empty list" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const input: []const [:0]const u8 = &.{};
+ const list = try StringList.create(alloc, input);
+ defer list.destroy();
+
+ try testing.expectEqual(@as(usize, 0), list.strings.len);
+}
+
+test "StringList boxedCopy and boxedFree" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const input: []const [:0]const u8 = &.{ "foo", "bar", "baz" };
+ const original = try StringList.create(alloc, input);
+ defer original.destroy();
+
+ const copied: *StringList = @ptrCast(@alignCast(gobject.boxedCopy(
+ StringList.getGObjectType(),
+ original,
+ )));
+ defer gobject.boxedFree(StringList.getGObjectType(), copied);
+
+ try testing.expectEqual(@as(usize, 3), copied.strings.len);
+ try testing.expectEqualStrings("foo", copied.strings[0]);
+ try testing.expectEqualStrings("bar", copied.strings[1]);
+ try testing.expectEqualStrings("baz", copied.strings[2]);
+
+ try testing.expect(original.strings.ptr != copied.strings.ptr);
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/flatpak.zig b/vendor/libghostty-vt/src/apprt/gtk/flatpak.zig
new file mode 100644
index 00000000..dc47c671
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/flatpak.zig
@@ -0,0 +1,29 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const build_config = @import("../../build_config.zig");
+const internal_os = @import("../../os/main.zig");
+const glib = @import("glib");
+
+pub fn resourcesDir(alloc: Allocator) !internal_os.ResourcesDir {
+ if (comptime build_config.flatpak) {
+ // Only consult Flatpak runtime data for host case.
+ if (internal_os.isFlatpak()) {
+ var result: internal_os.ResourcesDir = .{
+ .app_path = try alloc.dupe(u8, "/app/share/ghostty"),
+ };
+ errdefer alloc.free(result.app_path.?);
+
+ const keyfile = glib.KeyFile.new();
+ defer keyfile.unref();
+
+ if (keyfile.loadFromFile("/.flatpak-info", .{}, null) == 0) return result;
+ const app_dir = std.mem.span(keyfile.getString("Instance", "app-path", null)) orelse return result;
+ defer glib.free(app_dir.ptr);
+
+ result.host_path = try std.fs.path.join(alloc, &[_][]const u8{ app_dir, "share", "ghostty" });
+ return result;
+ }
+ }
+
+ return try internal_os.resourcesDir(alloc);
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/gsettings.zig b/vendor/libghostty-vt/src/apprt/gtk/gsettings.zig
new file mode 100644
index 00000000..8cf7f12d
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/gsettings.zig
@@ -0,0 +1,101 @@
+const std = @import("std");
+const gtk = @import("gtk");
+const gobject = @import("gobject");
+
+/// GTK Settings keys with well-defined types.
+pub const Key = enum {
+ @"gtk-enable-primary-paste",
+ @"gtk-xft-dpi",
+ @"gtk-font-name",
+
+ fn Type(comptime self: Key) type {
+ return switch (self) {
+ .@"gtk-enable-primary-paste" => bool,
+ .@"gtk-xft-dpi" => c_int,
+ .@"gtk-font-name" => []const u8,
+ };
+ }
+
+ fn GValueType(comptime self: Key) type {
+ return switch (self.Type()) {
+ bool => c_int,
+ c_int => c_int,
+ []const u8 => ?[*:0]const u8,
+ else => @compileError("Unsupported type for GTK settings"),
+ };
+ }
+
+ /// Returns true if this setting type requires memory allocation.
+ /// Types that do not need allocation must be explicitly marked.
+ fn requiresAllocation(comptime self: Key) bool {
+ const T = self.Type();
+ return switch (T) {
+ bool, c_int => false,
+ else => true,
+ };
+ }
+};
+
+/// Reads a GTK setting for non-allocating types.
+/// Automatically uses XDG Desktop Portal in Flatpak environments.
+/// Returns null if the setting is unavailable.
+pub fn get(comptime key: Key) ?key.Type() {
+ if (comptime key.requiresAllocation()) {
+ @compileError("Allocating types require an allocator; use getAlloc() instead");
+ }
+ const settings = gtk.Settings.getDefault() orelse return null;
+ return getImpl(settings, null, key) catch unreachable;
+}
+
+/// Reads a GTK setting, allocating memory if necessary.
+/// Automatically uses XDG Desktop Portal in Flatpak environments.
+/// Caller must free returned memory with the provided allocator.
+/// Returns null if the setting is unavailable.
+pub fn getAlloc(allocator: std.mem.Allocator, comptime key: Key) !?key.Type() {
+ const settings = gtk.Settings.getDefault() orelse return null;
+ return getImpl(settings, allocator, key);
+}
+
+fn getImpl(settings: *gtk.Settings, allocator: ?std.mem.Allocator, comptime key: Key) !?key.Type() {
+ const GValType = key.GValueType();
+ var value = gobject.ext.Value.new(GValType);
+ defer value.unset();
+
+ settings.as(gobject.Object).getProperty(@tagName(key).ptr, &value);
+
+ return switch (key.Type()) {
+ bool => value.getInt() != 0,
+ c_int => value.getInt(),
+ []const u8 => blk: {
+ const alloc = allocator.?;
+ const ptr = value.getString() orelse break :blk null;
+ const str = std.mem.span(ptr);
+ break :blk try alloc.dupe(u8, str);
+ },
+ else => @compileError("Unsupported type for GTK settings"),
+ };
+}
+
+test "Key.Type returns correct types" {
+ try std.testing.expectEqual(bool, Key.@"gtk-enable-primary-paste".Type());
+ try std.testing.expectEqual(c_int, Key.@"gtk-xft-dpi".Type());
+ try std.testing.expectEqual([]const u8, Key.@"gtk-font-name".Type());
+}
+
+test "Key.requiresAllocation identifies allocating types" {
+ try std.testing.expectEqual(false, Key.@"gtk-enable-primary-paste".requiresAllocation());
+ try std.testing.expectEqual(false, Key.@"gtk-xft-dpi".requiresAllocation());
+ try std.testing.expectEqual(true, Key.@"gtk-font-name".requiresAllocation());
+}
+
+test "Key.GValueType returns correct GObject types" {
+ try std.testing.expectEqual(c_int, Key.@"gtk-enable-primary-paste".GValueType());
+ try std.testing.expectEqual(c_int, Key.@"gtk-xft-dpi".GValueType());
+ try std.testing.expectEqual(?[*:0]const u8, Key.@"gtk-font-name".GValueType());
+}
+
+test "@tagName returns correct GTK property names" {
+ try std.testing.expectEqualStrings("gtk-enable-primary-paste", @tagName(Key.@"gtk-enable-primary-paste"));
+ try std.testing.expectEqualStrings("gtk-xft-dpi", @tagName(Key.@"gtk-xft-dpi"));
+ try std.testing.expectEqualStrings("gtk-font-name", @tagName(Key.@"gtk-font-name"));
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/gtk_version.zig b/vendor/libghostty-vt/src/apprt/gtk/gtk_version.zig
new file mode 100644
index 00000000..71edb076
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/gtk_version.zig
@@ -0,0 +1,140 @@
+const std = @import("std");
+
+// Until the gobject bindings are built at the same time we are building
+// Ghostty, we need to import `gtk/gtk.h` directly to ensure that the version
+// macros match the version of `gtk4` that we are building/linking against.
+const c = @cImport({
+ @cInclude("gtk/gtk.h");
+});
+
+const gtk = @import("gtk");
+
+const log = std.log.scoped(.gtk);
+
+pub const comptime_version: std.SemanticVersion = .{
+ .major = c.GTK_MAJOR_VERSION,
+ .minor = c.GTK_MINOR_VERSION,
+ .patch = c.GTK_MICRO_VERSION,
+};
+
+pub fn getRuntimeVersion() std.SemanticVersion {
+ return .{
+ .major = gtk.getMajorVersion(),
+ .minor = gtk.getMinorVersion(),
+ .patch = gtk.getMicroVersion(),
+ };
+}
+
+pub fn logVersion() void {
+ log.info("GTK version build={f} runtime={f}", .{
+ comptime_version,
+ getRuntimeVersion(),
+ });
+}
+
+/// Verifies that the GTK version is at least the given version.
+///
+/// This can be run in both a comptime and runtime context. If it is run in a
+/// comptime context, it will only check the version in the headers. If it is
+/// run in a runtime context, it will check the actual version of the library we
+/// are linked against.
+///
+/// This function should be used in cases where the version check would affect
+/// code generation, such as using symbols that are only available beyond a
+/// certain version. For checks which only depend on GTK's runtime behavior,
+/// use `runtimeAtLeast`.
+///
+/// This is inlined so that the comptime checks will disable the runtime checks
+/// if the comptime checks fail.
+pub inline fn atLeast(
+ comptime major: u16,
+ comptime minor: u16,
+ comptime micro: u16,
+) bool {
+ // If our header has lower versions than the given version,
+ // we can return false immediately. This prevents us from
+ // compiling against unknown symbols and makes runtime checks
+ // very slightly faster.
+ if (comptime comptime_version.order(.{
+ .major = major,
+ .minor = minor,
+ .patch = micro,
+ }) == .lt) return false;
+
+ // If we're in comptime then we can't check the runtime version.
+ if (@inComptime()) return true;
+
+ return runtimeAtLeast(major, minor, micro);
+}
+
+/// Verifies that the GTK version at runtime is at least the given version.
+///
+/// This function should be used in cases where the only the runtime behavior
+/// is affected by the version check. For checks which would affect code
+/// generation, use `atLeast`.
+pub inline fn runtimeAtLeast(
+ comptime major: u16,
+ comptime minor: u16,
+ comptime micro: u16,
+) bool {
+ // We use the functions instead of the constants such as c.GTK_MINOR_VERSION
+ // because the function gets the actual runtime version.
+ const runtime_version = getRuntimeVersion();
+ return runtime_version.order(.{
+ .major = major,
+ .minor = minor,
+ .patch = micro,
+ }) != .lt;
+}
+
+pub inline fn runtimeUntil(
+ comptime major: u16,
+ comptime minor: u16,
+ comptime micro: u16,
+) bool {
+ const runtime_version = getRuntimeVersion();
+ return runtime_version.order(.{
+ .major = major,
+ .minor = minor,
+ .patch = micro,
+ }) == .lt;
+}
+
+test "atLeast" {
+ const testing = std.testing;
+
+ const funs = &.{ atLeast, runtimeAtLeast };
+ inline for (funs) |fun| {
+ try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
+
+ try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
+ try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
+ try testing.expect(!fun(c.GTK_MAJOR_VERSION + 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
+
+ try testing.expect(fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
+ try testing.expect(fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
+ try testing.expect(fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
+
+ try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION - 1, c.GTK_MICRO_VERSION + 1));
+ }
+}
+
+test "runtimeUntil" {
+ const testing = std.testing;
+
+ // This is an array in case we add a comptime variant.
+ const funs = &.{runtimeUntil};
+ inline for (funs) |fun| {
+ try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
+
+ try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
+ try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
+ try testing.expect(fun(c.GTK_MAJOR_VERSION + 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
+
+ try testing.expect(!fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
+ try testing.expect(!fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
+ try testing.expect(!fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
+
+ try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION - 1, c.GTK_MICRO_VERSION + 1));
+ }
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/ipc/DBus.zig b/vendor/libghostty-vt/src/apprt/gtk/ipc/DBus.zig
new file mode 100644
index 00000000..fa4a6723
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/ipc/DBus.zig
@@ -0,0 +1,196 @@
+//! DBus helper for IPC
+const Self = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+const gio = @import("gio");
+const glib = @import("glib");
+
+const apprt = @import("../../../apprt.zig");
+const ApprtApp = @import("../App.zig");
+
+/// The target for this IPC.
+target: apprt.ipc.Target,
+
+/// Connection to the DBus session bus.
+dbus: *gio.DBusConnection,
+
+/// The bus name of the Ghostty instance that we are calling.
+bus_name: [:0]const u8,
+
+/// The object path of the Ghostty instance that we are calling.
+object_path: [:0]const u8,
+
+/// Used to build the DBus payload.
+payload_builder: *glib.VariantBuilder,
+
+/// Used to build the parameters for the IPC.
+parameters_builder: *glib.VariantBuilder,
+
+/// Initialize the helper.
+pub fn init(alloc: Allocator, target: apprt.ipc.Target, action: [:0]const u8) (Allocator.Error || std.Io.Writer.Error || apprt.ipc.Errors)!Self {
+ var buf: [256]u8 = undefined;
+ var stderr_writer = std.fs.File.stderr().writer(&buf);
+ const stderr = &stderr_writer.interface;
+
+ // Get the appropriate bus name and object path for contacting the
+ // Ghostty instance we're interested in.
+ const bus_name: [:0]const u8, const object_path: [:0]const u8 = switch (target) {
+ .class => |class| result: {
+ // Force the usage of the class specified on the CLI to determine the
+ // bus name and object path.
+ const object_path = try std.fmt.allocPrintSentinel(alloc, "/{s}", .{class}, 0);
+
+ std.mem.replaceScalar(u8, object_path, '.', '/');
+ std.mem.replaceScalar(u8, object_path, '-', '_');
+
+ break :result .{ class, object_path };
+ },
+ .detect => .{ ApprtApp.application_id, ApprtApp.object_path },
+ };
+ errdefer {
+ switch (target) {
+ .class => alloc.free(object_path),
+ .detect => {},
+ }
+ }
+
+ if (gio.Application.idIsValid(bus_name.ptr) == 0) {
+ try stderr.print("D-Bus bus name is not valid: {s}\n", .{bus_name});
+ try stderr.flush();
+ return error.IPCFailed;
+ }
+
+ if (glib.Variant.isObjectPath(object_path.ptr) == 0) {
+ try stderr.print("D-Bus object path is not valid: {s}\n", .{object_path});
+ try stderr.flush();
+ return error.IPCFailed;
+ }
+
+ // Get a connection to the DBus session bus.
+ const dbus = dbus: {
+ var err_: ?*glib.Error = null;
+ defer if (err_) |err| err.free();
+
+ const dbus_ = gio.busGetSync(.session, null, &err_);
+ if (err_) |err| {
+ try stderr.print(
+ "Unable to establish connection to D-Bus session bus: {s}\n",
+ .{err.f_message orelse "(unknown)"},
+ );
+ try stderr.flush();
+ return error.IPCFailed;
+ }
+
+ break :dbus dbus_ orelse {
+ try stderr.print("gio.busGetSync returned null\n", .{});
+ try stderr.flush();
+ return error.IPCFailed;
+ };
+ };
+
+ // Set up the payload builder.
+ const payload_variant_type = glib.VariantType.new("(sava{sv})");
+ defer glib.free(payload_variant_type);
+
+ const payload_builder = glib.VariantBuilder.new(payload_variant_type);
+
+ // Add the action name to the payload.
+ {
+ const s_variant_type = glib.VariantType.new("s");
+ defer s_variant_type.free();
+
+ const bytes = glib.Bytes.new(action.ptr, action.len + 1);
+ defer bytes.unref();
+ const value = glib.Variant.newFromBytes(s_variant_type, bytes, @intFromBool(true));
+
+ payload_builder.addValue(value);
+ }
+
+ // Set up the parameter builder.
+ const parameters_variant_type = glib.VariantType.new("av");
+ defer parameters_variant_type.free();
+
+ const parameters_builder = glib.VariantBuilder.new(parameters_variant_type);
+
+ return .{
+ .target = target,
+ .dbus = dbus,
+ .bus_name = bus_name,
+ .object_path = object_path,
+ .payload_builder = payload_builder,
+ .parameters_builder = parameters_builder,
+ };
+}
+
+/// Add a parameter to the IPC call.
+pub fn addParameter(self: *Self, variant: *glib.Variant) void {
+ self.parameters_builder.add("v", variant);
+}
+
+/// Send the IPC to the remote Ghostty. Once it completes, nothing further
+/// should be done with this object other than call `deinit`.
+pub fn send(self: *Self) (std.Io.Writer.Error || apprt.ipc.Errors)!void {
+ var buf: [256]u8 = undefined;
+ var stderr_writer = std.fs.File.stderr().writer(&buf);
+ const stderr = &stderr_writer.interface;
+
+ // finish building the parameters
+ const parameters = self.parameters_builder.end();
+
+ // Add the parameters to the payload.
+ self.payload_builder.addValue(parameters);
+
+ // Add the platform data to the payload.
+ {
+ const platform_data_variant_type = glib.VariantType.new("a{sv}");
+ defer platform_data_variant_type.free();
+
+ self.payload_builder.open(platform_data_variant_type);
+ defer self.payload_builder.close();
+
+ // We have no platform data.
+ }
+
+ const payload = self.payload_builder.end();
+
+ {
+ var err_: ?*glib.Error = null;
+ defer if (err_) |err| err.free();
+
+ const result_ = self.dbus.callSync(
+ self.bus_name,
+ self.object_path,
+ "org.gtk.Actions",
+ "Activate",
+ payload,
+ null, // We don't care about the return type, we don't do anything with it.
+ .{}, // no flags
+ -1, // default timeout
+ null, // not cancellable
+ &err_,
+ );
+ defer if (result_) |result| result.unref();
+
+ if (err_) |err| {
+ try stderr.print(
+ "D-Bus method call returned an error err={s}\n",
+ .{err.f_message orelse "(unknown)"},
+ );
+ try stderr.flush();
+ return error.IPCFailed;
+ }
+ }
+}
+
+/// Free/unref any data held by this instance.
+pub fn deinit(self: *Self, alloc: Allocator) void {
+ switch (self.target) {
+ .class => alloc.free(self.object_path),
+ .detect => {},
+ }
+ self.parameters_builder.unref();
+ self.payload_builder.unref();
+ self.dbus.unref();
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/ipc/new_window.zig b/vendor/libghostty-vt/src/apprt/gtk/ipc/new_window.zig
new file mode 100644
index 00000000..02fed322
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/ipc/new_window.zig
@@ -0,0 +1,62 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+const glib = @import("glib");
+
+const apprt = @import("../../../apprt.zig");
+const DBus = @import("DBus.zig");
+
+// Use a D-Bus method call to open a new window on GTK.
+// See: https://wiki.gnome.org/Projects/GLib/GApplication/DBusAPI
+//
+// `ghostty +new-window` is equivalent to the following command (on a release build):
+//
+// ```
+// gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new-window [] []
+// ```
+//
+// `ghostty +new-window -e echo hello` would be equivalent to the following command (on a release build):
+//
+// ```
+// gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new-window-command '[<@as ["-e" "echo" "hello"]>]' []
+// ```
+pub fn newWindow(alloc: Allocator, target: apprt.ipc.Target, value: apprt.ipc.Action.NewWindow) (Allocator.Error || std.Io.Writer.Error || apprt.ipc.Errors)!bool {
+ var dbus = try DBus.init(
+ alloc,
+ target,
+ if (value.arguments == null)
+ "new-window"
+ else
+ "new-window-command",
+ );
+ defer dbus.deinit(alloc);
+
+ if (value.arguments) |arguments| {
+ // If any arguments were specified on the command line, the first
+ // parameter is an array of strings that contain the arguments. They
+ // will be sent to the main Ghostty instance and interpreted as CLI
+ // arguments.
+ const as_variant_type = glib.VariantType.new("as");
+ defer as_variant_type.free();
+
+ const s_variant_type = glib.VariantType.new("s");
+ defer s_variant_type.free();
+
+ var command: glib.VariantBuilder = undefined;
+ command.init(as_variant_type);
+ errdefer command.clear();
+
+ for (arguments) |argument| {
+ const bytes = glib.Bytes.new(argument.ptr, argument.len + 1);
+ defer bytes.unref();
+ const string = glib.Variant.newFromBytes(s_variant_type, bytes, @intFromBool(true));
+ command.addValue(string);
+ }
+
+ dbus.addParameter(command.end());
+ }
+
+ try dbus.send();
+
+ return true;
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/key.zig b/vendor/libghostty-vt/src/apprt/gtk/key.zig
new file mode 100644
index 00000000..5f717e14
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/key.zig
@@ -0,0 +1,535 @@
+const std = @import("std");
+
+const gdk = @import("gdk");
+const glib = @import("glib");
+const gtk = @import("gtk");
+
+const input = @import("../../input.zig");
+const winproto = @import("winproto.zig");
+
+/// Returns a GTK accelerator string from a trigger.
+pub fn accelFromTrigger(
+ buf: []u8,
+ trigger: input.Binding.Trigger,
+) error{WriteFailed}!?[:0]const u8 {
+ var writer: std.Io.Writer = .fixed(buf);
+
+ // Modifiers
+ if (trigger.mods.shift) try writer.writeAll("<Shift>");
+ if (trigger.mods.ctrl) try writer.writeAll("<Ctrl>");
+ if (trigger.mods.alt) try writer.writeAll("<Alt>");
+ if (trigger.mods.super) try writer.writeAll("<Super>");
+
+ // Write our key
+ if (!try writeTriggerKey(&writer, trigger)) return null;
+
+ // We need to make the string null terminated.
+ try writer.writeByte(0);
+ const slice = writer.buffered();
+ return slice[0 .. slice.len - 1 :0];
+}
+
+/// Returns a XDG-compliant shortcuts string from a trigger.
+/// Spec: https://specifications.freedesktop.org/shortcuts-spec/latest/
+pub fn xdgShortcutFromTrigger(
+ buf: []u8,
+ trigger: input.Binding.Trigger,
+) error{WriteFailed}!?[:0]const u8 {
+ var writer: std.Io.Writer = .fixed(buf);
+
+ // Modifiers
+ if (trigger.mods.shift) try writer.writeAll("SHIFT+");
+ if (trigger.mods.ctrl) try writer.writeAll("CTRL+");
+ if (trigger.mods.alt) try writer.writeAll("ALT+");
+ if (trigger.mods.super) try writer.writeAll("LOGO+");
+
+ // Write our key
+ // NOTE: While the spec specifies that only libxkbcommon keysyms are
+ // expected, using GTK's keysyms should still work as they are identical
+ // to *X11's* keysyms (which I assume is a subset of libxkbcommon's).
+ // I haven't been able to any evidence to back up that assumption but
+ // this works for now
+ if (!try writeTriggerKey(&writer, trigger)) return null;
+
+ // We need to make the string null terminated.
+ try writer.writeByte(0);
+ const slice = writer.buffered();
+ return slice[0 .. slice.len - 1 :0];
+}
+
+fn writeTriggerKey(
+ writer: *std.Io.Writer,
+ trigger: input.Binding.Trigger,
+) error{WriteFailed}!bool {
+ switch (trigger.key) {
+ .physical => |k| {
+ const keyval = keyvalFromKey(k) orelse return false;
+ try writer.writeAll(std.mem.span(gdk.keyvalName(keyval) orelse return false));
+ },
+
+ .unicode => |cp| {
+ if (gdk.keyvalName(cp)) |name| {
+ try writer.writeAll(std.mem.span(name));
+ } else {
+ try writer.print("{u}", .{cp});
+ }
+ },
+
+ .catch_all => return false,
+ }
+
+ return true;
+}
+
+pub fn translateMods(state: gdk.ModifierType) input.Mods {
+ return .{
+ .shift = state.shift_mask,
+ .ctrl = state.control_mask,
+ .alt = state.alt_mask,
+ .super = state.super_mask,
+ // Lock is dependent on the X settings but we just assume caps lock.
+ .caps_lock = state.lock_mask,
+ };
+}
+
+// Get the unshifted unicode value of the keyval. This is used
+// by the Kitty keyboard protocol.
+pub fn keyvalUnicodeUnshifted(
+ widget: *gtk.Widget,
+ event: *gdk.KeyEvent,
+ keycode: u32,
+) u21 {
+ const display = widget.getDisplay();
+
+ // We need to get the currently active keyboard layout so we know
+ // what group to look at.
+ const layout = event.getLayout();
+
+ // Get all the possible keyboard mappings for this keycode. A keycode is the
+ // physical key pressed.
+ var keys: [*]gdk.KeymapKey = undefined;
+ var keyvals: [*]c_uint = undefined;
+ var n_entries: c_int = 0;
+ if (display.mapKeycode(keycode, &keys, &keyvals, &n_entries) == 0) return 0;
+
+ defer glib.free(keys);
+ defer glib.free(keyvals);
+
+ // debugging:
+ // std.log.debug("layout={}", .{layout});
+ // for (0..@intCast(n_entries)) |i| {
+ // std.log.debug("keymap key={} codepoint={x}", .{
+ // keys[i],
+ // gdk.keyvalToUnicode(keyvals[i]),
+ // });
+ // }
+
+ for (0..@intCast(n_entries)) |i| {
+ if (keys[i].f_group == layout and
+ keys[i].f_level == 0)
+ {
+ return std.math.cast(
+ u21,
+ gdk.keyvalToUnicode(keyvals[i]),
+ ) orelse 0;
+ }
+ }
+
+ return 0;
+}
+
+/// Returns the mods to use a key event from a GTK event.
+/// This requires a lot of context because the GdkEvent
+/// doesn't contain enough on its own.
+pub fn eventMods(
+ event: *gdk.Event,
+ physical_key: input.Key,
+ gtk_mods: gdk.ModifierType,
+ action: input.Action,
+ app_winproto: *winproto.App,
+) input.Mods {
+ const device = event.getDevice();
+
+ var mods = app_winproto.eventMods(device, gtk_mods);
+ mods.num_lock = if (device) |d| d.getNumLockState() != 0 else false;
+
+ // We use the physical key to determine sided modifiers. As
+ // far as I can tell there's no other way to reliably determine
+ // this.
+ //
+ // We also set the main modifier to true if either side is true,
+ // since on both X11/Wayland, GTK doesn't set the main modifier
+ // if only the modifier key is pressed, but our core logic
+ // relies on it.
+ switch (physical_key) {
+ .shift_left => {
+ mods.shift = action != .release;
+ mods.sides.shift = .left;
+ },
+
+ .shift_right => {
+ mods.shift = action != .release;
+ mods.sides.shift = .right;
+ },
+
+ .control_left => {
+ mods.ctrl = action != .release;
+ mods.sides.ctrl = .left;
+ },
+
+ .control_right => {
+ mods.ctrl = action != .release;
+ mods.sides.ctrl = .right;
+ },
+
+ .alt_left => {
+ mods.alt = action != .release;
+ mods.sides.alt = .left;
+ },
+
+ .alt_right => {
+ mods.alt = action != .release;
+ mods.sides.alt = .right;
+ },
+
+ .meta_left => {
+ mods.super = action != .release;
+ mods.sides.super = .left;
+ },
+
+ .meta_right => {
+ mods.super = action != .release;
+ mods.sides.super = .right;
+ },
+
+ else => {},
+ }
+
+ return mods;
+}
+
+/// Returns an input key from a keyval or null if we don't have a mapping.
+pub fn keyFromKeyval(keyval: c_uint) ?input.Key {
+ for (keymap) |entry| {
+ if (entry[0] == keyval) return entry[1];
+ }
+
+ return null;
+}
+
+/// Returns a keyval from an input key or null if we don't have a mapping.
+pub fn keyvalFromKey(key: input.Key) ?c_uint {
+ switch (key) {
+ inline else => |key_comptime| {
+ return comptime value: {
+ @setEvalBranchQuota(50_000);
+ for (keymap) |entry| {
+ if (entry[1] == key_comptime) break :value entry[0];
+ }
+
+ break :value null;
+ };
+ },
+ }
+}
+
+/// Converts a trigger to a human-readable label for display in UI.
+///
+/// Uses GTK accelerator-style formatting (e.g., "Ctrl+Shift+A").
+/// Returns false if the trigger cannot be formatted (e.g., catch_all).
+pub fn labelFromTrigger(
+ writer: *std.Io.Writer,
+ trigger: input.Binding.Trigger,
+) std.Io.Writer.Error!bool {
+ // Modifiers first, using human-readable format
+ if (trigger.mods.super) try writer.writeAll("Super+");
+ if (trigger.mods.ctrl) try writer.writeAll("Ctrl+");
+ if (trigger.mods.alt) try writer.writeAll("Alt+");
+ if (trigger.mods.shift) try writer.writeAll("Shift+");
+
+ // Write the key
+ return writeTriggerKeyLabel(writer, trigger);
+}
+
+/// Writes the key portion of a trigger in human-readable format.
+fn writeTriggerKeyLabel(
+ writer: *std.Io.Writer,
+ trigger: input.Binding.Trigger,
+) error{WriteFailed}!bool {
+ switch (trigger.key) {
+ .physical => |k| {
+ const keyval = keyvalFromKey(k) orelse return false;
+ const name = gdk.keyvalName(keyval) orelse return false;
+ // Capitalize the first letter for nicer display
+ const span = std.mem.span(name);
+ if (span.len > 0) {
+ if (span[0] >= 'a' and span[0] <= 'z') {
+ try writer.writeByte(span[0] - 'a' + 'A');
+ if (span.len > 1) try writer.writeAll(span[1..]);
+ } else {
+ try writer.writeAll(span);
+ }
+ }
+ },
+
+ .unicode => |cp| {
+ // Try to get a nice name from GDK first
+ if (gdk.keyvalName(cp)) |name| {
+ const span = std.mem.span(name);
+ if (span.len > 0) {
+ // Capitalize the first letter for nicer display
+ if (span[0] >= 'a' and span[0] <= 'z') {
+ try writer.writeByte(span[0] - 'a' + 'A');
+ if (span.len > 1) try writer.writeAll(span[1..]);
+ } else {
+ try writer.writeAll(span);
+ }
+ }
+ } else {
+ // Fall back to printing the character
+ try writer.print("{u}", .{cp});
+ }
+ },
+
+ .catch_all => return false,
+ }
+
+ return true;
+}
+
+test "accelFromTrigger" {
+ const testing = std.testing;
+ var buf: [256]u8 = undefined;
+
+ try testing.expectEqualStrings("<Super>q", (try accelFromTrigger(&buf, .{
+ .mods = .{ .super = true },
+ .key = .{ .unicode = 'q' },
+ })).?);
+
+ try testing.expectEqualStrings("<Shift><Ctrl><Alt><Super>backslash", (try accelFromTrigger(&buf, .{
+ .mods = .{ .ctrl = true, .alt = true, .super = true, .shift = true },
+ .key = .{ .unicode = 92 },
+ })).?);
+}
+
+test "xdgShortcutFromTrigger" {
+ const testing = std.testing;
+ var buf: [256]u8 = undefined;
+
+ try testing.expectEqualStrings("LOGO+q", (try xdgShortcutFromTrigger(&buf, .{
+ .mods = .{ .super = true },
+ .key = .{ .unicode = 'q' },
+ })).?);
+
+ try testing.expectEqualStrings("SHIFT+CTRL+ALT+LOGO+backslash", (try xdgShortcutFromTrigger(&buf, .{
+ .mods = .{ .ctrl = true, .alt = true, .super = true, .shift = true },
+ .key = .{ .unicode = 92 },
+ })).?);
+}
+
+test "labelFromTrigger" {
+ const testing = std.testing;
+
+ // Simple unicode key with modifier
+ {
+ var buf: std.Io.Writer.Allocating = .init(testing.allocator);
+ defer buf.deinit();
+ try testing.expect(try labelFromTrigger(&buf.writer, .{
+ .mods = .{ .super = true },
+ .key = .{ .unicode = 'q' },
+ }));
+ try testing.expectEqualStrings("Super+Q", buf.written());
+ }
+
+ // Multiple modifiers
+ {
+ var buf: std.Io.Writer.Allocating = .init(testing.allocator);
+ defer buf.deinit();
+ try testing.expect(try labelFromTrigger(&buf.writer, .{
+ .mods = .{ .ctrl = true, .alt = true, .super = true, .shift = true },
+ .key = .{ .unicode = 92 },
+ }));
+ try testing.expectEqualStrings("Super+Ctrl+Alt+Shift+Backslash", buf.written());
+ }
+
+ // Physical key
+ {
+ var buf: std.Io.Writer.Allocating = .init(testing.allocator);
+ defer buf.deinit();
+ try testing.expect(try labelFromTrigger(&buf.writer, .{
+ .mods = .{ .ctrl = true },
+ .key = .{ .physical = .key_a },
+ }));
+ try testing.expectEqualStrings("Ctrl+A", buf.written());
+ }
+
+ // No modifiers
+ {
+ var buf: std.Io.Writer.Allocating = .init(testing.allocator);
+ defer buf.deinit();
+ try testing.expect(try labelFromTrigger(&buf.writer, .{
+ .mods = .{},
+ .key = .{ .physical = .escape },
+ }));
+ try testing.expectEqualStrings("Escape", buf.written());
+ }
+
+ // catch_all returns false
+ {
+ var buf: std.Io.Writer.Allocating = .init(testing.allocator);
+ defer buf.deinit();
+ try testing.expect(!try labelFromTrigger(&buf.writer, .{
+ .mods = .{},
+ .key = .catch_all,
+ }));
+ }
+}
+
+/// A raw entry in the keymap. Our keymap contains mappings between
+/// GDK keys and our own key enum.
+const RawEntry = struct { c_uint, input.Key };
+
+const keymap: []const RawEntry = &.{
+ .{ gdk.KEY_a, .key_a },
+ .{ gdk.KEY_b, .key_b },
+ .{ gdk.KEY_c, .key_c },
+ .{ gdk.KEY_d, .key_d },
+ .{ gdk.KEY_e, .key_e },
+ .{ gdk.KEY_f, .key_f },
+ .{ gdk.KEY_g, .key_g },
+ .{ gdk.KEY_h, .key_h },
+ .{ gdk.KEY_i, .key_i },
+ .{ gdk.KEY_j, .key_j },
+ .{ gdk.KEY_k, .key_k },
+ .{ gdk.KEY_l, .key_l },
+ .{ gdk.KEY_m, .key_m },
+ .{ gdk.KEY_n, .key_n },
+ .{ gdk.KEY_o, .key_o },
+ .{ gdk.KEY_p, .key_p },
+ .{ gdk.KEY_q, .key_q },
+ .{ gdk.KEY_r, .key_r },
+ .{ gdk.KEY_s, .key_s },
+ .{ gdk.KEY_t, .key_t },
+ .{ gdk.KEY_u, .key_u },
+ .{ gdk.KEY_v, .key_v },
+ .{ gdk.KEY_w, .key_w },
+ .{ gdk.KEY_x, .key_x },
+ .{ gdk.KEY_y, .key_y },
+ .{ gdk.KEY_z, .key_z },
+
+ .{ gdk.KEY_0, .digit_0 },
+ .{ gdk.KEY_1, .digit_1 },
+ .{ gdk.KEY_2, .digit_2 },
+ .{ gdk.KEY_3, .digit_3 },
+ .{ gdk.KEY_4, .digit_4 },
+ .{ gdk.KEY_5, .digit_5 },
+ .{ gdk.KEY_6, .digit_6 },
+ .{ gdk.KEY_7, .digit_7 },
+ .{ gdk.KEY_8, .digit_8 },
+ .{ gdk.KEY_9, .digit_9 },
+
+ .{ gdk.KEY_semicolon, .semicolon },
+ .{ gdk.KEY_space, .space },
+ .{ gdk.KEY_apostrophe, .quote },
+ .{ gdk.KEY_comma, .comma },
+ .{ gdk.KEY_grave, .backquote },
+ .{ gdk.KEY_period, .period },
+ .{ gdk.KEY_slash, .slash },
+ .{ gdk.KEY_minus, .minus },
+ .{ gdk.KEY_equal, .equal },
+ .{ gdk.KEY_bracketleft, .bracket_left },
+ .{ gdk.KEY_bracketright, .bracket_right },
+ .{ gdk.KEY_backslash, .backslash },
+
+ .{ gdk.KEY_Up, .arrow_up },
+ .{ gdk.KEY_Down, .arrow_down },
+ .{ gdk.KEY_Right, .arrow_right },
+ .{ gdk.KEY_Left, .arrow_left },
+ .{ gdk.KEY_Home, .home },
+ .{ gdk.KEY_End, .end },
+ .{ gdk.KEY_Insert, .insert },
+ .{ gdk.KEY_Delete, .delete },
+ .{ gdk.KEY_Caps_Lock, .caps_lock },
+ .{ gdk.KEY_Scroll_Lock, .scroll_lock },
+ .{ gdk.KEY_Num_Lock, .num_lock },
+ .{ gdk.KEY_Page_Up, .page_up },
+ .{ gdk.KEY_Page_Down, .page_down },
+ .{ gdk.KEY_Escape, .escape },
+ .{ gdk.KEY_Return, .enter },
+ .{ gdk.KEY_Tab, .tab },
+ .{ gdk.KEY_BackSpace, .backspace },
+ .{ gdk.KEY_Print, .print_screen },
+ .{ gdk.KEY_Pause, .pause },
+
+ .{ gdk.KEY_F1, .f1 },
+ .{ gdk.KEY_F2, .f2 },
+ .{ gdk.KEY_F3, .f3 },
+ .{ gdk.KEY_F4, .f4 },
+ .{ gdk.KEY_F5, .f5 },
+ .{ gdk.KEY_F6, .f6 },
+ .{ gdk.KEY_F7, .f7 },
+ .{ gdk.KEY_F8, .f8 },
+ .{ gdk.KEY_F9, .f9 },
+ .{ gdk.KEY_F10, .f10 },
+ .{ gdk.KEY_F11, .f11 },
+ .{ gdk.KEY_F12, .f12 },
+ .{ gdk.KEY_F13, .f13 },
+ .{ gdk.KEY_F14, .f14 },
+ .{ gdk.KEY_F15, .f15 },
+ .{ gdk.KEY_F16, .f16 },
+ .{ gdk.KEY_F17, .f17 },
+ .{ gdk.KEY_F18, .f18 },
+ .{ gdk.KEY_F19, .f19 },
+ .{ gdk.KEY_F20, .f20 },
+ .{ gdk.KEY_F21, .f21 },
+ .{ gdk.KEY_F22, .f22 },
+ .{ gdk.KEY_F23, .f23 },
+ .{ gdk.KEY_F24, .f24 },
+ .{ gdk.KEY_F25, .f25 },
+
+ .{ gdk.KEY_KP_0, .numpad_0 },
+ .{ gdk.KEY_KP_1, .numpad_1 },
+ .{ gdk.KEY_KP_2, .numpad_2 },
+ .{ gdk.KEY_KP_3, .numpad_3 },
+ .{ gdk.KEY_KP_4, .numpad_4 },
+ .{ gdk.KEY_KP_5, .numpad_5 },
+ .{ gdk.KEY_KP_6, .numpad_6 },
+ .{ gdk.KEY_KP_7, .numpad_7 },
+ .{ gdk.KEY_KP_8, .numpad_8 },
+ .{ gdk.KEY_KP_9, .numpad_9 },
+ .{ gdk.KEY_KP_Decimal, .numpad_decimal },
+ .{ gdk.KEY_KP_Divide, .numpad_divide },
+ .{ gdk.KEY_KP_Multiply, .numpad_multiply },
+ .{ gdk.KEY_KP_Subtract, .numpad_subtract },
+ .{ gdk.KEY_KP_Add, .numpad_add },
+ .{ gdk.KEY_KP_Enter, .numpad_enter },
+ .{ gdk.KEY_KP_Equal, .numpad_equal },
+
+ .{ gdk.KEY_KP_Separator, .numpad_separator },
+ .{ gdk.KEY_KP_Left, .numpad_left },
+ .{ gdk.KEY_KP_Right, .numpad_right },
+ .{ gdk.KEY_KP_Up, .numpad_up },
+ .{ gdk.KEY_KP_Down, .numpad_down },
+ .{ gdk.KEY_KP_Page_Up, .numpad_page_up },
+ .{ gdk.KEY_KP_Page_Down, .numpad_page_down },
+ .{ gdk.KEY_KP_Home, .numpad_home },
+ .{ gdk.KEY_KP_End, .numpad_end },
+ .{ gdk.KEY_KP_Insert, .numpad_insert },
+ .{ gdk.KEY_KP_Delete, .numpad_delete },
+ .{ gdk.KEY_KP_Begin, .numpad_begin },
+
+ .{ gdk.KEY_Copy, .copy },
+ .{ gdk.KEY_Cut, .cut },
+ .{ gdk.KEY_Paste, .paste },
+
+ .{ gdk.KEY_Shift_L, .shift_left },
+ .{ gdk.KEY_Control_L, .control_left },
+ .{ gdk.KEY_Alt_L, .alt_left },
+ .{ gdk.KEY_Super_L, .meta_left },
+ .{ gdk.KEY_Shift_R, .shift_right },
+ .{ gdk.KEY_Control_R, .control_right },
+ .{ gdk.KEY_Alt_R, .alt_right },
+ .{ gdk.KEY_Super_R, .meta_right },
+
+ // TODO: media keys
+};
diff --git a/vendor/libghostty-vt/src/apprt/gtk/media.zig b/vendor/libghostty-vt/src/apprt/gtk/media.zig
new file mode 100644
index 00000000..1015c933
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/media.zig
@@ -0,0 +1,102 @@
+const std = @import("std");
+const assert = @import("../../quirks.zig").inlineAssert;
+
+const log = std.log.scoped(.gtk_media);
+
+const gio = @import("gio");
+const glib = @import("glib");
+const gobject = @import("gobject");
+const gtk = @import("gtk");
+
+pub fn fromFilename(path: [:0]const u8) ?*gtk.MediaFile {
+ assert(std.fs.path.isAbsolute(path));
+ std.fs.accessAbsolute(path, .{ .mode = .read_only }) catch |err| {
+ log.warn("unable to access {s}: {t}", .{ path, err });
+ return null;
+ };
+ return gtk.MediaFile.newForFilename(path);
+}
+
+pub fn fromResource(path: [:0]const u8) ?*gtk.MediaFile {
+ assert(std.fs.path.isAbsolute(path));
+ var gerr: ?*glib.Error = null;
+
+ const found = gio.resourcesGetInfo(path, .{}, null, null, &gerr);
+ if (gerr) |err| {
+ defer err.free();
+ log.warn(
+ "failed to find resource {s}: {s} {d} {s}",
+ .{
+ path,
+ glib.quarkToString(err.f_domain),
+ err.f_code,
+ err.f_message orelse "(no message)",
+ },
+ );
+ return null;
+ }
+
+ if (found == 0) {
+ log.warn("failed to find resource {s}", .{path});
+ return null;
+ }
+
+ return gtk.MediaFile.newForResource(path);
+}
+
+pub fn playMediaFile(media_file: *gtk.MediaFile, volume: f64, required: bool) void {
+ // If the audio file is marked as required, we'll emit an error if
+ // there was a problem playing it. Otherwise there will be silence.
+ if (required) {
+ _ = gobject.Object.signals.notify.connect(
+ media_file,
+ ?*anyopaque,
+ mediaFileError,
+ null,
+ .{ .detail = "error" },
+ );
+ }
+
+ // Watch for the "ended" signal so that we can clean up after
+ // ourselves.
+ _ = gobject.Object.signals.notify.connect(
+ media_file,
+ ?*anyopaque,
+ mediaFileEnded,
+ null,
+ .{ .detail = "ended" },
+ );
+
+ const media_stream = media_file.as(gtk.MediaStream);
+ media_stream.setVolume(volume);
+ media_stream.play();
+}
+
+fn mediaFileError(
+ media_file: *gtk.MediaFile,
+ _: *gobject.ParamSpec,
+ _: ?*anyopaque,
+) callconv(.c) void {
+ const path = path: {
+ const file = media_file.getFile() orelse break :path null;
+ break :path file.getPath();
+ };
+ defer if (path) |p| glib.free(p);
+
+ const media_stream = media_file.as(gtk.MediaStream);
+ const err = media_stream.getError() orelse return;
+ log.warn("error playing sound from {s}: {s} {d} {s}", .{
+ path orelse "<<unknown>>",
+ glib.quarkToString(err.f_domain),
+ err.f_code,
+ err.f_message orelse "",
+ });
+}
+
+fn mediaFileEnded(
+ media_file: *gtk.MediaFile,
+ _: *gobject.ParamSpec,
+ _: ?*anyopaque,
+) callconv(.c) void {
+ media_file.unref();
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/portal.zig b/vendor/libghostty-vt/src/apprt/gtk/portal.zig
new file mode 100644
index 00000000..3e51042d
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/portal.zig
@@ -0,0 +1,105 @@
+const std = @import("std");
+
+const gio = @import("gio");
+
+const Allocator = std.mem.Allocator;
+
+pub const OpenURI = @import("portal/OpenURI.zig");
+pub const token_hex_len = @sizeOf(usize) * 2;
+pub const TokenBuffer = [token_hex_len + 1]u8;
+const token_format = std.fmt.comptimePrint("{{x:0>{}}}", .{token_hex_len});
+
+/// Generate a token suitable for use in requests to the XDG Desktop Portal
+pub fn generateToken() usize {
+ return std.crypto.random.int(usize);
+}
+
+/// Format a request token consistently for use in portal object paths and payloads.
+pub fn formatToken(buf: *TokenBuffer, token: usize) [:0]const u8 {
+ return std.fmt.bufPrintZ(buf, token_format, .{token}) catch unreachable;
+}
+
+/// Get the XDG portal request path for the current Ghostty instance.
+///
+/// See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html
+/// for the protocol of the Request interface.
+pub fn getRequestPath(alloc: Allocator, dbus: *gio.DBusConnection, token: usize) (Allocator.Error || error{NoDBusUniqueName})![:0]const u8 {
+ // Get the unique name from D-Bus and strip the leading `:`
+ const unique_name = std.mem.span(
+ dbus.getUniqueName() orelse {
+ return error.NoDBusUniqueName;
+ },
+ )[1..];
+
+ return buildRequestPath(alloc, unique_name, token);
+}
+
+/// Build the XDG portal request path for given unique name and token.
+fn buildRequestPath(alloc: Allocator, unique_name: []const u8, token: usize) Allocator.Error![:0]const u8 {
+ var token_buf: TokenBuffer = undefined;
+ const token_string = formatToken(&token_buf, token);
+
+ const object_path = try std.mem.joinZ(
+ alloc,
+ "/",
+ &.{
+ "/org/freedesktop/portal/desktop/request",
+ unique_name,
+ token_string,
+ },
+ );
+
+ // Sanitize the unique name by replacing every `.` with `_`. In effect, this
+ // will turn a unique name like `1.192` into `1_192`.
+ // This sounds arbitrary, but it's part of the Request protocol.
+ _ = std.mem.replaceScalar(u8, object_path, '.', '_');
+
+ return object_path;
+}
+
+/// Try and parse the token out of a request path.
+pub fn parseRequestPathToken(request_path: []const u8) ?usize {
+ const index = std.mem.lastIndexOfScalar(u8, request_path, '/') orelse return null;
+ const token = request_path[index + 1 ..];
+ return std.fmt.parseUnsigned(usize, token, 16) catch return null;
+}
+
+test "formatToken pads to fixed width" {
+ const testing = std.testing;
+
+ var token_buf: TokenBuffer = undefined;
+ const token = formatToken(&token_buf, 0x42);
+
+ try testing.expectEqual(@as(usize, token_hex_len), token.len);
+ try testing.expectEqualStrings("0000000000000042", token);
+}
+
+test "buildRequestPath" {
+ const testing = std.testing;
+
+ const path = try buildRequestPath(testing.allocator, "1.42", 0x75af01a79c6fea34);
+ try testing.expectEqualStrings(
+ "/org/freedesktop/portal/desktop/request/1_42/75af01a79c6fea34",
+ path,
+ );
+ testing.allocator.free(path);
+}
+
+test "buildRequestPath pads token" {
+ const testing = std.testing;
+ const path = try buildRequestPath(testing.allocator, "1.42", 0x42);
+
+ try testing.expectEqualStrings(
+ "/org/freedesktop/portal/desktop/request/1_42/0000000000000042",
+ path,
+ );
+ testing.allocator.free(path);
+}
+
+test "parseRequestPathToken" {
+ const testing = std.testing;
+
+ try testing.expectEqual(0x75af01a79c6fea34, parseRequestPathToken("/org/freedesktop/portal/desktop/request/1_42/75af01a79c6fea34").?);
+ try testing.expectEqual(null, parseRequestPathToken("/org/freedesktop/portal/desktop/request/1_42/75af01a79c6fGa34"));
+ try testing.expectEqual(null, parseRequestPathToken("75af01a79c6fea34"));
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/portal/OpenURI.zig b/vendor/libghostty-vt/src/apprt/gtk/portal/OpenURI.zig
new file mode 100644
index 00000000..97aa013e
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/portal/OpenURI.zig
@@ -0,0 +1,565 @@
+//! Use DBus to call the XDG Desktop Portal to open an URI.
+//! See: https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.OpenURI.html#org-freedesktop-portal-openuri-openuri
+const OpenURI = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const assert = std.debug.assert;
+
+const gio = @import("gio");
+const glib = @import("glib");
+const gobject = @import("gobject");
+
+const App = @import("../App.zig");
+const portal = @import("../portal.zig");
+const apprt = @import("../../../apprt.zig");
+
+const log = std.log.scoped(.openuri);
+
+/// The GTK app that we "belong" to.
+app: *App,
+
+/// Connection to the D-Bus session bus that we'll use for all of our messaging.
+dbus: ?*gio.DBusConnection = null,
+
+/// Mutex to protect modification of the entries map or the cleanup timer.
+mutex: std.Thread.Mutex = .{},
+
+/// Map to store data about any in-flight calls to the portal.
+entries: std.AutoArrayHashMapUnmanaged(usize, *Entry) = .empty,
+
+/// Used to manage a timer to clean up any orphan entries in the map.
+cleanup_timer: ?c_uint = null,
+
+/// Set to false during shutdown so callbacks stop touching internal state.
+alive: bool = true,
+
+const RequestData = struct {
+ open_uri: *OpenURI,
+ token: usize,
+ kind: apprt.action.OpenUrl.Kind,
+ uri: [:0]const u8,
+ request_path: [:0]const u8,
+
+ pub fn init(
+ alloc: Allocator,
+ open_uri: *OpenURI,
+ token: usize,
+ kind: apprt.action.OpenUrl.Kind,
+ uri: []const u8,
+ request_path: []const u8,
+ ) Allocator.Error!*RequestData {
+ const uri_copy = try alloc.dupeZ(u8, uri);
+ errdefer alloc.free(uri_copy);
+
+ const request_path_copy = try alloc.dupeZ(u8, request_path);
+ errdefer alloc.free(request_path_copy);
+
+ const data = try alloc.create(RequestData);
+ errdefer alloc.destroy(data);
+
+ data.* = .{
+ .open_uri = open_uri,
+ .token = token,
+ .kind = kind,
+ .uri = uri_copy,
+ .request_path = request_path_copy,
+ };
+
+ return data;
+ }
+
+ pub fn deinit(self: *const RequestData, alloc: Allocator) void {
+ alloc.free(self.uri);
+ alloc.free(self.request_path);
+ }
+};
+
+/// Data about any in-flight calls to the portal.
+pub const Entry = struct {
+ /// When the request started.
+ start: std.time.Instant,
+ /// A token used by the portal to identify requests and responses. The
+ /// actual format of the token does not really matter as long as it can be
+ /// used as part of a D-Bus object path. `usize` was chosen since it's easy
+ /// to hash and to generate random tokens.
+ token: usize,
+ /// The "kind" of URI. Unused here, but we may need to pass it on to the
+ /// fallback URL opener if the D-Bus method fails.
+ kind: apprt.action.OpenUrl.Kind,
+ /// A copy of the URI that we are opening. We need our own copy since the
+ /// method calls are asynchronous and the original may have been freed by
+ /// the time we need it.
+ uri: [:0]const u8,
+ /// Used to manage a subscription to a D-Bus signal, which is how the XDG
+ /// Portal reports results of the method call.
+ subscription: ?c_uint = null,
+
+ pub fn deinit(self: *const Entry, alloc: Allocator) void {
+ alloc.free(self.uri);
+ }
+};
+
+pub const Errors = error{
+ /// Could not get a D-Bus connection
+ DBusConnectionRequired,
+ /// The D-Bus connection did not have a unique name. This _should_ be
+ /// impossible, but is handled for safety's sake.
+ NoDBusUniqueName,
+ /// The system was unable to give us the time.
+ TimerUnavailable,
+};
+
+pub fn init(app: *App) OpenURI {
+ return .{
+ .app = app,
+ };
+}
+
+pub fn setDbusConnection(self: *OpenURI, dbus: ?*gio.DBusConnection) void {
+ self.dbus = dbus;
+}
+
+pub fn deinit(self: *OpenURI) void {
+ const alloc = self.app.app.allocator();
+
+ self.mutex.lock();
+ defer self.mutex.unlock();
+
+ if (!self.alive) return;
+ self.alive = false;
+
+ self.stopCleanupTimer();
+
+ for (self.entries.entries.items(.value)) |entry| {
+ self.unsubscribeFromResponse(entry);
+ destroyEntry(alloc, entry);
+ }
+
+ self.entries.deinit(alloc);
+ self.entries = .empty;
+ self.dbus = null;
+}
+
+/// Send the D-Bus method call to the XDG Desktop portal. The result of the
+/// method call will be reported asynchronously.
+pub fn start(self: *OpenURI, value: apprt.action.OpenUrl) (Allocator.Error || Errors)!void {
+ const alloc = self.app.app.allocator();
+ const dbus = self.dbus orelse return error.DBusConnectionRequired;
+
+ const token = portal.generateToken();
+ const request_path = try portal.getRequestPath(alloc, dbus, token);
+ defer alloc.free(request_path);
+
+ const request = try RequestData.init(alloc, self, token, value.kind, value.url, request_path);
+ errdefer {
+ request.deinit(alloc);
+ alloc.destroy(request);
+ }
+
+ self.mutex.lock();
+ defer self.mutex.unlock();
+
+ // Create an entry that is used to track the results of the D-Bus method
+ // call.
+ const entry = entry: {
+ const entry = try alloc.create(Entry);
+ errdefer alloc.destroy(entry);
+ entry.* = .{
+ .start = std.time.Instant.now() catch return error.TimerUnavailable,
+ .token = token,
+ .kind = value.kind,
+ .uri = try alloc.dupeZ(u8, value.url),
+ };
+ errdefer entry.deinit(alloc);
+ try self.entries.putNoClobber(alloc, token, entry);
+ break :entry entry;
+ };
+
+ errdefer {
+ _ = self.entries.swapRemove(token);
+ destroyEntry(alloc, entry);
+ }
+
+ self.startCleanupTimer();
+ self.subscribeToResponse(entry, dbus, request_path.ptr);
+ self.sendRequest(entry, dbus, request);
+}
+
+/// Subscribe to the D-Bus signal that will contain the results of our method
+/// call to the portal. This must be called with the mutex locked.
+fn subscribeToResponse(
+ self: *OpenURI,
+ entry: *Entry,
+ dbus: *gio.DBusConnection,
+ request_path: [*:0]const u8,
+) void {
+ assert(!self.mutex.tryLock());
+
+ if (entry.subscription != null) return;
+
+ entry.subscription = dbus.signalSubscribe(
+ null,
+ "org.freedesktop.portal.Request",
+ "Response",
+ request_path,
+ null,
+ .{},
+ responseReceived,
+ self,
+ null,
+ );
+}
+
+/// Unsubscribe to the D-Bus signal that contains the result of the method call.
+/// This will prevent a response from being processed multiple times. This must
+/// be called when the mutex is locked.
+fn unsubscribeFromResponse(self: *OpenURI, entry: *Entry) void {
+ assert(!self.mutex.tryLock());
+
+ // Unsubscribe from the response signal
+ if (entry.subscription) |subscription| {
+ const dbus = self.dbus orelse {
+ entry.subscription = null;
+ log.warn("unable to unsubscribe open uri response without dbus connection", .{});
+ return;
+ };
+ dbus.signalUnsubscribe(subscription);
+ entry.subscription = null;
+ }
+}
+
+fn destroyEntry(alloc: Allocator, entry: *Entry) void {
+ entry.deinit(alloc);
+ alloc.destroy(entry);
+}
+
+fn failRequest(self: *OpenURI, token: usize) ?*Entry {
+ self.mutex.lock();
+ defer self.mutex.unlock();
+
+ if (!self.alive) return null;
+
+ const entry = (self.entries.fetchSwapRemove(token) orelse return null).value;
+ self.unsubscribeFromResponse(entry);
+ return entry;
+}
+
+fn failRequestAndFallback(self: *OpenURI, request: *const RequestData) void {
+ const alloc = self.app.app.allocator();
+ const entry = self.failRequest(request.token) orelse return;
+ defer destroyEntry(alloc, entry);
+
+ self.app.app.openUrlFallback(request.kind, request.uri);
+}
+
+/// Send the D-Bus method call to the portal. The mutex must be locked when this
+/// is called.
+fn sendRequest(
+ self: *OpenURI,
+ entry: *Entry,
+ dbus: *gio.DBusConnection,
+ request: *RequestData,
+) void {
+ assert(!self.mutex.tryLock());
+
+ const payload = payload: {
+ const builder_type = glib.VariantType.new("(ssa{sv})");
+ defer builder_type.free();
+
+ // Initialize our builder to build up our parameters
+ var builder: glib.VariantBuilder = undefined;
+ builder.init(builder_type);
+
+ // parent window - empty string means we have no window
+ builder.add("s", "");
+
+ // URI to open
+ builder.add("s", entry.uri.ptr);
+
+ // Options
+ {
+ const options = glib.VariantType.new("a{sv}");
+ defer options.free();
+
+ builder.open(options);
+ defer builder.close();
+
+ {
+ const option = glib.VariantType.new("{sv}");
+ defer option.free();
+
+ builder.open(option);
+ defer builder.close();
+
+ builder.add("s", "handle_token");
+
+ var token_buf: portal.TokenBuffer = undefined;
+ const token = portal.formatToken(&token_buf, entry.token);
+
+ const handle_token = glib.Variant.newString(token.ptr);
+ builder.add("v", handle_token);
+ }
+ {
+ const option = glib.VariantType.new("{sv}");
+ defer option.free();
+
+ builder.open(option);
+ defer builder.close();
+
+ builder.add("s", "writable");
+
+ const writable = glib.Variant.newBoolean(@intFromBool(false));
+ builder.add("v", writable);
+ }
+ {
+ const option = glib.VariantType.new("{sv}");
+ defer option.free();
+
+ builder.open(option);
+ defer builder.close();
+
+ builder.add("s", "ask");
+
+ const ask = glib.Variant.newBoolean(@intFromBool(false));
+ builder.add("v", ask);
+ }
+ }
+
+ break :payload builder.end();
+ };
+
+ // We're expecting an object path back from the method call.
+ const reply_type = glib.VariantType.new("(o)");
+ defer reply_type.free();
+
+ dbus.call(
+ "org.freedesktop.portal.Desktop",
+ "/org/freedesktop/portal/desktop",
+ "org.freedesktop.portal.OpenURI",
+ "OpenURI",
+ payload,
+ reply_type,
+ .{},
+ -1,
+ null,
+ requestCallback,
+ request,
+ );
+}
+
+/// Process the result of the original method call. Receiving this result does
+/// not indicate that the that the method call succeeded but it may contain an
+/// error message that is useful to log for debugging purposes.
+fn requestCallback(
+ source: ?*gobject.Object,
+ result: *gio.AsyncResult,
+ ud: ?*anyopaque,
+) callconv(.c) void {
+ const request: *RequestData = @ptrCast(@alignCast(ud orelse return));
+ const self = request.open_uri;
+ const alloc = self.app.app.allocator();
+ defer {
+ request.deinit(alloc);
+ alloc.destroy(request);
+ }
+
+ const dbus = gobject.ext.cast(gio.DBusConnection, source orelse {
+ log.err("Open URI request finished without a D-Bus source object", .{});
+ self.failRequestAndFallback(request);
+ return;
+ }) orelse {
+ log.err("Open URI request finished with an unexpected source object", .{});
+ self.failRequestAndFallback(request);
+ return;
+ };
+
+ var err_: ?*glib.Error = null;
+ defer if (err_) |err| err.free();
+
+ const reply_ = dbus.callFinish(result, &err_);
+
+ if (err_) |err| {
+ log.err("Open URI request failed={s} ({})", .{
+ err.f_message orelse "(unknown)",
+ err.f_code,
+ });
+ self.failRequestAndFallback(request);
+ return;
+ }
+
+ const reply = reply_ orelse {
+ log.err("D-Bus method call returned a null value!", .{});
+ self.failRequestAndFallback(request);
+ return;
+ };
+ defer reply.unref();
+
+ const reply_type = glib.VariantType.new("(o)");
+ defer reply_type.free();
+
+ if (reply.isOfType(reply_type) == 0) {
+ log.warn("Reply from D-Bus method call does not contain an object path!", .{});
+ self.failRequestAndFallback(request);
+ return;
+ }
+
+ var object_path: [*:0]const u8 = undefined;
+ reply.get("(&o)", &object_path);
+
+ const token = portal.parseRequestPathToken(std.mem.span(object_path)) orelse {
+ log.warn("Unable to parse token from the object path {s}", .{object_path});
+ self.failRequestAndFallback(request);
+ return;
+ };
+
+ if (token != request.token) {
+ log.warn("Open URI request returned mismatched token expected={x} actual={x}", .{
+ request.token,
+ token,
+ });
+ self.failRequestAndFallback(request);
+ return;
+ }
+
+ self.mutex.lock();
+ defer self.mutex.unlock();
+
+ if (!self.alive) return;
+
+ const entry = self.entries.get(token) orelse return;
+ if (std.mem.eql(u8, request.request_path, std.mem.span(object_path))) return;
+
+ log.debug("updating open uri request path old={s} new={s}", .{
+ request.request_path,
+ object_path,
+ });
+ self.unsubscribeFromResponse(entry);
+ self.subscribeToResponse(entry, dbus, object_path);
+}
+
+/// Handle the response signal from the portal. This should contain the actual
+/// results of the method call (success or failure).
+fn responseReceived(
+ _: *gio.DBusConnection,
+ _: ?[*:0]const u8,
+ object_path: [*:0]const u8,
+ _: [*:0]const u8,
+ _: [*:0]const u8,
+ params: *glib.Variant,
+ ud: ?*anyopaque,
+) callconv(.c) void {
+ const self: *OpenURI = @ptrCast(@alignCast(ud orelse {
+ log.err("OpenURI response received with null userdata", .{});
+ return;
+ }));
+
+ const alloc = self.app.app.allocator();
+
+ const token = portal.parseRequestPathToken(std.mem.span(object_path)) orelse {
+ log.warn("invalid object path: {s}", .{std.mem.span(object_path)});
+ return;
+ };
+
+ self.mutex.lock();
+ defer self.mutex.unlock();
+
+ if (!self.alive) return;
+
+ const entry = (self.entries.fetchSwapRemove(token) orelse {
+ log.warn("no entry for token {x}", .{token});
+ return;
+ }).value;
+
+ defer destroyEntry(alloc, entry);
+
+ self.unsubscribeFromResponse(entry);
+
+ var response: u32 = 0;
+ var results: ?*glib.Variant = null;
+ defer if (results) |variant| variant.unref();
+ params.get("(u@a{sv})", &response, &results);
+
+ switch (response) {
+ 0 => {
+ log.debug("open uri successful", .{});
+ },
+ 1 => {
+ log.debug("open uri request was cancelled by the user", .{});
+ },
+ 2 => {
+ log.warn("open uri request ended unexpectedly", .{});
+ self.app.app.openUrlFallback(entry.kind, entry.uri);
+ },
+ else => {
+ log.err("unrecognized response code={}", .{response});
+ self.app.app.openUrlFallback(entry.kind, entry.uri);
+ },
+ }
+}
+
+/// Wait this number of seconds and then clean up any orphaned entries.
+const cleanup_timeout = 30;
+
+/// If there is an active cleanup timer, cancel it. This must be called with the
+/// mutex locked
+fn stopCleanupTimer(self: *OpenURI) void {
+ assert(!self.mutex.tryLock());
+
+ if (self.cleanup_timer) |timer| {
+ if (glib.Source.remove(timer) == 0) {
+ log.warn("unable to remove cleanup timer source={d}", .{timer});
+ }
+ self.cleanup_timer = null;
+ }
+}
+
+/// Start a timer to clean up any entries that have not received a timely
+/// response. If there is already a timer it will be stopped and replaced with a
+/// new one. This must be called with the mutex locked.
+fn startCleanupTimer(self: *OpenURI) void {
+ assert(!self.mutex.tryLock());
+
+ self.stopCleanupTimer();
+ self.cleanup_timer = glib.timeoutAddSeconds(cleanup_timeout + 1, cleanup, self);
+}
+
+/// The cleanup timer is used to free up any entries that may have failed
+/// to get a response in a timely manner.
+fn cleanup(ud: ?*anyopaque) callconv(.c) c_int {
+ const self: *OpenURI = @ptrCast(@alignCast(ud orelse {
+ log.warn("cleanup called with null userdata", .{});
+ return @intFromBool(glib.SOURCE_REMOVE);
+ }));
+
+ const alloc = self.app.app.allocator();
+
+ self.mutex.lock();
+ defer self.mutex.unlock();
+
+ self.cleanup_timer = null;
+ if (!self.alive) return @intFromBool(glib.SOURCE_REMOVE);
+
+ const now = std.time.Instant.now() catch {
+ // `now()` should never fail, but if it does, don't crash, just return.
+ // This might cause a small memory leak in rare circumstances but it
+ // should get cleaned up the next time a URL is clicked.
+ return @intFromBool(glib.SOURCE_REMOVE);
+ };
+
+ loop: while (true) {
+ for (self.entries.entries.items(.value)) |entry| {
+ if (now.since(entry.start) > cleanup_timeout * std.time.ns_per_s) {
+ log.warn("open uri request timed out token={x}", .{entry.token});
+ self.unsubscribeFromResponse(entry);
+ _ = self.entries.swapRemove(entry.token);
+ self.app.app.openUrlFallback(entry.kind, entry.uri);
+ destroyEntry(alloc, entry);
+ continue :loop;
+ }
+ }
+ break :loop;
+ }
+
+ return @intFromBool(glib.SOURCE_REMOVE);
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/post_fork.zig b/vendor/libghostty-vt/src/apprt/gtk/post_fork.zig
new file mode 100644
index 00000000..ff021950
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/post_fork.zig
@@ -0,0 +1,121 @@
+const std = @import("std");
+
+const gio = @import("gio");
+const glib = @import("glib");
+
+const log = std.log.scoped(.gtk_post_fork);
+
+const configpkg = @import("../../config.zig");
+const internal_os = @import("../../os/main.zig");
+const Command = @import("../../Command.zig");
+const cgroup = @import("./cgroup.zig");
+
+const Application = @import("class/application.zig").Application;
+
+pub const PostForkInfo = struct {
+ gtk_single_instance: configpkg.Config.GtkSingleInstance,
+ linux_cgroup: configpkg.Config.LinuxCgroup,
+ linux_cgroup_hard_fail: bool,
+ linux_cgroup_memory_limit: ?u64,
+ linux_cgroup_processes_limit: ?u64,
+
+ pub fn init(cfg: *const configpkg.Config) PostForkInfo {
+ return .{
+ .gtk_single_instance = cfg.@"gtk-single-instance",
+ .linux_cgroup = cfg.@"linux-cgroup",
+ .linux_cgroup_hard_fail = cfg.@"linux-cgroup-hard-fail",
+ .linux_cgroup_memory_limit = cfg.@"linux-cgroup-memory-limit",
+ .linux_cgroup_processes_limit = cfg.@"linux-cgroup-processes-limit",
+ };
+ }
+};
+
+/// If we are configured to do so, tell `systemd` to move the new child PID into
+/// a transient `systemd` scope with the configured resource limits.
+///
+/// If we are configured to hard fail, log an error message and return an error
+/// code if we don't detect the move in time.
+pub fn postFork(cmd: *Command) Command.PostForkError!void {
+ switch (cmd.rt_post_fork_info.linux_cgroup) {
+ .always => {},
+ .never => return,
+ .@"single-instance" => switch (cmd.rt_post_fork_info.gtk_single_instance) {
+ .true => {},
+ .false => return,
+ .detect => {
+ log.err("gtk-single-instance is set to detect which should be impossible!", .{});
+ return error.PostForkError;
+ },
+ },
+ }
+
+ const pid: u32 = @intCast(cmd.pid orelse {
+ log.err("PID of child not known!", .{});
+ return error.PostForkError;
+ });
+
+ var expected_cgroup_buf: [256]u8 = undefined;
+ const expected_cgroup = cgroup.fmtScope(&expected_cgroup_buf, pid);
+
+ log.debug("beginning transition to transient systemd scope {s}", .{expected_cgroup});
+
+ const app = Application.default();
+
+ const dbus = app.as(gio.Application).getDbusConnection() orelse {
+ if (cmd.rt_post_fork_info.linux_cgroup_hard_fail) {
+ log.err("dbus connection required for cgroup isolation, exiting", .{});
+ return error.PostForkError;
+ }
+ return;
+ };
+
+ cgroup.createScope(
+ dbus,
+ pid,
+ .{
+ .memory_high = cmd.rt_post_fork_info.linux_cgroup_memory_limit,
+ .tasks_max = cmd.rt_post_fork_info.linux_cgroup_processes_limit,
+ },
+ ) catch |err| {
+ if (cmd.rt_post_fork_info.linux_cgroup_hard_fail) {
+ log.err("unable to create transient systemd scope {s}: {t}", .{ expected_cgroup, err });
+ return error.PostForkError;
+ }
+ log.warn("unable to create transient systemd scope {s}: {t}", .{ expected_cgroup, err });
+ return;
+ };
+
+ const start = std.time.Instant.now() catch unreachable;
+
+ loop: while (true) {
+ const now = std.time.Instant.now() catch unreachable;
+
+ if (now.since(start) > 250 * std.time.ns_per_ms) {
+ if (cmd.rt_pre_exec_info.linux_cgroup_hard_fail) {
+ log.err("transition to new transient systemd scope {s} took too long", .{expected_cgroup});
+ return error.PostForkError;
+ }
+ log.warn("transition to transient systemd scope {s} took too long", .{expected_cgroup});
+ break :loop;
+ }
+
+ not_found: {
+ var current_cgroup_buf: [4096]u8 = undefined;
+
+ const current_cgroup_raw = internal_os.cgroup.current(
+ &current_cgroup_buf,
+ @intCast(pid),
+ ) orelse break :not_found;
+
+ const index = std.mem.lastIndexOfScalar(u8, current_cgroup_raw, '/') orelse break :not_found;
+ const current_cgroup = current_cgroup_raw[index + 1 ..];
+
+ if (std.mem.eql(u8, current_cgroup, expected_cgroup)) {
+ log.debug("transition to transient systemd scope {s} complete", .{expected_cgroup});
+ break :loop;
+ }
+ }
+
+ std.Thread.sleep(25 * std.time.ns_per_ms);
+ }
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/pre_exec.zig b/vendor/libghostty-vt/src/apprt/gtk/pre_exec.zig
new file mode 100644
index 00000000..6f6a9ed5
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/pre_exec.zig
@@ -0,0 +1,81 @@
+const std = @import("std");
+
+const log = std.log.scoped(.gtk_pre_exec);
+
+const configpkg = @import("../../config.zig");
+
+const internal_os = @import("../../os/main.zig");
+const Command = @import("../../Command.zig");
+const cgroup = @import("./cgroup.zig");
+
+pub const PreExecInfo = struct {
+ gtk_single_instance: configpkg.Config.GtkSingleInstance,
+ linux_cgroup: configpkg.Config.LinuxCgroup,
+ linux_cgroup_hard_fail: bool,
+
+ pub fn init(cfg: *const configpkg.Config) PreExecInfo {
+ return .{
+ .gtk_single_instance = cfg.@"gtk-single-instance",
+ .linux_cgroup = cfg.@"linux-cgroup",
+ .linux_cgroup_hard_fail = cfg.@"linux-cgroup-hard-fail",
+ };
+ }
+};
+
+/// If we are expecting to be moved to a transient systemd scope, wait to see if
+/// that happens by checking for the correct name of the current cgroup. Wait at
+/// most 250ms so that we don't overly delay the soft-fail scenario.
+///
+/// If we are configured to hard fail, log an error message and return an error
+/// code if we don't detect the move in time.
+pub fn preExec(cmd: *Command) ?u8 {
+ switch (cmd.rt_pre_exec_info.linux_cgroup) {
+ .always => {},
+ .never => return null,
+ .@"single-instance" => switch (cmd.rt_pre_exec_info.gtk_single_instance) {
+ .true => {},
+ .false => return null,
+ .detect => {
+ log.err("gtk-single-instance is set to detect", .{});
+ return 127;
+ },
+ },
+ }
+
+ const pid: u32 = @intCast(std.os.linux.getpid());
+
+ var expected_cgroup_buf: [256]u8 = undefined;
+ const expected_cgroup = cgroup.fmtScope(&expected_cgroup_buf, pid);
+
+ const start = std.time.Instant.now() catch unreachable;
+
+ while (true) {
+ const now = std.time.Instant.now() catch unreachable;
+
+ if (now.since(start) > 250 * std.time.ns_per_ms) {
+ if (cmd.rt_pre_exec_info.linux_cgroup_hard_fail) {
+ log.err("transition to new transient systemd scope took too long", .{});
+ return 127;
+ }
+ break;
+ }
+
+ not_found: {
+ var current_cgroup_buf: [4096]u8 = undefined;
+
+ const current_cgroup_raw = internal_os.cgroup.current(
+ &current_cgroup_buf,
+ @intCast(pid),
+ ) orelse break :not_found;
+
+ const index = std.mem.lastIndexOfScalar(u8, current_cgroup_raw, '/') orelse break :not_found;
+ const current_cgroup = current_cgroup_raw[index + 1 ..];
+
+ if (std.mem.eql(u8, current_cgroup, expected_cgroup)) return null;
+ }
+
+ std.Thread.sleep(25 * std.time.ns_per_ms);
+ }
+
+ return null;
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp b/vendor/libghostty-vt/src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp
new file mode 100644
index 00000000..5f29981d
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp
@@ -0,0 +1,86 @@
+using Gtk 4.0;
+// This is unused but if we remove it we get a blueprint-compiler error.
+using Adw 1;
+
+template $GhosttyClipboardConfirmationDialog: $GhosttyDialog {
+ styles [
+ "clipboard-confirmation-dialog",
+ ]
+
+ notify::blur => $notify_blur();
+ notify::request => $notify_request();
+ heading: _("Authorize Clipboard Access");
+ // Not localized because this is a placeholder users never see.
+ body: "If you see this text, there is a bug in Ghostty. Please report it.";
+
+ responses [
+ cancel: _("Deny") suggested,
+ ok: _("Allow") destructive,
+ ]
+
+ default-response: "cancel";
+ close-response: "cancel";
+
+ extra-child: ListBox {
+ selection-mode: none;
+
+ Overlay {
+ styles [
+ "osd",
+ "clipboard-overlay",
+ ]
+
+ ScrolledWindow text_view_scroll {
+ width-request: 500;
+ height-request: 200;
+
+ TextView text_view {
+ styles [
+ "clipboard-contents",
+ ]
+
+ cursor-visible: false;
+ editable: false;
+ monospace: true;
+ top-margin: 8;
+ left-margin: 8;
+ bottom-margin: 8;
+ right-margin: 8;
+ buffer: bind template.clipboard-contents;
+ }
+ }
+
+ [overlay]
+ Button reveal_button {
+ clicked => $reveal_clicked();
+ visible: false;
+ halign: end;
+ valign: start;
+ margin-end: 12;
+ margin-top: 12;
+
+ Image {
+ icon-name: "view-reveal-symbolic";
+ }
+ }
+
+ [overlay]
+ Button hide_button {
+ clicked => $hide_clicked();
+ visible: false;
+ halign: end;
+ valign: start;
+ margin-end: 12;
+ margin-top: 12;
+
+ styles [
+ "opaque",
+ ]
+
+ Image {
+ icon-name: "view-conceal-symbolic";
+ }
+ }
+ }
+ };
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp b/vendor/libghostty-vt/src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp
new file mode 100644
index 00000000..f58ef523
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp
@@ -0,0 +1,12 @@
+using Gtk 4.0;
+// This is unused but if we remove it we get a blueprint-compiler error.
+using Adw 1;
+
+template $GhosttyCloseConfirmationDialog: $GhosttyDialog {
+ responses [
+ cancel: _("Cancel"),
+ close: _("Close") destructive,
+ ]
+
+ close-response: "cancel";
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/ui/1.2/config-errors-dialog.blp b/vendor/libghostty-vt/src/apprt/gtk/ui/1.2/config-errors-dialog.blp
new file mode 100644
index 00000000..845909eb
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/ui/1.2/config-errors-dialog.blp
@@ -0,0 +1,28 @@
+using Gtk 4.0;
+// This is unused but if we remove it we get a blueprint-compiler error.
+using Adw 1;
+
+template $GhosttyConfigErrorsDialog: $GhosttyDialog {
+ heading: _("Configuration Errors");
+ body: _("One or more configuration errors were found. Please review the errors below, and either reload your configuration or ignore these errors.");
+
+ responses [
+ ignore: _("Ignore"),
+ reload: _("Reload Configuration") suggested,
+ ]
+
+ extra-child: ScrolledWindow {
+ min-content-width: 500;
+ min-content-height: 100;
+
+ TextView {
+ editable: false;
+ cursor-visible: false;
+ top-margin: 8;
+ bottom-margin: 8;
+ left-margin: 8;
+ right-margin: 8;
+ buffer: bind (template.config as <$GhosttyConfig>).diagnostics-buffer;
+ }
+ };
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/ui/1.2/debug-warning.blp b/vendor/libghostty-vt/src/apprt/gtk/ui/1.2/debug-warning.blp
new file mode 100644
index 00000000..34411ce9
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/ui/1.2/debug-warning.blp
@@ -0,0 +1,11 @@
+using Gtk 4.0;
+
+template $GhosttyDebugWarning: Gtk.Box {
+ orientation: vertical;
+
+ Gtk.Label {
+ label: _("⚠️ You're running a debug build of Ghostty! Performance will be degraded.");
+ margin-top: 10;
+ margin-bottom: 10;
+ }
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/ui/1.2/key-state-overlay.blp b/vendor/libghostty-vt/src/apprt/gtk/ui/1.2/key-state-overlay.blp
new file mode 100644
index 00000000..c8654bfb
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/ui/1.2/key-state-overlay.blp
@@ -0,0 +1,58 @@
+using Gtk 4.0;
+using Adw 1;
+
+template $GhosttyKeyStateOverlay: Adw.Bin {
+ visible: bind $has_state(template.has-tables, template.has-sequence) as <bool>;
+ valign-target: end;
+ halign: center;
+ valign: bind template.valign-target;
+
+ GestureDrag {
+ button: 1;
+ propagation-phase: capture;
+ drag-end => $on_drag_end();
+ }
+
+ Adw.Bin {
+ Box container {
+ styles [
+ "background",
+ "key-state-overlay",
+ ]
+
+ orientation: horizontal;
+ spacing: 6;
+
+ Image {
+ icon-name: "input-keyboard-symbolic";
+ pixel-size: 16;
+ }
+
+ Label tables_label {
+ visible: bind template.has-tables;
+ label: bind $tables_text(template.tables) as <string>;
+ xalign: 0.0;
+ }
+
+ Label chevron_label {
+ visible: bind $show_chevron(template.has-tables, template.has-sequence) as <bool>;
+ label: "›";
+
+ styles [
+ "dim-label",
+ ]
+ }
+
+ Label sequence_label {
+ visible: bind template.has-sequence;
+ label: bind $sequence_text(template.sequence) as <string>;
+ xalign: 0.0;
+ }
+
+ Spinner pending_spinner {
+ visible: bind template.has-sequence;
+ spinning: bind template.has-sequence;
+ }
+ }
+ }
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/ui/1.2/resize-overlay.blp b/vendor/libghostty-vt/src/apprt/gtk/ui/1.2/resize-overlay.blp
new file mode 100644
index 00000000..a05986b4
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/ui/1.2/resize-overlay.blp
@@ -0,0 +1,33 @@
+using Gtk 4.0;
+using Adw 1;
+// We can't inherit directly from Label because its an opaque
+// type in zig-gobject.
+template $GhosttyResizeOverlay: Adw.Bin {
+ visible: false;
+ can-focus: false;
+ can-target: false;
+ focusable: false;
+ focus-on-click: false;
+ duration: 750;
+ first-delay: 250;
+ overlay-halign: center;
+ overlay-valign: center;
+ // See surface.blp for why we need to wrap this.
+ Adw.Bin {
+ Label label {
+ styles [
+ "background",
+ "resize-overlay",
+ ]
+
+ can-focus: false;
+ can-target: false;
+ focusable: false;
+ focus-on-click: false;
+ selectable: false;
+ justify: center;
+ halign: bind template.overlay-halign;
+ valign: bind template.overlay-valign;
+ }
+ }
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/ui/1.2/search-overlay.blp b/vendor/libghostty-vt/src/apprt/gtk/ui/1.2/search-overlay.blp
new file mode 100644
index 00000000..6523d414
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/ui/1.2/search-overlay.blp
@@ -0,0 +1,94 @@
+using Gtk 4.0;
+using Gdk 4.0;
+using Adw 1;
+
+template $GhosttySearchOverlay: Adw.Bin {
+ visible: bind template.active;
+ halign-target: end;
+ valign-target: start;
+ halign: bind template.halign-target;
+ valign: bind template.valign-target;
+
+ GestureDrag {
+ button: 1;
+ propagation-phase: capture;
+ drag-end => $on_drag_end();
+ }
+
+ Adw.Bin {
+ Box container {
+ styles [
+ "background",
+ "search-overlay",
+ ]
+
+ orientation: horizontal;
+ spacing: 6;
+
+ SearchEntry search_entry {
+ placeholder-text: _("Find…");
+ width-chars: 20;
+ hexpand: true;
+ stop-search => $stop_search();
+ search-changed => $search_changed();
+ next-match => $next_match();
+ previous-match => $previous_match();
+
+ EventControllerKey {
+ // We need this so we capture before the SearchEntry.
+ propagation-phase: capture;
+ key-pressed => $search_entry_key_pressed();
+ }
+ }
+
+ Label {
+ styles [
+ "dim-label",
+ ]
+
+ label: bind $match_label_closure(template.has-search-selected, template.search-selected, template.has-search-total, template.search-total) as <string>;
+ width-chars: 6;
+ xalign: 1.0;
+ }
+
+ Box button_box {
+ orientation: horizontal;
+ spacing: 1;
+
+ styles [
+ "linked",
+ ]
+
+ Button prev_button {
+ icon-name: "go-up-symbolic";
+ tooltip-text: _("Previous Match");
+ clicked => $next_match();
+
+ cursor: Gdk.Cursor {
+ name: "pointer";
+ };
+ }
+
+ Button next_button {
+ icon-name: "go-down-symbolic";
+ tooltip-text: _("Next Match");
+ clicked => $previous_match();
+
+ cursor: Gdk.Cursor {
+ name: "pointer";
+ };
+ }
+ }
+
+ Button close_button {
+ icon-name: "window-close-symbolic";
+ tooltip-text: _("Close");
+ clicked => $stop_search();
+
+ cursor: Gdk.Cursor {
+ name: "pointer";
+ };
+ }
+ }
+ }
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/ui/1.2/surface.blp b/vendor/libghostty-vt/src/apprt/gtk/ui/1.2/surface.blp
new file mode 100644
index 00000000..794ea180
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/ui/1.2/surface.blp
@@ -0,0 +1,371 @@
+using Gtk 4.0;
+using Adw 1;
+
+Adw.StatusPage error_page {
+ icon-name: "computer-fail-symbolic";
+ title: _("Oh, no.");
+ description: _("Unable to acquire an OpenGL context for rendering.");
+
+ child: LinkButton {
+ label: "https://ghostty.org/docs/help/gtk-opengl-context";
+ uri: "https://ghostty.org/docs/help/gtk-opengl-context";
+ };
+}
+
+Overlay terminal_page {
+ focusable: false;
+ focus-on-click: false;
+
+ child: Box {
+ hexpand: true;
+ vexpand: true;
+
+ GLArea gl_area {
+ realize => $gl_realize();
+ unrealize => $gl_unrealize();
+ render => $gl_render();
+ resize => $gl_resize();
+ hexpand: true;
+ vexpand: true;
+ focusable: true;
+ focus-on-click: true;
+ has-stencil-buffer: false;
+ has-depth-buffer: false;
+ allowed-apis: gl;
+ }
+
+ PopoverMenu context_menu {
+ closed => $context_menu_closed();
+ menu-model: context_menu_model;
+ flags: nested;
+ halign: start;
+ has-arrow: false;
+ }
+
+ EventControllerFocus {
+ enter => $focus_enter();
+ leave => $focus_leave();
+ }
+
+ EventControllerKey {
+ key-pressed => $key_pressed();
+ key-released => $key_released();
+ }
+
+ EventControllerScroll {
+ scroll => $scroll_vertical();
+ scroll-begin => $scroll_vertical_begin();
+ scroll-end => $scroll_vertical_end();
+ flags: vertical;
+ }
+
+ EventControllerScroll {
+ scroll => $scroll_horizontal();
+ flags: horizontal;
+ }
+
+ EventControllerMotion {
+ motion => $mouse_motion();
+ leave => $mouse_leave();
+ }
+
+ GestureClick {
+ pressed => $mouse_down();
+ released => $mouse_up();
+ button: 0;
+ }
+ };
+
+ [overlay]
+ Revealer {
+ reveal-child: bind template.readonly;
+ transition-type: crossfade;
+ transition-duration: 500;
+ // Revealers take up the full size, we need this to not capture events.
+ can-focus: false;
+ can-target: false;
+ focusable: false;
+
+ Box readonly_overlay {
+ styles [
+ "readonly_overlay",
+ ]
+
+ // TODO: the tooltip doesn't actually work, but keep it here for now so
+ // that we can get the tooltip text translated.
+ has-tooltip: true;
+ tooltip-text: _("This terminal is in read-only mode. You can still view, select, and scroll through the content, but no input events will be sent to the running application.");
+ halign: end;
+ valign: start;
+ spacing: 6;
+
+ Image {
+ icon-name: "changes-prevent-symbolic";
+ }
+
+ Label {
+ label: _("Read-only");
+ }
+ }
+ }
+
+ [overlay]
+ ProgressBar progress_bar_overlay {
+ styles [
+ "osd",
+ ]
+
+ visible: false;
+ halign: fill;
+ valign: start;
+ }
+
+ [overlay]
+ // The "border" bell feature is implemented here as an overlay rather than
+ // just adding a border to the GLArea or other widget for two reasons.
+ // First, adding a border to an existing widget causes a resize of the
+ // widget which undesirable side effects. Second, we can make it reactive
+ // here in the blueprint with relatively little code.
+ Revealer {
+ reveal-child: bind $should_border_be_shown(template.config, template.bell-ringing) as <bool>;
+ transition-type: crossfade;
+ transition-duration: 500;
+ // Revealers take up the full size, we need this to not capture events.
+ can-focus: false;
+ can-target: false;
+ focusable: false;
+
+ Box bell_overlay {
+ styles [
+ "bell-overlay",
+ ]
+
+ halign: fill;
+ valign: fill;
+ }
+ }
+
+ [overlay]
+ $GhosttySurfaceChildExited child_exited_overlay {
+ visible: bind template.child-exited;
+ close-request => $child_exited_close();
+ }
+
+ [overlay]
+ $GhosttyResizeOverlay resize_overlay {}
+
+ [overlay]
+ Label url_left {
+ styles [
+ "background",
+ "url-overlay",
+ ]
+
+ visible: false;
+ halign: start;
+ valign: end;
+ label: bind template.mouse-hover-url;
+
+ EventControllerMotion url_ec_motion {
+ enter => $url_mouse_enter();
+ leave => $url_mouse_leave();
+ }
+ }
+
+ [overlay]
+ Label url_right {
+ styles [
+ "background",
+ "url-overlay",
+ ]
+
+ visible: false;
+ halign: end;
+ valign: end;
+ label: bind template.mouse-hover-url;
+ }
+
+ [overlay]
+ $GhosttySearchOverlay search_overlay {
+ stop-search => $search_stop();
+ search-changed => $search_changed();
+ next-match => $search_next_match();
+ previous-match => $search_previous_match();
+ }
+
+ [overlay]
+ $GhosttyKeyStateOverlay key_state_overlay {
+ tables: bind template.key-table;
+ sequence: bind template.key-sequence;
+ }
+
+ [overlay]
+ // Apply unfocused-split-fill and unfocused-split-opacity to current surface
+ // this is only applied when a tab has more than one surface
+ Revealer {
+ reveal-child: bind $should_unfocused_split_be_shown(search_overlay.active, template.focused, template.is-split) as <bool>;
+ transition-duration: 0;
+ // This is all necessary so that the Revealer itself doesn't override
+ // any input events from the other overlays. Namely, if you don't have
+ // these then the search overlay won't get mouse events.
+ can-focus: false;
+ can-target: false;
+ focusable: false;
+
+ DrawingArea {
+ styles [
+ "unfocused-split",
+ ]
+ }
+ }
+
+ DropTarget drop_target {
+ drop => $drop();
+ actions: copy | move;
+ }
+}
+
+template $GhosttySurface: Adw.Bin {
+ styles [
+ "surface",
+ ]
+
+ notify::config => $notify_config();
+ notify::error => $notify_error();
+ notify::mouse-hover-url => $notify_mouse_hover_url();
+ notify::mouse-hidden => $notify_mouse_hidden();
+ notify::mouse-shape => $notify_mouse_shape();
+ notify::vadjustment => $notify_vadjustment();
+ // Some history: we used to use a Stack here and swap between the
+ // terminal and error pages as needed. But a Stack doesn't play nice
+ // with our SplitTree and Gtk.Paned usage[^1]. Replacing this with
+ // a manual programmatic child swap fixed this. So if you ever change
+ // this, be sure to test many splits!
+ //
+ // [^1]: https://github.com/ghostty-org/ghostty/issues/8533
+ child: terminal_page;
+}
+
+IMMulticontext im_context {
+ input-purpose: terminal;
+ preedit-start => $im_preedit_start();
+ preedit-changed => $im_preedit_changed();
+ preedit-end => $im_preedit_end();
+ commit => $im_commit();
+}
+
+menu context_menu_model {
+ section {
+ item {
+ label: _("Copy");
+ action: "win.copy";
+ }
+
+ item {
+ label: _("Paste");
+ action: "win.paste";
+ }
+
+ item {
+ label: _("Notify on Next Command Finish");
+ action: "surface.notify-on-next-command-finish";
+ }
+ }
+
+ section {
+ item {
+ label: _("Clear");
+ action: "win.clear";
+ }
+
+ item {
+ label: _("Reset");
+ action: "win.reset";
+ }
+ }
+
+ section {
+ submenu {
+ label: _("Split");
+
+ item {
+ label: _("Change Title…");
+ action: "surface.prompt-title";
+ }
+
+ item {
+ label: _("Split Up");
+ action: "split-tree.new-split";
+ target: "up";
+ }
+
+ item {
+ label: _("Split Down");
+ action: "split-tree.new-split";
+ target: "down";
+ }
+
+ item {
+ label: _("Split Left");
+ action: "split-tree.new-split";
+ target: "left";
+ }
+
+ item {
+ label: _("Split Right");
+ action: "split-tree.new-split";
+ target: "right";
+ }
+ }
+
+ submenu {
+ label: _("Tab");
+
+ item {
+ label: _("Change Tab Title…");
+ action: "tab.prompt-tab-title";
+ }
+
+ item {
+ label: _("New Tab");
+ action: "win.new-tab";
+ }
+
+ item {
+ label: _("Close Tab");
+ action: "tab.close";
+ target: "this";
+ }
+ }
+
+ submenu {
+ label: _("Window");
+
+ item {
+ label: _("New Window");
+ action: "win.new-window";
+ }
+
+ item {
+ label: _("Close Window");
+ action: "win.close";
+ }
+ }
+ }
+
+ section {
+ submenu {
+ label: _("Config");
+
+ item {
+ label: _("Open Configuration");
+ action: "app.open-config";
+ }
+
+ item {
+ label: _("Reload Configuration");
+ action: "app.reload-config";
+ }
+ }
+ }
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/ui/1.3/debug-warning.blp b/vendor/libghostty-vt/src/apprt/gtk/ui/1.3/debug-warning.blp
new file mode 100644
index 00000000..ad7246a8
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/ui/1.3/debug-warning.blp
@@ -0,0 +1,9 @@
+using Gtk 4.0;
+using Adw 1;
+
+template $GhosttyDebugWarning: Adw.Bin {
+ Adw.Banner {
+ title: _("⚠️ You're running a debug build of Ghostty! Performance will be degraded.");
+ revealed: true;
+ }
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/ui/1.3/surface-child-exited.blp b/vendor/libghostty-vt/src/apprt/gtk/ui/1.3/surface-child-exited.blp
new file mode 100644
index 00000000..6745f0ca
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/ui/1.3/surface-child-exited.blp
@@ -0,0 +1,22 @@
+using Gtk 4.0;
+using Adw 1;
+
+template $GhosttySurfaceChildExited: Adw.Bin {
+ styles [
+ "child-exited",
+ ]
+
+ notify::data => $notify_data();
+
+ Adw.Bin {
+ Adw.Banner banner {
+ button-clicked => $clicked();
+ revealed: true;
+ // Not localized on purpose because it should never be seen.
+ title: "This is a bug in Ghostty. Please report it.";
+ button-label: _("Close");
+ halign: fill;
+ valign: end;
+ }
+ }
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp b/vendor/libghostty-vt/src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp
new file mode 100644
index 00000000..144e3a37
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp
@@ -0,0 +1,96 @@
+using Gtk 4.0;
+// This is unused but if we remove it we get a blueprint-compiler error.
+using Adw 1;
+
+template $GhosttyClipboardConfirmationDialog: $GhosttyDialog {
+ styles [
+ "clipboard-confirmation-dialog",
+ ]
+
+ notify::blur => $notify_blur();
+ notify::request => $notify_request();
+ heading: _("Authorize Clipboard Access");
+ // Not localized because this is a placeholder users never see.
+ body: "If you see this text, there is a bug in Ghostty. Please report it.";
+
+ responses [
+ cancel: _("Deny") suggested,
+ ok: _("Allow") destructive,
+ ]
+
+ default-response: "cancel";
+ close-response: "cancel";
+
+ extra-child: ListBox {
+ selection-mode: none;
+
+ Overlay {
+ styles [
+ "osd",
+ "clipboard-overlay",
+ ]
+
+ ScrolledWindow text_view_scroll {
+ width-request: 500;
+ height-request: 200;
+
+ TextView text_view {
+ styles [
+ "clipboard-contents",
+ ]
+
+ cursor-visible: false;
+ editable: false;
+ monospace: true;
+ top-margin: 8;
+ left-margin: 8;
+ bottom-margin: 8;
+ right-margin: 8;
+ buffer: bind template.clipboard-contents;
+ }
+ }
+
+ [overlay]
+ Button reveal_button {
+ clicked => $reveal_clicked();
+ visible: false;
+ halign: end;
+ valign: start;
+ margin-end: 12;
+ margin-top: 12;
+
+ Image {
+ icon-name: "view-reveal-symbolic";
+ }
+ }
+
+ [overlay]
+ Button hide_button {
+ clicked => $hide_clicked();
+ visible: false;
+ halign: end;
+ valign: start;
+ margin-end: 12;
+ margin-top: 12;
+
+ styles [
+ "opaque",
+ ]
+
+ Image {
+ icon-name: "view-conceal-symbolic";
+ }
+ }
+ }
+
+ Adw.SwitchRow remember_choice {
+ styles [
+ "card",
+ ]
+
+ visible: bind template.can-remember;
+ title: _("Remember choice for this split");
+ subtitle: _("Reload configuration to show this prompt again");
+ }
+ };
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/command-palette.blp b/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/command-palette.blp
new file mode 100644
index 00000000..473fb1f0
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/command-palette.blp
@@ -0,0 +1,110 @@
+using Gtk 4.0;
+using Gio 2.0;
+using Adw 1;
+
+Adw.Dialog dialog {
+ content-width: 700;
+ closed => $closed();
+
+ Adw.ToolbarView {
+ top-bar-style: flat;
+
+ [top]
+ Adw.HeaderBar {
+ [title]
+ Gtk.SearchEntry search {
+ hexpand: true;
+ placeholder-text: _("Execute a command…");
+ stop-search => $search_stopped();
+ activate => $search_activated();
+
+ styles [
+ "command-palette-search",
+ ]
+ }
+ }
+
+ Gtk.ScrolledWindow {
+ min-content-height: 300;
+
+ Gtk.ListView view {
+ show-separators: true;
+ single-click-activate: true;
+ activate => $row_activated();
+
+ model: Gtk.SingleSelection model {
+ model: Gtk.FilterListModel {
+ incremental: true;
+
+ filter: Gtk.AnyFilter {
+ Gtk.StringFilter {
+ expression: expr item as <$GhosttyCommand>.title;
+ search: bind search.text;
+ }
+
+ Gtk.StringFilter {
+ expression: expr item as <$GhosttyCommand>.action-key;
+ search: bind search.text;
+ }
+ };
+
+ model: Gio.ListStore source {
+ item-type: typeof<$GhosttyCommand>;
+ };
+ };
+ };
+
+ styles [
+ "rich-list",
+ ]
+
+ factory: Gtk.BuilderListItemFactory {
+ template Gtk.ListItem {
+ child: Gtk.Box {
+ orientation: horizontal;
+ spacing: 10;
+ tooltip-text: bind template.item as <$GhosttyCommand>.description;
+
+ Gtk.Box {
+ orientation: vertical;
+ hexpand: true;
+
+ Gtk.Label {
+ ellipsize: end;
+ halign: start;
+ wrap: false;
+ single-line-mode: true;
+
+ styles [
+ "title",
+ ]
+
+ label: bind template.item as <$GhosttyCommand>.title;
+ }
+
+ Gtk.Label {
+ ellipsize: end;
+ halign: start;
+ wrap: false;
+ single-line-mode: true;
+
+ styles [
+ "subtitle",
+ "monospace",
+ ]
+
+ label: bind template.item as <$GhosttyCommand>.action-key;
+ }
+ }
+
+ Gtk.ShortcutLabel {
+ accelerator: bind template.item as <$GhosttyCommand>.action;
+ valign: center;
+ }
+ };
+ }
+ };
+ }
+ }
+ }
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/imgui-widget.blp b/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/imgui-widget.blp
new file mode 100644
index 00000000..d5b973a7
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/imgui-widget.blp
@@ -0,0 +1,51 @@
+using Gtk 4.0;
+using Adw 1;
+
+template $GhosttyImguiWidget: Adw.Bin {
+ styles [
+ "imgui",
+ ]
+
+ Adw.Bin {
+ Gtk.GLArea gl_area {
+ auto-render: true;
+ // needs to be focusable so that we can receive events
+ focusable: true;
+ focus-on-click: true;
+ allowed-apis: gl;
+ realize => $realize();
+ unrealize => $unrealize();
+ resize => $resize();
+ render => $render();
+
+ EventControllerFocus {
+ enter => $focus_enter();
+ leave => $focus_leave();
+ }
+
+ EventControllerKey {
+ key-pressed => $key_pressed();
+ key-released => $key_released();
+ }
+
+ GestureClick {
+ pressed => $mouse_pressed();
+ released => $mouse_released();
+ button: 0;
+ }
+
+ EventControllerMotion {
+ motion => $mouse_motion();
+ }
+
+ EventControllerScroll {
+ scroll => $scroll();
+ flags: both_axes;
+ }
+ }
+ }
+}
+
+IMMulticontext im_context {
+ commit => $im_commit();
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/inspector-widget.blp b/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/inspector-widget.blp
new file mode 100644
index 00000000..ac15ab4e
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/inspector-widget.blp
@@ -0,0 +1,10 @@
+using Gtk 4.0;
+
+template $GhosttyInspectorWidget: $GhosttyImguiWidget {
+ styles [
+ "inspector",
+ ]
+
+ hexpand: true;
+ vexpand: true;
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/inspector-window.blp b/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/inspector-window.blp
new file mode 100644
index 00000000..a7625bc2
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/inspector-window.blp
@@ -0,0 +1,38 @@
+using Gtk 4.0;
+using Adw 1;
+
+template $GhosttyInspectorWindow: Adw.ApplicationWindow {
+ title: _("Ghostty: Terminal Inspector");
+ icon-name: "com.mitchellh.ghostty";
+ default-width: 1000;
+ default-height: 600;
+
+ styles [
+ "inspector",
+ ]
+
+ content: Adw.ToolbarView {
+ [top]
+ Adw.HeaderBar {
+ title-widget: Adw.WindowTitle {
+ title: bind template.title;
+ };
+ }
+
+ Gtk.Box {
+ orientation: vertical;
+ spacing: 0;
+ hexpand: true;
+ vexpand: true;
+
+ $GhosttyDebugWarning {
+ visible: bind template.debug;
+ }
+
+ $GhosttyInspectorWidget inspector_widget {
+ notify::surface => $notify_inspector_surface();
+ surface: bind template.surface;
+ }
+ }
+ };
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/split-tree-split.blp b/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/split-tree-split.blp
new file mode 100644
index 00000000..182919f4
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/split-tree-split.blp
@@ -0,0 +1,20 @@
+using Gtk 4.0;
+using Adw 1;
+
+template $GhosttySplitTreeSplit: Adw.Bin {
+ styles [
+ "split",
+ ]
+
+ // The double-nesting is required due to a GTK bug where you can't
+ // bind the first child of a builder layout. If you do, you get a double
+ // dispose. Easiest way to see that is simply remove this and see the
+ // GTK critical errors (and sometimes crashes).
+ Adw.Bin {
+ Paned paned {
+ notify::max-position => $notify_max_position();
+ notify::min-position => $notify_min_position();
+ notify::position => $notify_position();
+ }
+ }
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/split-tree.blp b/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/split-tree.blp
new file mode 100644
index 00000000..e8c53b60
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/split-tree.blp
@@ -0,0 +1,25 @@
+using Gtk 4.0;
+using Adw 1;
+
+template $GhosttySplitTree: Box {
+ notify::tree => $notify_tree();
+ orientation: vertical;
+
+ Adw.Bin tree_bin {
+ visible: bind template.has-surfaces;
+ hexpand: true;
+ vexpand: true;
+ }
+
+ // This could be a lot more visually pleasing but in practice this doesn't
+ // ever happen at the time of writing this comment. A surface-less split
+ // tree always closes its parent.
+ Label {
+ visible: bind template.has-surfaces inverted;
+ // Purposely not localized currently because this shouldn't really
+ // ever appear. When we have a situation it does appear, we may want
+ // to change the styling and text so I don't want to burden localizers
+ // to handle this yet.
+ label: "No surfaces.";
+ }
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/surface-scrolled-window.blp b/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/surface-scrolled-window.blp
new file mode 100644
index 00000000..75582a89
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/surface-scrolled-window.blp
@@ -0,0 +1,11 @@
+using Gtk 4.0;
+using Adw 1;
+
+template $GhostttySurfaceScrolledWindow: Adw.Bin {
+ notify::surface => $notify_surface();
+
+ Gtk.ScrolledWindow scrolled_window {
+ hscrollbar-policy: never;
+ vscrollbar-policy: bind $scrollbar_policy(template.config) as <Gtk.PolicyType>;
+ }
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/tab.blp b/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/tab.blp
new file mode 100644
index 00000000..55f2e7ef
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/tab.blp
@@ -0,0 +1,18 @@
+using Gtk 4.0;
+
+template $GhosttyTab: Box {
+ styles [
+ "tab",
+ ]
+
+ orientation: vertical;
+ hexpand: true;
+ vexpand: true;
+ title: bind $computed_title(template.config, split_tree.active-surface as <$GhosttySurface>.title, split_tree.active-surface as <$GhosttySurface>.title-override, template.title-override, split_tree.is-zoomed, split_tree.active-surface as <$GhosttySurface>.bell-ringing) as <string>;
+ tooltip: bind split_tree.active-surface as <$GhosttySurface>.pwd;
+
+ $GhosttySplitTree split_tree {
+ notify::active-surface => $notify_active_surface();
+ notify::tree => $notify_tree();
+ }
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/title-dialog.blp b/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/title-dialog.blp
new file mode 100644
index 00000000..737a92b5
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/title-dialog.blp
@@ -0,0 +1,18 @@
+using Gtk 4.0;
+using Adw 1;
+
+template $GhosttyTitleDialog: Adw.AlertDialog {
+ body: _("Leave blank to restore the default title.");
+
+ responses [
+ cancel: _("Cancel"),
+ ok: _("OK") suggested,
+ ]
+
+ default-response: "ok";
+ focus-widget: entry;
+
+ extra-child: Entry entry {
+ activates-default: true;
+ };
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/window.blp b/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/window.blp
new file mode 100644
index 00000000..b66a9309
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/ui/1.5/window.blp
@@ -0,0 +1,323 @@
+using Gtk 4.0;
+using Adw 1;
+
+template $GhosttyWindow: Adw.ApplicationWindow {
+ styles [
+ "window",
+ ]
+
+ close-request => $close_request();
+ realize => $realize();
+ notify::config => $notify_config();
+ notify::fullscreened => $notify_fullscreened();
+ notify::is-active => $notify_is_active();
+ notify::maximized => $notify_maximized();
+ notify::quick-terminal => $notify_quick_terminal();
+ notify::scale-factor => $notify_scale_factor();
+ default-width: 800;
+ default-height: 600;
+ // GTK4 grabs F10 input by default to focus the menubar icon. We want
+ // to disable this so that terminal programs can capture F10 (such as htop)
+ handle-menubar-accel: false;
+
+ content: Adw.TabOverview tab_overview {
+ create-tab => $overview_create_tab();
+ notify::open => $overview_notify_open();
+ view: tab_view;
+ enable-new-tab: true;
+ // Disable the title buttons (close, maximize, minimize, ...)
+ // *inside* the tab overview if CSDs are disabled.
+ // We do spare the search button, though.
+ show-start-title-buttons: bind template.decorated;
+ show-end-title-buttons: bind template.decorated;
+
+ Adw.ToolbarView toolbar {
+ top-bar-style: bind template.toolbar-style;
+ bottom-bar-style: bind template.toolbar-style;
+
+ [top]
+ Adw.HeaderBar {
+ visible: bind template.headerbar-visible;
+
+ title-widget: Adw.WindowTitle {
+ title: bind template.title;
+ // Blueprint auto-formatter won't let me split this into multiple
+ // lines. Let me explain myself. All parameters to a closure are used
+ // as notifications to recompute the value of the closure. All
+ // elements of a property chain are also subscribed to for changes.
+ // This one long, ugly line saves us from manually building up this
+ // massive notify chain in code.
+ subtitle: bind $computed_subtitle(template.config, tab_view.selected-page.child as <$GhosttyTab>.active-surface as <$GhosttySurface>.pwd) as <string>;
+ };
+
+ [start]
+ Adw.SplitButton {
+ clicked => $new_tab();
+ icon-name: "tab-new-symbolic";
+ tooltip-text: _("New Tab");
+ dropdown-tooltip: _("New Split");
+ menu-model: split_menu;
+ can-focus: false;
+ focus-on-click: false;
+ }
+
+ [end]
+ Gtk.Box {
+ Gtk.ToggleButton {
+ icon-name: "view-grid-symbolic";
+ tooltip-text: _("View Open Tabs");
+ active: bind tab_overview.open bidirectional;
+ can-focus: false;
+ focus-on-click: false;
+ }
+
+ Gtk.MenuButton {
+ notify::active => $notify_menu_active();
+ icon-name: "open-menu-symbolic";
+ menu-model: main_menu;
+ tooltip-text: _("Main Menu");
+ can-focus: false;
+ }
+ }
+ }
+
+ [top]
+ Adw.TabBar tab_bar {
+ autohide: bind template.tabs-autohide;
+ expand-tabs: bind template.tabs-wide;
+ view: tab_view;
+ visible: bind template.tabs-visible;
+
+ [start]
+ Gtk.Box {
+ orientation: horizontal;
+ visible: bind $titlebar_style_is_tabs(template.titlebar-style) as <bool>;
+
+ Gtk.WindowControls {
+ side: start;
+ }
+
+ Adw.SplitButton {
+ styles [
+ "flat",
+ ]
+
+ clicked => $new_tab();
+ icon-name: "tab-new-symbolic";
+ tooltip-text: _("New Tab");
+ dropdown-tooltip: _("New Split");
+ menu-model: split_menu;
+ can-focus: false;
+ focus-on-click: false;
+ }
+ }
+
+ [end]
+ Gtk.Box {
+ orientation: horizontal;
+ visible: bind $titlebar_style_is_tabs(template.titlebar-style) as <bool>;
+
+ Gtk.ToggleButton {
+ styles [
+ "flat",
+ ]
+
+ icon-name: "view-grid-symbolic";
+ tooltip-text: _("View Open Tabs");
+ active: bind tab_overview.open bidirectional;
+ can-focus: false;
+ focus-on-click: false;
+ }
+
+ Gtk.MenuButton {
+ styles [
+ "flat",
+ ]
+
+ notify::active => $notify_menu_active();
+ icon-name: "open-menu-symbolic";
+ menu-model: main_menu;
+ tooltip-text: _("Main Menu");
+ can-focus: false;
+ }
+
+ Gtk.WindowControls {
+ side: end;
+ }
+ }
+ }
+
+ Box {
+ orientation: vertical;
+
+ $GhosttyDebugWarning {
+ visible: bind template.debug;
+ }
+
+ Adw.ToastOverlay toast_overlay {
+ Adw.TabView tab_view {
+ notify::n-pages => $notify_n_pages();
+ notify::selected-page => $notify_selected_page();
+ close-page => $close_page();
+ page-attached => $page_attached();
+ page-detached => $page_detached();
+ create-window => $tab_create_window();
+ setup-menu => $setup_tab_menu();
+ menu-model: tab_context_menu;
+ shortcuts: none;
+ }
+ }
+ }
+ }
+ };
+}
+
+menu split_menu {
+ item {
+ label: _("Split Up");
+ action: "win.split-up";
+ }
+
+ item {
+ label: _("Split Down");
+ action: "win.split-down";
+ }
+
+ item {
+ label: _("Split Left");
+ action: "win.split-left";
+ }
+
+ item {
+ label: _("Split Right");
+ action: "win.split-right";
+ }
+}
+
+menu main_menu {
+ section {
+ item {
+ label: _("Copy");
+ action: "win.copy";
+ }
+
+ item {
+ label: _("Paste");
+ action: "win.paste";
+ }
+ }
+
+ section {
+ item {
+ label: _("New Window");
+ action: "win.new-window";
+ }
+
+ item {
+ label: _("Close Window");
+ action: "win.close";
+ }
+ }
+
+ section {
+ item {
+ label: _("Change Tab Title…");
+ action: "win.prompt-tab-title";
+ }
+
+ item {
+ label: _("New Tab");
+ action: "win.new-tab";
+ }
+
+ item {
+ label: _("Close Tab");
+ action: "win.close-tab";
+ target: "this";
+ }
+ }
+
+ section {
+ submenu {
+ label: _("Split");
+
+ item {
+ label: _("Change Title…");
+ action: "win.prompt-surface-title";
+ }
+
+ item {
+ label: _("Split Up");
+ action: "win.split-up";
+ }
+
+ item {
+ label: _("Split Down");
+ action: "win.split-down";
+ }
+
+ item {
+ label: _("Split Left");
+ action: "win.split-left";
+ }
+
+ item {
+ label: _("Split Right");
+ action: "win.split-right";
+ }
+ }
+ }
+
+ section {
+ item {
+ label: _("Clear");
+ action: "win.clear";
+ }
+
+ item {
+ label: _("Reset");
+ action: "win.reset";
+ }
+ }
+
+ section {
+ item {
+ label: _("Command Palette");
+ action: "win.toggle-command-palette";
+ }
+
+ item {
+ label: _("Terminal Inspector");
+ action: "win.toggle-inspector";
+ }
+
+ item {
+ label: _("Open Configuration");
+ action: "app.open-config";
+ }
+
+ item {
+ label: _("Reload Configuration");
+ action: "app.reload-config";
+ }
+ }
+
+ section {
+ item {
+ label: _("About Ghostty");
+ action: "win.about";
+ }
+
+ item {
+ label: _("Quit");
+ action: "app.quit";
+ }
+ }
+}
+
+menu tab_context_menu {
+ item {
+ label: _("Change Tab Title…");
+ action: "win.prompt-context-tab-title";
+ }
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/weak_ref.zig b/vendor/libghostty-vt/src/apprt/gtk/weak_ref.zig
new file mode 100644
index 00000000..f689e45f
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/weak_ref.zig
@@ -0,0 +1,44 @@
+const std = @import("std");
+const gtk = @import("gtk");
+const gobject = @import("gobject");
+
+/// A lightweight wrapper around gobject.WeakRef to make it type-safe
+/// to hold a single type of value.
+pub fn WeakRef(comptime T: type) type {
+ return struct {
+ const Self = @This();
+
+ ref: gobject.WeakRef = std.mem.zeroes(gobject.WeakRef),
+
+ pub const empty: Self = .{};
+
+ /// Set the weak reference to the given object. This will not
+ /// increase the reference count of the object.
+ pub fn set(self: *Self, v_: ?*T) void {
+ if (v_) |v| {
+ self.ref.set(v.as(gobject.Object));
+ } else {
+ self.ref.set(null);
+ }
+ }
+
+ /// Get a strong reference to the object, or null if the object
+ /// has been finalized. This increases the reference count by one.
+ pub fn get(self: *Self) ?*T {
+ // We can't use `as` because `as` guarantees conversion and
+ // that can't be statically guaranteed.
+ return gobject.ext.cast(T, self.ref.get() orelse return null);
+ }
+ };
+}
+
+test WeakRef {
+ const testing = std.testing;
+
+ var ref: WeakRef(gtk.TextBuffer) = .empty;
+ const obj: *gtk.TextBuffer = .new(null);
+ ref.set(obj);
+ ref.get().?.unref(); // The "?" asserts non-null
+ obj.unref();
+ try testing.expect(ref.get() == null);
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/winproto.zig b/vendor/libghostty-vt/src/apprt/gtk/winproto.zig
new file mode 100644
index 00000000..d409d6c2
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/winproto.zig
@@ -0,0 +1,155 @@
+const std = @import("std");
+const build_options = @import("build_options");
+const Allocator = std.mem.Allocator;
+
+const gdk = @import("gdk");
+
+const Config = @import("../../config.zig").Config;
+const input = @import("../../input.zig");
+const key = @import("key.zig");
+const ApprtWindow = @import("class/window.zig").Window;
+
+pub const noop = @import("winproto/noop.zig");
+pub const x11 = @import("winproto/x11.zig");
+pub const wayland = @import("winproto/wayland.zig");
+
+pub const Protocol = enum {
+ none,
+ wayland,
+ x11,
+};
+
+/// App-state for the underlying windowing protocol. There should be one
+/// instance of this struct per application.
+pub const App = union(Protocol) {
+ none: noop.App,
+ wayland: if (build_options.wayland) wayland.App else noop.App,
+ x11: if (build_options.x11) x11.App else noop.App,
+
+ pub fn init(
+ alloc: Allocator,
+ gdk_display: *gdk.Display,
+ app_id: [:0]const u8,
+ config: *const Config,
+ ) !App {
+ inline for (@typeInfo(App).@"union".fields) |field| {
+ if (try field.type.init(
+ alloc,
+ gdk_display,
+ app_id,
+ config,
+ )) |v| {
+ return @unionInit(App, field.name, v);
+ }
+ }
+
+ return .{ .none = .{} };
+ }
+
+ pub fn deinit(self: *App) void {
+ switch (self.*) {
+ inline else => |*v| v.deinit(),
+ }
+ }
+
+ pub fn eventMods(
+ self: *App,
+ device: ?*gdk.Device,
+ gtk_mods: gdk.ModifierType,
+ ) input.Mods {
+ return switch (self.*) {
+ inline else => |*v| v.eventMods(device, gtk_mods),
+ } orelse key.translateMods(gtk_mods);
+ }
+
+ pub fn supportsQuickTerminal(self: App) bool {
+ return switch (self) {
+ inline else => |v| v.supportsQuickTerminal(),
+ };
+ }
+
+ /// Set up necessary support for the quick terminal that must occur
+ /// *before* the window-level winproto object is created.
+ ///
+ /// Only has an effect on the Wayland backend, where the gtk4-layer-shell
+ /// library is initialized.
+ pub fn initQuickTerminal(self: *App, apprt_window: *ApprtWindow) !void {
+ switch (self.*) {
+ inline else => |*v| try v.initQuickTerminal(apprt_window),
+ }
+ }
+};
+
+/// Per-Window state for the underlying windowing protocol.
+///
+/// In Wayland, the terminology used is "Surface" and for it, this is
+/// really "Surface"-specific state. But Ghostty uses the term "Surface"
+/// heavily to mean something completely different, so we use "Window" here
+/// to better match what it generally maps to in the Ghostty codebase.
+pub const Window = union(Protocol) {
+ none: noop.Window,
+ wayland: if (build_options.wayland) wayland.Window else noop.Window,
+ x11: if (build_options.x11) x11.Window else noop.Window,
+
+ pub fn init(
+ alloc: Allocator,
+ app: *App,
+ apprt_window: *ApprtWindow,
+ ) !Window {
+ return switch (app.*) {
+ inline else => |*v, tag| {
+ inline for (@typeInfo(Window).@"union".fields) |field| {
+ if (comptime std.mem.eql(
+ u8,
+ field.name,
+ @tagName(tag),
+ )) return @unionInit(
+ Window,
+ field.name,
+ try field.type.init(
+ alloc,
+ v,
+ apprt_window,
+ ),
+ );
+ }
+ },
+ };
+ }
+
+ pub fn deinit(self: *Window) void {
+ switch (self.*) {
+ inline else => |*v| v.deinit(),
+ }
+ }
+
+ pub fn resizeEvent(self: *Window) !void {
+ switch (self.*) {
+ inline else => |*v| try v.resizeEvent(),
+ }
+ }
+
+ pub fn syncAppearance(self: *Window) !void {
+ switch (self.*) {
+ inline else => |*v| try v.syncAppearance(),
+ }
+ }
+
+ pub fn clientSideDecorationEnabled(self: Window) bool {
+ return switch (self) {
+ inline else => |v| v.clientSideDecorationEnabled(),
+ };
+ }
+
+ pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
+ switch (self.*) {
+ inline else => |*v| try v.addSubprocessEnv(env),
+ }
+ }
+
+ pub fn setUrgent(self: *Window, urgent: bool) !void {
+ switch (self.*) {
+ inline else => |*v| try v.setUrgent(urgent),
+ }
+ }
+};
diff --git a/vendor/libghostty-vt/src/apprt/gtk/winproto/BlurRegion.zig b/vendor/libghostty-vt/src/apprt/gtk/winproto/BlurRegion.zig
new file mode 100644
index 00000000..f3041dae
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/winproto/BlurRegion.zig
@@ -0,0 +1,187 @@
+const BlurRegion = @This();
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+const gobject = @import("gobject");
+const gdk = @import("gdk");
+const gtk = @import("gtk");
+
+const Window = @import("../winproto.zig").Window;
+const ApprtWindow = @import("../class/window.zig").Window;
+
+slices: std.ArrayList(Slice),
+
+/// A rectangular slice of the blur region.
+// Marked `extern` since we want to be able to use this in X11 directly.
+pub const Slice = extern struct {
+ x: Pos,
+ y: Pos,
+ width: Pos,
+ height: Pos,
+};
+
+// X11 compatibility. Ideally this should just be an `i32` like Wayland,
+// but XLib sucks
+const Pos = c_long;
+
+pub const empty: BlurRegion = .{
+ .slices = .empty,
+};
+
+pub fn deinit(self: *BlurRegion, alloc: Allocator) void {
+ self.slices.deinit(alloc);
+ self.slices = .empty;
+}
+
+// Calculate the blur regions for a window.
+//
+// Since we have rounded corners by default, we need to carve out the
+// pixels on each corner to avoid the "korners bug".
+// (cf. https://github.com/cutefishos/fishui/blob/41d4ba194063a3c7fff4675619b57e6ac0504f06/src/platforms/linux/blurhelper/windowblur.cpp#L134)
+pub fn calcForWindow(
+ alloc: Allocator,
+ window: *ApprtWindow,
+ csd: bool,
+ to_device_coordinates: bool,
+) Allocator.Error!BlurRegion {
+ const native = window.as(gtk.Native);
+ const surface = native.getSurface() orelse return .empty;
+
+ var slices: std.ArrayList(Slice) = .empty;
+ errdefer slices.deinit(alloc);
+
+ // Calculate the primary blur region
+ // (the one that covers most of the screen).
+ // It's easier to do this inside a vector since we have to scale
+ // everything by the scale factor anyways.
+
+ // NOTE(pluiedev): CSDs are a f--king mistake.
+ // Please, GNOME, stop this nonsense of making a window ~30% bigger
+ // internally than how they really are just for your shadows and
+ // rounded corners and all that fluff. Please. I beg of you.
+ const x: Pos, const y: Pos = off: {
+ var x: f64 = 0;
+ var y: f64 = 0;
+ native.getSurfaceTransform(&x, &y);
+ // Slightly inset the corners if we're using CSDs
+ if (csd) {
+ x += 1;
+ y += 1;
+ }
+ break :off .{ @intFromFloat(x), @intFromFloat(y) };
+ };
+
+ var width = @as(Pos, surface.getWidth());
+ var height = @as(Pos, surface.getHeight());
+
+ // Trim off the offsets. Be careful not to get negative.
+ width -= x * 2;
+ height -= y * 2;
+ if (width <= 0 or height <= 0) return .empty;
+
+ // Empirically determined.
+ const are_corners_rounded = rounded: {
+ // This cast should always succeed as all of our windows
+ // should be toplevel. If this fails, something very strange
+ // is going on.
+ const toplevel = gobject.ext.cast(
+ gdk.Toplevel,
+ surface,
+ ) orelse break :rounded false;
+
+ const state = toplevel.getState();
+ if (state.fullscreen or state.maximized or state.tiled)
+ break :rounded false;
+
+ break :rounded csd;
+ };
+
+ const new_slices = try approxRoundedRect(
+ alloc,
+ x,
+ y,
+ width,
+ height,
+ // See https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/css-variables.html#window-radius
+ if (are_corners_rounded) 15 else 0,
+ );
+
+ if (to_device_coordinates) {
+ // Transform surface coordinates to device coordinates.
+ const sf = surface.getScaleFactor();
+ for (new_slices.items) |*s| {
+ s.x *= sf;
+ s.y *= sf;
+ s.width *= sf;
+ s.height *= sf;
+ }
+ }
+
+ return .{ .slices = new_slices };
+}
+
+/// Whether two sets of blur regions are equal.
+pub fn eql(self: BlurRegion, other: BlurRegion) bool {
+ if (self.slices.items.len != other.slices.items.len) return false;
+ for (self.slices.items, other.slices.items) |this, that| {
+ if (!std.meta.eql(this, that)) return false;
+ }
+ return true;
+}
+
+/// Approximate a rounded rectangle with many smaller rectangles.
+fn approxRoundedRect(
+ alloc: Allocator,
+ x: Pos,
+ y: Pos,
+ width: Pos,
+ height: Pos,
+ radius: Pos,
+) Allocator.Error!std.ArrayList(Slice) {
+ const r_f: f32 = @floatFromInt(radius);
+
+ var slices: std.ArrayList(Slice) = .empty;
+ errdefer slices.deinit(alloc);
+
+ // Add the central rectangle
+ try slices.append(alloc, .{
+ .x = x,
+ .y = y + radius,
+ .width = width,
+ .height = height - 2 * radius,
+ });
+
+ // Add the corner rows. This is honestly quite cursed.
+ var row: Pos = 0;
+ while (row < radius) : (row += 1) {
+ // y distance from this row to the center corner circle
+ const dy = @as(f32, @floatFromInt(radius - row)) - 0.5;
+
+ // x distance - as given by the definition of a circle
+ const dx = @sqrt(r_f * r_f - dy * dy);
+
+ // How much each row should be offset, rounded to an integer
+ const row_x: Pos = @intFromFloat(r_f - @round(dx + 0.5));
+
+ // Remove the offset from both ends
+ const row_w = width - 2 * row_x;
+
+ // Top slice
+ try slices.append(alloc, .{
+ .x = x + row_x,
+ .y = y + row,
+ .width = row_w,
+ .height = 1,
+ });
+
+ // Bottom slice
+ try slices.append(alloc, .{
+ .x = x + row_x,
+ .y = y + height - 1 - row,
+ .width = row_w,
+ .height = 1,
+ });
+ }
+
+ return slices;
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/winproto/noop.zig b/vendor/libghostty-vt/src/apprt/gtk/winproto/noop.zig
new file mode 100644
index 00000000..950ee0f3
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/winproto/noop.zig
@@ -0,0 +1,73 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+const gdk = @import("gdk");
+
+const Config = @import("../../../config.zig").Config;
+const input = @import("../../../input.zig");
+const ApprtWindow = @import("../class/window.zig").Window;
+
+const log = std.log.scoped(.winproto_noop);
+
+pub const App = struct {
+ pub fn init(
+ _: Allocator,
+ _: *gdk.Display,
+ _: [:0]const u8,
+ _: *const Config,
+ ) !?App {
+ return null;
+ }
+
+ pub fn deinit(self: *App) void {
+ _ = self;
+ }
+
+ pub fn eventMods(
+ _: *App,
+ _: ?*gdk.Device,
+ _: gdk.ModifierType,
+ ) ?input.Mods {
+ return null;
+ }
+
+ pub fn supportsQuickTerminal(_: App) bool {
+ return false;
+ }
+ pub fn initQuickTerminal(_: *App, _: *ApprtWindow) !void {}
+};
+
+pub const Window = struct {
+ pub fn init(
+ _: Allocator,
+ _: *App,
+ _: *ApprtWindow,
+ ) !Window {
+ return .{};
+ }
+
+ pub fn deinit(self: *Window) void {
+ _ = self;
+ }
+
+ pub fn updateConfigEvent(
+ _: *Window,
+ _: *const ApprtWindow.DerivedConfig,
+ ) !void {}
+
+ pub fn resizeEvent(_: *Window) !void {}
+
+ pub fn syncAppearance(_: *Window) !void {}
+
+ /// This returns true if CSD is enabled for this window. This
+ /// should be the actual present state of the window, not the
+ /// desired state.
+ pub fn clientSideDecorationEnabled(self: Window) bool {
+ _ = self;
+ return true;
+ }
+
+ pub fn addSubprocessEnv(_: *Window, _: *std.process.EnvMap) !void {}
+
+ pub fn setUrgent(_: *Window, _: bool) !void {}
+};
diff --git a/vendor/libghostty-vt/src/apprt/gtk/winproto/wayland.zig b/vendor/libghostty-vt/src/apprt/gtk/winproto/wayland.zig
new file mode 100644
index 00000000..12c7fb8a
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/winproto/wayland.zig
@@ -0,0 +1,486 @@
+//! Wayland protocol implementation for the Ghostty GTK apprt.
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+const gdk = @import("gdk");
+const gdk_wayland = @import("gdk_wayland");
+const gobject = @import("gobject");
+const gtk = @import("gtk");
+const layer_shell = @import("gtk4-layer-shell");
+
+const wayland = @import("wayland");
+const wl = wayland.client.wl;
+const ext = wayland.client.ext;
+const kde = wayland.client.kde;
+const org = wayland.client.org;
+const xdg = wayland.client.xdg;
+
+const Config = @import("../../../config.zig").Config;
+const Globals = @import("wayland/Globals.zig");
+const input = @import("../../../input.zig");
+const ApprtWindow = @import("../class/window.zig").Window;
+const BlurRegion = @import("BlurRegion.zig");
+
+const log = std.log.scoped(.winproto_wayland);
+
+/// Wayland state that contains application-wide Wayland objects (e.g. wl_display).
+pub const App = struct {
+ display: *wl.Display,
+ globals: *Globals,
+
+ pub fn init(
+ alloc: Allocator,
+ gdk_display: *gdk.Display,
+ app_id: [:0]const u8,
+ config: *const Config,
+ ) !?App {
+ _ = config;
+ _ = app_id;
+
+ const gdk_wayland_display = gobject.ext.cast(
+ gdk_wayland.WaylandDisplay,
+ gdk_display,
+ ) orelse return null;
+
+ const display: *wl.Display = @ptrCast(@alignCast(
+ gdk_wayland_display.getWlDisplay() orelse return error.NoWaylandDisplay,
+ ));
+
+ const globals: *Globals = try .init(alloc, display);
+ errdefer globals.deinit();
+
+ return .{
+ .display = display,
+ .globals = globals,
+ };
+ }
+
+ pub fn deinit(self: *App) void {
+ self.globals.deinit();
+ }
+
+ pub fn eventMods(
+ _: *App,
+ _: ?*gdk.Device,
+ _: gdk.ModifierType,
+ ) ?input.Mods {
+ return null;
+ }
+
+ pub fn supportsQuickTerminal(self: App) bool {
+ _ = self;
+ if (!layer_shell.isSupported()) {
+ log.warn("your compositor does not support the wlr-layer-shell protocol; disabling quick terminal", .{});
+ return false;
+ }
+
+ return true;
+ }
+
+ pub fn initQuickTerminal(self: *App, apprt_window: *ApprtWindow) !void {
+ const window = apprt_window.as(gtk.Window);
+ layer_shell.initForWindow(window);
+
+ // Set target monitor based on config (null lets compositor decide)
+ const monitor = resolveQuickTerminalMonitor(self.globals, apprt_window);
+ defer if (monitor) |v| v.unref();
+ layer_shell.setMonitor(window, monitor);
+ }
+};
+
+/// Per-window (wl_surface) state for the Wayland protocol.
+pub const Window = struct {
+ apprt_window: *ApprtWindow,
+
+ /// The Wayland surface for this window.
+ surface: *wl.Surface,
+
+ /// The context from the app where we can load our Wayland interfaces.
+ globals: *Globals,
+
+ /// Object that controls background effects like background blur.
+ bg_effect: ?*ext.BackgroundEffectSurfaceV1 = null,
+
+ /// Object that controls the decoration mode (client/server/auto)
+ /// of the window.
+ decoration: ?*org.KdeKwinServerDecoration = null,
+
+ /// Object that controls the slide-in/slide-out animations of the
+ /// quick terminal. Always null for windows other than the quick terminal.
+ slide: ?*org.KdeKwinSlide = null,
+
+ /// Object that, when present, denotes that the window is currently
+ /// requesting attention from the user.
+ activation_token: ?*xdg.ActivationTokenV1 = null,
+
+ blur_region: BlurRegion = .empty,
+
+ pub fn init(
+ alloc: Allocator,
+ app: *App,
+ apprt_window: *ApprtWindow,
+ ) !Window {
+ _ = alloc;
+
+ const gtk_native = apprt_window.as(gtk.Native);
+ const gdk_surface = gtk_native.getSurface() orelse return error.NotWaylandSurface;
+
+ // This should never fail, because if we're being called at this point
+ // then we've already asserted that our app state is Wayland.
+ const gdk_wl_surface = gobject.ext.cast(
+ gdk_wayland.WaylandSurface,
+ gdk_surface,
+ ) orelse return error.NoWaylandSurface;
+
+ const wl_surface: *wl.Surface = @ptrCast(@alignCast(
+ gdk_wl_surface.getWlSurface() orelse return error.NoWaylandSurface,
+ ));
+
+ // Get our decoration object so we can control the
+ // CSD vs SSD status of this surface.
+ const deco: ?*org.KdeKwinServerDecoration = deco: {
+ const mgr = app.globals.get(.kde_decoration_manager) orelse
+ break :deco null;
+
+ const deco: *org.KdeKwinServerDecoration = mgr.create(
+ wl_surface,
+ ) catch |err| {
+ log.warn("could not create decoration object={}", .{err});
+ break :deco null;
+ };
+
+ break :deco deco;
+ };
+
+ const bg_effect: ?*ext.BackgroundEffectSurfaceV1 = bg: {
+ const mgr = app.globals.get(.ext_background_effect) orelse
+ break :bg null;
+
+ const bg_effect: *ext.BackgroundEffectSurfaceV1 = mgr.getBackgroundEffect(
+ wl_surface,
+ ) catch |err| {
+ log.warn("could not create background effect object={}", .{err});
+ break :bg null;
+ };
+
+ break :bg bg_effect;
+ };
+
+ if (apprt_window.isQuickTerminal()) {
+ _ = gdk.Surface.signals.enter_monitor.connect(
+ gdk_surface,
+ *ApprtWindow,
+ enteredMonitor,
+ apprt_window,
+ .{},
+ );
+ }
+
+ return .{
+ .apprt_window = apprt_window,
+ .surface = wl_surface,
+ .globals = app.globals,
+ .decoration = deco,
+ .bg_effect = bg_effect,
+ };
+ }
+
+ pub fn deinit(self: *Window) void {
+ self.blur_region.deinit(self.globals.alloc);
+ if (self.bg_effect) |bg| bg.destroy();
+ if (self.decoration) |deco| deco.release();
+ if (self.slide) |slide| slide.release();
+ }
+
+ pub fn resizeEvent(self: *Window) !void {
+ self.syncBlur() catch |err| {
+ log.err("failed to sync blur={}", .{err});
+ };
+ }
+
+ pub fn syncAppearance(self: *Window) !void {
+ self.syncBlur() catch |err| {
+ log.err("failed to sync blur={}", .{err});
+ };
+ self.syncDecoration() catch |err| {
+ log.err("failed to sync decoration={}", .{err});
+ };
+
+ if (self.apprt_window.isQuickTerminal()) {
+ self.syncQuickTerminal() catch |err| {
+ log.warn("failed to sync quick terminal appearance={}", .{err});
+ };
+ }
+ }
+
+ pub fn clientSideDecorationEnabled(self: Window) bool {
+ return switch (self.getDecorationMode()) {
+ .Client => true,
+ // If we support SSDs, then we should *not* enable CSDs if we prefer SSDs.
+ // However, if we do not support SSDs (e.g. GNOME) then we should enable
+ // CSDs even if the user prefers SSDs.
+ .Server => if (self.globals.get(.kde_decoration_manager)) |_| false else true,
+ .None => false,
+ else => unreachable,
+ };
+ }
+
+ pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
+ _ = self;
+ _ = env;
+ }
+
+ pub fn setUrgent(self: *Window, urgent: bool) !void {
+ const activation = self.globals.get(.xdg_activation) orelse return;
+
+ // If there already is a token, destroy and unset it
+ if (self.activation_token) |token| token.destroy();
+
+ self.activation_token = if (urgent) token: {
+ const token = try activation.getActivationToken();
+ token.setSurface(self.surface);
+ token.setListener(*Window, onActivationTokenEvent, self);
+ token.commit();
+ break :token token;
+ } else null;
+ }
+
+ /// Update the blur state of the window.
+ fn syncBlur(self: *Window) !void {
+ const compositor = self.globals.get(.compositor) orelse return;
+ const bg = self.bg_effect orelse return;
+ if (!self.globals.state.bg_effect_capabilities.blur) return;
+
+ const config = if (self.apprt_window.getConfig()) |v|
+ v.get()
+ else
+ return;
+ const blur = config.@"background-blur";
+
+ if (!blur.enabled()) {
+ self.blur_region.deinit(self.globals.alloc);
+ bg.setBlurRegion(null);
+ return;
+ }
+
+ var region: BlurRegion = try .calcForWindow(
+ self.globals.alloc,
+ self.apprt_window,
+ self.clientSideDecorationEnabled(),
+ false,
+ );
+ errdefer region.deinit(self.globals.alloc);
+
+ if (region.eql(self.blur_region)) {
+ // Region didn't change. Don't do anything.
+ region.deinit(self.globals.alloc);
+ return;
+ }
+
+ const wl_region = try compositor.createRegion();
+ errdefer if (wl_region) |r| r.destroy();
+ for (region.slices.items) |s| wl_region.add(
+ @intCast(s.x),
+ @intCast(s.y),
+ @intCast(s.width),
+ @intCast(s.height),
+ );
+
+ bg.setBlurRegion(wl_region);
+ self.blur_region = region;
+ }
+
+ fn syncDecoration(self: *Window) !void {
+ const deco = self.decoration orelse return;
+
+ // The protocol requests uint instead of enum so we have
+ // to convert it.
+ deco.requestMode(@intCast(@intFromEnum(self.getDecorationMode())));
+ }
+
+ fn getDecorationMode(self: Window) org.KdeKwinServerDecorationManager.Mode {
+ return switch (self.apprt_window.getWindowDecoration()) {
+ .auto => self.globals.state.default_deco_mode orelse .Client,
+ .client => .Client,
+ .server => .Server,
+ .none => .None,
+ };
+ }
+
+ fn syncQuickTerminal(self: *Window) !void {
+ const window = self.apprt_window.as(gtk.Window);
+ const config = if (self.apprt_window.getConfig()) |v|
+ v.get()
+ else
+ return;
+
+ layer_shell.setLayer(window, switch (config.@"gtk-quick-terminal-layer") {
+ .overlay => .overlay,
+ .top => .top,
+ .bottom => .bottom,
+ .background => .background,
+ });
+ layer_shell.setNamespace(window, config.@"gtk-quick-terminal-namespace");
+
+ // Re-resolve the target monitor on every sync so that config reloads
+ // and primary-output changes take effect without recreating the window.
+ const target_monitor = resolveQuickTerminalMonitor(self.globals, self.apprt_window);
+ defer if (target_monitor) |v| v.unref();
+ layer_shell.setMonitor(window, target_monitor);
+
+ layer_shell.setKeyboardMode(
+ window,
+ switch (config.@"quick-terminal-keyboard-interactivity") {
+ .none => .none,
+ .@"on-demand" => on_demand: {
+ if (layer_shell.getProtocolVersion() < 4) {
+ log.warn("your compositor does not support on-demand keyboard access; falling back to exclusive access", .{});
+ break :on_demand .exclusive;
+ }
+ break :on_demand .on_demand;
+ },
+ .exclusive => .exclusive,
+ },
+ );
+
+ const anchored_edge: ?layer_shell.ShellEdge = switch (config.@"quick-terminal-position") {
+ .left => .left,
+ .right => .right,
+ .top => .top,
+ .bottom => .bottom,
+ .center => null,
+ };
+
+ for (std.meta.tags(layer_shell.ShellEdge)) |edge| {
+ if (anchored_edge) |anchored| {
+ if (edge == anchored) {
+ layer_shell.setMargin(window, edge, 0);
+ layer_shell.setAnchor(window, edge, true);
+ continue;
+ }
+ }
+
+ // Arbitrary margin - could be made customizable?
+ layer_shell.setMargin(window, edge, 20);
+ layer_shell.setAnchor(window, edge, false);
+ }
+
+ if (self.slide) |slide| slide.release();
+
+ self.slide = if (anchored_edge) |anchored| slide: {
+ const mgr = self.globals.get(.kde_slide_manager) orelse break :slide null;
+
+ const slide = mgr.create(self.surface) catch |err| {
+ log.warn("could not create slide object={}", .{err});
+ break :slide null;
+ };
+
+ const slide_location: org.KdeKwinSlide.Location = switch (anchored) {
+ .top => .top,
+ .bottom => .bottom,
+ .left => .left,
+ .right => .right,
+ };
+
+ slide.setLocation(@intCast(@intFromEnum(slide_location)));
+ slide.commit();
+ break :slide slide;
+ } else null;
+ }
+
+ /// Update the size of the quick terminal based on monitor dimensions.
+ fn enteredMonitor(
+ _: *gdk.Surface,
+ monitor: *gdk.Monitor,
+ apprt_window: *ApprtWindow,
+ ) callconv(.c) void {
+ const window = apprt_window.as(gtk.Window);
+ const config = if (apprt_window.getConfig()) |v| v.get() else return;
+
+ const resolved_monitor = resolveQuickTerminalMonitor(
+ apprt_window.winproto().wayland.globals,
+ apprt_window,
+ );
+ defer if (resolved_monitor) |v| v.unref();
+
+ // Use the configured monitor for sizing if not in mouse mode.
+ const size_monitor = resolved_monitor orelse monitor;
+
+ var monitor_size: gdk.Rectangle = undefined;
+ size_monitor.getGeometry(&monitor_size);
+
+ const dims = config.@"quick-terminal-size".calculate(
+ config.@"quick-terminal-position",
+ .{
+ .width = @intCast(monitor_size.f_width),
+ .height = @intCast(monitor_size.f_height),
+ },
+ );
+
+ window.setDefaultSize(@intCast(dims.width), @intCast(dims.height));
+ }
+
+ fn onActivationTokenEvent(
+ token: *xdg.ActivationTokenV1,
+ event: xdg.ActivationTokenV1.Event,
+ self: *Window,
+ ) void {
+ const activation = self.globals.get(.xdg_activation) orelse return;
+ const current_token = self.activation_token orelse return;
+
+ if (token.getId() != current_token.getId()) {
+ log.warn("received event for unknown activation token; ignoring", .{});
+ return;
+ }
+
+ switch (event) {
+ .done => |done| {
+ activation.activate(done.token, self.surface);
+ token.destroy();
+ self.activation_token = null;
+ },
+ }
+ }
+};
+
+/// Resolve the quick-terminal-screen config to a specific monitor.
+/// Returns null to let the compositor decide (used for .mouse mode).
+/// Caller owns the returned ref and must unref it.
+fn resolveQuickTerminalMonitor(
+ globals: *Globals,
+ apprt_window: *ApprtWindow,
+) ?*gdk.Monitor {
+ const config = if (apprt_window.getConfig()) |v| v.get() else return null;
+
+ switch (config.@"quick-terminal-screen") {
+ .mouse => return null,
+ .main, .@"macos-menu-bar" => {},
+ }
+
+ const display = apprt_window.as(gtk.Widget).getDisplay();
+ const monitors = display.getMonitors();
+
+ // Try to find the monitor matching the primary output name.
+ if (globals.state.primary_output_name) |stored_name| {
+ var i: u32 = 0;
+ while (monitors.getObject(i)) |item| : (i += 1) {
+ const monitor = gobject.ext.cast(gdk.Monitor, item) orelse {
+ item.unref();
+ continue;
+ };
+ if (monitor.getConnector()) |connector_z| {
+ if (std.mem.orderZ(u8, connector_z, stored_name) == .eq) {
+ return monitor;
+ }
+ }
+ monitor.unref();
+ }
+ }
+
+ // Fall back to the first monitor in the list.
+ const first = monitors.getObject(0) orelse return null;
+ return gobject.ext.cast(gdk.Monitor, first) orelse {
+ first.unref();
+ return null;
+ };
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/winproto/wayland/Globals.zig b/vendor/libghostty-vt/src/apprt/gtk/winproto/wayland/Globals.zig
new file mode 100644
index 00000000..83052cbe
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/winproto/wayland/Globals.zig
@@ -0,0 +1,252 @@
+const Globals = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+const wayland = @import("wayland");
+const wl = wayland.client.wl;
+const ext = wayland.client.ext;
+const kde = wayland.client.kde;
+const org = wayland.client.org;
+const xdg = wayland.client.xdg;
+
+const log = std.log.scoped(.winproto_wayland_globals);
+
+alloc: Allocator,
+state: State,
+map: std.EnumMap(Tag, Binding),
+
+/// Used in the initial roundtrip to determine whether more
+/// roundtrips are required to fetch the initial state.
+needs_roundtrip: bool = false,
+
+const Binding = struct {
+ // All globals can be casted into a wl.Proxy object.
+ proxy: *wl.Proxy,
+ name: u32,
+};
+
+pub const Tag = enum {
+ compositor,
+ ext_background_effect,
+ kde_decoration_manager,
+ kde_slide_manager,
+ kde_output_order,
+ xdg_activation,
+
+ fn Type(comptime self: Tag) type {
+ return switch (self) {
+ .compositor => wl.Compositor,
+ .ext_background_effect => ext.BackgroundEffectManagerV1,
+ .kde_decoration_manager => org.KdeKwinServerDecorationManager,
+ .kde_slide_manager => org.KdeKwinSlideManager,
+ .kde_output_order => kde.OutputOrderV1,
+ .xdg_activation => xdg.ActivationV1,
+ };
+ }
+};
+
+pub const State = struct {
+ /// Connector name of the primary output (e.g., "DP-1") as reported
+ /// by kde_output_order_v1. The first output in each priority list
+ /// is the primary.
+ primary_output_name: ?[:0]const u8 = null,
+
+ /// Tracks the output order event cycle. Set to true after a `done`
+ /// event so the next `output` event is captured as the new primary.
+ /// Initialized to true so the first event after binding is captured.
+ output_order_done: bool = true,
+
+ default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null,
+
+ bg_effect_capabilities: ext.BackgroundEffectManagerV1.Capability = .{},
+
+ /// Reset cached state derived from kde_output_order_v1.
+ fn resetOutputOrder(self: *State, alloc: Allocator) void {
+ if (self.primary_output_name) |name| alloc.free(name);
+ self.primary_output_name = null;
+ self.output_order_done = true;
+ }
+};
+
+pub fn init(alloc: Allocator, display: *wl.Display) !*Globals {
+ // We need to allocate here since the listener
+ // expects a stable memory address.
+ const self = try alloc.create(Globals);
+ self.* = .{
+ .alloc = alloc,
+ .state = .{},
+ .map = .{},
+ };
+
+ const registry = try display.getRegistry();
+ registry.setListener(*Globals, registryListener, self);
+ if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
+
+ // Do another roundtrip to process events emitted by globals we bound
+ // during registry discovery (e.g. default decoration mode, output
+ // order). Listeners are installed at bind time in registryListener.
+ if (self.needs_roundtrip) {
+ if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
+ }
+ return self;
+}
+
+pub fn deinit(self: *Globals) void {
+ if (self.state.primary_output_name) |name| self.alloc.free(name);
+ self.alloc.destroy(self);
+}
+
+pub fn get(self: *const Globals, comptime tag: Tag) ?*tag.Type() {
+ const binding = self.map.get(tag) orelse return null;
+ return @ptrCast(binding.proxy);
+}
+
+fn onGlobalAttached(self: *Globals, comptime tag: Tag) void {
+ // Install listeners immediately at bind time. This
+ // keeps listener setup and object lifetime in one
+ // place and also supports globals that appear later.
+ switch (tag) {
+ .ext_background_effect => {
+ const v = self.get(tag) orelse return;
+ v.setListener(*Globals, bgEffectListener, self);
+ self.needs_roundtrip = true;
+ },
+ .kde_decoration_manager => {
+ const v = self.get(tag) orelse return;
+ v.setListener(*Globals, decoManagerListener, self);
+ self.needs_roundtrip = true;
+ },
+ .kde_output_order => {
+ const v = self.get(tag) orelse return;
+ v.setListener(*Globals, outputOrderListener, self);
+ self.needs_roundtrip = true;
+ },
+ else => {},
+ }
+}
+
+fn onGlobalRemoved(self: *Globals, tag: Tag) void {
+ switch (tag) {
+ .kde_output_order => self.state.resetOutputOrder(self.alloc),
+ else => {},
+ }
+}
+
+fn registryListener(
+ registry: *wl.Registry,
+ event: wl.Registry.Event,
+ self: *Globals,
+) void {
+ switch (event) {
+ .global => |v| {
+ log.debug("found global {s}", .{v.interface});
+ inline for (comptime std.meta.tags(Tag)) |tag| {
+ const T = tag.Type();
+ if (std.mem.orderZ(u8, v.interface, T.interface.name) == .eq) {
+ log.debug("matched {}", .{T});
+
+ const new_proxy = registry.bind(
+ v.name,
+ T,
+ T.generated_version,
+ ) catch |err| {
+ log.warn(
+ "error binding interface {s} error={}",
+ .{ v.interface, err },
+ );
+ return;
+ };
+
+ // If this global was already bound,
+ // then we also need to destroy the old binding.
+ if (self.map.get(tag)) |old| {
+ self.onGlobalRemoved(tag);
+ old.proxy.destroy();
+ }
+
+ self.map.put(tag, .{
+ .proxy = @ptrCast(new_proxy),
+ .name = v.name,
+ });
+ self.onGlobalAttached(tag);
+ }
+ }
+ },
+
+ // This should be a rare occurrence, but in case a global
+ // is suddenly no longer available, we destroy and unset it
+ // as the protocol mandates.
+ .global_remove => |v| {
+ var it = self.map.iterator();
+ while (it.next()) |kv| {
+ if (kv.value.name != v.name) continue;
+ self.onGlobalRemoved(kv.key);
+ kv.value.proxy.destroy();
+ self.map.remove(kv.key);
+ }
+ },
+ }
+}
+
+fn bgEffectListener(
+ _: *ext.BackgroundEffectManagerV1,
+ event: ext.BackgroundEffectManagerV1.Event,
+ self: *Globals,
+) void {
+ switch (event) {
+ .capabilities => |cap| {
+ self.state.bg_effect_capabilities = cap.flags;
+ },
+ }
+}
+
+fn decoManagerListener(
+ _: *org.KdeKwinServerDecorationManager,
+ event: org.KdeKwinServerDecorationManager.Event,
+ self: *Globals,
+) void {
+ switch (event) {
+ .default_mode => |mode| {
+ self.state.default_deco_mode = @enumFromInt(mode.mode);
+ },
+ }
+}
+
+fn outputOrderListener(
+ _: *kde.OutputOrderV1,
+ event: kde.OutputOrderV1.Event,
+ self: *Globals,
+) void {
+ switch (event) {
+ .output => |v| {
+ // Only the first output event after a `done` is the new primary.
+ if (!self.state.output_order_done) return;
+ self.state.output_order_done = false;
+
+ const name = std.mem.sliceTo(v.output_name, 0);
+ if (self.state.primary_output_name) |old| self.alloc.free(old);
+
+ if (name.len == 0) {
+ self.state.primary_output_name = null;
+ log.warn("ignoring empty primary output name from kde_output_order_v1", .{});
+ } else {
+ self.state.primary_output_name = self.alloc.dupeZ(u8, name) catch |err| {
+ self.state.primary_output_name = null;
+ log.warn("failed to allocate primary output name: {}", .{err});
+ return;
+ };
+ log.debug("primary output: {s}", .{name});
+ }
+ },
+ .done => {
+ if (self.state.output_order_done) {
+ // No output arrived since the previous done. Treat this as
+ // an empty update and drop any stale cached primary.
+ self.state.resetOutputOrder(self.alloc);
+ return;
+ }
+ self.state.output_order_done = true;
+ },
+ }
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/winproto/x11.zig b/vendor/libghostty-vt/src/apprt/gtk/winproto/x11.zig
new file mode 100644
index 00000000..8109959d
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/winproto/x11.zig
@@ -0,0 +1,498 @@
+//! X11 window protocol implementation for the Ghostty GTK apprt.
+const std = @import("std");
+const builtin = @import("builtin");
+const Allocator = std.mem.Allocator;
+
+const gdk = @import("gdk");
+const gdk_x11 = @import("gdk_x11");
+const glib = @import("glib");
+const gobject = @import("gobject");
+const gtk = @import("gtk");
+const xlib = @import("xlib");
+
+pub const c = @cImport({
+ @cInclude("X11/Xlib.h");
+ @cInclude("X11/Xatom.h");
+ @cInclude("X11/XKBlib.h");
+});
+
+const input = @import("../../../input.zig");
+const Config = @import("../../../config.zig").Config;
+const ApprtWindow = @import("../class/window.zig").Window;
+const BlurRegion = @import("BlurRegion.zig");
+
+const log = std.log.scoped(.gtk_x11);
+
+pub const App = struct {
+ display: *xlib.Display,
+ base_event_code: c_int,
+ atoms: Atoms,
+
+ pub fn init(
+ _: Allocator,
+ gdk_display: *gdk.Display,
+ app_id: [:0]const u8,
+ config: *const Config,
+ ) !?App {
+ // If the display isn't X11, then we don't need to do anything.
+ const gdk_x11_display = gobject.ext.cast(
+ gdk_x11.X11Display,
+ gdk_display,
+ ) orelse return null;
+
+ const xlib_display = gdk_x11_display.getXdisplay();
+
+ const x11_program_name: [:0]const u8 = if (config.@"x11-instance-name") |pn|
+ pn
+ else if (builtin.mode == .Debug)
+ "ghostty-debug"
+ else
+ "ghostty";
+
+ // Set the X11 window class property (WM_CLASS) if we are on an X11
+ // display.
+ //
+ // Note that we also set the program name here using g_set_prgname.
+ // This is how the instance name field for WM_CLASS is derived when
+ // calling gdk_x11_display_set_program_class; there does not seem to be
+ // a way to set it directly. It does not look like this is being set by
+ // our other app initialization routines currently, but since we're
+ // currently deriving its value from x11-instance-name effectively, I
+ // feel like gating it behind an X11 check is better intent.
+ //
+ // This makes the property show up like so when using xprop:
+ //
+ // WM_CLASS(STRING) = "ghostty", "com.mitchellh.ghostty"
+ //
+ // Append "-debug" on both when using the debug build.
+ glib.setPrgname(x11_program_name);
+ gdk_x11.X11Display.setProgramClass(gdk_display, app_id);
+
+ // XKB
+ log.debug("Xkb.init: initializing Xkb", .{});
+ log.debug("Xkb.init: running XkbQueryExtension", .{});
+ var opcode: c_int = 0;
+ var base_event_code: c_int = 0;
+ var base_error_code: c_int = 0;
+ var major = c.XkbMajorVersion;
+ var minor = c.XkbMinorVersion;
+ if (c.XkbQueryExtension(
+ @ptrCast(@alignCast(xlib_display)),
+ &opcode,
+ &base_event_code,
+ &base_error_code,
+ &major,
+ &minor,
+ ) == 0) {
+ log.err("Fatal: error initializing Xkb extension: error executing XkbQueryExtension", .{});
+ return error.XkbInitializationError;
+ }
+
+ log.debug("Xkb.init: running XkbSelectEventDetails", .{});
+ if (c.XkbSelectEventDetails(
+ @ptrCast(@alignCast(xlib_display)),
+ c.XkbUseCoreKbd,
+ c.XkbStateNotify,
+ c.XkbModifierStateMask,
+ c.XkbModifierStateMask,
+ ) == 0) {
+ log.err("Fatal: error initializing Xkb extension: error executing XkbSelectEventDetails", .{});
+ return error.XkbInitializationError;
+ }
+
+ return .{
+ .display = xlib_display,
+ .base_event_code = base_event_code,
+ .atoms = .init(gdk_x11_display),
+ };
+ }
+
+ pub fn deinit(self: *App) void {
+ _ = self;
+ }
+
+ /// Checks for an immediate pending XKB state update event, and returns the
+ /// keyboard state based on if it finds any. This is necessary as the
+ /// standard GTK X11 API (and X11 in general) does not include the current
+ /// key pressed in any modifier state snapshot for that event (e.g. if the
+ /// pressed key is a modifier, that is not necessarily reflected in the
+ /// modifiers).
+ ///
+ /// Returns null if there is no event. In this case, the caller should fall
+ /// back to the standard GDK modifier state (this likely means the key
+ /// event did not result in a modifier change).
+ pub fn eventMods(
+ self: App,
+ device: ?*gdk.Device,
+ gtk_mods: gdk.ModifierType,
+ ) ?input.Mods {
+ _ = device;
+ _ = gtk_mods;
+
+ // Shoutout to Mozilla for figuring out a clean way to do this, this is
+ // paraphrased from Firefox/Gecko in widget/gtk/nsGtkKeyUtils.cpp.
+ if (c.XEventsQueued(
+ @ptrCast(@alignCast(self.display)),
+ c.QueuedAfterReading,
+ ) == 0) return null;
+
+ var nextEvent: c.XEvent = undefined;
+ _ = c.XPeekEvent(@ptrCast(@alignCast(self.display)), &nextEvent);
+ if (nextEvent.type != self.base_event_code) return null;
+
+ const xkb_event: *c.XkbEvent = @ptrCast(&nextEvent);
+ if (xkb_event.any.xkb_type != c.XkbStateNotify) return null;
+
+ const xkb_state_notify_event: *c.XkbStateNotifyEvent = @ptrCast(xkb_event);
+ // Check the state according to XKB masks.
+ const lookup_mods = xkb_state_notify_event.lookup_mods;
+ var mods: input.Mods = .{};
+
+ log.debug("X11: found extra XkbStateNotify event w/lookup_mods: {b}", .{lookup_mods});
+ if (lookup_mods & c.ShiftMask != 0) mods.shift = true;
+ if (lookup_mods & c.ControlMask != 0) mods.ctrl = true;
+ if (lookup_mods & c.Mod1Mask != 0) mods.alt = true;
+ if (lookup_mods & c.Mod4Mask != 0) mods.super = true;
+ if (lookup_mods & c.LockMask != 0) mods.caps_lock = true;
+
+ return mods;
+ }
+
+ pub fn supportsQuickTerminal(_: App) bool {
+ log.warn("quick terminal is not yet supported on X11", .{});
+ return false;
+ }
+
+ pub fn initQuickTerminal(_: *App, _: *ApprtWindow) !void {}
+};
+
+pub const Window = struct {
+ app: *App,
+ apprt_window: *ApprtWindow,
+ x11_surface: *gdk_x11.X11Surface,
+ alloc: Allocator,
+
+ blur_region: BlurRegion = .empty,
+
+ // Cache last applied values to avoid redundant X11 property updates.
+ // Redundant property updates seem to cause some visual glitches
+ // with some window managers: https://github.com/ghostty-org/ghostty/pull/8075
+ last_applied_decoration_hints: ?MotifWMHints = null,
+
+ pub fn init(
+ alloc: Allocator,
+ app: *App,
+ apprt_window: *ApprtWindow,
+ ) !Window {
+ const surface = apprt_window.as(gtk.Native).getSurface() orelse
+ return error.NotX11Surface;
+
+ const x11_surface = gobject.ext.cast(
+ gdk_x11.X11Surface,
+ surface,
+ ) orelse return error.NotX11Surface;
+
+ return .{
+ .app = app,
+ .alloc = alloc,
+ .apprt_window = apprt_window,
+ .x11_surface = x11_surface,
+ };
+ }
+
+ pub fn deinit(self: *Window) void {
+ self.blur_region.deinit(self.alloc);
+ }
+
+ pub fn resizeEvent(self: *Window) !void {
+ // The blur region must update with window resizes
+ self.syncBlur() catch |err| {
+ log.err("failed to sync blur={}", .{err});
+ };
+ }
+
+ pub fn syncAppearance(self: *Window) !void {
+ // The user could have toggled between CSDs and SSDs,
+ // therefore we need to recalculate the blur region offset.
+ self.syncBlur() catch |err| {
+ log.err("failed to sync blur={}", .{err});
+ };
+ self.syncDecorations() catch |err| {
+ log.err("failed to sync decorations={}", .{err});
+ };
+ }
+
+ pub fn clientSideDecorationEnabled(self: Window) bool {
+ return switch (self.apprt_window.getWindowDecoration()) {
+ .auto, .client => true,
+ .server, .none => false,
+ };
+ }
+
+ fn syncBlur(self: *Window) !void {
+ const config = if (self.apprt_window.getConfig()) |v| v.get() else return;
+
+ // When blur is disabled, remove the property if it was previously set
+ const blur = config.@"background-blur";
+
+ var region: BlurRegion = if (blur.enabled())
+ try .calcForWindow(
+ self.alloc,
+ self.apprt_window,
+ self.clientSideDecorationEnabled(),
+ true,
+ )
+ else
+ .empty;
+ errdefer region.deinit(self.alloc);
+
+ // Only update X11 properties when the blur region actually changes
+ if (region.eql(self.blur_region)) {
+ region.deinit(self.alloc);
+ return;
+ }
+
+ if (region.slices.items.len > 0) {
+ log.debug("set blur={}, window xid={}, region={}", .{
+ blur,
+ self.x11_surface.getXid(),
+ region,
+ });
+
+ try self.changeProperty(
+ BlurRegion.Slice,
+ self.app.atoms.kde_blur,
+ c.XA_CARDINAL,
+ ._32,
+ .{ .mode = .replace },
+ region.slices.items,
+ );
+ } else {
+ try self.deleteProperty(self.app.atoms.kde_blur);
+ }
+
+ self.blur_region.deinit(self.alloc);
+ self.blur_region = region;
+ }
+
+ fn syncDecorations(self: *Window) !void {
+ var hints: MotifWMHints = .{};
+
+ self.getWindowProperty(
+ MotifWMHints,
+ self.app.atoms.motif_wm_hints,
+ self.app.atoms.motif_wm_hints,
+ ._32,
+ .{},
+ &hints,
+ ) catch |err| switch (err) {
+ // motif_wm_hints is already initialized, so this is fine
+ error.PropertyNotFound => {},
+
+ error.RequestFailed,
+ error.PropertyTypeMismatch,
+ error.PropertyFormatMismatch,
+ => return err,
+ };
+
+ hints.flags.decorations = true;
+ hints.decorations.all = switch (self.apprt_window.getWindowDecoration()) {
+ .server => true,
+ .auto, .client, .none => false,
+ };
+
+ // Only update decoration hints when they actually change
+ if (self.last_applied_decoration_hints) |last| {
+ if (std.meta.eql(hints, last)) return;
+ }
+
+ try self.changeProperty(
+ MotifWMHints,
+ self.app.atoms.motif_wm_hints,
+ self.app.atoms.motif_wm_hints,
+ ._32,
+ .{ .mode = .replace },
+ &.{hints},
+ );
+ self.last_applied_decoration_hints = hints;
+ }
+
+ pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
+ var buf: [64]u8 = undefined;
+ const window_id = try std.fmt.bufPrint(
+ &buf,
+ "{}",
+ .{self.x11_surface.getXid()},
+ );
+
+ try env.put("WINDOWID", window_id);
+ }
+
+ pub fn setUrgent(self: *Window, urgent: bool) !void {
+ self.x11_surface.setUrgencyHint(@intFromBool(urgent));
+ }
+
+ fn getWindowProperty(
+ self: *Window,
+ comptime T: type,
+ name: c.Atom,
+ typ: c.Atom,
+ comptime format: PropertyFormat,
+ options: struct {
+ offset: c_long = 0,
+ length: c_long = std.math.maxInt(c_long),
+ delete: bool = false,
+ },
+ result: *T,
+ ) GetWindowPropertyError!void {
+ // FIXME: Maybe we should switch to libxcb one day.
+ // Sounds like a much better idea than whatever this is
+ var actual_type_return: c.Atom = undefined;
+ var actual_format_return: c_int = undefined;
+ var nitems_return: c_ulong = undefined;
+ var bytes_after_return: c_ulong = undefined;
+ var prop_return: ?format.bufferType() = null;
+
+ const code = c.XGetWindowProperty(
+ @ptrCast(@alignCast(self.app.display)),
+ self.x11_surface.getXid(),
+ name,
+ options.offset,
+ options.length,
+ @intFromBool(options.delete),
+ typ,
+ &actual_type_return,
+ &actual_format_return,
+ &nitems_return,
+ &bytes_after_return,
+ @ptrCast(&prop_return),
+ );
+ if (code != c.Success) return error.RequestFailed;
+
+ if (actual_type_return == c.None) return error.PropertyNotFound;
+ if (typ != actual_type_return) return error.PropertyTypeMismatch;
+ if (@intFromEnum(format) != actual_format_return) return error.PropertyFormatMismatch;
+
+ const data_ptr: *T = @ptrCast(prop_return);
+ result.* = data_ptr.*;
+ _ = c.XFree(prop_return);
+ }
+
+ fn changeProperty(
+ self: *Window,
+ comptime T: type,
+ name: c.Atom,
+ typ: c.Atom,
+ comptime format: PropertyFormat,
+ options: struct {
+ mode: PropertyChangeMode,
+ },
+ values: []const T,
+ ) X11Error!void {
+ const data: format.bufferType() = @ptrCast(@constCast(values));
+ // The number of "words" that each element `T` occupies.
+ const words_per_elem = @divExact(@sizeOf(T), @sizeOf(format.elemType()));
+
+ const status = c.XChangeProperty(
+ @ptrCast(@alignCast(self.app.display)),
+ self.x11_surface.getXid(),
+ name,
+ typ,
+ @intFromEnum(format),
+ @intFromEnum(options.mode),
+ data,
+ @intCast(words_per_elem * values.len),
+ );
+
+ // For some godforsaken reason Xlib alternates between
+ // error values (0 = success) and booleans (1 = success), and they look exactly
+ // the same in the signature (just `int`, since Xlib is written in C89)...
+ if (status == 0) return error.RequestFailed;
+ }
+
+ fn deleteProperty(self: *Window, name: c.Atom) X11Error!void {
+ const status = c.XDeleteProperty(
+ @ptrCast(@alignCast(self.app.display)),
+ self.x11_surface.getXid(),
+ name,
+ );
+ if (status == 0) return error.RequestFailed;
+ }
+};
+
+const X11Error = error{
+ RequestFailed,
+};
+
+const GetWindowPropertyError = X11Error || error{
+ PropertyNotFound,
+ PropertyTypeMismatch,
+ PropertyFormatMismatch,
+};
+
+const Atoms = struct {
+ kde_blur: c.Atom,
+ motif_wm_hints: c.Atom,
+
+ fn init(display: *gdk_x11.X11Display) Atoms {
+ return .{
+ .kde_blur = gdk_x11.x11GetXatomByNameForDisplay(
+ display,
+ "_KDE_NET_WM_BLUR_BEHIND_REGION",
+ ),
+ .motif_wm_hints = gdk_x11.x11GetXatomByNameForDisplay(
+ display,
+ "_MOTIF_WM_HINTS",
+ ),
+ };
+ }
+};
+
+const PropertyChangeMode = enum(c_int) {
+ replace = c.PropModeReplace,
+ prepend = c.PropModePrepend,
+ append = c.PropModeAppend,
+};
+
+const PropertyFormat = enum(c_int) {
+ _8 = 8,
+ _16 = 16,
+ _32 = 32,
+
+ fn elemType(comptime self: PropertyFormat) type {
+ return switch (self) {
+ ._8 => c_char,
+ ._16 => c_int,
+ ._32 => c_long,
+ };
+ }
+
+ fn bufferType(comptime self: PropertyFormat) type {
+ // The buffer type has to be a multi-pointer to bytes
+ // *aligned to the element type* (very important,
+ // otherwise you'll read garbage!)
+ //
+ // I know this is really ugly. X11 is ugly. I consider it apropos.
+ return [*]align(@alignOf(self.elemType())) u8;
+ }
+};
+
+// See Xm/MwmUtil.h, packaged with the Motif Window Manager
+const MotifWMHints = extern struct {
+ flags: packed struct(c_ulong) {
+ _pad: u1 = 0,
+ decorations: bool = false,
+
+ // We don't really care about the other flags
+ _rest: std.meta.Int(.unsigned, @bitSizeOf(c_ulong) - 2) = 0,
+ } = .{},
+ functions: c_ulong = 0,
+ decorations: packed struct(c_ulong) {
+ all: bool = false,
+
+ // We don't really care about the other flags
+ _rest: std.meta.Int(.unsigned, @bitSizeOf(c_ulong) - 1) = 0,
+ } = .{},
+ input_mode: c_long = 0,
+ status: c_ulong = 0,
+};
diff --git a/vendor/libghostty-vt/src/apprt/ipc.zig b/vendor/libghostty-vt/src/apprt/ipc.zig
new file mode 100644
index 00000000..b37647e0
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/ipc.zig
@@ -0,0 +1,192 @@
+//! Inter-process Communication to a running Ghostty instance from a separate
+//! process.
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const assert = @import("../quirks.zig").inlineAssert;
+const lib = @import("../lib/main.zig");
+
+pub const Errors = error{
+ /// The IPC failed. If a function returns this error, it's expected that
+ /// an a more specific error message will have been written to stderr (or
+ /// otherwise shown to the user in an appropriate way).
+ IPCFailed,
+};
+
+pub const Target = union(Key) {
+ /// Open up a new window in a custom instance of Ghostty.
+ class: [:0]const u8,
+
+ /// Detect which instance to open a new window in.
+ detect,
+
+ // Sync with: ghostty_ipc_target_tag_e
+ pub const Key = enum(c_int) {
+ class,
+ detect,
+
+ test "ghostty.h Target.Key" {
+ try lib.checkGhosttyHEnum(Key, "GHOSTTY_IPC_TARGET_");
+ }
+ };
+
+ // Sync with: ghostty_ipc_target_u
+ pub const CValue = extern union {
+ class: [*:0]const u8,
+ detect: void,
+ };
+
+ // Sync with: ghostty_ipc_target_s
+ pub const C = extern struct {
+ key: Key,
+ value: CValue,
+ };
+
+ /// Convert to ghostty_ipc_target_s.
+ pub fn cval(self: Target) C {
+ return .{
+ .key = @as(Key, self),
+ .value = switch (self) {
+ .class => |class| .{ .class = class.ptr },
+ .detect => .{ .detect = {} },
+ },
+ };
+ }
+};
+
+pub const Action = union(enum) {
+ // A GUIDE TO ADDING NEW ACTIONS:
+ //
+ // 1. Add the action to the `Key` enum. The order of the enum matters
+ // because it maps directly to the libghostty C enum. For ABI
+ // compatibility, new actions should be added to the end of the enum.
+ //
+ // 2. Add the action and optional value to the Action union.
+ //
+ // 3. If the value type is not void, ensure the value is C ABI
+ // compatible (extern). If it is not, add a `C` decl to the value
+ // and a `cval` function to convert to the C ABI compatible value.
+ //
+ // 4. Update `include/ghostty.h`: add the new key, value, and union
+ // entry. If the value type is void then only the key needs to be
+ // added. Ensure the order matches exactly with the Zig code.
+
+ /// The arguments to pass to Ghostty as the command.
+ new_window: NewWindow,
+
+ pub const NewWindow = struct {
+ /// A list of command arguments to launch in the new window. If this is
+ /// `null` the command configured in the config or the user's default
+ /// shell should be launched.
+ ///
+ /// It is an error for this to be non-`null`, but zero length.
+ arguments: ?[][:0]const u8,
+
+ pub const C = extern struct {
+ /// null terminated list of arguments
+ /// it will be null itself if there are no arguments
+ arguments: ?[*]?[*:0]const u8,
+
+ pub fn deinit(self: *NewWindow.C, alloc: Allocator) void {
+ if (self.arguments) |arguments| alloc.free(arguments);
+ }
+ };
+
+ pub fn cval(self: *NewWindow, alloc: Allocator) Allocator.Error!NewWindow.C {
+ var result: NewWindow.C = undefined;
+
+ if (self.arguments) |arguments| {
+ result.arguments = try alloc.alloc([*:0]const u8, arguments.len + 1);
+
+ for (arguments, 0..) |argument, i|
+ result.arguments[i] = argument.ptr;
+
+ // add null terminator
+ result.arguments[arguments.len] = null;
+ } else {
+ result.arguments = null;
+ }
+
+ return result;
+ }
+ };
+
+ /// Sync with: ghostty_ipc_action_tag_e
+ pub const Key = enum(c_int) {
+ new_window,
+
+ test "ghostty.h Action.Key" {
+ try lib.checkGhosttyHEnum(Key, "GHOSTTY_IPC_ACTION_");
+ }
+ };
+
+ /// Sync with: ghostty_ipc_action_u
+ pub const CValue = cvalue: {
+ const key_fields = @typeInfo(Key).@"enum".fields;
+ var union_fields: [key_fields.len]std.builtin.Type.UnionField = undefined;
+ for (key_fields, 0..) |field, i| {
+ const action = @unionInit(Action, field.name, undefined);
+ const Type = t: {
+ const Type = @TypeOf(@field(action, field.name));
+ // Types can provide custom types for their CValue.
+ if (Type != void and @hasDecl(Type, "C")) break :t Type.C;
+ break :t Type;
+ };
+
+ union_fields[i] = .{
+ .name = field.name,
+ .type = Type,
+ .alignment = @alignOf(Type),
+ };
+ }
+
+ break :cvalue @Type(.{ .@"union" = .{
+ .layout = .@"extern",
+ .tag_type = null,
+ .fields = &union_fields,
+ .decls = &.{},
+ } });
+ };
+
+ /// Sync with: ghostty_ipc_action_s
+ pub const C = extern struct {
+ key: Key,
+ value: CValue,
+ };
+
+ comptime {
+ // For ABI compatibility, we expect that this is our union size.
+ // At the time of writing, we don't promise ABI compatibility
+ // so we can change this but I want to be aware of it.
+ assert(@sizeOf(CValue) == switch (@sizeOf(usize)) {
+ 4 => 4,
+ 8 => 8,
+ else => unreachable,
+ });
+ }
+
+ /// Returns the value type for the given key.
+ pub fn Value(comptime key: Key) type {
+ inline for (@typeInfo(Action).@"union".fields) |field| {
+ const field_key = @field(Key, field.name);
+ if (field_key == key) return field.type;
+ }
+
+ unreachable;
+ }
+
+ /// Convert to ghostty_ipc_action_s.
+ pub fn cval(self: Action, alloc: Allocator) C {
+ const value: CValue = switch (self) {
+ inline else => |v, tag| @unionInit(
+ CValue,
+ @tagName(tag),
+ if (@TypeOf(v) != void and @hasDecl(@TypeOf(v), "cval")) v.cval(alloc) else v,
+ ),
+ };
+
+ return .{
+ .key = @as(Key, self),
+ .value = value,
+ };
+ }
+};
diff --git a/vendor/libghostty-vt/src/apprt/none.zig b/vendor/libghostty-vt/src/apprt/none.zig
new file mode 100644
index 00000000..0483d824
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/none.zig
@@ -0,0 +1,19 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+const internal_os = @import("../os/main.zig");
+const apprt = @import("../apprt.zig");
+pub const resourcesDir = internal_os.resourcesDir;
+
+pub const App = struct {
+ /// Always return false as there is no apprt to communicate with.
+ pub fn performIpc(
+ _: Allocator,
+ _: apprt.ipc.Target,
+ comptime action: apprt.ipc.Action.Key,
+ _: apprt.ipc.Action.Value(action),
+ ) !bool {
+ return false;
+ }
+};
+pub const Surface = struct {};
diff --git a/vendor/libghostty-vt/src/apprt/runtime.zig b/vendor/libghostty-vt/src/apprt/runtime.zig
new file mode 100644
index 00000000..1be50353
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/runtime.zig
@@ -0,0 +1,29 @@
+const std = @import("std");
+
+/// Runtime is the runtime to use for Ghostty. All runtimes do not provide
+/// equivalent feature sets.
+pub const Runtime = enum {
+ /// Will not produce an executable at all when `zig build` is called.
+ /// This is only useful if you're only interested in the lib only (macOS).
+ none,
+
+ /// GTK4. Rich windowed application. This uses a full GObject-based
+ /// approach to building the application.
+ gtk,
+
+ pub fn default(target: std.Target) Runtime {
+ return switch (target.os.tag) {
+ // The Linux and FreeBSD default is GTK because it is a full
+ // featured application.
+ .linux, .freebsd => .gtk,
+ // Otherwise, we do NONE so we don't create an exe and we create
+ // libghostty. On macOS, Xcode is used to build the app that links
+ // to libghostty.
+ else => .none,
+ };
+ }
+};
+
+test {
+ _ = Runtime;
+}
diff --git a/vendor/libghostty-vt/src/apprt/structs.zig b/vendor/libghostty-vt/src/apprt/structs.zig
new file mode 100644
index 00000000..2c37dbd5
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/structs.zig
@@ -0,0 +1,114 @@
+const build_config = @import("../build_config.zig");
+
+/// ContentScale is the ratio between the current DPI and the platform's
+/// default DPI. This is used to determine how much certain rendered elements
+/// need to be scaled up or down.
+pub const ContentScale = struct {
+ x: f32,
+ y: f32,
+};
+
+/// The size of the surface in pixels.
+pub const SurfaceSize = struct {
+ width: u32,
+ height: u32,
+
+ pub fn eql(self: *const SurfaceSize, other: *const SurfaceSize) bool {
+ return self.width == other.width and self.height == other.height;
+ }
+};
+
+/// The position of the cursor in pixels.
+pub const CursorPos = struct {
+ x: f32,
+ y: f32,
+};
+
+/// Input Method Editor (IME) position.
+pub const IMEPos = struct {
+ x: f64,
+ y: f64,
+ width: f64,
+ height: f64,
+};
+
+/// The clipboard type.
+///
+/// If this is changed, you must also update ghostty.h
+pub const Clipboard = enum(Backing) {
+ standard = 0, // ctrl+c/v
+ selection = 1,
+ primary = 2,
+
+ // Our backing isn't is as small as we can in Zig, but a full
+ // C int if we're binding to C APIs.
+ const Backing = switch (build_config.app_runtime) {
+ .gtk => c_int,
+ else => u2,
+ };
+
+ /// Make this a valid gobject if we're in a GTK environment.
+ pub const getGObjectType = switch (build_config.app_runtime) {
+ .gtk => @import("gobject").ext.defineEnum(
+ Clipboard,
+ .{ .name = "GhosttyApprtClipboard" },
+ ),
+
+ .none => void,
+ };
+};
+
+pub const ClipboardContent = struct {
+ mime: [:0]const u8,
+ data: [:0]const u8,
+};
+
+pub const ClipboardRequestType = enum(u8) {
+ paste,
+ osc_52_read,
+ osc_52_write,
+};
+
+/// Clipboard request. This is used to request clipboard contents and must
+/// be sent as a response to a ClipboardRequest event.
+pub const ClipboardRequest = union(ClipboardRequestType) {
+ /// A direct paste of clipboard contents.
+ paste: void,
+
+ /// A request to read clipboard contents via OSC 52.
+ osc_52_read: Clipboard,
+
+ /// A request to write clipboard contents via OSC 52.
+ osc_52_write: Clipboard,
+
+ /// Make this a valid gobject if we're in a GTK environment.
+ pub const getGObjectType = switch (build_config.app_runtime) {
+ .gtk => @import("gobject").ext.defineBoxed(
+ ClipboardRequest,
+ .{ .name = "GhosttyClipboardRequest" },
+ ),
+
+ .none => void,
+ };
+};
+
+/// The color scheme in use (light vs dark).
+pub const ColorScheme = enum(u2) {
+ light = 0,
+ dark = 1,
+};
+
+/// Selection information
+pub const Selection = struct {
+ /// Top-left point of the selection in the viewport in scaled
+ /// window pixels. (0,0) is the top-left of the window.
+ tl_x_px: f64,
+ tl_y_px: f64,
+
+ /// The offset of the selection start in cells from the top-left
+ /// of the viewport.
+ ///
+ /// This is a strange metric but its used by macOS.
+ offset_start: u32,
+ offset_len: u32,
+};
diff --git a/vendor/libghostty-vt/src/apprt/surface.zig b/vendor/libghostty-vt/src/apprt/surface.zig
new file mode 100644
index 00000000..3cb0016f
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/surface.zig
@@ -0,0 +1,197 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+const apprt = @import("../apprt.zig");
+const build_config = @import("../build_config.zig");
+const App = @import("../App.zig");
+const Surface = @import("../Surface.zig");
+const renderer = @import("../renderer.zig");
+const terminal = @import("../terminal/main.zig");
+const Config = @import("../config.zig").Config;
+const MessageData = @import("../datastruct/main.zig").MessageData;
+
+/// The message types that can be sent to a single surface.
+pub const Message = union(enum) {
+ /// Represents a write request. Magic number comes from the max size
+ /// we want this union to be.
+ pub const WriteReq = MessageData(u8, 255);
+
+ /// Set the title of the surface.
+ /// TODO: we should change this to a "WriteReq" style structure in
+ /// the termio message so that we can more efficiently send strings
+ /// of any length
+ set_title: [256]u8,
+
+ /// Report the window title back to the terminal
+ report_title: ReportTitleStyle,
+
+ /// Set the mouse shape.
+ set_mouse_shape: terminal.MouseShape,
+
+ /// Read the clipboard and write to the pty.
+ clipboard_read: apprt.Clipboard,
+
+ /// Write the clipboard contents.
+ clipboard_write: struct {
+ clipboard_type: apprt.Clipboard,
+ req: WriteReq,
+ },
+
+ /// Change the configuration to the given configuration. The pointer is
+ /// not valid after receiving this message so any config must be used
+ /// and derived immediately.
+ change_config: *const Config,
+
+ /// Close the surface. This will only close the current surface that
+ /// receives this, not the full application.
+ close: void,
+
+ /// The child process running in the surface has exited. This may trigger
+ /// a surface close, it may not. Additional details about the child
+ /// command are given in the `ChildExited` struct.
+ child_exited: ChildExited,
+
+ /// Show a desktop notification.
+ desktop_notification: struct {
+ /// Desktop notification title.
+ title: [63:0]u8,
+
+ /// Desktop notification body.
+ body: [255:0]u8,
+ },
+
+ /// Health status change for the renderer.
+ renderer_health: renderer.Health,
+
+ /// Tell the surface to present itself to the user. This may require raising
+ /// a window and switching tabs.
+ present_surface: void,
+
+ /// Notifies the surface that password input has started within
+ /// the terminal. This should always be followed by a false value
+ /// unless the surface exits.
+ password_input: bool,
+
+ /// A terminal color was changed using OSC sequences.
+ color_change: terminal.osc.color.ColoredTarget,
+
+ /// Notifies the surface that a tick of the timer that is timing
+ /// out selection scrolling has occurred. "selection scrolling"
+ /// is when the user has clicked and dragged the mouse outside
+ /// the viewport of the terminal and the terminal is scrolling
+ /// the viewport to follow the mouse cursor.
+ selection_scroll_tick: bool,
+
+ /// The terminal has reported a change in the working directory.
+ pwd_change: WriteReq,
+
+ /// The terminal encountered a bell character.
+ ring_bell,
+
+ /// Report the progress of an action using a GUI element
+ progress_report: terminal.osc.Command.ProgressReport,
+
+ /// A command has started in the shell, start a timer.
+ start_command,
+
+ /// A command has finished in the shell, stop the timer and send out
+ /// notifications as appropriate. The optional u8 is the exit code
+ /// of the command.
+ stop_command: ?u8,
+
+ /// The scrollbar state changed for the surface.
+ scrollbar: terminal.Scrollbar,
+
+ /// Search progress update
+ search_total: ?usize,
+
+ /// Selected search index change
+ search_selected: ?usize,
+
+ pub const ReportTitleStyle = enum {
+ csi_21_t,
+
+ // This enum is a placeholder for future title styles.
+ };
+
+ pub const ChildExited = extern struct {
+ exit_code: u32,
+ runtime_ms: u64,
+
+ /// Make this a valid gobject if we're in a GTK environment.
+ pub const getGObjectType = switch (build_config.app_runtime) {
+ .gtk,
+ => @import("gobject").ext.defineBoxed(
+ ChildExited,
+ .{ .name = "GhosttyApprtChildExited" },
+ ),
+
+ .none => void,
+ };
+ };
+};
+
+/// A surface mailbox.
+pub const Mailbox = struct {
+ surface: *Surface,
+ app: App.Mailbox,
+
+ /// Send a message to the surface.
+ pub fn push(
+ self: Mailbox,
+ msg: Message,
+ timeout: App.Mailbox.Queue.Timeout,
+ ) App.Mailbox.Queue.Size {
+ // Surface message sending is actually implemented on the app
+ // thread, so we have to rewrap the message with our surface
+ // pointer and send it to the app thread.
+ return self.app.push(.{
+ .surface_message = .{
+ .surface = self.surface,
+ .message = msg,
+ },
+ }, timeout);
+ }
+};
+
+/// Context for new surface creation to determine inheritance behavior
+pub const NewSurfaceContext = enum(c_int) {
+ window = 0,
+ tab = 1,
+ split = 2,
+};
+
+pub fn shouldInheritWorkingDirectory(context: NewSurfaceContext, config: *const Config) bool {
+ return switch (context) {
+ .window => config.@"window-inherit-working-directory",
+ .tab => config.@"tab-inherit-working-directory",
+ .split => config.@"split-inherit-working-directory",
+ };
+}
+
+/// Returns a new config for a surface for the given app that should be
+/// used for any new surfaces. The resulting config should be deinitialized
+/// after the surface is initialized.
+pub fn newConfig(
+ app: *const App,
+ config: *const Config,
+ context: NewSurfaceContext,
+) Allocator.Error!Config {
+ // Create a shallow clone
+ var copy = config.shallowClone(app.alloc);
+
+ // Our allocator is our config's arena
+ const alloc = copy._arena.?.allocator();
+
+ // Get our previously focused surface for some inherited values.
+ const prev = app.focusedSurface();
+ if (prev) |p| {
+ if (shouldInheritWorkingDirectory(context, config)) {
+ if (try p.pwd(alloc)) |pwd| {
+ copy.@"working-directory" = .{ .path = pwd };
+ }
+ }
+ }
+
+ return copy;
+}