From 96f8f0d93c19efec5cc349f71965a405f5c9c2a3 Mon Sep 17 00:00:00 2001 From: Jake Guthmiller Date: Sat, 7 Feb 2026 20:20:49 -0600 Subject: [PATCH 01/11] gtk: add setMonitor binding and kde-output-order-v1 protocol Add the missing setMonitor() function to the gtk4-layer-shell Zig bindings and provide the gdk module so it can reference gdk.Monitor. Register the kde-output-order-v1 Wayland protocol from plasma-wayland-protocols and generate its scanner binding. This protocol reports the compositor's monitor priority ordering and is needed to correctly identify the primary monitor for quick-terminal-screen support on Linux. Co-Authored-By: Claude Opus 4.6 --- pkg/gtk4-layer-shell/src/main.zig | 5 +++++ src/build/SharedDeps.zig | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/pkg/gtk4-layer-shell/src/main.zig b/pkg/gtk4-layer-shell/src/main.zig index f7848ea94..e61ce3508 100644 --- a/pkg/gtk4-layer-shell/src/main.zig +++ b/pkg/gtk4-layer-shell/src/main.zig @@ -3,6 +3,7 @@ const std = @import("std"); const c = @cImport({ @cInclude("gtk4-layer-shell.h"); }); +const gdk = @import("gdk"); const gtk = @import("gtk"); pub const ShellLayer = enum(c_uint) { @@ -61,6 +62,10 @@ pub fn setKeyboardMode(window: *gtk.Window, mode: KeyboardMode) void { c.gtk_layer_set_keyboard_mode(@ptrCast(window), @intFromEnum(mode)); } +pub fn setMonitor(window: *gtk.Window, monitor: ?*gdk.Monitor) void { + c.gtk_layer_set_monitor(@ptrCast(window), if (monitor) |m| @ptrCast(m) else null); +} + pub fn setNamespace(window: *gtk.Window, name: [:0]const u8) void { c.gtk_layer_set_namespace(@ptrCast(window), name.ptr); } diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 9276c9914..bd922c591 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -636,12 +636,16 @@ fn addGtkNg( scanner.addCustomProtocol( plasma_wayland_protocols_dep.path("src/protocols/slide.xml"), ); + scanner.addCustomProtocol( + plasma_wayland_protocols_dep.path("src/protocols/kde-output-order-v1.xml"), + ); scanner.addSystemProtocol("staging/xdg-activation/xdg-activation-v1.xml"); scanner.generate("wl_compositor", 1); scanner.generate("org_kde_kwin_blur_manager", 1); scanner.generate("org_kde_kwin_server_decoration_manager", 1); scanner.generate("org_kde_kwin_slide_manager", 1); + scanner.generate("kde_output_order_v1", 1); scanner.generate("xdg_activation_v1", 1); step.root_module.addImport("wayland", b.createModule(.{ @@ -661,6 +665,10 @@ fn addGtkNg( "gtk", gobject.module("gtk4"), ); + if (gobject_) |gobject| layer_shell_module.addImport( + "gdk", + gobject.module("gdk4"), + ); step.root_module.addImport( "gtk4-layer-shell", layer_shell_module, From 6da660a9a5eb9e57175035d23434b4c44b1b4151 Mon Sep 17 00:00:00 2001 From: Jake Guthmiller Date: Sat, 7 Feb 2026 20:22:15 -0600 Subject: [PATCH 02/11] gtk: implement quick-terminal-screen for Wayland Implement the quick-terminal-screen config option on Linux/Wayland so users can pin the quick terminal to a specific monitor instead of always following the mouse cursor. Use the kde_output_order_v1 protocol to identify the compositor's primary monitor by connector name (e.g. "DP-1"). When the protocol is unavailable, fall back to the first monitor in the GDK list. - Add resolveQuickTerminalMonitor() to map config values to a gdk.Monitor: .mouse returns null (compositor decides), .main and .macos-menu-bar match by connector name via the protocol - Call layer_shell.setMonitor() in both initQuickTerminal and syncQuickTerminal so config reloads take effect - Update enteredMonitor to size the window using the configured monitor rather than whichever monitor was entered - Update config documentation to reflect Linux support Co-Authored-By: Claude Opus 4.6 --- src/apprt/gtk/winproto/wayland.zig | 128 +++++++++++++++++++++++++++-- src/config/Config.zig | 8 +- 2 files changed, 128 insertions(+), 8 deletions(-) diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index ec02fbee5..f2216bbf2 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -14,6 +14,7 @@ const input = @import("../../../input.zig"); const ApprtWindow = @import("../class/window.zig").Window; const wl = wayland.client.wl; +const kde = wayland.client.kde; const org = wayland.client.org; const xdg = wayland.client.xdg; @@ -33,6 +34,18 @@ pub const App = struct { kde_slide_manager: ?*org.KdeKwinSlideManager = null, + kde_output_order: ?*kde.OutputOrderV1 = null, + + /// 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: ?[63:0]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, xdg_activation: ?*xdg.ActivationV1 = null, @@ -83,9 +96,16 @@ pub const App = struct { registry.setListener(*Context, registryListener, context); if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed; - // Do another round-trip to get the default decoration mode + // Set up listeners for protocols that send events on bind. + // All listeners must be set before the roundtrip so that + // events aren't lost. if (context.kde_decoration_manager) |deco_manager| { deco_manager.setListener(*Context, decoManagerListener, context); + } + if (context.kde_output_order) |output_order| { + output_order.setListener(*Context, outputOrderListener, context); + } + if (context.kde_decoration_manager != null or context.kde_output_order != null) { if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed; } @@ -127,9 +147,55 @@ pub const App = struct { 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); + + // Set target monitor based on config (null lets compositor decide) + const monitor = resolveQuickTerminalMonitor(self.context, apprt_window); + layer_shell.setMonitor(window, monitor); + } + + /// Resolve the quick-terminal-screen config to a specific monitor. + /// Returns null to let the compositor decide (used for .mouse mode). + fn resolveQuickTerminalMonitor( + context: *Context, + apprt_window: *ApprtWindow, + ) ?*gdk.Monitor { + const config = if (apprt_window.getConfig()) |v| v.get() else return null; + const display = apprt_window.as(gtk.Widget).getDisplay(); + + return switch (config.@"quick-terminal-screen") { + .mouse => null, + .main, .@"macos-menu-bar" => blk: { + const monitors = display.getMonitors(); + const primary_name: ?[]const u8 = if (context.primary_output_name) |*buf| + std.mem.sliceTo(buf, 0) + else + null; + + var fallback: ?*gdk.Monitor = null; + var i: u32 = 0; + while (monitors.getObject(i)) |item| : (i += 1) { + // getObject returns transfer-full; release immediately. + // The display keeps its own ref so the pointer stays valid. + item.unref(); + const monitor = gobject.ext.cast(gdk.Monitor, item) orelse continue; + if (fallback == null) fallback = monitor; + + if (primary_name) |name| { + const connector = std.mem.sliceTo( + monitor.getConnector() orelse continue, + 0, + ); + if (std.mem.eql(u8, connector, name)) { + break :blk monitor; + } + } + } + break :blk fallback; + }, + }; } fn getInterfaceType(comptime field: std.builtin.Type.StructField) ?type { @@ -200,10 +266,20 @@ pub const App = struct { .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; + if (@field(context, field.name)) |global| { + if (global.getId() == v.name) { + global.destroy(); + @field(context, field.name) = null; + + // Reset cached primary-output state if the protocol + // providing it disappears. + if (comptime std.mem.eql(u8, field.name, "kde_output_order")) { + context.primary_output_name = null; + context.primary_output_match_failed_logged = false; + context.output_order_done = true; + } + break :remove; + } } } }, @@ -221,6 +297,30 @@ pub const App = struct { }, } } + + fn outputOrderListener( + _: *kde.OutputOrderV1, + event: kde.OutputOrderV1.Event, + context: *Context, + ) void { + switch (event) { + .output => |v| { + if (context.output_order_done) { + context.output_order_done = false; + const name = std.mem.sliceTo(v.output_name, 0); + if (name.len <= 63) { + var buf: [63:0]u8 = @splat(0); + @memcpy(buf[0..name.len], name); + context.primary_output_name = buf; + log.debug("primary output: {s}", .{name}); + } + } + }, + .done => { + context.output_order_done = true; + }, + } + } }; /// Per-window (wl_surface) state for the Wayland protocol. @@ -417,6 +517,11 @@ 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 = App.resolveQuickTerminalMonitor(self.app_context, self.apprt_window); + layer_shell.setMonitor(window, target_monitor); + layer_shell.setKeyboardMode( window, switch (config.@"quick-terminal-keyboard-interactivity") { @@ -486,8 +591,17 @@ pub const Window = struct { const window = apprt_window.as(gtk.Window); const config = if (apprt_window.getConfig()) |v| v.get() else return; + // Use the configured monitor for sizing if not in mouse mode + const size_monitor = switch (config.@"quick-terminal-screen") { + .mouse => monitor, + .main, .@"macos-menu-bar" => App.resolveQuickTerminalMonitor( + apprt_window.winproto().wayland.app_context, + apprt_window, + ) 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", diff --git a/src/config/Config.zig b/src/config/Config.zig index bf9860c13..94d2ba8d3 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2680,7 +2680,13 @@ keybind: Keybinds = .{}, /// The default value is `main` because this is the recommended screen /// by the operating system. /// -/// Only implemented on macOS. +/// On macOS, `macos-menu-bar` uses the screen containing the menu bar. +/// On Linux/Wayland, `macos-menu-bar` is treated as equivalent to `main`. +/// +/// Note: On Linux, there is no universal concept of a "primary" monitor. +/// Ghostty uses the compositor-reported primary output when available and +/// falls back to the first monitor reported by GDK if no primary output can +/// be resolved. @"quick-terminal-screen": QuickTerminalScreen = .main, /// Duration (in seconds) of the quick terminal enter and exit animation. From 630c2dff190af14b7915f3e0d4df639e95c4f21b Mon Sep 17 00:00:00 2001 From: Jake Guthmiller Date: Sat, 7 Feb 2026 21:03:29 -0600 Subject: [PATCH 03/11] gtk: fix monitor ref ownership in Wayland quick terminal Handle g_list_model_get_object transfer-full semantics in resolveQuickTerminalMonitor by retaining exactly one monitor reference to return and unreffing the rest. Update init/sync/sizing call sites to unref the resolved monitor after setMonitor/getGeometry so monitor lifetimes are explicit and consistent. Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com> --- src/apprt/gtk/winproto/wayland.zig | 64 +++++++++++++++++++++++------- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index f2216bbf2..9ce89146f 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -41,6 +41,10 @@ pub const App = struct { /// is the primary. primary_output_name: ?[63:0]u8 = null, + /// Used to avoid repeatedly logging the same primary-name mismatch + /// when we can't map the compositor connector name to a GDK monitor. + primary_output_match_failed_logged: bool = false, + /// 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. @@ -153,6 +157,7 @@ pub const App = struct { // Set target monitor based on config (null lets compositor decide) const monitor = resolveQuickTerminalMonitor(self.context, apprt_window); + defer if (monitor) |v| v.unref(); layer_shell.setMonitor(window, monitor); } @@ -174,25 +179,44 @@ pub const App = struct { else null; + // We own a strong ref for every object returned by getObject. + // Keep one ref as a fallback to return, and release all others. var fallback: ?*gdk.Monitor = null; + var matched_primary = false; var i: u32 = 0; while (monitors.getObject(i)) |item| : (i += 1) { - // getObject returns transfer-full; release immediately. - // The display keeps its own ref so the pointer stays valid. - item.unref(); - const monitor = gobject.ext.cast(gdk.Monitor, item) orelse continue; - if (fallback == null) fallback = monitor; + const monitor = gobject.ext.cast(gdk.Monitor, item) orelse { + item.unref(); + continue; + }; + const keep_as_fallback = fallback == null; + if (keep_as_fallback) fallback = monitor; if (primary_name) |name| { - const connector = std.mem.sliceTo( - monitor.getConnector() orelse continue, - 0, - ); - if (std.mem.eql(u8, connector, name)) { - break :blk monitor; + if (monitor.getConnector()) |connector_z| { + const connector = std.mem.sliceTo(connector_z, 0); + if (std.mem.eql(u8, connector, name)) { + matched_primary = true; + context.primary_output_match_failed_logged = false; + if (fallback) |v| { + if (v != monitor) v.unref(); + } + break :blk monitor; + } } } + + if (!keep_as_fallback) monitor.unref(); } + + if (primary_name != null and !matched_primary and !context.primary_output_match_failed_logged) { + context.primary_output_match_failed_logged = true; + log.debug( + "could not match primary output connector to a GDK monitor; falling back to first monitor", + .{}, + ); + } + break :blk fallback; }, }; @@ -312,7 +336,13 @@ pub const App = struct { var buf: [63:0]u8 = @splat(0); @memcpy(buf[0..name.len], name); context.primary_output_name = buf; + context.primary_output_match_failed_logged = false; log.debug("primary output: {s}", .{name}); + } else { + log.warn( + "ignoring primary output name longer than 63 bytes from kde_output_order_v1", + .{}, + ); } } }, @@ -520,6 +550,7 @@ pub const Window = struct { // 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 = App.resolveQuickTerminalMonitor(self.app_context, self.apprt_window); + defer if (target_monitor) |v| v.unref(); layer_shell.setMonitor(window, target_monitor); layer_shell.setKeyboardMode( @@ -591,14 +622,17 @@ pub const Window = struct { const window = apprt_window.as(gtk.Window); const config = if (apprt_window.getConfig()) |v| v.get() else return; - // Use the configured monitor for sizing if not in mouse mode - const size_monitor = switch (config.@"quick-terminal-screen") { - .mouse => monitor, + const resolved_monitor = switch (config.@"quick-terminal-screen") { + .mouse => null, .main, .@"macos-menu-bar" => App.resolveQuickTerminalMonitor( apprt_window.winproto().wayland.app_context, apprt_window, - ) orelse monitor, + ), }; + 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; size_monitor.getGeometry(&monitor_size); From e25d8a6f2f4a1d384866ab222f920f351b8905da Mon Sep 17 00:00:00 2001 From: Jake Guthmiller Date: Fri, 13 Feb 2026 21:26:34 -0600 Subject: [PATCH 04/11] gtk: harden quick-terminal output-order state handling Install Wayland protocol listeners at bind time so late-added globals still receive events and listener setup stays tied to object lifetime. Track whether kde_output_order_v1 emitted any outputs in a cycle and clear cached primary-output state on empty or invalid updates. Also reset this cycle tracking when the protocol global is removed to avoid stale monitor selection. --- src/apprt/gtk/winproto/wayland.zig | 62 ++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index 9ce89146f..52ac92831 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -50,6 +50,10 @@ pub const App = struct { /// Initialized to true so the first event after binding is captured. output_order_done: bool = true, + /// True if we've received an `output` event in the current cycle. + /// This lets us detect empty cycles and clear stale cached state. + output_order_seen_output: bool = false, + default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null, xdg_activation: ?*xdg.ActivationV1 = null, @@ -100,15 +104,9 @@ pub const App = struct { registry.setListener(*Context, registryListener, context); if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed; - // Set up listeners for protocols that send events on bind. - // All listeners must be set before the roundtrip so that - // events aren't lost. - if (context.kde_decoration_manager) |deco_manager| { - deco_manager.setListener(*Context, decoManagerListener, context); - } - if (context.kde_output_order) |output_order| { - output_order.setListener(*Context, outputOrderListener, context); - } + // 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 (context.kde_decoration_manager != null or context.kde_output_order != null) { if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed; } @@ -269,7 +267,15 @@ pub const App = struct { ) == .eq) { log.debug("matched {}", .{T}); - @field(context, field.name) = registry.bind( + if (@field(context, field.name) != null) { + log.warn( + "duplicate global for {s}; keeping existing binding", + .{v.interface}, + ); + break; + } + + const global = registry.bind( v.name, T, T.generated_version, @@ -280,6 +286,22 @@ pub const App = struct { ); return; }; + @field(context, field.name) = global; + + // Install listeners immediately at bind time. This + // keeps listener setup and object lifetime in one + // place and also supports globals that appear later. + if (comptime std.mem.eql(u8, field.name, "kde_decoration_manager")) { + const deco_manager: *org.KdeKwinServerDecorationManager = + @field(context, field.name).?; + deco_manager.setListener(*Context, decoManagerListener, context); + } + if (comptime std.mem.eql(u8, field.name, "kde_output_order")) { + const output_order: *kde.OutputOrderV1 = + @field(context, field.name).?; + output_order.setListener(*Context, outputOrderListener, context); + } + break; } } }, @@ -301,6 +323,7 @@ pub const App = struct { context.primary_output_name = null; context.primary_output_match_failed_logged = false; context.output_order_done = true; + context.output_order_seen_output = false; } break :remove; } @@ -331,14 +354,24 @@ pub const App = struct { .output => |v| { if (context.output_order_done) { context.output_order_done = false; + context.output_order_seen_output = true; const name = std.mem.sliceTo(v.output_name, 0); - if (name.len <= 63) { + if (name.len == 0) { + context.primary_output_name = null; + context.primary_output_match_failed_logged = false; + log.warn( + "ignoring empty primary output name from kde_output_order_v1", + .{}, + ); + } else if (name.len <= 63) { var buf: [63:0]u8 = @splat(0); @memcpy(buf[0..name.len], name); context.primary_output_name = buf; context.primary_output_match_failed_logged = false; log.debug("primary output: {s}", .{name}); } else { + context.primary_output_name = null; + context.primary_output_match_failed_logged = false; log.warn( "ignoring primary output name longer than 63 bytes from kde_output_order_v1", .{}, @@ -347,7 +380,14 @@ pub const App = struct { } }, .done => { + // An empty update means the compositor currently reports no + // outputs in priority order, so drop any stale cached primary. + if (!context.output_order_seen_output) { + context.primary_output_name = null; + context.primary_output_match_failed_logged = false; + } context.output_order_done = true; + context.output_order_seen_output = false; }, } } From 34473b069bd74a729d001e3f71df3b03e890a739 Mon Sep 17 00:00:00 2001 From: Jake Guthmiller Date: Fri, 13 Feb 2026 21:32:12 -0600 Subject: [PATCH 05/11] gtk: simplify quick-terminal monitor resolution and state management Restructure resolveQuickTerminalMonitor into a two-phase approach (match by name, then fall back to first monitor) to eliminate the interleaved fallback/match ref tracking. Remove redundant switch in enteredMonitor that duplicated the .mouse handling already in resolveQuickTerminalMonitor. Hoist the primary_output_match_failed_logged reset above the name-length branches in outputOrderListener. Co-Authored-By: Claude Opus 4.6 --- src/apprt/gtk/winproto/wayland.zig | 81 ++++++++++++------------------ 1 file changed, 31 insertions(+), 50 deletions(-) diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index 52ac92831..2624cd721 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -161,6 +161,7 @@ pub const App = 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( context: *Context, apprt_window: *ApprtWindow, @@ -172,50 +173,41 @@ pub const App = struct { .mouse => null, .main, .@"macos-menu-bar" => blk: { const monitors = display.getMonitors(); - const primary_name: ?[]const u8 = if (context.primary_output_name) |*buf| - std.mem.sliceTo(buf, 0) - else - null; - // We own a strong ref for every object returned by getObject. - // Keep one ref as a fallback to return, and release all others. - var fallback: ?*gdk.Monitor = null; - var matched_primary = false; - var i: u32 = 0; - while (monitors.getObject(i)) |item| : (i += 1) { - const monitor = gobject.ext.cast(gdk.Monitor, item) orelse { - item.unref(); - continue; - }; - const keep_as_fallback = fallback == null; - if (keep_as_fallback) fallback = monitor; - - if (primary_name) |name| { + // Try to find the monitor matching the primary output name. + if (context.primary_output_name) |*stored_name| { + const name = std.mem.sliceTo(stored_name, 0); + 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| { const connector = std.mem.sliceTo(connector_z, 0); if (std.mem.eql(u8, connector, name)) { - matched_primary = true; context.primary_output_match_failed_logged = false; - if (fallback) |v| { - if (v != monitor) v.unref(); - } break :blk monitor; } } + monitor.unref(); } - if (!keep_as_fallback) monitor.unref(); + if (!context.primary_output_match_failed_logged) { + context.primary_output_match_failed_logged = true; + log.debug( + "could not match primary output connector to a GDK monitor; falling back to first monitor", + .{}, + ); + } } - if (primary_name != null and !matched_primary and !context.primary_output_match_failed_logged) { - context.primary_output_match_failed_logged = true; - log.debug( - "could not match primary output connector to a GDK monitor; falling back to first monitor", - .{}, - ); - } - - break :blk fallback; + // Fall back to the first monitor in the list. + const first = monitors.getObject(0) orelse break :blk null; + break :blk gobject.ext.cast(gdk.Monitor, first) orelse { + first.unref(); + break :blk null; + }; }, }; } @@ -355,27 +347,19 @@ pub const App = struct { if (context.output_order_done) { context.output_order_done = false; context.output_order_seen_output = true; + context.primary_output_match_failed_logged = false; const name = std.mem.sliceTo(v.output_name, 0); if (name.len == 0) { context.primary_output_name = null; - context.primary_output_match_failed_logged = false; - log.warn( - "ignoring empty primary output name from kde_output_order_v1", - .{}, - ); + log.warn("ignoring empty primary output name from kde_output_order_v1", .{}); } else if (name.len <= 63) { var buf: [63:0]u8 = @splat(0); @memcpy(buf[0..name.len], name); context.primary_output_name = buf; - context.primary_output_match_failed_logged = false; log.debug("primary output: {s}", .{name}); } else { context.primary_output_name = null; - context.primary_output_match_failed_logged = false; - log.warn( - "ignoring primary output name longer than 63 bytes from kde_output_order_v1", - .{}, - ); + log.warn("ignoring primary output name longer than 63 bytes from kde_output_order_v1", .{}); } } }, @@ -662,13 +646,10 @@ 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 = switch (config.@"quick-terminal-screen") { - .mouse => null, - .main, .@"macos-menu-bar" => App.resolveQuickTerminalMonitor( - apprt_window.winproto().wayland.app_context, - apprt_window, - ), - }; + const resolved_monitor = App.resolveQuickTerminalMonitor( + apprt_window.winproto().wayland.app_context, + apprt_window, + ); defer if (resolved_monitor) |v| v.unref(); // Use the configured monitor for sizing if not in mouse mode. From 19feaa058b333a559697fa21f7d600db0f2386fc Mon Sep 17 00:00:00 2001 From: Jake Guthmiller Date: Sun, 1 Mar 2026 15:21:03 -0600 Subject: [PATCH 06/11] gtk: improve readability of Wayland quick-terminal monitor code Flatten resolveQuickTerminalMonitor by replacing the labeled-block switch with early returns, extract max_output_name_len constant, and reduce nesting in the output-order event handler. Co-Authored-By: Claude Opus 4.6 --- src/apprt/gtk/winproto/wayland.zig | 114 +++++++++++++++-------------- 1 file changed, 59 insertions(+), 55 deletions(-) diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index 2624cd721..206221e26 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -26,6 +26,8 @@ pub const App = struct { context: *Context, const Context = struct { + const max_output_name_len = 63; + kde_blur_manager: ?*org.KdeKwinBlurManager = null, // FIXME: replace with `zxdg_decoration_v1` once GTK merges @@ -39,7 +41,7 @@ pub const App = 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: ?[63:0]u8 = null, + primary_output_name: ?[max_output_name_len:0]u8 = null, /// Used to avoid repeatedly logging the same primary-name mismatch /// when we can't map the compositor connector name to a GDK monitor. @@ -167,48 +169,48 @@ pub const App = struct { 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(); - return switch (config.@"quick-terminal-screen") { - .mouse => null, - .main, .@"macos-menu-bar" => blk: { - const monitors = display.getMonitors(); - - // Try to find the monitor matching the primary output name. - if (context.primary_output_name) |*stored_name| { - const name = std.mem.sliceTo(stored_name, 0); - 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| { - const connector = std.mem.sliceTo(connector_z, 0); - if (std.mem.eql(u8, connector, name)) { - context.primary_output_match_failed_logged = false; - break :blk monitor; - } - } - monitor.unref(); - } - - if (!context.primary_output_match_failed_logged) { - context.primary_output_match_failed_logged = true; - log.debug( - "could not match primary output connector to a GDK monitor; falling back to first monitor", - .{}, - ); + // Try to find the monitor matching the primary output name. + if (context.primary_output_name) |*stored_name| { + const name = std.mem.sliceTo(stored_name, 0); + 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| { + const connector = std.mem.sliceTo(connector_z, 0); + if (std.mem.eql(u8, connector, name)) { + context.primary_output_match_failed_logged = false; + return monitor; } } + monitor.unref(); + } - // Fall back to the first monitor in the list. - const first = monitors.getObject(0) orelse break :blk null; - break :blk gobject.ext.cast(gdk.Monitor, first) orelse { - first.unref(); - break :blk null; - }; - }, + if (!context.primary_output_match_failed_logged) { + context.primary_output_match_failed_logged = true; + log.debug( + "could not match primary output connector to a GDK monitor; falling back to first monitor", + .{}, + ); + } + } + + // 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; }; } @@ -344,23 +346,25 @@ pub const App = struct { ) void { switch (event) { .output => |v| { - if (context.output_order_done) { - context.output_order_done = false; - context.output_order_seen_output = true; - context.primary_output_match_failed_logged = false; - const name = std.mem.sliceTo(v.output_name, 0); - if (name.len == 0) { - context.primary_output_name = null; - log.warn("ignoring empty primary output name from kde_output_order_v1", .{}); - } else if (name.len <= 63) { - var buf: [63:0]u8 = @splat(0); - @memcpy(buf[0..name.len], name); - context.primary_output_name = buf; - log.debug("primary output: {s}", .{name}); - } else { - context.primary_output_name = null; - log.warn("ignoring primary output name longer than 63 bytes from kde_output_order_v1", .{}); - } + // Only the first output event after a `done` is the new primary. + if (!context.output_order_done) return; + context.output_order_done = false; + context.output_order_seen_output = true; + // A new primary invalidates any cached match-failure state. + context.primary_output_match_failed_logged = false; + + const name = std.mem.sliceTo(v.output_name, 0); + if (name.len == 0) { + context.primary_output_name = null; + log.warn("ignoring empty primary output name from kde_output_order_v1", .{}); + } else if (name.len <= Context.max_output_name_len) { + var buf: [Context.max_output_name_len:0]u8 = @splat(0); + @memcpy(buf[0..name.len], name); + context.primary_output_name = buf; + log.debug("primary output: {s}", .{name}); + } else { + context.primary_output_name = null; + log.warn("ignoring primary output name longer than {} bytes from kde_output_order_v1", .{Context.max_output_name_len}); } }, .done => { From c9822535436c60587d136129a7c5beb44829d81b Mon Sep 17 00:00:00 2001 From: Jake Guthmiller Date: Sun, 1 Mar 2026 16:12:19 -0600 Subject: [PATCH 07/11] gtk: handle replacement Wayland globals before remove Track registry global names for kde decoration manager and kde_output_order bindings so we can distinguish same-global duplicates from valid replacements announced before global_remove. On global_remove, match and clear these bindings by registry global name to avoid dropping a replacement when the old global is removed. --- src/apprt/gtk/winproto/wayland.zig | 107 +++++++++++++++++++++-------- 1 file changed, 77 insertions(+), 30 deletions(-) diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index 206221e26..3dcbd89da 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -33,10 +33,12 @@ pub const App = struct { // 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_decoration_manager_global_name: ?u32 = null, kde_slide_manager: ?*org.KdeKwinSlideManager = null, kde_output_order: ?*kde.OutputOrderV1 = null, + kde_output_order_global_name: ?u32 = null, /// Connector name of the primary output (e.g., "DP-1") as reported /// by kde_output_order_v1. The first output in each priority list @@ -229,6 +231,18 @@ pub const App = struct { return T; } + /// Returns the Context field that stores the registry global name for + /// protocols that support replacement, or null for simple protocols. + fn getGlobalNameField(comptime field_name: []const u8) ?[]const u8 { + if (std.mem.eql(u8, field_name, "kde_decoration_manager")) { + return "kde_decoration_manager_global_name"; + } + if (std.mem.eql(u8, field_name, "kde_output_order")) { + return "kde_output_order_global_name"; + } + return null; + } + fn registryListener( registry: *wl.Registry, event: wl.Registry.Event, @@ -253,20 +267,34 @@ pub const App = struct { inline for (ctx_fields) |field| { const T = getInterfaceType(field) orelse continue; - - if (std.mem.orderZ( - u8, - v.interface, - T.interface.name, - ) == .eq) { + if (std.mem.orderZ(u8, v.interface, T.interface.name) == .eq) { log.debug("matched {}", .{T}); - if (@field(context, field.name) != null) { - log.warn( - "duplicate global for {s}; keeping existing binding", - .{v.interface}, - ); - break; + const existing_global = @field(context, field.name); + const global_name_field = comptime getGlobalNameField(field.name); + const existing_global_name: ?u32 = if (global_name_field) |name_field| + @field(context, name_field) + else + null; + + // Already bound: skip duplicate, allow replacement for + // protocols tracked by registry global name. + if (existing_global != null) { + if (global_name_field != null) { + if (existing_global_name != null and existing_global_name.? == v.name) { + log.debug( + "duplicate global for {s} with name={}; keeping existing binding", + .{ v.interface, v.name }, + ); + break; + } + } else { + log.warn( + "duplicate global for {s}; keeping existing binding", + .{v.interface}, + ); + break; + } } const global = registry.bind( @@ -280,20 +308,28 @@ pub const App = struct { ); return; }; + + if (existing_global) |old| { + log.debug( + "replacement global for {s}; switching old_name={} to new_name={}", + .{ v.interface, existing_global_name orelse 0, v.name }, + ); + old.destroy(); + } + @field(context, field.name) = global; + if (global_name_field) |name_field| { + @field(context, name_field) = v.name; + } // Install listeners immediately at bind time. This // keeps listener setup and object lifetime in one // place and also supports globals that appear later. if (comptime std.mem.eql(u8, field.name, "kde_decoration_manager")) { - const deco_manager: *org.KdeKwinServerDecorationManager = - @field(context, field.name).?; - deco_manager.setListener(*Context, decoManagerListener, context); + global.setListener(*Context, decoManagerListener, context); } if (comptime std.mem.eql(u8, field.name, "kde_output_order")) { - const output_order: *kde.OutputOrderV1 = - @field(context, field.name).?; - output_order.setListener(*Context, outputOrderListener, context); + global.setListener(*Context, outputOrderListener, context); } break; } @@ -306,20 +342,31 @@ pub const App = struct { .global_remove => |v| remove: { inline for (ctx_fields) |field| { if (getInterfaceType(field) == null) continue; - if (@field(context, field.name)) |global| { - if (global.getId() == v.name) { - global.destroy(); - @field(context, field.name) = null; - // Reset cached primary-output state if the protocol - // providing it disappears. - if (comptime std.mem.eql(u8, field.name, "kde_output_order")) { - context.primary_output_name = null; - context.primary_output_match_failed_logged = false; - context.output_order_done = true; - context.output_order_seen_output = false; + const global_name_field = comptime getGlobalNameField(field.name); + if (global_name_field) |name_field| { + if (@field(context, name_field)) |stored_name| { + if (stored_name == v.name) { + if (@field(context, field.name)) |global| global.destroy(); + @field(context, field.name) = null; + @field(context, name_field) = null; + + if (comptime std.mem.eql(u8, field.name, "kde_output_order")) { + context.primary_output_name = null; + context.primary_output_match_failed_logged = false; + context.output_order_done = true; + context.output_order_seen_output = false; + } + break :remove; + } + } + } else { + if (@field(context, field.name)) |global| { + if (global.getId() == v.name) { + global.destroy(); + @field(context, field.name) = null; + break :remove; } - break :remove; } } } From 18fa161222916c537fb5e71e6d7bbe2479805fb1 Mon Sep 17 00:00:00 2001 From: Jake Guthmiller Date: Sun, 1 Mar 2026 17:50:57 -0600 Subject: [PATCH 08/11] gtk: simplify Wayland output-order state handling --- src/apprt/gtk/winproto/wayland.zig | 34 +++++++++++++++++------------- src/build/SharedDeps.zig | 12 ++++------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index 3dcbd89da..158774149 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -54,10 +54,6 @@ pub const App = struct { /// Initialized to true so the first event after binding is captured. output_order_done: bool = true, - /// True if we've received an `output` event in the current cycle. - /// This lets us detect empty cycles and clear stale cached state. - output_order_seen_output: bool = false, - default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null, xdg_activation: ?*xdg.ActivationV1 = null, @@ -243,6 +239,13 @@ pub const App = struct { return null; } + /// Reset cached state derived from kde_output_order_v1. + fn resetOutputOrderState(context: *Context) void { + context.primary_output_name = null; + context.primary_output_match_failed_logged = false; + context.output_order_done = true; + } + fn registryListener( registry: *wl.Registry, event: wl.Registry.Event, @@ -315,6 +318,12 @@ pub const App = struct { .{ v.interface, existing_global_name orelse 0, v.name }, ); old.destroy(); + + if (comptime std.mem.eql(u8, field.name, "kde_output_order")) { + // Replacement means the previous primary may be stale + // until the new object sends a fresh cycle. + resetOutputOrderState(context); + } } @field(context, field.name) = global; @@ -352,10 +361,7 @@ pub const App = struct { @field(context, name_field) = null; if (comptime std.mem.eql(u8, field.name, "kde_output_order")) { - context.primary_output_name = null; - context.primary_output_match_failed_logged = false; - context.output_order_done = true; - context.output_order_seen_output = false; + resetOutputOrderState(context); } break :remove; } @@ -396,7 +402,6 @@ pub const App = struct { // Only the first output event after a `done` is the new primary. if (!context.output_order_done) return; context.output_order_done = false; - context.output_order_seen_output = true; // A new primary invalidates any cached match-failure state. context.primary_output_match_failed_logged = false; @@ -415,14 +420,13 @@ pub const App = struct { } }, .done => { - // An empty update means the compositor currently reports no - // outputs in priority order, so drop any stale cached primary. - if (!context.output_order_seen_output) { - context.primary_output_name = null; - context.primary_output_match_failed_logged = false; + if (context.output_order_done) { + // No output arrived since the previous done. Treat this as + // an empty update and drop any stale cached primary. + resetOutputOrderState(context); + return; } context.output_order_done = true; - context.output_order_seen_output = false; }, } } diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index bd922c591..4b1ea936d 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -661,14 +661,10 @@ fn addGtkNg( .optimize = optimize, })) |gtk4_layer_shell| { const layer_shell_module = gtk4_layer_shell.module("gtk4-layer-shell"); - if (gobject_) |gobject| layer_shell_module.addImport( - "gtk", - gobject.module("gtk4"), - ); - if (gobject_) |gobject| layer_shell_module.addImport( - "gdk", - gobject.module("gdk4"), - ); + if (gobject_) |gobject| { + layer_shell_module.addImport("gtk", gobject.module("gtk4")); + layer_shell_module.addImport("gdk", gobject.module("gdk4")); + } step.root_module.addImport( "gtk4-layer-shell", layer_shell_module, From beeb810c04d0b7c93cd74d215dd194eb03759cdb Mon Sep 17 00:00:00 2001 From: Jake Guthmiller Date: Mon, 2 Mar 2026 23:33:19 -0600 Subject: [PATCH 09/11] gtk: address PR review feedback for quick-terminal-screen --- pkg/gtk4-layer-shell/src/main.zig | 2 +- src/apprt/gtk/winproto/wayland.zig | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pkg/gtk4-layer-shell/src/main.zig b/pkg/gtk4-layer-shell/src/main.zig index e61ce3508..a15313231 100644 --- a/pkg/gtk4-layer-shell/src/main.zig +++ b/pkg/gtk4-layer-shell/src/main.zig @@ -63,7 +63,7 @@ pub fn setKeyboardMode(window: *gtk.Window, mode: KeyboardMode) void { } pub fn setMonitor(window: *gtk.Window, monitor: ?*gdk.Monitor) void { - c.gtk_layer_set_monitor(@ptrCast(window), if (monitor) |m| @ptrCast(m) else null); + c.gtk_layer_set_monitor(@ptrCast(window), @ptrCast(monitor)); } pub fn setNamespace(window: *gtk.Window, name: [:0]const u8) void { diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index 158774149..d2b0b33db 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -178,7 +178,6 @@ pub const App = struct { // Try to find the monitor matching the primary output name. if (context.primary_output_name) |*stored_name| { - const name = std.mem.sliceTo(stored_name, 0); var i: u32 = 0; while (monitors.getObject(i)) |item| : (i += 1) { const monitor = gobject.ext.cast(gdk.Monitor, item) orelse { @@ -186,8 +185,7 @@ pub const App = struct { continue; }; if (monitor.getConnector()) |connector_z| { - const connector = std.mem.sliceTo(connector_z, 0); - if (std.mem.eql(u8, connector, name)) { + if (std.mem.orderZ(u8, connector_z, stored_name) == .eq) { context.primary_output_match_failed_logged = false; return monitor; } @@ -282,6 +280,11 @@ pub const App = struct { // Already bound: skip duplicate, allow replacement for // protocols tracked by registry global name. + // Compositors may re-advertise globals at runtime + // (e.g. when a display server component restarts). + // For protocols with a stored global name we detect + // replacement (different name) vs harmless duplicate + // (same name); simple protocols just keep the first. if (existing_global != null) { if (global_name_field != null) { if (existing_global_name != null and existing_global_name.? == v.name) { From b823c07ae30635b4641d45db0f06f6f416756b94 Mon Sep 17 00:00:00 2001 From: Jake Guthmiller Date: Tue, 3 Mar 2026 20:56:24 -0600 Subject: [PATCH 10/11] PR feedback - simplify --- src/apprt/gtk/winproto/wayland.zig | 60 ++---------------------------- 1 file changed, 4 insertions(+), 56 deletions(-) diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index d2b0b33db..b5c9f6a52 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -45,10 +45,6 @@ pub const App = struct { /// is the primary. primary_output_name: ?[max_output_name_len:0]u8 = null, - /// Used to avoid repeatedly logging the same primary-name mismatch - /// when we can't map the compositor connector name to a GDK monitor. - primary_output_match_failed_logged: bool = false, - /// 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. @@ -186,20 +182,11 @@ pub const App = struct { }; if (monitor.getConnector()) |connector_z| { if (std.mem.orderZ(u8, connector_z, stored_name) == .eq) { - context.primary_output_match_failed_logged = false; return monitor; } } monitor.unref(); } - - if (!context.primary_output_match_failed_logged) { - context.primary_output_match_failed_logged = true; - log.debug( - "could not match primary output connector to a GDK monitor; falling back to first monitor", - .{}, - ); - } } // Fall back to the first monitor in the list. @@ -240,7 +227,6 @@ pub const App = struct { /// Reset cached state derived from kde_output_order_v1. fn resetOutputOrderState(context: *Context) void { context.primary_output_name = null; - context.primary_output_match_failed_logged = false; context.output_order_done = true; } @@ -271,38 +257,6 @@ pub const App = struct { if (std.mem.orderZ(u8, v.interface, T.interface.name) == .eq) { log.debug("matched {}", .{T}); - const existing_global = @field(context, field.name); - const global_name_field = comptime getGlobalNameField(field.name); - const existing_global_name: ?u32 = if (global_name_field) |name_field| - @field(context, name_field) - else - null; - - // Already bound: skip duplicate, allow replacement for - // protocols tracked by registry global name. - // Compositors may re-advertise globals at runtime - // (e.g. when a display server component restarts). - // For protocols with a stored global name we detect - // replacement (different name) vs harmless duplicate - // (same name); simple protocols just keep the first. - if (existing_global != null) { - if (global_name_field != null) { - if (existing_global_name != null and existing_global_name.? == v.name) { - log.debug( - "duplicate global for {s} with name={}; keeping existing binding", - .{ v.interface, v.name }, - ); - break; - } - } else { - log.warn( - "duplicate global for {s}; keeping existing binding", - .{v.interface}, - ); - break; - } - } - const global = registry.bind( v.name, T, @@ -315,22 +269,18 @@ pub const App = struct { return; }; - if (existing_global) |old| { - log.debug( - "replacement global for {s}; switching old_name={} to new_name={}", - .{ v.interface, existing_global_name orelse 0, v.name }, - ); + // Destroy old binding if this global was re-advertised. + // Bind first so a failed bind preserves the old binding. + if (@field(context, field.name)) |old| { old.destroy(); if (comptime std.mem.eql(u8, field.name, "kde_output_order")) { - // Replacement means the previous primary may be stale - // until the new object sends a fresh cycle. resetOutputOrderState(context); } } @field(context, field.name) = global; - if (global_name_field) |name_field| { + if (comptime getGlobalNameField(field.name)) |name_field| { @field(context, name_field) = v.name; } @@ -405,8 +355,6 @@ pub const App = struct { // Only the first output event after a `done` is the new primary. if (!context.output_order_done) return; context.output_order_done = false; - // A new primary invalidates any cached match-failure state. - context.primary_output_match_failed_logged = false; const name = std.mem.sliceTo(v.output_name, 0); if (name.len == 0) { From bec4c61d4dbbd1ad667e7cdcafaf15d0836c3143 Mon Sep 17 00:00:00 2001 From: Jake Guthmiller Date: Tue, 3 Mar 2026 21:28:02 -0600 Subject: [PATCH 11/11] PR feedback: heap-allocate primary_output_name --- src/apprt/gtk/winproto/wayland.zig | 32 ++++++++++++++++++------------ 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index b5c9f6a52..a4678f4e4 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -26,7 +26,7 @@ pub const App = struct { context: *Context, const Context = struct { - const max_output_name_len = 63; + alloc: Allocator, kde_blur_manager: ?*org.KdeKwinBlurManager = null, @@ -43,7 +43,7 @@ pub const App = 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: ?[max_output_name_len:0]u8 = null, + 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. @@ -91,8 +91,11 @@ pub const App = struct { // 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.* = .{}; + errdefer { + if (context.primary_output_name) |name| alloc.free(name); + alloc.destroy(context); + } + context.* = .{ .alloc = alloc }; // Get our display registry so we can get all the available interfaces // and bind to what we need. @@ -114,6 +117,7 @@ pub const App = struct { } pub fn deinit(self: *App, alloc: Allocator) void { + if (self.context.primary_output_name) |name| alloc.free(name); alloc.destroy(self.context); } @@ -173,7 +177,7 @@ pub const App = struct { const monitors = display.getMonitors(); // Try to find the monitor matching the primary output name. - if (context.primary_output_name) |*stored_name| { + if (context.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 { @@ -201,7 +205,7 @@ pub const App = struct { // Globals should be optional pointers const T = switch (@typeInfo(field.type)) { .optional => |o| switch (@typeInfo(o.child)) { - .pointer => |v| v.child, + .pointer => |v| if (v.size == .one) v.child else return null, else => return null, }, else => return null, @@ -226,6 +230,7 @@ pub const App = struct { /// Reset cached state derived from kde_output_order_v1. fn resetOutputOrderState(context: *Context) void { + if (context.primary_output_name) |name| context.alloc.free(name); context.primary_output_name = null; context.output_order_done = true; } @@ -357,17 +362,18 @@ pub const App = struct { context.output_order_done = false; const name = std.mem.sliceTo(v.output_name, 0); + if (context.primary_output_name) |old| context.alloc.free(old); + if (name.len == 0) { context.primary_output_name = null; log.warn("ignoring empty primary output name from kde_output_order_v1", .{}); - } else if (name.len <= Context.max_output_name_len) { - var buf: [Context.max_output_name_len:0]u8 = @splat(0); - @memcpy(buf[0..name.len], name); - context.primary_output_name = buf; - log.debug("primary output: {s}", .{name}); } else { - context.primary_output_name = null; - log.warn("ignoring primary output name longer than {} bytes from kde_output_order_v1", .{Context.max_output_name_len}); + context.primary_output_name = context.alloc.dupeZ(u8, name) catch |err| { + context.primary_output_name = null; + log.warn("failed to allocate primary output name: {}", .{err}); + return; + }; + log.debug("primary output: {s}", .{name}); } }, .done => {