//! The vocabulary of things att_wm can be asked to do. //! //! This module deliberately depends on nothing but xkbcommon. Keeping it free //! of Wayland objects and window manager state is what lets `config.zig` import //! it to declare key bindings without creating an import cycle back into the //! window manager, and it is what lets key bindings and IPC commands share a //! single execution path: both become an `Action`, and `Wm.perform` is the only //! place that interprets one. const std = @import("std"); const mem = std.mem; pub const xkb = @import("xkbcommon"); /// Number of tags. Nine is dwm's default and what the example quickshell bar /// assumes; changing it here changes it everywhere. pub const tag_count = 9; pub const all_tags: u32 = (1 << tag_count) - 1; /// Which slot of an output's per-tag arrangement settings a view of `tags` uses. /// /// A view of exactly one tag gets that tag's own slot, numbered from 1. Viewing /// several at once has no single tag whose settings should win, so all such /// views share slot 0 — the compromise dwm's pertag patch makes. It leaves the /// individual tags' settings untouched, so they are still there on the way back. /// An empty mask is not reachable through `Output.setTags`, but shares slot 0 /// too rather than being a case callers have to think about. pub fn tagSlot(tags: u32) usize { const t = tags & all_tags; if (@popCount(t) != 1) return 0; return @ctz(t) + 1; } /// The tags belonging to output `index` of `count`, when the tag set is split /// across the displays. /// /// The tags are divided into contiguous ranges in the order the outputs are /// arranged on the desk, so with two monitors the left one owns 1–5 and the /// right 6–9. Contiguous rather than interleaved because the keys are what the /// user reaches for: 1–5 under the left hand for the left screen reads as one /// screen's worth of workspaces, 1,3,5,7,9 does not. /// /// A lone output owns every tag, which is what makes the split invisible on a /// laptop with nothing plugged in. pub fn tagsForOutput(index: usize, count: usize) u32 { if (count <= 1 or index >= count) return all_tags; // More outputs than tags: one each, and the outputs left over share the // last tag rather than getting none. An output owning no tag could show no // window at all, which is worse than two screens showing the same one. if (count >= tag_count) { return @as(u32, 1) << @intCast(@min(index, tag_count - 1)); } // Earlier outputs take one of the leftover tags each, so the ranges differ // by at most one and it is never the first screen that comes up short. const base = tag_count / count; const rem = tag_count % count; const start = index * base + @min(index, rem); const len = base + @as(usize, @intFromBool(index < rem)); const ones: u32 = (@as(u32, 1) << @intCast(len)) - 1; return ones << @intCast(start); } /// The lowest tag in a mask, as a mask of its own. What a view falls back to /// when the tags it was showing have moved to another screen. pub fn lowestTag(tags: u32) u32 { const t = tags & all_tags; if (t == 0) return 0; return @as(u32, 1) << @intCast(@ctz(t)); } /// Keyboard modifiers, matching the values of river_seat_v1.modifiers so the /// mask can be bit-cast straight into the protocol type. pub const Mods = struct { pub const none: u32 = 0; pub const shift: u32 = 1; pub const ctrl: u32 = 4; /// Commonly called alt. pub const alt: u32 = 8; pub const mod3: u32 = 32; /// Commonly called super or logo. pub const super: u32 = 64; pub const mod5: u32 = 128; }; pub const Direction = enum { next, prev, pub fn parse(s: []const u8) ?Direction { if (mem.eql(u8, s, "next")) return .next; if (mem.eql(u8, s, "prev") or mem.eql(u8, s, "previous")) return .prev; return null; } }; pub const Layout = enum { master, monocle, tabbed, pub fn parse(s: []const u8) ?Layout { return std.meta.stringToEnum(Layout, s); } /// dwm-style short symbol for the bar. pub fn symbol(self: Layout) []const u8 { return switch (self) { .master => "[]=", .monocle => "[M]", .tabbed => "|||", }; } }; /// A relative or absolute adjustment to a numeric setting. dwm only ever does /// relative ones, but IPC callers frequently want to set a value outright. pub fn Delta(comptime T: type) type { return union(enum) { relative: T, absolute: T, const Self = @This(); /// A leading `+` or `-` means relative, anything else absolute, so /// `att_wmctl mfact +0.05` nudges and `att_wmctl mfact 0.5` sets. pub fn parse(s: []const u8) ?Self { if (s.len == 0) return null; const signed = s[0] == '+' or s[0] == '-'; const value = switch (@typeInfo(T)) { .int => std.fmt.parseInt(T, s, 10) catch return null, .float => std.fmt.parseFloat(T, s) catch return null, else => @compileError("unsupported Delta type"), }; return if (signed) Self{ .relative = value } else Self{ .absolute = value }; } pub fn apply(self: Self, current: T) T { return switch (self) { .relative => |d| current + d, .absolute => |v| v, }; } }; } pub const Action = union(enum) { /// Run a command. The slice is argv; it is executed without a shell. spawn: []const []const u8, /// Ask the focused window to close. close, /// Terminate att_wm, leaving river running. quit, /// End the Wayland session entirely (river exits too). exit_session, /// Move keyboard focus through the visible windows of the focused output. focus: Direction, /// Focus one particular window, named by the identifier published over IPC. /// Key bindings only ever want a direction; a bar's task list needs to name /// the window the user clicked, and river's `identifier` is the only handle /// that is stable and never reused. focus_window: []const u8, /// Close one particular window, likewise by identifier, so a bar need not /// focus a window first just to close it. close_window: []const u8, /// Move the focused window through the arrangement order. swap: Direction, /// Promote the focused window to master, or if it is already master, /// promote the one below it. This is dwm's zoom(). zoom, /// Replace the set of visible tags on the focused output. view: u32, /// Add or remove tags from the visible set. toggle_view: u32, /// Switch back to the previously viewed tag set. view_prev, /// Replace the focused window's tags. tag: u32, /// Add or remove tags from the focused window's tags. toggle_tag: u32, set_layout: Layout, cycle_layout: Direction, /// Toggle between the current layout and the previous one, as dwm's /// Mod+space does. toggle_layout, nmaster: Delta(i32), mfact: Delta(f32), toggle_float, toggle_fullscreen, focus_output: Direction, send_to_output: Direction, /// Re-broadcast state to IPC subscribers. A hook for bars that reconnect. refresh, /// True for actions where holding the key down should keep applying the /// action. river reports key press/release and leaves repeat up to us. pub fn repeats(self: Action) bool { return switch (self) { .focus, .swap, .nmaster, .mfact, .cycle_layout => true, else => false, }; } }; pub const ParseError = error{ UnknownCommand, MissingArgument, InvalidArgument, }; /// Parse an `att_wmctl` command line into an Action. /// /// Tag arguments accept either a 1-based tag index (`view 3`) or an explicit /// bitmask (`view 0x4`, `view mask:4`, `view all`), because bars find masks /// convenient and humans find indices convenient. pub fn parse(argv: []const []const u8) ParseError!Action { if (argv.len == 0) return error.UnknownCommand; const rest = argv[1..]; const Cmd = enum { spawn, close, quit, @"exit-session", focus, @"focus-window", @"close-window", swap, zoom, view, @"toggle-view", @"view-prev", tag, @"toggle-tag", layout, @"cycle-layout", @"toggle-layout", nmaster, mfact, @"toggle-float", @"toggle-fullscreen", @"focus-output", @"send-to-output", refresh, }; const c = std.meta.stringToEnum(Cmd, argv[0]) orelse return error.UnknownCommand; return switch (c) { .spawn => if (rest.len == 0) error.MissingArgument else Action{ .spawn = rest }, .close => .close, .quit => .quit, .@"exit-session" => .exit_session, .zoom => .zoom, .@"view-prev" => .view_prev, .@"toggle-layout" => .toggle_layout, .@"toggle-float" => .toggle_float, .@"toggle-fullscreen" => .toggle_fullscreen, .refresh => .refresh, .focus => .{ .focus = try dir(rest) }, .@"focus-window" => .{ .focus_window = try windowId(rest) }, .@"close-window" => .{ .close_window = try windowId(rest) }, .swap => .{ .swap = try dir(rest) }, .@"focus-output" => .{ .focus_output = try dir(rest) }, .@"send-to-output" => .{ .send_to_output = try dir(rest) }, .@"cycle-layout" => .{ .cycle_layout = dir(rest) catch .next }, .view => .{ .view = try tagMask(rest) }, .@"toggle-view" => .{ .toggle_view = try tagMask(rest) }, .tag => .{ .tag = try tagMask(rest) }, .@"toggle-tag" => .{ .toggle_tag = try tagMask(rest) }, .layout => blk: { if (rest.len == 0) return error.MissingArgument; break :blk .{ .set_layout = Layout.parse(rest[0]) orelse return error.InvalidArgument }; }, .nmaster => blk: { if (rest.len == 0) return error.MissingArgument; break :blk .{ .nmaster = Delta(i32).parse(rest[0]) orelse return error.InvalidArgument }; }, .mfact => blk: { if (rest.len == 0) return error.MissingArgument; break :blk .{ .mfact = Delta(f32).parse(rest[0]) orelse return error.InvalidArgument }; }, }; } fn dir(rest: []const []const u8) ParseError!Direction { if (rest.len == 0) return error.MissingArgument; return Direction.parse(rest[0]) orelse error.InvalidArgument; } /// The identifier is opaque to us — river only promises up to 32 printable /// ASCII bytes — so the one thing worth rejecting is an empty argument, which /// would otherwise silently match no window. fn windowId(rest: []const []const u8) ParseError![]const u8 { if (rest.len == 0) return error.MissingArgument; if (rest[0].len == 0) return error.InvalidArgument; return rest[0]; } fn tagMask(rest: []const []const u8) ParseError!u32 { if (rest.len == 0) return error.MissingArgument; const s = rest[0]; if (mem.eql(u8, s, "all")) return all_tags; if (mem.startsWith(u8, s, "mask:")) { const v = std.fmt.parseInt(u32, s["mask:".len..], 0) catch return error.InvalidArgument; return v & all_tags; } // A 0x/0b-prefixed value is a mask; a bare decimal is a 1-based index. if (mem.startsWith(u8, s, "0x") or mem.startsWith(u8, s, "0b")) { const v = std.fmt.parseInt(u32, s, 0) catch return error.InvalidArgument; return v & all_tags; } const idx = std.fmt.parseInt(u32, s, 10) catch return error.InvalidArgument; if (idx < 1 or idx > tag_count) return error.InvalidArgument; return @as(u32, 1) << @intCast(idx - 1); } /// A single key binding, as declared in config.zig. pub const Key = struct { mods: u32, keysym: xkb.Keysym, action: Action, /// Overrides `Action.repeats()` when set. repeat: ?bool = null, pub fn shouldRepeat(self: Key) bool { return self.repeat orelse self.action.repeats(); } }; /// A pointer binding, as declared in config.zig. pub const Button = struct { mods: u32, /// Linux input event code, e.g. `btn.left`. button: u32, action: PointerAction, }; pub const PointerAction = enum { move, resize }; /// Linux input event codes for the buttons worth binding. pub const btn = struct { pub const left: u32 = 0x110; pub const right: u32 = 0x111; pub const middle: u32 = 0x112; };