Merge remote-tracking branch 'origin/main' into close-split-gtk

# Conflicts:
#	po/hu.po
#	po/id.po
This commit is contained in:
Caleb
2026-03-25 16:24:11 -07:00
369 changed files with 25011 additions and 4443 deletions

View File

@@ -201,6 +201,9 @@ pub const Action = union(Key) {
/// Set the title of the target to the requested value.
set_title: SetTitle,
/// Set the tab title override for the target's tab.
set_tab_title: SetTitle,
/// Set the title of the target to a prompted value. It is up to
/// the apprt to prompt. The value specifies whether to prompt for the
/// surface title or the tab title.
@@ -375,6 +378,7 @@ pub const Action = union(Key) {
render_inspector,
desktop_notification,
set_title,
set_tab_title,
prompt_title,
pwd,
mouse_shape,

View File

@@ -50,10 +50,11 @@ pub const App = struct {
/// Callback called to handle an action.
action: *const fn (*App, apprt.Target.C, apprt.Action.C) callconv(.c) bool,
/// Read the clipboard value. The return value must be preserved
/// by the host until the next call. If there is no valid clipboard
/// value then this should return null.
read_clipboard: *const fn (SurfaceUD, c_int, *apprt.ClipboardRequest) callconv(.c) void,
/// Read the clipboard value. Returns true if the clipboard request
/// was started and complete_clipboard_request may be called with the
/// given state pointer. Returns false if the clipboard request couldn't
/// be started (such as when no text is available for a paste request).
read_clipboard: *const fn (SurfaceUD, c_int, *apprt.ClipboardRequest) callconv(.c) bool,
/// This may be called after a read clipboard call to request
/// confirmation that the clipboard value is safe to read. The embedder
@@ -512,7 +513,15 @@ pub const Surface = struct {
break :wd;
}
config.@"working-directory" = wd;
var wd_val: configpkg.WorkingDirectory = .{ .path = wd };
if (wd_val.finalize(config.arenaAlloc())) |_| {
config.@"working-directory" = wd_val;
} else |err| {
log.warn(
"error finalizing working directory config dir={s} err={}",
.{ wd_val.path, err },
);
}
}
}
@@ -672,14 +681,16 @@ pub const Surface = struct {
errdefer alloc.destroy(state_ptr);
state_ptr.* = state;
self.app.opts.read_clipboard(
const started = self.app.opts.read_clipboard(
self.userdata,
@intCast(@intFromEnum(clipboard_type)),
state_ptr,
);
if (!started) {
alloc.destroy(state_ptr);
return false;
}
// Embedded apprt can't synchronously check clipboard content types,
// so we always return true to indicate the request was started.
return true;
}

View File

@@ -13,4 +13,5 @@ test {
@import("std").testing.refAllDecls(@This());
_ = @import("gtk/ext.zig");
_ = @import("gtk/key.zig");
_ = @import("gtk/portal.zig");
}

View File

@@ -22,16 +22,10 @@ const log = std.log.scoped(.gtk);
pub const must_draw_from_app_thread = true;
/// GTK application ID
pub const application_id = switch (builtin.mode) {
.Debug, .ReleaseSafe => "com.mitchellh.ghostty-debug",
.ReleaseFast, .ReleaseSmall => "com.mitchellh.ghostty",
};
pub const application_id = @import("build/info.zig").application_id;
/// GTK object path
pub const object_path = switch (builtin.mode) {
.Debug, .ReleaseSafe => "/com/mitchellh/ghostty_debug",
.ReleaseFast, .ReleaseSmall => "/com/mitchellh/ghostty",
};
pub const object_path = @import("build/info.zig").object_path;
/// The GObject Application instance
app: *Application,

View File

@@ -7,9 +7,7 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
/// Prefix/appid for the gresource file.
pub const prefix = "/com/mitchellh/ghostty";
pub const app_id = "com.mitchellh.ghostty";
const build_info = @import("info.zig");
/// The path to the Blueprint files. The folder structure is expected to be
/// `{version}/{name}.blp` where `version` is the major and minor
@@ -112,7 +110,7 @@ pub fn blueprint(comptime bp: Blueprint) [:0]const u8 {
std.mem.eql(u8, candidate.name, bp.name))
{
return std.fmt.comptimePrint("{s}/ui/{d}.{d}/{s}.ui", .{
prefix,
build_info.resource_path,
candidate.major,
candidate.minor,
candidate.name,
@@ -173,7 +171,7 @@ fn genIcons(writer: *std.Io.Writer) !void {
try writer.print(
\\ <gresource prefix="{s}/icons">
\\
, .{prefix});
, .{build_info.resource_path});
const cwd = std.fs.cwd();
inline for (icon_sizes) |size| {
@@ -186,7 +184,7 @@ fn genIcons(writer: *std.Io.Writer) !void {
\\ <file alias="{s}/apps/{s}.png">{s}</file>
\\
,
.{ alias, app_id, source },
.{ alias, build_info.base_application_id, source },
);
}
@@ -199,7 +197,7 @@ fn genIcons(writer: *std.Io.Writer) !void {
\\ <file alias="{s}/apps/{s}.png">{s}</file>
\\
,
.{ alias, app_id, source },
.{ alias, build_info.base_application_id, source },
);
}
}
@@ -215,7 +213,7 @@ fn genRoot(writer: *std.Io.Writer) !void {
try writer.print(
\\ <gresource prefix="{s}">
\\
, .{prefix});
, .{build_info.resource_path});
const cwd = std.fs.cwd();
inline for (css) |name| {
@@ -249,7 +247,7 @@ fn genUi(
try writer.print(
\\ <gresource prefix="{s}/ui">
\\
, .{prefix});
, .{build_info.resource_path});
for (files.items) |ui_file| {
for (blueprints) |bp| {

View File

@@ -0,0 +1,18 @@
const builtin = @import("builtin");
/// Base application ID
pub const base_application_id = "com.mitchellh.ghostty";
/// GTK application ID
pub const application_id = switch (builtin.mode) {
.Debug, .ReleaseSafe => base_application_id ++ "-debug",
.ReleaseFast, .ReleaseSmall => base_application_id,
};
pub const resource_path = "/com/mitchellh/ghostty";
/// GTK object path
pub const object_path = switch (builtin.mode) {
.Debug, .ReleaseSafe => resource_path ++ "_debug",
.ReleaseFast, .ReleaseSmall => resource_path,
};

View File

@@ -9,6 +9,7 @@ const gobject = @import("gobject");
const gtk = @import("gtk");
const build_config = @import("../../../build_config.zig");
const build_info = @import("../build/info.zig");
const state = &@import("../../../global.zig").state;
const i18n = @import("../../../os/main.zig").i18n;
const apprt = @import("../../../apprt.zig");
@@ -40,6 +41,7 @@ const Tab = @import("tab.zig").Tab;
const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog;
const ConfigErrorsDialog = @import("config_errors_dialog.zig").ConfigErrorsDialog;
const GlobalShortcuts = @import("global_shortcuts.zig").GlobalShortcuts;
const OpenURI = @import("../portal.zig").OpenURI;
const log = std.log.scoped(.gtk_ghostty_application);
@@ -214,6 +216,8 @@ pub const Application = extern struct {
/// not exist in Ghostty's environment variable.
saved_language: ?[:0]const u8 = null,
open_uri: OpenURI = undefined,
pub var offset: c_int = 0;
};
@@ -327,7 +331,7 @@ pub const Application = extern struct {
}
}
break :app_id ApprtApp.application_id;
break :app_id build_info.application_id;
};
const display: *gdk.Display = gdk.Display.getDefault() orelse {
@@ -350,7 +354,7 @@ pub const Application = extern struct {
log.warn("error initializing windowing protocol err={}", .{err});
break :wp .{ .none = .{} };
};
errdefer wp.deinit(alloc);
errdefer wp.deinit();
log.debug("windowing protocol={s}", .{@tagName(wp)});
// Create our GTK Application which encapsulates our process.
@@ -381,7 +385,7 @@ pub const Application = extern struct {
// Force the resource path to a known value so it doesn't depend
// on the app id (which changes between debug/release and can be
// user-configured) and force it to load in compiled resources.
.resource_base_path = "/com/mitchellh/ghostty",
.resource_base_path = build_info.resource_path,
});
// Setup our private state. More setup is done in the init
@@ -397,6 +401,7 @@ pub const Application = extern struct {
.custom_css_providers = .empty,
.global_shortcuts = gobject.ext.newInstance(GlobalShortcuts, .{}),
.saved_language = saved_language,
.open_uri = .init(rt_app),
};
// Signals
@@ -431,7 +436,8 @@ pub const Application = extern struct {
const alloc = self.allocator();
const priv: *Private = self.private();
priv.config.unref();
priv.winproto.deinit(alloc);
priv.winproto.deinit();
priv.open_uri.deinit();
priv.global_shortcuts.unref();
if (priv.saved_language) |language| alloc.free(language);
if (gdk.Display.getDefault()) |display| {
@@ -740,6 +746,7 @@ pub const Application = extern struct {
.scrollbar => Action.scrollbar(target, value),
.set_title => Action.setTitle(target, value),
.set_tab_title => return Action.setTabTitle(target, value),
.show_child_exited => return Action.showChildExited(target, value),
@@ -804,6 +811,11 @@ pub const Application = extern struct {
return &self.private().winproto;
}
/// Returns the open URI portal implementation.
pub fn openUri(self: *Self) *OpenURI {
return &self.private().open_uri;
}
/// This will get called when there are no more open surfaces.
fn startQuitTimer(self: *Self) void {
const priv = self.private();
@@ -1287,6 +1299,11 @@ pub const Application = extern struct {
// Set ourselves as the default application.
gio.Application.setDefault(self.as(gio.Application));
// The D-Bus connection is only valid after GApplication startup.
self.openUri().setDbusConnection(
self.as(gio.Application).getDbusConnection(),
);
// Setup our event loop
self.startupXev();
@@ -1871,6 +1888,17 @@ pub const Application = extern struct {
gobject.Object.virtual_methods.finalize.implement(class, &finalize);
}
};
pub fn openUrlFallback(self: *Application, kind: apprt.action.OpenUrl.Kind, url: []const u8) void {
// Fallback to the minimal cross-platform way of opening a URL.
// This is always a safe fallback and enables for example Windows
// to open URLs (GTK on Windows via WSL is a thing).
internal_os.open(
self.allocator(),
kind,
url,
) catch |err| log.warn("unable to open url: {}", .{err});
}
};
/// All apprt action handlers
@@ -2315,16 +2343,20 @@ const Action = struct {
self: *Application,
value: apprt.action.OpenUrl,
) void {
// TODO: use https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.OpenURI.html
if (std.mem.startsWith(u8, value.url, "/")) {
self.openUrlFallback(value.kind, value.url);
return;
}
if (std.mem.startsWith(u8, value.url, "file://")) {
self.openUrlFallback(value.kind, value.url);
return;
}
// Fallback to the minimal cross-platform way of opening a URL.
// This is always a safe fallback and enables for example Windows
// to open URLs (GTK on Windows via WSL is a thing).
internal_os.open(
self.allocator(),
value.kind,
value.url,
) catch |err| log.warn("unable to open url: {}", .{err});
self.openUri().start(value) catch |err| {
log.err("unable to open uri err={}", .{err});
self.openUrlFallback(value.kind, value.url);
return;
};
}
pub fn pwd(
@@ -2545,6 +2577,30 @@ const Action = struct {
}
}
pub fn setTabTitle(
target: apprt.Target,
value: apprt.action.SetTitle,
) bool {
switch (target) {
.app => {
log.warn("set_tab_title to app is unexpected", .{});
return false;
},
.surface => |core| {
const surface = core.rt_surface.surface;
const tab = ext.getAncestor(
Tab,
surface.as(gtk.Widget),
) orelse {
log.warn("surface is not in a tab, ignoring set_tab_title", .{});
return false;
};
tab.setTitleOverride(if (value.title.len == 0) null else value.title);
return true;
},
}
}
pub fn showChildExited(
target: apprt.Target,
value: apprt.surface.Message.ChildExited,

View File

@@ -353,7 +353,7 @@ pub const CommandPalette = extern struct {
// Regular command - emit trigger signal
const action = cmd.getAction() orelse return;
// Signal that an an action has been selected. Signals are synchronous
// Signal that an action has been selected. Signals are synchronous
// so we shouldn't need to worry about cloning the action.
signals.trigger.impl.emit(
self,

View File

@@ -35,6 +35,7 @@ const TitleDialog = @import("title_dialog.zig").TitleDialog;
const Window = @import("window.zig").Window;
const InspectorWindow = @import("inspector_window.zig").InspectorWindow;
const i18n = @import("../../../os/i18n.zig");
const media = @import("../media.zig");
const log = std.log.scoped(.gtk_ghostty_surface);
@@ -823,10 +824,11 @@ pub const Surface = extern struct {
/// should be applied to the surface
fn closureShouldUnfocusedSplitBeShown(
_: *Self,
search_active: c_int,
focused: c_int,
is_split: c_int,
) callconv(.c) c_int {
return @intFromBool(focused == 0 and is_split != 0);
return @intFromBool(search_active == 0 and focused == 0 and is_split != 0);
}
pub fn toggleFullscreen(self: *Self) void {
@@ -1012,6 +1014,14 @@ pub const Surface = extern struct {
priv.progress_bar_timer = null;
}
if (priv.config) |config| {
if (!config.get().@"progress-style") {
log.debug("progress_report action blocked by config", .{});
priv.progress_bar_overlay.as(gtk.Widget).setVisible(@intFromBool(false));
return;
}
}
const progress_bar = priv.progress_bar_overlay;
switch (value.state) {
// Remove the progress bar
@@ -2448,34 +2458,8 @@ pub const Surface = extern struct {
1.0,
);
assert(std.fs.path.isAbsolute(path));
const media_file = gtk.MediaFile.newForFilename(path);
// If the audio file is marked as required, we'll emit an error if
// there was a problem playing it. Otherwise there will be silence.
if (required) {
_ = gobject.Object.signals.notify.connect(
media_file,
?*anyopaque,
mediaFileError,
null,
.{ .detail = "error" },
);
}
// Watch for the "ended" signal so that we can clean up after
// ourselves.
_ = gobject.Object.signals.notify.connect(
media_file,
?*anyopaque,
mediaFileEnded,
null,
.{ .detail = "ended" },
);
const media_stream = media_file.as(gtk.MediaStream);
media_stream.setVolume(volume);
media_stream.play();
const media_file = media.fromFilename(path) orelse break :audio;
media.playMediaFile(media_file, volume, required);
}
}
@@ -3380,12 +3364,20 @@ pub const Surface = extern struct {
config.command = try c.clone(config._arena.?.allocator());
}
if (priv.overrides.working_directory) |wd| {
config.@"working-directory" = try config._arena.?.allocator().dupeZ(u8, wd);
const config_alloc = config.arenaAlloc();
var wd_val: configpkg.WorkingDirectory = .{ .path = try config_alloc.dupe(u8, wd) };
try wd_val.finalize(config_alloc);
config.@"working-directory" = wd_val;
}
// Properties that can impact surface init
if (priv.font_size_request) |size| config.@"font-size" = size.points;
if (priv.pwd) |pwd| config.@"working-directory" = pwd;
if (priv.pwd) |pwd| {
const config_alloc = config.arenaAlloc();
var wd_val: configpkg.WorkingDirectory = .{ .path = try config_alloc.dupe(u8, pwd) };
try wd_val.finalize(config_alloc);
config.@"working-directory" = wd_val;
}
// Initialize the surface
surface.init(
@@ -3464,35 +3456,6 @@ pub const Surface = extern struct {
right.setVisible(0);
}
fn mediaFileError(
media_file: *gtk.MediaFile,
_: *gobject.ParamSpec,
_: ?*anyopaque,
) callconv(.c) void {
const path = path: {
const file = media_file.getFile() orelse break :path null;
break :path file.getPath();
};
defer if (path) |p| glib.free(p);
const media_stream = media_file.as(gtk.MediaStream);
const err = media_stream.getError() orelse return;
log.warn("error playing bell from {s}: {s} {d} {s}", .{
path orelse "<<unknown>>",
glib.quarkToString(err.f_domain),
err.f_code,
err.f_message orelse "",
});
}
fn mediaFileEnded(
media_file: *gtk.MediaFile,
_: *gobject.ParamSpec,
_: ?*anyopaque,
) callconv(.c) void {
media_file.unref();
}
fn titleDialogSet(
_: *TitleDialog,
title_ptr: [*:0]const u8,

View File

@@ -1071,21 +1071,6 @@ pub const Window = extern struct {
self.syncAppearance();
}
fn propGdkSurfaceHeight(
_: *gdk.Surface,
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
// X11 needs to fix blurring on resize, but winproto implementations
// could do anything.
self.private().winproto.resizeEvent() catch |err| {
log.warn(
"winproto resize event failed error={}",
.{err},
);
};
}
fn propIsActive(
_: *gtk.Window,
_: *gobject.ParamSpec,
@@ -1111,7 +1096,7 @@ pub const Window = extern struct {
};
}
fn propGdkSurfaceWidth(
fn propGdkSurfaceDims(
_: *gdk.Surface,
_: *gobject.ParamSpec,
self: *Self,
@@ -1250,7 +1235,7 @@ pub const Window = extern struct {
fn finalize(self: *Self) callconv(.c) void {
const priv = self.private();
priv.tab_bindings.unref();
priv.winproto.deinit(Application.default().allocator());
priv.winproto.deinit();
gobject.Object.virtual_methods.finalize.call(
Class.parent,
@@ -1282,14 +1267,14 @@ pub const Window = extern struct {
_ = gobject.Object.signals.notify.connect(
gdk_surface,
*Self,
propGdkSurfaceWidth,
propGdkSurfaceDims,
self,
.{ .detail = "width" },
);
_ = gobject.Object.signals.notify.connect(
gdk_surface,
*Self,
propGdkSurfaceHeight,
propGdkSurfaceDims,
self,
.{ .detail = "height" },
);

102
src/apprt/gtk/media.zig Normal file
View File

@@ -0,0 +1,102 @@
const std = @import("std");
const assert = @import("../../quirks.zig").inlineAssert;
const log = std.log.scoped(.gtk_media);
const gio = @import("gio");
const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");
pub fn fromFilename(path: [:0]const u8) ?*gtk.MediaFile {
assert(std.fs.path.isAbsolute(path));
std.fs.accessAbsolute(path, .{ .mode = .read_only }) catch |err| {
log.warn("unable to access {s}: {t}", .{ path, err });
return null;
};
return gtk.MediaFile.newForFilename(path);
}
pub fn fromResource(path: [:0]const u8) ?*gtk.MediaFile {
assert(std.fs.path.isAbsolute(path));
var gerr: ?*glib.Error = null;
const found = gio.resourcesGetInfo(path, .{}, null, null, &gerr);
if (gerr) |err| {
defer err.free();
log.warn(
"failed to find resource {s}: {s} {d} {s}",
.{
path,
glib.quarkToString(err.f_domain),
err.f_code,
err.f_message orelse "(no message)",
},
);
return null;
}
if (found == 0) {
log.warn("failed to find resource {s}", .{path});
return null;
}
return gtk.MediaFile.newForResource(path);
}
pub fn playMediaFile(media_file: *gtk.MediaFile, volume: f64, required: bool) void {
// If the audio file is marked as required, we'll emit an error if
// there was a problem playing it. Otherwise there will be silence.
if (required) {
_ = gobject.Object.signals.notify.connect(
media_file,
?*anyopaque,
mediaFileError,
null,
.{ .detail = "error" },
);
}
// Watch for the "ended" signal so that we can clean up after
// ourselves.
_ = gobject.Object.signals.notify.connect(
media_file,
?*anyopaque,
mediaFileEnded,
null,
.{ .detail = "ended" },
);
const media_stream = media_file.as(gtk.MediaStream);
media_stream.setVolume(volume);
media_stream.play();
}
fn mediaFileError(
media_file: *gtk.MediaFile,
_: *gobject.ParamSpec,
_: ?*anyopaque,
) callconv(.c) void {
const path = path: {
const file = media_file.getFile() orelse break :path null;
break :path file.getPath();
};
defer if (path) |p| glib.free(p);
const media_stream = media_file.as(gtk.MediaStream);
const err = media_stream.getError() orelse return;
log.warn("error playing sound from {s}: {s} {d} {s}", .{
path orelse "<<unknown>>",
glib.quarkToString(err.f_domain),
err.f_code,
err.f_message orelse "",
});
}
fn mediaFileEnded(
media_file: *gtk.MediaFile,
_: *gobject.ParamSpec,
_: ?*anyopaque,
) callconv(.c) void {
media_file.unref();
}

105
src/apprt/gtk/portal.zig Normal file
View File

@@ -0,0 +1,105 @@
const std = @import("std");
const gio = @import("gio");
const Allocator = std.mem.Allocator;
pub const OpenURI = @import("portal/OpenURI.zig");
pub const token_hex_len = @sizeOf(usize) * 2;
pub const TokenBuffer = [token_hex_len + 1]u8;
const token_format = std.fmt.comptimePrint("{{x:0>{}}}", .{token_hex_len});
/// Generate a token suitable for use in requests to the XDG Desktop Portal
pub fn generateToken() usize {
return std.crypto.random.int(usize);
}
/// Format a request token consistently for use in portal object paths and payloads.
pub fn formatToken(buf: *TokenBuffer, token: usize) [:0]const u8 {
return std.fmt.bufPrintZ(buf, token_format, .{token}) catch unreachable;
}
/// Get the XDG portal request path for the current Ghostty instance.
///
/// See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html
/// for the protocol of the Request interface.
pub fn getRequestPath(alloc: Allocator, dbus: *gio.DBusConnection, token: usize) (Allocator.Error || error{NoDBusUniqueName})![:0]const u8 {
// Get the unique name from D-Bus and strip the leading `:`
const unique_name = std.mem.span(
dbus.getUniqueName() orelse {
return error.NoDBusUniqueName;
},
)[1..];
return buildRequestPath(alloc, unique_name, token);
}
/// Build the XDG portal request path for given unique name and token.
fn buildRequestPath(alloc: Allocator, unique_name: []const u8, token: usize) Allocator.Error![:0]const u8 {
var token_buf: TokenBuffer = undefined;
const token_string = formatToken(&token_buf, token);
const object_path = try std.mem.joinZ(
alloc,
"/",
&.{
"/org/freedesktop/portal/desktop/request",
unique_name,
token_string,
},
);
// Sanitize the unique name by replacing every `.` with `_`. In effect, this
// will turn a unique name like `1.192` into `1_192`.
// This sounds arbitrary, but it's part of the Request protocol.
_ = std.mem.replaceScalar(u8, object_path, '.', '_');
return object_path;
}
/// Try and parse the token out of a request path.
pub fn parseRequestPathToken(request_path: []const u8) ?usize {
const index = std.mem.lastIndexOfScalar(u8, request_path, '/') orelse return null;
const token = request_path[index + 1 ..];
return std.fmt.parseUnsigned(usize, token, 16) catch return null;
}
test "formatToken pads to fixed width" {
const testing = std.testing;
var token_buf: TokenBuffer = undefined;
const token = formatToken(&token_buf, 0x42);
try testing.expectEqual(@as(usize, token_hex_len), token.len);
try testing.expectEqualStrings("0000000000000042", token);
}
test "buildRequestPath" {
const testing = std.testing;
const path = try buildRequestPath(testing.allocator, "1.42", 0x75af01a79c6fea34);
try testing.expectEqualStrings(
"/org/freedesktop/portal/desktop/request/1_42/75af01a79c6fea34",
path,
);
testing.allocator.free(path);
}
test "buildRequestPath pads token" {
const testing = std.testing;
const path = try buildRequestPath(testing.allocator, "1.42", 0x42);
try testing.expectEqualStrings(
"/org/freedesktop/portal/desktop/request/1_42/0000000000000042",
path,
);
testing.allocator.free(path);
}
test "parseRequestPathToken" {
const testing = std.testing;
try testing.expectEqual(0x75af01a79c6fea34, parseRequestPathToken("/org/freedesktop/portal/desktop/request/1_42/75af01a79c6fea34").?);
try testing.expectEqual(null, parseRequestPathToken("/org/freedesktop/portal/desktop/request/1_42/75af01a79c6fGa34"));
try testing.expectEqual(null, parseRequestPathToken("75af01a79c6fea34"));
}

View File

@@ -0,0 +1,565 @@
//! Use DBus to call the XDG Desktop Portal to open an URI.
//! See: https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.OpenURI.html#org-freedesktop-portal-openuri-openuri
const OpenURI = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const gio = @import("gio");
const glib = @import("glib");
const gobject = @import("gobject");
const App = @import("../App.zig");
const portal = @import("../portal.zig");
const apprt = @import("../../../apprt.zig");
const log = std.log.scoped(.openuri);
/// The GTK app that we "belong" to.
app: *App,
/// Connection to the D-Bus session bus that we'll use for all of our messaging.
dbus: ?*gio.DBusConnection = null,
/// Mutex to protect modification of the entries map or the cleanup timer.
mutex: std.Thread.Mutex = .{},
/// Map to store data about any in-flight calls to the portal.
entries: std.AutoArrayHashMapUnmanaged(usize, *Entry) = .empty,
/// Used to manage a timer to clean up any orphan entries in the map.
cleanup_timer: ?c_uint = null,
/// Set to false during shutdown so callbacks stop touching internal state.
alive: bool = true,
const RequestData = struct {
open_uri: *OpenURI,
token: usize,
kind: apprt.action.OpenUrl.Kind,
uri: [:0]const u8,
request_path: [:0]const u8,
pub fn init(
alloc: Allocator,
open_uri: *OpenURI,
token: usize,
kind: apprt.action.OpenUrl.Kind,
uri: []const u8,
request_path: []const u8,
) Allocator.Error!*RequestData {
const uri_copy = try alloc.dupeZ(u8, uri);
errdefer alloc.free(uri_copy);
const request_path_copy = try alloc.dupeZ(u8, request_path);
errdefer alloc.free(request_path_copy);
const data = try alloc.create(RequestData);
errdefer alloc.destroy(data);
data.* = .{
.open_uri = open_uri,
.token = token,
.kind = kind,
.uri = uri_copy,
.request_path = request_path_copy,
};
return data;
}
pub fn deinit(self: *const RequestData, alloc: Allocator) void {
alloc.free(self.uri);
alloc.free(self.request_path);
}
};
/// Data about any in-flight calls to the portal.
pub const Entry = struct {
/// When the request started.
start: std.time.Instant,
/// A token used by the portal to identify requests and responses. The
/// actual format of the token does not really matter as long as it can be
/// used as part of a D-Bus object path. `usize` was chosen since it's easy
/// to hash and to generate random tokens.
token: usize,
/// The "kind" of URI. Unused here, but we may need to pass it on to the
/// fallback URL opener if the D-Bus method fails.
kind: apprt.action.OpenUrl.Kind,
/// A copy of the URI that we are opening. We need our own copy since the
/// method calls are asynchronous and the original may have been freed by
/// the time we need it.
uri: [:0]const u8,
/// Used to manage a subscription to a D-Bus signal, which is how the XDG
/// Portal reports results of the method call.
subscription: ?c_uint = null,
pub fn deinit(self: *const Entry, alloc: Allocator) void {
alloc.free(self.uri);
}
};
pub const Errors = error{
/// Could not get a D-Bus connection
DBusConnectionRequired,
/// The D-Bus connection did not have a unique name. This _should_ be
/// impossible, but is handled for safety's sake.
NoDBusUniqueName,
/// The system was unable to give us the time.
TimerUnavailable,
};
pub fn init(app: *App) OpenURI {
return .{
.app = app,
};
}
pub fn setDbusConnection(self: *OpenURI, dbus: ?*gio.DBusConnection) void {
self.dbus = dbus;
}
pub fn deinit(self: *OpenURI) void {
const alloc = self.app.app.allocator();
self.mutex.lock();
defer self.mutex.unlock();
if (!self.alive) return;
self.alive = false;
self.stopCleanupTimer();
for (self.entries.entries.items(.value)) |entry| {
self.unsubscribeFromResponse(entry);
destroyEntry(alloc, entry);
}
self.entries.deinit(alloc);
self.entries = .empty;
self.dbus = null;
}
/// Send the D-Bus method call to the XDG Desktop portal. The result of the
/// method call will be reported asynchronously.
pub fn start(self: *OpenURI, value: apprt.action.OpenUrl) (Allocator.Error || Errors)!void {
const alloc = self.app.app.allocator();
const dbus = self.dbus orelse return error.DBusConnectionRequired;
const token = portal.generateToken();
const request_path = try portal.getRequestPath(alloc, dbus, token);
defer alloc.free(request_path);
const request = try RequestData.init(alloc, self, token, value.kind, value.url, request_path);
errdefer {
request.deinit(alloc);
alloc.destroy(request);
}
self.mutex.lock();
defer self.mutex.unlock();
// Create an entry that is used to track the results of the D-Bus method
// call.
const entry = entry: {
const entry = try alloc.create(Entry);
errdefer alloc.destroy(entry);
entry.* = .{
.start = std.time.Instant.now() catch return error.TimerUnavailable,
.token = token,
.kind = value.kind,
.uri = try alloc.dupeZ(u8, value.url),
};
errdefer entry.deinit(alloc);
try self.entries.putNoClobber(alloc, token, entry);
break :entry entry;
};
errdefer {
_ = self.entries.swapRemove(token);
destroyEntry(alloc, entry);
}
self.startCleanupTimer();
self.subscribeToResponse(entry, dbus, request_path.ptr);
self.sendRequest(entry, dbus, request);
}
/// Subscribe to the D-Bus signal that will contain the results of our method
/// call to the portal. This must be called with the mutex locked.
fn subscribeToResponse(
self: *OpenURI,
entry: *Entry,
dbus: *gio.DBusConnection,
request_path: [*:0]const u8,
) void {
assert(!self.mutex.tryLock());
if (entry.subscription != null) return;
entry.subscription = dbus.signalSubscribe(
null,
"org.freedesktop.portal.Request",
"Response",
request_path,
null,
.{},
responseReceived,
self,
null,
);
}
/// Unsubscribe to the D-Bus signal that contains the result of the method call.
/// This will prevent a response from being processed multiple times. This must
/// be called when the mutex is locked.
fn unsubscribeFromResponse(self: *OpenURI, entry: *Entry) void {
assert(!self.mutex.tryLock());
// Unsubscribe from the response signal
if (entry.subscription) |subscription| {
const dbus = self.dbus orelse {
entry.subscription = null;
log.warn("unable to unsubscribe open uri response without dbus connection", .{});
return;
};
dbus.signalUnsubscribe(subscription);
entry.subscription = null;
}
}
fn destroyEntry(alloc: Allocator, entry: *Entry) void {
entry.deinit(alloc);
alloc.destroy(entry);
}
fn failRequest(self: *OpenURI, token: usize) ?*Entry {
self.mutex.lock();
defer self.mutex.unlock();
if (!self.alive) return null;
const entry = (self.entries.fetchSwapRemove(token) orelse return null).value;
self.unsubscribeFromResponse(entry);
return entry;
}
fn failRequestAndFallback(self: *OpenURI, request: *const RequestData) void {
const alloc = self.app.app.allocator();
const entry = self.failRequest(request.token) orelse return;
defer destroyEntry(alloc, entry);
self.app.app.openUrlFallback(request.kind, request.uri);
}
/// Send the D-Bus method call to the portal. The mutex must be locked when this
/// is called.
fn sendRequest(
self: *OpenURI,
entry: *Entry,
dbus: *gio.DBusConnection,
request: *RequestData,
) void {
assert(!self.mutex.tryLock());
const payload = payload: {
const builder_type = glib.VariantType.new("(ssa{sv})");
defer builder_type.free();
// Initialize our builder to build up our parameters
var builder: glib.VariantBuilder = undefined;
builder.init(builder_type);
// parent window - empty string means we have no window
builder.add("s", "");
// URI to open
builder.add("s", entry.uri.ptr);
// Options
{
const options = glib.VariantType.new("a{sv}");
defer options.free();
builder.open(options);
defer builder.close();
{
const option = glib.VariantType.new("{sv}");
defer option.free();
builder.open(option);
defer builder.close();
builder.add("s", "handle_token");
var token_buf: portal.TokenBuffer = undefined;
const token = portal.formatToken(&token_buf, entry.token);
const handle_token = glib.Variant.newString(token.ptr);
builder.add("v", handle_token);
}
{
const option = glib.VariantType.new("{sv}");
defer option.free();
builder.open(option);
defer builder.close();
builder.add("s", "writable");
const writable = glib.Variant.newBoolean(@intFromBool(false));
builder.add("v", writable);
}
{
const option = glib.VariantType.new("{sv}");
defer option.free();
builder.open(option);
defer builder.close();
builder.add("s", "ask");
const ask = glib.Variant.newBoolean(@intFromBool(false));
builder.add("v", ask);
}
}
break :payload builder.end();
};
// We're expecting an object path back from the method call.
const reply_type = glib.VariantType.new("(o)");
defer reply_type.free();
dbus.call(
"org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.OpenURI",
"OpenURI",
payload,
reply_type,
.{},
-1,
null,
requestCallback,
request,
);
}
/// Process the result of the original method call. Receiving this result does
/// not indicate that the that the method call succeeded but it may contain an
/// error message that is useful to log for debugging purposes.
fn requestCallback(
source: ?*gobject.Object,
result: *gio.AsyncResult,
ud: ?*anyopaque,
) callconv(.c) void {
const request: *RequestData = @ptrCast(@alignCast(ud orelse return));
const self = request.open_uri;
const alloc = self.app.app.allocator();
defer {
request.deinit(alloc);
alloc.destroy(request);
}
const dbus = gobject.ext.cast(gio.DBusConnection, source orelse {
log.err("Open URI request finished without a D-Bus source object", .{});
self.failRequestAndFallback(request);
return;
}) orelse {
log.err("Open URI request finished with an unexpected source object", .{});
self.failRequestAndFallback(request);
return;
};
var err_: ?*glib.Error = null;
defer if (err_) |err| err.free();
const reply_ = dbus.callFinish(result, &err_);
if (err_) |err| {
log.err("Open URI request failed={s} ({})", .{
err.f_message orelse "(unknown)",
err.f_code,
});
self.failRequestAndFallback(request);
return;
}
const reply = reply_ orelse {
log.err("D-Bus method call returned a null value!", .{});
self.failRequestAndFallback(request);
return;
};
defer reply.unref();
const reply_type = glib.VariantType.new("(o)");
defer reply_type.free();
if (reply.isOfType(reply_type) == 0) {
log.warn("Reply from D-Bus method call does not contain an object path!", .{});
self.failRequestAndFallback(request);
return;
}
var object_path: [*:0]const u8 = undefined;
reply.get("(&o)", &object_path);
const token = portal.parseRequestPathToken(std.mem.span(object_path)) orelse {
log.warn("Unable to parse token from the object path {s}", .{object_path});
self.failRequestAndFallback(request);
return;
};
if (token != request.token) {
log.warn("Open URI request returned mismatched token expected={x} actual={x}", .{
request.token,
token,
});
self.failRequestAndFallback(request);
return;
}
self.mutex.lock();
defer self.mutex.unlock();
if (!self.alive) return;
const entry = self.entries.get(token) orelse return;
if (std.mem.eql(u8, request.request_path, std.mem.span(object_path))) return;
log.debug("updating open uri request path old={s} new={s}", .{
request.request_path,
object_path,
});
self.unsubscribeFromResponse(entry);
self.subscribeToResponse(entry, dbus, object_path);
}
/// Handle the response signal from the portal. This should contain the actual
/// results of the method call (success or failure).
fn responseReceived(
_: *gio.DBusConnection,
_: ?[*:0]const u8,
object_path: [*:0]const u8,
_: [*:0]const u8,
_: [*:0]const u8,
params: *glib.Variant,
ud: ?*anyopaque,
) callconv(.c) void {
const self: *OpenURI = @ptrCast(@alignCast(ud orelse {
log.err("OpenURI response received with null userdata", .{});
return;
}));
const alloc = self.app.app.allocator();
const token = portal.parseRequestPathToken(std.mem.span(object_path)) orelse {
log.warn("invalid object path: {s}", .{std.mem.span(object_path)});
return;
};
self.mutex.lock();
defer self.mutex.unlock();
if (!self.alive) return;
const entry = (self.entries.fetchSwapRemove(token) orelse {
log.warn("no entry for token {x}", .{token});
return;
}).value;
defer destroyEntry(alloc, entry);
self.unsubscribeFromResponse(entry);
var response: u32 = 0;
var results: ?*glib.Variant = null;
defer if (results) |variant| variant.unref();
params.get("(u@a{sv})", &response, &results);
switch (response) {
0 => {
log.debug("open uri successful", .{});
},
1 => {
log.debug("open uri request was cancelled by the user", .{});
},
2 => {
log.warn("open uri request ended unexpectedly", .{});
self.app.app.openUrlFallback(entry.kind, entry.uri);
},
else => {
log.err("unrecognized response code={}", .{response});
self.app.app.openUrlFallback(entry.kind, entry.uri);
},
}
}
/// Wait this number of seconds and then clean up any orphaned entries.
const cleanup_timeout = 30;
/// If there is an active cleanup timer, cancel it. This must be called with the
/// mutex locked
fn stopCleanupTimer(self: *OpenURI) void {
assert(!self.mutex.tryLock());
if (self.cleanup_timer) |timer| {
if (glib.Source.remove(timer) == 0) {
log.warn("unable to remove cleanup timer source={d}", .{timer});
}
self.cleanup_timer = null;
}
}
/// Start a timer to clean up any entries that have not received a timely
/// response. If there is already a timer it will be stopped and replaced with a
/// new one. This must be called with the mutex locked.
fn startCleanupTimer(self: *OpenURI) void {
assert(!self.mutex.tryLock());
self.stopCleanupTimer();
self.cleanup_timer = glib.timeoutAddSeconds(cleanup_timeout + 1, cleanup, self);
}
/// The cleanup timer is used to free up any entries that may have failed
/// to get a response in a timely manner.
fn cleanup(ud: ?*anyopaque) callconv(.c) c_int {
const self: *OpenURI = @ptrCast(@alignCast(ud orelse {
log.warn("cleanup called with null userdata", .{});
return @intFromBool(glib.SOURCE_REMOVE);
}));
const alloc = self.app.app.allocator();
self.mutex.lock();
defer self.mutex.unlock();
self.cleanup_timer = null;
if (!self.alive) return @intFromBool(glib.SOURCE_REMOVE);
const now = std.time.Instant.now() catch {
// `now()` should never fail, but if it does, don't crash, just return.
// This might cause a small memory leak in rare circumstances but it
// should get cleaned up the next time a URL is clicked.
return @intFromBool(glib.SOURCE_REMOVE);
};
loop: while (true) {
for (self.entries.entries.items(.value)) |entry| {
if (now.since(entry.start) > cleanup_timeout * std.time.ns_per_s) {
log.warn("open uri request timed out token={x}", .{entry.token});
self.unsubscribeFromResponse(entry);
_ = self.entries.swapRemove(entry.token);
self.app.app.openUrlFallback(entry.kind, entry.uri);
destroyEntry(alloc, entry);
continue :loop;
}
}
break :loop;
}
return @intFromBool(glib.SOURCE_REMOVE);
}

View File

@@ -203,7 +203,7 @@ Overlay terminal_page {
// Apply unfocused-split-fill and unfocused-split-opacity to current surface
// this is only applied when a tab has more than one surface
Revealer {
reveal-child: bind $should_unfocused_split_be_shown(template.focused, template.is-split) as <bool>;
reveal-child: bind $should_unfocused_split_be_shown(search_overlay.active, template.focused, template.is-split) as <bool>;
transition-duration: 0;
// This is all necessary so that the Revealer itself doesn't override
// any input events from the other overlays. Namely, if you don't have
@@ -221,7 +221,7 @@ Overlay terminal_page {
DropTarget drop_target {
drop => $drop();
actions: copy;
actions: copy | move;
}
}

View File

@@ -46,9 +46,9 @@ pub const App = union(Protocol) {
return .{ .none = .{} };
}
pub fn deinit(self: *App, alloc: Allocator) void {
pub fn deinit(self: *App) void {
switch (self.*) {
inline else => |*v| v.deinit(alloc),
inline else => |*v| v.deinit(),
}
}
@@ -117,9 +117,9 @@ pub const Window = union(Protocol) {
};
}
pub fn deinit(self: *Window, alloc: Allocator) void {
pub fn deinit(self: *Window) void {
switch (self.*) {
inline else => |*v| v.deinit(alloc),
inline else => |*v| v.deinit(),
}
}

View File

@@ -0,0 +1,187 @@
const BlurRegion = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const gobject = @import("gobject");
const gdk = @import("gdk");
const gtk = @import("gtk");
const Window = @import("../winproto.zig").Window;
const ApprtWindow = @import("../class/window.zig").Window;
slices: std.ArrayList(Slice),
/// A rectangular slice of the blur region.
// Marked `extern` since we want to be able to use this in X11 directly.
pub const Slice = extern struct {
x: Pos,
y: Pos,
width: Pos,
height: Pos,
};
// X11 compatibility. Ideally this should just be an `i32` like Wayland,
// but XLib sucks
const Pos = c_long;
pub const empty: BlurRegion = .{
.slices = .empty,
};
pub fn deinit(self: *BlurRegion, alloc: Allocator) void {
self.slices.deinit(alloc);
self.slices = .empty;
}
// Calculate the blur regions for a window.
//
// Since we have rounded corners by default, we need to carve out the
// pixels on each corner to avoid the "korners bug".
// (cf. https://github.com/cutefishos/fishui/blob/41d4ba194063a3c7fff4675619b57e6ac0504f06/src/platforms/linux/blurhelper/windowblur.cpp#L134)
pub fn calcForWindow(
alloc: Allocator,
window: *ApprtWindow,
csd: bool,
to_device_coordinates: bool,
) Allocator.Error!BlurRegion {
const native = window.as(gtk.Native);
const surface = native.getSurface() orelse return .empty;
var slices: std.ArrayList(Slice) = .empty;
errdefer slices.deinit(alloc);
// Calculate the primary blur region
// (the one that covers most of the screen).
// It's easier to do this inside a vector since we have to scale
// everything by the scale factor anyways.
// NOTE(pluiedev): CSDs are a f--king mistake.
// Please, GNOME, stop this nonsense of making a window ~30% bigger
// internally than how they really are just for your shadows and
// rounded corners and all that fluff. Please. I beg of you.
const x: Pos, const y: Pos = off: {
var x: f64 = 0;
var y: f64 = 0;
native.getSurfaceTransform(&x, &y);
// Slightly inset the corners if we're using CSDs
if (csd) {
x += 1;
y += 1;
}
break :off .{ @intFromFloat(x), @intFromFloat(y) };
};
var width = @as(Pos, surface.getWidth());
var height = @as(Pos, surface.getHeight());
// Trim off the offsets. Be careful not to get negative.
width -= x * 2;
height -= y * 2;
if (width <= 0 or height <= 0) return .empty;
// Empirically determined.
const are_corners_rounded = rounded: {
// This cast should always succeed as all of our windows
// should be toplevel. If this fails, something very strange
// is going on.
const toplevel = gobject.ext.cast(
gdk.Toplevel,
surface,
) orelse break :rounded false;
const state = toplevel.getState();
if (state.fullscreen or state.maximized or state.tiled)
break :rounded false;
break :rounded csd;
};
const new_slices = try approxRoundedRect(
alloc,
x,
y,
width,
height,
// See https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/css-variables.html#window-radius
if (are_corners_rounded) 15 else 0,
);
if (to_device_coordinates) {
// Transform surface coordinates to device coordinates.
const sf = surface.getScaleFactor();
for (new_slices.items) |*s| {
s.x *= sf;
s.y *= sf;
s.width *= sf;
s.height *= sf;
}
}
return .{ .slices = new_slices };
}
/// Whether two sets of blur regions are equal.
pub fn eql(self: BlurRegion, other: BlurRegion) bool {
if (self.slices.items.len != other.slices.items.len) return false;
for (self.slices.items, other.slices.items) |this, that| {
if (!std.meta.eql(this, that)) return false;
}
return true;
}
/// Approximate a rounded rectangle with many smaller rectangles.
fn approxRoundedRect(
alloc: Allocator,
x: Pos,
y: Pos,
width: Pos,
height: Pos,
radius: Pos,
) Allocator.Error!std.ArrayList(Slice) {
const r_f: f32 = @floatFromInt(radius);
var slices: std.ArrayList(Slice) = .empty;
errdefer slices.deinit(alloc);
// Add the central rectangle
try slices.append(alloc, .{
.x = x,
.y = y + radius,
.width = width,
.height = height - 2 * radius,
});
// Add the corner rows. This is honestly quite cursed.
var row: Pos = 0;
while (row < radius) : (row += 1) {
// y distance from this row to the center corner circle
const dy = @as(f32, @floatFromInt(radius - row)) - 0.5;
// x distance - as given by the definition of a circle
const dx = @sqrt(r_f * r_f - dy * dy);
// How much each row should be offset, rounded to an integer
const row_x: Pos = @intFromFloat(r_f - @round(dx + 0.5));
// Remove the offset from both ends
const row_w = width - 2 * row_x;
// Top slice
try slices.append(alloc, .{
.x = x + row_x,
.y = y + row,
.width = row_w,
.height = 1,
});
// Bottom slice
try slices.append(alloc, .{
.x = x + row_x,
.y = y + height - 1 - row,
.width = row_w,
.height = 1,
});
}
return slices;
}

View File

@@ -19,9 +19,8 @@ pub const App = struct {
return null;
}
pub fn deinit(self: *App, alloc: Allocator) void {
pub fn deinit(self: *App) void {
_ = self;
_ = alloc;
}
pub fn eventMods(
@@ -47,9 +46,8 @@ pub const Window = struct {
return .{};
}
pub fn deinit(self: Window, alloc: Allocator) void {
pub fn deinit(self: *Window) void {
_ = self;
_ = alloc;
}
pub fn updateConfigEvent(

View File

@@ -7,49 +7,26 @@ const gdk_wayland = @import("gdk_wayland");
const gobject = @import("gobject");
const gtk = @import("gtk");
const layer_shell = @import("gtk4-layer-shell");
const wayland = @import("wayland");
const Config = @import("../../../config.zig").Config;
const input = @import("../../../input.zig");
const ApprtWindow = @import("../class/window.zig").Window;
const wl = wayland.client.wl;
const ext = wayland.client.ext;
const kde = wayland.client.kde;
const org = wayland.client.org;
const xdg = wayland.client.xdg;
const Config = @import("../../../config.zig").Config;
const Globals = @import("wayland/Globals.zig");
const input = @import("../../../input.zig");
const ApprtWindow = @import("../class/window.zig").Window;
const BlurRegion = @import("BlurRegion.zig");
const log = std.log.scoped(.winproto_wayland);
/// Wayland state that contains application-wide Wayland objects (e.g. wl_display).
pub const App = struct {
display: *wl.Display,
context: *Context,
const Context = struct {
kde_blur_manager: ?*org.KdeKwinBlurManager = null,
// FIXME: replace with `zxdg_decoration_v1` once GTK merges
// https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398
kde_decoration_manager: ?*org.KdeKwinServerDecorationManager = null,
kde_slide_manager: ?*org.KdeKwinSlideManager = null,
default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null,
xdg_activation: ?*xdg.ActivationV1 = null,
/// Whether the xdg_wm_dialog_v1 protocol is present.
///
/// If it is present, gtk4-layer-shell < 1.0.4 may crash when the user
/// creates a quick terminal, and we need to ensure this fails
/// gracefully if this situation occurs.
///
/// FIXME: This is a temporary workaround - we should remove this when
/// all of our supported distros drop support for affected old
/// gtk4-layer-shell versions.
///
/// See https://github.com/wmww/gtk4-layer-shell/issues/50
xdg_wm_dialog_present: bool = false,
};
globals: *Globals,
pub fn init(
alloc: Allocator,
@@ -69,34 +46,17 @@ pub const App = struct {
gdk_wayland_display.getWlDisplay() orelse return error.NoWaylandDisplay,
));
// Create our context for our callbacks so we have a stable pointer.
// Note: at the time of writing this comment, we don't really need
// a stable pointer, but it's too scary that we'd need one in the future
// and not have it and corrupt memory or something so let's just do it.
const context = try alloc.create(Context);
errdefer alloc.destroy(context);
context.* = .{};
// Get our display registry so we can get all the available interfaces
// and bind to what we need.
const registry = try display.getRegistry();
registry.setListener(*Context, registryListener, context);
if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
// Do another round-trip to get the default decoration mode
if (context.kde_decoration_manager) |deco_manager| {
deco_manager.setListener(*Context, decoManagerListener, context);
if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
}
const globals: *Globals = try .init(alloc, display);
errdefer globals.deinit();
return .{
.display = display,
.context = context,
.globals = globals,
};
}
pub fn deinit(self: *App, alloc: Allocator) void {
alloc.destroy(self.context);
pub fn deinit(self: *App) void {
self.globals.deinit();
}
pub fn eventMods(
@@ -108,118 +68,23 @@ pub const App = struct {
}
pub fn supportsQuickTerminal(self: App) bool {
_ = self;
if (!layer_shell.isSupported()) {
log.warn("your compositor does not support the wlr-layer-shell protocol; disabling quick terminal", .{});
return false;
}
if (self.context.xdg_wm_dialog_present and
layer_shell.getLibraryVersion().order(.{
.major = 1,
.minor = 0,
.patch = 4,
}) == .lt)
{
log.warn("the version of gtk4-layer-shell installed on your system is too old (must be 1.0.4 or newer); disabling quick terminal", .{});
return false;
}
return true;
}
pub fn initQuickTerminal(_: *App, apprt_window: *ApprtWindow) !void {
pub fn initQuickTerminal(self: *App, apprt_window: *ApprtWindow) !void {
const window = apprt_window.as(gtk.Window);
layer_shell.initForWindow(window);
}
fn getInterfaceType(comptime field: std.builtin.Type.StructField) ?type {
// Globals should be optional pointers
const T = switch (@typeInfo(field.type)) {
.optional => |o| switch (@typeInfo(o.child)) {
.pointer => |v| v.child,
else => return null,
},
else => return null,
};
// Only process Wayland interfaces
if (!@hasDecl(T, "interface")) return null;
return T;
}
fn registryListener(
registry: *wl.Registry,
event: wl.Registry.Event,
context: *Context,
) void {
const ctx_fields = @typeInfo(Context).@"struct".fields;
switch (event) {
.global => |v| {
log.debug("found global {s}", .{v.interface});
// We don't actually do anything with this other than checking
// for its existence, so we process this separately.
if (std.mem.orderZ(
u8,
v.interface,
"xdg_wm_dialog_v1",
) == .eq) {
context.xdg_wm_dialog_present = true;
return;
}
inline for (ctx_fields) |field| {
const T = getInterfaceType(field) orelse continue;
if (std.mem.orderZ(
u8,
v.interface,
T.interface.name,
) == .eq) {
log.debug("matched {}", .{T});
@field(context, field.name) = registry.bind(
v.name,
T,
T.generated_version,
) catch |err| {
log.warn(
"error binding interface {s} error={}",
.{ v.interface, err },
);
return;
};
}
}
},
// This should be a rare occurrence, but in case a global
// is suddenly no longer available, we destroy and unset it
// as the protocol mandates.
.global_remove => |v| remove: {
inline for (ctx_fields) |field| {
if (getInterfaceType(field) == null) continue;
const global = @field(context, field.name) orelse break :remove;
if (global.getId() == v.name) {
global.destroy();
@field(context, field.name) = null;
}
}
},
}
}
fn decoManagerListener(
_: *org.KdeKwinServerDecorationManager,
event: org.KdeKwinServerDecorationManager.Event,
context: *Context,
) void {
switch (event) {
.default_mode => |mode| {
context.default_deco_mode = @enumFromInt(mode.mode);
},
}
// Set target monitor based on config (null lets compositor decide)
const monitor = resolveQuickTerminalMonitor(self.globals, apprt_window);
defer if (monitor) |v| v.unref();
layer_shell.setMonitor(window, monitor);
}
};
@@ -231,10 +96,10 @@ pub const Window = struct {
surface: *wl.Surface,
/// The context from the app where we can load our Wayland interfaces.
app_context: *App.Context,
globals: *Globals,
/// A token that, when present, indicates that the window is blurred.
blur_token: ?*org.KdeKwinBlur = null,
/// Object that controls background effects like background blur.
bg_effect: ?*ext.BackgroundEffectSurfaceV1 = null,
/// Object that controls the decoration mode (client/server/auto)
/// of the window.
@@ -248,6 +113,8 @@ pub const Window = struct {
/// requesting attention from the user.
activation_token: ?*xdg.ActivationTokenV1 = null,
blur_region: BlurRegion = .empty,
pub fn init(
alloc: Allocator,
app: *App,
@@ -272,7 +139,7 @@ pub const Window = struct {
// Get our decoration object so we can control the
// CSD vs SSD status of this surface.
const deco: ?*org.KdeKwinServerDecoration = deco: {
const mgr = app.context.kde_decoration_manager orelse
const mgr = app.globals.get(.kde_decoration_manager) orelse
break :deco null;
const deco: *org.KdeKwinServerDecoration = mgr.create(
@@ -285,6 +152,20 @@ pub const Window = struct {
break :deco deco;
};
const bg_effect: ?*ext.BackgroundEffectSurfaceV1 = bg: {
const mgr = app.globals.get(.ext_background_effect) orelse
break :bg null;
const bg_effect: *ext.BackgroundEffectSurfaceV1 = mgr.getBackgroundEffect(
wl_surface,
) catch |err| {
log.warn("could not create background effect object={}", .{err});
break :bg null;
};
break :bg bg_effect;
};
if (apprt_window.isQuickTerminal()) {
_ = gdk.Surface.signals.enter_monitor.connect(
gdk_surface,
@@ -298,26 +179,31 @@ pub const Window = struct {
return .{
.apprt_window = apprt_window,
.surface = wl_surface,
.app_context = app.context,
.globals = app.globals,
.decoration = deco,
.bg_effect = bg_effect,
};
}
pub fn deinit(self: Window, alloc: Allocator) void {
_ = alloc;
if (self.blur_token) |blur| blur.release();
pub fn deinit(self: *Window) void {
self.blur_region.deinit(self.globals.alloc);
if (self.bg_effect) |bg| bg.destroy();
if (self.decoration) |deco| deco.release();
if (self.slide) |slide| slide.release();
}
pub fn resizeEvent(_: *Window) !void {}
pub fn resizeEvent(self: *Window) !void {
self.syncBlur() catch |err| {
log.err("failed to sync blur={}", .{err});
};
}
pub fn syncAppearance(self: *Window) !void {
self.syncBlur() catch |err| {
log.err("failed to sync blur={}", .{err});
};
self.syncDecoration() catch |err| {
log.err("failed to sync blur={}", .{err});
log.err("failed to sync decoration={}", .{err});
};
if (self.apprt_window.isQuickTerminal()) {
@@ -333,7 +219,7 @@ pub const Window = struct {
// If we support SSDs, then we should *not* enable CSDs if we prefer SSDs.
// However, if we do not support SSDs (e.g. GNOME) then we should enable
// CSDs even if the user prefers SSDs.
.Server => if (self.app_context.kde_decoration_manager) |_| false else true,
.Server => if (self.globals.get(.kde_decoration_manager)) |_| false else true,
.None => false,
else => unreachable,
};
@@ -345,7 +231,7 @@ pub const Window = struct {
}
pub fn setUrgent(self: *Window, urgent: bool) !void {
const activation = self.app_context.xdg_activation orelse return;
const activation = self.globals.get(.xdg_activation) orelse return;
// If there already is a token, destroy and unset it
if (self.activation_token) |token| token.destroy();
@@ -361,28 +247,47 @@ pub const Window = struct {
/// Update the blur state of the window.
fn syncBlur(self: *Window) !void {
const manager = self.app_context.kde_blur_manager orelse return;
const compositor = self.globals.get(.compositor) orelse return;
const bg = self.bg_effect orelse return;
if (!self.globals.state.bg_effect_capabilities.blur) return;
const config = if (self.apprt_window.getConfig()) |v|
v.get()
else
return;
const blur = config.@"background-blur";
if (self.blur_token) |tok| {
// Only release token when transitioning from blurred -> not blurred
if (!blur.enabled()) {
manager.unset(self.surface);
tok.release();
self.blur_token = null;
}
} else {
// Only acquire token when transitioning from not blurred -> blurred
if (blur.enabled()) {
const tok = try manager.create(self.surface);
tok.commit();
self.blur_token = tok;
}
if (!blur.enabled()) {
self.blur_region.deinit(self.globals.alloc);
bg.setBlurRegion(null);
return;
}
var region: BlurRegion = try .calcForWindow(
self.globals.alloc,
self.apprt_window,
self.clientSideDecorationEnabled(),
false,
);
errdefer region.deinit(self.globals.alloc);
if (region.eql(self.blur_region)) {
// Region didn't change. Don't do anything.
region.deinit(self.globals.alloc);
return;
}
const wl_region = try compositor.createRegion();
errdefer if (wl_region) |r| r.destroy();
for (region.slices.items) |s| wl_region.add(
@intCast(s.x),
@intCast(s.y),
@intCast(s.width),
@intCast(s.height),
);
bg.setBlurRegion(wl_region);
self.blur_region = region;
}
fn syncDecoration(self: *Window) !void {
@@ -395,7 +300,7 @@ pub const Window = struct {
fn getDecorationMode(self: Window) org.KdeKwinServerDecorationManager.Mode {
return switch (self.apprt_window.getWindowDecoration()) {
.auto => self.app_context.default_deco_mode orelse .Client,
.auto => self.globals.state.default_deco_mode orelse .Client,
.client => .Client,
.server => .Server,
.none => .None,
@@ -417,6 +322,12 @@ pub const Window = struct {
});
layer_shell.setNamespace(window, config.@"gtk-quick-terminal-namespace");
// Re-resolve the target monitor on every sync so that config reloads
// and primary-output changes take effect without recreating the window.
const target_monitor = resolveQuickTerminalMonitor(self.globals, self.apprt_window);
defer if (target_monitor) |v| v.unref();
layer_shell.setMonitor(window, target_monitor);
layer_shell.setKeyboardMode(
window,
switch (config.@"quick-terminal-keyboard-interactivity") {
@@ -457,7 +368,7 @@ pub const Window = struct {
if (self.slide) |slide| slide.release();
self.slide = if (anchored_edge) |anchored| slide: {
const mgr = self.app_context.kde_slide_manager orelse break :slide null;
const mgr = self.globals.get(.kde_slide_manager) orelse break :slide null;
const slide = mgr.create(self.surface) catch |err| {
log.warn("could not create slide object={}", .{err});
@@ -486,8 +397,17 @@ pub const Window = struct {
const window = apprt_window.as(gtk.Window);
const config = if (apprt_window.getConfig()) |v| v.get() else return;
const resolved_monitor = resolveQuickTerminalMonitor(
apprt_window.winproto().wayland.globals,
apprt_window,
);
defer if (resolved_monitor) |v| v.unref();
// Use the configured monitor for sizing if not in mouse mode.
const size_monitor = resolved_monitor orelse monitor;
var monitor_size: gdk.Rectangle = undefined;
monitor.getGeometry(&monitor_size);
size_monitor.getGeometry(&monitor_size);
const dims = config.@"quick-terminal-size".calculate(
config.@"quick-terminal-position",
@@ -505,7 +425,7 @@ pub const Window = struct {
event: xdg.ActivationTokenV1.Event,
self: *Window,
) void {
const activation = self.app_context.xdg_activation orelse return;
const activation = self.globals.get(.xdg_activation) orelse return;
const current_token = self.activation_token orelse return;
if (token.getId() != current_token.getId()) {
@@ -522,3 +442,45 @@ pub const Window = struct {
}
}
};
/// Resolve the quick-terminal-screen config to a specific monitor.
/// Returns null to let the compositor decide (used for .mouse mode).
/// Caller owns the returned ref and must unref it.
fn resolveQuickTerminalMonitor(
globals: *Globals,
apprt_window: *ApprtWindow,
) ?*gdk.Monitor {
const config = if (apprt_window.getConfig()) |v| v.get() else return null;
switch (config.@"quick-terminal-screen") {
.mouse => return null,
.main, .@"macos-menu-bar" => {},
}
const display = apprt_window.as(gtk.Widget).getDisplay();
const monitors = display.getMonitors();
// Try to find the monitor matching the primary output name.
if (globals.state.primary_output_name) |stored_name| {
var i: u32 = 0;
while (monitors.getObject(i)) |item| : (i += 1) {
const monitor = gobject.ext.cast(gdk.Monitor, item) orelse {
item.unref();
continue;
};
if (monitor.getConnector()) |connector_z| {
if (std.mem.orderZ(u8, connector_z, stored_name) == .eq) {
return monitor;
}
}
monitor.unref();
}
}
// Fall back to the first monitor in the list.
const first = monitors.getObject(0) orelse return null;
return gobject.ext.cast(gdk.Monitor, first) orelse {
first.unref();
return null;
};
}

View File

@@ -0,0 +1,252 @@
const Globals = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const wayland = @import("wayland");
const wl = wayland.client.wl;
const ext = wayland.client.ext;
const kde = wayland.client.kde;
const org = wayland.client.org;
const xdg = wayland.client.xdg;
const log = std.log.scoped(.winproto_wayland_globals);
alloc: Allocator,
state: State,
map: std.EnumMap(Tag, Binding),
/// Used in the initial roundtrip to determine whether more
/// roundtrips are required to fetch the initial state.
needs_roundtrip: bool = false,
const Binding = struct {
// All globals can be casted into a wl.Proxy object.
proxy: *wl.Proxy,
name: u32,
};
pub const Tag = enum {
compositor,
ext_background_effect,
kde_decoration_manager,
kde_slide_manager,
kde_output_order,
xdg_activation,
fn Type(comptime self: Tag) type {
return switch (self) {
.compositor => wl.Compositor,
.ext_background_effect => ext.BackgroundEffectManagerV1,
.kde_decoration_manager => org.KdeKwinServerDecorationManager,
.kde_slide_manager => org.KdeKwinSlideManager,
.kde_output_order => kde.OutputOrderV1,
.xdg_activation => xdg.ActivationV1,
};
}
};
pub const State = struct {
/// Connector name of the primary output (e.g., "DP-1") as reported
/// by kde_output_order_v1. The first output in each priority list
/// is the primary.
primary_output_name: ?[:0]const u8 = null,
/// Tracks the output order event cycle. Set to true after a `done`
/// event so the next `output` event is captured as the new primary.
/// Initialized to true so the first event after binding is captured.
output_order_done: bool = true,
default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null,
bg_effect_capabilities: ext.BackgroundEffectManagerV1.Capability = .{},
/// Reset cached state derived from kde_output_order_v1.
fn resetOutputOrder(self: *State, alloc: Allocator) void {
if (self.primary_output_name) |name| alloc.free(name);
self.primary_output_name = null;
self.output_order_done = true;
}
};
pub fn init(alloc: Allocator, display: *wl.Display) !*Globals {
// We need to allocate here since the listener
// expects a stable memory address.
const self = try alloc.create(Globals);
self.* = .{
.alloc = alloc,
.state = .{},
.map = .{},
};
const registry = try display.getRegistry();
registry.setListener(*Globals, registryListener, self);
if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
// Do another roundtrip to process events emitted by globals we bound
// during registry discovery (e.g. default decoration mode, output
// order). Listeners are installed at bind time in registryListener.
if (self.needs_roundtrip) {
if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
}
return self;
}
pub fn deinit(self: *Globals) void {
if (self.state.primary_output_name) |name| self.alloc.free(name);
self.alloc.destroy(self);
}
pub fn get(self: *const Globals, comptime tag: Tag) ?*tag.Type() {
const binding = self.map.get(tag) orelse return null;
return @ptrCast(binding.proxy);
}
fn onGlobalAttached(self: *Globals, comptime tag: Tag) void {
// Install listeners immediately at bind time. This
// keeps listener setup and object lifetime in one
// place and also supports globals that appear later.
switch (tag) {
.ext_background_effect => {
const v = self.get(tag) orelse return;
v.setListener(*Globals, bgEffectListener, self);
self.needs_roundtrip = true;
},
.kde_decoration_manager => {
const v = self.get(tag) orelse return;
v.setListener(*Globals, decoManagerListener, self);
self.needs_roundtrip = true;
},
.kde_output_order => {
const v = self.get(tag) orelse return;
v.setListener(*Globals, outputOrderListener, self);
self.needs_roundtrip = true;
},
else => {},
}
}
fn onGlobalRemoved(self: *Globals, tag: Tag) void {
switch (tag) {
.kde_output_order => self.state.resetOutputOrder(self.alloc),
else => {},
}
}
fn registryListener(
registry: *wl.Registry,
event: wl.Registry.Event,
self: *Globals,
) void {
switch (event) {
.global => |v| {
log.debug("found global {s}", .{v.interface});
inline for (comptime std.meta.tags(Tag)) |tag| {
const T = tag.Type();
if (std.mem.orderZ(u8, v.interface, T.interface.name) == .eq) {
log.debug("matched {}", .{T});
const new_proxy = registry.bind(
v.name,
T,
T.generated_version,
) catch |err| {
log.warn(
"error binding interface {s} error={}",
.{ v.interface, err },
);
return;
};
// If this global was already bound,
// then we also need to destroy the old binding.
if (self.map.get(tag)) |old| {
self.onGlobalRemoved(tag);
old.proxy.destroy();
}
self.map.put(tag, .{
.proxy = @ptrCast(new_proxy),
.name = v.name,
});
self.onGlobalAttached(tag);
}
}
},
// This should be a rare occurrence, but in case a global
// is suddenly no longer available, we destroy and unset it
// as the protocol mandates.
.global_remove => |v| {
var it = self.map.iterator();
while (it.next()) |kv| {
if (kv.value.name != v.name) continue;
self.onGlobalRemoved(kv.key);
kv.value.proxy.destroy();
self.map.remove(kv.key);
}
},
}
}
fn bgEffectListener(
_: *ext.BackgroundEffectManagerV1,
event: ext.BackgroundEffectManagerV1.Event,
self: *Globals,
) void {
switch (event) {
.capabilities => |cap| {
self.state.bg_effect_capabilities = cap.flags;
},
}
}
fn decoManagerListener(
_: *org.KdeKwinServerDecorationManager,
event: org.KdeKwinServerDecorationManager.Event,
self: *Globals,
) void {
switch (event) {
.default_mode => |mode| {
self.state.default_deco_mode = @enumFromInt(mode.mode);
},
}
}
fn outputOrderListener(
_: *kde.OutputOrderV1,
event: kde.OutputOrderV1.Event,
self: *Globals,
) void {
switch (event) {
.output => |v| {
// Only the first output event after a `done` is the new primary.
if (!self.state.output_order_done) return;
self.state.output_order_done = false;
const name = std.mem.sliceTo(v.output_name, 0);
if (self.state.primary_output_name) |old| self.alloc.free(old);
if (name.len == 0) {
self.state.primary_output_name = null;
log.warn("ignoring empty primary output name from kde_output_order_v1", .{});
} else {
self.state.primary_output_name = self.alloc.dupeZ(u8, name) catch |err| {
self.state.primary_output_name = null;
log.warn("failed to allocate primary output name: {}", .{err});
return;
};
log.debug("primary output: {s}", .{name});
}
},
.done => {
if (self.state.output_order_done) {
// No output arrived since the previous done. Treat this as
// an empty update and drop any stale cached primary.
self.state.resetOutputOrder(self.alloc);
return;
}
self.state.output_order_done = true;
},
}
}

View File

@@ -19,6 +19,7 @@ pub const c = @cImport({
const input = @import("../../../input.zig");
const Config = @import("../../../config.zig").Config;
const ApprtWindow = @import("../class/window.zig").Window;
const BlurRegion = @import("BlurRegion.zig");
const log = std.log.scoped(.gtk_x11);
@@ -48,7 +49,7 @@ pub const App = struct {
else
"ghostty";
// Set the X11 window class property (WM_CLASS) if are are on an X11
// Set the X11 window class property (WM_CLASS) if we are on an X11
// display.
//
// Note that we also set the program name here using g_set_prgname.
@@ -106,9 +107,8 @@ pub const App = struct {
};
}
pub fn deinit(self: *App, alloc: Allocator) void {
pub fn deinit(self: *App) void {
_ = self;
_ = alloc;
}
/// Checks for an immediate pending XKB state update event, and returns the
@@ -170,13 +170,13 @@ pub const Window = struct {
app: *App,
apprt_window: *ApprtWindow,
x11_surface: *gdk_x11.X11Surface,
alloc: Allocator,
blur_region: Region = .{},
blur_region: BlurRegion = .empty,
// Cache last applied values to avoid redundant X11 property updates.
// Redundant property updates seem to cause some visual glitches
// with some window managers: https://github.com/ghostty-org/ghostty/pull/8075
last_applied_blur_region: ?Region = null,
last_applied_decoration_hints: ?MotifWMHints = null,
pub fn init(
@@ -184,8 +184,6 @@ pub const Window = struct {
app: *App,
apprt_window: *ApprtWindow,
) !Window {
_ = alloc;
const surface = apprt_window.as(gtk.Native).getSurface() orelse
return error.NotX11Surface;
@@ -196,49 +194,31 @@ pub const Window = struct {
return .{
.app = app,
.alloc = alloc,
.apprt_window = apprt_window,
.x11_surface = x11_surface,
};
}
pub fn deinit(self: Window, alloc: Allocator) void {
_ = self;
_ = alloc;
pub fn deinit(self: *Window) void {
self.blur_region.deinit(self.alloc);
}
pub fn resizeEvent(self: *Window) !void {
// The blur region must update with window resizes
try self.syncBlur();
self.syncBlur() catch |err| {
log.err("failed to sync blur={}", .{err});
};
}
pub fn syncAppearance(self: *Window) !void {
// The user could have toggled between CSDs and SSDs,
// therefore we need to recalculate the blur region offset.
self.blur_region = blur: {
// NOTE(pluiedev): CSDs are a f--king mistake.
// Please, GNOME, stop this nonsense of making a window ~30% bigger
// internally than how they really are just for your shadows and
// rounded corners and all that fluff. Please. I beg of you.
var x: f64 = 0;
var y: f64 = 0;
self.apprt_window.as(gtk.Native).getSurfaceTransform(&x, &y);
// Transform surface coordinates to device coordinates.
const scale: f64 = @floatFromInt(self.apprt_window.as(gtk.Widget).getScaleFactor());
x *= scale;
y *= scale;
break :blur .{
.x = @intFromFloat(x),
.y = @intFromFloat(y),
};
};
self.syncBlur() catch |err| {
log.err("failed to synchronize blur={}", .{err});
log.err("failed to sync blur={}", .{err});
};
self.syncDecorations() catch |err| {
log.err("failed to synchronize decorations={}", .{err});
log.err("failed to sync decorations={}", .{err});
};
}
@@ -250,53 +230,49 @@ pub const Window = struct {
}
fn syncBlur(self: *Window) !void {
// FIXME: This doesn't currently factor in rounded corners on Adwaita,
// which means that the blur region will grow slightly outside of the
// window borders. Unfortunately, actually calculating the rounded
// region can be quite complex without having access to existing APIs
// (cf. https://github.com/cutefishos/fishui/blob/41d4ba194063a3c7fff4675619b57e6ac0504f06/src/platforms/linux/blurhelper/windowblur.cpp#L134)
// and I think it's not really noticeable enough to justify the effort.
// (Wayland also has this visual artifact anyway...)
const gtk_widget = self.apprt_window.as(gtk.Widget);
const config = if (self.apprt_window.getConfig()) |v| v.get() else return;
// When blur is disabled, remove the property if it was previously set
const blur = config.@"background-blur";
if (!blur.enabled()) {
if (self.last_applied_blur_region != null) {
try self.deleteProperty(self.app.atoms.kde_blur);
self.last_applied_blur_region = null;
}
var region: BlurRegion = if (blur.enabled())
try .calcForWindow(
self.alloc,
self.apprt_window,
self.clientSideDecorationEnabled(),
true,
)
else
.empty;
errdefer region.deinit(self.alloc);
// Only update X11 properties when the blur region actually changes
if (region.eql(self.blur_region)) {
region.deinit(self.alloc);
return;
}
// Transform surface coordinates to device coordinates.
const scale = gtk_widget.getScaleFactor();
self.blur_region.width = gtk_widget.getWidth() * scale;
self.blur_region.height = gtk_widget.getHeight() * scale;
if (region.slices.items.len > 0) {
log.debug("set blur={}, window xid={}, region={}", .{
blur,
self.x11_surface.getXid(),
region,
});
// Only update X11 properties when the blur region actually changes
if (self.last_applied_blur_region) |last| {
if (std.meta.eql(self.blur_region, last)) return;
try self.changeProperty(
BlurRegion.Slice,
self.app.atoms.kde_blur,
c.XA_CARDINAL,
._32,
.{ .mode = .replace },
region.slices.items,
);
} else {
try self.deleteProperty(self.app.atoms.kde_blur);
}
log.debug("set blur={}, window xid={}, region={}", .{
blur,
self.x11_surface.getXid(),
self.blur_region,
});
try self.changeProperty(
Region,
self.app.atoms.kde_blur,
c.XA_CARDINAL,
._32,
.{ .mode = .replace },
&self.blur_region,
);
self.last_applied_blur_region = self.blur_region;
self.blur_region.deinit(self.alloc);
self.blur_region = region;
}
fn syncDecorations(self: *Window) !void {
@@ -336,7 +312,7 @@ pub const Window = struct {
self.app.atoms.motif_wm_hints,
._32,
.{ .mode = .replace },
&hints,
&.{hints},
);
self.last_applied_decoration_hints = hints;
}
@@ -411,9 +387,11 @@ pub const Window = struct {
options: struct {
mode: PropertyChangeMode,
},
value: *T,
values: []const T,
) X11Error!void {
const data: format.bufferType() = @ptrCast(value);
const data: format.bufferType() = @ptrCast(@constCast(values));
// The number of "words" that each element `T` occupies.
const words_per_elem = @divExact(@sizeOf(T), @sizeOf(format.elemType()));
const status = c.XChangeProperty(
@ptrCast(@alignCast(self.app.display)),
@@ -423,7 +401,7 @@ pub const Window = struct {
@intFromEnum(format),
@intFromEnum(options.mode),
data,
@divExact(@sizeOf(T), @sizeOf(format.elemType())),
@intCast(words_per_elem * values.len),
);
// For some godforsaken reason Xlib alternates between
@@ -499,13 +477,6 @@ const PropertyFormat = enum(c_int) {
}
};
const Region = extern struct {
x: c_long = 0,
y: c_long = 0,
width: c_long = 0,
height: c_long = 0,
};
// See Xm/MwmUtil.h, packaged with the Motif Window Manager
const MotifWMHints = extern struct {
flags: packed struct(c_ulong) {

View File

@@ -188,7 +188,7 @@ pub fn newConfig(
if (prev) |p| {
if (shouldInheritWorkingDirectory(context, config)) {
if (try p.pwd(alloc)) |pwd| {
copy.@"working-directory" = pwd;
copy.@"working-directory" = .{ .path = pwd };
}
}
}