initial commit

This commit is contained in:
2026-07-26 20:47:19 +02:00
commit fff3a5e734
37 changed files with 10781 additions and 0 deletions
+323
View File
@@ -0,0 +1,323 @@
//! 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;
}
/// 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;
};