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