2zw

Owner: IIIlllIIIllI URL: git@git.0x00nyx.xyz:seb/2zw.git

init commit, new repo

Commit ab24debf4d365e998e68b7dd6b1cd71f70524cfa by IIIlllIIIllI <seb.michalk@gmail.com> on 2025-12-20 12:02:13 +0100
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f52e419
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+zig-cache/
+zig-out/
+TAGS
+.dirlocals
diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..4c691e5
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,61 @@
+2zw
+===
+2zw is a fast, lean window manager for X written in Zig.
+
+It follows loosely the 2wm design from suckless.org.
+
+The codebase now targets portable POSIX APIs so it builds cleanly on both Linux and OpenBSD.
+
+Features
+--------
+- Small hackable codebase
+- Master/stack tiling
+- Attach/detach instead of workspaces
+- No configuration file parsing, configured via source
+- Floating window support
+- Focus border colouring
+- Bar (bat, date/time)
+- Gaps
+
+Requirements
+------------
+In order to build 2zw you need:
+- Zig compiler (0.13.0 or newer, tested with 0.14.0)
+- Xlib header files
+- libX11-dev
+- libXrandr-dev
+  - On OpenBSD these live under `/usr/X11R6`; the build script adds the paths automatically.
+
+Installation
+------------
+Edit `build.zig` to match your local setup if you want a different prefix
+(2zw installs into `/usr/local` namespace by default). No manual edits are required
+for OpenBSD include/library search paths.
+
+Afterwards enter the following command to build and install 2zw:
+
+   zig build install
+
+Running 2zw
+-----------
+Add the following line to your .xinitrc to start 2zw using startx:
+
+   exec 2zw
+
+In order to connect 2zw to a specific display, make sure that
+the DISPLAY environment variable is set correctly, e.g.:
+
+   DISPLAY=foo.bar:1 exec 2zw
+
+Configuration
+-------------
+Everything lives in `src/main.zig`; edit constants and rebuild.
+
+Defaults worth noting:
+- `FOCUS_BORDER_COLOR = 0xffd787`
+- `NORMAL_BORDER_COLOR = 0x333333`
+- `BORDER_WIDTH = 2`
+- `terminal = "st"`
+- `launcher = "dmenu_run"`
+
+Key bindings (Mod4): q kill, a attach, d detach, , prev, . next, Return terminal, p launcher, s slock.
diff --git a/build.zig b/build.zig
new file mode 100644
index 0000000..c387b74
--- /dev/null
+++ b/build.zig
@@ -0,0 +1,54 @@
+const std = @import("std");
+
+pub fn build(b: *std.Build) void {
+    const target = b.standardTargetOptions(.{});
+    const optimize = b.standardOptimizeOption(.{});
+    const exe = b.addExecutable(.{
+        .name = "2zw",
+        .root_source_file = b.path("src/main.zig"),
+        .target = target,
+        .optimize = optimize,
+    });
+
+    exe.linkLibC();
+    exe.linkSystemLibrary("X11");
+    exe.linkSystemLibrary("Xrandr");
+    exe.linkSystemLibrary("Xft");
+
+    if (target.result.os.tag == .openbsd) {
+        exe.addSystemIncludePath(lazyAbsolutePath("/usr/X11R6/include"));
+        exe.addLibraryPath(lazyAbsolutePath("/usr/X11R6/lib"));
+    }
+    b.installArtifact(exe);
+
+    const run_cmd = b.addRunArtifact(exe);
+
+    run_cmd.step.dependOn(b.getInstallStep());
+
+    if (b.args) |args| {
+        run_cmd.addArgs(args);
+    }
+
+    const run_step = b.step("run", "Run the app");
+    run_step.dependOn(&run_cmd.step);
+
+    const unit_tests = b.addTest(.{
+        .root_source_file = b.path("src/main.zig"),
+        .target = target,
+        .optimize = optimize,
+    });
+    const run_unit_tests = b.addRunArtifact(unit_tests);
+
+    const test_step = b.step("test", "Run unit tests");
+    test_step.dependOn(&run_unit_tests.step);
+}
+
+fn lazyAbsolutePath(path: []const u8) std.Build.LazyPath {
+    if (@hasField(std.Build.LazyPath, "path")) {
+        return .{ .path = path };
+    } else if (@hasField(std.Build.LazyPath, "cwd_relative")) {
+        return .{ .cwd_relative = path };
+    } else {
+        @compileError("unsupported Zig std.Build.LazyPath layout; please update lazyAbsolutePath");
+    }
+}
diff --git a/src/bar.zig b/src/bar.zig
new file mode 100644
index 0000000..ed8e5ad
--- /dev/null
+++ b/src/bar.zig
@@ -0,0 +1,507 @@
+const std = @import("std");
+const C = @import("c.zig");
+const drawlib = @import("draw.zig");
+
+pub const BarPos = enum {
+    top,
+    bottom,
+};
+
+pub const ModAlign = enum {
+    left,
+    center,
+    right,
+};
+
+const ModFn = *const fn (*Mod, std.mem.Allocator, i64) anyerror!void;
+
+const Mod = struct {
+    name: []const u8,
+    text: std.ArrayList(u8),
+    interval_ms: u64,
+    next_update_ms: i64,
+    updater: ModFn,
+    side: ModAlign,
+
+    pub fn init(
+        alloc: std.mem.Allocator,
+        name: []const u8,
+        interval_ms: u64,
+        updater: ModFn,
+        side: ModAlign,
+    ) Mod {
+        return Mod{
+            .name = name,
+            .text = std.ArrayList(u8).init(alloc),
+            .interval_ms = interval_ms,
+            .next_update_ms = 0,
+            .updater = updater,
+            .side = side,
+        };
+    }
+
+    pub fn deinit(self: *Mod) void {
+        self.text.deinit();
+    }
+
+    pub fn due(self: *Mod, now_ms: i64) bool {
+        return now_ms >= self.next_update_ms;
+    }
+
+    pub fn run(self: *Mod, alloc: std.mem.Allocator, now_ms: i64) !void {
+        try self.updater(self, alloc, now_ms);
+        const interval: i64 = @intCast(self.interval_ms);
+        self.next_update_ms = now_ms + interval;
+    }
+
+    pub fn set(self: *Mod, text: []const u8) !void {
+        self.text.clearRetainingCapacity();
+        try self.text.appendSlice(text);
+    }
+};
+
+pub const Bar = struct {
+    alloc: std.mem.Allocator,
+    dpy: *C.Display,
+    root: C.Window,
+    scr: c_int,
+    win: C.Window,
+    pos: BarPos,
+    w: c_uint,
+    h: c_uint,
+    base: c_int,
+    drw: drawlib.Drw,
+    xft: *C.XftDraw,
+    bg: C.ulong,
+    fg: C.XftColor,
+    mods: std.ArrayList(Mod),
+    dirty: bool,
+    pad: c_int,
+    delim: []const u8,
+    mon_x: c_int,
+    mon_y: c_int,
+    mon_h: c_uint,
+    font: [*:0]const u8,
+    sel_win: ?C.Window,
+
+    pub fn init(
+        alloc: std.mem.Allocator,
+        dpy: *C.Display,
+        root: C.Window,
+        scr: c_int,
+        w: c_uint,
+        mon_x: c_int,
+        mon_y: c_int,
+        mon_h: c_uint,
+        pos: BarPos,
+        font: [*:0]const u8,
+        delim: []const u8,
+    ) !Bar {
+        var drw = try drawlib.Drw.init(alloc, dpy, scr, font);
+        errdefer drw.deinit();
+
+        const h: c_uint = @intCast(drw.lh + 4);
+        const bg = try drw.alloccol(0x1c1c1c);
+        const fg = try drw.allocxftcol(0xdddddd);
+
+        const win = C.XCreateSimpleWindow(
+            dpy,
+            root,
+            mon_x,
+            mon_y,
+            w,
+            h,
+            0,
+            bg,
+            bg,
+        );
+        if (win == 0) return error.CreateWindowFailed;
+
+        var attrs: C.XSetWindowAttributes = undefined;
+        attrs.override_redirect = 1;
+        attrs.event_mask = C.ExposureMask | C.StructureNotifyMask;
+        attrs.background_pixel = bg;
+        attrs.border_pixel = bg;
+
+        _ = C.XChangeWindowAttributes(
+            dpy,
+            win,
+            C.CWOverrideRedirect | C.CWEventMask |
+                C.CWBackPixel | C.CWBorderPixel,
+            &attrs,
+        );
+
+        const xft_draw = C.XftDrawCreate(dpy, win, drw.vis, drw.cmap);
+        if (xft_draw == null) return error.XftDrawCreateFailed;
+
+        _ = C.XSelectInput(dpy, win, C.ExposureMask | C.StructureNotifyMask);
+        _ = C.XMapRaised(dpy, win);
+
+        const base = 2 + drw.ascent;
+
+        return Bar{
+            .alloc = alloc,
+            .dpy = dpy,
+            .root = root,
+            .scr = scr,
+            .win = win,
+            .pos = pos,
+            .w = w,
+            .h = h,
+            .base = base,
+            .drw = drw,
+            .xft = xft_draw.?,
+            .bg = bg,
+            .fg = fg,
+            .mods = std.ArrayList(Mod).init(alloc),
+            .dirty = true,
+            .pad = 8,
+            .delim = delim,
+            .mon_x = mon_x,
+            .mon_y = mon_y,
+            .mon_h = mon_h,
+            .font = font,
+            .sel_win = null,
+        };
+    }
+
+    pub fn deinit(self: *Bar) void {
+        for (self.mods.items) |*mod| mod.deinit();
+        self.mods.deinit();
+        C.XftColorFree(self.dpy, self.drw.vis, self.drw.cmap, &self.fg);
+        C.XftDrawDestroy(self.xft);
+        if (self.win != 0) {
+            _ = C.XDestroyWindow(self.dpy, self.win);
+            self.win = 0;
+        }
+        self.drw.deinit();
+    }
+
+    pub fn addMod(
+        self: *Bar,
+        name: []const u8,
+        interval_ms: u64,
+        updater: ModFn,
+        side: ModAlign,
+    ) !void {
+        var mod = Mod.init(self.alloc, name, interval_ms, updater, side);
+        errdefer mod.deinit();
+        try self.mods.append(mod);
+        self.dirty = true;
+    }
+
+    pub fn addClock(self: *Bar, interval_ms: u64) !void {
+        try self.addMod("clock", interval_ms, clkUpd, .right);
+    }
+
+    pub fn addBat(self: *Bar, interval_ms: u64) !void {
+        try self.addMod("battery", interval_ms, batUpd, .right);
+    }
+
+    pub fn addWin(self: *Bar, interval_ms: u64) !void {
+        try self.addMod("window", interval_ms, winUpd, .center);
+    }
+
+    pub fn setSel(self: *Bar, win: ?C.Window) void {
+        if (self.sel_win != win) {
+            self.sel_win = win;
+            self.dirty = true;
+        }
+    }
+
+    pub fn mark(self: *Bar) void {
+        self.dirty = true;
+    }
+
+    pub fn resize(
+        self: *Bar,
+        w: c_uint,
+        mon_x: c_int,
+        mon_y: c_int,
+        mon_h: c_uint,
+    ) void {
+        self.w = w;
+        self.mon_x = mon_x;
+        self.mon_y = mon_y;
+        self.mon_h = mon_h;
+        const mon_h_i: c_int = @intCast(mon_h);
+        const bar_h_i: c_int = @intCast(self.h);
+        const y = switch (self.pos) {
+            .top => mon_y,
+            .bottom => mon_y + mon_h_i - bar_h_i,
+        };
+
+        _ = C.XMoveResizeWindow(self.dpy, self.win, mon_x, y, w, self.h);
+        self.dirty = true;
+    }
+
+    pub fn barH(self: *Bar) c_int {
+        return @intCast(self.h);
+    }
+
+    pub fn tick(self: *Bar, now_ms: i64) !bool {
+        var changed = false;
+        for (self.mods.items) |*mod| {
+            if (!mod.due(now_ms)) continue;
+            try mod.run(self.alloc, now_ms);
+            changed = true;
+        }
+        if (changed) self.dirty = true;
+        return changed;
+    }
+
+    pub fn nextDelay(self: *Bar, now_ms: i64) u64 {
+        if (self.mods.items.len == 0) return 1000;
+        var best: i64 = std.math.maxInt(i64);
+        for (self.mods.items) |mod| {
+            const delta = mod.next_update_ms - now_ms;
+            const clamped = if (delta <= 0) 0 else delta;
+            if (clamped < best) best = clamped;
+        }
+        if (best == std.math.maxInt(i64)) return 1000;
+        return @intCast(best);
+    }
+
+    pub fn draw(self: *Bar) void {
+        if (!self.dirty) return;
+        self.drw.rect(self.win, self.bg, 0, 0, self.w, self.h);
+
+        const baseline = self.base;
+        const delim_txt = self.delim;
+        const delim_w = self.drw.textw(delim_txt);
+
+        var x_right: c_int = @intCast(self.w);
+        x_right -= self.pad;
+        var first_right = true;
+        var i: usize = self.mods.items.len;
+        while (i > 0) {
+            i -= 1;
+            const mod = &self.mods.items[i];
+            if (mod.side != .right) continue;
+            const txt = mod.text.items;
+            if (txt.len == 0) continue;
+            if (!first_right and delim_txt.len != 0) {
+                x_right -= delim_w;
+                var fg_delim = self.fg;
+                self.drw.text(
+                    self.win,
+                    self.xft,
+                    &fg_delim,
+                    x_right,
+                    baseline,
+                    delim_txt,
+                );
+                x_right -= self.pad;
+            }
+            const txt_w = self.drw.textw(txt);
+            x_right -= txt_w;
+            var fg_copy = self.fg;
+            self.drw.text(
+                self.win,
+                self.xft,
+                &fg_copy,
+                x_right,
+                baseline,
+                txt,
+            );
+            x_right -= self.pad;
+            first_right = false;
+        }
+
+        for (self.mods.items) |*mod| {
+            if (mod.side != .center) continue;
+            const txt = mod.text.items;
+            if (txt.len == 0) continue;
+            const txt_w = self.drw.textw(txt);
+            const bar_w: c_int = @intCast(self.w);
+            const x_center = @divTrunc(bar_w - txt_w, 2);
+            var fg_copy = self.fg;
+            self.drw.text(
+                self.win,
+                self.xft,
+                &fg_copy,
+                x_center,
+                baseline,
+                txt,
+            );
+        }
+
+        var x_left: c_int = self.pad;
+        var first_left = true;
+        for (self.mods.items) |*mod| {
+            if (mod.side != .left) continue;
+            const txt = mod.text.items;
+            if (txt.len == 0) continue;
+            if (!first_left and delim_txt.len != 0) {
+                var fg_delim = self.fg;
+                self.drw.text(
+                    self.win,
+                    self.xft,
+                    &fg_delim,
+                    x_left,
+                    baseline,
+                    delim_txt,
+                );
+                x_left += delim_w;
+                x_left += self.pad;
+            }
+            var fg_copy = self.fg;
+            self.drw.text(
+                self.win,
+                self.xft,
+                &fg_copy,
+                x_left,
+                baseline,
+                txt,
+            );
+            const txt_w = self.drw.textw(txt);
+            x_left += txt_w + self.pad;
+            first_left = false;
+        }
+
+        _ = C.XSync(self.dpy, 0);
+        self.dirty = false;
+    }
+};
+
+fn clkUpd(mod: *Mod, alloc: std.mem.Allocator, now_ms: i64) !void {
+    _ = alloc;
+    _ = now_ms;
+    const timestamp = std.time.timestamp();
+    if (timestamp < 0) {
+        try mod.set("time err");
+        return;
+    }
+
+    const wall_seconds: u64 = @intCast(timestamp);
+    const epoch_seconds = std.time.epoch.EpochSeconds{ .secs = wall_seconds };
+    const epoch_day = epoch_seconds.getEpochDay();
+    const day_seconds = epoch_seconds.getDaySeconds();
+    const year_day = epoch_day.calculateYearDay();
+    const month_day = year_day.calculateMonthDay();
+
+    const hours = day_seconds.getHoursIntoDay();
+    const minutes = day_seconds.getMinutesIntoHour();
+
+    const weekday_names = [_][]const u8{
+        "Sun",
+        "Mon",
+        "Tue",
+        "Wed",
+        "Thu",
+        "Fri",
+        "Sat",
+    };
+    const month_names = [_][]const u8{
+        "Jan",
+        "Feb",
+        "Mar",
+        "Apr",
+        "May",
+        "Jun",
+        "Jul",
+        "Aug",
+        "Sep",
+        "Oct",
+        "Nov",
+        "Dec",
+    };
+
+    const day_index: usize = @intCast(epoch_day.day);
+    const weekday_index = (day_index + 4) % weekday_names.len;
+    const weekday_name = weekday_names[weekday_index];
+
+    const month_index: usize = @intCast(month_day.month.numeric() - 1);
+    const month_name = month_names[month_index];
+
+    const day_of_month: u8 = @intCast(month_day.day_index + 1);
+
+    var buf: [64]u8 = undefined;
+    const text = try std.fmt.bufPrint(&buf, "{s}, {s} {d}, {d:0>2}:{d:0>2}", .{
+        weekday_name,
+        month_name,
+        day_of_month,
+        hours,
+        minutes,
+    });
+    try mod.set(text);
+}
+
+fn batUpd(mod: *Mod, alloc: std.mem.Allocator, _: i64) !void {
+    const capacity_path = "/sys/class/power_supply/BAT0/capacity";
+    const file = std.fs.openFileAbsolute(capacity_path, .{}) catch |err| {
+        if (err == error.FileNotFound) {
+            try mod.set("N/A");
+            return;
+        }
+        return err;
+    };
+    defer file.close();
+
+    var buf: [8]u8 = undefined;
+    const bytes_read = try file.readAll(&buf);
+    const content = std.mem.trimRight(
+        u8,
+        buf[0..bytes_read],
+        &std.ascii.whitespace,
+    );
+    const capacity = try std.fmt.parseInt(u8, content, 10);
+
+    var buffer: [16]u8 = undefined;
+    const text = try std.fmt.bufPrint(&buffer, "BAT {d}%", .{capacity});
+    const owned = try alloc.dupe(u8, text);
+    defer alloc.free(owned);
+    try mod.set(owned);
+}
+
+var g_bar: ?*Bar = null;
+
+pub fn setGlobalBar(bar_ptr: ?*Bar) void {
+    g_bar = bar_ptr;
+}
+
+fn winUpd(mod: *Mod, alloc: std.mem.Allocator, _: i64) !void {
+    const bar_ptr = g_bar orelse {
+        try mod.set("");
+        return;
+    };
+
+    const win = bar_ptr.sel_win orelse {
+        try mod.set("");
+        return;
+    };
+
+    var class_hint: C.XClassHint = undefined;
+    _ = C.XSetErrorHandler(ignoreError);
+    const status = C.XGetClassHint(bar_ptr.dpy, win, &class_hint);
+    _ = C.XSetErrorHandler(handleError);
+
+    if (status == 0) {
+        try mod.set("");
+        return;
+    }
+
+    defer {
+        if (class_hint.res_class != null) _ = C.XFree(class_hint.res_class);
+        if (class_hint.res_name != null) _ = C.XFree(class_hint.res_name);
+    }
+
+    if (class_hint.res_class != null) {
+        const class_name = std.mem.span(
+            @as([*:0]const u8, @ptrCast(class_hint.res_class)),
+        );
+        const owned = try alloc.dupe(u8, class_name);
+        defer alloc.free(owned);
+        try mod.set(owned);
+    } else {
+        try mod.set("");
+    }
+}
+
+fn ignoreError(_: ?*C.Display, _: [*c]C.XErrorEvent) callconv(.C) c_int {
+    return 0;
+}
+
+fn handleError(_: ?*C.Display, _: [*c]C.XErrorEvent) callconv(.C) c_int {
+    return 0;
+}
diff --git a/src/c.zig b/src/c.zig
new file mode 100644
index 0000000..1a40856
--- /dev/null
+++ b/src/c.zig
@@ -0,0 +1,10 @@
+pub usingnamespace @cImport({
+    @cInclude("X11/Xlib.h");
+    @cInclude("X11/XF86keysym.h");
+    @cInclude("X11/keysym.h");
+    @cInclude("X11/XKBlib.h");
+    @cInclude("X11/Xatom.h");
+    @cInclude("X11/Xutil.h");
+    @cInclude("X11/extensions/Xrandr.h");
+    @cInclude("X11/Xft/Xft.h");
+});
diff --git a/src/draw.zig b/src/draw.zig
new file mode 100644
index 0000000..cd4b652
--- /dev/null
+++ b/src/draw.zig
@@ -0,0 +1,162 @@
+const std = @import("std");
+const C = @import("c.zig");
+
+pub const Drw = struct {
+    alloc: std.mem.Allocator,
+    dpy: *C.Display,
+    scr: c_int,
+    vis: *C.Visual,
+    cmap: C.Colormap,
+    gc: C.GC,
+    font: *C.XftFont,
+    ascent: c_int,
+    descent: c_int,
+    lh: c_int,
+
+    pub fn init(
+        alloc: std.mem.Allocator,
+        dpy: *C.Display,
+        scr: c_int,
+        font_name: [*:0]const u8,
+    ) !Drw {
+        const font = loadfont(dpy, scr, font_name) orelse
+            return error.FontUnavailable;
+
+        const root = C.RootWindow(dpy, scr);
+        const gc_opt = C.XCreateGC(dpy, root, 0, null);
+        if (gc_opt == null) {
+            C.XftFontClose(dpy, font);
+            return error.CreateGcFailed;
+        }
+        const gc = gc_opt.?;
+
+        const vis = C.XDefaultVisual(dpy, scr);
+        const cmap = C.XDefaultColormap(dpy, scr);
+
+        return Drw{
+            .alloc = alloc,
+            .dpy = dpy,
+            .scr = scr,
+            .vis = vis,
+            .cmap = cmap,
+            .gc = gc,
+            .font = font,
+            .ascent = font.*.ascent,
+            .descent = font.*.descent,
+            .lh = font.*.ascent + font.*.descent,
+        };
+    }
+
+    fn loadfont(
+        dpy: *C.Display,
+        scr: c_int,
+        preferred: [*:0]const u8,
+    ) ?*C.XftFont {
+        const candidates = [_][*:0]const u8{
+            preferred,
+            "monospace:size=10",
+            "fixed",
+        };
+        for (candidates) |name| {
+            if (name[0] == 0) continue;
+            const font_ptr = C.XftFontOpenName(dpy, scr, name);
+            if (font_ptr != null) {
+                if (name != preferred) {
+                    std.log.warn("bar font fallback to {s}", .{name});
+                }
+                return font_ptr;
+            }
+        }
+        return null;
+    }
+
+    pub fn deinit(self: *Drw) void {
+        C.XftFontClose(self.dpy, self.font);
+        _ = C.XFreeGC(self.dpy, self.gc);
+    }
+
+    pub fn alloccol(self: *Drw, rgb: u32) !C.ulong {
+        var color: C.XColor = undefined;
+        color.pixel = 0;
+        color.red = @intCast(((rgb >> 16) & 0xff) * 257);
+        color.green = @intCast(((rgb >> 8) & 0xff) * 257);
+        color.blue = @intCast((rgb & 0xff) * 257);
+        color.flags = @intCast(C.DoRed | C.DoGreen | C.DoBlue);
+        color.pad = 0;
+
+        if (C.XAllocColor(self.dpy, self.cmap, &color) == 0) {
+            return error.ColorAllocationFailed;
+        }
+
+        return color.pixel;
+    }
+
+    pub fn allocxftcol(self: *Drw, rgb: u32) !C.XftColor {
+        const r: u16 = @intCast(((rgb >> 16) & 0xff) * 257);
+        const g: u16 = @intCast(((rgb >> 8) & 0xff) * 257);
+        const b: u16 = @intCast((rgb & 0xff) * 257);
+        var xft_color: C.XftColor = undefined;
+        var render_color: C.XRenderColor = undefined;
+        render_color.red = r;
+        render_color.green = g;
+        render_color.blue = b;
+        render_color.alpha = 0xffff;
+        if (C.XftColorAllocValue(
+            self.dpy,
+            self.vis,
+            self.cmap,
+            &render_color,
+            &xft_color,
+        ) == 0) {
+            return error.ColorAllocationFailed;
+        }
+        return xft_color;
+    }
+
+    pub fn rect(
+        self: *Drw,
+        draw: C.Drawable,
+        col: C.ulong,
+        x: c_int,
+        y: c_int,
+        w: c_uint,
+        h: c_uint,
+    ) void {
+        _ = C.XSetForeground(self.dpy, self.gc, col);
+        _ = C.XFillRectangle(self.dpy, draw, self.gc, x, y, w, h);
+    }
+
+    pub fn text(
+        self: *Drw,
+        _: C.Drawable,
+        xft: *C.XftDraw,
+        col: *C.XftColor,
+        x: c_int,
+        y: c_int,
+        txt: []const u8,
+    ) void {
+        if (txt.len == 0) return;
+        C.XftDrawStringUtf8(
+            xft,
+            col,
+            self.font,
+            x,
+            y,
+            @ptrCast(txt.ptr),
+            @intCast(txt.len),
+        );
+    }
+
+    pub fn textw(self: *Drw, txt: []const u8) c_int {
+        if (txt.len == 0) return 0;
+        var extents: C.XGlyphInfo = undefined;
+        C.XftTextExtentsUtf8(
+            self.dpy,
+            self.font,
+            @ptrCast(txt.ptr),
+            @intCast(txt.len),
+            &extents,
+        );
+        return @intCast(extents.xOff);
+    }
+};
diff --git a/src/main.zig b/src/main.zig
new file mode 100644
index 0000000..d5e864c
--- /dev/null
+++ b/src/main.zig
@@ -0,0 +1,1559 @@
+const std = @import("std");
+const C = @import("c.zig");
+const bar_mod = @import("bar.zig");
+const posix = std.posix;
+
+const Client = struct {
+    name: [256]u8 = std.mem.zeroes([256]u8),
+    x: c_int,
+    y: c_int,
+    w: c_int,
+    h: c_int,
+    rx: c_int = 0,
+    ry: c_int = 0,
+    rw: c_int = 0,
+    rh: c_int = 0,
+    basew: c_int = 0,
+    baseh: c_int = 0,
+    incw: c_int = 0,
+    inch: c_int = 0,
+    maxw: c_int = 0,
+    maxh: c_int = 0,
+    minw: c_int = 0,
+    minh: c_int = 0,
+    minax: c_int = 0,
+    minay: c_int = 0,
+    maxax: c_int = 0,
+    maxay: c_int = 0,
+    flags: c_long = 0,
+    border: c_uint = BORDER,
+    isfixed: bool = false,
+    isfloat: bool = false,
+    ismax: bool = false,
+    win: C.Window,
+};
+
+var cl: std.ArrayList(Client) = undefined;
+var hidden: std.ArrayList(Client) = undefined;
+var stack: std.ArrayList(C.Window) = undefined;
+var sel: ?usize = null;
+
+const FOCUS_COL = 0x6699cc;
+const NORMAL_COL = 0x333333;
+const BORDER = 2;
+const GAP: c_int = 3;
+const MFACT = 0.6;
+
+var mfact: f32 = MFACT;
+var nmaster: usize = 1;
+var keyacts: [256]?*const fn () void = .{null} ** 256;
+
+const user = "seb";
+const terminal = "st";
+const launcher = "zmen";
+
+const autostart = std.fmt.comptimePrint("/home/{s}/.scripts/2zw.sh", .{user});
+const bar_font: [*:0]const u8 = "monospace:size=15";
+const delim = "::";
+
+const Bind = struct { sym: C.KeySym, action: *const fn () void };
+
+const binds = [_]Bind{
+    .{ .sym = C.XK_q, .action = &killclient },
+    .{ .sym = C.XK_comma, .action = &winprev },
+    .{ .sym = C.XK_period, .action = &winnext },
+    .{ .sym = C.XK_a, .action = &act_show },
+    .{ .sym = C.XK_d, .action = &act_hide },
+    .{ .sym = C.XK_h, .action = &act_mfact_dec },
+    .{ .sym = C.XK_l, .action = &act_mfact_inc },
+    .{ .sym = C.XK_Return, .action = spawnfn(terminal) },
+    .{ .sym = C.XK_p, .action = spawnfn(launcher) },
+    .{ .sym = C.XK_s, .action = spawnfn("slock") },
+};
+
+var cur_resize: C.Cursor = undefined;
+var cur_move: C.Cursor = undefined;
+var cur_normal: C.Cursor = undefined;
+
+var alloc: std.mem.Allocator = undefined;
+
+var atom_proto: C.Atom = undefined;
+var atom_del: C.Atom = undefined;
+var atom_state: C.Atom = undefined;
+var atom_net: C.Atom = undefined;
+var atom_name: C.Atom = undefined;
+var atom_active: C.Atom = undefined;
+
+fn spawnfn(comptime cmd: [*:0]const u8) *const fn () void {
+    return struct {
+        fn action() void {
+            spawn(cmd);
+        }
+    }.action;
+}
+
+fn act_show() void {
+    show();
+}
+
+fn act_hide() void {
+    hide();
+}
+
+fn act_mfact_dec() void {
+    mfact = @max(0.1, mfact - 0.05);
+    tile();
+}
+
+fn act_mfact_inc() void {
+    mfact = @min(0.9, mfact + 0.05);
+    tile();
+}
+
+fn sigchldignore() void {
+    var sa: std.c.Sigaction = .{
+        .handler = .{ .handler = std.c.SIG.IGN },
+        .mask = std.c.empty_sigset,
+        .flags = 0,
+    };
+    _ = std.c.sigaction(std.c.SIG.CHLD, &sa, null);
+}
+
+fn detachstack(win: C.Window) void {
+    var i: usize = 0;
+    while (i < stack.items.len) : (i += 1) {
+        if (stack.items[i] == win) {
+            _ = stack.orderedRemove(i);
+            return;
+        }
+    }
+}
+
+fn attachstack(win: C.Window) !void {
+    detachstack(win);
+    try stack.insert(0, win);
+}
+
+fn restack() void {
+    const selected_idx = sel orelse return;
+    const selected = &cl.items[selected_idx];
+
+    if (selected.isfloat) {
+        _ = C.XRaiseWindow(display, selected.win);
+    } else {
+        _ = C.XLowerWindow(display, selected.win);
+    }
+
+    var to_lower = std.ArrayList(C.Window).init(alloc);
+    defer to_lower.deinit();
+
+    for (cl.items, 0..) |client, idx| {
+        if (idx == selected_idx or client.isfloat) continue;
+        to_lower.append(client.win) catch continue;
+    }
+
+    for (to_lower.items) |win| {
+        _ = C.XLowerWindow(display, win);
+    }
+
+    _ = C.XSync(display, 0);
+}
+
+fn grabinputs(window: C.Window) void {
+    _ = C.XUngrabKey(display, C.AnyKey, C.AnyModifier, root);
+
+    for (binds) |b| {
+        _ = C.XGrabKey(
+            display,
+            C.XKeysymToKeycode(display, b.sym),
+            C.Mod4Mask,
+            window,
+            0,
+            C.GrabModeAsync,
+            C.GrabModeAsync,
+        );
+    }
+    for ([_]u8{ 1, 3 }) |btn| {
+        const mask = C.ButtonPressMask |
+            C.ButtonReleaseMask |
+            C.PointerMotionMask;
+        _ = C.XGrabButton(
+            display,
+            btn,
+            C.Mod4Mask,
+            root,
+            0,
+            mask,
+            C.GrabModeAsync,
+            C.GrabModeAsync,
+            0,
+            0,
+        );
+    }
+}
+
+fn setkeys() void {
+    for (&keyacts) |*slot| slot.* = null;
+    for (binds) |b| {
+        const kc = C.XKeysymToKeycode(display, b.sym);
+        if (kc < keyacts.len) {
+            keyacts[kc] = b.action;
+        }
+    }
+}
+
+fn show() void {
+    if (hidden.items.len == 0) return;
+    const client = hidden.orderedRemove(0);
+
+    _ = C.XMapWindow(display, client.win);
+    cl.append(client) catch return;
+
+    tile();
+    focus(cl.items.len - 1);
+}
+
+fn hide() void {
+    const idx = sel orelse return;
+    const client = cl.orderedRemove(idx);
+
+    detachstack(client.win);
+    _ = C.XUnmapWindow(display, client.win);
+    hidden.append(client) catch return;
+
+    sel = if (cl.items.len > 0) @min(idx, cl.items.len - 1) else null;
+    focus(sel);
+    tile();
+}
+
+fn scan(allocator: std.mem.Allocator) void {
+    var root_return: C.Window = undefined;
+    var parent_return: C.Window = undefined;
+    var children: [*c]C.Window = undefined;
+    var num_children: c_uint = 0;
+
+    if (C.XQueryTree(
+        display,
+        root,
+        &root_return,
+        &parent_return,
+        &children,
+        &num_children,
+    ) == 0) return;
+
+    for (0..num_children) |i| {
+        var wa: C.XWindowAttributes = undefined;
+        if (C.XGetWindowAttributes(display, children[i], &wa) == 0) continue;
+        if (wa.override_redirect != 0 or wa.map_state != C.IsViewable)
+            continue;
+        manage(allocator, children[i]);
+    }
+
+    if (children != null) _ = C.XFree(children);
+
+    if (cl.items.len > 0) tile();
+}
+
+fn floatwins() void {
+    var last: ?usize = null;
+
+    for (cl.items, 0..) |*c, i| {
+        if (!c.isfloat) continue;
+        _ = C.XMoveWindow(display, c.win, c.x, c.y);
+        _ = C.XRaiseWindow(display, c.win);
+        last = i;
+    }
+
+    if (last) |idx| {
+        focus(idx);
+    }
+}
+
+inline fn todim(v: c_int) c_uint {
+    return @intCast(@max(0, v));
+}
+
+fn tile() void {
+    const n = counttilable();
+    if (n == 0) {
+        floatwins();
+        return;
+    }
+
+    const bar_h: c_int = if (bar) |b| b.barH() else 0;
+    const my: c_int = sy_offset + if (bar) |b| if (b.pos == .top) bar_h else 0 else 0;
+    const mh: c_int = @intCast(sh - @as(c_uint, @intCast(bar_h)));
+    const mw: c_int = @intCast(sw);
+
+    const n_c: c_int = @intCast(n);
+    const nmaster_c: c_int = @min(@as(c_int, @intCast(nmaster)), n_c);
+    const stack_n: c_int = n_c - nmaster_c;
+
+    const master_w: c_int = if (stack_n == 0)
+        mw - 2 * GAP
+    else
+        @as(c_int, @intFromFloat(@as(f32, @floatFromInt(mw)) * mfact)) - GAP - GAP / 2;
+
+    const stack_w: c_int = mw - master_w - 3 * GAP;
+
+    var mi: c_int = 0;
+    var si: c_int = 0;
+
+    for (cl.items) |*c| {
+        if (c.isfloat) continue;
+
+        if (mi < nmaster_c) {
+            const h = @divTrunc(mh - GAP * (nmaster_c + 1), nmaster_c);
+            resize(c, sx_offset + GAP, my + GAP + mi * (h + GAP), master_w, h);
+            mi += 1;
+        } else {
+            const h = @divTrunc(mh - GAP * (stack_n + 1), stack_n);
+            resize(c, sx_offset + master_w + 2 * GAP, my + GAP + si * (h + GAP), stack_w, h);
+            si += 1;
+        }
+    }
+
+    floatwins();
+}
+
+fn counttilable() usize {
+    var n: usize = 0;
+    for (cl.items) |c| {
+        if (!c.isfloat) n += 1;
+    }
+    return n;
+}
+
+fn resize(c: *Client, x: c_int, y: c_int, w: c_int, h: c_int) void {
+    c.x = x;
+    c.y = y;
+    c.w = w;
+    c.h = h;
+    _ = C.XMoveResizeWindow(display, c.win, x, y, todim(w - 2 * BORDER), todim(h - 2 * BORDER));
+}
+
+fn addclient(allocator: std.mem.Allocator, window: C.Window) !usize {
+    var attributes: C.XWindowAttributes = undefined;
+    _ = C.XGetWindowAttributes(display, window, &attributes);
+
+    const m_float = isfloat(window);
+
+    const client = Client{
+        .x = attributes.x,
+        .y = attributes.y,
+        .w = attributes.width,
+        .h = attributes.height,
+        .win = window,
+        .isfloat = m_float,
+    };
+
+    try cl.insert(0, client);
+
+    if (sel) |sel_idx| {
+        sel = sel_idx + 1;
+    }
+
+    sizehints(&cl.items[0]);
+    title(allocator, &cl.items[0]);
+
+    return 0;
+}
+
+// refactor
+fn isfloat(window: C.Window) bool {
+    var transient_for: C.Window = undefined;
+    if (C.XGetTransientForHint(display, window, &transient_for) != 0) {
+        return true;
+    }
+
+    var class_hint: C.XClassHint = undefined;
+    if (C.XGetClassHint(display, window, &class_hint) != 0) {
+        var m_float = false;
+
+        if (class_hint.res_class != null) {
+            const class_name = std.mem.span(
+                @as([*:0]const u8, @ptrCast(class_hint.res_class)),
+            );
+            m_float = (std.mem.indexOf(u8, class_name, "Dialog") != null);
+            _ = C.XFree(class_hint.res_class);
+        }
+
+        if (class_hint.res_name != null) {
+            const res_name = std.mem.span(
+                @as([*:0]const u8, @ptrCast(class_hint.res_name)),
+            );
+            m_float = m_float or
+                (std.mem.indexOf(u8, res_name, "dialog") != null);
+            _ = C.XFree(class_hint.res_name);
+        }
+
+        if (m_float) {
+            return true;
+        }
+    }
+
+    var actual_type: C.Atom = undefined;
+    var actual_format: c_int = undefined;
+    var nitems: c_ulong = undefined;
+    var bytes_after: c_ulong = undefined;
+    var prop_return: [*c]u8 = undefined;
+
+    _ = C.XSetErrorHandler(ignoreerr);
+
+    const atom_type = C.XInternAtom(display, "_NET_WM_WINDOW_TYPE", 0);
+    if (atom_type != 0) {
+        const status = C.XGetWindowProperty(
+            display,
+            window,
+            atom_type,
+            0,
+            32,
+            0,
+            C.XA_ATOM,
+            &actual_type,
+            &actual_format,
+            &nitems,
+            &bytes_after,
+            &prop_return,
+        );
+
+        const got_atoms = status == 0 and prop_return != null;
+        const good_format =
+            actual_type == C.XA_ATOM and actual_format == 32 and nitems > 0;
+
+        if (got_atoms and good_format) {
+            const atoms = @as([*]C.Atom, @ptrCast(@alignCast(prop_return)));
+            const atom_dialog = C.XInternAtom(
+                display,
+                "_NET_WM_WINDOW_TYPE_DIALOG",
+                0,
+            );
+            const atom_utility = C.XInternAtom(
+                display,
+                "_NET_WM_WINDOW_TYPE_UTILITY",
+                0,
+            );
+            const atom_popup = C.XInternAtom(
+                display,
+                "_NET_WM_WINDOW_TYPE_POPUP_MENU",
+                0,
+            );
+
+            for (0..nitems) |i| {
+                const atom = atoms[i];
+                if (atom == atom_dialog or
+                    atom == atom_utility or
+                    atom == atom_popup)
+                {
+                    _ = C.XFree(prop_return);
+                    _ = C.XSetErrorHandler(xerr);
+                    return true;
+                }
+            }
+
+            _ = C.XFree(prop_return);
+        }
+    }
+
+    _ = C.XSetErrorHandler(xerr);
+
+    return false;
+}
+
+fn focus(target_index_opt: ?usize) void {
+    if (cl.items.len == 0) {
+        sel = null;
+        if (bar) |b_ptr| b_ptr.setSel(null);
+        _ = C.XSetInputFocus(
+            display,
+            root,
+            C.RevertToPointerRoot,
+            C.CurrentTime,
+        );
+        _ = C.XFlush(display);
+        return;
+    }
+
+    const target_index = target_index_opt orelse {
+        if (sel) |sel_idx| {
+            if (sel_idx < cl.items.len) {
+                _ = C.XSetErrorHandler(ignoreerr);
+                _ = C.XSetWindowBorder(
+                    display,
+                    cl.items[sel_idx].win,
+                    NORMAL_COL,
+                );
+                _ = C.XSetErrorHandler(xerr);
+            }
+        }
+        sel = null;
+        if (bar) |b_ptr| b_ptr.setSel(null);
+        _ = C.XSetInputFocus(
+            display,
+            root,
+            C.RevertToPointerRoot,
+            C.CurrentTime,
+        );
+        _ = C.XFlush(display);
+        return;
+    };
+
+    var index = target_index;
+    if (index >= cl.items.len) {
+        index = 0;
+    }
+
+    if (sel) |sel_idx| {
+        if (sel_idx < cl.items.len and sel_idx != index) {
+            _ = C.XSetErrorHandler(ignoreerr);
+            _ = C.XSetWindowBorder(display, cl.items[sel_idx].win, NORMAL_COL);
+            _ = C.XSetErrorHandler(xerr);
+        }
+    }
+
+    const client = &cl.items[index];
+
+    detachstack(client.win);
+    attachstack(client.win) catch {};
+
+    _ = C.XSetErrorHandler(ignoreerr);
+    _ = C.XSetInputFocus(display, client.win, C.RevertToParent, C.CurrentTime);
+    _ = C.XRaiseWindow(display, client.win);
+    _ = C.XSetWindowBorder(display, client.win, FOCUS_COL);
+    _ = C.XChangeProperty(
+        display,
+        root,
+        atom_active,
+        C.XA_WINDOW,
+        32,
+        C.PropModeReplace,
+        @ptrCast(&client.win),
+        1,
+    );
+    _ = C.XSetErrorHandler(xerr);
+
+    sel = index;
+    if (bar) |b_ptr| b_ptr.setSel(client.win);
+
+    _ = C.XFlush(display);
+}
+
+fn getclient(w: C.Window) ?usize {
+    for (cl.items, 0..) |client, idx| {
+        if (client.win == w) return idx;
+    }
+    return null;
+}
+
+fn hiddenidx(w: C.Window) ?usize {
+    for (hidden.items, 0..) |client, idx| {
+        if (client.win == w) return idx;
+    }
+    return null;
+}
+
+fn purgehidden(w: C.Window) void {
+    if (hiddenidx(w)) |idx| {
+        _ = hidden.orderedRemove(idx);
+        detachstack(w);
+    }
+}
+
+fn nextfocus(exclude_win: C.Window) ?usize {
+    for (stack.items) |win| {
+        if (win == exclude_win) continue;
+        if (getclient(win)) |idx| {
+            return idx;
+        }
+    }
+    return null;
+}
+
+fn unmanage(index: usize, destroyed: bool) void {
+    if (index >= cl.items.len) return;
+
+    _ = C.XGrabServer(display);
+    _ = C.XSetErrorHandler(ignoreerr);
+
+    const client = cl.items[index];
+
+    if (!destroyed) {
+        _ = C.XSelectInput(display, client.win, C.NoEventMask);
+        _ = C.XUngrabButton(display, C.AnyButton, C.AnyModifier, client.win);
+
+        const data = [_]c_long{ C.WithdrawnState, C.None };
+        _ = C.XChangeProperty(
+            display,
+            client.win,
+            atom_state,
+            atom_state,
+            32,
+            C.PropModeReplace,
+            @ptrCast(&data),
+            data.len,
+        );
+    }
+
+    var removed_was_selected = false;
+    if (sel) |sel_idx| {
+        if (sel_idx == index) {
+            removed_was_selected = true;
+        } else if (sel_idx > index) {
+            sel = sel_idx - 1;
+        }
+    }
+
+    detachstack(client.win);
+    _ = cl.orderedRemove(index);
+
+    var next_focus_index: ?usize = null;
+    if (removed_was_selected) {
+        sel = null;
+        next_focus_index = nextfocus(client.win);
+    }
+
+    _ = C.XSync(display, 0);
+    _ = C.XSetErrorHandler(xerr);
+    _ = C.XUngrabServer(display);
+
+    if (cl.items.len > 0) {
+        tile();
+        const fallback = next_focus_index orelse blk: {
+            if (index < cl.items.len) break :blk index;
+            break :blk cl.items.len - 1;
+        };
+        focus(fallback);
+    } else {
+        focus(null);
+        _ = C.XSync(display, 0);
+    }
+}
+
+var sw: c_uint = 0;
+var sh: c_uint = 0;
+var sx_offset: c_int = 0;
+var sy_offset: c_int = 0;
+var srotation: c_uint = 0;
+
+var rr_event: c_int = 0;
+var rr_error: c_int = 0;
+
+var bar: ?*bar_mod.Bar = null;
+
+const NS_PER_MS: u64 = 1_000_000;
+
+fn initrandr(allocator: std.mem.Allocator) !void {
+    if (C.XRRQueryExtension(display, &rr_event, &rr_error) == 0) {
+        return;
+    }
+
+    try geom(allocator);
+    const rr_mask =
+        C.RROutputChangeNotifyMask |
+        C.RRCrtcChangeNotifyMask |
+        C.RRScreenChangeNotifyMask;
+    _ = C.XRRSelectInput(display, root, rr_mask);
+}
+
+fn geom(allocator: std.mem.Allocator) !void {
+    var res = C.XRRGetScreenResourcesCurrent(display, root);
+    if (res == null) {
+        res = C.XRRGetScreenResources(display, root);
+    }
+    if (res == null) {
+        const x_screen = C.DefaultScreen(display);
+        sw = @intCast(C.XDisplayWidth(display, x_screen));
+        sh = @intCast(C.XDisplayHeight(display, x_screen));
+        sx_offset = 0;
+        sy_offset = 0;
+        srotation = 0;
+        return;
+    }
+    defer C.XRRFreeScreenResources(res);
+
+    var found_active_monitor = false;
+
+    const primary_output = C.XRRGetOutputPrimary(display, root);
+    if (primary_output != 0) {
+        found_active_monitor = try usemon(
+            allocator,
+            res,
+            primary_output,
+            "primary",
+        );
+    }
+
+    if (!found_active_monitor) {
+        found_active_monitor = try lmon(allocator, res);
+    }
+
+    if (!found_active_monitor) {
+        const x_screen = C.DefaultScreen(display);
+        sw = @intCast(C.XDisplayWidth(display, x_screen));
+        sh = @intCast(C.XDisplayHeight(display, x_screen));
+        sx_offset = 0;
+        sy_offset = 0;
+        srotation = 0;
+    }
+}
+
+fn usemon(
+    allocator: std.mem.Allocator,
+    res: *C.XRRScreenResources,
+    output_id: C.RROutput,
+    monitor_type: []const u8,
+) !bool {
+    _ = allocator;
+    const output_info = C.XRRGetOutputInfo(display, res, output_id);
+    if (output_info == null) return false;
+    defer C.XRRFreeOutputInfo(output_info);
+
+    if (output_info.*.connection != C.RR_Connected or output_info.*.crtc == 0) {
+        return false;
+    }
+
+    const crtc_info = C.XRRGetCrtcInfo(display, res, output_info.*.crtc);
+    if (crtc_info == null) return false;
+    defer C.XRRFreeCrtcInfo(crtc_info);
+
+    if (crtc_info.*.width == 0 or crtc_info.*.height == 0) {
+        return false;
+    }
+
+    sw = @intCast(crtc_info.*.width);
+    sh = @intCast(crtc_info.*.height);
+    sx_offset = crtc_info.*.x;
+    sy_offset = crtc_info.*.y;
+    srotation = @intCast(crtc_info.*.rotation);
+
+    const output_name = std.mem.span(
+        @as([*:0]const u8, @ptrCast(output_info.*.name)),
+    );
+    std.log.info("Using {s} monitor: {s} at ({d},{d}) size {d}x{d}", .{
+        monitor_type,
+        output_name,
+        sx_offset,
+        sy_offset,
+        sw,
+        sh,
+    });
+
+    return true;
+}
+
+fn lmon(_: std.mem.Allocator, res: *C.XRRScreenResources) !bool {
+    var largest_width: c_uint = 0;
+    var largest_height: c_uint = 0;
+    var largest_area: c_uint = 0;
+    var largest_x: c_int = 0;
+    var largest_y: c_int = 0;
+    var largest_rotation: c_uint = 0;
+
+    const output_count: usize = @intCast(res.*.noutput);
+    for (0..output_count) |i| {
+        const output_info = C.XRRGetOutputInfo(display, res, res.*.outputs[i]);
+        if (output_info == null) continue;
+        defer C.XRRFreeOutputInfo(output_info);
+
+        if (output_info.*.connection != C.RR_Connected or
+            output_info.*.crtc == 0)
+        {
+            continue;
+        }
+
+        const crtc_info = C.XRRGetCrtcInfo(display, res, output_info.*.crtc);
+        if (crtc_info == null) continue;
+        defer C.XRRFreeCrtcInfo(crtc_info);
+
+        if (crtc_info.*.width == 0 or crtc_info.*.height == 0) continue;
+
+        const output_width: c_uint = @intCast(crtc_info.*.width);
+        const output_height: c_uint = @intCast(crtc_info.*.height);
+        const output_area = output_width * output_height;
+
+        if (output_area > largest_area) {
+            largest_area = output_area;
+            largest_width = output_width;
+            largest_height = output_height;
+            largest_x = crtc_info.*.x;
+            largest_y = crtc_info.*.y;
+            largest_rotation = @intCast(crtc_info.*.rotation);
+        }
+    }
+
+    if (largest_area > 0) {
+        sw = largest_width;
+        sh = largest_height;
+        sx_offset = largest_x;
+        sy_offset = largest_y;
+        srotation = largest_rotation;
+        return true;
+    }
+
+    return false;
+}
+
+fn syncmon(allocator: std.mem.Allocator) void {
+    geom(allocator) catch {};
+}
+
+fn onrr(allocator: std.mem.Allocator, e: *C.XEvent) !void {
+    const rrev = @as(*C.XRRNotifyEvent, @ptrCast(e));
+
+    switch (rrev.subtype) {
+        C.RRNotify_OutputChange,
+        C.RRNotify_CrtcChange,
+        => try refreshmon(allocator),
+        else => {},
+    }
+}
+
+fn onrrscreen(allocator: std.mem.Allocator, e: *C.XEvent) !void {
+    _ = C.XRRUpdateConfiguration(e);
+    try refreshmon(allocator);
+}
+
+fn refreshmon(allocator: std.mem.Allocator) !void {
+    try geom(allocator);
+    if (bar) |b_ptr| {
+        b_ptr.resize(sw, sx_offset, sy_offset, sh);
+        b_ptr.mark();
+    }
+    if (cl.items.len > 0) {
+        tile();
+    }
+}
+
+fn runautostart() void {
+    const pid = posix.fork() catch return;
+    if (pid == 0) {
+        _ = setsid();
+        const args = [_:null]?[*:0]const u8{ "/bin/sh", autostart, null };
+        _ = execvp("/bin/sh", &args);
+        posix.exit(1);
+    }
+}
+
+fn killclient() void {
+    const index = sel orelse return;
+
+    if (index >= cl.items.len) return;
+
+    const client_win = cl.items[index].win;
+
+    if (protodel(index)) {
+        _ = C.XSetErrorHandler(ignoreerr);
+        sendev(client_win, atom_proto, @as(c_long, @bitCast(atom_del)));
+        _ = C.XSetErrorHandler(xerr);
+    } else {
+        _ = C.XGrabServer(display);
+        _ = C.XSetErrorHandler(ignoreerr);
+        _ = C.XKillClient(display, client_win);
+        _ = C.XSync(display, 0);
+        _ = C.XSetErrorHandler(xerr);
+        _ = C.XUngrabServer(display);
+    }
+
+    _ = C.XSync(display, 0);
+}
+
+fn protodel(client_index: usize) bool {
+    if (client_index >= cl.items.len) return false;
+
+    const client = cl.items[client_index];
+    var protocols: [*c]C.Atom = undefined;
+    var n: c_int = 0;
+    var ret = false;
+
+    if (C.XGetWMProtocols(display, client.win, &protocols, &n) != 0) {
+        const hint_count: usize = @intCast(n);
+        for (0..hint_count) |i| {
+            if (protocols[i] == atom_del) {
+                ret = true;
+                break;
+            }
+        }
+        _ = C.XFree(protocols);
+    }
+    return ret;
+}
+
+fn sendev(win: C.Window, atom: C.Atom, value: c_long) void {
+    var event: C.XEvent = undefined;
+    event.type = C.ClientMessage;
+    event.xclient.window = win;
+    event.xclient.message_type = atom;
+    event.xclient.format = 32;
+    event.xclient.data.l[0] = value;
+    event.xclient.data.l[1] = C.CurrentTime;
+    _ = C.XSendEvent(display, win, 0, C.NoEventMask, &event);
+    _ = C.XSync(display, 0);
+}
+
+fn sizehints(client: *Client) void {
+    var size: C.XSizeHints = undefined;
+    var msize: c_long = 0;
+
+    if (C.XGetWMNormalHints(display, client.win, &size, &msize) == 0 or
+        size.flags == 0)
+    {
+        size.flags = C.PSize;
+    }
+
+    client.flags = size.flags;
+
+    if ((client.flags & C.PBaseSize) != 0) {
+        client.basew = size.base_width;
+        client.baseh = size.base_height;
+    } else {
+        client.basew = 0;
+        client.baseh = 0;
+    }
+
+    if ((client.flags & C.PResizeInc) != 0) {
+        client.incw = size.width_inc;
+        client.inch = size.height_inc;
+    } else {
+        client.incw = 0;
+        client.inch = 0;
+    }
+
+    if ((client.flags & C.PMaxSize) != 0) {
+        client.maxw = size.max_width;
+        client.maxh = size.max_height;
+    } else {
+        client.maxw = 0;
+        client.maxh = 0;
+    }
+
+    if ((client.flags & C.PMinSize) != 0) {
+        client.minw = size.min_width;
+        client.minh = size.min_height;
+    } else {
+        client.minw = 0;
+        client.minh = 0;
+    }
+
+    if ((client.flags & C.PAspect) != 0) {
+        client.minax = size.min_aspect.x;
+        client.minay = size.min_aspect.y;
+        client.maxax = size.max_aspect.x;
+        client.maxay = size.max_aspect.y;
+    } else {
+        client.minax = 0;
+        client.minay = 0;
+        client.maxax = 0;
+        client.maxay = 0;
+    }
+
+    client.isfixed = (client.maxw != 0 and client.minw != 0 and
+        client.maxh != 0 and client.minh != 0 and
+        client.maxw == client.minw and
+        client.maxh == client.minh);
+}
+
+fn title(_: std.mem.Allocator, client: *Client) void {
+    var name: C.XTextProperty = undefined;
+
+    client.name[0] = 0;
+
+    _ = C.XGetTextProperty(display, client.win, &name, atom_name);
+    if (name.nitems == 0) {
+        _ = C.XGetWMName(display, client.win, &name);
+    }
+
+    if (name.nitems == 0) return;
+
+    if (name.encoding == C.XA_STRING) {
+        const title_len = @min(name.nitems, client.name.len - 1);
+        @memcpy(client.name[0..title_len], name.value[0..title_len]);
+        client.name[title_len] = 0;
+    } else {
+        var list: [*c][*c]u8 = undefined;
+        var count: c_int = 0;
+
+        if (C.XmbTextPropertyToTextList(display, &name, &list, &count) >= 0 and
+            count > 0)
+        {
+            const src_title = std.mem.span(
+                @as([*:0]const u8, @ptrCast(list[0])),
+            );
+            const title_len = @min(src_title.len, client.name.len - 1);
+            @memcpy(client.name[0..title_len], src_title[0..title_len]);
+            client.name[title_len] = 0;
+            _ = C.XFreeStringList(list);
+        }
+    }
+
+    _ = C.XFree(name.value);
+}
+
+fn winnext() void {
+    if (cl.items.len == 0) return;
+
+    if (sel) |idx| {
+        if (idx + 1 < cl.items.len) {
+            focus(idx + 1);
+        } else {
+            focus(0);
+        }
+    } else {
+        focus(0);
+    }
+}
+
+fn winprev() void {
+    if (cl.items.len == 0) return;
+
+    if (sel) |idx| {
+        if (idx > 0) {
+            focus(idx - 1);
+        } else {
+            focus(cl.items.len - 1);
+        }
+    } else {
+        focus(0);
+    }
+}
+
+extern fn execvp(prog: [*:0]const u8, argv: [*]const ?[*:0]const u8) c_int;
+extern fn setsid() c_int;
+
+fn spawn(cmd: [*:0]const u8) void {
+    const pid = posix.fork() catch return;
+    if (pid == 0) {
+        _ = std.c.close(C.ConnectionNumber(display));
+        _ = setsid();
+        var args = [_:null]?[*:0]const u8{ cmd, null };
+        _ = execvp(cmd, &args);
+        posix.exit(1);
+    }
+}
+
+fn xerr(_: ?*C.Display, event: [*c]C.XErrorEvent) callconv(.C) c_int {
+    if (@as(*C.XErrorEvent, @ptrCast(event)).error_code == C.BadAccess)
+        std.log.err("another wm running", .{});
+    return 0;
+}
+
+fn ignoreerr(_: ?*C.Display, _: [*c]C.XErrorEvent) callconv(.C) c_int {
+    return 0;
+}
+
+fn onconfig(e: *C.XConfigureRequestEvent) void {
+    wc.x = e.x;
+    wc.y = e.y;
+    wc.width = e.width;
+    wc.height = e.height;
+    wc.border_width = e.border_width;
+    wc.sibling = e.above;
+    wc.stack_mode = e.detail;
+
+    _ = C.XSetErrorHandler(ignoreerr);
+    _ = C.XConfigureWindow(display, e.window, @intCast(e.value_mask), &wc);
+    _ = C.XSync(display, 0);
+    _ = C.XSetErrorHandler(xerr);
+}
+
+fn onmap(allocator: std.mem.Allocator, event: *C.XEvent) !void {
+    const window: C.Window = event.xmaprequest.window;
+
+    var wa: C.XWindowAttributes = undefined;
+
+    if (C.XGetWindowAttributes(display, window, &wa) == 0) {
+        return;
+    }
+
+    if (wa.override_redirect != 0) {
+        return;
+    }
+
+    if (getclient(window) != null) {
+        return;
+    }
+
+    _ = C.XSetErrorHandler(ignoreerr);
+    _ = C.XSelectInput(
+        display,
+        window,
+        C.StructureNotifyMask | C.EnterWindowMask,
+    );
+    _ = C.XSetWindowBorderWidth(display, window, BORDER);
+    _ = C.XSetWindowBorder(display, window, NORMAL_COL);
+    _ = C.XSetErrorHandler(xerr);
+
+    const index = addclient(allocator, window) catch |err| {
+        _ = C.XSetErrorHandler(ignoreerr);
+        _ = C.XUnmapWindow(display, window);
+        _ = C.XSync(display, 0);
+        _ = C.XSetErrorHandler(xerr);
+        return err;
+    };
+
+    const client = &cl.items[index];
+
+    _ = C.XSetErrorHandler(ignoreerr);
+    const screen_width_i: c_int = @intCast(sw);
+    _ = C.XMoveWindow(display, window, client.x + 2 * screen_width_i, client.y);
+    _ = C.XMapWindow(display, window);
+    _ = C.XSetErrorHandler(xerr);
+
+    const data = [_]c_long{ C.NormalState, C.None };
+    _ = C.XChangeProperty(
+        display,
+        window,
+        atom_state,
+        atom_state,
+        32,
+        C.PropModeReplace,
+        @ptrCast(&data),
+        data.len,
+    );
+
+    tile();
+    focus(index);
+
+    if (client.isfloat) {
+        _ = C.XSetErrorHandler(ignoreerr);
+        _ = C.XRaiseWindow(display, client.win);
+        _ = C.XSetErrorHandler(xerr);
+        focus(index);
+    }
+
+    _ = C.XSync(display, 0);
+}
+
+fn manage(allocator: std.mem.Allocator, win: C.Window) void {
+    _ = C.XSelectInput(display, win, C.StructureNotifyMask | C.EnterWindowMask);
+    _ = C.XSetWindowBorderWidth(display, win, BORDER);
+    _ = C.XSetWindowBorder(display, win, NORMAL_COL);
+
+    if (addclient(allocator, win)) |idx| {
+        focus(idx);
+    } else |_| {}
+}
+
+fn onunmap(_: std.mem.Allocator, e: *C.XEvent) void {
+    const ev = &e.xunmap;
+    if (getclient(ev.window)) |index| {
+        if (ev.send_event == 1) {
+            const data = [_]c_long{ C.WithdrawnState, C.None };
+            _ = C.XChangeProperty(
+                display,
+                ev.window,
+                atom_state,
+                atom_state,
+                32,
+                C.PropModeReplace,
+                @ptrCast(&data),
+                data.len,
+            );
+        } else {
+            unmanage(index, false);
+        }
+    }
+}
+
+fn onkey(e: *C.XEvent) void {
+    if (e.xkey.keycode < keyacts.len) {
+        if (keyacts[e.xkey.keycode]) |action| action();
+    }
+}
+
+fn onenter(e: *C.XEvent) void {
+    for (cl.items) |client| {
+        if (client.isfloat) return;
+    }
+
+    if (e.xcrossing.mode != C.NotifyNormal or
+        e.xcrossing.detail == C.NotifyInferior)
+    {
+        return;
+    }
+
+    _ = C.XSetErrorHandler(ignoreerr);
+    defer _ = C.XSetErrorHandler(xerr);
+
+    if (getclient(e.xcrossing.window)) |index| {
+        if (sel) |sel_idx| {
+            if (sel_idx < cl.items.len and
+                cl.items[sel_idx].win == e.xcrossing.window)
+            {
+                return;
+            }
+        }
+
+        if (!cl.items[index].isfloat) {
+            focus(index);
+        }
+    }
+}
+
+fn onbtn(e: *C.XEvent) void {
+    if (e.xbutton.subwindow == 0) return;
+
+    _ = C.XSetErrorHandler(ignoreerr);
+    defer _ = C.XSetErrorHandler(xerr);
+
+    var attributes: C.XWindowAttributes = undefined;
+    if (C.XGetWindowAttributes(
+        display,
+        e.xbutton.subwindow,
+        &attributes,
+    ) == 0) {
+        return;
+    }
+
+    win_w = attributes.width;
+    win_h = attributes.height;
+    win_x = attributes.x;
+    win_y = attributes.y;
+
+    const grab_mask = C.ButtonReleaseMask | C.PointerMotionMask;
+
+    if (e.xbutton.button == 3 and (e.xbutton.state & C.Mod4Mask) != 0) {
+        const grab = C.XGrabPointer(
+            display,
+            e.xbutton.subwindow,
+            1,
+            grab_mask,
+            C.GrabModeAsync,
+            C.GrabModeAsync,
+            C.None,
+            cur_resize,
+            C.CurrentTime,
+        );
+        if (grab == C.GrabSuccess) {
+            _ = C.XWarpPointer(
+                display,
+                C.None,
+                e.xbutton.subwindow,
+                0,
+                0,
+                0,
+                0,
+                @intCast(attributes.width),
+                @intCast(attributes.height),
+            );
+
+            mouse = e.xbutton;
+            var dummy_root: C.Window = undefined;
+            var dummy_child: C.Window = undefined;
+            var new_root_x: c_int = undefined;
+            var new_root_y: c_int = undefined;
+            var win_x_pos: c_int = undefined;
+            var win_y_pos: c_int = undefined;
+            var mask: c_uint = undefined;
+
+            _ = C.XQueryPointer(
+                display,
+                e.xbutton.subwindow,
+                &dummy_root,
+                &dummy_child,
+                &new_root_x,
+                &new_root_y,
+                &win_x_pos,
+                &win_y_pos,
+                &mask,
+            );
+
+            mouse.x_root = @intCast(new_root_x);
+            mouse.y_root = @intCast(new_root_y);
+        }
+    } else if (e.xbutton.button == 1 and (e.xbutton.state & C.Mod4Mask) != 0) {
+        const grab = C.XGrabPointer(
+            display,
+            e.xbutton.subwindow,
+            1,
+            grab_mask,
+            C.GrabModeAsync,
+            C.GrabModeAsync,
+            C.None,
+            cur_move,
+            C.CurrentTime,
+        );
+        if (grab == C.GrabSuccess) {
+            mouse = e.xbutton;
+        }
+    }
+
+    if (getclient(e.xbutton.subwindow)) |index| {
+        if (sel == null or sel.? != index) {
+            focus(index);
+        }
+    }
+
+    _ = C.XSync(display, 0);
+}
+
+fn onmotion(e: *C.XEvent) void {
+    if (mouse.subwindow == 0) return;
+
+    const dx: i32 = @intCast(e.xbutton.x_root - mouse.x_root);
+    const dy: i32 = @intCast(e.xbutton.y_root - mouse.y_root);
+
+    const button: i32 = @intCast(mouse.button);
+
+    if (button == 1) {
+        _ = C.XMoveWindow(display, mouse.subwindow, win_x + dx, win_y + dy);
+    } else if (button == 3) {
+        const new_w: c_int = @intCast(@max(10, win_w + dx));
+        const new_h: c_int = @intCast(@max(10, win_h + dy));
+        _ = C.XMoveResizeWindow(
+            display,
+            mouse.subwindow,
+            win_x,
+            win_y,
+            todim(new_w),
+            todim(new_h),
+        );
+    }
+    _ = C.XSync(display, 0);
+}
+
+fn ondestroy(_: std.mem.Allocator, e: *C.XEvent) void {
+    const ev = &e.xdestroywindow;
+    if (getclient(ev.window)) |index| {
+        unmanage(index, true);
+    } else {
+        purgehidden(ev.window);
+    }
+}
+
+fn onbtnup(_: *C.XEvent) void {
+    if (mouse.subwindow != 0) {
+        _ = C.XUngrabPointer(display, C.CurrentTime);
+    }
+    mouse.subwindow = 0;
+}
+
+var shouldquit = false;
+var display: *C.Display = undefined;
+var root: C.Window = undefined;
+var wc: C.XWindowChanges = undefined;
+var lists = false;
+var cur = false;
+var disp = false;
+var barset = false;
+
+var win_x: i32 = 0;
+var win_y: i32 = 0;
+var win_w: i32 = 0;
+var win_h: i32 = 0;
+var mouse: C.XButtonEvent = undefined;
+
+fn setbar(allocator: std.mem.Allocator, screen: c_int) !void {
+    if (bar != null) return;
+
+    const bar_ptr = allocator.create(bar_mod.Bar) catch |err| return err;
+    bar_ptr.* = bar_mod.Bar.init(
+        allocator,
+        display,
+        root,
+        screen,
+        sw,
+        sx_offset,
+        sy_offset,
+        sh,
+        .top,
+        bar_font,
+        delim,
+    ) catch |err| {
+        allocator.destroy(bar_ptr);
+        return err;
+    };
+    bar_mod.setGlobalBar(bar_ptr);
+    bar = bar_ptr;
+    barset = true;
+    try bar_ptr.addBat(30000);
+    try bar_ptr.addWin(100);
+    try bar_ptr.addClock(1000);
+}
+
+fn setup(allocator: std.mem.Allocator) !void {
+    errdefer cleanup();
+
+    cl = std.ArrayList(Client).init(allocator);
+    hidden = std.ArrayList(Client).init(allocator);
+    stack = std.ArrayList(C.Window).init(allocator);
+    lists = true;
+
+    display = C.XOpenDisplay(0) orelse {
+        std.c._exit(1);
+    };
+    disp = true;
+
+    cur_resize = C.XCreateFontCursor(display, 120);
+    cur_move = C.XCreateFontCursor(display, 52);
+    cur_normal = C.XCreateFontCursor(display, 68);
+    cur = true;
+
+    const screen = C.DefaultScreen(display);
+    root = C.RootWindow(display, screen);
+
+    sw = @intCast(C.XDisplayWidth(display, screen));
+    sh = @intCast(C.XDisplayHeight(display, screen));
+    sx_offset = 0;
+    sy_offset = 0;
+    srotation = 0;
+
+    // setup atoms
+    atom_proto = C.XInternAtom(display, "WM_PROTOCOLS", 0);
+    atom_del = C.XInternAtom(display, "WM_DELETE_WINDOW", 0);
+    atom_state = C.XInternAtom(display, "WM_STATE", 0);
+    atom_net = C.XInternAtom(display, "_NET_SUPPORTED", 0);
+    atom_name = C.XInternAtom(display, "_NET_WM_NAME", 0);
+    atom_active = C.XInternAtom(display, "_NET_ACTIVE_WINDOW", 0);
+
+    const supported_atoms = [_]C.Atom{ atom_net, atom_name, atom_active };
+    _ = C.XChangeProperty(
+        display,
+        root,
+        atom_net,
+        C.XA_ATOM,
+        32,
+        C.PropModeReplace,
+        @ptrCast(&supported_atoms),
+        supported_atoms.len,
+    );
+
+    try initrandr(allocator);
+
+    try setbar(allocator, screen);
+
+    _ = C.XSetErrorHandler(xerr);
+    _ = C.XSelectInput(
+        display,
+        root,
+        C.SubstructureRedirectMask | C.EnterWindowMask,
+    );
+    _ = C.XDefineCursor(display, root, C.XCreateFontCursor(display, 68));
+
+    grabinputs(root);
+    setkeys();
+    sigchldignore();
+
+    runautostart();
+
+    syncmon(allocator);
+    if (bar) |b_ptr| {
+        b_ptr.resize(sw, sx_offset, sy_offset, sh);
+        b_ptr.mark();
+    }
+
+    _ = C.XSync(display, 0);
+
+    scan(allocator);
+
+    if (bar) |b_ptr| {
+        const now_ms = std.time.milliTimestamp();
+        _ = b_ptr.tick(now_ms) catch {};
+        b_ptr.draw();
+    }
+}
+
+fn cleanup() void {
+    if (barset) {
+        if (bar) |b_ptr| {
+            b_ptr.deinit();
+            alloc.destroy(b_ptr);
+        }
+        bar = null;
+        barset = false;
+    }
+
+    if (cur and disp) {
+        _ = C.XFreeCursor(display, cur_resize);
+        _ = C.XFreeCursor(display, cur_move);
+        _ = C.XFreeCursor(display, cur_normal);
+        cur = false;
+    }
+
+    if (disp) {
+        _ = C.XCloseDisplay(display);
+        disp = false;
+    }
+
+    if (lists) {
+        stack.deinit();
+        hidden.deinit();
+        cl.deinit();
+        lists = false;
+    }
+}
+
+fn run(allocator: std.mem.Allocator) !void {
+    var event: C.XEvent = undefined;
+
+    while (!shouldquit) {
+        const pending = C.XPending(display);
+        if (pending == 0) {
+            var sleep_ns: u64 = 50 * NS_PER_MS;
+            if (bar) |b_ptr| {
+                const now_ms = std.time.milliTimestamp();
+                _ = b_ptr.tick(now_ms) catch {};
+                if (b_ptr.dirty) b_ptr.draw();
+                const wait_ms = b_ptr.nextDelay(now_ms);
+                const desired_ns = wait_ms * NS_PER_MS;
+                if (desired_ns < sleep_ns) {
+                    sleep_ns = desired_ns;
+                }
+            }
+            if (sleep_ns > 0) {
+                std.time.sleep(sleep_ns);
+            }
+            continue;
+        }
+
+        _ = C.XNextEvent(display, &event);
+
+        switch (event.type) {
+            C.MapRequest => try onmap(allocator, &event),
+            C.UnmapNotify => onunmap(allocator, &event),
+            C.KeyPress => onkey(&event),
+            C.ButtonPress => onbtn(&event),
+            C.ButtonRelease => onbtnup(&event),
+            C.MotionNotify => onmotion(&event),
+            C.DestroyNotify => ondestroy(allocator, &event),
+            C.ConfigureRequest => onconfig(@ptrCast(&event)),
+            C.EnterNotify => onenter(&event),
+            C.Expose => {
+                if (bar) |b_ptr| {
+                    if (event.xexpose.window == b_ptr.win and
+                        event.xexpose.count == 0)
+                    {
+                        b_ptr.mark();
+                        b_ptr.draw();
+                    }
+                }
+            },
+            else => {
+                if (rr_event != 0) {
+                    if (event.type == rr_event + C.RRScreenChangeNotify) {
+                        try onrrscreen(allocator, &event);
+                    } else if (event.type == rr_event + C.RRNotify) {
+                        try onrr(allocator, &event);
+                    }
+                }
+            },
+        }
+
+        if (bar) |b_ptr| {
+            const now_ms = std.time.milliTimestamp();
+            _ = b_ptr.tick(now_ms) catch {};
+            if (b_ptr.dirty) b_ptr.draw();
+        }
+    }
+}
+
+pub fn main() !void {
+    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+    const allocator = gpa.allocator();
+    defer _ = gpa.deinit();
+    alloc = allocator;
+
+    try setup(allocator);
+    defer cleanup();
+
+    try run(allocator);
+}