gtk: implement quick terminal (#6027)

This commit is contained in:
Leah Amelia Chen
2025-02-28 23:31:39 +01:00
committed by GitHub
15 changed files with 216 additions and 31 deletions

View File

@ -31,6 +31,7 @@
glib, glib,
glslang, glslang,
gtk4, gtk4,
gtk4-layer-shell,
gobject-introspection, gobject-introspection,
libadwaita, libadwaita,
blueprint-compiler, blueprint-compiler,
@ -88,6 +89,7 @@
libadwaita libadwaita
gtk4 gtk4
gtk4-layer-shell
glib glib
gobject-introspection gobject-introspection
wayland wayland
@ -167,6 +169,7 @@ in
blueprint-compiler blueprint-compiler
libadwaita libadwaita
gtk4 gtk4
gtk4-layer-shell
glib glib
gobject-introspection gobject-introspection
wayland wayland

View File

@ -13,6 +13,7 @@
libGL, libGL,
glib, glib,
gtk4, gtk4,
gtk4-layer-shell,
gobject-introspection, gobject-introspection,
libadwaita, libadwaita,
blueprint-compiler, blueprint-compiler,
@ -118,6 +119,7 @@ in
libXrandr libXrandr
] ]
++ lib.optionals enableWayland [ ++ lib.optionals enableWayland [
gtk4-layer-shell
wayland wayland
]; ];

View File

@ -73,6 +73,8 @@ parts:
- blueprint-compiler - blueprint-compiler
- libgtk-4-dev - libgtk-4-dev
- libadwaita-1-dev - libadwaita-1-dev
# TODO: Add when the Snap is updated to Ubuntu 24.10+
# - gtk4-layer-shell
- libxml2-utils - libxml2-utils
- git - git
- patchelf - patchelf

View File

@ -70,6 +70,10 @@ config_errors_window: ?*ConfigErrorsWindow = null,
/// The clipboard confirmation window, if it is currently open. /// The clipboard confirmation window, if it is currently open.
clipboard_confirmation_window: ?*ClipboardConfirmationWindow = null, 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. /// This is set to false when the main loop should exit.
running: bool = true, running: bool = true,
@ -497,10 +501,10 @@ pub fn performAction(
.toggle_window_decorations => self.toggleWindowDecorations(target), .toggle_window_decorations => self.toggleWindowDecorations(target),
.quit_timer => self.quitTimer(value), .quit_timer => self.quitTimer(value),
.prompt_title => try self.promptTitle(target), .prompt_title => try self.promptTitle(target),
.toggle_quick_terminal => return try self.toggleQuickTerminal(),
// Unimplemented // Unimplemented
.close_all_windows, .close_all_windows,
.toggle_quick_terminal,
.toggle_visibility, .toggle_visibility,
.cell_size, .cell_size,
.secure_input, .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 { fn quitTimer(self: *App, mode: apprt.action.QuitTimer) void {
switch (mode) { switch (mode) {
.start => self.startQuitTimer(), .start => self.startQuitTimer(),
@ -1372,6 +1403,9 @@ fn newWindow(self: *App, parent_: ?*CoreSurface) !void {
// Add our initial tab // Add our initial tab
try window.newTab(parent_); try window.newTab(parent_);
// Show the new window
window.present();
} }
fn quit(self: *App) void { fn quit(self: *App) void {

View File

@ -193,10 +193,6 @@ pub fn addTab(self: *TabView, tab: *Tab, title: [:0]const u8) void {
} }
pub fn closeTab(self: *TabView, tab: *Tab) 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 // closeTab always expects to close unconditionally so we mark this
// as true so that the close_page call below doesn't request // as true so that the close_page call below doesn't request
// confirmation. // confirmation.
@ -225,7 +221,7 @@ pub fn closeTab(self: *TabView, tab: *Tab) void {
box.as(gobject.Object).unref(); box.as(gobject.Object).unref();
} }
window.destroy(); self.window.close();
} }
} }
@ -234,7 +230,9 @@ pub fn createWindow(currentWindow: *Window) !*Window {
const app = currentWindow.app; const app = currentWindow.app;
// Create a new window // 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 { fn adwPageAttached(_: *adw.TabView, page: *adw.TabPage, _: c_int, self: *TabView) callconv(.C) void {

View File

@ -79,6 +79,8 @@ pub const DerivedConfig = struct {
gtk_wide_tabs: bool, gtk_wide_tabs: bool,
gtk_toolbar_style: configpkg.Config.GtkToolbarStyle, gtk_toolbar_style: configpkg.Config.GtkToolbarStyle,
quick_terminal_position: configpkg.Config.QuickTerminalPosition,
maximize: bool, maximize: bool,
fullscreen: bool, fullscreen: bool,
window_decoration: configpkg.Config.WindowDecoration, window_decoration: configpkg.Config.WindowDecoration,
@ -94,6 +96,8 @@ pub const DerivedConfig = struct {
.gtk_wide_tabs = config.@"gtk-wide-tabs", .gtk_wide_tabs = config.@"gtk-wide-tabs",
.gtk_toolbar_style = config.@"gtk-toolbar-style", .gtk_toolbar_style = config.@"gtk-toolbar-style",
.quick_terminal_position = config.@"quick-terminal-position",
.maximize = config.maximize, .maximize = config.maximize,
.fullscreen = config.fullscreen, .fullscreen = config.fullscreen,
.window_decoration = config.@"window-decoration", .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 we are in fullscreen mode, new windows start fullscreen.
if (self.config.fullscreen) c.gtk_window_fullscreen(self.window); if (self.config.fullscreen) c.gtk_window_fullscreen(self.window);
}
// Show the window pub fn present(self: *Window) void {
c.gtk_widget_show(gtk_widget); 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( pub fn updateConfig(
@ -408,6 +419,9 @@ pub fn syncAppearance(self: *Window) !void {
// Never display the header bar when CSDs are disabled. // Never display the header bar when CSDs are disabled.
if (!csd_enabled) break :visible false; 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. // Unconditionally disable the header bar when fullscreened.
if (self.config.fullscreen) break :visible false; 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}); log.warn("failed to sync winproto appearance error={}", .{err});
}; };
toggleCssClass( if (self.app.quick_terminal == self) {
@ptrCast(self.window), self.winproto.syncQuickTerminal() catch |err| {
"background", log.warn("failed to sync quick terminal appearance error={}", .{err});
self.config.background_opacity >= 1, };
); }
} }
fn toggleCssClass( fn toggleCssClass(
@ -780,11 +794,23 @@ fn adwTabOverviewFocusTimer(
return 0; 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 { fn gtkCloseRequest(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
_ = v; _ = v;
log.debug("window close request", .{}); log.debug("window close request", .{});
const self = userdataSelf(ud.?); 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. // If none of our surfaces need confirmation, we can just exit.
for (self.app.core_app.surfaces.items) |surface| { for (self.app.core_app.surfaces.items) |surface| {
if (surface.container.window()) |window| { if (surface.container.window()) |window| {
@ -792,7 +818,7 @@ fn gtkCloseRequest(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
surface.core_surface.needsConfirmQuit()) break; surface.core_surface.needsConfirmQuit()) break;
} }
} else { } else {
c.gtk_window_destroy(self.window); self.close();
return true; return true;
} }
@ -836,7 +862,7 @@ fn gtkCloseConfirmation(
c.gtk_window_destroy(@ptrCast(alert)); c.gtk_window_destroy(@ptrCast(alert));
if (response == c.GTK_RESPONSE_YES) { if (response == c.GTK_RESPONSE_YES) {
const self = userdataSelf(ud.?); const self = userdataSelf(ud.?);
c.gtk_window_destroy(self.window); self.close();
} }
} }
@ -934,7 +960,7 @@ fn gtkActionClose(
_: ?*glib.Variant, _: ?*glib.Variant,
self: *Window, self: *Window,
) callconv(.C) void { ) callconv(.C) void {
c.gtk_window_destroy(self.window); self.close();
} }
fn gtkActionNewWindow( fn gtkActionNewWindow(

View File

@ -15,6 +15,7 @@ pub const c = @cImport({
@cInclude("X11/XKBlib.h"); @cInclude("X11/XKBlib.h");
} }
if (build_options.wayland) { if (build_options.wayland) {
if (build_options.layer_shell) @cInclude("gtk4-layer-shell/gtk4-layer-shell.h");
@cInclude("gdk/wayland/gdkwayland.h"); @cInclude("gdk/wayland/gdkwayland.h");
} }

View File

@ -59,6 +59,23 @@ pub const App = union(Protocol) {
inline else => |*v| v.eventMods(device, gtk_mods), inline else => |*v| v.eventMods(device, gtk_mods),
} orelse key.translateMods(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. /// 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 { pub fn clientSideDecorationEnabled(self: Window) bool {
return switch (self) { return switch (self) {
inline else => |v| v.clientSideDecorationEnabled(), inline else => |v| v.clientSideDecorationEnabled(),

View File

@ -29,6 +29,11 @@ pub const App = struct {
) ?input.Mods { ) ?input.Mods {
return null; return null;
} }
pub fn supportsQuickTerminal(_: App) bool {
return false;
}
pub fn initQuickTerminal(_: *App, _: *ApprtWindow) !void {}
}; };
pub const Window = struct { pub const Window = struct {
@ -54,6 +59,8 @@ pub const Window = struct {
pub fn syncAppearance(_: *Window) !void {} pub fn syncAppearance(_: *Window) !void {}
pub fn syncQuickTerminal(_: *Window) !void {}
/// This returns true if CSD is enabled for this window. This /// This returns true if CSD is enabled for this window. This
/// should be the actual present state of the window, not the /// should be the actual present state of the window, not the
/// desired state. /// desired state.

View File

@ -1,7 +1,10 @@
//! Wayland protocol implementation for the Ghostty GTK apprt. //! Wayland protocol implementation for the Ghostty GTK apprt.
const std = @import("std"); const std = @import("std");
const wayland = @import("wayland");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const build_options = @import("build_options");
const wayland = @import("wayland");
const c = @import("../c.zig").c; const c = @import("../c.zig").c;
const Config = @import("../../../config.zig").Config; const Config = @import("../../../config.zig").Config;
const input = @import("../../../input.zig"); const input = @import("../../../input.zig");
@ -84,6 +87,20 @@ pub const App = struct {
return null; 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( fn registryListener(
registry: *wl.Registry, registry: *wl.Registry,
event: wl.Registry.Event, event: wl.Registry.Event,
@ -156,7 +173,7 @@ pub const App = struct {
/// Per-window (wl_surface) state for the Wayland protocol. /// Per-window (wl_surface) state for the Wayland protocol.
pub const Window = struct { pub const Window = struct {
config: *const ApprtWindow.DerivedConfig, apprt_window: *ApprtWindow,
/// The Wayland surface for this window. /// The Wayland surface for this window.
surface: *wl.Surface, surface: *wl.Surface,
@ -210,7 +227,7 @@ pub const Window = struct {
}; };
return .{ return .{
.config = &apprt_window.config, .apprt_window = apprt_window,
.surface = wl_surface, .surface = wl_surface,
.app_context = app.context, .app_context = app.context,
.blur_token = null, .blur_token = null,
@ -255,7 +272,7 @@ pub const Window = struct {
/// Update the blur state of the window. /// Update the blur state of the window.
fn syncBlur(self: *Window) !void { fn syncBlur(self: *Window) !void {
const manager = self.app_context.kde_blur_manager orelse return; 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| { if (self.blur_token) |tok| {
// Only release token when transitioning from blurred -> not blurred // Only release token when transitioning from blurred -> not blurred
@ -283,11 +300,51 @@ pub const Window = struct {
} }
fn getDecorationMode(self: Window) org.KdeKwinServerDecorationManager.Mode { 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, .auto => self.app_context.default_deco_mode orelse .Client,
.client => .Client, .client => .Client,
.server => .Server, .server => .Server,
.none => .None, .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,
}; };

View File

@ -148,6 +148,12 @@ pub const App = struct {
return mods; return mods;
} }
pub fn supportsQuickTerminal(_: App) bool {
return false;
}
pub fn initQuickTerminal(_: *App, _: *ApprtWindow) !void {}
}; };
pub const Window = struct { pub const Window = struct {
@ -222,6 +228,8 @@ pub const Window = struct {
}; };
} }
pub fn syncQuickTerminal(_: *Window) !void {}
pub fn clientSideDecorationEnabled(self: Window) bool { pub fn clientSideDecorationEnabled(self: Window) bool {
return switch (self.config.window_decoration) { return switch (self.config.window_decoration) {
.auto, .client => true, .auto, .client => true,

View File

@ -34,6 +34,7 @@ font_backend: font.Backend = .freetype,
/// Feature flags /// Feature flags
x11: bool = false, x11: bool = false,
wayland: bool = false, wayland: bool = false,
layer_shell: bool = false,
sentry: bool = true, sentry: bool = true,
wasm_shared: bool = true, wasm_shared: bool = true,
@ -109,7 +110,6 @@ pub fn init(b: *std.Build) !Config {
//--------------------------------------------------------------- //---------------------------------------------------------------
// Comptime Interfaces // Comptime Interfaces
config.font_backend = b.option( config.font_backend = b.option(
font.Backend, font.Backend,
"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.", "Enables linking against X11 libraries when using the GTK rendering backend.",
) orelse gtk_targets.x11; ) 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 // 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, "flatpak", self.flatpak);
step.addOption(bool, "x11", self.x11); step.addOption(bool, "x11", self.x11);
step.addOption(bool, "wayland", self.wayland); step.addOption(bool, "wayland", self.wayland);
step.addOption(bool, "layer_shell", self.layer_shell);
step.addOption(bool, "sentry", self.sentry); step.addOption(bool, "sentry", self.sentry);
step.addOption(apprt.Runtime, "app_runtime", self.app_runtime); step.addOption(apprt.Runtime, "app_runtime", self.app_runtime);
step.addOption(font.Backend, "font_backend", self.font_backend); step.addOption(font.Backend, "font_backend", self.font_backend);

View File

@ -460,12 +460,8 @@ pub fn add(
if (self.config.wayland) { if (self.config.wayland) {
const scanner = Scanner.create(b.dependency("zig_wayland", .{}), .{ const scanner = Scanner.create(b.dependency("zig_wayland", .{}), .{
// We shouldn't be using getPath but we need to for now .wayland_xml = b.dependency("wayland", .{}).path("protocol/wayland.xml"),
// https://codeberg.org/ifreund/zig-wayland/issues/66 .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 }); 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("wayland", wayland);
step.root_module.addImport("gdk_wayland", gobject.module("gdkwayland4")); 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); step.linkSystemLibrary2("wayland-client", dynamic_link_opts);
} }

View File

@ -18,6 +18,8 @@ RUN DEBIAN_FRONTEND="noninteractive" apt-get -qq update && \
# Ghostty Dependencies # Ghostty Dependencies
libadwaita-1-dev \ libadwaita-1-dev \
libgtk-4-dev && \ libgtk-4-dev && \
# TODO: Add when this is updated to Debian 13++
# gtk4-layer-shell
# Clean up for better caching # Clean up for better caching
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*

View File

@ -3,6 +3,7 @@ const std = @import("std");
pub const Targets = packed struct { pub const Targets = packed struct {
x11: bool = false, x11: bool = false,
wayland: bool = false, wayland: bool = false,
layer_shell: bool = false,
}; };
/// Returns the targets that GTK4 was compiled with. /// Returns the targets that GTK4 was compiled with.
@ -17,8 +18,24 @@ pub fn targets(b: *std.Build) Targets {
.Ignore, .Ignore,
) catch return .{}; ) 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 .{ return .{
.x11 = std.mem.indexOf(u8, output, "x11") != null, .x11 = x11,
.wayland = std.mem.indexOf(u8, output, "wayland") != null, .wayland = wayland,
.layer_shell = layer_shell,
}; };
} }