From a85651fe4f11375257ca7b4c33c44cca06a1353e Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Fri, 28 Feb 2025 11:33:08 +0100 Subject: [PATCH] gtk: implement quick terminal Using `gtk4-layer-shell` still seems like the path of least resistance, and to my delight it pretty much Just Works. Hurrah! This implementation could do with some further polish (e.g. animations, which can be implemented via libadwaita's animations API, and global shortcuts), but as a MVP it works well enough. It even supports tabs! Fixes #4624. --- nix/devShell.nix | 3 ++ nix/package.nix | 2 + snap/snapcraft.yaml | 2 + src/apprt/gtk/App.zig | 36 +++++++++++++++- src/apprt/gtk/TabView.zig | 10 ++--- src/apprt/gtk/Window.zig | 46 +++++++++++++++----- src/apprt/gtk/c.zig | 1 + src/apprt/gtk/winproto.zig | 23 ++++++++++ src/apprt/gtk/winproto/noop.zig | 7 ++++ src/apprt/gtk/winproto/wayland.zig | 67 +++++++++++++++++++++++++++--- src/apprt/gtk/winproto/x11.zig | 8 ++++ src/build/Config.zig | 9 +++- src/build/SharedDeps.zig | 10 ++--- src/build/docker/debian/Dockerfile | 2 + src/build/gtk.zig | 21 +++++++++- 15 files changed, 216 insertions(+), 31 deletions(-) diff --git a/nix/devShell.nix b/nix/devShell.nix index 66f259656..0a084f445 100644 --- a/nix/devShell.nix +++ b/nix/devShell.nix @@ -31,6 +31,7 @@ glib, glslang, gtk4, + gtk4-layer-shell, gobject-introspection, libadwaita, blueprint-compiler, @@ -88,6 +89,7 @@ libadwaita gtk4 + gtk4-layer-shell glib gobject-introspection wayland @@ -167,6 +169,7 @@ in blueprint-compiler libadwaita gtk4 + gtk4-layer-shell glib gobject-introspection wayland diff --git a/nix/package.nix b/nix/package.nix index 832dfdb84..84744c6e0 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -13,6 +13,7 @@ libGL, glib, gtk4, + gtk4-layer-shell, gobject-introspection, libadwaita, blueprint-compiler, @@ -118,6 +119,7 @@ in libXrandr ] ++ lib.optionals enableWayland [ + gtk4-layer-shell wayland ]; diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 49d452ef9..8ccb7a009 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -73,6 +73,8 @@ parts: - blueprint-compiler - libgtk-4-dev - libadwaita-1-dev + # TODO: Add when the Snap is updated to Ubuntu 24.10+ + # - gtk4-layer-shell - libxml2-utils - git - patchelf diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index e8e43cb02..0bcb5aad3 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -70,6 +70,10 @@ config_errors_window: ?*ConfigErrorsWindow = null, /// The clipboard confirmation window, if it is currently open. clipboard_confirmation_window: ?*ClipboardConfirmationWindow = 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, @@ -497,10 +501,10 @@ pub fn performAction( .toggle_window_decorations => self.toggleWindowDecorations(target), .quit_timer => self.quitTimer(value), .prompt_title => try self.promptTitle(target), + .toggle_quick_terminal => return try self.toggleQuickTerminal(), // Unimplemented .close_all_windows, - .toggle_quick_terminal, .toggle_visibility, .cell_size, .secure_input, @@ -764,6 +768,33 @@ fn toggleWindowDecorations( } } +fn toggleQuickTerminal(self: *App) !bool { + if (self.quick_terminal) |qt| { + qt.toggleVisibility(); + return true; + } + + if (!self.winproto.supportsQuickTerminal()) { + log.err("quick terminal not supported on current platform", .{}); + 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 quitTimer(self: *App, mode: apprt.action.QuitTimer) void { switch (mode) { .start => self.startQuitTimer(), @@ -1372,6 +1403,9 @@ fn newWindow(self: *App, parent_: ?*CoreSurface) !void { // Add our initial tab try window.newTab(parent_); + + // Show the new window + window.present(); } fn quit(self: *App) void { diff --git a/src/apprt/gtk/TabView.zig b/src/apprt/gtk/TabView.zig index efef3d621..defdedace 100644 --- a/src/apprt/gtk/TabView.zig +++ b/src/apprt/gtk/TabView.zig @@ -193,10 +193,6 @@ pub fn addTab(self: *TabView, tab: *Tab, title: [:0]const u8) void { } pub fn closeTab(self: *TabView, tab: *Tab) void { - // Save a pointer to the GTK window in case we need it later. It may be - // impossible to access later due to how resources are cleaned up. - const window: *gtk.Window = @ptrCast(@alignCast(self.window.window)); - // closeTab always expects to close unconditionally so we mark this // as true so that the close_page call below doesn't request // confirmation. @@ -225,7 +221,7 @@ pub fn closeTab(self: *TabView, tab: *Tab) void { box.as(gobject.Object).unref(); } - window.destroy(); + self.window.close(); } } @@ -234,7 +230,9 @@ pub fn createWindow(currentWindow: *Window) !*Window { const app = currentWindow.app; // Create a new window - return Window.create(alloc, app); + const window = try Window.create(alloc, app); + window.present(); + return window; } fn adwPageAttached(_: *adw.TabView, page: *adw.TabPage, _: c_int, self: *TabView) callconv(.C) void { diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 2fe76fa6d..862927fe9 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -79,6 +79,8 @@ pub const DerivedConfig = struct { gtk_wide_tabs: bool, gtk_toolbar_style: configpkg.Config.GtkToolbarStyle, + quick_terminal_position: configpkg.Config.QuickTerminalPosition, + maximize: bool, fullscreen: bool, window_decoration: configpkg.Config.WindowDecoration, @@ -94,6 +96,8 @@ pub const DerivedConfig = struct { .gtk_wide_tabs = config.@"gtk-wide-tabs", .gtk_toolbar_style = config.@"gtk-toolbar-style", + .quick_terminal_position = config.@"quick-terminal-position", + .maximize = config.maximize, .fullscreen = config.fullscreen, .window_decoration = config.@"window-decoration", @@ -364,9 +368,16 @@ pub fn init(self: *Window, app: *App) !void { // If we are in fullscreen mode, new windows start fullscreen. if (self.config.fullscreen) c.gtk_window_fullscreen(self.window); +} - // Show the window - c.gtk_widget_show(gtk_widget); +pub fn present(self: *Window) void { + const window: *gtk.Window = @ptrCast(self.window); + window.present(); +} + +pub fn toggleVisibility(self: *Window) void { + const window: *gtk.Widget = @ptrCast(self.window); + window.setVisible(@intFromBool(window.isVisible() == 0)); } pub fn updateConfig( @@ -408,6 +419,9 @@ pub fn syncAppearance(self: *Window) !void { // 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.app.quick_terminal == self) break :visible false; + // Unconditionally disable the header bar when fullscreened. if (self.config.fullscreen) break :visible false; @@ -458,11 +472,11 @@ pub fn syncAppearance(self: *Window) !void { log.warn("failed to sync winproto appearance error={}", .{err}); }; - toggleCssClass( - @ptrCast(self.window), - "background", - self.config.background_opacity >= 1, - ); + if (self.app.quick_terminal == self) { + self.winproto.syncQuickTerminal() catch |err| { + log.warn("failed to sync quick terminal appearance error={}", .{err}); + }; + } } fn toggleCssClass( @@ -780,11 +794,23 @@ fn adwTabOverviewFocusTimer( return 0; } +pub fn close(self: *Window) void { + const window: *gtk.Window = @ptrCast(self.window); + + // Unset the quick terminal on the app level + if (self.app.quick_terminal == self) self.app.quick_terminal = null; + + window.destroy(); +} + fn gtkCloseRequest(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool { _ = v; log.debug("window close request", .{}); const self = userdataSelf(ud.?); + // This path should never occur, but this is here as a safety measure. + if (self.app.quick_terminal == self) return true; + // If none of our surfaces need confirmation, we can just exit. for (self.app.core_app.surfaces.items) |surface| { if (surface.container.window()) |window| { @@ -792,7 +818,7 @@ fn gtkCloseRequest(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool { surface.core_surface.needsConfirmQuit()) break; } } else { - c.gtk_window_destroy(self.window); + self.close(); return true; } @@ -836,7 +862,7 @@ fn gtkCloseConfirmation( c.gtk_window_destroy(@ptrCast(alert)); if (response == c.GTK_RESPONSE_YES) { const self = userdataSelf(ud.?); - c.gtk_window_destroy(self.window); + self.close(); } } @@ -934,7 +960,7 @@ fn gtkActionClose( _: ?*glib.Variant, self: *Window, ) callconv(.C) void { - c.gtk_window_destroy(self.window); + self.close(); } fn gtkActionNewWindow( diff --git a/src/apprt/gtk/c.zig b/src/apprt/gtk/c.zig index c42c35d46..6ad064d12 100644 --- a/src/apprt/gtk/c.zig +++ b/src/apprt/gtk/c.zig @@ -15,6 +15,7 @@ pub const c = @cImport({ @cInclude("X11/XKBlib.h"); } if (build_options.wayland) { + if (build_options.layer_shell) @cInclude("gtk4-layer-shell/gtk4-layer-shell.h"); @cInclude("gdk/wayland/gdkwayland.h"); } diff --git a/src/apprt/gtk/winproto.zig b/src/apprt/gtk/winproto.zig index 81340cf26..e99a5fb2b 100644 --- a/src/apprt/gtk/winproto.zig +++ b/src/apprt/gtk/winproto.zig @@ -59,6 +59,23 @@ pub const App = union(Protocol) { 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. @@ -116,6 +133,12 @@ pub const Window = union(Protocol) { } } + pub fn syncQuickTerminal(self: *Window) !void { + switch (self.*) { + inline else => |*v| try v.syncQuickTerminal(), + } + } + pub fn clientSideDecorationEnabled(self: Window) bool { return switch (self) { inline else => |v| v.clientSideDecorationEnabled(), diff --git a/src/apprt/gtk/winproto/noop.zig b/src/apprt/gtk/winproto/noop.zig index 82cab206e..d030d884e 100644 --- a/src/apprt/gtk/winproto/noop.zig +++ b/src/apprt/gtk/winproto/noop.zig @@ -29,6 +29,11 @@ pub const App = struct { ) ?input.Mods { return null; } + + pub fn supportsQuickTerminal(_: App) bool { + return false; + } + pub fn initQuickTerminal(_: *App, _: *ApprtWindow) !void {} }; pub const Window = struct { @@ -54,6 +59,8 @@ pub const Window = struct { pub fn syncAppearance(_: *Window) !void {} + pub fn syncQuickTerminal(_: *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. diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index fa2bb3b0b..f9ea7c7de 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -1,7 +1,10 @@ //! Wayland protocol implementation for the Ghostty GTK apprt. const std = @import("std"); -const wayland = @import("wayland"); const Allocator = std.mem.Allocator; + +const build_options = @import("build_options"); +const wayland = @import("wayland"); + const c = @import("../c.zig").c; const Config = @import("../../../config.zig").Config; const input = @import("../../../input.zig"); @@ -84,6 +87,20 @@ pub const App = struct { return null; } + pub fn supportsQuickTerminal(_: App) bool { + if (comptime !build_options.layer_shell) return false; + + return c.gtk_layer_is_supported() != 0; + } + + pub fn initQuickTerminal(_: *App, apprt_window: *ApprtWindow) !void { + if (comptime !build_options.layer_shell) unreachable; + + c.gtk_layer_init_for_window(apprt_window.window); + c.gtk_layer_set_layer(apprt_window.window, c.GTK_LAYER_SHELL_LAYER_TOP); + c.gtk_layer_set_keyboard_mode(apprt_window.window, c.GTK_LAYER_SHELL_KEYBOARD_MODE_ON_DEMAND); + } + fn registryListener( registry: *wl.Registry, event: wl.Registry.Event, @@ -156,7 +173,7 @@ pub const App = struct { /// Per-window (wl_surface) state for the Wayland protocol. pub const Window = struct { - config: *const ApprtWindow.DerivedConfig, + apprt_window: *ApprtWindow, /// The Wayland surface for this window. surface: *wl.Surface, @@ -210,7 +227,7 @@ pub const Window = struct { }; return .{ - .config = &apprt_window.config, + .apprt_window = apprt_window, .surface = wl_surface, .app_context = app.context, .blur_token = null, @@ -255,7 +272,7 @@ pub const Window = struct { /// 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.config.background_blur; + const blur = self.apprt_window.config.background_blur; if (self.blur_token) |tok| { // Only release token when transitioning from blurred -> not blurred @@ -283,11 +300,51 @@ pub const Window = struct { } fn getDecorationMode(self: Window) org.KdeKwinServerDecorationManager.Mode { - return switch (self.config.window_decoration) { + return switch (self.apprt_window.config.window_decoration) { .auto => self.app_context.default_deco_mode orelse .Client, .client => .Client, .server => .Server, .none => .None, }; } + + pub fn syncQuickTerminal(self: *Window) !void { + if (comptime !build_options.layer_shell) return; + + const window = self.apprt_window.window; + + const anchored_edge: ?LayerShellEdge = switch (self.apprt_window.config.quick_terminal_position) { + .left => .left, + .right => .right, + .top => .top, + .bottom => .bottom, + .center => null, + }; + + for (std.meta.tags(LayerShellEdge)) |edge| { + if (anchored_edge) |anchored| { + if (edge == anchored) { + c.gtk_layer_set_margin(window, @intFromEnum(edge), 0); + c.gtk_layer_set_anchor(window, @intFromEnum(edge), @intFromBool(true)); + continue; + } + } + + // Arbitrary margin - could be made customizable? + c.gtk_layer_set_margin(window, @intFromEnum(edge), 20); + c.gtk_layer_set_anchor(window, @intFromEnum(edge), @intFromBool(false)); + } + + switch (self.apprt_window.config.quick_terminal_position) { + .top, .bottom, .center => c.gtk_window_set_default_size(window, 800, 400), + .left, .right => c.gtk_window_set_default_size(window, 400, 800), + } + } +}; + +const LayerShellEdge = enum(c_uint) { + left = c.GTK_LAYER_SHELL_EDGE_LEFT, + right = c.GTK_LAYER_SHELL_EDGE_RIGHT, + top = c.GTK_LAYER_SHELL_EDGE_TOP, + bottom = c.GTK_LAYER_SHELL_EDGE_BOTTOM, }; diff --git a/src/apprt/gtk/winproto/x11.zig b/src/apprt/gtk/winproto/x11.zig index c7c47b65d..6d20e6e8b 100644 --- a/src/apprt/gtk/winproto/x11.zig +++ b/src/apprt/gtk/winproto/x11.zig @@ -148,6 +148,12 @@ pub const App = struct { return mods; } + + pub fn supportsQuickTerminal(_: App) bool { + return false; + } + + pub fn initQuickTerminal(_: *App, _: *ApprtWindow) !void {} }; pub const Window = struct { @@ -222,6 +228,8 @@ pub const Window = struct { }; } + pub fn syncQuickTerminal(_: *Window) !void {} + pub fn clientSideDecorationEnabled(self: Window) bool { return switch (self.config.window_decoration) { .auto, .client => true, diff --git a/src/build/Config.zig b/src/build/Config.zig index f7bf96d36..00f040622 100644 --- a/src/build/Config.zig +++ b/src/build/Config.zig @@ -34,6 +34,7 @@ font_backend: font.Backend = .freetype, /// Feature flags x11: bool = false, wayland: bool = false, +layer_shell: bool = false, sentry: bool = true, wasm_shared: bool = true, @@ -109,7 +110,6 @@ pub fn init(b: *std.Build) !Config { //--------------------------------------------------------------- // Comptime Interfaces - config.font_backend = b.option( font.Backend, "font-backend", @@ -163,6 +163,12 @@ pub fn init(b: *std.Build) !Config { "Enables linking against X11 libraries when using the GTK rendering backend.", ) orelse gtk_targets.x11; + config.layer_shell = b.option( + bool, + "gtk-layer-shell", + "Enables linking against the gtk4-layer-shell library for quick terminal support. Requires Wayland.", + ) orelse gtk_targets.layer_shell; + //--------------------------------------------------------------- // Ghostty Exe Properties @@ -392,6 +398,7 @@ pub fn addOptions(self: *const Config, step: *std.Build.Step.Options) !void { step.addOption(bool, "flatpak", self.flatpak); step.addOption(bool, "x11", self.x11); step.addOption(bool, "wayland", self.wayland); + step.addOption(bool, "layer_shell", self.layer_shell); step.addOption(bool, "sentry", self.sentry); step.addOption(apprt.Runtime, "app_runtime", self.app_runtime); step.addOption(font.Backend, "font_backend", self.font_backend); diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 38d4787d6..26c4d84c5 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -460,12 +460,8 @@ pub fn add( if (self.config.wayland) { const scanner = Scanner.create(b.dependency("zig_wayland", .{}), .{ - // We shouldn't be using getPath but we need to for now - // https://codeberg.org/ifreund/zig-wayland/issues/66 - .wayland_xml = b.dependency("wayland", .{}) - .path("protocol/wayland.xml"), - .wayland_protocols = b.dependency("wayland_protocols", .{}) - .path(""), + .wayland_xml = b.dependency("wayland", .{}).path("protocol/wayland.xml"), + .wayland_protocols = b.dependency("wayland_protocols", .{}).path(""), }); const wayland = b.createModule(.{ .root_source_file = scanner.result }); @@ -485,6 +481,8 @@ pub fn add( step.root_module.addImport("wayland", wayland); step.root_module.addImport("gdk_wayland", gobject.module("gdkwayland4")); + + if (self.config.layer_shell) step.linkSystemLibrary2("gtk4-layer-shell", dynamic_link_opts); step.linkSystemLibrary2("wayland-client", dynamic_link_opts); } diff --git a/src/build/docker/debian/Dockerfile b/src/build/docker/debian/Dockerfile index 7f60ddf1d..8e0f324e2 100644 --- a/src/build/docker/debian/Dockerfile +++ b/src/build/docker/debian/Dockerfile @@ -18,6 +18,8 @@ RUN DEBIAN_FRONTEND="noninteractive" apt-get -qq update && \ # Ghostty Dependencies libadwaita-1-dev \ libgtk-4-dev && \ + # TODO: Add when this is updated to Debian 13++ + # gtk4-layer-shell # Clean up for better caching rm -rf /var/lib/apt/lists/* diff --git a/src/build/gtk.zig b/src/build/gtk.zig index f33219988..8ded0df00 100644 --- a/src/build/gtk.zig +++ b/src/build/gtk.zig @@ -3,6 +3,7 @@ const std = @import("std"); pub const Targets = packed struct { x11: bool = false, wayland: bool = false, + layer_shell: bool = false, }; /// Returns the targets that GTK4 was compiled with. @@ -17,8 +18,24 @@ pub fn targets(b: *std.Build) Targets { .Ignore, ) catch return .{}; + const x11 = std.mem.indexOf(u8, output, "x11") != null; + const wayland = std.mem.indexOf(u8, output, "wayland") != null; + + const layer_shell = layer_shell: { + if (!wayland) break :layer_shell false; + + _ = b.runAllowFail( + &.{ "pkg-config", "--exists", "gtk4-layer-shell-0" }, + &code, + .Ignore, + ) catch break :layer_shell false; + + break :layer_shell true; + }; + return .{ - .x11 = std.mem.indexOf(u8, output, "x11") != null, - .wayland = std.mem.indexOf(u8, output, "wayland") != null, + .x11 = x11, + .wayland = wayland, + .layer_shell = layer_shell, }; }