2zw

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

src/main.zig

// MIT License

// Copyright (c) 2025 Sebastian <sebastian.michalk@pm.me>

// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

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);
}