summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLeah Amelia Chen <hi@pluie.me>2025-09-04 17:19:42 +0200
committerLeah Amelia Chen <hi@pluie.me>2025-09-05 00:21:41 +0200
commitac52af27d3e0fa8c1ac466585df1696bc9817670 (patch)
tree0e12070d5471661b4bfb1f56c86a9130038f52a8
parentc3e7857a2c5031e8274a411c84695351e8e3fffe (diff)
gtk: nuke the legacy apprt from orbit
We don't really have any large outstanding regressions on -ng to warrant keeping this alive anymore. ¡Adiós!
-rw-r--r--.github/workflows/test.yml53
-rw-r--r--CODEOWNERS1
-rw-r--r--src/apprt.zig7
-rw-r--r--src/apprt/action.zig2
-rw-r--r--src/apprt/gtk.zig12
-rw-r--r--src/apprt/gtk/App.zig1900
-rw-r--r--src/apprt/gtk/Builder.zig77
-rw-r--r--src/apprt/gtk/ClipboardConfirmationWindow.zig212
-rw-r--r--src/apprt/gtk/CloseDialog.zig151
-rw-r--r--src/apprt/gtk/CommandPalette.zig258
-rw-r--r--src/apprt/gtk/ConfigErrorsDialog.zig102
-rw-r--r--src/apprt/gtk/GlobalShortcuts.zig422
-rw-r--r--src/apprt/gtk/ImguiWidget.zig470
-rw-r--r--src/apprt/gtk/ProgressBar.zig165
-rw-r--r--src/apprt/gtk/ResizeOverlay.zig206
-rw-r--r--src/apprt/gtk/Split.zig441
-rw-r--r--src/apprt/gtk/Surface.zig2561
-rw-r--r--src/apprt/gtk/Tab.zig171
-rw-r--r--src/apprt/gtk/TabView.zig284
-rw-r--r--src/apprt/gtk/URLWidget.zig115
-rw-r--r--src/apprt/gtk/Window.zig1190
-rw-r--r--src/apprt/gtk/adw_version.zig122
-rw-r--r--src/apprt/gtk/blueprint_compiler.zig160
-rw-r--r--src/apprt/gtk/cgroup.zig205
-rw-r--r--src/apprt/gtk/flatpak.zig29
-rw-r--r--src/apprt/gtk/gresource.zig168
-rw-r--r--src/apprt/gtk/gtk_version.zig140
-rw-r--r--src/apprt/gtk/headerbar.zig54
-rw-r--r--src/apprt/gtk/inspector.zig184
-rw-r--r--src/apprt/gtk/ipc.zig1
-rw-r--r--src/apprt/gtk/ipc/new_window.zig172
-rw-r--r--src/apprt/gtk/key.zig405
-rw-r--r--src/apprt/gtk/menu.zig139
-rw-r--r--src/apprt/gtk/style-dark.css8
-rw-r--r--src/apprt/gtk/style-hc-dark.css3
-rw-r--r--src/apprt/gtk/style-hc.css3
-rw-r--r--src/apprt/gtk/style.css116
-rw-r--r--src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp25
-rw-r--r--src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp102
-rw-r--r--src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp116
-rw-r--r--src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp71
-rw-r--r--src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp71
-rw-r--r--src/apprt/gtk/ui/1.2/ccw-paste.blp71
-rw-r--r--src/apprt/gtk/ui/1.2/config-errors-dialog.blp28
-rw-r--r--src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp85
-rw-r--r--src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp81
-rw-r--r--src/apprt/gtk/ui/1.5/ccw-paste.blp71
-rw-r--r--src/apprt/gtk/ui/1.5/command-palette.blp106
-rw-r--r--src/apprt/gtk/ui/1.5/config-errors-dialog.blp28
-rw-r--r--src/apprt/gtk/ui/1.5/prompt-title-dialog.blp16
-rw-r--r--src/apprt/gtk/ui/README.md15
-rw-r--r--src/apprt/gtk/winproto.zig155
-rw-r--r--src/apprt/gtk/winproto/noop.zig75
-rw-r--r--src/apprt/gtk/winproto/wayland.zig511
-rw-r--r--src/apprt/gtk/winproto/x11.zig507
-rw-r--r--src/apprt/structs.zig6
-rw-r--r--src/apprt/surface.zig1
-rw-r--r--src/build/GhosttyDist.zig5
-rw-r--r--src/build/GhosttyI18n.zig10
-rw-r--r--src/build/SharedDeps.zig229
-rw-r--r--src/cli/version.zig6
-rw-r--r--src/config/Config.zig4
-rw-r--r--src/datastruct/split_tree.zig2
-rw-r--r--src/font/face.zig2
-rw-r--r--src/input/Binding.zig2
-rw-r--r--src/renderer/OpenGL.zig7
-rw-r--r--src/terminal/mouse_shape.zig2
67 files changed, 21 insertions, 13098 deletions
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 419e83235..9ec50e494 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -22,7 +22,6 @@ jobs:
- build-macos-matrix
- build-windows
- test
- - test-gtk
- test-gtk-ng
- test-sentry-linux
- test-macos
@@ -492,9 +491,6 @@ jobs:
- name: test
run: nix develop -c zig build -Dapp-runtime=none test
- - name: Test GTK Build
- run: nix develop -c zig build -Dapp-runtime=gtk -Demit-docs -Demit-webdata
-
- name: Test GTK-NG Build
run: nix develop -c zig build -Dapp-runtime=gtk-ng -Demit-docs -Demit-webdata
@@ -502,55 +498,6 @@ jobs:
- name: Test System Build
run: nix develop -c zig build --system ${ZIG_GLOBAL_CACHE_DIR}/p
- test-gtk:
- strategy:
- fail-fast: false
- matrix:
- x11: ["true", "false"]
- wayland: ["true", "false"]
- name: GTK x11=${{ matrix.x11 }} wayland=${{ matrix.wayland }}
- runs-on: namespace-profile-ghostty-sm
- needs: test
- env:
- ZIG_LOCAL_CACHE_DIR: /zig/local-cache
- ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
- steps:
- - name: Checkout code
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
-
- - name: Setup Cache
- uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
- with:
- path: |
- /nix
- /zig
-
- # Install Nix and use that to run our tests so our environment matches exactly.
- - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
- with:
- nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
- with:
- name: ghostty
- authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
-
- - name: Test
- run: |
- nix develop -c \
- zig build \
- -Dapp-runtime=gtk \
- -Dgtk-x11=${{ matrix.x11 }} \
- -Dgtk-wayland=${{ matrix.wayland }} \
- test
-
- - name: Build
- run: |
- nix develop -c \
- zig build \
- -Dapp-runtime=gtk \
- -Dgtk-x11=${{ matrix.x11 }} \
- -Dgtk-wayland=${{ matrix.wayland }}
-
test-gtk-ng:
strategy:
fail-fast: false
diff --git a/CODEOWNERS b/CODEOWNERS
index 770c08860..0f7e18ed8 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -118,7 +118,6 @@
/pkg/harfbuzz/ @ghostty-org/font
# GTK
-/src/apprt/gtk/ @ghostty-org/gtk
/src/apprt/gtk-ng/ @ghostty-org/gtk
/src/os/cgroup.zig @ghostty-org/gtk
/src/os/flatpak.zig @ghostty-org/gtk
diff --git a/src/apprt.zig b/src/apprt.zig
index 2e3a722a6..cbde56312 100644
--- a/src/apprt.zig
+++ b/src/apprt.zig
@@ -16,7 +16,6 @@ const structs = @import("apprt/structs.zig");
pub const action = @import("apprt/action.zig");
pub const ipc = @import("apprt/ipc.zig");
-pub const gtk = @import("apprt/gtk.zig");
pub const gtk_ng = @import("apprt/gtk-ng.zig");
pub const none = @import("apprt/none.zig");
pub const browser = @import("apprt/browser.zig");
@@ -43,7 +42,6 @@ pub const SurfaceSize = structs.SurfaceSize;
pub const runtime = switch (build_config.artifact) {
.exe => switch (build_config.app_runtime) {
.none => none,
- .gtk => gtk,
.@"gtk-ng" => gtk_ng,
},
.lib => embedded,
@@ -64,11 +62,6 @@ pub const Runtime = enum {
/// approach to building the application.
@"gtk-ng",
- /// GTK-backed. Rich windowed application. GTK is dynamically linked.
- /// WARNING: Deprecated. This will be removed very soon. All bug fixes
- /// and features should go into the gtk-ng backend.
- 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
diff --git a/src/apprt/action.zig b/src/apprt/action.zig
index a41a4627f..fdd328a24 100644
--- a/src/apprt/action.zig
+++ b/src/apprt/action.zig
@@ -542,7 +542,7 @@ pub const InitialSize = extern struct {
/// Make this a valid gobject if we're in a GTK environment.
pub const getGObjectType = switch (build_config.app_runtime) {
- .gtk, .@"gtk-ng" => @import("gobject").ext.defineBoxed(
+ .@"gtk-ng" => @import("gobject").ext.defineBoxed(
InitialSize,
.{ .name = "GhosttyApprtInitialSize" },
),
diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig
deleted file mode 100644
index 3193065c4..000000000
--- a/src/apprt/gtk.zig
+++ /dev/null
@@ -1,12 +0,0 @@
-//! Application runtime that uses GTK4.
-
-pub const App = @import("gtk/App.zig");
-pub const Surface = @import("gtk/Surface.zig");
-pub const resourcesDir = @import("gtk/flatpak.zig").resourcesDir;
-
-test {
- @import("std").testing.refAllDecls(@This());
-
- _ = @import("gtk/inspector.zig");
- _ = @import("gtk/key.zig");
-}
diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig
deleted file mode 100644
index ee5f3eb96..000000000
--- a/src/apprt/gtk/App.zig
+++ /dev/null
@@ -1,1900 +0,0 @@
-/// App is the entrypoint for the application. This is called after all
-/// of the runtime-agnostic initialization is complete and we're ready
-/// to start.
-///
-/// There is only ever one App instance per process. This is because most
-/// application frameworks also have this restriction so it simplifies
-/// the assumptions.
-///
-/// In GTK, the App contains the primary GApplication and GMainContext
-/// (event loop) along with any global app state.
-const App = @This();
-
-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 std = @import("std");
-const assert = std.debug.assert;
-const testing = std.testing;
-const Allocator = std.mem.Allocator;
-const builtin = @import("builtin");
-const build_config = @import("../../build_config.zig");
-const xev = @import("../../global.zig").xev;
-const build_options = @import("build_options");
-const apprt = @import("../../apprt.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 Config = configpkg.Config;
-const CoreApp = @import("../../App.zig");
-const CoreSurface = @import("../../Surface.zig");
-const ipc = @import("ipc.zig");
-
-const cgroup = @import("cgroup.zig");
-const Surface = @import("Surface.zig");
-const Window = @import("Window.zig");
-const ConfigErrorsDialog = @import("ConfigErrorsDialog.zig");
-const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig");
-const CloseDialog = @import("CloseDialog.zig");
-const GlobalShortcuts = @import("GlobalShortcuts.zig");
-const Split = @import("Split.zig");
-const inspector = @import("inspector.zig");
-const key = @import("key.zig");
-const winprotopkg = @import("winproto.zig");
-const gtk_version = @import("gtk_version.zig");
-const adw_version = @import("adw_version.zig");
-
-pub const c = @cImport({
- // generated header files
- @cInclude("ghostty_resources.h");
-});
-
-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;
-
-pub const Options = struct {};
-
-core_app: *CoreApp,
-config: Config,
-
-app: *adw.Application,
-ctx: *glib.MainContext,
-
-/// State and logic for the underlying windowing protocol.
-winproto: winprotopkg.App,
-
-/// True if the app was launched with single instance mode.
-single_instance: bool,
-
-/// The "none" cursor. We use one that is shared across the entire app.
-cursor_none: ?*gdk.Cursor,
-
-/// The clipboard confirmation window, if it is currently open.
-clipboard_confirmation_window: ?*ClipboardConfirmationWindow = null,
-
-/// The config errors dialog, if it is currently open.
-config_errors_dialog: ?ConfigErrorsDialog = null,
-
-/// The window containing the quick terminal.
-/// Null when never initialized.
-quick_terminal: ?*Window = null,
-
-/// This is set to false when the main loop should exit.
-running: bool = true,
-
-/// The base path of the transient cgroup used to put all surfaces
-/// into their own cgroup. This is only set if cgroups are enabled
-/// and initialization was successful.
-transient_cgroup_base: ?[]const u8 = 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) = .{},
-
-global_shortcuts: ?GlobalShortcuts,
-
-/// The timer used to quit the application after the last window is closed.
-quit_timer: union(enum) {
- off: void,
- active: c_uint,
- expired: void,
-} = .{ .off = {} },
-
-pub fn init(self: *App, core_app: *CoreApp, opts: Options) !void {
- _ = opts;
-
- // Log our GTK version
- gtk_version.logVersion();
-
- // log the adwaita version
- adw_version.logVersion();
-
- // Set gettext global domain to be our app so that our unqualified
- // translations map to our translations.
- try internal_os.i18n.initGlobalDomain();
-
- // Load our configuration
- var config = try Config.load(core_app.alloc);
- errdefer config.deinit();
-
- // If we had configuration errors, then log them.
- if (!config._diagnostics.empty()) {
- var buf = std.ArrayList(u8).init(core_app.alloc);
- defer buf.deinit();
- for (config._diagnostics.items()) |diag| {
- try diag.write(buf.writer());
- log.warn("configuration error: {s}", .{buf.items});
- buf.clearRetainingCapacity();
- }
-
- // If we have any CLI errors, exit.
- if (config._diagnostics.containsLocation(.cli)) {
- log.warn("CLI errors detected, exiting", .{});
- std.posix.exit(1);
- }
- }
-
- // Setup our event loop backend
- if (config.@"async-backend" != .auto) {
- 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)},
- );
- }
- }
-
- 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,
- } = .{
- .opengl = 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: [128]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: [128]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]);
- }
-
- adw.init();
-
- 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);
- };
-
- // The "none" cursor is used for hiding the cursor
- const cursor_none = gdk.Cursor.newFromName("none", null);
- errdefer if (cursor_none) |cursor| cursor.unref();
-
- const single_instance = switch (config.@"gtk-single-instance") {
- .true => true,
- .false => false,
- .desktop => switch (config.@"launched-from".?) {
- .desktop, .systemd, .dbus => true,
- .cli => false,
- },
- };
-
- // 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", .{});
- }
- }
-
- const default_id = comptime build_config.bundle_id;
- break :app_id if (builtin.mode == .Debug) default_id ++ "-debug" else default_id;
- };
-
- // Create our GTK Application which encapsulates our process.
- log.debug("creating GTK application id={s} single-instance={}", .{
- app_id,
- single_instance,
- });
-
- // Using an AdwApplication lets us use Adwaita widgets and access things
- // such as the color scheme.
- const adw_app = adw.Application.new(
- app_id.ptr,
- app_flags,
- );
- errdefer adw_app.unref();
-
- const style_manager = adw_app.getStyleManager();
- style_manager.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,
- },
- );
-
- const gio_app = adw_app.as(gio.Application);
-
- // force the resource path to a known value so that it doesn't depend on
- // the app id and load in compiled resources
- gio_app.setResourceBasePath("/com/mitchellh/ghostty");
- gio.resourcesRegister(@ptrCast(@alignCast(c.ghostty_get_resource() orelse {
- log.err("unable to load resources", .{});
- return error.GtkNoResources;
- })));
-
- // The `activate` signal is used when Ghostty is first launched and when a
- // secondary Ghostty is launched and requests a new window.
- _ = gio.Application.signals.activate.connect(
- adw_app,
- *CoreApp,
- gtkActivate,
- core_app,
- .{},
- );
-
- // Other signals
- _ = gtk.Application.signals.window_added.connect(
- adw_app,
- *CoreApp,
- gtkWindowAdded,
- core_app,
- .{},
- );
- _ = gtk.Application.signals.window_removed.connect(
- adw_app,
- *CoreApp,
- gtkWindowRemoved,
- core_app,
- .{},
- );
-
- // Setup a listener for SIGUSR2 to reload the configuration.
- _ = glib.unixSignalAdd(
- std.posix.SIG.USR2,
- sigusr2,
- self,
- );
-
- // We don't use g_application_run, we want to manually control the
- // loop so we have to do the same things the run function does:
- // https://github.com/GNOME/glib/blob/a8e8b742e7926e33eb635a8edceac74cf239d6ed/gio/gapplication.c#L2533
- const ctx = glib.MainContext.default();
- if (glib.MainContext.acquire(ctx) == 0) return error.GtkContextAcquireFailed;
- errdefer glib.MainContext.release(ctx);
-
- var err_: ?*glib.Error = null;
- if (gio_app.register(
- null,
- &err_,
- ) == 0) {
- if (err_) |err| {
- log.warn("error registering application: {s}", .{err.f_message orelse "(unknown)"});
- err.free();
- }
- return error.GtkApplicationRegisterFailed;
- }
-
- // Setup our windowing protocol logic
- var winproto_app = try winprotopkg.App.init(
- core_app.alloc,
- display,
- app_id,
- &config,
- );
- errdefer winproto_app.deinit(core_app.alloc);
- log.debug("windowing protocol={s}", .{@tagName(winproto_app)});
-
- // 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). An initial window will not be immediately created if we were
- // launched by D-Bus activation or systemd. D-Bus activation will send it's
- // own `activate` or `new-window` signal later.
- //
- // https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302
- if (config.@"initial-window") switch (config.@"launched-from".?) {
- .desktop, .cli => gio_app.activate(),
- .dbus, .systemd => {},
- };
-
- // 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,
- );
-
- self.* = .{
- .core_app = core_app,
- .app = adw_app,
- .config = config,
- .ctx = ctx,
- .cursor_none = cursor_none,
- .winproto = winproto_app,
- .single_instance = single_instance,
- // If we are NOT the primary instance, then we never want to run.
- // This means that another instance of the GTK app is running and
- // our "activate" call above will open a window.
- .running = gio_app.getIsRemote() == 0,
- .css_provider = css_provider,
- .global_shortcuts = .init(core_app.alloc, gio_app),
- };
-}
-
-// Terminate the application. The application will not be restarted after
-// this so all global state can be cleaned up.
-pub fn terminate(self: *App) void {
- gio.Settings.sync();
- while (glib.MainContext.iteration(self.ctx, 0) != 0) {}
- glib.MainContext.release(self.ctx);
- self.app.unref();
-
- if (self.cursor_none) |cursor| cursor.unref();
- if (self.transient_cgroup_base) |path| self.core_app.alloc.free(path);
-
- for (self.custom_css_providers.items) |provider| {
- provider.unref();
- }
- self.custom_css_providers.deinit(self.core_app.alloc);
-
- self.winproto.deinit(self.core_app.alloc);
-
- if (self.global_shortcuts) |*shortcuts| shortcuts.deinit();
-
- self.config.deinit();
-}
-
-/// 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 {
- switch (action) {
- .quit => self.quit(),
- .new_window => _ = try self.newWindow(switch (target) {
- .app => null,
- .surface => |v| v,
- }),
- .close_window => return try self.closeWindow(target),
- .toggle_maximize => self.toggleMaximize(target),
- .toggle_fullscreen => self.toggleFullscreen(target, value),
- .new_tab => try self.newTab(target),
- .close_tab => return try self.closeTab(target, value),
- .goto_tab => return self.gotoTab(target, value),
- .move_tab => self.moveTab(target, value),
- .new_split => try self.newSplit(target, value),
- .resize_split => self.resizeSplit(target, value),
- .equalize_splits => self.equalizeSplits(target),
- .goto_split => return self.gotoSplit(target, value),
- .open_config => return self.openConfig(),
- .config_change => self.configChange(target, value.config),
- .reload_config => try self.reloadConfig(target, value),
- .inspector => self.controlInspector(target, value),
- .show_gtk_inspector => self.showGTKInspector(),
- .desktop_notification => self.showDesktopNotification(target, value),
- .set_title => try self.setTitle(target, value),
- .pwd => try self.setPwd(target, value),
- .present_terminal => self.presentTerminal(target),
- .initial_size => try self.setInitialSize(target, value),
- .size_limit => try self.setSizeLimit(target, value),
- .mouse_visibility => self.setMouseVisibility(target, value),
- .mouse_shape => try self.setMouseShape(target, value),
- .mouse_over_link => self.setMouseOverLink(target, value),
- .toggle_tab_overview => self.toggleTabOverview(target),
- .toggle_split_zoom => self.toggleSplitZoom(target),
- .toggle_window_decorations => self.toggleWindowDecorations(target),
- .quit_timer => self.quitTimer(value),
- .prompt_title => try self.promptTitle(target),
- .toggle_quick_terminal => return try self.toggleQuickTerminal(),
- .secure_input => self.setSecureInput(target, value),
- .ring_bell => try self.ringBell(target),
- .toggle_command_palette => try self.toggleCommandPalette(target),
- .open_url => self.openUrl(value),
- .show_child_exited => return try self.showChildExited(target, value),
- .progress_report => return try self.handleProgressReport(target, value),
- .render => self.render(target),
-
- // Unimplemented
- .close_all_windows,
- .float_window,
- .toggle_visibility,
- .cell_size,
- .key_sequence,
- .render_inspector,
- .renderer_health,
- .color_change,
- .reset_window_size,
- .check_for_updates,
- .undo,
- .redo,
- .show_on_screen_keyboard,
- => {
- log.warn("unimplemented action={}", .{action});
- return false;
- },
- }
-
- // We can assume it was handled because all unknown/unimplemented actions
- // are caught above.
- return true;
-}
-
-/// 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),
-) (Allocator.Error || std.posix.WriteError || apprt.ipc.Errors)!bool {
- switch (action) {
- .new_window => return try ipc.openNewWindow(alloc, target, value),
- }
-}
-
-fn newTab(_: *App, target: apprt.Target) !void {
- switch (target) {
- .app => {},
- .surface => |v| {
- const window = v.rt_surface.container.window() orelse {
- log.info(
- "new_tab invalid for container={s}",
- .{@tagName(v.rt_surface.container)},
- );
- return;
- };
-
- try window.newTab(v);
- },
- }
-}
-
-fn closeTab(_: *App, target: apprt.Target, value: apprt.Action.Value(.close_tab)) !bool {
- switch (target) {
- .app => return false,
- .surface => |v| {
- const tab = v.rt_surface.container.tab() orelse {
- log.info(
- "close_tab invalid for container={s}",
- .{@tagName(v.rt_surface.container)},
- );
- return false;
- };
-
- switch (value) {
- .this => {
- tab.closeWithConfirmation();
- return true;
- },
- .other => {
- log.warn("close-tab:other is not implemented", .{});
- return false;
- },
- }
- },
- }
-}
-
-fn gotoTab(_: *App, target: apprt.Target, tab: apprt.action.GotoTab) bool {
- switch (target) {
- .app => return false,
- .surface => |v| {
- const window = v.rt_surface.container.window() orelse {
- log.info(
- "gotoTab invalid for container={s}",
- .{@tagName(v.rt_surface.container)},
- );
- return false;
- };
-
- return switch (tab) {
- .previous => window.gotoPreviousTab(v.rt_surface),
- .next => window.gotoNextTab(v.rt_surface),
- .last => window.gotoLastTab(),
- else => window.gotoTab(@intCast(@intFromEnum(tab))),
- };
- },
- }
-}
-
-fn moveTab(_: *App, target: apprt.Target, move_tab: apprt.action.MoveTab) void {
- switch (target) {
- .app => {},
- .surface => |v| {
- const window = v.rt_surface.container.window() orelse {
- log.info(
- "moveTab invalid for container={s}",
- .{@tagName(v.rt_surface.container)},
- );
- return;
- };
-
- window.moveTab(v.rt_surface, @intCast(move_tab.amount));
- },
- }
-}
-
-fn newSplit(
- self: *App,
- target: apprt.Target,
- direction: apprt.action.SplitDirection,
-) !void {
- switch (target) {
- .app => {},
- .surface => |v| {
- const alloc = self.core_app.alloc;
- _ = try Split.create(alloc, v.rt_surface, direction);
- },
- }
-}
-
-fn equalizeSplits(_: *App, target: apprt.Target) void {
- switch (target) {
- .app => {},
- .surface => |v| {
- const tab = v.rt_surface.container.tab() orelse return;
- const top_split = switch (tab.elem) {
- .split => |s| s,
- else => return,
- };
- _ = top_split.equalize();
- },
- }
-}
-
-fn gotoSplit(
- _: *const App,
- target: apprt.Target,
- direction: apprt.action.GotoSplit,
-) bool {
- switch (target) {
- .app => return false,
- .surface => |v| {
- const s = v.rt_surface.container.split() orelse return false;
- const map = s.directionMap(switch (v.rt_surface.container) {
- .split_tl => .top_left,
- .split_br => .bottom_right,
- .none, .tab_ => unreachable,
- });
- const surface_ = map.get(direction) orelse return false;
- if (surface_) |surface| {
- surface.grabFocus();
- return true;
- }
- return false;
- },
- }
-}
-
-fn resizeSplit(
- _: *const App,
- target: apprt.Target,
- resize: apprt.action.ResizeSplit,
-) void {
- switch (target) {
- .app => {},
- .surface => |v| {
- const s = v.rt_surface.container.firstSplitWithOrientation(
- Split.Orientation.fromResizeDirection(resize.direction),
- ) orelse return;
- s.moveDivider(resize.direction, resize.amount);
- },
- }
-}
-
-fn presentTerminal(
- _: *const App,
- target: apprt.Target,
-) void {
- switch (target) {
- .app => {},
- .surface => |v| v.rt_surface.present(),
- }
-}
-
-fn controlInspector(
- _: *const App,
- target: apprt.Target,
- mode: apprt.action.Inspector,
-) void {
- const surface: *Surface = switch (target) {
- .app => return,
- .surface => |v| v.rt_surface,
- };
-
- surface.controlInspector(mode);
-}
-
-fn showGTKInspector(
- _: *const App,
-) void {
- gtk.Window.setInteractiveDebugging(@intFromBool(true));
-}
-
-fn toggleMaximize(_: *App, target: apprt.Target) void {
- switch (target) {
- .app => {},
- .surface => |v| {
- const window = v.rt_surface.container.window() orelse {
- log.info(
- "toggleMaximize invalid for container={s}",
- .{@tagName(v.rt_surface.container)},
- );
- return;
- };
- window.toggleMaximize();
- },
- }
-}
-
-fn toggleFullscreen(
- _: *App,
- target: apprt.Target,
- _: apprt.action.Fullscreen,
-) void {
- switch (target) {
- .app => {},
- .surface => |v| {
- const window = v.rt_surface.container.window() orelse {
- log.info(
- "toggleFullscreen invalid for container={s}",
- .{@tagName(v.rt_surface.container)},
- );
- return;
- };
-
- window.toggleFullscreen();
- },
- }
-}
-
-fn toggleTabOverview(_: *App, target: apprt.Target) void {
- switch (target) {
- .app => {},
- .surface => |v| {
- const window = v.rt_surface.container.window() orelse {
- log.info(
- "toggleTabOverview invalid for container={s}",
- .{@tagName(v.rt_surface.container)},
- );
- return;
- };
-
- window.toggleTabOverview();
- },
- }
-}
-
-fn toggleSplitZoom(_: *App, target: apprt.Target) void {
- switch (target) {
- .app => {},
- .surface => |surface| surface.rt_surface.toggleSplitZoom(),
- }
-}
-
-fn toggleWindowDecorations(
- _: *App,
- target: apprt.Target,
-) void {
- switch (target) {
- .app => {},
- .surface => |v| {
- const window = v.rt_surface.container.window() orelse {
- log.info(
- "toggleWindowDecorations invalid for container={s}",
- .{@tagName(v.rt_surface.container)},
- );
- return;
- };
-
- window.toggleWindowDecorations();
- },
- }
-}
-
-fn toggleQuickTerminal(self: *App) !bool {
- if (self.quick_terminal) |qt| {
- qt.toggleVisibility();
- return true;
- }
-
- if (!self.winproto.supportsQuickTerminal()) return false;
-
- const qt = Window.create(self.core_app.alloc, self) catch |err| {
- log.err("failed to initialize quick terminal={}", .{err});
- return true;
- };
- self.quick_terminal = qt;
-
- // The setup has to happen *before* the window-specific winproto is
- // initialized, so we need to initialize it through the app winproto
- try self.winproto.initQuickTerminal(qt);
-
- // Finalize creating the quick terminal
- try qt.newTab(null);
- qt.present();
- return true;
-}
-
-fn ringBell(_: *App, target: apprt.Target) !void {
- switch (target) {
- .app => {},
- .surface => |surface| try surface.rt_surface.ringBell(),
- }
-}
-
-fn toggleCommandPalette(_: *App, target: apprt.Target) !void {
- switch (target) {
- .app => {},
- .surface => |surface| {
- const window = surface.rt_surface.container.window() orelse {
- log.info(
- "toggleCommandPalette invalid for container={s}",
- .{@tagName(surface.rt_surface.container)},
- );
- return;
- };
-
- window.toggleCommandPalette();
- },
- }
-}
-
-fn showChildExited(_: *App, target: apprt.Target, value: apprt.surface.Message.ChildExited) error{}!bool {
- switch (target) {
- .app => return false,
- .surface => |surface| return try surface.rt_surface.showChildExited(value),
- }
-}
-
-/// Show a native GUI element to indicate the progress of a TUI operation.
-fn handleProgressReport(_: *App, target: apprt.Target, value: terminal.osc.Command.ProgressReport) error{}!bool {
- switch (target) {
- .app => return false,
- .surface => |surface| return try surface.rt_surface.progress_bar.handleProgressReport(value),
- }
-}
-
-fn render(_: *App, target: apprt.Target) void {
- switch (target) {
- .app => {},
- .surface => |v| v.rt_surface.redraw(),
- }
-}
-
-fn quitTimer(self: *App, mode: apprt.action.QuitTimer) void {
- switch (mode) {
- .start => self.startQuitTimer(),
- .stop => self.stopQuitTimer(),
- }
-}
-
-fn promptTitle(_: *App, target: apprt.Target) !void {
- switch (target) {
- .app => {},
- .surface => |v| {
- try v.rt_surface.promptTitle();
- },
- }
-}
-
-fn setTitle(
- _: *App,
- target: apprt.Target,
- title: apprt.action.SetTitle,
-) !void {
- switch (target) {
- .app => {},
- .surface => |v| try v.rt_surface.setTitle(title.title, .terminal),
- }
-}
-
-fn setPwd(
- _: *App,
- target: apprt.Target,
- pwd: apprt.action.Pwd,
-) !void {
- switch (target) {
- .app => {},
- .surface => |v| try v.rt_surface.setPwd(pwd.pwd),
- }
-}
-
-fn setMouseVisibility(
- _: *App,
- target: apprt.Target,
- visibility: apprt.action.MouseVisibility,
-) void {
- switch (target) {
- .app => {},
- .surface => |v| v.rt_surface.setMouseVisibility(switch (visibility) {
- .visible => true,
- .hidden => false,
- }),
- }
-}
-
-fn setMouseShape(
- _: *App,
- target: apprt.Target,
- shape: terminal.MouseShape,
-) !void {
- switch (target) {
- .app => {},
- .surface => |v| try v.rt_surface.setMouseShape(shape),
- }
-}
-
-fn setMouseOverLink(
- _: *App,
- target: apprt.Target,
- value: apprt.action.MouseOverLink,
-) void {
- switch (target) {
- .app => {},
- .surface => |v| v.rt_surface.mouseOverLink(if (value.url.len > 0)
- value.url
- else
- null),
- }
-}
-
-fn setInitialSize(
- _: *App,
- target: apprt.Target,
- value: apprt.action.InitialSize,
-) !void {
- switch (target) {
- .app => {},
- .surface => |v| try v.rt_surface.setInitialWindowSize(
- value.width,
- value.height,
- ),
- }
-}
-
-fn setSizeLimit(
- _: *App,
- target: apprt.Target,
- value: apprt.action.SizeLimit,
-) !void {
- switch (target) {
- .app => {},
- .surface => |v| try v.rt_surface.setSizeLimits(.{
- .width = value.min_width,
- .height = value.min_height,
- }, if (value.max_width > 0) .{
- .width = value.max_width,
- .height = value.max_height,
- } else null),
- }
-}
-
-fn showDesktopNotification(
- self: *App,
- target: apprt.Target,
- n: apprt.action.DesktopNotification,
-) void {
- // 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));
-
- const pointer = glib.Variant.newUint64(switch (target) {
- .app => 0,
- .surface => |v| @intFromPtr(v),
- });
- notification.setDefaultActionAndTargetValue("app.present-surface", pointer);
-
- const gio_app = self.app.as(gio.Application);
-
- // We set the notification ID to the body content. If the content is the
- // same, this notification may replace a previous notification
- gio_app.sendNotification(n.body, notification);
-}
-
-fn configChange(
- self: *App,
- target: apprt.Target,
- new_config: *const Config,
-) void {
- switch (target) {
- .surface => |surface| surface: {
- surface.rt_surface.updateConfig(new_config) catch |err| {
- log.err("unable to update surface config: {}", .{err});
- };
- const window = surface.rt_surface.container.window() orelse break :surface;
- window.updateConfig(new_config) catch |err| {
- log.warn("error updating config for window err={}", .{err});
- };
- },
-
- .app => {
- // We clone (to take ownership) and update our configuration.
- if (new_config.clone(self.core_app.alloc)) |config_clone| {
- self.config.deinit();
- self.config = config_clone;
- } else |err| {
- log.warn("error cloning configuration err={}", .{err});
- }
-
- // App changes needs to show a toast that our configuration
- // has reloaded.
- const window = window: {
- if (self.core_app.focusedSurface()) |core_surface| {
- const surface = core_surface.rt_surface;
- if (surface.container.window()) |window| {
- window.onConfigReloaded();
- break :window window;
- }
- }
- break :window null;
- };
-
- self.syncConfigChanges(window) catch |err| {
- log.warn("error handling configuration changes err={}", .{err});
- };
- },
- }
-}
-
-pub fn reloadConfig(
- self: *App,
- target: apprt.action.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();
-
- if (opts.soft) {
- switch (target) {
- .app => try self.core_app.updateConfig(self, &self.config),
- .surface => |core_surface| try core_surface.updateConfig(
- &self.config,
- ),
- }
- return;
- }
-
- // Load our configuration
- var config = try Config.load(self.core_app.alloc);
- errdefer config.deinit();
-
- // Call into our app to update
- switch (target) {
- .app => try self.core_app.updateConfig(self, &config),
- .surface => |core_surface| try core_surface.updateConfig(&config),
- }
-
- // Update the existing config, be sure to clean up the old one.
- self.config.deinit();
- self.config = config;
-}
-
-/// Call this anytime the configuration changes.
-fn syncConfigChanges(self: *App, window: ?*Window) !void {
- ConfigErrorsDialog.maybePresent(self, window);
- try self.syncActionAccelerators();
-
- if (self.global_shortcuts) |*shortcuts| {
- shortcuts.refreshSession(self) catch |err| {
- log.warn("failed to refresh global shortcuts={}", .{err});
- };
- }
-
- // 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 sync operation.
- self.loadRuntimeCss() catch |err| switch (err) {
- 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});
- };
-}
-
-fn syncActionAccelerators(self: *App) !void {
- try self.syncActionAccelerator("app.quit", .{ .quit = {} });
- try self.syncActionAccelerator("app.open-config", .{ .open_config = {} });
- try self.syncActionAccelerator("app.reload-config", .{ .reload_config = {} });
- try self.syncActionAccelerator("win.toggle-inspector", .{ .inspector = .toggle });
- try self.syncActionAccelerator("app.show-gtk-inspector", .show_gtk_inspector);
- try self.syncActionAccelerator("win.toggle-command-palette", .toggle_command_palette);
- try self.syncActionAccelerator("win.close", .{ .close_window = {} });
- try self.syncActionAccelerator("win.new-window", .{ .new_window = {} });
- try self.syncActionAccelerator("win.new-tab", .{ .new_tab = {} });
- try self.syncActionAccelerator("win.close-tab", .{ .close_tab = .this });
- try self.syncActionAccelerator("win.split-right", .{ .new_split = .right });
- try self.syncActionAccelerator("win.split-down", .{ .new_split = .down });
- try self.syncActionAccelerator("win.split-left", .{ .new_split = .left });
- try self.syncActionAccelerator("win.split-up", .{ .new_split = .up });
- try self.syncActionAccelerator("win.copy", .{ .copy_to_clipboard = {} });
- try self.syncActionAccelerator("win.paste", .{ .paste_from_clipboard = {} });
- try self.syncActionAccelerator("win.reset", .{ .reset = {} });
- try self.syncActionAccelerator("win.clear", .{ .clear_screen = {} });
- try self.syncActionAccelerator("win.prompt-title", .{ .prompt_surface_title = {} });
-}
-
-fn syncActionAccelerator(
- self: *App,
- gtk_action: [:0]const u8,
- action: input.Binding.Action,
-) !void {
- const gtk_app = self.app.as(gtk.Application);
-
- // Reset it initially
- const zero = [_:null]?[*:0]const u8{};
- gtk_app.setAccelsForAction(gtk_action, &zero);
-
- const trigger = self.config.keybind.set.getTrigger(action) orelse return;
- var buf: [256]u8 = undefined;
- const accel = try key.accelFromTrigger(&buf, trigger) orelse return;
- const accels = [_:null]?[*:0]const u8{accel};
-
- gtk_app.setAccelsForAction(gtk_action, &accels);
-}
-
-fn loadRuntimeCss(
- self: *const App,
-) Allocator.Error!void {
- const alloc = self.core_app.alloc;
-
- var buf: std.ArrayListUnmanaged(u8) = .empty;
- defer buf.deinit(alloc);
-
- const writer = buf.writer(alloc);
-
- const config: *const Config = &self.config;
- const window_theme = config.@"window-theme";
- const unfocused_fill: Config.Color = config.@"unfocused-split-fill" orelse config.background;
- const headerbar_background = config.@"window-titlebar-background" orelse config.background;
- const headerbar_foreground = config.@"window-titlebar-foreground" orelse config.foreground;
-
- 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(
- \\.terminal-window .notebook 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 });
- }
-
- if (gtk_version.runtimeAtLeast(4, 16, 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 => {},
- }
- } else {
- try writer.print(
- \\window.window-theme-ghostty .top-bar,
- \\window.window-theme-ghostty .bottom-bar,
- \\window.window-theme-ghostty box > tabbar {{
- \\ background-color: rgb({d},{d},{d});
- \\ color: rgb({d},{d},{d});
- \\}}
- , .{
- headerbar_background.r,
- headerbar_background.g,
- headerbar_background.b,
- headerbar_foreground.r,
- headerbar_foreground.g,
- headerbar_foreground.b,
- });
- }
-
- const data = try alloc.dupeZ(u8, buf.items);
- defer alloc.free(data);
-
- // Clears any previously loaded CSS from this provider
- loadCssProviderFromData(self.css_provider, data);
-}
-
-fn loadCustomCss(self: *App) !void {
- const alloc = self.core_app.alloc;
-
- const display = gdk.Display.getDefault() orelse {
- log.warn("unable to get display", .{});
- return;
- };
-
- // unload the previously loaded style providers
- for (self.custom_css_providers.items) |provider| {
- gtk.StyleContext.removeProviderForDisplay(
- display,
- provider.as(gtk.StyleProvider),
- );
- provider.unref();
- }
- self.custom_css_providers.clearRetainingCapacity();
-
- for (self.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.err(
- "error opening gtk-custom-css file {s}: {}",
- .{ path, err },
- );
- }
- continue;
- };
- defer file.close();
-
- log.info("loading gtk-custom-css path={s}", .{path});
- const contents = try file.reader().readAllAlloc(
- self.core_app.alloc,
- 5 * 1024 * 1024, // 5MB,
- );
- defer alloc.free(contents);
-
- const data = try alloc.dupeZ(u8, contents);
- defer alloc.free(data);
-
- const provider = gtk.CssProvider.new();
- loadCssProviderFromData(provider, data);
- gtk.StyleContext.addProviderForDisplay(
- display,
- provider.as(gtk.StyleProvider),
- gtk.STYLE_PROVIDER_PRIORITY_USER,
- );
-
- try self.custom_css_providers.append(self.core_app.alloc, provider);
- }
-}
-
-fn loadCssProviderFromData(provider: *gtk.CssProvider, data: [:0]const u8) void {
- if (gtk_version.atLeast(4, 12, 0)) {
- const g_bytes = glib.Bytes.new(data.ptr, data.len);
- defer g_bytes.unref();
-
- provider.loadFromBytes(g_bytes);
- } else {
- provider.loadFromData(data, @intCast(data.len));
- }
-}
-
-/// Called by CoreApp to wake up the event loop.
-pub fn wakeup(_: App) void {
- glib.MainContext.wakeup(null);
-}
-
-/// Run the event loop. This doesn't return until the app exits.
-pub fn run(self: *App) !void {
- // Running will be false when we're not the primary instance and should
- // exit (GTK single instance mode). If we're not running, we're done
- // right away.
- if (!self.running) return;
-
- // If we are running, then we proceed to setup our app.
-
- // Setup our cgroup configurations for our surfaces.
- if (switch (self.config.@"linux-cgroup") {
- .never => false,
- .always => true,
- .@"single-instance" => self.single_instance,
- }) cgroup: {
- const path = cgroup.init(self) catch |err| {
- // If we can't initialize cgroups then that's okay. We
- // want to continue to run so we just won't isolate surfaces.
- // NOTE(mitchellh): do we want a config to force it?
- log.warn(
- "failed to initialize cgroups, terminals will not be isolated err={}",
- .{err},
- );
-
- // If we have hard fail enabled then we exit now.
- if (self.config.@"linux-cgroup-hard-fail") {
- log.err("linux-cgroup-hard-fail enabled, exiting", .{});
- return error.CgroupInitFailed;
- }
-
- break :cgroup;
- };
-
- log.info("cgroup isolation enabled base={s}", .{path});
- self.transient_cgroup_base = path;
- } else log.debug("cgroup isolation disabled config={}", .{self.config.@"linux-cgroup"});
-
- // Setup color scheme notifications
- const style_manager: *adw.StyleManager = self.app.getStyleManager();
- _ = gobject.Object.signals.notify.connect(
- style_manager,
- *App,
- adwNotifyDark,
- self,
- .{
- .detail = "dark",
- },
- );
-
- // Make an initial request to set up the color scheme
- const light = style_manager.getDark() == 0;
- self.colorSchemeEvent(if (light) .light else .dark);
-
- // Setup our actions
- self.initActions();
-
- // On startup, we want to check for configuration errors right away
- // so we can show our error window. We also need to setup other initial
- // state.
- self.syncConfigChanges(null) catch |err| {
- log.warn("error handling configuration changes err={}", .{err});
- };
-
- // Tell systemd that we are ready.
- systemd.notify.ready();
-
- while (self.running) {
- _ = glib.MainContext.iteration(self.ctx, 1);
-
- // Tick the terminal app and see if we should quit.
- try self.core_app.tick(self);
-
- // 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.
- if (!self.config.@"quit-after-last-window-closed") break :q false;
-
- // If the quit timer has expired, quit.
- if (self.quit_timer == .expired) break :q true;
-
- // There's no quit timer running, or it hasn't expired, don't quit.
- break :q false;
- };
-
- if (must_quit) self.quit();
- }
-}
-
-// This timeout function is started when no surfaces are open. It can be
-// cancelled if a new surface is opened before the timer expires.
-pub fn gtkQuitTimerExpired(ud: ?*anyopaque) callconv(.c) c_int {
- const self: *App = @ptrCast(@alignCast(ud));
- self.quit_timer = .{ .expired = {} };
- return 0;
-}
-
-/// This will get called when there are no more open surfaces.
-fn startQuitTimer(self: *App) void {
- // Cancel any previous timer.
- self.stopQuitTimer();
-
- // This is a no-op unless we are configured to quit after last window is closed.
- if (!self.config.@"quit-after-last-window-closed") return;
-
- if (self.config.@"quit-after-last-window-closed-delay") |v| {
- // If a delay is configured, set a timeout function to quit after the delay.
- self.quit_timer = .{
- .active = glib.timeoutAdd(
- v.asMilliseconds(),
- gtkQuitTimerExpired,
- self,
- ),
- };
- } else {
- // If no delay is configured, treat it as expired.
- self.quit_timer = .{ .expired = {} };
- }
-}
-
-/// This will get called when a new surface gets opened.
-fn stopQuitTimer(self: *App) void {
- switch (self.quit_timer) {
- .off => {},
- .expired => self.quit_timer = .{ .off = {} },
- .active => |source| {
- if (glib.Source.remove(source) == 0) {
- log.warn("unable to remove quit timer source={d}", .{source});
- }
- self.quit_timer = .{ .off = {} };
- },
- }
-}
-
-/// Redraw the inspector for the given surface.
-pub fn redrawInspector(self: *App, surface: *Surface) void {
- _ = self;
- surface.queueInspectorRender();
-}
-
-/// Called by CoreApp to create a new window with a new surface.
-fn newWindow(self: *App, parent_: ?*CoreSurface) !void {
- const alloc = self.core_app.alloc;
-
- // Allocate a fixed pointer for our window. We try to minimize
- // allocations but windows and other GUI requirements are so minimal
- // compared to the steady-state terminal operation so we use heap
- // allocation for this.
- //
- // The allocation is owned by the GtkWindow created. It will be
- // freed when the window is closed.
- var window = try Window.create(alloc, self);
-
- // Add our initial tab
- try window.newTab(parent_);
-
- // Show the new window
- window.present();
-}
-
-fn setSecureInput(_: *App, target: apprt.Target, value: apprt.action.SecureInput) void {
- switch (target) {
- .app => {},
- .surface => |surface| {
- surface.rt_surface.setSecureInput(value);
- },
- }
-}
-
-fn closeWindow(_: *App, target: apprt.action.Target) !bool {
- switch (target) {
- .app => return false,
- .surface => |v| {
- const window = v.rt_surface.container.window() orelse return false;
- window.closeWithConfirmation();
- return true;
- },
- }
-}
-
-fn quit(self: *App) void {
- // If we're already not running, do nothing.
- if (!self.running) return;
-
- // If the app says we don't need to confirm, then we can quit now.
- if (!self.core_app.needsConfirmQuit()) {
- self.quitNow();
- return;
- }
-
- CloseDialog.show(.{ .app = self }) catch |err| {
- log.err("failed to open close dialog={}", .{err});
- };
-}
-
-/// This immediately destroys all windows, forcing the application to quit.
-pub fn quitNow(self: *App) void {
- 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!
- //
- // Since we don't use gobject directly we can't check class,
- // so we use a heuristic based on CSS class.
- if (window.as(gtk.Widget).hasCssClass("terminal-window") != 0) {
- window.destroy();
- }
- }
- }.callback, null);
-
- self.running = false;
-}
-
-// SIGUSR2 signal handler via g_unix_signal_add
-fn sigusr2(ud: ?*anyopaque) callconv(.c) c_int {
- const self: *App = @ptrCast(@alignCast(ud orelse
- return @intFromBool(glib.SOURCE_CONTINUE)));
-
- log.info("received SIGUSR2, reloading configuration", .{});
- self.reloadConfig(.app, .{ .soft = false }) catch |err| {
- log.err(
- "error reloading configuration for SIGUSR2: {}",
- .{err},
- );
- };
-
- return @intFromBool(glib.SOURCE_CONTINUE);
-}
-
-/// This is called by the `activate` signal. This is sent on program startup and
-/// also when a secondary instance launches and requests a new window.
-fn gtkActivate(_: *adw.Application, core_app: *CoreApp) callconv(.c) void {
- // Queue a new window
- _ = core_app.mailbox.push(.{
- .new_window = .{},
- }, .{ .forever = {} });
-}
-
-fn gtkWindowAdded(
- _: *adw.Application,
- window: *gtk.Window,
- core_app: *CoreApp,
-) callconv(.c) void {
- // Request the is-active property change so we can detect
- // when our app loses focus.
- _ = gobject.Object.signals.notify.connect(
- window,
- *CoreApp,
- gtkWindowIsActive,
- core_app,
- .{
- .detail = "is-active",
- },
- );
-}
-
-fn gtkWindowRemoved(
- _: *adw.Application,
- _: *gtk.Window,
- core_app: *CoreApp,
-) callconv(.c) void {
- // Recheck if we are focused
- gtkWindowIsActive(null, undefined, core_app);
-}
-
-fn gtkWindowIsActive(
- window: ?*gtk.Window,
- _: *gobject.ParamSpec,
- core_app: *CoreApp,
-) callconv(.c) void {
- // If our window is active, then we can tell the app
- // that we are focused.
- if (window) |w| {
- if (w.isActive() != 0) {
- core_app.focusEvent(true);
- return;
- }
- }
-
- // If the window becomes inactive, we need to check if any
- // other windows are active. If not, then we are no longer
- // focused.
- {
- const list = gtk.Window.listToplevels();
- defer list.free();
- var current: ?*glib.List = list;
- while (current) |elem| : (current = elem.f_next) {
- // If the window is active then we are still focused.
- // This is another window since we did our check above.
- // That window should trigger its own is-active
- // callback so we don't need to call it here.
- const w: *gtk.Window = @alignCast(@ptrCast(elem.f_data));
- if (w.isActive() == 1) return;
- }
- }
-
- // We are not focused
- core_app.focusEvent(false);
-}
-
-fn adwNotifyDark(
- style_manager: *adw.StyleManager,
- _: *gobject.ParamSpec,
- self: *App,
-) callconv(.c) void {
- const color_scheme: apprt.ColorScheme = if (style_manager.getDark() == 0)
- .light
- else
- .dark;
-
- self.colorSchemeEvent(color_scheme);
-}
-
-fn colorSchemeEvent(
- self: *App,
- scheme: apprt.ColorScheme,
-) void {
- self.core_app.colorSchemeEvent(self, scheme) catch |err| {
- log.err("error updating app color scheme err={}", .{err});
- };
-
- for (self.core_app.surfaces.items) |surface| {
- surface.core_surface.colorSchemeCallback(scheme) catch |err| {
- log.err("unable to tell surface about color scheme change err={}", .{err});
- };
- }
-}
-
-fn gtkActionOpenConfig(
- _: *gio.SimpleAction,
- _: ?*glib.Variant,
- self: *App,
-) callconv(.c) void {
- _ = self.core_app.mailbox.push(.{
- .open_config = {},
- }, .{ .forever = {} });
-}
-
-fn gtkActionReloadConfig(
- _: *gio.SimpleAction,
- _: ?*glib.Variant,
- self: *App,
-) callconv(.c) void {
- self.reloadConfig(.app, .{}) catch |err| {
- log.err("error reloading configuration: {}", .{err});
- };
-}
-
-fn gtkActionQuit(
- _: *gio.SimpleAction,
- _: ?*glib.Variant,
- self: *App,
-) callconv(.c) void {
- self.core_app.performAction(self, .quit) catch |err| {
- log.err("error quitting err={}", .{err});
- };
-}
-
-/// Action sent by the window manager asking us to present a specific surface to
-/// the user. Usually because the user clicked on a desktop notification.
-fn gtkActionPresentSurface(
- _: *gio.SimpleAction,
- parameter_: ?*glib.Variant,
- self: *App,
-) 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 receiived a u64 from the system.
- if (glib.Variant.isOfType(parameter, t) == 0) {
- return;
- }
-
- // Convert that u64 to pointer to a core surface. A value of zero
- // means that there was no target surface for the notification so
- // we don't focus any surface.
- const ptr_int = parameter.getUint64();
- if (ptr_int == 0) return;
- const surface: *CoreSurface = @ptrFromInt(ptr_int);
-
- // Send a message through the core app mailbox rather than presenting the
- // surface directly so that it can validate that the surface pointer is
- // valid. We could get an invalid pointer if a desktop notification outlives
- // a Ghostty instance and a new one starts up, or there are multiple Ghostty
- // instances running.
- _ = self.core_app.mailbox.push(
- .{
- .surface_message = .{
- .surface = surface,
- .message = .{ .present_surface = {} },
- },
- },
- .{ .forever = {} },
- );
-}
-
-fn gtkActionShowGTKInspector(
- _: *gio.SimpleAction,
- _: ?*glib.Variant,
- self: *App,
-) callconv(.c) void {
- self.core_app.performAction(self, .show_gtk_inspector) catch |err| {
- log.err("error showing GTK inspector err={}", .{err});
- };
-}
-
-fn gtkActionNewWindow(
- _: *gio.SimpleAction,
- parameter_: ?*glib.Variant,
- self: *App,
-) callconv(.c) void {
- log.debug("received new window action", .{});
-
- parameter: {
- // were we given a parameter?
- const parameter = parameter_ orelse break :parameter;
-
- const as = glib.VariantType.new("as");
- defer as.free();
-
- // ensure that the supplied parameter is an array of strings
- if (glib.Variant.isOfType(parameter, as) == 0) {
- log.warn("parameter is of type {s}", .{parameter.getTypeString()});
- break :parameter;
- }
-
- const s = glib.VariantType.new("s");
- defer s.free();
-
- var it: glib.VariantIter = undefined;
- _ = it.init(parameter);
-
- while (it.nextValue()) |value| {
- defer value.unref();
-
- // just to be sure
- if (value.isOfType(s) == 0) continue;
-
- var len: usize = undefined;
- const buf = value.getString(&len);
- const str = buf[0..len];
-
- log.debug("new-window command argument: {s}", .{str});
- }
- }
-
- _ = self.core_app.mailbox.push(.{
- .new_window = .{},
- }, .{ .forever = {} });
-}
-
-/// This is called to setup the action map that this application supports.
-/// This should be called only once on startup.
-fn initActions(self: *App) void {
- // The set of actions. Each action has (in order):
- // [0] The action name
- // [1] The callback function
- // [2] The GVariantType of the parameter
- //
- // For action names:
- // https://docs.gtk.org/gio/type_func.Action.name_is_valid.html
- const t = glib.ext.VariantType.newFor(u64);
- defer t.free();
-
- const as = glib.VariantType.new("as");
- defer as.free();
-
- const actions = .{
- .{ "quit", gtkActionQuit, null },
- .{ "open-config", gtkActionOpenConfig, null },
- .{ "reload-config", gtkActionReloadConfig, null },
- .{ "present-surface", gtkActionPresentSurface, t },
- .{ "show-gtk-inspector", gtkActionShowGTKInspector, null },
- .{ "new-window", gtkActionNewWindow, null },
- .{ "new-window-command", gtkActionNewWindow, as },
- };
-
- inline for (actions) |entry| {
- const action = gio.SimpleAction.new(entry[0], entry[2]);
- defer action.unref();
- _ = gio.SimpleAction.signals.activate.connect(
- action,
- *App,
- entry[1],
- self,
- .{},
- );
- const action_map = self.app.as(gio.ActionMap);
- action_map.addAction(action.as(gio.Action));
- }
-}
-
-fn openConfig(self: *App) !bool {
- // Get the config file path
- const alloc = self.core_app.alloc;
- 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.
- self.openUrl(.{ .kind = .text, .url = path });
- return true;
-}
-
-fn openUrl(
- app: *App,
- value: apprt.action.OpenUrl,
-) void {
- // TODO: use https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.OpenURI.html
-
- // 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(
- app.core_app.alloc,
- value.kind,
- value.url,
- ) catch |err| log.warn("unable to open url: {}", .{err});
-}
diff --git a/src/apprt/gtk/Builder.zig b/src/apprt/gtk/Builder.zig
deleted file mode 100644
index dbd765ba3..000000000
--- a/src/apprt/gtk/Builder.zig
+++ /dev/null
@@ -1,77 +0,0 @@
-/// Wrapper around GTK's builder APIs that perform some comptime checks.
-const Builder = @This();
-
-const std = @import("std");
-
-const gtk = @import("gtk");
-const gobject = @import("gobject");
-
-resource_name: [:0]const u8,
-builder: ?*gtk.Builder,
-
-pub fn init(
- /// The "name" of the resource.
- comptime name: []const u8,
- /// The major version of the minimum Adwaita version that is required to use
- /// this resource.
- comptime major: u16,
- /// The minor version of the minimum Adwaita version that is required to use
- /// this resource.
- comptime minor: u16,
-) Builder {
- const resource_path = comptime resource_path: {
- const gresource = @import("gresource.zig");
- // Check to make sure that our file is listed as a
- // `blueprint_file` in `gresource.zig`. If it isn't Ghostty
- // could crash at runtime when we try and load a nonexistent
- // GResource.
- for (gresource.blueprint_files) |file| {
- if (major != file.major or minor != file.minor or !std.mem.eql(u8, file.name, name)) continue;
- // Use @embedFile to make sure that the `.blp` file exists
- // at compile time. Zig _should_ discard the data so that
- // it doesn't end up in the final executable. At runtime we
- // will load the data from a GResource.
- const blp_filename = std.fmt.comptimePrint(
- "ui/{d}.{d}/{s}.blp",
- .{
- file.major,
- file.minor,
- file.name,
- },
- );
- _ = @embedFile(blp_filename);
- break :resource_path std.fmt.comptimePrint(
- "/com/mitchellh/ghostty/ui/{d}.{d}/{s}.ui",
- .{
- file.major,
- file.minor,
- file.name,
- },
- );
- } else @compileError("missing blueprint file '" ++ name ++ "' in gresource.zig");
- };
-
- return .{
- .resource_name = resource_path,
- .builder = null,
- };
-}
-
-pub fn setWidgetClassTemplate(self: *const Builder, class: *gtk.WidgetClass) void {
- class.setTemplateFromResource(self.resource_name);
-}
-
-pub fn getObject(self: *Builder, comptime T: type, name: [:0]const u8) ?*T {
- const builder = builder: {
- if (self.builder) |builder| break :builder builder;
- const builder = gtk.Builder.newFromResource(self.resource_name);
- self.builder = builder;
- break :builder builder;
- };
-
- return gobject.ext.cast(T, builder.getObject(name) orelse return null);
-}
-
-pub fn deinit(self: *const Builder) void {
- if (self.builder) |builder| builder.unref();
-}
diff --git a/src/apprt/gtk/ClipboardConfirmationWindow.zig b/src/apprt/gtk/ClipboardConfirmationWindow.zig
deleted file mode 100644
index a1d622143..000000000
--- a/src/apprt/gtk/ClipboardConfirmationWindow.zig
+++ /dev/null
@@ -1,212 +0,0 @@
-/// Clipboard Confirmation Window
-const ClipboardConfirmation = @This();
-
-const std = @import("std");
-const Allocator = std.mem.Allocator;
-
-const gtk = @import("gtk");
-const adw = @import("adw");
-const gobject = @import("gobject");
-const gio = @import("gio");
-
-const apprt = @import("../../apprt.zig");
-const CoreSurface = @import("../../Surface.zig");
-const App = @import("App.zig");
-const Builder = @import("Builder.zig");
-const adw_version = @import("adw_version.zig");
-
-const log = std.log.scoped(.gtk);
-
-const DialogType = if (adw_version.supportsDialogs()) adw.AlertDialog else adw.MessageDialog;
-
-app: *App,
-dialog: *DialogType,
-data: [:0]u8,
-core_surface: *CoreSurface,
-pending_req: apprt.ClipboardRequest,
-text_view: *gtk.TextView,
-text_view_scroll: *gtk.ScrolledWindow,
-reveal_button: *gtk.Button,
-hide_button: *gtk.Button,
-remember_choice: if (adw_version.supportsSwitchRow()) ?*adw.SwitchRow else ?*anyopaque,
-
-pub fn create(
- app: *App,
- data: []const u8,
- core_surface: *CoreSurface,
- request: apprt.ClipboardRequest,
- is_secure_input: bool,
-) !void {
- if (app.clipboard_confirmation_window != null) return error.WindowAlreadyExists;
-
- const alloc = app.core_app.alloc;
- const self = try alloc.create(ClipboardConfirmation);
- errdefer alloc.destroy(self);
-
- try self.init(
- app,
- data,
- core_surface,
- request,
- is_secure_input,
- );
-
- app.clipboard_confirmation_window = self;
-}
-
-/// Not public because this should be called by the GTK lifecycle.
-fn destroy(self: *ClipboardConfirmation) void {
- const alloc = self.app.core_app.alloc;
- self.app.clipboard_confirmation_window = null;
- alloc.free(self.data);
- alloc.destroy(self);
-}
-
-fn init(
- self: *ClipboardConfirmation,
- app: *App,
- data: []const u8,
- core_surface: *CoreSurface,
- request: apprt.ClipboardRequest,
- is_secure_input: bool,
-) !void {
- var builder: Builder = switch (DialogType) {
- adw.AlertDialog => switch (request) {
- .osc_52_read => .init("ccw-osc-52-read", 1, 5),
- .osc_52_write => .init("ccw-osc-52-write", 1, 5),
- .paste => .init("ccw-paste", 1, 5),
- },
- adw.MessageDialog => switch (request) {
- .osc_52_read => .init("ccw-osc-52-read", 1, 2),
- .osc_52_write => .init("ccw-osc-52-write", 1, 2),
- .paste => .init("ccw-paste", 1, 2),
- },
- else => unreachable,
- };
- defer builder.deinit();
-
- const dialog = builder.getObject(DialogType, "clipboard_confirmation_window").?;
- const text_view = builder.getObject(gtk.TextView, "text_view").?;
- const reveal_button = builder.getObject(gtk.Button, "reveal_button").?;
- const hide_button = builder.getObject(gtk.Button, "hide_button").?;
- const text_view_scroll = builder.getObject(gtk.ScrolledWindow, "text_view_scroll").?;
- const remember_choice = if (adw_version.supportsSwitchRow())
- builder.getObject(adw.SwitchRow, "remember_choice")
- else
- null;
-
- const copy = try app.core_app.alloc.dupeZ(u8, data);
- errdefer app.core_app.alloc.free(copy);
- self.* = .{
- .app = app,
- .dialog = dialog,
- .data = copy,
- .core_surface = core_surface,
- .pending_req = request,
- .text_view = text_view,
- .text_view_scroll = text_view_scroll,
- .reveal_button = reveal_button,
- .hide_button = hide_button,
- .remember_choice = remember_choice,
- };
-
- const buffer = gtk.TextBuffer.new(null);
- errdefer buffer.unref();
- buffer.insertAtCursor(copy.ptr, @intCast(copy.len));
- text_view.setBuffer(buffer);
-
- if (is_secure_input) {
- text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(false));
- self.text_view.as(gtk.Widget).addCssClass("blurred");
-
- self.reveal_button.as(gtk.Widget).setVisible(@intFromBool(true));
-
- _ = gtk.Button.signals.clicked.connect(
- reveal_button,
- *ClipboardConfirmation,
- gtkRevealButtonClicked,
- self,
- .{},
- );
- _ = gtk.Button.signals.clicked.connect(
- hide_button,
- *ClipboardConfirmation,
- gtkHideButtonClicked,
- self,
- .{},
- );
- }
-
- _ = DialogType.signals.response.connect(
- dialog,
- *ClipboardConfirmation,
- gtkResponse,
- self,
- .{},
- );
-
- switch (DialogType) {
- adw.AlertDialog => {
- const parent: ?*gtk.Widget = widget: {
- const window = core_surface.rt_surface.container.window() orelse break :widget null;
- break :widget window.window.as(gtk.Widget);
- };
- dialog.as(adw.Dialog).present(parent);
- },
- adw.MessageDialog => dialog.as(gtk.Window).present(),
- else => unreachable,
- }
-}
-
-fn handleResponse(self: *ClipboardConfirmation, response: [*:0]const u8) void {
- const is_ok = std.mem.orderZ(u8, response, "ok") == .eq;
-
- if (is_ok) {
- self.core_surface.completeClipboardRequest(
- self.pending_req,
- self.data,
- true,
- ) catch |err| {
- log.err("Failed to requeue clipboard request: {}", .{err});
- };
- }
-
- if (self.remember_choice) |remember| remember: {
- if (!adw_version.supportsSwitchRow()) break :remember;
- if (remember.getActive() == 0) break :remember;
-
- switch (self.pending_req) {
- .osc_52_read => self.core_surface.config.clipboard_read = if (is_ok) .allow else .deny,
- .osc_52_write => self.core_surface.config.clipboard_write = if (is_ok) .allow else .deny,
- .paste => {},
- }
- }
-
- self.destroy();
-}
-fn gtkChoose(dialog_: ?*gobject.Object, result: *gio.AsyncResult, ud: ?*anyopaque) callconv(.c) void {
- const dialog = gobject.ext.cast(DialogType, dialog_.?).?;
- const self: *ClipboardConfirmation = @ptrCast(@alignCast(ud.?));
- const response = dialog.chooseFinish(result);
- self.handleResponse(response);
-}
-
-fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation) callconv(.c) void {
- self.handleResponse(response);
-}
-
-fn gtkRevealButtonClicked(_: *gtk.Button, self: *ClipboardConfirmation) callconv(.c) void {
- self.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(true));
- self.text_view.as(gtk.Widget).removeCssClass("blurred");
-
- self.hide_button.as(gtk.Widget).setVisible(@intFromBool(true));
- self.reveal_button.as(gtk.Widget).setVisible(@intFromBool(false));
-}
-
-fn gtkHideButtonClicked(_: *gtk.Button, self: *ClipboardConfirmation) callconv(.c) void {
- self.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(false));
- self.text_view.as(gtk.Widget).addCssClass("blurred");
-
- self.hide_button.as(gtk.Widget).setVisible(@intFromBool(false));
- self.reveal_button.as(gtk.Widget).setVisible(@intFromBool(true));
-}
diff --git a/src/apprt/gtk/CloseDialog.zig b/src/apprt/gtk/CloseDialog.zig
deleted file mode 100644
index 559737cf4..000000000
--- a/src/apprt/gtk/CloseDialog.zig
+++ /dev/null
@@ -1,151 +0,0 @@
-const CloseDialog = @This();
-const std = @import("std");
-
-const gobject = @import("gobject");
-const gio = @import("gio");
-const adw = @import("adw");
-const gtk = @import("gtk");
-
-const i18n = @import("../../os/main.zig").i18n;
-const App = @import("App.zig");
-const Window = @import("Window.zig");
-const Tab = @import("Tab.zig");
-const Surface = @import("Surface.zig");
-const adwaita = @import("adw_version.zig");
-
-const log = std.log.scoped(.close_dialog);
-
-// We don't fall back to the GTK Message/AlertDialogs since
-// we don't plan to support libadw < 1.2 as of time of writing
-// TODO: Switch to just adw.AlertDialog when we drop Debian 12 support
-const DialogType = if (adwaita.supportsDialogs()) adw.AlertDialog else adw.MessageDialog;
-
-/// Open the dialog when the user requests to close a window/tab/split/etc.
-/// but there's still one or more running processes inside the target that
-/// cannot be closed automatically. We then ask the user whether they want
-/// to terminate existing processes.
-pub fn show(target: Target) !void {
- // If we don't have a possible window to ask the user,
- // in most situations (e.g. when a split isn't attached to a window)
- // we should just close unconditionally.
- const dialog_window = target.dialogWindow() orelse {
- target.close();
- return;
- };
-
- const dialog = switch (DialogType) {
- adw.AlertDialog => adw.AlertDialog.new(target.title(), target.body()),
- adw.MessageDialog => adw.MessageDialog.new(dialog_window, target.title(), target.body()),
- else => unreachable,
- };
-
- // AlertDialog and MessageDialog have essentially the same API,
- // so we can cheat a little here
- dialog.addResponse("cancel", i18n._("Cancel"));
- dialog.setCloseResponse("cancel");
-
- dialog.addResponse("close", i18n._("Close"));
- dialog.setResponseAppearance("close", .destructive);
-
- // Need a stable pointer
- const target_ptr = try target.allocator().create(Target);
- target_ptr.* = target;
-
- _ = DialogType.signals.response.connect(dialog, *Target, responseCallback, target_ptr, .{});
-
- switch (DialogType) {
- adw.AlertDialog => dialog.as(adw.Dialog).present(dialog_window.as(gtk.Widget)),
- adw.MessageDialog => dialog.as(gtk.Window).present(),
- else => unreachable,
- }
-}
-
-fn responseCallback(
- _: *DialogType,
- response: [*:0]const u8,
- target: *Target,
-) callconv(.c) void {
- const alloc = target.allocator();
- defer alloc.destroy(target);
-
- if (std.mem.orderZ(u8, response, "close") == .eq) target.close();
-}
-
-/// 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 = union(enum) {
- app: *App,
- window: *Window,
- tab: *Tab,
- surface: *Surface,
-
- pub fn title(self: Target) [*:0]const u8 {
- return switch (self) {
- .app => i18n._("Quit Ghostty?"),
- .window => i18n._("Close Window?"),
- .tab => i18n._("Close Tab?"),
- .surface => i18n._("Close Split?"),
- };
- }
-
- pub fn body(self: Target) [*:0]const u8 {
- return switch (self) {
- .app => i18n._("All terminal sessions will be terminated."),
- .window => i18n._("All terminal sessions in this window will be terminated."),
- .tab => i18n._("All terminal sessions in this tab will be terminated."),
- .surface => i18n._("The currently running process in this split will be terminated."),
- };
- }
-
- pub fn dialogWindow(self: Target) ?*gtk.Window {
- return switch (self) {
- .app => {
- // Find the currently focused window. We don't store this
- // anywhere inside the App structure for some reason, so
- // we have to query every single open window and see which
- // one is active (focused and receiving keyboard input)
- const list = gtk.Window.listToplevels();
- defer list.free();
-
- const focused = list.findCustom(null, findActiveWindow);
- return @ptrCast(@alignCast(focused.f_data));
- },
- .window => |v| v.window.as(gtk.Window),
- .tab => |v| v.window.window.as(gtk.Window),
- .surface => |v| {
- const window_ = v.container.window() orelse return null;
- return window_.window.as(gtk.Window);
- },
- };
- }
-
- fn allocator(self: Target) std.mem.Allocator {
- return switch (self) {
- .app => |v| v.core_app.alloc,
- .window => |v| v.app.core_app.alloc,
- .tab => |v| v.window.app.core_app.alloc,
- .surface => |v| v.app.core_app.alloc,
- };
- }
-
- fn close(self: Target) void {
- switch (self) {
- .app => |v| v.quitNow(),
- .window => |v| v.window.as(gtk.Window).destroy(),
- .tab => |v| v.remove(),
- .surface => |v| v.container.remove(),
- }
- }
-};
-
-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/src/apprt/gtk/CommandPalette.zig b/src/apprt/gtk/CommandPalette.zig
deleted file mode 100644
index 076459dbd..000000000
--- a/src/apprt/gtk/CommandPalette.zig
+++ /dev/null
@@ -1,258 +0,0 @@
-const CommandPalette = @This();
-
-const std = @import("std");
-const Allocator = std.mem.Allocator;
-
-const adw = @import("adw");
-const gio = @import("gio");
-const gobject = @import("gobject");
-const gtk = @import("gtk");
-
-const configpkg = @import("../../config.zig");
-const inputpkg = @import("../../input.zig");
-const key = @import("key.zig");
-const Builder = @import("Builder.zig");
-const Window = @import("Window.zig");
-
-const log = std.log.scoped(.command_palette);
-
-window: *Window,
-
-arena: std.heap.ArenaAllocator,
-
-/// 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 fn init(self: *CommandPalette, window: *Window) !void {
- // Register the custom command type *before* initializing the builder
- // If we don't do this now, the builder will complain that it doesn't know
- // about this type and fail to initialize
- _ = Command.getGObjectType();
-
- var builder = Builder.init("command-palette", 1, 5);
- defer builder.deinit();
-
- self.* = .{
- .window = window,
- .arena = .init(window.app.core_app.alloc),
- .dialog = builder.getObject(adw.Dialog, "command-palette").?,
- .search = builder.getObject(gtk.SearchEntry, "search").?,
- .view = builder.getObject(gtk.ListView, "view").?,
- .model = builder.getObject(gtk.SingleSelection, "model").?,
- .source = builder.getObject(gio.ListStore, "source").?,
- };
-
- // Manually take a reference here so that the dialog
- // remains in memory after closing
- self.dialog.ref();
- errdefer self.dialog.unref();
-
- _ = gtk.SearchEntry.signals.stop_search.connect(
- self.search,
- *CommandPalette,
- searchStopped,
- self,
- .{},
- );
-
- _ = gtk.SearchEntry.signals.activate.connect(
- self.search,
- *CommandPalette,
- searchActivated,
- self,
- .{},
- );
-
- _ = gtk.ListView.signals.activate.connect(
- self.view,
- *CommandPalette,
- rowActivated,
- self,
- .{},
- );
-
- try self.updateConfig(&self.window.app.config);
-}
-
-pub fn deinit(self: *CommandPalette) void {
- self.arena.deinit();
- self.dialog.unref();
-}
-
-pub fn toggle(self: *CommandPalette) void {
- self.dialog.present(self.window.window.as(gtk.Widget));
- // Focus on the search bar when opening the dialog
- _ = self.search.as(gtk.Widget).grabFocus();
-}
-
-pub fn updateConfig(self: *CommandPalette, config: *const configpkg.Config) !void {
- // Clear existing binds and clear allocated data
- self.source.removeAll();
- _ = self.arena.reset(.retain_capacity);
-
- for (config.@"command-palette-entry".value.items) |command| {
- // Filter out actions that are not implemented
- // or don't make sense for GTK
- switch (command.action) {
- .close_all_windows,
- .toggle_secure_input,
- .check_for_updates,
- .redo,
- .undo,
- .reset_window_size,
- .toggle_window_float_on_top,
- => continue,
-
- else => {},
- }
-
- const cmd = try Command.new(
- self.arena.allocator(),
- command,
- config.keybind.set,
- );
- const cmd_ref = cmd.as(gobject.Object);
- self.source.append(cmd_ref);
- cmd_ref.unref();
- }
-}
-
-fn activated(self: *CommandPalette, pos: c_uint) void {
- // Use self.model and not self.source here to use the list of *visible* results
- const object = self.model.as(gio.ListModel).getObject(pos) orelse return;
- const cmd = gobject.ext.cast(Command, object) orelse return;
-
- // 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.dialog.close();
-
- const action = inputpkg.Binding.Action.parse(
- std.mem.span(cmd.cmd_c.action_key),
- ) catch |err| {
- log.err("got invalid action={s} ({})", .{ cmd.cmd_c.action_key, err });
- return;
- };
-
- self.window.performBindingAction(action);
-}
-
-fn searchStopped(_: *gtk.SearchEntry, self: *CommandPalette) callconv(.c) void {
- // ESC was pressed - close the palette
- _ = self.dialog.close();
-}
-
-fn searchActivated(_: *gtk.SearchEntry, self: *CommandPalette) callconv(.c) void {
- // If Enter is pressed, activate the selected entry
- self.activated(self.model.getSelected());
-}
-
-fn rowActivated(_: *gtk.ListView, pos: c_uint, self: *CommandPalette) callconv(.c) void {
- self.activated(pos);
-}
-
-/// 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 {
- parent: Parent,
- cmd_c: inputpkg.Command.C,
-
- pub const getGObjectType = gobject.ext.defineClass(Command, .{
- .name = "GhosttyCommand",
- .classInit = Class.init,
- });
-
- pub fn new(alloc: Allocator, cmd: inputpkg.Command, keybinds: inputpkg.Binding.Set) !*Command {
- const self = gobject.ext.newInstance(Command, .{});
- var buf: [64]u8 = undefined;
-
- const action = action: {
- const trigger = keybinds.getTrigger(cmd.action) orelse break :action null;
- const accel = try key.accelFromTrigger(&buf, trigger) orelse break :action null;
- break :action try alloc.dupeZ(u8, accel);
- };
-
- self.cmd_c = .{
- .title = cmd.title.ptr,
- .description = cmd.description.ptr,
- .action = if (action) |v| v.ptr else "",
- .action_key = try std.fmt.allocPrintZ(alloc, "{}", .{cmd.action}),
- };
-
- return self;
- }
-
- fn as(self: *Command, comptime T: type) *T {
- return gobject.ext.as(T, self);
- }
-
- pub const Parent = gobject.Object;
-
- pub const Class = extern struct {
- parent: Parent.Class,
-
- pub const Instance = Command;
-
- pub fn init(class: *Class) callconv(.c) void {
- const info = @typeInfo(inputpkg.Command.C).@"struct";
-
- // Expose all fields on the Command.C struct as properties
- // that can be accessed by the GObject type system
- // (and by extension, blueprints)
- const properties = comptime props: {
- var props: [info.fields.len]type = undefined;
-
- for (info.fields, 0..) |field, i| {
- const accessor = struct {
- fn getter(cmd: *Command) ?[:0]const u8 {
- return std.mem.span(@field(cmd.cmd_c, field.name));
- }
- };
-
- // "Canonicalize" field names into the format GObject expects
- const prop_name = prop_name: {
- var buf: [field.name.len:0]u8 = undefined;
- _ = std.mem.replace(u8, field.name, "_", "-", &buf);
- break :prop_name buf;
- };
-
- props[i] = gobject.ext.defineProperty(
- &prop_name,
- Command,
- ?[:0]const u8,
- .{
- .default = null,
- .accessor = gobject.ext.typedAccessor(
- Command,
- ?[:0]const u8,
- .{
- .getter = &accessor.getter,
- },
- ),
- },
- );
- }
-
- break :props props;
- };
-
- gobject.ext.registerProperties(class, &properties);
- }
- };
-};
diff --git a/src/apprt/gtk/ConfigErrorsDialog.zig b/src/apprt/gtk/ConfigErrorsDialog.zig
deleted file mode 100644
index da70ccce1..000000000
--- a/src/apprt/gtk/ConfigErrorsDialog.zig
+++ /dev/null
@@ -1,102 +0,0 @@
-/// Configuration errors window.
-const ConfigErrorsDialog = @This();
-
-const std = @import("std");
-const Allocator = std.mem.Allocator;
-
-const gobject = @import("gobject");
-const gio = @import("gio");
-const gtk = @import("gtk");
-const adw = @import("adw");
-
-const build_config = @import("../../build_config.zig");
-const configpkg = @import("../../config.zig");
-const Config = configpkg.Config;
-
-const App = @import("App.zig");
-const Window = @import("Window.zig");
-const Builder = @import("Builder.zig");
-const adw_version = @import("adw_version.zig");
-
-const log = std.log.scoped(.gtk);
-
-const DialogType = if (adw_version.supportsDialogs()) adw.AlertDialog else adw.MessageDialog;
-
-builder: Builder,
-dialog: *DialogType,
-error_message: *gtk.TextBuffer,
-
-pub fn maybePresent(app: *App, window: ?*Window) void {
- if (app.config._diagnostics.empty()) return;
-
- const config_errors_dialog = config_errors_dialog: {
- if (app.config_errors_dialog) |config_errors_dialog| break :config_errors_dialog config_errors_dialog;
-
- var builder: Builder = switch (DialogType) {
- adw.AlertDialog => .init("config-errors-dialog", 1, 5),
- adw.MessageDialog => .init("config-errors-dialog", 1, 2),
- else => unreachable,
- };
-
- const dialog = builder.getObject(DialogType, "config_errors_dialog").?;
- const error_message = builder.getObject(gtk.TextBuffer, "error_message").?;
-
- _ = DialogType.signals.response.connect(dialog, *App, onResponse, app, .{});
-
- app.config_errors_dialog = .{
- .builder = builder,
- .dialog = dialog,
- .error_message = error_message,
- };
-
- break :config_errors_dialog app.config_errors_dialog.?;
- };
-
- {
- var start = std.mem.zeroes(gtk.TextIter);
- config_errors_dialog.error_message.getStartIter(&start);
-
- var end = std.mem.zeroes(gtk.TextIter);
- config_errors_dialog.error_message.getEndIter(&end);
-
- config_errors_dialog.error_message.delete(&start, &end);
- }
-
- var msg_buf: [4095:0]u8 = undefined;
- var fbs = std.io.fixedBufferStream(&msg_buf);
-
- for (app.config._diagnostics.items()) |diag| {
- fbs.reset();
- diag.write(fbs.writer()) catch |err| {
- log.warn(
- "error writing diagnostic to buffer err={}",
- .{err},
- );
- continue;
- };
-
- config_errors_dialog.error_message.insertAtCursor(&msg_buf, @intCast(fbs.pos));
- config_errors_dialog.error_message.insertAtCursor("\n", 1);
- }
-
- switch (DialogType) {
- adw.AlertDialog => {
- const parent = if (window) |w| w.window.as(gtk.Widget) else null;
- config_errors_dialog.dialog.as(adw.Dialog).present(parent);
- },
- adw.MessageDialog => config_errors_dialog.dialog.as(gtk.Window).present(),
- else => unreachable,
- }
-}
-
-fn onResponse(_: *DialogType, response: [*:0]const u8, app: *App) callconv(.c) void {
- if (app.config_errors_dialog) |config_errors_dialog| config_errors_dialog.builder.deinit();
- app.config_errors_dialog = null;
-
- if (std.mem.orderZ(u8, response, "reload") == .eq) {
- app.reloadConfig(.app, .{}) catch |err| {
- log.warn("error reloading config error={}", .{err});
- return;
- };
- }
-}
diff --git a/src/apprt/gtk/GlobalShortcuts.zig b/src/apprt/gtk/GlobalShortcuts.zig
deleted file mode 100644
index 2506bef97..000000000
--- a/src/apprt/gtk/GlobalShortcuts.zig
+++ /dev/null
@@ -1,422 +0,0 @@
-const GlobalShortcuts = @This();
-
-const std = @import("std");
-const Allocator = std.mem.Allocator;
-
-const gio = @import("gio");
-const glib = @import("glib");
-const gobject = @import("gobject");
-
-const App = @import("App.zig");
-const configpkg = @import("../../config.zig");
-const Binding = @import("../../input.zig").Binding;
-const key = @import("key.zig");
-
-const log = std.log.scoped(.global_shortcuts);
-const Token = [16]u8;
-
-app: *App,
-arena: std.heap.ArenaAllocator,
-dbus: *gio.DBusConnection,
-
-/// 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 fn init(alloc: Allocator, gio_app: *gio.Application) ?GlobalShortcuts {
- const dbus = gio_app.getDbusConnection() orelse return null;
-
- return .{
- // To be initialized later
- .app = undefined,
- .arena = .init(alloc),
- .dbus = dbus,
- };
-}
-
-pub fn deinit(self: *GlobalShortcuts) void {
- self.close();
- self.arena.deinit();
-}
-
-fn close(self: *GlobalShortcuts) void {
- if (self.response_subscription != 0) {
- self.dbus.signalUnsubscribe(self.response_subscription);
- self.response_subscription = 0;
- }
-
- if (self.activate_subscription != 0) {
- self.dbus.signalUnsubscribe(self.activate_subscription);
- self.activate_subscription = 0;
- }
-
- if (self.handle) |handle| {
- // Close existing session
- self.dbus.call(
- "org.freedesktop.portal.Desktop",
- handle,
- "org.freedesktop.portal.Session",
- "Close",
- null,
- null,
- .{},
- -1,
- null,
- null,
- null,
- );
- self.handle = null;
- }
-}
-
-pub fn refreshSession(self: *GlobalShortcuts, app: *App) !void {
- // Ensure we have a valid reference to the app
- // (it was left uninitialized in `init`)
- self.app = app;
-
- // Close any existing sessions
- self.close();
-
- // Update map
- var trigger_buf: [256]u8 = undefined;
-
- self.map.clearRetainingCapacity();
- var it = self.app.config.keybind.set.bindings.iterator();
-
- while (it.next()) |entry| {
- const leaf = switch (entry.value_ptr.*) {
- // Global shortcuts can't have leaders
- .leader => continue,
- .leaf => |leaf| leaf,
- };
- if (!leaf.flags.global) continue;
-
- const trigger = try key.xdgShortcutFromTrigger(
- &trigger_buf,
- entry.key_ptr.*,
- ) orelse continue;
-
- try self.map.put(
- self.arena.allocator(),
- try self.arena.allocator().dupeZ(u8, trigger),
- leaf.action,
- );
- }
-
- if (self.map.count() > 0) {
- try self.request(.create_session);
- }
-}
-
-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: *GlobalShortcuts = @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.map.get(std.mem.span(shortcut_id)) orelse return;
-
- self.app.core_app.performAllAction(self.app, action) catch |err| {
- log.err("failed to perform action={}", .{err});
- };
-}
-
-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 handle = shortcuts.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 = shortcuts.map.iterator();
- while (it.next()) |entry| {
- const trigger = entry.key_ptr.*.ptr;
- const action = std.fmt.bufPrintZ(
- &action_buf,
- "{}",
- .{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.err(
- "session handle not found in response={s}",
- .{vardict.print(@intFromBool(true))},
- );
- return;
- }
-
- shortcuts.handle = shortcuts.arena.allocator().dupeZ(u8, std.mem.span(handle.?)) catch {
- log.err("out of memory: failed to clone session handle", .{});
- return;
- };
-
- log.debug("session_handle={?s}", .{handle});
-
- // Subscribe to keybind activations
- shortcuts.activate_subscription = shortcuts.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.err("failed to bind shortcuts={}", .{err});
- return;
- };
- },
- .bind_shortcuts => {},
- }
- }
-};
-
-/// Submit a request to the global shortcuts portal.
-fn request(
- self: *GlobalShortcuts,
- comptime method: Method,
-) !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.err("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_: *GlobalShortcuts = @ptrCast(@alignCast(ud));
-
- // Unsubscribe from the response signal
- if (self_.response_subscription != 0) {
- dbus.signalUnsubscribe(self_.response_subscription);
- self_.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_, vardict.?);
- },
- 1 => log.debug("request was cancelled by user", .{}),
- 2 => log.warn("request ended unexpectedly", .{}),
- else => log.err("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);
-
- self.response_subscription = self.dbus.signalSubscribe(
- null,
- "org.freedesktop.portal.Request",
- "Response",
- request_path,
- null,
- .{},
- callbacks.responded,
- self,
- null,
- );
-
- self.dbus.call(
- "org.freedesktop.portal.Desktop",
- "/org/freedesktop/portal/desktop",
- "org.freedesktop.portal.GlobalShortcuts",
- method.name(),
- payload,
- null,
- .{},
- -1,
- null,
- callbacks.gotResponseHandle,
- null,
- );
-}
-
-/// 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;
-}
-
-/// 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.
-fn getRequestPath(self: *GlobalShortcuts, token: [:0]const u8) ![:0]const u8 {
- // 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(self.dbus.getUniqueName().?);
-
- const object_path = try std.mem.joinZ(self.arena.allocator(), "/", &.{
- "/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;
-}
diff --git a/src/apprt/gtk/ImguiWidget.zig b/src/apprt/gtk/ImguiWidget.zig
deleted file mode 100644
index 338fd7982..000000000
--- a/src/apprt/gtk/ImguiWidget.zig
+++ /dev/null
@@ -1,470 +0,0 @@
-const ImguiWidget = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-
-const gdk = @import("gdk");
-const gtk = @import("gtk");
-const cimgui = @import("cimgui");
-const gl = @import("opengl");
-
-const key = @import("key.zig");
-const input = @import("../../input.zig");
-
-const log = std.log.scoped(.gtk_imgui_widget);
-
-/// This is called every frame to populate the ImGui frame.
-render_callback: ?*const fn (?*anyopaque) void = null,
-render_userdata: ?*anyopaque = null,
-
-/// Our OpenGL widget
-gl_area: *gtk.GLArea,
-im_context: *gtk.IMContext,
-
-/// ImGui Context
-ig_ctx: *cimgui.c.ImGuiContext,
-
-/// Our previous instant used to calculate delta time for animations.
-instant: ?std.time.Instant = null,
-
-/// Initialize the widget. This must have a stable pointer for events.
-pub fn init(self: *ImguiWidget) !void {
- // Each widget gets its own imgui context so we can have multiple
- // imgui views in the same application.
- const ig_ctx = cimgui.c.igCreateContext(null) orelse return error.OutOfMemory;
- errdefer cimgui.c.igDestroyContext(ig_ctx);
- cimgui.c.igSetCurrentContext(ig_ctx);
- const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
- io.BackendPlatformName = "ghostty_gtk";
-
- // Our OpenGL area for drawing
- const gl_area = gtk.GLArea.new();
- gl_area.setAutoRender(@intFromBool(true));
-
- // The GL area has to be focusable so that it can receive events
- gl_area.as(gtk.Widget).setFocusable(@intFromBool(true));
- gl_area.as(gtk.Widget).setFocusOnClick(@intFromBool(true));
-
- // Clicks
- const gesture_click = gtk.GestureClick.new();
- errdefer gesture_click.unref();
- gesture_click.as(gtk.GestureSingle).setButton(0);
- gl_area.as(gtk.Widget).addController(gesture_click.as(gtk.EventController));
-
- // Mouse movement
- const ec_motion = gtk.EventControllerMotion.new();
- errdefer ec_motion.unref();
- gl_area.as(gtk.Widget).addController(ec_motion.as(gtk.EventController));
-
- // Scroll events
- const ec_scroll = gtk.EventControllerScroll.new(.flags_both_axes);
- errdefer ec_scroll.unref();
- gl_area.as(gtk.Widget).addController(ec_scroll.as(gtk.EventController));
-
- // Focus controller will tell us about focus enter/exit events
- const ec_focus = gtk.EventControllerFocus.new();
- errdefer ec_focus.unref();
- gl_area.as(gtk.Widget).addController(ec_focus.as(gtk.EventController));
-
- // Key event controller will tell us about raw keypress events.
- const ec_key = gtk.EventControllerKey.new();
- errdefer ec_key.unref();
- gl_area.as(gtk.Widget).addController(ec_key.as(gtk.EventController));
- errdefer gl_area.as(gtk.Widget).removeController(ec_key.as(gtk.EventController));
-
- // The input method context that we use to translate key events into
- // characters. This doesn't have an event key controller attached because
- // we call it manually from our own key controller.
- const im_context = gtk.IMMulticontext.new();
- errdefer im_context.unref();
-
- // Signals
- _ = gtk.Widget.signals.realize.connect(
- gl_area,
- *ImguiWidget,
- gtkRealize,
- self,
- .{},
- );
- _ = gtk.Widget.signals.unrealize.connect(
- gl_area,
- *ImguiWidget,
- gtkUnrealize,
- self,
- .{},
- );
- _ = gtk.Widget.signals.destroy.connect(
- gl_area,
- *ImguiWidget,
- gtkDestroy,
- self,
- .{},
- );
- _ = gtk.GLArea.signals.render.connect(
- gl_area,
- *ImguiWidget,
- gtkRender,
- self,
- .{},
- );
- _ = gtk.GLArea.signals.resize.connect(
- gl_area,
- *ImguiWidget,
- gtkResize,
- self,
- .{},
- );
- _ = gtk.EventControllerKey.signals.key_pressed.connect(
- ec_key,
- *ImguiWidget,
- gtkKeyPressed,
- self,
- .{},
- );
- _ = gtk.EventControllerKey.signals.key_released.connect(
- ec_key,
- *ImguiWidget,
- gtkKeyReleased,
- self,
- .{},
- );
- _ = gtk.EventControllerFocus.signals.enter.connect(
- ec_focus,
- *ImguiWidget,
- gtkFocusEnter,
- self,
- .{},
- );
- _ = gtk.EventControllerFocus.signals.leave.connect(
- ec_focus,
- *ImguiWidget,
- gtkFocusLeave,
- self,
- .{},
- );
- _ = gtk.GestureClick.signals.pressed.connect(
- gesture_click,
- *ImguiWidget,
- gtkMouseDown,
- self,
- .{},
- );
- _ = gtk.GestureClick.signals.released.connect(
- gesture_click,
- *ImguiWidget,
- gtkMouseUp,
- self,
- .{},
- );
- _ = gtk.EventControllerMotion.signals.motion.connect(
- ec_motion,
- *ImguiWidget,
- gtkMouseMotion,
- self,
- .{},
- );
- _ = gtk.EventControllerScroll.signals.scroll.connect(
- ec_scroll,
- *ImguiWidget,
- gtkMouseScroll,
- self,
- .{},
- );
- _ = gtk.IMContext.signals.commit.connect(
- im_context,
- *ImguiWidget,
- gtkInputCommit,
- self,
- .{},
- );
-
- self.* = .{
- .gl_area = gl_area,
- .im_context = im_context.as(gtk.IMContext),
- .ig_ctx = ig_ctx,
- };
-}
-
-/// Deinitialize the widget. This should ONLY be called if the widget gl_area
-/// was never added to a parent. Otherwise, cleanup automatically happens
-/// when the widget is destroyed and this should NOT be called.
-pub fn deinit(self: *ImguiWidget) void {
- cimgui.c.igDestroyContext(self.ig_ctx);
-}
-
-/// This should be called anytime the underlying data for the UI changes
-/// so that the UI can be refreshed.
-pub fn queueRender(self: *const ImguiWidget) void {
- self.gl_area.queueRender();
-}
-
-/// Initialize the frame. Expects that the context is already current.
-fn newFrame(self: *ImguiWidget) !void {
- const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
-
- // Determine our delta time
- const now = try std.time.Instant.now();
- io.DeltaTime = if (self.instant) |prev| delta: {
- const since_ns = now.since(prev);
- const since_s: f32 = @floatFromInt(since_ns / std.time.ns_per_s);
- break :delta @max(0.00001, since_s);
- } else (1 / 60);
- self.instant = now;
-}
-
-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,
- };
-}
-
-fn gtkDestroy(_: *gtk.GLArea, self: *ImguiWidget) callconv(.c) void {
- log.debug("imgui widget destroy", .{});
- self.deinit();
-}
-
-fn gtkRealize(area: *gtk.GLArea, self: *ImguiWidget) callconv(.c) void {
- log.debug("gl surface realized", .{});
-
- // We need to make the context current so we can call GL functions.
- area.makeCurrent();
- if (area.getError()) |err| {
- log.err("surface failed to realize: {s}", .{err.f_message orelse "(unknown)"});
- return;
- }
-
- // realize means that our OpenGL context is ready, so we can now
- // initialize the ImgUI OpenGL backend for our context.
- cimgui.c.igSetCurrentContext(self.ig_ctx);
- _ = cimgui.ImGui_ImplOpenGL3_Init(null);
-}
-
-fn gtkUnrealize(area: *gtk.GLArea, self: *ImguiWidget) callconv(.c) void {
- _ = area;
- log.debug("gl surface unrealized", .{});
-
- cimgui.c.igSetCurrentContext(self.ig_ctx);
- cimgui.ImGui_ImplOpenGL3_Shutdown();
-}
-
-fn gtkResize(area: *gtk.GLArea, width: c_int, height: c_int, self: *ImguiWidget) callconv(.c) void {
- cimgui.c.igSetCurrentContext(self.ig_ctx);
- const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
- const scale_factor = area.as(gtk.Widget).getScaleFactor();
- log.debug("gl resize width={} height={} scale={}", .{
- width,
- height,
- scale_factor,
- });
-
- // 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.
- const style = cimgui.c.ImGuiStyle_ImGuiStyle();
- defer cimgui.c.ImGuiStyle_destroy(style);
- cimgui.c.ImGuiStyle_ScaleAllSizes(style, @floatFromInt(scale_factor));
- const active_style = cimgui.c.igGetStyle();
- active_style.* = style.*;
-}
-
-fn gtkRender(_: *gtk.GLArea, _: *gdk.GLContext, self: *ImguiWidget) callconv(.c) c_int {
- cimgui.c.igSetCurrentContext(self.ig_ctx);
-
- // 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() catch |err| {
- log.err("failed to setup frame: {}", .{err});
- return 0;
- };
- cimgui.c.igNewFrame();
-
- // Build our UI
- if (self.render_callback) |cb| cb(self.render_userdata);
-
- // Render
- cimgui.c.igRender();
- }
-
- // 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.igGetDrawData());
-
- return 1;
-}
-
-fn gtkMouseMotion(
- _: *gtk.EventControllerMotion,
- x: f64,
- y: f64,
- self: *ImguiWidget,
-) callconv(.c) void {
- cimgui.c.igSetCurrentContext(self.ig_ctx);
- const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
- const scale_factor: f64 = @floatFromInt(self.gl_area.as(gtk.Widget).getScaleFactor());
- cimgui.c.ImGuiIO_AddMousePosEvent(
- io,
- @floatCast(x * scale_factor),
- @floatCast(y * scale_factor),
- );
- self.queueRender();
-}
-
-fn gtkMouseDown(
- gesture: *gtk.GestureClick,
- _: c_int,
- _: f64,
- _: f64,
- self: *ImguiWidget,
-) callconv(.c) void {
- self.queueRender();
-
- cimgui.c.igSetCurrentContext(self.ig_ctx);
- const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
-
- const gdk_button = gesture.as(gtk.GestureSingle).getCurrentButton();
- if (translateMouseButton(gdk_button)) |button| {
- cimgui.c.ImGuiIO_AddMouseButtonEvent(io, button, true);
- }
-}
-
-fn gtkMouseUp(
- gesture: *gtk.GestureClick,
- _: c_int,
- _: f64,
- _: f64,
- self: *ImguiWidget,
-) callconv(.c) void {
- self.queueRender();
-
- cimgui.c.igSetCurrentContext(self.ig_ctx);
- const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
- const gdk_button = gesture.as(gtk.GestureSingle).getCurrentButton();
- if (translateMouseButton(gdk_button)) |button| {
- cimgui.c.ImGuiIO_AddMouseButtonEvent(io, button, false);
- }
-}
-
-fn gtkMouseScroll(
- _: *gtk.EventControllerScroll,
- x: f64,
- y: f64,
- self: *ImguiWidget,
-) callconv(.c) c_int {
- self.queueRender();
-
- cimgui.c.igSetCurrentContext(self.ig_ctx);
- const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
- cimgui.c.ImGuiIO_AddMouseWheelEvent(
- io,
- @floatCast(x),
- @floatCast(-y),
- );
-
- return @intFromBool(true);
-}
-
-fn gtkFocusEnter(_: *gtk.EventControllerFocus, self: *ImguiWidget) callconv(.c) void {
- self.queueRender();
-
- cimgui.c.igSetCurrentContext(self.ig_ctx);
- const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
- cimgui.c.ImGuiIO_AddFocusEvent(io, true);
-}
-
-fn gtkFocusLeave(_: *gtk.EventControllerFocus, self: *ImguiWidget) callconv(.c) void {
- self.queueRender();
-
- cimgui.c.igSetCurrentContext(self.ig_ctx);
- const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
- cimgui.c.ImGuiIO_AddFocusEvent(io, false);
-}
-
-fn gtkInputCommit(
- _: *gtk.IMMulticontext,
- bytes: [*:0]u8,
- self: *ImguiWidget,
-) callconv(.c) void {
- self.queueRender();
-
- cimgui.c.igSetCurrentContext(self.ig_ctx);
- const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
- cimgui.c.ImGuiIO_AddInputCharactersUTF8(io, bytes);
-}
-
-fn gtkKeyPressed(
- 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 gtkKeyReleased(
- 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 keyEvent(
- self: *ImguiWidget,
- action: input.Action,
- ec_key: *gtk.EventControllerKey,
- keyval: c_uint,
- keycode: c_uint,
- gtk_mods: gdk.ModifierType,
-) bool {
- _ = keycode;
-
- self.queueRender();
-
- cimgui.c.igSetCurrentContext(self.ig_ctx);
- const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
-
- 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| {
- _ = self.im_context.filterKeypress(event);
- }
-
- return true;
-}
diff --git a/src/apprt/gtk/ProgressBar.zig b/src/apprt/gtk/ProgressBar.zig
deleted file mode 100644
index 1518e84c2..000000000
--- a/src/apprt/gtk/ProgressBar.zig
+++ /dev/null
@@ -1,165 +0,0 @@
-//! Structure for managing GUI progress bar for a surface.
-const ProgressBar = @This();
-
-const std = @import("std");
-
-const glib = @import("glib");
-const gtk = @import("gtk");
-
-const Surface = @import("./Surface.zig");
-const terminal = @import("../../terminal/main.zig");
-
-const log = std.log.scoped(.gtk_progress_bar);
-
-/// The surface that we belong to.
-surface: *Surface,
-
-/// Widget for showing progress bar.
-progress_bar: ?*gtk.ProgressBar = null,
-
-/// Timer used to remove the progress bar if we have not received an update from
-/// the TUI in a while.
-progress_bar_timer: ?c_uint = null,
-
-pub fn init(surface: *Surface) ProgressBar {
- return .{
- .surface = surface,
- };
-}
-
-pub fn deinit(self: *ProgressBar) void {
- self.stopProgressBarTimer();
-}
-
-/// Show (or update if it already exists) a GUI progress bar.
-pub fn handleProgressReport(self: *ProgressBar, value: terminal.osc.Command.ProgressReport) error{}!bool {
- // Remove the progress bar.
- if (value.state == .remove) {
- self.stopProgressBarTimer();
- self.removeProgressBar();
-
- return true;
- }
-
- const progress_bar = self.addProgressBar();
- self.startProgressBarTimer();
-
- switch (value.state) {
- // already handled above
- .remove => unreachable,
-
- // 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));
- }
- },
- }
-
- return true;
-}
-
-/// 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));
-}
-
-/// Add a progress bar to our overlay.
-fn addProgressBar(self: *ProgressBar) *gtk.ProgressBar {
- if (self.progress_bar) |progress_bar| return progress_bar;
-
- const progress_bar = gtk.ProgressBar.new();
- self.progress_bar = progress_bar;
-
- const progress_bar_widget = progress_bar.as(gtk.Widget);
- progress_bar_widget.setHalign(.fill);
- progress_bar_widget.setValign(.start);
- progress_bar_widget.addCssClass("osd");
-
- self.surface.overlay.addOverlay(progress_bar_widget);
-
- return progress_bar;
-}
-
-/// Remove the progress bar from our overlay.
-fn removeProgressBar(self: *ProgressBar) void {
- if (self.progress_bar) |progress_bar| {
- const progress_bar_widget = progress_bar.as(gtk.Widget);
- self.surface.overlay.removeOverlay(progress_bar_widget);
- self.progress_bar = null;
- }
-}
-
-/// Start a timer that will remove the progress bar if the TUI forgets to remove
-/// it.
-fn startProgressBarTimer(self: *ProgressBar) void {
- const progress_bar_timeout_seconds = 15;
-
- // Remove an old timer that hasn't fired yet.
- self.stopProgressBarTimer();
-
- self.progress_bar_timer = glib.timeoutAdd(
- progress_bar_timeout_seconds * std.time.ms_per_s,
- handleProgressBarTimeout,
- self,
- );
-}
-
-/// Stop any existing timer for removing the progress bar.
-fn stopProgressBarTimer(self: *ProgressBar) void {
- if (self.progress_bar_timer) |timer| {
- if (glib.Source.remove(timer) == 0) {
- log.warn("unable to remove progress bar timer", .{});
- }
- self.progress_bar_timer = null;
- }
-}
-
-/// The progress bar hasn't been updated by the TUI recently, remove it.
-fn handleProgressBarTimeout(ud: ?*anyopaque) callconv(.c) c_int {
- const self: *ProgressBar = @ptrCast(@alignCast(ud.?));
-
- self.progress_bar_timer = null;
- self.removeProgressBar();
-
- return @intFromBool(glib.SOURCE_REMOVE);
-}
diff --git a/src/apprt/gtk/ResizeOverlay.zig b/src/apprt/gtk/ResizeOverlay.zig
deleted file mode 100644
index 2ab59624a..000000000
--- a/src/apprt/gtk/ResizeOverlay.zig
+++ /dev/null
@@ -1,206 +0,0 @@
-const ResizeOverlay = @This();
-
-const std = @import("std");
-
-const glib = @import("glib");
-const gtk = @import("gtk");
-
-const configpkg = @import("../../config.zig");
-const Surface = @import("Surface.zig");
-
-const log = std.log.scoped(.gtk);
-
-/// local copy of configuration data
-const DerivedConfig = struct {
- resize_overlay: configpkg.Config.ResizeOverlay,
- resize_overlay_position: configpkg.Config.ResizeOverlayPosition,
- resize_overlay_duration: configpkg.Config.Duration,
-
- pub fn init(config: *const configpkg.Config) DerivedConfig {
- return .{
- .resize_overlay = config.@"resize-overlay",
- .resize_overlay_position = config.@"resize-overlay-position",
- .resize_overlay_duration = config.@"resize-overlay-duration",
- };
- }
-};
-
-/// the surface that we are attached to
-surface: *Surface,
-
-/// a copy of the configuration that we need to operate
-config: DerivedConfig,
-
-/// If non-null this is the widget on the overlay that shows the size of the
-/// surface when it is resized.
-label: ?*gtk.Label = null,
-
-/// If non-null this is a timer for dismissing the resize overlay.
-timer: ?c_uint = null,
-
-/// If non-null this is a timer for dismissing the resize overlay.
-idler: ?c_uint = null,
-
-/// If true, the next resize event will be the first one.
-first: bool = true,
-
-/// Initialize the ResizeOverlay. This doesn't do anything more than save a
-/// pointer to the surface that we are a part of as all of the widget creation
-/// is done later.
-pub fn init(self: *ResizeOverlay, surface: *Surface, config: *const configpkg.Config) void {
- self.* = .{
- .surface = surface,
- .config = .init(config),
- };
-}
-
-pub fn updateConfig(self: *ResizeOverlay, config: *const configpkg.Config) void {
- self.config = .init(config);
-}
-
-/// De-initialize the ResizeOverlay. This removes any pending idlers/timers that
-/// may not have fired yet.
-pub fn deinit(self: *ResizeOverlay) void {
- if (self.idler) |idler| {
- if (glib.Source.remove(idler) == 0) {
- log.warn("unable to remove resize overlay idler", .{});
- }
- self.idler = null;
- }
-
- if (self.timer) |timer| {
- if (glib.Source.remove(timer) == 0) {
- log.warn("unable to remove resize overlay timer", .{});
- }
- self.timer = null;
- }
-}
-
-/// If we're configured to do so, update the text in the resize overlay widget
-/// and make it visible. Schedule a timer to hide the widget after the delay
-/// expires.
-///
-/// If we're not configured to show the overlay, do nothing.
-pub fn maybeShow(self: *ResizeOverlay) void {
- switch (self.config.resize_overlay) {
- .never => return,
- .always => {},
- .@"after-first" => if (self.first) {
- self.first = false;
- return;
- },
- }
-
- self.first = false;
-
- // 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 (self.idler != null) return;
- self.idler = glib.idleAdd(gtkUpdate, self);
-}
-
-/// Actually update the overlay widget. This should only be called from a GTK
-/// idle handler.
-fn gtkUpdate(ud: ?*anyopaque) callconv(.c) c_int {
- const self: *ResizeOverlay = @ptrCast(@alignCast(ud orelse return 0));
-
- // No matter what our idler is complete with this callback
- self.idler = null;
-
- const grid_size = self.surface.core_surface.size.grid();
- var buf: [32]u8 = undefined;
- const text = std.fmt.bufPrintZ(
- &buf,
- "{d} x {d}",
- .{
- grid_size.columns,
- grid_size.rows,
- },
- ) catch |err| {
- log.err("unable to format text: {}", .{err});
- return 0;
- };
-
- if (self.label) |label| {
- // The resize overlay widget already exists, just update it.
- label.setText(text.ptr);
- setPosition(label, &self.config);
- show(label);
- } else {
- // Create the resize overlay widget.
- const label = gtk.Label.new(text.ptr);
- label.setJustify(gtk.Justification.center);
- label.setSelectable(0);
- setPosition(label, &self.config);
-
- const widget = label.as(gtk.Widget);
- widget.addCssClass("view");
- widget.addCssClass("size-overlay");
- widget.setFocusable(0);
- widget.setCanTarget(0);
-
- const overlay: *gtk.Overlay = @ptrCast(@alignCast(self.surface.overlay));
- overlay.addOverlay(widget);
-
- self.label = label;
- }
-
- if (self.timer) |timer| {
- if (glib.Source.remove(timer) == 0) {
- log.warn("unable to remove size overlay timer", .{});
- }
- }
-
- self.timer = glib.timeoutAdd(
- self.surface.app.config.@"resize-overlay-duration".asMilliseconds(),
- gtkTimerExpired,
- self,
- );
-
- return 0;
-}
-
-// This should only be called from a GTK idle handler or timer.
-fn show(label: *gtk.Label) void {
- const widget = label.as(gtk.Widget);
- widget.removeCssClass("hidden");
-}
-
-// This should only be called from a GTK idle handler or timer.
-fn hide(label: *gtk.Label) void {
- const widget = label.as(gtk.Widget);
- widget.addCssClass("hidden");
-}
-
-/// Update the position of the resize overlay widget. It might seem excessive to
-/// do this often, but it should make hot config reloading of the position work.
-/// This should only be called from a GTK idle handler.
-fn setPosition(label: *gtk.Label, config: *DerivedConfig) void {
- const widget = label.as(gtk.Widget);
- widget.setHalign(
- switch (config.resize_overlay_position) {
- .center, .@"top-center", .@"bottom-center" => gtk.Align.center,
- .@"top-left", .@"bottom-left" => gtk.Align.start,
- .@"top-right", .@"bottom-right" => gtk.Align.end,
- },
- );
- widget.setValign(
- switch (config.resize_overlay_position) {
- .center => gtk.Align.center,
- .@"top-left", .@"top-center", .@"top-right" => gtk.Align.start,
- .@"bottom-left", .@"bottom-center", .@"bottom-right" => gtk.Align.end,
- },
- );
-}
-
-/// If this fires, it means that the delay period has expired and the resize
-/// overlay widget should be hidden.
-fn gtkTimerExpired(ud: ?*anyopaque) callconv(.c) c_int {
- const self: *ResizeOverlay = @ptrCast(@alignCast(ud orelse return 0));
- self.timer = null;
- if (self.label) |label| hide(label);
- return 0;
-}
diff --git a/src/apprt/gtk/Split.zig b/src/apprt/gtk/Split.zig
deleted file mode 100644
index fb719c3c9..000000000
--- a/src/apprt/gtk/Split.zig
+++ /dev/null
@@ -1,441 +0,0 @@
-/// Split represents a surface split where two surfaces are shown side-by-side
-/// within the same window either vertically or horizontally.
-const Split = @This();
-
-const std = @import("std");
-const Allocator = std.mem.Allocator;
-const assert = std.debug.assert;
-
-const gobject = @import("gobject");
-const gtk = @import("gtk");
-
-const apprt = @import("../../apprt.zig");
-const font = @import("../../font/main.zig");
-const CoreSurface = @import("../../Surface.zig");
-
-const Surface = @import("Surface.zig");
-const Tab = @import("Tab.zig");
-
-const log = std.log.scoped(.gtk);
-
-/// The split orientation.
-pub const Orientation = enum {
- horizontal,
- vertical,
-
- pub fn fromDirection(direction: apprt.action.SplitDirection) Orientation {
- return switch (direction) {
- .right, .left => .horizontal,
- .down, .up => .vertical,
- };
- }
-
- pub fn fromResizeDirection(direction: apprt.action.ResizeSplit.Direction) Orientation {
- return switch (direction) {
- .up, .down => .vertical,
- .left, .right => .horizontal,
- };
- }
-};
-
-/// Our actual GtkPaned widget
-paned: *gtk.Paned,
-
-/// The container for this split panel.
-container: Surface.Container,
-
-/// The orientation of this split panel.
-orientation: Orientation,
-
-/// The elements of this split panel.
-top_left: Surface.Container.Elem,
-bottom_right: Surface.Container.Elem,
-
-/// Create a new split panel with the given sibling surface in the given
-/// direction. The direction is where the new surface will be initialized.
-///
-/// The sibling surface can be in a split already or it can be within a
-/// tab. This properly handles updating the surface container so that
-/// it represents the new split.
-pub fn create(
- alloc: Allocator,
- sibling: *Surface,
- direction: apprt.action.SplitDirection,
-) !*Split {
- var split = try alloc.create(Split);
- errdefer alloc.destroy(split);
- try split.init(sibling, direction);
- return split;
-}
-
-pub fn init(
- self: *Split,
- sibling: *Surface,
- direction: apprt.action.SplitDirection,
-) !void {
- // If our sibling is too small to be split in half then we don't
- // allow the split to happen. This avoids a situation where the
- // split becomes too small.
- //
- // This is kind of a hack. Ideally we'd use gtk_widget_set_size_request
- // properly along the path to ensure minimum sizes. I don't know if
- // GTK even respects that all but any way GTK does this for us seems
- // better than this.
- {
- // This is the min size of the sibling split. This means the
- // smallest split is half of this.
- const multiplier = 4;
-
- const size = &sibling.core_surface.size;
- const small = switch (direction) {
- .right, .left => size.screen.width < size.cell.width * multiplier,
- .down, .up => size.screen.height < size.cell.height * multiplier,
- };
- if (small) return error.SplitTooSmall;
- }
-
- // Create the new child surface for the other direction.
- const alloc = sibling.app.core_app.alloc;
- var surface = try Surface.create(alloc, sibling.app, .{
- .parent = &sibling.core_surface,
- });
- errdefer surface.destroy(alloc);
- sibling.dimSurface();
- sibling.setSplitZoom(false);
-
- // Create the actual GTKPaned, attach the proper children.
- const orientation: gtk.Orientation = switch (direction) {
- .right, .left => .horizontal,
- .down, .up => .vertical,
- };
- const paned = gtk.Paned.new(orientation);
- errdefer paned.unref();
-
- // Keep a long-lived reference, which we unref in destroy.
- paned.ref();
-
- // Update all of our containers to point to the right place.
- // The split has to point to where the sibling pointed to because
- // we're inheriting its parent. The sibling points to its location
- // in the split, and the surface points to the other location.
- const container = sibling.container;
- const tl: *Surface, const br: *Surface = switch (direction) {
- .right, .down => right_down: {
- sibling.container = .{ .split_tl = &self.top_left };
- surface.container = .{ .split_br = &self.bottom_right };
- break :right_down .{ sibling, surface };
- },
-
- .left, .up => left_up: {
- sibling.container = .{ .split_br = &self.bottom_right };
- surface.container = .{ .split_tl = &self.top_left };
- break :left_up .{ surface, sibling };
- },
- };
-
- self.* = .{
- .paned = paned,
- .container = container,
- .top_left = .{ .surface = tl },
- .bottom_right = .{ .surface = br },
- .orientation = .fromDirection(direction),
- };
-
- // Replace the previous containers element with our split. This allows a
- // non-split to become a split, a split to become a nested split, etc.
- container.replace(.{ .split = self });
-
- // Update our children so that our GL area is properly added to the paned.
- self.updateChildren();
-
- // The new surface should always grab focus
- surface.grabFocus();
-}
-
-pub fn destroy(self: *Split, alloc: Allocator) void {
- self.top_left.deinit(alloc);
- self.bottom_right.deinit(alloc);
-
- // Clean up our GTK reference. This will trigger all the destroy callbacks
- // that are necessary for the surfaces to clean up.
- self.paned.unref();
-
- alloc.destroy(self);
-}
-
-/// Remove the top left child.
-pub fn removeTopLeft(self: *Split) void {
- self.removeChild(self.top_left, self.bottom_right);
-}
-
-/// Remove the top left child.
-pub fn removeBottomRight(self: *Split) void {
- self.removeChild(self.bottom_right, self.top_left);
-}
-
-fn removeChild(
- self: *Split,
- remove: Surface.Container.Elem,
- keep: Surface.Container.Elem,
-) void {
- const window = self.container.window() orelse return;
- const alloc = window.app.core_app.alloc;
-
- // Remove our children since we are going to no longer be a split anyways.
- // This prevents widgets with multiple parents.
- self.removeChildren();
-
- // Our container must become whatever our top left is
- self.container.replace(keep);
-
- // Grab focus of the left-over side
- keep.grabFocus();
-
- // When a child is removed we are no longer a split, so destroy ourself
- remove.deinit(alloc);
- alloc.destroy(self);
-}
-
-/// Move the divider in the given direction by the given amount.
-pub fn moveDivider(
- self: *Split,
- direction: apprt.action.ResizeSplit.Direction,
- amount: u16,
-) void {
- const min_pos = 10;
-
- const pos = self.paned.getPosition();
- const new = switch (direction) {
- .up, .left => @max(pos - amount, min_pos),
- .down, .right => new_pos: {
- const max_pos: u16 = @as(u16, @intFromFloat(self.maxPosition())) - min_pos;
- break :new_pos @min(pos + amount, max_pos);
- },
- };
-
- self.paned.setPosition(new);
-}
-
-/// Equalize the splits in this split panel. Each split is equalized based on
-/// its weight, i.e. the number of Surfaces it contains.
-///
-/// It works recursively by equalizing the children of each split.
-///
-/// It returns this split's weight.
-pub fn equalize(self: *Split) f64 {
- // Calculate weights of top_left/bottom_right
- const top_left_weight = self.top_left.equalize();
- const bottom_right_weight = self.bottom_right.equalize();
- const weight = top_left_weight + bottom_right_weight;
-
- // Ratio of top_left weight to overall weight, which gives the split ratio
- const ratio = top_left_weight / weight;
-
- // Convert split ratio into new position for divider
- self.paned.setPosition(@intFromFloat(self.maxPosition() * ratio));
-
- return weight;
-}
-
-// maxPosition returns the maximum position of the GtkPaned, which is the
-// "max-position" attribute.
-fn maxPosition(self: *Split) f64 {
- var value: gobject.Value = std.mem.zeroes(gobject.Value);
- defer value.unset();
-
- _ = value.init(gobject.ext.types.int);
- self.paned.as(gobject.Object).getProperty(
- "max-position",
- &value,
- );
-
- return @floatFromInt(value.getInt());
-}
-
-// This replaces the element at the given pointer with a new element.
-// The ptr must be either top_left or bottom_right (asserted in debug).
-// The memory of the old element must be freed or otherwise handled by
-// the caller.
-pub fn replace(
- self: *Split,
- ptr: *Surface.Container.Elem,
- new: Surface.Container.Elem,
-) void {
- // We can write our element directly. There's nothing special.
- assert(&self.top_left == ptr or &self.bottom_right == ptr);
- ptr.* = new;
-
- // Update our paned children. This will reset the divider
- // position but we want to keep it in place so save and restore it.
- const pos = self.paned.getPosition();
- defer self.paned.setPosition(pos);
- self.updateChildren();
-}
-
-// grabFocus grabs the focus of the top-left element.
-pub fn grabFocus(self: *Split) void {
- self.top_left.grabFocus();
-}
-
-/// Update the paned children to represent the current state.
-/// This should be called anytime the top/left or bottom/right
-/// element is changed.
-pub fn updateChildren(self: *const Split) void {
- // We have to set both to null. If we overwrite the pane with
- // the same value, then GTK bugs out (the GL area unrealizes
- // and never rerealizes).
- self.removeChildren();
-
- // Set our current children
- self.paned.setStartChild(self.top_left.widget());
- self.paned.setEndChild(self.bottom_right.widget());
-}
-
-/// A mapping of direction to the element (if any) in that direction.
-pub const DirectionMap = std.EnumMap(
- apprt.action.GotoSplit,
- ?*Surface,
-);
-
-pub const Side = enum { top_left, bottom_right };
-
-/// Returns the map that can be used to determine elements in various
-/// directions (primarily for gotoSplit).
-pub fn directionMap(self: *const Split, from: Side) DirectionMap {
- var result = DirectionMap.initFull(null);
-
- if (self.directionPrevious(from)) |prev| {
- result.put(.previous, prev.surface);
- if (!prev.wrapped) {
- result.put(.up, prev.surface);
- }
- }
-
- if (self.directionNext(from)) |next| {
- result.put(.next, next.surface);
- if (!next.wrapped) {
- result.put(.down, next.surface);
- }
- }
-
- if (self.directionLeft(from)) |left| {
- result.put(.left, left);
- }
-
- if (self.directionRight(from)) |right| {
- result.put(.right, right);
- }
-
- return result;
-}
-
-fn directionLeft(self: *const Split, from: Side) ?*Surface {
- switch (from) {
- .bottom_right => {
- switch (self.orientation) {
- .horizontal => return self.top_left.deepestSurface(.bottom_right),
- .vertical => return directionLeft(
- self.container.split() orelse return null,
- .bottom_right,
- ),
- }
- },
- .top_left => return directionLeft(
- self.container.split() orelse return null,
- .bottom_right,
- ),
- }
-}
-
-fn directionRight(self: *const Split, from: Side) ?*Surface {
- switch (from) {
- .top_left => {
- switch (self.orientation) {
- .horizontal => return self.bottom_right.deepestSurface(.top_left),
- .vertical => return directionRight(
- self.container.split() orelse return null,
- .top_left,
- ),
- }
- },
- .bottom_right => return directionRight(
- self.container.split() orelse return null,
- .top_left,
- ),
- }
-}
-
-fn directionPrevious(self: *const Split, from: Side) ?struct {
- surface: *Surface,
- wrapped: bool,
-} {
- switch (from) {
- // From the bottom right, our previous is the deepest surface
- // in the top-left of our own split.
- .bottom_right => return .{
- .surface = self.top_left.deepestSurface(.bottom_right) orelse return null,
- .wrapped = false,
- },
-
- // From the top left its more complicated. It is the de
- .top_left => {
- // If we have no parent split then there can be no unwrapped prev.
- // We can still have a wrapped previous.
- const parent = self.container.split() orelse return .{
- .surface = self.bottom_right.deepestSurface(.bottom_right) orelse return null,
- .wrapped = true,
- };
-
- // The previous value is the previous of the side that we are.
- const side = self.container.splitSide() orelse return null;
- return switch (side) {
- .top_left => parent.directionPrevious(.top_left),
- .bottom_right => parent.directionPrevious(.bottom_right),
- };
- },
- }
-}
-
-fn directionNext(self: *const Split, from: Side) ?struct {
- surface: *Surface,
- wrapped: bool,
-} {
- switch (from) {
- // From the top left, our next is the earliest surface in the
- // top-left direction of the bottom-right side of our split. Fun!
- .top_left => return .{
- .surface = self.bottom_right.deepestSurface(.top_left) orelse return null,
- .wrapped = false,
- },
-
- // From the bottom right is more compliated. It is the deepest
- // (last) surface in the
- .bottom_right => {
- // If we have no parent split then there can be no next.
- const parent = self.container.split() orelse return .{
- .surface = self.top_left.deepestSurface(.top_left) orelse return null,
- .wrapped = true,
- };
-
- // The previous value is the previous of the side that we are.
- const side = self.container.splitSide() orelse return null;
- return switch (side) {
- .top_left => parent.directionNext(.top_left),
- .bottom_right => parent.directionNext(.bottom_right),
- };
- },
- }
-}
-
-pub fn detachTopLeft(self: *const Split) void {
- self.paned.setStartChild(null);
-}
-
-pub fn detachBottomRight(self: *const Split) void {
- self.paned.setEndChild(null);
-}
-
-fn removeChildren(self: *const Split) void {
- self.detachTopLeft();
- self.detachBottomRight();
-}
diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig
deleted file mode 100644
index 231ab0c09..000000000
--- a/src/apprt/gtk/Surface.zig
+++ /dev/null
@@ -1,2561 +0,0 @@
-/// A surface represents one drawable terminal surface. The surface may be
-/// attached to a window or it may be some other kind of surface. This struct
-/// is meant to be generic to all scenarios.
-const Surface = @This();
-
-const std = @import("std");
-
-const adw = @import("adw");
-const gtk = @import("gtk");
-const gdk = @import("gdk");
-const glib = @import("glib");
-const gio = @import("gio");
-const gobject = @import("gobject");
-
-const Allocator = std.mem.Allocator;
-const build_config = @import("../../build_config.zig");
-const build_options = @import("build_options");
-const configpkg = @import("../../config.zig");
-const apprt = @import("../../apprt.zig");
-const font = @import("../../font/main.zig");
-const i18n = @import("../../os/main.zig").i18n;
-const input = @import("../../input.zig");
-const renderer = @import("../../renderer.zig");
-const terminal = @import("../../terminal/main.zig");
-const CoreSurface = @import("../../Surface.zig");
-const internal_os = @import("../../os/main.zig");
-
-const App = @import("App.zig");
-const Split = @import("Split.zig");
-const Tab = @import("Tab.zig");
-const Window = @import("Window.zig");
-const Menu = @import("menu.zig").Menu;
-const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig");
-const ResizeOverlay = @import("ResizeOverlay.zig");
-const URLWidget = @import("URLWidget.zig");
-const CloseDialog = @import("CloseDialog.zig");
-const inspectorpkg = @import("inspector.zig");
-const gtk_key = @import("key.zig");
-const Builder = @import("Builder.zig");
-const ProgressBar = @import("ProgressBar.zig");
-const adw_version = @import("adw_version.zig");
-
-const log = std.log.scoped(.gtk_surface);
-
-pub const Options = struct {
- /// The parent surface to inherit settings such as font size, working
- /// directory, etc. from.
- parent: ?*CoreSurface = null,
-};
-
-/// The container that this surface is directly attached to.
-pub const Container = union(enum) {
- /// The surface is not currently attached to anything. This means
- /// that the GLArea has been created and potentially initialized
- /// but the widget is currently floating and not part of any parent.
- none: void,
-
- /// Directly attached to a tab. (i.e. no splits)
- tab_: *Tab,
-
- /// A split within a split hierarchy. The key determines the
- /// position of the split within the parent split.
- split_tl: *Elem,
- split_br: *Elem,
-
- /// The side of the split.
- pub const SplitSide = enum { top_left, bottom_right };
-
- /// Elem is the possible element of any container. A container can
- /// hold both a surface and a split. Any valid container should
- /// have an Elem value so that it can be properly used with
- /// splits.
- pub const Elem = union(enum) {
- /// A surface is a leaf element of the split -- a terminal
- /// surface.
- surface: *Surface,
-
- /// A split is a nested split within a split. This lets you
- /// for example have a horizontal split with a vertical split
- /// on the left side (amongst all other possible
- /// combinations).
- split: *Split,
-
- /// Returns the GTK widget to add to the paned for the given
- /// element
- pub fn widget(self: Elem) *gtk.Widget {
- return switch (self) {
- .surface => |s| s.primaryWidget(),
- .split => |s| s.paned.as(gtk.Widget),
- };
- }
-
- pub fn containerPtr(self: Elem) *Container {
- return switch (self) {
- .surface => |s| &s.container,
- .split => |s| &s.container,
- };
- }
-
- pub fn deinit(self: Elem, alloc: Allocator) void {
- switch (self) {
- .surface => |s| s.unref(),
- .split => |s| s.destroy(alloc),
- }
- }
-
- pub fn grabFocus(self: Elem) void {
- switch (self) {
- .surface => |s| s.grabFocus(),
- .split => |s| s.grabFocus(),
- }
- }
-
- pub fn equalize(self: Elem) f64 {
- return switch (self) {
- .surface => 1,
- .split => |s| s.equalize(),
- };
- }
-
- /// The last surface in this container in the direction specified.
- /// Direction must be "top_left" or "bottom_right".
- pub fn deepestSurface(self: Elem, side: SplitSide) ?*Surface {
- return switch (self) {
- .surface => |s| s,
- .split => |s| (switch (side) {
- .top_left => s.top_left,
- .bottom_right => s.bottom_right,
- }).deepestSurface(side),
- };
- }
- };
-
- /// Returns the window that this surface is attached to.
- pub fn window(self: Container) ?*Window {
- return switch (self) {
- .none => null,
- .tab_ => |v| v.window,
- .split_tl, .split_br => split: {
- const s = self.split() orelse break :split null;
- break :split s.container.window();
- },
- };
- }
-
- /// Returns the tab container if it exists.
- pub fn tab(self: Container) ?*Tab {
- return switch (self) {
- .none => null,
- .tab_ => |v| v,
- .split_tl, .split_br => split: {
- const s = self.split() orelse break :split null;
- break :split s.container.tab();
- },
- };
- }
-
- /// Returns the split containing this surface (if any).
- pub fn split(self: Container) ?*Split {
- return switch (self) {
- .none, .tab_ => null,
- .split_tl => |ptr| @fieldParentPtr("top_left", ptr),
- .split_br => |ptr| @fieldParentPtr("bottom_right", ptr),
- };
- }
-
- /// The side that we are in the split.
- pub fn splitSide(self: Container) ?SplitSide {
- return switch (self) {
- .none, .tab_ => null,
- .split_tl => .top_left,
- .split_br => .bottom_right,
- };
- }
-
- /// Returns the first split with the given orientation, walking upwards in
- /// the tree.
- pub fn firstSplitWithOrientation(
- self: Container,
- orientation: Split.Orientation,
- ) ?*Split {
- return switch (self) {
- .none, .tab_ => null,
- .split_tl, .split_br => split: {
- const s = self.split() orelse break :split null;
- if (s.orientation == orientation) break :split s;
- break :split s.container.firstSplitWithOrientation(orientation);
- },
- };
- }
-
- /// Replace the container's element with this element. This is
- /// used by children to modify their parents to for example change
- /// from a surface to a split or a split back to a surface or
- /// a split to a nested split and so on.
- pub fn replace(self: Container, elem: Elem) void {
- // Move the element into the container
- switch (self) {
- .none => {},
- .tab_ => |t| t.replaceElem(elem),
- inline .split_tl, .split_br => |ptr| {
- const s = self.split().?;
- s.replace(ptr, elem);
- },
- }
-
- // Update the reverse reference to the container
- elem.containerPtr().* = self;
- }
-
- /// Remove ourselves from the container. This is used by
- /// children to effectively notify they're container that
- /// all children at this level are exiting.
- pub fn remove(self: Container) void {
- switch (self) {
- .none => {},
- .tab_ => |t| t.remove(),
- .split_tl => self.split().?.removeTopLeft(),
- .split_br => self.split().?.removeBottomRight(),
- }
- }
-};
-
-/// Whether the surface has been realized or not yet. When a surface is
-/// "realized" it means that the OpenGL context is ready and the core
-/// surface has been initialized.
-realized: bool = false,
-
-/// The config to use to initialize a surface.
-init_config: InitConfig,
-
-/// The GUI container that this surface has been attached to. This
-/// dictates some behaviors such as new splits, etc.
-container: Container = .{ .none = {} },
-
-/// The app we're part of
-app: *App,
-
-/// The overlay, this is the primary widget
-overlay: *gtk.Overlay,
-
-/// Our GTK area
-gl_area: *gtk.GLArea,
-
-/// If non-null this is the widget on the overlay that shows the URL.
-url_widget: ?URLWidget = null,
-
-/// The overlay that shows resizing information.
-resize_overlay: ResizeOverlay = undefined,
-
-/// Whether or not the current surface is zoomed in (see `toggle_split_zoom`).
-zoomed_in: bool = false,
-
-/// If non-null this is the widget on the overlay which dims the surface when it is unfocused
-unfocused_widget: ?*gtk.Widget = null,
-
-/// Any active cursor we may have
-cursor: ?*gdk.Cursor = null,
-
-/// Our title. The raw value of the title. This will be kept up to date and
-/// .title will be updated if we have focus.
-/// When set the text in this buf will be null-terminated, because we need to
-/// pass it to GTK.
-title_text: ?[:0]const u8 = null,
-
-/// The title of the surface as reported by the terminal. If it is null, the
-/// title reported by the terminal is currently being used. If the title was
-/// manually overridden by the user, this will be set to a non-null value
-/// representing the default terminal title.
-title_from_terminal: ?[:0]const u8 = null,
-
-/// Our current working directory. We use this value for setting tooltips in
-/// the headerbar subtitle if we have focus. When set, the text in this buf
-/// will be null-terminated because we need to pass it to GTK.
-pwd: ?[:0]const u8 = null,
-
-/// The timer used to delay title updates in order to prevent flickering.
-update_title_timer: ?c_uint = null,
-
-/// The core surface backing this surface
-core_surface: CoreSurface,
-
-/// The font size to use for this surface once realized.
-font_size: ?font.face.DesiredSize = null,
-
-/// Cached metrics about the surface from GTK callbacks.
-size: apprt.SurfaceSize,
-cursor_pos: apprt.CursorPos,
-
-/// Inspector state.
-inspector: ?*inspectorpkg.Inspector = null,
-
-/// Key input states. See gtkKeyPressed for detailed descriptions.
-in_keyevent: IMKeyEvent = .false,
-im_context: *gtk.IMMulticontext,
-im_composing: bool = false,
-im_buf: [128]u8 = undefined,
-im_len: u7 = 0,
-
-/// The surface-specific cgroup path. See App.transient_cgroup_path for
-/// details on what this is.
-cgroup_path: ?[]const u8 = null,
-
-/// Our context menu.
-context_menu: Menu(Surface, "context_menu", false),
-
-/// True when we have a precision scroll in progress
-precision_scroll: bool = false,
-
-/// Flag indicating whether the surface is in secure input mode.
-is_secure_input: bool = false,
-
-/// Structure for managing GUI progress bar
-progress_bar: ProgressBar,
-
-/// 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,
-};
-
-/// Configuration used for initializing the surface. We have to copy some
-/// data since initialization is delayed with GTK (on realize).
-pub const InitConfig = struct {
- parent: bool = false,
- pwd: ?[]const u8 = null,
-
- pub fn init(
- alloc: Allocator,
- app: *App,
- opts: Options,
- ) Allocator.Error!InitConfig {
- const parent = opts.parent orelse return .{};
-
- const pwd: ?[]const u8 = if (app.config.@"window-inherit-working-directory")
- try parent.pwd(alloc)
- else
- null;
- errdefer if (pwd) |p| alloc.free(p);
-
- return .{
- .parent = true,
- .pwd = pwd,
- };
- }
-
- pub fn deinit(self: *InitConfig, alloc: Allocator) void {
- if (self.pwd) |pwd| alloc.free(pwd);
- }
-};
-
-pub fn create(alloc: Allocator, app: *App, opts: Options) !*Surface {
- var surface = try alloc.create(Surface);
- errdefer alloc.destroy(surface);
- try surface.init(app, opts);
- return surface;
-}
-
-pub fn init(self: *Surface, app: *App, opts: Options) !void {
- const gl_area = gtk.GLArea.new();
- const gl_area_widget = gl_area.as(gtk.Widget);
-
- // Create an overlay so we can layer the GL area with other widgets.
- const overlay = gtk.Overlay.new();
- errdefer overlay.unref();
- const overlay_widget = overlay.as(gtk.Widget);
- overlay.setChild(gl_area_widget);
-
- // Overlay is not focusable, but the GL area is.
- overlay_widget.setFocusable(0);
- overlay_widget.setFocusOnClick(0);
-
- // We grab the floating reference to the primary widget. This allows the
- // widget tree to be moved around i.e. between a split, a tab, etc.
- // without having to be really careful about ordering to
- // prevent a destroy.
- //
- // This is unref'd in the unref() method that's called by the
- // self.container through Elem.deinit.
- _ = overlay.as(gobject.Object).refSink();
- errdefer overlay.unref();
-
- // We want the gl area to expand to fill the parent container.
- gl_area_widget.setHexpand(1);
- gl_area_widget.setVexpand(1);
-
- // Various other GL properties
- gl_area_widget.setCursorFromName("text");
- gl_area.setRequiredVersion(
- renderer.OpenGL.MIN_VERSION_MAJOR,
- renderer.OpenGL.MIN_VERSION_MINOR,
- );
- gl_area.setHasStencilBuffer(0);
- gl_area.setHasDepthBuffer(0);
- gl_area.setUseEs(0);
-
- // Key event controller will tell us about raw keypress events.
- const ec_key = gtk.EventControllerKey.new();
- errdefer ec_key.unref();
- overlay_widget.addController(ec_key.as(gtk.EventController));
- errdefer overlay_widget.removeController(ec_key.as(gtk.EventController));
-
- // Focus controller will tell us about focus enter/exit events
- const ec_focus = gtk.EventControllerFocus.new();
- errdefer ec_focus.unref();
- overlay_widget.addController(ec_focus.as(gtk.EventController));
- errdefer overlay_widget.removeController(ec_focus.as(gtk.EventController));
-
- // Create a second key controller so we can receive the raw
- // key-press events BEFORE the input method gets them.
- const ec_key_press = gtk.EventControllerKey.new();
- errdefer ec_key_press.unref();
- overlay_widget.addController(ec_key_press.as(gtk.EventController));
- errdefer overlay_widget.removeController(ec_key_press.as(gtk.EventController));
-
- // Clicks
- const gesture_click = gtk.GestureClick.new();
- errdefer gesture_click.unref();
- gesture_click.as(gtk.GestureSingle).setButton(0);
- overlay_widget.addController(gesture_click.as(gtk.EventController));
- errdefer overlay_widget.removeController(gesture_click.as(gtk.EventController));
-
- // Mouse movement
- const ec_motion = gtk.EventControllerMotion.new();
- errdefer ec_motion.unref();
- overlay_widget.addController(ec_motion.as(gtk.EventController));
- errdefer overlay_widget.removeController(ec_motion.as(gtk.EventController));
-
- // Scroll events
- const ec_scroll = gtk.EventControllerScroll.new(.flags_both_axes);
- errdefer ec_scroll.unref();
- overlay_widget.addController(ec_scroll.as(gtk.EventController));
- errdefer overlay_widget.removeController(ec_scroll.as(gtk.EventController));
-
- // The input method context that we use to translate key events into
- // characters. This doesn't have an event key controller attached because
- // we call it manually from our own key controller.
- const im_context = gtk.IMMulticontext.new();
- errdefer im_context.unref();
-
- // The GL area has to be focusable so that it can receive events
- gl_area_widget.setFocusable(1);
- gl_area_widget.setFocusOnClick(1);
-
- // Set up to handle items being dropped on our surface. Files can be dropped
- // from Nautilus and strings can be dropped from many programs.
- const drop_target = gtk.DropTarget.new(gobject.ext.types.invalid, .flags_copy);
- errdefer drop_target.unref();
- // The order of the types matters.
- var drop_target_types = [_]gobject.Type{
- gdk.FileList.getGObjectType(),
- gio.File.getGObjectType(),
- gobject.ext.types.string,
- };
- drop_target.setGtypes(&drop_target_types, drop_target_types.len);
- overlay_widget.addController(drop_target.as(gtk.EventController));
- errdefer overlay_widget.removeController(drop_target.as(gtk.EventController));
-
- // Inherit the parent's font size if we have a parent.
- const font_size: ?font.face.DesiredSize = font_size: {
- if (!app.config.@"window-inherit-font-size") break :font_size null;
- const parent = opts.parent orelse break :font_size null;
- break :font_size parent.font_size;
- };
-
- // If the parent has a transient cgroup, then we're creating cgroups
- // for each surface if we can. We need to create a child cgroup.
- const cgroup_path: ?[]const u8 = cgroup: {
- const base = app.transient_cgroup_base orelse break :cgroup null;
-
- // For the unique group name we use the self pointer. This may
- // not be a good idea for security reasons but not sure yet. We
- // may want to change this to something else eventually to be safe.
- var buf: [256]u8 = undefined;
- const name = std.fmt.bufPrint(
- &buf,
- "surfaces/{X}.scope",
- .{@intFromPtr(self)},
- ) catch unreachable;
-
- // Create the cgroup. If it fails, no big deal... just ignore.
- internal_os.cgroup.create(base, name, null) catch |err| {
- log.err("failed to create surface cgroup err={}", .{err});
- break :cgroup null;
- };
-
- // Success, save the cgroup path.
- break :cgroup std.fmt.allocPrint(
- app.core_app.alloc,
- "{s}/{s}",
- .{ base, name },
- ) catch null;
- };
- errdefer if (cgroup_path) |path| app.core_app.alloc.free(path);
-
- // Build our initialization config
- const init_config = try InitConfig.init(app.core_app.alloc, app, opts);
- errdefer init_config.deinit(app.core_app.alloc);
-
- // Build our result
- self.* = .{
- .app = app,
- .container = .{ .none = {} },
- .overlay = overlay,
- .gl_area = gl_area,
- .resize_overlay = undefined,
- .title_text = null,
- .core_surface = undefined,
- .font_size = font_size,
- .init_config = init_config,
- .size = .{ .width = 800, .height = 600 },
- .cursor_pos = .{ .x = -1, .y = -1 },
- .im_context = im_context,
- .cgroup_path = cgroup_path,
- .context_menu = undefined,
- .progress_bar = .init(self),
- };
- errdefer self.* = undefined;
-
- // initialize the context menu
- self.context_menu.init(self);
- self.context_menu.setParent(overlay.as(gtk.Widget));
-
- // initialize the resize overlay
- self.resize_overlay.init(self, &app.config);
-
- // Set our default mouse shape
- try self.setMouseShape(.text);
-
- // GL events
- _ = gtk.Widget.signals.realize.connect(
- gl_area,
- *Surface,
- gtkRealize,
- self,
- .{},
- );
- _ = gtk.Widget.signals.unrealize.connect(
- gl_area,
- *Surface,
- gtkUnrealize,
- self,
- .{},
- );
- _ = gtk.Widget.signals.destroy.connect(
- gl_area,
- *Surface,
- gtkDestroy,
- self,
- .{},
- );
- _ = gtk.GLArea.signals.render.connect(
- gl_area,
- *Surface,
- gtkRender,
- self,
- .{},
- );
- _ = gtk.GLArea.signals.resize.connect(
- gl_area,
- *Surface,
- gtkResize,
- self,
- .{},
- );
- _ = gtk.EventControllerKey.signals.key_pressed.connect(
- ec_key_press,
- *Surface,
- gtkKeyPressed,
- self,
- .{},
- );
- _ = gtk.EventControllerKey.signals.key_released.connect(
- ec_key_press,
- *Surface,
- gtkKeyReleased,
- self,
- .{},
- );
- _ = gtk.EventControllerFocus.signals.enter.connect(
- ec_focus,
- *Surface,
- gtkFocusEnter,
- self,
- .{},
- );
- _ = gtk.EventControllerFocus.signals.leave.connect(
- ec_focus,
- *Surface,
- gtkFocusLeave,
- self,
- .{},
- );
- _ = gtk.GestureClick.signals.pressed.connect(
- gesture_click,
- *Surface,
- gtkMouseDown,
- self,
- .{},
- );
- _ = gtk.GestureClick.signals.released.connect(
- gesture_click,
- *Surface,
- gtkMouseUp,
- self,
- .{},
- );
- _ = gtk.EventControllerMotion.signals.motion.connect(
- ec_motion,
- *Surface,
- gtkMouseMotion,
- self,
- .{},
- );
- _ = gtk.EventControllerMotion.signals.leave.connect(
- ec_motion,
- *Surface,
- gtkMouseLeave,
- self,
- .{},
- );
- _ = gtk.EventControllerScroll.signals.scroll.connect(
- ec_scroll,
- *Surface,
- gtkMouseScroll,
- self,
- .{},
- );
- _ = gtk.EventControllerScroll.signals.scroll_begin.connect(
- ec_scroll,
- *Surface,
- gtkMouseScrollPrecisionBegin,
- self,
- .{},
- );
- _ = gtk.EventControllerScroll.signals.scroll_end.connect(
- ec_scroll,
- *Surface,
- gtkMouseScrollPrecisionEnd,
- self,
- .{},
- );
- _ = gtk.IMContext.signals.preedit_start.connect(
- im_context,
- *Surface,
- gtkInputPreeditStart,
- self,
- .{},
- );
- _ = gtk.IMContext.signals.preedit_changed.connect(
- im_context,
- *Surface,
- gtkInputPreeditChanged,
- self,
- .{},
- );
- _ = gtk.IMContext.signals.preedit_end.connect(
- im_context,
- *Surface,
- gtkInputPreeditEnd,
- self,
- .{},
- );
- _ = gtk.IMContext.signals.commit.connect(
- im_context,
- *Surface,
- gtkInputCommit,
- self,
- .{},
- );
- _ = gtk.DropTarget.signals.drop.connect(
- drop_target,
- *Surface,
- gtkDrop,
- self,
- .{},
- );
-}
-
-fn realize(self: *Surface) !void {
- // If this surface has already been realized, then we don't need to
- // reinitialize. This can happen if a surface is moved from one GDK
- // surface to another (i.e. a tab is pulled out into a window).
- if (self.realized) {
- // If we have no OpenGL state though, we do need to reinitialize.
- // We allow the renderer to figure that out, and then queue a draw.
- try self.core_surface.renderer.displayRealized();
- self.redraw();
- return;
- }
-
- // Add ourselves to the list of surfaces on the app.
- try self.app.core_app.addSurface(self);
- errdefer self.app.core_app.deleteSurface(self);
-
- // Get our new surface config
- var config = try apprt.surface.newConfig(self.app.core_app, &self.app.config);
- defer config.deinit();
-
- if (self.init_config.pwd) |pwd| {
- // If we have a working directory we want, then we force that.
- config.@"working-directory" = pwd;
- } else if (!self.init_config.parent) {
- // A hack, see the "parent_surface" field for more information.
- config.@"working-directory" = self.app.config.@"working-directory";
- }
-
- // Initialize our surface now that we have the stable pointer.
- try self.core_surface.init(
- self.app.core_app.alloc,
- &config,
- self.app.core_app,
- self.app,
- self,
- );
- errdefer self.core_surface.deinit();
-
- // If we have a font size we want, set that now
- if (self.font_size) |size| {
- try self.core_surface.setFontSize(size);
- }
-
- // Note we're realized
- self.realized = true;
-}
-
-pub fn deinit(self: *Surface) void {
- self.init_config.deinit(self.app.core_app.alloc);
- if (self.title_text) |title| self.app.core_app.alloc.free(title);
- if (self.title_from_terminal) |title| self.app.core_app.alloc.free(title);
- if (self.pwd) |pwd| self.app.core_app.alloc.free(pwd);
-
- // We don't allocate anything if we aren't realized.
- if (!self.realized) return;
-
- // Cleanup the progress bar.
- self.progress_bar.deinit();
-
- // Delete our inspector if we have one
- self.controlInspector(.hide);
-
- // 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();
- self.core_surface = undefined;
-
- // Remove the cgroup if we have one. We do this after deiniting the core
- // surface to ensure all processes have exited.
- if (self.cgroup_path) |path| {
- internal_os.cgroup.remove(path) catch |err| {
- // We don't want this to be fatal in any way so we just log
- // and continue. A dangling empty cgroup is not a big deal
- // and this should be rare.
- log.warn(
- "failed to remove cgroup for surface path={s} err={}",
- .{ path, err },
- );
- };
-
- self.app.core_app.alloc.free(path);
- }
-
- // Free all our GTK stuff
- //
- // Note we don't do anything with the "unfocused_overlay" because
- // it is attached to the overlay which by this point has been destroyed
- // and therefore the unfocused_overlay has been destroyed as well.
- self.im_context.unref();
- if (self.cursor) |cursor| cursor.unref();
- if (self.update_title_timer) |timer| _ = glib.Source.remove(timer);
- self.resize_overlay.deinit();
-}
-
-pub fn core(self: *Surface) *CoreSurface {
- return &self.core_surface;
-}
-
-pub fn rtApp(self: *const Surface) *App {
- return self.app;
-}
-
-/// Update our local copy of any configuration that we use.
-pub fn updateConfig(self: *Surface, config: *const configpkg.Config) !void {
- self.resize_overlay.updateConfig(config);
-}
-
-// unref removes the long-held reference to the gl_area and kicks off the
-// deinit/destroy process for this surface.
-pub fn unref(self: *Surface) void {
- self.overlay.unref();
-}
-
-pub fn destroy(self: *Surface, alloc: Allocator) void {
- self.deinit();
- alloc.destroy(self);
-}
-
-pub fn primaryWidget(self: *Surface) *gtk.Widget {
- return self.overlay.as(gtk.Widget);
-}
-
-fn render(self: *Surface) !void {
- try self.core_surface.renderer.drawFrame(true);
-}
-
-/// Called by core surface to get the cgroup.
-pub fn cgroup(self: *const Surface) ?[]const u8 {
- return self.cgroup_path;
-}
-
-/// Queue the inspector to render if we have one.
-pub fn queueInspectorRender(self: *Surface) void {
- if (self.inspector) |v| v.queueRender();
-}
-
-/// Invalidate the surface so that it forces a redraw on the next tick.
-pub fn redraw(self: *Surface) void {
- self.gl_area.queueRender();
-}
-
-/// Close this surface.
-pub fn close(self: *Surface, process_active: bool) void {
- self.closeWithConfirmation(process_active, .{ .surface = self });
-}
-
-/// Close this surface.
-pub fn closeWithConfirmation(self: *Surface, process_active: bool, target: CloseDialog.Target) void {
- self.setSplitZoom(false);
-
- if (!process_active) {
- self.container.remove();
- return;
- }
-
- CloseDialog.show(target) catch |err| {
- log.err("failed to open close dialog={}", .{err});
- };
-}
-
-pub fn controlInspector(
- self: *Surface,
- mode: apprt.action.Inspector,
-) void {
- const show = switch (mode) {
- .toggle => self.inspector == null,
- .show => true,
- .hide => false,
- };
-
- if (!show) {
- if (self.inspector) |v| {
- v.close();
- self.inspector = null;
- }
-
- return;
- }
-
- // If we already have an inspector, we don't need to show anything.
- if (self.inspector != null) return;
- self.inspector = inspectorpkg.Inspector.create(
- self,
- .{ .window = {} },
- ) catch |err| {
- log.err("failed to control inspector err={}", .{err});
- return;
- };
-}
-
-pub fn getContentScale(self: *const Surface) !apprt.ContentScale {
- const gtk_scale: f32 = scale: {
- const widget = self.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 settings = gtk.Settings.getDefault() orelse break :xft_scale 1.0;
- var value = std.mem.zeroes(gobject.Value);
- defer value.unset();
- _ = value.init(gobject.ext.typeFor(c_int));
- settings.as(gobject.Object).getProperty("gtk-xft-dpi", &value);
- const gtk_xft_dpi = value.getInt();
-
- // 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 was not set, using default value", .{});
- 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: *const Surface) !apprt.SurfaceSize {
- return self.size;
-}
-
-pub fn setInitialWindowSize(self: *const Surface, width: u32, height: u32) !void {
- // If we've already become realized once then we ignore this
- // request. The apprt initial_size action should only modify
- // the physical size of the window during initialization.
- // Subsequent actions are only informative in case we want to
- // implement a "return to default size" action later.
- if (self.realized) return;
-
- // If we are within a split, do not set the size.
- if (self.container.split() != null) return;
-
- // This operation only makes sense if we're within a window view
- // hierarchy and we're the first tab in the window.
- const window = self.container.window() orelse return;
- if (window.notebook.nPages() > 1) return;
-
- const gtk_window = window.window.as(gtk.Window);
-
- // Note: this doesn't properly take into account the window decorations.
- // I'm not currently sure how to do that.
- gtk_window.setDefaultSize(@intCast(width), @intCast(height));
-}
-
-pub fn setSizeLimits(self: *const Surface, min: apprt.SurfaceSize, max_: ?apprt.SurfaceSize) !void {
-
- // There's no support for setting max size at the moment.
- _ = max_;
-
- // If we are within a split, do not set the size.
- if (self.container.split() != null) return;
-
- // This operation only makes sense if we're within a window view
- // hierarchy and we're the first tab in the window.
- const window = self.container.window() orelse return;
- if (window.notebook.nPages() > 1) return;
-
- const widget = window.window.as(gtk.Widget);
-
- // Note: this doesn't properly take into account the window decorations.
- // I'm not currently sure how to do that.
- widget.setSizeRequest(@intCast(min.width), @intCast(min.height));
-}
-
-pub fn grabFocus(self: *Surface) void {
- if (self.container.tab()) |tab| {
- // If any other surface was focused and zoomed in, set it to non zoomed in
- // so that self can grab focus.
- if (tab.focus_child) |focus_child| {
- if (focus_child.zoomed_in and focus_child != self) {
- focus_child.setSplitZoom(false);
- }
- }
- tab.focus_child = self;
- }
-
- _ = self.gl_area.as(gtk.Widget).grabFocus();
-
- self.updateTitleLabels();
-}
-
-fn updateTitleLabels(self: *Surface) void {
- // If we have no title, then we have nothing to update.
- const title = self.getTitle() orelse return;
-
- // If we have a tab and are the focused child, then we have to update the tab
- if (self.container.tab()) |tab| {
- if (tab.focus_child == self) tab.setTitleText(title);
- }
-
- // If we have a window and are focused, then we have to update the window title.
- if (self.container.window()) |window| {
- const widget = self.gl_area.as(gtk.Widget);
- if (widget.isFocus() != 0) {
- // Changing the title somehow unhides our cursor.
- // https://github.com/ghostty-org/ghostty/issues/1419
- // I don't know a way around this yet. I've tried re-hiding the
- // cursor after setting the title but it doesn't work, I think
- // due to some gtk event loop things...
- window.setTitle(title);
- }
- }
-}
-
-const zoom_title_prefix = "🔍 ";
-pub const SetTitleSource = enum { user, terminal };
-
-pub fn setTitle(self: *Surface, slice: [:0]const u8, source: SetTitleSource) !void {
- const alloc = self.app.core_app.alloc;
-
- // Always allocate with the "🔍 " at the beginning and slice accordingly
- // is the surface is zoomed in or not.
- const copy: [:0]const u8 = copy: {
- const new_title = try alloc.allocSentinel(u8, zoom_title_prefix.len + slice.len, 0);
- @memcpy(new_title[0..zoom_title_prefix.len], zoom_title_prefix);
- @memcpy(new_title[zoom_title_prefix.len..], slice);
- break :copy new_title;
- };
- errdefer alloc.free(copy);
-
- // The user has overridden the title
- // We only want to update the terminal provided title so that it can be restored to the most recent state.
- if (self.title_from_terminal != null and source == .terminal) {
- alloc.free(self.title_from_terminal.?);
- self.title_from_terminal = copy;
- return;
- }
-
- if (self.title_text) |old| alloc.free(old);
- self.title_text = copy;
-
- // delay the title update to prevent flickering
- if (self.update_title_timer) |timer| {
- if (glib.Source.remove(timer) == 0) {
- log.warn("unable to remove update title timer", .{});
- }
- self.update_title_timer = null;
- }
- self.update_title_timer = glib.timeoutAdd(75, updateTitleTimerExpired, self);
-}
-
-fn updateTitleTimerExpired(ud: ?*anyopaque) callconv(.c) c_int {
- const self: *Surface = @ptrCast(@alignCast(ud.?));
-
- self.updateTitleLabels();
- self.update_title_timer = null;
-
- return 0;
-}
-
-pub fn getTitle(self: *Surface) ?[:0]const u8 {
- if (self.title_text) |title_text| {
- return self.resolveTitle(title_text);
- }
-
- return null;
-}
-
-pub fn getTerminalTitle(self: *Surface) ?[:0]const u8 {
- if (self.title_from_terminal) |title_text| {
- return self.resolveTitle(title_text);
- }
-
- return null;
-}
-
-fn resolveTitle(self: *Surface, title: [:0]const u8) [:0]const u8 {
- return if (self.zoomed_in)
- title
- else
- title[zoom_title_prefix.len..];
-}
-
-pub fn promptTitle(self: *Surface) !void {
- if (!adw_version.atLeast(1, 5, 0)) return;
- const window = self.container.window() orelse return;
-
- var builder = Builder.init("prompt-title-dialog", 1, 5);
- defer builder.deinit();
-
- const entry = builder.getObject(gtk.Entry, "title_entry").?;
- entry.getBuffer().setText(self.getTitle() orelse "", -1);
-
- const dialog = builder.getObject(adw.AlertDialog, "prompt_title_dialog").?;
- dialog.choose(window.window.as(gtk.Widget), null, gtkPromptTitleResponse, self);
-}
-
-/// Set the current working directory of the surface.
-///
-/// In addition, update the tab's tooltip text, and if we are the focused child,
-/// update the subtitle of the containing window.
-pub fn setPwd(self: *Surface, pwd: [:0]const u8) !void {
- if (self.container.tab()) |tab| {
- tab.setTooltipText(pwd);
-
- if (tab.focus_child == self) {
- if (self.container.window()) |window| {
- if (self.app.config.@"window-subtitle" == .@"working-directory") window.setSubtitle(pwd);
- }
- }
- }
-
- const alloc = self.app.core_app.alloc;
-
- // Failing to set the surface's current working directory is not a big
- // deal since we just used our slice parameter which is the same value.
- if (self.pwd) |old| alloc.free(old);
- self.pwd = alloc.dupeZ(u8, pwd) catch null;
-}
-
-pub fn setMouseShape(
- self: *Surface,
- shape: terminal.MouseShape,
-) !void {
- const name: [:0]const u8 = switch (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",
- };
-
- const cursor = gdk.Cursor.newFromName(name.ptr, null) orelse {
- log.warn("unsupported cursor name={s}", .{name});
- return;
- };
- errdefer cursor.unref();
-
- // Set our new cursor. We only do this if the cursor we currently
- // have is NOT set to "none" because setting the cursor causes it
- // to become visible again.
- const widget = self.gl_area.as(gtk.Widget);
- if (widget.getCursor() != self.app.cursor_none) {
- widget.setCursor(cursor);
- }
-
- // Free our existing cursor
- if (self.cursor) |old| old.unref();
- self.cursor = cursor;
-}
-
-/// Set the visibility of the mouse cursor.
-pub fn setMouseVisibility(self: *Surface, visible: bool) void {
- // Note in there that self.cursor or cursor_none may be null. That's
- // not a problem because NULL is a valid argument for set cursor
- // which means to just use the parent value.
- const widget = self.gl_area.as(gtk.Widget);
-
- if (visible) {
- widget.setCursor(self.cursor);
- return;
- }
-
- // Set our new cursor to the app "none" cursor
- widget.setCursor(self.app.cursor_none);
-}
-
-pub fn mouseOverLink(self: *Surface, uri_: ?[]const u8) void {
- const uri = uri_ orelse {
- if (self.url_widget) |*widget| {
- widget.deinit(self.overlay);
- self.url_widget = null;
- }
-
- return;
- };
-
- // We need a null-terminated string
- const alloc = self.app.core_app.alloc;
- const uriZ = alloc.dupeZ(u8, uri) catch return;
- defer alloc.free(uriZ);
-
- // If we have a URL widget already just change the text.
- if (self.url_widget) |widget| {
- widget.setText(uriZ);
- return;
- }
-
- self.url_widget = .init(self.overlay, uriZ);
-}
-
-pub fn supportsClipboard(
- self: *const Surface,
- clipboard_type: apprt.Clipboard,
-) bool {
- _ = self;
- return switch (clipboard_type) {
- .standard,
- .selection,
- .primary,
- => true,
- };
-}
-
-pub fn clipboardRequest(
- self: *Surface,
- clipboard_type: apprt.Clipboard,
- state: apprt.ClipboardRequest,
-) !void {
- // We allocate for userdata for the clipboard request. Not ideal but
- // clipboard requests aren't common so probably not a big deal.
- const alloc = self.app.core_app.alloc;
- const ud_ptr = try alloc.create(ClipboardRequest);
- errdefer alloc.destroy(ud_ptr);
- ud_ptr.* = .{ .self = self, .state = state };
-
- // Start our async request
- const clipboard = getClipboard(self.gl_area.as(gtk.Widget), clipboard_type) orelse return;
-
- clipboard.readTextAsync(null, gtkClipboardRead, ud_ptr);
-}
-
-pub fn setClipboardString(
- self: *Surface,
- val: [:0]const u8,
- clipboard_type: apprt.Clipboard,
- confirm: bool,
-) !void {
- if (!confirm) {
- const clipboard = getClipboard(self.gl_area.as(gtk.Widget), clipboard_type) orelse return;
- clipboard.setText(val);
-
- // We only toast if we are copying to the standard clipboard.
- if (clipboard_type == .standard and
- self.app.config.@"app-notifications".@"clipboard-copy")
- toast: {
- const window = self.container.window() orelse break :toast;
-
- if (val.len > 0)
- window.sendToast(i18n._("Copied to clipboard"))
- else
- window.sendToast(i18n._("Cleared clipboard"));
- }
- return;
- }
-
- ClipboardConfirmationWindow.create(
- self.app,
- val,
- &self.core_surface,
- .{ .osc_52_write = clipboard_type },
- self.is_secure_input,
- ) catch |window_err| {
- log.err("failed to create clipboard confirmation window err={}", .{window_err});
- };
-}
-
-const ClipboardRequest = struct {
- self: *Surface,
- state: apprt.ClipboardRequest,
-};
-
-fn gtkClipboardRead(
- 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: *ClipboardRequest = @ptrCast(@alignCast(ud orelse return));
- const self = req.self;
- const alloc = self.app.core_app.alloc;
- defer alloc.destroy(req);
-
- 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);
-
- self.core_surface.completeClipboardRequest(
- req.state,
- str,
- false,
- ) catch |err| switch (err) {
- error.UnsafePaste,
- error.UnauthorizedPaste,
- => {
- // Create a dialog and ask the user if they want to paste anyway.
- ClipboardConfirmationWindow.create(
- self.app,
- str,
- &self.core_surface,
- req.state,
- self.is_secure_input,
- ) catch |window_err| {
- log.err("failed to create clipboard confirmation window err={}", .{window_err});
- };
- return;
- },
-
- else => log.err("failed to complete clipboard request err={}", .{err}),
- };
-}
-
-fn getClipboard(widget: *gtk.Widget, clipboard: apprt.Clipboard) ?*gdk.Clipboard {
- return switch (clipboard) {
- .standard => widget.getClipboard(),
- .selection, .primary => widget.getPrimaryClipboard(),
- };
-}
-
-pub fn getCursorPos(self: *const Surface) !apprt.CursorPos {
- return self.cursor_pos;
-}
-
-pub fn showDesktopNotification(
- self: *Surface,
- title: []const u8,
- body: []const u8,
-) !void {
- // Set a default title if we don't already have one
- 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(build_config.bundle_id);
- defer icon.unref();
-
- notification.setIcon(icon);
-
- const pointer = glib.Variant.newUint64(@intFromPtr(&self.core_surface));
- notification.setDefaultActionAndTargetValue("app.present-surface", pointer);
-
- const app = self.app.app.as(gio.Application);
-
- // We set the notification ID to the body content. If the content is the
- // same, this notification may replace a previous notification
- app.sendNotification(body.ptr, notification);
-}
-
-fn gtkRealize(gl_area: *gtk.GLArea, self: *Surface) callconv(.c) void {
- log.debug("gl surface realized", .{});
-
- // We need to make the context current so we can call GL functions.
- gl_area.makeCurrent();
- if (gl_area.getError()) |err| {
- log.err("surface failed to realize: {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;
- }
-
- // realize means that our OpenGL context is ready, so we can now
- // initialize the core surface which will setup the renderer.
- self.realize() catch |err| {
- // TODO: we need to destroy the GL area here.
- log.err("surface failed to realize: {}", .{err});
- return;
- };
-
- // When we have a realized surface, we also attach our input method context.
- // We do this here instead of init because this allows us to release the ref
- // to the GLArea when we unrealized.
- self.im_context.as(gtk.IMContext).setClientWidget(self.overlay.as(gtk.Widget));
-}
-
-/// This is called when the underlying OpenGL resources must be released.
-/// This is usually due to the OpenGL area changing GDK surfaces.
-fn gtkUnrealize(gl_area: *gtk.GLArea, self: *Surface) callconv(.c) void {
- log.debug("gl surface unrealized", .{});
-
- // See gtkRealize for why we do this here.
- self.im_context.as(gtk.IMContext).setClientWidget(null);
-
- // 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;
- } else {
- self.core_surface.renderer.displayUnrealized();
- }
-}
-
-/// render signal
-fn gtkRender(_: *gtk.GLArea, _: *gdk.GLContext, self: *Surface) callconv(.c) c_int {
- self.render() catch |err| {
- log.err("surface failed to render: {}", .{err});
- return 0;
- };
-
- return 1;
-}
-
-/// resize signal
-fn gtkResize(gl_area: *gtk.GLArea, width: c_int, height: c_int, self: *Surface) callconv(.c) void {
- // Some debug output to help understand what GTK is telling us.
- {
- const scale_factor = scale: {
- const widget = gl_area.as(gtk.Widget);
- break :scale widget.getScaleFactor();
- };
-
- const window_scale_factor = scale: {
- const window = self.container.window() orelse break :scale 0;
- const gtk_window = window.window.as(gtk.Window);
- const gtk_native = gtk_window.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,
- });
- }
-
- self.size = .{
- .width = @intCast(width),
- .height = @intCast(height),
- };
-
- // We also update the content scale because there is no signal for
- // content scale change and it seems to trigger a resize event.
- if (self.getContentScale()) |scale| {
- self.core_surface.contentScaleCallback(scale) catch |err| {
- log.err("error in content scale callback err={}", .{err});
- return;
- };
- } else |_| {}
-
- // Call the primary callback.
- if (self.realized) {
- self.core_surface.sizeCallback(self.size) catch |err| {
- log.err("error in size callback err={}", .{err});
- return;
- };
-
- if (self.container.window()) |window| {
- window.winproto.resizeEvent() catch |err| {
- log.warn("failed to notify window protocol of resize={}", .{err});
- };
- }
-
- self.resize_overlay.maybeShow();
- }
-}
-
-/// "destroy" signal for surface
-fn gtkDestroy(_: *gtk.GLArea, self: *Surface) callconv(.c) void {
- log.debug("gl destroy", .{});
-
- const alloc = self.app.core_app.alloc;
- self.deinit();
- alloc.destroy(self);
-}
-
-/// Scale x/y by the GDK device scale.
-fn scaledCoordinates(
- self: *const Surface,
- x: f64,
- y: f64,
-) struct {
- x: f64,
- y: f64,
-} {
- const gl_are_widget = self.gl_area.as(gtk.Widget);
- const scale_factor: f64 = @floatFromInt(
- gl_are_widget.getScaleFactor(),
- );
-
- return .{
- .x = x * scale_factor,
- .y = y * scale_factor,
- };
-}
-
-fn gtkMouseDown(
- gesture: *gtk.GestureClick,
- _: c_int,
- x: f64,
- y: f64,
- self: *Surface,
-) callconv(.c) void {
- const event = gesture.as(gtk.EventController).getCurrentEvent() orelse return;
-
- const gtk_mods = event.getModifierState();
-
- const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton());
- const mods = gtk_key.translateMods(gtk_mods);
-
- // If we don't have focus, grab it.
- const gl_area_widget = self.gl_area.as(gtk.Widget);
- if (gl_area_widget.hasFocus() == 0) {
- self.grabFocus();
- }
-
- const consumed = self.core_surface.mouseButtonCallback(.press, button, mods) catch |err| {
- log.err("error in key callback err={}", .{err});
- return;
- };
-
- // 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) {
- self.context_menu.popupAt(@intFromFloat(x), @intFromFloat(y));
- }
-}
-
-fn gtkMouseUp(
- gesture: *gtk.GestureClick,
- _: c_int,
- _: f64,
- _: f64,
- self: *Surface,
-) callconv(.c) void {
- const event = gesture.as(gtk.EventController).getCurrentEvent() orelse return;
-
- const gtk_mods = event.getModifierState();
-
- const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton());
- const mods = gtk_key.translateMods(gtk_mods);
-
- _ = self.core_surface.mouseButtonCallback(.release, button, mods) catch |err| {
- log.err("error in key callback err={}", .{err});
- return;
- };
-}
-
-fn gtkMouseMotion(
- ec: *gtk.EventControllerMotion,
- x: f64,
- y: f64,
- self: *Surface,
-) callconv(.c) void {
- const event = ec.as(gtk.EventController).getCurrentEvent() orelse return;
-
- 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(self.cursor_pos.x - pos.x) < 1 and
- @abs(self.cursor_pos.y - pos.y) < 1;
-
- if (!is_cursor_still) {
- // If we don't have focus, and we want it, grab it.
- const gl_area_widget = self.gl_area.as(gtk.Widget);
- if (gl_area_widget.hasFocus() == 0 and self.app.config.@"focus-follows-mouse") {
- self.grabFocus();
- }
-
- // Our pos changed, update
- self.cursor_pos = pos;
-
- // Get our modifiers
- const gtk_mods = event.getModifierState();
- const mods = gtk_key.translateMods(gtk_mods);
-
- self.core_surface.cursorPosCallback(self.cursor_pos, mods) catch |err| {
- log.err("error in cursor pos callback err={}", .{err});
- return;
- };
- }
-}
-
-fn gtkMouseLeave(
- ec_motion: *gtk.EventControllerMotion,
- self: *Surface,
-) callconv(.c) void {
- const event = ec_motion.as(gtk.EventController).getCurrentEvent() orelse return;
-
- // Get our modifiers
- const gtk_mods = event.getModifierState();
- const mods = gtk_key.translateMods(gtk_mods);
- self.core_surface.cursorPosCallback(.{ .x = -1, .y = -1 }, mods) catch |err| {
- log.err("error in cursor pos callback err={}", .{err});
- return;
- };
-}
-
-fn gtkMouseScrollPrecisionBegin(
- _: *gtk.EventControllerScroll,
- self: *Surface,
-) callconv(.c) void {
- self.precision_scroll = true;
-}
-
-fn gtkMouseScrollPrecisionEnd(
- _: *gtk.EventControllerScroll,
- self: *Surface,
-) callconv(.c) void {
- self.precision_scroll = false;
-}
-
-fn gtkMouseScroll(
- _: *gtk.EventControllerScroll,
- x: f64,
- y: f64,
- self: *Surface,
-) callconv(.c) c_int {
- const scaled = self.scaledCoordinates(x, y);
-
- // GTK doesn't support any of the scroll mods.
- const scroll_mods: input.ScrollMods = .{ .precision = self.precision_scroll };
- // Multiply precision scrolls by 10 to get a better response from touchpad scrolling
- const multiplier: f64 = if (self.precision_scroll) 10.0 else 1.0;
-
- self.core_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.err("error in scroll callback err={}", .{err});
- return 0;
- };
-
- return 1;
-}
-
-fn gtkKeyPressed(
- ec_key: *gtk.EventControllerKey,
- keyval: c_uint,
- keycode: c_uint,
- gtk_mods: gdk.ModifierType,
- self: *Surface,
-) callconv(.c) c_int {
- return @intFromBool(self.keyEvent(
- .press,
- ec_key,
- keyval,
- keycode,
- gtk_mods,
- ));
-}
-
-fn gtkKeyReleased(
- ec_key: *gtk.EventControllerKey,
- keyval: c_uint,
- keycode: c_uint,
- state: gdk.ModifierType,
- self: *Surface,
-) callconv(.c) void {
- _ = self.keyEvent(
- .release,
- ec_key,
- keyval,
- keycode,
- state,
- );
-}
-
-/// 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("GTKIM: 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;
-
- // 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.
- const ime_point = self.core_surface.imePoint();
- self.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.
- self.in_keyevent = if (self.im_composing) .composing else .not_composing;
- defer self.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 = self.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 (self.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 (self.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 (self.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 self.im_len = 0;
-
- // Get the keyvals for this event.
- const keyval_unicode = gdk.keyvalToUnicode(keyval);
- const keyval_unicode_unshifted: u21 = gtk_key.keyvalUnicodeUnshifted(
- self.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: for (input.keycodes.entries) |entry| {
- if (entry.native == keycode) break :keycode entry.key;
- } else .unidentified;
-
- // Get our modifier for the event
- const mods: input.Mods = gtk_key.eventMods(
- event,
- physical_key,
- gtk_mods,
- action,
- &self.app.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,
- // self.im_composing,
- // self.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 (self.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, &self.im_buf)) |len| {
- self.im_len = len;
- } else |_| {}
- }
- }
-
- // Invoke the core Ghostty logic to handle this input.
- const effect = self.core_surface.keyCallback(.{
- .action = action,
- .key = physical_key,
- .mods = mods,
- .consumed_mods = consumed_mods,
- .composing = self.im_composing,
- .utf8 = self.im_buf[0..self.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 (self.im_composing) {
- self.im_context.as(gtk.IMContext).reset();
- self.core_surface.preeditCallback(null) catch {};
- }
-
- return true;
- },
- }
-
- return false;
-}
-
-fn gtkInputPreeditStart(
- _: *gtk.IMMulticontext,
- self: *Surface,
-) callconv(.c) void {
- // log.warn("GTKIM: preedit start", .{});
-
- // Start our composing state for the input method and reset our
- // input buffer to empty.
- self.im_composing = true;
- self.im_len = 0;
-}
-
-fn gtkInputPreeditChanged(
- ctx: *gtk.IMMulticontext,
- self: *Surface,
-) callconv(.c) void {
- // 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.
- self.im_composing = true;
-
- // 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});
- self.core_surface.preeditCallback(str) catch |err| {
- log.err("error in preedit callback err={}", .{err});
- };
-}
-
-fn gtkInputPreeditEnd(
- _: *gtk.IMMulticontext,
- self: *Surface,
-) callconv(.c) void {
- // log.warn("GTKIM: preedit end", .{});
-
- // End our composing state for GTK, allowing us to commit the text.
- self.im_composing = false;
-
- // End our preedit state in Ghostty core
- self.core_surface.preeditCallback(null) catch |err| {
- log.err("error in preedit callback err={}", .{err});
- };
-}
-
-fn gtkInputCommit(
- _: *gtk.IMMulticontext,
- bytes: [*:0]u8,
- self: *Surface,
-) callconv(.c) void {
- 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 (self.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 > self.im_buf.len) {
- log.warn("not enough buffer space for input method commit", .{});
- return;
- }
-
- // Copy our committed text to the buffer
- @memcpy(self.im_buf[0..str.len], str);
- self.im_len = @intCast(str.len);
-
- // log.debug("input commit len={}", .{self.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
- self.im_composing = false;
-
- // 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).
- self.core_surface.preeditCallback(null) catch |err| {
- log.err("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).
- _ = self.core_surface.keyCallback(.{
- .action = .press,
- .key = .unidentified,
- .mods = .{},
- .consumed_mods = .{},
- .composing = false,
- .utf8 = str,
- }) catch |err| {
- log.warn("error in key callback err={}", .{err});
- return;
- };
-}
-
-fn gtkFocusEnter(_: *gtk.EventControllerFocus, self: *Surface) callconv(.c) void {
- if (!self.realized) return;
-
- // Notify our IM context
- self.im_context.as(gtk.IMContext).focusIn();
-
- // Remove the unfocused widget overlay, if we have one
- if (self.unfocused_widget) |widget| {
- self.overlay.removeOverlay(widget);
- self.unfocused_widget = null;
- }
-
- if (self.pwd) |pwd| {
- if (self.container.window()) |window| {
- if (self.app.config.@"window-subtitle" == .@"working-directory") window.setSubtitle(pwd);
- }
- }
-
- // Notify our surface
- self.core_surface.focusCallback(true) catch |err| {
- log.err("error in focus callback err={}", .{err});
- return;
- };
-}
-
-fn gtkFocusLeave(_: *gtk.EventControllerFocus, self: *Surface) callconv(.c) void {
- if (!self.realized) return;
-
- // Notify our IM context
- self.im_context.as(gtk.IMContext).focusOut();
-
- // We only try dimming the surface if we are a split
- switch (self.container) {
- .split_br,
- .split_tl,
- => self.dimSurface(),
- else => {},
- }
-
- self.core_surface.focusCallback(false) catch |err| {
- log.err("error in focus callback err={}", .{err});
- return;
- };
-}
-
-/// Adds the unfocused_widget to the overlay. If the unfocused_widget has
-/// already been added, this is a no-op.
-pub fn dimSurface(self: *Surface) void {
- _ = self.container.window() orelse {
- log.warn("dimSurface invalid for container={}", .{self.container});
- return;
- };
-
- // Don't dim surface if context menu is open.
- // This means we got unfocused due to it opening.
- if (self.context_menu.isVisible()) return;
-
- // If there's already an unfocused_widget do nothing;
- if (self.unfocused_widget) |_| return;
-
- self.unfocused_widget = unfocused_widget: {
- const drawing_area = gtk.DrawingArea.new();
- const unfocused_widget = drawing_area.as(gtk.Widget);
- unfocused_widget.addCssClass("unfocused-split");
- self.overlay.addOverlay(unfocused_widget);
- break :unfocused_widget unfocused_widget;
- };
-}
-
-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,
- };
-}
-
-pub fn present(self: *Surface) void {
- if (self.container.window()) |window| {
- if (self.container.tab()) |tab| {
- if (window.notebook.getTabPosition(tab)) |position|
- _ = window.notebook.gotoNthTab(position);
- }
- window.window.as(gtk.Window).present();
- }
-
- self.grabFocus();
-}
-
-fn detachFromSplit(self: *Surface) void {
- const split = self.container.split() orelse return;
- switch (self.container.splitSide() orelse unreachable) {
- .top_left => split.detachTopLeft(),
- .bottom_right => split.detachBottomRight(),
- }
-}
-
-fn attachToSplit(self: *Surface) void {
- const split = self.container.split() orelse return;
- split.updateChildren();
-}
-
-pub fn setSplitZoom(self: *Surface, new_split_zoom: bool) void {
- if (new_split_zoom == self.zoomed_in) return;
- const tab = self.container.tab() orelse return;
-
- const tab_widget = tab.elem.widget();
- const surface_widget = self.primaryWidget();
-
- if (new_split_zoom) {
- self.detachFromSplit();
- tab.box.remove(tab_widget);
- tab.box.append(surface_widget);
- } else {
- tab.box.remove(surface_widget);
- self.attachToSplit();
- tab.box.append(tab_widget);
- }
-
- self.zoomed_in = new_split_zoom;
- self.grabFocus();
-}
-
-pub fn toggleSplitZoom(self: *Surface) void {
- self.setSplitZoom(!self.zoomed_in);
-}
-
-/// Handle items being dropped on our surface.
-fn gtkDrop(
- _: *gtk.DropTarget,
- value: *gobject.Value,
- _: f64,
- _: f64,
- self: *Surface,
-) callconv(.c) c_int {
- const alloc = self.app.core_app.alloc;
-
- if (g_value_holds(value, gdk.FileList.getGObjectType())) {
- var data = std.ArrayList(u8).init(alloc);
- defer data.deinit();
-
- var shell_escape_writer: internal_os.ShellEscapeWriter(std.ArrayList(u8).Writer) = .{
- .child_writer = data.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 = data.toOwnedSliceSentinel(0) catch |err| {
- log.err("unable to convert to a slice: {}", .{err});
- return 0;
- };
- defer alloc.free(string);
-
- self.doPaste(string);
-
- return 1;
- }
-
- if (g_value_holds(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 data = std.ArrayList(u8).init(alloc);
- defer data.deinit();
-
- var shell_escape_writer: internal_os.ShellEscapeWriter(std.ArrayList(u8).Writer) = .{
- .child_writer = data.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 = data.toOwnedSliceSentinel(0) catch |err| {
- log.err("unable to convert to a slice: {}", .{err});
- return 0;
- };
- defer alloc.free(string);
-
- self.doPaste(string);
-
- return 1;
- }
-
- if (g_value_holds(value, gobject.ext.types.string)) {
- if (value.getString()) |string| {
- const text = std.mem.span(string);
- if (text.len > 0) self.doPaste(text);
- }
- return 1;
- }
-
- return 1;
-}
-
-fn doPaste(self: *Surface, data: [:0]const u8) void {
- if (data.len == 0) return;
-
- self.core_surface.completeClipboardRequest(.paste, data, false) catch |err| switch (err) {
- error.UnsafePaste,
- error.UnauthorizedPaste,
- => {
- ClipboardConfirmationWindow.create(
- self.app,
- data,
- &self.core_surface,
- .paste,
- self.is_secure_input,
- ) catch |window_err| {
- log.err("failed to create clipboard confirmation window err={}", .{window_err});
- };
- },
- error.OutOfMemory,
- error.NoSpaceLeft,
- => log.err("failed to complete clipboard request err={}", .{err}),
- };
-}
-
-pub fn defaultTermioEnv(self: *Surface) !std.process.EnvMap {
- const alloc = self.app.core_app.alloc;
- var env = try internal_os.getEnvMap(alloc);
- errdefer env.deinit();
-
- // 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 (env.get("SNAP")) |_| {
- env.remove("SNAP");
- env.remove("DRIRC_CONFIGDIR");
- env.remove("__EGL_EXTERNAL_PLATFORM_CONFIG_DIRS");
- env.remove("__EGL_VENDOR_LIBRARY_DIRS");
- env.remove("LD_LIBRARY_PATH");
- env.remove("LIBGL_DRIVERS_PATH");
- env.remove("LIBVA_DRIVERS_PATH");
- env.remove("VK_LAYER_PATH");
- env.remove("XLOCALEDIR");
- env.remove("GDK_PIXBUF_MODULEDIR");
- env.remove("GDK_PIXBUF_MODULE_FILE");
- env.remove("GTK_PATH");
- }
-
- if (self.container.window()) |window| {
- // On some window protocols we might want to add specific
- // environment variables to subprocesses, such as WINDOWID on X11.
- try window.winproto.addSubprocessEnv(&env);
- }
-
- return env;
-}
-
-/// Check a GValue to see what's type its wrapping. This is equivalent to GTK's
-/// `G_VALUE_HOLDS` macro but Zig's C translator does not like it.
-fn g_value_holds(value_: ?*gobject.Value, g_type: gobject.Type) bool {
- if (value_) |value| {
- if (value.f_g_type == g_type) return true;
- return gobject.typeCheckValueHolds(value, g_type) != 0;
- }
- return false;
-}
-
-fn gtkPromptTitleResponse(source_object: ?*gobject.Object, result: *gio.AsyncResult, ud: ?*anyopaque) callconv(.c) void {
- if (!adw_version.supportsDialogs()) return;
- const dialog = gobject.ext.cast(adw.AlertDialog, source_object.?).?;
- const self: *Surface = @ptrCast(@alignCast(ud));
-
- const response = dialog.chooseFinish(result);
- if (std.mem.orderZ(u8, "ok", response) == .eq) {
- const title_entry = gobject.ext.cast(gtk.Entry, dialog.getExtraChild().?).?;
- const title = std.mem.span(title_entry.getBuffer().getText());
-
- // if the new title is empty and the user has set the title previously, restore the terminal provided title
- if (title.len == 0) {
- if (self.getTerminalTitle()) |terminal_title| {
- self.setTitle(terminal_title, .user) catch |err| {
- log.err("failed to set title={}", .{err});
- };
- self.app.core_app.alloc.free(self.title_from_terminal.?);
- self.title_from_terminal = null;
- }
- } else if (title.len > 0) {
- // if this is the first time the user is setting the title, save the current terminal provided title
- if (self.title_from_terminal == null and self.title_text != null) {
- self.title_from_terminal = self.app.core_app.alloc.dupeZ(u8, self.title_text.?) catch |err| switch (err) {
- error.OutOfMemory => {
- log.err("failed to allocate memory for title={}", .{err});
- return;
- },
- };
- }
-
- self.setTitle(title, .user) catch |err| {
- log.err("failed to set title={}", .{err});
- };
- }
- }
-}
-
-pub fn setSecureInput(self: *Surface, value: apprt.action.SecureInput) void {
- switch (value) {
- .on => self.is_secure_input = true,
- .off => self.is_secure_input = false,
- .toggle => self.is_secure_input = !self.is_secure_input,
- }
-}
-
-pub fn ringBell(self: *Surface) !void {
- const features = self.app.config.@"bell-features";
- const window = self.container.window() orelse {
- log.warn("failed to ring bell: surface is not attached to any window", .{});
- return;
- };
-
- // System beep
- if (features.system) system: {
- const surface = window.window.as(gtk.Native).getSurface() orelse break :system;
- surface.beep();
- }
-
- if (features.audio) audio: {
- // Play a user-specified audio file.
-
- const pathname, const required = switch (self.app.config.@"bell-audio-path" orelse break :audio) {
- .optional => |path| .{ path, false },
- .required => |path| .{ path, true },
- };
-
- const volume = std.math.clamp(self.app.config.@"bell-audio-volume", 0.0, 1.0);
-
- std.debug.assert(std.fs.path.isAbsolute(pathname));
- const media_file = gtk.MediaFile.newForFilename(pathname);
-
- if (required) {
- _ = gobject.Object.signals.notify.connect(
- media_file,
- ?*anyopaque,
- gtkStreamError,
- null,
- .{ .detail = "error" },
- );
- }
- _ = gobject.Object.signals.notify.connect(
- media_file,
- ?*anyopaque,
- gtkStreamEnded,
- null,
- .{ .detail = "ended" },
- );
-
- const media_stream = media_file.as(gtk.MediaStream);
- media_stream.setVolume(volume);
- media_stream.play();
- }
-
- if (features.attention) {
- // Request user attention
- window.winproto.setUrgent(true) catch |err| {
- log.err("failed to request user attention={}", .{err});
- };
- }
-
- // Mark tab as needing attention
- if (self.container.tab()) |tab| tab: {
- const page = window.notebook.getTabPage(tab) orelse break :tab;
-
- // Need attention if we're not the currently selected tab
- if (page.getSelected() == 0) page.setNeedsAttention(@intFromBool(true));
- }
-}
-
-/// Handle a stream that is in an error state.
-fn gtkStreamError(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 bell from {s}: {s} {d} {s}", .{
- path orelse "<<unknown>>",
- glib.quarkToString(err.f_domain),
- err.f_code,
- err.f_message orelse "",
- });
-}
-
-/// Stream is finished, release the memory.
-fn gtkStreamEnded(media_file: *gtk.MediaFile, _: *gobject.ParamSpec, _: ?*anyopaque) callconv(.c) void {
- media_file.unref();
-}
-
-/// Show native GUI element with a notification that the child process has
-/// closed. Return `true` if we are able to show the GUI notification, and
-/// `false` if we are not.
-pub fn showChildExited(self: *Surface, info: apprt.surface.Message.ChildExited) error{}!bool {
- if (!adw_version.supportsBanner()) return false;
-
- const warning_text, const css_class = if (info.exit_code == 0)
- .{ i18n._("Command succeeded"), "child_exited_normally" }
- else
- .{ i18n._("Command failed"), "child_exited_abnormally" };
-
- const banner = adw.Banner.new(warning_text);
- banner.setRevealed(1);
- banner.setButtonLabel(i18n._("Close"));
-
- _ = adw.Banner.signals.button_clicked.connect(
- banner,
- *Surface,
- showChildExitedButtonClosed,
- self,
- .{},
- );
-
- const banner_widget = banner.as(gtk.Widget);
- banner_widget.setHalign(.fill);
- banner_widget.setValign(.end);
- banner_widget.addCssClass(css_class);
-
- self.overlay.addOverlay(banner_widget);
-
- return true;
-}
-
-fn showChildExitedButtonClosed(_: *adw.Banner, self: *Surface) callconv(.c) void {
- self.close(false);
-}
diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig
deleted file mode 100644
index c32fa19fc..000000000
--- a/src/apprt/gtk/Tab.zig
+++ /dev/null
@@ -1,171 +0,0 @@
-//! The state associated with a single tab in the window.
-//!
-//! A tab can contain one or more terminals due to splits.
-const Tab = @This();
-
-const std = @import("std");
-const Allocator = std.mem.Allocator;
-const assert = std.debug.assert;
-
-const gobject = @import("gobject");
-const gtk = @import("gtk");
-
-const font = @import("../../font/main.zig");
-const input = @import("../../input.zig");
-const CoreSurface = @import("../../Surface.zig");
-
-const Surface = @import("Surface.zig");
-const Window = @import("Window.zig");
-const CloseDialog = @import("CloseDialog.zig");
-
-const log = std.log.scoped(.gtk);
-
-pub const GHOSTTY_TAB = "ghostty_tab";
-
-/// The window that owns this tab.
-window: *Window,
-
-/// The tab label. The tab label is the text that appears on the tab.
-label_text: *gtk.Label,
-
-/// We'll put our children into this box instead of packing them
-/// directly, so that we can send the box into `c.g_signal_connect_data`
-/// for the close button
-box: *gtk.Box,
-
-/// The element of this tab so that we can handle splits and so on.
-elem: Surface.Container.Elem,
-
-// We'll update this every time a Surface gains focus, so that we have it
-// when we switch to another Tab. Then when we switch back to this tab, we
-// can easily re-focus that terminal.
-focus_child: ?*Surface,
-
-pub fn create(alloc: Allocator, window: *Window, parent_: ?*CoreSurface) !*Tab {
- var tab = try alloc.create(Tab);
- errdefer alloc.destroy(tab);
- try tab.init(window, parent_);
- return tab;
-}
-
-/// Initialize the tab, create a surface, and add it to the window. "self" needs
-/// to be a stable pointer, since it is used for GTK events.
-pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void {
- self.* = .{
- .window = window,
- .label_text = undefined,
- .box = undefined,
- .elem = undefined,
- .focus_child = null,
- };
-
- // Create a Box in which we'll later keep either Surface or Split. Using a
- // box makes it easier to maintain the tab contents because we never need to
- // change the root widget of the notebook page (tab).
- const box = gtk.Box.new(.vertical, 0);
- errdefer box.unref();
- const box_widget = box.as(gtk.Widget);
- box_widget.setHexpand(1);
- box_widget.setVexpand(1);
- self.box = box;
-
- // Create the initial surface since all tabs start as a single non-split
- var surface = try Surface.create(window.app.core_app.alloc, window.app, .{
- .parent = parent_,
- });
- errdefer surface.unref();
- surface.container = .{ .tab_ = self };
- self.elem = .{ .surface = surface };
-
- // Add Surface to the Tab
- self.box.append(surface.primaryWidget());
-
- // Set the userdata of the box to point to this tab.
- self.box.as(gobject.Object).setData(GHOSTTY_TAB, self);
- window.notebook.addTab(self, "Ghostty");
-
- // Attach all events
- _ = gtk.Widget.signals.destroy.connect(
- self.box,
- *Tab,
- gtkDestroy,
- self,
- .{},
- );
-
- // We need to grab focus after Surface and Tab is added to the window. When
- // creating a Tab we want to always focus on the widget.
- surface.grabFocus();
-}
-
-/// Deinits tab by deiniting child elem.
-pub fn deinit(self: *Tab, alloc: Allocator) void {
- self.elem.deinit(alloc);
-}
-
-/// Deinit and deallocate the tab.
-pub fn destroy(self: *Tab, alloc: Allocator) void {
- self.deinit(alloc);
- alloc.destroy(self);
-}
-
-// TODO: move this
-/// Replace the surface element that this tab is showing.
-pub fn replaceElem(self: *Tab, elem: Surface.Container.Elem) void {
- // Remove our previous widget
- self.box.remove(self.elem.widget());
-
- // Add our new one
- self.box.append(elem.widget());
- self.elem = elem;
-}
-
-pub fn setTitleText(self: *Tab, title: [:0]const u8) void {
- self.window.notebook.setTabTitle(self, title);
-}
-
-pub fn setTooltipText(self: *Tab, tooltip: [:0]const u8) void {
- self.window.notebook.setTabTooltip(self, tooltip);
-}
-
-/// Remove this tab from the window.
-pub fn remove(self: *Tab) void {
- self.window.closeTab(self);
-}
-
-/// Helper function to check if any surface in the split hierarchy needs close confirmation
-fn needsConfirm(elem: Surface.Container.Elem) bool {
- return switch (elem) {
- .surface => |s| s.core_surface.needsConfirmQuit(),
- .split => |s| needsConfirm(s.top_left) or needsConfirm(s.bottom_right),
- };
-}
-
-/// Close the tab, asking for confirmation if any surface requests it.
-pub fn closeWithConfirmation(tab: *Tab) void {
- switch (tab.elem) {
- .surface => |s| s.closeWithConfirmation(
- s.core_surface.needsConfirmQuit(),
- .{ .tab = tab },
- ),
- .split => |s| {
- if (!needsConfirm(s.top_left) and !needsConfirm(s.bottom_right)) {
- tab.remove();
- return;
- }
-
- CloseDialog.show(.{ .tab = tab }) catch |err| {
- log.err("failed to open close dialog={}", .{err});
- };
- },
- }
-}
-
-fn gtkDestroy(_: *gtk.Box, self: *Tab) callconv(.c) void {
- log.debug("tab box destroy", .{});
-
- const alloc = self.window.app.core_app.alloc;
-
- // When our box is destroyed, we want to destroy our tab, too.
- self.destroy(alloc);
-}
diff --git a/src/apprt/gtk/TabView.zig b/src/apprt/gtk/TabView.zig
deleted file mode 100644
index 8a4145b5f..000000000
--- a/src/apprt/gtk/TabView.zig
+++ /dev/null
@@ -1,284 +0,0 @@
-/// An abstraction over the Adwaita tab view to manage all the terminal tabs in
-/// a window.
-const TabView = @This();
-
-const std = @import("std");
-
-const gtk = @import("gtk");
-const adw = @import("adw");
-const gobject = @import("gobject");
-const glib = @import("glib");
-
-const Window = @import("Window.zig");
-const Tab = @import("Tab.zig");
-const adw_version = @import("adw_version.zig");
-
-const log = std.log.scoped(.gtk);
-
-/// our window
-window: *Window,
-
-/// the tab view
-tab_view: *adw.TabView,
-
-/// Set to true so that the adw close-page handler knows we're forcing
-/// and to allow a close to happen with no confirm. This is a bit of a hack
-/// because we currently use GTK alerts to confirm tab close and they
-/// don't carry with them the ADW state that we are confirming or not.
-/// Long term we should move to ADW alerts so we can know if we are
-/// confirming or not.
-forcing_close: bool = false,
-
-pub fn init(self: *TabView, window: *Window) void {
- self.* = .{
- .window = window,
- .tab_view = adw.TabView.new(),
- };
- self.tab_view.as(gtk.Widget).addCssClass("notebook");
-
- if (adw_version.atLeast(1, 2, 0)) {
- // Adwaita enables all of the shortcuts by default.
- // We want to manage keybindings ourselves.
- self.tab_view.removeShortcuts(.{
- .alt_digits = true,
- .alt_zero = true,
- .control_end = true,
- .control_home = true,
- .control_page_down = true,
- .control_page_up = true,
- .control_shift_end = true,
- .control_shift_home = true,
- .control_shift_page_down = true,
- .control_shift_page_up = true,
- .control_shift_tab = true,
- .control_tab = true,
- });
- }
-
- _ = adw.TabView.signals.page_attached.connect(
- self.tab_view,
- *TabView,
- adwPageAttached,
- self,
- .{},
- );
- _ = adw.TabView.signals.close_page.connect(
- self.tab_view,
- *TabView,
- adwClosePage,
- self,
- .{},
- );
- _ = adw.TabView.signals.create_window.connect(
- self.tab_view,
- *TabView,
- adwTabViewCreateWindow,
- self,
- .{},
- );
- _ = gobject.Object.signals.notify.connect(
- self.tab_view,
- *TabView,
- adwSelectPage,
- self,
- .{
- .detail = "selected-page",
- },
- );
-}
-
-pub fn asWidget(self: *TabView) *gtk.Widget {
- return self.tab_view.as(gtk.Widget);
-}
-
-pub fn nPages(self: *TabView) c_int {
- return self.tab_view.getNPages();
-}
-
-/// Returns the index of the currently selected page.
-/// Returns null if the notebook has no pages.
-fn currentPage(self: *TabView) ?c_int {
- const page = self.tab_view.getSelectedPage() orelse return null;
- return self.tab_view.getPagePosition(page);
-}
-
-/// Returns the currently selected tab or null if there are none.
-pub fn currentTab(self: *TabView) ?*Tab {
- const page = self.tab_view.getSelectedPage() orelse return null;
- const child = page.getChild().as(gobject.Object);
- return @ptrCast(@alignCast(child.getData(Tab.GHOSTTY_TAB) orelse return null));
-}
-
-pub fn gotoNthTab(self: *TabView, position: c_int) bool {
- const page_to_select = self.tab_view.getNthPage(position);
- self.tab_view.setSelectedPage(page_to_select);
- return true;
-}
-
-pub fn getTabPage(self: *TabView, tab: *Tab) ?*adw.TabPage {
- return self.tab_view.getPage(tab.box.as(gtk.Widget));
-}
-
-pub fn getTabPosition(self: *TabView, tab: *Tab) ?c_int {
- return self.tab_view.getPagePosition(self.getTabPage(tab) orelse return null);
-}
-
-pub fn gotoPreviousTab(self: *TabView, tab: *Tab) bool {
- const page_idx = self.getTabPosition(tab) orelse return false;
-
- // The next index is the previous or we wrap around.
- const next_idx = if (page_idx > 0) page_idx - 1 else next_idx: {
- const max = self.nPages();
- break :next_idx max -| 1;
- };
-
- // Do nothing if we have one tab
- if (next_idx == page_idx) return false;
-
- return self.gotoNthTab(next_idx);
-}
-
-pub fn gotoNextTab(self: *TabView, tab: *Tab) bool {
- const page_idx = self.getTabPosition(tab) orelse return false;
-
- const max = self.nPages() -| 1;
- const next_idx = if (page_idx < max) page_idx + 1 else 0;
- if (next_idx == page_idx) return false;
-
- return self.gotoNthTab(next_idx);
-}
-
-pub fn moveTab(self: *TabView, tab: *Tab, position: c_int) void {
- const page_idx = self.getTabPosition(tab) orelse return;
-
- const max = self.nPages() -| 1;
- var new_position: c_int = page_idx + position;
-
- if (new_position < 0) {
- new_position = max + new_position + 1;
- } else if (new_position > max) {
- new_position = new_position - max - 1;
- }
-
- if (new_position == page_idx) return;
- self.reorderPage(tab, new_position);
-}
-
-pub fn reorderPage(self: *TabView, tab: *Tab, position: c_int) void {
- _ = self.tab_view.reorderPage(self.getTabPage(tab) orelse return, position);
-}
-
-pub fn setTabTitle(self: *TabView, tab: *Tab, title: [:0]const u8) void {
- const page = self.getTabPage(tab) orelse return;
- page.setTitle(title.ptr);
-}
-
-pub fn setTabTooltip(self: *TabView, tab: *Tab, tooltip: [:0]const u8) void {
- const page = self.getTabPage(tab) orelse return;
- page.setTooltip(tooltip.ptr);
-}
-
-fn newTabInsertPosition(self: *TabView, tab: *Tab) c_int {
- const numPages = self.nPages();
- return switch (tab.window.app.config.@"window-new-tab-position") {
- .current => if (self.currentPage()) |page| page + 1 else numPages,
- .end => numPages,
- };
-}
-
-/// Adds a new tab with the given title to the notebook.
-pub fn addTab(self: *TabView, tab: *Tab, title: [:0]const u8) void {
- const position = self.newTabInsertPosition(tab);
- const page = self.tab_view.insert(tab.box.as(gtk.Widget), position);
- self.setTabTitle(tab, title);
- self.tab_view.setSelectedPage(page);
-}
-
-pub fn closeTab(self: *TabView, tab: *Tab) void {
- // closeTab always expects to close unconditionally so we mark this
- // as true so that the close_page call below doesn't request
- // confirmation.
- self.forcing_close = true;
- const n = self.nPages();
- defer {
- // self becomes invalid if we close the last page because we close
- // the whole window
- if (n > 1) self.forcing_close = false;
- }
-
- if (self.getTabPage(tab)) |page| self.tab_view.closePage(page);
-
- // If we have no more tabs we close the window
- if (self.nPages() == 0) {
- // libadw versions < 1.5.1 leak the final page view
- // which causes our surface to not properly cleanup. We
- // unref to force the cleanup. This will trigger a critical
- // warning from GTK, but I don't know any other workaround.
- if (!adw_version.atLeast(1, 5, 1)) {
- tab.box.unref();
- }
-
- self.window.close();
- }
-}
-
-pub fn createWindow(window: *Window) !*Window {
- const new_window = try Window.create(window.app.core_app.alloc, window.app);
- new_window.present();
- return new_window;
-}
-
-fn adwPageAttached(_: *adw.TabView, page: *adw.TabPage, _: c_int, self: *TabView) callconv(.c) void {
- const child = page.getChild().as(gobject.Object);
- const tab: *Tab = @ptrCast(@alignCast(child.getData(Tab.GHOSTTY_TAB) orelse return));
- tab.window = self.window;
-
- self.window.focusCurrentTab();
-}
-
-fn adwClosePage(
- _: *adw.TabView,
- page: *adw.TabPage,
- self: *TabView,
-) callconv(.c) c_int {
- const child = page.getChild().as(gobject.Object);
- const tab: *Tab = @ptrCast(@alignCast(child.getData(Tab.GHOSTTY_TAB) orelse return 0));
- self.tab_view.closePageFinish(page, @intFromBool(self.forcing_close));
- if (!self.forcing_close) {
- // We cannot trigger a close directly in here as the page will stay
- // alive until this handler returns, breaking the assumption where
- // no pages means they are all destroyed.
- //
- // Schedule the close request to happen in the next event cycle.
- _ = glib.idleAddOnce(glibIdleOnceCloseTab, tab);
- }
-
- return 1;
-}
-
-fn adwTabViewCreateWindow(
- _: *adw.TabView,
- self: *TabView,
-) callconv(.c) ?*adw.TabView {
- const window = createWindow(self.window) catch |err| {
- log.warn("error creating new window error={}", .{err});
- return null;
- };
- return window.notebook.tab_view;
-}
-
-fn adwSelectPage(_: *adw.TabView, _: *gobject.ParamSpec, self: *TabView) callconv(.c) void {
- const page = self.tab_view.getSelectedPage() orelse return;
-
- // If the tab was previously marked as needing attention
- // (e.g. due to a bell character), we now unmark that
- page.setNeedsAttention(@intFromBool(false));
-
- const title = page.getTitle();
- self.window.setTitle(std.mem.span(title));
-}
-
-fn glibIdleOnceCloseTab(data: ?*anyopaque) callconv(.c) void {
- const tab: *Tab = @ptrCast(@alignCast(data orelse return));
- tab.closeWithConfirmation();
-}
diff --git a/src/apprt/gtk/URLWidget.zig b/src/apprt/gtk/URLWidget.zig
deleted file mode 100644
index e59827aaf..000000000
--- a/src/apprt/gtk/URLWidget.zig
+++ /dev/null
@@ -1,115 +0,0 @@
-//! Represents the URL hover widgets that show the hovered URL.
-//!
-//! To explain a bit how this all works since its split across a few places:
-//! We create a left/right pair of labels. The left label is shown by default,
-//! and the right label is hidden. When the mouse enters the left label, we
-//! show the right label. When the mouse leaves the left label, we hide the
-//! right label.
-//!
-//! The hover and styling is done with a combination of GTK event controllers
-//! and CSS in style.css.
-const URLWidget = @This();
-
-const gtk = @import("gtk");
-
-/// The label that appears on the bottom left.
-left: *gtk.Label,
-
-/// The label that appears on the bottom right.
-right: *gtk.Label,
-
-pub fn init(
- /// The overlay that we will attach our labels to.
- overlay: *gtk.Overlay,
- /// The URL to display.
- str: [:0]const u8,
-) URLWidget {
- // Create the left
- const left = left: {
- const left = gtk.Label.new(str.ptr);
- left.setEllipsize(.middle);
- const widget = left.as(gtk.Widget);
- widget.addCssClass("view");
- widget.addCssClass("url-overlay");
- widget.addCssClass("left");
- widget.setHalign(.start);
- widget.setValign(.end);
- break :left left;
- };
-
- // Create the right
- const right = right: {
- const right = gtk.Label.new(str.ptr);
- right.setEllipsize(.middle);
- const widget = right.as(gtk.Widget);
- widget.addCssClass("hidden");
- widget.addCssClass("view");
- widget.addCssClass("url-overlay");
- widget.addCssClass("right");
- widget.setHalign(.end);
- widget.setValign(.end);
- break :right right;
- };
-
- // Setup our mouse hover event controller for the left label.
- const ec_motion = gtk.EventControllerMotion.new();
- errdefer ec_motion.unref();
-
- left.as(gtk.Widget).addController(ec_motion.as(gtk.EventController));
-
- _ = gtk.EventControllerMotion.signals.enter.connect(
- ec_motion,
- *gtk.Label,
- gtkLeftEnter,
- right,
- .{},
- );
- _ = gtk.EventControllerMotion.signals.leave.connect(
- ec_motion,
- *gtk.Label,
- gtkLeftLeave,
- right,
- .{},
- );
-
- // Show it
- overlay.addOverlay(left.as(gtk.Widget));
- overlay.addOverlay(right.as(gtk.Widget));
-
- return .{
- .left = left,
- .right = right,
- };
-}
-
-/// Remove our labels from the overlay.
-pub fn deinit(self: *URLWidget, overlay: *gtk.Overlay) void {
- overlay.removeOverlay(self.left.as(gtk.Widget));
- overlay.removeOverlay(self.right.as(gtk.Widget));
-}
-
-/// Change the URL that is displayed.
-pub fn setText(self: *const URLWidget, str: [:0]const u8) void {
- self.left.setText(str.ptr);
- self.right.setText(str.ptr);
-}
-
-/// Callback for when the mouse enters the left label. That means that we should
-/// show the right label. CSS will handle hiding the left label.
-fn gtkLeftEnter(
- _: *gtk.EventControllerMotion,
- _: f64,
- _: f64,
- right: *gtk.Label,
-) callconv(.c) void {
- right.as(gtk.Widget).removeCssClass("hidden");
-}
-
-/// Callback for when the mouse leaves the left label. That means that we should
-/// hide the right label. CSS will handle showing the left label.
-fn gtkLeftLeave(
- _: *gtk.EventControllerMotion,
- right: *gtk.Label,
-) callconv(.c) void {
- right.as(gtk.Widget).addCssClass("hidden");
-}
diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig
deleted file mode 100644
index 8c02396a6..000000000
--- a/src/apprt/gtk/Window.zig
+++ /dev/null
@@ -1,1190 +0,0 @@
-/// A Window is a single, real GTK window that holds terminal surfaces.
-///
-/// A Window always contains a notebook (what GTK calls a tabbed container)
-/// even while no tabs are in use, because a notebook without a tab bar has
-/// no visible UI chrome.
-const Window = @This();
-
-const std = @import("std");
-const builtin = @import("builtin");
-const Allocator = std.mem.Allocator;
-const assert = std.debug.assert;
-
-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 configpkg = @import("../../config.zig");
-const font = @import("../../font/main.zig");
-const i18n = @import("../../os/main.zig").i18n;
-const input = @import("../../input.zig");
-const CoreSurface = @import("../../Surface.zig");
-
-const App = @import("App.zig");
-const Builder = @import("Builder.zig");
-const Color = configpkg.Config.Color;
-const Surface = @import("Surface.zig");
-const Menu = @import("menu.zig").Menu;
-const Tab = @import("Tab.zig");
-const gtk_key = @import("key.zig");
-const TabView = @import("TabView.zig");
-const HeaderBar = @import("headerbar.zig");
-const CloseDialog = @import("CloseDialog.zig");
-const CommandPalette = @import("CommandPalette.zig");
-const winprotopkg = @import("winproto.zig");
-const gtk_version = @import("gtk_version.zig");
-const adw_version = @import("adw_version.zig");
-
-const log = std.log.scoped(.gtk);
-
-app: *App,
-
-/// Used to deduplicate updateConfig invocations
-last_config: usize,
-
-/// Local copy of any configuration
-config: DerivedConfig,
-
-/// Our window
-window: *adw.ApplicationWindow,
-
-/// The header bar for the window.
-headerbar: HeaderBar,
-
-/// The tab bar for the window.
-tab_bar: *adw.TabBar,
-
-/// The tab overview for the window. This is possibly null since there is no
-/// taboverview without a AdwApplicationWindow (libadwaita >= 1.4.0).
-tab_overview: ?*adw.TabOverview,
-
-/// The notebook (tab grouping) for this window.
-notebook: TabView,
-
-/// The "main" menu that is attached to a button in the headerbar.
-titlebar_menu: Menu(Window, "titlebar_menu", true),
-
-/// The libadwaita widget for receiving toast send requests.
-toast_overlay: *adw.ToastOverlay,
-
-/// The command palette.
-command_palette: CommandPalette,
-
-/// See adwTabOverviewOpen for why we have this.
-adw_tab_overview_focus_timer: ?c_uint = null,
-
-/// State and logic for windowing protocol for a window.
-winproto: winprotopkg.Window,
-
-pub const DerivedConfig = struct {
- background_opacity: f64,
- background_blur: configpkg.Config.BackgroundBlur,
- window_theme: configpkg.Config.WindowTheme,
- gtk_titlebar: bool,
- gtk_titlebar_hide_when_maximized: bool,
- gtk_tabs_location: configpkg.Config.GtkTabsLocation,
- gtk_wide_tabs: bool,
- gtk_toolbar_style: configpkg.Config.GtkToolbarStyle,
- window_show_tab_bar: configpkg.Config.WindowShowTabBar,
-
- quick_terminal_position: configpkg.Config.QuickTerminalPosition,
- quick_terminal_size: configpkg.Config.QuickTerminalSize,
- quick_terminal_autohide: bool,
- quick_terminal_keyboard_interactivity: configpkg.Config.QuickTerminalKeyboardInteractivity,
-
- maximize: bool,
- fullscreen: bool,
- window_decoration: configpkg.Config.WindowDecoration,
-
- pub fn init(config: *const configpkg.Config) DerivedConfig {
- return .{
- .background_opacity = config.@"background-opacity",
- .background_blur = config.@"background-blur",
- .window_theme = config.@"window-theme",
- .gtk_titlebar = config.@"gtk-titlebar",
- .gtk_titlebar_hide_when_maximized = config.@"gtk-titlebar-hide-when-maximized",
- .gtk_tabs_location = config.@"gtk-tabs-location",
- .gtk_wide_tabs = config.@"gtk-wide-tabs",
- .gtk_toolbar_style = config.@"gtk-toolbar-style",
- .window_show_tab_bar = config.@"window-show-tab-bar",
-
- .quick_terminal_position = config.@"quick-terminal-position",
- .quick_terminal_size = config.@"quick-terminal-size",
- .quick_terminal_autohide = config.@"quick-terminal-autohide",
- .quick_terminal_keyboard_interactivity = config.@"quick-terminal-keyboard-interactivity",
-
- .maximize = config.maximize,
- .fullscreen = config.fullscreen,
- .window_decoration = config.@"window-decoration",
- };
- }
-};
-
-pub fn create(alloc: Allocator, app: *App) !*Window {
- // Allocate a fixed pointer for our window. We try to minimize
- // allocations but windows and other GUI requirements are so minimal
- // compared to the steady-state terminal operation so we use heap
- // allocation for this.
- //
- // The allocation is owned by the GtkWindow created. It will be
- // freed when the window is closed.
- var window = try alloc.create(Window);
- errdefer alloc.destroy(window);
- try window.init(app);
- return window;
-}
-
-pub fn init(self: *Window, app: *App) !void {
- // Set up our own state
- self.* = .{
- .app = app,
- .last_config = @intFromPtr(&app.config),
- .config = .init(&app.config),
- .window = undefined,
- .headerbar = undefined,
- .tab_bar = undefined,
- .tab_overview = null,
- .notebook = undefined,
- .titlebar_menu = undefined,
- .toast_overlay = undefined,
- .command_palette = undefined,
- .winproto = .none,
- };
-
- // Create the window
- self.window = .new(app.app.as(gtk.Application));
- const gtk_window = self.window.as(gtk.Window);
- const gtk_widget = self.window.as(gtk.Widget);
- errdefer gtk_window.destroy();
-
- gtk_window.setTitle("Ghostty");
- gtk_window.setDefaultSize(1000, 600);
- gtk_widget.addCssClass("window");
- gtk_widget.addCssClass("terminal-window");
-
- // 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)
- gtk_window.setHandleMenubarAccel(0);
- gtk_window.setIconName(build_config.bundle_id);
-
- // Create our box which will hold our widgets in the main content area.
- const box = gtk.Box.new(.vertical, 0);
-
- // Set up the menus
- self.titlebar_menu.init(self);
-
- // Setup our notebook
- self.notebook.init(self);
-
- if (adw_version.supportsDialogs()) try self.command_palette.init(self);
-
- // If we are using Adwaita, then we can support the tab overview.
- self.tab_overview = if (adw_version.supportsTabOverview()) overview: {
- const tab_overview = adw.TabOverview.new();
- tab_overview.setView(self.notebook.tab_view);
- tab_overview.setEnableNewTab(1);
- _ = adw.TabOverview.signals.create_tab.connect(
- tab_overview,
- *Window,
- gtkNewTabFromOverview,
- self,
- .{},
- );
- _ = gobject.Object.signals.notify.connect(
- tab_overview,
- *Window,
- adwTabOverviewOpen,
- self,
- .{
- .detail = "open",
- },
- );
- break :overview tab_overview;
- } else null;
-
- // gtk-titlebar can be used to disable the header bar (but keep the window
- // manager's decorations). We create this no matter if we are decorated or
- // not because we can have a keybind to toggle the decorations.
- self.headerbar.init(self);
-
- {
- const btn = gtk.MenuButton.new();
- btn.as(gtk.Widget).setTooltipText(i18n._("Main Menu"));
- btn.as(gtk.Widget).setCanFocus(0);
- btn.setIconName("open-menu-symbolic");
- btn.setPopover(self.titlebar_menu.asWidget());
- _ = gobject.Object.signals.notify.connect(
- btn,
- *Window,
- gtkTitlebarMenuActivate,
- self,
- .{
- .detail = "active",
- },
- );
- self.headerbar.packEnd(btn.as(gtk.Widget));
- }
-
- // If we're using an AdwWindow then we can support the tab overview.
- if (self.tab_overview) |tab_overview| {
- if (!adw_version.supportsTabOverview()) unreachable;
-
- const btn = switch (self.config.window_show_tab_bar) {
- .always, .auto => btn: {
- const btn = gtk.ToggleButton.new();
- btn.as(gtk.Widget).setTooltipText(i18n._("View Open Tabs"));
- btn.as(gtk.Button).setIconName("view-grid-symbolic");
- _ = btn.as(gobject.Object).bindProperty(
- "active",
- tab_overview.as(gobject.Object),
- "open",
- .{ .bidirectional = true, .sync_create = true },
- );
- break :btn btn.as(gtk.Widget);
- },
- .never => btn: {
- const btn = adw.TabButton.new();
- btn.setView(self.notebook.tab_view);
- btn.as(gtk.Actionable).setActionName("overview.open");
- break :btn btn.as(gtk.Widget);
- },
- };
-
- btn.setCanFocus(0);
- btn.setFocusOnClick(0);
- self.headerbar.packEnd(btn);
- }
-
- {
- const btn = adw.SplitButton.new();
- btn.setIconName("tab-new-symbolic");
- btn.as(gtk.Widget).setTooltipText(i18n._("New Tab"));
- btn.setDropdownTooltip(i18n._("New Split"));
-
- var builder = Builder.init("menu-headerbar-split_menu", 1, 0);
- defer builder.deinit();
- btn.setMenuModel(builder.getObject(gio.MenuModel, "menu"));
-
- _ = adw.SplitButton.signals.clicked.connect(
- btn,
- *Window,
- adwNewTabClick,
- self,
- .{},
- );
- self.headerbar.packStart(btn.as(gtk.Widget));
- }
-
- _ = gobject.Object.signals.notify.connect(
- self.window,
- *Window,
- gtkWindowNotifyMaximized,
- self,
- .{
- .detail = "maximized",
- },
- );
- _ = gobject.Object.signals.notify.connect(
- self.window,
- *Window,
- gtkWindowNotifyFullscreened,
- self,
- .{
- .detail = "fullscreened",
- },
- );
- _ = gobject.Object.signals.notify.connect(
- self.window,
- *Window,
- gtkWindowNotifyIsActive,
- self,
- .{
- .detail = "is-active",
- },
- );
- _ = gobject.Object.signals.notify.connect(
- self.window,
- *Window,
- gtkWindowUpdateScaleFactor,
- self,
- .{
- .detail = "scale-factor",
- },
- );
-
- // If Adwaita is enabled and is older than 1.4.0 we don't have the tab overview and so we
- // need to stick the headerbar into the content box.
- if (!adw_version.supportsTabOverview()) {
- box.append(self.headerbar.asWidget());
- }
-
- // In debug we show a warning and apply the 'devel' class to the window.
- // This is a really common issue where people build from source in debug and performance is really bad.
- if (comptime std.debug.runtime_safety) {
- const warning_box = gtk.Box.new(.vertical, 0);
- const warning_text = i18n._("⚠️ You're running a debug build of Ghostty! Performance will be degraded.");
- if (adw_version.supportsBanner()) {
- const banner = adw.Banner.new(warning_text);
- banner.setRevealed(1);
- warning_box.append(banner.as(gtk.Widget));
- } else {
- const warning = gtk.Label.new(warning_text);
- warning.as(gtk.Widget).setMarginTop(10);
- warning.as(gtk.Widget).setMarginBottom(10);
- warning_box.append(warning.as(gtk.Widget));
- }
- gtk_widget.addCssClass("devel");
- warning_box.as(gtk.Widget).addCssClass("background");
- box.append(warning_box.as(gtk.Widget));
- }
-
- // Setup our toast overlay if we have one
- self.toast_overlay = .new();
- self.toast_overlay.setChild(self.notebook.asWidget());
- box.append(self.toast_overlay.as(gtk.Widget));
-
- // If we have a tab overview then we can set it on our notebook.
- if (self.tab_overview) |tab_overview| {
- if (!adw_version.supportsTabOverview()) unreachable;
- tab_overview.setView(self.notebook.tab_view);
- }
-
- // We register a key event controller with the window so
- // we can catch key events when our surface may not be
- // focused (i.e. when the libadw tab overview is shown).
- const ec_key_press = gtk.EventControllerKey.new();
- errdefer ec_key_press.unref();
- gtk_widget.addController(ec_key_press.as(gtk.EventController));
-
- // All of our events
- _ = gtk.Widget.signals.realize.connect(
- self.window,
- *Window,
- gtkRealize,
- self,
- .{},
- );
- _ = gtk.Window.signals.close_request.connect(
- self.window,
- *Window,
- gtkCloseRequest,
- self,
- .{},
- );
- _ = gtk.Widget.signals.destroy.connect(
- self.window,
- *Window,
- gtkDestroy,
- self,
- .{},
- );
- _ = gtk.EventControllerKey.signals.key_pressed.connect(
- ec_key_press,
- *Window,
- gtkKeyPressed,
- self,
- .{},
- );
-
- // Our actions for the menu
- initActions(self);
-
- self.tab_bar = adw.TabBar.new();
- self.tab_bar.setView(self.notebook.tab_view);
-
- if (adw_version.supportsToolbarView()) {
- const toolbar_view = adw.ToolbarView.new();
- toolbar_view.addTopBar(self.headerbar.asWidget());
-
- switch (self.config.gtk_tabs_location) {
- .top => toolbar_view.addTopBar(self.tab_bar.as(gtk.Widget)),
- .bottom => toolbar_view.addBottomBar(self.tab_bar.as(gtk.Widget)),
- }
- toolbar_view.setContent(box.as(gtk.Widget));
-
- const toolbar_style: adw.ToolbarStyle = switch (self.config.gtk_toolbar_style) {
- .flat => .flat,
- .raised => .raised,
- .@"raised-border" => .raised_border,
- };
- toolbar_view.setTopBarStyle(toolbar_style);
- toolbar_view.setTopBarStyle(toolbar_style);
-
- // Set our application window content.
- self.tab_overview.?.setChild(toolbar_view.as(gtk.Widget));
- self.window.setContent(self.tab_overview.?.as(gtk.Widget));
- } else {
- // In earlier adwaita versions, we need to add the tabbar manually since we do not use
- // an AdwToolbarView.
- self.tab_bar.as(gtk.Widget).addCssClass("inline");
-
- switch (self.config.gtk_tabs_location) {
- .top => box.insertChildAfter(
- self.tab_bar.as(gtk.Widget),
- self.headerbar.asWidget(),
- ),
- .bottom => box.append(self.tab_bar.as(gtk.Widget)),
- }
- }
-
- // If we want the window to be maximized, we do that here.
- if (self.config.maximize) self.window.as(gtk.Window).maximize();
-
- // If we are in fullscreen mode, new windows start fullscreen.
- if (self.config.fullscreen) self.window.as(gtk.Window).fullscreen();
-}
-
-pub fn present(self: *Window) void {
- self.window.as(gtk.Window).present();
-}
-
-pub fn toggleVisibility(self: *Window) void {
- const widget = self.window.as(gtk.Widget);
-
- widget.setVisible(@intFromBool(widget.isVisible() == 0));
-}
-
-pub fn isQuickTerminal(self: *Window) bool {
- return self.app.quick_terminal == self;
-}
-
-pub fn updateConfig(
- self: *Window,
- config: *const configpkg.Config,
-) !void {
- // avoid multiple reconfigs when we have many surfaces contained in this
- // window using the integer value of config as a simple marker to know if
- // we've "seen" this particular config before
- const this_config = @intFromPtr(config);
- if (self.last_config == this_config) return;
- self.last_config = this_config;
-
- self.config = .init(config);
-
- // We always resync our appearance whenever the config changes.
- try self.syncAppearance();
-
- // Update binds inside the command palette
- try self.command_palette.updateConfig(config);
-}
-
-/// Updates appearance based on config settings. Will be called once upon window
-/// realization, every time the config is reloaded, and every time a window state
-/// is toggled (un-/maximized, un-/fullscreened, window decorations toggled, etc.)
-///
-/// TODO: Many of the initial style settings in `create` could possibly be made
-/// reactive by moving them here.
-pub fn syncAppearance(self: *Window) !void {
- const csd_enabled = self.winproto.clientSideDecorationEnabled();
- const gtk_window = self.window.as(gtk.Window);
- const gtk_widget = self.window.as(gtk.Widget);
- gtk_window.setDecorated(@intFromBool(csd_enabled));
-
- // Fix any artifacting that may occur in window corners. The .ssd CSS
- // class is defined in the GtkWindow documentation:
- // https://docs.gtk.org/gtk4/class.Window.html#css-nodes. A definition
- // for .ssd is provided by GTK and Adwaita.
- toggleCssClass(gtk_widget, "csd", csd_enabled);
- toggleCssClass(gtk_widget, "ssd", !csd_enabled);
- toggleCssClass(gtk_widget, "no-border-radius", !csd_enabled);
-
- self.headerbar.setVisible(visible: {
- // Never display the header bar when CSDs are disabled.
- if (!csd_enabled) break :visible false;
-
- // Never display the header bar as a quick terminal.
- if (self.isQuickTerminal()) break :visible false;
-
- // Unconditionally disable the header bar when fullscreened.
- if (self.window.as(gtk.Window).isFullscreen() != 0)
- break :visible false;
-
- // *Conditionally* disable the header bar when maximized,
- // and gtk-titlebar-hide-when-maximized is set
- if (self.window.as(gtk.Window).isMaximized() != 0 and
- self.config.gtk_titlebar_hide_when_maximized)
- break :visible false;
-
- break :visible self.config.gtk_titlebar;
- });
-
- toggleCssClass(
- gtk_widget,
- "background",
- self.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.
- toggleCssClass(
- gtk_widget,
- "window-theme-ghostty",
- !gtk_version.atLeast(4, 16, 0) and self.config.window_theme == .ghostty,
- );
-
- if (self.tab_overview) |tab_overview| {
- if (!adw_version.supportsTabOverview()) unreachable;
-
- // Disable the title buttons (close, maximize, minimize, ...)
- // *inside* the tab overview if CSDs are disabled.
- // We do spare the search button, though.
- tab_overview.setShowStartTitleButtons(@intFromBool(csd_enabled));
- tab_overview.setShowEndTitleButtons(@intFromBool(csd_enabled));
-
- // Update toolbar view style
- toolbar_view: {
- const tab_overview_child = tab_overview.getChild() orelse break :toolbar_view;
- const toolbar_view = gobject.ext.cast(
- adw.ToolbarView,
- tab_overview_child,
- ) orelse break :toolbar_view;
- const toolbar_style: adw.ToolbarStyle = switch (self.config.gtk_toolbar_style) {
- .flat => .flat,
- .raised => .raised,
- .@"raised-border" => .raised_border,
- };
- toolbar_view.setTopBarStyle(toolbar_style);
- toolbar_view.setBottomBarStyle(toolbar_style);
- }
- }
-
- self.tab_bar.setExpandTabs(@intFromBool(self.config.gtk_wide_tabs));
- self.tab_bar.setAutohide(switch (self.config.window_show_tab_bar) {
- .auto, .never => @intFromBool(true),
- .always => @intFromBool(false),
- });
- self.tab_bar.as(gtk.Widget).setVisible(switch (self.config.window_show_tab_bar) {
- .always, .auto => @intFromBool(true),
- .never => @intFromBool(false),
- });
-
- self.winproto.syncAppearance() catch |err| {
- log.warn("failed to sync winproto appearance error={}", .{err});
- };
-}
-
-fn toggleCssClass(
- widget: *gtk.Widget,
- class: [:0]const u8,
- v: bool,
-) void {
- if (v) {
- widget.addCssClass(class);
- } else {
- widget.removeCssClass(class);
- }
-}
-
-/// Sets up the GTK actions for the window scope. Actions are how GTK handles
-/// menus and such. The menu is defined in App.zig but the action is defined
-/// here. The string name binds them.
-fn initActions(self: *Window) void {
- const window = self.window.as(gtk.ApplicationWindow);
- const action_map = window.as(gio.ActionMap);
- const actions = .{
- .{ "about", gtkActionAbout },
- .{ "close", gtkActionClose },
- .{ "new-window", gtkActionNewWindow },
- .{ "new-tab", gtkActionNewTab },
- .{ "close-tab", gtkActionCloseTab },
- .{ "split-right", gtkActionSplitRight },
- .{ "split-down", gtkActionSplitDown },
- .{ "split-left", gtkActionSplitLeft },
- .{ "split-up", gtkActionSplitUp },
- .{ "toggle-inspector", gtkActionToggleInspector },
- .{ "toggle-command-palette", gtkActionToggleCommandPalette },
- .{ "copy", gtkActionCopy },
- .{ "paste", gtkActionPaste },
- .{ "reset", gtkActionReset },
- .{ "clear", gtkActionClear },
- .{ "prompt-title", gtkActionPromptTitle },
- };
-
- inline for (actions) |entry| {
- const action = gio.SimpleAction.new(entry[0], null);
- defer action.unref();
- _ = gio.SimpleAction.signals.activate.connect(
- action,
- *Window,
- entry[1],
- self,
- .{},
- );
- action_map.addAction(action.as(gio.Action));
- }
-}
-
-pub fn deinit(self: *Window) void {
- self.winproto.deinit(self.app.core_app.alloc);
- if (adw_version.supportsDialogs()) self.command_palette.deinit();
-
- if (self.adw_tab_overview_focus_timer) |timer| {
- _ = glib.Source.remove(timer);
- }
-}
-
-/// Set the title of the window.
-pub fn setTitle(self: *Window, title: [:0]const u8) void {
- self.headerbar.setTitle(title);
-}
-
-/// Set the subtitle of the window if it has one.
-pub fn setSubtitle(self: *Window, subtitle: [:0]const u8) void {
- self.headerbar.setSubtitle(subtitle);
-}
-
-/// Add a new tab to this window.
-pub fn newTab(self: *Window, parent: ?*CoreSurface) !void {
- const alloc = self.app.core_app.alloc;
- _ = try Tab.create(alloc, self, parent);
-
- // TODO: When this is triggered through a GTK action, the new surface
- // redraws correctly. When it's triggered through keyboard shortcuts, it
- // does not (cursor doesn't blink) unless reactivated by refocusing.
-}
-
-/// Close the tab for the given notebook page. This will automatically
-/// handle closing the window if there are no more tabs.
-pub fn closeTab(self: *Window, tab: *Tab) void {
- self.notebook.closeTab(tab);
-}
-
-/// Go to the previous tab for a surface.
-pub fn gotoPreviousTab(self: *Window, surface: *Surface) bool {
- const tab = surface.container.tab() orelse {
- log.info("surface is not attached to a tab bar, cannot navigate", .{});
- return false;
- };
- if (!self.notebook.gotoPreviousTab(tab)) return false;
- self.focusCurrentTab();
- return true;
-}
-
-/// Go to the next tab for a surface.
-pub fn gotoNextTab(self: *Window, surface: *Surface) bool {
- const tab = surface.container.tab() orelse {
- log.info("surface is not attached to a tab bar, cannot navigate", .{});
- return false;
- };
- if (!self.notebook.gotoNextTab(tab)) return false;
- self.focusCurrentTab();
- return true;
-}
-
-/// Move the current tab for a surface.
-pub fn moveTab(self: *Window, surface: *Surface, position: c_int) void {
- const tab = surface.container.tab() orelse {
- log.info("surface is not attached to a tab bar, cannot navigate", .{});
- return;
- };
- self.notebook.moveTab(tab, position);
-}
-
-/// Go to the last tab for a surface.
-pub fn gotoLastTab(self: *Window) bool {
- const max = self.notebook.nPages();
- return self.gotoTab(@intCast(max));
-}
-
-/// Go to the specific tab index.
-pub fn gotoTab(self: *Window, n: usize) bool {
- if (n == 0) return false;
- const max = self.notebook.nPages();
- if (max == 0) return false;
- const page_idx = std.math.cast(c_int, n - 1) orelse return false;
- if (!self.notebook.gotoNthTab(@min(page_idx, max - 1))) return false;
- self.focusCurrentTab();
- return true;
-}
-
-/// Toggle tab overview (if present)
-pub fn toggleTabOverview(self: *Window) void {
- if (self.tab_overview) |tab_overview| {
- if (!adw_version.supportsTabOverview()) unreachable;
- const is_open = tab_overview.getOpen() != 0;
- tab_overview.setOpen(@intFromBool(!is_open));
- }
-}
-
-/// Toggle the maximized state for this window.
-pub fn toggleMaximize(self: *Window) void {
- if (self.window.as(gtk.Window).isMaximized() != 0) {
- self.window.as(gtk.Window).unmaximize();
- } else {
- self.window.as(gtk.Window).maximize();
- }
- // We update the config and call syncAppearance
- // in the gtkWindowNotifyMaximized callback
-}
-
-/// Toggle fullscreen for this window.
-pub fn toggleFullscreen(self: *Window) void {
- if (self.window.as(gtk.Window).isFullscreen() != 0) {
- self.window.as(gtk.Window).unfullscreen();
- } else {
- self.window.as(gtk.Window).fullscreen();
- }
- // We update the config and call syncAppearance
- // in the gtkWindowNotifyFullscreened callback
-}
-
-/// Toggle the window decorations for this window.
-pub fn toggleWindowDecorations(self: *Window) void {
- self.config.window_decoration = switch (self.config.window_decoration) {
- .none => switch (self.app.config.@"window-decoration") {
- // If we started as none, then we switch to auto
- .none => .auto,
- // Switch back
- .auto, .client, .server => |v| v,
- },
- // Always set to none
- .auto, .client, .server => .none,
- };
-
- self.syncAppearance() catch |err| {
- log.err("failed to sync appearance={}", .{err});
- };
-}
-
-/// Toggle the window decorations for this window.
-pub fn toggleCommandPalette(self: *Window) void {
- if (adw_version.supportsDialogs()) {
- self.command_palette.toggle();
- } else {
- log.warn("libadwaita 1.5+ is required for the command palette", .{});
- }
-}
-
-/// Grabs focus on the currently selected tab.
-pub fn focusCurrentTab(self: *Window) void {
- const tab = self.notebook.currentTab() orelse return;
- const surface = tab.focus_child orelse return;
- _ = surface.gl_area.as(gtk.Widget).grabFocus();
-
- if (surface.getTitle()) |title| {
- self.setTitle(title);
- }
-}
-
-pub fn onConfigReloaded(self: *Window) void {
- if (self.app.config.@"app-notifications".@"config-reload") {
- self.sendToast(i18n._("Reloaded the configuration"));
- }
-}
-
-pub fn sendToast(self: *Window, title: [*:0]const u8) void {
- const toast = adw.Toast.new(title);
- toast.setTimeout(3);
- self.toast_overlay.addToast(toast);
-}
-
-fn gtkRealize(_: *adw.ApplicationWindow, self: *Window) callconv(.c) void {
- // Initialize our window protocol logic
- if (winprotopkg.Window.init(
- self.app.core_app.alloc,
- &self.app.winproto,
- self,
- )) |wp| {
- self.winproto = wp;
- } else |err| {
- log.warn("failed to initialize window protocol error={}", .{err});
- }
-
- // When we are realized we always setup our appearance
- self.syncAppearance() catch |err| {
- log.err("failed to initialize appearance={}", .{err});
- };
-}
-
-fn gtkWindowNotifyMaximized(
- _: *adw.ApplicationWindow,
- _: *gobject.ParamSpec,
- self: *Window,
-) callconv(.c) void {
- self.syncAppearance() catch |err| {
- log.err("failed to sync appearance={}", .{err});
- };
-}
-
-fn gtkWindowNotifyFullscreened(
- _: *adw.ApplicationWindow,
- _: *gobject.ParamSpec,
- self: *Window,
-) callconv(.c) void {
- self.syncAppearance() catch |err| {
- log.err("failed to sync appearance={}", .{err});
- };
-}
-
-fn gtkWindowNotifyIsActive(
- _: *adw.ApplicationWindow,
- _: *gobject.ParamSpec,
- self: *Window,
-) callconv(.c) void {
- self.winproto.setUrgent(false) catch |err| {
- log.err("failed to unrequest user attention={}", .{err});
- };
-
- if (self.isQuickTerminal()) {
- // Hide when we're unfocused
- if (self.config.quick_terminal_autohide and self.window.as(gtk.Window).isActive() == 0) {
- self.toggleVisibility();
- }
- }
-}
-
-fn gtkWindowUpdateScaleFactor(
- _: *adw.ApplicationWindow,
- _: *gobject.ParamSpec,
- self: *Window,
-) 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.winproto.syncAppearance() catch |err| {
- log.err(
- "failed to sync appearance after scale factor has been updated={}",
- .{err},
- );
- return;
- };
-}
-
-/// Perform a binding action on the window's action surface.
-pub fn performBindingAction(self: *Window, action: input.Binding.Action) void {
- const surface = self.actionSurface() orelse return;
- _ = surface.performBindingAction(action) catch |err| {
- log.warn("error performing binding action error={}", .{err});
- return;
- };
-}
-
-fn gtkTabNewClick(_: *gtk.Button, self: *Window) callconv(.c) void {
- self.performBindingAction(.{ .new_tab = {} });
-}
-
-/// Create a new surface (tab or split).
-fn adwNewTabClick(_: *adw.SplitButton, self: *Window) callconv(.c) void {
- self.performBindingAction(.{ .new_tab = {} });
-}
-
-/// Create a new tab from the AdwTabOverview. We can't copy gtkTabNewClick
-/// because we need to return an AdwTabPage from this function.
-fn gtkNewTabFromOverview(_: *adw.TabOverview, self: *Window) callconv(.c) *adw.TabPage {
- if (!adw_version.supportsTabOverview()) unreachable;
-
- const alloc = self.app.core_app.alloc;
- const surface = self.actionSurface();
- const tab = Tab.create(alloc, self, surface) catch unreachable;
- return self.notebook.tab_view.getPage(tab.box.as(gtk.Widget));
-}
-
-fn adwTabOverviewOpen(
- tab_overview: *adw.TabOverview,
- _: *gobject.ParamSpec,
- self: *Window,
-) callconv(.c) void {
- if (!adw_version.supportsTabOverview()) unreachable;
-
- // 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
- if (self.adw_tab_overview_focus_timer) |timer| {
- _ = glib.Source.remove(timer);
- }
-
- // Restart our timer
- self.adw_tab_overview_focus_timer = glib.timeoutAdd(
- 500,
- adwTabOverviewFocusTimer,
- self,
- );
-}
-
-fn adwTabOverviewFocusTimer(
- ud: ?*anyopaque,
-) callconv(.c) c_int {
- if (!adw_version.supportsTabOverview()) unreachable;
- const self: *Window = @ptrCast(@alignCast(ud orelse return 0));
- self.adw_tab_overview_focus_timer = null;
- self.focusCurrentTab();
-
- // Remove the timer
- return 0;
-}
-
-pub fn close(self: *Window) void {
- const window = self.window.as(gtk.Window);
-
- // Unset the quick terminal on the app level
- if (self.isQuickTerminal()) self.app.quick_terminal = null;
-
- window.destroy();
-}
-
-pub fn closeWithConfirmation(self: *Window) void {
- // If none of our surfaces need confirmation, we can just exit.
- for (self.app.core_app.surfaces.items) |surface| {
- if (surface.container.window()) |window| {
- if (window == self and
- surface.core_surface.needsConfirmQuit()) break;
- }
- } else {
- self.close();
- return;
- }
-
- CloseDialog.show(.{ .window = self }) catch |err| {
- log.err("failed to open close dialog={}", .{err});
- };
-}
-
-fn gtkCloseRequest(_: *adw.ApplicationWindow, self: *Window) callconv(.c) c_int {
- log.debug("window close request", .{});
-
- self.closeWithConfirmation();
- return 1;
-}
-
-/// "destroy" signal for the window
-fn gtkDestroy(_: *adw.ApplicationWindow, self: *Window) callconv(.c) void {
- log.debug("window destroy", .{});
-
- const alloc = self.app.core_app.alloc;
- self.deinit();
- alloc.destroy(self);
-}
-
-fn gtkKeyPressed(
- ec_key: *gtk.EventControllerKey,
- keyval: c_uint,
- keycode: c_uint,
- gtk_mods: gdk.ModifierType,
- self: *Window,
-) callconv(.c) c_int {
- // We only process window-level events currently for the tab
- // overview. This is primarily defensive programming because
- // I'm not 100% certain how our logic below will interact with
- // other parts of the application but I know for sure we must
- // handle this during the tab overview.
- //
- // If someone can confidently show or explain that this is not
- // necessary, please remove this check.
- if (adw_version.supportsTabOverview()) {
- if (self.tab_overview) |tab_overview| {
- if (tab_overview.getOpen() == 0) return 0;
- }
- }
-
- const surface = self.app.core_app.focusedSurface() orelse return 0;
- return if (surface.rt_surface.keyEvent(
- .press,
- ec_key,
- keyval,
- keycode,
- gtk_mods,
- )) 1 else 0;
-}
-
-fn gtkActionAbout(
- _: *gio.SimpleAction,
- _: ?*glib.Variant,
- self: *Window,
-) callconv(.c) void {
- const name = "Ghostty";
- const icon = "com.mitchellh.ghostty";
- const website = "https://ghostty.org";
-
- if (adw_version.supportsDialogs()) {
- adw.showAboutDialog(
- self.window.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.window.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 gtkActionClose(
- _: *gio.SimpleAction,
- _: ?*glib.Variant,
- self: *Window,
-) callconv(.c) void {
- self.closeWithConfirmation();
-}
-
-fn gtkActionNewWindow(
- _: *gio.SimpleAction,
- _: ?*glib.Variant,
- self: *Window,
-) callconv(.c) void {
- self.performBindingAction(.{ .new_window = {} });
-}
-
-fn gtkActionNewTab(
- _: *gio.SimpleAction,
- _: ?*glib.Variant,
- self: *Window,
-) callconv(.c) void {
- self.performBindingAction(.{ .new_tab = {} });
-}
-
-fn gtkActionCloseTab(
- _: *gio.SimpleAction,
- _: ?*glib.Variant,
- self: *Window,
-) callconv(.c) void {
- self.performBindingAction(.{ .close_tab = .this });
-}
-
-fn gtkActionSplitRight(
- _: *gio.SimpleAction,
- _: ?*glib.Variant,
- self: *Window,
-) callconv(.c) void {
- self.performBindingAction(.{ .new_split = .right });
-}
-
-fn gtkActionSplitDown(
- _: *gio.SimpleAction,
- _: ?*glib.Variant,
- self: *Window,
-) callconv(.c) void {
- self.performBindingAction(.{ .new_split = .down });
-}
-
-fn gtkActionSplitLeft(
- _: *gio.SimpleAction,
- _: ?*glib.Variant,
- self: *Window,
-) callconv(.c) void {
- self.performBindingAction(.{ .new_split = .left });
-}
-
-fn gtkActionSplitUp(
- _: *gio.SimpleAction,
- _: ?*glib.Variant,
- self: *Window,
-) callconv(.c) void {
- self.performBindingAction(.{ .new_split = .up });
-}
-
-fn gtkActionToggleInspector(
- _: *gio.SimpleAction,
- _: ?*glib.Variant,
- self: *Window,
-) callconv(.c) void {
- self.performBindingAction(.{ .inspector = .toggle });
-}
-
-fn gtkActionToggleCommandPalette(
- _: *gio.SimpleAction,
- _: ?*glib.Variant,
- self: *Window,
-) callconv(.c) void {
- self.performBindingAction(.toggle_command_palette);
-}
-
-fn gtkActionCopy(
- _: *gio.SimpleAction,
- _: ?*glib.Variant,
- self: *Window,
-) callconv(.c) void {
- self.performBindingAction(.{ .copy_to_clipboard = {} });
-}
-
-fn gtkActionPaste(
- _: *gio.SimpleAction,
- _: ?*glib.Variant,
- self: *Window,
-) callconv(.c) void {
- self.performBindingAction(.{ .paste_from_clipboard = {} });
-}
-
-fn gtkActionReset(
- _: *gio.SimpleAction,
- _: ?*glib.Variant,
- self: *Window,
-) callconv(.c) void {
- self.performBindingAction(.{ .reset = {} });
-}
-
-fn gtkActionClear(
- _: *gio.SimpleAction,
- _: ?*glib.Variant,
- self: *Window,
-) callconv(.c) void {
- self.performBindingAction(.{ .clear_screen = {} });
-}
-
-fn gtkActionPromptTitle(
- _: *gio.SimpleAction,
- _: ?*glib.Variant,
- self: *Window,
-) callconv(.c) void {
- self.performBindingAction(.{ .prompt_surface_title = {} });
-}
-
-/// Returns the surface to use for an action.
-pub fn actionSurface(self: *Window) ?*CoreSurface {
- const tab = self.notebook.currentTab() orelse return null;
- const surface = tab.focus_child orelse return null;
- return &surface.core_surface;
-}
-
-fn gtkTitlebarMenuActivate(
- btn: *gtk.MenuButton,
- _: *gobject.ParamSpec,
- self: *Window,
-) callconv(.c) void {
- // debian 12 is stuck on GTK 4.8
- if (!gtk_version.atLeast(4, 10, 0)) return;
- const active = btn.getActive() != 0;
- if (active) {
- self.titlebar_menu.refresh();
- } else {
- self.focusCurrentTab();
- }
-}
diff --git a/src/apprt/gtk/adw_version.zig b/src/apprt/gtk/adw_version.zig
deleted file mode 100644
index 7ce88f585..000000000
--- a/src/apprt/gtk/adw_version.zig
+++ /dev/null
@@ -1,122 +0,0 @@
-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={} runtime={}", .{
- 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/src/apprt/gtk/blueprint_compiler.zig b/src/apprt/gtk/blueprint_compiler.zig
deleted file mode 100644
index 9bc515655..000000000
--- a/src/apprt/gtk/blueprint_compiler.zig
+++ /dev/null
@@ -1,160 +0,0 @@
-const std = @import("std");
-
-pub const c = @cImport({
- @cInclude("adwaita.h");
-});
-
-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();
-
- var it = try std.process.argsWithAllocator(alloc);
- defer it.deinit();
-
- _ = it.next();
-
- const required_adwaita_version = std.SemanticVersion{
- .major = try std.fmt.parseUnsigned(u8, it.next() orelse return error.NoMajorVersion, 10),
- .minor = try std.fmt.parseUnsigned(u8, it.next() orelse return error.NoMinorVersion, 10),
- .patch = 0,
- };
- const output = it.next() orelse return error.NoOutput;
- const input = it.next() orelse return error.NoInput;
-
- if (adwaita_version.order(required_adwaita_version) == .lt) {
- std.debug.print(
- \\`libadwaita` is too old.
- \\
- \\Ghostty requires a version {} 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);
- }
-
- {
- 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.
- \\
- \\Ghostty requires version {} or newer of
- \\`blueprint-compiler` as a build-time dependency starting
- \\from version 1.2. Please install it, ensure that it is
- \\available on your PATH, and then retry building Ghostty.
- \\
- , .{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.
- \\
- \\Ghostty requires version {} or newer of
- \\`blueprint-compiler` as a build-time dependency starting
- \\from version 1.2. Please install it, ensure that it is
- \\available on your PATH, and then retry building Ghostty.
- \\
- , .{required_blueprint_version});
- std.posix.exit(1);
- }
- }
-
- {
- 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.
- \\
- \\Ghostty requires version {} or newer of
- \\`blueprint-compiler` as a build-time dependency starting
- \\from version 1.2. Please install it, ensure that it is
- \\available on your PATH, and then retry building Ghostty.
- \\
- , .{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/src/apprt/gtk/cgroup.zig b/src/apprt/gtk/cgroup.zig
deleted file mode 100644
index 2f5104d09..000000000
--- a/src/apprt/gtk/cgroup.zig
+++ /dev/null
@@ -1,205 +0,0 @@
-/// Contains all the logic for putting the Ghostty process and
-/// each individual surface into its own cgroup.
-const std = @import("std");
-const assert = std.debug.assert;
-
-const gio = @import("gio");
-const glib = @import("glib");
-const gobject = @import("gobject");
-
-const Allocator = std.mem.Allocator;
-const App = @import("App.zig");
-const internal_os = @import("../../os/main.zig");
-
-const log = std.log.scoped(.gtk_systemd_cgroup);
-
-/// Initialize the cgroup for the app. This will create our
-/// transient scope, initialize the cgroups we use for the app,
-/// configure them, and return the cgroup path for the app.
-pub fn init(app: *App) ![]const u8 {
- const pid = std.os.linux.getpid();
- const alloc = app.core_app.alloc;
-
- // Get our initial cgroup. We need this so we can compare
- // and detect when we've switched to our transient group.
- const original = try internal_os.cgroup.current(
- alloc,
- pid,
- ) orelse "";
- defer alloc.free(original);
-
- // Create our transient scope. If this succeeds then the unit
- // was created, but we may not have moved into it yet, so we need
- // to do a dumb busy loop to wait for the move to complete.
- try createScope(app, pid);
- const transient = transient: while (true) {
- const current = try internal_os.cgroup.current(
- alloc,
- pid,
- ) orelse "";
- if (!std.mem.eql(u8, original, current)) break :transient current;
- alloc.free(current);
- std.time.sleep(25 * std.time.ns_per_ms);
- };
- errdefer alloc.free(transient);
- log.info("transient scope created cgroup={s}", .{transient});
-
- // Create the app cgroup and put ourselves in it. This is
- // required because controllers can't be configured while a
- // process is in a cgroup.
- try internal_os.cgroup.create(transient, "app", pid);
-
- // Create a cgroup that will contain all our surfaces. We will
- // enable the controllers and configure resource limits for surfaces
- // only on this cgroup so that it doesn't affect our main app.
- try internal_os.cgroup.create(transient, "surfaces", null);
- const surfaces = try std.fmt.allocPrint(alloc, "{s}/surfaces", .{transient});
- defer alloc.free(surfaces);
-
- // Enable all of our cgroup controllers. If these fail then
- // we just log. We can't reasonably undo what we've done above
- // so we log the warning and still return the transient group.
- // I don't know a scenario where this fails yet.
- try enableControllers(alloc, transient);
- try enableControllers(alloc, surfaces);
-
- // Configure the "high" memory limit. This limit is used instead
- // of "max" because it's a soft limit that can be exceeded and
- // can be monitored by things like systemd-oomd to kill if needed,
- // versus an instant hard kill.
- if (app.config.@"linux-cgroup-memory-limit") |limit| {
- try internal_os.cgroup.configureLimit(surfaces, .{
- .memory_high = limit,
- });
- }
-
- // Configure the "max" pids limit. This is a hard limit and cannot be
- // exceeded.
- if (app.config.@"linux-cgroup-processes-limit") |limit| {
- try internal_os.cgroup.configureLimit(surfaces, .{
- .pids_max = limit,
- });
- }
-
- return transient;
-}
-
-/// Enable all the cgroup controllers for the given cgroup.
-fn enableControllers(alloc: Allocator, cgroup: []const u8) !void {
- const raw = try internal_os.cgroup.controllers(alloc, cgroup);
- defer alloc.free(raw);
-
- // Build our string builder for enabling all controllers
- var builder = std.ArrayList(u8).init(alloc);
- defer builder.deinit();
-
- // Controllers are space-separated
- var it = std.mem.splitScalar(u8, raw, ' ');
- while (it.next()) |controller| {
- try builder.append('+');
- try builder.appendSlice(controller);
- if (it.rest().len > 0) try builder.append(' ');
- }
-
- // Enable them all
- try internal_os.cgroup.configureControllers(
- cgroup,
- builder.items,
- );
-}
-
-/// Create a transient systemd scope unit for the current process.
-///
-/// On success this will return the name of the transient scope
-/// cgroup prefix, allocated with the given allocator.
-fn createScope(app: *App, pid_: std.os.linux.pid_t) !void {
- const gio_app = app.app.as(gio.Application);
- const connection = gio_app.getDbusConnection() orelse
- return error.DbusConnectionRequired;
-
- const pid: u32 = @intCast(pid_);
-
- // The unit name needs to be unique. We use the pid for this.
- var name_buf: [256]u8 = undefined;
- const name = std.fmt.bufPrintZ(
- &name_buf,
- "app-ghostty-transient-{}.scope",
- .{pid},
- ) catch unreachable;
-
- 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();
-
- // https://www.freedesktop.org/software/systemd/man/latest/systemd-oomd.service.html
- const pressure_value = glib.Variant.newString("kill");
-
- builder.add("(sv)", "ManagedOOMMemoryPressure", pressure_value);
-
- // Delegate
- const delegate_value = glib.Variant.newBoolean(1);
- builder.add("(sv)", "Delegate", delegate_value);
-
- // 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
- 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 = connection.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/src/apprt/gtk/flatpak.zig b/src/apprt/gtk/flatpak.zig
deleted file mode 100644
index dc47c671b..000000000
--- a/src/apprt/gtk/flatpak.zig
+++ /dev/null
@@ -1,29 +0,0 @@
-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/src/apprt/gtk/gresource.zig b/src/apprt/gtk/gresource.zig
deleted file mode 100644
index 4a2e42085..000000000
--- a/src/apprt/gtk/gresource.zig
+++ /dev/null
@@ -1,168 +0,0 @@
-const std = @import("std");
-
-const css_files = [_][]const u8{
- "style.css",
- "style-dark.css",
- "style-hc.css",
- "style-hc-dark.css",
-};
-
-const icons = [_]struct {
- alias: []const u8,
- source: []const u8,
-}{
- .{
- .alias = "16x16",
- .source = "16",
- },
- .{
- .alias = "16x16@2",
- .source = "32",
- },
- .{
- .alias = "32x32",
- .source = "32",
- },
- .{
- .alias = "32x32@2",
- .source = "64",
- },
- .{
- .alias = "128x128",
- .source = "128",
- },
- .{
- .alias = "128x128@2",
- .source = "256",
- },
- .{
- .alias = "256x256",
- .source = "256",
- },
- .{
- .alias = "256x256@2",
- .source = "512",
- },
- .{
- .alias = "512x512",
- .source = "512",
- },
- .{
- .alias = "1024x1024",
- .source = "1024",
- },
-};
-
-pub const VersionedBlueprint = struct {
- major: u16,
- minor: u16,
- name: []const u8,
-};
-
-pub const blueprint_files = [_]VersionedBlueprint{
- .{ .major = 1, .minor = 5, .name = "prompt-title-dialog" },
- .{ .major = 1, .minor = 5, .name = "config-errors-dialog" },
- .{ .major = 1, .minor = 0, .name = "menu-headerbar-split_menu" },
- .{ .major = 1, .minor = 5, .name = "command-palette" },
- .{ .major = 1, .minor = 0, .name = "menu-surface-context_menu" },
- .{ .major = 1, .minor = 0, .name = "menu-window-titlebar_menu" },
- .{ .major = 1, .minor = 5, .name = "ccw-osc-52-read" },
- .{ .major = 1, .minor = 5, .name = "ccw-osc-52-write" },
- .{ .major = 1, .minor = 5, .name = "ccw-paste" },
- .{ .major = 1, .minor = 2, .name = "config-errors-dialog" },
- .{ .major = 1, .minor = 2, .name = "ccw-osc-52-read" },
- .{ .major = 1, .minor = 2, .name = "ccw-osc-52-write" },
- .{ .major = 1, .minor = 2, .name = "ccw-paste" },
-};
-
-pub fn main() !void {
- var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
- defer _ = debug_allocator.deinit();
- const alloc = debug_allocator.allocator();
-
- var extra_ui_files: std.ArrayListUnmanaged([]const u8) = .empty;
- defer {
- for (extra_ui_files.items) |item| alloc.free(item);
- extra_ui_files.deinit(alloc);
- }
-
- var it = try std.process.argsWithAllocator(alloc);
- defer it.deinit();
-
- while (it.next()) |argument| {
- if (std.mem.eql(u8, std.fs.path.extension(argument), ".ui")) {
- try extra_ui_files.append(alloc, try alloc.dupe(u8, argument));
- }
- }
-
- const writer = std.io.getStdOut().writer();
-
- try writer.writeAll(
- \\<?xml version="1.0" encoding="UTF-8"?>
- \\<gresources>
- \\ <gresource prefix="/com/mitchellh/ghostty">
- \\
- );
- for (css_files) |css_file| {
- try writer.print(
- " <file compressed=\"true\" alias=\"{s}\">src/apprt/gtk/{s}</file>\n",
- .{ css_file, css_file },
- );
- }
- try writer.writeAll(
- \\ </gresource>
- \\ <gresource prefix="/com/mitchellh/ghostty/icons">
- \\
- );
- for (icons) |icon| {
- try writer.print(
- " <file alias=\"{s}/apps/com.mitchellh.ghostty.png\">images/gnome/{s}.png</file>\n",
- .{ icon.alias, icon.source },
- );
- }
- try writer.writeAll(
- \\ </gresource>
- \\ <gresource prefix="/com/mitchellh/ghostty/ui">
- \\
- );
- for (extra_ui_files.items) |ui_file| {
- for (blueprint_files) |file| {
- const expected = try std.fmt.allocPrint(alloc, "/{d}.{d}/{s}.ui", .{ file.major, file.minor, file.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",
- .{ file.major, file.minor, file.name, ui_file },
- );
- break;
- } else return error.BlueprintNotFound;
- }
- try writer.writeAll(
- \\ </gresource>
- \\</gresources>
- \\
- );
-}
-
-pub const dependencies = deps: {
- const total = css_files.len + icons.len + blueprint_files.len;
- var deps: [total][]const u8 = undefined;
- var index: usize = 0;
- for (css_files) |css_file| {
- deps[index] = std.fmt.comptimePrint("src/apprt/gtk/{s}", .{css_file});
- index += 1;
- }
- for (icons) |icon| {
- deps[index] = std.fmt.comptimePrint("images/gnome/{s}.png", .{icon.source});
- index += 1;
- }
- for (blueprint_files) |blueprint_file| {
- deps[index] = std.fmt.comptimePrint("src/apprt/gtk/ui/{d}.{d}/{s}.blp", .{
- blueprint_file.major,
- blueprint_file.minor,
- blueprint_file.name,
- });
- index += 1;
- }
- break :deps deps;
-};
diff --git a/src/apprt/gtk/gtk_version.zig b/src/apprt/gtk/gtk_version.zig
deleted file mode 100644
index 6f3d733a5..000000000
--- a/src/apprt/gtk/gtk_version.zig
+++ /dev/null
@@ -1,140 +0,0 @@
-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={} runtime={}", .{
- 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/src/apprt/gtk/headerbar.zig b/src/apprt/gtk/headerbar.zig
deleted file mode 100644
index 03c1b427b..000000000
--- a/src/apprt/gtk/headerbar.zig
+++ /dev/null
@@ -1,54 +0,0 @@
-const HeaderBar = @This();
-
-const std = @import("std");
-
-const adw = @import("adw");
-const gtk = @import("gtk");
-
-const Window = @import("Window.zig");
-
-/// the Adwaita headerbar widget
-headerbar: *adw.HeaderBar,
-
-/// the Window that we belong to
-window: *Window,
-
-/// the Adwaita window title widget
-title: *adw.WindowTitle,
-
-pub fn init(self: *HeaderBar, window: *Window) void {
- self.* = .{
- .headerbar = adw.HeaderBar.new(),
- .window = window,
- .title = adw.WindowTitle.new(
- window.window.as(gtk.Window).getTitle() orelse "Ghostty",
- "",
- ),
- };
- self.headerbar.setTitleWidget(self.title.as(gtk.Widget));
-}
-
-pub fn setVisible(self: *const HeaderBar, visible: bool) void {
- self.headerbar.as(gtk.Widget).setVisible(@intFromBool(visible));
-}
-
-pub fn asWidget(self: *const HeaderBar) *gtk.Widget {
- return self.headerbar.as(gtk.Widget);
-}
-
-pub fn packEnd(self: *const HeaderBar, widget: *gtk.Widget) void {
- self.headerbar.packEnd(widget);
-}
-
-pub fn packStart(self: *const HeaderBar, widget: *gtk.Widget) void {
- self.headerbar.packStart(widget);
-}
-
-pub fn setTitle(self: *const HeaderBar, title: [:0]const u8) void {
- self.window.window.as(gtk.Window).setTitle(title);
- self.title.setTitle(title);
-}
-
-pub fn setSubtitle(self: *const HeaderBar, subtitle: [:0]const u8) void {
- self.title.setSubtitle(subtitle);
-}
diff --git a/src/apprt/gtk/inspector.zig b/src/apprt/gtk/inspector.zig
deleted file mode 100644
index 3adeb9711..000000000
--- a/src/apprt/gtk/inspector.zig
+++ /dev/null
@@ -1,184 +0,0 @@
-const std = @import("std");
-const Allocator = std.mem.Allocator;
-const assert = std.debug.assert;
-
-const gtk = @import("gtk");
-
-const build_config = @import("../../build_config.zig");
-const i18n = @import("../../os/main.zig").i18n;
-const App = @import("App.zig");
-const Surface = @import("Surface.zig");
-const TerminalWindow = @import("Window.zig");
-const ImguiWidget = @import("ImguiWidget.zig");
-const CoreInspector = @import("../../inspector/main.zig").Inspector;
-
-const log = std.log.scoped(.inspector);
-
-/// Inspector is the primary stateful object that represents a terminal
-/// inspector. An inspector is 1:1 with a Surface and is owned by a Surface.
-/// Closing a surface must close its inspector.
-pub const Inspector = struct {
- /// The surface that owns this inspector.
- surface: *Surface,
-
- /// The current state of where this inspector is rendered. The Inspector
- /// is the state of the inspector but this is the state of the GUI.
- location: LocationState,
-
- /// This is true if we want to destroy this inspector as soon as the
- /// location is closed. For example: set this to true, request the
- /// window be closed, let GTK do its cleanup, then note this to destroy
- /// the inner state.
- destroy_on_close: bool = true,
-
- /// Location where the inspector will be launched.
- pub const Location = union(LocationKey) {
- hidden: void,
- window: void,
- };
-
- /// The internal state for each possible location.
- const LocationState = union(LocationKey) {
- hidden: void,
- window: Window,
- };
-
- const LocationKey = enum {
- /// No GUI, but load the inspector state.
- hidden,
-
- /// A dedicated window for the inspector.
- window,
- };
-
- /// Create an inspector for the given surface in the given location.
- pub fn create(surface: *Surface, location: Location) !*Inspector {
- const alloc = surface.app.core_app.alloc;
- var ptr = try alloc.create(Inspector);
- errdefer alloc.destroy(ptr);
- try ptr.init(surface, location);
- return ptr;
- }
-
- /// Destroy all memory associated with this inspector. You generally
- /// should NOT call this publicly and should call `close` instead to
- /// use the GTK lifecycle.
- pub fn destroy(self: *Inspector) void {
- assert(self.location == .hidden);
- const alloc = self.allocator();
- self.surface.inspector = null;
- self.deinit();
- alloc.destroy(self);
- }
-
- fn init(self: *Inspector, surface: *Surface, location: Location) !void {
- self.* = .{
- .surface = surface,
- .location = undefined,
- };
-
- // Activate the inspector. If it doesn't work we ignore the error
- // because we can just show an error in the inspector window.
- self.surface.core_surface.activateInspector() catch |err| {
- log.err("failed to activate inspector err={}", .{err});
- };
-
- switch (location) {
- .hidden => self.location = .{ .hidden = {} },
- .window => try self.initWindow(),
- }
- }
-
- fn deinit(self: *Inspector) void {
- self.surface.core_surface.deactivateInspector();
- }
-
- /// Request the inspector is closed.
- pub fn close(self: *Inspector) void {
- switch (self.location) {
- .hidden => self.locationDidClose(),
- .window => |v| v.close(),
- }
- }
-
- fn locationDidClose(self: *Inspector) void {
- self.location = .{ .hidden = {} };
- if (self.destroy_on_close) self.destroy();
- }
-
- pub fn queueRender(self: *const Inspector) void {
- switch (self.location) {
- .hidden => {},
- .window => |v| v.imgui_widget.queueRender(),
- }
- }
-
- fn allocator(self: *const Inspector) Allocator {
- return self.surface.app.core_app.alloc;
- }
-
- fn initWindow(self: *Inspector) !void {
- self.location = .{ .window = undefined };
- try self.location.window.init(self);
- }
-};
-
-/// A dedicated window to hold an inspector instance.
-const Window = struct {
- inspector: *Inspector,
- window: *gtk.ApplicationWindow,
- imgui_widget: ImguiWidget,
-
- pub fn init(self: *Window, inspector: *Inspector) !void {
- // Initialize to undefined
- self.* = .{
- .inspector = inspector,
- .window = undefined,
- .imgui_widget = undefined,
- };
-
- // Create the window
- self.window = .new(inspector.surface.app.app.as(gtk.Application));
- errdefer self.window.as(gtk.Window).destroy();
-
- self.window.as(gtk.Window).setTitle(i18n._("Ghostty: Terminal Inspector"));
- self.window.as(gtk.Window).setDefaultSize(1000, 600);
- self.window.as(gtk.Window).setIconName(build_config.bundle_id);
- self.window.as(gtk.Widget).addCssClass("window");
- self.window.as(gtk.Widget).addCssClass("inspector-window");
-
- // Initialize our imgui widget
- try self.imgui_widget.init();
- errdefer self.imgui_widget.deinit();
- self.imgui_widget.render_callback = &imguiRender;
- self.imgui_widget.render_userdata = self;
- CoreInspector.setup();
-
- // Signals
- _ = gtk.Widget.signals.destroy.connect(self.window, *Window, gtkDestroy, self, .{});
- // Show the window
- self.window.as(gtk.Window).setChild(self.imgui_widget.gl_area.as(gtk.Widget));
- self.window.as(gtk.Window).present();
- }
-
- pub fn deinit(self: *Window) void {
- self.inspector.locationDidClose();
- }
-
- pub fn close(self: *const Window) void {
- self.window.as(gtk.Window).destroy();
- }
-
- fn imguiRender(ud: ?*anyopaque) void {
- const self: *Window = @ptrCast(@alignCast(ud orelse return));
- const surface = &self.inspector.surface.core_surface;
- const inspector = surface.inspector orelse return;
- inspector.render();
- }
-
- /// "destroy" signal for the window
- fn gtkDestroy(_: *gtk.ApplicationWindow, self: *Window) callconv(.c) void {
- log.debug("window destroy", .{});
- self.deinit();
- }
-};
diff --git a/src/apprt/gtk/ipc.zig b/src/apprt/gtk/ipc.zig
deleted file mode 100644
index 7c2dc3887..000000000
--- a/src/apprt/gtk/ipc.zig
+++ /dev/null
@@ -1 +0,0 @@
-pub const openNewWindow = @import("ipc/new_window.zig").openNewWindow;
diff --git a/src/apprt/gtk/ipc/new_window.zig b/src/apprt/gtk/ipc/new_window.zig
deleted file mode 100644
index 1c29ebd3f..000000000
--- a/src/apprt/gtk/ipc/new_window.zig
+++ /dev/null
@@ -1,172 +0,0 @@
-const std = @import("std");
-const builtin = @import("builtin");
-const Allocator = std.mem.Allocator;
-
-const gio = @import("gio");
-const glib = @import("glib");
-const apprt = @import("../../../apprt.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 ["echo" "hello"]>]' []
-// ```
-pub fn openNewWindow(alloc: Allocator, target: apprt.ipc.Target, value: apprt.ipc.Action.NewWindow) (Allocator.Error || std.posix.WriteError || apprt.ipc.Errors)!bool {
- const stderr = std.io.getStdErr().writer();
-
- // 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.allocPrintZ(alloc, "/{s}", .{class});
-
- std.mem.replaceScalar(u8, object_path, '.', '/');
- std.mem.replaceScalar(u8, object_path, '-', '_');
-
- break :result .{ class, object_path };
- },
- .detect => switch (builtin.mode) {
- .Debug, .ReleaseSafe => .{ "com.mitchellh.ghostty-debug", "/com/mitchellh/ghostty_debug" },
- .ReleaseFast, .ReleaseSmall => .{ "com.mitchellh.ghostty", "/com/mitchellh/ghostty" },
- },
- };
- defer {
- 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});
- 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});
- return error.IPCFailed;
- }
-
- 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)"},
- );
- return error.IPCFailed;
- }
-
- break :dbus dbus_ orelse {
- try stderr.print("gio.busGetSync returned null\n", .{});
- return error.IPCFailed;
- };
- };
- defer dbus.unref();
-
- // use a builder to create the D-Bus method call payload
- const payload = payload: {
- const builder_type = glib.VariantType.new("(sava{sv})");
- defer glib.free(builder_type);
-
- // Initialize our builder to build up our parameters
- var builder: glib.VariantBuilder = undefined;
- builder.init(builder_type);
- errdefer builder.clear();
-
- // action
- if (value.arguments == null) {
- builder.add("s", "new-window");
- } else {
- builder.add("s", "new-window-command");
- }
-
- // parameters
- {
- const av = glib.VariantType.new("av");
- defer av.free();
-
- var parameters: glib.VariantBuilder = undefined;
- parameters.init(av);
- errdefer parameters.clear();
-
- if (value.arguments) |arguments| {
- // If `-e` was specified on the command line, the first
- // parameter is an array of strings that contain the arguments
- // that came after `-e`, which will be interpreted as a command
- // to run.
- {
- const as = glib.VariantType.new("as");
- defer as.free();
-
- var command: glib.VariantBuilder = undefined;
- command.init(as);
- errdefer command.clear();
-
- for (arguments) |argument| {
- command.add("s", argument.ptr);
- }
-
- parameters.add("v", command.end());
- }
- }
-
- builder.addValue(parameters.end());
- }
-
- {
- const platform_data = glib.VariantType.new("a{sv}");
- defer platform_data.free();
-
- builder.open(platform_data);
- defer builder.close();
-
- // we have no platform data
- }
-
- break :payload builder.end();
- };
-
- {
- var err_: ?*glib.Error = null;
- defer if (err_) |err| err.free();
-
- const result_ = dbus.callSync(
- bus_name,
- 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)"},
- );
- return error.IPCFailed;
- }
- }
-
- return true;
-}
diff --git a/src/apprt/gtk/key.zig b/src/apprt/gtk/key.zig
deleted file mode 100644
index fc3296366..000000000
--- a/src/apprt/gtk/key.zig
+++ /dev/null
@@ -1,405 +0,0 @@
-const std = @import("std");
-const build_options = @import("build_options");
-
-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) !?[:0]const u8 {
- var buf_stream = std.io.fixedBufferStream(buf);
- const writer = buf_stream.writer();
-
- // 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 = buf_stream.getWritten();
- 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) !?[:0]const u8 {
- var buf_stream = std.io.fixedBufferStream(buf);
- const writer = buf_stream.writer();
-
- // 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 = buf_stream.getWritten();
- return slice[0 .. slice.len - 1 :0];
-}
-
-fn writeTriggerKey(writer: anytype, trigger: input.Binding.Trigger) !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});
- }
- },
- }
-
- 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;
- };
- },
- }
-}
-
-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 },
- })).?);
-}
-
-/// 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/src/apprt/gtk/menu.zig b/src/apprt/gtk/menu.zig
deleted file mode 100644
index 50d0d1227..000000000
--- a/src/apprt/gtk/menu.zig
+++ /dev/null
@@ -1,139 +0,0 @@
-const std = @import("std");
-
-const gtk = @import("gtk");
-const gdk = @import("gdk");
-const gio = @import("gio");
-const gobject = @import("gobject");
-
-const apprt = @import("../../apprt.zig");
-const App = @import("App.zig");
-const Window = @import("Window.zig");
-const Surface = @import("Surface.zig");
-const Builder = @import("Builder.zig");
-
-/// Abstract GTK menus to take advantage of machinery for buildtime/comptime
-/// error checking.
-pub fn Menu(
- /// GTK apprt type that the menu is "for". Window and Surface are supported
- /// right now.
- comptime T: type,
- /// Name of the menu. Along with the apprt type, this is used to look up the
- /// builder ui definitions of the menu.
- comptime menu_name: []const u8,
- /// Should the popup have a pointer pointing to the location that it's
- /// attached to.
- comptime arrow: bool,
-) type {
- return struct {
- const Self = @This();
-
- /// parent apprt object
- parent: *T,
-
- /// our widget
- menu_widget: *gtk.PopoverMenu,
-
- /// initialize the menu
- pub fn init(self: *Self, parent: *T) void {
- const object_type = switch (T) {
- Window => "window",
- Surface => "surface",
- else => unreachable,
- };
-
- var builder = Builder.init("menu-" ++ object_type ++ "-" ++ menu_name, 1, 0);
- defer builder.deinit();
-
- const menu_model = builder.getObject(gio.MenuModel, "menu").?;
-
- const menu_widget = gtk.PopoverMenu.newFromModelFull(menu_model, .{ .nested = true });
-
- // If this menu has an arrow, don't modify the horizontal alignment
- // or you get visual anomalies. See PR #6087. Otherwise set the
- // horizontal alignment to `start` so that the top left corner of
- // the menu aligns with the point that the menu is popped up at.
- if (!arrow) menu_widget.as(gtk.Widget).setHalign(.start);
-
- menu_widget.as(gtk.Popover).setHasArrow(@intFromBool(arrow));
-
- _ = gtk.Popover.signals.closed.connect(
- menu_widget,
- *Self,
- gtkRefocusTerm,
- self,
- .{},
- );
-
- self.* = .{
- .parent = parent,
- .menu_widget = menu_widget,
- };
- }
-
- pub fn setParent(self: *const Self, widget: *gtk.Widget) void {
- self.menu_widget.as(gtk.Widget).setParent(widget);
- }
-
- pub fn asWidget(self: *const Self) *gtk.Widget {
- return self.menu_widget.as(gtk.Widget);
- }
-
- pub fn isVisible(self: *const Self) bool {
- return self.menu_widget.as(gtk.Widget).getVisible() != 0;
- }
-
- /// Refresh the menu. Right now that means enabling/disabling the "Copy"
- /// menu item based on whether there is an active selection or not, but
- /// that may change in the future.
- pub fn refresh(self: *const Self) void {
- const window: *gtk.Window, const has_selection: bool = switch (T) {
- Window => window: {
- const has_selection = if (self.parent.actionSurface()) |core_surface|
- core_surface.hasSelection()
- else
- false;
-
- break :window .{ self.parent.window.as(gtk.Window), has_selection };
- },
- Surface => surface: {
- const window = self.parent.container.window() orelse return;
- const has_selection = self.parent.core_surface.hasSelection();
- break :surface .{ window.window.as(gtk.Window), has_selection };
- },
- else => unreachable,
- };
-
- const action_map: *gio.ActionMap = gobject.ext.cast(gio.ActionMap, window) orelse return;
- const action: *gio.SimpleAction = gobject.ext.cast(
- gio.SimpleAction,
- action_map.lookupAction("copy") orelse return,
- ) orelse return;
- action.setEnabled(@intFromBool(has_selection));
- }
-
- /// Pop up the menu at the given coordinates
- pub fn popupAt(self: *const Self, x: c_int, y: c_int) void {
- const rect: gdk.Rectangle = .{
- .f_x = x,
- .f_y = y,
- .f_width = 1,
- .f_height = 1,
- };
- const popover = self.menu_widget.as(gtk.Popover);
- popover.setPointingTo(&rect);
- self.refresh();
- popover.popup();
- }
-
- /// Refocus tab that lost focus because of the popover menu
- fn gtkRefocusTerm(_: *gtk.PopoverMenu, self: *Self) callconv(.c) void {
- const window: *Window = switch (T) {
- Window => self.parent,
- Surface => self.parent.container.window() orelse return,
- else => unreachable,
- };
-
- window.focusCurrentTab();
- }
- };
-}
diff --git a/src/apprt/gtk/style-dark.css b/src/apprt/gtk/style-dark.css
deleted file mode 100644
index 1ea2aeb4b..000000000
--- a/src/apprt/gtk/style-dark.css
+++ /dev/null
@@ -1,8 +0,0 @@
-.transparent {
- background-color: transparent;
-}
-
-.terminal-window .notebook paned > separator {
- background-color: rgba(36, 36, 36, 1);
- background-clip: content-box;
-}
diff --git a/src/apprt/gtk/style-hc-dark.css b/src/apprt/gtk/style-hc-dark.css
deleted file mode 100644
index a9aa2dcc0..000000000
--- a/src/apprt/gtk/style-hc-dark.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.transparent {
- background-color: transparent;
-}
diff --git a/src/apprt/gtk/style-hc.css b/src/apprt/gtk/style-hc.css
deleted file mode 100644
index a9aa2dcc0..000000000
--- a/src/apprt/gtk/style-hc.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.transparent {
- background-color: transparent;
-}
diff --git a/src/apprt/gtk/style.css b/src/apprt/gtk/style.css
deleted file mode 100644
index 777ab3810..000000000
--- a/src/apprt/gtk/style.css
+++ /dev/null
@@ -1,116 +0,0 @@
-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;
-}
-
-label.url-overlay.hidden {
- opacity: 0;
-}
-
-label.size-overlay {
- padding: 4px 8px 4px 8px;
- border-radius: 6px 6px 6px 6px;
- outline-style: solid;
- outline-width: 1px;
- outline-color: #555555;
-}
-
-label.size-overlay.hidden {
- opacity: 0;
-}
-
-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;
-}
-
-.transparent {
- background-color: transparent;
-}
-
-.terminal-window .notebook paned > separator {
- background-color: rgba(250, 250, 250, 1);
- 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;
-}
-
-.clipboard-overlay {
- border-radius: 10px;
-}
-
-.clipboard-content-view {
- filter: blur(0px);
- transition: filter 0.3s ease;
- border-radius: 10px;
-}
-
-.clipboard-content-view.blurred {
- filter: blur(5px);
-}
-
-.command-palette-search {
- font-size: 1.25rem;
- padding: 4px;
- -gtk-icon-size: 20px;
-}
-
-.command-palette-search > image:first-child {
- margin-left: 8px;
- margin-right: 4px;
-}
-
-.command-palette-search > image:last-child {
- margin-left: 4px;
- margin-right: 8px;
-}
-
-banner.child_exited_normally 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%); */
-}
-
-banner.child_exited_abnormally 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%); */
-}
-
-/*
-* Change the color of an error progressbar
-*/
-progressbar.error trough progress {
- background-color: rgb(192, 28, 40);
- /* after GTK 4.16 is a requirement, switch to the following: */
- /* background-color: var(--error-bg-color); */
-}
diff --git a/src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp b/src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp
deleted file mode 100644
index 90de02845..000000000
--- a/src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp
+++ /dev/null
@@ -1,25 +0,0 @@
-using Gtk 4.0;
-
-menu menu {
- section {
- 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";
- }
- }
-}
diff --git a/src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp b/src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp
deleted file mode 100644
index ab48552db..000000000
--- a/src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp
+++ /dev/null
@@ -1,102 +0,0 @@
-using Gtk 4.0;
-
-menu menu {
- section {
- item {
- label: _("Copy");
- action: "win.copy";
- }
-
- item {
- label: _("Paste");
- action: "win.paste";
- }
- }
-
- section {
- item {
- label: _("Clear");
- action: "win.clear";
- }
-
- item {
- label: _("Reset");
- action: "win.reset";
- }
- }
-
- section {
- submenu {
- label: _("Split");
-
- item {
- label: _("Change Title…");
- action: "win.prompt-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";
- }
- }
-
- submenu {
- label: _("Tab");
-
- item {
- label: _("New Tab");
- action: "win.new-tab";
- }
-
- item {
- label: _("Close Tab");
- action: "win.close-tab";
- }
- }
-
- 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/src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp b/src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp
deleted file mode 100644
index 3273aa81c..000000000
--- a/src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp
+++ /dev/null
@@ -1,116 +0,0 @@
-using Gtk 4.0;
-
-menu 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: _("New Tab");
- action: "win.new-tab";
- }
-
- item {
- label: _("Close Tab");
- action: "win.close-tab";
- }
- }
-
- section {
- submenu {
- label: _("Split");
-
- item {
- label: _("Change Title…");
- action: "win.prompt-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";
- }
- }
-}
diff --git a/src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp b/src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp
deleted file mode 100644
index b250073d2..000000000
--- a/src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp
+++ /dev/null
@@ -1,71 +0,0 @@
-using Gtk 4.0;
-using Adw 1;
-translation-domain "com.mitchellh.ghostty";
-
-Adw.MessageDialog clipboard_confirmation_window {
- heading: _("Authorize Clipboard Access");
- body: _("An application is attempting to read from the clipboard. The current clipboard contents are shown below.");
-
- responses [
- cancel: _("Deny") suggested,
- ok: _("Allow") destructive,
- ]
-
- default-response: "cancel";
- close-response: "cancel";
-
- extra-child: Overlay {
- styles [
- "osd",
- ]
-
- ScrolledWindow text_view_scroll {
- width-request: 500;
- height-request: 250;
-
- TextView text_view {
- cursor-visible: false;
- editable: false;
- monospace: true;
- top-margin: 8;
- left-margin: 8;
- bottom-margin: 8;
- right-margin: 8;
-
- styles [
- "clipboard-content-view",
- ]
- }
- }
-
- [overlay]
- Button reveal_button {
- visible: false;
- halign: end;
- valign: start;
- margin-end: 12;
- margin-top: 12;
-
- Image {
- icon-name: "view-reveal-symbolic";
- }
- }
-
- [overlay]
- Button hide_button {
- visible: false;
- halign: end;
- valign: start;
- margin-end: 12;
- margin-top: 12;
-
- styles [
- "opaque",
- ]
-
- Image {
- icon-name: "view-conceal-symbolic";
- }
- }
- };
-}
diff --git a/src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp b/src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp
deleted file mode 100644
index d880df5f2..000000000
--- a/src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp
+++ /dev/null
@@ -1,71 +0,0 @@
-using Gtk 4.0;
-using Adw 1;
-translation-domain "com.mitchellh.ghostty";
-
-Adw.MessageDialog clipboard_confirmation_window {
- heading: _("Authorize Clipboard Access");
- body: _("An application is attempting to write to the clipboard. The current clipboard contents are shown below.");
-
- responses [
- cancel: _("Deny") suggested,
- ok: _("Allow") destructive,
- ]
-
- default-response: "cancel";
- close-response: "cancel";
-
- extra-child: Overlay {
- styles [
- "osd",
- ]
-
- ScrolledWindow text_view_scroll {
- width-request: 500;
- height-request: 250;
-
- TextView text_view {
- cursor-visible: false;
- editable: false;
- monospace: true;
- top-margin: 8;
- left-margin: 8;
- bottom-margin: 8;
- right-margin: 8;
-
- styles [
- "clipboard-content-view",
- ]
- }
- }
-
- [overlay]
- Button reveal_button {
- visible: false;
- halign: end;
- valign: start;
- margin-end: 12;
- margin-top: 12;
-
- Image {
- icon-name: "view-reveal-symbolic";
- }
- }
-
- [overlay]
- Button hide_button {
- visible: false;
- halign: end;
- valign: start;
- margin-end: 12;
- margin-top: 12;
-
- styles [
- "opaque",
- ]
-
- Image {
- icon-name: "view-conceal-symbolic";
- }
- }
- };
-}
diff --git a/src/apprt/gtk/ui/1.2/ccw-paste.blp b/src/apprt/gtk/ui/1.2/ccw-paste.blp
deleted file mode 100644
index f26921803..000000000
--- a/src/apprt/gtk/ui/1.2/ccw-paste.blp
+++ /dev/null
@@ -1,71 +0,0 @@
-using Gtk 4.0;
-using Adw 1;
-translation-domain "com.mitchellh.ghostty";
-
-Adw.MessageDialog clipboard_confirmation_window {
- heading: _("Warning: Potentially Unsafe Paste");
- body: _("Pasting this text into the terminal may be dangerous as it looks like some commands may be executed.");
-
- responses [
- cancel: _("Cancel") suggested,
- ok: _("Paste") destructive,
- ]
-
- default-response: "cancel";
- close-response: "cancel";
-
- extra-child: Overlay {
- styles [
- "osd",
- ]
-
- ScrolledWindow text_view_scroll {
- width-request: 500;
- height-request: 250;
-
- TextView text_view {
- cursor-visible: false;
- editable: false;
- monospace: true;
- top-margin: 8;
- left-margin: 8;
- bottom-margin: 8;
- right-margin: 8;
-
- styles [
- "clipboard-content-view",
- ]
- }
- }
-
- [overlay]
- Button reveal_button {
- visible: false;
- halign: end;
- valign: start;
- margin-end: 12;
- margin-top: 12;
-
- Image {
- icon-name: "view-reveal-symbolic";
- }
- }
-
- [overlay]
- Button hide_button {
- visible: false;
- halign: end;
- valign: start;
- margin-end: 12;
- margin-top: 12;
-
- styles [
- "opaque",
- ]
-
- Image {
- icon-name: "view-conceal-symbolic";
- }
- }
- };
-}
diff --git a/src/apprt/gtk/ui/1.2/config-errors-dialog.blp b/src/apprt/gtk/ui/1.2/config-errors-dialog.blp
deleted file mode 100644
index b844d6347..000000000
--- a/src/apprt/gtk/ui/1.2/config-errors-dialog.blp
+++ /dev/null
@@ -1,28 +0,0 @@
-using Gtk 4.0;
-using Adw 1;
-
-Adw.MessageDialog config_errors_dialog {
- 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: TextBuffer error_message {};
- }
- };
-}
diff --git a/src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp b/src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp
deleted file mode 100644
index ad0b5c01f..000000000
--- a/src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp
+++ /dev/null
@@ -1,85 +0,0 @@
-using Gtk 4.0;
-using Adw 1;
-translation-domain "com.mitchellh.ghostty";
-
-Adw.AlertDialog clipboard_confirmation_window {
- heading: _("Authorize Clipboard Access");
- body: _("An application is attempting to read from the clipboard. The current clipboard contents are shown below.");
-
- responses [
- cancel: _("Deny") suggested,
- ok: _("Allow") destructive,
- ]
-
- default-response: "cancel";
- close-response: "cancel";
-
- extra-child: ListBox {
- selection-mode: none;
-
- styles [
- "boxed-list-separate",
- ]
-
- Overlay {
- styles [
- "osd",
- "clipboard-overlay",
- ]
-
- ScrolledWindow text_view_scroll {
- width-request: 500;
- height-request: 200;
-
- TextView text_view {
- cursor-visible: false;
- editable: false;
- monospace: true;
- top-margin: 8;
- left-margin: 8;
- bottom-margin: 8;
- right-margin: 8;
-
- styles [
- "clipboard-content-view",
- ]
- }
- }
-
- [overlay]
- Button reveal_button {
- visible: false;
- halign: end;
- valign: start;
- margin-end: 12;
- margin-top: 12;
-
- Image {
- icon-name: "view-reveal-symbolic";
- }
- }
-
- [overlay]
- Button hide_button {
- visible: false;
- halign: end;
- valign: start;
- margin-end: 12;
- margin-top: 12;
-
- styles [
- "opaque",
- ]
-
- Image {
- icon-name: "view-conceal-symbolic";
- }
- }
- }
-
- Adw.SwitchRow remember_choice {
- title: _("Remember choice for this split");
- subtitle: _("Reload configuration to show this prompt again");
- }
- };
-}
diff --git a/src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp b/src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp
deleted file mode 100644
index b71131940..000000000
--- a/src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp
+++ /dev/null
@@ -1,81 +0,0 @@
-using Gtk 4.0;
-using Adw 1;
-translation-domain "com.mitchellh.ghostty";
-
-Adw.AlertDialog clipboard_confirmation_window {
- heading: _("Authorize Clipboard Access");
- body: _("An application is attempting to write to the clipboard. The current clipboard contents are shown below.");
-
- responses [
- cancel: _("Deny") suggested,
- ok: _("Allow") destructive,
- ]
-
- default-response: "cancel";
- close-response: "cancel";
-
- extra-child: ListBox {
- selection-mode: none;
-
- styles [
- "boxed-list-separate",
- ]
-
- Overlay {
- styles [
- "osd",
- "clipboard-overlay",
- ]
-
- ScrolledWindow text_view_scroll {
- width-request: 500;
- height-request: 200;
-
- TextView text_view {
- cursor-visible: false;
- editable: false;
- monospace: true;
- top-margin: 8;
- left-margin: 8;
- bottom-margin: 8;
- right-margin: 8;
-
- styles [
- "clipboard-content-view",
- ]
- }
- }
-
- [overlay]
- Button reveal_button {
- visible: false;
- halign: end;
- valign: start;
- margin-end: 12;
- margin-top: 12;
-
- Image {
- icon-name: "view-reveal-symbolic";
- }
- }
-
- [overlay]
- Button hide_button {
- visible: false;
- halign: end;
- valign: start;
- margin-end: 12;
- margin-top: 12;
-
- styles [
- "opaque",
- ]
- }
- }
-
- Adw.SwitchRow remember_choice {
- title: _("Remember choice for this split");
- subtitle: _("Reload configuration to show this prompt again");
- }
- };
-}
diff --git a/src/apprt/gtk/ui/1.5/ccw-paste.blp b/src/apprt/gtk/ui/1.5/ccw-paste.blp
deleted file mode 100644
index a5f909526..000000000
--- a/src/apprt/gtk/ui/1.5/ccw-paste.blp
+++ /dev/null
@@ -1,71 +0,0 @@
-using Gtk 4.0;
-using Adw 1;
-translation-domain "com.mitchellh.ghostty";
-
-Adw.AlertDialog clipboard_confirmation_window {
- heading: _("Warning: Potentially Unsafe Paste");
- body: _("Pasting this text into the terminal may be dangerous as it looks like some commands may be executed.");
-
- responses [
- cancel: _("Cancel") suggested,
- ok: _("Paste") destructive,
- ]
-
- default-response: "cancel";
- close-response: "cancel";
-
- extra-child: Overlay {
- styles [
- "osd",
- ]
-
- ScrolledWindow text_view_scroll {
- width-request: 500;
- height-request: 250;
-
- TextView text_view {
- cursor-visible: false;
- editable: false;
- monospace: true;
- top-margin: 8;
- left-margin: 8;
- bottom-margin: 8;
- right-margin: 8;
-
- styles [
- "clipboard-content-view",
- ]
- }
- }
-
- [overlay]
- Button reveal_button {
- visible: false;
- halign: end;
- valign: start;
- margin-end: 12;
- margin-top: 12;
-
- Image {
- icon-name: "view-reveal-symbolic";
- }
- }
-
- [overlay]
- Button hide_button {
- visible: false;
- halign: end;
- valign: start;
- margin-end: 12;
- margin-top: 12;
-
- styles [
- "opaque",
- ]
-
- Image {
- icon-name: "view-conceal-symbolic";
- }
- }
- };
-}
diff --git a/src/apprt/gtk/ui/1.5/command-palette.blp b/src/apprt/gtk/ui/1.5/command-palette.blp
deleted file mode 100644
index a84482091..000000000
--- a/src/apprt/gtk/ui/1.5/command-palette.blp
+++ /dev/null
@@ -1,106 +0,0 @@
-using Gtk 4.0;
-using Gio 2.0;
-using Adw 1;
-
-Adw.Dialog command-palette {
- content-width: 700;
-
- Adw.ToolbarView {
- top-bar-style: flat;
-
- [top]
- Adw.HeaderBar {
- [title]
- SearchEntry search {
- hexpand: true;
- placeholder-text: _("Execute a command…");
-
- styles [
- "command-palette-search",
- ]
- }
- }
-
- ScrolledWindow {
- min-content-height: 300;
-
- ListView view {
- show-separators: true;
- single-click-activate: true;
-
- model: SingleSelection model {
- model: FilterListModel {
- incremental: true;
-
- filter: AnyFilter {
- StringFilter {
- expression: expr item as <$GhosttyCommand>.title;
- search: bind search.text;
- }
-
- StringFilter {
- expression: expr item as <$GhosttyCommand>.action-key;
- search: bind search.text;
- }
- };
-
- model: Gio.ListStore source {
- item-type: typeof<$GhosttyCommand>;
- };
- };
- };
-
- styles [
- "rich-list",
- ]
-
- factory: BuilderListItemFactory {
- template ListItem {
- child: Box {
- orientation: horizontal;
- spacing: 10;
- tooltip-text: bind template.item as <$GhosttyCommand>.description;
-
- Box {
- orientation: vertical;
- hexpand: true;
-
- Label {
- ellipsize: end;
- halign: start;
- wrap: false;
- single-line-mode: true;
-
- styles [
- "title",
- ]
-
- label: bind template.item as <$GhosttyCommand>.title;
- }
-
- Label {
- ellipsize: end;
- halign: start;
- wrap: false;
- single-line-mode: true;
-
- styles [
- "subtitle",
- "monospace",
- ]
-
- label: bind template.item as <$GhosttyCommand>.action-key;
- }
- }
-
- ShortcutLabel {
- accelerator: bind template.item as <$GhosttyCommand>.action;
- valign: center;
- }
- };
- }
- };
- }
- }
- }
-}
diff --git a/src/apprt/gtk/ui/1.5/config-errors-dialog.blp b/src/apprt/gtk/ui/1.5/config-errors-dialog.blp
deleted file mode 100644
index 793e9295a..000000000
--- a/src/apprt/gtk/ui/1.5/config-errors-dialog.blp
+++ /dev/null
@@ -1,28 +0,0 @@
-using Gtk 4.0;
-using Adw 1;
-
-Adw.AlertDialog config_errors_dialog {
- 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: TextBuffer error_message {};
- }
- };
-}
diff --git a/src/apprt/gtk/ui/1.5/prompt-title-dialog.blp b/src/apprt/gtk/ui/1.5/prompt-title-dialog.blp
deleted file mode 100644
index d23594ba4..000000000
--- a/src/apprt/gtk/ui/1.5/prompt-title-dialog.blp
+++ /dev/null
@@ -1,16 +0,0 @@
-using Gtk 4.0;
-using Adw 1;
-
-Adw.AlertDialog prompt_title_dialog {
- heading: _("Change Terminal Title");
- body: _("Leave blank to restore the default title.");
-
- responses [
- cancel: _("Cancel") suggested,
- ok: _("OK") destructive,
- ]
-
- focus-widget: title_entry;
-
- extra-child: Entry title_entry {};
-}
diff --git a/src/apprt/gtk/ui/README.md b/src/apprt/gtk/ui/README.md
deleted file mode 100644
index b9dc732b6..000000000
--- a/src/apprt/gtk/ui/README.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# GTK UI files
-
-This directory is for storing GTK blueprints. GTK blueprints are compiled into
-GTK resource builder `.ui` files by `blueprint-compiler` at build time and then
-converted into an embeddable resource by `glib-compile-resources`.
-
-Blueprint files should be stored in directories that represent the minimum
-Adwaita version needed to use that resource. Blueprint files should also be
-formatted using `blueprint-compiler format` as well to ensure consistency
-(formatting will be checked in CI).
-
-`blueprint-compiler` version 0.16.0 or newer is required to compile Blueprint
-files. If your system does not have `blueprint-compiler` or does not have a
-new enough version you can use the generated source tarballs, which contain
-precompiled versions of the blueprints.
diff --git a/src/apprt/gtk/winproto.zig b/src/apprt/gtk/winproto.zig
deleted file mode 100644
index 2dbe5a7a0..000000000
--- a/src/apprt/gtk/winproto.zig
+++ /dev/null
@@ -1,155 +0,0 @@
-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("Window.zig");
-
-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, alloc: Allocator) void {
- switch (self.*) {
- inline else => |*v| v.deinit(alloc),
- }
- }
-
- 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, alloc: Allocator) void {
- switch (self.*) {
- inline else => |*v| v.deinit(alloc),
- }
- }
-
- 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/src/apprt/gtk/winproto/noop.zig b/src/apprt/gtk/winproto/noop.zig
deleted file mode 100644
index fb732b756..000000000
--- a/src/apprt/gtk/winproto/noop.zig
+++ /dev/null
@@ -1,75 +0,0 @@
-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("../Window.zig");
-
-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, alloc: Allocator) void {
- _ = self;
- _ = alloc;
- }
-
- 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, alloc: Allocator) void {
- _ = self;
- _ = alloc;
- }
-
- 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/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig
deleted file mode 100644
index 0973499cc..000000000
--- a/src/apprt/gtk/winproto/wayland.zig
+++ /dev/null
@@ -1,511 +0,0 @@
-//! Wayland protocol implementation for the Ghostty GTK apprt.
-const std = @import("std");
-const Allocator = std.mem.Allocator;
-const build_options = @import("build_options");
-
-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 Config = @import("../../../config.zig").Config;
-const input = @import("../../../input.zig");
-const ApprtWindow = @import("../Window.zig");
-
-const wl = wayland.client.wl;
-const org = wayland.client.org;
-const xdg = wayland.client.xdg;
-
-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,
- context: *Context,
-
- const Context = struct {
- kde_blur_manager: ?*org.KdeKwinBlurManager = null,
-
- // FIXME: replace with `zxdg_decoration_v1` once GTK merges
- // https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398
- kde_decoration_manager: ?*org.KdeKwinServerDecorationManager = null,
-
- kde_slide_manager: ?*org.KdeKwinSlideManager = null,
-
- default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null,
-
- xdg_activation: ?*xdg.ActivationV1 = null,
-
- /// Whether the xdg_wm_dialog_v1 protocol is present.
- ///
- /// If it is present, gtk4-layer-shell < 1.0.4 may crash when the user
- /// creates a quick terminal, and we need to ensure this fails
- /// gracefully if this situation occurs.
- ///
- /// FIXME: This is a temporary workaround - we should remove this when
- /// all of our supported distros drop support for affected old
- /// gtk4-layer-shell versions.
- ///
- /// See https://github.com/wmww/gtk4-layer-shell/issues/50
- xdg_wm_dialog_present: bool = false,
- };
-
- 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,
- ));
-
- // Create our context for our callbacks so we have a stable pointer.
- // Note: at the time of writing this comment, we don't really need
- // a stable pointer, but it's too scary that we'd need one in the future
- // and not have it and corrupt memory or something so let's just do it.
- const context = try alloc.create(Context);
- errdefer alloc.destroy(context);
- context.* = .{};
-
- // Get our display registry so we can get all the available interfaces
- // and bind to what we need.
- const registry = try display.getRegistry();
- registry.setListener(*Context, registryListener, context);
- if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
-
- // Do another round-trip to get the default decoration mode
- if (context.kde_decoration_manager) |deco_manager| {
- deco_manager.setListener(*Context, decoManagerListener, context);
- if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
- }
-
- return .{
- .display = display,
- .context = context,
- };
- }
-
- pub fn deinit(self: *App, alloc: Allocator) void {
- alloc.destroy(self.context);
- }
-
- pub fn eventMods(
- _: *App,
- _: ?*gdk.Device,
- _: gdk.ModifierType,
- ) ?input.Mods {
- return null;
- }
-
- pub fn supportsQuickTerminal(self: App) bool {
- if (!layer_shell.isSupported()) {
- log.warn("your compositor does not support the wlr-layer-shell protocol; disabling quick terminal", .{});
- return false;
- }
-
- if (self.context.xdg_wm_dialog_present and layer_shell.getLibraryVersion().order(.{
- .major = 1,
- .minor = 0,
- .patch = 4,
- }) == .lt) {
- log.warn("the version of gtk4-layer-shell installed on your system is too old (must be 1.0.4 or newer); disabling quick terminal", .{});
- return false;
- }
-
- return true;
- }
-
- pub fn initQuickTerminal(_: *App, apprt_window: *ApprtWindow) !void {
- const window = apprt_window.window.as(gtk.Window);
-
- layer_shell.initForWindow(window);
- layer_shell.setLayer(window, .top);
- layer_shell.setNamespace(window, "ghostty-quick-terminal");
- }
-
- fn getInterfaceType(comptime field: std.builtin.Type.StructField) ?type {
- // Globals should be optional pointers
- const T = switch (@typeInfo(field.type)) {
- .optional => |o| switch (@typeInfo(o.child)) {
- .pointer => |v| v.child,
- else => return null,
- },
- else => return null,
- };
-
- // Only process Wayland interfaces
- if (!@hasDecl(T, "interface")) return null;
- return T;
- }
-
- fn registryListener(
- registry: *wl.Registry,
- event: wl.Registry.Event,
- context: *Context,
- ) void {
- const ctx_fields = @typeInfo(Context).@"struct".fields;
-
- switch (event) {
- .global => |v| {
- log.debug("found global {s}", .{v.interface});
-
- // We don't actually do anything with this other than checking
- // for its existence, so we process this separately.
- if (std.mem.orderZ(
- u8,
- v.interface,
- "xdg_wm_dialog_v1",
- ) == .eq) {
- context.xdg_wm_dialog_present = true;
- return;
- }
-
- inline for (ctx_fields) |field| {
- const T = getInterfaceType(field) orelse continue;
-
- if (std.mem.orderZ(
- u8,
- v.interface,
- T.interface.name,
- ) == .eq) {
- log.debug("matched {}", .{T});
-
- @field(context, field.name) = registry.bind(
- v.name,
- T,
- T.generated_version,
- ) catch |err| {
- log.warn(
- "error binding interface {s} error={}",
- .{ v.interface, err },
- );
- return;
- };
- }
- }
- },
-
- // 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| remove: {
- inline for (ctx_fields) |field| {
- if (getInterfaceType(field) == null) continue;
- const global = @field(context, field.name) orelse break :remove;
- if (global.getId() == v.name) {
- global.destroy();
- @field(context, field.name) = null;
- }
- }
- },
- }
- }
-
- fn decoManagerListener(
- _: *org.KdeKwinServerDecorationManager,
- event: org.KdeKwinServerDecorationManager.Event,
- context: *Context,
- ) void {
- switch (event) {
- .default_mode => |mode| {
- context.default_deco_mode = @enumFromInt(mode.mode);
- },
- }
- }
-};
-
-/// 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.
- app_context: *App.Context,
-
- /// A token that, when present, indicates that the window is blurred.
- blur_token: ?*org.KdeKwinBlur = 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,
-
- pub fn init(
- alloc: Allocator,
- app: *App,
- apprt_window: *ApprtWindow,
- ) !Window {
- _ = alloc;
-
- const gtk_native = apprt_window.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.context.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;
- };
-
- if (apprt_window.isQuickTerminal()) {
- _ = gdk.Surface.signals.enter_monitor.connect(
- gdk_surface,
- *ApprtWindow,
- enteredMonitor,
- apprt_window,
- .{},
- );
- }
-
- return .{
- .apprt_window = apprt_window,
- .surface = wl_surface,
- .app_context = app.context,
- .decoration = deco,
- };
- }
-
- pub fn deinit(self: Window, alloc: Allocator) void {
- _ = alloc;
- if (self.blur_token) |blur| blur.release();
- if (self.decoration) |deco| deco.release();
- if (self.slide) |slide| slide.release();
- }
-
- pub fn resizeEvent(_: *Window) !void {}
-
- 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 blur={}", .{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.app_context.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.app_context.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 manager = self.app_context.kde_blur_manager orelse return;
- const blur = self.apprt_window.config.background_blur;
-
- if (self.blur_token) |tok| {
- // Only release token when transitioning from blurred -> not blurred
- if (!blur.enabled()) {
- manager.unset(self.surface);
- tok.release();
- self.blur_token = null;
- }
- } else {
- // Only acquire token when transitioning from not blurred -> blurred
- if (blur.enabled()) {
- const tok = try manager.create(self.surface);
- tok.commit();
- self.blur_token = tok;
- }
- }
- }
-
- 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.config.window_decoration) {
- .auto => self.app_context.default_deco_mode orelse .Client,
- .client => .Client,
- .server => .Server,
- .none => .None,
- };
- }
-
- fn syncQuickTerminal(self: *Window) !void {
- const window = self.apprt_window.window.as(gtk.Window);
- const config = &self.apprt_window.config;
-
- 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.app_context.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.window.as(gtk.Window);
- const config = &apprt_window.config;
-
- var monitor_size: gdk.Rectangle = undefined;
- 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.app_context.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;
- },
- }
- }
-};
diff --git a/src/apprt/gtk/winproto/x11.zig b/src/apprt/gtk/winproto/x11.zig
deleted file mode 100644
index 624de03f8..000000000
--- a/src/apprt/gtk/winproto/x11.zig
+++ /dev/null
@@ -1,507 +0,0 @@
-//! X11 window protocol implementation for the Ghostty GTK apprt.
-const std = @import("std");
-const builtin = @import("builtin");
-const build_options = @import("build_options");
-const Allocator = std.mem.Allocator;
-
-const adw = @import("adw");
-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("../Window.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 are 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, alloc: Allocator) void {
- _ = self;
- _ = alloc;
- }
-
- /// 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,
- config: *const ApprtWindow.DerivedConfig,
- gtk_window: *adw.ApplicationWindow,
- x11_surface: *gdk_x11.X11Surface,
-
- blur_region: Region = .{},
-
- pub fn init(
- alloc: Allocator,
- app: *App,
- apprt_window: *ApprtWindow,
- ) !Window {
- _ = alloc;
-
- const surface = apprt_window.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,
- .config = &apprt_window.config,
- .gtk_window = apprt_window.window,
- .x11_surface = x11_surface,
- };
- }
-
- pub fn deinit(self: Window, alloc: Allocator) void {
- _ = self;
- _ = alloc;
- }
-
- pub fn resizeEvent(self: *Window) !void {
- // The blur region must update with window resizes
- try self.syncBlur();
- }
-
- 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.blur_region = blur: {
- // 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.
- var x: f64 = 0;
- var y: f64 = 0;
-
- self.gtk_window.as(gtk.Native).getSurfaceTransform(&x, &y);
-
- // Transform surface coordinates to device coordinates.
- const scale: f64 = @floatFromInt(self.gtk_window.as(gtk.Widget).getScaleFactor());
- x *= scale;
- y *= scale;
-
- break :blur .{
- .x = @intFromFloat(x),
- .y = @intFromFloat(y),
- };
- };
- self.syncBlur() catch |err| {
- log.err("failed to synchronize blur={}", .{err});
- };
- self.syncDecorations() catch |err| {
- log.err("failed to synchronize decorations={}", .{err});
- };
- }
-
- pub fn clientSideDecorationEnabled(self: Window) bool {
- return switch (self.config.window_decoration) {
- .auto, .client => true,
- .server, .none => false,
- };
- }
-
- fn syncBlur(self: *Window) !void {
- // FIXME: This doesn't currently factor in rounded corners on Adwaita,
- // which means that the blur region will grow slightly outside of the
- // window borders. Unfortunately, actually calculating the rounded
- // region can be quite complex without having access to existing APIs
- // (cf. https://github.com/cutefishos/fishui/blob/41d4ba194063a3c7fff4675619b57e6ac0504f06/src/platforms/linux/blurhelper/windowblur.cpp#L134)
- // and I think it's not really noticeable enough to justify the effort.
- // (Wayland also has this visual artifact anyway...)
-
- const gtk_widget = self.gtk_window.as(gtk.Widget);
-
- // Transform surface coordinates to device coordinates.
- const scale = self.gtk_window.as(gtk.Widget).getScaleFactor();
- self.blur_region.width = gtk_widget.getWidth() * scale;
- self.blur_region.height = gtk_widget.getHeight() * scale;
-
- const blur = self.config.background_blur;
- log.debug("set blur={}, window xid={}, region={}", .{
- blur,
- self.x11_surface.getXid(),
- self.blur_region,
- });
-
- if (blur.enabled()) {
- try self.changeProperty(
- Region,
- self.app.atoms.kde_blur,
- c.XA_CARDINAL,
- ._32,
- .{ .mode = .replace },
- &self.blur_region,
- );
- } else {
- try self.deleteProperty(self.app.atoms.kde_blur);
- }
- }
-
- 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.config.window_decoration) {
- .server => true,
- .auto, .client, .none => false,
- };
-
- try self.changeProperty(
- MotifWMHints,
- self.app.atoms.motif_wm_hints,
- self.app.atoms.motif_wm_hints,
- ._32,
- .{ .mode = .replace },
- &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,
- },
- value: *T,
- ) X11Error!void {
- const data: format.bufferType() = @ptrCast(value);
-
- const status = c.XChangeProperty(
- @ptrCast(@alignCast(self.app.display)),
- self.x11_surface.getXid(),
- name,
- typ,
- @intFromEnum(format),
- @intFromEnum(options.mode),
- data,
- @divExact(@sizeOf(T), @sizeOf(format.elemType())),
- );
-
- // 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;
- }
-};
-
-const Region = extern struct {
- x: c_long = 0,
- y: c_long = 0,
- width: c_long = 0,
- height: c_long = 0,
-};
-
-// 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/src/apprt/structs.zig b/src/apprt/structs.zig
index b9e93e9ba..363b1f63a 100644
--- a/src/apprt/structs.zig
+++ b/src/apprt/structs.zig
@@ -39,13 +39,13 @@ pub const Clipboard = enum(Backing) {
// 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, .@"gtk-ng" => c_int,
+ .@"gtk-ng" => 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, .@"gtk-ng" => @import("gobject").ext.defineEnum(
+ .@"gtk-ng" => @import("gobject").ext.defineEnum(
Clipboard,
.{ .name = "GhosttyApprtClipboard" },
),
@@ -74,7 +74,7 @@ pub const ClipboardRequest = union(ClipboardRequestType) {
/// Make this a valid gobject if we're in a GTK environment.
pub const getGObjectType = switch (build_config.app_runtime) {
- .gtk, .@"gtk-ng" => @import("gobject").ext.defineBoxed(
+ .@"gtk-ng" => @import("gobject").ext.defineBoxed(
ClipboardRequest,
.{ .name = "GhosttyClipboardRequest" },
),
diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig
index f76e3d05a..e571fc9f8 100644
--- a/src/apprt/surface.zig
+++ b/src/apprt/surface.zig
@@ -111,7 +111,6 @@ pub const Message = union(enum) {
/// Make this a valid gobject if we're in a GTK environment.
pub const getGObjectType = switch (build_config.app_runtime) {
- .gtk,
.@"gtk-ng",
=> @import("gobject").ext.defineBoxed(
ChildExited,
diff --git a/src/build/GhosttyDist.zig b/src/build/GhosttyDist.zig
index 6123582b7..d889f2350 100644
--- a/src/build/GhosttyDist.zig
+++ b/src/build/GhosttyDist.zig
@@ -21,11 +21,6 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist {
const alloc = b.allocator;
var resources: std.ArrayListUnmanaged(Resource) = .empty;
{
- const gtk = SharedDeps.gtkDistResources(b);
- try resources.append(alloc, gtk.resources_c);
- try resources.append(alloc, gtk.resources_h);
- }
- {
const gtk = SharedDeps.gtkNgDistResources(b);
try resources.append(alloc, gtk.resources_c);
try resources.append(alloc, gtk.resources_h);
diff --git a/src/build/GhosttyI18n.zig b/src/build/GhosttyI18n.zig
index 9dcc67a31..72a553603 100644
--- a/src/build/GhosttyI18n.zig
+++ b/src/build/GhosttyI18n.zig
@@ -3,7 +3,7 @@ const GhosttyI18n = @This();
const std = @import("std");
const builtin = @import("builtin");
const Config = @import("Config.zig");
-const gresource = @import("../apprt/gtk/gresource.zig");
+const gresource = @import("../apprt/gtk-ng/build/gresource.zig");
const internal_os = @import("../os/main.zig");
const domain = "com.mitchellh.ghostty";
@@ -78,9 +78,9 @@ fn createUpdateStep(b: *std.Build) !*std.Build.Step {
// Not cacheable due to the gresource files
xgettext.has_side_effects = true;
- inline for (gresource.blueprint_files) |blp| {
+ inline for (gresource.blueprints) |blp| {
const path = std.fmt.comptimePrint(
- "src/apprt/gtk/ui/{[major]}.{[minor]}/{[name]s}.blp",
+ "src/apprt/gtk-ng/ui/{[major]}.{[minor]}/{[name]s}.blp",
blp,
);
// The arguments to xgettext must be the relative path in the build root
@@ -105,7 +105,7 @@ fn createUpdateStep(b: *std.Build) !*std.Build.Step {
}
var gtk_dir = try b.build_root.handle.openDir(
- "src/apprt/gtk",
+ "src/apprt/gtk-ng",
.{ .iterate = true },
);
defer gtk_dir.close();
@@ -138,7 +138,7 @@ fn createUpdateStep(b: *std.Build) !*std.Build.Step {
);
for (gtk_files.items) |item| {
- const path = b.pathJoin(&.{ "src/apprt/gtk", item });
+ const path = b.pathJoin(&.{ "src/apprt/gtk-ng", item });
// The arguments to xgettext must be the relative path in the build root
// or the resulting files will contain the absolute path. This will
// cause a lot of churn because not everyone has the Ghostty code
diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig
index c03746a48..86390a496 100644
--- a/src/build/SharedDeps.zig
+++ b/src/build/SharedDeps.zig
@@ -550,7 +550,6 @@ pub fn add(
switch (self.config.app_runtime) {
.none => {},
- .gtk => try self.addGTK(step),
.@"gtk-ng" => try self.addGtkNg(step),
}
}
@@ -789,234 +788,6 @@ pub fn gtkNgDistResources(
};
}
-/// Setup the dependencies for the GTK apprt build. The GTK apprt
-/// is particularly involved compared to others so we pull this out
-/// into a dedicated function.
-fn addGTK(
- self: *const SharedDeps,
- step: *std.Build.Step.Compile,
-) !void {
- const b = step.step.owner;
- const target = step.root_module.resolved_target.?;
- const optimize = step.root_module.optimize.?;
-
- const gobject_ = b.lazyDependency("gobject", .{
- .target = target,
- .optimize = optimize,
- });
- if (gobject_) |gobject| {
- const gobject_imports = .{
- .{ "adw", "adw1" },
- .{ "gdk", "gdk4" },
- .{ "gio", "gio2" },
- .{ "glib", "glib2" },
- .{ "gobject", "gobject2" },
- .{ "gtk", "gtk4" },
- .{ "xlib", "xlib2" },
- };
- inline for (gobject_imports) |import| {
- const name, const module = import;
- step.root_module.addImport(name, gobject.module(module));
- }
- }
-
- step.linkSystemLibrary2("gtk4", dynamic_link_opts);
- step.linkSystemLibrary2("libadwaita-1", dynamic_link_opts);
-
- if (self.config.x11) {
- step.linkSystemLibrary2("X11", dynamic_link_opts);
- if (gobject_) |gobject| {
- step.root_module.addImport(
- "gdk_x11",
- gobject.module("gdkx114"),
- );
- }
- }
-
- if (self.config.wayland) wayland: {
- // These need to be all be called to note that we need them.
- const wayland_dep_ = b.lazyDependency("wayland", .{});
- const wayland_protocols_dep_ = b.lazyDependency(
- "wayland_protocols",
- .{},
- );
- const plasma_wayland_protocols_dep_ = b.lazyDependency(
- "plasma_wayland_protocols",
- .{},
- );
-
- // Unwrap or return, there are no more dependencies below.
- const wayland_dep = wayland_dep_ orelse break :wayland;
- const wayland_protocols_dep = wayland_protocols_dep_ orelse break :wayland;
- const plasma_wayland_protocols_dep = plasma_wayland_protocols_dep_ orelse break :wayland;
-
- // Note that zig_wayland cannot be lazy because lazy dependencies
- // can't be imported since they don't exist and imports are
- // resolved at compile time of the build.
- const zig_wayland_dep = b.dependency("zig_wayland", .{});
- const Scanner = @import("zig_wayland").Scanner;
- const scanner = Scanner.create(zig_wayland_dep.builder, .{
- .wayland_xml = wayland_dep.path("protocol/wayland.xml"),
- .wayland_protocols = wayland_protocols_dep.path(""),
- });
-
- scanner.addCustomProtocol(
- plasma_wayland_protocols_dep.path("src/protocols/blur.xml"),
- );
- // FIXME: replace with `zxdg_decoration_v1` once GTK merges https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398
- scanner.addCustomProtocol(
- plasma_wayland_protocols_dep.path("src/protocols/server-decoration.xml"),
- );
- scanner.addCustomProtocol(
- plasma_wayland_protocols_dep.path("src/protocols/slide.xml"),
- );
- scanner.addSystemProtocol("staging/xdg-activation/xdg-activation-v1.xml");
-
- scanner.generate("wl_compositor", 1);
- scanner.generate("org_kde_kwin_blur_manager", 1);
- scanner.generate("org_kde_kwin_server_decoration_manager", 1);
- scanner.generate("org_kde_kwin_slide_manager", 1);
- scanner.generate("xdg_activation_v1", 1);
-
- step.root_module.addImport("wayland", b.createModule(.{
- .root_source_file = scanner.result,
- }));
- if (gobject_) |gobject| step.root_module.addImport(
- "gdk_wayland",
- gobject.module("gdkwayland4"),
- );
-
- if (b.lazyDependency("gtk4_layer_shell", .{
- .target = target,
- .optimize = optimize,
- })) |gtk4_layer_shell| {
- const layer_shell_module = gtk4_layer_shell.module("gtk4-layer-shell");
- if (gobject_) |gobject| layer_shell_module.addImport(
- "gtk",
- gobject.module("gtk4"),
- );
- step.root_module.addImport(
- "gtk4-layer-shell",
- layer_shell_module,
- );
-
- // IMPORTANT: gtk4-layer-shell must be linked BEFORE
- // wayland-client, as it relies on shimming libwayland's APIs.
- if (b.systemIntegrationOption("gtk4-layer-shell", .{})) {
- step.linkSystemLibrary2("gtk4-layer-shell-0", dynamic_link_opts);
- } else {
- // gtk4-layer-shell *must* be dynamically linked,
- // so we don't add it as a static library
- const shared_lib = gtk4_layer_shell.artifact("gtk4-layer-shell");
- b.installArtifact(shared_lib);
- step.linkLibrary(shared_lib);
- }
- }
-
- step.linkSystemLibrary2("wayland-client", dynamic_link_opts);
- }
-
- {
- // Get our gresource c/h files and add them to our build.
- const dist = gtkDistResources(b);
- step.addCSourceFile(.{ .file = dist.resources_c.path(b), .flags = &.{} });
- step.addIncludePath(dist.resources_h.path(b).dirname());
- }
-}
-
-/// Creates the resources that can be prebuilt for our dist build.
-pub fn gtkDistResources(
- b: *std.Build,
-) struct {
- resources_c: DistResource,
- resources_h: DistResource,
-} {
- const gresource = @import("../apprt/gtk/gresource.zig");
-
- const gresource_xml = gresource_xml: {
- const xml_exe = b.addExecutable(.{
- .name = "generate_gresource_xml",
- .root_source_file = b.path("src/apprt/gtk/gresource.zig"),
- .target = b.graph.host,
- });
- const xml_run = b.addRunArtifact(xml_exe);
-
- const blueprint_exe = b.addExecutable(.{
- .name = "gtk_blueprint_compiler",
- .root_source_file = b.path("src/apprt/gtk/blueprint_compiler.zig"),
- .target = b.graph.host,
- });
- blueprint_exe.linkLibC();
- blueprint_exe.linkSystemLibrary2("gtk4", dynamic_link_opts);
- blueprint_exe.linkSystemLibrary2("libadwaita-1", dynamic_link_opts);
-
- for (gresource.blueprint_files) |blueprint_file| {
- const blueprint_run = b.addRunArtifact(blueprint_exe);
- blueprint_run.addArgs(&.{
- b.fmt("{d}", .{blueprint_file.major}),
- b.fmt("{d}", .{blueprint_file.minor}),
- });
- const ui_file = blueprint_run.addOutputFileArg(b.fmt(
- "{d}.{d}/{s}.ui",
- .{
- blueprint_file.major,
- blueprint_file.minor,
- blueprint_file.name,
- },
- ));
- blueprint_run.addFileArg(b.path(b.fmt(
- "src/apprt/gtk/ui/{d}.{d}/{s}.blp",
- .{
- blueprint_file.major,
- blueprint_file.minor,
- blueprint_file.name,
- },
- )));
-
- xml_run.addFileArg(ui_file);
- }
-
- break :gresource_xml xml_run.captureStdOut();
- };
-
- const generate_c = b.addSystemCommand(&.{
- "glib-compile-resources",
- "--c-name",
- "ghostty",
- "--generate-source",
- "--target",
- });
- const resources_c = generate_c.addOutputFileArg("ghostty_resources.c");
- generate_c.addFileArg(gresource_xml);
- for (gresource.dependencies) |file| {
- generate_c.addFileInput(b.path(file));
- }
-
- const generate_h = b.addSystemCommand(&.{
- "glib-compile-resources",
- "--c-name",
- "ghostty",
- "--generate-header",
- "--target",
- });
- const resources_h = generate_h.addOutputFileArg("ghostty_resources.h");
- generate_h.addFileArg(gresource_xml);
- for (gresource.dependencies) |file| {
- generate_h.addFileInput(b.path(file));
- }
-
- return .{
- .resources_c = .{
- .dist = "src/apprt/gtk/ghostty_resources.c",
- .generated = resources_c,
- },
- .resources_h = .{
- .dist = "src/apprt/gtk/ghostty_resources.h",
- .generated = resources_h,
- },
- };
-}
-
// For dynamic linking, we prefer dynamic linking and to search by
// mode first. Mode first will search all paths for a dynamic library
// before falling back to static.
diff --git a/src/cli/version.zig b/src/cli/version.zig
index 22608fa88..2dd208180 100644
--- a/src/cli/version.zig
+++ b/src/cli/version.zig
@@ -7,8 +7,8 @@ const internal_os = @import("../os/main.zig");
const xev = @import("../global.zig").xev;
const renderer = @import("../renderer.zig");
-const gtk_version = @import("../apprt/gtk/gtk_version.zig");
-const adw_version = @import("../apprt/gtk/adw_version.zig");
+const gtk_version = @import("../apprt/gtk-ng/gtk_version.zig");
+const adw_version = @import("../apprt/gtk-ng/adw_version.zig");
pub const Options = struct {};
@@ -38,7 +38,7 @@ pub fn run(alloc: Allocator) !u8 {
try stdout.print(" - font engine : {}\n", .{build_config.font_backend});
try stdout.print(" - renderer : {}\n", .{renderer.Renderer});
try stdout.print(" - libxev : {s}\n", .{@tagName(xev.backend)});
- if (comptime build_config.app_runtime == .gtk) {
+ if (comptime build_config.app_runtime == .@"gtk-ng") {
if (comptime builtin.os.tag == .linux) {
const kernel_info = internal_os.getKernelInfo(alloc);
defer if (kernel_info) |k| alloc.free(k);
diff --git a/src/config/Config.zig b/src/config/Config.zig
index 1ec0bafce..cea2c1a28 100644
--- a/src/config/Config.zig
+++ b/src/config/Config.zig
@@ -7149,7 +7149,7 @@ pub const GtkTitlebarStyle = enum(c_int) {
tabs,
pub const getGObjectType = switch (build_config.app_runtime) {
- .gtk, .@"gtk-ng" => @import("gobject").ext.defineEnum(
+ .@"gtk-ng" => @import("gobject").ext.defineEnum(
GtkTitlebarStyle,
.{ .name = "GhosttyGtkTitlebarStyle" },
),
@@ -7717,7 +7717,7 @@ pub const WindowDecoration = enum(c_int) {
/// Make this a valid gobject if we're in a GTK environment.
pub const getGObjectType = switch (build_config.app_runtime) {
- .gtk, .@"gtk-ng" => @import("gobject").ext.defineEnum(
+ .@"gtk-ng" => @import("gobject").ext.defineEnum(
WindowDecoration,
.{ .name = "GhosttyConfigWindowDecoration" },
),
diff --git a/src/datastruct/split_tree.zig b/src/datastruct/split_tree.zig
index 57da22109..5cb959af4 100644
--- a/src/datastruct/split_tree.zig
+++ b/src/datastruct/split_tree.zig
@@ -1266,7 +1266,7 @@ pub fn SplitTree(comptime V: type) type {
/// Make this a valid gobject if we're in a GTK environment.
pub const getGObjectType = switch (build_config.app_runtime) {
- .gtk, .@"gtk-ng" => @import("gobject").ext.defineBoxed(
+ .@"gtk-ng" => @import("gobject").ext.defineBoxed(
Self,
.{
// To get the type name we get the non-qualified type name
diff --git a/src/font/face.zig b/src/font/face.zig
index 2902f97ae..054f542fc 100644
--- a/src/font/face.zig
+++ b/src/font/face.zig
@@ -59,7 +59,7 @@ pub const DesiredSize = struct {
/// Make this a valid gobject if we're in a GTK environment.
pub const getGObjectType = switch (build_config.app_runtime) {
- .gtk, .@"gtk-ng" => @import("gobject").ext.defineBoxed(
+ .@"gtk-ng" => @import("gobject").ext.defineBoxed(
DesiredSize,
.{ .name = "GhosttyFontDesiredSize" },
),
diff --git a/src/input/Binding.zig b/src/input/Binding.zig
index d475db539..77d93e4aa 100644
--- a/src/input/Binding.zig
+++ b/src/input/Binding.zig
@@ -744,7 +744,7 @@ pub const Action = union(enum) {
/// Make this a valid gobject if we're in a GTK environment.
pub const getGObjectType = switch (build_config.app_runtime) {
- .gtk, .@"gtk-ng" => @import("gobject").ext.defineBoxed(
+ .@"gtk-ng" => @import("gobject").ext.defineBoxed(
Action,
.{ .name = "GhosttyBindingAction" },
),
diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig
index 908e1c828..8a1c465e9 100644
--- a/src/renderer/OpenGL.zig
+++ b/src/renderer/OpenGL.zig
@@ -165,7 +165,6 @@ pub fn surfaceInit(surface: *apprt.Surface) !void {
else => @compileError("unsupported app runtime for OpenGL"),
// GTK uses global OpenGL context so we load from null.
- apprt.gtk,
apprt.gtk_ng,
=> try prepareContext(null),
@@ -201,7 +200,7 @@ pub fn threadEnter(self: *const OpenGL, surface: *apprt.Surface) !void {
switch (apprt.runtime) {
else => @compileError("unsupported app runtime for OpenGL"),
- apprt.gtk, apprt.gtk_ng => {
+ apprt.gtk_ng => {
// GTK doesn't support threaded OpenGL operations as far as I can
// tell, so we use the renderer thread to setup all the state
// but then do the actual draws and texture syncs and all that
@@ -223,7 +222,7 @@ pub fn threadExit(self: *const OpenGL) void {
switch (apprt.runtime) {
else => @compileError("unsupported app runtime for OpenGL"),
- apprt.gtk, apprt.gtk_ng => {
+ apprt.gtk_ng => {
// We don't need to do any unloading for GTK because we may
// be sharing the global bindings with other windows.
},
@@ -238,7 +237,7 @@ pub fn displayRealized(self: *const OpenGL) void {
_ = self;
switch (apprt.runtime) {
- apprt.gtk, apprt.gtk_ng => prepareContext(null) catch |err| {
+ apprt.gtk_ng => prepareContext(null) catch |err| {
log.warn(
"Error preparing GL context in displayRealized, err={}",
.{err},
diff --git a/src/terminal/mouse_shape.zig b/src/terminal/mouse_shape.zig
index e71d4fb3b..16434f3f6 100644
--- a/src/terminal/mouse_shape.zig
+++ b/src/terminal/mouse_shape.zig
@@ -49,7 +49,7 @@ pub const MouseShape = enum(c_int) {
/// Make this a valid gobject if we're in a GTK environment.
pub const getGObjectType = switch (build_config.app_runtime) {
- .gtk, .@"gtk-ng" => @import("gobject").ext.defineEnum(
+ .@"gtk-ng" => @import("gobject").ext.defineEnum(
MouseShape,
.{ .name = "GhosttyMouseShape" },
),