2zw - X11 Windowmanager
Files | Log | Commits | Refs | README
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); }