diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index 039e853aa..63f1c35d0 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -350,7 +350,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. @@ -431,7 +431,7 @@ 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.global_shortcuts.unref(); if (priv.saved_language) |language| alloc.free(language); if (gdk.Display.getDefault()) |display| { diff --git a/src/apprt/gtk/winproto.zig b/src/apprt/gtk/winproto.zig index 3c1da2b21..6731bfb31 100644 --- a/src/apprt/gtk/winproto.zig +++ b/src/apprt/gtk/winproto.zig @@ -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(), } } diff --git a/src/apprt/gtk/winproto/noop.zig b/src/apprt/gtk/winproto/noop.zig index ed69736f8..0d8cc18ce 100644 --- a/src/apprt/gtk/winproto/noop.zig +++ b/src/apprt/gtk/winproto/noop.zig @@ -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( diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index a4678f4e4..b90e9d115 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -7,66 +7,24 @@ 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 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 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 { - alloc: Allocator, - - 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_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 - /// 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, - - 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, @@ -86,39 +44,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 { - 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. - const registry = try display.getRegistry(); - registry.setListener(*Context, registryListener, context); - 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 (context.kde_decoration_manager != null or context.kde_output_order != null) { - 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 { - if (self.context.primary_output_name) |name| alloc.free(name); - alloc.destroy(self.context); + pub fn deinit(self: *App) void { + self.globals.deinit(); } pub fn eventMods( @@ -130,22 +66,12 @@ 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; } @@ -154,239 +80,10 @@ pub const App = struct { layer_shell.initForWindow(window); // Set target monitor based on config (null lets compositor decide) - const monitor = resolveQuickTerminalMonitor(self.context, apprt_window); + const monitor = resolveQuickTerminalMonitor(self.globals, apprt_window); defer if (monitor) |v| v.unref(); 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). - /// Caller owns the returned ref and must unref it. - fn resolveQuickTerminalMonitor( - context: *Context, - 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 (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 { - 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; - }; - } - - 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| if (v.size == .one) v.child else return null, - else => return null, - }, - else => return null, - }; - - // Only process Wayland interfaces - if (!@hasDecl(T, "interface")) return null; - 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; - } - - /// 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; - } - - 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}); - - const global = registry.bind( - v.name, - T, - T.generated_version, - ) catch |err| { - log.warn( - "error binding interface {s} error={}", - .{ v.interface, err }, - ); - return; - }; - - // 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")) { - resetOutputOrderState(context); - } - } - - @field(context, field.name) = global; - if (comptime getGlobalNameField(field.name)) |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")) { - global.setListener(*Context, decoManagerListener, context); - } - if (comptime std.mem.eql(u8, field.name, "kde_output_order")) { - global.setListener(*Context, outputOrderListener, context); - } - break; - } - } - }, - - // 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_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")) { - resetOutputOrderState(context); - } - break :remove; - } - } - } else { - if (@field(context, field.name)) |global| { - if (global.getId() == v.name) { - global.destroy(); - @field(context, field.name) = null; - break :remove; - } - } - } - } - }, - } - } - - fn decoManagerListener( - _: *org.KdeKwinServerDecorationManager, - event: org.KdeKwinServerDecorationManager.Event, - context: *Context, - ) void { - switch (event) { - .default_mode => |mode| { - context.default_deco_mode = @enumFromInt(mode.mode); - }, - } - } - - fn outputOrderListener( - _: *kde.OutputOrderV1, - event: kde.OutputOrderV1.Event, - context: *Context, - ) void { - switch (event) { - .output => |v| { - // Only the first output event after a `done` is the new primary. - if (!context.output_order_done) return; - 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 { - 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 => { - 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; - }, - } - } }; /// Per-window (wl_surface) state for the Wayland protocol. @@ -397,7 +94,7 @@ 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, @@ -438,7 +135,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( @@ -464,7 +161,7 @@ pub const Window = struct { return .{ .apprt_window = apprt_window, .surface = wl_surface, - .app_context = app.context, + .globals = app.globals, .decoration = deco, }; } @@ -499,7 +196,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, }; @@ -511,7 +208,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(); @@ -527,7 +224,7 @@ 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 manager = self.globals.get(.kde_blur_manager) orelse return; const config = if (self.apprt_window.getConfig()) |v| v.get() else @@ -561,7 +258,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, @@ -585,7 +282,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); + const target_monitor = resolveQuickTerminalMonitor(self.globals, self.apprt_window); defer if (target_monitor) |v| v.unref(); layer_shell.setMonitor(window, target_monitor); @@ -629,7 +326,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}); @@ -658,8 +355,8 @@ 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 = App.resolveQuickTerminalMonitor( - apprt_window.winproto().wayland.app_context, + const resolved_monitor = resolveQuickTerminalMonitor( + apprt_window.winproto().wayland.globals, apprt_window, ); defer if (resolved_monitor) |v| v.unref(); @@ -686,7 +383,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()) { @@ -703,3 +400,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; + }; +} diff --git a/src/apprt/gtk/winproto/wayland/Globals.zig b/src/apprt/gtk/winproto/wayland/Globals.zig new file mode 100644 index 000000000..369d6a473 --- /dev/null +++ b/src/apprt/gtk/winproto/wayland/Globals.zig @@ -0,0 +1,230 @@ +const Globals = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const wayland = @import("wayland"); +const wl = wayland.client.wl; +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 { + kde_blur_manager, + kde_decoration_manager, + kde_slide_manager, + kde_output_order, + xdg_activation, + + fn Type(comptime self: Tag) type { + return switch (self) { + .kde_blur_manager => org.KdeKwinBlurManager, + .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, + + /// 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) { + .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 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; + }, + } +} diff --git a/src/apprt/gtk/winproto/x11.zig b/src/apprt/gtk/winproto/x11.zig index 1e73c6139..7344de908 100644 --- a/src/apprt/gtk/winproto/x11.zig +++ b/src/apprt/gtk/winproto/x11.zig @@ -106,9 +106,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