From 4e0d9b1b277aac2e60e253750a175e80ff2ab973 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Mon, 6 Jan 2025 21:58:22 +0100 Subject: [PATCH] gtk(wayland): implement server-side decorations --- macos/Sources/Ghostty/Ghostty.Config.swift | 24 +++- src/apprt/gtk/App.zig | 2 +- src/apprt/gtk/Surface.zig | 8 +- src/apprt/gtk/Window.zig | 102 +++++++++++------ src/apprt/gtk/headerbar.zig | 3 - src/apprt/gtk/winproto.zig | 8 +- src/apprt/gtk/winproto/noop.zig | 8 ++ src/apprt/gtk/winproto/wayland.zig | 75 ++++++++++++- src/apprt/gtk/winproto/x11.zig | 9 ++ src/build/SharedDeps.zig | 4 + src/config/Config.zig | 125 +++++++++++++++++---- 11 files changed, 293 insertions(+), 75 deletions(-) diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index d6e1710ae2..9d45ea9bbb 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -165,11 +165,14 @@ extension Ghostty { } var windowDecorations: Bool { - guard let config = self.config else { return true } - var v = false; + let defaultValue = true + guard let config = self.config else { return defaultValue } + var v: UnsafePointer? = nil let key = "window-decoration" - _ = ghostty_config_get(config, &v, key, UInt(key.count)) - return v; + guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue } + guard let ptr = v else { return defaultValue } + let str = String(cString: ptr) + return WindowDecoration(rawValue: str)?.enabled() ?? defaultValue } var windowTheme: String? { @@ -554,4 +557,17 @@ extension Ghostty.Config { } } } + + enum WindowDecoration: String { + case none + case client + case server + + func enabled() -> Bool { + switch self { + case .client, .server: return true + case .none: return false + } + } + } } diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index f49d275de7..96275684e5 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -1881,7 +1881,7 @@ fn initContextMenu(self: *App) void { c.g_menu_append(section, "Terminal Inspector", "win.toggle_inspector"); } - if (!self.config.@"window-decoration") { + if (!self.config.@"window-decoration".isCSD()) { const section = c.g_menu_new(); defer c.g_object_unref(section); const submenu = c.g_menu_new(); diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index c5a001f340..61866dcec6 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -1384,11 +1384,9 @@ fn gtkResize(area: *c.GtkGLArea, width: c.gint, height: c.gint, ud: ?*anyopaque) }; if (self.container.window()) |window| { - if (window.winproto) |*winproto| { - winproto.resizeEvent() catch |err| { - log.warn("failed to notify window protocol of resize={}", .{err}); - }; - } + window.winproto.resizeEvent() catch |err| { + log.warn("failed to notify window protocol of resize={}", .{err}); + }; } self.resize_overlay.maybeShow(); diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 3c8c2c2e7d..3e972ca021 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -57,7 +57,7 @@ toast_overlay: ?*c.GtkWidget, adw_tab_overview_focus_timer: ?c.guint = null, /// State and logic for windowing protocol for a window. -winproto: ?winproto.Window, +winproto: winproto.Window, pub fn create(alloc: Allocator, app: *App) !*Window { // Allocate a fixed pointer for our window. We try to minimize @@ -83,7 +83,7 @@ pub fn init(self: *Window, app: *App) !void { .notebook = undefined, .context_menu = undefined, .toast_overlay = undefined, - .winproto = null, + .winproto = .none, }; // Create the window @@ -207,11 +207,6 @@ pub fn init(self: *Window, app: *App) !void { _ = c.g_signal_connect_data(gtk_window, "notify::maximized", c.G_CALLBACK(>kWindowNotifyMaximized), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(gtk_window, "notify::fullscreened", c.G_CALLBACK(>kWindowNotifyFullscreened), self, null, c.G_CONNECT_DEFAULT); - // If we are disabling decorations then disable them right away. - if (!app.config.@"window-decoration") { - c.gtk_window_set_decorated(gtk_window, 0); - } - // 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 (!adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) { @@ -379,7 +374,11 @@ pub fn updateConfig( self: *Window, config: *const configpkg.Config, ) !void { - if (self.winproto) |*v| try v.updateConfigEvent(config); + self.winproto.updateConfigEvent(config) catch |err| { + // We want to continue attempting to make the other config + // changes necessary so we just log the error and continue. + log.warn("failed to update window protocol config error={}", .{err}); + }; // We always resync our appearance whenever the config changes. try self.syncAppearance(config); @@ -391,16 +390,52 @@ pub fn updateConfig( /// TODO: Many of the initial style settings in `create` could possibly be made /// reactive by moving them here. pub fn syncAppearance(self: *Window, config: *const configpkg.Config) !void { - if (config.@"background-opacity" < 1) { - c.gtk_widget_remove_css_class(@ptrCast(self.window), "background"); - } else { - c.gtk_widget_add_css_class(@ptrCast(self.window), "background"); + self.winproto.syncAppearance() catch |err| { + log.warn("failed to sync winproto appearance error={}", .{err}); + }; + + toggleCssClass( + @ptrCast(self.window), + "background", + config.@"background-opacity" >= 1, + ); + + // If we are disabling CSDs then disable them right away. + const csd_enabled = self.winproto.clientSideDecorationEnabled(); + c.gtk_window_set_decorated(self.window, @intFromBool(csd_enabled)); + + // If we are not decorated then we hide the titlebar. + self.headerbar.setVisible(config.@"gtk-titlebar" and csd_enabled); + + // Disable the title buttons (close, maximize, minimize, ...) + // *inside* the tab overview if CSDs are disabled. + // We do spare the search button, though. + if ((comptime adwaita.versionAtLeast(0, 0, 0)) and + adwaita.enabled(&self.app.config)) + { + if (self.tab_overview) |tab_overview| { + c.adw_tab_overview_set_show_start_title_buttons( + @ptrCast(tab_overview), + @intFromBool(csd_enabled), + ); + c.adw_tab_overview_set_show_end_title_buttons( + @ptrCast(tab_overview), + @intFromBool(csd_enabled), + ); + } } +} - // Window protocol specific appearance updates - if (self.winproto) |*v| v.syncAppearance() catch |err| { - log.warn("failed to sync window protocol appearance error={}", .{err}); - }; +fn toggleCssClass( + widget: *c.GtkWidget, + class: [:0]const u8, + v: bool, +) void { + if (v) { + c.gtk_widget_add_css_class(widget, class); + } else { + c.gtk_widget_remove_css_class(widget, class); + } } /// Sets up the GTK actions for the window scope. Actions are how GTK handles @@ -440,7 +475,7 @@ fn initActions(self: *Window) void { pub fn deinit(self: *Window) void { c.gtk_widget_unparent(@ptrCast(self.context_menu)); - if (self.winproto) |*v| v.deinit(self.app.core_app.alloc); + self.winproto.deinit(self.app.core_app.alloc); if (self.adw_tab_overview_focus_timer) |timer| { _ = c.g_source_remove(timer); @@ -548,15 +583,11 @@ pub fn toggleFullscreen(self: *Window) void { /// Toggle the window decorations for this window. pub fn toggleWindowDecorations(self: *Window) void { - const old_decorated = c.gtk_window_get_decorated(self.window) == 1; - const new_decorated = !old_decorated; - c.gtk_window_set_decorated(self.window, @intFromBool(new_decorated)); - - // If we have a titlebar, then we also show/hide it depending on the - // decorated state. GTK tends to consider the titlebar part of the frame - // and hides it with decorations, but libadwaita doesn't. This makes it - // explicit. - self.headerbar.setVisible(new_decorated); + self.app.config.@"window-decoration" = switch (self.app.config.@"window-decoration") { + .client, .server => .none, + .none => .server, + }; + self.updateConfig(&self.app.config) catch {}; } /// Grabs focus on the currently selected tab. @@ -623,17 +654,14 @@ fn gtkWindowNotifyDecorated( _: *c.GParamSpec, _: ?*anyopaque, ) callconv(.C) void { - if (c.gtk_window_get_decorated(@ptrCast(object)) == 1) { - c.gtk_widget_remove_css_class(@ptrCast(object), "ssd"); - c.gtk_widget_remove_css_class(@ptrCast(object), "no-border-radius"); - } else { - // 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. - c.gtk_widget_add_css_class(@ptrCast(object), "ssd"); - c.gtk_widget_add_css_class(@ptrCast(object), "no-border-radius"); - } + const is_decorated = c.gtk_window_get_decorated(@ptrCast(object)) == 1; + + // 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(@ptrCast(object), "ssd", !is_decorated); + toggleCssClass(@ptrCast(object), "no-border-radius", !is_decorated); } fn gtkWindowNotifyFullscreened( diff --git a/src/apprt/gtk/headerbar.zig b/src/apprt/gtk/headerbar.zig index 2b47ea4b73..0f7f15bf8a 100644 --- a/src/apprt/gtk/headerbar.zig +++ b/src/apprt/gtk/headerbar.zig @@ -18,9 +18,6 @@ pub const HeaderBar = union(enum) { } else { HeaderBarGtk.init(self); } - - if (!window.app.config.@"gtk-titlebar" or !window.app.config.@"window-decoration") - self.setVisible(false); } pub fn setVisible(self: HeaderBar, visible: bool) void { diff --git a/src/apprt/gtk/winproto.zig b/src/apprt/gtk/winproto.zig index cb873fe013..e6020f49e8 100644 --- a/src/apprt/gtk/winproto.zig +++ b/src/apprt/gtk/winproto.zig @@ -62,7 +62,7 @@ pub const App = union(Protocol) { /// Per-Window state for the underlying windowing protocol. /// -/// In both X and Wayland, the terminology used is "Surface" and this is +/// 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. @@ -125,4 +125,10 @@ pub const Window = union(Protocol) { inline else => |*v| try v.syncAppearance(), } } + + 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 14f3dc6a7f..38703aecbe 100644 --- a/src/apprt/gtk/winproto/noop.zig +++ b/src/apprt/gtk/winproto/noop.zig @@ -53,4 +53,12 @@ pub const Window = struct { 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; + } }; diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index 3f7ad00680..efe0d89cd8 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -18,6 +18,10 @@ pub const App = struct { 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, }; pub fn init( @@ -89,6 +93,14 @@ pub const App = struct { )) |blur_manager| { context.kde_blur_manager = blur_manager; break :global; + } else if (registryBind( + org.KdeKwinServerDecorationManager, + registry, + global, + 1, + )) |deco_manager| { + context.kde_decoration_manager = deco_manager; + break :global; } }, @@ -97,6 +109,12 @@ pub const App = struct { } } + /// Bind a Wayland interface to a global object. Returns non-null + /// if the binding was successful, otherwise null. + /// + /// The type T is the Wayland interface type that we're requesting. + /// This function will verify that the global object is the correct + /// interface and version before binding. fn registryBind( comptime T: type, registry: *wl.Registry, @@ -130,14 +148,20 @@ pub const Window = struct { app_context: *App.Context, /// A token that, when present, indicates that the window is blurred. - blur_token: ?*org.KdeKwinBlur = null, + blur_token: ?*org.KdeKwinBlur, + + /// Object that controls the decoration mode (client/server/auto) + /// of the window. + decoration: ?*org.KdeKwinServerDecoration, const DerivedConfig = struct { blur: bool, + window_decoration: Config.WindowDecoration, pub fn init(config: *const Config) DerivedConfig { return .{ .blur = config.@"background-blur-radius".enabled(), + .window_decoration = config.@"window-decoration", }; } }; @@ -165,19 +189,41 @@ pub const Window = struct { gdk_surface, ) 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; + }; + return .{ .config = DerivedConfig.init(config), .surface = wl_surface, .app_context = app.context, + .blur_token = null, + .decoration = deco, }; } pub fn deinit(self: Window, alloc: Allocator) void { _ = alloc; if (self.blur_token) |blur| blur.release(); + if (self.decoration) |deco| deco.release(); } - pub fn updateConfigEvent(self: *Window, config: *const Config) !void { + pub fn updateConfigEvent( + self: *Window, + config: *const Config, + ) !void { self.config = DerivedConfig.init(config); } @@ -185,6 +231,17 @@ pub const Window = struct { pub fn syncAppearance(self: *Window) !void { try self.syncBlur(); + try self.syncDecoration(); + } + + pub fn clientSideDecorationEnabled(self: Window) bool { + // Note: we should change this to being the actual mode + // state emitted by the decoration manager. + + // We are CSD if we don't support the SSD Wayland protocol + // or if we do but we're in CSD mode. + return self.decoration == null or + self.config.window_decoration.isCSD(); } /// Update the blur state of the window. @@ -208,4 +265,18 @@ pub const Window = struct { } } } + + fn syncDecoration(self: *Window) !void { + const deco = self.decoration orelse return; + + const mode: org.KdeKwinServerDecoration.Mode = switch (self.config.window_decoration) { + .client => .Client, + .server => .Server, + .none => .None, + }; + + // The protocol requests uint instead of enum so we have + // to convert it. + deco.requestMode(@intCast(@intFromEnum(mode))); + } }; diff --git a/src/apprt/gtk/winproto/x11.zig b/src/apprt/gtk/winproto/x11.zig index 4eac9cdf3a..fe3b9218d4 100644 --- a/src/apprt/gtk/winproto/x11.zig +++ b/src/apprt/gtk/winproto/x11.zig @@ -161,10 +161,15 @@ pub const Window = struct { const DerivedConfig = struct { blur: bool, + has_decoration: bool, pub fn init(config: *const Config) DerivedConfig { return .{ .blur = config.@"background-blur-radius".enabled(), + .has_decoration = switch (config.@"window-decoration") { + .none => false, + .client, .server => true, + }, }; } }; @@ -239,6 +244,10 @@ pub const Window = struct { try self.syncBlur(); } + pub fn clientSideDecorationEnabled(self: Window) bool { + return self.config.has_decoration; + } + 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 diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 16e7381fab..64068658de 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -450,10 +450,14 @@ pub fn add( .target = target, .optimize = optimize, }); + + // 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.generate("wl_compositor", 1); scanner.generate("org_kde_kwin_blur_manager", 1); + scanner.generate("org_kde_kwin_server_decoration_manager", 1); step.root_module.addImport("wayland", wayland); step.linkSystemLibrary2("wayland-client", dynamic_link_opts); diff --git a/src/config/Config.zig b/src/config/Config.zig index 36b2a8494a..6ae8a353e0 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -282,7 +282,7 @@ const c = @cImport({ /// For example, a value of `1` increases the value by 1; it does not set it to /// literally 1. A value of `20%` increases the value by 20%. And so on. /// -/// There is little to no validation on these values so the wrong values (i.e. +/// There is little to no validation on these values so the wrong values (e.g. /// `-100%`) can cause the terminal to be unusable. Use with caution and reason. /// /// Some values are clamped to minimum or maximum values. This can make it @@ -467,7 +467,7 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// The minimum contrast ratio between the foreground and background colors. /// The contrast ratio is a value between 1 and 21. A value of 1 allows for no -/// contrast (i.e. black on black). This value is the contrast ratio as defined +/// contrast (e.g. black on black). This value is the contrast ratio as defined /// by the [WCAG 2.0 specification](https://www.w3.org/TR/WCAG20/). /// /// If you want to avoid invisible text (same color as background), a value of @@ -722,7 +722,7 @@ command: ?[]const u8 = null, /// injecting any configured shell integration into the command's /// environment. With `-e` its highly unlikely that you're executing a /// shell and forced shell integration is likely to cause problems -/// (i.e. by wrapping your command in a shell, setting env vars, etc.). +/// (e.g. by wrapping your command in a shell, setting env vars, etc.). /// This is a safety measure to prevent unexpected behavior. If you want /// shell integration with a `-e`-executed command, you must either /// name your binary appropriately or source the shell integration script @@ -770,7 +770,7 @@ command: ?[]const u8 = null, /// Match a regular expression against the terminal text and associate clicking /// it with an action. This can be used to match URLs, file paths, etc. Actions -/// can be opening using the system opener (i.e. `open` or `xdg-open`) or +/// can be opening using the system opener (e.g. `open` or `xdg-open`) or /// executing any arbitrary binding action. /// /// Links that are configured earlier take precedence over links that are @@ -876,7 +876,7 @@ class: ?[:0]const u8 = null, /// Valid keys are currently only listed in the /// [Ghostty source code](https://github.com/ghostty-org/ghostty/blob/d6e76858164d52cff460fedc61ddf2e560912d71/src/input/key.zig#L255). /// This is a documentation limitation and we will improve this in the future. -/// A common gotcha is that numeric keys are written as words: i.e. `one`, +/// A common gotcha is that numeric keys are written as words: e.g. `one`, /// `two`, `three`, etc. and not `1`, `2`, `3`. This will also be improved in /// the future. /// @@ -919,7 +919,7 @@ class: ?[:0]const u8 = null, /// * Ghostty will wait an indefinite amount of time for the next key in /// the sequence. There is no way to specify a timeout. The only way to /// force the output of a prefix key is to assign another keybind to -/// specifically output that key (i.e. `ctrl+a>ctrl+a=text:foo`) or +/// specifically output that key (e.g. `ctrl+a>ctrl+a=text:foo`) or /// press an unbound key which will send both keys to the program. /// /// * If a prefix in a sequence is previously bound, the sequence will @@ -949,13 +949,13 @@ class: ?[:0]const u8 = null, /// including `physical:`-prefixed triggers without specifying the /// prefix. /// -/// * `csi:text` - Send a CSI sequence. i.e. `csi:A` sends "cursor up". +/// * `csi:text` - Send a CSI sequence. e.g. `csi:A` sends "cursor up". /// -/// * `esc:text` - Send an escape sequence. i.e. `esc:d` deletes to the +/// * `esc:text` - Send an escape sequence. e.g. `esc:d` deletes to the /// end of the word to the right. /// /// * `text:text` - Send a string. Uses Zig string literal syntax. -/// i.e. `text:\x15` sends Ctrl-U. +/// e.g. `text:\x15` sends Ctrl-U. /// /// * All other actions can be found in the documentation or by using the /// `ghostty +list-actions` command. @@ -981,12 +981,12 @@ class: ?[:0]const u8 = null, /// keybinds only apply to the focused terminal surface. If this is true, /// then the keybind will be sent to all terminal surfaces. This only /// applies to actions that are surface-specific. For actions that -/// are already global (i.e. `quit`), this prefix has no effect. +/// are already global (e.g. `quit`), this prefix has no effect. /// /// * `global:` - Make the keybind global. By default, keybinds only work /// within Ghostty and under the right conditions (application focused, /// sometimes terminal focused, etc.). If you want a keybind to work -/// globally across your system (i.e. even when Ghostty is not focused), +/// globally across your system (e.g. even when Ghostty is not focused), /// specify this prefix. This prefix implies `all:`. Note: this does not /// work in all environments; see the additional notes below for more /// information. @@ -1087,7 +1087,7 @@ keybind: Keybinds = .{}, /// any of the heuristics that disable extending noted below. /// /// The "extend" value will be disabled in certain scenarios. On primary -/// screen applications (i.e. not something like Neovim), the color will not +/// screen applications (e.g. not something like Neovim), the color will not /// be extended vertically if any of the following are true: /// /// * The nearest row has any cells that have the default background color. @@ -1127,21 +1127,46 @@ keybind: Keybinds = .{}, /// configuration `font-size` will be used. @"window-inherit-font-size": bool = true, +/// Configure a preference for window decorations. This setting specifies +/// a _preference_; the actual OS, desktop environment, window manager, etc. +/// may override this preference. Ghostty will do its best to respect this +/// preference but it may not always be possible. +/// /// Valid values: /// -/// * `true` -/// * `false` - windows won't have native decorations, i.e. titlebar and -/// borders. On macOS this also disables tabs and tab overview. +/// * `none` - All window decorations will be disabled. Titlebar, +/// borders, etc. will not be shown. On macOS, this will also disable +/// tabs (enforced by the system). +/// +/// * `client` - Prefer client-side decorations. This is the default. +/// +/// * `server` - Prefer server-side decorations. This is only relevant +/// on Linux with GTK. This currently only works on Linux with Wayland +/// and the `org_kde_kwin_server_decoration` protocol available (e.g. +/// KDE Plasma, but almost any non-Gnome desktop supports this protocol). +/// +/// If `server` is set but the environment doesn't support server-side +/// decorations, client-side decorations will be used instead. +/// +/// The default value is `client`. +/// +/// This setting also accepts boolean true and false values. If set to `true`, +/// this is equivalent to `client`. If set to `false`, this is equivalent to +/// `none`. This is a convenience for users who live primarily on systems +/// that don't differentiate between client and server-side decorations +/// (e.g. macOS and Windows). /// /// The "toggle_window_decorations" keybind action can be used to create -/// a keybinding to toggle this setting at runtime. +/// a keybinding to toggle this setting at runtime. This will always toggle +/// back to "server" if the current value is "none" (this is an issue +/// that will be fixed in the future). /// /// Changing this configuration in your configuration and reloading will /// only affect new windows. Existing windows will not be affected. /// /// macOS: To hide the titlebar without removing the native window borders /// or rounded corners, use `macos-titlebar-style = hidden` instead. -@"window-decoration": bool = true, +@"window-decoration": WindowDecoration = .client, /// The font that will be used for the application's window and tab titles. /// @@ -1364,7 +1389,7 @@ keybind: Keybinds = .{}, @"resize-overlay-duration": Duration = .{ .duration = 750 * std.time.ns_per_ms }, /// If true, when there are multiple split panes, the mouse selects the pane -/// that is focused. This only applies to the currently focused window; i.e. +/// that is focused. This only applies to the currently focused window; e.g. /// mousing over a split in an unfocused window will not focus that split /// and bring the window to front. /// @@ -1408,7 +1433,7 @@ keybind: Keybinds = .{}, /// and a minor amount of user interaction). @"title-report": bool = false, -/// The total amount of bytes that can be used for image data (i.e. the Kitty +/// The total amount of bytes that can be used for image data (e.g. the Kitty /// image protocol) per terminal screen. The maximum value is 4,294,967,295 /// (4GiB). The default is 320MB. If this is set to zero, then all image /// protocols will be disabled. @@ -1668,7 +1693,7 @@ keybind: Keybinds = .{}, /// /// * `none` - OSC 4/10/11 queries receive no reply /// -/// * `8-bit` - Color components are return unscaled, i.e. `rr/gg/bb` +/// * `8-bit` - Color components are return unscaled, e.g. `rr/gg/bb` /// /// * `16-bit` - Color components are returned scaled, e.g. `rrrr/gggg/bbbb` /// @@ -1767,7 +1792,7 @@ keybind: Keybinds = .{}, /// typical for a macOS application and may not work well with all themes. /// /// The "transparent" style will also update in real-time to dynamic -/// changes to the window background color, i.e. via OSC 11. To make this +/// changes to the window background color, e.g. via OSC 11. To make this /// more aesthetically pleasing, this only happens if the terminal is /// a window, tab, or split that borders the top of the window. This /// avoids a disjointed appearance where the titlebar color changes @@ -1834,7 +1859,7 @@ keybind: Keybinds = .{}, /// - U.S. International /// /// Note that if an *Option*-sequence doesn't produce a printable character, it -/// will be treated as *Alt* regardless of this setting. (i.e. `alt+ctrl+a`). +/// will be treated as *Alt* regardless of this setting. (e.g. `alt+ctrl+a`). /// /// Explicit values that can be set: /// @@ -5890,6 +5915,62 @@ pub const BackgroundBlur = union(enum) { } }; +/// See window-decoration +pub const WindowDecoration = enum { + client, + server, + none, + + pub fn parseCLI(input: ?[]const u8) !WindowDecoration { + const input_ = input orelse return .client; + + return if (cli.args.parseBool(input_)) |b| + if (b) .client else .none + else |_| if (std.mem.eql(u8, input_, "server")) + .server + else + error.InvalidValue; + } + + /// Returns true if the window decoration setting results in + /// CSD (client-side decorations). Note that this only returns the + /// user requested behavior. Depending on available APIs (e.g. + /// Wayland protocols), the actual behavior may differ and the apprt + /// should rely on actual windowing APIs to determine the actual + /// status. + pub fn isCSD(self: WindowDecoration) bool { + return switch (self) { + .client => true, + .server, .none => false, + }; + } + + test "parse WindowDecoration" { + const testing = std.testing; + + { + const v = try WindowDecoration.parseCLI(null); + try testing.expectEqual(WindowDecoration.client, v); + } + { + const v = try WindowDecoration.parseCLI("true"); + try testing.expectEqual(WindowDecoration.client, v); + } + { + const v = try WindowDecoration.parseCLI("false"); + try testing.expectEqual(WindowDecoration.none, v); + } + { + const v = try WindowDecoration.parseCLI("server"); + try testing.expectEqual(WindowDecoration.server, v); + } + { + try testing.expectError(error.InvalidValue, WindowDecoration.parseCLI("")); + try testing.expectError(error.InvalidValue, WindowDecoration.parseCLI("aaaa")); + } + } +}; + /// See theme pub const Theme = struct { light: []const u8,