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, }; }