Browse Source

add bar; drawing (with X11); work on randr function/logic, add time/date and bat module to bar

SM 9 months ago
parent
commit
3fe3447209
6 changed files with 996 additions and 89 deletions
  1. 3 3
      README.txt
  2. 1 0
      build.zig
  3. 507 0
      src/bar.zig
  4. 1 0
      src/c.zig
  5. 162 0
      src/draw.zig
  6. 322 86
      src/main.zig

+ 3 - 3
README.txt

@@ -1,5 +1,5 @@
 2zw
-===============
+===
 2zw is a fast, lean window manager for X written in Zig.
 
 It follows the 2wm design from suckless.org but is reworked in Zig for type safety while keeping the same minimalist behaviour.
@@ -7,13 +7,13 @@ The codebase now targets portable POSIX APIs so it builds cleanly on both Linux
 
 Features
 --------
-- Small hackable codebase (~1k LOC)
+- Small hackable codebase (~1.8k LOC)
 - Master/stack tiling
 - Attach/detach instead of workspaces
 - No configuration file parsing, configured via source
 - Floating window support
 - Focus border colouring
-- No status bar, no extras
+- Bar (bat, date/time)
 - Only depends on X11 and Zig std
 - Client state stored in contiguous arrays for better cache use
 

+ 1 - 0
build.zig

@@ -13,6 +13,7 @@ pub fn build(b: *std.Build) void {
     exe.linkLibC();
     exe.linkSystemLibrary("X11");
     exe.linkSystemLibrary("Xrandr");
+    exe.linkSystemLibrary("Xft");
 
     if (target.result.os.tag == .openbsd) {
         exe.addSystemIncludePath(lazyAbsolutePath("/usr/X11R6/include"));

+ 507 - 0
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;
+}

+ 1 - 0
src/c.zig

@@ -6,4 +6,5 @@ pub usingnamespace @cImport({
     @cInclude("X11/Xatom.h");
     @cInclude("X11/Xutil.h");
     @cInclude("X11/extensions/Xrandr.h");
+    @cInclude("X11/Xft/Xft.h");
 });

+ 162 - 0
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);
+    }
+};

+ 322 - 86
src/main.zig

@@ -1,6 +1,7 @@
 const std = @import("std");
 const C = @import("c.zig");
 const posix = std.posix;
+const bar_mod = @import("bar.zig");
 
 const Client = struct {
     name: [256]u8 = std.mem.zeroes([256]u8),
@@ -49,6 +50,8 @@ var nmaster: usize = 1;
 const terminal = "st";
 const launcher = "zmen";
 const autostart = "/home/smi/.scripts/ewm.sh";
+const bar_font: [*:0]const u8 = "monospace:size=15";
+const delim = "::";
 
 var cur_resize: C.Cursor = undefined;
 var cur_move: C.Cursor = undefined;
@@ -163,7 +166,9 @@ fn restack() void {
     _ = C.XSync(display, 0);
 }
 
-fn initKeyMap(allocator: std.mem.Allocator) !std.AutoHashMap(c_uint, *const fn () void) {
+fn initKeyMap(
+    allocator: std.mem.Allocator,
+) !std.AutoHashMap(c_uint, *const fn () void) {
     var map = std.AutoHashMap(c_uint, *const fn () void).init(allocator);
     errdefer map.deinit();
     for (keys) |key| {
@@ -187,7 +192,9 @@ fn grabInput(window: C.Window) void {
         );
     }
     for ([_]u8{ 1, 3 }) |btn| {
-        const mask = C.ButtonPressMask | C.ButtonReleaseMask | C.PointerMotionMask;
+        const mask = C.ButtonPressMask |
+            C.ButtonReleaseMask |
+            C.PointerMotionMask;
         _ = C.XGrabButton(
             display,
             btn,
@@ -224,7 +231,11 @@ fn show(allocator: std.mem.Allocator) void {
 
         client.border = BORDER;
         _ = C.XSetErrorHandler(ignoreError);
-        _ = C.XSelectInput(display, client.win, C.StructureNotifyMask | C.EnterWindowMask);
+        _ = C.XSelectInput(
+            display,
+            client.win,
+            C.StructureNotifyMask | C.EnterWindowMask,
+        );
         _ = C.XSetWindowBorderWidth(display, client.win, BORDER);
         _ = C.XSetWindowBorder(display, client.win, NORMAL_COL);
         _ = C.XSetErrorHandler(handleError);
@@ -296,7 +307,14 @@ fn scan(allocator: std.mem.Allocator) void {
     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) {
+    if (C.XQueryTree(
+        display,
+        root,
+        &root_return,
+        &parent_return,
+        &children,
+        &num_children,
+    ) != 0) {
         if (num_children > 0) {
             for (0..num_children) |i| {
                 const win = children[i];
@@ -307,8 +325,15 @@ fn scan(allocator: std.mem.Allocator) void {
                 const status = C.XGetWindowAttributes(display, win, &wa);
                 _ = C.XSetErrorHandler(handleError);
 
-                if (status != 0 and wa.map_state == C.IsViewable and wa.override_redirect == 0) {
-                    _ = C.XSelectInput(display, win, C.StructureNotifyMask | C.EnterWindowMask);
+                if (status != 0 and
+                    wa.map_state == C.IsViewable and
+                    wa.override_redirect == 0)
+                {
+                    _ = C.XSelectInput(
+                        display,
+                        win,
+                        C.StructureNotifyMask | C.EnterWindowMask,
+                    );
                     _ = C.XSetWindowBorderWidth(display, win, BORDER);
 
                     if (addClient(allocator, win)) |index| {
@@ -360,7 +385,12 @@ fn floatwins() void {
     attachStack(focus_client.win) catch {};
 
     _ = C.XSetErrorHandler(ignoreError);
-    _ = C.XSetInputFocus(display, focus_client.win, C.RevertToParent, C.CurrentTime);
+    _ = C.XSetInputFocus(
+        display,
+        focus_client.win,
+        C.RevertToParent,
+        C.CurrentTime,
+    );
     _ = C.XSetWindowBorder(display, focus_client.win, FOCUS_COL);
     _ = C.XChangeProperty(
         display,
@@ -379,7 +409,7 @@ fn floatwins() void {
 }
 
 inline fn toDim(v: c_int) c_uint {
-    return @as(c_uint, @intCast(@max(0, v)));
+    return @intCast(@max(0, v));
 }
 
 fn tile() void {
@@ -395,6 +425,20 @@ fn tile() void {
 
     const scr_w: c_int = @intCast(sw);
     const scr_h: c_int = @intCast(sh);
+    const monitor_x: c_int = sx_offset;
+    const monitor_y: c_int = sy_offset;
+    var top_bar_offset: c_int = 0;
+    var bottom_bar_offset: c_int = 0;
+    if (bar) |b_ptr| {
+        const height = b_ptr.barH();
+        switch (b_ptr.pos) {
+            .top => top_bar_offset = height,
+            .bottom => bottom_bar_offset = height,
+        }
+    }
+    const layout_origin_y: c_int = monitor_y + top_bar_offset;
+    const available_h_total: c_int = scr_h - top_bar_offset - bottom_bar_offset;
+    if (available_h_total <= 0) return;
     const border2: c_int = 2 * @as(c_int, BORDER);
     const gap2: c_int = 2 * GAP;
     const frame_w = border2 + gap2;
@@ -402,23 +446,36 @@ fn tile() void {
 
     const used_mw = @min(nmaster, tilable);
     const stack_n = tilable - used_mw;
-    const base_mw = @as(c_int, @intFromFloat(@as(f32, @floatFromInt(scr_w)) * mfact));
+    const base_mw = @as(
+        c_int,
+        @intFromFloat(@as(f32, @floatFromInt(scr_w)) * mfact),
+    );
     const master_w = if (stack_n == 0) scr_w else base_mw;
 
-    const master_gap = @as(c_int, @intCast((used_mw + 1) * GAP));
+    const used_mw_ci: c_int = @intCast(used_mw);
+    const master_gap = (used_mw_ci + 1) * GAP;
+    const master_span = @max(available_h_total - frame_h, @as(c_int, 1));
     const master_h = if (used_mw <= 1)
-        scr_h - frame_h
+        master_span
     else
-        @divTrunc(scr_h - master_gap, @as(c_int, @intCast(used_mw)));
+        @divTrunc(
+            @max(available_h_total - master_gap, @as(c_int, 1)),
+            used_mw_ci,
+        );
 
+    const stack_n_ci: c_int = @intCast(stack_n);
     const stack_gap = if (stack_n <= 1)
         0
     else
-        @as(c_int, @intCast((stack_n + 1) * GAP));
+        (stack_n_ci + 1) * GAP;
+    const stack_span = @max(available_h_total - frame_h, @as(c_int, 1));
     const stack_h = if (stack_n <= 1)
-        scr_h - frame_h
+        stack_span
     else
-        @divTrunc(scr_h - stack_gap, @as(c_int, @intCast(stack_n)));
+        @divTrunc(
+            @max(available_h_total - stack_gap, @as(c_int, 1)),
+            stack_n_ci,
+        );
     const stack_w = scr_w - master_w - GAP;
 
     var master_idx: usize = 0;
@@ -429,10 +486,11 @@ fn tile() void {
         const ptr = &cl.items[i];
 
         if (master_idx < used_mw) {
+            const idx_offset: c_int = @intCast(master_idx);
             const y = if (used_mw <= 1)
-                GAP
+                layout_origin_y + GAP
             else
-                GAP + @as(c_int, @intCast(master_idx)) * (master_h + GAP);
+                layout_origin_y + GAP + idx_offset * (master_h + GAP);
             const inner_w = master_w - frame_w;
             const inner_h = if (used_mw <= 1)
                 master_h
@@ -442,7 +500,7 @@ fn tile() void {
             _ = C.XMoveResizeWindow(
                 display,
                 ptr.win,
-                GAP,
+                monitor_x + GAP,
                 y,
                 toDim(inner_w),
                 toDim(inner_h),
@@ -450,10 +508,11 @@ fn tile() void {
             master_idx += 1;
         } else {
             const pos = stack_idx;
+            const pos_offset: c_int = @intCast(pos);
             const y = if (stack_n <= 1)
-                GAP
+                layout_origin_y + GAP
             else
-                GAP + @as(c_int, @intCast(pos)) * (stack_h + GAP);
+                layout_origin_y + GAP + pos_offset * (stack_h + GAP);
             const inner_w = stack_w - GAP - border2;
             const inner_h = if (stack_n <= 1)
                 stack_h
@@ -463,7 +522,7 @@ fn tile() void {
             _ = C.XMoveResizeWindow(
                 display,
                 ptr.win,
-                master_w + GAP,
+                monitor_x + master_w + GAP,
                 y,
                 toDim(inner_w),
                 toDim(inner_h),
@@ -519,14 +578,19 @@ fn isfloat(window: C.Window) bool {
         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)));
+            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);
+            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);
         }
 
@@ -565,14 +629,29 @@ fn isfloat(window: C.Window) bool {
             actual_type == C.XA_ATOM and actual_format == 32 and nitems > 0;
 
         if (got_atoms and good_format) {
-            const atoms = @as([*]C.Atom, @alignCast(@ptrCast(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);
+            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) {
+                if (atom == atom_dialog or
+                    atom == atom_utility or
+                    atom == atom_popup)
+                {
                     _ = C.XFree(prop_return);
                     _ = C.XSetErrorHandler(handleError);
                     return true;
@@ -591,7 +670,13 @@ fn isfloat(window: C.Window) bool {
 fn focus(target_index_opt: ?usize) void {
     if (cl.items.len == 0) {
         sel = null;
-        _ = C.XSetInputFocus(display, root, C.RevertToPointerRoot, C.CurrentTime);
+        if (bar) |b_ptr| b_ptr.setSel(null);
+        _ = C.XSetInputFocus(
+            display,
+            root,
+            C.RevertToPointerRoot,
+            C.CurrentTime,
+        );
         _ = C.XFlush(display);
         return;
     }
@@ -600,12 +685,22 @@ fn focus(target_index_opt: ?usize) void {
         if (sel) |sel_idx| {
             if (sel_idx < cl.items.len) {
                 _ = C.XSetErrorHandler(ignoreError);
-                _ = C.XSetWindowBorder(display, cl.items[sel_idx].win, NORMAL_COL);
+                _ = C.XSetWindowBorder(
+                    display,
+                    cl.items[sel_idx].win,
+                    NORMAL_COL,
+                );
                 _ = C.XSetErrorHandler(handleError);
             }
         }
         sel = null;
-        _ = C.XSetInputFocus(display, root, C.RevertToPointerRoot, C.CurrentTime);
+        if (bar) |b_ptr| b_ptr.setSel(null);
+        _ = C.XSetInputFocus(
+            display,
+            root,
+            C.RevertToPointerRoot,
+            C.CurrentTime,
+        );
         _ = C.XFlush(display);
         return;
     };
@@ -645,6 +740,7 @@ fn focus(target_index_opt: ?usize) void {
     _ = C.XSetErrorHandler(handleError);
 
     sel = index;
+    if (bar) |b_ptr| b_ptr.setSel(client.win);
 
     _ = C.XFlush(display);
 }
@@ -742,12 +838,17 @@ fn unmanage(index: usize, destroyed: bool) void {
 
 var sw: c_uint = 0;
 var sh: c_uint = 0;
-var cw: c_uint = 0;
-var ch: 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;
@@ -762,13 +863,17 @@ fn initRandR(allocator: std.mem.Allocator) !void {
 }
 
 fn geom(allocator: std.mem.Allocator) !void {
-    const res = C.XRRGetScreenResources(display, root);
+    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));
-        cw = @divTrunc((3 * sw), 5);
-        ch = sh - 20;
+        sx_offset = 0;
+        sy_offset = 0;
+        srotation = 0;
         return;
     }
     defer C.XRRFreeScreenResources(res);
@@ -777,7 +882,12 @@ fn geom(allocator: std.mem.Allocator) !void {
 
     const primary_output = C.XRRGetOutputPrimary(display, root);
     if (primary_output != 0) {
-        found_active_monitor = try tryUseMonitor(allocator, res, primary_output, "primary");
+        found_active_monitor = try tryUseMonitor(
+            allocator,
+            res,
+            primary_output,
+            "primary",
+        );
     }
 
     if (!found_active_monitor) {
@@ -788,12 +898,9 @@ fn geom(allocator: std.mem.Allocator) !void {
         const x_screen = C.DefaultScreen(display);
         sw = @intCast(C.XDisplayWidth(display, x_screen));
         sh = @intCast(C.XDisplayHeight(display, x_screen));
-        cw = @divTrunc((3 * sw), 5);
-        ch = sh - 20;
-    }
-
-    if (cl.items.len > 0 and sw > 0 and sh > 0) {
-        tile();
+        sx_offset = 0;
+        sy_offset = 0;
+        srotation = 0;
     }
 }
 
@@ -803,6 +910,7 @@ fn tryUseMonitor(
     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);
@@ -821,16 +929,21 @@ fn tryUseMonitor(
 
     sw = @intCast(crtc_info.*.width);
     sh = @intCast(crtc_info.*.height);
-    cw = @divTrunc((3 * sw), 5);
-    ch = sh - 20;
-
-    const output_name = std.mem.span(@as([*:0]const u8, @ptrCast(output_info.*.name)));
-    const log_msg = try std.fmt.allocPrint(
-        allocator,
-        "Using {s} monitor: {s} ({d}x{d})",
-        .{ monitor_type, output_name, sw, sh },
+    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)),
     );
-    defer allocator.free(log_msg);
+    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;
 }
@@ -839,13 +952,21 @@ fn findLargestMonitor(_: 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;
 
-    for (0..@intCast(res.*.noutput)) |i| {
+    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;
+        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;
@@ -853,22 +974,26 @@ fn findLargestMonitor(_: std.mem.Allocator, res: *C.XRRScreenResources) !bool {
 
         if (crtc_info.*.width == 0 or crtc_info.*.height == 0) continue;
 
-        const output_width = @as(c_uint, @intCast(crtc_info.*.width));
-        const output_height = @as(c_uint, @intCast(crtc_info.*.height));
+        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;
-        cw = @divTrunc((3 * sw), 5);
-        ch = sh - 20;
+        sx_offset = largest_x;
+        sy_offset = largest_y;
+        srotation = largest_rotation;
         return true;
     }
 
@@ -883,17 +1008,26 @@ fn onRRNotify(allocator: std.mem.Allocator, e: *C.XEvent) !void {
     const rrev = @as(*C.XRRNotifyEvent, @ptrCast(e));
 
     switch (rrev.subtype) {
-        C.RRNotify_OutputChange, C.RRNotify_CrtcChange => {
-            const old_w = sw;
-            const old_h = sh;
+        C.RRNotify_OutputChange,
+        C.RRNotify_CrtcChange,
+        => try refreshMonitor(allocator),
+        else => {},
+    }
+}
 
-            try geom(allocator);
+fn onRRScreenChange(allocator: std.mem.Allocator, e: *C.XEvent) !void {
+    _ = C.XRRUpdateConfiguration(e);
+    try refreshMonitor(allocator);
+}
 
-            if (old_w != sw or old_h != sh and cl.items.len > 0) {
-                tile();
-            }
-        },
-        else => {},
+fn refreshMonitor(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();
     }
 }
 
@@ -966,7 +1100,8 @@ fn isProtoDel(client_index: usize) bool {
     var ret = false;
 
     if (C.XGetWMProtocols(display, client.win, &protocols, &n) != 0) {
-        for (0..@intCast(n)) |i| {
+        const hint_count: usize = @intCast(n);
+        for (0..hint_count) |i| {
             if (protocols[i] == atom_del) {
                 ret = true;
                 break;
@@ -993,7 +1128,9 @@ 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) {
+    if (C.XGetWMNormalHints(display, client.win, &size, &msize) == 0 or
+        size.flags == 0)
+    {
         size.flags = C.PSize;
     }
 
@@ -1069,8 +1206,12 @@ fn title(_: std.mem.Allocator, client: *Client) void {
         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])));
+        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;
@@ -1204,7 +1345,11 @@ fn onmap(allocator: std.mem.Allocator, event: *C.XEvent) !void {
     }
 
     _ = C.XSetErrorHandler(ignoreError);
-    _ = C.XSelectInput(display, window, C.StructureNotifyMask | C.EnterWindowMask);
+    _ = C.XSelectInput(
+        display,
+        window,
+        C.StructureNotifyMask | C.EnterWindowMask,
+    );
     _ = C.XSetWindowBorderWidth(display, window, BORDER);
     _ = C.XSetWindowBorder(display, window, NORMAL_COL);
     _ = C.XSetErrorHandler(handleError);
@@ -1220,7 +1365,8 @@ fn onmap(allocator: std.mem.Allocator, event: *C.XEvent) !void {
     const client = &cl.items[index];
 
     _ = C.XSetErrorHandler(ignoreError);
-    _ = C.XMoveWindow(display, window, client.x + 2 * @as(c_int, @intCast(sw)), client.y);
+    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(handleError);
 
@@ -1279,7 +1425,9 @@ fn onenter(e: *C.XEvent) void {
         if (client.isfloat) return;
     }
 
-    if (e.xcrossing.mode != C.NotifyNormal or e.xcrossing.detail == C.NotifyInferior) {
+    if (e.xcrossing.mode != C.NotifyNormal or
+        e.xcrossing.detail == C.NotifyInferior)
+    {
         return;
     }
 
@@ -1288,7 +1436,9 @@ fn onenter(e: *C.XEvent) void {
 
     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) {
+            if (sel_idx < cl.items.len and
+                cl.items[sel_idx].win == e.xcrossing.window)
+            {
                 return;
             }
         }
@@ -1306,7 +1456,11 @@ fn onbtn(e: *C.XEvent) void {
     defer _ = C.XSetErrorHandler(handleError);
 
     var attributes: C.XWindowAttributes = undefined;
-    if (C.XGetWindowAttributes(display, e.xbutton.subwindow, &attributes) == 0) {
+    if (C.XGetWindowAttributes(
+        display,
+        e.xbutton.subwindow,
+        &attributes,
+    ) == 0) {
         return;
     }
 
@@ -1476,8 +1630,9 @@ pub fn main() !void {
 
     sw = @intCast(C.XDisplayWidth(display, screen));
     sh = @intCast(C.XDisplayHeight(display, screen));
-    cw = @divTrunc((3 * sw), 5);
-    ch = sh - 20;
+    sx_offset = 0;
+    sy_offset = 0;
+    srotation = 0;
 
     atom_proto = C.XInternAtom(display, "WM_PROTOCOLS", 0);
     atom_del = C.XInternAtom(display, "WM_DELETE_WINDOW", 0);
@@ -1500,8 +1655,41 @@ pub fn main() !void {
 
     try initRandR(allocator);
 
+    if (bar == null) {
+        const bar_ptr = try allocator.create(bar_mod.Bar);
+        errdefer allocator.destroy(bar_ptr);
+        bar_ptr.* = try bar_mod.Bar.init(
+            allocator,
+            display,
+            root,
+            screen,
+            sw,
+            sx_offset,
+            sy_offset,
+            sh,
+            .top,
+            bar_font,
+            delim,
+        );
+        errdefer bar_ptr.*.deinit();
+        try bar_ptr.addBat(30000);
+        try bar_ptr.addWin(100);
+        try bar_ptr.addClock(1000);
+        bar_mod.setGlobalBar(bar_ptr);
+        bar = bar_ptr;
+    }
+
+    defer if (bar) |b_ptr| {
+        b_ptr.deinit();
+        allocator.destroy(b_ptr);
+    };
+
     _ = C.XSetErrorHandler(handleError);
-    _ = C.XSelectInput(display, root, C.SubstructureRedirectMask | C.EnterWindowMask);
+    _ = C.XSelectInput(
+        display,
+        root,
+        C.SubstructureRedirectMask | C.EnterWindowMask,
+    );
     _ = C.XDefineCursor(display, root, C.XCreateFontCursor(display, 68));
 
     grabInput(root);
@@ -1510,12 +1698,43 @@ pub fn main() !void {
     run();
 
     syncmon(allocator);
+    if (bar) |b_ptr| {
+        b_ptr.resize(sw, sx_offset, sy_offset, sh);
+        b_ptr.mark();
+    }
 
     _ = C.XSync(display, 0);
 
     scan(allocator);
 
-    while (!shouldQuit and C.XNextEvent(display, &event) == 0) {
+    if (bar) |b_ptr| {
+        const now_ms = std.time.milliTimestamp();
+        _ = b_ptr.tick(now_ms) catch {};
+        b_ptr.draw();
+    }
+
+    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),
@@ -1526,15 +1745,32 @@ pub fn main() !void {
             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 and
-                    (event.type == rr_event + C.RRScreenChangeNotify or
-                        event.type == rr_event + C.RRNotify))
-                {
-                    try onRRNotify(allocator, &event);
+                if (rr_event != 0) {
+                    if (event.type == rr_event + C.RRScreenChangeNotify) {
+                        try onRRScreenChange(allocator, &event);
+                    } else if (event.type == rr_event + C.RRNotify) {
+                        try onRRNotify(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();
+        }
     }
 
     keymap.deinit();