2zw

2zw - X11 Windowmanager

Files | Log | Commits | Refs | README


src/main.zig

Size: 34136 bytes

const std = @import("std");
const C = @import("c.zig");

const Client = struct {
    full: bool,
    wx: c_int,
    wy: c_int,
    ww: c_int,
    wh: c_int,
    w: C.Window,
    is_floating: bool = false,
};

const L = std.DoublyLinkedList(Client);
var hl = L{};
var list = L{};
var cursor: ?*L.Node = null;
var previously_focused: ?*L.Node = undefined;

const FOCUS_BORDER_COLOR = 0x6699cc;
const NORMAL_BORDER_COLOR = 0x333333;
const BORDER_WIDTH = 1;
const GAP_SIZE = 0;
const MASTER_FACTOR = 0.6;

var master_factor: f32 = MASTER_FACTOR;
var n_master: usize = 1;

const terminal = "st";
const launcher = "zmen";
const autostart_path = "/home/smi/.scripts/ewm.sh";

var resize_cursor: C.Cursor = undefined;
var move_cursor: C.Cursor = undefined;
var normal_cursor: C.Cursor = undefined;

const Key = struct {
    keysym: C.KeySym,
    action: *const fn () void,
};

var keymap: std.AutoHashMap(c_uint, *const fn () void) = undefined;

const keys = [_]Key{
    .{ .keysym = C.XK_q, .action = &killClient },
    .{ .keysym = C.XK_comma, .action = &winPrev },
    .{ .keysym = C.XK_period, .action = &winNext },
    .{ .keysym = C.XK_a, .action = &attachWindow },
    .{ .keysym = C.XK_d, .action = &detachWindow },
    .{ .keysym = C.XK_h, .action = struct {
        fn action() void {
            master_factor = @max(0.1, master_factor - 0.05);
            applyLayout();
        }
    }.action },
    .{
        .keysym = C.XK_l,
        .action = struct {
            fn action() void {
                master_factor = @min(0.9, master_factor + 0.05);
                applyLayout();
            }
        }.action,
    },

    .{ .keysym = C.XK_Return, .action = struct {
        fn action() void {
            spawn(terminal);
        }
    }.action },
    .{ .keysym = C.XK_p, .action = struct {
        fn action() void {
            spawn(launcher);
        }
    }.action },

    .{ .keysym = C.XK_s, .action = struct {
        fn action() void {
            spawn("slock");
        }
    }.action },

    // add more in the format:
    // .{ .keysym = C.XK_b, .action = struct {
    //     fn action() void { spawn("firefox"); }
    // }.action },
};

fn logError(msg: []const u8) void {
    const stderr = std.io.getStdErr().writer();
    stderr.print("Error: {s}\n", .{msg}) catch return;
}

fn logInfo(msg: []const u8) void {
    const stdInfo = std.io.getStdOut().writer();
    stdInfo.print("INFO: {s}\n", .{msg}) catch return;
}

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| {
        try map.put(C.XKeysymToKeycode(display, key.keysym), key.action);
    }
    return map;
}

fn grabInput(window: C.Window) void {
    _ = C.XUngrabKey(display, C.AnyKey, C.AnyModifier, root);

    for (keys) |key| {
        _ = C.XGrabKey(display, C.XKeysymToKeycode(display, key.keysym), C.Mod4Mask, window, 0, C.GrabModeAsync, C.GrabModeAsync);
    }
    for ([_]u8{ 1, 3 }) |btn| {
        _ = C.XGrabButton(display, btn, C.Mod4Mask, root, 0, C.ButtonPressMask | C.ButtonReleaseMask | C.PointerMotionMask, C.GrabModeAsync, C.GrabModeAsync, 0, 0);
    }
}

fn attachWindow() void {
    if (hl.len == 0) return;

    const node_to_attach = hl.first.?;

    hl.remove(node_to_attach);

    _ = C.XMapWindow(display, node_to_attach.data.w);
    _ = C.XSync(display, 0);

    list.append(node_to_attach);

    focus(node_to_attach);

    applyLayout();
}

fn detachWindow() void {
    if (cursor == null) return;

    const node_to_detach = cursor.?;

    if (cursor.?.next) |next| {
        focus(next);
    } else if (cursor.?.prev) |prev| {
        focus(prev);
    } else {
        cursor = null;
    }

    list.remove(node_to_detach);

    _ = C.XUnmapWindow(display, node_to_detach.data.w);
    _ = C.XSync(display, 0);

    hl.append(node_to_detach);

    applyLayout();
}

fn initialWin(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) {
        if (num_children > 0) {
            for (0..num_children) |i| {
                const win = children[i];

                var wa: C.XWindowAttributes = undefined;

                _ = C.XSetErrorHandler(ignoreError);
                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);
                    _ = C.XSetWindowBorderWidth(display, win, BORDER_WIDTH);

                    if (addClient(allocator, win)) |node| {
                        focus(node);
                    } else |err| {
                        const err_msg = std.fmt.allocPrint(allocator, "Error adding existing window: {any}", .{err}) catch "Error adding window";
                        defer if (@TypeOf(err_msg) == []const u8) allocator.free(err_msg);
                        logError(err_msg);
                    }
                }
            }

            _ = C.XFree(children);

            if (list.len > 0) {
                applyLayout();
            }
        }
    }
}

fn handleFloatingWindows() void {
    var node = list.first;
    while (node) |n| : (node = n.next) {
        if (n.data.is_floating) {
            _ = C.XRaiseWindow(display, n.data.w);

            if (cursor) |c| {
                _ = C.XSetWindowBorder(display, c.data.w, NORMAL_BORDER_COLOR);
            }
            _ = C.XSetWindowBorder(display, n.data.w, FOCUS_BORDER_COLOR);

            _ = C.XSetInputFocus(display, n.data.w, C.RevertToParent, C.CurrentTime);

            cursor = n;

            const net_active_window = C.XInternAtom(display, "_NET_ACTIVE_WINDOW", 0);
            _ = C.XChangeProperty(display, root, net_active_window, C.XA_WINDOW, 32, C.PropModeReplace, @ptrCast(&n.data.w), 1);

            _ = C.XFlush(display);
        }
    }
}

fn applyLayout() void {
    if (list.len == 0) return;

    handleFloatingWindows();

    var tiled_count: usize = 0;
    var next = list.first;
    while (next) |node| : (next = node.next) {
        if (!node.data.is_floating) {
            tiled_count += 1;
        }
    }

    if (tiled_count == 0) return;

    const sw: c_int = @intCast(screen_w);
    const sh: c_int = @intCast(screen_h);

    if (tiled_count == 1) {
        next = list.first;
        while (next) |node| : (next = node.next) {
            if (!node.data.is_floating) {
                _ = C.XMoveResizeWindow(display, node.data.w, GAP_SIZE, GAP_SIZE, @as(c_uint, @intCast(@max(0, screen_w - (2 * GAP_SIZE) - (2 * BORDER_WIDTH)))), @as(c_uint, @intCast(@max(0, screen_h - (2 * GAP_SIZE) - (2 * BORDER_WIDTH)))));
                break;
            }
        }
        return;
    }

    const master_width = @as(c_uint, @intCast(@as(c_int, @intFromFloat(master_factor * @as(f32, @floatFromInt(sw))))));

    var actual_masters: usize = 0;
    var masters_found: usize = 0;

    next = list.first;
    while (next) |node| : (next = node.next) {
        if (!node.data.is_floating) {
            if (masters_found < n_master) {
                if (actual_masters == 0) {
                    actual_masters = 1;

                    var temp = next;
                    while (temp) |t| : (temp = t.next) {
                        if (!t.data.is_floating and masters_found < n_master) {
                            masters_found += 1;
                        }
                    }
                }

                if (masters_found == 1) {
                    _ = C.XMoveResizeWindow(display, node.data.w, GAP_SIZE, GAP_SIZE, @as(c_uint, @intCast(@max(0, master_width - (2 * GAP_SIZE) - (2 * BORDER_WIDTH)))), @as(c_uint, @intCast(@max(0, sh - (2 * GAP_SIZE) - (2 * BORDER_WIDTH)))));
                } else {
                    const gap_total = @as(c_int, @intCast((masters_found + 1) * GAP_SIZE));
                    const master_height = @divTrunc(sh - gap_total, @as(c_int, @intCast(masters_found)));
                    const master_index = actual_masters - 1; // 0-based index
                    const y_position = GAP_SIZE + (@as(c_int, @intCast(master_index)) * (master_height + GAP_SIZE));

                    _ = C.XMoveResizeWindow(display, node.data.w, GAP_SIZE, y_position, @as(c_uint, @intCast(@max(0, master_width - (2 * GAP_SIZE) - (2 * BORDER_WIDTH)))), @as(c_uint, @intCast(@max(0, master_height - (2 * BORDER_WIDTH)))));
                }

                actual_masters += 1;
            }
        }
    }

    const stack_windows = tiled_count - masters_found;

    if (stack_windows > 0) {
        const stack_width = sw - @as(c_int, @intCast(master_width)) - GAP_SIZE;
        var stack_i: usize = 0;

        next = list.first;
        while (next) |node| : (next = node.next) {
            if (!node.data.is_floating) {
                var is_master = false;
                var master_idx: usize = 0;
                var check = list.first;
                while (check != null and master_idx < masters_found) {
                    if (check.? == node and !check.?.data.is_floating) {
                        is_master = true;
                        break;
                    }
                    if (!check.?.data.is_floating) {
                        master_idx += 1;
                    }
                    check = check.?.next;
                }

                if (!is_master) {
                    if (stack_windows == 1) {
                        _ = C.XMoveResizeWindow(display, node.data.w, @as(c_int, @intCast(master_width)) + GAP_SIZE, GAP_SIZE, @as(c_uint, @intCast(@max(0, stack_width - GAP_SIZE - (2 * BORDER_WIDTH)))), @as(c_uint, @intCast(@max(0, sh - (2 * GAP_SIZE) - (2 * BORDER_WIDTH)))));
                    } else {
                        const gap_total_stack = @as(c_int, @intCast((stack_windows + 1) * GAP_SIZE));
                        const stack_height = @divTrunc(sh - gap_total_stack, @as(c_int, @intCast(stack_windows)));
                        const y_position = GAP_SIZE + (@as(c_int, @intCast(stack_i)) * (stack_height + GAP_SIZE));

                        _ = C.XMoveResizeWindow(display, node.data.w, @as(c_int, @intCast(master_width)) + GAP_SIZE, y_position, @as(c_uint, @intCast(@max(0, stack_width - GAP_SIZE - (2 * BORDER_WIDTH)))), @as(c_uint, @intCast(@max(0, stack_height - (2 * BORDER_WIDTH)))));

                        stack_i += 1;
                    }
                }
            }
        }
    }
}

fn addClient(allocator: std.mem.Allocator, window: C.Window) !*L.Node {
    var attributes: C.XWindowAttributes = undefined;
    _ = C.XGetWindowAttributes(display, window, &attributes);

    const m_float = shouldFloat(window);

    const client = Client{
        .full = false,
        .wx = attributes.x,
        .wy = attributes.y,
        .ww = attributes.width,
        .wh = attributes.height,
        .w = window,
        .is_floating = m_float,
    };

    var node = try allocator.create(L.Node);
    node.data = client;
    list.append(node);

    return node;
}

fn shouldFloat(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(ignoreError);

    const net_wm_window_type = C.XInternAtom(display, "_NET_WM_WINDOW_TYPE", 0);
    if (net_wm_window_type != 0) {
        const status = C.XGetWindowProperty(display, window, net_wm_window_type, 0, 32, 0, C.XA_ATOM, &actual_type, &actual_format, &nitems, &bytes_after, &prop_return);

        if (status == 0 and prop_return != null and actual_type == C.XA_ATOM and actual_format == 32 and nitems > 0) {
            const atoms = @as([*]C.Atom, @alignCast(@ptrCast(prop_return)));

            for (0..nitems) |i| {
                const atom = atoms[i];
                const dialog_atom = C.XInternAtom(display, "_NET_WM_WINDOW_TYPE_DIALOG", 0);
                const utility_atom = C.XInternAtom(display, "_NET_WM_WINDOW_TYPE_UTILITY", 0);
                const popup_atom = C.XInternAtom(display, "_NET_WM_WINDOW_TYPE_POPUP_MENU", 0);

                if (atom == dialog_atom or atom == utility_atom or atom == popup_atom) {
                    _ = C.XFree(prop_return);
                    _ = C.XSetErrorHandler(handleError);
                    return true;
                }
            }

            _ = C.XFree(prop_return);
        }
    }

    _ = C.XSetErrorHandler(handleError);

    return false;
}

fn focus(node: ?*L.Node) void {
    if (list.len == 0) return;
    if (cursor) |c| _ = C.XSetWindowBorder(display, c.data.w, NORMAL_BORDER_COLOR);

    const target = node orelse previously_focused orelse list.first.?;
    previously_focused = cursor;

    _ = C.XSetInputFocus(
        display,
        target.data.w,
        C.RevertToParent,
        C.CurrentTime,
    );
    _ = C.XRaiseWindow(display, target.data.w);
    _ = C.XSetWindowBorder(display, target.data.w, FOCUS_BORDER_COLOR);

    cursor = target;
}

fn winToNode(w: C.Window) ?*L.Node {
    var next = list.first;
    while (next) |node| : (next = node.next) {
        if (node.data.w == w) return node;
    }
    return null;
}

fn unmanage(allocator: std.mem.Allocator, node: *L.Node, destroyed: bool) void {
    if (!destroyed) {
        _ = C.XGrabServer(display);
        _ = C.XSetErrorHandler(ignoreError);
        _ = C.XSelectInput(display, node.data.w, C.NoEventMask);
        _ = C.XUngrabButton(display, C.AnyButton, C.AnyModifier, node.data.w);
        _ = C.XSync(display, 0);
        _ = C.XSetErrorHandler(handleError);
        _ = C.XUngrabServer(display);
    }

    var next_focus: ?*L.Node = null;
    if (node == cursor) {
        if (node.next) |next| {
            next_focus = next;
        } else if (node.prev) |prev| {
            next_focus = prev;
        } else if (list.len > 1) {
            var temp = list.first;
            while (temp) |t| : (temp = t.next) {
                if (t != node) {
                    next_focus = t;
                    break;
                }
            }
        }
        cursor = null; 
    }

    if (previously_focused) |pf| {
        if (node.data.w == pf.data.w) previously_focused = null;
    }

    list.remove(node);
    allocator.destroy(node);

    if (list.len > 0) {
        applyLayout();
        if (next_focus) |focus_target| {
            focus(focus_target);
        } else {
            focus(list.first);
        }
    } else {
        cursor = null;
        previously_focused = null;
    }
}

var screen_w: c_uint = 0;
var screen_h: c_uint = 0;
var center_w: c_uint = 0;
var center_h: c_uint = 0;

var randr_event_base: c_int = 0;
var randr_error_base: c_int = 0;

fn initRandR(allocator: std.mem.Allocator) !void {
    if (C.XRRQueryExtension(display, &randr_event_base, &randr_error_base) == 0) {
        logError("RandR extension not available");
        return;
    }

    const base_msg = try std.fmt.allocPrint(allocator, "RandR extension initialized with event base: {d}, error base: {d}", .{ randr_event_base, randr_error_base });
    defer allocator.free(base_msg);
    logInfo(base_msg);

    var major: c_int = 0;
    var minor: c_int = 0;
    if (C.XRRQueryVersion(display, &major, &minor) != 0) {
        const ver_msg = try std.fmt.allocPrint(allocator, "RandR version: {d}.{d}", .{ major, minor });
        defer allocator.free(ver_msg);
        logInfo(ver_msg);
    }

    try updateScreenDimensions(allocator);

    _ = C.XRRSelectInput(display, root, C.RROutputChangeNotifyMask | C.RRCrtcChangeNotifyMask | C.RRScreenChangeNotifyMask);
}

fn updateScreenDimensions(allocator: std.mem.Allocator) !void {
    const res = C.XRRGetScreenResources(display, root);
    if (res == null) {
        logError("Failed to get screen resources");
        
        const x_screen = C.DefaultScreen(display);
        screen_w = @intCast(C.XDisplayWidth(display, x_screen));
        screen_h = @intCast(C.XDisplayHeight(display, x_screen));
        center_w = @divTrunc((3 * screen_w), 5);
        center_h = screen_h - 20;
        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 tryUseMonitor(allocator, res, primary_output, "primary");
    }

    if (!found_active_monitor) {
        found_active_monitor = try findLargestMonitor(allocator, res);
    }

    if (!found_active_monitor) {
        const x_screen = C.DefaultScreen(display);
        const old_w = screen_w;
        const old_h = screen_h;
        
        screen_w = @intCast(C.XDisplayWidth(display, x_screen));
        screen_h = @intCast(C.XDisplayHeight(display, x_screen));
        center_w = @divTrunc((3 * screen_w), 5);
        center_h = screen_h - 20;

        if (old_w != screen_w or old_h != screen_h) {
            const log_msg = try std.fmt.allocPrint(allocator, "Using X screen dimensions: {d}x{d}", .{ screen_w, screen_h });
            defer allocator.free(log_msg);
            logInfo(log_msg);
        }
    }

    if (list.len > 0 and screen_w > 0 and screen_h > 0) {
        applyLayout();
    }
}

fn tryUseMonitor(allocator: std.mem.Allocator, res: *C.XRRScreenResources, output_id: C.RROutput, monitor_type: []const u8) !bool {
    for (0..@intCast(res.*.noutput)) |i| {
        if (res.*.outputs[i] != output_id) continue;

        const output_info = C.XRRGetOutputInfo(display, res, res.*.outputs[i]);
        if (output_info == null) continue;
        defer C.XRRFreeOutputInfo(output_info);

        const output_name = std.mem.span(@as([*:0]const u8, @ptrCast(output_info.*.name)));

        if (output_info.*.connection != C.RR_Connected or output_info.*.crtc == 0) {
            const skip_msg = try std.fmt.allocPrint(allocator, "{s} monitor {s} is not active, skipping", .{ monitor_type, output_name });
            defer allocator.free(skip_msg);
            logInfo(skip_msg);
            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) {
            const zero_dim_msg = try std.fmt.allocPrint(allocator, "{s} monitor {s} has zero dimensions, skipping", .{ monitor_type, output_name });
            defer allocator.free(zero_dim_msg);
            logInfo(zero_dim_msg);
            continue;
        }

        screen_w = @intCast(crtc_info.*.width);
        screen_h = @intCast(crtc_info.*.height);
        center_w = @divTrunc((3 * screen_w), 5);
        center_h = screen_h - 20;

        const log_msg = try std.fmt.allocPrint(allocator, "Using {s} monitor: {s} ({d}x{d})", .{ monitor_type, output_name, screen_w, screen_h });
        defer allocator.free(log_msg);
        logInfo(log_msg);

        return true;
    }

    return false;
}

fn findLargestMonitor(allocator: 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;

    for (0..@intCast(res.*.noutput)) |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 = @as(c_uint, @intCast(crtc_info.*.width));
        const output_height = @as(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;
        }
    }

    if (largest_area > 0) {
        const old_w = screen_w;
        const old_h = screen_h;
        
        screen_w = largest_width;
        screen_h = largest_height;
        center_w = @divTrunc((3 * screen_w), 5);
        center_h = screen_h - 20;

        if (old_w != screen_w or old_h != screen_h) {
            const log_msg = try std.fmt.allocPrint(allocator, "Using largest monitor: {d}x{d}", .{ screen_w, screen_h });
            defer allocator.free(log_msg);
            logInfo(log_msg);
        }

        return true;
    }

    return false;
}

fn forceSyncMonitors(allocator: std.mem.Allocator) void {
    _ = C.XSync(display, 0);

    updateScreenDimensions(allocator) catch |err| {
        const err_msg = std.fmt.allocPrint(allocator, "Error updating screen dimensions: {any}", .{err}) catch "Error updating dimensions";
        defer if (@TypeOf(err_msg) == []const u8) allocator.free(err_msg);
        logError(err_msg);
    };
}

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 => {
            _ = C.XSync(display, 0);
            
            const old_w = screen_w;
            const old_h = screen_h;
            
            try updateScreenDimensions(allocator);
            
            if (old_w != screen_w or old_h != screen_h) {
                const change_msg = try std.fmt.allocPrint(allocator, "Screen dimensions changed: {d}x{d} -> {d}x{d}", .{ old_w, old_h, screen_w, screen_h });
                defer allocator.free(change_msg);
                logInfo(change_msg);
                
                if (list.len > 0) {
                    applyLayout();
                }
            }
        },
        else => {
            // left blank 
        },
    }
}

fn run() void {
    const script_path = autostart_path;

    const pid = std.posix.fork() catch |err| {
        std.debug.print("fork failed: {}\n", .{err});
        return;
    };

    if (pid == 0) {
        _ = std.os.linux.setsid();

        const args = [_]?[*:0]const u8{ "/bin/sh", script_path, null };
        const rc = execvp("/bin/sh", &args);
        if (rc == -1) {
            std.debug.print("failed to execute autostart script: errno={}\n", .{errno});
            std.posix.exit(1);
        } else {
            std.debug.print("Autostart launched with PID {}\n", .{pid});
        }
    }
}

fn killClient() void {
    if (cursor == null) return;

    _ = C.XGrabServer(display);
    _ = C.XSetErrorHandler(ignoreError);
    _ = C.XSetCloseDownMode(display, C.DestroyAll);
    _ = C.XKillClient(display, cursor.?.data.w);
    _ = C.XSync(display, 0);
    _ = C.XSetErrorHandler(handleError);
    _ = C.XUngrabServer(display);
}

fn winNext() void {
    if (cursor) |c| {
        if (c.next) |next|
            focus(next)
        else if (list.first) |first|
            focus(first);
    }
}

fn winPrev() void {
    if (cursor) |c| {
        if (c.prev) |prev|
            focus(prev)
        else if (list.last) |last|
            focus(last);
    }
}

extern fn execvp(prog: [*:0]const u8, argv: [*]const ?[*:0]const u8) c_int;
extern var errno: c_int;

fn spawn(cmd: [*:0]const u8) void {
    const log = std.log.scoped(.launcher);

    const pid = std.posix.fork() catch {
        log.err("fork failed", .{});
        return;
    };

    if (pid == 0) {
        _ = std.os.linux.setsid();

        const second_pid = std.posix.fork() catch {
            log.err("second fork failed", .{});
            std.posix.exit(1);
        };

        if (second_pid > 0) {
            std.posix.exit(0);
        }

        var args = [_:null]?[*:0]const u8{
            cmd,
            null,
        };

        const rc = execvp(cmd, &args);
        if (rc == -1) {
            log.err("execvp failed with errno={}", .{errno});
            std.posix.exit(1);
        }
    } else {
        _ = std.posix.waitpid(pid, 0);
    }
}

fn handleError(_: ?*C.Display, event: [*c]C.XErrorEvent) callconv(.C) c_int {
    const evt: *C.XErrorEvent = @ptrCast(event);

    switch (evt.error_code) {
        C.BadMatch => logError("BadMatch"),
        C.BadWindow => logError("BadWindow"),
        C.BadDrawable => logError("BadDrawable"),
        else => logError("Unknown X error"),
    }
    return 0;
}

fn ignoreError(_: ?*C.Display, _: [*c]C.XErrorEvent) callconv(.C) c_int {
    return 0;
}

fn onConfigureRequest(e: *C.XConfigureRequestEvent) void {
    window_changes.x = e.x;
    window_changes.y = e.y;
    window_changes.width = e.width;
    window_changes.height = e.height;
    window_changes.border_width = e.border_width;
    window_changes.sibling = e.above;
    window_changes.stack_mode = e.detail;

    _ = C.XConfigureWindow(display, e.window, @intCast(e.value_mask), &window_changes);
}

// TODO: Windows show for a brief of a second on the left, and then tiling layout gets applied.
// We want to directly have it on the stack.
// the C.XMapWindow position is determining. However, we have a problem then with our app launcher.
fn onMapRequest(allocator: std.mem.Allocator, event: *C.XEvent) !void {
    const window: C.Window = event.xmaprequest.window;
    _ = C.XSelectInput(display, window, C.StructureNotifyMask | C.EnterWindowMask);

    _ = C.XMapWindow(display, window);

    _ = C.XSetWindowBorderWidth(display, window, BORDER_WIDTH);

    const node = try addClient(allocator, window);
    focus(node);

    applyLayout();

    if (node.data.is_floating) {
        _ = C.XRaiseWindow(display, node.data.w);
        focus(node);
    }
}

fn onUnmapNotify(allocator: std.mem.Allocator, e: *C.XEvent) void {
    const ev = &e.xunmap;
    if (winToNode(ev.window)) |node| {
        if (ev.send_event == 1) {
            // INVESTIGATE: Is this what we want to do?
            const data = [_]c_long{ C.WithdrawnState, C.None };
            // Data Format: Specifies whether the data should be viewed as a list
            // of 8-bit, 16-bit, or 32-bit quantities.
            const data_format = 32;
            _ = C.XChangeProperty(
                display,
                node.data.w,
                C.XInternAtom(display, "WM_STATE", 0),
                C.XInternAtom(display, "WM_STATE", 0),
                data_format,
                C.PropModeReplace,
                @ptrCast(&data),
                data.len,
            );
        } else {
            unmanage(allocator, node, false);
        }
    }
}

fn onKeyPress(e: *C.XEvent) void {
    if (keymap.get(e.xkey.keycode)) |action| action();
}

fn onEnterNotify(e: *C.XEvent) void {
    var current_node = list.first;
    while (current_node) |n| : (current_node = n.next) {
        if (n.data.is_floating) {
            return; // Skip focus changes while dialogs are present
        }
    }

    if (e.xcrossing.mode != C.NotifyNormal or e.xcrossing.detail == C.NotifyInferior) {
        return;
    }

    _ = C.XSetErrorHandler(ignoreError);
    if (winToNode(e.xcrossing.window)) |window_node| {
        if (cursor != null and window_node.data.w == cursor.?.data.w) {
            _ = C.XSetErrorHandler(handleError);
            return;
        }

        if (!window_node.data.is_floating) {
            focus(window_node);
        }
    }
    _ = C.XSetErrorHandler(handleError);
}

fn onButtonPress(e: *C.XEvent) void {
    if (e.xbutton.subwindow == 0) return;

    var attributes: C.XWindowAttributes = undefined;
    _ = C.XGetWindowAttributes(display, e.xbutton.subwindow, &attributes);
    win_w = attributes.width;
    win_h = attributes.height;
    win_x = attributes.x;
    win_y = attributes.y;

    if (e.xbutton.button == 3 and (e.xbutton.state & C.Mod4Mask) != 0) {
        _ = C.XGrabPointer(display, e.xbutton.subwindow, 1, C.ButtonReleaseMask | C.PointerMotionMask, C.GrabModeAsync, C.GrabModeAsync, C.None, resize_cursor, C.CurrentTime);

        _ = 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) {
        _ = C.XGrabPointer(display, e.xbutton.subwindow, 1, C.ButtonReleaseMask | C.PointerMotionMask, C.GrabModeAsync, C.GrabModeAsync, C.None, move_cursor, C.CurrentTime);

        mouse = e.xbutton;
    }

    if (winToNode(e.xbutton.subwindow)) |node| if (node != cursor) {
        focus(node);
    };
}

fn onNotifyMotion(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) {
        _ = C.XMoveResizeWindow(display, mouse.subwindow, win_x, win_y, @as(c_uint, @intCast(@max(10, win_w + dx))), @as(c_uint, @intCast(@max(10, win_h + dy))));
    }
}

fn onNotifyDestroy(allocator: std.mem.Allocator, e: *C.XEvent) void {
    const ev = &e.xdestroywindow;
    if (winToNode(ev.window)) |node| {
        unmanage(allocator, node, true);
    }
}

fn onButtonRelease(_: *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 window_changes: C.XWindowChanges = undefined;

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;

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();

    var event: C.XEvent = undefined;

    display = C.XOpenDisplay(0) orelse {
        logError("Could not open display");
        std.c._exit(1);
    };

    resize_cursor = C.XCreateFontCursor(display, 120);
    move_cursor = C.XCreateFontCursor(display, 52);
    normal_cursor = C.XCreateFontCursor(display, 68);

    const screen = C.DefaultScreen(display);
    root = C.RootWindow(display, screen);

    screen_w = @intCast(C.XDisplayWidth(display, screen));
    screen_h = @intCast(C.XDisplayHeight(display, screen));
    center_w = @divTrunc((3 * screen_w), 5);
    center_h = screen_h - 20;

    try initRandR(allocator);

    _ = C.XSetErrorHandler(handleError);
    _ = C.XSelectInput(display, root, C.SubstructureRedirectMask | C.EnterWindowMask);
    _ = C.XDefineCursor(display, root, C.XCreateFontCursor(display, 68));

    grabInput(root);
    keymap = try initKeyMap(allocator);

    run();

    forceSyncMonitors(allocator);

    _ = C.XSync(display, 0);

    initialWin(allocator);

    while (!shouldQuit and C.XNextEvent(display, &event) == 0) {
        switch (event.type) {
            C.MapRequest => try onMapRequest(allocator, &event),
            C.UnmapNotify => onUnmapNotify(allocator, &event),
            C.KeyPress => onKeyPress(&event),
            C.ButtonPress => onButtonPress(&event),
            C.ButtonRelease => onButtonRelease(&event),
            C.MotionNotify => onNotifyMotion(&event),
            C.DestroyNotify => onNotifyDestroy(allocator, &event),
            C.ConfigureRequest => onConfigureRequest(@ptrCast(&event)),
            C.EnterNotify => onEnterNotify(&event),
            else => {
                if (randr_event_base != 0) {
                    if (event.type == randr_event_base + C.RRScreenChangeNotify) {
                        logInfo("RandR: Screen change event detected");
                        try onRRNotify(allocator, &event);
                    } else if (event.type == randr_event_base + C.RRNotify) {
                        logInfo("RandR: Output change event detected");
                        try onRRNotify(allocator, &event);
                    }
                }
            },
        }
    }

    keymap.deinit();
    _ = C.XFreeCursor(display, resize_cursor);
    _ = C.XFreeCursor(display, move_cursor);
    _ = C.XFreeCursor(display, normal_cursor);
    _ = C.XCloseDisplay(display);
    std.c.exit(0);
}