From cd32463d5205f1ddb33c29b698f3002e1ed0e796 Mon Sep 17 00:00:00 2001 From: Ben Buhse Date: Sat, 31 Jan 2026 10:47:20 -0600 Subject: [PATCH 1/2] Add a keybind to reload config; still needs more testing --- src/Config.zig | 27 ++++++++++++--------------- src/XkbBindings.zig | 26 +++++++++++++++++++++----- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/Config.zig b/src/Config.zig index 09893fd..43c636d 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -313,15 +313,6 @@ fn loadKeybindsChildBlock(config: *Config, parser: *kdl.Parser) !void { const split_exec = try utils.tokenizeToOwnedSlices(exec_str, ' '); break :sw .{ .spawn = split_exec }; }, - .focus_next => { - break :sw .focus_next; - }, - .focus_prev => { - break :sw .focus_prev; - }, - .zoom => { - break :sw .zoom; - }, .change_ratio => { const diff_str = utils.stripQuotes(node.arg(parser, 2) orelse { logWarnMissingNodeArg(name, "diff"); @@ -333,11 +324,13 @@ fn loadKeybindsChildBlock(config: *Config, parser: *kdl.Parser) !void { }; break :sw .{ .change_ratio = diff }; }, - .toggle_fullscreen => { - break :sw .toggle_fullscreen; - }, - .close_window => { - break :sw .close_window; + inline .focus_next, .focus_prev, .zoom, .reload_config, .toggle_fullscreen, .close_window => |cmd| { + // None of these have arguments, just create the union and give it back + break :sw @unionInit( + XkbBindings.Command, + @tagName(cmd), + {}, + ); }, inline .set_output_tags, .set_window_tags, .toggle_output_tags, .toggle_window_tags => |cmd| { const tags_str = utils.stripQuotes(node.arg(parser, 2) orelse { @@ -348,7 +341,11 @@ fn loadKeybindsChildBlock(config: *Config, parser: *kdl.Parser) !void { logWarnInvalidNodeArg(name, tags_str); continue; }; - break :sw @unionInit(XkbBindings.Command, @tagName(cmd), tags); + break :sw @unionInit( + XkbBindings.Command, + @tagName(cmd), + tags, + ); }, }; diff --git a/src/XkbBindings.zig b/src/XkbBindings.zig index 5b8428c..34bedfd 100644 --- a/src/XkbBindings.zig +++ b/src/XkbBindings.zig @@ -10,10 +10,9 @@ pub const Command = union(enum) { focus_prev, zoom, change_ratio: f32, - // reload_config, // TODO + reload_config, toggle_fullscreen, close_window, - // exit, // TODO: Delete? // Tag management set_output_tags: u32, set_window_tags: u32, @@ -128,10 +127,26 @@ const XkbBinding = struct { current_focus.link.swapWith(&first_window.link); }, .change_ratio => |diff| { - const new_ratio = context.wm.primary_ratio + diff; - if (new_ratio >= 0.10 and new_ratio <= 0.90) { - context.wm.pending_manage.primary_ratio = context.wm.primary_ratio + diff; + context.wm.pending_manage.primary_ratio = std.math.clamp(context.wm.primary_ratio + diff, 0.10, 0.90); + }, + .reload_config => { + // Try create new config + const new_config = Config.create() catch { + // We do this so that, if the Config fails to reload, the + // user still has *some* config. + log.err("Failed to reload Config. Not deleting old one", .{}); + return; + }; + // Clean up old one + var it = context.xkb_bindings.bindings.safeIterator(.forward); + while (it.next()) |binding| { + binding.link.remove(); + binding.destroy(); } + context.config.destroy(); + // Replace the old one + context.config = new_config; + context.initialized = false; }, .toggle_fullscreen => { const seat = context.wm.seats.first() orelse return; @@ -241,6 +256,7 @@ const xkbcommon = @import("xkbcommon"); const utils = @import("utils.zig"); const Context = @import("Context.zig"); +const Config = @import("Config.zig"); const Seat = @import("Seat.zig"); const Window = @import("Window.zig"); From 4157dd67f6b2bc22695cff53b11f7753b083cc00 Mon Sep 17 00:00:00 2001 From: Ben Buhse Date: Sat, 31 Jan 2026 14:08:46 -0600 Subject: [PATCH 2/2] Fix use-after-free by moving config-reload into the manage cycle --- src/Context.zig | 23 +++++++++++++++++++++++ src/WindowManager.zig | 9 ++++++--- src/XkbBindings.zig | 15 ++++----------- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/Context.zig b/src/Context.zig index 8fbdc5d..552ab66 100644 --- a/src/Context.zig +++ b/src/Context.zig @@ -21,6 +21,13 @@ xkb_bindings: *XkbBindings, // WM Configuration config: *Config, +/// State consumed in manage() phase, reset at end of manage(). +pending_manage: PendingManage = .{}, + +pub const PendingManage = struct { + config: ?*Config = null, +}; + pub fn create( wl_display: *wl.Display, wl_registry: *wl.Registry, @@ -52,6 +59,22 @@ pub fn destroy(context: *Context) void { utils.allocator.destroy(context); } +pub fn manage(context: *Context) void { + defer context.pending_manage = .{}; + + if (context.pending_manage.config) |new_config| { + // Destroy all existing bindings + var it = context.xkb_bindings.bindings.safeIterator(.forward); + while (it.next()) |binding| { + binding.link.remove(); + binding.destroy(); + } + context.config.destroy(); + context.config = new_config; + context.initialized = false; + } +} + const std = @import("std"); const wayland = @import("wayland"); diff --git a/src/WindowManager.zig b/src/WindowManager.zig index d125c0a..e6db884 100644 --- a/src/WindowManager.zig +++ b/src/WindowManager.zig @@ -186,9 +186,12 @@ fn manage_start(wm: *WindowManager) void { const river_window_manager_v1 = wm.river_window_manager_v1; const context = wm.context; + + // This gets used shortly, so it goes at the beginning + context.manage(); + if (!context.initialized) { - // This code (should) only be run once while initializing the WM, so let's - // mark it as cold. + // This code runs during initial startup and after config reloads. @branchHint(.cold); context.initialized = true; @@ -220,7 +223,7 @@ fn manage_start(wm: *WindowManager) void { } } - // Manage the WM itself first + // Manage the WM itself before outputs/windows/seats if (wm.pending_manage.primary_ratio) |primary_ratio| { // Ratios outside of this range can cause crashes anyways (when doing the layout calulcation) std.debug.assert(primary_ratio >= 0.10 and primary_ratio <= 0.90); diff --git a/src/XkbBindings.zig b/src/XkbBindings.zig index 34bedfd..1429b0b 100644 --- a/src/XkbBindings.zig +++ b/src/XkbBindings.zig @@ -45,7 +45,7 @@ const XkbBinding = struct { return xkb_binding; } - fn destroy(xkb_binding: *XkbBinding) void { + pub fn destroy(xkb_binding: *XkbBinding) void { xkb_binding.xkb_binding_v1.destroy(); utils.allocator.destroy(xkb_binding); } @@ -137,16 +137,9 @@ const XkbBinding = struct { log.err("Failed to reload Config. Not deleting old one", .{}); return; }; - // Clean up old one - var it = context.xkb_bindings.bindings.safeIterator(.forward); - while (it.next()) |binding| { - binding.link.remove(); - binding.destroy(); - } - context.config.destroy(); - // Replace the old one - context.config = new_config; - context.initialized = false; + // Send the config to the WM to handle during next manage + context.pending_manage.config = new_config; + context.wm.river_window_manager_v1.manageDirty(); }, .toggle_fullscreen => { const seat = context.wm.seats.first() orelse return;