2zw - X11 Windowmanager
Files | Log | Commits | Refs | README
Author: SeMi
Date: 2025-04-23
Subject: rewrite codebase, polish some stuff
commit a385dd13cf1bac1fac98665bbdc3c879628563fb Author: SeMi <sebastian.michalk@protonmail.com> Date: Wed Apr 23 21:03:44 2025 +0200 rewrite codebase, polish some stuff diff --git a/README.md b/README.md index 6514824..1befa9b 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,67 @@ -# EWM - -A Window Manager designed specifically for my workflow on an ultrawide monitor. - - - -## Why -I find that tiling window managers are terrible on ultrawide monitors, -the main issue being that if you only have a single window on the -screen, say a text editor, then you end up having the bulk of the text -off to the far left. Floating window managers don't have this issue -but most of them fall short (for me) in other aspects. - -Instead of writing hundereds of lines in some bespoke configuration -language trying to add missing functionality I figured it would be -easier to just write a window manager that just does the thing. - -## Features -- It does what I want -- No configuration -- Floating -- Pseudo Tiling - -## Keybinds - -| Key | Action | -| ----------- | ------------------ | -| Mod4+q | quit | -| Mod4+f | fullscreen | -| Mod4+m | center | -| Mod4+comma | previous window | -| Mod4+period | next window | -| Mod4+h | tile left | -| Mod4+l | tile right | -| Mod4+t | tile all | -| Mod4+s | stack (center) all | - -## Building -Requires zig version 0.12.0 or later. +zw - minimal window manager +=========================== +zw is an extremely fast and lean dynamic window manager for X written in Zig. + +Features +-------- +- Small hackable Codebase (<900 LOC) +- No configuration files, configured via source +- Floating window management +- Pseudo window tiling +- Window focusing with colored borders +- Minimal approach to UI (no status bar) +- Zero dependencies beyond X11 and Zig standard library + +Requirements +------------ +In order to build zw you need: +- Zig compiler (0.11.0 or newer) +- Xlib header files +- libX11-dev +- libXrandr-dev + +Installation +------------ +Edit build.zig to match your local setup (mwm is installed into +the /usr/local namespace by default). + +Afterwards enter the following command to build and install mwm: + + zig build install + +Running mwm +----------- +Add the following line to your .xinitrc to start mwm using startx: + + exec mwm + +In order to connect mwm to a specific display, make sure that +the DISPLAY environment variable is set correctly, e.g.: + + DISPLAY=foo.bar:1 exec mwm + +Configuration +------------- +All configuration is done directly in main.zig and rebuilding. + +The keybindings and configuration constants are at the top of the file +for easy modification: + + FOCUS_BORDER_COLOR = 0xffd787; // Focused window border color + NORMAL_BORDER_COLOR = 0x333333; // Normal window border color + BORDER_WIDTH = 2; // Border width in pixels + terminal = "st"; // Default terminal + launcher = "dmenu_run"; // Default application launcher + +Keybindings (with MOD4/Super key): +- MOD+q Kill focused window +- MOD+f Toggle fullscreen +- MOD+m Center current window +- MOD+, Focus previous window +- MOD+. Focus next window +- MOD+h Tile current window to left half +- MOD+l Tile current window to right half +- MOD+t Tile all windows +- MOD+s Stack all windows +- MOD+Return Launch terminal (default st) +- MOD+p Launch application launcher (default dmenu) \ No newline at end of file diff --git a/build.zig b/build.zig index 3ab3c77..164ac08 100644 --- a/build.zig +++ b/build.zig @@ -16,7 +16,7 @@ pub fn build(b: *std.Build) void { const optimize = b.standardOptimizeOption(.{}); const exe = b.addExecutable(.{ - .name = "ewm", + .name = "zw", .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, @@ -24,6 +24,7 @@ pub fn build(b: *std.Build) void { exe.linkLibC(); exe.linkSystemLibrary("X11"); + exe.linkSystemLibrary("Xrandr"); // This declares intent for the executable to be installed into the // standard location when the user invokes the "install" step (the default // step when running `zig build`). diff --git a/gif.gif b/gif.gif deleted file mode 100644 index 214b0b1..0000000 Binary files a/gif.gif and /dev/null differ diff --git a/src/main.zig b/src/main.zig index 8ec5f68..de046e7 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,18 +1,39 @@ const std = @import("std"); -const C = @import("c.zig"); -const xlib = @cImport({ +//------------------------------------------------------------------------------ +// X11 Imports and C Bindings +//------------------------------------------------------------------------------ + +const C = @cImport({ @cInclude("X11/Xlib.h"); + @cInclude("X11/XF86keysym.h"); + @cInclude("X11/keysym.h"); + @cInclude("X11/XKBlib.h"); + @cInclude("X11/Xatom.h"); + @cInclude("X11/Xutil.h"); + @cInclude("X11/extensions/Xrandr.h"); }); +//------------------------------------------------------------------------------ +// Configuration and Constants +//------------------------------------------------------------------------------ + +// Appearance const FOCUS_BORDER_COLOR = 0xffd787; const NORMAL_BORDER_COLOR = 0x333333; const BORDER_WIDTH = 2; -// Keybinds, currently every key is directly under Mod4Mask but I will probably add -// the ability to specify modifiers. +// Application settings +const terminal = "st"; +const launcher = "dmenu_run"; +const autostart_path = "/home/smi/.scritps/ewm.sh"; + +//------------------------------------------------------------------------------ +// Keybindings Configuration +//------------------------------------------------------------------------------ + +// Define keybindings with actions const keys = [_]Key{ - // .{ .keysym = C.XK_q, .action = &quit }, .{ .keysym = C.XK_q, .action = &killClient }, .{ .keysym = C.XK_f, .action = &winFullscreen }, .{ .keysym = C.XK_m, .action = ¢erCurrent }, @@ -22,37 +43,54 @@ const keys = [_]Key{ .{ .keysym = C.XK_l, .action = &tileCurrentRight }, .{ .keysym = C.XK_t, .action = &tileAll }, .{ .keysym = C.XK_s, .action = &stackAll }, - .{ .keysym = C.XK_Return, .action = &spawnTerminal }, // conf for terminal - .{ .keysym = C.XK_p, .action = &spawnDmenu }, + + .{ .keysym = C.XK_Return, .action = struct { + fn action() void { + spawn(terminal); + } + }.action }, + .{ .keysym = C.XK_p, .action = struct { + fn action() void { + spawn(launcher); + } + }.action }, + + // add more in the format: + // .{ .keysym = C.XK_b, .action = struct { + // fn action() void { spawn("firefox"); } + // }.action }, }; -fn killClient() void { - // If no window is selected, return - if (cursor == null) return; +//------------------------------------------------------------------------------ +// Logging Utilities +//------------------------------------------------------------------------------ - _ = 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 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; +} + +//------------------------------------------------------------------------------ +// Keybindings and Input Handling +//------------------------------------------------------------------------------ + const Key = struct { keysym: C.KeySym, action: *const fn () void, }; -// Generate a keymap with key: keysym and value: function pointer, -// this is to avoid having to define keys to grab and then having to add same -// keys to be handled in keypress handling code. +// Generate a keymap with key: keysym and value: function pointer var keymap: std.AutoHashMap(c_uint, *const fn () void) = undefined; 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(); - inline for (keys) |key| { + for (keys) |key| { try map.put(C.XKeysymToKeycode(display, key.keysym), key.action); } return map; @@ -69,7 +107,11 @@ fn grabInput(window: C.Window) void { } } -// Application state +//------------------------------------------------------------------------------ +// Client/Window Management +//------------------------------------------------------------------------------ + +// Client represents a managed window const Client = struct { full: bool, wx: c_int, @@ -79,38 +121,10 @@ const Client = struct { w: C.Window, }; -var shouldQuit = false; - -// Primarly used to store window attributes when a window is being -// clicked on before we start potentially moving/resizing it. -var win_x: i32 = 0; -var win_y: i32 = 0; -var win_w: i32 = 0; -var win_h: i32 = 0; - -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 display: *C.Display = undefined; -var root: C.Window = undefined; -var mouse: C.XButtonEvent = undefined; -var window_changes: C.XWindowChanges = undefined; - -const ClientNode = struct { - prev: ?*ClientNode = null, - next: ?*ClientNode = null, - data: Client, -}; - // Use the node type with DoublyLinkedList const L = std.DoublyLinkedList(Client); var list = L{}; -var cursor: ?*L.Node = null; // having the cursor be nullable is annoying.. - -// IMPROVE: Keeping a pointer to previously_focused window as the previs node in the window list -// may or may not be the previously focused one -- because a circular dl list is used. +var cursor: ?*L.Node = null; var previously_focused: ?*L.Node = undefined; fn addClient(allocator: std.mem.Allocator, window: C.Window) !*L.Node { @@ -127,7 +141,6 @@ fn addClient(allocator: std.mem.Allocator, window: C.Window) !*L.Node { }; var node = try allocator.create(L.Node); - node.data = client; list.append(node); @@ -155,12 +168,11 @@ fn center(c: *L.Node) void { ); } -// IMPROVE: node is optional so that we don't have to do focusing logic in other places. fn focus(node: ?*L.Node) void { if (list.len == 0) return; if (cursor) |c| _ = C.XSetWindowBorder(display, c.data.w, NORMAL_BORDER_COLOR); - // IMPROVE: trying to do the most sensible thing here + // Most sensible target to focus const target = node orelse previously_focused orelse list.first.?; previously_focused = cursor; @@ -176,7 +188,6 @@ fn focus(node: ?*L.Node) void { cursor = target; } -// Utils fn winToNode(w: C.Window) ?*L.Node { var next = list.first; while (next) |node| : (next = node.next) { @@ -196,9 +207,8 @@ fn unmanage(allocator: std.mem.Allocator, node: *L.Node, destroyed: bool) void { _ = C.XUngrabServer(display); } if (node == cursor) cursor = node.prev; - // IMPROVE: There is no way of determining if a window is still alive so we have to make sure we set - // previously_focused to null if we destroy it. Another way is to set an error handler to handle - // BadWindow errors if we ever try to access it. + + // Update previously_focused if needed if (previously_focused) |pf| { if (node.data.w == pf.data.w) previously_focused = null; } @@ -216,150 +226,320 @@ fn unmanage(allocator: std.mem.Allocator, node: *L.Node, destroyed: bool) void { focus(null); } -// Event handlers -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; +//------------------------------------------------------------------------------ +// Monitor/Display Management with RandR +//------------------------------------------------------------------------------ - _ = C.XConfigureWindow(display, e.window, @intCast(e.value_mask), &window_changes); -} +// Screen dimensions +var screen_w: c_uint = 0; +var screen_h: c_uint = 0; +var center_w: c_uint = 0; +var center_h: c_uint = 0; -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); +// RandR extension data +var randr_event_base: c_int = 0; +var randr_error_base: c_int = 0; - _ = C.XMapWindow(display, window); - _ = C.XSetWindowBorderWidth(display, window, BORDER_WIDTH); +fn initRandR(allocator: std.mem.Allocator) !void { + // Check if RandR extension is available + if (C.XRRQueryExtension(display, &randr_event_base, &randr_error_base) == 0) { + logError("RandR extension not available"); + return; + } - const node = try addClient(allocator, window); - focus(node); + // Log RandR base event + 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); + + // Get RandR version + 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); + } + + // Update screen dimensions based on active monitor + try updateScreenDimensions(allocator); + + // Select RandR events to listen for monitor changes + _ = C.XRRSelectInput(display, root, C.RROutputChangeNotifyMask | C.RRCrtcChangeNotifyMask | C.RRScreenChangeNotifyMask); } -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 updateScreenDimensions(allocator: std.mem.Allocator) !void { + // Get screen resources + const res = C.XRRGetScreenResources(display, root); + if (res == null) { + logError("Failed to get screen resources"); + return; + } + defer C.XRRFreeScreenResources(res); + + // Log all available outputs for debugging + logInfo("Scanning all available outputs:"); + for (0..@intCast(res.*.noutput)) |i| { + const output_info = C.XRRGetOutputInfo(display, res, res.*.outputs[i]); + if (output_info == null) continue; + + const output_name = std.mem.span(@as([*:0]const u8, @ptrCast(output_info.*.name))); + const connection_status = switch (output_info.*.connection) { + C.RR_Connected => "connected", + C.RR_Disconnected => "disconnected", + C.RR_UnknownConnection => "unknown", + else => "invalid", + }; + + const has_crtc = output_info.*.crtc != 0; + + const log_msg = try std.fmt.allocPrint(allocator, "Output {d}: {s} - {s}, has_crtc: {}", .{ i, output_name, connection_status, has_crtc }); + defer allocator.free(log_msg); + logInfo(log_msg); + + // If it has a CRTC, print its dimensions + if (has_crtc) { + const crtc_info = C.XRRGetCrtcInfo(display, res, output_info.*.crtc); + if (crtc_info != null) { + const crtc_msg = try std.fmt.allocPrint(allocator, " CRTC dimensions: {d}x{d} at {d},{d}", .{ crtc_info.*.width, crtc_info.*.height, crtc_info.*.x, crtc_info.*.y }); + defer allocator.free(crtc_msg); + logInfo(crtc_msg); + C.XRRFreeCrtcInfo(crtc_info); + } } + + C.XRRFreeOutputInfo(output_info); } -} -fn onKeyPress(e: *C.XEvent) void { - if (keymap.get(e.xkey.keycode)) |action| action(); -} + var found_active_monitor = false; -fn onNotifyEnter(e: *C.XEvent) void { - while (C.XCheckTypedEvent(display, C.EnterNotify, e)) {} -} + // First try to find and use the primary monitor + const primary_output = C.XRRGetOutputPrimary(display, root); + const primary_msg = try std.fmt.allocPrint(allocator, "Primary output ID: {d}", .{primary_output}); + defer allocator.free(primary_msg); + logInfo(primary_msg); -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; - mouse = e.xbutton; + if (primary_output != 0) { + found_active_monitor = try tryUseMonitor(allocator, res, primary_output, "primary"); + } - if (winToNode(e.xbutton.subwindow)) |node| if (node != cursor) { - focus(node); - }; + // If no primary monitor is active, find any connected monitor with largest dimensions + if (!found_active_monitor) { + found_active_monitor = try findLargestMonitor(allocator, res); + } + + // If still no monitor found, use default X screen dimensions + if (!found_active_monitor) { + 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; + + const log_msg = try std.fmt.allocPrint(allocator, "No active monitors found, using X screen dimensions: {d}x{d}", .{ screen_w, screen_h }); + defer allocator.free(log_msg); + logInfo(log_msg); + } + + // Log final selected dimensions + const final_dim_msg = try std.fmt.allocPrint(allocator, "Final selected dimensions: {d}x{d}, center window: {d}x{d}", .{ screen_w, screen_h, center_w, center_h }); + defer allocator.free(final_dim_msg); + logInfo(final_dim_msg); } -fn onNotifyMotion(e: *C.XEvent) void { - if (mouse.subwindow == 0) return; +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 dx: i32 = @intCast(e.xbutton.x_root - mouse.x_root); - const dy: i32 = @intCast(e.xbutton.y_root - mouse.y_root); + const output_info = C.XRRGetOutputInfo(display, res, res.*.outputs[i]); + if (output_info == null) continue; + defer C.XRRFreeOutputInfo(output_info); - const button: i32 = @intCast(mouse.button); + const output_name = std.mem.span(@as([*:0]const u8, @ptrCast(output_info.*.name))); - _ = C.XMoveResizeWindow( - display, - mouse.subwindow, - win_x + if (button == 1) dx else 0, - win_y + if (button == 1) dy else 0, - @max(10, win_w + if (button == 3) dx else 0), - @max(10, win_h + if (button == 3) dy else 0), - ); -} + // Only use if connected and has a CRTC (active) + 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; + } -fn onNotifyDestroy(allocator: std.mem.Allocator, e: *C.XEvent) void { - const ev = &e.xdestroywindow; - if (winToNode(ev.window)) |node| { - unmanage(allocator, node, true); + const crtc_info = C.XRRGetCrtcInfo(display, res, output_info.*.crtc); + if (crtc_info == null) continue; + defer C.XRRFreeCrtcInfo(crtc_info); + + // Check if this monitor is actually enabled (non-zero dimensions) + 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; + } + + // Update screen dimensions from primary monitor + 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; } -} -fn onButtonRelease(_: *C.XEvent) void { - mouse.subwindow = 0; + return false; } -// Error handlers -fn handleError(_: ?*C.Display, event: [*c]C.XErrorEvent) callconv(.C) c_int { - const evt: *C.XErrorEvent = @ptrCast(event); - // TODO: - switch (evt.error_code) { - C.BadMatch => logError("BadMatch"), - C.BadWindow => logError("BadWindow"), - C.BadDrawable => logError("BadDrawable"), - else => logError("TODO: I should handle this error"), +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; + var largest_output_name: []const u8 = "none"; + + logInfo("No active primary monitor, searching for largest connected monitor"); + + 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); + + // Skip if not connected or no CRTC + 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); + + // Skip if dimensions are zero + if (crtc_info.*.width == 0 or crtc_info.*.height == 0) continue; + + const output_name = std.mem.span(@as([*:0]const u8, @ptrCast(output_info.*.name))); + 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; + + const monitor_msg = try std.fmt.allocPrint(allocator, "Found connected monitor: {s} ({d}x{d}, area: {d})", .{ output_name, output_width, output_height, output_area }); + defer allocator.free(monitor_msg); + logInfo(monitor_msg); + + // Keep track of the largest monitor (by area) + if (output_area > largest_area) { + largest_area = output_area; + largest_width = output_width; + largest_height = output_height; + largest_output_name = output_name; + } } - return 0; + + // Use the largest monitor if found + if (largest_area > 0) { + screen_w = largest_width; + screen_h = largest_height; + center_w = @divTrunc((3 * screen_w), 5); + center_h = screen_h - 20; + + const log_msg = try std.fmt.allocPrint(allocator, "Using largest monitor: {s} ({d}x{d})", .{ largest_output_name, screen_w, screen_h }); + defer allocator.free(log_msg); + logInfo(log_msg); + + return true; + } + + return false; } -fn ignoreError(_: ?*C.Display, _: [*c]C.XErrorEvent) callconv(.C) c_int { - return 0; +fn forceSyncMonitors(allocator: std.mem.Allocator) void { + // Force an XSync to make sure we have the latest monitor info + _ = C.XSync(display, 0); + + // Directly update screen dimensions + 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); + }; } -// Logging -fn logError(msg: []const u8) void { - const stderr = std.io.getStdErr().writer(); - stderr.print("Error: {s}\n", .{msg}) catch return; +fn onRRNotify(allocator: std.mem.Allocator, e: *C.XEvent) !void { + // Access the event data properly + const rrev = @as(*C.XRRNotifyEvent, @ptrCast(e)); + + // Log the event subtype for debugging + const subtype_msg = try std.fmt.allocPrint(allocator, "Processing RandR notification, subtype: {d}", .{rrev.subtype}); + defer allocator.free(subtype_msg); + logInfo(subtype_msg); + + // Force a sync to make sure we have the latest monitor info + _ = C.XSync(display, 0); + + // Wait a bit for xrandr changes to complete + std.time.sleep(100 * std.time.ns_per_ms); // 100ms delay + + // Update screen dimensions + try updateScreenDimensions(allocator); + + // Restack all windows with new dimensions + stackAll(); } -fn logInfo(msg: []const u8) void { - const stdInfo = std.io.getStdOut().writer(); - stdInfo.print("INFO: {s}\n", .{msg}) catch return; +//------------------------------------------------------------------------------ +// Actions for Keybindings +//------------------------------------------------------------------------------ + +// Handle autostart script +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}); + } + } } -// Actions. None of these take any arguments and only work on global state and are -// meant to be mapped to keys. -fn quit() void { - shouldQuit = true; +fn killClient() void { + // If no window is selected, return + 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); + 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); + if (c.prev) |prev| + focus(prev) + else if (list.last) |last| + focus(last); } } @@ -440,6 +620,7 @@ fn winFullscreen() void { } } +// Process handling (for application launching) extern fn execvp(prog: [*:0]const u8, argv: [*]const ?[*:0]const u8) c_int; extern var errno: c_int; @@ -469,39 +650,210 @@ fn spawn(cmd: [*:0]const u8) void { } } -fn spawnTerminal() void { - spawn("st"); +//------------------------------------------------------------------------------ +// X11 Event Handlers +//------------------------------------------------------------------------------ + +// Error handlers +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); +} + +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); +} + +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 onNotifyEnter(e: *C.XEvent) void { + while (C.XCheckTypedEvent(display, C.EnterNotify, e)) {} +} + +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; + 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); + + _ = C.XMoveResizeWindow( + display, + mouse.subwindow, + win_x + if (button == 1) dx else 0, + win_y + if (button == 1) dy else 0, + @max(10, win_w + if (button == 3) dx else 0), + @max(10, win_h + if (button == 3) dy else 0), + ); +} + +fn onNotifyDestroy(allocator: std.mem.Allocator, e: *C.XEvent) void { + const ev = &e.xdestroywindow; + if (winToNode(ev.window)) |node| { + unmanage(allocator, node, true); + } } -fn spawnDmenu() void { - spawn("dmenu_run"); +fn onButtonRelease(_: *C.XEvent) void { + mouse.subwindow = 0; } -// Main loop +//------------------------------------------------------------------------------ +// Global Application State +//------------------------------------------------------------------------------ + +// Application state variables +var shouldQuit = false; +var display: *C.Display = undefined; +var root: C.Window = undefined; +var window_changes: C.XWindowChanges = undefined; + +// Primarly used to store window attributes when a window is being +// clicked on before we start potentially moving/resizing it. +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; + +//------------------------------------------------------------------------------ +// Main Application Entry Point +//------------------------------------------------------------------------------ + pub fn main() !void { + // Setup memory allocator var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const allocator = gpa.allocator(); var event: C.XEvent = undefined; - display = C.XOpenDisplay(0) orelse std.c._exit(1); + // Initialize X11 connection + display = C.XOpenDisplay(0) orelse { + logError("Could not open display"); + std.c._exit(1); + }; + // Setup X11 display and root window const screen = C.DefaultScreen(display); root = C.RootWindow(display, screen); + + // Initialize screen dimensions (will be updated by initRandR) 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; + logInfo("Initializing window manager"); + + // Initialize RandR for multi-monitor support + try initRandR(allocator); + + // Set error handler and input masks _ = C.XSetErrorHandler(handleError); _ = C.XSelectInput(display, root, C.SubstructureRedirectMask); _ = C.XDefineCursor(display, root, C.XCreateFontCursor(display, 68)); + // Setup input grabbing grabInput(root); - keymap = initKeyMap(allocator) catch @panic("failed to init keymap"); + keymap = try initKeyMap(allocator); + + // Run autostart script if configured + run(); + // Force initial monitor detection + forceSyncMonitors(allocator); + + // Sync X11 state before event loop _ = C.XSync(display, 0); + + // Log RandR event base for debugging + logInfo("Waiting for events. RandR event base is:"); + const event_base_msg = try std.fmt.allocPrint(allocator, "{d}", .{randr_event_base}); + defer allocator.free(event_base_msg); + logInfo(event_base_msg); + + // Main event loop while (!shouldQuit and C.XNextEvent(display, &event) == 0) { + // Debug event logging for RandR events + if (event.type > 64 and event.type < 128) { + const event_msg = try std.fmt.allocPrint(allocator, "Received event type: {d}", .{event.type}); + defer allocator.free(event_msg); + logInfo(event_msg); + } + switch (event.type) { C.MapRequest => try onMapRequest(allocator, &event), C.UnmapNotify => onUnmapNotify(allocator, &event), @@ -511,10 +863,22 @@ pub fn main() !void { C.MotionNotify => onNotifyMotion(&event), C.DestroyNotify => onNotifyDestroy(allocator, &event), C.ConfigureRequest => onConfigureRequest(@ptrCast(&event)), - else => continue, + else => { + // Handle RandR events + 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.XCloseDisplay(display); std.c.exit(0); }