gtk(wayland): add support for background blur on KDE Plasma (#4403)

Also establishes a foundation for Wayland support and fixes a minor bug
(GTK windows remaining opaque when `background-opacity` is set to 1 on
startup and later updated to less than 1 with a config reload)

Can't update the Zig cache hash myself since I'm currently in China and
my proxy's broken for some reason :(

See also #4361, part of #4626
This commit is contained in:
Mitchell Hashimoto
2025-01-05 12:53:39 -08:00
committed by GitHub
15 changed files with 418 additions and 44 deletions

View File

@ -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");

View File

@ -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",
},
},
}

View File

@ -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

View File

@ -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

View File

@ -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="

View File

@ -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()),
);
}

View File

@ -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.

View File

@ -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(&gtkRefocusTerm), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(window, "realize", c.G_CALLBACK(&gtkRealize), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(window, "close-request", c.G_CALLBACK(&gtkCloseRequest), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(&gtkDestroy), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(ec_key_press, "key-pressed", c.G_CALLBACK(&gtkKeyPressed), 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 {

View File

@ -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");

125
src/apprt/gtk/wayland.zig Normal file
View File

@ -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;
}
}

View File

@ -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);

View File

@ -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" };

View File

@ -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;
}

View File

@ -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,

View File

@ -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);
}
}