wm

init

197ffef71b83b698c134544a25a9dbb404d4f3bd

SM <seb.michalk@gmail.com>

2026-04-27 07:33:03 +0000

 build.zig      |  18 ++
 src/atoms.zig  | 121 +++++++++++
 src/bar.zig    | 214 +++++++++++++++++++
 src/c.zig      |   8 +
 src/client.zig | 636 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/config.zig |  56 +++++
 src/event.zig  | 350 +++++++++++++++++++++++++++++++
 src/main.zig   | 205 +++++++++++++++++++
 src/util.zig   |  21 ++
 zig-out/bin/wm | Bin 0 -> 50304 bytes
 10 files changed, 1629 insertions(+)

diff --git a/build.zig b/build.zig
new file mode 100644
index 0000000..62d3956
--- /dev/null
+++ b/build.zig
@@ -0,0 +1,18 @@
+const std = @import("std");
+
+pub fn build(b: *std.Build) void {
+    const target = b.standardTargetOptions(.{});
+    const optimize = b.standardOptimizeOption(.{});
+
+    const exe = b.addExecutable(.{
+        .name = "wm",
+        .root_source_file = b.path("src/main.zig"),
+        .target = target,
+        .optimize = optimize,
+    });
+    exe.linkSystemLibrary("X11");
+    exe.linkSystemLibrary("Xft");
+    exe.linkLibC();
+
+    b.installArtifact(exe);
+}
diff --git a/src/atoms.zig b/src/atoms.zig
new file mode 100644
index 0000000..7a597da
--- /dev/null
+++ b/src/atoms.zig
@@ -0,0 +1,121 @@
+const c = @import("c.zig").c;
+
+pub const Atoms = struct {
+    wm_protocols: c.Atom,
+    wm_delete_window: c.Atom,
+    wm_take_focus: c.Atom,
+    wm_state: c.Atom,
+    wm_change_state: c.Atom,
+    net_supported: c.Atom,
+    net_supporting_wm_check: c.Atom,
+    net_wm_name: c.Atom,
+    net_wm_state: c.Atom,
+    net_wm_state_fullscreen: c.Atom,
+    net_wm_state_hidden: c.Atom,
+    net_wm_state_demands_attention: c.Atom,
+    net_wm_state_above: c.Atom,
+    net_wm_window_type: c.Atom,
+    net_wm_window_type_normal: c.Atom,
+    net_wm_window_type_dialog: c.Atom,
+    net_wm_window_type_utility: c.Atom,
+    net_wm_window_type_toolbar: c.Atom,
+    net_wm_window_type_splash: c.Atom,
+    net_wm_window_type_menu: c.Atom,
+    net_wm_window_type_dock: c.Atom,
+    net_active_window: c.Atom,
+    net_close_window: c.Atom,
+    net_client_list: c.Atom,
+    net_client_list_stacking: c.Atom,
+    net_frame_extents: c.Atom,
+    net_wm_pid: c.Atom,
+
+    pub fn init(display: ?*c.Display) Atoms {
+        const names = [_][*:0]const u8{
+            "WM_PROTOCOLS",
+            "WM_DELETE_WINDOW",
+            "WM_TAKE_FOCUS",
+            "WM_STATE",
+            "WM_CHANGE_STATE",
+            "_NET_SUPPORTED",
+            "_NET_SUPPORTING_WM_CHECK",
+            "_NET_WM_NAME",
+            "_NET_WM_STATE",
+            "_NET_WM_STATE_FULLSCREEN",
+            "_NET_WM_STATE_HIDDEN",
+            "_NET_WM_STATE_DEMANDS_ATTENTION",
+            "_NET_WM_STATE_ABOVE",
+            "_NET_WM_WINDOW_TYPE",
+            "_NET_WM_WINDOW_TYPE_NORMAL",
+            "_NET_WM_WINDOW_TYPE_DIALOG",
+            "_NET_WM_WINDOW_TYPE_UTILITY",
+            "_NET_WM_WINDOW_TYPE_TOOLBAR",
+            "_NET_WM_WINDOW_TYPE_SPLASH",
+            "_NET_WM_WINDOW_TYPE_MENU",
+            "_NET_WM_WINDOW_TYPE_DOCK",
+            "_NET_ACTIVE_WINDOW",
+            "_NET_CLOSE_WINDOW",
+            "_NET_CLIENT_LIST",
+            "_NET_CLIENT_LIST_STACKING",
+            "_NET_FRAME_EXTENTS",
+            "_NET_WM_PID",
+        };
+        var atoms: [names.len]c.Atom = undefined;
+        _ = c.XInternAtoms(display, @ptrCast(@constCast(&names)), @intCast(names.len), 0, &atoms);
+        return .{
+            .wm_protocols = atoms[0],
+            .wm_delete_window = atoms[1],
+            .wm_take_focus = atoms[2],
+            .wm_state = atoms[3],
+            .wm_change_state = atoms[4],
+            .net_supported = atoms[5],
+            .net_supporting_wm_check = atoms[6],
+            .net_wm_name = atoms[7],
+            .net_wm_state = atoms[8],
+            .net_wm_state_fullscreen = atoms[9],
+            .net_wm_state_hidden = atoms[10],
+            .net_wm_state_demands_attention = atoms[11],
+            .net_wm_state_above = atoms[12],
+            .net_wm_window_type = atoms[13],
+            .net_wm_window_type_normal = atoms[14],
+            .net_wm_window_type_dialog = atoms[15],
+            .net_wm_window_type_utility = atoms[16],
+            .net_wm_window_type_toolbar = atoms[17],
+            .net_wm_window_type_splash = atoms[18],
+            .net_wm_window_type_menu = atoms[19],
+            .net_wm_window_type_dock = atoms[20],
+            .net_active_window = atoms[21],
+            .net_close_window = atoms[22],
+            .net_client_list = atoms[23],
+            .net_client_list_stacking = atoms[24],
+            .net_frame_extents = atoms[25],
+            .net_wm_pid = atoms[26],
+        };
+    }
+
+    pub fn supportedList(self: *const Atoms) []const c.Atom {
+        return &.{
+            self.net_supported,
+            self.net_supporting_wm_check,
+            self.net_wm_name,
+            self.net_wm_state,
+            self.net_wm_state_fullscreen,
+            self.net_wm_state_hidden,
+            self.net_wm_state_demands_attention,
+            self.net_wm_state_above,
+            self.net_wm_window_type,
+            self.net_wm_window_type_normal,
+            self.net_wm_window_type_dialog,
+            self.net_wm_window_type_utility,
+            self.net_wm_window_type_toolbar,
+            self.net_wm_window_type_splash,
+            self.net_wm_window_type_menu,
+            self.net_wm_window_type_dock,
+            self.net_active_window,
+            self.net_close_window,
+            self.net_client_list,
+            self.net_client_list_stacking,
+            self.net_frame_extents,
+            self.net_wm_pid,
+        };
+    }
+};
diff --git a/src/bar.zig b/src/bar.zig
new file mode 100644
index 0000000..d563ef4
--- /dev/null
+++ b/src/bar.zig
@@ -0,0 +1,214 @@
+const std = @import("std");
+const c = @import("c.zig").c;
+const config = @import("config.zig");
+const client_mod = @import("client.zig");
+const WM = client_mod.WM;
+const Client = client_mod.Client;
+const util = @import("util.zig");
+
+fn allocColor(display: ?*c.Display, visual: ?*c.Visual, cmap: c.Colormap, name: [:0]const u8) !c.XftColor {
+    var color: c.XftColor = undefined;
+    if (c.XftColorAllocName(display, visual, cmap, name, &color) == 0)
+        return error.ColorAllocFailed;
+    return color;
+}
+
+fn fontHeight(fonts: []?*c.XftFont) i32 {
+    for (fonts) |f| {
+        if (f) |font| return font.ascent + font.descent;
+    }
+    return config.bar_height;
+}
+
+fn fontAscent(fonts: []?*c.XftFont) i32 {
+    for (fonts) |f| {
+        if (f) |font| return font.ascent;
+    }
+    return 0;
+}
+
+fn textWidth(dpy: ?*c.Display, fonts: []?*c.XftFont, text: []const u8) i32 {
+    for (fonts) |f| {
+        if (f == null) continue;
+        var ext: c.XGlyphInfo = undefined;
+        c.XftTextExtents8(dpy, f.?, text.ptr, @intCast(text.len), &ext);
+        return @intCast(ext.xOff);
+    }
+    return @intCast(text.len * 8);
+}
+
+fn drawChar(dpy: ?*c.Display, xft_draw: ?*c.XftDraw, fonts: []?*c.XftFont, color: *c.XftColor, x: i32, y: i32, ch: u8) i32 {
+    const rune: c.FcChar32 = ch;
+    for (fonts) |f| {
+        if (f == null) continue;
+        const glyph = c.XftCharIndex(dpy, f.?, rune);
+        if (glyph == 0) continue;
+        var ext: c.XGlyphInfo = undefined;
+        c.XftGlyphExtents(dpy, f.?, &glyph, 1, &ext);
+        c.XftDrawGlyphs(xft_draw, color, f.?, x, y, &glyph, 1);
+        return @intCast(ext.xOff);
+    }
+    return 8;
+}
+
+fn drawText(dpy: ?*c.Display, xft_draw: ?*c.XftDraw, fonts: []?*c.XftFont, color: *c.XftColor, x: i32, y: i32, text: []const u8) i32 {
+    var cx = x;
+    for (text) |ch| {
+        cx += drawChar(dpy, xft_draw, fonts, color, cx, y, ch);
+    }
+    return cx;
+}
+
+pub fn create(wm: *WM) !void {
+    const d = wm.display;
+    const s = wm.screen;
+    const root = wm.root;
+    const visual = wm.visual;
+    const cmap = wm.colormap;
+
+    for (&config.font_names, 0..) |name, i| {
+        wm.bar.fonts[i] = c.XftFontOpenName(d, s, name);
+        if (wm.bar.fonts[i] == null)
+            util.die("cannot open font: {s}\n", .{name});
+    }
+    wm.bar.colors.bg = allocColor(d, visual, cmap, config.col.bar_bg) catch
+        util.die("cannot allocate color: {s}\n", .{config.col.bar_bg});
+    wm.bar.colors.fg = allocColor(d, visual, cmap, config.col.bar_fg) catch
+        util.die("cannot allocate color: {s}\n", .{config.col.bar_fg});
+    wm.bar.colors.sel_bg = allocColor(d, visual, cmap, config.col.bar_sel_bg) catch
+        util.die("cannot allocate color: {s}\n", .{config.col.bar_sel_bg});
+    wm.bar.colors.min_fg = allocColor(d, visual, cmap, config.col.bar_min_fg) catch
+        util.die("cannot allocate color: {s}\n", .{config.col.bar_min_fg});
+    wm.bar.colors.urg_fg = allocColor(d, visual, cmap, config.col.bar_urg_fg) catch
+        util.die("cannot allocate color: {s}\n", .{config.col.bar_urg_fg});
+    wm.bar.colors.border_focused = allocColor(d, visual, cmap, config.col.border_focused) catch
+        util.die("cannot allocate color: {s}\n", .{config.col.border_focused});
+    wm.bar.colors.border_unfocused = allocColor(d, visual, cmap, config.col.border_unfocused) catch
+        util.die("cannot allocate color: {s}\n", .{config.col.border_unfocused});
+
+    var wa = std.mem.zeroes(c.XSetWindowAttributes);
+    wa.override_redirect = 1;
+    wa.background_pixmap = c.ParentRelative;
+    wa.event_mask = c.ExposureMask | c.ButtonPressMask;
+
+    wm.bar.window = c.XCreateWindow(d, root, 0, 0, wm.sw, config.bar_height, 0,
+        c.CopyFromParent, c.InputOutput, visual,
+        c.CWOverrideRedirect | c.CWBackPixmap | c.CWEventMask, &wa);
+
+    _ = c.XChangeProperty(d, wm.bar.window, wm.atoms.net_wm_window_type,
+        c.XA_ATOM, 32, c.PropModeReplace,
+        @ptrCast(@constCast(&wm.atoms.net_wm_window_type_dock)), 1);
+    _ = c.XMapWindow(d, wm.bar.window);
+
+    wm.bar.pixmap = c.XCreatePixmap(d, wm.bar.window, wm.sw, config.bar_height,
+        @intCast(c.DefaultDepth(d, s)));
+    wm.bar.draw = c.XftDrawCreate(d, wm.bar.pixmap, visual, cmap);
+    wm.bar.button_count = 0;
+}
+
+pub fn draw(wm: *WM) void {
+    const bar = &wm.bar;
+    const d = wm.display;
+    const fonts = &bar.fonts;
+    const bh: i32 = @intCast(config.bar_height);
+    const asc = fontAscent(fonts) + @divTrunc(bh - fontHeight(fonts), 2);
+
+    _ = c.XftDrawRect(bar.draw, &bar.colors.bg, 0, 0, @intCast(wm.sw), bh);
+
+    var x: i32 = 0;
+    var btn_idx: usize = 0;
+    var it = wm.clients;
+    while (it) |cl| : (it = cl.next) {
+        if (btn_idx >= bar.buttons.len) break;
+
+        const title = if (cl.title.len > 0) cl.title else "???";
+        var tw = textWidth(d, fonts, title) + 12;
+        if (x + tw > @as(i32, @intCast(wm.sw))) tw = @as(i32, @intCast(wm.sw)) - x;
+
+        if (wm.focused == cl) {
+            _ = c.XftDrawRect(bar.draw, &bar.colors.sel_bg, x, 0, @intCast(tw), @intCast(bh));
+        }
+
+        const fg_color: *c.XftColor = if (cl.flags.minimized)
+            &bar.colors.min_fg
+        else if (cl.flags.urgent)
+            &bar.colors.urg_fg
+        else
+            &bar.colors.fg;
+
+        const draw_w = @max(tw - 12, 0);
+        if (draw_w > 0) {
+            var end: usize = 0;
+            while (end < title.len) : (end += 1) {
+                if (textWidth(d, fonts, title[0 .. end + 1]) > draw_w) break;
+            }
+            if (end > 0)
+                _ = drawText(d, bar.draw, fonts, fg_color, x + 6, asc, title[0..end]);
+        }
+
+        bar.buttons[btn_idx] = .{ .x = x, .w = @intCast(tw), .win = cl.window };
+        btn_idx += 1;
+        x += tw;
+    }
+    bar.button_count = btn_idx;
+
+    var status_buf: [256]u8 = undefined;
+    const status = readStatus(wm, &status_buf);
+    if (status.len > 0) {
+        const sw = textWidth(d, fonts, status);
+        const sx = @as(i32, @intCast(wm.sw)) - sw - 6;
+        if (sx > x) {
+            _ = drawText(d, bar.draw, fonts, &bar.colors.fg, sx, asc, status);
+        }
+    }
+
+    _ = c.XCopyArea(d, bar.pixmap, bar.window,
+        c.DefaultGC(d, wm.screen), 0, 0, @intCast(wm.sw), bh, 0, 0);
+    _ = c.XRaiseWindow(d, wm.bar.window);
+}
+
+fn readPropText(wm: *WM, prop_atom: c.Atom, buf: *[256]u8) []u8 {
+    var actual_type: c.Atom = 0;
+    var actual_format: c_int = 0;
+    var nitems: c_ulong = 0;
+    var bytes_after: c_ulong = 0;
+    var prop: [*c]u8 = null;
+    if (c.XGetWindowProperty(wm.display, wm.root, prop_atom,
+        0, 1024, 0, c.AnyPropertyType, &actual_type, &actual_format,
+        &nitems, &bytes_after, &prop) == c.Success and prop != null)
+    {
+        defer _ = c.XFree(prop);
+        if (actual_format != 8 or nitems == 0) return &.{};
+        const len = @min(nitems, buf.len - 1);
+        @memcpy(buf[0..len], prop[0..len]);
+        buf[len] = 0;
+        return buf[0..len];
+    }
+    return &.{};
+}
+
+fn readStatus(wm: *WM, buf: *[256]u8) []u8 {
+    const utf8 = readPropText(wm, wm.atoms.net_wm_name, buf);
+    if (utf8.len > 0) return utf8;
+    return readPropText(wm, c.XA_WM_NAME, buf);
+}
+
+pub fn destroy(wm: *WM) void {
+    if (wm.bar.draw) |d| c.XftDrawDestroy(d);
+    if (wm.bar.pixmap != 0) {
+        _ = c.XFreePixmap(wm.display, wm.bar.pixmap);
+    }
+    if (wm.bar.window != 0) {
+        _ = c.XDestroyWindow(wm.display, wm.bar.window);
+    }
+    for (&wm.bar.fonts) |f| {
+        if (f) |font| c.XftFontClose(wm.display, font);
+    }
+    c.XftColorFree(wm.display, wm.visual, wm.colormap, &wm.bar.colors.bg);
+    c.XftColorFree(wm.display, wm.visual, wm.colormap, &wm.bar.colors.fg);
+    c.XftColorFree(wm.display, wm.visual, wm.colormap, &wm.bar.colors.sel_bg);
+    c.XftColorFree(wm.display, wm.visual, wm.colormap, &wm.bar.colors.min_fg);
+    c.XftColorFree(wm.display, wm.visual, wm.colormap, &wm.bar.colors.urg_fg);
+    c.XftColorFree(wm.display, wm.visual, wm.colormap, &wm.bar.colors.border_focused);
+    c.XftColorFree(wm.display, wm.visual, wm.colormap, &wm.bar.colors.border_unfocused);
+}
diff --git a/src/c.zig b/src/c.zig
new file mode 100644
index 0000000..18e2e8c
--- /dev/null
+++ b/src/c.zig
@@ -0,0 +1,8 @@
+pub const c = @cImport({
+    @cDefine("XK_MISCELLANY", {});
+    @cInclude("X11/Xlib.h");
+    @cInclude("X11/Xutil.h");
+    @cInclude("X11/Xatom.h");
+    @cInclude("X11/Xft/Xft.h");
+    @cInclude("X11/XKBlib.h");
+});
diff --git a/src/client.zig b/src/client.zig
new file mode 100644
index 0000000..9f6aec9
--- /dev/null
+++ b/src/client.zig
@@ -0,0 +1,636 @@
+const std = @import("std");
+const c = @import("c.zig").c;
+const config = @import("config.zig");
+const atoms_mod = @import("atoms.zig");
+const Atoms = atoms_mod.Atoms;
+
+pub const Client = struct {
+    window: c.Window,
+    x: i32 = 0,
+    y: i32 = 0,
+    w: u32 = 0,
+    h: u32 = 0,
+    bw: u32 = config.border_width,
+    saved: struct {
+        x: i32 = 0,
+        y: i32 = 0,
+        w: u32 = 0,
+        h: u32 = 0,
+    } = .{},
+    min_w: i32 = 0,
+    min_h: i32 = 0,
+    max_w: i32 = 0,
+    max_h: i32 = 0,
+    base_w: i32 = 0,
+    base_h: i32 = 0,
+    inc_w: i32 = 0,
+    inc_h: i32 = 0,
+    min_aspect: f64 = 0,
+    max_aspect: f64 = 0,
+    has_aspect: bool = false,
+    flags: packed struct {
+        floating: bool = false,
+        fullscreen: bool = false,
+        minimized: bool = false,
+        urgent: bool = false,
+        never_focus: bool = false,
+        fixed_size: bool = false,
+    } = .{},
+    title: []u8 = &.{},
+    class: []u8 = &.{},
+    prev: ?*Client = null,
+    next: ?*Client = null,
+    unmap_count: u32 = 0,
+    supports_delete: bool = false,
+    supports_take_focus: bool = false,
+    input_hint: bool = true,
+    transient_for: c.Window = 0,
+};
+
+pub const GrabState = struct {
+    client: *Client,
+    orig_x: i32,
+    orig_y: i32,
+    orig_w: u32,
+    orig_h: u32,
+    start_x: i32,
+    start_y: i32,
+    resizing: bool,
+};
+
+pub const ButtonEntry = struct {
+    x: i32,
+    w: u32,
+    win: c.Window,
+};
+
+pub const Bar = struct {
+    window: c.Window = 0,
+    pixmap: c.Pixmap = 0,
+    draw: ?*c.XftDraw = null,
+    fonts: [config.font_names.len]?*c.XftFont,
+    colors: struct {
+        bg: c.XftColor,
+        fg: c.XftColor,
+        sel_bg: c.XftColor,
+        min_fg: c.XftColor,
+        urg_fg: c.XftColor,
+        border_focused: c.XftColor,
+        border_unfocused: c.XftColor,
+    },
+    buttons: [128]ButtonEntry,
+    button_count: usize = 0,
+};
+
+pub const WM = struct {
+    display: ?*c.Display,
+    screen: c_int,
+    root: c.Window,
+    sw: u32,
+    sh: u32,
+    atoms: Atoms,
+    bar: Bar,
+    clients: ?*Client,
+    focused: ?*Client,
+    running: bool,
+    allocator: std.mem.Allocator,
+    support_win: c.Window,
+    grab: ?GrabState,
+    visual: ?*c.Visual,
+    colormap: c.Colormap,
+};
+
+pub fn listAppend(head: *?*Client, cl: *Client) void {
+    cl.next = null;
+    cl.prev = null;
+    if (head.*) |h| {
+        var it: *Client = h;
+        while (it.next) |next| it = next;
+        it.next = cl;
+        cl.prev = it;
+    } else {
+        head.* = cl;
+    }
+}
+
+pub fn listRemove(head: *?*Client, cl: *Client) void {
+    if (cl.prev) |p| p.next = cl.next else head.* = cl.next;
+    if (cl.next) |n| n.prev = cl.prev;
+    cl.prev = null;
+    cl.next = null;
+}
+
+pub fn listFind(head: ?*Client, win: c.Window) ?*Client {
+    var it = head;
+    while (it) |cl| : (it = cl.next) {
+        if (cl.window == win) return cl;
+    }
+    return null;
+}
+
+pub fn listCount(head: ?*Client) usize {
+    var n: usize = 0;
+    var it = head;
+    while (it) |cl| : (it = cl.next) n += 1;
+    return n;
+}
+
+pub fn listLast(head: ?*Client) ?*Client {
+    var it = head orelse return null;
+    while (it.next) |next| it = next;
+    return it;
+}
+
+fn setWmState(wm: *WM, cl: *Client, state: i32) void {
+    const data = [2]c_long{ @intCast(state), 0 };
+    _ = c.XChangeProperty(wm.display, cl.window, wm.atoms.wm_state,
+        wm.atoms.wm_state, 32, c.PropModeReplace, @ptrCast(&data), 2);
+}
+
+fn sendProtocol(wm: *WM, cl: *Client, protocol: c.Atom) void {
+    var ev: c.XEvent = undefined;
+    @memset(@as([*]u8, @ptrCast(&ev))[0..@sizeOf(c.XEvent)], 0);
+    ev.type = c.ClientMessage;
+    ev.xclient.window = cl.window;
+    ev.xclient.message_type = wm.atoms.wm_protocols;
+    ev.xclient.format = 32;
+    ev.xclient.data.l[0] = @intCast(protocol);
+    ev.xclient.data.l[1] = @intCast(c.CurrentTime);
+    _ = c.XSendEvent(wm.display, cl.window, 0, c.NoEventMask, &ev);
+}
+
+fn updateFocusPolicy(cl: *Client) void {
+    cl.flags.never_focus = !cl.input_hint and !cl.supports_take_focus;
+}
+
+pub fn closeClient(wm: *WM, cl: *Client) void {
+    if (cl.supports_delete) {
+        sendProtocol(wm, cl, wm.atoms.wm_delete_window);
+    } else {
+        _ = c.XKillClient(wm.display, cl.window);
+    }
+}
+
+pub fn readTitle(wm: *WM, cl: *Client) void {
+    if (cl.title.len > 0) {
+        wm.allocator.free(cl.title);
+        cl.title = &.{};
+    }
+    var text: c.XTextProperty = undefined;
+    if (c.XGetTextProperty(wm.display, cl.window, &text, wm.atoms.net_wm_name) != 0) {
+        if (text.value != null) {
+            defer _ = c.XFree(text.value);
+            if (text.nitems > 0) {
+                var len: usize = @intCast(text.nitems);
+                if (text.value[len - 1] == 0) len -= 1;
+                cl.title = wm.allocator.dupe(u8, text.value[0..len]) catch &.{};
+                return;
+            }
+        }
+    }
+    var name: [*c]u8 = null;
+    if (c.XFetchName(wm.display, cl.window, &name) != 0) {
+        if (name != null) {
+            cl.title = wm.allocator.dupe(u8, std.mem.sliceTo(name.?, 0)) catch &.{};
+            _ = c.XFree(name);
+        }
+    }
+}
+
+pub fn readClass(wm: *WM, cl: *Client) void {
+    if (cl.class.len > 0) {
+        wm.allocator.free(cl.class);
+        cl.class = &.{};
+    }
+    var hint: c.XClassHint = .{ .res_name = null, .res_class = null };
+    if (c.XGetClassHint(wm.display, cl.window, &hint) != 0) {
+        if (hint.res_class != null) {
+            cl.class = wm.allocator.dupe(u8, std.mem.sliceTo(hint.res_class.?, 0)) catch &.{};
+            _ = c.XFree(hint.res_class);
+        }
+        if (hint.res_name != null) _ = c.XFree(hint.res_name);
+    }
+}
+
+fn readProtocols(wm: *WM, cl: *Client) void {
+    var protos: [*c]c.Atom = null;
+    var n: c_int = 0;
+    if (c.XGetWMProtocols(wm.display, cl.window, &protos, &n) != 0) {
+        var i: c_int = 0;
+        while (i < n) : (i += 1) {
+            if (protos[@intCast(i)] == wm.atoms.wm_delete_window)
+                cl.supports_delete = true;
+            if (protos[@intCast(i)] == wm.atoms.wm_take_focus)
+                cl.supports_take_focus = true;
+        }
+        _ = c.XFree(protos);
+    }
+    updateFocusPolicy(cl);
+}
+
+fn readHints(wm: *WM, cl: *Client) void {
+    var hints: c.XSizeHints = undefined;
+    var supplied: c_long = 0;
+    if (c.XGetWMNormalHints(wm.display, cl.window, &hints, &supplied) != 0) {
+        if (hints.flags & c.PMinSize != 0) {
+            cl.min_w = hints.min_width;
+            cl.min_h = hints.min_height;
+        }
+        if (hints.flags & c.PMaxSize != 0) {
+            cl.max_w = hints.max_width;
+            cl.max_h = hints.max_height;
+        }
+        if (hints.flags & c.PBaseSize != 0) {
+            cl.base_w = hints.base_width;
+            cl.base_h = hints.base_height;
+        }
+        if (hints.flags & c.PResizeInc != 0) {
+            cl.inc_w = hints.width_inc;
+            cl.inc_h = hints.height_inc;
+        }
+        if (hints.flags & c.PAspect != 0) {
+            if (hints.min_aspect.y > 0 and hints.max_aspect.y > 0) {
+                cl.min_aspect = @as(f64, @floatFromInt(hints.min_aspect.x)) /
+                    @as(f64, @floatFromInt(hints.min_aspect.y));
+                cl.max_aspect = @as(f64, @floatFromInt(hints.max_aspect.x)) /
+                    @as(f64, @floatFromInt(hints.max_aspect.y));
+                cl.has_aspect = true;
+            }
+        }
+        if (hints.flags & (c.PMinSize | c.PBaseSize) != 0 and
+            hints.flags & c.PMaxSize != 0 and
+            hints.min_width == hints.max_width and
+            hints.min_height == hints.max_height)
+        {
+            cl.flags.fixed_size = true;
+            cl.flags.floating = true;
+        }
+    }
+
+    const wm_hints: [*c]c.XWMHints = c.XGetWMHints(wm.display, cl.window);
+    if (wm_hints != null) {
+        if (wm_hints.*.flags & c.InputHint != 0)
+            cl.input_hint = wm_hints.*.input != 0;
+        if (wm_hints.*.flags & c.XUrgencyHint != 0)
+            cl.flags.urgent = true;
+        _ = c.XFree(wm_hints);
+    }
+    updateFocusPolicy(cl);
+
+    var trans: c.Window = 0;
+    _ = c.XGetTransientForHint(wm.display, cl.window, &trans);
+    if (trans != 0) {
+        cl.transient_for = trans;
+        cl.flags.floating = true;
+    }
+}
+
+fn checkWindowType(wm: *WM, cl: *Client) void {
+    var actual_type: c.Atom = 0;
+    var actual_format: c_int = 0;
+    var nitems: c_ulong = 0;
+    var bytes_after: c_ulong = 0;
+    var prop: [*c]u8 = null;
+    if (c.XGetWindowProperty(wm.display, cl.window, wm.atoms.net_wm_window_type,
+        0, 1024, 0, c.XA_ATOM, &actual_type, &actual_format,
+        &nitems, &bytes_after, &prop) == c.Success and prop != null)
+    {
+        const atoms: [*]c.Atom = @ptrCast(@alignCast(prop));
+        var i: usize = 0;
+        while (i < nitems) : (i += 1) {
+            const a = atoms[i];
+            if (a == wm.atoms.net_wm_window_type_dialog or
+                a == wm.atoms.net_wm_window_type_utility or
+                a == wm.atoms.net_wm_window_type_toolbar or
+                a == wm.atoms.net_wm_window_type_splash or
+                a == wm.atoms.net_wm_window_type_menu or
+                a == wm.atoms.net_wm_window_type_dock)
+            {
+                cl.flags.floating = true;
+            }
+        }
+        _ = c.XFree(prop);
+    }
+}
+
+fn applyRules(cl: *Client) void {
+    for (&config.rules) |rule| {
+        if (rule.class) |rc| {
+            if (cl.class.len > 0 and std.mem.indexOf(u8, cl.class, rc) != null) {
+                cl.flags.floating = rule.floating;
+            }
+        }
+    }
+}
+
+pub fn applySizeHints(cl: *Client, w: i32, h: i32) struct { w: i32, h: i32 } {
+    var nw = w;
+    var nh = h;
+    if (cl.inc_w > 0 and cl.base_w >= 0)
+        nw = cl.base_w + cl.inc_w * @as(i32, @intFromFloat(@round(@as(f64, @floatFromInt(nw - cl.base_w)) / @as(f64, @floatFromInt(cl.inc_w)))));
+    if (cl.inc_h > 0 and cl.base_h >= 0)
+        nh = cl.base_h + cl.inc_h * @as(i32, @intFromFloat(@round(@as(f64, @floatFromInt(nh - cl.base_h)) / @as(f64, @floatFromInt(cl.inc_h)))));
+    if (cl.min_w > 0 and nw < cl.min_w) nw = cl.min_w;
+    if (cl.min_h > 0 and nh < cl.min_h) nh = cl.min_h;
+    if (cl.max_w > 0 and nw > cl.max_w) nw = cl.max_w;
+    if (cl.max_h > 0 and nh > cl.max_h) nh = cl.max_h;
+    if (cl.has_aspect) {
+        const aspect = @as(f64, @floatFromInt(nw)) / @as(f64, @floatFromInt(@max(nh, 1)));
+        if (aspect < cl.min_aspect)
+            nw = @intFromFloat(@as(f64, @floatFromInt(nh)) * cl.min_aspect);
+        if (aspect > cl.max_aspect)
+            nh = @intFromFloat(@as(f64, @floatFromInt(nw)) / cl.max_aspect);
+    }
+    if (nw < 1) nw = 1;
+    if (nh < 1) nh = 1;
+    return .{ .w = nw, .h = nh };
+}
+
+pub fn configure(wm: *WM, cl: *Client) void {
+    var ce: c.XConfigureEvent = undefined;
+    ce.type = c.ConfigureNotify;
+    ce.display = wm.display;
+    ce.event = cl.window;
+    ce.window = cl.window;
+    ce.x = cl.x;
+    ce.y = cl.y;
+    ce.width = @intCast(cl.w);
+    ce.height = @intCast(cl.h);
+    ce.border_width = @intCast(cl.bw);
+    ce.above = 0;
+    ce.override_redirect = 0;
+    _ = c.XSendEvent(wm.display, cl.window, 0, c.StructureNotifyMask, @ptrCast(&ce));
+}
+
+pub fn resize(wm: *WM, cl: *Client, x: i32, y: i32, w: u32, h: u32) void {
+    const s = applySizeHints(cl, @intCast(w), @intCast(h));
+    const sw: u32 = @intCast(@max(s.w, 1));
+    const sh: u32 = @intCast(@max(s.h, 1));
+    cl.x = x;
+    cl.y = y;
+    cl.w = sw;
+    cl.h = sh;
+    _ = c.XMoveResizeWindow(wm.display, cl.window, x, y, sw, sh);
+    configure(wm, cl);
+    updateFrameExtents(wm, cl);
+}
+
+pub fn setFullscreen(wm: *WM, cl: *Client, fullscreen: bool) void {
+    const was_fs = cl.flags.fullscreen;
+    cl.flags.fullscreen = fullscreen;
+
+    if (fullscreen and !was_fs) {
+        cl.saved = .{ .x = cl.x, .y = cl.y, .w = cl.w, .h = cl.h };
+        cl.bw = 0;
+        _ = c.XSetWindowBorderWidth(wm.display, cl.window, 0);
+        resize(wm, cl, 0, @intCast(config.bar_height), wm.sw, wm.sh - config.bar_height);
+        _ = c.XRaiseWindow(wm.display, cl.window);
+        setNetState(wm, cl);
+    } else if (!fullscreen and was_fs) {
+        cl.bw = config.border_width;
+        _ = c.XSetWindowBorderWidth(wm.display, cl.window, @intCast(config.border_width));
+        resize(wm, cl, cl.saved.x, cl.saved.y, cl.saved.w, cl.saved.h);
+        setNetState(wm, cl);
+    }
+}
+
+pub fn setMinimized(wm: *WM, cl: *Client, minimized: bool) void {
+    const was_min = cl.flags.minimized;
+    cl.flags.minimized = minimized;
+    if (minimized and !was_min) {
+        cl.unmap_count += 1;
+        _ = c.XUnmapWindow(wm.display, cl.window);
+        setWmState(wm, cl, 3);
+        setNetState(wm, cl);
+    } else if (!minimized and was_min) {
+        _ = c.XMapWindow(wm.display, cl.window);
+        setWmState(wm, cl, 1);
+        setNetState(wm, cl);
+    }
+}
+
+pub fn setNetState(wm: *WM, cl: *Client) void {
+    var atoms: [4]c.Atom = undefined;
+    var n: usize = 0;
+    if (cl.flags.fullscreen) {
+        atoms[n] = wm.atoms.net_wm_state_fullscreen;
+        n += 1;
+    }
+    if (cl.flags.minimized) {
+        atoms[n] = wm.atoms.net_wm_state_hidden;
+        n += 1;
+    }
+    if (cl.flags.urgent) {
+        atoms[n] = wm.atoms.net_wm_state_demands_attention;
+        n += 1;
+    }
+    if (n > 0) {
+        _ = c.XChangeProperty(wm.display, cl.window, wm.atoms.net_wm_state,
+            c.XA_ATOM, 32, c.PropModeReplace, @ptrCast(&atoms), @intCast(n));
+    } else {
+        _ = c.XDeleteProperty(wm.display, cl.window, wm.atoms.net_wm_state);
+    }
+}
+
+pub fn grabClientButtons(wm: *WM, win: c.Window, focused: bool) void {
+    _ = c.XUngrabButton(wm.display, c.AnyButton, c.AnyModifier, win);
+
+    _ = c.XGrabButton(wm.display, c.Button1, c.Mod1Mask, win, 0,
+        c.ButtonPressMask | c.ButtonReleaseMask | c.PointerMotionMask,
+        c.GrabModeAsync, c.GrabModeAsync, 0, 0);
+    _ = c.XGrabButton(wm.display, c.Button3, c.Mod1Mask, win, 0,
+        c.ButtonPressMask | c.ButtonReleaseMask | c.PointerMotionMask,
+        c.GrabModeAsync, c.GrabModeAsync, 0, 0);
+    _ = c.XGrabButton(wm.display, c.Button1, c.Mod1Mask | c.LockMask, win, 0,
+        c.ButtonPressMask | c.ButtonReleaseMask | c.PointerMotionMask,
+        c.GrabModeAsync, c.GrabModeAsync, 0, 0);
+    _ = c.XGrabButton(wm.display, c.Button3, c.Mod1Mask | c.LockMask, win, 0,
+        c.ButtonPressMask | c.ButtonReleaseMask | c.PointerMotionMask,
+        c.GrabModeAsync, c.GrabModeAsync, 0, 0);
+
+    if (!focused) {
+        _ = c.XGrabButton(wm.display, c.AnyButton, c.AnyModifier, win, 0,
+            c.ButtonPressMask, c.GrabModeSync, c.GrabModeSync, 0, 0);
+    }
+}
+
+pub fn focus(wm: *WM, cl: ?*Client) void {
+    if (wm.focused) |prev| {
+        setBorder(wm, prev, false);
+        grabClientButtons(wm, prev.window, false);
+    }
+    wm.focused = cl;
+    if (cl) |client| {
+        setBorder(wm, client, true);
+        grabClientButtons(wm, client.window, true);
+        _ = c.XRaiseWindow(wm.display, client.window);
+        if (!client.flags.never_focus) {
+            if (client.input_hint) {
+                _ = c.XSetInputFocus(wm.display, client.window, c.RevertToPointerRoot, c.CurrentTime);
+            }
+            if (client.supports_take_focus) {
+                sendProtocol(wm, client, wm.atoms.wm_take_focus);
+            }
+        }
+        _ = c.XChangeProperty(wm.display, wm.root, wm.atoms.net_active_window,
+            c.XA_WINDOW, 32, c.PropModeReplace, @ptrCast(&client.window), 1);
+    } else {
+        _ = c.XSetInputFocus(wm.display, wm.root, c.RevertToPointerRoot, c.CurrentTime);
+        _ = c.XDeleteProperty(wm.display, wm.root, wm.atoms.net_active_window);
+    }
+}
+
+fn setBorder(wm: *WM, cl: *Client, focused: bool) void {
+    const color = if (focused)
+        wm.bar.colors.border_focused.pixel
+    else
+        wm.bar.colors.border_unfocused.pixel;
+    _ = c.XSetWindowBorder(wm.display, cl.window, color);
+}
+
+pub fn focusNext(wm: *WM) void {
+    const start = wm.focused orelse wm.clients orelse return;
+    var it = start;
+    while (true) {
+        it = it.next orelse wm.clients orelse return;
+        if (!it.flags.minimized) {
+            focus(wm, it);
+            _ = c.XRaiseWindow(wm.display, it.window);
+            return;
+        }
+        if (it == start) break;
+    }
+}
+
+pub fn focusVisible(wm: *WM) void {
+    var it = wm.clients;
+    while (it) |cl| : (it = cl.next) {
+        if (!cl.flags.minimized) {
+            focus(wm, cl);
+            return;
+        }
+    }
+    focus(wm, null);
+}
+
+pub fn raise(wm: *WM, cl: *Client) void {
+    _ = c.XRaiseWindow(wm.display, cl.window);
+    listRemove(&wm.clients, cl);
+    listAppend(&wm.clients, cl);
+    updateClientList(wm);
+}
+
+pub fn updateClientList(wm: *WM) void {
+    const n = listCount(wm.clients);
+    if (n == 0) {
+        _ = c.XDeleteProperty(wm.display, wm.root, wm.atoms.net_client_list);
+        _ = c.XDeleteProperty(wm.display, wm.root, wm.atoms.net_client_list_stacking);
+        return;
+    }
+    var buf: [256]c.Window = undefined;
+    var i: usize = 0;
+    var it = wm.clients;
+    while (it) |cl| : (it = cl.next) {
+        if (i < buf.len) {
+            buf[i] = cl.window;
+            i += 1;
+        }
+    }
+    _ = c.XChangeProperty(wm.display, wm.root, wm.atoms.net_client_list,
+        c.XA_WINDOW, 32, c.PropModeReplace, @ptrCast(&buf), @intCast(i));
+
+    i = 0;
+    it = wm.clients;
+    while (it) |cl| : (it = cl.next) {
+        if (i < buf.len) {
+            buf[i] = cl.window;
+            i += 1;
+        }
+    }
+    _ = c.XChangeProperty(wm.display, wm.root, wm.atoms.net_client_list_stacking,
+        c.XA_WINDOW, 32, c.PropModeReplace, @ptrCast(&buf), @intCast(i));
+}
+
+pub fn updateFrameExtents(wm: *WM, cl: *Client) void {
+    const data = [4]c_long{
+        @intCast(cl.bw),
+        @intCast(cl.bw),
+        @intCast(cl.bw),
+        @intCast(cl.bw),
+    };
+    _ = c.XChangeProperty(wm.display, cl.window, wm.atoms.net_frame_extents,
+        c.XA_CARDINAL, 32, c.PropModeReplace, @ptrCast(&data), 4);
+}
+
+pub fn manage(wm: *WM, window: c.Window) !void {
+    if (listFind(wm.clients, window) != null) return;
+    if (window == wm.bar.window) return;
+
+    var wa: c.XWindowAttributes = undefined;
+    if (c.XGetWindowAttributes(wm.display, window, &wa) == 0) return;
+    if (wa.override_redirect != 0) return;
+
+    const cl = try wm.allocator.create(Client);
+    errdefer wm.allocator.destroy(cl);
+    cl.* = .{
+        .window = window,
+        .x = wa.x,
+        .y = wa.y,
+        .w = @intCast(wa.width),
+        .h = @intCast(wa.height),
+        .bw = config.border_width,
+    };
+
+    readHints(wm, cl);
+    readTitle(wm, cl);
+    readClass(wm, cl);
+    readProtocols(wm, cl);
+    checkWindowType(wm, cl);
+    applyRules(cl);
+
+    if (!cl.flags.floating) {
+        cl.y = @max(cl.y, @as(i32, @intCast(config.bar_height)));
+    }
+
+    listAppend(&wm.clients, cl);
+    setWmState(wm, cl, 1);
+
+    _ = c.XSelectInput(wm.display, window,
+        c.StructureNotifyMask | c.PropertyChangeMask |
+        c.EnterWindowMask | c.FocusChangeMask);
+
+    _ = c.XSetWindowBorderWidth(wm.display, window, @intCast(config.border_width));
+    _ = c.XMoveResizeWindow(wm.display, window, cl.x, cl.y, cl.w, cl.h);
+    _ = c.XMapWindow(wm.display, window);
+
+    grabClientButtons(wm, window, false);
+    updateFrameExtents(wm, cl);
+    focus(wm, cl);
+    updateClientList(wm);
+}
+
+pub fn unmanage(wm: *WM, cl: *Client) void {
+    if (wm.focused == cl) {
+        const next = cl.next orelse cl.prev;
+        focus(wm, next);
+    }
+    listRemove(&wm.clients, cl);
+    setWmState(wm, cl, 0);
+
+    if (cl.title.len > 0) wm.allocator.free(cl.title);
+    if (cl.class.len > 0) wm.allocator.free(cl.class);
+    wm.allocator.destroy(cl);
+
+    updateClientList(wm);
+}
+
+pub fn findClientByBarClick(wm: *WM, x: i32) ?*Client {
+    var i: usize = 0;
+    while (i < wm.bar.button_count) : (i += 1) {
+        const btn = wm.bar.buttons[i];
+        if (x >= btn.x and x < btn.x + @as(i32, @intCast(btn.w))) {
+            return listFind(wm.clients, btn.win);
+        }
+    }
+    return null;
+}
diff --git a/src/config.zig b/src/config.zig
new file mode 100644
index 0000000..2f46a37
--- /dev/null
+++ b/src/config.zig
@@ -0,0 +1,56 @@
+const c = @import("c.zig").c;
+
+pub const bar_height: u32 = 30;
+pub const border_width: u32 = 2;
+pub const snap_distance: i32 = 10;
+pub const focus_follows_mouse: bool = false;
+
+pub const font_names = [_][:0]const u8{"monospace:size=14"};
+
+pub const col = struct {
+    pub const border_focused = "#005f5f";
+    pub const border_unfocused = "#444444";
+    pub const bar_bg = "#222222";
+    pub const bar_fg = "#eeeeee";
+    pub const bar_sel_bg = "#555555";
+    pub const bar_min_fg = "#888888";
+    pub const bar_urg_fg = "#ff0000";
+};
+
+pub const Action = union(enum) {
+    close_focused,
+    toggle_fullscreen,
+    focus_next,
+    minimize,
+    spawn: []const [:0]const u8,
+    quit,
+};
+
+pub const Keybind = struct {
+    mod: c_uint,
+    keysym: c.KeySym,
+    action: Action,
+};
+
+pub const keybinds = [_]Keybind{
+    .{ .mod = c.Mod1Mask | c.ShiftMask, .keysym = c.XK_q, .action = .quit },
+    .{ .mod = c.Mod1Mask | c.ShiftMask, .keysym = c.XK_c, .action = .close_focused },
+    .{ .mod = c.Mod1Mask, .keysym = c.XK_f, .action = .toggle_fullscreen },
+    .{ .mod = c.Mod1Mask, .keysym = c.XK_Tab, .action = .focus_next },
+    .{ .mod = c.Mod1Mask, .keysym = c.XK_m, .action = .minimize },
+    .{ .mod = c.Mod1Mask, .keysym = c.XK_Return, .action = .{ .spawn = &.{"st"} } },
+    .{ .mod = c.Mod1Mask, .keysym = c.XK_p, .action = .{ .spawn = &.{"/home/smi/.scripts/run.sh"} } },
+};
+
+pub const autostart = [_][]const [:0]const u8{
+    &.{ "sh", "/home/smi/.scripts/2zw.sh" },
+};
+
+pub const Rule = struct {
+    class: ?[:0]const u8,
+    floating: bool,
+};
+
+pub const rules = [_]Rule{
+    .{ .class = "float", .floating = true },
+};
diff --git a/src/event.zig b/src/event.zig
new file mode 100644
index 0000000..32dfd51
--- /dev/null
+++ b/src/event.zig
@@ -0,0 +1,350 @@
+const std = @import("std");
+const c = @import("c.zig").c;
+const config = @import("config.zig");
+const client_mod = @import("client.zig");
+const WM = client_mod.WM;
+const Client = client_mod.Client;
+const bar_mod = @import("bar.zig");
+const util = @import("util.zig");
+
+pub fn dispatch(wm: *WM, ev: *c.XEvent) void {
+    switch (ev.type) {
+        c.MapRequest => mapRequest(wm, &ev.xmaprequest),
+        c.UnmapNotify => unmapNotify(wm, &ev.xunmap),
+        c.DestroyNotify => destroyNotify(wm, &ev.xdestroywindow),
+        c.ConfigureRequest => configureRequest(wm, &ev.xconfigurerequest),
+        c.ClientMessage => clientMessage(wm, &ev.xclient),
+        c.PropertyNotify => propertyNotify(wm, &ev.xproperty),
+        c.ButtonPress => buttonPress(wm, &ev.xbutton),
+        c.MotionNotify => motionNotify(wm, &ev.xmotion),
+        c.ButtonRelease => buttonRelease(wm, &ev.xbutton),
+        c.KeyPress => keyPress(wm, &ev.xkey),
+        c.Expose => expose(wm, &ev.xexpose),
+        c.FocusIn => focusIn(wm, &ev.xfocus),
+        c.EnterNotify => enterNotify(wm, &ev.xcrossing),
+        c.MappingNotify => mappingNotify(wm, &ev.xmapping),
+        c.SelectionClear => selectionClear(wm),
+        else => {},
+    }
+}
+
+fn mapRequest(wm: *WM, ev: *c.XMapRequestEvent) void {
+    client_mod.manage(wm, ev.window) catch |err| {
+        util.log("manage: {}\n", .{err});
+    };
+    bar_mod.draw(wm);
+}
+
+fn unmapNotify(wm: *WM, ev: *c.XUnmapEvent) void {
+    if (ev.event != wm.root) return;
+    const cl = client_mod.listFind(wm.clients, ev.window) orelse return;
+    if (cl.unmap_count > 0) {
+        cl.unmap_count -= 1;
+        return;
+    }
+    client_mod.unmanage(wm, cl);
+    bar_mod.draw(wm);
+}
+
+fn destroyNotify(wm: *WM, ev: *c.XDestroyWindowEvent) void {
+    const cl = client_mod.listFind(wm.clients, ev.window) orelse return;
+    client_mod.unmanage(wm, cl);
+    bar_mod.draw(wm);
+}
+
+fn configureRequest(wm: *WM, ev: *c.XConfigureRequestEvent) void {
+    const cl = client_mod.listFind(wm.clients, ev.window);
+    if (cl) |client| {
+        if (client.flags.fullscreen) {
+            client_mod.configure(wm, client);
+            return;
+        }
+        if (ev.value_mask & c.CWX != 0) client.x = ev.x;
+        if (ev.value_mask & c.CWY != 0) client.y = ev.y;
+        if (ev.value_mask & c.CWWidth != 0) client.w = @intCast(ev.width);
+        if (ev.value_mask & c.CWHeight != 0) client.h = @intCast(ev.height);
+        if (ev.value_mask & c.CWBorderWidth != 0)
+            client.bw = @intCast(ev.border_width);
+        _ = c.XMoveResizeWindow(wm.display, client.window,
+            client.x, client.y, client.w, client.h);
+        client_mod.configure(wm, client);
+        client_mod.updateFrameExtents(wm, client);
+    } else {
+        var changes: c.XWindowChanges = undefined;
+        changes.x = ev.x;
+        changes.y = ev.y;
+        changes.width = ev.width;
+        changes.height = ev.height;
+        changes.border_width = ev.border_width;
+        changes.sibling = ev.above;
+        changes.stack_mode = ev.detail;
+        _ = c.XConfigureWindow(wm.display, ev.window,
+            @intCast(ev.value_mask), &changes);
+    }
+}
+
+fn clientMessage(wm: *WM, ev: *c.XClientMessageEvent) void {
+    const cl = client_mod.listFind(wm.clients, ev.window) orelse return;
+    const a = wm.atoms;
+
+    if (ev.message_type == a.net_wm_state) {
+        const action = ev.data.l[0];
+        const p1: c.Atom = @intCast(ev.data.l[1]);
+        const p2: c.Atom = @intCast(ev.data.l[2]);
+        const toggle = action == 2;
+        const add = action == 1;
+        const remove = action == 0;
+
+        if (p1 == a.net_wm_state_fullscreen or p2 == a.net_wm_state_fullscreen) {
+            const want = if (toggle) !cl.flags.fullscreen else add;
+            client_mod.setFullscreen(wm, cl, want);
+        }
+        if (p1 == a.net_wm_state_hidden or p2 == a.net_wm_state_hidden) {
+            if (add or (toggle and !cl.flags.minimized)) {
+                client_mod.setMinimized(wm, cl, true);
+                if (wm.focused == cl) client_mod.focusVisible(wm);
+            } else if (remove or (toggle and cl.flags.minimized)) {
+                client_mod.setMinimized(wm, cl, false);
+                client_mod.focus(wm, cl);
+            }
+        }
+        if (p1 == a.net_wm_state_demands_attention or p2 == a.net_wm_state_demands_attention) {
+            if (remove or toggle) {
+                cl.flags.urgent = if (toggle) !cl.flags.urgent else false;
+            } else if (add) {
+                cl.flags.urgent = true;
+            }
+            client_mod.setNetState(wm, cl);
+        }
+        if (p1 == a.net_wm_state_above or p2 == a.net_wm_state_above) {
+            if (add) client_mod.raise(wm, cl);
+        }
+        bar_mod.draw(wm);
+    } else if (ev.message_type == a.net_active_window) {
+        if (cl.flags.minimized)
+            client_mod.setMinimized(wm, cl, false);
+        client_mod.focus(wm, cl);
+        client_mod.raise(wm, cl);
+        bar_mod.draw(wm);
+    } else if (ev.message_type == a.net_close_window) {
+        client_mod.closeClient(wm, cl);
+    }
+}
+
+fn propertyNotify(wm: *WM, ev: *c.XPropertyEvent) void {
+    if (ev.window == wm.root) {
+        if (ev.atom == wm.atoms.net_wm_name or ev.atom == c.XA_WM_NAME) {
+            bar_mod.draw(wm);
+        }
+        return;
+    }
+    const cl = client_mod.listFind(wm.clients, ev.window) orelse return;
+    if (ev.state == c.PropertyDelete) return;
+
+    if (ev.atom == c.XA_WM_NAME or ev.atom == wm.atoms.net_wm_name) {
+        client_mod.readTitle(wm, cl);
+        bar_mod.draw(wm);
+    } else if (ev.atom == c.XA_WM_CLASS) {
+        client_mod.readClass(wm, cl);
+    } else if (ev.atom == c.XA_WM_HINTS) {
+        const wm_hints: [*c]c.XWMHints = c.XGetWMHints(wm.display, cl.window);
+        if (wm_hints != null) {
+            const old_urgent = cl.flags.urgent;
+            cl.flags.urgent = (wm_hints.*.flags & c.XUrgencyHint) != 0;
+            if (wm_hints.*.flags & c.InputHint != 0)
+                cl.input_hint = wm_hints.*.input != 0;
+            cl.flags.never_focus = !cl.input_hint and !cl.supports_take_focus;
+            client_mod.setNetState(wm, cl);
+            if (old_urgent != cl.flags.urgent)
+                bar_mod.draw(wm);
+            _ = c.XFree(wm_hints);
+        }
+    }
+}
+
+fn buttonPress(wm: *WM, ev: *c.XButtonEvent) void {
+    if (ev.window == wm.bar.window) {
+        barClick(wm, ev);
+        return;
+    }
+
+    const cl = client_mod.listFind(wm.clients, ev.window) orelse return;
+
+    if (ev.state & c.Mod1Mask != 0 and !cl.flags.fullscreen) {
+        if (wm.focused != cl) {
+            client_mod.focus(wm, cl);
+            client_mod.raise(wm, cl);
+            bar_mod.draw(wm);
+        }
+        const resizing = ev.button == 3;
+        wm.grab = .{
+            .client = cl,
+            .orig_x = cl.x,
+            .orig_y = cl.y,
+            .orig_w = cl.w,
+            .orig_h = cl.h,
+            .start_x = ev.x_root,
+            .start_y = ev.y_root,
+            .resizing = resizing,
+        };
+        _ = c.XGrabPointer(wm.display, wm.root, 0,
+            c.ButtonPressMask | c.ButtonReleaseMask | c.PointerMotionMask,
+            c.GrabModeAsync, c.GrabModeAsync, 0, 0, c.CurrentTime);
+        return;
+    }
+
+    if (wm.focused != cl) {
+        client_mod.focus(wm, cl);
+        client_mod.raise(wm, cl);
+        bar_mod.draw(wm);
+    }
+    _ = c.XAllowEvents(wm.display, c.ReplayPointer, c.CurrentTime);
+}
+
+fn barClick(wm: *WM, ev: *c.XButtonEvent) void {
+    const cl = client_mod.findClientByBarClick(wm, ev.x) orelse return;
+    switch (ev.button) {
+        c.Button1 => {
+            if (cl.flags.minimized)
+                client_mod.setMinimized(wm, cl, false);
+            client_mod.focus(wm, cl);
+            client_mod.raise(wm, cl);
+            bar_mod.draw(wm);
+        },
+        c.Button2 => {
+            client_mod.closeClient(wm, cl);
+        },
+        c.Button3 => {
+            client_mod.setMinimized(wm, cl, !cl.flags.minimized);
+            if (cl.flags.minimized and wm.focused == cl) {
+                client_mod.focusVisible(wm);
+            }
+            bar_mod.draw(wm);
+        },
+        else => {},
+    }
+}
+
+fn motionNotify(wm: *WM, ev: *c.XMotionEvent) void {
+    const grab = wm.grab orelse return;
+    const cl = grab.client;
+
+    var dummy: c.XEvent = undefined;
+    while (c.XCheckMaskEvent(wm.display, c.PointerMotionMask, &dummy) != 0) {
+        ev.x_root = dummy.xmotion.x_root;
+        ev.y_root = dummy.xmotion.y_root;
+    }
+
+    const dx = ev.x_root - grab.start_x;
+    const dy = ev.y_root - grab.start_y;
+
+    if (grab.resizing) {
+        var nw: i32 = @as(i32, @intCast(grab.orig_w)) + dx;
+        var nh: i32 = @as(i32, @intCast(grab.orig_h)) + dy;
+        if (nw < 1) nw = 1;
+        if (nh < 1) nh = 1;
+        const s = client_mod.applySizeHints(cl, nw, nh);
+        nw = @max(s.w, 1);
+        nh = @max(s.h, 1);
+        client_mod.resize(wm, cl, cl.x, cl.y, @intCast(nw), @intCast(nh));
+    } else {
+        var nx = grab.orig_x + dx;
+        var ny = grab.orig_y + dy;
+        nx = applySnap(nx, @intCast(cl.w), @intCast(wm.sw));
+        ny = applySnap(ny, @intCast(cl.h), @intCast(wm.sh));
+        cl.x = nx;
+        cl.y = ny;
+        _ = c.XMoveWindow(wm.display, cl.window, nx, ny);
+        client_mod.configure(wm, cl);
+    }
+}
+
+fn applySnap(pos: i32, size: i32, limit: i32) i32 {
+    if (@abs(pos) < config.snap_distance) return 0;
+    if (@abs(pos + size - limit) < config.snap_distance) return limit - size;
+    return pos;
+}
+
+fn buttonRelease(wm: *WM, ev: *c.XButtonEvent) void {
+    _ = ev;
+    if (wm.grab != null) {
+        wm.grab = null;
+        _ = c.XUngrabPointer(wm.display, c.CurrentTime);
+    }
+}
+
+fn keyPress(wm: *WM, ev: *c.XKeyEvent) void {
+    const keysym = c.XkbKeycodeToKeysym(wm.display, @intCast(ev.keycode), 0, 0);
+    for (&config.keybinds) |kb| {
+        if (kb.keysym != keysym) continue;
+        if (kb.mod != (ev.state & ~@as(c_uint, c.LockMask))) continue;
+
+        switch (kb.action) {
+            .quit => wm.running = false,
+            .close_focused => {
+                if (wm.focused) |cl| client_mod.closeClient(wm, cl);
+            },
+            .toggle_fullscreen => {
+                if (wm.focused) |cl| {
+                    client_mod.setFullscreen(wm, cl, !cl.flags.fullscreen);
+                    bar_mod.draw(wm);
+                }
+            },
+            .focus_next => client_mod.focusNext(wm),
+            .minimize => {
+                if (wm.focused) |cl| {
+                    client_mod.setMinimized(wm, cl, true);
+                    client_mod.focusVisible(wm);
+                    bar_mod.draw(wm);
+                }
+            },
+            .spawn => |cmd| util.spawn(wm.allocator, cmd),
+        }
+        return;
+    }
+}
+
+fn expose(wm: *WM, ev: *c.XExposeEvent) void {
+    if (ev.window == wm.bar.window and ev.count == 0)
+        bar_mod.draw(wm);
+}
+
+fn focusIn(wm: *WM, ev: *c.XFocusChangeEvent) void {
+    if (ev.detail == c.NotifyInferior or ev.detail == c.NotifyPointer) return;
+    if (ev.window == wm.root) return;
+    const cl = client_mod.listFind(wm.clients, ev.window) orelse return;
+    if (wm.focused != cl) {
+        client_mod.focus(wm, cl);
+        bar_mod.draw(wm);
+    }
+}
+
+fn enterNotify(wm: *WM, ev: *c.XCrossingEvent) void {
+    if (ev.detail == c.NotifyInferior or ev.mode == c.NotifyGrab or ev.mode == c.NotifyUngrab) return;
+    if (!config.focus_follows_mouse) return;
+    const cl = client_mod.listFind(wm.clients, ev.window) orelse return;
+    if (cl.flags.never_focus) return;
+    client_mod.focus(wm, cl);
+    bar_mod.draw(wm);
+}
+
+fn mappingNotify(wm: *WM, ev: *c.XMappingEvent) void {
+    _ = c.XRefreshKeyboardMapping(ev);
+    if (ev.request == c.MappingKeyboard)
+        grabKeys(wm);
+}
+
+fn selectionClear(wm: *WM) void {
+    wm.running = false;
+}
+
+pub fn grabKeys(wm: *WM) void {
+    _ = c.XUngrabKey(wm.display, c.AnyKey, c.AnyModifier, wm.root);
+    for (&config.keybinds) |kb| {
+        const code = c.XKeysymToKeycode(wm.display, kb.keysym);
+        _ = c.XGrabKey(wm.display, code, kb.mod, wm.root, 1, c.GrabModeAsync, c.GrabModeAsync);
+        _ = c.XGrabKey(wm.display, code, kb.mod | c.LockMask, wm.root, 1, c.GrabModeAsync, c.GrabModeAsync);
+        _ = c.XGrabKey(wm.display, code, kb.mod | c.Mod2Mask, wm.root, 1, c.GrabModeAsync, c.GrabModeAsync);
+        _ = c.XGrabKey(wm.display, code, kb.mod | c.LockMask | c.Mod2Mask, wm.root, 1, c.GrabModeAsync, c.GrabModeAsync);
+    }
+}
diff --git a/src/main.zig b/src/main.zig
new file mode 100644
index 0000000..f2f22d1
--- /dev/null
+++ b/src/main.zig
@@ -0,0 +1,205 @@
+const std = @import("std");
+const c = @import("c.zig").c;
+const config = @import("config.zig");
+const util = @import("util.zig");
+const atoms_mod = @import("atoms.zig");
+const client_mod = @import("client.zig");
+const WM = client_mod.WM;
+const bar_mod = @import("bar.zig");
+const event_mod = @import("event.zig");
+
+var wm_global: ?*WM = null;
+
+fn xerrorStrict(dpy: ?*c.Display, ev: [*c]c.XErrorEvent) callconv(.c) c_int {
+    _ = dpy;
+    _ = ev;
+    util.die("wm: another window manager is already running\n", .{});
+    return 0;
+}
+
+fn xerrorNormal(dpy: ?*c.Display, ev: [*c]c.XErrorEvent) callconv(.c) c_int {
+    const ec = ev.*.error_code;
+    if (ec == c.BadWindow or ec == c.BadMatch or ec == c.BadDrawable)
+        return 0;
+    var buf: [128]u8 = undefined;
+    _ = c.XGetErrorText(dpy, ec, &buf, buf.len);
+    util.log("wm: xerror: {s} ({d})\n", .{ std.mem.sliceTo(&buf, 0), ec });
+    return 0;
+}
+
+fn sigTerm(sig: c_int) callconv(.c) void {
+    _ = sig;
+    if (wm_global) |wm| wm.running = false;
+}
+
+fn sigChld(sig: c_int) callconv(.c) void {
+    _ = sig;
+    while (true) {
+        var status: c_int = 0;
+        const pid = std.c.waitpid(-1, &status, 1);
+        if (pid <= 0) break;
+    }
+}
+
+pub fn main() !void {
+    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+    defer _ = gpa.deinit();
+    const alloc = gpa.allocator();
+
+    var args = std.process.args();
+    _ = args.next();
+    while (args.next()) |arg| {
+        if (std.mem.eql(u8, arg, "-v")) {
+            util.log("wm 0.1.0\n", .{});
+            std.process.exit(0);
+        } else {
+            util.die("usage: wm [-v]\n", .{});
+        }
+    }
+
+    if (c.XOpenDisplay(null)) |dpy| {
+        var wm = WM{
+            .display = dpy,
+            .screen = c.DefaultScreen(dpy),
+            .root = c.RootWindow(dpy, c.DefaultScreen(dpy)),
+            .sw = @intCast(c.DisplayWidth(dpy, c.DefaultScreen(dpy))),
+            .sh = @intCast(c.DisplayHeight(dpy, c.DefaultScreen(dpy))),
+            .atoms = undefined,
+            .bar = undefined,
+            .clients = null,
+            .focused = null,
+            .running = true,
+            .allocator = alloc,
+            .support_win = 0,
+            .grab = null,
+            .visual = c.DefaultVisual(dpy, c.DefaultScreen(dpy)),
+            .colormap = c.DefaultColormap(dpy, c.DefaultScreen(dpy)),
+        };
+        wm_global = &wm;
+
+        _ = c.XSetErrorHandler(xerrorStrict);
+        _ = c.XSelectInput(dpy, wm.root,
+            c.SubstructureRedirectMask | c.SubstructureNotifyMask |
+            c.ButtonPressMask | c.StructureNotifyMask |
+            c.PropertyChangeMask | c.EnterWindowMask |
+            c.FocusChangeMask | c.KeymapStateMask);
+        _ = c.XSync(dpy, 0);
+        _ = c.XSetErrorHandler(xerrorNormal);
+
+        const cursor = c.XCreateFontCursor(dpy, 68);
+        _ = c.XDefineCursor(dpy, wm.root, cursor);
+
+        wm.atoms = atoms_mod.Atoms.init(dpy);
+
+        var wm_sn_buf: [32]u8 = undefined;
+        const wm_sn_name = std.fmt.bufPrintZ(&wm_sn_buf, "WM_S{d}", .{wm.screen}) catch "WM_S0";
+        const wm_sn = c.XInternAtom(dpy, wm_sn_name, 0);
+
+        wm.support_win = c.XCreateSimpleWindow(dpy, wm.root, -1, -1, 1, 1, 0, 0, 0);
+        _ = c.XChangeProperty(dpy, wm.support_win, wm.atoms.net_supporting_wm_check,
+            c.XA_WINDOW, 32, c.PropModeReplace,
+            @ptrCast(&wm.support_win), 1);
+        _ = c.XChangeProperty(dpy, wm.root, wm.atoms.net_supporting_wm_check,
+            c.XA_WINDOW, 32, c.PropModeReplace,
+            @ptrCast(&wm.support_win), 1);
+        _ = c.XSetSelectionOwner(dpy, wm_sn, wm.support_win, c.CurrentTime);
+
+        const pid_val: c_long = @intCast(std.os.linux.getpid());
+        _ = c.XChangeProperty(dpy, wm.support_win, wm.atoms.net_wm_pid,
+            c.XA_CARDINAL, 32, c.PropModeReplace,
+            @ptrCast(@constCast(&pid_val)), 1);
+
+        const supported = wm.atoms.supportedList();
+        _ = c.XChangeProperty(dpy, wm.root, wm.atoms.net_supported,
+            c.XA_ATOM, 32, c.PropModeReplace,
+            @ptrCast(@constCast(supported.ptr)), @intCast(supported.len));
+
+        var btn_init: [128]client_mod.ButtonEntry = undefined;
+        for (&btn_init) |*b| b.* = .{ .x = 0, .w = 0, .win = 0 };
+        wm.bar = .{
+            .window = 0,
+            .pixmap = 0,
+            .draw = null,
+            .fonts = .{null} ** config.font_names.len,
+            .colors = undefined,
+            .buttons = btn_init,
+            .button_count = 0,
+        };
+        bar_mod.create(&wm) catch |err| util.die("bar init: {}\n", .{err});
+
+        installSignals();
+
+        _ = c.XGrabServer(dpy);
+        scanWindows(&wm);
+        _ = c.XUngrabServer(dpy);
+
+        event_mod.grabKeys(&wm);
+        bar_mod.draw(&wm);
+
+        for (&config.autostart) |cmd| {
+            util.spawn(alloc, cmd);
+        }
+
+        var ev: c.XEvent = undefined;
+        while (wm.running) {
+            _ = c.XNextEvent(dpy, &ev);
+            if (wm.running)
+                event_mod.dispatch(&wm, &ev);
+        }
+
+        cleanup(&wm);
+    } else {
+        util.die("wm: cannot open display\n", .{});
+    }
+}
+
+fn installSignals() void {
+    const act = std.posix.Sigaction{
+        .handler = .{ .handler = sigTerm },
+        .mask = std.posix.empty_sigset,
+        .flags = 0,
+    };
+    std.posix.sigaction(std.posix.SIG.TERM, &act, null);
+    std.posix.sigaction(std.posix.SIG.INT, &act, null);
+
+    const chld_act = std.posix.Sigaction{
+        .handler = .{ .handler = sigChld },
+        .mask = std.posix.empty_sigset,
+        .flags = 0,
+    };
+    std.posix.sigaction(std.posix.SIG.CHLD, &chld_act, null);
+}
+
+fn scanWindows(wm: *WM) void {
+    var root_ret: c.Window = 0;
+    var parent_ret: c.Window = 0;
+    var children: [*c]c.Window = null;
+    var nchildren: c_uint = 0;
+    if (c.XQueryTree(wm.display, wm.root, &root_ret, &parent_ret, &children, &nchildren) != 0) {
+        if (children != null) {
+            var i: c_uint = 0;
+            while (i < nchildren) : (i += 1) {
+                var wa: c.XWindowAttributes = undefined;
+                if (c.XGetWindowAttributes(wm.display, children[@intCast(i)], &wa) != 0) {
+                    if (wa.override_redirect == 0 and wa.map_state == c.IsViewable) {
+                        client_mod.manage(wm, children[@intCast(i)]) catch {};
+                    }
+                }
+            }
+            _ = c.XFree(children);
+        }
+    }
+}
+
+fn cleanup(wm: *WM) void {
+    var it = wm.clients;
+    while (it) |cl| {
+        const next = cl.next;
+        client_mod.unmanage(wm, cl);
+        it = next;
+    }
+    bar_mod.destroy(wm);
+    if (wm.support_win != 0)
+        _ = c.XDestroyWindow(wm.display, wm.support_win);
+    _ = c.XCloseDisplay(wm.display);
+}
diff --git a/src/util.zig b/src/util.zig
new file mode 100644
index 0000000..25aafb2
--- /dev/null
+++ b/src/util.zig
@@ -0,0 +1,21 @@
+const std = @import("std");
+
+pub fn die(comptime fmt: []const u8, args: anytype) noreturn {
+    const stderr = std.io.getStdErr().writer();
+    stderr.print(fmt, args) catch {};
+    std.process.exit(1);
+}
+
+pub fn log(comptime fmt: []const u8, args: anytype) void {
+    const stderr = std.io.getStdErr().writer();
+    stderr.print(fmt, args) catch {};
+}
+
+pub fn spawn(alloc: std.mem.Allocator, cmd: []const [:0]const u8) void {
+    if (cmd.len == 0) return;
+    var child = std.process.Child.init(cmd, alloc);
+    child.stdin_behavior = .Ignore;
+    child.stdout_behavior = .Ignore;
+    child.stderr_behavior = .Ignore;
+    _ = child.spawn() catch {};
+}
diff --git a/zig-out/bin/wm b/zig-out/bin/wm
new file mode 100755
index 0000000..334f96e
Binary files /dev/null and b/zig-out/bin/wm differ