2zw
init commit, new repo
ab24debf4d365e998e68b7dd6b1cd71f70524cfa
IIIlllIIIllI <seb.michalk@gmail.com>
2025-12-20 11:02:13 +0000
.gitignore | 4 + README.txt | 61 +++ build.zig | 54 ++ src/bar.zig | 507 +++++++++++++++++++ src/c.zig | 10 + src/draw.zig | 162 ++++++ src/main.zig | 1559 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 2357 insertions(+) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f52e419 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +zig-cache/ +zig-out/ +TAGS +.dirlocals diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..4c691e5 --- /dev/null +++ b/README.txt @@ -0,0 +1,61 @@ +2zw +=== +2zw is a fast, lean window manager for X written in Zig. + +It follows loosely the 2wm design from suckless.org. + +The codebase now targets portable POSIX APIs so it builds cleanly on both Linux and OpenBSD. + +Features +-------- +- Small hackable codebase +- Master/stack tiling +- Attach/detach instead of workspaces +- No configuration file parsing, configured via source +- Floating window support +- Focus border colouring +- Bar (bat, date/time) +- Gaps + +Requirements +------------ +In order to build 2zw you need: +- Zig compiler (0.13.0 or newer, tested with 0.14.0) +- Xlib header files +- libX11-dev +- libXrandr-dev + - On OpenBSD these live under `/usr/X11R6`; the build script adds the paths automatically. + +Installation +------------ +Edit `build.zig` to match your local setup if you want a different prefix +(2zw installs into `/usr/local` namespace by default). No manual edits are required +for OpenBSD include/library search paths. + +Afterwards enter the following command to build and install 2zw: + + zig build install + +Running 2zw +----------- +Add the following line to your .xinitrc to start 2zw using startx: + + exec 2zw + +In order to connect 2zw to a specific display, make sure that +the DISPLAY environment variable is set correctly, e.g.: + + DISPLAY=foo.bar:1 exec 2zw + +Configuration +------------- +Everything lives in `src/main.zig`; edit constants and rebuild. + +Defaults worth noting: +- `FOCUS_BORDER_COLOR = 0xffd787` +- `NORMAL_BORDER_COLOR = 0x333333` +- `BORDER_WIDTH = 2` +- `terminal = "st"` +- `launcher = "dmenu_run"` + +Key bindings (Mod4): q kill, a attach, d detach, , prev, . next, Return terminal, p launcher, s slock. diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..c387b74 --- /dev/null +++ b/build.zig @@ -0,0 +1,54 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + const exe = b.addExecutable(.{ + .name = "2zw", + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + exe.linkLibC(); + exe.linkSystemLibrary("X11"); + exe.linkSystemLibrary("Xrandr"); + exe.linkSystemLibrary("Xft"); + + if (target.result.os.tag == .openbsd) { + exe.addSystemIncludePath(lazyAbsolutePath("/usr/X11R6/include")); + exe.addLibraryPath(lazyAbsolutePath("/usr/X11R6/lib")); + } + b.installArtifact(exe); + + const run_cmd = b.addRunArtifact(exe); + + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + const unit_tests = b.addTest(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + const run_unit_tests = b.addRunArtifact(unit_tests); + + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_unit_tests.step); +} + +fn lazyAbsolutePath(path: []const u8) std.Build.LazyPath { + if (@hasField(std.Build.LazyPath, "path")) { + return .{ .path = path }; + } else if (@hasField(std.Build.LazyPath, "cwd_relative")) { + return .{ .cwd_relative = path }; + } else { + @compileError("unsupported Zig std.Build.LazyPath layout; please update lazyAbsolutePath"); + } +} diff --git a/src/bar.zig b/src/bar.zig new file mode 100644 index 0000000..ed8e5ad --- /dev/null +++ b/src/bar.zig @@ -0,0 +1,507 @@ +const std = @import("std"); +const C = @import("c.zig"); +const drawlib = @import("draw.zig"); + +pub const BarPos = enum { + top, + bottom, +}; + +pub const ModAlign = enum { + left, + center, + right, +}; + +const ModFn = *const fn (*Mod, std.mem.Allocator, i64) anyerror!void; + +const Mod = struct { + name: []const u8, + text: std.ArrayList(u8), + interval_ms: u64, + next_update_ms: i64, + updater: ModFn, + side: ModAlign, + + pub fn init( + alloc: std.mem.Allocator, + name: []const u8, + interval_ms: u64, + updater: ModFn, + side: ModAlign, + ) Mod { + return Mod{ + .name = name, + .text = std.ArrayList(u8).init(alloc), + .interval_ms = interval_ms, + .next_update_ms = 0, + .updater = updater, + .side = side, + }; + } + + pub fn deinit(self: *Mod) void { + self.text.deinit(); + } + + pub fn due(self: *Mod, now_ms: i64) bool { + return now_ms >= self.next_update_ms; + } + + pub fn run(self: *Mod, alloc: std.mem.Allocator, now_ms: i64) !void { + try self.updater(self, alloc, now_ms); + const interval: i64 = @intCast(self.interval_ms); + self.next_update_ms = now_ms + interval; + } + + pub fn set(self: *Mod, text: []const u8) !void { + self.text.clearRetainingCapacity(); + try self.text.appendSlice(text); + } +}; + +pub const Bar = struct { + alloc: std.mem.Allocator, + dpy: *C.Display, + root: C.Window, + scr: c_int, + win: C.Window, + pos: BarPos, + w: c_uint, + h: c_uint, + base: c_int, + drw: drawlib.Drw, + xft: *C.XftDraw, + bg: C.ulong, + fg: C.XftColor, + mods: std.ArrayList(Mod), + dirty: bool, + pad: c_int, + delim: []const u8, + mon_x: c_int, + mon_y: c_int, + mon_h: c_uint, + font: [*:0]const u8, + sel_win: ?C.Window, + + pub fn init( + alloc: std.mem.Allocator, + dpy: *C.Display, + root: C.Window, + scr: c_int, + w: c_uint, + mon_x: c_int, + mon_y: c_int, + mon_h: c_uint, + pos: BarPos, + font: [*:0]const u8, + delim: []const u8, + ) !Bar { + var drw = try drawlib.Drw.init(alloc, dpy, scr, font); + errdefer drw.deinit(); + + const h: c_uint = @intCast(drw.lh + 4); + const bg = try drw.alloccol(0x1c1c1c); + const fg = try drw.allocxftcol(0xdddddd); + + const win = C.XCreateSimpleWindow( + dpy, + root, + mon_x, + mon_y, + w, + h, + 0, + bg, + bg, + ); + if (win == 0) return error.CreateWindowFailed; + + var attrs: C.XSetWindowAttributes = undefined; + attrs.override_redirect = 1; + attrs.event_mask = C.ExposureMask | C.StructureNotifyMask; + attrs.background_pixel = bg; + attrs.border_pixel = bg; + + _ = C.XChangeWindowAttributes( + dpy, + win, + C.CWOverrideRedirect | C.CWEventMask | + C.CWBackPixel | C.CWBorderPixel, + &attrs, + ); + + const xft_draw = C.XftDrawCreate(dpy, win, drw.vis, drw.cmap); + if (xft_draw == null) return error.XftDrawCreateFailed; + + _ = C.XSelectInput(dpy, win, C.ExposureMask | C.StructureNotifyMask); + _ = C.XMapRaised(dpy, win); + + const base = 2 + drw.ascent; + + return Bar{ + .alloc = alloc, + .dpy = dpy, + .root = root, + .scr = scr, + .win = win, + .pos = pos, + .w = w, + .h = h, + .base = base, + .drw = drw, + .xft = xft_draw.?, + .bg = bg, + .fg = fg, + .mods = std.ArrayList(Mod).init(alloc), + .dirty = true, + .pad = 8, + .delim = delim, + .mon_x = mon_x, + .mon_y = mon_y, + .mon_h = mon_h, + .font = font, + .sel_win = null, + }; + } + + pub fn deinit(self: *Bar) void { + for (self.mods.items) |*mod| mod.deinit(); + self.mods.deinit(); + C.XftColorFree(self.dpy, self.drw.vis, self.drw.cmap, &self.fg); + C.XftDrawDestroy(self.xft); + if (self.win != 0) { + _ = C.XDestroyWindow(self.dpy, self.win); + self.win = 0; + } + self.drw.deinit(); + } + + pub fn addMod( + self: *Bar, + name: []const u8, + interval_ms: u64, + updater: ModFn, + side: ModAlign, + ) !void { + var mod = Mod.init(self.alloc, name, interval_ms, updater, side); + errdefer mod.deinit(); + try self.mods.append(mod); + self.dirty = true; + } + + pub fn addClock(self: *Bar, interval_ms: u64) !void { + try self.addMod("clock", interval_ms, clkUpd, .right); + } + + pub fn addBat(self: *Bar, interval_ms: u64) !void { + try self.addMod("battery", interval_ms, batUpd, .right); + } + + pub fn addWin(self: *Bar, interval_ms: u64) !void { + try self.addMod("window", interval_ms, winUpd, .center); + } + + pub fn setSel(self: *Bar, win: ?C.Window) void { + if (self.sel_win != win) { + self.sel_win = win; + self.dirty = true; + } + } + + pub fn mark(self: *Bar) void { + self.dirty = true; + } + + pub fn resize( + self: *Bar, + w: c_uint, + mon_x: c_int, + mon_y: c_int, + mon_h: c_uint, + ) void { + self.w = w; + self.mon_x = mon_x; + self.mon_y = mon_y; + self.mon_h = mon_h; + const mon_h_i: c_int = @intCast(mon_h); + const bar_h_i: c_int = @intCast(self.h); + const y = switch (self.pos) { + .top => mon_y, + .bottom => mon_y + mon_h_i - bar_h_i, + }; + + _ = C.XMoveResizeWindow(self.dpy, self.win, mon_x, y, w, self.h); + self.dirty = true; + } + + pub fn barH(self: *Bar) c_int { + return @intCast(self.h); + } + + pub fn tick(self: *Bar, now_ms: i64) !bool { + var changed = false; + for (self.mods.items) |*mod| { + if (!mod.due(now_ms)) continue; + try mod.run(self.alloc, now_ms); + changed = true; + } + if (changed) self.dirty = true; + return changed; + } + + pub fn nextDelay(self: *Bar, now_ms: i64) u64 { + if (self.mods.items.len == 0) return 1000; + var best: i64 = std.math.maxInt(i64); + for (self.mods.items) |mod| { + const delta = mod.next_update_ms - now_ms; + const clamped = if (delta <= 0) 0 else delta; + if (clamped < best) best = clamped; + } + if (best == std.math.maxInt(i64)) return 1000; + return @intCast(best); + } + + pub fn draw(self: *Bar) void { + if (!self.dirty) return; + self.drw.rect(self.win, self.bg, 0, 0, self.w, self.h); + + const baseline = self.base; + const delim_txt = self.delim; + const delim_w = self.drw.textw(delim_txt); + + var x_right: c_int = @intCast(self.w); + x_right -= self.pad; + var first_right = true; + var i: usize = self.mods.items.len; + while (i > 0) { + i -= 1; + const mod = &self.mods.items[i]; + if (mod.side != .right) continue; + const txt = mod.text.items; + if (txt.len == 0) continue; + if (!first_right and delim_txt.len != 0) { + x_right -= delim_w; + var fg_delim = self.fg; + self.drw.text( + self.win, + self.xft, + &fg_delim, + x_right, + baseline, + delim_txt, + ); + x_right -= self.pad; + } + const txt_w = self.drw.textw(txt); + x_right -= txt_w; + var fg_copy = self.fg; + self.drw.text( + self.win, + self.xft, + &fg_copy, + x_right, + baseline, + txt, + ); + x_right -= self.pad; + first_right = false; + } + + for (self.mods.items) |*mod| { + if (mod.side != .center) continue; + const txt = mod.text.items; + if (txt.len == 0) continue; + const txt_w = self.drw.textw(txt); + const bar_w: c_int = @intCast(self.w); + const x_center = @divTrunc(bar_w - txt_w, 2); + var fg_copy = self.fg; + self.drw.text( + self.win, + self.xft, + &fg_copy, + x_center, + baseline, + txt, + ); + } + + var x_left: c_int = self.pad; + var first_left = true; + for (self.mods.items) |*mod| { + if (mod.side != .left) continue; + const txt = mod.text.items; + if (txt.len == 0) continue; + if (!first_left and delim_txt.len != 0) { + var fg_delim = self.fg; + self.drw.text( + self.win, + self.xft, + &fg_delim, + x_left, + baseline, + delim_txt, + ); + x_left += delim_w; + x_left += self.pad; + } + var fg_copy = self.fg; + self.drw.text( + self.win, + self.xft, + &fg_copy, + x_left, + baseline, + txt, + ); + const txt_w = self.drw.textw(txt); + x_left += txt_w + self.pad; + first_left = false; + } + + _ = C.XSync(self.dpy, 0); + self.dirty = false; + } +}; + +fn clkUpd(mod: *Mod, alloc: std.mem.Allocator, now_ms: i64) !void { + _ = alloc; + _ = now_ms; + const timestamp = std.time.timestamp(); + if (timestamp < 0) { + try mod.set("time err"); + return; + } + + const wall_seconds: u64 = @intCast(timestamp); + const epoch_seconds = std.time.epoch.EpochSeconds{ .secs = wall_seconds }; + const epoch_day = epoch_seconds.getEpochDay(); + const day_seconds = epoch_seconds.getDaySeconds(); + const year_day = epoch_day.calculateYearDay(); + const month_day = year_day.calculateMonthDay(); + + const hours = day_seconds.getHoursIntoDay(); + const minutes = day_seconds.getMinutesIntoHour(); + + const weekday_names = [_][]const u8{ + "Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + }; + const month_names = [_][]const u8{ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + }; + + const day_index: usize = @intCast(epoch_day.day); + const weekday_index = (day_index + 4) % weekday_names.len; + const weekday_name = weekday_names[weekday_index]; + + const month_index: usize = @intCast(month_day.month.numeric() - 1); + const month_name = month_names[month_index]; + + const day_of_month: u8 = @intCast(month_day.day_index + 1); + + var buf: [64]u8 = undefined; + const text = try std.fmt.bufPrint(&buf, "{s}, {s} {d}, {d:0>2}:{d:0>2}", .{ + weekday_name, + month_name, + day_of_month, + hours, + minutes, + }); + try mod.set(text); +} + +fn batUpd(mod: *Mod, alloc: std.mem.Allocator, _: i64) !void { + const capacity_path = "/sys/class/power_supply/BAT0/capacity"; + const file = std.fs.openFileAbsolute(capacity_path, .{}) catch |err| { + if (err == error.FileNotFound) { + try mod.set("N/A"); + return; + } + return err; + }; + defer file.close(); + + var buf: [8]u8 = undefined; + const bytes_read = try file.readAll(&buf); + const content = std.mem.trimRight( + u8, + buf[0..bytes_read], + &std.ascii.whitespace, + ); + const capacity = try std.fmt.parseInt(u8, content, 10); + + var buffer: [16]u8 = undefined; + const text = try std.fmt.bufPrint(&buffer, "BAT {d}%", .{capacity}); + const owned = try alloc.dupe(u8, text); + defer alloc.free(owned); + try mod.set(owned); +} + +var g_bar: ?*Bar = null; + +pub fn setGlobalBar(bar_ptr: ?*Bar) void { + g_bar = bar_ptr; +} + +fn winUpd(mod: *Mod, alloc: std.mem.Allocator, _: i64) !void { + const bar_ptr = g_bar orelse { + try mod.set(""); + return; + }; + + const win = bar_ptr.sel_win orelse { + try mod.set(""); + return; + }; + + var class_hint: C.XClassHint = undefined; + _ = C.XSetErrorHandler(ignoreError); + const status = C.XGetClassHint(bar_ptr.dpy, win, &class_hint); + _ = C.XSetErrorHandler(handleError); + + if (status == 0) { + try mod.set(""); + return; + } + + defer { + if (class_hint.res_class != null) _ = C.XFree(class_hint.res_class); + if (class_hint.res_name != null) _ = C.XFree(class_hint.res_name); + } + + if (class_hint.res_class != null) { + const class_name = std.mem.span( + @as([*:0]const u8, @ptrCast(class_hint.res_class)), + ); + const owned = try alloc.dupe(u8, class_name); + defer alloc.free(owned); + try mod.set(owned); + } else { + try mod.set(""); + } +} + +fn ignoreError(_: ?*C.Display, _: [*c]C.XErrorEvent) callconv(.C) c_int { + return 0; +} + +fn handleError(_: ?*C.Display, _: [*c]C.XErrorEvent) callconv(.C) c_int { + return 0; +} diff --git a/src/c.zig b/src/c.zig new file mode 100644 index 0000000..1a40856 --- /dev/null +++ b/src/c.zig @@ -0,0 +1,10 @@ +pub usingnamespace @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"); + @cInclude("X11/Xft/Xft.h"); +}); diff --git a/src/draw.zig b/src/draw.zig new file mode 100644 index 0000000..cd4b652 --- /dev/null +++ b/src/draw.zig @@ -0,0 +1,162 @@ +const std = @import("std"); +const C = @import("c.zig"); + +pub const Drw = struct { + alloc: std.mem.Allocator, + dpy: *C.Display, + scr: c_int, + vis: *C.Visual, + cmap: C.Colormap, + gc: C.GC, + font: *C.XftFont, + ascent: c_int, + descent: c_int, + lh: c_int, + + pub fn init( + alloc: std.mem.Allocator, + dpy: *C.Display, + scr: c_int, + font_name: [*:0]const u8, + ) !Drw { + const font = loadfont(dpy, scr, font_name) orelse + return error.FontUnavailable; + + const root = C.RootWindow(dpy, scr); + const gc_opt = C.XCreateGC(dpy, root, 0, null); + if (gc_opt == null) { + C.XftFontClose(dpy, font); + return error.CreateGcFailed; + } + const gc = gc_opt.?; + + const vis = C.XDefaultVisual(dpy, scr); + const cmap = C.XDefaultColormap(dpy, scr); + + return Drw{ + .alloc = alloc, + .dpy = dpy, + .scr = scr, + .vis = vis, + .cmap = cmap, + .gc = gc, + .font = font, + .ascent = font.*.ascent, + .descent = font.*.descent, + .lh = font.*.ascent + font.*.descent, + }; + } + + fn loadfont( + dpy: *C.Display, + scr: c_int, + preferred: [*:0]const u8, + ) ?*C.XftFont { + const candidates = [_][*:0]const u8{ + preferred, + "monospace:size=10", + "fixed", + }; + for (candidates) |name| { + if (name[0] == 0) continue; + const font_ptr = C.XftFontOpenName(dpy, scr, name); + if (font_ptr != null) { + if (name != preferred) { + std.log.warn("bar font fallback to {s}", .{name}); + } + return font_ptr; + } + } + return null; + } + + pub fn deinit(self: *Drw) void { + C.XftFontClose(self.dpy, self.font); + _ = C.XFreeGC(self.dpy, self.gc); + } + + pub fn alloccol(self: *Drw, rgb: u32) !C.ulong { + var color: C.XColor = undefined; + color.pixel = 0; + color.red = @intCast(((rgb >> 16) & 0xff) * 257); + color.green = @intCast(((rgb >> 8) & 0xff) * 257); + color.blue = @intCast((rgb & 0xff) * 257); + color.flags = @intCast(C.DoRed | C.DoGreen | C.DoBlue); + color.pad = 0; + + if (C.XAllocColor(self.dpy, self.cmap, &color) == 0) { + return error.ColorAllocationFailed; + } + + return color.pixel; + } + + pub fn allocxftcol(self: *Drw, rgb: u32) !C.XftColor { + const r: u16 = @intCast(((rgb >> 16) & 0xff) * 257); + const g: u16 = @intCast(((rgb >> 8) & 0xff) * 257); + const b: u16 = @intCast((rgb & 0xff) * 257); + var xft_color: C.XftColor = undefined; + var render_color: C.XRenderColor = undefined; + render_color.red = r; + render_color.green = g; + render_color.blue = b; + render_color.alpha = 0xffff; + if (C.XftColorAllocValue( + self.dpy, + self.vis, + self.cmap, + &render_color, + &xft_color, + ) == 0) { + return error.ColorAllocationFailed; + } + return xft_color; + } + + pub fn rect( + self: *Drw, + draw: C.Drawable, + col: C.ulong, + x: c_int, + y: c_int, + w: c_uint, + h: c_uint, + ) void { + _ = C.XSetForeground(self.dpy, self.gc, col); + _ = C.XFillRectangle(self.dpy, draw, self.gc, x, y, w, h); + } + + pub fn text( + self: *Drw, + _: C.Drawable, + xft: *C.XftDraw, + col: *C.XftColor, + x: c_int, + y: c_int, + txt: []const u8, + ) void { + if (txt.len == 0) return; + C.XftDrawStringUtf8( + xft, + col, + self.font, + x, + y, + @ptrCast(txt.ptr), + @intCast(txt.len), + ); + } + + pub fn textw(self: *Drw, txt: []const u8) c_int { + if (txt.len == 0) return 0; + var extents: C.XGlyphInfo = undefined; + C.XftTextExtentsUtf8( + self.dpy, + self.font, + @ptrCast(txt.ptr), + @intCast(txt.len), + &extents, + ); + return @intCast(extents.xOff); + } +}; diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..d5e864c --- /dev/null +++ b/src/main.zig @@ -0,0 +1,1559 @@ +const std = @import("std"); +const C = @import("c.zig"); +const bar_mod = @import("bar.zig"); +const posix = std.posix; + +const Client = struct { + name: [256]u8 = std.mem.zeroes([256]u8), + x: c_int, + y: c_int, + w: c_int, + h: c_int, + rx: c_int = 0, + ry: c_int = 0, + rw: c_int = 0, + rh: c_int = 0, + basew: c_int = 0, + baseh: c_int = 0, + incw: c_int = 0, + inch: c_int = 0, + maxw: c_int = 0, + maxh: c_int = 0, + minw: c_int = 0, + minh: c_int = 0, + minax: c_int = 0, + minay: c_int = 0, + maxax: c_int = 0, + maxay: c_int = 0, + flags: c_long = 0, + border: c_uint = BORDER, + isfixed: bool = false, + isfloat: bool = false, + ismax: bool = false, + win: C.Window, +}; + +var cl: std.ArrayList(Client) = undefined; +var hidden: std.ArrayList(Client) = undefined; +var stack: std.ArrayList(C.Window) = undefined; +var sel: ?usize = null; + +const FOCUS_COL = 0x6699cc; +const NORMAL_COL = 0x333333; +const BORDER = 2; +const GAP: c_int = 3; +const MFACT = 0.6; + +var mfact: f32 = MFACT; +var nmaster: usize = 1; +var keyacts: [256]?*const fn () void = .{null} ** 256; + +const user = "seb"; +const terminal = "st"; +const launcher = "zmen"; + +const autostart = std.fmt.comptimePrint("/home/{s}/.scripts/2zw.sh", .{user}); +const bar_font: [*:0]const u8 = "monospace:size=15"; +const delim = "::"; + +const Bind = struct { sym: C.KeySym, action: *const fn () void }; + +const binds = [_]Bind{ + .{ .sym = C.XK_q, .action = &killclient }, + .{ .sym = C.XK_comma, .action = &winprev }, + .{ .sym = C.XK_period, .action = &winnext }, + .{ .sym = C.XK_a, .action = &act_show }, + .{ .sym = C.XK_d, .action = &act_hide }, + .{ .sym = C.XK_h, .action = &act_mfact_dec }, + .{ .sym = C.XK_l, .action = &act_mfact_inc }, + .{ .sym = C.XK_Return, .action = spawnfn(terminal) }, + .{ .sym = C.XK_p, .action = spawnfn(launcher) }, + .{ .sym = C.XK_s, .action = spawnfn("slock") }, +}; + +var cur_resize: C.Cursor = undefined; +var cur_move: C.Cursor = undefined; +var cur_normal: C.Cursor = undefined; + +var alloc: std.mem.Allocator = undefined; + +var atom_proto: C.Atom = undefined; +var atom_del: C.Atom = undefined; +var atom_state: C.Atom = undefined; +var atom_net: C.Atom = undefined; +var atom_name: C.Atom = undefined; +var atom_active: C.Atom = undefined; + +fn spawnfn(comptime cmd: [*:0]const u8) *const fn () void { + return struct { + fn action() void { + spawn(cmd); + } + }.action; +} + +fn act_show() void { + show(); +} + +fn act_hide() void { + hide(); +} + +fn act_mfact_dec() void { + mfact = @max(0.1, mfact - 0.05); + tile(); +} + +fn act_mfact_inc() void { + mfact = @min(0.9, mfact + 0.05); + tile(); +} + +fn sigchldignore() void { + var sa: std.c.Sigaction = .{ + .handler = .{ .handler = std.c.SIG.IGN }, + .mask = std.c.empty_sigset, + .flags = 0, + }; + _ = std.c.sigaction(std.c.SIG.CHLD, &sa, null); +} + +fn detachstack(win: C.Window) void { + var i: usize = 0; + while (i < stack.items.len) : (i += 1) { + if (stack.items[i] == win) { + _ = stack.orderedRemove(i); + return; + } + } +} + +fn attachstack(win: C.Window) !void { + detachstack(win); + try stack.insert(0, win); +} + +fn restack() void { + const selected_idx = sel orelse return; + const selected = &cl.items[selected_idx]; + + if (selected.isfloat) { + _ = C.XRaiseWindow(display, selected.win); + } else { + _ = C.XLowerWindow(display, selected.win); + } + + var to_lower = std.ArrayList(C.Window).init(alloc); + defer to_lower.deinit(); + + for (cl.items, 0..) |client, idx| { + if (idx == selected_idx or client.isfloat) continue; + to_lower.append(client.win) catch continue; + } + + for (to_lower.items) |win| { + _ = C.XLowerWindow(display, win); + } + + _ = C.XSync(display, 0); +} + +fn grabinputs(window: C.Window) void { + _ = C.XUngrabKey(display, C.AnyKey, C.AnyModifier, root); + + for (binds) |b| { + _ = C.XGrabKey( + display, + C.XKeysymToKeycode(display, b.sym), + C.Mod4Mask, + window, + 0, + C.GrabModeAsync, + C.GrabModeAsync, + ); + } + for ([_]u8{ 1, 3 }) |btn| { + const mask = C.ButtonPressMask | + C.ButtonReleaseMask | + C.PointerMotionMask; + _ = C.XGrabButton( + display, + btn, + C.Mod4Mask, + root, + 0, + mask, + C.GrabModeAsync, + C.GrabModeAsync, + 0, + 0, + ); + } +} + +fn setkeys() void { + for (&keyacts) |*slot| slot.* = null; + for (binds) |b| { + const kc = C.XKeysymToKeycode(display, b.sym); + if (kc < keyacts.len) { + keyacts[kc] = b.action; + } + } +} + +fn show() void { + if (hidden.items.len == 0) return; + const client = hidden.orderedRemove(0); + + _ = C.XMapWindow(display, client.win); + cl.append(client) catch return; + + tile(); + focus(cl.items.len - 1); +} + +fn hide() void { + const idx = sel orelse return; + const client = cl.orderedRemove(idx); + + detachstack(client.win); + _ = C.XUnmapWindow(display, client.win); + hidden.append(client) catch return; + + sel = if (cl.items.len > 0) @min(idx, cl.items.len - 1) else null; + focus(sel); + tile(); +} + +fn scan(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) return; + + for (0..num_children) |i| { + var wa: C.XWindowAttributes = undefined; + if (C.XGetWindowAttributes(display, children[i], &wa) == 0) continue; + if (wa.override_redirect != 0 or wa.map_state != C.IsViewable) + continue; + manage(allocator, children[i]); + } + + if (children != null) _ = C.XFree(children); + + if (cl.items.len > 0) tile(); +} + +fn floatwins() void { + var last: ?usize = null; + + for (cl.items, 0..) |*c, i| { + if (!c.isfloat) continue; + _ = C.XMoveWindow(display, c.win, c.x, c.y); + _ = C.XRaiseWindow(display, c.win); + last = i; + } + + if (last) |idx| { + focus(idx); + } +} + +inline fn todim(v: c_int) c_uint { + return @intCast(@max(0, v)); +} + +fn tile() void { + const n = counttilable(); + if (n == 0) { + floatwins(); + return; + } + + const bar_h: c_int = if (bar) |b| b.barH() else 0; + const my: c_int = sy_offset + if (bar) |b| if (b.pos == .top) bar_h else 0 else 0; + const mh: c_int = @intCast(sh - @as(c_uint, @intCast(bar_h))); + const mw: c_int = @intCast(sw); + + const n_c: c_int = @intCast(n); + const nmaster_c: c_int = @min(@as(c_int, @intCast(nmaster)), n_c); + const stack_n: c_int = n_c - nmaster_c; + + const master_w: c_int = if (stack_n == 0) + mw - 2 * GAP + else + @as(c_int, @intFromFloat(@as(f32, @floatFromInt(mw)) * mfact)) - GAP - GAP / 2; + + const stack_w: c_int = mw - master_w - 3 * GAP; + + var mi: c_int = 0; + var si: c_int = 0; + + for (cl.items) |*c| { + if (c.isfloat) continue; + + if (mi < nmaster_c) { + const h = @divTrunc(mh - GAP * (nmaster_c + 1), nmaster_c); + resize(c, sx_offset + GAP, my + GAP + mi * (h + GAP), master_w, h); + mi += 1; + } else { + const h = @divTrunc(mh - GAP * (stack_n + 1), stack_n); + resize(c, sx_offset + master_w + 2 * GAP, my + GAP + si * (h + GAP), stack_w, h); + si += 1; + } + } + + floatwins(); +} + +fn counttilable() usize { + var n: usize = 0; + for (cl.items) |c| { + if (!c.isfloat) n += 1; + } + return n; +} + +fn resize(c: *Client, x: c_int, y: c_int, w: c_int, h: c_int) void { + c.x = x; + c.y = y; + c.w = w; + c.h = h; + _ = C.XMoveResizeWindow(display, c.win, x, y, todim(w - 2 * BORDER), todim(h - 2 * BORDER)); +} + +fn addclient(allocator: std.mem.Allocator, window: C.Window) !usize { + var attributes: C.XWindowAttributes = undefined; + _ = C.XGetWindowAttributes(display, window, &attributes); + + const m_float = isfloat(window); + + const client = Client{ + .x = attributes.x, + .y = attributes.y, + .w = attributes.width, + .h = attributes.height, + .win = window, + .isfloat = m_float, + }; + + try cl.insert(0, client); + + if (sel) |sel_idx| { + sel = sel_idx + 1; + } + + sizehints(&cl.items[0]); + title(allocator, &cl.items[0]); + + return 0; +} + +// refactor +fn isfloat(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(ignoreerr); + + const atom_type = C.XInternAtom(display, "_NET_WM_WINDOW_TYPE", 0); + if (atom_type != 0) { + const status = C.XGetWindowProperty( + display, + window, + atom_type, + 0, + 32, + 0, + C.XA_ATOM, + &actual_type, + &actual_format, + &nitems, + &bytes_after, + &prop_return, + ); + + const got_atoms = status == 0 and prop_return != null; + const good_format = + actual_type == C.XA_ATOM and actual_format == 32 and nitems > 0; + + if (got_atoms and good_format) { + const atoms = @as([*]C.Atom, @ptrCast(@alignCast(prop_return))); + const atom_dialog = C.XInternAtom( + display, + "_NET_WM_WINDOW_TYPE_DIALOG", + 0, + ); + const atom_utility = C.XInternAtom( + display, + "_NET_WM_WINDOW_TYPE_UTILITY", + 0, + ); + const atom_popup = C.XInternAtom( + display, + "_NET_WM_WINDOW_TYPE_POPUP_MENU", + 0, + ); + + for (0..nitems) |i| { + const atom = atoms[i]; + if (atom == atom_dialog or + atom == atom_utility or + atom == atom_popup) + { + _ = C.XFree(prop_return); + _ = C.XSetErrorHandler(xerr); + return true; + } + } + + _ = C.XFree(prop_return); + } + } + + _ = C.XSetErrorHandler(xerr); + + return false; +} + +fn focus(target_index_opt: ?usize) void { + if (cl.items.len == 0) { + sel = null; + if (bar) |b_ptr| b_ptr.setSel(null); + _ = C.XSetInputFocus( + display, + root, + C.RevertToPointerRoot, + C.CurrentTime, + ); + _ = C.XFlush(display); + return; + } + + const target_index = target_index_opt orelse { + if (sel) |sel_idx| { + if (sel_idx < cl.items.len) { + _ = C.XSetErrorHandler(ignoreerr); + _ = C.XSetWindowBorder( + display, + cl.items[sel_idx].win, + NORMAL_COL, + ); + _ = C.XSetErrorHandler(xerr); + } + } + sel = null; + if (bar) |b_ptr| b_ptr.setSel(null); + _ = C.XSetInputFocus( + display, + root, + C.RevertToPointerRoot, + C.CurrentTime, + ); + _ = C.XFlush(display); + return; + }; + + var index = target_index; + if (index >= cl.items.len) { + index = 0; + } + + if (sel) |sel_idx| { + if (sel_idx < cl.items.len and sel_idx != index) { + _ = C.XSetErrorHandler(ignoreerr); + _ = C.XSetWindowBorder(display, cl.items[sel_idx].win, NORMAL_COL); + _ = C.XSetErrorHandler(xerr); + } + } + + const client = &cl.items[index]; + + detachstack(client.win); + attachstack(client.win) catch {}; + + _ = C.XSetErrorHandler(ignoreerr); + _ = C.XSetInputFocus(display, client.win, C.RevertToParent, C.CurrentTime); + _ = C.XRaiseWindow(display, client.win); + _ = C.XSetWindowBorder(display, client.win, FOCUS_COL); + _ = C.XChangeProperty( + display, + root, + atom_active, + C.XA_WINDOW, + 32, + C.PropModeReplace, + @ptrCast(&client.win), + 1, + ); + _ = C.XSetErrorHandler(xerr); + + sel = index; + if (bar) |b_ptr| b_ptr.setSel(client.win); + + _ = C.XFlush(display); +} + +fn getclient(w: C.Window) ?usize { + for (cl.items, 0..) |client, idx| { + if (client.win == w) return idx; + } + return null; +} + +fn hiddenidx(w: C.Window) ?usize { + for (hidden.items, 0..) |client, idx| { + if (client.win == w) return idx; + } + return null; +} + +fn purgehidden(w: C.Window) void { + if (hiddenidx(w)) |idx| { + _ = hidden.orderedRemove(idx); + detachstack(w); + } +} + +fn nextfocus(exclude_win: C.Window) ?usize { + for (stack.items) |win| { + if (win == exclude_win) continue; + if (getclient(win)) |idx| { + return idx; + } + } + return null; +} + +fn unmanage(index: usize, destroyed: bool) void { + if (index >= cl.items.len) return; + + _ = C.XGrabServer(display); + _ = C.XSetErrorHandler(ignoreerr); + + const client = cl.items[index]; + + if (!destroyed) { + _ = C.XSelectInput(display, client.win, C.NoEventMask); + _ = C.XUngrabButton(display, C.AnyButton, C.AnyModifier, client.win); + + const data = [_]c_long{ C.WithdrawnState, C.None }; + _ = C.XChangeProperty( + display, + client.win, + atom_state, + atom_state, + 32, + C.PropModeReplace, + @ptrCast(&data), + data.len, + ); + } + + var removed_was_selected = false; + if (sel) |sel_idx| { + if (sel_idx == index) { + removed_was_selected = true; + } else if (sel_idx > index) { + sel = sel_idx - 1; + } + } + + detachstack(client.win); + _ = cl.orderedRemove(index); + + var next_focus_index: ?usize = null; + if (removed_was_selected) { + sel = null; + next_focus_index = nextfocus(client.win); + } + + _ = C.XSync(display, 0); + _ = C.XSetErrorHandler(xerr); + _ = C.XUngrabServer(display); + + if (cl.items.len > 0) { + tile(); + const fallback = next_focus_index orelse blk: { + if (index < cl.items.len) break :blk index; + break :blk cl.items.len - 1; + }; + focus(fallback); + } else { + focus(null); + _ = C.XSync(display, 0); + } +} + +var sw: c_uint = 0; +var sh: c_uint = 0; +var sx_offset: c_int = 0; +var sy_offset: c_int = 0; +var srotation: c_uint = 0; + +var rr_event: c_int = 0; +var rr_error: c_int = 0; + +var bar: ?*bar_mod.Bar = null; + +const NS_PER_MS: u64 = 1_000_000; + +fn initrandr(allocator: std.mem.Allocator) !void { + if (C.XRRQueryExtension(display, &rr_event, &rr_error) == 0) { + return; + } + + try geom(allocator); + const rr_mask = + C.RROutputChangeNotifyMask | + C.RRCrtcChangeNotifyMask | + C.RRScreenChangeNotifyMask; + _ = C.XRRSelectInput(display, root, rr_mask); +} + +fn geom(allocator: std.mem.Allocator) !void { + var res = C.XRRGetScreenResourcesCurrent(display, root); + if (res == null) { + res = C.XRRGetScreenResources(display, root); + } + if (res == null) { + const x_screen = C.DefaultScreen(display); + sw = @intCast(C.XDisplayWidth(display, x_screen)); + sh = @intCast(C.XDisplayHeight(display, x_screen)); + sx_offset = 0; + sy_offset = 0; + srotation = 0; + 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 usemon( + allocator, + res, + primary_output, + "primary", + ); + } + + if (!found_active_monitor) { + found_active_monitor = try lmon(allocator, res); + } + + if (!found_active_monitor) { + const x_screen = C.DefaultScreen(display); + sw = @intCast(C.XDisplayWidth(display, x_screen)); + sh = @intCast(C.XDisplayHeight(display, x_screen)); + sx_offset = 0; + sy_offset = 0; + srotation = 0; + } +} + +fn usemon( + allocator: std.mem.Allocator, + res: *C.XRRScreenResources, + output_id: C.RROutput, + monitor_type: []const u8, +) !bool { + _ = allocator; + const output_info = C.XRRGetOutputInfo(display, res, output_id); + if (output_info == null) return false; + defer C.XRRFreeOutputInfo(output_info); + + if (output_info.*.connection != C.RR_Connected or output_info.*.crtc == 0) { + return false; + } + + const crtc_info = C.XRRGetCrtcInfo(display, res, output_info.*.crtc); + if (crtc_info == null) return false; + defer C.XRRFreeCrtcInfo(crtc_info); + + if (crtc_info.*.width == 0 or crtc_info.*.height == 0) { + return false; + } + + sw = @intCast(crtc_info.*.width); + sh = @intCast(crtc_info.*.height); + sx_offset = crtc_info.*.x; + sy_offset = crtc_info.*.y; + srotation = @intCast(crtc_info.*.rotation); + + const output_name = std.mem.span( + @as([*:0]const u8, @ptrCast(output_info.*.name)), + ); + std.log.info("Using {s} monitor: {s} at ({d},{d}) size {d}x{d}", .{ + monitor_type, + output_name, + sx_offset, + sy_offset, + sw, + sh, + }); + + return true; +} + +fn lmon(_: 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_x: c_int = 0; + var largest_y: c_int = 0; + var largest_rotation: c_uint = 0; + + const output_count: usize = @intCast(res.*.noutput); + for (0..output_count) |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: c_uint = @intCast(crtc_info.*.width); + const output_height: 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; + largest_x = crtc_info.*.x; + largest_y = crtc_info.*.y; + largest_rotation = @intCast(crtc_info.*.rotation); + } + } + + if (largest_area > 0) { + sw = largest_width; + sh = largest_height; + sx_offset = largest_x; + sy_offset = largest_y; + srotation = largest_rotation; + return true; + } + + return false; +} + +fn syncmon(allocator: std.mem.Allocator) void { + geom(allocator) catch {}; +} + +fn onrr(allocator: std.mem.Allocator, e: *C.XEvent) !void { + const rrev = @as(*C.XRRNotifyEvent, @ptrCast(e)); + + switch (rrev.subtype) { + C.RRNotify_OutputChange, + C.RRNotify_CrtcChange, + => try refreshmon(allocator), + else => {}, + } +} + +fn onrrscreen(allocator: std.mem.Allocator, e: *C.XEvent) !void { + _ = C.XRRUpdateConfiguration(e); + try refreshmon(allocator); +} + +fn refreshmon(allocator: std.mem.Allocator) !void { + try geom(allocator); + if (bar) |b_ptr| { + b_ptr.resize(sw, sx_offset, sy_offset, sh); + b_ptr.mark(); + } + if (cl.items.len > 0) { + tile(); + } +} + +fn runautostart() void { + const pid = posix.fork() catch return; + if (pid == 0) { + _ = setsid(); + const args = [_:null]?[*:0]const u8{ "/bin/sh", autostart, null }; + _ = execvp("/bin/sh", &args); + posix.exit(1); + } +} + +fn killclient() void { + const index = sel orelse return; + + if (index >= cl.items.len) return; + + const client_win = cl.items[index].win; + + if (protodel(index)) { + _ = C.XSetErrorHandler(ignoreerr); + sendev(client_win, atom_proto, @as(c_long, @bitCast(atom_del))); + _ = C.XSetErrorHandler(xerr); + } else { + _ = C.XGrabServer(display); + _ = C.XSetErrorHandler(ignoreerr); + _ = C.XKillClient(display, client_win); + _ = C.XSync(display, 0); + _ = C.XSetErrorHandler(xerr); + _ = C.XUngrabServer(display); + } + + _ = C.XSync(display, 0); +} + +fn protodel(client_index: usize) bool { + if (client_index >= cl.items.len) return false; + + const client = cl.items[client_index]; + var protocols: [*c]C.Atom = undefined; + var n: c_int = 0; + var ret = false; + + if (C.XGetWMProtocols(display, client.win, &protocols, &n) != 0) { + const hint_count: usize = @intCast(n); + for (0..hint_count) |i| { + if (protocols[i] == atom_del) { + ret = true; + break; + } + } + _ = C.XFree(protocols); + } + return ret; +} + +fn sendev(win: C.Window, atom: C.Atom, value: c_long) void { + var event: C.XEvent = undefined; + event.type = C.ClientMessage; + event.xclient.window = win; + event.xclient.message_type = atom; + event.xclient.format = 32; + event.xclient.data.l[0] = value; + event.xclient.data.l[1] = C.CurrentTime; + _ = C.XSendEvent(display, win, 0, C.NoEventMask, &event); + _ = C.XSync(display, 0); +} + +fn sizehints(client: *Client) void { + var size: C.XSizeHints = undefined; + var msize: c_long = 0; + + if (C.XGetWMNormalHints(display, client.win, &size, &msize) == 0 or + size.flags == 0) + { + size.flags = C.PSize; + } + + client.flags = size.flags; + + if ((client.flags & C.PBaseSize) != 0) { + client.basew = size.base_width; + client.baseh = size.base_height; + } else { + client.basew = 0; + client.baseh = 0; + } + + if ((client.flags & C.PResizeInc) != 0) { + client.incw = size.width_inc; + client.inch = size.height_inc; + } else { + client.incw = 0; + client.inch = 0; + } + + if ((client.flags & C.PMaxSize) != 0) { + client.maxw = size.max_width; + client.maxh = size.max_height; + } else { + client.maxw = 0; + client.maxh = 0; + } + + if ((client.flags & C.PMinSize) != 0) { + client.minw = size.min_width; + client.minh = size.min_height; + } else { + client.minw = 0; + client.minh = 0; + } + + if ((client.flags & C.PAspect) != 0) { + client.minax = size.min_aspect.x; + client.minay = size.min_aspect.y; + client.maxax = size.max_aspect.x; + client.maxay = size.max_aspect.y; + } else { + client.minax = 0; + client.minay = 0; + client.maxax = 0; + client.maxay = 0; + } + + client.isfixed = (client.maxw != 0 and client.minw != 0 and + client.maxh != 0 and client.minh != 0 and + client.maxw == client.minw and + client.maxh == client.minh); +} + +fn title(_: std.mem.Allocator, client: *Client) void { + var name: C.XTextProperty = undefined; + + client.name[0] = 0; + + _ = C.XGetTextProperty(display, client.win, &name, atom_name); + if (name.nitems == 0) { + _ = C.XGetWMName(display, client.win, &name); + } + + if (name.nitems == 0) return; + + if (name.encoding == C.XA_STRING) { + const title_len = @min(name.nitems, client.name.len - 1); + @memcpy(client.name[0..title_len], name.value[0..title_len]); + client.name[title_len] = 0; + } else { + var list: [*c][*c]u8 = undefined; + var count: c_int = 0; + + if (C.XmbTextPropertyToTextList(display, &name, &list, &count) >= 0 and + count > 0) + { + const src_title = std.mem.span( + @as([*:0]const u8, @ptrCast(list[0])), + ); + const title_len = @min(src_title.len, client.name.len - 1); + @memcpy(client.name[0..title_len], src_title[0..title_len]); + client.name[title_len] = 0; + _ = C.XFreeStringList(list); + } + } + + _ = C.XFree(name.value); +} + +fn winnext() void { + if (cl.items.len == 0) return; + + if (sel) |idx| { + if (idx + 1 < cl.items.len) { + focus(idx + 1); + } else { + focus(0); + } + } else { + focus(0); + } +} + +fn winprev() void { + if (cl.items.len == 0) return; + + if (sel) |idx| { + if (idx > 0) { + focus(idx - 1); + } else { + focus(cl.items.len - 1); + } + } else { + focus(0); + } +} + +extern fn execvp(prog: [*:0]const u8, argv: [*]const ?[*:0]const u8) c_int; +extern fn setsid() c_int; + +fn spawn(cmd: [*:0]const u8) void { + const pid = posix.fork() catch return; + if (pid == 0) { + _ = std.c.close(C.ConnectionNumber(display)); + _ = setsid(); + var args = [_:null]?[*:0]const u8{ cmd, null }; + _ = execvp(cmd, &args); + posix.exit(1); + } +} + +fn xerr(_: ?*C.Display, event: [*c]C.XErrorEvent) callconv(.C) c_int { + if (@as(*C.XErrorEvent, @ptrCast(event)).error_code == C.BadAccess) + std.log.err("another wm running", .{}); + return 0; +} + +fn ignoreerr(_: ?*C.Display, _: [*c]C.XErrorEvent) callconv(.C) c_int { + return 0; +} + +fn onconfig(e: *C.XConfigureRequestEvent) void { + wc.x = e.x; + wc.y = e.y; + wc.width = e.width; + wc.height = e.height; + wc.border_width = e.border_width; + wc.sibling = e.above; + wc.stack_mode = e.detail; + + _ = C.XSetErrorHandler(ignoreerr); + _ = C.XConfigureWindow(display, e.window, @intCast(e.value_mask), &wc); + _ = C.XSync(display, 0); + _ = C.XSetErrorHandler(xerr); +} + +fn onmap(allocator: std.mem.Allocator, event: *C.XEvent) !void { + const window: C.Window = event.xmaprequest.window; + + var wa: C.XWindowAttributes = undefined; + + if (C.XGetWindowAttributes(display, window, &wa) == 0) { + return; + } + + if (wa.override_redirect != 0) { + return; + } + + if (getclient(window) != null) { + return; + } + + _ = C.XSetErrorHandler(ignoreerr); + _ = C.XSelectInput( + display, + window, + C.StructureNotifyMask | C.EnterWindowMask, + ); + _ = C.XSetWindowBorderWidth(display, window, BORDER); + _ = C.XSetWindowBorder(display, window, NORMAL_COL); + _ = C.XSetErrorHandler(xerr); + + const index = addclient(allocator, window) catch |err| { + _ = C.XSetErrorHandler(ignoreerr); + _ = C.XUnmapWindow(display, window); + _ = C.XSync(display, 0); + _ = C.XSetErrorHandler(xerr); + return err; + }; + + const client = &cl.items[index]; + + _ = C.XSetErrorHandler(ignoreerr); + const screen_width_i: c_int = @intCast(sw); + _ = C.XMoveWindow(display, window, client.x + 2 * screen_width_i, client.y); + _ = C.XMapWindow(display, window); + _ = C.XSetErrorHandler(xerr); + + const data = [_]c_long{ C.NormalState, C.None }; + _ = C.XChangeProperty( + display, + window, + atom_state, + atom_state, + 32, + C.PropModeReplace, + @ptrCast(&data), + data.len, + ); + + tile(); + focus(index); + + if (client.isfloat) { + _ = C.XSetErrorHandler(ignoreerr); + _ = C.XRaiseWindow(display, client.win); + _ = C.XSetErrorHandler(xerr); + focus(index); + } + + _ = C.XSync(display, 0); +} + +fn manage(allocator: std.mem.Allocator, win: C.Window) void { + _ = C.XSelectInput(display, win, C.StructureNotifyMask | C.EnterWindowMask); + _ = C.XSetWindowBorderWidth(display, win, BORDER); + _ = C.XSetWindowBorder(display, win, NORMAL_COL); + + if (addclient(allocator, win)) |idx| { + focus(idx); + } else |_| {} +} + +fn onunmap(_: std.mem.Allocator, e: *C.XEvent) void { + const ev = &e.xunmap; + if (getclient(ev.window)) |index| { + if (ev.send_event == 1) { + const data = [_]c_long{ C.WithdrawnState, C.None }; + _ = C.XChangeProperty( + display, + ev.window, + atom_state, + atom_state, + 32, + C.PropModeReplace, + @ptrCast(&data), + data.len, + ); + } else { + unmanage(index, false); + } + } +} + +fn onkey(e: *C.XEvent) void { + if (e.xkey.keycode < keyacts.len) { + if (keyacts[e.xkey.keycode]) |action| action(); + } +} + +fn onenter(e: *C.XEvent) void { + for (cl.items) |client| { + if (client.isfloat) return; + } + + if (e.xcrossing.mode != C.NotifyNormal or + e.xcrossing.detail == C.NotifyInferior) + { + return; + } + + _ = C.XSetErrorHandler(ignoreerr); + defer _ = C.XSetErrorHandler(xerr); + + if (getclient(e.xcrossing.window)) |index| { + if (sel) |sel_idx| { + if (sel_idx < cl.items.len and + cl.items[sel_idx].win == e.xcrossing.window) + { + return; + } + } + + if (!cl.items[index].isfloat) { + focus(index); + } + } +} + +fn onbtn(e: *C.XEvent) void { + if (e.xbutton.subwindow == 0) return; + + _ = C.XSetErrorHandler(ignoreerr); + defer _ = C.XSetErrorHandler(xerr); + + var attributes: C.XWindowAttributes = undefined; + if (C.XGetWindowAttributes( + display, + e.xbutton.subwindow, + &attributes, + ) == 0) { + return; + } + + win_w = attributes.width; + win_h = attributes.height; + win_x = attributes.x; + win_y = attributes.y; + + const grab_mask = C.ButtonReleaseMask | C.PointerMotionMask; + + if (e.xbutton.button == 3 and (e.xbutton.state & C.Mod4Mask) != 0) { + const grab = C.XGrabPointer( + display, + e.xbutton.subwindow, + 1, + grab_mask, + C.GrabModeAsync, + C.GrabModeAsync, + C.None, + cur_resize, + C.CurrentTime, + ); + if (grab == C.GrabSuccess) { + _ = 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) { + const grab = C.XGrabPointer( + display, + e.xbutton.subwindow, + 1, + grab_mask, + C.GrabModeAsync, + C.GrabModeAsync, + C.None, + cur_move, + C.CurrentTime, + ); + if (grab == C.GrabSuccess) { + mouse = e.xbutton; + } + } + + if (getclient(e.xbutton.subwindow)) |index| { + if (sel == null or sel.? != index) { + focus(index); + } + } + + _ = C.XSync(display, 0); +} + +fn onmotion(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) { + const new_w: c_int = @intCast(@max(10, win_w + dx)); + const new_h: c_int = @intCast(@max(10, win_h + dy)); + _ = C.XMoveResizeWindow( + display, + mouse.subwindow, + win_x, + win_y, + todim(new_w), + todim(new_h), + ); + } + _ = C.XSync(display, 0); +} + +fn ondestroy(_: std.mem.Allocator, e: *C.XEvent) void { + const ev = &e.xdestroywindow; + if (getclient(ev.window)) |index| { + unmanage(index, true); + } else { + purgehidden(ev.window); + } +} + +fn onbtnup(_: *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 wc: C.XWindowChanges = undefined; +var lists = false; +var cur = false; +var disp = false; +var barset = false; + +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; + +fn setbar(allocator: std.mem.Allocator, screen: c_int) !void { + if (bar != null) return; + + const bar_ptr = allocator.create(bar_mod.Bar) catch |err| return err; + bar_ptr.* = bar_mod.Bar.init( + allocator, + display, + root, + screen, + sw, + sx_offset, + sy_offset, + sh, + .top, + bar_font, + delim, + ) catch |err| { + allocator.destroy(bar_ptr); + return err; + }; + bar_mod.setGlobalBar(bar_ptr); + bar = bar_ptr; + barset = true; + try bar_ptr.addBat(30000); + try bar_ptr.addWin(100); + try bar_ptr.addClock(1000); +} + +fn setup(allocator: std.mem.Allocator) !void { + errdefer cleanup(); + + cl = std.ArrayList(Client).init(allocator); + hidden = std.ArrayList(Client).init(allocator); + stack = std.ArrayList(C.Window).init(allocator); + lists = true; + + display = C.XOpenDisplay(0) orelse { + std.c._exit(1); + }; + disp = true; + + cur_resize = C.XCreateFontCursor(display, 120); + cur_move = C.XCreateFontCursor(display, 52); + cur_normal = C.XCreateFontCursor(display, 68); + cur = true; + + const screen = C.DefaultScreen(display); + root = C.RootWindow(display, screen); + + sw = @intCast(C.XDisplayWidth(display, screen)); + sh = @intCast(C.XDisplayHeight(display, screen)); + sx_offset = 0; + sy_offset = 0; + srotation = 0; + + // setup atoms + atom_proto = C.XInternAtom(display, "WM_PROTOCOLS", 0); + atom_del = C.XInternAtom(display, "WM_DELETE_WINDOW", 0); + atom_state = C.XInternAtom(display, "WM_STATE", 0); + atom_net = C.XInternAtom(display, "_NET_SUPPORTED", 0); + atom_name = C.XInternAtom(display, "_NET_WM_NAME", 0); + atom_active = C.XInternAtom(display, "_NET_ACTIVE_WINDOW", 0); + + const supported_atoms = [_]C.Atom{ atom_net, atom_name, atom_active }; + _ = C.XChangeProperty( + display, + root, + atom_net, + C.XA_ATOM, + 32, + C.PropModeReplace, + @ptrCast(&supported_atoms), + supported_atoms.len, + ); + + try initrandr(allocator); + + try setbar(allocator, screen); + + _ = C.XSetErrorHandler(xerr); + _ = C.XSelectInput( + display, + root, + C.SubstructureRedirectMask | C.EnterWindowMask, + ); + _ = C.XDefineCursor(display, root, C.XCreateFontCursor(display, 68)); + + grabinputs(root); + setkeys(); + sigchldignore(); + + runautostart(); + + syncmon(allocator); + if (bar) |b_ptr| { + b_ptr.resize(sw, sx_offset, sy_offset, sh); + b_ptr.mark(); + } + + _ = C.XSync(display, 0); + + scan(allocator); + + if (bar) |b_ptr| { + const now_ms = std.time.milliTimestamp(); + _ = b_ptr.tick(now_ms) catch {}; + b_ptr.draw(); + } +} + +fn cleanup() void { + if (barset) { + if (bar) |b_ptr| { + b_ptr.deinit(); + alloc.destroy(b_ptr); + } + bar = null; + barset = false; + } + + if (cur and disp) { + _ = C.XFreeCursor(display, cur_resize); + _ = C.XFreeCursor(display, cur_move); + _ = C.XFreeCursor(display, cur_normal); + cur = false; + } + + if (disp) { + _ = C.XCloseDisplay(display); + disp = false; + } + + if (lists) { + stack.deinit(); + hidden.deinit(); + cl.deinit(); + lists = false; + } +} + +fn run(allocator: std.mem.Allocator) !void { + var event: C.XEvent = undefined; + + while (!shouldquit) { + const pending = C.XPending(display); + if (pending == 0) { + var sleep_ns: u64 = 50 * NS_PER_MS; + if (bar) |b_ptr| { + const now_ms = std.time.milliTimestamp(); + _ = b_ptr.tick(now_ms) catch {}; + if (b_ptr.dirty) b_ptr.draw(); + const wait_ms = b_ptr.nextDelay(now_ms); + const desired_ns = wait_ms * NS_PER_MS; + if (desired_ns < sleep_ns) { + sleep_ns = desired_ns; + } + } + if (sleep_ns > 0) { + std.time.sleep(sleep_ns); + } + continue; + } + + _ = C.XNextEvent(display, &event); + + switch (event.type) { + C.MapRequest => try onmap(allocator, &event), + C.UnmapNotify => onunmap(allocator, &event), + C.KeyPress => onkey(&event), + C.ButtonPress => onbtn(&event), + C.ButtonRelease => onbtnup(&event), + C.MotionNotify => onmotion(&event), + C.DestroyNotify => ondestroy(allocator, &event), + C.ConfigureRequest => onconfig(@ptrCast(&event)), + C.EnterNotify => onenter(&event), + C.Expose => { + if (bar) |b_ptr| { + if (event.xexpose.window == b_ptr.win and + event.xexpose.count == 0) + { + b_ptr.mark(); + b_ptr.draw(); + } + } + }, + else => { + if (rr_event != 0) { + if (event.type == rr_event + C.RRScreenChangeNotify) { + try onrrscreen(allocator, &event); + } else if (event.type == rr_event + C.RRNotify) { + try onrr(allocator, &event); + } + } + }, + } + + if (bar) |b_ptr| { + const now_ms = std.time.milliTimestamp(); + _ = b_ptr.tick(now_ms) catch {}; + if (b_ptr.dirty) b_ptr.draw(); + } + } +} + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + defer _ = gpa.deinit(); + alloc = allocator; + + try setup(allocator); + defer cleanup(); + + try run(allocator); +}