364 lines
12 KiB
Zig
364 lines
12 KiB
Zig
//! 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;
|
||
};
|