diff --git a/build.zig b/build.zig index d92d3e719..ceb7ed381 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,24 @@ 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 }); + + 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); + } + { 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..166a3c4fb 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,15 @@ 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 +118,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 +151,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/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=" 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/App.zig b/src/apprt/gtk/App.zig index 6a1c089e5..3cc1782c8 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 @@ -838,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 9b9e26383..63ee57d95 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 @@ -115,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); @@ -290,6 +289,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); @@ -387,6 +387,28 @@ 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 (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| { + const blurred = switch (config.@"background-blur-radius") { + .false => false, + .true => true, + .radius => |v| v > 0, + }; + try wl.setBlur(blurred); + } +} + /// 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. @@ -424,6 +446,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 +575,20 @@ 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); + } + + self.syncAppearance(&self.app.config) catch |err| { + log.err("failed to initialize appearance={}", .{err}); + }; + + 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..92446cc46 --- /dev/null +++ b/src/apprt/gtk/wayland.zig @@ -0,0 +1,125 @@ +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); + +/// 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; + + // 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, + + /// 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; + + 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 { + 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; + } + } + } +}; + +fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, state: *AppState) void { + 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 => {}, + } +} + +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/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/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; } diff --git a/src/config/Config.zig b/src/config/Config.zig index 4198847d0..b0580cf20 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -592,13 +592,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. 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. +/// Whether to blur the background when `background-opacity` is less than 1. /// -/// This is only supported on macOS. -@"background-blur-radius": u8 = 0, +/// Valid values are: +/// +/// * 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 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": 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 @@ -5647,6 +5672,70 @@ pub const AutoUpdate = enum { download, }; +/// See background-blur-radius +pub const BackgroundBlur = union(enum) { + false, + true, + radius: u8, + + pub fn parseCLI(self: *BackgroundBlur, input: ?[]const u8) !void { + const input_ = input orelse { + // Emulate behavior for bools + self.* = .true; + return; + }; + + 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, + .radius => |v| v, + }; + } + + pub fn formatEntry( + self: BackgroundBlur, + formatter: anytype, + ) !void { + switch (self) { + .false => try formatter.formatEntry(bool, false), + .true => try formatter.formatEntry(bool, true), + .radius => |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.radius); + + 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..6804b0ae0 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" = .{ .radius = 42 }; + var cval: u8 = undefined; + try testing.expect(get(&c, .@"background-blur-radius", @ptrCast(&cval))); + try testing.expectEqual(42, cval); + } +}