├── .gitignore ├── gif.gif ├── generate-tags ├── src ├── c.zig └── main.zig └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache/ 2 | zig-out/ 3 | TAGS 4 | .dirlocals 5 | -------------------------------------------------------------------------------- /gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erikbackman/ewm/HEAD/gif.gif -------------------------------------------------------------------------------- /generate-tags: -------------------------------------------------------------------------------- 1 | #!/usr/bin/sh 2 | 3 | ctags -e -R --language-force=zig --exclude=zig-cache 4 | -------------------------------------------------------------------------------- /src/c.zig: -------------------------------------------------------------------------------- 1 | pub usingnamespace @cImport({ 2 | @cInclude("X11/Xlib.h"); 3 | @cInclude("X11/XF86keysym.h"); 4 | @cInclude("X11/keysym.h"); 5 | @cInclude("X11/XKBlib.h"); 6 | @cInclude("X11/Xatom.h"); 7 | @cInclude("X11/Xutil.h"); 8 | }); 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EWM 2 | 3 | A Window Manager designed specifically for my workflow on an ultrawide monitor. 4 | 5 | ![Alt Text](gif.gif) 6 | 7 | ## Why 8 | I find that tiling window managers are terrible on ultrawide monitors, 9 | the main issue being that if you only have a single window on the 10 | screen, say a text editor, then you end up having the bulk of the text 11 | off to the far left. Floating window managers don't have this issue 12 | but most of them fall short (for me) in other aspects. 13 | 14 | Instead of writing hundereds of lines in some bespoke configuration 15 | language trying to add missing functionality I figured it would be 16 | easier to just write a window manager that just does the thing. 17 | 18 | ## Features 19 | - It does what I want 20 | - No configuration 21 | - Floating 22 | - Pseudo Tiling 23 | 24 | ## Keybinds 25 | 26 | | Key | Action | 27 | | ----------- | ------------------ | 28 | | Mod4+q | quit | 29 | | Mod4+f | fullscreen | 30 | | Mod4+m | center | 31 | | Mod4+comma | previous window | 32 | | Mod4+period | next window | 33 | | Mod4+h | tile left | 34 | | Mod4+l | tile right | 35 | | Mod4+t | tile all | 36 | | Mod4+s | stack (center) all | 37 | 38 | ## Building 39 | Requires zig version 0.12.0 or later. 40 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const C = @import("c.zig"); 3 | 4 | const FOCUS_BORDER_COLOR = 0xffd787; 5 | const NORMAL_BORDER_COLOR = 0x333333; 6 | const BORDER_WIDTH = 2; 7 | 8 | // Keybinds, currently every key is directly under Mod4Mask but I will probably add 9 | // the ability to specify modifiers. 10 | const keys = [_]Key{ 11 | .{ .keysym = C.XK_q, .action = &quit }, 12 | .{ .keysym = C.XK_f, .action = &winFullscreen }, 13 | .{ .keysym = C.XK_m, .action = ¢erCurrent }, 14 | .{ .keysym = C.XK_comma, .action = &winPrev }, 15 | .{ .keysym = C.XK_period, .action = &winNext }, 16 | .{ .keysym = C.XK_h, .action = &tileCurrentLeft }, 17 | .{ .keysym = C.XK_l, .action = &tileCurrentRight }, 18 | .{ .keysym = C.XK_t, .action = &tileAll }, 19 | .{ .keysym = C.XK_s, .action = &stackAll }, 20 | }; 21 | 22 | const Key = struct { 23 | keysym: C.KeySym, 24 | action: *const fn () void, 25 | }; 26 | 27 | // Generate a keymap with key: keysym and value: function pointer, 28 | // this is to avoid having to define keys to grab and then having to add same 29 | // keys to be handled in keypress handling code. 30 | var keymap: std.AutoHashMap(c_uint, *const fn () void) = undefined; 31 | 32 | fn initKeyMap(allocator: std.mem.Allocator) !std.AutoHashMap(c_uint, *const fn () void) { 33 | var map = std.AutoHashMap(c_uint, *const fn () void).init(allocator); 34 | errdefer map.deinit(); 35 | inline for (keys) |key| { 36 | try map.put(C.XKeysymToKeycode(display, key.keysym), key.action); 37 | } 38 | return map; 39 | } 40 | 41 | fn grabInput(window: C.Window) void { 42 | _ = C.XUngrabKey(display, C.AnyKey, C.AnyModifier, root); 43 | 44 | for (keys) |key| { 45 | _ = C.XGrabKey(display, C.XKeysymToKeycode(display, key.keysym), C.Mod4Mask, window, 0, C.GrabModeAsync, C.GrabModeAsync); 46 | } 47 | for ([_]u8{ 1, 3 }) |btn| { 48 | _ = C.XGrabButton(display, btn, C.Mod4Mask, root, 0, C.ButtonPressMask | C.ButtonReleaseMask | C.PointerMotionMask, C.GrabModeAsync, C.GrabModeAsync, 0, 0); 49 | } 50 | } 51 | 52 | // Application state 53 | const Client = struct { 54 | full: bool, 55 | wx: c_int, 56 | wy: c_int, 57 | ww: c_int, 58 | wh: c_int, 59 | w: C.Window, 60 | }; 61 | 62 | var shouldQuit = false; 63 | 64 | // Primarly used to store window attributes when a window is being 65 | // clicked on before we start potentially moving/resizing it. 66 | var win_x: i32 = 0; 67 | var win_y: i32 = 0; 68 | var win_w: i32 = 0; 69 | var win_h: i32 = 0; 70 | 71 | var screen_w: c_uint = 0; 72 | var screen_h: c_uint = 0; 73 | var center_w: c_uint = 0; 74 | var center_h: c_uint = 0; 75 | 76 | var display: *C.Display = undefined; 77 | var root: C.Window = undefined; 78 | var mouse: C.XButtonEvent = undefined; 79 | var window_changes: C.XWindowChanges = undefined; 80 | 81 | // Clients are kept in a doubly-linked list 82 | const L = std.DoublyLinkedList(Client); 83 | var list = L{}; 84 | var cursor: ?*L.Node = null; // having the cursor be nullable is annoying.. 85 | 86 | // IMPROVE: Keeping a pointer to previously_focused window as the previs node in the window list 87 | // may or may not be the previously focused one -- because a circular dl list is used. 88 | var previously_focused: ?*L.Node = undefined; 89 | 90 | fn addClient(allocator: std.mem.Allocator, window: C.Window) !*L.Node { 91 | var attributes: C.XWindowAttributes = undefined; 92 | _ = C.XGetWindowAttributes(display, window, &attributes); 93 | 94 | const client = Client{ 95 | .full = false, 96 | .wx = attributes.x, 97 | .wy = attributes.y, 98 | .ww = attributes.width, 99 | .wh = attributes.height, 100 | .w = window, 101 | }; 102 | 103 | var node = try allocator.create(L.Node); 104 | 105 | node.data = client; 106 | list.append(node); 107 | 108 | return node; 109 | } 110 | 111 | fn center(c: *L.Node) void { 112 | _ = C.XResizeWindow(display, c.data.w, center_w, center_h); 113 | var attributes: C.XWindowAttributes = undefined; 114 | _ = C.XGetWindowAttributes(display, c.data.w, &attributes); 115 | 116 | const sw: c_int = @intCast(screen_w); 117 | const sh: c_int = @intCast(screen_h); 118 | 119 | c.data.wx = @divTrunc((sw - attributes.width), 2); 120 | c.data.wy = @divTrunc((sh - attributes.height), 2); 121 | c.data.ww = attributes.width; 122 | c.data.wh = attributes.height; 123 | 124 | _ = C.XMoveWindow( 125 | display, 126 | c.data.w, 127 | c.data.wx, 128 | c.data.wy, 129 | ); 130 | } 131 | 132 | // IMPROVE: node is optional so that we don't have to do focusing logic in other places. 133 | fn focus(node: ?*L.Node) void { 134 | if (list.len == 0) return; 135 | if (cursor) |c| _ = C.XSetWindowBorder(display, c.data.w, NORMAL_BORDER_COLOR); 136 | 137 | // IMPROVE: trying to do the most sensible thing here 138 | const target = node orelse previously_focused orelse list.first.?; 139 | previously_focused = cursor; 140 | 141 | _ = C.XSetInputFocus( 142 | display, 143 | target.data.w, 144 | C.RevertToParent, 145 | C.CurrentTime, 146 | ); 147 | _ = C.XRaiseWindow(display, target.data.w); 148 | _ = C.XSetWindowBorder(display, target.data.w, FOCUS_BORDER_COLOR); 149 | 150 | cursor = target; 151 | } 152 | 153 | // Utils 154 | fn winToNode(w: C.Window) ?*L.Node { 155 | var next = list.first; 156 | while (next) |node| : (next = node.next) { 157 | if (node.data.w == w) return node; 158 | } 159 | return null; 160 | } 161 | 162 | fn unmanage(allocator: std.mem.Allocator, node: *L.Node, destroyed: bool) void { 163 | if (!destroyed) { 164 | _ = C.XGrabServer(display); 165 | _ = C.XSetErrorHandler(ignoreError); 166 | _ = C.XSelectInput(display, node.data.w, C.NoEventMask); 167 | _ = C.XUngrabButton(display, C.AnyButton, C.AnyModifier, node.data.w); 168 | _ = C.XSync(display, 0); 169 | _ = C.XSetErrorHandler(handleError); 170 | _ = C.XUngrabServer(display); 171 | } 172 | if (node == cursor) cursor = node.prev; 173 | // IMPROVE: There is no way of determining if a window is still alive so we have to make sure we set 174 | // previously_focused to null if we destroy it. Another way is to set an error handler to handle 175 | // BadWindow errors if we ever try to access it. 176 | if (previously_focused) |pf| { 177 | if (node.data.w == pf.data.w) previously_focused = null; 178 | } 179 | 180 | _ = C.XSetInputFocus( 181 | display, 182 | root, 183 | C.RevertToPointerRoot, 184 | C.CurrentTime, 185 | ); 186 | _ = C.XDeleteProperty(display, root, C.XInternAtom(display, "_NET_ACTIVE_WINDOW", 0)); 187 | 188 | list.remove(node); 189 | allocator.destroy(node); 190 | focus(null); 191 | } 192 | 193 | // Event handlers 194 | fn onConfigureRequest(e: *C.XConfigureRequestEvent) void { 195 | window_changes.x = e.x; 196 | window_changes.y = e.y; 197 | window_changes.width = e.width; 198 | window_changes.height = e.height; 199 | window_changes.border_width = e.border_width; 200 | window_changes.sibling = e.above; 201 | window_changes.stack_mode = e.detail; 202 | 203 | _ = C.XConfigureWindow(display, e.window, @intCast(e.value_mask), &window_changes); 204 | } 205 | 206 | fn onMapRequest(allocator: std.mem.Allocator, event: *C.XEvent) !void { 207 | const window: C.Window = event.xmaprequest.window; 208 | _ = C.XSelectInput(display, window, C.StructureNotifyMask | C.EnterWindowMask); 209 | 210 | _ = C.XMapWindow(display, window); 211 | _ = C.XSetWindowBorderWidth(display, window, BORDER_WIDTH); 212 | 213 | const node = try addClient(allocator, window); 214 | focus(node); 215 | } 216 | 217 | fn onUnmapNotify(allocator: std.mem.Allocator, e: *C.XEvent) void { 218 | const ev = &e.xunmap; 219 | if (winToNode(ev.window)) |node| { 220 | if (ev.send_event == 1) { 221 | // INVESTIGATE: Is this what we want to do? 222 | const data = [_]c_long{ C.WithdrawnState, C.None }; 223 | // Data Format: Specifies whether the data should be viewed as a list 224 | // of 8-bit, 16-bit, or 32-bit quantities. 225 | const data_format = 32; 226 | _ = C.XChangeProperty( 227 | display, 228 | node.data.w, 229 | C.XInternAtom(display, "WM_STATE", 0), 230 | C.XInternAtom(display, "WM_STATE", 0), 231 | data_format, 232 | C.PropModeReplace, 233 | @ptrCast(&data), 234 | data.len, 235 | ); 236 | } else { 237 | unmanage(allocator, node, false); 238 | } 239 | } 240 | } 241 | 242 | fn onKeyPress(e: *C.XEvent) void { 243 | if (keymap.get(e.xkey.keycode)) |action| action(); 244 | } 245 | 246 | fn onNotifyEnter(e: *C.XEvent) void { 247 | while (C.XCheckTypedEvent(display, C.EnterNotify, e)) {} 248 | } 249 | 250 | fn onButtonPress(e: *C.XEvent) void { 251 | if (e.xbutton.subwindow == 0) return; 252 | var attributes: C.XWindowAttributes = undefined; 253 | _ = C.XGetWindowAttributes(display, e.xbutton.subwindow, &attributes); 254 | win_w = attributes.width; 255 | win_h = attributes.height; 256 | win_x = attributes.x; 257 | win_y = attributes.y; 258 | mouse = e.xbutton; 259 | 260 | if (winToNode(e.xbutton.subwindow)) |node| if (node != cursor) { 261 | focus(node); 262 | }; 263 | } 264 | 265 | fn onNotifyMotion(e: *C.XEvent) void { 266 | if (mouse.subwindow == 0) return; 267 | 268 | const dx: i32 = @intCast(e.xbutton.x_root - mouse.x_root); 269 | const dy: i32 = @intCast(e.xbutton.y_root - mouse.y_root); 270 | 271 | const button: i32 = @intCast(mouse.button); 272 | 273 | _ = C.XMoveResizeWindow( 274 | display, 275 | mouse.subwindow, 276 | win_x + if (button == 1) dx else 0, 277 | win_y + if (button == 1) dy else 0, 278 | @max(10, win_w + if (button == 3) dx else 0), 279 | @max(10, win_h + if (button == 3) dy else 0), 280 | ); 281 | } 282 | 283 | fn onNotifyDestroy(allocator: std.mem.Allocator, e: *C.XEvent) void { 284 | const ev = &e.xdestroywindow; 285 | if (winToNode(ev.window)) |node| { 286 | unmanage(allocator, node, true); 287 | } 288 | } 289 | 290 | fn onButtonRelease(_: *C.XEvent) void { 291 | mouse.subwindow = 0; 292 | } 293 | 294 | // Error handlers 295 | fn handleError(_: ?*C.Display, event: [*c]C.XErrorEvent) callconv(.C) c_int { 296 | const evt: *C.XErrorEvent = @ptrCast(event); 297 | // TODO: 298 | switch (evt.error_code) { 299 | C.BadMatch => logError("BadMatch"), 300 | C.BadWindow => logError("BadWindow"), 301 | C.BadDrawable => logError("BadDrawable"), 302 | else => logError("TODO: I should handle this error"), 303 | } 304 | return 0; 305 | } 306 | 307 | fn ignoreError(_: ?*C.Display, _: [*c]C.XErrorEvent) callconv(.C) c_int { 308 | return 0; 309 | } 310 | 311 | // Logging 312 | fn logError(msg: []const u8) void { 313 | const stderr = std.io.getStdErr().writer(); 314 | stderr.print("Error: {s}\n", .{msg}) catch return; 315 | } 316 | 317 | fn logInfo(msg: []const u8) void { 318 | const stdInfo = std.io.getStdOut().writer(); 319 | stdInfo.print("INFO: {s}\n", .{msg}) catch return; 320 | } 321 | 322 | // Actions. None of these take any arguments and only work on global state and are 323 | // meant to be mapped to keys. 324 | fn quit() void { 325 | shouldQuit = true; 326 | } 327 | 328 | fn winNext() void { 329 | if (cursor) |c| { 330 | if (c.next) |next| focus(next) else if (list.first) |first| focus(first); 331 | } 332 | } 333 | 334 | fn winPrev() void { 335 | if (cursor) |c| { 336 | if (c.prev) |prev| focus(prev) else if (list.last) |last| focus(last); 337 | } 338 | } 339 | 340 | fn centerCurrent() void { 341 | if (cursor) |node| center(node); 342 | } 343 | 344 | fn tileCurrentLeft() void { 345 | if (cursor) |node| { 346 | _ = C.XMoveResizeWindow( 347 | display, 348 | node.data.w, 349 | 0, 350 | 0, 351 | screen_w / 2, 352 | screen_h - 3 * BORDER_WIDTH, 353 | ); 354 | } 355 | } 356 | 357 | fn tileCurrentRight() void { 358 | if (cursor) |node| { 359 | _ = C.XMoveResizeWindow( 360 | display, 361 | node.data.w, 362 | @intCast((screen_w / 2) + 2), 363 | 0, 364 | (screen_w / 2) - (3 * BORDER_WIDTH), 365 | screen_h - (3 * BORDER_WIDTH), 366 | ); 367 | } 368 | } 369 | 370 | fn tileAll() void { 371 | if (list.len < 2) return; 372 | const vert_split_height: c_uint = @intCast((screen_h - 3 * BORDER_WIDTH) / (list.len - 1)); 373 | 374 | var i: c_uint = 0; 375 | var next = list.first; 376 | while (next) |node| : (next = node.next) { 377 | if (node.data.w != cursor.?.data.w) { 378 | _ = C.XMoveResizeWindow( 379 | display, 380 | node.data.w, 381 | 0, 382 | @intCast(i * vert_split_height), 383 | (screen_w / 2) - 2, 384 | vert_split_height, 385 | ); 386 | i += 1; 387 | } 388 | } 389 | tileCurrentRight(); 390 | } 391 | 392 | fn stackAll() void { 393 | var next = list.first; 394 | while (next) |node| : (next = node.next) center(node); 395 | } 396 | 397 | fn winFullscreen() void { 398 | if (cursor) |node| { 399 | const c = node.data; 400 | if (!c.full) { 401 | var attributes: C.XWindowAttributes = undefined; 402 | _ = C.XGetWindowAttributes(display, c.w, &attributes); 403 | node.data.wx = attributes.x; 404 | node.data.wy = attributes.y; 405 | node.data.ww = attributes.width; 406 | node.data.wh = attributes.height; 407 | 408 | _ = C.XMoveResizeWindow(display, c.w, 0 + BORDER_WIDTH, 0 + BORDER_WIDTH, screen_w - 3 * BORDER_WIDTH, screen_h - 3 * BORDER_WIDTH); 409 | node.data.full = true; 410 | } else { 411 | _ = C.XMoveResizeWindow(display, c.w, c.wx, c.wy, @as(c_uint, @intCast(c.ww)), @as(c_uint, @intCast(c.wh))); 412 | node.data.full = false; 413 | } 414 | } 415 | } 416 | 417 | // Main loop 418 | pub fn main() !void { 419 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 420 | const allocator = gpa.allocator(); 421 | 422 | var event: C.XEvent = undefined; 423 | 424 | display = C.XOpenDisplay(0) orelse std.os.exit(1); 425 | 426 | const screen = C.DefaultScreen(display); 427 | root = C.RootWindow(display, screen); 428 | screen_w = @intCast(C.XDisplayWidth(display, screen)); 429 | screen_h = @intCast(C.XDisplayHeight(display, screen)); 430 | center_w = @divTrunc((3 * screen_w), 5); 431 | center_h = screen_h - 20; 432 | 433 | _ = C.XSetErrorHandler(handleError); 434 | _ = C.XSelectInput(display, root, C.SubstructureRedirectMask); 435 | _ = C.XDefineCursor(display, root, C.XCreateFontCursor(display, 68)); 436 | 437 | grabInput(root); 438 | keymap = initKeyMap(allocator) catch @panic("failed to init keymap"); 439 | 440 | _ = C.XSync(display, 0); 441 | while (!shouldQuit and C.XNextEvent(display, &event) == 0) { 442 | switch (event.type) { 443 | C.MapRequest => try onMapRequest(allocator, &event), 444 | C.UnmapNotify => onUnmapNotify(allocator, &event), 445 | C.KeyPress => onKeyPress(&event), 446 | C.ButtonPress => onButtonPress(&event), 447 | C.ButtonRelease => onButtonRelease(&event), 448 | C.MotionNotify => onNotifyMotion(&event), 449 | C.DestroyNotify => onNotifyDestroy(allocator, &event), 450 | C.ConfigureRequest => onConfigureRequest(@ptrCast(&event)), 451 | else => continue, 452 | } 453 | } 454 | 455 | _ = C.XCloseDisplay(display); 456 | std.os.exit(0); 457 | } 458 | --------------------------------------------------------------------------------