oxwm

https://git.tonybtw.com/oxwm.git git://git.tonybtw.com/oxwm.git

refactor: create `WindowManager` type to hold wm state, cleanup bar and monitor module

Commit
82fd9ea645555288b5ec9f2911e9b3025b54a1ed
Parent
15a76df
Author
emzywastaken <amiamemetoo@gmail.com>
Date
2026-02-21 16:36:58
- moved almost all global state into new `WindowManager` struct
- clean up `bar` module and remove reliance on global state
- add doc commnets for some functions in `bar` module
- document and clean up `monitor` module, removing some global state

some temporary global state still exists but will be removed in the
coming commits

Diff

diff --git a/src/bar/bar.zig b/src/bar/bar.zig
index a4b1366..bd69d7e 100644
--- a/src/bar/bar.zig
+++ b/src/bar/bar.zig
@@ -1,7 +1,6 @@
 const std = @import("std");
 const xlib = @import("../x11/xlib.zig");
 const monitor_mod = @import("../monitor.zig");
-const client_mod = @import("../client.zig");
 const blocks_mod = @import("blocks/blocks.zig");
 const config_mod = @import("../config/config.zig");
 const ColorScheme = config_mod.ColorScheme;
@@ -9,22 +8,12 @@ const ColorScheme = config_mod.ColorScheme;
 const Monitor = monitor_mod.Monitor;
 const Block = blocks_mod.Block;
 
-fn get_layout_symbol(layout_index: u32, config: ?config_mod.Config) []const u8 {
-    if (config) |conf| {
-        return switch (layout_index) {
-            0 => conf.layout_tile_symbol,
-            1 => conf.layout_monocle_symbol,
-            2 => conf.layout_floating_symbol,
-            3 => "[S]",
-            4 => "[#]",
-            else => "[?]",
-        };
-    }
+fn get_layout_symbol(layout_index: u32, config: config_mod.Config) []const u8 {
     return switch (layout_index) {
-        0 => "[]=",
-        1 => "[M]",
-        2 => "><>",
-        3 => "[S]",
+        0 => config.layout_tile_symbol,
+        1 => config.layout_monocle_symbol,
+        2 => config.layout_floating_symbol,
+        3 => config.layout_scrolling_symbol,
         4 => "[#]",
         else => "[?]",
     };
@@ -53,6 +42,8 @@ pub const Bar = struct {
     needs_redraw: bool,
     next: ?*Bar,
 
+    /// Creates a bar window for `monitor` using the given config.
+    /// Returns null on allocation failure or if the font cannot be loaded.
     pub fn create(
         allocator: std.mem.Allocator,
         display: *xlib.Display,
@@ -67,7 +58,10 @@ pub const Bar = struct {
         const depth = xlib.XDefaultDepth(display, screen);
         const root = xlib.XRootWindow(display, screen);
 
-        const font_name_z = allocator.dupeZ(u8, config.font) catch return null;
+        const font_name_z = allocator.dupeZ(u8, config.font) catch {
+            allocator.destroy(bar);
+            return null;
+        };
         defer allocator.free(font_name_z);
 
         const font = xlib.XftFontOpenName(display, screen, font_name_z);
@@ -91,7 +85,6 @@ pub const Bar = struct {
             0x1a1b26,
         );
 
-        _ = xlib.c.XSetWindowAttributes{};
         var attributes: xlib.c.XSetWindowAttributes = undefined;
         attributes.override_redirect = xlib.True;
         attributes.event_mask = xlib.c.ExposureMask | xlib.c.ButtonPressMask;
@@ -106,7 +99,6 @@ pub const Bar = struct {
         );
 
         const graphics_context = xlib.XCreateGC(display, pixmap, 0, null);
-
         const xft_draw = xlib.XftDrawCreate(display, pixmap, visual, colormap);
 
         _ = xlib.XMapWindow(display, window);
@@ -127,7 +119,7 @@ pub const Bar = struct {
             .scheme_urgent = config.scheme_urgent,
             .hide_vacant_tags = config.hide_vacant_tags,
             .allocator = allocator,
-            .blocks = .{},
+            .blocks = .empty,
             .needs_redraw = true,
             .next = null,
         };
@@ -139,13 +131,12 @@ pub const Bar = struct {
         return bar;
     }
 
+    /// Destroys the bar's X resources and frees the allocation.
+    // TODO: Remove `allocator` param as its already stored in `Bar`.
     pub fn destroy(self: *Bar, allocator: std.mem.Allocator, display: *xlib.Display) void {
-        if (self.xft_draw) |xft_draw| {
-            xlib.XftDrawDestroy(xft_draw);
-        }
-        if (self.font) |font| {
-            xlib.XftFontClose(display, font);
-        }
+        if (self.xft_draw) |xft_draw| xlib.XftDrawDestroy(xft_draw);
+        if (self.font) |font| xlib.XftFontClose(display, font);
+
         _ = xlib.XFreeGC(display, self.graphics_context);
         _ = xlib.XFreePixmap(display, self.pixmap);
         _ = xlib.c.XDestroyWindow(display, self.window);
@@ -157,11 +148,16 @@ pub const Bar = struct {
         self.blocks.append(self.allocator, block) catch {};
     }
 
+    pub fn clear_blocks(self: *Bar) void {
+        self.blocks.clearRetainingCapacity();
+    }
+
     pub fn invalidate(self: *Bar) void {
         self.needs_redraw = true;
     }
 
-    pub fn draw(self: *Bar, display: *xlib.Display, tags: []const []const u8, config: ?config_mod.Config) void {
+    /// Redraws the bar if marked dirty. Tags are taken from `config.tags`
+    pub fn draw(self: *Bar, display: *xlib.Display, config: config_mod.Config) void {
         if (!self.needs_redraw) return;
 
         self.fill_rect(display, 0, 0, self.width, self.height, self.scheme_normal.background);
@@ -171,14 +167,19 @@ pub const Bar = struct {
         const monitor = self.monitor;
         const current_tags = monitor.tagset[monitor.sel_tags];
 
-        for (tags, 0..) |tag, index| {
+        for (config.tags, 0..) |tag, index| {
             const tag_mask: u32 = @as(u32, 1) << @intCast(index);
             const is_selected = (current_tags & tag_mask) != 0;
             const is_occupied = has_clients_on_tag(monitor, tag_mask);
 
             if (self.hide_vacant_tags and !is_occupied and !is_selected) continue;
 
-            const scheme = if (is_selected) self.scheme_selected else if (is_occupied) self.scheme_occupied else self.scheme_normal;
+            const scheme = if (is_selected)
+                self.scheme_selected
+            else if (is_occupied)
+                self.scheme_occupied
+            else
+                self.scheme_normal;
 
             const tag_text_width = self.text_width(display, tag);
             const tag_width = tag_text_width + padding * 2;
@@ -189,7 +190,6 @@ pub const Bar = struct {
 
             const text_y = @divTrunc(self.height + self.font_height, 2) - 4;
             self.draw_text(display, x_position + padding, text_y, tag, scheme.foreground);
-
             x_position += tag_width;
         }
 
@@ -220,48 +220,15 @@ pub const Bar = struct {
         self.needs_redraw = false;
     }
 
-    fn fill_rect(self: *Bar, display: *xlib.Display, x: i32, y: i32, width: i32, height: i32, color: c_ulong) void {
-        _ = xlib.XSetForeground(display, self.graphics_context, color);
-        _ = xlib.XFillRectangle(display, self.pixmap, self.graphics_context, x, y, @intCast(width), @intCast(height));
-    }
-
-    fn draw_text(self: *Bar, display: *xlib.Display, x: i32, y: i32, text: []const u8, color: c_ulong) void {
-        if (self.xft_draw == null or self.font == null) return;
-
-        var xft_color: xlib.XftColor = undefined;
-        var render_color: xlib.XRenderColor = undefined;
-        render_color.red = @intCast((color >> 16 & 0xff) * 257);
-        render_color.green = @intCast((color >> 8 & 0xff) * 257);
-        render_color.blue = @intCast((color & 0xff) * 257);
-        render_color.alpha = 0xffff;
-
-        const visual = xlib.XDefaultVisual(display, 0);
-        const colormap = xlib.XDefaultColormap(display, 0);
-
-        _ = xlib.XftColorAllocValue(display, visual, colormap, &render_color, &xft_color);
-
-        xlib.XftDrawStringUtf8(self.xft_draw, &xft_color, self.font, x, y, text.ptr, @intCast(text.len));
-
-        xlib.XftColorFree(display, visual, colormap, &xft_color);
-    }
-
-    fn text_width(self: *Bar, display: *xlib.Display, text: []const u8) i32 {
-        if (self.font == null) return 0;
-
-        var extents: xlib.XGlyphInfo = undefined;
-        xlib.XftTextExtentsUtf8(display, self.font, text.ptr, @intCast(text.len), &extents);
-        return extents.xOff;
-    }
-
-    pub fn handle_click(self: *Bar, click_x: i32, tags: []const []const u8) ?usize {
+    /// Returns the index of the tag the user clicked on, or null if the
+    /// click was outside the tag area.
+    pub fn handle_click(self: *Bar, display: *xlib.Display, click_x: i32, config: config_mod.Config) ?usize {
         var x_position: i32 = 0;
         const padding: i32 = 8;
-        const display = xlib.c.XOpenDisplay(null) orelse return null;
-        defer _ = xlib.XCloseDisplay(display);
-
         const monitor = self.monitor;
         const current_tags = monitor.tagset[monitor.sel_tags];
-        for (tags, 0..) |tag, index| {
+
+        for (config.tags, 0..) |tag, index| {
             const tag_mask = @as(u32, 1) << @intCast(index);
             const is_selected = (current_tags & tag_mask) != 0;
             const is_occupied = has_clients_on_tag(monitor, tag_mask);
@@ -279,59 +246,51 @@ pub const Bar = struct {
         return null;
     }
 
+    /// Updates all blocks and marks the bar dirty if any block changed.
     pub fn update_blocks(self: *Bar) void {
         var changed = false;
         for (self.blocks.items) |*block| {
-            if (block.update()) {
-                changed = true;
-            }
-        }
-        if (changed) {
-            self.needs_redraw = true;
+            if (block.update()) changed = true;
         }
+        if (changed) self.needs_redraw = true;
     }
 
-    pub fn clear_blocks(self: *Bar) void {
-        self.blocks.clearRetainingCapacity();
+    fn fill_rect(self: *Bar, display: *xlib.Display, x: i32, y: i32, width: i32, height: i32, color: c_ulong) void {
+        _ = xlib.XSetForeground(display, self.graphics_context, color);
+        _ = xlib.XFillRectangle(display, self.pixmap, self.graphics_context, x, y, @intCast(width), @intCast(height));
     }
-};
 
-fn has_clients_on_tag(monitor: *Monitor, tag_mask: u32) bool {
-    var current = monitor.clients;
-    while (current) |client| {
-        if ((client.tags & tag_mask) != 0) {
-            return true;
-        }
-        current = client.next;
-    }
-    return false;
-}
+    fn draw_text(self: *Bar, display: *xlib.Display, x: i32, y: i32, text: []const u8, color: c_ulong) void {
+        if (self.xft_draw == null or self.font == null) return;
 
-pub var bars: ?*Bar = null;
+        var xft_color: xlib.XftColor = undefined;
+        var render_color: xlib.XRenderColor = undefined;
+        render_color.red = @intCast((color >> 16 & 0xff) * 257);
+        render_color.green = @intCast((color >> 8 & 0xff) * 257);
+        render_color.blue = @intCast((color & 0xff) * 257);
+        render_color.alpha = 0xffff;
 
-pub fn create_bars(allocator: std.mem.Allocator, display: *xlib.Display, screen: c_int) void {
-    var current_monitor = monitor_mod.monitors;
-    while (current_monitor) |monitor| {
-        const bar = Bar.create(allocator, display, screen, monitor, "monospace:size=10");
-        if (bar) |created_bar| {
-            bars = created_bar;
-        }
-        current_monitor = monitor.next;
+        const visual = xlib.XDefaultVisual(display, 0);
+        const colormap = xlib.XDefaultColormap(display, 0);
+
+        _ = xlib.XftColorAllocValue(display, visual, colormap, &render_color, &xft_color);
+        xlib.XftDrawStringUtf8(self.xft_draw, &xft_color, self.font, x, y, text.ptr, @intCast(text.len));
+        xlib.XftColorFree(display, visual, colormap, &xft_color);
     }
-}
 
-pub fn draw_bars(display: *xlib.Display, tags: []const []const u8) void {
-    var current_monitor = monitor_mod.monitors;
-    while (current_monitor) |monitor| {
-        _ = monitor;
-        if (bars) |bar| {
-            bar.draw(display, tags, null);
-        }
-        current_monitor = if (current_monitor) |m| m.next else null;
+    fn text_width(self: *Bar, display: *xlib.Display, text: []const u8) i32 {
+        if (self.font == null) return 0;
+
+        var extents: xlib.XGlyphInfo = undefined;
+        xlib.XftTextExtentsUtf8(display, self.font, text.ptr, @intCast(text.len), &extents);
+        return extents.xOff;
     }
-}
+};
+
+// Bar list helpers >.<
 
-pub fn invalidate_bars() void {
+/// Marks all bars in the list as needing a redraw.
+pub fn invalidate_bars(bars: ?*Bar) void {
     var current = bars;
     while (current) |bar| {
         bar.invalidate();
@@ -339,23 +298,31 @@ pub fn invalidate_bars() void {
     }
 }
 
-pub fn destroy_bars(allocator: std.mem.Allocator, display: *xlib.Display) void {
+/// Destroys all bars in the list and frees their resources.
+pub fn destroy_bars(bars: ?*Bar, allocator: std.mem.Allocator, display: *xlib.Display) void {
     var current = bars;
     while (current) |bar| {
         const next = bar.next;
         bar.destroy(allocator, display);
         current = next;
     }
-    bars = null;
 }
 
-pub fn window_to_bar(win: xlib.Window) ?*Bar {
+/// Returns the bar whose window matches `win`, or null.
+pub fn window_to_bar(bars: ?*Bar, win: xlib.Window) ?*Bar {
     var current = bars;
     while (current) |bar| {
-        if (bar.window == win) {
-            return bar;
-        }
+        if (bar.window == win) return bar;
         current = bar.next;
     }
     return null;
 }
+
+fn has_clients_on_tag(monitor: *Monitor, tag_mask: u32) bool {
+    var current = monitor.clients;
+    while (current) |client| {
+        if ((client.tags & tag_mask) != 0) return true;
+        current = client.next;
+    }
+    return false;
+}
diff --git a/src/main.zig b/src/main.zig
index faf0b6d..e9160ce 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -1,20 +1,14 @@
 const std = @import("std");
 const VERSION = "v0.11.2";
-const Atoms = @import("x11/atoms.zig").Atoms;
-const kchord = @import("keyboard/chord.zig");
+const wm_mod = @import("wm.zig");
 const display_mod = @import("x11/display.zig");
 const events = @import("x11/events.zig");
 const xlib = @import("x11/xlib.zig");
 const client_mod = @import("client.zig");
 const monitor_mod = @import("monitor.zig");
 const tiling = @import("layouts/tiling.zig");
-const monocle = @import("layouts/monocle.zig");
-const floating = @import("layouts/floating.zig");
 const scrolling = @import("layouts/scrolling.zig");
-const grid = @import("layouts/grid.zig");
-const animations = @import("animations.zig");
 const bar_mod = @import("bar/bar.zig");
-const blocks_mod = @import("bar/blocks/blocks.zig");
 const config_mod = @import("config/config.zig");
 const lua = @import("config/lua.zig");
 const overlay_mod = @import("overlay.zig");
@@ -22,50 +16,47 @@ const overlay_mod = @import("overlay.zig");
 const Display = display_mod.Display;
 const Client = client_mod.Client;
 const Monitor = monitor_mod.Monitor;
+const WindowManager = wm_mod.WindowManager;
 
-var running: bool = true;
 var gpa = std.heap.GeneralPurposeAllocator(.{}){};
 
-var atoms: Atoms = undefined;
-var wm_check_window: xlib.Window = undefined;
-
-const Cursors = struct {
-    normal: xlib.Cursor,
-    resize: xlib.Cursor,
-    move: xlib.Cursor,
-
-    fn init(display: *Display) Cursors {
-        return .{
-            .normal = xlib.XCreateFontCursor(display.handle, xlib.XC_left_ptr),
-            .resize = xlib.XCreateFontCursor(display.handle, xlib.XC_sizing),
-            .move = xlib.XCreateFontCursor(display.handle, xlib.XC_fleur),
-        };
-    }
-};
-
-var cursors: Cursors = undefined;
-
 const NormalState: c_long = 1;
 const WithdrawnState: c_long = 0;
 const IconicState: c_long = 3;
 const IsViewable: c_int = 2;
 const snap_distance: i32 = 32;
 
-var numlock_mask: c_uint = 0;
+// Remaining module-level state
 
+/// Config built from Lua, passed into WindowManager.init.
 var config: config_mod.Config = undefined;
+/// Path to the loaded config file, kept for hot-reload.
 var config_path_global: ?[]const u8 = null;
 
-/// Interim pointer to the X11 display, used only by `arrange` until the
-/// WindowManager struct refactor moves display ownership there properly.
+/// TODO(wm-refactor): move into WindowManager, waiting on handle_key_press
+/// and execute_action being converted to methods.
+var chord = @import("keyboard/chord.zig").ChordState{};
+
+/// TODO(wm-refactor): remove once all callers are WindowManager methods.
+/// Aliases wm.atoms so existing functions that take display but not wm
+/// can still reach atom values without requiring a full signature change.
+var atoms: @import("x11/atoms.zig").Atoms = undefined;
+
+/// TODO(wm-refactor): move into WindowManager, waiting on arrange() becoming
+/// a method so it can access self.display directly.
 var wm_display: ?*Display = null;
 
-var scroll_animation: animations.Scroll_Animation = .{};
-var animation_config: animations.Animation_Config = .{ .duration_ms = 150, .easing = .ease_out };
+/// TODO(wm-refactor): remove once all callers are WindowManager methods.
+/// Used only by functions that need to invalidate bars but don't yet receive
+/// wm as a parameter.
+var wm_ptr: ?*WindowManager = null;
 
-var chord = kchord.ChordState{};
+/// TODO(wm-refactor): move into WindowManager.
+var numlock_mask: c_uint = 0;
 
-var keybind_overlay: ?*overlay_mod.Keybind_Overlay = null;
+/// File descriptor of the X11 connection, used in spawn_child_setup.
+/// Set from wm.x11_fd after init.
+var x11_fd: c_int = -1;
 
 fn print_help() void {
     std.debug.print(
@@ -98,6 +89,7 @@ fn get_config_path(allocator: std.mem.Allocator) ![]u8 {
         const home = std.posix.getenv("HOME") orelse return error.CouldNotGetHomeDir;
         break :blk try std.fs.path.join(allocator, &.{ home, ".config" });
     };
+    // TODO: wtf is this shit
     defer if (std.posix.getenv("XDG_CONFIG_HOME") == null) allocator.free(config_home);
 
     const config_path = try std.fs.path.join(allocator, &.{ config_home, "oxwm", "config.lua" });
@@ -201,7 +193,6 @@ pub fn main() !void {
     std.debug.print("oxwm starting\n", .{});
 
     config = config_mod.Config.init(allocator);
-    defer config.deinit();
 
     if (lua.init(&config)) {
         const loaded = if (std.fs.cwd().statFile(config_path)) |_|
@@ -223,229 +214,34 @@ pub fn main() !void {
         initialize_default_config();
     }
 
-    var display = Display.open() catch |err| {
-        std.debug.print("failed to open display: {}\n", .{err});
+    var wm = WindowManager.init(allocator, config, config_path_global) catch |err| {
+        std.debug.print("failed to start window manager: {}\n", .{err});
         return;
     };
-    defer display.close();
-
-    x11_fd = xlib.XConnectionNumber(display.handle);
-    wm_display = &display;
+    defer wm.deinit();
 
-    std.debug.print("display opened: screen={d} root=0x{x}\n", .{ display.screen, display.root });
-    std.debug.print("screen size: {d}x{d}\n", .{ display.screen_width(), display.screen_height() });
-
-    display.become_window_manager() catch |err| {
-        std.debug.print("failed to become window manager: {}\n", .{err});
-        return;
-    };
+    // Propagate values that transitional code in this file still reads
+    // from module-level vars. These will be removed soon.
+    x11_fd = wm.x11_fd;
+    wm_display = &wm.display;
+    wm_ptr = &wm;
+    atoms = wm.atoms;
 
+    std.debug.print("display opened: screen={d} root=0x{x}\n", .{ wm.display.screen, wm.display.root });
     std.debug.print("successfully became window manager\n", .{});
-
-    const atoms_result = Atoms.init(display.handle, display.root);
-    atoms = atoms_result.atoms;
-    wm_check_window = atoms_result.check_window;
     std.debug.print("atoms initialized with EWMH support\n", .{});
 
-    cursors = Cursors.init(&display);
-    _ = xlib.XDefineCursor(display.handle, display.root, cursors.normal);
-    monitor_mod.init(allocator);
-    tiling.set_display(display.handle);
-    tiling.set_screen_size(display.screen_width(), display.screen_height());
-
-    setup_monitors(&display);
-    setup_bars(allocator, &display);
-    setup_overlay(allocator, &display);
-    grab_keybinds(&display);
-    scan_existing_windows(&display);
+    grab_keybinds(&wm.display);
+    scan_existing_windows(&wm.display);
 
     try run_autostart_commands(allocator, config.autostart.items);
     std.debug.print("entering event loop\n", .{});
-    run_event_loop(&display);
-
-    bar_mod.destroy_bars(allocator, display.handle);
-
-    var mon = monitor_mod.monitors;
-    while (mon) |m| {
-        const next = m.next;
-        monitor_mod.destroy(m);
-        mon = next;
-    }
-
-    if (keybind_overlay) |overlay| {
-        overlay.deinit(allocator);
-    }
+    run_event_loop(&wm);
 
     lua.deinit();
     std.debug.print("oxwm exiting\n", .{});
 }
 
-fn setup_bars(allocator: std.mem.Allocator, display: *Display) void {
-    var current_monitor = monitor_mod.monitors;
-    var last_bar: ?*bar_mod.Bar = null;
-
-    while (current_monitor) |monitor| {
-        const bar = bar_mod.Bar.create(allocator, display.handle, display.screen, monitor, config);
-        if (bar) |created_bar| {
-            if (tiling.bar_height == 0) {
-                tiling.set_bar_height(created_bar.height);
-            }
-
-            if (config.blocks.items.len > 0) {
-                for (config.blocks.items) |cfg_block| {
-                    const block = config_block_to_bar_block(cfg_block);
-                    created_bar.add_block(block);
-                }
-            } else {
-                created_bar.add_block(blocks_mod.Block.init_ram("", 5, 0x7aa2f7, true));
-                created_bar.add_block(blocks_mod.Block.init_static(" | ", 0x666666, false));
-                created_bar.add_block(blocks_mod.Block.init_datetime("", "%H:%M", 1, 0x0db9d7, true));
-            }
-
-            if (last_bar) |prev| {
-                prev.next = created_bar;
-            } else {
-                bar_mod.bars = created_bar;
-            }
-            last_bar = created_bar;
-            std.debug.print("bar created for monitor {d}\n", .{monitor.num});
-        }
-        current_monitor = monitor.next;
-    }
-}
-
-fn setup_overlay(allocator: std.mem.Allocator, display: *Display) void {
-    keybind_overlay = overlay_mod.Keybind_Overlay.init(display.handle, display.screen, display.root, config.font, allocator);
-}
-
-fn config_block_to_bar_block(cfg: config_mod.Block) blocks_mod.Block {
-    return switch (cfg.block_type) {
-        .static => blocks_mod.Block.init_static(cfg.format, cfg.color, cfg.underline),
-        .datetime => blocks_mod.Block.init_datetime(cfg.format, cfg.datetime_format orelse "%H:%M", cfg.interval, cfg.color, cfg.underline),
-        .ram => blocks_mod.Block.init_ram(cfg.format, cfg.interval, cfg.color, cfg.underline),
-        .shell => blocks_mod.Block.init_shell(cfg.format, cfg.command orelse "", cfg.interval, cfg.color, cfg.underline),
-        .battery => blocks_mod.Block.init_battery(
-            cfg.format_charging orelse "",
-            cfg.format_discharging orelse "",
-            cfg.format_full orelse "",
-            cfg.battery_name orelse "BAT0",
-            cfg.interval,
-            cfg.color,
-            cfg.underline,
-        ),
-        .cpu_temp => blocks_mod.Block.init_cpu_temp(
-            cfg.format,
-            cfg.thermal_zone orelse "thermal_zone0",
-            cfg.interval,
-            cfg.color,
-            cfg.underline,
-        ),
-    };
-}
-
-fn setup_monitors(display: *Display) void {
-    std.debug.print("checking xinerama...\n", .{});
-    if (xlib.XineramaIsActive(display.handle) != 0) {
-        std.debug.print("xinerama is active!\n", .{});
-        var screen_count: c_int = 0;
-        const screens = xlib.XineramaQueryScreens(display.handle, &screen_count);
-
-        if (screen_count > 0 and screens != null) {
-            var prev_monitor: ?*Monitor = null;
-            var index: usize = 0;
-
-            while (index < @as(usize, @intCast(screen_count))) : (index += 1) {
-                const screen = screens[index];
-                const mon = monitor_mod.create() orelse continue;
-
-                mon.num = @intCast(index);
-                mon.mon_x = screen.x_org;
-                mon.mon_y = screen.y_org;
-                mon.mon_w = screen.width;
-                mon.mon_h = screen.height;
-                mon.win_x = screen.x_org;
-                mon.win_y = screen.y_org;
-                mon.win_w = screen.width;
-                mon.win_h = screen.height;
-                mon.lt[0] = &tiling.layout;
-                mon.lt[1] = &monocle.layout;
-                mon.lt[2] = &floating.layout;
-                mon.lt[3] = &scrolling.layout;
-                mon.lt[4] = &grid.layout;
-                for (0..10) |i| {
-                    mon.pertag.ltidxs[i][0] = mon.lt[0];
-                    mon.pertag.ltidxs[i][1] = mon.lt[1];
-                    mon.pertag.ltidxs[i][2] = mon.lt[2];
-                    mon.pertag.ltidxs[i][3] = mon.lt[3];
-                    mon.pertag.ltidxs[i][4] = mon.lt[4];
-                }
-
-                init_monitor_gaps(mon);
-
-                if (prev_monitor) |prev| {
-                    prev.next = mon;
-                } else {
-                    monitor_mod.monitors = mon;
-                }
-                prev_monitor = mon;
-
-                std.debug.print("monitor {d}: {d}x{d} at ({d},{d})\n", .{ index, mon.mon_w, mon.mon_h, mon.mon_x, mon.mon_y });
-            }
-
-            monitor_mod.selected_monitor = monitor_mod.monitors;
-            _ = xlib.XFree(@ptrCast(screens));
-            return;
-        }
-    } else {
-        std.debug.print("xinerama not active, using single monitor\n", .{});
-    }
-
-    const mon = monitor_mod.create() orelse return;
-    mon.mon_x = 0;
-    mon.mon_y = 0;
-    mon.mon_w = display.screen_width();
-    mon.mon_h = display.screen_height();
-    mon.win_x = 0;
-    mon.win_y = 0;
-    mon.win_w = display.screen_width();
-    mon.win_h = display.screen_height();
-    mon.lt[0] = &tiling.layout;
-    mon.lt[1] = &monocle.layout;
-    mon.lt[2] = &floating.layout;
-    mon.lt[3] = &scrolling.layout;
-    mon.lt[4] = &grid.layout;
-    for (0..10) |i| {
-        mon.pertag.ltidxs[i][0] = mon.lt[0];
-        mon.pertag.ltidxs[i][1] = mon.lt[1];
-        mon.pertag.ltidxs[i][2] = mon.lt[2];
-        mon.pertag.ltidxs[i][3] = mon.lt[3];
-        mon.pertag.ltidxs[i][4] = mon.lt[4];
-    }
-
-    init_monitor_gaps(mon);
-
-    monitor_mod.monitors = mon;
-    monitor_mod.selected_monitor = mon;
-    std.debug.print("monitor created: {d}x{d}\n", .{ mon.mon_w, mon.mon_h });
-}
-
-fn init_monitor_gaps(mon: *Monitor) void {
-    const any_gap_nonzero = config.gap_inner_h != 0 or config.gap_inner_v != 0 or
-        config.gap_outer_h != 0 or config.gap_outer_v != 0;
-
-    if (config.gaps_enabled and any_gap_nonzero) {
-        mon.gap_inner_h = config.gap_inner_h;
-        mon.gap_inner_v = config.gap_inner_v;
-        mon.gap_outer_h = config.gap_outer_h;
-        mon.gap_outer_v = config.gap_outer_v;
-    } else {
-        mon.gap_inner_h = 0;
-        mon.gap_inner_v = 0;
-        mon.gap_outer_h = 0;
-        mon.gap_outer_v = 0;
-    }
-}
-
 fn make_keybind(mod: u32, key: u64, action: config_mod.Action) config_mod.Keybind {
     var kb: config_mod.Keybind = .{ .action = action };
     kb.keys[0] = .{ .mod_mask = mod, .keysym = key };
@@ -634,35 +430,34 @@ fn scan_existing_windows(display: *Display) void {
     }
 }
 
-fn run_event_loop(display: *Display) void {
-    const fd = xlib.XConnectionNumber(display.handle);
+fn run_event_loop(wm: *WindowManager) void {
     var fds = [_]std.posix.pollfd{
-        .{ .fd = fd, .events = std.posix.POLL.IN, .revents = 0 },
+        .{ .fd = wm.x11_fd, .events = std.posix.POLL.IN, .revents = 0 },
     };
 
-    _ = xlib.XSync(display.handle, xlib.False);
+    _ = xlib.XSync(wm.display.handle, xlib.False);
 
-    while (running) {
-        while (xlib.XPending(display.handle) > 0) {
-            var event = display.next_event();
-            handle_event(display, &event);
+    while (wm.running) {
+        while (xlib.XPending(wm.display.handle) > 0) {
+            var event = wm.display.next_event();
+            handle_event(&wm.display, &event, wm);
         }
 
-        tick_animations();
+        tick_animations(wm);
 
-        var current_bar = bar_mod.bars;
+        var current_bar = wm.bars;
         while (current_bar) |bar| {
             bar.update_blocks();
-            bar.draw(display.handle, &config.tags, config);
+            bar.draw(wm.display.handle, wm.config);
             current_bar = bar.next;
         }
 
-        const poll_timeout: i32 = if (scroll_animation.is_active()) 16 else 1000;
+        const poll_timeout: i32 = if (wm.scroll_animation.is_active()) 16 else 1000;
         _ = std.posix.poll(&fds, poll_timeout) catch 0;
     }
 }
 
-fn handle_event(display: *Display, event: *xlib.XEvent) void {
+fn handle_event(display: *Display, event: *xlib.XEvent, wm: *WindowManager) void {
     const event_type = events.get_event_type(event);
 
     if (event_type == .button_press) {
@@ -672,15 +467,15 @@ fn handle_event(display: *Display, event: *xlib.XEvent) void {
     switch (event_type) {
         .map_request => handle_map_request(display, &event.xmaprequest),
         .configure_request => handle_configure_request(display, &event.xconfigurerequest),
-        .key_press => handle_key_press(display, &event.xkey),
+        .key_press => handle_key_press(display, &event.xkey, wm),
         .destroy_notify => handle_destroy_notify(display, &event.xdestroywindow),
         .unmap_notify => handle_unmap_notify(display, &event.xunmap),
-        .enter_notify => handle_enter_notify(display, &event.xcrossing),
+        .enter_notify => handle_enter_notify(display, &event.xcrossing, wm),
         .focus_in => handle_focus_in(display, &event.xfocus),
-        .motion_notify => handle_motion_notify(display, &event.xmotion),
+        .motion_notify => handle_motion_notify(display, &event.xmotion, wm),
         .client_message => handle_client_message(display, &event.xclient),
-        .button_press => handle_button_press(display, &event.xbutton),
-        .expose => handle_expose(display, &event.xexpose),
+        .button_press => handle_button_press(display, &event.xbutton, wm),
+        .expose => handle_expose(display, &event.xexpose, wm),
         .property_notify => handle_property_notify(display, &event.xproperty),
         else => {},
     }
@@ -843,42 +638,37 @@ fn handle_configure_request(display: *Display, event: *xlib.XConfigureRequestEve
     _ = xlib.XSync(display.handle, xlib.False);
 }
 
-fn reset_chord_state(display_handle: *xlib.Display) void {
-    chord.reset(display_handle);
-}
-
-fn handle_key_press(display: *Display, event: *xlib.XKeyEvent) void {
+fn handle_key_press(display: *Display, event: *xlib.XKeyEvent, wm: *WindowManager) void {
     const keysym = xlib.XKeycodeToKeysym(display.handle, @intCast(event.keycode), 0);
 
-    if (keybind_overlay) |overlay| {
-        if (overlay.handle_key(keysym)) {
-            return;
-        }
+    if (wm.overlay) |overlay| {
+        if (overlay.handle_key(keysym)) return;
     }
 
     const clean_state = event.state & ~@as(c_uint, xlib.LockMask | xlib.Mod2Mask);
-    const current_time = std.time.milliTimestamp();
 
-    if (chord.index > 0 and (current_time - chord.last_timestamp) > kchord.timeout_ms) {
-        reset_chord_state(display.handle);
+    if (wm.chord.is_timed_out()) {
+        wm.chord.reset(display.handle);
     }
 
-    _ = chord.push(.{ .mod_mask = clean_state, .keysym = keysym });
+    _ = wm.chord.push(.{ .mod_mask = clean_state, .keysym = keysym });
 
     for (config.keybinds.items) |keybind| {
         if (keybind.key_count == 0) continue;
 
-        if (keybind.key_count == chord.index) {
+        if (keybind.key_count == wm.chord.index) {
             var matches = true;
             for (0..keybind.key_count) |i| {
-                if (chord.keys[i].keysym != keybind.keys[i].keysym or chord.keys[i].mod_mask != keybind.keys[i].mod_mask) {
+                if (wm.chord.keys[i].keysym != keybind.keys[i].keysym or
+                    wm.chord.keys[i].mod_mask != keybind.keys[i].mod_mask)
+                {
                     matches = false;
                     break;
                 }
             }
             if (matches) {
-                execute_action(display, keybind.action, keybind.int_arg, keybind.str_arg);
-                reset_chord_state(display.handle);
+                execute_action(display, keybind.action, keybind.int_arg, keybind.str_arg, wm);
+                wm.chord.reset(display.handle);
                 return;
             }
         }
@@ -886,10 +676,12 @@ fn handle_key_press(display: *Display, event: *xlib.XKeyEvent) void {
 
     var has_partial_match = false;
     for (config.keybinds.items) |keybind| {
-        if (keybind.key_count > chord.index) {
+        if (keybind.key_count > wm.chord.index) {
             var matches = true;
-            for (0..chord.index) |i| {
-                if (chord.keys[i].keysym != keybind.keys[i].keysym or chord.keys[i].mod_mask != keybind.keys[i].mod_mask) {
+            for (0..wm.chord.index) |i| {
+                if (wm.chord.keys[i].keysym != keybind.keys[i].keysym or
+                    wm.chord.keys[i].mod_mask != keybind.keys[i].mod_mask)
+                {
                     matches = false;
                     break;
                 }
@@ -902,32 +694,30 @@ fn handle_key_press(display: *Display, event: *xlib.XKeyEvent) void {
     }
 
     if (has_partial_match) {
-        chord.grab_keyboard(display.handle, display.root);
-    } else if (!has_partial_match) {
-        reset_chord_state(display.handle);
+        wm.chord.grab_keyboard(display.handle, display.root);
+    } else {
+        wm.chord.reset(display.handle);
     }
 }
 
-fn execute_action(display: *Display, action: config_mod.Action, int_arg: i32, str_arg: ?[]const u8) void {
+fn execute_action(display: *Display, action: config_mod.Action, int_arg: i32, str_arg: ?[]const u8, wm: *WindowManager) void {
     switch (action) {
         .spawn_terminal => spawn_terminal(),
         .spawn => {
-            if (str_arg) |cmd| {
-                spawn_command(cmd);
-            }
+            if (str_arg) |cmd| spawn_command(cmd);
         },
         .kill_client => kill_focused(display),
         .quit => {
             std.debug.print("quit keybind pressed\n", .{});
-            running = false;
+            wm.running = false;
         },
-        .reload_config => reload_config(display),
-        .restart => reload_config(display),
+        .reload_config => reload_config(display, wm),
+        .restart => reload_config(display, wm),
         .show_keybinds => {
-            if (keybind_overlay) |overlay| {
-                const mon = monitor_mod.selected_monitor orelse monitor_mod.monitors;
+            if (wm.overlay) |overlay| {
+                const mon = wm.selected_monitor orelse wm.monitors;
                 if (mon) |m| {
-                    overlay.toggle(m.mon_x, m.mon_y, m.mon_w, m.mon_h, &config);
+                    overlay.toggle(m.mon_x, m.mon_y, m.mon_w, m.mon_h, &wm.config);
                 }
             }
         },
@@ -967,16 +757,12 @@ fn execute_action(display: *Display, action: config_mod.Action, int_arg: i32, st
         },
         .focus_monitor => focusmon(display, int_arg),
         .send_to_monitor => sendmon(display, int_arg),
-        .scroll_left => {
-            scroll_layout(-1);
-        },
-        .scroll_right => {
-            scroll_layout(1);
-        },
+        .scroll_left => scroll_layout(-1, wm),
+        .scroll_right => scroll_layout(1, wm),
     }
 }
 
-fn reload_config(display: *Display) void {
+fn reload_config(display: *Display, wm: *WindowManager) void {
     std.debug.print("reloading config...\n", .{});
 
     ungrab_keybinds(display);
@@ -1005,27 +791,19 @@ fn reload_config(display: *Display) void {
         initialize_default_config();
     }
 
-    bar_mod.destroy_bars(gpa.allocator(), display.handle);
-    setup_bars(gpa.allocator(), display);
-    rebuild_bar_blocks();
+    bar_mod.destroy_bars(wm.bars, gpa.allocator(), display.handle);
+    wm.bars = null;
+    wm.setup_bars();
+    rebuild_bar_blocks(wm);
 
     grab_keybinds(display);
 }
 
-fn rebuild_bar_blocks() void {
-    var current_bar = bar_mod.bars;
+fn rebuild_bar_blocks(wm: *WindowManager) void {
+    var current_bar = wm.bars;
     while (current_bar) |bar| {
         bar.clear_blocks();
-        if (config.blocks.items.len > 0) {
-            for (config.blocks.items) |cfg_block| {
-                const block = config_block_to_bar_block(cfg_block);
-                bar.add_block(block);
-            }
-        } else {
-            bar.add_block(blocks_mod.Block.init_ram("", 5, 0x7aa2f7, true));
-            bar.add_block(blocks_mod.Block.init_static(" | ", 0x666666, false));
-            bar.add_block(blocks_mod.Block.init_datetime("", "%H:%M", 1, 0x0db9d7, true));
-        }
+        wm.populate_bar_blocks(bar);
         current_bar = bar.next;
     }
 }
@@ -1034,10 +812,6 @@ fn ungrab_keybinds(display: *Display) void {
     _ = xlib.XUngrabKey(display.handle, xlib.AnyKey, xlib.AnyModifier, display.root);
 }
 
-/// File descriptor for the X11 connection.  Set once after the display is
-/// opened and used only in post-fork child setup to close the inherited fd.
-var x11_fd: c_int = -1;
-
 fn spawn_child_setup() void {
     _ = std.c.setsid();
     if (x11_fd >= 0) std.posix.close(@intCast(x11_fd));
@@ -1173,7 +947,7 @@ fn toggle_view(display: *Display, tag_mask: u32) void {
 
         focus_top_client(display, monitor);
         arrange(monitor);
-        bar_mod.invalidate_bars();
+        if (wm_ptr) |wm| wm.invalidate_bars();
     }
 }
 
@@ -1185,7 +959,7 @@ fn toggle_client_tag(display: *Display, tag_mask: u32) void {
         client.tags = new_tags;
         focus_top_client(display, monitor);
         arrange(monitor);
-        bar_mod.invalidate_bars();
+        if (wm_ptr) |wm| wm.invalidate_bars();
     }
 }
 
@@ -1309,7 +1083,7 @@ fn view(display: *Display, tag_mask: u32) void {
 
     focus_top_client(display, monitor);
     arrange(monitor);
-    bar_mod.invalidate_bars();
+    if (wm_ptr) |wm| wm.invalidate_bars();
     std.debug.print("view: tag_mask={d}\n", .{monitor.tagset[monitor.sel_tags]});
 }
 
@@ -1365,7 +1139,7 @@ fn tag_client(display: *Display, tag_mask: u32) void {
     client.tags = tag_mask;
     focus_top_client(display, monitor);
     arrange(monitor);
-    bar_mod.invalidate_bars();
+    if (wm_ptr) |wm| wm.invalidate_bars();
     std.debug.print("tag_client: window=0x{x} tag_mask={d}\n", .{ client.window, tag_mask });
 }
 
@@ -1485,7 +1259,7 @@ fn cycle_layout() void {
         monitor.scroll_offset = 0;
     }
     arrange(monitor);
-    bar_mod.invalidate_bars();
+    if (wm_ptr) |wm| wm.invalidate_bars();
     if (monitor.lt[monitor.sel_lt]) |layout| {
         std.debug.print("cycle_layout: {s}\n", .{layout.symbol});
     }
@@ -1516,7 +1290,7 @@ fn set_layout(layout_name: ?[]const u8) void {
         monitor.scroll_offset = 0;
     }
     arrange(monitor);
-    bar_mod.invalidate_bars();
+    if (wm_ptr) |wm| wm.invalidate_bars();
     if (monitor.lt[monitor.sel_lt]) |layout| {
         std.debug.print("set_layout: {s}\n", .{layout.symbol});
     }
@@ -1530,7 +1304,7 @@ fn set_layout_index(index: u32) void {
         monitor.scroll_offset = 0;
     }
     arrange(monitor);
-    bar_mod.invalidate_bars();
+    if (wm_ptr) |wm| wm.invalidate_bars();
     if (monitor.lt[monitor.sel_lt]) |layout| {
         std.debug.print("set_layout_index: {s}\n", .{layout.symbol});
     }
@@ -1591,7 +1365,7 @@ fn snap_y(client: *Client, new_y: i32, monitor: *Monitor) i32 {
     return new_y;
 }
 
-fn movemouse(display: *Display) void {
+fn movemouse(display: *Display, wm: *WindowManager) void {
     const monitor = monitor_mod.selected_monitor orelse return;
     const client = monitor.sel orelse return;
 
@@ -1622,7 +1396,7 @@ fn movemouse(display: *Display) void {
         xlib.GrabModeAsync,
         xlib.GrabModeAsync,
         xlib.None,
-        cursors.move,
+        wm.cursors.move,
         xlib.CurrentTime,
     );
 
@@ -1698,7 +1472,7 @@ fn movemouse(display: *Display) void {
     }
 }
 
-fn resizemouse(display: *Display) void {
+fn resizemouse(display: *Display, wm: *WindowManager) void {
     const monitor = monitor_mod.selected_monitor orelse return;
     const client = monitor.sel orelse return;
 
@@ -1720,7 +1494,7 @@ fn resizemouse(display: *Display) void {
         xlib.GrabModeAsync,
         xlib.GrabModeAsync,
         xlib.None,
-        cursors.resize,
+        wm.cursors.resize,
         xlib.CurrentTime,
     );
 
@@ -1771,12 +1545,11 @@ fn resizemouse(display: *Display) void {
     arrange(monitor);
 }
 
-fn handle_expose(display: *Display, event: *xlib.XExposeEvent) void {
+fn handle_expose(display: *Display, event: *xlib.XExposeEvent, wm: *WindowManager) void {
     if (event.count != 0) return;
-
-    if (bar_mod.window_to_bar(event.window)) |bar| {
+    if (bar_mod.window_to_bar(wm.bars, event.window)) |bar| {
         bar.invalidate();
-        bar.draw(display.handle, &config.tags, config);
+        bar.draw(display.handle, wm.config);
     }
 }
 
@@ -1792,7 +1565,7 @@ fn clean_mask(mask: c_uint) c_uint {
     return mask & ~(lock | numlock_mask) & (shift | ctrl | mod1 | mod2 | mod3 | mod4 | mod5);
 }
 
-fn handle_button_press(display: *Display, event: *xlib.XButtonEvent) void {
+fn handle_button_press(display: *Display, event: *xlib.XButtonEvent, wm: *WindowManager) void {
     std.debug.print("button_press: window=0x{x} subwindow=0x{x}\n", .{ event.window, event.subwindow });
 
     const clicked_monitor = monitor_mod.window_to_monitor(display.handle, display.root, event.window);
@@ -1806,8 +1579,8 @@ fn handle_button_press(display: *Display, event: *xlib.XButtonEvent) void {
         }
     }
 
-    if (bar_mod.window_to_bar(event.window)) |bar| {
-        const clicked_tag = bar.handle_click(event.x, &config.tags);
+    if (bar_mod.window_to_bar(wm.bars, event.window)) |bar| {
+        const clicked_tag = bar.handle_click(display.handle, event.x, wm.config);
         if (clicked_tag) |tag_index| {
             const tag_mask: u32 = @as(u32, 1) << @intCast(tag_index);
             view(display, tag_mask);
@@ -1830,8 +1603,8 @@ fn handle_button_press(display: *Display, event: *xlib.XButtonEvent) void {
         const button_clean_mask = clean_mask(button.mod_mask);
         if (clean_state == button_clean_mask and event.button == button.button) {
             switch (button.action) {
-                .move_mouse => movemouse(display),
-                .resize_mouse => resizemouse(display),
+                .move_mouse => movemouse(display, wm),
+                .resize_mouse => resizemouse(display, wm),
                 .toggle_floating => {
                     if (click_client) |found_client| {
                         found_client.is_floating = !found_client.is_floating;
@@ -1933,10 +1706,11 @@ fn unmanage(display: *Display, client: *Client) void {
 
     client_mod.destroy(gpa.allocator(), client);
     update_client_list(display);
-    bar_mod.invalidate_bars();
+    if (wm_ptr) |wm| wm.invalidate_bars();
 }
 
-fn handle_enter_notify(display: *Display, event: *xlib.XCrossingEvent) void {
+fn handle_enter_notify(display: *Display, event: *xlib.XCrossingEvent, wm: *WindowManager) void {
+    _ = wm;
     if ((event.mode != xlib.NotifyNormal or event.detail == xlib.NotifyInferior) and event.window != display.root) {
         return;
     }
@@ -1977,22 +1751,18 @@ fn set_focus(display: *Display, client: *Client) void {
     _ = send_event(display, client, atoms.wm_take_focus);
 }
 
-var last_motion_monitor: ?*Monitor = null;
-
-fn handle_motion_notify(display: *Display, event: *xlib.XMotionEvent) void {
-    if (event.window != display.root) {
-        return;
-    }
+fn handle_motion_notify(display: *Display, event: *xlib.XMotionEvent, wm: *WindowManager) void {
+    if (event.window != display.root) return;
 
     const target_mon = monitor_mod.rect_to_monitor(event.x_root, event.y_root, 1, 1);
-    if (target_mon != last_motion_monitor and last_motion_monitor != null) {
+    if (target_mon != wm.last_motion_monitor and wm.last_motion_monitor != null) {
         if (monitor_mod.selected_monitor) |selmon| {
             unfocus_client(display, selmon.sel, true);
         }
         monitor_mod.selected_monitor = target_mon;
         focus(display, null);
     }
-    last_motion_monitor = target_mon;
+    wm.last_motion_monitor = target_mon;
 }
 
 fn handle_property_notify(display: *Display, event: *xlib.XPropertyEvent) void {
@@ -2016,7 +1786,7 @@ fn handle_property_notify(display: *Display, event: *xlib.XPropertyEvent) void {
         client.hints_valid = false;
     } else if (event.atom == xlib.XA_WM_HINTS) {
         update_wm_hints(display, client);
-        bar_mod.invalidate_bars();
+        if (wm_ptr) |wm| wm.invalidate_bars();
     } else if (event.atom == xlib.XA_WM_NAME or event.atom == atoms.net_wm_name) {
         update_title(display, client);
     } else if (event.atom == atoms.net_wm_window_type) {
@@ -2140,15 +1910,15 @@ fn focus(display: *Display, target_client: ?*Client) void {
 
     if (focus_client) |client| {
         if (is_scrolling_layout(current_selmon)) {
-            scroll_to_window(client, true);
+            scroll_to_window(client, true, wm_ptr orelse return);
         }
     }
 
-    bar_mod.invalidate_bars();
+    if (wm_ptr) |wm| wm.invalidate_bars();
 }
 
 fn restack(display: *Display, monitor: *Monitor) void {
-    bar_mod.invalidate_bars();
+    if (wm_ptr) |wm| wm.invalidate_bars();
     const selected_client = monitor.sel orelse return;
 
     if (selected_client.is_floating or monitor.lt[monitor.sel_lt] == null) {
@@ -2177,7 +1947,7 @@ fn restack(display: *Display, monitor: *Monitor) void {
 }
 
 fn arrange(monitor: *Monitor) void {
-    // TODO: display will become a WindowManager field; for now
+    // TODO(wm-refactor): display will become a WindowManager field; for now
     // we use a module-level pointer set once after the display is opened.
     if (wm_display) |display| {
         showhide(display, monitor);
@@ -2192,11 +1962,11 @@ fn arrange(monitor: *Monitor) void {
     }
 }
 
-fn tick_animations() void {
-    if (!scroll_animation.is_active()) return;
+fn tick_animations(wm: *WindowManager) void {
+    if (!wm.scroll_animation.is_active()) return;
 
-    const monitor = monitor_mod.selected_monitor orelse return;
-    if (scroll_animation.update()) |new_offset| {
+    const monitor = wm.selected_monitor orelse return;
+    if (wm.scroll_animation.update()) |new_offset| {
         monitor.scroll_offset = new_offset;
         arrange(monitor);
     }
@@ -2209,32 +1979,32 @@ fn is_scrolling_layout(monitor: *Monitor) bool {
     return false;
 }
 
-fn scroll_layout(direction: i32) void {
-    const monitor = monitor_mod.selected_monitor orelse return;
+fn scroll_layout(direction: i32, wm: *WindowManager) void {
+    const monitor = wm.selected_monitor orelse return;
     if (!is_scrolling_layout(monitor)) return;
 
     const scroll_step = scrolling.get_scroll_step(monitor);
     const max_scroll = scrolling.get_max_scroll(monitor);
 
-    const current = if (scroll_animation.is_active())
-        scroll_animation.target()
+    const current = if (wm.scroll_animation.is_active())
+        wm.scroll_animation.target()
     else
         monitor.scroll_offset;
 
     var target = current + direction * scroll_step;
     target = @max(0, @min(target, max_scroll));
 
-    scroll_animation.start(monitor.scroll_offset, target, animation_config);
+    wm.scroll_animation.start(monitor.scroll_offset, target, wm.animation_config);
 }
 
-fn scroll_to_window(client: *Client, animate: bool) void {
+fn scroll_to_window(client: *Client, animate: bool, wm: *WindowManager) void {
     const monitor = client.monitor orelse return;
     if (!is_scrolling_layout(monitor)) return;
 
     const target = scrolling.get_target_scroll_for_window(monitor, client);
 
     if (animate) {
-        scroll_animation.start(monitor.scroll_offset, target, animation_config);
+        wm.scroll_animation.start(monitor.scroll_offset, target, wm.animation_config);
     } else {
         monitor.scroll_offset = target;
         arrange(monitor);
diff --git a/src/monitor.zig b/src/monitor.zig
index 173e1e7..4cfae13 100644
--- a/src/monitor.zig
+++ b/src/monitor.zig
@@ -7,6 +7,7 @@ pub const Layout = struct {
     arrange_fn: ?*const fn (*Monitor) void,
 };
 
+// TODO: Make clearer, document? refactor?
 pub const Pertag = struct {
     curtag: u32 = 1,
     prevtag: u32 = 1,
@@ -17,6 +18,7 @@ pub const Pertag = struct {
     showbars: [10]bool = [_]bool{true} ** 10,
 };
 
+// TODO: Make clearer, document? refactor?
 pub const Monitor = struct {
     lt_symbol: [16]u8 = std.mem.zeroes([16]u8),
     mfact: f32 = 0.55,
@@ -46,58 +48,61 @@ pub const Monitor = struct {
     stack: ?*Client = null,
     next: ?*Monitor = null,
     bar_win: xlib.Window = 0,
-    lt: [5]?*const Layout = .{ null, null, null, null, null },
-    pertag: Pertag = Pertag{},
+    lt: [5]?*const Layout = .{null} ** 5,
+    pertag: Pertag = .{},
 };
 
+// NOTE: `monitors` and `selected_monitor` will soon be removed, they will
+// move to `WindowManager` fields in a future refactor step.  All new
+// code should prefer receiving a `*Monitor` or `?*Monitor` as a parameter
+// rather than reaching into these globals directly.
+
 pub var monitors: ?*Monitor = null;
 pub var selected_monitor: ?*Monitor = null;
 
 var allocator: std.mem.Allocator = undefined;
 
+/// Must be called once before any other function in this module.
 pub fn init(alloc: std.mem.Allocator) void {
     allocator = alloc;
 }
 
+/// Allocates and zero-initialises a new `Monitor`.
 pub fn create() ?*Monitor {
     const mon = allocator.create(Monitor) catch return null;
     mon.* = Monitor{};
     return mon;
 }
 
+/// Frees a monitor previously returned by `create`.
 pub fn destroy(mon: *Monitor) void {
     allocator.destroy(mon);
 }
 
-var root_window: xlib.Window = 0;
-var display_handle: ?*xlib.Display = null;
-
-pub fn set_root_window(root: xlib.Window, display: *xlib.Display) void {
-    root_window = root;
-    display_handle = display;
-}
-
-pub fn window_to_monitor(win: xlib.Window) ?*Monitor {
-    if (win == root_window and display_handle != null) {
+/// Returns the monitor whose bar window or client matches `win`, falling
+/// back to a pointer-position query when `win` is the root window.
+///
+/// `display` and `root` are passed explicitly rather than cached in module
+/// state, this keeps ownership clear and avoids a stale-pointer hazard.
+pub fn window_to_monitor(display: *xlib.Display, root: xlib.Window, win: xlib.Window) ?*Monitor {
+    if (win == root) {
         var root_x: c_int = undefined;
         var root_y: c_int = undefined;
         var dummy_win: xlib.Window = undefined;
         var dummy_int: c_int = undefined;
         var dummy_uint: c_uint = undefined;
-        if (xlib.XQueryPointer(display_handle.?, root_window, &dummy_win, &dummy_win, &root_x, &root_y, &dummy_int, &dummy_int, &dummy_uint) != 0) {
+        if (xlib.XQueryPointer(display, root, &dummy_win, &dummy_win, &root_x, &root_y, &dummy_int, &dummy_int, &dummy_uint) != 0) {
             return rect_to_monitor(root_x, root_y, 1, 1);
         }
     }
 
     var current = monitors;
     while (current) |monitor| {
-        if (monitor.bar_win == win) {
-            return monitor;
-        }
+        if (monitor.bar_win == win) return monitor;
         current = monitor.next;
     }
 
-    const client = @import("client.zig").window_to_client(win);
+    const client = @import("client.zig").window_to_client(monitors, win);
     if (client) |found_client| {
         return found_client.monitor;
     }
@@ -105,6 +110,8 @@ pub fn window_to_monitor(win: xlib.Window) ?*Monitor {
     return selected_monitor;
 }
 
+/// Returns the monitor with the greatest intersection area with the given
+/// rectangle, or `selected_monitor` if no intersection is found.
 pub fn rect_to_monitor(x: i32, y: i32, width: i32, height: i32) ?*Monitor {
     var result = selected_monitor;
     var max_area: i32 = 0;
@@ -123,6 +130,14 @@ pub fn rect_to_monitor(x: i32, y: i32, width: i32, height: i32) ?*Monitor {
     return result;
 }
 
+/// Returns the next or previous monitor relative to `selected_monitor`.
+///
+/// Positive `direction` moves forward through the linked list (wrapping to
+/// the head); negative moves backward (wrapping to the tail).
+///
+// TODO:
+// - Change direction to an enum/enum_literal
+// - Rename function
 pub fn dir_to_monitor(direction: i32) ?*Monitor {
     var target: ?*Monitor = null;
 
@@ -132,6 +147,7 @@ pub fn dir_to_monitor(direction: i32) ?*Monitor {
             target = monitors;
         }
     } else if (selected_monitor == monitors) {
+        // Already at head, walk to tail.
         var last = monitors;
         while (last) |iter| {
             if (iter.next == null) {
@@ -141,6 +157,7 @@ pub fn dir_to_monitor(direction: i32) ?*Monitor {
             last = iter.next;
         }
     } else {
+        // Walk until we find the node just before selected_monitor.
         var previous = monitors;
         while (previous) |iter| {
             if (iter.next == selected_monitor) {
diff --git a/src/wm.zig b/src/wm.zig
new file mode 100644
index 0000000..c5d4c4a
--- /dev/null
+++ b/src/wm.zig
@@ -0,0 +1,366 @@
+const std = @import("std");
+const mem = std.mem;
+
+const display_mod = @import("x11/display.zig");
+const xlib = @import("x11/xlib.zig");
+const events = @import("x11/events.zig");
+const atoms_mod = @import("x11/atoms.zig");
+const chord_mod = @import("keyboard/chord.zig");
+const client_mod = @import("client.zig");
+const monitor_mod = @import("monitor.zig");
+const bar_mod = @import("bar/bar.zig");
+const blocks_mod = @import("bar/blocks/blocks.zig");
+const config_mod = @import("config/config.zig");
+const overlay_mod = @import("overlay.zig");
+const animations = @import("animations.zig");
+const tiling = @import("layouts/tiling.zig");
+const monocle = @import("layouts/monocle.zig");
+const floating = @import("layouts/floating.zig");
+const scrolling = @import("layouts/scrolling.zig");
+const grid = @import("layouts/grid.zig");
+
+const Display = display_mod.Display;
+const Atoms = atoms_mod.Atoms;
+const ChordState = chord_mod.ChordState;
+const Client = client_mod.Client;
+const Monitor = monitor_mod.Monitor;
+const Bar = bar_mod.Bar;
+const Config = config_mod.Config;
+
+pub const Cursors = struct {
+    normal: xlib.Cursor,
+    resize: xlib.Cursor,
+    move: xlib.Cursor,
+
+    pub fn init(display: *Display) Cursors {
+        return .{
+            .normal = xlib.XCreateFontCursor(display.handle, xlib.XC_left_ptr),
+            .resize = xlib.XCreateFontCursor(display.handle, xlib.XC_sizing),
+            .move = xlib.XCreateFontCursor(display.handle, xlib.XC_fleur),
+        };
+    }
+};
+
+pub const WindowManager = struct {
+    allocator: mem.Allocator,
+
+    /// The connection to the X server.
+    display: Display,
+    x11_fd: c_int,
+    /// Invisible 1×1 window used to satisfy the _NET_SUPPORTING_WM_CHECK
+    /// EWMH convention.
+    wm_check_window: xlib.Window,
+
+    atoms: Atoms,
+    cursors: Cursors,
+    numlock_mask: c_uint,
+
+    config: Config,
+    /// Path to the config file that was loaded.
+    /// Null if using default config.
+    config_path: ?[]const u8,
+
+    /// Head of the linked list of all managed monitors.
+    monitors: ?*Monitor,
+    /// The monitor that currently has input focus.
+    selected_monitor: ?*Monitor,
+
+    /// Head of the linked list of status bars (one per monitor).
+    bars: ?*Bar,
+
+    chord: ChordState,
+
+    overlay: ?*overlay_mod.Keybind_Overlay,
+
+    scroll_animation: animations.Scroll_Animation,
+    animation_config: animations.Animation_Config,
+
+    running: bool,
+    last_motion_monitor: ?*Monitor,
+
+    /// Initialises the window manager
+    ///
+    /// Returns an error if the display cannot be opened or another WM is
+    /// already running.
+    pub fn init(allocator: mem.Allocator, config: Config, config_path: ?[]const u8) !WindowManager {
+        var display = try Display.open();
+        errdefer display.close();
+
+        try display.become_window_manager();
+
+        const x11_fd = xlib.XConnectionNumber(display.handle);
+
+        const atoms_result = Atoms.init(display.handle, display.root);
+        const cursors = Cursors.init(&display);
+        _ = xlib.XDefineCursor(display.handle, display.root, cursors.normal);
+
+        tiling.set_display(display.handle);
+        tiling.set_screen_size(display.screen_width(), display.screen_height());
+
+        monitor_mod.init(allocator);
+        // client_mod.init(allocator);
+
+        var wm = WindowManager{
+            .allocator = allocator,
+            .display = display,
+            .x11_fd = x11_fd,
+            .wm_check_window = atoms_result.check_window,
+            .atoms = atoms_result.atoms,
+            .cursors = cursors,
+            .numlock_mask = 0,
+            .config = config,
+            .config_path = config_path,
+            .monitors = null,
+            .selected_monitor = null,
+            .bars = null,
+            .chord = .{},
+            .overlay = null,
+            .scroll_animation = .{},
+            .animation_config = .{ .duration_ms = 150, .easing = .ease_out },
+            .running = true,
+            .last_motion_monitor = null,
+        };
+
+        wm.setup_monitors();
+        wm.setup_bars();
+        wm.setup_overlay();
+
+        return wm;
+    }
+
+    /// Release all allocated memory owned by the WM.
+    pub fn deinit(self: *WindowManager) void {
+        bar_mod.destroy_bars(self.bars, self.allocator, self.display.handle);
+        self.bars = null;
+
+        if (self.overlay) |o| {
+            o.deinit(self.allocator);
+            self.overlay = null;
+        }
+
+        var mon = self.monitors;
+        while (mon) |m| {
+            const next = m.next;
+            monitor_mod.destroy(m);
+            mon = next;
+        }
+        self.monitors = null;
+        self.selected_monitor = null;
+
+        _ = xlib.XDestroyWindow(self.display.handle, self.wm_check_window);
+
+        self.display.close();
+        self.config.deinit();
+    }
+
+    fn setup_monitors(self: *WindowManager) void {
+        if (xlib.XineramaIsActive(self.display.handle) != 0) {
+            var screen_count: c_int = 0;
+            const screens = xlib.XineramaQueryScreens(self.display.handle, &screen_count);
+
+            if (screen_count > 0 and screens != null) {
+                var prev_monitor: ?*Monitor = null;
+                var index: usize = 0;
+
+                while (index < @as(usize, @intCast(screen_count))) : (index += 1) {
+                    const screen = screens[index];
+                    const mon = monitor_mod.create() orelse continue;
+
+                    mon.num = @intCast(index);
+                    mon.mon_x = screen.x_org;
+                    mon.mon_y = screen.y_org;
+                    mon.mon_w = screen.width;
+                    mon.mon_h = screen.height;
+                    mon.win_x = screen.x_org;
+                    mon.win_y = screen.y_org;
+                    mon.win_w = screen.width;
+                    mon.win_h = screen.height;
+                    mon.lt[0] = &tiling.layout;
+                    mon.lt[1] = &monocle.layout;
+                    mon.lt[2] = &floating.layout;
+                    mon.lt[3] = &scrolling.layout;
+                    mon.lt[4] = &grid.layout;
+
+                    for (0..10) |i| {
+                        mon.pertag.ltidxs[i][0] = mon.lt[0];
+                        mon.pertag.ltidxs[i][1] = mon.lt[1];
+                        mon.pertag.ltidxs[i][2] = mon.lt[2];
+                        mon.pertag.ltidxs[i][3] = mon.lt[3];
+                        mon.pertag.ltidxs[i][4] = mon.lt[4];
+                    }
+
+                    self.init_monitor_gaps(mon);
+
+                    if (prev_monitor) |prev| {
+                        prev.next = mon;
+                    } else {
+                        self.monitors = mon;
+                        self.selected_monitor = mon;
+                    }
+                    prev_monitor = mon;
+                }
+
+                _ = xlib.XFree(@ptrCast(screens));
+            }
+        }
+
+        // Fallback: single monitor covering the full screen.
+        if (self.monitors == null) {
+            const mon = monitor_mod.create() orelse return;
+            mon.num = 0;
+            mon.mon_x = 0;
+            mon.mon_y = 0;
+            mon.mon_w = self.display.screen_width();
+            mon.mon_h = self.display.screen_height();
+            mon.win_x = 0;
+            mon.win_y = 0;
+            mon.win_w = mon.mon_w;
+            mon.win_h = mon.mon_h;
+            mon.lt[0] = &tiling.layout;
+            mon.lt[1] = &monocle.layout;
+            mon.lt[2] = &floating.layout;
+            mon.lt[3] = &scrolling.layout;
+            mon.lt[4] = &grid.layout;
+
+            for (0..10) |i| {
+                mon.pertag.ltidxs[i][0] = mon.lt[0];
+                mon.pertag.ltidxs[i][1] = mon.lt[1];
+                mon.pertag.ltidxs[i][2] = mon.lt[2];
+                mon.pertag.ltidxs[i][3] = mon.lt[3];
+                mon.pertag.ltidxs[i][4] = mon.lt[4];
+            }
+
+            self.init_monitor_gaps(mon);
+            self.monitors = mon;
+            self.selected_monitor = mon;
+        }
+
+        // Mirror into monitor_mod so legacy code that still reads the
+        // module-level vars continues to work during the transition.
+        // TODO(wm-refactor): remove once all callers use self.monitors.
+        monitor_mod.monitors = self.monitors;
+        monitor_mod.selected_monitor = self.selected_monitor;
+    }
+
+    fn init_monitor_gaps(self: *WindowManager, mon: *Monitor) void {
+        const cfg = &self.config;
+        const any_gap_nonzero = cfg.gap_inner_h != 0 or cfg.gap_inner_v != 0 or
+            cfg.gap_outer_h != 0 or cfg.gap_outer_v != 0;
+
+        if (cfg.gaps_enabled and any_gap_nonzero) {
+            mon.gap_inner_h = cfg.gap_inner_h;
+            mon.gap_inner_v = cfg.gap_inner_v;
+            mon.gap_outer_h = cfg.gap_outer_h;
+            mon.gap_outer_v = cfg.gap_outer_v;
+        } else {
+            mon.gap_inner_h = 0;
+            mon.gap_inner_v = 0;
+            mon.gap_outer_h = 0;
+            mon.gap_outer_v = 0;
+        }
+    }
+
+    pub fn setup_bars(self: *WindowManager) void {
+        var current_monitor = self.monitors;
+        var last_bar: ?*Bar = null;
+
+        while (current_monitor) |monitor| {
+            const bar = Bar.create(
+                self.allocator,
+                self.display.handle,
+                self.display.screen,
+                monitor,
+                self.config,
+            ) orelse {
+                current_monitor = monitor.next;
+                continue;
+            };
+
+            if (tiling.bar_height == 0) {
+                tiling.set_bar_height(bar.height);
+            }
+
+            self.populate_bar_blocks(bar);
+
+            if (last_bar) |prev| {
+                prev.next = bar;
+            } else {
+                self.bars = bar;
+            }
+            last_bar = bar;
+
+            std.debug.print("bar created for monitor {d}\n", .{monitor.num});
+            current_monitor = monitor.next;
+        }
+    }
+
+    pub fn populate_bar_blocks(self: *WindowManager, bar: *Bar) void {
+        if (self.config.blocks.items.len > 0) {
+            for (self.config.blocks.items) |cfg_block| {
+                bar.add_block(config_block_to_bar_block(cfg_block));
+            }
+        } else {
+            bar.add_block(blocks_mod.Block.init_ram("", 5, 0x7aa2f7, true));
+            bar.add_block(blocks_mod.Block.init_static(" | ", 0x666666, false));
+            bar.add_block(blocks_mod.Block.init_datetime("", "%H:%M", 1, 0x0db9d7, true));
+        }
+    }
+
+    fn setup_overlay(self: *WindowManager) void {
+        self.overlay = overlay_mod.Keybind_Overlay.init(
+            self.display.handle,
+            self.display.screen,
+            self.display.root,
+            self.config.font,
+            self.allocator,
+        );
+    }
+
+    // Wrap free functions in `bar_mod` with `self.bars` passed in.
+
+    pub fn invalidate_bars(self: *WindowManager) void {
+        bar_mod.invalidate_bars(self.bars);
+    }
+
+    pub fn window_to_bar(self: *WindowManager, win: xlib.Window) ?*Bar {
+        return bar_mod.window_to_bar(self.bars, win);
+    }
+};
+
+/// Converts a config block description into a live status bar block.
+pub fn config_block_to_bar_block(cfg: config_mod.Block) blocks_mod.Block {
+    return switch (cfg.block_type) {
+        .static => blocks_mod.Block.init_static(cfg.format, cfg.color, cfg.underline),
+        .datetime => blocks_mod.Block.init_datetime(
+            cfg.format,
+            cfg.datetime_format orelse "%H:%M",
+            cfg.interval,
+            cfg.color,
+            cfg.underline,
+        ),
+        .ram => blocks_mod.Block.init_ram(cfg.format, cfg.interval, cfg.color, cfg.underline),
+        .shell => blocks_mod.Block.init_shell(
+            cfg.format,
+            cfg.command orelse "",
+            cfg.interval,
+            cfg.color,
+            cfg.underline,
+        ),
+        .battery => blocks_mod.Block.init_battery(
+            cfg.format_charging orelse "",
+            cfg.format_discharging orelse "",
+            cfg.format_full orelse "",
+            cfg.battery_name orelse "BAT0",
+            cfg.interval,
+            cfg.color,
+            cfg.underline,
+        ),
+        .cpu_temp => blocks_mod.Block.init_cpu_temp(
+            cfg.format,
+            cfg.thermal_zone orelse "thermal_zone0",
+            cfg.interval,
+            cfg.color,
+            cfg.underline,
+        ),
+    };
+}