initial commit
This commit is contained in:
+323
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user