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 Version = @import("src/build/Version.zig");
const Command = @import("src/Command.zig"); const Command = @import("src/Command.zig");
const Scanner = @import("zig_wayland").Scanner;
comptime { comptime {
// This is the required Zig version for building this project. We allow // This is the required Zig version for building this project. We allow
// any patch version but the major and minor must match exactly. // 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.", "Enables the use of Adwaita when using the GTK rendering backend.",
) orelse true; ) orelse true;
config.x11 = b.option( var x11 = false;
bool, var wayland = false;
"gtk-x11",
"Enables linking against X11 libraries when using the GTK rendering backend.",
) orelse x11: {
if (target.result.os.tag != .linux) break :x11 false;
if (target.result.os.tag == .linux) pkgconfig: {
var pkgconfig = std.process.Child.init(&.{ "pkg-config", "--variable=targets", "gtk4" }, b.allocator); var pkgconfig = std.process.Child.init(&.{ "pkg-config", "--variable=targets", "gtk4" }, b.allocator);
pkgconfig.stdout_behavior = .Pipe; pkgconfig.stdout_behavior = .Pipe;
pkgconfig.stderr_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; const output_max_size = 50 * 1024;
@ -139,18 +141,31 @@ pub fn build(b: *std.Build) !void {
switch (term) { switch (term) {
.Exited => |code| { .Exited => |code| {
if (code == 0) { if (code == 0) {
if (std.mem.indexOf(u8, stdout.items, "x11")) |_| break :x11 true; if (std.mem.indexOf(u8, stdout.items, "x11")) |_| x11 = true;
break :x11 false; if (std.mem.indexOf(u8, stdout.items, "wayland")) |_| wayland = true;
} } else {
std.log.warn("pkg-config: {s} with code {d}", .{ @tagName(term), code }); std.log.warn("pkg-config: {s} with code {d}", .{ @tagName(term), code });
break :x11 false; return error.Unexpected;
}
}, },
inline else => |code| { inline else => |code| {
std.log.warn("pkg-config: {s} with code {d}", .{ @tagName(term), code }); std.log.warn("pkg-config: {s} with code {d}", .{ @tagName(term), code });
return error.Unexpected; 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( config.sentry = b.option(
bool, bool,
@ -1459,6 +1474,24 @@ fn addDeps(
if (config.adwaita) step.linkSystemLibrary2("adwaita-1", dynamic_link_opts); if (config.adwaita) step.linkSystemLibrary2("adwaita-1", dynamic_link_opts);
if (config.x11) step.linkSystemLibrary2("X11", 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"); const gresource = @import("src/apprt/gtk/gresource.zig");

View File

@ -25,6 +25,10 @@
.url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz", .url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz",
.hash = "12207831bce7d4abce57b5a98e8f3635811cfefd160bca022eb91fe905d36a02cf25", .hash = "12207831bce7d4abce57b5a98e8f3635811cfefd160bca022eb91fe905d36a02cf25",
}, },
.zig_wayland = .{
.url = "https://codeberg.org/ifreund/zig-wayland/archive/a5e2e9b6a6d7fba638ace4d4b24a3b576a02685b.tar.gz",
.hash = "1220d41b23ae70e93355bb29dac1c07aa6aeb92427a2dffc4375e94b4de18111248c",
},
// C libs // C libs
.cimgui = .{ .path = "./pkg/cimgui" }, .cimgui = .{ .path = "./pkg/cimgui" },
@ -64,5 +68,9 @@
.url = "git+https://github.com/vancluever/z2d?ref=v0.4.0#4638bb02a9dc41cc2fb811f092811f6a951c752a", .url = "git+https://github.com/vancluever/z2d?ref=v0.4.0#4638bb02a9dc41cc2fb811f092811f6a951c752a",
.hash = "12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a", .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, pandoc,
hyperfine, hyperfine,
typos, typos,
wayland,
wayland-scanner,
wayland-protocols,
}: let }: let
# See package.nix. Keep in sync. # See package.nix. Keep in sync.
rpathLibs = rpathLibs =
@ -80,6 +83,7 @@
libadwaita libadwaita
gtk4 gtk4
glib glib
wayland
]; ];
in in
mkShell { mkShell {
@ -153,6 +157,9 @@ in
libadwaita libadwaita
gtk4 gtk4
glib glib
wayland
wayland-scanner
wayland-protocols
]; ];
# This should be set onto the rpath of the ghostty binary if you want # This should be set onto the rpath of the ghostty binary if you want

View File

@ -10,10 +10,6 @@
oniguruma, oniguruma,
zlib, zlib,
libGL, libGL,
libX11,
libXcursor,
libXi,
libXrandr,
glib, glib,
gtk4, gtk4,
libadwaita, libadwaita,
@ -26,7 +22,15 @@
pandoc, pandoc,
revision ? "dirty", revision ? "dirty",
optimize ? "Debug", optimize ? "Debug",
x11 ? true, enableX11 ? true,
libX11,
libXcursor,
libXi,
libXrandr,
enableWayland ? true,
wayland,
wayland-protocols,
wayland-scanner,
}: let }: let
# The Zig hook has no way to select the release type without actual # The Zig hook has no way to select the release type without actual
# overriding of the default flags. # overriding of the default flags.
@ -114,13 +118,18 @@ in
version = "1.0.2"; version = "1.0.2";
inherit src; inherit src;
nativeBuildInputs = [ nativeBuildInputs =
[
git git
ncurses ncurses
pandoc pandoc
pkg-config pkg-config
zig_hook zig_hook
wrapGAppsHook4 wrapGAppsHook4
]
++ lib.optionals enableWayland [
wayland-scanner
wayland-protocols
]; ];
buildInputs = buildInputs =
@ -142,16 +151,19 @@ in
glib glib
gsettings-desktop-schemas gsettings-desktop-schemas
] ]
++ lib.optionals x11 [ ++ lib.optionals enableX11 [
libX11 libX11
libXcursor libXcursor
libXi libXi
libXrandr libXrandr
]
++ lib.optionals enableWayland [
wayland
]; ];
dontConfigure = true; 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 = '' preBuild = ''
rm -rf $ZIG_GLOBAL_CACHE_DIR 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 # This file is auto-generated! check build-support/check-zig-cache-hash.sh for
# more details. # more details.
"sha256-l+tZVL18qhm8BoBsQVbKfYmXQVObD0QMzQe6VBM/8Oo=" "sha256-eUY6MS3//r6pA/w9b+E4+YqmqUbzpUfL3afJJlnMhLY="

View File

@ -1953,7 +1953,7 @@ pub const CAPI = struct {
_ = CGSSetWindowBackgroundBlurRadius( _ = CGSSetWindowBackgroundBlurRadius(
CGSDefaultConnectionForThread(), CGSDefaultConnectionForThread(),
nswindow.msgSend(usize, objc.sel("windowNumber"), .{}), 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 inspector = @import("inspector.zig");
const key = @import("key.zig"); const key = @import("key.zig");
const x11 = @import("x11.zig"); const x11 = @import("x11.zig");
const wayland = @import("wayland.zig");
const testing = std.testing; const testing = std.testing;
const log = std.log.scoped(.gtk); const log = std.log.scoped(.gtk);
@ -73,6 +74,9 @@ running: bool = true,
/// Xkb state (X11 only). Will be null on Wayland. /// Xkb state (X11 only). Will be null on Wayland.
x11_xkb: ?x11.Xkb = null, 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 /// The base path of the transient cgroup used to put all surfaces
/// into their own cgroup. This is only set if cgroups are enabled /// into their own cgroup. This is only set if cgroups are enabled
/// and initialization was successful. /// 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); 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 // 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 // routine so we just call it, but only if the config allows it (this allows
// for launching Ghostty in the "background" without immediately opening // for launching Ghostty in the "background" without immediately opening
@ -422,6 +430,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
.ctx = ctx, .ctx = ctx,
.cursor_none = cursor_none, .cursor_none = cursor_none,
.x11_xkb = x11_xkb, .x11_xkb = x11_xkb,
.wayland = wl,
.single_instance = single_instance, .single_instance = single_instance,
// If we are NOT the primary instance, then we never want to run. // 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 // This means that another instance of the GTK app is running and
@ -838,9 +847,11 @@ fn configChange(
new_config: *const Config, new_config: *const Config,
) void { ) void {
switch (target) { switch (target) {
// We don't do anything for surface config change events. There .surface => |surface| {
// is nothing to sync with regards to a surface today. if (surface.rt_surface.container.window()) |window| window.syncAppearance(new_config) catch |err| {
.surface => {}, log.warn("error syncing appearance changes to window err={}", .{err});
};
},
.app => { .app => {
// We clone (to take ownership) and update our configuration. // 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 Notebook = @import("notebook.zig").Notebook;
const HeaderBar = @import("headerbar.zig").HeaderBar; const HeaderBar = @import("headerbar.zig").HeaderBar;
const version = @import("version.zig"); const version = @import("version.zig");
const wayland = @import("wayland.zig");
const log = std.log.scoped(.gtk); const log = std.log.scoped(.gtk);
@ -55,6 +56,8 @@ toast_overlay: ?*c.GtkWidget,
/// See adwTabOverviewOpen for why we have this. /// See adwTabOverviewOpen for why we have this.
adw_tab_overview_focus_timer: ?c.guint = null, adw_tab_overview_focus_timer: ?c.guint = null,
wayland: ?wayland.SurfaceState,
pub fn create(alloc: Allocator, app: *App) !*Window { pub fn create(alloc: Allocator, app: *App) !*Window {
// Allocate a fixed pointer for our window. We try to minimize // Allocate a fixed pointer for our window. We try to minimize
// allocations but windows and other GUI requirements are so minimal // allocations but windows and other GUI requirements are so minimal
@ -79,6 +82,7 @@ pub fn init(self: *Window, app: *App) !void {
.notebook = undefined, .notebook = undefined,
.context_menu = undefined, .context_menu = undefined,
.toast_overlay = undefined, .toast_overlay = undefined,
.wayland = null,
}; };
// Create the window // 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"); 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. // Create our box which will hold our widgets in the main content area.
const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0); 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 // 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(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, "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(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); _ = 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); 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 /// 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 /// menus and such. The menu is defined in App.zig but the action is defined
/// here. The string name binds them. /// here. The string name binds them.
@ -424,6 +446,8 @@ fn initActions(self: *Window) void {
pub fn deinit(self: *Window) void { pub fn deinit(self: *Window) void {
c.gtk_widget_unparent(@ptrCast(self.context_menu)); c.gtk_widget_unparent(@ptrCast(self.context_menu));
if (self.wayland) |*wl| wl.deinit();
if (self.adw_tab_overview_focus_timer) |timer| { if (self.adw_tab_overview_focus_timer) |timer| {
_ = c.g_source_remove(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); 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 // Note: we MUST NOT use the GtkButton parameter because gtkActionNewTab
// sends an undefined value. // sends an undefined value.
fn gtkTabNewClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void { fn gtkTabNewClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {

View File

@ -14,6 +14,9 @@ pub const c = @cImport({
// Xkb for X11 state handling // Xkb for X11 state handling
@cInclude("X11/XKBlib.h"); @cInclude("X11/XKBlib.h");
} }
if (build_options.wayland) {
@cInclude("gdk/wayland/gdkwayland.h");
}
// generated header files // generated header files
@cInclude("ghostty_resources.h"); @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, flatpak: bool = false,
adwaita: bool = false, adwaita: bool = false,
x11: bool = false, x11: bool = false,
wayland: bool = false,
sentry: bool = true, sentry: bool = true,
app_runtime: apprt.Runtime = .none, app_runtime: apprt.Runtime = .none,
renderer: rendererpkg.Impl = .opengl, renderer: rendererpkg.Impl = .opengl,
@ -44,6 +45,7 @@ pub const BuildConfig = struct {
step.addOption(bool, "flatpak", self.flatpak); step.addOption(bool, "flatpak", self.flatpak);
step.addOption(bool, "adwaita", self.adwaita); step.addOption(bool, "adwaita", self.adwaita);
step.addOption(bool, "x11", self.x11); step.addOption(bool, "x11", self.x11);
step.addOption(bool, "wayland", self.wayland);
step.addOption(bool, "sentry", self.sentry); step.addOption(bool, "sentry", self.sentry);
step.addOption(apprt.Runtime, "app_runtime", self.app_runtime); step.addOption(apprt.Runtime, "app_runtime", self.app_runtime);
step.addOption(font.Backend, "font_backend", self.font_backend); step.addOption(font.Backend, "font_backend", self.font_backend);

View File

@ -533,7 +533,7 @@ fn parsePackedStruct(comptime T: type, v: []const u8) !T {
return result; return result;
} }
fn parseBool(v: []const u8) !bool { pub fn parseBool(v: []const u8) !bool {
const t = &[_][]const u8{ "1", "t", "T", "true" }; const t = &[_][]const u8{ "1", "t", "T", "true" };
const f = &[_][]const u8{ "0", "f", "F", "false" }; const f = &[_][]const u8{ "0", "f", "F", "false" };

View File

@ -68,6 +68,14 @@ pub fn run(alloc: Allocator) !u8 {
} else { } else {
try stdout.print(" - libX11 : disabled\n", .{}); 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; return 0;
} }

View File

@ -592,13 +592,38 @@ palette: Palette = .{},
/// On macOS, changing this configuration requires restarting Ghostty completely. /// On macOS, changing this configuration requires restarting Ghostty completely.
@"background-opacity": f64 = 1.0, @"background-opacity": f64 = 1.0,
/// A positive value enables blurring of the background when background-opacity /// Whether to blur the background when `background-opacity` is less than 1.
/// 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.
/// ///
/// This is only supported on macOS. /// Valid values are:
@"background-blur-radius": u8 = 0, ///
/// * 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. /// The opacity level (opposite of transparency) of an unfocused split.
/// Unfocused splits by default are slightly faded out to make it easier to see /// Unfocused splits by default are slightly faded out to make it easier to see
@ -5647,6 +5672,70 @@ pub const AutoUpdate = enum {
download, 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 /// See theme
pub const Theme = struct { pub const Theme = struct {
light: []const u8, light: []const u8,

View File

@ -84,6 +84,17 @@ fn getValue(ptr_raw: *anyopaque, value: anytype) bool {
ptr.* = @intCast(@as(Backing, @bitCast(value))); 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, else => return false,
}, },
} }
@ -172,3 +183,30 @@ test "c_get: optional" {
try testing.expectEqual(0, cval.b); 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);
}
}