From 8f7425f78c5d284bf149cc43847af2d998fa067f Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Tue, 4 Mar 2025 21:01:04 +0100 Subject: [PATCH 1/3] gtk: implement quick terminal slide animation Yet another protocol that as far as I know only KWin implements. Oh well, might as well let KDE users such as myself enjoy it OOTB --- src/apprt/gtk/Window.zig | 18 ++++------ src/apprt/gtk/winproto.zig | 6 ---- src/apprt/gtk/winproto/noop.zig | 2 -- src/apprt/gtk/winproto/wayland.zig | 56 ++++++++++++++++++++++++++++-- src/apprt/gtk/winproto/x11.zig | 2 -- src/build/SharedDeps.zig | 2 ++ 6 files changed, 63 insertions(+), 23 deletions(-) diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 6af20118b..c77a15e03 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -377,9 +377,14 @@ pub fn present(self: *Window) void { pub fn toggleVisibility(self: *Window) void { const window: *gtk.Widget = @ptrCast(self.window); + window.setVisible(@intFromBool(window.isVisible() == 0)); } +pub fn isQuickTerminal(self: *Window) bool { + return self.app.quick_terminal == self; +} + pub fn updateConfig( self: *Window, config: *const configpkg.Config, @@ -420,7 +425,7 @@ pub fn syncAppearance(self: *Window) !void { if (!csd_enabled) break :visible false; // Never display the header bar as a quick terminal. - if (self.app.quick_terminal == self) break :visible false; + if (self.isQuickTerminal()) break :visible false; // Unconditionally disable the header bar when fullscreened. if (self.config.fullscreen) break :visible false; @@ -471,12 +476,6 @@ pub fn syncAppearance(self: *Window) !void { self.winproto.syncAppearance() catch |err| { log.warn("failed to sync winproto appearance error={}", .{err}); }; - - if (self.app.quick_terminal == self) { - self.winproto.syncQuickTerminal() catch |err| { - log.warn("failed to sync quick terminal appearance error={}", .{err}); - }; - } } fn toggleCssClass( @@ -802,7 +801,7 @@ 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; + if (self.isQuickTerminal()) self.app.quick_terminal = null; window.destroy(); } @@ -812,9 +811,6 @@ fn gtkCloseRequest(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool { 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| { diff --git a/src/apprt/gtk/winproto.zig b/src/apprt/gtk/winproto.zig index e99a5fb2b..01587a226 100644 --- a/src/apprt/gtk/winproto.zig +++ b/src/apprt/gtk/winproto.zig @@ -133,12 +133,6 @@ 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 d030d884e..c71394e7a 100644 --- a/src/apprt/gtk/winproto/noop.zig +++ b/src/apprt/gtk/winproto/noop.zig @@ -59,8 +59,6 @@ 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 f9ea7c7de..e27b6bf77 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -27,6 +27,8 @@ pub const App = struct { // 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, }; @@ -117,13 +119,26 @@ pub const App = struct { global, )) |blur_manager| { context.kde_blur_manager = blur_manager; - } else if (registryBind( + return; + } + + if (registryBind( org.KdeKwinServerDecorationManager, registry, global, )) |deco_manager| { context.kde_decoration_manager = deco_manager; deco_manager.setListener(*Context, decoManagerListener, context); + return; + } + + if (registryBind( + org.KdeKwinSlideManager, + registry, + global, + )) |slide_manager| { + context.kde_slide_manager = slide_manager; + return; } }, @@ -188,6 +203,10 @@ pub const Window = struct { /// of the window. decoration: ?*org.KdeKwinServerDecoration, + /// 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, + pub fn init( alloc: Allocator, app: *App, @@ -232,6 +251,7 @@ pub const Window = struct { .app_context = app.context, .blur_token = null, .decoration = deco, + .slide = null, }; } @@ -239,6 +259,7 @@ pub const Window = struct { _ = 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 {} @@ -250,6 +271,12 @@ pub const Window = struct { 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 { @@ -308,7 +335,7 @@ pub const Window = struct { }; } - pub fn syncQuickTerminal(self: *Window) !void { + fn syncQuickTerminal(self: *Window) !void { if (comptime !build_options.layer_shell) return; const window = self.apprt_window.window; @@ -339,6 +366,22 @@ pub const Window = struct { .top, .bottom, .center => c.gtk_window_set_default_size(window, 800, 400), .left, .right => c.gtk_window_set_default_size(window, 400, 800), } + + if (self.apprt_window.isQuickTerminal()) { + 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; + }; + slide.setLocation(@intCast(@intFromEnum(anchored.toKdeSlideLocation()))); + slide.commit(); + break :slide slide; + } else null; + } } }; @@ -347,4 +390,13 @@ const LayerShellEdge = enum(c_uint) { right = c.GTK_LAYER_SHELL_EDGE_RIGHT, top = c.GTK_LAYER_SHELL_EDGE_TOP, bottom = c.GTK_LAYER_SHELL_EDGE_BOTTOM, + + fn toKdeSlideLocation(self: LayerShellEdge) org.KdeKwinSlide.Location { + return switch (self) { + .left => .left, + .top => .top, + .right => .right, + .bottom => .bottom, + }; + } }; diff --git a/src/apprt/gtk/winproto/x11.zig b/src/apprt/gtk/winproto/x11.zig index 6d20e6e8b..c8f5385dd 100644 --- a/src/apprt/gtk/winproto/x11.zig +++ b/src/apprt/gtk/winproto/x11.zig @@ -228,8 +228,6 @@ 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/SharedDeps.zig b/src/build/SharedDeps.zig index 26c4d84c5..7b2d10c63 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -474,10 +474,12 @@ pub fn add( // FIXME: replace with `zxdg_decoration_v1` once GTK merges https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398 scanner.addCustomProtocol(plasma_wayland_protocols.path("src/protocols/blur.xml")); scanner.addCustomProtocol(plasma_wayland_protocols.path("src/protocols/server-decoration.xml")); + scanner.addCustomProtocol(plasma_wayland_protocols.path("src/protocols/slide.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); step.root_module.addImport("wayland", wayland); step.root_module.addImport("gdk_wayland", gobject.module("gdkwayland4")); From 58b04340920d4e27cf52eac92e2706af6f2f8784 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Wed, 5 Mar 2025 21:36:58 +0100 Subject: [PATCH 2/3] docs: update information about quick terminal support on Linux --- src/config/Config.zig | 12 +++++++++++- src/input/Binding.zig | 10 +++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index d2c033cdc..d7539b971 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1639,7 +1639,8 @@ keybind: Keybinds = .{}, /// * `right` - Terminal appears at the right of the screen. /// * `center` - Terminal appears at the center of the screen. /// -/// Changing this configuration requires restarting Ghostty completely. +/// On macOS, changing this configuration requires restarting Ghostty +/// completely. /// /// Note: There is no default keybind for toggling the quick terminal. /// To enable this feature, bind the `toggle_quick_terminal` action to a key. @@ -1663,15 +1664,21 @@ keybind: Keybinds = .{}, /// /// The default value is `main` because this is the recommended screen /// by the operating system. +/// +/// Only implemented on macOS. @"quick-terminal-screen": QuickTerminalScreen = .main, /// Duration (in seconds) of the quick terminal enter and exit animation. /// Set it to 0 to disable animation completely. This can be changed at /// runtime. +/// +/// Only implemented on macOS. @"quick-terminal-animation-duration": f64 = 0.2, /// Automatically hide the quick terminal when focus shifts to another window. /// Set it to false for the quick terminal to remain open even when it loses focus. +/// +/// Only implemented on macOS. @"quick-terminal-autohide": bool = true, /// This configuration option determines the behavior of the quick terminal @@ -1689,6 +1696,9 @@ keybind: Keybinds = .{}, /// space. /// /// The default value is `move`. +/// +/// Only implemented on macOS. +/// On Linux the behavior is always equivalent to `move`. @"quick-terminal-space-behavior": QuickTerminalSpaceBehavior = .move, /// Whether to enable shell integration auto-injection or not. Shell integration diff --git a/src/input/Binding.zig b/src/input/Binding.zig index ddee672a5..b684e6cd6 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -471,7 +471,15 @@ pub const Action = union(enum) { /// See the various configurations for the quick terminal in the /// configuration file to customize its behavior. /// - /// This currently only works on macOS. + /// Supported on macOS and some desktop environments on Linux, namely + /// those that support the `wlr-layer-shell` Wayland protocol + /// (i.e. most desktop environments and window managers except GNOME). + /// + /// Slide-in animations on Linux are only supported on KDE when the + /// "Sliding Popups" KWin plugin is enabled. If you do not have this + /// plugin enabled, open System Settings > Apps & Windows > Window + /// Management > Desktop Effects, and enable the plugin in the plugin list. + /// Ghostty would then need to be restarted for this to take effect. toggle_quick_terminal: void, /// Show/hide all windows. If all windows become shown, we also ensure From 44d4990eb21f8858454c365ddf87cb28d068d552 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Wed, 5 Mar 2025 21:51:35 +0100 Subject: [PATCH 3/3] gtk: implement quick-terminal-autohide --- src/apprt/gtk/Window.zig | 17 +++++++++++++++++ src/config/Config.zig | 2 -- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index c77a15e03..6119e40dc 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -80,6 +80,7 @@ pub const DerivedConfig = struct { gtk_toolbar_style: configpkg.Config.GtkToolbarStyle, quick_terminal_position: configpkg.Config.QuickTerminalPosition, + quick_terminal_autohide: bool, maximize: bool, fullscreen: bool, @@ -97,6 +98,7 @@ pub const DerivedConfig = struct { .gtk_toolbar_style = config.@"gtk-toolbar-style", .quick_terminal_position = config.@"quick-terminal-position", + .quick_terminal_autohide = config.@"quick-terminal-autohide", .maximize = config.maximize, .fullscreen = config.fullscreen, @@ -246,6 +248,7 @@ pub fn init(self: *Window, app: *App) !void { _ = c.g_signal_connect_data(self.window, "notify::maximized", c.G_CALLBACK(>kWindowNotifyMaximized), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(self.window, "notify::fullscreened", c.G_CALLBACK(>kWindowNotifyFullscreened), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(self.window, "notify::is-active", c.G_CALLBACK(>kWindowNotifyIsActive), self, null, c.G_CONNECT_DEFAULT); // 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. @@ -728,6 +731,20 @@ fn gtkWindowNotifyFullscreened( }; } +fn gtkWindowNotifyIsActive( + _: *c.GObject, + _: *c.GParamSpec, + ud: ?*anyopaque, +) callconv(.C) void { + const self = userdataSelf(ud orelse return); + if (!self.isQuickTerminal()) return; + + // Hide when we're unfocused + if (self.config.quick_terminal_autohide and c.gtk_window_is_active(self.window) == 0) { + self.toggleVisibility(); + } +} + // Note: we MUST NOT use the GtkButton parameter because gtkActionNewTab // sends an undefined value. fn gtkTabNewClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void { diff --git a/src/config/Config.zig b/src/config/Config.zig index d7539b971..2a76de1e8 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1677,8 +1677,6 @@ keybind: Keybinds = .{}, /// Automatically hide the quick terminal when focus shifts to another window. /// Set it to false for the quick terminal to remain open even when it loses focus. -/// -/// Only implemented on macOS. @"quick-terminal-autohide": bool = true, /// This configuration option determines the behavior of the quick terminal