2zw
Owner: IIIlllIIIllI URL: git@git.0x00nyx.xyz:seb/2zw.git
init commit, new repo
Commit ab24debf4d365e998e68b7dd6b1cd71f70524cfa by IIIlllIIIllI <seb.michalk@gmail.com> on 2025-12-20 12:02:13 +0100
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);
+}