From 31439f311d511421690cd134d9f613960ea3de33 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Thu, 2 Jan 2025 21:44:16 +0800 Subject: [PATCH 1/8] build: add wayland --- build.zig | 50 ++++++++++++++++------ build.zig.zon | 8 ++++ nix/devShell.nix | 7 +++ nix/package.nix | 44 ++++++++++++------- src/apprt/gtk/App.zig | 9 ++++ src/apprt/gtk/Window.zig | 17 ++++++++ src/apprt/gtk/c.zig | 3 ++ src/apprt/gtk/wayland.zig | 90 +++++++++++++++++++++++++++++++++++++++ src/build_config.zig | 2 + src/cli/version.zig | 8 ++++ 10 files changed, 211 insertions(+), 27 deletions(-) create mode 100644 src/apprt/gtk/wayland.zig diff --git a/build.zig b/build.zig index d92d3e719..6c030a9c5 100644 --- a/build.zig +++ b/build.zig @@ -24,6 +24,8 @@ const XCFrameworkStep = @import("src/build/XCFrameworkStep.zig"); const Version = @import("src/build/Version.zig"); const Command = @import("src/Command.zig"); +const Scanner = @import("zig_wayland").Scanner; + comptime { // This is the required Zig version for building this project. We allow // any patch version but the major and minor must match exactly. @@ -105,19 +107,19 @@ pub fn build(b: *std.Build) !void { "Enables the use of Adwaita when using the GTK rendering backend.", ) orelse true; - config.x11 = b.option( - bool, - "gtk-x11", - "Enables linking against X11 libraries when using the GTK rendering backend.", - ) orelse x11: { - if (target.result.os.tag != .linux) break :x11 false; + var x11 = false; + var wayland = false; + if (target.result.os.tag == .linux) pkgconfig: { var pkgconfig = std.process.Child.init(&.{ "pkg-config", "--variable=targets", "gtk4" }, b.allocator); pkgconfig.stdout_behavior = .Pipe; pkgconfig.stderr_behavior = .Pipe; - try pkgconfig.spawn(); + pkgconfig.spawn() catch |err| { + std.log.warn("failed to spawn pkg-config - disabling X11 and Wayland integrations: {}", .{err}); + break :pkgconfig; + }; const output_max_size = 50 * 1024; @@ -139,18 +141,31 @@ pub fn build(b: *std.Build) !void { switch (term) { .Exited => |code| { if (code == 0) { - if (std.mem.indexOf(u8, stdout.items, "x11")) |_| break :x11 true; - break :x11 false; + if (std.mem.indexOf(u8, stdout.items, "x11")) |_| x11 = true; + if (std.mem.indexOf(u8, stdout.items, "wayland")) |_| wayland = true; + } else { + std.log.warn("pkg-config: {s} with code {d}", .{ @tagName(term), code }); + return error.Unexpected; } - std.log.warn("pkg-config: {s} with code {d}", .{ @tagName(term), code }); - break :x11 false; }, inline else => |code| { std.log.warn("pkg-config: {s} with code {d}", .{ @tagName(term), code }); return error.Unexpected; }, } - }; + } + + config.x11 = b.option( + bool, + "gtk-x11", + "Enables linking against X11 libraries when using the GTK rendering backend.", + ) orelse x11; + + config.wayland = b.option( + bool, + "gtk-wayland", + "Enables linking against Wayland libraries when using the GTK rendering backend.", + ) orelse wayland; config.sentry = b.option( bool, @@ -1459,6 +1474,17 @@ fn addDeps( if (config.adwaita) step.linkSystemLibrary2("adwaita-1", dynamic_link_opts); if (config.x11) step.linkSystemLibrary2("X11", dynamic_link_opts); + if (config.wayland) { + const scanner = Scanner.create(b, .{}); + + const wayland = b.createModule(.{ .root_source_file = scanner.result }); + + scanner.generate("wl_compositor", 1); + + step.root_module.addImport("wayland", wayland); + step.linkSystemLibrary2("wayland-client", dynamic_link_opts); + } + { const gresource = @import("src/apprt/gtk/gresource.zig"); diff --git a/build.zig.zon b/build.zig.zon index 4a6fdb4b1..33be26193 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -25,6 +25,10 @@ .url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz", .hash = "12207831bce7d4abce57b5a98e8f3635811cfefd160bca022eb91fe905d36a02cf25", }, + .zig_wayland = .{ + .url = "https://codeberg.org/ifreund/zig-wayland/archive/a5e2e9b6a6d7fba638ace4d4b24a3b576a02685b.tar.gz", + .hash = "1220d41b23ae70e93355bb29dac1c07aa6aeb92427a2dffc4375e94b4de18111248c", + }, // C libs .cimgui = .{ .path = "./pkg/cimgui" }, @@ -64,5 +68,9 @@ .url = "git+https://github.com/vancluever/z2d?ref=v0.4.0#4638bb02a9dc41cc2fb811f092811f6a951c752a", .hash = "12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a", }, + .plasma_wayland_protocols = .{ + .url = "git+https://invent.kde.org/libraries/plasma-wayland-protocols.git?ref=master#db525e8f9da548cffa2ac77618dd0fbe7f511b86", + .hash = "12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566", + }, }, } diff --git a/nix/devShell.nix b/nix/devShell.nix index 5e86427fe..c52afb6c0 100644 --- a/nix/devShell.nix +++ b/nix/devShell.nix @@ -51,6 +51,9 @@ pandoc, hyperfine, typos, + wayland, + wayland-scanner, + wayland-protocols, }: let # See package.nix. Keep in sync. rpathLibs = @@ -80,6 +83,7 @@ libadwaita gtk4 glib + wayland ]; in mkShell { @@ -153,6 +157,9 @@ in libadwaita gtk4 glib + wayland + wayland-scanner + wayland-protocols ]; # This should be set onto the rpath of the ghostty binary if you want diff --git a/nix/package.nix b/nix/package.nix index 78d2e2fdd..1155b76b6 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -10,10 +10,6 @@ oniguruma, zlib, libGL, - libX11, - libXcursor, - libXi, - libXrandr, glib, gtk4, libadwaita, @@ -26,7 +22,17 @@ pandoc, revision ? "dirty", optimize ? "Debug", - x11 ? true, + + enableX11 ? true, + libX11, + libXcursor, + libXi, + libXrandr, + + enableWayland ? true, + wayland, + wayland-protocols, + wayland-scanner, }: let # The Zig hook has no way to select the release type without actual # overriding of the default flags. @@ -114,14 +120,19 @@ in version = "1.0.2"; inherit src; - nativeBuildInputs = [ - git - ncurses - pandoc - pkg-config - zig_hook - wrapGAppsHook4 - ]; + nativeBuildInputs = + [ + git + ncurses + pandoc + pkg-config + zig_hook + wrapGAppsHook4 + ] + ++ lib.optionals enableWayland [ + wayland-scanner + wayland-protocols + ]; buildInputs = [ @@ -142,16 +153,19 @@ in glib gsettings-desktop-schemas ] - ++ lib.optionals x11 [ + ++ lib.optionals enableX11 [ libX11 libXcursor libXi libXrandr + ] + ++ lib.optionals enableWayland [ + wayland ]; dontConfigure = true; - zigBuildFlags = "-Dversion-string=${finalAttrs.version}-${revision}-nix -Dgtk-x11=${lib.boolToString x11}"; + zigBuildFlags = "-Dversion-string=${finalAttrs.version}-${revision}-nix -Dgtk-x11=${lib.boolToString enableX11} -Dgtk-wayland=${lib.boolToString enableWayland}"; preBuild = '' rm -rf $ZIG_GLOBAL_CACHE_DIR diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 6a1c089e5..b43f79274 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -37,6 +37,7 @@ const version = @import("version.zig"); const inspector = @import("inspector.zig"); const key = @import("key.zig"); const x11 = @import("x11.zig"); +const wayland = @import("wayland.zig"); const testing = std.testing; const log = std.log.scoped(.gtk); @@ -73,6 +74,9 @@ running: bool = true, /// Xkb state (X11 only). Will be null on Wayland. x11_xkb: ?x11.Xkb = null, +/// Wayland app state. Will be null on X11. +wayland: ?wayland.AppState = null, + /// The base path of the transient cgroup used to put all surfaces /// into their own cgroup. This is only set if cgroups are enabled /// and initialization was successful. @@ -397,6 +401,10 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { break :x11_xkb try x11.Xkb.init(display); }; + // Initialize Wayland state + var wl = wayland.AppState.init(display); + if (wl) |*w| try w.register(); + // This just calls the `activate` signal but its part of the normal startup // routine so we just call it, but only if the config allows it (this allows // for launching Ghostty in the "background" without immediately opening @@ -422,6 +430,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { .ctx = ctx, .cursor_none = cursor_none, .x11_xkb = x11_xkb, + .wayland = wl, .single_instance = single_instance, // If we are NOT the primary instance, then we never want to run. // This means that another instance of the GTK app is running and diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 9b9e26383..d41fda138 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -25,6 +25,7 @@ const gtk_key = @import("key.zig"); const Notebook = @import("notebook.zig").Notebook; const HeaderBar = @import("headerbar.zig").HeaderBar; const version = @import("version.zig"); +const wayland = @import("wayland.zig"); const log = std.log.scoped(.gtk); @@ -55,6 +56,8 @@ toast_overlay: ?*c.GtkWidget, /// See adwTabOverviewOpen for why we have this. adw_tab_overview_focus_timer: ?c.guint = null, +wayland: ?wayland.SurfaceState, + pub fn create(alloc: Allocator, app: *App) !*Window { // Allocate a fixed pointer for our window. We try to minimize // allocations but windows and other GUI requirements are so minimal @@ -79,6 +82,7 @@ pub fn init(self: *Window, app: *App) !void { .notebook = undefined, .context_menu = undefined, .toast_overlay = undefined, + .wayland = null, }; // Create the window @@ -290,6 +294,7 @@ pub fn init(self: *Window, app: *App) !void { // All of our events _ = c.g_signal_connect_data(self.context_menu, "closed", c.G_CALLBACK(>kRefocusTerm), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(window, "realize", c.G_CALLBACK(>kRealize), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(window, "close-request", c.G_CALLBACK(>kCloseRequest), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(ec_key_press, "key-pressed", c.G_CALLBACK(>kKeyPressed), self, null, c.G_CONNECT_DEFAULT); @@ -424,6 +429,8 @@ fn initActions(self: *Window) void { pub fn deinit(self: *Window) void { c.gtk_widget_unparent(@ptrCast(self.context_menu)); + if (self.wayland) |*wl| wl.deinit(); + if (self.adw_tab_overview_focus_timer) |timer| { _ = c.g_source_remove(timer); } @@ -551,6 +558,16 @@ pub fn sendToast(self: *Window, title: [:0]const u8) void { c.adw_toast_overlay_add_toast(@ptrCast(toast_overlay), toast); } +fn gtkRealize(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool { + const self = userdataSelf(ud.?); + + if (self.app.wayland) |*wl| { + self.wayland = wayland.SurfaceState.init(v, wl); + } + + return true; +} + // 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/apprt/gtk/c.zig b/src/apprt/gtk/c.zig index abd4821d3..dde99c78e 100644 --- a/src/apprt/gtk/c.zig +++ b/src/apprt/gtk/c.zig @@ -14,6 +14,9 @@ pub const c = @cImport({ // Xkb for X11 state handling @cInclude("X11/XKBlib.h"); } + if (build_options.wayland) { + @cInclude("gdk/wayland/gdkwayland.h"); + } // generated header files @cInclude("ghostty_resources.h"); diff --git a/src/apprt/gtk/wayland.zig b/src/apprt/gtk/wayland.zig new file mode 100644 index 000000000..034309812 --- /dev/null +++ b/src/apprt/gtk/wayland.zig @@ -0,0 +1,90 @@ +const std = @import("std"); +const c = @import("c.zig").c; +const wayland = @import("wayland"); +const wl = wayland.client.wl; +const build_options = @import("build_options"); + +const log = std.log.scoped(.gtk_wayland); + +/// Wayland state that contains application-wide Wayland objects (e.g. wl_display). +pub const AppState = struct { + display: *wl.Display, + + pub fn init(display: ?*c.GdkDisplay) ?AppState { + if (comptime !build_options.wayland) return null; + + // It should really never be null + const display_ = display orelse return null; + + // Check if we're actually on Wayland + if (c.g_type_check_instance_is_a( + @ptrCast(@alignCast(display_)), + c.gdk_wayland_display_get_type(), + ) == 0) + return null; + + const wl_display: *wl.Display = @ptrCast(c.gdk_wayland_display_get_wl_display(display_) orelse return null); + + return .{ + .display = wl_display, + }; + } + + pub fn register(self: *AppState) !void { + const registry = try self.display.getRegistry(); + + registry.setListener(*AppState, registryListener, self); + if (self.display.roundtrip() != .SUCCESS) return error.RoundtripFailed; + + log.debug("app wayland init={}", .{self}); + } +}; + +/// Wayland state that contains Wayland objects associated with a window (e.g. wl_surface). +pub const SurfaceState = struct { + app_state: *AppState, + surface: *wl.Surface, + + pub fn init(window: *c.GtkWindow, app_state: *AppState) ?SurfaceState { + if (comptime !build_options.wayland) return null; + + const surface = c.gtk_native_get_surface(@ptrCast(window)) orelse return null; + + // Check if we're actually on Wayland + if (c.g_type_check_instance_is_a( + @ptrCast(@alignCast(surface)), + c.gdk_wayland_surface_get_type(), + ) == 0) + return null; + + const wl_surface: *wl.Surface = @ptrCast(c.gdk_wayland_surface_get_wl_surface(surface) orelse return null); + + return .{ + .app_state = app_state, + .surface = wl_surface, + }; + } + + pub fn deinit(self: *SurfaceState) void { + } +}; + +fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, state: *AppState) void { + switch (event) { + .global => |global| { + log.debug("got global interface={s}", .{global.interface}); + }, + .global_remove => {}, + } +} + +fn bindInterface(comptime T: type, registry: *wl.Registry, global: anytype, version: u32) ?*T { + if (std.mem.orderZ(u8, global.interface, T.interface.name) == .eq) { + return registry.bind(global.name, T, version) catch |err| { + log.warn("encountered error={} while binding interface {s}", .{ err, global.interface }); + return null; + }; + } else { + return null; + } +} diff --git a/src/build_config.zig b/src/build_config.zig index c70615144..13131c132 100644 --- a/src/build_config.zig +++ b/src/build_config.zig @@ -23,6 +23,7 @@ pub const BuildConfig = struct { flatpak: bool = false, adwaita: bool = false, x11: bool = false, + wayland: bool = false, sentry: bool = true, app_runtime: apprt.Runtime = .none, renderer: rendererpkg.Impl = .opengl, @@ -44,6 +45,7 @@ pub const BuildConfig = struct { step.addOption(bool, "flatpak", self.flatpak); step.addOption(bool, "adwaita", self.adwaita); step.addOption(bool, "x11", self.x11); + step.addOption(bool, "wayland", self.wayland); 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/cli/version.zig b/src/cli/version.zig index 99f03384b..b00152589 100644 --- a/src/cli/version.zig +++ b/src/cli/version.zig @@ -68,6 +68,14 @@ pub fn run(alloc: Allocator) !u8 { } else { try stdout.print(" - libX11 : disabled\n", .{}); } + + // We say `libwayland` since it is possible to build Ghostty without + // Wayland integration but with Wayland-enabled GTK + if (comptime build_options.wayland) { + try stdout.print(" - libwayland : enabled\n", .{}); + } else { + try stdout.print(" - libwayland : disabled\n", .{}); + } } return 0; } From 9184395cbaf7c5db4c87dba2493328173eececa5 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Thu, 2 Jan 2025 21:44:16 +0800 Subject: [PATCH 2/8] gtk(wayland): add support for background blur on KDE Plasma --- build.zig | 7 +++++++ src/apprt/gtk/App.zig | 8 +++++--- src/apprt/gtk/Window.zig | 15 +++++++++++++++ src/apprt/gtk/wayland.zig | 35 +++++++++++++++++++++++++++++++++++ src/config/Config.zig | 20 ++++++++++++++++++-- 5 files changed, 80 insertions(+), 5 deletions(-) diff --git a/build.zig b/build.zig index 6c030a9c5..ceb7ed381 100644 --- a/build.zig +++ b/build.zig @@ -1479,7 +1479,14 @@ fn addDeps( const wayland = b.createModule(.{ .root_source_file = scanner.result }); + const plasma_wayland_protocols = b.dependency("plasma_wayland_protocols", .{ + .target = target, + .optimize = optimize, + }); + scanner.addCustomProtocol(plasma_wayland_protocols.path("src/protocols/blur.xml")); + scanner.generate("wl_compositor", 1); + scanner.generate("org_kde_kwin_blur_manager", 1); step.root_module.addImport("wayland", wayland); step.linkSystemLibrary2("wayland-client", dynamic_link_opts); diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index b43f79274..3cc1782c8 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -847,9 +847,11 @@ fn configChange( new_config: *const Config, ) void { switch (target) { - // We don't do anything for surface config change events. There - // is nothing to sync with regards to a surface today. - .surface => {}, + .surface => |surface| { + if (surface.rt_surface.container.window()) |window| window.syncAppearance(new_config) catch |err| { + log.warn("error syncing appearance changes to window err={}", .{err}); + }; + }, .app => { // We clone (to take ownership) and update our configuration. diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index d41fda138..26598d03a 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -392,6 +392,17 @@ pub fn init(self: *Window, app: *App) !void { c.gtk_widget_show(window); } +/// Updates appearance based on config settings. Will be called once upon window +/// realization, and every time the config is reloaded. +/// +/// 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 (self.wayland) |*wl| { + try wl.setBlur(config.@"background-blur-radius" > 0); + } +} + /// Sets up the GTK actions for the window scope. Actions are how GTK handles /// menus and such. The menu is defined in App.zig but the action is defined /// here. The string name binds them. @@ -565,6 +576,10 @@ fn gtkRealize(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool { self.wayland = wayland.SurfaceState.init(v, wl); } + self.syncAppearance(&self.app.config) catch |err| { + log.err("failed to initialize appearance={}", .{err}); + }; + return true; } diff --git a/src/apprt/gtk/wayland.zig b/src/apprt/gtk/wayland.zig index 034309812..92446cc46 100644 --- a/src/apprt/gtk/wayland.zig +++ b/src/apprt/gtk/wayland.zig @@ -2,6 +2,7 @@ const std = @import("std"); const c = @import("c.zig").c; const wayland = @import("wayland"); const wl = wayland.client.wl; +const org = wayland.client.org; const build_options = @import("build_options"); const log = std.log.scoped(.gtk_wayland); @@ -9,6 +10,7 @@ const log = std.log.scoped(.gtk_wayland); /// Wayland state that contains application-wide Wayland objects (e.g. wl_display). pub const AppState = struct { display: *wl.Display, + blur_manager: ?*org.KdeKwinBlurManager = null, pub fn init(display: ?*c.GdkDisplay) ?AppState { if (comptime !build_options.wayland) return null; @@ -45,6 +47,9 @@ pub const SurfaceState = struct { app_state: *AppState, surface: *wl.Surface, + /// A token that, when present, indicates that the window is blurred. + blur_token: ?*org.KdeKwinBlur = null, + pub fn init(window: *c.GtkWindow, app_state: *AppState) ?SurfaceState { if (comptime !build_options.wayland) return null; @@ -66,6 +71,32 @@ pub const SurfaceState = struct { } pub fn deinit(self: *SurfaceState) void { + if (self.blur_token) |blur| blur.release(); + } + + pub fn setBlur(self: *SurfaceState, blurred: bool) !void { + log.debug("setting blur={}", .{blurred}); + + const mgr = self.app_state.blur_manager orelse { + log.warn("can't set blur: org_kde_kwin_blur_manager protocol unavailable", .{}); + return; + }; + + if (self.blur_token) |blur| { + // Only release token when transitioning from blurred -> not blurred + if (!blurred) { + mgr.unset(self.surface); + blur.release(); + self.blur_token = null; + } + } else { + // Only acquire token when transitioning from not blurred -> blurred + if (blurred) { + const blur_token = try mgr.create(self.surface); + blur_token.commit(); + self.blur_token = blur_token; + } + } } }; @@ -73,6 +104,10 @@ fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, state: *Ap switch (event) { .global => |global| { log.debug("got global interface={s}", .{global.interface}); + if (bindInterface(org.KdeKwinBlurManager, registry, global, 1)) |iface| { + state.blur_manager = iface; + return; + } }, .global_remove => {}, } diff --git a/src/config/Config.zig b/src/config/Config.zig index e31b9c5e9..7c25d5095 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -583,11 +583,27 @@ palette: Palette = .{}, @"background-opacity": f64 = 1.0, /// A positive value enables blurring of the background when background-opacity -/// is less than 1. The value is the blur radius to apply. A value of 20 +/// is less than 1. +/// +/// On macOS, the value is the blur radius to apply. A value of 20 /// is reasonable for a good looking blur. Higher values will cause strange /// rendering issues as well as performance issues. /// -/// This is only supported on macOS. +/// On KDE Plasma under Wayland, the exact value is _ignored_ — the reason is +/// that KWin, the window compositor powering Plasma, only has one global blur +/// setting and does not allow applications to have individual blur settings. +/// +/// To configure KWin's global blur setting, open System Settings and go to +/// "Apps & Windows" > "Window Management" > "Desktop Effects" and select the +/// "Blur" plugin. If disabled, enable it by ticking the checkbox to the left. +/// Then click on the "Configure" button and there will be two sliders that +/// allow you to set background blur and noise strengths for all apps, +/// including Ghostty. +/// +/// All other Linux desktop environments are as of now unsupported. Users may +/// need to set environment-specific settings and/or install third-party plugins +/// in order to support background blur, as there isn't a unified interface for +/// doing so. @"background-blur-radius": u8 = 0, /// The opacity level (opposite of transparency) of an unfocused split. From cd90821b937ce4f9619eac2e5606012b155cefe0 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Thu, 2 Jan 2025 21:44:16 +0800 Subject: [PATCH 3/8] fix(gtk): adjust `background` CSS class dynamically on config reload Currently the `background` CSS class is added once on startup and never removed or re-added. This is problematic as that if Ghostty was started with an opaque window but then its config was reloaded with a `background-opacity` less than 1, the window won't actually become translucent, and it would only appear as if the background colors had become faded (because the window is still styled to be opaque). --- src/apprt/gtk/Window.zig | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 26598d03a..554584127 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -119,11 +119,6 @@ pub fn init(self: *Window, app: *App) !void { c.gtk_widget_add_css_class(@ptrCast(gtk_window), "window-theme-ghostty"); } - // Remove the window's background if any of the widgets need to be transparent - if (app.config.@"background-opacity" < 1) { - c.gtk_widget_remove_css_class(@ptrCast(window), "background"); - } - // Create our box which will hold our widgets in the main content area. const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0); @@ -398,6 +393,12 @@ pub fn init(self: *Window, app: *App) !void { /// 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"); + } + if (self.wayland) |*wl| { try wl.setBlur(config.@"background-blur-radius" > 0); } From f2c357a2099420043edcb26b38b142ff3da0259f Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Sat, 4 Jan 2025 14:11:35 +0800 Subject: [PATCH 4/8] config: allow booleans for `background-blur-radius` --- src/apprt/embedded.zig | 2 +- src/apprt/gtk/Window.zig | 7 +++- src/cli/args.zig | 2 +- src/config/Config.zig | 91 +++++++++++++++++++++++++++++++++++----- src/config/c_get.zig | 38 +++++++++++++++++ 5 files changed, 127 insertions(+), 13 deletions(-) diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 10d09988d..50d1e90e4 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1953,7 +1953,7 @@ pub const CAPI = struct { _ = CGSSetWindowBackgroundBlurRadius( CGSDefaultConnectionForThread(), nswindow.msgSend(usize, objc.sel("windowNumber"), .{}), - @intCast(config.@"background-blur-radius"), + @intCast(config.@"background-blur-radius".cval()), ); } diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 554584127..430a46f61 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -400,7 +400,12 @@ pub fn syncAppearance(self: *Window, config: *const configpkg.Config) !void { } if (self.wayland) |*wl| { - try wl.setBlur(config.@"background-blur-radius" > 0); + const blurred = switch (config.@"background-blur-radius") { + .false => false, + .true => true, + .value => |v| v > 0, + }; + try wl.setBlur(blurred); } } diff --git a/src/cli/args.zig b/src/cli/args.zig index be71b9096..23dcf7733 100644 --- a/src/cli/args.zig +++ b/src/cli/args.zig @@ -533,7 +533,7 @@ fn parsePackedStruct(comptime T: type, v: []const u8) !T { return result; } -fn parseBool(v: []const u8) !bool { +pub fn parseBool(v: []const u8) !bool { const t = &[_][]const u8{ "1", "t", "T", "true" }; const f = &[_][]const u8{ "0", "f", "F", "false" }; diff --git a/src/config/Config.zig b/src/config/Config.zig index 7c25d5095..60f396d62 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -582,29 +582,38 @@ palette: Palette = .{}, /// On macOS, changing this configuration requires restarting Ghostty completely. @"background-opacity": f64 = 1.0, -/// A positive value enables blurring of the background when background-opacity -/// is less than 1. +/// Whether to blur the background when `background-opacity` is less than 1. /// -/// On macOS, the value is the blur radius to apply. A value of 20 -/// is reasonable for a good looking blur. Higher values will cause strange -/// rendering issues as well as performance issues. +/// Valid values are: /// -/// On KDE Plasma under Wayland, the exact value is _ignored_ — the reason is -/// that KWin, the window compositor powering Plasma, only has one global blur -/// setting and does not allow applications to have individual blur settings. +/// * a nonnegative integer specifying the *blur intensity* +/// * `false`, equivalent to a blur intensity of 0 +/// * `true`, equivalent to the default blur intensity of 20, which is +/// reasonable for a good looking blur. Higher blur intensities may +/// cause strange rendering and performance issues. +/// +/// Supported on macOS and on some Linux desktop environments, including: +/// +/// * KDE Plasma (Wayland only) +/// +/// Warning: the exact blur intensity is _ignored_ under KDE Plasma, and setting +/// this setting to either `true` or any positive blur intensity value would +/// achieve the same effect. The reason is that KWin, the window compositor +/// powering Plasma, only has one global blur setting and does not allow +/// applications to specify individual blur settings. /// /// To configure KWin's global blur setting, open System Settings and go to /// "Apps & Windows" > "Window Management" > "Desktop Effects" and select the /// "Blur" plugin. If disabled, enable it by ticking the checkbox to the left. /// Then click on the "Configure" button and there will be two sliders that -/// allow you to set background blur and noise strengths for all apps, +/// allow you to set background blur and noise intensities for all apps, /// including Ghostty. /// /// All other Linux desktop environments are as of now unsupported. Users may /// need to set environment-specific settings and/or install third-party plugins /// in order to support background blur, as there isn't a unified interface for /// doing so. -@"background-blur-radius": u8 = 0, +@"background-blur-radius": BackgroundBlur = .false, /// The opacity level (opposite of transparency) of an unfocused split. /// Unfocused splits by default are slightly faded out to make it easier to see @@ -5653,6 +5662,68 @@ pub const AutoUpdate = enum { download, }; +/// See background-blur-radius +pub const BackgroundBlur = union(enum) { + false, + true, + value: u8, + + pub fn parseCLI(self: *BackgroundBlur, input: ?[]const u8) !void { + const input_ = input orelse { + // Emulate behavior for bools + self.* = .true; + return; + }; + + if (cli.args.parseBool(input_)) |b| { + self.* = if (b) .true else .false; + } else |_| { + const value = std.fmt.parseInt(u8, input_, 0) catch return error.InvalidValue; + self.* = .{ .value = value }; + } + } + + pub fn cval(self: BackgroundBlur) u8 { + return switch (self) { + .false => 0, + .true => 20, + .value => |v| v, + }; + } + + pub fn formatEntry( + self: BackgroundBlur, + formatter: anytype, + ) !void { + switch (self) { + .false => try formatter.formatEntry(bool, false), + .true => try formatter.formatEntry(bool, true), + .value => |v| try formatter.formatEntry(u8, v), + } + } + + test "parse BackgroundBlur" { + const testing = std.testing; + var v: BackgroundBlur = undefined; + + try v.parseCLI(null); + try testing.expectEqual(.true, v); + + try v.parseCLI("true"); + try testing.expectEqual(.true, v); + + try v.parseCLI("false"); + try testing.expectEqual(.false, v); + + try v.parseCLI("42"); + try testing.expectEqual(42, v.value); + + try testing.expectError(error.InvalidValue, v.parseCLI("")); + try testing.expectError(error.InvalidValue, v.parseCLI("aaaa")); + try testing.expectError(error.InvalidValue, v.parseCLI("420")); + } +}; + /// See theme pub const Theme = struct { light: []const u8, diff --git a/src/config/c_get.zig b/src/config/c_get.zig index d3f38415e..5b0db2531 100644 --- a/src/config/c_get.zig +++ b/src/config/c_get.zig @@ -84,6 +84,17 @@ fn getValue(ptr_raw: *anyopaque, value: anytype) bool { ptr.* = @intCast(@as(Backing, @bitCast(value))); }, + .Union => |_| { + if (@hasDecl(T, "cval")) { + const PtrT = @typeInfo(@TypeOf(T.cval)).Fn.return_type.?; + const ptr: *PtrT = @ptrCast(@alignCast(ptr_raw)); + ptr.* = value.cval(); + return true; + } + + return false; + }, + else => return false, }, } @@ -172,3 +183,30 @@ test "c_get: optional" { try testing.expectEqual(0, cval.b); } } + +test "c_get: background-blur" { + const testing = std.testing; + const alloc = testing.allocator; + + var c = try Config.default(alloc); + defer c.deinit(); + + { + c.@"background-blur-radius" = .false; + var cval: u8 = undefined; + try testing.expect(get(&c, .@"background-blur-radius", @ptrCast(&cval))); + try testing.expectEqual(0, cval); + } + { + c.@"background-blur-radius" = .true; + var cval: u8 = undefined; + try testing.expect(get(&c, .@"background-blur-radius", @ptrCast(&cval))); + try testing.expectEqual(20, cval); + } + { + c.@"background-blur-radius" = .{ .value = 42 }; + var cval: u8 = undefined; + try testing.expect(get(&c, .@"background-blur-radius", @ptrCast(&cval))); + try testing.expectEqual(42, cval); + } +} From 0ae8d9ed4211e1f1d795bb1f1256845482b0bff6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Jan 2025 12:30:10 -0800 Subject: [PATCH 5/8] nix: update hash --- nix/zigCacheHash.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/zigCacheHash.nix b/nix/zigCacheHash.nix index 0523f8e96..f2592adf4 100644 --- a/nix/zigCacheHash.nix +++ b/nix/zigCacheHash.nix @@ -1,3 +1,3 @@ # This file is auto-generated! check build-support/check-zig-cache-hash.sh for # more details. -"sha256-l+tZVL18qhm8BoBsQVbKfYmXQVObD0QMzQe6VBM/8Oo=" +"sha256-eUY6MS3//r6pA/w9b+E4+YqmqUbzpUfL3afJJlnMhLY=" From bb83a14d7a764bcc5061c7c475878ad3ff68b0f3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Jan 2025 12:35:41 -0800 Subject: [PATCH 6/8] config: minor config changes --- src/apprt/gtk/Window.zig | 2 +- src/config/Config.zig | 22 ++++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 430a46f61..63ee57d95 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -403,7 +403,7 @@ pub fn syncAppearance(self: *Window, config: *const configpkg.Config) !void { const blurred = switch (config.@"background-blur-radius") { .false => false, .true => true, - .value => |v| v > 0, + .radius => |v| v > 0, }; try wl.setBlur(blurred); } diff --git a/src/config/Config.zig b/src/config/Config.zig index 60f396d62..01cb924fc 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -5666,7 +5666,7 @@ pub const AutoUpdate = enum { pub const BackgroundBlur = union(enum) { false, true, - value: u8, + radius: u8, pub fn parseCLI(self: *BackgroundBlur, input: ?[]const u8) !void { const input_ = input orelse { @@ -5675,19 +5675,21 @@ pub const BackgroundBlur = union(enum) { return; }; - if (cli.args.parseBool(input_)) |b| { - self.* = if (b) .true else .false; - } else |_| { - const value = std.fmt.parseInt(u8, input_, 0) catch return error.InvalidValue; - self.* = .{ .value = value }; - } + self.* = if (cli.args.parseBool(input_)) |b| + if (b) .true else .false + else |_| + .{ .radius = std.fmt.parseInt( + u8, + input_, + 0, + ) catch return error.InvalidValue }; } pub fn cval(self: BackgroundBlur) u8 { return switch (self) { .false => 0, .true => 20, - .value => |v| v, + .radius => |v| v, }; } @@ -5698,7 +5700,7 @@ pub const BackgroundBlur = union(enum) { switch (self) { .false => try formatter.formatEntry(bool, false), .true => try formatter.formatEntry(bool, true), - .value => |v| try formatter.formatEntry(u8, v), + .radius => |v| try formatter.formatEntry(u8, v), } } @@ -5716,7 +5718,7 @@ pub const BackgroundBlur = union(enum) { try testing.expectEqual(.false, v); try v.parseCLI("42"); - try testing.expectEqual(42, v.value); + try testing.expectEqual(42, v.radius); try testing.expectError(error.InvalidValue, v.parseCLI("")); try testing.expectError(error.InvalidValue, v.parseCLI("aaaa")); From ce77b91bf67f4119783bc5b8ed8ccb3e74b35f6f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Jan 2025 12:37:51 -0800 Subject: [PATCH 7/8] nix fmt --- nix/package.nix | 2 -- 1 file changed, 2 deletions(-) diff --git a/nix/package.nix b/nix/package.nix index 1155b76b6..166a3c4fb 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -22,13 +22,11 @@ pandoc, revision ? "dirty", optimize ? "Debug", - enableX11 ? true, libX11, libXcursor, libXi, libXrandr, - enableWayland ? true, wayland, wayland-protocols, From 2fbe680aedc14b6272fe4221af0bb851d0afc0bd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 5 Jan 2025 12:38:20 -0800 Subject: [PATCH 8/8] config: fix tests --- src/config/c_get.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/c_get.zig b/src/config/c_get.zig index 5b0db2531..6804b0ae0 100644 --- a/src/config/c_get.zig +++ b/src/config/c_get.zig @@ -204,7 +204,7 @@ test "c_get: background-blur" { try testing.expectEqual(20, cval); } { - c.@"background-blur-radius" = .{ .value = 42 }; + c.@"background-blur-radius" = .{ .radius = 42 }; var cval: u8 = undefined; try testing.expect(get(&c, .@"background-blur-radius", @ptrCast(&cval))); try testing.expectEqual(42, cval);