wm
init
197ffef71b83b698c134544a25a9dbb404d4f3bd
SM <seb.michalk@gmail.com>
2026-04-27 07:33:03 +0000
build.zig | 18 ++ src/atoms.zig | 121 +++++++++++ src/bar.zig | 214 +++++++++++++++++++ src/c.zig | 8 + src/client.zig | 636 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/config.zig | 56 +++++ src/event.zig | 350 +++++++++++++++++++++++++++++++ src/main.zig | 205 +++++++++++++++++++ src/util.zig | 21 ++ zig-out/bin/wm | Bin 0 -> 50304 bytes 10 files changed, 1629 insertions(+) diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..62d3956 --- /dev/null +++ b/build.zig @@ -0,0 +1,18 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "wm", + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + exe.linkSystemLibrary("X11"); + exe.linkSystemLibrary("Xft"); + exe.linkLibC(); + + b.installArtifact(exe); +} diff --git a/src/atoms.zig b/src/atoms.zig new file mode 100644 index 0000000..7a597da --- /dev/null +++ b/src/atoms.zig @@ -0,0 +1,121 @@ +const c = @import("c.zig").c; + +pub const Atoms = struct { + wm_protocols: c.Atom, + wm_delete_window: c.Atom, + wm_take_focus: c.Atom, + wm_state: c.Atom, + wm_change_state: c.Atom, + net_supported: c.Atom, + net_supporting_wm_check: c.Atom, + net_wm_name: c.Atom, + net_wm_state: c.Atom, + net_wm_state_fullscreen: c.Atom, + net_wm_state_hidden: c.Atom, + net_wm_state_demands_attention: c.Atom, + net_wm_state_above: c.Atom, + net_wm_window_type: c.Atom, + net_wm_window_type_normal: c.Atom, + net_wm_window_type_dialog: c.Atom, + net_wm_window_type_utility: c.Atom, + net_wm_window_type_toolbar: c.Atom, + net_wm_window_type_splash: c.Atom, + net_wm_window_type_menu: c.Atom, + net_wm_window_type_dock: c.Atom, + net_active_window: c.Atom, + net_close_window: c.Atom, + net_client_list: c.Atom, + net_client_list_stacking: c.Atom, + net_frame_extents: c.Atom, + net_wm_pid: c.Atom, + + pub fn init(display: ?*c.Display) Atoms { + const names = [_][*:0]const u8{ + "WM_PROTOCOLS", + "WM_DELETE_WINDOW", + "WM_TAKE_FOCUS", + "WM_STATE", + "WM_CHANGE_STATE", + "_NET_SUPPORTED", + "_NET_SUPPORTING_WM_CHECK", + "_NET_WM_NAME", + "_NET_WM_STATE", + "_NET_WM_STATE_FULLSCREEN", + "_NET_WM_STATE_HIDDEN", + "_NET_WM_STATE_DEMANDS_ATTENTION", + "_NET_WM_STATE_ABOVE", + "_NET_WM_WINDOW_TYPE", + "_NET_WM_WINDOW_TYPE_NORMAL", + "_NET_WM_WINDOW_TYPE_DIALOG", + "_NET_WM_WINDOW_TYPE_UTILITY", + "_NET_WM_WINDOW_TYPE_TOOLBAR", + "_NET_WM_WINDOW_TYPE_SPLASH", + "_NET_WM_WINDOW_TYPE_MENU", + "_NET_WM_WINDOW_TYPE_DOCK", + "_NET_ACTIVE_WINDOW", + "_NET_CLOSE_WINDOW", + "_NET_CLIENT_LIST", + "_NET_CLIENT_LIST_STACKING", + "_NET_FRAME_EXTENTS", + "_NET_WM_PID", + }; + var atoms: [names.len]c.Atom = undefined; + _ = c.XInternAtoms(display, @ptrCast(@constCast(&names)), @intCast(names.len), 0, &atoms); + return .{ + .wm_protocols = atoms[0], + .wm_delete_window = atoms[1], + .wm_take_focus = atoms[2], + .wm_state = atoms[3], + .wm_change_state = atoms[4], + .net_supported = atoms[5], + .net_supporting_wm_check = atoms[6], + .net_wm_name = atoms[7], + .net_wm_state = atoms[8], + .net_wm_state_fullscreen = atoms[9], + .net_wm_state_hidden = atoms[10], + .net_wm_state_demands_attention = atoms[11], + .net_wm_state_above = atoms[12], + .net_wm_window_type = atoms[13], + .net_wm_window_type_normal = atoms[14], + .net_wm_window_type_dialog = atoms[15], + .net_wm_window_type_utility = atoms[16], + .net_wm_window_type_toolbar = atoms[17], + .net_wm_window_type_splash = atoms[18], + .net_wm_window_type_menu = atoms[19], + .net_wm_window_type_dock = atoms[20], + .net_active_window = atoms[21], + .net_close_window = atoms[22], + .net_client_list = atoms[23], + .net_client_list_stacking = atoms[24], + .net_frame_extents = atoms[25], + .net_wm_pid = atoms[26], + }; + } + + pub fn supportedList(self: *const Atoms) []const c.Atom { + return &.{ + self.net_supported, + self.net_supporting_wm_check, + self.net_wm_name, + self.net_wm_state, + self.net_wm_state_fullscreen, + self.net_wm_state_hidden, + self.net_wm_state_demands_attention, + self.net_wm_state_above, + self.net_wm_window_type, + self.net_wm_window_type_normal, + self.net_wm_window_type_dialog, + self.net_wm_window_type_utility, + self.net_wm_window_type_toolbar, + self.net_wm_window_type_splash, + self.net_wm_window_type_menu, + self.net_wm_window_type_dock, + self.net_active_window, + self.net_close_window, + self.net_client_list, + self.net_client_list_stacking, + self.net_frame_extents, + self.net_wm_pid, + }; + } +}; diff --git a/src/bar.zig b/src/bar.zig new file mode 100644 index 0000000..d563ef4 --- /dev/null +++ b/src/bar.zig @@ -0,0 +1,214 @@ +const std = @import("std"); +const c = @import("c.zig").c; +const config = @import("config.zig"); +const client_mod = @import("client.zig"); +const WM = client_mod.WM; +const Client = client_mod.Client; +const util = @import("util.zig"); + +fn allocColor(display: ?*c.Display, visual: ?*c.Visual, cmap: c.Colormap, name: [:0]const u8) !c.XftColor { + var color: c.XftColor = undefined; + if (c.XftColorAllocName(display, visual, cmap, name, &color) == 0) + return error.ColorAllocFailed; + return color; +} + +fn fontHeight(fonts: []?*c.XftFont) i32 { + for (fonts) |f| { + if (f) |font| return font.ascent + font.descent; + } + return config.bar_height; +} + +fn fontAscent(fonts: []?*c.XftFont) i32 { + for (fonts) |f| { + if (f) |font| return font.ascent; + } + return 0; +} + +fn textWidth(dpy: ?*c.Display, fonts: []?*c.XftFont, text: []const u8) i32 { + for (fonts) |f| { + if (f == null) continue; + var ext: c.XGlyphInfo = undefined; + c.XftTextExtents8(dpy, f.?, text.ptr, @intCast(text.len), &ext); + return @intCast(ext.xOff); + } + return @intCast(text.len * 8); +} + +fn drawChar(dpy: ?*c.Display, xft_draw: ?*c.XftDraw, fonts: []?*c.XftFont, color: *c.XftColor, x: i32, y: i32, ch: u8) i32 { + const rune: c.FcChar32 = ch; + for (fonts) |f| { + if (f == null) continue; + const glyph = c.XftCharIndex(dpy, f.?, rune); + if (glyph == 0) continue; + var ext: c.XGlyphInfo = undefined; + c.XftGlyphExtents(dpy, f.?, &glyph, 1, &ext); + c.XftDrawGlyphs(xft_draw, color, f.?, x, y, &glyph, 1); + return @intCast(ext.xOff); + } + return 8; +} + +fn drawText(dpy: ?*c.Display, xft_draw: ?*c.XftDraw, fonts: []?*c.XftFont, color: *c.XftColor, x: i32, y: i32, text: []const u8) i32 { + var cx = x; + for (text) |ch| { + cx += drawChar(dpy, xft_draw, fonts, color, cx, y, ch); + } + return cx; +} + +pub fn create(wm: *WM) !void { + const d = wm.display; + const s = wm.screen; + const root = wm.root; + const visual = wm.visual; + const cmap = wm.colormap; + + for (&config.font_names, 0..) |name, i| { + wm.bar.fonts[i] = c.XftFontOpenName(d, s, name); + if (wm.bar.fonts[i] == null) + util.die("cannot open font: {s}\n", .{name}); + } + wm.bar.colors.bg = allocColor(d, visual, cmap, config.col.bar_bg) catch + util.die("cannot allocate color: {s}\n", .{config.col.bar_bg}); + wm.bar.colors.fg = allocColor(d, visual, cmap, config.col.bar_fg) catch + util.die("cannot allocate color: {s}\n", .{config.col.bar_fg}); + wm.bar.colors.sel_bg = allocColor(d, visual, cmap, config.col.bar_sel_bg) catch + util.die("cannot allocate color: {s}\n", .{config.col.bar_sel_bg}); + wm.bar.colors.min_fg = allocColor(d, visual, cmap, config.col.bar_min_fg) catch + util.die("cannot allocate color: {s}\n", .{config.col.bar_min_fg}); + wm.bar.colors.urg_fg = allocColor(d, visual, cmap, config.col.bar_urg_fg) catch + util.die("cannot allocate color: {s}\n", .{config.col.bar_urg_fg}); + wm.bar.colors.border_focused = allocColor(d, visual, cmap, config.col.border_focused) catch + util.die("cannot allocate color: {s}\n", .{config.col.border_focused}); + wm.bar.colors.border_unfocused = allocColor(d, visual, cmap, config.col.border_unfocused) catch + util.die("cannot allocate color: {s}\n", .{config.col.border_unfocused}); + + var wa = std.mem.zeroes(c.XSetWindowAttributes); + wa.override_redirect = 1; + wa.background_pixmap = c.ParentRelative; + wa.event_mask = c.ExposureMask | c.ButtonPressMask; + + wm.bar.window = c.XCreateWindow(d, root, 0, 0, wm.sw, config.bar_height, 0, + c.CopyFromParent, c.InputOutput, visual, + c.CWOverrideRedirect | c.CWBackPixmap | c.CWEventMask, &wa); + + _ = c.XChangeProperty(d, wm.bar.window, wm.atoms.net_wm_window_type, + c.XA_ATOM, 32, c.PropModeReplace, + @ptrCast(@constCast(&wm.atoms.net_wm_window_type_dock)), 1); + _ = c.XMapWindow(d, wm.bar.window); + + wm.bar.pixmap = c.XCreatePixmap(d, wm.bar.window, wm.sw, config.bar_height, + @intCast(c.DefaultDepth(d, s))); + wm.bar.draw = c.XftDrawCreate(d, wm.bar.pixmap, visual, cmap); + wm.bar.button_count = 0; +} + +pub fn draw(wm: *WM) void { + const bar = &wm.bar; + const d = wm.display; + const fonts = &bar.fonts; + const bh: i32 = @intCast(config.bar_height); + const asc = fontAscent(fonts) + @divTrunc(bh - fontHeight(fonts), 2); + + _ = c.XftDrawRect(bar.draw, &bar.colors.bg, 0, 0, @intCast(wm.sw), bh); + + var x: i32 = 0; + var btn_idx: usize = 0; + var it = wm.clients; + while (it) |cl| : (it = cl.next) { + if (btn_idx >= bar.buttons.len) break; + + const title = if (cl.title.len > 0) cl.title else "???"; + var tw = textWidth(d, fonts, title) + 12; + if (x + tw > @as(i32, @intCast(wm.sw))) tw = @as(i32, @intCast(wm.sw)) - x; + + if (wm.focused == cl) { + _ = c.XftDrawRect(bar.draw, &bar.colors.sel_bg, x, 0, @intCast(tw), @intCast(bh)); + } + + const fg_color: *c.XftColor = if (cl.flags.minimized) + &bar.colors.min_fg + else if (cl.flags.urgent) + &bar.colors.urg_fg + else + &bar.colors.fg; + + const draw_w = @max(tw - 12, 0); + if (draw_w > 0) { + var end: usize = 0; + while (end < title.len) : (end += 1) { + if (textWidth(d, fonts, title[0 .. end + 1]) > draw_w) break; + } + if (end > 0) + _ = drawText(d, bar.draw, fonts, fg_color, x + 6, asc, title[0..end]); + } + + bar.buttons[btn_idx] = .{ .x = x, .w = @intCast(tw), .win = cl.window }; + btn_idx += 1; + x += tw; + } + bar.button_count = btn_idx; + + var status_buf: [256]u8 = undefined; + const status = readStatus(wm, &status_buf); + if (status.len > 0) { + const sw = textWidth(d, fonts, status); + const sx = @as(i32, @intCast(wm.sw)) - sw - 6; + if (sx > x) { + _ = drawText(d, bar.draw, fonts, &bar.colors.fg, sx, asc, status); + } + } + + _ = c.XCopyArea(d, bar.pixmap, bar.window, + c.DefaultGC(d, wm.screen), 0, 0, @intCast(wm.sw), bh, 0, 0); + _ = c.XRaiseWindow(d, wm.bar.window); +} + +fn readPropText(wm: *WM, prop_atom: c.Atom, buf: *[256]u8) []u8 { + var actual_type: c.Atom = 0; + var actual_format: c_int = 0; + var nitems: c_ulong = 0; + var bytes_after: c_ulong = 0; + var prop: [*c]u8 = null; + if (c.XGetWindowProperty(wm.display, wm.root, prop_atom, + 0, 1024, 0, c.AnyPropertyType, &actual_type, &actual_format, + &nitems, &bytes_after, &prop) == c.Success and prop != null) + { + defer _ = c.XFree(prop); + if (actual_format != 8 or nitems == 0) return &.{}; + const len = @min(nitems, buf.len - 1); + @memcpy(buf[0..len], prop[0..len]); + buf[len] = 0; + return buf[0..len]; + } + return &.{}; +} + +fn readStatus(wm: *WM, buf: *[256]u8) []u8 { + const utf8 = readPropText(wm, wm.atoms.net_wm_name, buf); + if (utf8.len > 0) return utf8; + return readPropText(wm, c.XA_WM_NAME, buf); +} + +pub fn destroy(wm: *WM) void { + if (wm.bar.draw) |d| c.XftDrawDestroy(d); + if (wm.bar.pixmap != 0) { + _ = c.XFreePixmap(wm.display, wm.bar.pixmap); + } + if (wm.bar.window != 0) { + _ = c.XDestroyWindow(wm.display, wm.bar.window); + } + for (&wm.bar.fonts) |f| { + if (f) |font| c.XftFontClose(wm.display, font); + } + c.XftColorFree(wm.display, wm.visual, wm.colormap, &wm.bar.colors.bg); + c.XftColorFree(wm.display, wm.visual, wm.colormap, &wm.bar.colors.fg); + c.XftColorFree(wm.display, wm.visual, wm.colormap, &wm.bar.colors.sel_bg); + c.XftColorFree(wm.display, wm.visual, wm.colormap, &wm.bar.colors.min_fg); + c.XftColorFree(wm.display, wm.visual, wm.colormap, &wm.bar.colors.urg_fg); + c.XftColorFree(wm.display, wm.visual, wm.colormap, &wm.bar.colors.border_focused); + c.XftColorFree(wm.display, wm.visual, wm.colormap, &wm.bar.colors.border_unfocused); +} diff --git a/src/c.zig b/src/c.zig new file mode 100644 index 0000000..18e2e8c --- /dev/null +++ b/src/c.zig @@ -0,0 +1,8 @@ +pub const c = @cImport({ + @cDefine("XK_MISCELLANY", {}); + @cInclude("X11/Xlib.h"); + @cInclude("X11/Xutil.h"); + @cInclude("X11/Xatom.h"); + @cInclude("X11/Xft/Xft.h"); + @cInclude("X11/XKBlib.h"); +}); diff --git a/src/client.zig b/src/client.zig new file mode 100644 index 0000000..9f6aec9 --- /dev/null +++ b/src/client.zig @@ -0,0 +1,636 @@ +const std = @import("std"); +const c = @import("c.zig").c; +const config = @import("config.zig"); +const atoms_mod = @import("atoms.zig"); +const Atoms = atoms_mod.Atoms; + +pub const Client = struct { + window: c.Window, + x: i32 = 0, + y: i32 = 0, + w: u32 = 0, + h: u32 = 0, + bw: u32 = config.border_width, + saved: struct { + x: i32 = 0, + y: i32 = 0, + w: u32 = 0, + h: u32 = 0, + } = .{}, + min_w: i32 = 0, + min_h: i32 = 0, + max_w: i32 = 0, + max_h: i32 = 0, + base_w: i32 = 0, + base_h: i32 = 0, + inc_w: i32 = 0, + inc_h: i32 = 0, + min_aspect: f64 = 0, + max_aspect: f64 = 0, + has_aspect: bool = false, + flags: packed struct { + floating: bool = false, + fullscreen: bool = false, + minimized: bool = false, + urgent: bool = false, + never_focus: bool = false, + fixed_size: bool = false, + } = .{}, + title: []u8 = &.{}, + class: []u8 = &.{}, + prev: ?*Client = null, + next: ?*Client = null, + unmap_count: u32 = 0, + supports_delete: bool = false, + supports_take_focus: bool = false, + input_hint: bool = true, + transient_for: c.Window = 0, +}; + +pub const GrabState = struct { + client: *Client, + orig_x: i32, + orig_y: i32, + orig_w: u32, + orig_h: u32, + start_x: i32, + start_y: i32, + resizing: bool, +}; + +pub const ButtonEntry = struct { + x: i32, + w: u32, + win: c.Window, +}; + +pub const Bar = struct { + window: c.Window = 0, + pixmap: c.Pixmap = 0, + draw: ?*c.XftDraw = null, + fonts: [config.font_names.len]?*c.XftFont, + colors: struct { + bg: c.XftColor, + fg: c.XftColor, + sel_bg: c.XftColor, + min_fg: c.XftColor, + urg_fg: c.XftColor, + border_focused: c.XftColor, + border_unfocused: c.XftColor, + }, + buttons: [128]ButtonEntry, + button_count: usize = 0, +}; + +pub const WM = struct { + display: ?*c.Display, + screen: c_int, + root: c.Window, + sw: u32, + sh: u32, + atoms: Atoms, + bar: Bar, + clients: ?*Client, + focused: ?*Client, + running: bool, + allocator: std.mem.Allocator, + support_win: c.Window, + grab: ?GrabState, + visual: ?*c.Visual, + colormap: c.Colormap, +}; + +pub fn listAppend(head: *?*Client, cl: *Client) void { + cl.next = null; + cl.prev = null; + if (head.*) |h| { + var it: *Client = h; + while (it.next) |next| it = next; + it.next = cl; + cl.prev = it; + } else { + head.* = cl; + } +} + +pub fn listRemove(head: *?*Client, cl: *Client) void { + if (cl.prev) |p| p.next = cl.next else head.* = cl.next; + if (cl.next) |n| n.prev = cl.prev; + cl.prev = null; + cl.next = null; +} + +pub fn listFind(head: ?*Client, win: c.Window) ?*Client { + var it = head; + while (it) |cl| : (it = cl.next) { + if (cl.window == win) return cl; + } + return null; +} + +pub fn listCount(head: ?*Client) usize { + var n: usize = 0; + var it = head; + while (it) |cl| : (it = cl.next) n += 1; + return n; +} + +pub fn listLast(head: ?*Client) ?*Client { + var it = head orelse return null; + while (it.next) |next| it = next; + return it; +} + +fn setWmState(wm: *WM, cl: *Client, state: i32) void { + const data = [2]c_long{ @intCast(state), 0 }; + _ = c.XChangeProperty(wm.display, cl.window, wm.atoms.wm_state, + wm.atoms.wm_state, 32, c.PropModeReplace, @ptrCast(&data), 2); +} + +fn sendProtocol(wm: *WM, cl: *Client, protocol: c.Atom) void { + var ev: c.XEvent = undefined; + @memset(@as([*]u8, @ptrCast(&ev))[0..@sizeOf(c.XEvent)], 0); + ev.type = c.ClientMessage; + ev.xclient.window = cl.window; + ev.xclient.message_type = wm.atoms.wm_protocols; + ev.xclient.format = 32; + ev.xclient.data.l[0] = @intCast(protocol); + ev.xclient.data.l[1] = @intCast(c.CurrentTime); + _ = c.XSendEvent(wm.display, cl.window, 0, c.NoEventMask, &ev); +} + +fn updateFocusPolicy(cl: *Client) void { + cl.flags.never_focus = !cl.input_hint and !cl.supports_take_focus; +} + +pub fn closeClient(wm: *WM, cl: *Client) void { + if (cl.supports_delete) { + sendProtocol(wm, cl, wm.atoms.wm_delete_window); + } else { + _ = c.XKillClient(wm.display, cl.window); + } +} + +pub fn readTitle(wm: *WM, cl: *Client) void { + if (cl.title.len > 0) { + wm.allocator.free(cl.title); + cl.title = &.{}; + } + var text: c.XTextProperty = undefined; + if (c.XGetTextProperty(wm.display, cl.window, &text, wm.atoms.net_wm_name) != 0) { + if (text.value != null) { + defer _ = c.XFree(text.value); + if (text.nitems > 0) { + var len: usize = @intCast(text.nitems); + if (text.value[len - 1] == 0) len -= 1; + cl.title = wm.allocator.dupe(u8, text.value[0..len]) catch &.{}; + return; + } + } + } + var name: [*c]u8 = null; + if (c.XFetchName(wm.display, cl.window, &name) != 0) { + if (name != null) { + cl.title = wm.allocator.dupe(u8, std.mem.sliceTo(name.?, 0)) catch &.{}; + _ = c.XFree(name); + } + } +} + +pub fn readClass(wm: *WM, cl: *Client) void { + if (cl.class.len > 0) { + wm.allocator.free(cl.class); + cl.class = &.{}; + } + var hint: c.XClassHint = .{ .res_name = null, .res_class = null }; + if (c.XGetClassHint(wm.display, cl.window, &hint) != 0) { + if (hint.res_class != null) { + cl.class = wm.allocator.dupe(u8, std.mem.sliceTo(hint.res_class.?, 0)) catch &.{}; + _ = c.XFree(hint.res_class); + } + if (hint.res_name != null) _ = c.XFree(hint.res_name); + } +} + +fn readProtocols(wm: *WM, cl: *Client) void { + var protos: [*c]c.Atom = null; + var n: c_int = 0; + if (c.XGetWMProtocols(wm.display, cl.window, &protos, &n) != 0) { + var i: c_int = 0; + while (i < n) : (i += 1) { + if (protos[@intCast(i)] == wm.atoms.wm_delete_window) + cl.supports_delete = true; + if (protos[@intCast(i)] == wm.atoms.wm_take_focus) + cl.supports_take_focus = true; + } + _ = c.XFree(protos); + } + updateFocusPolicy(cl); +} + +fn readHints(wm: *WM, cl: *Client) void { + var hints: c.XSizeHints = undefined; + var supplied: c_long = 0; + if (c.XGetWMNormalHints(wm.display, cl.window, &hints, &supplied) != 0) { + if (hints.flags & c.PMinSize != 0) { + cl.min_w = hints.min_width; + cl.min_h = hints.min_height; + } + if (hints.flags & c.PMaxSize != 0) { + cl.max_w = hints.max_width; + cl.max_h = hints.max_height; + } + if (hints.flags & c.PBaseSize != 0) { + cl.base_w = hints.base_width; + cl.base_h = hints.base_height; + } + if (hints.flags & c.PResizeInc != 0) { + cl.inc_w = hints.width_inc; + cl.inc_h = hints.height_inc; + } + if (hints.flags & c.PAspect != 0) { + if (hints.min_aspect.y > 0 and hints.max_aspect.y > 0) { + cl.min_aspect = @as(f64, @floatFromInt(hints.min_aspect.x)) / + @as(f64, @floatFromInt(hints.min_aspect.y)); + cl.max_aspect = @as(f64, @floatFromInt(hints.max_aspect.x)) / + @as(f64, @floatFromInt(hints.max_aspect.y)); + cl.has_aspect = true; + } + } + if (hints.flags & (c.PMinSize | c.PBaseSize) != 0 and + hints.flags & c.PMaxSize != 0 and + hints.min_width == hints.max_width and + hints.min_height == hints.max_height) + { + cl.flags.fixed_size = true; + cl.flags.floating = true; + } + } + + const wm_hints: [*c]c.XWMHints = c.XGetWMHints(wm.display, cl.window); + if (wm_hints != null) { + if (wm_hints.*.flags & c.InputHint != 0) + cl.input_hint = wm_hints.*.input != 0; + if (wm_hints.*.flags & c.XUrgencyHint != 0) + cl.flags.urgent = true; + _ = c.XFree(wm_hints); + } + updateFocusPolicy(cl); + + var trans: c.Window = 0; + _ = c.XGetTransientForHint(wm.display, cl.window, &trans); + if (trans != 0) { + cl.transient_for = trans; + cl.flags.floating = true; + } +} + +fn checkWindowType(wm: *WM, cl: *Client) void { + var actual_type: c.Atom = 0; + var actual_format: c_int = 0; + var nitems: c_ulong = 0; + var bytes_after: c_ulong = 0; + var prop: [*c]u8 = null; + if (c.XGetWindowProperty(wm.display, cl.window, wm.atoms.net_wm_window_type, + 0, 1024, 0, c.XA_ATOM, &actual_type, &actual_format, + &nitems, &bytes_after, &prop) == c.Success and prop != null) + { + const atoms: [*]c.Atom = @ptrCast(@alignCast(prop)); + var i: usize = 0; + while (i < nitems) : (i += 1) { + const a = atoms[i]; + if (a == wm.atoms.net_wm_window_type_dialog or + a == wm.atoms.net_wm_window_type_utility or + a == wm.atoms.net_wm_window_type_toolbar or + a == wm.atoms.net_wm_window_type_splash or + a == wm.atoms.net_wm_window_type_menu or + a == wm.atoms.net_wm_window_type_dock) + { + cl.flags.floating = true; + } + } + _ = c.XFree(prop); + } +} + +fn applyRules(cl: *Client) void { + for (&config.rules) |rule| { + if (rule.class) |rc| { + if (cl.class.len > 0 and std.mem.indexOf(u8, cl.class, rc) != null) { + cl.flags.floating = rule.floating; + } + } + } +} + +pub fn applySizeHints(cl: *Client, w: i32, h: i32) struct { w: i32, h: i32 } { + var nw = w; + var nh = h; + if (cl.inc_w > 0 and cl.base_w >= 0) + nw = cl.base_w + cl.inc_w * @as(i32, @intFromFloat(@round(@as(f64, @floatFromInt(nw - cl.base_w)) / @as(f64, @floatFromInt(cl.inc_w))))); + if (cl.inc_h > 0 and cl.base_h >= 0) + nh = cl.base_h + cl.inc_h * @as(i32, @intFromFloat(@round(@as(f64, @floatFromInt(nh - cl.base_h)) / @as(f64, @floatFromInt(cl.inc_h))))); + if (cl.min_w > 0 and nw < cl.min_w) nw = cl.min_w; + if (cl.min_h > 0 and nh < cl.min_h) nh = cl.min_h; + if (cl.max_w > 0 and nw > cl.max_w) nw = cl.max_w; + if (cl.max_h > 0 and nh > cl.max_h) nh = cl.max_h; + if (cl.has_aspect) { + const aspect = @as(f64, @floatFromInt(nw)) / @as(f64, @floatFromInt(@max(nh, 1))); + if (aspect < cl.min_aspect) + nw = @intFromFloat(@as(f64, @floatFromInt(nh)) * cl.min_aspect); + if (aspect > cl.max_aspect) + nh = @intFromFloat(@as(f64, @floatFromInt(nw)) / cl.max_aspect); + } + if (nw < 1) nw = 1; + if (nh < 1) nh = 1; + return .{ .w = nw, .h = nh }; +} + +pub fn configure(wm: *WM, cl: *Client) void { + var ce: c.XConfigureEvent = undefined; + ce.type = c.ConfigureNotify; + ce.display = wm.display; + ce.event = cl.window; + ce.window = cl.window; + ce.x = cl.x; + ce.y = cl.y; + ce.width = @intCast(cl.w); + ce.height = @intCast(cl.h); + ce.border_width = @intCast(cl.bw); + ce.above = 0; + ce.override_redirect = 0; + _ = c.XSendEvent(wm.display, cl.window, 0, c.StructureNotifyMask, @ptrCast(&ce)); +} + +pub fn resize(wm: *WM, cl: *Client, x: i32, y: i32, w: u32, h: u32) void { + const s = applySizeHints(cl, @intCast(w), @intCast(h)); + const sw: u32 = @intCast(@max(s.w, 1)); + const sh: u32 = @intCast(@max(s.h, 1)); + cl.x = x; + cl.y = y; + cl.w = sw; + cl.h = sh; + _ = c.XMoveResizeWindow(wm.display, cl.window, x, y, sw, sh); + configure(wm, cl); + updateFrameExtents(wm, cl); +} + +pub fn setFullscreen(wm: *WM, cl: *Client, fullscreen: bool) void { + const was_fs = cl.flags.fullscreen; + cl.flags.fullscreen = fullscreen; + + if (fullscreen and !was_fs) { + cl.saved = .{ .x = cl.x, .y = cl.y, .w = cl.w, .h = cl.h }; + cl.bw = 0; + _ = c.XSetWindowBorderWidth(wm.display, cl.window, 0); + resize(wm, cl, 0, @intCast(config.bar_height), wm.sw, wm.sh - config.bar_height); + _ = c.XRaiseWindow(wm.display, cl.window); + setNetState(wm, cl); + } else if (!fullscreen and was_fs) { + cl.bw = config.border_width; + _ = c.XSetWindowBorderWidth(wm.display, cl.window, @intCast(config.border_width)); + resize(wm, cl, cl.saved.x, cl.saved.y, cl.saved.w, cl.saved.h); + setNetState(wm, cl); + } +} + +pub fn setMinimized(wm: *WM, cl: *Client, minimized: bool) void { + const was_min = cl.flags.minimized; + cl.flags.minimized = minimized; + if (minimized and !was_min) { + cl.unmap_count += 1; + _ = c.XUnmapWindow(wm.display, cl.window); + setWmState(wm, cl, 3); + setNetState(wm, cl); + } else if (!minimized and was_min) { + _ = c.XMapWindow(wm.display, cl.window); + setWmState(wm, cl, 1); + setNetState(wm, cl); + } +} + +pub fn setNetState(wm: *WM, cl: *Client) void { + var atoms: [4]c.Atom = undefined; + var n: usize = 0; + if (cl.flags.fullscreen) { + atoms[n] = wm.atoms.net_wm_state_fullscreen; + n += 1; + } + if (cl.flags.minimized) { + atoms[n] = wm.atoms.net_wm_state_hidden; + n += 1; + } + if (cl.flags.urgent) { + atoms[n] = wm.atoms.net_wm_state_demands_attention; + n += 1; + } + if (n > 0) { + _ = c.XChangeProperty(wm.display, cl.window, wm.atoms.net_wm_state, + c.XA_ATOM, 32, c.PropModeReplace, @ptrCast(&atoms), @intCast(n)); + } else { + _ = c.XDeleteProperty(wm.display, cl.window, wm.atoms.net_wm_state); + } +} + +pub fn grabClientButtons(wm: *WM, win: c.Window, focused: bool) void { + _ = c.XUngrabButton(wm.display, c.AnyButton, c.AnyModifier, win); + + _ = c.XGrabButton(wm.display, c.Button1, c.Mod1Mask, win, 0, + c.ButtonPressMask | c.ButtonReleaseMask | c.PointerMotionMask, + c.GrabModeAsync, c.GrabModeAsync, 0, 0); + _ = c.XGrabButton(wm.display, c.Button3, c.Mod1Mask, win, 0, + c.ButtonPressMask | c.ButtonReleaseMask | c.PointerMotionMask, + c.GrabModeAsync, c.GrabModeAsync, 0, 0); + _ = c.XGrabButton(wm.display, c.Button1, c.Mod1Mask | c.LockMask, win, 0, + c.ButtonPressMask | c.ButtonReleaseMask | c.PointerMotionMask, + c.GrabModeAsync, c.GrabModeAsync, 0, 0); + _ = c.XGrabButton(wm.display, c.Button3, c.Mod1Mask | c.LockMask, win, 0, + c.ButtonPressMask | c.ButtonReleaseMask | c.PointerMotionMask, + c.GrabModeAsync, c.GrabModeAsync, 0, 0); + + if (!focused) { + _ = c.XGrabButton(wm.display, c.AnyButton, c.AnyModifier, win, 0, + c.ButtonPressMask, c.GrabModeSync, c.GrabModeSync, 0, 0); + } +} + +pub fn focus(wm: *WM, cl: ?*Client) void { + if (wm.focused) |prev| { + setBorder(wm, prev, false); + grabClientButtons(wm, prev.window, false); + } + wm.focused = cl; + if (cl) |client| { + setBorder(wm, client, true); + grabClientButtons(wm, client.window, true); + _ = c.XRaiseWindow(wm.display, client.window); + if (!client.flags.never_focus) { + if (client.input_hint) { + _ = c.XSetInputFocus(wm.display, client.window, c.RevertToPointerRoot, c.CurrentTime); + } + if (client.supports_take_focus) { + sendProtocol(wm, client, wm.atoms.wm_take_focus); + } + } + _ = c.XChangeProperty(wm.display, wm.root, wm.atoms.net_active_window, + c.XA_WINDOW, 32, c.PropModeReplace, @ptrCast(&client.window), 1); + } else { + _ = c.XSetInputFocus(wm.display, wm.root, c.RevertToPointerRoot, c.CurrentTime); + _ = c.XDeleteProperty(wm.display, wm.root, wm.atoms.net_active_window); + } +} + +fn setBorder(wm: *WM, cl: *Client, focused: bool) void { + const color = if (focused) + wm.bar.colors.border_focused.pixel + else + wm.bar.colors.border_unfocused.pixel; + _ = c.XSetWindowBorder(wm.display, cl.window, color); +} + +pub fn focusNext(wm: *WM) void { + const start = wm.focused orelse wm.clients orelse return; + var it = start; + while (true) { + it = it.next orelse wm.clients orelse return; + if (!it.flags.minimized) { + focus(wm, it); + _ = c.XRaiseWindow(wm.display, it.window); + return; + } + if (it == start) break; + } +} + +pub fn focusVisible(wm: *WM) void { + var it = wm.clients; + while (it) |cl| : (it = cl.next) { + if (!cl.flags.minimized) { + focus(wm, cl); + return; + } + } + focus(wm, null); +} + +pub fn raise(wm: *WM, cl: *Client) void { + _ = c.XRaiseWindow(wm.display, cl.window); + listRemove(&wm.clients, cl); + listAppend(&wm.clients, cl); + updateClientList(wm); +} + +pub fn updateClientList(wm: *WM) void { + const n = listCount(wm.clients); + if (n == 0) { + _ = c.XDeleteProperty(wm.display, wm.root, wm.atoms.net_client_list); + _ = c.XDeleteProperty(wm.display, wm.root, wm.atoms.net_client_list_stacking); + return; + } + var buf: [256]c.Window = undefined; + var i: usize = 0; + var it = wm.clients; + while (it) |cl| : (it = cl.next) { + if (i < buf.len) { + buf[i] = cl.window; + i += 1; + } + } + _ = c.XChangeProperty(wm.display, wm.root, wm.atoms.net_client_list, + c.XA_WINDOW, 32, c.PropModeReplace, @ptrCast(&buf), @intCast(i)); + + i = 0; + it = wm.clients; + while (it) |cl| : (it = cl.next) { + if (i < buf.len) { + buf[i] = cl.window; + i += 1; + } + } + _ = c.XChangeProperty(wm.display, wm.root, wm.atoms.net_client_list_stacking, + c.XA_WINDOW, 32, c.PropModeReplace, @ptrCast(&buf), @intCast(i)); +} + +pub fn updateFrameExtents(wm: *WM, cl: *Client) void { + const data = [4]c_long{ + @intCast(cl.bw), + @intCast(cl.bw), + @intCast(cl.bw), + @intCast(cl.bw), + }; + _ = c.XChangeProperty(wm.display, cl.window, wm.atoms.net_frame_extents, + c.XA_CARDINAL, 32, c.PropModeReplace, @ptrCast(&data), 4); +} + +pub fn manage(wm: *WM, window: c.Window) !void { + if (listFind(wm.clients, window) != null) return; + if (window == wm.bar.window) return; + + var wa: c.XWindowAttributes = undefined; + if (c.XGetWindowAttributes(wm.display, window, &wa) == 0) return; + if (wa.override_redirect != 0) return; + + const cl = try wm.allocator.create(Client); + errdefer wm.allocator.destroy(cl); + cl.* = .{ + .window = window, + .x = wa.x, + .y = wa.y, + .w = @intCast(wa.width), + .h = @intCast(wa.height), + .bw = config.border_width, + }; + + readHints(wm, cl); + readTitle(wm, cl); + readClass(wm, cl); + readProtocols(wm, cl); + checkWindowType(wm, cl); + applyRules(cl); + + if (!cl.flags.floating) { + cl.y = @max(cl.y, @as(i32, @intCast(config.bar_height))); + } + + listAppend(&wm.clients, cl); + setWmState(wm, cl, 1); + + _ = c.XSelectInput(wm.display, window, + c.StructureNotifyMask | c.PropertyChangeMask | + c.EnterWindowMask | c.FocusChangeMask); + + _ = c.XSetWindowBorderWidth(wm.display, window, @intCast(config.border_width)); + _ = c.XMoveResizeWindow(wm.display, window, cl.x, cl.y, cl.w, cl.h); + _ = c.XMapWindow(wm.display, window); + + grabClientButtons(wm, window, false); + updateFrameExtents(wm, cl); + focus(wm, cl); + updateClientList(wm); +} + +pub fn unmanage(wm: *WM, cl: *Client) void { + if (wm.focused == cl) { + const next = cl.next orelse cl.prev; + focus(wm, next); + } + listRemove(&wm.clients, cl); + setWmState(wm, cl, 0); + + if (cl.title.len > 0) wm.allocator.free(cl.title); + if (cl.class.len > 0) wm.allocator.free(cl.class); + wm.allocator.destroy(cl); + + updateClientList(wm); +} + +pub fn findClientByBarClick(wm: *WM, x: i32) ?*Client { + var i: usize = 0; + while (i < wm.bar.button_count) : (i += 1) { + const btn = wm.bar.buttons[i]; + if (x >= btn.x and x < btn.x + @as(i32, @intCast(btn.w))) { + return listFind(wm.clients, btn.win); + } + } + return null; +} diff --git a/src/config.zig b/src/config.zig new file mode 100644 index 0000000..2f46a37 --- /dev/null +++ b/src/config.zig @@ -0,0 +1,56 @@ +const c = @import("c.zig").c; + +pub const bar_height: u32 = 30; +pub const border_width: u32 = 2; +pub const snap_distance: i32 = 10; +pub const focus_follows_mouse: bool = false; + +pub const font_names = [_][:0]const u8{"monospace:size=14"}; + +pub const col = struct { + pub const border_focused = "#005f5f"; + pub const border_unfocused = "#444444"; + pub const bar_bg = "#222222"; + pub const bar_fg = "#eeeeee"; + pub const bar_sel_bg = "#555555"; + pub const bar_min_fg = "#888888"; + pub const bar_urg_fg = "#ff0000"; +}; + +pub const Action = union(enum) { + close_focused, + toggle_fullscreen, + focus_next, + minimize, + spawn: []const [:0]const u8, + quit, +}; + +pub const Keybind = struct { + mod: c_uint, + keysym: c.KeySym, + action: Action, +}; + +pub const keybinds = [_]Keybind{ + .{ .mod = c.Mod1Mask | c.ShiftMask, .keysym = c.XK_q, .action = .quit }, + .{ .mod = c.Mod1Mask | c.ShiftMask, .keysym = c.XK_c, .action = .close_focused }, + .{ .mod = c.Mod1Mask, .keysym = c.XK_f, .action = .toggle_fullscreen }, + .{ .mod = c.Mod1Mask, .keysym = c.XK_Tab, .action = .focus_next }, + .{ .mod = c.Mod1Mask, .keysym = c.XK_m, .action = .minimize }, + .{ .mod = c.Mod1Mask, .keysym = c.XK_Return, .action = .{ .spawn = &.{"st"} } }, + .{ .mod = c.Mod1Mask, .keysym = c.XK_p, .action = .{ .spawn = &.{"/home/smi/.scripts/run.sh"} } }, +}; + +pub const autostart = [_][]const [:0]const u8{ + &.{ "sh", "/home/smi/.scripts/2zw.sh" }, +}; + +pub const Rule = struct { + class: ?[:0]const u8, + floating: bool, +}; + +pub const rules = [_]Rule{ + .{ .class = "float", .floating = true }, +}; diff --git a/src/event.zig b/src/event.zig new file mode 100644 index 0000000..32dfd51 --- /dev/null +++ b/src/event.zig @@ -0,0 +1,350 @@ +const std = @import("std"); +const c = @import("c.zig").c; +const config = @import("config.zig"); +const client_mod = @import("client.zig"); +const WM = client_mod.WM; +const Client = client_mod.Client; +const bar_mod = @import("bar.zig"); +const util = @import("util.zig"); + +pub fn dispatch(wm: *WM, ev: *c.XEvent) void { + switch (ev.type) { + c.MapRequest => mapRequest(wm, &ev.xmaprequest), + c.UnmapNotify => unmapNotify(wm, &ev.xunmap), + c.DestroyNotify => destroyNotify(wm, &ev.xdestroywindow), + c.ConfigureRequest => configureRequest(wm, &ev.xconfigurerequest), + c.ClientMessage => clientMessage(wm, &ev.xclient), + c.PropertyNotify => propertyNotify(wm, &ev.xproperty), + c.ButtonPress => buttonPress(wm, &ev.xbutton), + c.MotionNotify => motionNotify(wm, &ev.xmotion), + c.ButtonRelease => buttonRelease(wm, &ev.xbutton), + c.KeyPress => keyPress(wm, &ev.xkey), + c.Expose => expose(wm, &ev.xexpose), + c.FocusIn => focusIn(wm, &ev.xfocus), + c.EnterNotify => enterNotify(wm, &ev.xcrossing), + c.MappingNotify => mappingNotify(wm, &ev.xmapping), + c.SelectionClear => selectionClear(wm), + else => {}, + } +} + +fn mapRequest(wm: *WM, ev: *c.XMapRequestEvent) void { + client_mod.manage(wm, ev.window) catch |err| { + util.log("manage: {}\n", .{err}); + }; + bar_mod.draw(wm); +} + +fn unmapNotify(wm: *WM, ev: *c.XUnmapEvent) void { + if (ev.event != wm.root) return; + const cl = client_mod.listFind(wm.clients, ev.window) orelse return; + if (cl.unmap_count > 0) { + cl.unmap_count -= 1; + return; + } + client_mod.unmanage(wm, cl); + bar_mod.draw(wm); +} + +fn destroyNotify(wm: *WM, ev: *c.XDestroyWindowEvent) void { + const cl = client_mod.listFind(wm.clients, ev.window) orelse return; + client_mod.unmanage(wm, cl); + bar_mod.draw(wm); +} + +fn configureRequest(wm: *WM, ev: *c.XConfigureRequestEvent) void { + const cl = client_mod.listFind(wm.clients, ev.window); + if (cl) |client| { + if (client.flags.fullscreen) { + client_mod.configure(wm, client); + return; + } + if (ev.value_mask & c.CWX != 0) client.x = ev.x; + if (ev.value_mask & c.CWY != 0) client.y = ev.y; + if (ev.value_mask & c.CWWidth != 0) client.w = @intCast(ev.width); + if (ev.value_mask & c.CWHeight != 0) client.h = @intCast(ev.height); + if (ev.value_mask & c.CWBorderWidth != 0) + client.bw = @intCast(ev.border_width); + _ = c.XMoveResizeWindow(wm.display, client.window, + client.x, client.y, client.w, client.h); + client_mod.configure(wm, client); + client_mod.updateFrameExtents(wm, client); + } else { + var changes: c.XWindowChanges = undefined; + changes.x = ev.x; + changes.y = ev.y; + changes.width = ev.width; + changes.height = ev.height; + changes.border_width = ev.border_width; + changes.sibling = ev.above; + changes.stack_mode = ev.detail; + _ = c.XConfigureWindow(wm.display, ev.window, + @intCast(ev.value_mask), &changes); + } +} + +fn clientMessage(wm: *WM, ev: *c.XClientMessageEvent) void { + const cl = client_mod.listFind(wm.clients, ev.window) orelse return; + const a = wm.atoms; + + if (ev.message_type == a.net_wm_state) { + const action = ev.data.l[0]; + const p1: c.Atom = @intCast(ev.data.l[1]); + const p2: c.Atom = @intCast(ev.data.l[2]); + const toggle = action == 2; + const add = action == 1; + const remove = action == 0; + + if (p1 == a.net_wm_state_fullscreen or p2 == a.net_wm_state_fullscreen) { + const want = if (toggle) !cl.flags.fullscreen else add; + client_mod.setFullscreen(wm, cl, want); + } + if (p1 == a.net_wm_state_hidden or p2 == a.net_wm_state_hidden) { + if (add or (toggle and !cl.flags.minimized)) { + client_mod.setMinimized(wm, cl, true); + if (wm.focused == cl) client_mod.focusVisible(wm); + } else if (remove or (toggle and cl.flags.minimized)) { + client_mod.setMinimized(wm, cl, false); + client_mod.focus(wm, cl); + } + } + if (p1 == a.net_wm_state_demands_attention or p2 == a.net_wm_state_demands_attention) { + if (remove or toggle) { + cl.flags.urgent = if (toggle) !cl.flags.urgent else false; + } else if (add) { + cl.flags.urgent = true; + } + client_mod.setNetState(wm, cl); + } + if (p1 == a.net_wm_state_above or p2 == a.net_wm_state_above) { + if (add) client_mod.raise(wm, cl); + } + bar_mod.draw(wm); + } else if (ev.message_type == a.net_active_window) { + if (cl.flags.minimized) + client_mod.setMinimized(wm, cl, false); + client_mod.focus(wm, cl); + client_mod.raise(wm, cl); + bar_mod.draw(wm); + } else if (ev.message_type == a.net_close_window) { + client_mod.closeClient(wm, cl); + } +} + +fn propertyNotify(wm: *WM, ev: *c.XPropertyEvent) void { + if (ev.window == wm.root) { + if (ev.atom == wm.atoms.net_wm_name or ev.atom == c.XA_WM_NAME) { + bar_mod.draw(wm); + } + return; + } + const cl = client_mod.listFind(wm.clients, ev.window) orelse return; + if (ev.state == c.PropertyDelete) return; + + if (ev.atom == c.XA_WM_NAME or ev.atom == wm.atoms.net_wm_name) { + client_mod.readTitle(wm, cl); + bar_mod.draw(wm); + } else if (ev.atom == c.XA_WM_CLASS) { + client_mod.readClass(wm, cl); + } else if (ev.atom == c.XA_WM_HINTS) { + const wm_hints: [*c]c.XWMHints = c.XGetWMHints(wm.display, cl.window); + if (wm_hints != null) { + const old_urgent = cl.flags.urgent; + cl.flags.urgent = (wm_hints.*.flags & c.XUrgencyHint) != 0; + if (wm_hints.*.flags & c.InputHint != 0) + cl.input_hint = wm_hints.*.input != 0; + cl.flags.never_focus = !cl.input_hint and !cl.supports_take_focus; + client_mod.setNetState(wm, cl); + if (old_urgent != cl.flags.urgent) + bar_mod.draw(wm); + _ = c.XFree(wm_hints); + } + } +} + +fn buttonPress(wm: *WM, ev: *c.XButtonEvent) void { + if (ev.window == wm.bar.window) { + barClick(wm, ev); + return; + } + + const cl = client_mod.listFind(wm.clients, ev.window) orelse return; + + if (ev.state & c.Mod1Mask != 0 and !cl.flags.fullscreen) { + if (wm.focused != cl) { + client_mod.focus(wm, cl); + client_mod.raise(wm, cl); + bar_mod.draw(wm); + } + const resizing = ev.button == 3; + wm.grab = .{ + .client = cl, + .orig_x = cl.x, + .orig_y = cl.y, + .orig_w = cl.w, + .orig_h = cl.h, + .start_x = ev.x_root, + .start_y = ev.y_root, + .resizing = resizing, + }; + _ = c.XGrabPointer(wm.display, wm.root, 0, + c.ButtonPressMask | c.ButtonReleaseMask | c.PointerMotionMask, + c.GrabModeAsync, c.GrabModeAsync, 0, 0, c.CurrentTime); + return; + } + + if (wm.focused != cl) { + client_mod.focus(wm, cl); + client_mod.raise(wm, cl); + bar_mod.draw(wm); + } + _ = c.XAllowEvents(wm.display, c.ReplayPointer, c.CurrentTime); +} + +fn barClick(wm: *WM, ev: *c.XButtonEvent) void { + const cl = client_mod.findClientByBarClick(wm, ev.x) orelse return; + switch (ev.button) { + c.Button1 => { + if (cl.flags.minimized) + client_mod.setMinimized(wm, cl, false); + client_mod.focus(wm, cl); + client_mod.raise(wm, cl); + bar_mod.draw(wm); + }, + c.Button2 => { + client_mod.closeClient(wm, cl); + }, + c.Button3 => { + client_mod.setMinimized(wm, cl, !cl.flags.minimized); + if (cl.flags.minimized and wm.focused == cl) { + client_mod.focusVisible(wm); + } + bar_mod.draw(wm); + }, + else => {}, + } +} + +fn motionNotify(wm: *WM, ev: *c.XMotionEvent) void { + const grab = wm.grab orelse return; + const cl = grab.client; + + var dummy: c.XEvent = undefined; + while (c.XCheckMaskEvent(wm.display, c.PointerMotionMask, &dummy) != 0) { + ev.x_root = dummy.xmotion.x_root; + ev.y_root = dummy.xmotion.y_root; + } + + const dx = ev.x_root - grab.start_x; + const dy = ev.y_root - grab.start_y; + + if (grab.resizing) { + var nw: i32 = @as(i32, @intCast(grab.orig_w)) + dx; + var nh: i32 = @as(i32, @intCast(grab.orig_h)) + dy; + if (nw < 1) nw = 1; + if (nh < 1) nh = 1; + const s = client_mod.applySizeHints(cl, nw, nh); + nw = @max(s.w, 1); + nh = @max(s.h, 1); + client_mod.resize(wm, cl, cl.x, cl.y, @intCast(nw), @intCast(nh)); + } else { + var nx = grab.orig_x + dx; + var ny = grab.orig_y + dy; + nx = applySnap(nx, @intCast(cl.w), @intCast(wm.sw)); + ny = applySnap(ny, @intCast(cl.h), @intCast(wm.sh)); + cl.x = nx; + cl.y = ny; + _ = c.XMoveWindow(wm.display, cl.window, nx, ny); + client_mod.configure(wm, cl); + } +} + +fn applySnap(pos: i32, size: i32, limit: i32) i32 { + if (@abs(pos) < config.snap_distance) return 0; + if (@abs(pos + size - limit) < config.snap_distance) return limit - size; + return pos; +} + +fn buttonRelease(wm: *WM, ev: *c.XButtonEvent) void { + _ = ev; + if (wm.grab != null) { + wm.grab = null; + _ = c.XUngrabPointer(wm.display, c.CurrentTime); + } +} + +fn keyPress(wm: *WM, ev: *c.XKeyEvent) void { + const keysym = c.XkbKeycodeToKeysym(wm.display, @intCast(ev.keycode), 0, 0); + for (&config.keybinds) |kb| { + if (kb.keysym != keysym) continue; + if (kb.mod != (ev.state & ~@as(c_uint, c.LockMask))) continue; + + switch (kb.action) { + .quit => wm.running = false, + .close_focused => { + if (wm.focused) |cl| client_mod.closeClient(wm, cl); + }, + .toggle_fullscreen => { + if (wm.focused) |cl| { + client_mod.setFullscreen(wm, cl, !cl.flags.fullscreen); + bar_mod.draw(wm); + } + }, + .focus_next => client_mod.focusNext(wm), + .minimize => { + if (wm.focused) |cl| { + client_mod.setMinimized(wm, cl, true); + client_mod.focusVisible(wm); + bar_mod.draw(wm); + } + }, + .spawn => |cmd| util.spawn(wm.allocator, cmd), + } + return; + } +} + +fn expose(wm: *WM, ev: *c.XExposeEvent) void { + if (ev.window == wm.bar.window and ev.count == 0) + bar_mod.draw(wm); +} + +fn focusIn(wm: *WM, ev: *c.XFocusChangeEvent) void { + if (ev.detail == c.NotifyInferior or ev.detail == c.NotifyPointer) return; + if (ev.window == wm.root) return; + const cl = client_mod.listFind(wm.clients, ev.window) orelse return; + if (wm.focused != cl) { + client_mod.focus(wm, cl); + bar_mod.draw(wm); + } +} + +fn enterNotify(wm: *WM, ev: *c.XCrossingEvent) void { + if (ev.detail == c.NotifyInferior or ev.mode == c.NotifyGrab or ev.mode == c.NotifyUngrab) return; + if (!config.focus_follows_mouse) return; + const cl = client_mod.listFind(wm.clients, ev.window) orelse return; + if (cl.flags.never_focus) return; + client_mod.focus(wm, cl); + bar_mod.draw(wm); +} + +fn mappingNotify(wm: *WM, ev: *c.XMappingEvent) void { + _ = c.XRefreshKeyboardMapping(ev); + if (ev.request == c.MappingKeyboard) + grabKeys(wm); +} + +fn selectionClear(wm: *WM) void { + wm.running = false; +} + +pub fn grabKeys(wm: *WM) void { + _ = c.XUngrabKey(wm.display, c.AnyKey, c.AnyModifier, wm.root); + for (&config.keybinds) |kb| { + const code = c.XKeysymToKeycode(wm.display, kb.keysym); + _ = c.XGrabKey(wm.display, code, kb.mod, wm.root, 1, c.GrabModeAsync, c.GrabModeAsync); + _ = c.XGrabKey(wm.display, code, kb.mod | c.LockMask, wm.root, 1, c.GrabModeAsync, c.GrabModeAsync); + _ = c.XGrabKey(wm.display, code, kb.mod | c.Mod2Mask, wm.root, 1, c.GrabModeAsync, c.GrabModeAsync); + _ = c.XGrabKey(wm.display, code, kb.mod | c.LockMask | c.Mod2Mask, wm.root, 1, c.GrabModeAsync, c.GrabModeAsync); + } +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..f2f22d1 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,205 @@ +const std = @import("std"); +const c = @import("c.zig").c; +const config = @import("config.zig"); +const util = @import("util.zig"); +const atoms_mod = @import("atoms.zig"); +const client_mod = @import("client.zig"); +const WM = client_mod.WM; +const bar_mod = @import("bar.zig"); +const event_mod = @import("event.zig"); + +var wm_global: ?*WM = null; + +fn xerrorStrict(dpy: ?*c.Display, ev: [*c]c.XErrorEvent) callconv(.c) c_int { + _ = dpy; + _ = ev; + util.die("wm: another window manager is already running\n", .{}); + return 0; +} + +fn xerrorNormal(dpy: ?*c.Display, ev: [*c]c.XErrorEvent) callconv(.c) c_int { + const ec = ev.*.error_code; + if (ec == c.BadWindow or ec == c.BadMatch or ec == c.BadDrawable) + return 0; + var buf: [128]u8 = undefined; + _ = c.XGetErrorText(dpy, ec, &buf, buf.len); + util.log("wm: xerror: {s} ({d})\n", .{ std.mem.sliceTo(&buf, 0), ec }); + return 0; +} + +fn sigTerm(sig: c_int) callconv(.c) void { + _ = sig; + if (wm_global) |wm| wm.running = false; +} + +fn sigChld(sig: c_int) callconv(.c) void { + _ = sig; + while (true) { + var status: c_int = 0; + const pid = std.c.waitpid(-1, &status, 1); + if (pid <= 0) break; + } +} + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const alloc = gpa.allocator(); + + var args = std.process.args(); + _ = args.next(); + while (args.next()) |arg| { + if (std.mem.eql(u8, arg, "-v")) { + util.log("wm 0.1.0\n", .{}); + std.process.exit(0); + } else { + util.die("usage: wm [-v]\n", .{}); + } + } + + if (c.XOpenDisplay(null)) |dpy| { + var wm = WM{ + .display = dpy, + .screen = c.DefaultScreen(dpy), + .root = c.RootWindow(dpy, c.DefaultScreen(dpy)), + .sw = @intCast(c.DisplayWidth(dpy, c.DefaultScreen(dpy))), + .sh = @intCast(c.DisplayHeight(dpy, c.DefaultScreen(dpy))), + .atoms = undefined, + .bar = undefined, + .clients = null, + .focused = null, + .running = true, + .allocator = alloc, + .support_win = 0, + .grab = null, + .visual = c.DefaultVisual(dpy, c.DefaultScreen(dpy)), + .colormap = c.DefaultColormap(dpy, c.DefaultScreen(dpy)), + }; + wm_global = &wm; + + _ = c.XSetErrorHandler(xerrorStrict); + _ = c.XSelectInput(dpy, wm.root, + c.SubstructureRedirectMask | c.SubstructureNotifyMask | + c.ButtonPressMask | c.StructureNotifyMask | + c.PropertyChangeMask | c.EnterWindowMask | + c.FocusChangeMask | c.KeymapStateMask); + _ = c.XSync(dpy, 0); + _ = c.XSetErrorHandler(xerrorNormal); + + const cursor = c.XCreateFontCursor(dpy, 68); + _ = c.XDefineCursor(dpy, wm.root, cursor); + + wm.atoms = atoms_mod.Atoms.init(dpy); + + var wm_sn_buf: [32]u8 = undefined; + const wm_sn_name = std.fmt.bufPrintZ(&wm_sn_buf, "WM_S{d}", .{wm.screen}) catch "WM_S0"; + const wm_sn = c.XInternAtom(dpy, wm_sn_name, 0); + + wm.support_win = c.XCreateSimpleWindow(dpy, wm.root, -1, -1, 1, 1, 0, 0, 0); + _ = c.XChangeProperty(dpy, wm.support_win, wm.atoms.net_supporting_wm_check, + c.XA_WINDOW, 32, c.PropModeReplace, + @ptrCast(&wm.support_win), 1); + _ = c.XChangeProperty(dpy, wm.root, wm.atoms.net_supporting_wm_check, + c.XA_WINDOW, 32, c.PropModeReplace, + @ptrCast(&wm.support_win), 1); + _ = c.XSetSelectionOwner(dpy, wm_sn, wm.support_win, c.CurrentTime); + + const pid_val: c_long = @intCast(std.os.linux.getpid()); + _ = c.XChangeProperty(dpy, wm.support_win, wm.atoms.net_wm_pid, + c.XA_CARDINAL, 32, c.PropModeReplace, + @ptrCast(@constCast(&pid_val)), 1); + + const supported = wm.atoms.supportedList(); + _ = c.XChangeProperty(dpy, wm.root, wm.atoms.net_supported, + c.XA_ATOM, 32, c.PropModeReplace, + @ptrCast(@constCast(supported.ptr)), @intCast(supported.len)); + + var btn_init: [128]client_mod.ButtonEntry = undefined; + for (&btn_init) |*b| b.* = .{ .x = 0, .w = 0, .win = 0 }; + wm.bar = .{ + .window = 0, + .pixmap = 0, + .draw = null, + .fonts = .{null} ** config.font_names.len, + .colors = undefined, + .buttons = btn_init, + .button_count = 0, + }; + bar_mod.create(&wm) catch |err| util.die("bar init: {}\n", .{err}); + + installSignals(); + + _ = c.XGrabServer(dpy); + scanWindows(&wm); + _ = c.XUngrabServer(dpy); + + event_mod.grabKeys(&wm); + bar_mod.draw(&wm); + + for (&config.autostart) |cmd| { + util.spawn(alloc, cmd); + } + + var ev: c.XEvent = undefined; + while (wm.running) { + _ = c.XNextEvent(dpy, &ev); + if (wm.running) + event_mod.dispatch(&wm, &ev); + } + + cleanup(&wm); + } else { + util.die("wm: cannot open display\n", .{}); + } +} + +fn installSignals() void { + const act = std.posix.Sigaction{ + .handler = .{ .handler = sigTerm }, + .mask = std.posix.empty_sigset, + .flags = 0, + }; + std.posix.sigaction(std.posix.SIG.TERM, &act, null); + std.posix.sigaction(std.posix.SIG.INT, &act, null); + + const chld_act = std.posix.Sigaction{ + .handler = .{ .handler = sigChld }, + .mask = std.posix.empty_sigset, + .flags = 0, + }; + std.posix.sigaction(std.posix.SIG.CHLD, &chld_act, null); +} + +fn scanWindows(wm: *WM) void { + var root_ret: c.Window = 0; + var parent_ret: c.Window = 0; + var children: [*c]c.Window = null; + var nchildren: c_uint = 0; + if (c.XQueryTree(wm.display, wm.root, &root_ret, &parent_ret, &children, &nchildren) != 0) { + if (children != null) { + var i: c_uint = 0; + while (i < nchildren) : (i += 1) { + var wa: c.XWindowAttributes = undefined; + if (c.XGetWindowAttributes(wm.display, children[@intCast(i)], &wa) != 0) { + if (wa.override_redirect == 0 and wa.map_state == c.IsViewable) { + client_mod.manage(wm, children[@intCast(i)]) catch {}; + } + } + } + _ = c.XFree(children); + } + } +} + +fn cleanup(wm: *WM) void { + var it = wm.clients; + while (it) |cl| { + const next = cl.next; + client_mod.unmanage(wm, cl); + it = next; + } + bar_mod.destroy(wm); + if (wm.support_win != 0) + _ = c.XDestroyWindow(wm.display, wm.support_win); + _ = c.XCloseDisplay(wm.display); +} diff --git a/src/util.zig b/src/util.zig new file mode 100644 index 0000000..25aafb2 --- /dev/null +++ b/src/util.zig @@ -0,0 +1,21 @@ +const std = @import("std"); + +pub fn die(comptime fmt: []const u8, args: anytype) noreturn { + const stderr = std.io.getStdErr().writer(); + stderr.print(fmt, args) catch {}; + std.process.exit(1); +} + +pub fn log(comptime fmt: []const u8, args: anytype) void { + const stderr = std.io.getStdErr().writer(); + stderr.print(fmt, args) catch {}; +} + +pub fn spawn(alloc: std.mem.Allocator, cmd: []const [:0]const u8) void { + if (cmd.len == 0) return; + var child = std.process.Child.init(cmd, alloc); + child.stdin_behavior = .Ignore; + child.stdout_behavior = .Ignore; + child.stderr_behavior = .Ignore; + _ = child.spawn() catch {}; +} diff --git a/zig-out/bin/wm b/zig-out/bin/wm new file mode 100755 index 0000000..334f96e Binary files /dev/null and b/zig-out/bin/wm differ