initial commit
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
/.zig-cache/
|
||||
/zig-out/
|
||||
/zig-pkg/
|
||||
/result
|
||||
/result-*
|
||||
core.*
|
||||
@@ -0,0 +1,500 @@
|
||||
# att_wm
|
||||
|
||||
A dwm-like window manager for the [river](https://codeberg.org/river/river) Wayland compositor.
|
||||
|
||||
river 0.4 is *non-monolithic*: it ships no window management policy of its own — no
|
||||
`riverctl`, no `rivertile` — and instead hands the entire job to a single external
|
||||
client speaking `river-window-management-v1`. att_wm is that client. It gives you
|
||||
dwm's model on top of river: nine tags, a master/stack layout, monocle and tabbed
|
||||
layouts, and keybindings compiled into the binary.
|
||||
|
||||
Tag and window state is published as JSON lines on a unix socket so bars such as
|
||||
[quickshell](https://quickshell.org) can render it, and `att_wmctl` drives the same
|
||||
socket in the other direction.
|
||||
|
||||
Written in Zig, built with a Nix flake.
|
||||
|
||||
---
|
||||
|
||||
## Status
|
||||
|
||||
Verified working against river 0.4.5 / Zig 0.16 / quickshell 0.3.0:
|
||||
|
||||
- tags, master / monocle / tabbed layouts, floating and fullscreen windows
|
||||
- focus cycling, `zoom`, stack reordering, per-tag layout state
|
||||
- the IPC socket, `att_wmctl`, and a quickshell bar that maps as a layer surface
|
||||
and whose exclusive zone correctly shrinks the tiling area
|
||||
|
||||
- keybindings: all 63 register and fire, verified by injecting real key events
|
||||
through a virtual keyboard (`wtype`) into a nested river
|
||||
|
||||
- input: a keymap compiled from `layout`/`variant`/`options` is accepted by river
|
||||
and assigned to each keyboard, per-device rules match on name glob and device
|
||||
type, and key repeat and scroll factor are applied. `map_to_output` resolves
|
||||
the right output by name among several, and re-maps across an output being
|
||||
turned off and back on. The **libinput** half — tap to click and its
|
||||
neighbours — is written against the protocol but not yet exercised on hardware:
|
||||
river cannot expose libinput devices to a nested session, so it needs a real
|
||||
one to confirm.
|
||||
|
||||
---
|
||||
|
||||
## Building
|
||||
|
||||
```sh
|
||||
nix build # produces ./result/bin/{att_wm,att_wmctl}
|
||||
nix develop # dev shell: zig, zls, river, quickshell
|
||||
zig build # inside the dev shell
|
||||
zig build test # unit tests for the layout maths and command parser
|
||||
```
|
||||
|
||||
`./dev.sh` enters the dev shell without refetching nixpkgs when the pinned
|
||||
revision (nixos-26.05) is already in the local store.
|
||||
|
||||
## Running
|
||||
|
||||
river runs `$XDG_CONFIG_HOME/river/init` on startup. Start att_wm from there and
|
||||
keep it in the foreground, so quitting it ends the session:
|
||||
|
||||
```sh
|
||||
#!/bin/sh
|
||||
# ~/.config/river/init
|
||||
quickshell -p ~/.config/quickshell/att_wm &
|
||||
exec att_wm
|
||||
```
|
||||
|
||||
Or, for a one-off:
|
||||
|
||||
```sh
|
||||
river -c att_wm
|
||||
```
|
||||
|
||||
### Trying it without touching your system config
|
||||
|
||||
river's wayland backend runs it as an ordinary window inside your existing
|
||||
session, so you can drive att_wm for real without installing anything or
|
||||
rebuilding NixOS:
|
||||
|
||||
```sh
|
||||
nix develop # or: nix shell nixpkgs#river nixpkgs#foot nixpkgs#quickshell
|
||||
./run-nested.sh --bar --term
|
||||
```
|
||||
|
||||
That opens a nested river with att_wm, the quickshell bar and a terminal. Close
|
||||
the window to exit. It prints the nested display name, so you can drive that
|
||||
instance from another terminal:
|
||||
|
||||
```sh
|
||||
WAYLAND_DISPLAY=wayland-2 att_wmctl state
|
||||
WAYLAND_DISPLAY=wayland-2 att_wmctl layout tabbed
|
||||
```
|
||||
|
||||
Two caveats when nested:
|
||||
|
||||
- **Your outer compositor sees keys first.** If it already binds `Alt+Return` or
|
||||
similar, those never reach att_wm. Either test with `att_wmctl`, or change `mod`
|
||||
in `config.zig` and rebuild — one line, dwm-style.
|
||||
- Without a Wayland session (on a TTY) there is nothing to nest inside; use the
|
||||
headless backend instead, and drive it entirely over IPC:
|
||||
|
||||
```sh
|
||||
WLR_BACKENDS=headless WLR_HEADLESS_OUTPUTS=1 WLR_RENDERER=pixman \
|
||||
river -no-xwayland -c 'att_wm'
|
||||
```
|
||||
|
||||
att_wm must be started by river — `river_window_manager_v1` is what it binds, and
|
||||
only one client may hold it at a time. If a window manager is already running,
|
||||
att_wm reports it and exits rather than fighting for the global.
|
||||
|
||||
> **river 0.4 or newer is required.** Check with `river -version`. If it says
|
||||
> `0.3.x` you have **river-classic**, a different package that predates this
|
||||
> protocol — it does its own window management and is configured with
|
||||
> `riverctl`, so att_wm cannot drive it. Installing both leaves whichever comes
|
||||
> first on `PATH` in charge, and the symptom is a bare background with no
|
||||
> working keybindings: river-classic has no built-in bindings, and att_wm exits
|
||||
> because the global it needs is missing. Remove river-classic, or call river
|
||||
> 0.4 by its absolute path.
|
||||
|
||||
### Home Manager
|
||||
|
||||
```nix
|
||||
{
|
||||
inputs.att_wm.url = "git+https://git.project-cloud.net/asmir/att_wm.git";
|
||||
|
||||
# ...
|
||||
imports = [ inputs.att_wm.homeManagerModules.default ];
|
||||
|
||||
programs.att_wm = {
|
||||
enable = true;
|
||||
settings = ./my-config.zig; # optional, see Configuration
|
||||
autostart = [ "quickshell" ];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Keybindings
|
||||
|
||||
`Mod` is **Alt**, as dwm ships it. Change `mod` in `config.zig` to `Mods.super`
|
||||
if you would rather not compete with applications that bind Alt themselves.
|
||||
|
||||
| Binding | Action |
|
||||
| --- | --- |
|
||||
| `Mod+Shift+Return` | Spawn terminal |
|
||||
| `Mod+p` | Spawn menu |
|
||||
| `Mod+Shift+c` | Close focused window |
|
||||
| `Mod+Shift+q` | Quit att_wm (river keeps running) |
|
||||
| `Mod+Ctrl+Shift+q` | End the Wayland session |
|
||||
| `Mod+j` / `Mod+k` | Focus next / previous window |
|
||||
| `Mod+Shift+j` / `Mod+Shift+k` | Move focused window down / up the stack |
|
||||
| `Mod+Return` | Zoom — promote focused window to master |
|
||||
| `Mod+h` / `Mod+l` | Shrink / grow the master area |
|
||||
| `Mod+i` / `Mod+d` | Increase / decrease windows in master |
|
||||
| `Mod+t` / `Mod+m` / `Mod+u` | Master / monocle / tabbed layout |
|
||||
| `Mod+space` | Toggle between current and previous layout |
|
||||
| `Mod+Shift+space` | Toggle floating |
|
||||
| `Mod+f` | Toggle fullscreen |
|
||||
| `Mod+1..9` | View tag |
|
||||
| `Mod+Shift+1..9` | Move focused window to tag |
|
||||
| `Mod+Ctrl+1..9` | Toggle tag in view |
|
||||
| `Mod+Ctrl+Shift+1..9` | Toggle tag on focused window |
|
||||
| `Mod+0` / `Mod+Shift+0` | View all tags / put window on all tags |
|
||||
| `Mod+Tab` | Back to previously viewed tags |
|
||||
| `Mod+,` / `Mod+.` | Focus previous / next output |
|
||||
| `Mod+Shift+,` / `Mod+Shift+.` | Send window to previous / next output |
|
||||
| `Mod+Left drag` | Move window (floats it) |
|
||||
| `Mod+Right drag` | Resize window |
|
||||
|
||||
river reserves `Ctrl+Alt+F1`–`F12` for VT switching; no window manager can
|
||||
override those.
|
||||
|
||||
Key repeat is handled by att_wm, not river: the protocol reports press and
|
||||
release, so bindings where holding the key should keep acting (`focus`, `swap`,
|
||||
`nmaster`, `mfact`) repeat on a timer — `binding_repeat_delay` and
|
||||
`binding_repeat_interval` in `config.zig`. The repeat *applications* see is a
|
||||
separate setting, `repeat`, under [Input](#input).
|
||||
|
||||
## Layouts
|
||||
|
||||
**master** — dwm's tile. `nmaster` windows share a left-hand column of width
|
||||
`mfact`; the rest divide the remainder. Leftover pixels are absorbed as the
|
||||
column is divided, so the bottom edge always lands exactly on the output edge.
|
||||
|
||||
**monocle** — every window gets the full area; only the most recently focused
|
||||
one is rendered. A window spawned into monocle or tabbed takes focus as it
|
||||
maps, so it comes up in front instead of hidden behind the current one.
|
||||
|
||||
**tabbed** — same geometry as monocle, minus a strip at the top where att_wm
|
||||
draws one solid colour block per window, the focused one highlighted. The blocks
|
||||
are clickable. Titles are *not* drawn — that would mean a font stack — but the
|
||||
window list is published over IPC, so a bar can draw a textual tab strip
|
||||
alongside (the bundled quickshell config does). Set `tabbar_height = 0` to drop
|
||||
the built-in strip entirely and let the bar own it.
|
||||
|
||||
Layout, `nmaster` and `mfact` are kept per tag, per output — dwm with its
|
||||
pertag patch. Setting a layout on tag 3 leaves tag 4 as it was, and switching
|
||||
back to 3 restores it. Viewing several tags at once has no single tag whose
|
||||
settings should win, so all such views share one extra set; the individual tags
|
||||
keep theirs for the way back.
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
att_wm is configured at compile time, like dwm. Edit `src/config.zig` and rebuild.
|
||||
|
||||
Nix users need not patch the source tree:
|
||||
|
||||
```sh
|
||||
zig build -Dconfig=/path/to/my-config.zig
|
||||
```
|
||||
|
||||
```nix
|
||||
att_wm.override { configFile = ./my-config.zig; }
|
||||
```
|
||||
|
||||
`config.zig` covers border width and colours, gaps, tab bar height and colours,
|
||||
the default layout, `nmaster` / `mfact`, tag names, focus-follows-mouse, key and
|
||||
pointer bindings, autostart commands, input devices, and window rules:
|
||||
|
||||
```zig
|
||||
pub const rules = [_]Rule{
|
||||
.{ .app_id = "pavucontrol", .floating = true },
|
||||
.{ .title = "Picture-in-Picture", .floating = true },
|
||||
};
|
||||
```
|
||||
|
||||
Bindings are declared as data, and the same `Action` type backs both keys and
|
||||
IPC commands — so anything bindable is scriptable and vice versa.
|
||||
|
||||
### Input
|
||||
|
||||
river 0.4 hands input configuration to the window manager too, over three more
|
||||
protocols: `river-input-management-v1` enumerates devices, `river-xkb-config-v1`
|
||||
compiles keymaps, and `river-libinput-config-v1` exposes libinput's own settings.
|
||||
There is no `riverctl input`, so this is where it lives.
|
||||
|
||||
**Keyboard layout** is the RMLVO that `setxkbmap` takes, compiled once and given
|
||||
to every keyboard:
|
||||
|
||||
```zig
|
||||
pub const keymap: input.Keymap = .{
|
||||
.layout = "us,se",
|
||||
.options = "grp:alt_shift_toggle,caps:escape",
|
||||
};
|
||||
```
|
||||
|
||||
Leaving it alone keeps river's default, which honours the `XKB_DEFAULT_*`
|
||||
environment variables. With more than one layout, switching between them is a
|
||||
`grp:` option — xkb does it itself, so att_wm needs no binding for it.
|
||||
|
||||
**Key repeat**, as applications see it. Not to be confused with
|
||||
`binding_repeat_delay` / `binding_repeat_interval`, which are how fast a
|
||||
held-down att_wm *binding* re-fires — river reports binding press and release and
|
||||
leaves repeating to att_wm:
|
||||
|
||||
```zig
|
||||
pub const repeat: input.Repeat = .{ .rate = 40, .delay = 400 };
|
||||
```
|
||||
|
||||
**Per-device settings** are rules matched on name and type. `name` is a glob, so
|
||||
`*` saves you writing out `ELAN0501:00 04F3:3060 Touchpad` in full:
|
||||
|
||||
```zig
|
||||
pub const input_rules = [_]input.Rule{
|
||||
.{
|
||||
.name = "*Touchpad*",
|
||||
.tap = true,
|
||||
.natural_scroll = true,
|
||||
.disable_while_typing = true,
|
||||
.click_method = .clickfinger,
|
||||
},
|
||||
.{ .type = .keyboard, .repeat = .{ .rate = 50, .delay = 250 } },
|
||||
.{ .name = "*Logitech*", .accel_profile = .flat, .scroll_factor = 1.5 },
|
||||
};
|
||||
```
|
||||
|
||||
Every setting defaults to null, meaning "leave libinput's own default alone", so
|
||||
a rule need only say what it wants changed. Rules apply in declaration order and
|
||||
a later one overrides an earlier one field by field, so a broad rule can set a
|
||||
house style and a narrower one dissent from it — the same last-one-wins as dwm's
|
||||
window rules.
|
||||
|
||||
`src/input.zig` is the full list. Beyond the above it covers `tap_button_map`,
|
||||
`drag`, `drag_lock`, `three_finger_drag`, `clickfinger_button_map`,
|
||||
`middle_emulation`, `left_handed`, `scroll_method`, `scroll_button`,
|
||||
`scroll_button_lock`, `accel_speed`, `disable_while_trackpointing`, `rotation`,
|
||||
`send_events` and `map_to_output`.
|
||||
|
||||
Note that a **touchpad reports `pointer`, not `touch`** — libinput models it as a
|
||||
pointer that happens to support tapping. `touch` is a touchscreen.
|
||||
|
||||
#### Touchscreens and display rotation
|
||||
|
||||
An unmapped touchscreen spans the whole output layout, so on two monitors a touch
|
||||
near the left edge of the panel lands on the wrong screen. `map_to_output`
|
||||
confines it to one, named as river names it — the same name the IPC `outputs`
|
||||
list uses:
|
||||
|
||||
```zig
|
||||
.{ .type = .touch, .map_to_output = "eDP-1" },
|
||||
.{ .type = .tablet, .map_to_output = "eDP-1" },
|
||||
```
|
||||
|
||||
This is also **all that is needed for touch to survive display rotation**.
|
||||
wlroots applies the output's transform to a mapped device's coordinates on every
|
||||
event, reading the transform live — so rotating with `wlr-randr --transform`, or
|
||||
with a daemon like [rot8](https://github.com/efernandesng/rot8), rotates touch
|
||||
along with the screen. Nothing is re-sent on rotation and no rotation hook is
|
||||
needed:
|
||||
|
||||
```sh
|
||||
rot8 # no --hooks, no calibration matrix
|
||||
```
|
||||
|
||||
Do **not** also apply a libinput calibration matrix for rotation. The two
|
||||
transforms compose and the result is rotated twice. A calibration matrix is for
|
||||
correcting a panel that is wired wrong, which is a different job.
|
||||
|
||||
Mappings are re-evaluated as outputs come and go, because river drops its side of
|
||||
the mapping when the output named is destroyed — so undocking and redocking
|
||||
re-maps rather than silently losing touch. A rule naming an output that is not
|
||||
present says so once:
|
||||
|
||||
```
|
||||
warning(input): cannot map Wacom HID 5380 Finger to output eDP-9: no output by that name
|
||||
```
|
||||
|
||||
Window tiling follows rotation on its own: river reports the output's new
|
||||
dimensions and att_wm re-lays out.
|
||||
|
||||
att_wm logs one line per device as it appears, which is where the names come from:
|
||||
|
||||
```
|
||||
info(input): input device: ELAN0501:00 04F3:3060 Touchpad (pointer)
|
||||
info(input): input device: AT Translated Set 2 keyboard (keyboard)
|
||||
```
|
||||
|
||||
There is no `list-inputs` command because the protocol shows input devices to the
|
||||
window manager alone. Asking a device for something it cannot do is reported
|
||||
rather than swallowed — libinput answers every request with success, unsupported
|
||||
or invalid, and att_wm logs the latter two:
|
||||
|
||||
```
|
||||
warning(input): Logitech USB Receiver does not support tap; setting ignored
|
||||
```
|
||||
|
||||
Two things do not work everywhere:
|
||||
|
||||
- **libinput settings need real hardware.** river cannot expose libinput devices
|
||||
when it has no access to them, which is exactly the case running nested inside
|
||||
another compositor — so tap to click cannot be tested with `run-nested.sh`.
|
||||
Keyboard layout and repeat work nested; the rest needs a real session.
|
||||
- **river 0.4.5 or newer** is required for these three protocols. On an older
|
||||
0.4 att_wm warns once and carries on, rather than failing to start.
|
||||
|
||||
---
|
||||
|
||||
## IPC
|
||||
|
||||
att_wm listens on `$XDG_RUNTIME_DIR/att_wm-$WAYLAND_DISPLAY.sock` (override with
|
||||
`ATT_WM_SOCKET`). It is a line protocol: send a command line, get `ok` or
|
||||
`err <reason>`. Send `subscribe` and you get a JSON state object on every
|
||||
change, starting with one immediately.
|
||||
|
||||
A subscriber may keep sending commands on the same connection — the bundled
|
||||
quickshell config uses one socket for both, avoiding a process spawn per click.
|
||||
|
||||
### State
|
||||
|
||||
```json
|
||||
{
|
||||
"tag_count": 9,
|
||||
"tag_names": ["1","2","3","4","5","6","7","8","9"],
|
||||
"locked": false,
|
||||
"outputs": [{
|
||||
"name": "DP-1",
|
||||
"focused": true,
|
||||
"tags": 1,
|
||||
"occupied": 5,
|
||||
"layout": "master",
|
||||
"layout_symbol": "[]=",
|
||||
"nmaster": 1,
|
||||
"mfact": 0.550,
|
||||
"x": 0, "y": 0, "width": 2560, "height": 1440,
|
||||
"usable": { "x": 0, "y": 26, "width": 2560, "height": 1414 },
|
||||
"windows": [{
|
||||
"id": "6389ff21ec27eefea415425a8eaa1fd7",
|
||||
"title": "~/proj",
|
||||
"app_id": "foot",
|
||||
"tags": 1,
|
||||
"focused": true,
|
||||
"visible": true,
|
||||
"floating": false,
|
||||
"fullscreen": false
|
||||
}]
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
`tags` and `occupied` are bitmasks — `occupied` is the set of tags holding at
|
||||
least one window, which is what dwm's bar draws its corner squares from.
|
||||
`usable` is the area left after layer-shell exclusive zones, i.e. where windows
|
||||
are actually laid out.
|
||||
|
||||
There is no `urgent` field: `river-window-management-v1` has no
|
||||
attention-request event, so att_wm does not pretend to model urgency.
|
||||
|
||||
`id` is river's window identifier: up to 32 printable ASCII bytes, unique and
|
||||
never reused, and equal to the `ext_foreign_toplevel_handle_v1.identifier` of the
|
||||
same window. It is the handle `focus-window` and `close-window` take, so a bar
|
||||
can act on the window a click names rather than on whatever is focused.
|
||||
|
||||
### att_wmctl
|
||||
|
||||
```sh
|
||||
att_wmctl view 3 # view tag 3 (or: 0x4, mask:4, all)
|
||||
att_wmctl toggle-view 3
|
||||
att_wmctl tag 2 # move focused window to tag 2
|
||||
att_wmctl focus next
|
||||
att_wmctl focus-window 6389ff21ec27eefea415425a8eaa1fd7
|
||||
att_wmctl swap prev
|
||||
att_wmctl zoom
|
||||
att_wmctl layout tabbed
|
||||
att_wmctl cycle-layout next
|
||||
att_wmctl nmaster +1 # +N/-N relative, bare N absolute
|
||||
att_wmctl mfact 0.6
|
||||
att_wmctl toggle-float
|
||||
att_wmctl toggle-fullscreen
|
||||
att_wmctl focus-output next
|
||||
att_wmctl send-to-output next
|
||||
att_wmctl spawn foot -e htop
|
||||
att_wmctl close
|
||||
att_wmctl close-window 6389ff21ec27eefea415425a8eaa1fd7
|
||||
att_wmctl quit # stop att_wm, leave river running
|
||||
att_wmctl exit-session # end the Wayland session
|
||||
|
||||
att_wmctl state # print state once
|
||||
att_wmctl subscribe # stream state on every change
|
||||
```
|
||||
|
||||
Exit status is non-zero on an unknown or malformed command.
|
||||
|
||||
## quickshell
|
||||
|
||||
`quickshell/` holds a dwm-style bar: clickable tags with occupied indicators,
|
||||
the layout symbol, the focused window title, and a tab strip in tabbed layout.
|
||||
|
||||
```sh
|
||||
quickshell -p /path/to/att_wm/quickshell
|
||||
```
|
||||
|
||||
`AttWm.qml` is a singleton wrapping the socket — reconnecting if att_wm restarts,
|
||||
exposing `outputs`, `focusedOutput`, `focusedTitle`, `tagActive()` and
|
||||
`tagOccupied()`, plus `send()` for commands. Reuse it in your own bar and ignore
|
||||
`shell.qml`.
|
||||
|
||||
Note that **a bar only works because att_wm binds `river_layer_shell_v1`**. river
|
||||
refuses to map layer surfaces at all unless the window manager declares support
|
||||
for them, so under a window manager that skips it, no bar can appear.
|
||||
|
||||
**A task list must focus windows over this socket, not over
|
||||
`wlr-foreign-toplevel-management`.** river advertises that protocol, so a bar
|
||||
built on it shows the right titles and tracks focus correctly — but
|
||||
`river-window-management-v1` has no `activate_requested` event, and focus is set
|
||||
solely by the window manager through `river_seat_v1.focus_window`. An `activate`
|
||||
request from a bar therefore reaches nobody and the click does nothing. Use
|
||||
`focus-window <id>`; matching up the two protocols is not needed either, since
|
||||
river's identifier is shared between them.
|
||||
|
||||
---
|
||||
|
||||
## Layout of the source
|
||||
|
||||
| File | Purpose |
|
||||
| --- | --- |
|
||||
| `src/Wm.zig` | Globals, the manage/render sequence state machine, actions, event loop |
|
||||
| `src/Window.zig` | Per-window state; handlers only mutate fields |
|
||||
| `src/Output.zig` | Output tags, the per-tag layout/nmaster/mfact, and the tab bar |
|
||||
| `src/Seat.zig` | Focus, key and pointer bindings, interactive move/resize |
|
||||
| `src/InputManager.zig` | Input devices: keymaps, key repeat, libinput settings |
|
||||
| `src/layout.zig` | Pure tiling geometry — no Wayland, unit tested |
|
||||
| `src/action.zig` | The Action vocabulary shared by config and IPC |
|
||||
| `src/input.zig` | Input configuration types — no Wayland, unit tested |
|
||||
| `src/config.zig` | Compile-time configuration |
|
||||
| `src/ipc.zig` | Socket server and JSON encoding |
|
||||
| `src/shm.zig` | memfd buffers for the tab bar |
|
||||
| `src/sys.zig` | Thin Linux syscall wrappers |
|
||||
|
||||
The single most important invariant: **river only permits window management
|
||||
state changes between `manage_start` and `manage_finish`, and rendering state
|
||||
changes between those or `render_start`/`render_finish`.** Every event handler
|
||||
in att_wm therefore only mutates plain Zig fields and calls `needsManage()`;
|
||||
`Wm.manage()` and `Wm.render()` are the only places that issue protocol
|
||||
requests. Violating this is a fatal protocol error — river also disconnects a
|
||||
window manager that takes longer than 3 seconds inside a sequence.
|
||||
|
||||
## Licence
|
||||
|
||||
GPL-3.0-only, as dwm and river are.
|
||||
@@ -0,0 +1,123 @@
|
||||
const std = @import("std");
|
||||
const Build = std.Build;
|
||||
|
||||
const Scanner = @import("wayland").Scanner;
|
||||
|
||||
pub fn build(b: *Build) void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const strip = b.option(bool, "strip", "Omit debug information") orelse false;
|
||||
const pie = b.option(bool, "pie", "Build a Position Independent Executable") orelse false;
|
||||
|
||||
// Allow packagers and Nix users to swap in their own config.zig without
|
||||
// patching the source tree, which is how one configures dwm too.
|
||||
const config_path = b.option(
|
||||
[]const u8,
|
||||
"config",
|
||||
"Path to a config.zig to use instead of src/config.zig",
|
||||
);
|
||||
|
||||
const scanner = Scanner.create(b, .{});
|
||||
|
||||
scanner.addCustomProtocol(b.path("protocol/river-window-management-v1.xml"));
|
||||
scanner.addCustomProtocol(b.path("protocol/river-xkb-bindings-v1.xml"));
|
||||
scanner.addCustomProtocol(b.path("protocol/river-layer-shell-v1.xml"));
|
||||
scanner.addCustomProtocol(b.path("protocol/river-input-management-v1.xml"));
|
||||
scanner.addCustomProtocol(b.path("protocol/river-libinput-config-v1.xml"));
|
||||
scanner.addCustomProtocol(b.path("protocol/river-xkb-config-v1.xml"));
|
||||
|
||||
// Besides the river protocols we are an ordinary Wayland client: wl_shm and
|
||||
// wl_compositor back the tab bar, wl_seat lets us click on it.
|
||||
scanner.generate("wl_compositor", 4);
|
||||
scanner.generate("wl_shm", 1);
|
||||
scanner.generate("wl_seat", 7);
|
||||
scanner.generate("wl_output", 4);
|
||||
|
||||
scanner.generate("river_window_manager_v1", 4);
|
||||
scanner.generate("river_xkb_bindings_v1", 3);
|
||||
scanner.generate("river_layer_shell_v1", 1);
|
||||
scanner.generate("river_input_manager_v1", 1);
|
||||
scanner.generate("river_libinput_config_v1", 1);
|
||||
scanner.generate("river_xkb_config_v1", 1);
|
||||
|
||||
const wayland = b.createModule(.{ .root_source_file = scanner.result });
|
||||
const xkbcommon = b.dependency("xkbcommon", .{}).module("xkbcommon");
|
||||
|
||||
// action.zig is deliberately free of any Wayland or compositor state so
|
||||
// that config.zig can import it without a dependency cycle back into the
|
||||
// window manager.
|
||||
const action = b.createModule(.{
|
||||
.root_source_file = b.path("src/action.zig"),
|
||||
});
|
||||
action.addImport("xkbcommon", xkbcommon);
|
||||
|
||||
// Likewise Wayland-free, for the same reason: config.zig declares input
|
||||
// device settings with these types, and the protocol objects they end up on
|
||||
// live in src/InputManager.zig.
|
||||
const input = b.createModule(.{
|
||||
.root_source_file = b.path("src/input.zig"),
|
||||
});
|
||||
|
||||
const config = b.createModule(.{
|
||||
.root_source_file = if (config_path) |p| .{ .cwd_relative = p } else b.path("src/config.zig"),
|
||||
});
|
||||
config.addImport("action", action);
|
||||
config.addImport("input", input);
|
||||
config.addImport("xkbcommon", xkbcommon);
|
||||
|
||||
{
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "att_wm",
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.strip = strip,
|
||||
.link_libc = true,
|
||||
}),
|
||||
});
|
||||
exe.root_module.addImport("wayland", wayland);
|
||||
exe.root_module.addImport("xkbcommon", xkbcommon);
|
||||
exe.root_module.addImport("action", action);
|
||||
exe.root_module.addImport("input", input);
|
||||
exe.root_module.addImport("config", config);
|
||||
exe.root_module.linkSystemLibrary("wayland-client", .{});
|
||||
exe.root_module.linkSystemLibrary("xkbcommon", .{});
|
||||
exe.pie = pie;
|
||||
|
||||
b.installArtifact(exe);
|
||||
}
|
||||
|
||||
{
|
||||
const ctl = b.addExecutable(.{
|
||||
.name = "att_wmctl",
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/ctl.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.strip = strip,
|
||||
.link_libc = true,
|
||||
}),
|
||||
});
|
||||
ctl.pie = pie;
|
||||
b.installArtifact(ctl);
|
||||
}
|
||||
|
||||
{
|
||||
const tests = b.addTest(.{
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/test.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.link_libc = true,
|
||||
}),
|
||||
});
|
||||
tests.root_module.addImport("action", action);
|
||||
tests.root_module.addImport("input", input);
|
||||
tests.root_module.linkSystemLibrary("xkbcommon", .{});
|
||||
|
||||
const run_tests = b.addRunArtifact(tests);
|
||||
b.step("test", "Run unit tests").dependOn(&run_tests.step);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
.{
|
||||
.name = .att_wm,
|
||||
.version = "0.1.0",
|
||||
.minimum_zig_version = "0.16.0",
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"protocol",
|
||||
"src",
|
||||
"README.md",
|
||||
},
|
||||
.dependencies = .{
|
||||
.wayland = .{
|
||||
.url = "https://codeberg.org/ifreund/zig-wayland/archive/v0.6.0.tar.gz",
|
||||
.hash = "wayland-0.6.0-lQa1kqz8AQADQmdNJsNhLoNHcnEGEUjrOaPV-dtEnEmX",
|
||||
},
|
||||
.xkbcommon = .{
|
||||
.url = "https://codeberg.org/ifreund/zig-xkbcommon/archive/v0.4.0.tar.gz",
|
||||
.hash = "xkbcommon-0.4.0-VDqIe0i2AgDRsok2GpMFYJ8SVhQS10_PI2M_CnHXsJJZ",
|
||||
},
|
||||
},
|
||||
.fingerprint = 0xaef897c1fba5ef4c,
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Enter the dev shell without re-fetching nixpkgs.
|
||||
#
|
||||
# flake.nix pins nixos-26.05. When the machine already has that revision in its
|
||||
# store (NixOS systems built from the same channel usually do), pointing the
|
||||
# input at the local path skips the tarball fetch entirely and makes
|
||||
# `nix develop` near-instant. Falls back to the pinned input otherwise.
|
||||
set -euo pipefail
|
||||
|
||||
local_nixpkgs=$(nix registry list 2>/dev/null |
|
||||
awk '$1 == "system" && $2 == "flake:nixpkgs" { print $3 }' |
|
||||
sed 's/?.*//')
|
||||
|
||||
args=()
|
||||
if [[ -n ${local_nixpkgs} && -d ${local_nixpkgs#path:} ]]; then
|
||||
args=(--override-input nixpkgs "${local_nixpkgs}")
|
||||
fi
|
||||
|
||||
exec nix develop "${args[@]}" --command "${@:-bash}"
|
||||
Generated
+61
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1784160687,
|
||||
"narHash": "sha256-iYL/bixrb6FlHFu/gIuBYzq6c6lM5AAXsXNSWXtIgQc=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "4382ed2b7a6839d4280a9b386db49cbc5907414d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-26.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
description = "att_wm - a dwm-like window manager for the river Wayland compositor";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in
|
||||
{
|
||||
packages = rec {
|
||||
att_wm = pkgs.callPackage ./nix/package.nix {
|
||||
inherit self;
|
||||
zig = pkgs.zig_0_16;
|
||||
};
|
||||
default = att_wm;
|
||||
};
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
strictDeps = false;
|
||||
nativeBuildInputs = with pkgs; [
|
||||
zig_0_16
|
||||
zls
|
||||
pkg-config
|
||||
wayland-scanner
|
||||
];
|
||||
buildInputs = with pkgs; [
|
||||
wayland
|
||||
wayland-protocols
|
||||
libxkbcommon
|
||||
];
|
||||
packages = with pkgs; [
|
||||
river
|
||||
quickshell
|
||||
zon2nix
|
||||
];
|
||||
};
|
||||
|
||||
formatter = pkgs.nixfmt-rfc-style;
|
||||
}
|
||||
)
|
||||
// {
|
||||
overlays.default = final: prev: {
|
||||
att_wm = final.callPackage ./nix/package.nix {
|
||||
inherit self;
|
||||
zig = final.zig_0_16;
|
||||
};
|
||||
};
|
||||
|
||||
# `homeModules` is the name nix recognises; the older spelling is kept
|
||||
# so existing configs importing it keep working.
|
||||
homeModules.default = import ./nix/hm-module.nix self;
|
||||
homeManagerModules.default = import ./nix/hm-module.nix self;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
# generated by zon2nix (https://github.com/nix-community/zon2nix)
|
||||
|
||||
{ linkFarm, fetchzip, fetchgit }:
|
||||
|
||||
linkFarm "zig-packages" [
|
||||
{
|
||||
name = "wayland-0.6.0-lQa1kqz8AQADQmdNJsNhLoNHcnEGEUjrOaPV-dtEnEmX";
|
||||
path = fetchzip {
|
||||
url = "https://codeberg.org/ifreund/zig-wayland/archive/v0.6.0.tar.gz";
|
||||
hash = "sha256-3m/ITNhZUJ/5uD/Tqm+0uZSktGoYgWF5oldOqOCUkIE=";
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "xkbcommon-0.4.0-VDqIe0i2AgDRsok2GpMFYJ8SVhQS10_PI2M_CnHXsJJZ";
|
||||
path = fetchzip {
|
||||
url = "https://codeberg.org/ifreund/zig-xkbcommon/archive/v0.4.0.tar.gz";
|
||||
hash = "sha256-zQkmP/cuhAtjOLqYS5D15khKzpqyhbyZ0TD6/8jOkqE=";
|
||||
};
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,77 @@
|
||||
self:
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.programs.att_wm;
|
||||
in
|
||||
{
|
||||
options.programs.att_wm = {
|
||||
enable = lib.mkEnableOption "att_wm, a dwm-like window manager for river";
|
||||
|
||||
package = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
default = self.packages.${pkgs.stdenv.hostPlatform.system}.att_wm;
|
||||
defaultText = lib.literalMD "the flake's `att_wm` package";
|
||||
description = "The att_wm package to use.";
|
||||
};
|
||||
|
||||
settings = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
example = lib.literalExpression "./config.zig";
|
||||
description = ''
|
||||
A replacement for att_wm's `src/config.zig`. att_wm is configured at
|
||||
compile time in the manner of dwm, so setting this rebuilds it.
|
||||
'';
|
||||
};
|
||||
|
||||
autostart = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [ "quickshell" ];
|
||||
description = ''
|
||||
Commands appended to river's init script, run once the session starts.
|
||||
att_wm itself is always started last and kept in the foreground.
|
||||
'';
|
||||
};
|
||||
|
||||
riverPackage = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
default = pkgs.river;
|
||||
description = "The river package the init script is written for.";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable (
|
||||
let
|
||||
att_wm =
|
||||
if cfg.settings == null then
|
||||
cfg.package
|
||||
else
|
||||
cfg.package.override { configFile = cfg.settings; };
|
||||
in
|
||||
{
|
||||
home.packages = [
|
||||
att_wm
|
||||
cfg.riverPackage
|
||||
];
|
||||
|
||||
# river runs this executable on startup and expects the window manager to
|
||||
# connect; keeping att_wm in the foreground ties the session's lifetime to
|
||||
# it, so quitting att_wm ends the session cleanly.
|
||||
xdg.configFile."river/init" = {
|
||||
executable = true;
|
||||
text = ''
|
||||
#!${pkgs.runtimeShell}
|
||||
${lib.concatMapStringsSep "\n" (c: "${c} &") cfg.autostart}
|
||||
exec ${lib.getExe att_wm}
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
lib,
|
||||
self,
|
||||
stdenv,
|
||||
callPackage,
|
||||
zig,
|
||||
pkg-config,
|
||||
wayland,
|
||||
wayland-protocols,
|
||||
wayland-scanner,
|
||||
libxkbcommon,
|
||||
# Path to a replacement src/config.zig. dwm-style compile-time configuration:
|
||||
# att_wm.override { configFile = ./my-config.zig; }
|
||||
# Deliberately not called `config`: callPackage would fill that from the
|
||||
# nixpkgs config set.
|
||||
configFile ? null,
|
||||
}:
|
||||
|
||||
let
|
||||
deps = callPackage ./deps.nix { };
|
||||
in
|
||||
stdenv.mkDerivation {
|
||||
pname = "att_wm";
|
||||
version = "0.1.0";
|
||||
|
||||
src = lib.cleanSource self;
|
||||
|
||||
strictDeps = true;
|
||||
|
||||
nativeBuildInputs = [
|
||||
zig
|
||||
pkg-config
|
||||
wayland-scanner
|
||||
];
|
||||
|
||||
buildInputs = [
|
||||
wayland
|
||||
wayland-protocols
|
||||
wayland-scanner
|
||||
libxkbcommon
|
||||
];
|
||||
|
||||
zigBuildFlags = [
|
||||
"--system"
|
||||
"${deps}"
|
||||
"-Dpie"
|
||||
]
|
||||
++ lib.optional (configFile != null) "-Dconfig=${configFile}";
|
||||
|
||||
doCheck = true;
|
||||
|
||||
# The check phase gets its own flag list, so it needs --system too; without
|
||||
# it `zig build test` tries to fetch the dependencies over the network and
|
||||
# fails in the sandbox.
|
||||
zigCheckFlags = [
|
||||
"--system"
|
||||
"${deps}"
|
||||
];
|
||||
|
||||
meta = {
|
||||
description = "A dwm-like window manager for the river Wayland compositor";
|
||||
longDescription = ''
|
||||
att_wm implements river-window-management-v1, providing dwm's window
|
||||
management model — tags, a master/stack layout, monocle and tabbed
|
||||
layouts — on top of river 0.4's non-monolithic compositor. Tag and window
|
||||
state is published over a JSON-lines unix socket for bars such as
|
||||
quickshell, and driven back the other way with the att_wmctl CLI.
|
||||
'';
|
||||
license = lib.licenses.gpl3Only;
|
||||
platforms = lib.platforms.linux;
|
||||
mainProgram = "att_wm";
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<protocol name="river_input_management_v1">
|
||||
<copyright>
|
||||
SPDX-FileCopyrightText: © 2025 Isaac Freund
|
||||
SPDX-License-Identifier: MIT
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
</copyright>
|
||||
|
||||
<description summary="manage seats and input devices">
|
||||
This protocol supports creating/destroying seats, assigning input devices to
|
||||
seats, and configuring input devices (e.g. setting keyboard repeat rate).
|
||||
|
||||
The key words "must", "must not", "required", "shall", "shall not",
|
||||
"should", "should not", "recommended", "may", and "optional" in this
|
||||
document are to be interpreted as described in IETF RFC 2119.
|
||||
</description>
|
||||
|
||||
<interface name="river_input_manager_v1" version="1">
|
||||
<description summary="input manager global interface">
|
||||
Input manager global interface.
|
||||
</description>
|
||||
|
||||
<enum name="error">
|
||||
<entry name="invalid_destroy" value="0"/>
|
||||
</enum>
|
||||
|
||||
<request name="stop">
|
||||
<description summary="stop sending events">
|
||||
This request indicates that the client no longer wishes to receive
|
||||
events on this object.
|
||||
|
||||
The Wayland protocol is asynchronous, which means the server may send
|
||||
further events until the stop request is processed. The client must wait
|
||||
for a river_input_manager_v1.finished event before destroying this
|
||||
object.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<event name="finished">
|
||||
<description summary="the server has finished with the input manager">
|
||||
This event indicates that the server will send no further events on this
|
||||
object. The client should destroy the object. See
|
||||
river_input_manager_v1.destroy for more information.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the river_input_manager_v1 object">
|
||||
This request should be called after the finished event has been received
|
||||
to complete destruction of the object.
|
||||
|
||||
It is a protocol error to make this request before the finished event
|
||||
has been received.
|
||||
|
||||
If a client wishes to destroy this object it should send a
|
||||
river_input_manager_v1.stop request and wait for a
|
||||
river_input_manager_v1.finished event. Once the finished event is
|
||||
received it is safe to destroy this object and any other objects created
|
||||
through this interface.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="create_seat">
|
||||
<description summary="create a new seat">
|
||||
Create a new seat with the given name. Has no effect if a seat with the
|
||||
given name already exists.
|
||||
|
||||
The default seat with name "default" always exists and does not need to
|
||||
be explicitly created.
|
||||
</description>
|
||||
<arg name="name" type="string"/>
|
||||
</request>
|
||||
|
||||
<request name="destroy_seat">
|
||||
<description summary="destroy a seat">
|
||||
Destroy the seat with the given name. Has no effect if a seat with the
|
||||
given name does not exist.
|
||||
|
||||
The default seat with name "default" cannot be destroyed and attempting
|
||||
to destroy it will have no effect.
|
||||
|
||||
Any input devices assigned to the destroyed seat at the time of
|
||||
destruction are assigned to the default seat.
|
||||
</description>
|
||||
<arg name="name" type="string"/>
|
||||
</request>
|
||||
|
||||
<event name="input_device">
|
||||
<description summary="new input device">
|
||||
A new input device has been created.
|
||||
</description>
|
||||
<arg name="id" type="new_id" interface="river_input_device_v1"/>
|
||||
</event>
|
||||
</interface>
|
||||
|
||||
<interface name="river_input_device_v1" version="1">
|
||||
<description summary="an input device">
|
||||
An input device represents a physical keyboard, mouse, touchscreen, or
|
||||
drawing tablet tool. It is assigned to exactly one seat at a time.
|
||||
By default, all input devices are assigned to the default seat.
|
||||
</description>
|
||||
|
||||
<enum name="error">
|
||||
<entry name="invalid_repeat_info" value="0"/>
|
||||
<entry name="invalid_scroll_factor" value="1"/>
|
||||
<entry name="invalid_map_to_rectangle" value="2"/>
|
||||
</enum>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the input device object">
|
||||
This request indicates that the client will no longer use the input
|
||||
device object and that it may be safely destroyed.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<event name="removed">
|
||||
<description summary="the input device is removed">
|
||||
This event indicates that the input device has been removed.
|
||||
|
||||
The server will send no further events on this object and ignore any
|
||||
request (other than river_input_device_v1.destroy) made after this event is
|
||||
sent. The client should destroy this object with the
|
||||
river_input_device_v1.destroy request to free up resources.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<enum name="type">
|
||||
<entry name="keyboard" value="0"/>
|
||||
<entry name="pointer" value="1"/>
|
||||
<entry name="touch" value="2"/>
|
||||
<entry name="tablet" value="3"/>
|
||||
</enum>
|
||||
|
||||
<event name="type">
|
||||
<description summary="the type of the input device">
|
||||
The type of the input device. This event is sent once when the
|
||||
river_input_device_v1 object is created. The device type cannot
|
||||
change during the lifetime of the object.
|
||||
</description>
|
||||
<arg name="type" type="uint" enum="type"/>
|
||||
</event>
|
||||
|
||||
<event name="name">
|
||||
<description summary="the name of the input device">
|
||||
The name of the input device. This event is sent once when the
|
||||
river_input_device_v1 object is created. The device name cannot
|
||||
change during the lifetime of the object.
|
||||
</description>
|
||||
<arg name="name" type="string"/>
|
||||
</event>
|
||||
|
||||
<request name="assign_to_seat">
|
||||
<description summary="assign the input device to a seat">
|
||||
Assign the input device to a seat. All input devices not explicitly
|
||||
assigned to a seat are considered assigned to the default seat.
|
||||
|
||||
Has no effect if a seat with the given name does not exist.
|
||||
</description>
|
||||
<arg name="name" type="string" summary="name of the seat"/>
|
||||
</request>
|
||||
|
||||
<request name="set_repeat_info">
|
||||
<description summary="set keyboard repeat rate and delay">
|
||||
Set repeat rate and delay for a keyboard input device. Has no effect if
|
||||
the device is not a keyboard.
|
||||
|
||||
Negative values for either rate or delay are illegal. A rate of zero
|
||||
will disable any repeating (regardless of the value of delay).
|
||||
</description>
|
||||
<arg name="rate" type="int" summary="rate in key repeats per second"/>
|
||||
<arg name="delay" type="int" summary="delay in milliseconds"/>
|
||||
</request>
|
||||
|
||||
<request name="set_scroll_factor">
|
||||
<description summary="set scroll factor">
|
||||
Set the scroll factor for a pointer input device. Has no effect if the
|
||||
device is not a pointer.
|
||||
|
||||
For example, a factor of 0.5 will make scrolling twice as slow while a
|
||||
factor of 3.0 will make scrolling 3 times as fast.
|
||||
|
||||
Setting a scroll factor less than 0 is a protocol error.
|
||||
</description>
|
||||
<arg name="factor" type="fixed"/>
|
||||
</request>
|
||||
|
||||
<request name="map_to_output">
|
||||
<description summary="map input device to the given output">
|
||||
Map the input device to the given output. Has no effect if the device is
|
||||
not a pointer, touch, or tablet device.
|
||||
|
||||
If mapped to both an output and a rectangle, the rectangle has priority.
|
||||
|
||||
Passing null clears an existing mapping.
|
||||
</description>
|
||||
<arg name="output" type="object" interface="wl_output" allow-null="true"/>
|
||||
</request>
|
||||
|
||||
<request name="map_to_rectangle">
|
||||
<description summary="map input device to the given rectangle">
|
||||
Map the input device to the given rectangle in the global compositor
|
||||
coordinate space. Has no effect if the device is not a pointer, touch,
|
||||
or tablet device.
|
||||
|
||||
If mapped to both an output and a rectangle, the rectangle has priority.
|
||||
|
||||
Width and height must be greater than or equal to 0.
|
||||
|
||||
Passing 0 for width or height clears an existing mapping.
|
||||
</description>
|
||||
<arg name="x" type="int"/>
|
||||
<arg name="y" type="int"/>
|
||||
<arg name="width" type="int"/>
|
||||
<arg name="height" type="int"/>
|
||||
</request>
|
||||
</interface>
|
||||
</protocol>
|
||||
@@ -0,0 +1,191 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<protocol name="river_layer_shell_v1">
|
||||
<copyright>
|
||||
SPDX-FileCopyrightText: © 2025 Isaac Freund
|
||||
SPDX-License-Identifier: MIT
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
</copyright>
|
||||
|
||||
<description summary="optional layer shell support">
|
||||
This protocol allows the river-window-management-v1 window manager to
|
||||
support the wlr-layer-shell-unstable-v1 protocol.
|
||||
|
||||
The key words "must", "must not", "required", "shall", "shall not",
|
||||
"should", "should not", "recommended", "may", and "optional" in this
|
||||
document are to be interpreted as described in IETF RFC 2119.
|
||||
</description>
|
||||
|
||||
<interface name="river_layer_shell_v1" version="1">
|
||||
<description summary="river layer shell global interface">
|
||||
This global interface should only be advertised to the client if the
|
||||
river_window_manager_v1 global is also advertised. Binding this interface
|
||||
indicates that the window manager supports layer shell.
|
||||
|
||||
If the window manager does not bind this interface, the compositor should
|
||||
not allow clients to map layer surfaces. This can be achieved by
|
||||
closing layer surfaces immediately.
|
||||
</description>
|
||||
|
||||
<enum name="error">
|
||||
<entry name="object_already_created" value="0"
|
||||
summary="the layer_shell_output/seat object was already created."/>
|
||||
</enum>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the river_layer_shell_v1 object">
|
||||
This request indicates that the client will no longer use the
|
||||
river_layer_shell_v1 object.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="get_output">
|
||||
<description summary="get layer shell output state">
|
||||
It is a protocol error to make this request more than once for a given
|
||||
river_output_v1 object.
|
||||
</description>
|
||||
<arg name="id" type="new_id" interface="river_layer_shell_output_v1"/>
|
||||
<arg name="output" type="object" interface="river_output_v1"/>
|
||||
</request>
|
||||
|
||||
<request name="get_seat">
|
||||
<description summary="get layer shell seat state">
|
||||
It is a protocol error to make this request more than once for a given
|
||||
river_seat_v1 object.
|
||||
</description>
|
||||
<arg name="id" type="new_id" interface="river_layer_shell_seat_v1"/>
|
||||
<arg name="seat" type="object" interface="river_seat_v1"/>
|
||||
</request>
|
||||
</interface>
|
||||
|
||||
<interface name="river_layer_shell_output_v1" version="1">
|
||||
<description summary="layer shell output state">
|
||||
The lifetime of this object is tied to the corresponding river_output_v1.
|
||||
This object is made inert when the river_output_v1.removed event is sent
|
||||
and should be destroyed.
|
||||
</description>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the object">
|
||||
This request indicates that the client will no longer use the
|
||||
river_layer_shell_output_v1 object and that it may be safely destroyed.
|
||||
|
||||
This request should be made after the river_output_v1.removed event is
|
||||
received to complete destruction of the output.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<event name="non_exclusive_area">
|
||||
<description summary="area left after subtracting exclusive zones">
|
||||
This event indicates the area of the output remaining after subtracting
|
||||
the exclusive zones of layer surfaces. Exclusive zones are a hint, the
|
||||
window manager is free to ignore this area hint if it wishes.
|
||||
|
||||
The x and y values are in the global coordinate space, not relative to
|
||||
the position of the output.
|
||||
|
||||
This event will be followed by a manage_start event after all other new
|
||||
state has been sent by the server.
|
||||
</description>
|
||||
<arg name="x" type="int" summary="global x coordinate"/>
|
||||
<arg name="y" type="int" summary="global y coordinate"/>
|
||||
<arg name="width" type="int" summary="area width"/>
|
||||
<arg name="height" type="int" summary="area height"/>
|
||||
</event>
|
||||
|
||||
<request name="set_default">
|
||||
<description summary="Set default output for layer surfaces">
|
||||
Mark this output as the default for new layer surfaces which do not
|
||||
request a specific output themselves. This request overrides any
|
||||
previous set_default request on any river_layer_shell_output_v1 object.
|
||||
|
||||
If no set_default request is made or if the default output is destroyed,
|
||||
the default output is undefined until the next set_default request.
|
||||
|
||||
This request modifies window management state and may only be made as
|
||||
part of a manage sequence, see the river_window_manager_v1 description.
|
||||
</description>
|
||||
</request>
|
||||
</interface>
|
||||
|
||||
<interface name="river_layer_shell_seat_v1" version="1">
|
||||
<description summary="layer shell seat state">
|
||||
The lifetime of this object is tied to the corresponding river_seat_v1.
|
||||
This object is made inert when the river_seat_v1.removed event is sent and
|
||||
should be destroyed.
|
||||
</description>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the object">
|
||||
This request indicates that the client will no longer use the
|
||||
river_layer_shell_seat_v1 object and that it may be safely destroyed.
|
||||
|
||||
This request should be made after the river_seat_v1.removed event is
|
||||
received to complete destruction of the seat.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<event name="focus_exclusive">
|
||||
<description summary="layer shell surface has exclusive focus">
|
||||
A layer shell surface will be given exclusive keyboard focus at the end
|
||||
of the manage sequence in which this event is sent. The window manager
|
||||
may want to update window decorations or similar to indicate that no
|
||||
window is focused.
|
||||
|
||||
Until the focus_non_exclusive or focus_none event is sent, all window
|
||||
manager requests to change focus are ignored.
|
||||
|
||||
This event will be followed by a manage_start event after all other new
|
||||
state has been sent by the server.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<event name="focus_non_exclusive">
|
||||
<description summary="layer shell surface wants non-exclusive focus">
|
||||
A layer shell surface will be given non-exclusive keyboard focus at the
|
||||
end of the manage sequence in which this event is sent. The window
|
||||
manager may want to update window decorations or similar to indicate
|
||||
that no window is focused.
|
||||
|
||||
The window manager continues to control focus and may choose to focus a
|
||||
different window/shell surface at any time. If the window manager sets
|
||||
focus during the same manage sequence in which this event is sent, the
|
||||
layer surface will not be focused.
|
||||
|
||||
If the layer surface with non-exclusive focus is closed or the window
|
||||
manager chooses to move focus away from the layer surface, a focus_none
|
||||
event will be sent in the next manage sequence.
|
||||
|
||||
This event will be followed by a manage_start event after all other new
|
||||
state has been sent by the server.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<event name="focus_none">
|
||||
<description summary="no layer shell surface has focus">
|
||||
No layer shell surface will have keyboard focus at the end of the manage
|
||||
sequence in which this event is sent. The window manager may want to
|
||||
return focus to whichever window last had focus, for example.
|
||||
|
||||
This event will be followed by a manage_start event after all other new
|
||||
state has been sent by the server.
|
||||
</description>
|
||||
</event>
|
||||
</interface>
|
||||
</protocol>
|
||||
@@ -0,0 +1,891 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<protocol name="river_libinput_config_v1">
|
||||
<copyright>
|
||||
SPDX-FileCopyrightText: © 2025 Isaac Freund
|
||||
SPDX-License-Identifier: MIT
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
</copyright>
|
||||
|
||||
<description summary="configure libinput devices">
|
||||
This protocol exposes libinput device configuration APIs. The libinput
|
||||
documentation should be referred to for detailed information on libinput's
|
||||
behavior.
|
||||
|
||||
Note that the compositor will not be able to expose libinput devices through
|
||||
this protocol when it does not have access to the hardware, for example when
|
||||
running nested in another Wayland compositor or X11 session.
|
||||
|
||||
This protocol is designed so that (hopefully) any backwards compatible
|
||||
change to libinput's API can be matched with a backwards compatible change
|
||||
to this protocol.
|
||||
|
||||
Note: the libinput API uses floating point types (float and double in C)
|
||||
which are not (yet?) natively supported by the Wayland protocol. However,
|
||||
the Wayland protocol does support sending arbitrary bytes through the array
|
||||
argument type. This protocol uses e.g. type="array" summary="double" to
|
||||
indicate a native-endian IEEE-754 64-bit double value.
|
||||
|
||||
The key words "must", "must not", "required", "shall", "shall not",
|
||||
"should", "should not", "recommended", "may", and "optional" in this
|
||||
document are to be interpreted as described in IETF RFC 2119.
|
||||
</description>
|
||||
|
||||
<interface name="river_libinput_config_v1" version="1">
|
||||
<description summary="libinput config global interface">
|
||||
Global interface for configuring libinput devices. This global should
|
||||
only be advertised if river_input_manager_v1 is advertised as well.
|
||||
</description>
|
||||
|
||||
<enum name="error">
|
||||
<entry name="invalid_arg" value="0"
|
||||
summary="invalid enum value or similar"/>
|
||||
<entry name="invalid_destroy" value="1"/>
|
||||
</enum>
|
||||
|
||||
<request name="stop">
|
||||
<description summary="stop sending events">
|
||||
This request indicates that the client no longer wishes to receive
|
||||
events on this object.
|
||||
|
||||
The Wayland protocol is asynchronous, which means the server may send
|
||||
further events until the stop request is processed. The client must wait
|
||||
for a river_libinput_config_v1.finished event before destroying this
|
||||
object.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<event name="finished">
|
||||
<description summary="the server has finished with the object">
|
||||
This event indicates that the server will send no further events on this
|
||||
object. The client should destroy the object. See
|
||||
river_libinput_config_v1.destroy for more information.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the river_libinput_config_v1 object">
|
||||
This request should be called after the finished event has been received
|
||||
to complete destruction of the object.
|
||||
|
||||
It is a protocol error to make this request before the finished event
|
||||
has been received.
|
||||
|
||||
If a client wishes to destroy this object it should send a
|
||||
river_libinput_config_v1.stop request and wait for a
|
||||
river_libinput_config_v1.finished event. Once the finished event is
|
||||
received it is safe to destroy this object and any other objects created
|
||||
through this interface.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<event name="libinput_device">
|
||||
<description summary="new libinput device">
|
||||
A new libinput device has been created. Not every river_input_device_v1
|
||||
is necessarily a libinput device as well.
|
||||
</description>
|
||||
<arg name="id" type="new_id" interface="river_libinput_device_v1"/>
|
||||
</event>
|
||||
|
||||
<request name="create_accel_config">
|
||||
<description summary="create a acceleration config">
|
||||
Create a acceleration config which can be applied
|
||||
with river_libinput_device_v1.apply_accel_config.
|
||||
</description>
|
||||
<arg name="id" type="new_id"
|
||||
interface="river_libinput_accel_config_v1"/>
|
||||
<arg name="profile" type="uint"
|
||||
enum="river_libinput_device_v1.accel_profile"/>
|
||||
</request>
|
||||
</interface>
|
||||
|
||||
<interface name="river_libinput_device_v1" version="1">
|
||||
<description summary="a libinput device">
|
||||
In general, *_support events will be sent exactly once directly after the
|
||||
river_libinput_device_v1 is created. *_default events will be sent after
|
||||
*_support events if the config option is supported, and *_current events
|
||||
willl be sent after the *_default events and again whenever the config
|
||||
option is changed.
|
||||
</description>
|
||||
|
||||
<enum name="error">
|
||||
<entry name="invalid_arg" value="0"
|
||||
summary="invalid enum value or similar"/>
|
||||
</enum>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the libinput device object">
|
||||
This request indicates that the client will no longer use the input
|
||||
device object and that it may be safely destroyed.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<event name="removed">
|
||||
<description summary="the libinput device is removed">
|
||||
This event indicates that the libinput device has been removed.
|
||||
|
||||
The server will send no further events on this object and ignore any
|
||||
request (other than river_libinput_device_v1.destroy) made after this
|
||||
event is sent. The client should destroy this object with the
|
||||
river_libinput_device_v1.destroy request to free up resources.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<event name="input_device">
|
||||
<description summary="corresponding river input device">
|
||||
The river_input_device_v1 corresponding to this libinput device.
|
||||
This event will always be the first event sent on the
|
||||
river_libinput_device_v1 object, and it will be sent exactly once.
|
||||
</description>
|
||||
<arg name="device" type="object" interface="river_input_device_v1"/>
|
||||
</event>
|
||||
|
||||
<enum name="send_events_modes" bitfield="true">
|
||||
<entry name="enabled" value="0"/>
|
||||
<entry name="disabled" value="1"/>
|
||||
<entry name="disabled_on_external_mouse" value="2"/>
|
||||
</enum>
|
||||
|
||||
<event name="send_events_support">
|
||||
<description summary="supported send events modes">
|
||||
Supported send events modes.
|
||||
</description>
|
||||
<arg name="modes" type="uint" enum="send_events_modes"/>
|
||||
</event>
|
||||
|
||||
<event name="send_events_default">
|
||||
<description summary="default send events mode">
|
||||
Default send events mode.
|
||||
</description>
|
||||
<arg name="mode" type="uint" enum="send_events_modes"/>
|
||||
</event>
|
||||
|
||||
<event name="send_events_current">
|
||||
<description summary="current send events mode">
|
||||
Current send events mode.
|
||||
</description>
|
||||
<arg name="mode" type="uint" enum="send_events_modes"/>
|
||||
</event>
|
||||
|
||||
<request name="set_send_events">
|
||||
<description summary="set send events mode">
|
||||
Set the send events mode for the device.
|
||||
</description>
|
||||
<arg name="result" type="new_id" interface="river_libinput_result_v1"/>
|
||||
<arg name="mode" type="uint" enum="send_events_modes"/>
|
||||
</request>
|
||||
|
||||
<enum name="tap_state">
|
||||
<entry name="disabled" value="0"/>
|
||||
<entry name="enabled" value="1"/>
|
||||
</enum>
|
||||
|
||||
<event name="tap_support">
|
||||
<description summary="tap-to-click/drag support">
|
||||
The number of fingers supported for tap-to-click/drag.
|
||||
If finger_count is 0, tap-to-click and drag are unsupported.
|
||||
</description>
|
||||
<arg name="finger_count" type="int"/>
|
||||
</event>
|
||||
|
||||
<event name="tap_default">
|
||||
<description summary="default tap-to-click state">
|
||||
Default tap-to-click state.
|
||||
</description>
|
||||
<arg name="state" type="uint" enum="tap_state"/>
|
||||
</event>
|
||||
|
||||
<event name="tap_current">
|
||||
<description summary="current tap-to-click state">
|
||||
Current tap-to-click state.
|
||||
</description>
|
||||
<arg name="state" type="uint" enum="tap_state"/>
|
||||
</event>
|
||||
|
||||
<request name="set_tap">
|
||||
<description summary="enable/disable tap-to-click">
|
||||
Configure tap-to-click on this device, with a default mapping of
|
||||
1, 2, 3 finger tap mapping to left, right, middle click, respectively.
|
||||
</description>
|
||||
<arg name="result" type="new_id" interface="river_libinput_result_v1"/>
|
||||
<arg name="state" type="uint" enum="tap_state"/>
|
||||
</request>
|
||||
|
||||
<enum name="tap_button_map">
|
||||
<entry name="lrm" value="0"
|
||||
summary="1/2/3 finger tap maps to left/right/middle"/>
|
||||
<entry name="lmr" value="1"
|
||||
summary="1/2/3 finger tap maps to left/middle/right"/>
|
||||
</enum>
|
||||
|
||||
<event name="tap_button_map_default">
|
||||
<description summary="default tap-to-click button map">
|
||||
Default tap-to-click button map.
|
||||
</description>
|
||||
<arg name="button_map" type="uint" enum="tap_button_map"/>
|
||||
</event>
|
||||
|
||||
<event name="tap_button_map_current">
|
||||
<description summary="current tap-to-click button map">
|
||||
Current tap-to-click button map.
|
||||
</description>
|
||||
<arg name="button_map" type="uint" enum="tap_button_map"/>
|
||||
</event>
|
||||
|
||||
<request name="set_tap_button_map">
|
||||
<description summary="set tap-to-click button map">
|
||||
Set the finger number to button number mapping for tap-to-click. The
|
||||
default mapping on most devices is to have a 1, 2 and 3 finger tap to
|
||||
map to the left, right and middle button, respectively.
|
||||
</description>
|
||||
<arg name="result" type="new_id" interface="river_libinput_result_v1"/>
|
||||
<arg name="button_map" type="uint" enum="tap_button_map"/>
|
||||
</request>
|
||||
|
||||
<enum name="drag_state">
|
||||
<entry name="disabled" value="0"/>
|
||||
<entry name="enabled" value="1"/>
|
||||
</enum>
|
||||
|
||||
<event name="drag_default">
|
||||
<description summary="default tap-and-drag state">
|
||||
Default tap-and-drag state.
|
||||
</description>
|
||||
<arg name="state" type="uint" enum="drag_state"/>
|
||||
</event>
|
||||
|
||||
<event name="drag_current">
|
||||
<description summary="current tap-and-drag state">
|
||||
Current tap-and-drag state.
|
||||
</description>
|
||||
<arg name="state" type="uint" enum="drag_state"/>
|
||||
</event>
|
||||
|
||||
<request name="set_drag">
|
||||
<description summary="set tap-and-drag state">
|
||||
Configure tap-and-drag functionality on the device.
|
||||
</description>
|
||||
<arg name="result" type="new_id" interface="river_libinput_result_v1"/>
|
||||
<arg name="state" type="uint" enum="drag_state"/>
|
||||
</request>
|
||||
|
||||
<enum name="drag_lock_state">
|
||||
<entry name="disabled" value="0"/>
|
||||
<entry name="enabled_timeout" value="1"/>
|
||||
<entry name="enabled_sticky" value="2"/>
|
||||
</enum>
|
||||
|
||||
<event name="drag_lock_default">
|
||||
<description summary="default drag lock state">
|
||||
Default drag lock state.
|
||||
</description>
|
||||
<arg name="state" type="uint" enum="drag_lock_state"/>
|
||||
</event>
|
||||
|
||||
<event name="drag_lock_current">
|
||||
<description summary="current drag lock state">
|
||||
Current drag lock state.
|
||||
</description>
|
||||
<arg name="state" type="uint" enum="drag_lock_state"/>
|
||||
</event>
|
||||
|
||||
<request name="set_drag_lock">
|
||||
<description summary="set drag lock state">
|
||||
Configure drag-lock during tapping on this device. When enabled, a
|
||||
finger may be lifted and put back on the touchpad and the drag process
|
||||
continues. A timeout for lifting the finger is optional. When disabled,
|
||||
lifting the finger during a tap-and-drag will immediately stop the drag.
|
||||
See the libinput documentation for more details.
|
||||
</description>
|
||||
<arg name="result" type="new_id" interface="river_libinput_result_v1"/>
|
||||
<arg name="state" type="uint" enum="drag_lock_state"/>
|
||||
</request>
|
||||
|
||||
<event name="three_finger_drag_support">
|
||||
<description summary="three finger drag support">
|
||||
The number of fingers supported for three/four finger drag.
|
||||
If finger_count is less than 3, three finger drag is unsupported.
|
||||
</description>
|
||||
<arg name="finger_count" type="int"/>
|
||||
</event>
|
||||
|
||||
<enum name="three_finger_drag_state">
|
||||
<entry name="disabled" value="0"/>
|
||||
<entry name="enabled_3fg" value="1"/>
|
||||
<entry name="enabled_4fg" value="2"/>
|
||||
</enum>
|
||||
|
||||
<event name="three_finger_drag_default">
|
||||
<description summary="default three finger drag state">
|
||||
Default three finger drag state.
|
||||
</description>
|
||||
<arg name="state" type="uint" enum="three_finger_drag_state"/>
|
||||
</event>
|
||||
|
||||
<event name="three_finger_drag_current">
|
||||
<description summary="current three finger drag state">
|
||||
Current three finger drag state.
|
||||
</description>
|
||||
<arg name="state" type="uint" enum="three_finger_drag_state"/>
|
||||
</event>
|
||||
|
||||
<request name="set_three_finger_drag">
|
||||
<description summary="set three finger drag state">
|
||||
Configure three finger drag functionality for the device.
|
||||
</description>
|
||||
<arg name="result" type="new_id" interface="river_libinput_result_v1"/>
|
||||
<arg name="state" type="uint" enum="three_finger_drag_state"/>
|
||||
</request>
|
||||
|
||||
<event name="calibration_matrix_support">
|
||||
<description summary="support for a calibration matrix">
|
||||
A calibration matrix is supported if the supported argument is non-zero.
|
||||
</description>
|
||||
<arg name="supported" type="int" summary="boolean"/>
|
||||
</event>
|
||||
|
||||
<event name="calibration_matrix_default">
|
||||
<description summary="default calibration matrix">
|
||||
Default calibration matrix.
|
||||
</description>
|
||||
<arg name="matrix" type="array" summary="array of 6 floats"/>
|
||||
</event>
|
||||
|
||||
<event name="calibration_matrix_current">
|
||||
<description summary="current calibration matrix">
|
||||
Current calibration matrix.
|
||||
</description>
|
||||
<arg name="matrix" type="array" summary="array of 6 floats"/>
|
||||
</event>
|
||||
|
||||
<request name="set_calibration_matrix">
|
||||
<description summary="set calibration matrix">
|
||||
Set calibration matrix.
|
||||
</description>
|
||||
<arg name="result" type="new_id" interface="river_libinput_result_v1"/>
|
||||
<arg name="matrix" type="array" summary="array of 6 floats"/>
|
||||
</request>
|
||||
|
||||
<enum name="accel_profile">
|
||||
<entry name="none" value="0"/>
|
||||
<entry name="flat" value="1"/>
|
||||
<entry name="adaptive" value="2"/>
|
||||
<entry name="custom" value="4"/>
|
||||
</enum>
|
||||
|
||||
<enum name="accel_profiles" bitfield="true">
|
||||
<entry name="none" value="0"/>
|
||||
<entry name="flat" value="1"/>
|
||||
<entry name="adaptive" value="2"/>
|
||||
<entry name="custom" value="4"/>
|
||||
</enum>
|
||||
|
||||
<event name="accel_profiles_support">
|
||||
<description summary="supported acceleration profiles">
|
||||
Supported acceleration profiles.
|
||||
</description>
|
||||
<arg name="profiles" type="uint" enum="accel_profiles"/>
|
||||
</event>
|
||||
|
||||
<event name="accel_profile_default">
|
||||
<description summary="default acceleration profile">
|
||||
Default acceleration profile.
|
||||
</description>
|
||||
<arg name="profile" type="uint" enum="accel_profile"/>
|
||||
</event>
|
||||
|
||||
<event name="accel_profile_current">
|
||||
<description summary="current send events mode">
|
||||
Current acceleration profile.
|
||||
</description>
|
||||
<arg name="profile" type="uint" enum="accel_profile"/>
|
||||
</event>
|
||||
|
||||
<request name="set_accel_profile">
|
||||
<description summary="set send events mode">
|
||||
Set the acceleration profile.
|
||||
</description>
|
||||
<arg name="result" type="new_id" interface="river_libinput_result_v1"/>
|
||||
<arg name="profile" type="uint" enum="accel_profile"/>
|
||||
</request>
|
||||
|
||||
<event name="accel_speed_default">
|
||||
<description summary="default acceleration speed">
|
||||
Default acceleration speed.
|
||||
</description>
|
||||
<arg name="speed" type="array" summary="double"/>
|
||||
</event>
|
||||
|
||||
<event name="accel_speed_current">
|
||||
<description summary="current acceleration speed">
|
||||
Current acceleration speed.
|
||||
</description>
|
||||
<arg name="speed" type="array" summary="double"/>
|
||||
</event>
|
||||
|
||||
<request name="set_accel_speed">
|
||||
<description summary="set acceleration speed">
|
||||
Set the acceleration speed within a range of [-1, 1], where 0 is
|
||||
the default acceleration for this device, -1 is the slowest acceleration
|
||||
and 1 is the maximum acceleration available on this device.
|
||||
</description>
|
||||
<arg name="result" type="new_id" interface="river_libinput_result_v1"/>
|
||||
<arg name="speed" type="array" summary="double"/>
|
||||
</request>
|
||||
|
||||
<request name="apply_accel_config">
|
||||
<description summary="apply acceleration config">
|
||||
Apply a pointer accleration config.
|
||||
</description>
|
||||
<arg name="result" type="new_id" interface="river_libinput_result_v1"/>
|
||||
<arg name="config" type="object" interface="river_libinput_accel_config_v1"/>
|
||||
</request>
|
||||
|
||||
<event name="natural_scroll_support">
|
||||
<description summary="support for natural scroll">
|
||||
Natural scroll is supported if the supported argument is non-zero.
|
||||
</description>
|
||||
<arg name="supported" type="int" summary="boolean"/>
|
||||
</event>
|
||||
|
||||
<enum name="natural_scroll_state">
|
||||
<entry name="disabled" value="0"/>
|
||||
<entry name="enabled" value="1"/>
|
||||
</enum>
|
||||
|
||||
<event name="natural_scroll_default">
|
||||
<description summary="default natural scroll">
|
||||
Default natural scroll.
|
||||
</description>
|
||||
<arg name="state" type="uint" enum="natural_scroll_state"/>
|
||||
</event>
|
||||
|
||||
<event name="natural_scroll_current">
|
||||
<description summary="current natural scroll state">
|
||||
Current natural scroll.
|
||||
</description>
|
||||
<arg name="state" type="uint" enum="natural_scroll_state"/>
|
||||
</event>
|
||||
|
||||
<request name="set_natural_scroll">
|
||||
<description summary="set natural scroll state">
|
||||
Set natural scroll state.
|
||||
</description>
|
||||
<arg name="result" type="new_id" interface="river_libinput_result_v1"/>
|
||||
<arg name="state" type="uint" enum="natural_scroll_state"/>
|
||||
</request>
|
||||
|
||||
<event name="left_handed_support">
|
||||
<description summary="support for left-handed mode">
|
||||
Left-handed mode is supported if the supported argument is non-zero.
|
||||
</description>
|
||||
<arg name="supported" type="int" summary="boolean"/>
|
||||
</event>
|
||||
|
||||
<enum name="left_handed_state">
|
||||
<entry name="disabled" value="0"/>
|
||||
<entry name="enabled" value="1"/>
|
||||
</enum>
|
||||
|
||||
<event name="left_handed_default">
|
||||
<description summary="default left-handed mode">
|
||||
Default left-handed mode.
|
||||
</description>
|
||||
<arg name="state" type="uint" enum="left_handed_state"/>
|
||||
</event>
|
||||
|
||||
<event name="left_handed_current">
|
||||
<description summary="current left-handed mode state">
|
||||
Current left-handed mode.
|
||||
</description>
|
||||
<arg name="state" type="uint" enum="left_handed_state"/>
|
||||
</event>
|
||||
|
||||
<request name="set_left_handed">
|
||||
<description summary="set left-handed mode state">
|
||||
Set left-handed mode state.
|
||||
</description>
|
||||
<arg name="result" type="new_id" interface="river_libinput_result_v1"/>
|
||||
<arg name="state" type="uint" enum="left_handed_state"/>
|
||||
</request>
|
||||
|
||||
<enum name="click_method">
|
||||
<entry name="none" value="0"/>
|
||||
<entry name="button_areas" value="1"/>
|
||||
<entry name="clickfinger" value="2"/>
|
||||
</enum>
|
||||
|
||||
<enum name="click_methods" bitfield="true">
|
||||
<entry name="none" value="0"/>
|
||||
<entry name="button_areas" value="1"/>
|
||||
<entry name="clickfinger" value="2"/>
|
||||
</enum>
|
||||
|
||||
<event name="click_method_support">
|
||||
<description summary="supported click methods">
|
||||
The click methods supported by the device.
|
||||
</description>
|
||||
<arg name="methods" type="uint" enum="click_methods"/>
|
||||
</event>
|
||||
|
||||
<event name="click_method_default">
|
||||
<description summary="default click method">
|
||||
Default click method.
|
||||
</description>
|
||||
<arg name="method" type="uint" enum="click_method"/>
|
||||
</event>
|
||||
|
||||
<event name="click_method_current">
|
||||
<description summary="current click method">
|
||||
Current click method.
|
||||
</description>
|
||||
<arg name="method" type="uint" enum="click_method"/>
|
||||
</event>
|
||||
|
||||
<request name="set_click_method">
|
||||
<description summary="set click method">
|
||||
Set click method.
|
||||
</description>
|
||||
<arg name="result" type="new_id" interface="river_libinput_result_v1"/>
|
||||
<arg name="method" type="uint" enum="click_method"/>
|
||||
</request>
|
||||
|
||||
<enum name="clickfinger_button_map">
|
||||
<entry name="lrm" value="0"/>
|
||||
<entry name="lmr" value="1"/>
|
||||
</enum>
|
||||
|
||||
<event name="clickfinger_button_map_default">
|
||||
<description summary="default clickfinger button map">
|
||||
Default clickfinger button map.
|
||||
Supported if click_methods.clickfinger is supported.
|
||||
</description>
|
||||
<arg name="button_map" type="uint" enum="clickfinger_button_map"/>
|
||||
</event>
|
||||
|
||||
<event name="clickfinger_button_map_current">
|
||||
<description summary="current clickfinger button map">
|
||||
Current clickfinger button map.
|
||||
Supported if click_methods.clickfinger is supported.
|
||||
</description>
|
||||
<arg name="button_map" type="uint" enum="clickfinger_button_map"/>
|
||||
</event>
|
||||
|
||||
<request name="set_clickfinger_button_map">
|
||||
<description summary="set clickfinger button map">
|
||||
Set clickfinger button map.
|
||||
Supported if click_methods.clickfinger is supported.
|
||||
</description>
|
||||
<arg name="result" type="new_id" interface="river_libinput_result_v1"/>
|
||||
<arg name="button_map" type="uint" enum="clickfinger_button_map"/>
|
||||
</request>
|
||||
|
||||
<event name="middle_emulation_support">
|
||||
<description summary="support for middle mouse button emulation">
|
||||
Middle mouse button emulation is supported if the supported argument is
|
||||
non-zero.
|
||||
</description>
|
||||
<arg name="supported" type="int" summary="boolean"/>
|
||||
</event>
|
||||
|
||||
<enum name="middle_emulation_state">
|
||||
<entry name="disabled" value="0"/>
|
||||
<entry name="enabled" value="1"/>
|
||||
</enum>
|
||||
|
||||
<event name="middle_emulation_default">
|
||||
<description summary="default middle mouse button emulation">
|
||||
Default middle mouse button emulation.
|
||||
</description>
|
||||
<arg name="state" type="uint" enum="middle_emulation_state"/>
|
||||
</event>
|
||||
|
||||
<event name="middle_emulation_current">
|
||||
<description summary="current middle mouse button emulation state">
|
||||
Current middle mouse button emulation.
|
||||
</description>
|
||||
<arg name="state" type="uint" enum="middle_emulation_state"/>
|
||||
</event>
|
||||
|
||||
<request name="set_middle_emulation">
|
||||
<description summary="set middle mouse button emulation state">
|
||||
Set middle mouse button emulation state.
|
||||
</description>
|
||||
<arg name="result" type="new_id" interface="river_libinput_result_v1"/>
|
||||
<arg name="state" type="uint" enum="middle_emulation_state"/>
|
||||
</request>
|
||||
|
||||
<enum name="scroll_method">
|
||||
<entry name="no_scroll" value="0"/>
|
||||
<entry name="two_finger" value="1"/>
|
||||
<entry name="edge" value="2"/>
|
||||
<entry name="on_button_down" value="4"/>
|
||||
</enum>
|
||||
|
||||
<enum name="scroll_methods" bitfield="true">
|
||||
<entry name="no_scroll" value="0"/>
|
||||
<entry name="two_finger" value="1"/>
|
||||
<entry name="edge" value="2"/>
|
||||
<entry name="on_button_down" value="4"/>
|
||||
</enum>
|
||||
|
||||
<event name="scroll_method_support">
|
||||
<description summary="supported scroll methods">
|
||||
The scroll methods supported by the device.
|
||||
</description>
|
||||
<arg name="methods" type="uint" enum="scroll_methods"/>
|
||||
</event>
|
||||
|
||||
<event name="scroll_method_default">
|
||||
<description summary="default scroll method">
|
||||
Default scroll method.
|
||||
</description>
|
||||
<arg name="method" type="uint" enum="scroll_method"/>
|
||||
</event>
|
||||
|
||||
<event name="scroll_method_current">
|
||||
<description summary="current scroll method">
|
||||
Current scroll method.
|
||||
</description>
|
||||
<arg name="method" type="uint" enum="scroll_method"/>
|
||||
</event>
|
||||
|
||||
<request name="set_scroll_method">
|
||||
<description summary="set scroll method">
|
||||
Set scroll method.
|
||||
</description>
|
||||
<arg name="result" type="new_id" interface="river_libinput_result_v1"/>
|
||||
<arg name="method" type="uint" enum="scroll_method"/>
|
||||
</request>
|
||||
|
||||
<event name="scroll_button_default">
|
||||
<description summary="default scroll button">
|
||||
Default scroll button.
|
||||
Supported if scroll_methods.on_button_down is supported.
|
||||
</description>
|
||||
<arg name="button" type="uint"/>
|
||||
</event>
|
||||
|
||||
<event name="scroll_button_current">
|
||||
<description summary="current scroll button">
|
||||
Current scroll button.
|
||||
Supported if scroll_methods.on_button_down is supported.
|
||||
</description>
|
||||
<arg name="button" type="uint"/>
|
||||
</event>
|
||||
|
||||
<request name="set_scroll_button">
|
||||
<description summary="set scroll button">
|
||||
Set scroll button.
|
||||
Supported if scroll_methods.on_button_down is supported.
|
||||
</description>
|
||||
<arg name="result" type="new_id" interface="river_libinput_result_v1"/>
|
||||
<arg name="button" type="uint"/>
|
||||
</request>
|
||||
|
||||
<enum name="scroll_button_lock_state">
|
||||
<entry name="disabled" value="0"/>
|
||||
<entry name="enabled" value="1"/>
|
||||
</enum>
|
||||
|
||||
<event name="scroll_button_lock_default">
|
||||
<description summary="default scroll button lock state">
|
||||
Default scroll button lock state.
|
||||
Supported if scroll_methods.on_button_down is supported.
|
||||
</description>
|
||||
<arg name="state" type="uint" enum="scroll_button_lock_state"/>
|
||||
</event>
|
||||
|
||||
<event name="scroll_button_lock_current">
|
||||
<description summary="current scroll button lock state">
|
||||
Current scroll button lock state.
|
||||
Supported if scroll_methods.on_button_down is supported.
|
||||
</description>
|
||||
<arg name="state" type="uint" enum="scroll_button_lock_state"/>
|
||||
</event>
|
||||
|
||||
<request name="set_scroll_button_lock">
|
||||
<description summary="set scroll button lock state">
|
||||
Set scroll button lock state.
|
||||
Supported if scroll_methods.on_button_down is supported.
|
||||
</description>
|
||||
<arg name="result" type="new_id" interface="river_libinput_result_v1"/>
|
||||
<arg name="state" type="uint" enum="scroll_button_lock_state"/>
|
||||
</request>
|
||||
|
||||
<event name="dwt_support">
|
||||
<description summary="support for disable-while-typing">
|
||||
Disable-while-typing is supported if the supported argument is
|
||||
non-zero.
|
||||
</description>
|
||||
<arg name="supported" type="int" summary="boolean"/>
|
||||
</event>
|
||||
|
||||
<enum name="dwt_state">
|
||||
<entry name="disabled" value="0"/>
|
||||
<entry name="enabled" value="1"/>
|
||||
</enum>
|
||||
|
||||
<event name="dwt_default">
|
||||
<description summary="default disable-while-typing state">
|
||||
Default disable-while-typing state.
|
||||
</description>
|
||||
<arg name="state" type="uint" enum="dwt_state"/>
|
||||
</event>
|
||||
|
||||
<event name="dwt_current">
|
||||
<description summary="current disable-while-typing state">
|
||||
Current disable-while-typing state.
|
||||
</description>
|
||||
<arg name="state" type="uint" enum="dwt_state"/>
|
||||
</event>
|
||||
|
||||
<request name="set_dwt">
|
||||
<description summary="set disable-while-typing state">
|
||||
Set disable-while-typing state.
|
||||
</description>
|
||||
<arg name="result" type="new_id" interface="river_libinput_result_v1"/>
|
||||
<arg name="state" type="uint" enum="dwt_state"/>
|
||||
</request>
|
||||
|
||||
<event name="dwtp_support">
|
||||
<description summary="support for disable-while-trackpointing">
|
||||
Disable-while-trackpointing is supported if the supported argument is
|
||||
non-zero.
|
||||
</description>
|
||||
<arg name="supported" type="int" summary="boolean"/>
|
||||
</event>
|
||||
|
||||
<enum name="dwtp_state">
|
||||
<entry name="disabled" value="0"/>
|
||||
<entry name="enabled" value="1"/>
|
||||
</enum>
|
||||
|
||||
<event name="dwtp_default">
|
||||
<description summary="default disable-while-trackpointing state">
|
||||
Default disable-while-trackpointing state.
|
||||
</description>
|
||||
<arg name="state" type="uint" enum="dwtp_state"/>
|
||||
</event>
|
||||
|
||||
<event name="dwtp_current">
|
||||
<description summary="current disable-while-trackpointing state">
|
||||
Current disable-while-trackpointing state.
|
||||
</description>
|
||||
<arg name="state" type="uint" enum="dwtp_state"/>
|
||||
</event>
|
||||
|
||||
<request name="set_dwtp">
|
||||
<description summary="set disable-while-trackpointing state">
|
||||
Set disable-while-trackpointing state.
|
||||
</description>
|
||||
<arg name="result" type="new_id" interface="river_libinput_result_v1"/>
|
||||
<arg name="state" type="uint" enum="dwtp_state"/>
|
||||
</request>
|
||||
|
||||
<event name="rotation_support">
|
||||
<description summary="support for rotation">
|
||||
Rotation is supported if the supported argument is non-zero.
|
||||
</description>
|
||||
<arg name="supported" type="int" summary="boolean"/>
|
||||
</event>
|
||||
|
||||
<event name="rotation_default">
|
||||
<description summary="default rotation angle">
|
||||
Default rotation angle.
|
||||
</description>
|
||||
<arg name="angle" type="uint"/>
|
||||
</event>
|
||||
|
||||
<event name="rotation_current">
|
||||
<description summary="current rotation angle">
|
||||
Current rotation angle.
|
||||
</description>
|
||||
<arg name="angle" type="uint"/>
|
||||
</event>
|
||||
|
||||
<request name="set_rotation">
|
||||
<description summary="set rotation angle">
|
||||
Set rotation angle in degrees clockwise off the logical neutral
|
||||
position. Angle must be in the range [0-360).
|
||||
</description>
|
||||
<arg name="result" type="new_id" interface="river_libinput_result_v1"/>
|
||||
<arg name="angle" type="uint"/>
|
||||
</request>
|
||||
</interface>
|
||||
|
||||
<interface name="river_libinput_accel_config_v1" version="1">
|
||||
<description summary="acceleration config">
|
||||
The result returned by libinput on setting configuration for a device.
|
||||
</description>
|
||||
|
||||
<enum name="error">
|
||||
<entry name="invalid_arg" value="0"
|
||||
summary="invalid enum value or similar"/>
|
||||
</enum>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the accel object">
|
||||
This request indicates that the client will no longer use the accel
|
||||
config object and that it may be safely destroyed.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<enum name="accel_type">
|
||||
<entry name="fallback" value="0"/>
|
||||
<entry name="motion" value="1"/>
|
||||
<entry name="scroll" value="2"/>
|
||||
</enum>
|
||||
|
||||
<request name="set_points">
|
||||
<description summary="define custom acceleration function">
|
||||
Defines the acceleration function for a given movement type
|
||||
in an acceleration configuration with custom accel profile.
|
||||
</description>
|
||||
<arg name="result" type="new_id" interface="river_libinput_result_v1"/>
|
||||
<arg name="type" type="uint" enum="accel_type"/>
|
||||
<arg name="step" type="array" summary="double"/>
|
||||
<arg name="points" type="array" summary="array of doubles"/>
|
||||
</request>
|
||||
</interface>
|
||||
|
||||
<interface name="river_libinput_result_v1" version="1">
|
||||
<description summary="config application result">
|
||||
The result returned by libinput on setting configuration for a device.
|
||||
</description>
|
||||
|
||||
<event name="success" type="destructor">
|
||||
<description summary="config success">
|
||||
The configuration was successfully applied to the device.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<event name="unsupported" type="destructor">
|
||||
<description summary="config unsupported">
|
||||
The configuration is unsupported by the device and was ignored.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<event name="invalid" type="destructor">
|
||||
<description summary="config invalid">
|
||||
The configuration is invalid and was ignored.
|
||||
</description>
|
||||
</event>
|
||||
</interface>
|
||||
</protocol>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,314 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<protocol name="river_xkb_bindings_v1">
|
||||
<copyright>
|
||||
SPDX-FileCopyrightText: © 2025 Isaac Freund
|
||||
SPDX-License-Identifier: MIT
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
</copyright>
|
||||
|
||||
<description summary="xkbcommon-based key bindings">
|
||||
This protocol allows the river-window-management-v1 window manager to
|
||||
define key bindings in terms of xkbcommon keysyms and other configurable
|
||||
properties.
|
||||
|
||||
The key words "must", "must not", "required", "shall", "shall not",
|
||||
"should", "should not", "recommended", "may", and "optional" in this
|
||||
document are to be interpreted as described in IETF RFC 2119.
|
||||
</description>
|
||||
|
||||
<interface name="river_xkb_bindings_v1" version="3">
|
||||
<description summary="xkbcommon bindings global interface">
|
||||
This global interface should only be advertised to the client if the
|
||||
river_window_manager_v1 global is also advertised.
|
||||
</description>
|
||||
|
||||
<enum name="error" since="2">
|
||||
<entry name="object_already_created" value="0" since="2"/>
|
||||
</enum>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the river_xkb_bindings_v1 object">
|
||||
This request indicates that the client will no longer use the
|
||||
river_xkb_bindings_v1 object.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="get_xkb_binding">
|
||||
<description summary="define a new xkbcommon key binding">
|
||||
Define a key binding for the given seat in terms of an xkbcommon keysym
|
||||
and other configurable properties.
|
||||
|
||||
The new key binding is not enabled until initial configuration is
|
||||
completed and the enable request is made during a manage sequence.
|
||||
</description>
|
||||
<arg name="seat" type="object" interface="river_seat_v1"/>
|
||||
<arg name="id" type="new_id" interface="river_xkb_binding_v1"/>
|
||||
<arg name="keysym" type="uint" summary="an xkbcommon keysym"/>
|
||||
<arg name="modifiers" type="uint" enum="river_seat_v1.modifiers"/>
|
||||
</request>
|
||||
|
||||
<request name="get_seat" since="2">
|
||||
<description summary="manage seat-specific state">
|
||||
Create an object to manage seat-specific xkb bindings state.
|
||||
|
||||
It is a protocol error to make this request more than once for a given
|
||||
river_seat_v1 object.
|
||||
</description>
|
||||
<arg name="id" type="new_id" interface="river_xkb_bindings_seat_v1"/>
|
||||
<arg name="seat" type="object" interface="river_seat_v1"/>
|
||||
</request>
|
||||
</interface>
|
||||
|
||||
<interface name="river_xkb_binding_v1" version="3">
|
||||
<description summary="configure a xkb key binding, receive trigger events">
|
||||
This object allows the window manager to configure a xkbcommon key binding
|
||||
and receive events when the key binding is triggered.
|
||||
|
||||
The new key binding is not enabled until the enable request is made during
|
||||
a manage sequence.
|
||||
|
||||
Normally, all key events are sent to the surface with keyboard focus by
|
||||
the compositor. Key events that trigger a key binding are not sent to the
|
||||
surface with keyboard focus.
|
||||
|
||||
If multiple key bindings would be triggered by a single physical key event
|
||||
on the compositor side, it is compositor policy which key binding(s) will
|
||||
receive press/release events or if all of the matched key bindings receive
|
||||
press/release events.
|
||||
|
||||
Key bindings might be matched by the same physical key event due to shared
|
||||
keysym and modifiers. The layout override feature may also cause the same
|
||||
physical key event to trigger two key bindings with different keysyms and
|
||||
different layout overrides configured.
|
||||
</description>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the xkb binding object">
|
||||
This request indicates that the client will no longer use the xkb key
|
||||
binding object and that it may be safely destroyed.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="set_layout_override">
|
||||
<description summary="override currently active xkb layout">
|
||||
Specify an xkb layout that should be used to translate key events for
|
||||
the purpose of triggering this key binding irrespective of the currently
|
||||
active xkb layout.
|
||||
|
||||
The layout argument is a 0-indexed xkbcommon layout number for the
|
||||
keyboard that generated the key event.
|
||||
|
||||
If this request is never made, the currently active xkb layout of the
|
||||
keyboard that generated the key event will be used.
|
||||
|
||||
This request modifies window management state and may only be made as
|
||||
part of a manage sequence, see the river_window_manager_v1 description.
|
||||
</description>
|
||||
<arg name="layout" type="uint" summary="0-indexed xkbcommon layout"/>
|
||||
</request>
|
||||
|
||||
<request name="enable">
|
||||
<description summary="enable the key binding">
|
||||
This request should be made after all initial configuration has been
|
||||
completed and the window manager wishes the key binding to be able to be
|
||||
triggered.
|
||||
|
||||
This request modifies window management state and may only be made as
|
||||
part of a manage sequence, see the river_window_manager_v1 description.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="disable">
|
||||
<description summary="disable the key binding">
|
||||
This request may be used to temporarily disable the key binding. It may
|
||||
be later re-enabled with the enable request.
|
||||
|
||||
This request modifies window management state and may only be made as
|
||||
part of a manage sequence, see the river_window_manager_v1 description.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<event name="pressed">
|
||||
<description summary="the key triggering the binding has been pressed">
|
||||
This event indicates that the physical key triggering the binding has
|
||||
been pressed.
|
||||
|
||||
This event will be followed by a manage_start event after all other new
|
||||
state has been sent by the server.
|
||||
|
||||
The compositor should wait for the manage sequence to complete before
|
||||
processing further input events. This allows the window manager client
|
||||
to, for example, modify key bindings and keyboard focus without racing
|
||||
against future input events. The window manager should of course respond
|
||||
as soon as possible as the capacity of the compositor to buffer incoming
|
||||
input events is finite.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<event name="released">
|
||||
<description summary="the key triggering the binding has been released">
|
||||
This event indicates that the physical key triggering the binding has
|
||||
been released.
|
||||
|
||||
Releasing the modifiers for the binding without releasing the "main"
|
||||
physical key that produces the bound keysym does not trigger the release
|
||||
event. This event is sent when the "main" key is released, even if the
|
||||
modifiers have changed since the pressed event.
|
||||
|
||||
This event will be followed by a manage_start event after all other new
|
||||
state has been sent by the server.
|
||||
|
||||
The compositor should wait for the manage sequence to complete before
|
||||
processing further input events. This allows the window manager client
|
||||
to, for example, modify key bindings and keyboard focus without racing
|
||||
against future input events. The window manager should of course respond
|
||||
as soon as possible as the capacity of the compositor to buffer incoming
|
||||
input events is finite.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<event name="stop_repeat" since="2">
|
||||
<description summary="repeating should be stopped">
|
||||
This event indicates that repeating should be stopped for the binding if
|
||||
the window manager has been repeating some action since the pressed
|
||||
event.
|
||||
|
||||
This event is generally sent when some other (possible unbound) key is
|
||||
pressed after the pressed event is sent and before the released event
|
||||
is sent for this binding.
|
||||
|
||||
This event will be followed by a manage_start event after all other new
|
||||
state has been sent by the server.
|
||||
</description>
|
||||
</event>
|
||||
</interface>
|
||||
|
||||
<interface name="river_xkb_bindings_seat_v1" version="3">
|
||||
<description summary="xkb bindings seat">
|
||||
This object manages xkb bindings state associated with a specific seat.
|
||||
</description>
|
||||
|
||||
<request name="destroy" type="destructor" since="2">
|
||||
<description summary="destroy the object">
|
||||
This request indicates that the client will no longer use the object and
|
||||
that it may be safely destroyed.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="ensure_next_key_eaten" since="2">
|
||||
<description summary="ensure the next key press event is eaten">
|
||||
Ensure that the next non-modifier key press and corresponding release
|
||||
events for this seat are not sent to the currently focused surface.
|
||||
|
||||
If the next non-modifier key press triggers a binding, the
|
||||
pressed/released events are sent to the river_xkb_binding_v1 object as
|
||||
usual.
|
||||
|
||||
If the next non-modifier key press does not trigger a binding, the
|
||||
ate_unbound_key event is sent instead.
|
||||
|
||||
Rationale: the window manager may wish to implement "chorded"
|
||||
keybindings where triggering a binding activates a "submap" with a
|
||||
different set of keybindings. Without a way to eat the next key
|
||||
press event, there is no good way for the window manager to know that it
|
||||
should error out and exit the submap when a key not bound in the submap
|
||||
is pressed.
|
||||
|
||||
This request modifies window management state and may only be made as
|
||||
part of a manage sequence, see the river_window_manager_v1 description.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="cancel_ensure_next_key_eaten" since="2">
|
||||
<description summary="cancel an ensure_next_key_eaten request">
|
||||
This requests cancels the effect of the latest ensure_next_key_eaten
|
||||
request if no key has been eaten due to the request yet. This request
|
||||
has no effect if a key has already been eaten or no
|
||||
ensure_next_key_eaten was made.
|
||||
|
||||
Rationale: the window manager may wish cancel an uncompleted "chorded"
|
||||
keybinding after a timeout of a few seconds. Note that since this
|
||||
timeout use-case requires the window manager to trigger a manage sequence
|
||||
with the river_window_manager_v1.manage_dirty request it is possible that
|
||||
the ate_unbound_key key event may be sent before the window manager has
|
||||
a chance to make the cancel_ensure_next_key_eaten request.
|
||||
|
||||
This request modifies window management state and may only be made as
|
||||
part of a manage sequence, see the river_window_manager_v1 description.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<event name="ate_unbound_key" since="2">
|
||||
<description summary="an unbound key press event was eaten">
|
||||
An unbound key press event was eaten due to the ensure_next_key_eaten
|
||||
request.
|
||||
|
||||
This event will be followed by a manage_start event after all other new
|
||||
state has been sent by the server.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<request name="modifiers_watch" since="3">
|
||||
<description summary="watch for change in active modifiers">
|
||||
Request that the server send the modifiers_update event whenever a state
|
||||
change occurs for at least one of the modifiers specified by the
|
||||
modifiers argument.
|
||||
|
||||
The window manager should make this request with the modifiers argument
|
||||
set to 0 when it no longer wishes to take action based on a change in
|
||||
modifiers.
|
||||
|
||||
This request modifies window management state and may only be made as
|
||||
part of a manage sequence, see the river_window_manager_v1 description.
|
||||
</description>
|
||||
<arg name="modifiers" type="uint" enum="river_seat_v1.modifiers"/>
|
||||
</request>
|
||||
|
||||
<event name="modifiers_update" since="3">
|
||||
<description summary="active modifiers for the seat changed">
|
||||
The set of currently active modifiers for the seat changed. This event
|
||||
is only sent when there is a change in state for modifiers marked as
|
||||
watched using the modifiers_watch request.
|
||||
|
||||
The old and new arguments convey the set of modifiers active before and
|
||||
after the change. All modifiers are included in the old and new
|
||||
arguments, including modifiers that are not watched.
|
||||
|
||||
Since this event is only sent when there is a change in state for
|
||||
watched modifiers, it follows that at least one watched modifier is
|
||||
active in old but inactive in new or vice-versa.
|
||||
|
||||
This event will be followed by a manage_start event after all other new
|
||||
state has been sent by the server.
|
||||
|
||||
The compositor should wait for the manage sequence to complete before
|
||||
processing further input events. This allows the window manager client
|
||||
to, for example, modify key bindings and keyboard focus without racing
|
||||
against future input events. The window manager should of course respond
|
||||
as soon as possible as the capacity of the compositor to buffer incoming
|
||||
input events is finite.
|
||||
</description>
|
||||
<arg name="old" type="uint" enum="river_seat_v1.modifiers"
|
||||
summary="previously active modifiers"/>
|
||||
<arg name="new" type="uint" enum="river_seat_v1.modifiers"
|
||||
summary="currently active modifiers"/>
|
||||
</event>
|
||||
</interface>
|
||||
</protocol>
|
||||
@@ -0,0 +1,277 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<protocol name="river_xkb_config_v1">
|
||||
<copyright>
|
||||
SPDX-FileCopyrightText: © 2026 Isaac Freund
|
||||
SPDX-License-Identifier: MIT
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
</copyright>
|
||||
|
||||
<description summary="configure xkbcommon keyboards">
|
||||
This protocol allow a client to set the xkbcommon keymap of individual
|
||||
keyboard input devices. It also allows switching between the layouts of a
|
||||
keymap and toggling capslock/numlock state.
|
||||
|
||||
The key words "must", "must not", "required", "shall", "shall not",
|
||||
"should", "should not", "recommended", "may", and "optional" in this
|
||||
document are to be interpreted as described in IETF RFC 2119.
|
||||
</description>
|
||||
|
||||
<interface name="river_xkb_config_v1" version="1">
|
||||
<description summary="xkb config global interface">
|
||||
Global interface for configuring xkb devices.
|
||||
|
||||
This global should only be advertised if river_input_manager_v1 is
|
||||
advertised as well.
|
||||
</description>
|
||||
|
||||
<enum name="error">
|
||||
<entry name="invalid_destroy" value="0"/>
|
||||
<entry name="invalid_format" value="1"/>
|
||||
</enum>
|
||||
|
||||
<request name="stop">
|
||||
<description summary="stop sending events">
|
||||
This request indicates that the client no longer wishes to receive
|
||||
events on this object.
|
||||
|
||||
The Wayland protocol is asynchronous, which means the server may send
|
||||
further events until the stop request is processed. The client must wait
|
||||
for a river_xkb_config_v1.finished event before destroying this object.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<event name="finished">
|
||||
<description summary="the server has finished with the object">
|
||||
This event indicates that the server will send no further events on this
|
||||
object. The client should destroy the object. See
|
||||
river_xkb_config_v1.destroy for more information.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the river_xkb_config_v1 object">
|
||||
This request should be called after the finished event has been received
|
||||
to complete destruction of the object.
|
||||
|
||||
It is a protocol error to make this request before the finished event
|
||||
has been received.
|
||||
|
||||
If a client wishes to destroy this object it should send a
|
||||
river_xkb_config_v1.stop request and wait for a
|
||||
river_xkb_config_v1.finished event. Once the finished event is received
|
||||
it is safe to destroy this object and any other objects created through
|
||||
this interface.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<enum name="keymap_format">
|
||||
<entry name="text_v1" value="1" summary="XKB_KEYMAP_FORMAT_TEXT_V1"/>
|
||||
<entry name="text_v2" value="2" summary="XKB_KEYMAP_FORMAT_TEXT_V2"/>
|
||||
</enum>
|
||||
|
||||
<request name="create_keymap">
|
||||
<description summary="create a keymap object">
|
||||
The server must be able to mmap the fd with MAP_PRIVATE.
|
||||
The server will fstat the fd to obtain the size of the keymap.
|
||||
The client must not modify the contents of the fd after making this request.
|
||||
The client should seal the fd with fcntl.
|
||||
</description>
|
||||
<arg name="id" type="new_id" interface="river_xkb_keymap_v1"/>
|
||||
<arg name="fd" type="fd"/>
|
||||
<arg name="format" type="uint" enum="keymap_format"/>
|
||||
</request>
|
||||
|
||||
<event name="xkb_keyboard">
|
||||
<description summary="new xkb keyboard">
|
||||
A new xkbcommon keyboard has been created. Not every
|
||||
river_input_device_v1 is necessarily an xkbcommon keyboard as well.
|
||||
</description>
|
||||
<arg name="id" type="new_id" interface="river_xkb_keyboard_v1"/>
|
||||
</event>
|
||||
</interface>
|
||||
|
||||
<interface name="river_xkb_keymap_v1" version="1">
|
||||
<description summary="xkbcommon keymap">
|
||||
This object is the result of attempting to create an xkbcommon keymap.
|
||||
</description>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the keymap object">
|
||||
This request indicates that the client will no longer use the keymap
|
||||
object and that it may be safely destroyed.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<event name="success">
|
||||
<description summary="keymap creation succeeded">
|
||||
The keymap object was successfully created and may be used with the
|
||||
river_xkb_keyboard_v1.set_keymap request.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<event name="failure">
|
||||
<description summary="keymap creation failed">
|
||||
The compositor failed to create a keymap from the given parameters.
|
||||
|
||||
It is a protocol error to use this keymap object with
|
||||
river_xkb_keyboard_v1.set_keymap.
|
||||
</description>
|
||||
<arg name="error_msg" type="string"/>
|
||||
</event>
|
||||
</interface>
|
||||
|
||||
<interface name="river_xkb_keyboard_v1" version="1">
|
||||
<description summary="xkbcommon keyboard device">
|
||||
This object represent a physical keyboard which has its configuration and
|
||||
state managed by xkbcommon.
|
||||
</description>
|
||||
|
||||
<enum name="error">
|
||||
<entry name="invalid_keymap" value="0"/>
|
||||
</enum>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the xkb keyboard object">
|
||||
This request indicates that the client will no longer use the keyboard
|
||||
object and that it may be safely destroyed.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<event name="removed">
|
||||
<description summary="the xkb keyboard is removed">
|
||||
This event indicates that the xkb keyboard has been removed.
|
||||
|
||||
The server will send no further events on this object and ignore any
|
||||
request (other than river_xkb_keyboard_v1.destroy) made after this event
|
||||
is sent. The client should destroy this object with the
|
||||
river_xkb_keyboard_v1.destroy request to free up resources.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<event name="input_device">
|
||||
<description summary="corresponding river input device">
|
||||
The river_input_device_v1 corresponding to this xkb keyboard. This event
|
||||
will always be the first event sent on the river_xkb_keyboard_v1 object,
|
||||
and it will be sent exactly once.
|
||||
</description>
|
||||
<arg name="device" type="object" interface="river_input_device_v1"/>
|
||||
</event>
|
||||
|
||||
<request name="set_keymap">
|
||||
<description summary="set the keymap">
|
||||
Set the keymap for the keyboard.
|
||||
|
||||
Setting a keymap will reset all layout/modifier state.
|
||||
|
||||
It is a protocol error to pass a keymap object for which the
|
||||
river_xkb_keymap_v1.success event was not received.
|
||||
</description>
|
||||
<arg name="keymap" type="object" interface="river_xkb_keymap_v1"/>
|
||||
</request>
|
||||
|
||||
<request name="set_layout_by_index">
|
||||
<description summary="set the active layout by index">
|
||||
Set the active layout for the keyboard's keymap. Has no effect if the
|
||||
layout index is out of bounds for the current keymap.
|
||||
</description>
|
||||
<arg name="index" type="int"/>
|
||||
</request>
|
||||
|
||||
<request name="set_layout_by_name">
|
||||
<description summary="set the active layout by name">
|
||||
Set the active layout for the keyboard's keymap. Has no effect if there
|
||||
is no layout with the give name for the keyboard's keymap.
|
||||
</description>
|
||||
<arg name="name" type="string"/>
|
||||
</request>
|
||||
|
||||
<event name="layout">
|
||||
<description summary="currently active layout">
|
||||
The currently active layout index and name. The name arg may be null if
|
||||
the active layout does not have a name.
|
||||
|
||||
This event is sent once when the river_xkb_keyboard_v1 is created and
|
||||
again whenever the layout changes.
|
||||
</description>
|
||||
<arg name="index" type="uint"/>
|
||||
<arg name="name" type="string" allow-null="true"/>
|
||||
</event>
|
||||
|
||||
<request name="capslock_enable">
|
||||
<description summary="enable capslock">
|
||||
Enable capslock for the keyboard.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="capslock_disable">
|
||||
<description summary="disable capslock">
|
||||
Disable capslock for the keyboard.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<event name="capslock_enabled">
|
||||
<description summary="capslock is currently enabled">
|
||||
Capslock is currently enabled for the keyboard.
|
||||
|
||||
This event is sent once when the river_xkb_keyboard_v1 is created and
|
||||
again whenever the capslock state changes.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<event name="capslock_disabled">
|
||||
<description summary="capslock is currently disabled">
|
||||
Capslock is currently disabled for the keyboard.
|
||||
|
||||
This event is sent once when the river_xkb_keyboard_v1 is created and
|
||||
again whenever the capslock state changes.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<request name="numlock_enable">
|
||||
<description summary="enable numlock">
|
||||
Enable numlock for the keyboard.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="numlock_disable">
|
||||
<description summary="disable numlock">
|
||||
Disable numlock for the keyboard.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<event name="numlock_enabled">
|
||||
<description summary="numlock is currently enabled">
|
||||
Numlock is currently enabled for the keyboard.
|
||||
|
||||
This event is sent once when the river_xkb_keyboard_v1 is created and
|
||||
again whenever the numlock state changes.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<event name="numlock_disabled">
|
||||
<description summary="numlock is currently disabled">
|
||||
Numlock is currently disabled for the keyboard.
|
||||
|
||||
This event is sent once when the river_xkb_keyboard_v1 is created and
|
||||
again whenever the numlock state changes.
|
||||
</description>
|
||||
</event>
|
||||
</interface>
|
||||
</protocol>
|
||||
@@ -0,0 +1,115 @@
|
||||
// Singleton service wrapping att_wm's IPC socket.
|
||||
//
|
||||
// One connection does both jobs: it subscribes to state, and commands are
|
||||
// written back down the same socket. att_wm keeps subscribers connected after a
|
||||
// command precisely so a bar does not need a second connection or a process
|
||||
// spawn per click.
|
||||
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Latest state from att_wm, parsed.
|
||||
readonly property var outputs: state.outputs ?? []
|
||||
readonly property int tagCount: state.tag_count ?? 9
|
||||
readonly property var tagNames: state.tag_names ?? []
|
||||
readonly property bool locked: state.locked ?? false
|
||||
readonly property bool connected: link.item ? link.item.connected : false
|
||||
|
||||
// The output the user is currently working on.
|
||||
readonly property var focusedOutput: {
|
||||
for (const o of outputs)
|
||||
if (o.focused) return o;
|
||||
return outputs.length > 0 ? outputs[0] : null;
|
||||
}
|
||||
|
||||
readonly property string focusedTitle: {
|
||||
if (!focusedOutput) return "";
|
||||
for (const w of focusedOutput.windows)
|
||||
if (w.focused) return w.title;
|
||||
return "";
|
||||
}
|
||||
|
||||
property var state: ({})
|
||||
|
||||
/// Look up an output by name, so a multi-monitor bar can show per-screen
|
||||
/// state rather than the focused output's.
|
||||
function outputFor(name) {
|
||||
for (const o of outputs)
|
||||
if (o.name === name) return o;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Send a command. Same grammar as att_wmctl, e.g. send("view 3").
|
||||
function send(command) {
|
||||
if (root.connected) link.item.write(command + "\n");
|
||||
}
|
||||
|
||||
// A tag is "occupied" if some window carries it, mirroring dwm's bar.
|
||||
function tagOccupied(output, index) {
|
||||
return output ? (output.occupied & (1 << index)) !== 0 : false;
|
||||
}
|
||||
|
||||
function tagActive(output, index) {
|
||||
return output ? (output.tags & (1 << index)) !== 0 : false;
|
||||
}
|
||||
|
||||
// A Socket that fails to connect cannot be revived: no state change is
|
||||
// reported and re-asserting `connected` does not make it try again. So the
|
||||
// socket lives in a Loader and a retry means building a new one. This is
|
||||
// what lets the bar be started before att_wm, as `river/init` does.
|
||||
Loader {
|
||||
id: link
|
||||
|
||||
active: true
|
||||
sourceComponent: Socket {
|
||||
// att_wm names its socket after the Wayland display, so several
|
||||
// river sessions can run side by side without colliding.
|
||||
path: {
|
||||
const explicit = Quickshell.env("ATT_WM_SOCKET");
|
||||
if (explicit) return explicit;
|
||||
const dir = Quickshell.env("XDG_RUNTIME_DIR");
|
||||
const display = Quickshell.env("WAYLAND_DISPLAY") || "wayland-0";
|
||||
return `${dir}/att_wm-${display}.sock`;
|
||||
}
|
||||
|
||||
connected: true
|
||||
|
||||
// att_wm replies with a full state object immediately, so there is
|
||||
// no empty-bar flash on connect.
|
||||
onConnectionStateChanged: if (connected) write("subscribe\n")
|
||||
|
||||
parser: SplitParser {
|
||||
onRead: line => {
|
||||
// Command acknowledgements ("ok" / "err ...") share the
|
||||
// stream with state objects; only the latter are JSON.
|
||||
if (!line.startsWith("{")) return;
|
||||
try {
|
||||
root.state = JSON.parse(line);
|
||||
} catch (e) {
|
||||
console.warn("att_wm: bad state line:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// att_wm may be restarted independently of the bar; keep trying.
|
||||
Timer {
|
||||
interval: 1000
|
||||
repeat: true
|
||||
running: !root.connected
|
||||
onTriggered: {
|
||||
// Drop what the dead connection last said, so a reconnect cannot
|
||||
// briefly show state from before the restart.
|
||||
root.state = ({});
|
||||
link.active = false;
|
||||
link.active = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
singleton AttWm 1.0 AttWm.qml
|
||||
@@ -0,0 +1,175 @@
|
||||
// A dwm-style bar for att_wm.
|
||||
//
|
||||
// Run with: quickshell -p /path/to/att_wm/quickshell
|
||||
//
|
||||
// Left click a tag to view it, right click to toggle it into the view,
|
||||
// middle click to move the focused window there. The layout symbol cycles
|
||||
// layouts on click, mirroring dwm's bar.
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
ShellRoot {
|
||||
Variants {
|
||||
// One bar per monitor.
|
||||
model: Quickshell.screens
|
||||
|
||||
PanelWindow {
|
||||
id: bar
|
||||
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
|
||||
// att_wm reads layer-shell exclusive zones and lays windows out in
|
||||
// what is left, so the bar never overlaps a window.
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
implicitHeight: 26
|
||||
exclusiveZone: 26
|
||||
color: "#1a1a1a"
|
||||
|
||||
readonly property var output: AttWm.outputFor(modelData.name)
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
// ── Tags ────────────────────────────────────────────────
|
||||
Repeater {
|
||||
model: AttWm.tagCount
|
||||
|
||||
Rectangle {
|
||||
required property int index
|
||||
|
||||
readonly property bool active: AttWm.tagActive(bar.output, index)
|
||||
readonly property bool occupied: AttWm.tagOccupied(bar.output, index)
|
||||
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: label.implicitWidth + 16
|
||||
color: active ? "#5294e2" : "transparent"
|
||||
|
||||
Text {
|
||||
id: label
|
||||
anchors.centerIn: parent
|
||||
text: AttWm.tagNames[parent.index] ?? (parent.index + 1)
|
||||
color: parent.active ? "#1a1a1a" : (parent.occupied ? "#eeeeee" : "#666666")
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 13
|
||||
font.bold: parent.active
|
||||
}
|
||||
|
||||
// dwm draws a small square in the corner of a tag that
|
||||
// holds windows but is not currently shown.
|
||||
Rectangle {
|
||||
visible: parent.occupied && !parent.active
|
||||
x: 3
|
||||
y: 3
|
||||
width: 4
|
||||
height: 4
|
||||
color: "#5294e2"
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onClicked: event => {
|
||||
const tag = parent.index + 1;
|
||||
if (event.button === Qt.LeftButton)
|
||||
AttWm.send(`view ${tag}`);
|
||||
else if (event.button === Qt.RightButton)
|
||||
AttWm.send(`toggle-view ${tag}`);
|
||||
else
|
||||
AttWm.send(`tag ${tag}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Layout symbol ───────────────────────────────────────
|
||||
Rectangle {
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: layoutLabel.implicitWidth + 16
|
||||
color: "#242424"
|
||||
|
||||
Text {
|
||||
id: layoutLabel
|
||||
anchors.centerIn: parent
|
||||
text: bar.output ? bar.output.layout_symbol : "[]="
|
||||
color: "#bbbbbb"
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 13
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: event => AttWm.send(
|
||||
event.button === Qt.LeftButton ? "cycle-layout next" : "cycle-layout prev")
|
||||
}
|
||||
}
|
||||
|
||||
// ── Focused window title ────────────────────────────────
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 10
|
||||
elide: Text.ElideRight
|
||||
text: AttWm.focusedTitle
|
||||
color: "#dddddd"
|
||||
font.pixelSize: 13
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
// ── Tabs, for the tabbed layout ─────────────────────────
|
||||
//
|
||||
// att_wm draws its own solid-colour tab strip; this shows the
|
||||
// titles alongside it. Set `tabbar_height = 0` in config.zig to
|
||||
// rely on this instead.
|
||||
RowLayout {
|
||||
Layout.fillHeight: true
|
||||
spacing: 2
|
||||
visible: bar.output && bar.output.layout === "tabbed"
|
||||
|
||||
Repeater {
|
||||
model: bar.output
|
||||
? bar.output.windows.filter(w => w.visible && !w.floating)
|
||||
: []
|
||||
|
||||
Rectangle {
|
||||
required property var modelData
|
||||
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: Math.min(160, tabText.implicitWidth + 16)
|
||||
color: modelData.focused ? "#5294e2" : "#2c2c2c"
|
||||
|
||||
Text {
|
||||
id: tabText
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - 12
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: modelData.title || modelData.app_id
|
||||
color: parent.modelData.focused ? "#1a1a1a" : "#cccccc"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Connection indicator ────────────────────────────────
|
||||
Text {
|
||||
Layout.rightMargin: 10
|
||||
visible: !AttWm.connected
|
||||
text: "att_wm ✕"
|
||||
color: "#e06c75"
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run att_wm inside a nested river, as a window in your existing session.
|
||||
#
|
||||
# Nothing is installed and no system rebuild is needed — river's wayland
|
||||
# backend opens a normal window, and att_wm manages what is inside it.
|
||||
#
|
||||
# ./run-nested.sh # just att_wm
|
||||
# ./run-nested.sh --bar # also start the bundled quickshell bar
|
||||
# ./run-nested.sh --term # also open a terminal to look at
|
||||
#
|
||||
# Anything else you pass is run inside the session, e.g.
|
||||
# ./run-nested.sh --bar --term
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
if [[ -z ${WAYLAND_DISPLAY:-} ]]; then
|
||||
echo "error: no WAYLAND_DISPLAY — this needs an existing Wayland session." >&2
|
||||
echo " On a TTY, use the headless backend instead:" >&2
|
||||
echo " WLR_BACKENDS=headless WLR_HEADLESS_OUTPUTS=1 ./run-nested.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
want_bar=0
|
||||
want_term=0
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--bar) want_bar=1 ;;
|
||||
--term) want_term=1 ;;
|
||||
*)
|
||||
echo "unknown option: $arg" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
bin=zig-out/bin
|
||||
if [[ ! -x ${bin}/att_wm ]]; then
|
||||
echo "==> building"
|
||||
./dev.sh bash -c 'zig build'
|
||||
fi
|
||||
bin=$(realpath "${bin}")
|
||||
|
||||
init=$(mktemp)
|
||||
trap 'rm -f "${init}"' EXIT
|
||||
{
|
||||
echo '#!/bin/sh'
|
||||
# river exports the nested display to its children; surface it so you can
|
||||
# point att_wmctl at this instance from another terminal.
|
||||
echo 'echo "" >&2'
|
||||
echo 'echo " nested session is on WAYLAND_DISPLAY=$WAYLAND_DISPLAY" >&2'
|
||||
echo 'echo " drive it with: WAYLAND_DISPLAY=$WAYLAND_DISPLAY '"${bin}"'/att_wmctl state" >&2'
|
||||
echo 'echo "" >&2'
|
||||
[[ ${want_bar} -eq 1 ]] && echo "quickshell -p $(realpath quickshell) &"
|
||||
[[ ${want_term} -eq 1 ]] && echo 'foot &'
|
||||
echo "exec ${bin}/att_wm"
|
||||
} >"${init}"
|
||||
chmod +x "${init}"
|
||||
|
||||
echo "==> starting nested river (close the window to exit)"
|
||||
exec env WLR_BACKENDS=wayland river -no-xwayland -c "${init}"
|
||||
@@ -0,0 +1,755 @@
|
||||
//! Input device configuration: keyboard layout and repeat, and the libinput
|
||||
//! knobs that matter on a touchpad.
|
||||
//!
|
||||
//! Three river globals cooperate here. `river_input_manager_v1` enumerates
|
||||
//! devices and owns the settings river implements itself (key repeat, scroll
|
||||
//! factor). `river_xkb_config_v1` compiles keymaps and assigns them to
|
||||
//! keyboards. `river_libinput_config_v1` exposes libinput's own configuration —
|
||||
//! tap to click and the rest — one object per device that libinput drives.
|
||||
//!
|
||||
//! None of these requests are part of a manage sequence: unlike window state,
|
||||
//! input configuration is not sequenced by river, so it can be sent the moment
|
||||
//! we know what to send.
|
||||
//!
|
||||
//! What the handlers do *not* do is apply settings, because a device's identity
|
||||
//! and its per-protocol objects arrive as separate events. They record what
|
||||
//! arrived and mark us dirty; `flush()` — called once per event loop iteration,
|
||||
//! after a whole batch of events has been dispatched — is the only place that
|
||||
//! matches rules and issues requests. That way it never matters which order the
|
||||
//! events came in.
|
||||
|
||||
const InputManager = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const posix = std.posix;
|
||||
const linux = std.os.linux;
|
||||
|
||||
const wayland = @import("wayland");
|
||||
const wl = wayland.client.wl;
|
||||
const river = wayland.client.river;
|
||||
|
||||
const xkb = @import("xkbcommon");
|
||||
|
||||
const config = @import("config");
|
||||
const input = @import("input");
|
||||
|
||||
const Wm = @import("Wm.zig");
|
||||
const sys = @import("sys.zig");
|
||||
|
||||
const log = std.log.scoped(.input);
|
||||
|
||||
wm: *Wm,
|
||||
|
||||
manager: *river.InputManagerV1,
|
||||
libinput_config: ?*river.LibinputConfigV1 = null,
|
||||
xkb_config: ?*river.XkbConfigV1 = null,
|
||||
|
||||
devices: std.ArrayList(*Device) = .empty,
|
||||
|
||||
/// libinput settings whose result river has yet to report. Tracked only so that
|
||||
/// shutting down mid-flight frees them.
|
||||
pending_results: std.ArrayList(*Result) = .empty,
|
||||
|
||||
/// The keymap compiled from `config.keymap`. river validates it asynchronously,
|
||||
/// so it may only be handed to a keyboard once `success` has arrived.
|
||||
keymap: ?*river.XkbKeymapV1 = null,
|
||||
keymap_state: enum { unset, pending, ready, failed } = .unset,
|
||||
|
||||
/// Some event handler recorded something `flush` has yet to act on.
|
||||
dirty: bool = false,
|
||||
|
||||
/// One input device, and whichever of river's per-device objects have shown up
|
||||
/// for it so far.
|
||||
pub const Device = struct {
|
||||
im: *InputManager,
|
||||
device: *river.InputDeviceV1,
|
||||
|
||||
name: ?[]u8 = null,
|
||||
kind: ?input.Type = null,
|
||||
|
||||
/// Present only for devices libinput drives. river cannot offer these at
|
||||
/// all when it has no access to the hardware, which is the case whenever it
|
||||
/// runs nested inside another compositor.
|
||||
libinput: ?*river.LibinputDeviceV1 = null,
|
||||
/// Present only for keyboards.
|
||||
keyboard: ?*river.XkbKeyboardV1 = null,
|
||||
|
||||
/// The settings living on each object, once sent. Tracked separately
|
||||
/// because the objects appear independently of one another.
|
||||
applied_core: bool = false,
|
||||
applied_libinput: bool = false,
|
||||
applied_keymap: bool = false,
|
||||
|
||||
/// The output this device is currently mapped to, so the request is only
|
||||
/// re-sent when it actually changes. Borrowed, and cleared by `forgetOutput`
|
||||
/// before the proxy is destroyed.
|
||||
mapped_output: ?*wl.Output = null,
|
||||
/// Set once we have complained that a rule names an output that is not here,
|
||||
/// so unplugging a monitor costs one log line rather than one per flush.
|
||||
warned_missing_output: bool = false,
|
||||
|
||||
removed: bool = false,
|
||||
|
||||
fn destroy(self: *Device) void {
|
||||
const gpa = self.im.wm.gpa;
|
||||
if (self.name) |n| gpa.free(n);
|
||||
if (self.libinput) |l| l.destroy();
|
||||
if (self.keyboard) |k| k.destroy();
|
||||
self.device.destroy();
|
||||
gpa.destroy(self);
|
||||
}
|
||||
|
||||
fn displayName(self: *const Device) []const u8 {
|
||||
return self.name orelse "";
|
||||
}
|
||||
};
|
||||
|
||||
pub fn create(wm: *Wm, manager: *river.InputManagerV1) !*InputManager {
|
||||
const self = try wm.gpa.create(InputManager);
|
||||
self.* = .{ .wm = wm, .manager = manager };
|
||||
manager.setListener(*InputManager, onManagerEvent, self);
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *InputManager) void {
|
||||
const gpa = self.wm.gpa;
|
||||
for (self.devices.items) |device| device.destroy();
|
||||
self.devices.deinit(gpa);
|
||||
for (self.pending_results.items) |pending| gpa.destroy(pending);
|
||||
self.pending_results.deinit(gpa);
|
||||
if (self.keymap) |k| k.destroy();
|
||||
if (self.libinput_config) |l| l.destroy();
|
||||
if (self.xkb_config) |x| x.destroy();
|
||||
self.manager.destroy();
|
||||
gpa.destroy(self);
|
||||
}
|
||||
|
||||
/// Bind the two configuration globals, which must happen after
|
||||
/// `river_input_manager_v1` — river only tells us which input device a libinput
|
||||
/// device or xkb keyboard belongs to if we already hold an object for it.
|
||||
pub fn bindConfigGlobals(
|
||||
self: *InputManager,
|
||||
registry: *wl.Registry,
|
||||
libinput_name: ?u32,
|
||||
xkb_name: ?u32,
|
||||
) void {
|
||||
if (libinput_name) |name| {
|
||||
self.libinput_config = registry.bind(name, river.LibinputConfigV1, 1) catch null;
|
||||
if (self.libinput_config) |lc| {
|
||||
lc.setListener(*InputManager, onLibinputConfigEvent, self);
|
||||
}
|
||||
}
|
||||
if (xkb_name) |name| {
|
||||
self.xkb_config = registry.bind(name, river.XkbConfigV1, 1) catch null;
|
||||
if (self.xkb_config) |xc| {
|
||||
xc.setListener(*InputManager, onXkbConfigEvent, self);
|
||||
self.createKeymap(xc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Events ──────────────────────────────────────────────────────────────────
|
||||
|
||||
fn onManagerEvent(_: *river.InputManagerV1, event: river.InputManagerV1.Event, self: *InputManager) void {
|
||||
switch (event) {
|
||||
.input_device => |ev| {
|
||||
const device = self.wm.gpa.create(Device) catch {
|
||||
log.err("out of memory tracking a new input device", .{});
|
||||
ev.id.destroy();
|
||||
return;
|
||||
};
|
||||
device.* = .{ .im = self, .device = ev.id };
|
||||
self.devices.append(self.wm.gpa, device) catch {
|
||||
self.wm.gpa.destroy(device);
|
||||
ev.id.destroy();
|
||||
return;
|
||||
};
|
||||
// Lets the libinput and xkb objects find their way back to us from
|
||||
// the river_input_device_v1 they name.
|
||||
ev.id.setListener(*Device, onDeviceEvent, device);
|
||||
self.dirty = true;
|
||||
},
|
||||
.finished => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn onDeviceEvent(_: *river.InputDeviceV1, event: river.InputDeviceV1.Event, device: *Device) void {
|
||||
switch (event) {
|
||||
.name => |ev| {
|
||||
const gpa = device.im.wm.gpa;
|
||||
if (device.name) |old| gpa.free(old);
|
||||
device.name = gpa.dupe(u8, std.mem.span(ev.name)) catch null;
|
||||
},
|
||||
.type => |ev| device.kind = switch (ev.type) {
|
||||
.keyboard => .keyboard,
|
||||
.pointer => .pointer,
|
||||
.touch => .touch,
|
||||
.tablet => .tablet,
|
||||
// A device type this build of att_wm has never heard of. Leaving the
|
||||
// kind unset means rules that name a type skip it, which is the
|
||||
// conservative reading.
|
||||
_ => null,
|
||||
},
|
||||
.removed => device.removed = true,
|
||||
}
|
||||
device.im.dirty = true;
|
||||
}
|
||||
|
||||
fn onLibinputConfigEvent(
|
||||
_: *river.LibinputConfigV1,
|
||||
event: river.LibinputConfigV1.Event,
|
||||
self: *InputManager,
|
||||
) void {
|
||||
switch (event) {
|
||||
.libinput_device => |ev| {
|
||||
// Which device it belongs to arrives in its own event; park a
|
||||
// listener on it until then.
|
||||
ev.id.setListener(*InputManager, onLibinputDeviceEvent, self);
|
||||
},
|
||||
.finished => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn onLibinputDeviceEvent(
|
||||
proxy: *river.LibinputDeviceV1,
|
||||
event: river.LibinputDeviceV1.Event,
|
||||
self: *InputManager,
|
||||
) void {
|
||||
switch (event) {
|
||||
.input_device => |ev| {
|
||||
const device = deviceFromProxy(ev.device) orelse return;
|
||||
device.libinput = proxy;
|
||||
self.dirty = true;
|
||||
},
|
||||
.removed => {
|
||||
if (self.deviceForLibinput(proxy)) |device| device.libinput = null;
|
||||
proxy.destroy();
|
||||
},
|
||||
// The support, default and current events describe what the device can
|
||||
// do and what it is doing. att_wm states what it wants and lets the
|
||||
// result object report whether the device could oblige, so none of this
|
||||
// needs tracking.
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn onXkbConfigEvent(_: *river.XkbConfigV1, event: river.XkbConfigV1.Event, self: *InputManager) void {
|
||||
switch (event) {
|
||||
.xkb_keyboard => |ev| ev.id.setListener(*InputManager, onXkbKeyboardEvent, self),
|
||||
.finished => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn onXkbKeyboardEvent(
|
||||
proxy: *river.XkbKeyboardV1,
|
||||
event: river.XkbKeyboardV1.Event,
|
||||
self: *InputManager,
|
||||
) void {
|
||||
switch (event) {
|
||||
.input_device => |ev| {
|
||||
const device = deviceFromProxy(ev.device) orelse return;
|
||||
device.keyboard = proxy;
|
||||
self.dirty = true;
|
||||
},
|
||||
.removed => {
|
||||
if (self.deviceForKeyboard(proxy)) |device| device.keyboard = null;
|
||||
proxy.destroy();
|
||||
},
|
||||
// Sent on creation and on every layout switch, so a `grp:` option makes
|
||||
// this routine — hence debug rather than info.
|
||||
.layout => |ev| {
|
||||
const device = self.deviceForKeyboard(proxy);
|
||||
log.debug("layout {d} ({s}) active on {s}", .{
|
||||
ev.index,
|
||||
if (ev.name) |n| std.mem.span(n) else "unnamed",
|
||||
if (device) |d| d.displayName() else "?",
|
||||
});
|
||||
},
|
||||
// Capslock and numlock state; att_wm does not model either.
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
/// setListener stores our pointer as the proxy's user data, which is how an
|
||||
/// event naming a river_input_device_v1 gets back to our own struct.
|
||||
fn deviceFromProxy(proxy: ?*river.InputDeviceV1) ?*Device {
|
||||
const p = proxy orelse return null;
|
||||
return @ptrCast(@alignCast(p.getUserData()));
|
||||
}
|
||||
|
||||
fn deviceForLibinput(self: *InputManager, proxy: *river.LibinputDeviceV1) ?*Device {
|
||||
for (self.devices.items) |device| {
|
||||
if (device.libinput == proxy) return device;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn deviceForKeyboard(self: *InputManager, proxy: *river.XkbKeyboardV1) ?*Device {
|
||||
for (self.devices.items) |device| {
|
||||
if (device.keyboard == proxy) return device;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── Applying configuration ──────────────────────────────────────────────────
|
||||
|
||||
/// Act on everything the handlers have recorded since the last call. Safe to
|
||||
/// call as often as you like; it does nothing unless something changed.
|
||||
pub fn flush(self: *InputManager) void {
|
||||
if (!self.dirty) return;
|
||||
self.dirty = false;
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < self.devices.items.len) {
|
||||
const device = self.devices.items[i];
|
||||
if (device.removed) {
|
||||
_ = self.devices.orderedRemove(i);
|
||||
device.destroy();
|
||||
continue;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
for (self.devices.items) |device| {
|
||||
// Both the name and the type are sent as the device object is created,
|
||||
// so waiting for them costs at most one turn of the event loop, and
|
||||
// matching a rule before they land would match the wrong thing.
|
||||
const kind = device.kind orelse continue;
|
||||
const name = device.name orelse continue;
|
||||
|
||||
const rule = ruleFor(name, kind);
|
||||
|
||||
if (!device.applied_core) {
|
||||
device.applied_core = true;
|
||||
log.info("input device: {s} ({t})", .{ name, kind });
|
||||
self.applyCore(device, kind, rule);
|
||||
}
|
||||
|
||||
if (device.libinput != null and !device.applied_libinput) {
|
||||
device.applied_libinput = true;
|
||||
self.applyLibinput(device, rule);
|
||||
}
|
||||
|
||||
self.applyOutputMapping(device, kind, rule);
|
||||
|
||||
if (device.keyboard) |keyboard| {
|
||||
if (!device.applied_keymap and self.keymap_state == .ready) {
|
||||
device.applied_keymap = true;
|
||||
keyboard.setKeymap(self.keymap.?);
|
||||
log.debug("keymap set on {s}", .{name});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An output has appeared, or one has been named. Either may be the output an
|
||||
/// input rule is waiting for.
|
||||
pub fn outputsChanged(self: *InputManager) void {
|
||||
self.dirty = true;
|
||||
}
|
||||
|
||||
/// An output is going away: drop it from any device mapped to it, so the proxy
|
||||
/// is not remembered past its destruction and the device is mapped afresh should
|
||||
/// the output return.
|
||||
pub fn forgetOutput(self: *InputManager, proxy: *wl.Output) void {
|
||||
for (self.devices.items) |device| {
|
||||
if (device.mapped_output == proxy) device.mapped_output = null;
|
||||
}
|
||||
self.dirty = true;
|
||||
}
|
||||
|
||||
/// Confine a device to one output.
|
||||
///
|
||||
/// Unlike every other setting this is re-evaluated on every flush rather than
|
||||
/// applied once, because outputs come and go — and river drops its own side of
|
||||
/// the mapping when the output named is destroyed, so a monitor that comes back
|
||||
/// has to be mapped again.
|
||||
///
|
||||
/// Rotation needs no such care: wlroots reads the output's transform on each
|
||||
/// input event, so a mapped device follows the display around without anything
|
||||
/// being re-sent.
|
||||
fn applyOutputMapping(self: *InputManager, device: *Device, kind: input.Type, rule: input.Rule) void {
|
||||
const want = rule.map_to_output orelse return;
|
||||
// Keyboards have no coordinates to map, and river ignores the request for
|
||||
// them. Skipping quietly keeps a catch-all rule from being noisy.
|
||||
if (kind == .keyboard) return;
|
||||
|
||||
const proxy = self.wm.wlOutputByName(want) orelse {
|
||||
if (!device.warned_missing_output) {
|
||||
device.warned_missing_output = true;
|
||||
log.warn("cannot map {s} to output {s}: no output by that name", .{
|
||||
device.displayName(),
|
||||
want,
|
||||
});
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
if (device.mapped_output == proxy) return;
|
||||
device.device.mapToOutput(proxy);
|
||||
device.mapped_output = proxy;
|
||||
device.warned_missing_output = false;
|
||||
log.info("{s}: mapped to output {s}", .{ device.displayName(), want });
|
||||
}
|
||||
|
||||
/// Fold every matching rule together, in declaration order, so a later rule can
|
||||
/// override an earlier one field by field.
|
||||
fn ruleFor(name: []const u8, kind: input.Type) input.Rule {
|
||||
var rule: input.Rule = .{};
|
||||
for (config.input_rules) |candidate| {
|
||||
if (candidate.matchesDevice(name, kind)) rule = rule.merge(candidate);
|
||||
}
|
||||
return rule;
|
||||
}
|
||||
|
||||
/// Well beyond anything usable, and comfortably inside what a 24.8 fixed point
|
||||
/// number can hold.
|
||||
const max_scroll_factor = 1000;
|
||||
|
||||
/// The settings river implements itself, on river_input_device_v1.
|
||||
fn applyCore(self: *InputManager, device: *Device, kind: input.Type, rule: input.Rule) void {
|
||||
_ = self;
|
||||
|
||||
if (kind == .keyboard) {
|
||||
const repeat = rule.repeat orelse config.repeat;
|
||||
// Either negative is a protocol error, and river would disconnect us
|
||||
// over a typo in a config file.
|
||||
if (repeat.rate < 0 or repeat.delay < 0) {
|
||||
log.err("ignoring negative key repeat for {s}: rate {d}, delay {d}", .{
|
||||
device.displayName(),
|
||||
repeat.rate,
|
||||
repeat.delay,
|
||||
});
|
||||
} else {
|
||||
device.device.setRepeatInfo(repeat.rate, repeat.delay);
|
||||
log.debug("{s}: repeat rate {d}, delay {d}", .{
|
||||
device.displayName(),
|
||||
repeat.rate,
|
||||
repeat.delay,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (rule.scroll_factor) |factor| {
|
||||
// Likewise a protocol error below zero. The upper bound is ours: the
|
||||
// protocol carries the factor as a 24.8 fixed point number, and
|
||||
// converting something that does not fit is undefined rather than
|
||||
// merely wrong.
|
||||
if (factor < 0 or factor > max_scroll_factor) {
|
||||
log.err("ignoring out of range scroll factor for {s}: {d} (want 0 to {d})", .{
|
||||
device.displayName(),
|
||||
factor,
|
||||
max_scroll_factor,
|
||||
});
|
||||
} else {
|
||||
device.device.setScrollFactor(.fromDouble(factor));
|
||||
log.debug("{s}: scroll factor {d}", .{ device.displayName(), factor });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// libinput's own configuration, on river_libinput_device_v1.
|
||||
///
|
||||
/// Every request here returns a result object reporting whether the device could
|
||||
/// honour it, which is the only way to find out that, say, a mouse has no tap to
|
||||
/// click to enable. `track` attaches the listener that turns that into a log
|
||||
/// line naming the setting.
|
||||
fn applyLibinput(self: *InputManager, device: *Device, rule: input.Rule) void {
|
||||
const li = device.libinput.?;
|
||||
const name = device.displayName();
|
||||
|
||||
if (rule.tap) |on| {
|
||||
self.track(name, "tap", li.setTap(if (on) .enabled else .disabled));
|
||||
}
|
||||
if (rule.tap_button_map) |map| {
|
||||
self.track(name, "tap-button-map", li.setTapButtonMap(switch (map) {
|
||||
.lrm => .lrm,
|
||||
.lmr => .lmr,
|
||||
}));
|
||||
}
|
||||
if (rule.drag) |on| {
|
||||
self.track(name, "drag", li.setDrag(if (on) .enabled else .disabled));
|
||||
}
|
||||
if (rule.drag_lock) |state| {
|
||||
self.track(name, "drag-lock", li.setDragLock(switch (state) {
|
||||
.disabled => .disabled,
|
||||
.timeout => .enabled_timeout,
|
||||
.sticky => .enabled_sticky,
|
||||
}));
|
||||
}
|
||||
if (rule.three_finger_drag) |state| {
|
||||
self.track(name, "three-finger-drag", li.setThreeFingerDrag(switch (state) {
|
||||
.disabled => .disabled,
|
||||
.three_finger => .enabled_3fg,
|
||||
.four_finger => .enabled_4fg,
|
||||
}));
|
||||
}
|
||||
|
||||
if (rule.click_method) |method| {
|
||||
self.track(name, "click-method", li.setClickMethod(switch (method) {
|
||||
.none => .none,
|
||||
.button_areas => .button_areas,
|
||||
.clickfinger => .clickfinger,
|
||||
}));
|
||||
}
|
||||
if (rule.clickfinger_button_map) |map| {
|
||||
self.track(name, "clickfinger-button-map", li.setClickfingerButtonMap(switch (map) {
|
||||
.lrm => .lrm,
|
||||
.lmr => .lmr,
|
||||
}));
|
||||
}
|
||||
if (rule.middle_emulation) |on| {
|
||||
self.track(name, "middle-emulation", li.setMiddleEmulation(if (on) .enabled else .disabled));
|
||||
}
|
||||
if (rule.left_handed) |on| {
|
||||
self.track(name, "left-handed", li.setLeftHanded(if (on) .enabled else .disabled));
|
||||
}
|
||||
|
||||
if (rule.natural_scroll) |on| {
|
||||
self.track(name, "natural-scroll", li.setNaturalScroll(if (on) .enabled else .disabled));
|
||||
}
|
||||
if (rule.scroll_method) |method| {
|
||||
self.track(name, "scroll-method", li.setScrollMethod(switch (method) {
|
||||
.none => .no_scroll,
|
||||
.two_finger => .two_finger,
|
||||
.edge => .edge,
|
||||
.on_button_down => .on_button_down,
|
||||
}));
|
||||
}
|
||||
if (rule.scroll_button) |button| {
|
||||
self.track(name, "scroll-button", li.setScrollButton(button));
|
||||
}
|
||||
if (rule.scroll_button_lock) |on| {
|
||||
self.track(name, "scroll-button-lock", li.setScrollButtonLock(if (on) .enabled else .disabled));
|
||||
}
|
||||
|
||||
if (rule.accel_profile) |profile| {
|
||||
self.track(name, "accel-profile", li.setAccelProfile(switch (profile) {
|
||||
.none => .none,
|
||||
.flat => .flat,
|
||||
.adaptive => .adaptive,
|
||||
}));
|
||||
}
|
||||
if (rule.accel_speed) |speed| {
|
||||
// libinput takes a native-endian double, which the protocol carries as
|
||||
// an array of bytes for want of a floating point argument type.
|
||||
var value = speed;
|
||||
var array: wl.Array = .{
|
||||
.size = @sizeOf(f64),
|
||||
.alloc = @sizeOf(f64),
|
||||
.data = @ptrCast(&value),
|
||||
};
|
||||
self.track(name, "accel-speed", li.setAccelSpeed(&array));
|
||||
}
|
||||
|
||||
if (rule.disable_while_typing) |on| {
|
||||
self.track(name, "disable-while-typing", li.setDwt(if (on) .enabled else .disabled));
|
||||
}
|
||||
if (rule.disable_while_trackpointing) |on| {
|
||||
self.track(name, "disable-while-trackpointing", li.setDwtp(if (on) .enabled else .disabled));
|
||||
}
|
||||
|
||||
if (rule.rotation) |angle| {
|
||||
self.track(name, "rotation", li.setRotation(angle));
|
||||
}
|
||||
|
||||
if (rule.send_events) |mode| {
|
||||
self.track(name, "send-events", li.setSendEvents(switch (mode) {
|
||||
.enabled => .{},
|
||||
.disabled => .{ .disabled = true },
|
||||
.disabled_on_external_mouse => .{ .disabled_on_external_mouse = true },
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/// A pending libinput setting, waiting to hear whether it took.
|
||||
///
|
||||
/// The device name is copied in rather than borrowed: a device can be unplugged
|
||||
/// between the request and the reply, and a diagnostic is not worth a dangling
|
||||
/// slice. `what` is always a literal from `applyLibinput`.
|
||||
const Result = struct {
|
||||
im: *InputManager,
|
||||
what: []const u8,
|
||||
name_buf: [64]u8 = undefined,
|
||||
name_len: usize = 0,
|
||||
|
||||
fn name(self: *const Result) []const u8 {
|
||||
return self.name_buf[0..self.name_len];
|
||||
}
|
||||
};
|
||||
|
||||
fn track(
|
||||
self: *InputManager,
|
||||
device_name: []const u8,
|
||||
what: []const u8,
|
||||
result: anyerror!*river.LibinputResultV1,
|
||||
) void {
|
||||
const object = result catch |err| {
|
||||
log.err("failed to set {s} on {s}: {s}", .{ what, device_name, @errorName(err) });
|
||||
return;
|
||||
};
|
||||
|
||||
const pending = self.wm.gpa.create(Result) catch {
|
||||
// Without the listener we simply never learn the outcome; the setting
|
||||
// itself was still requested.
|
||||
object.destroy();
|
||||
return;
|
||||
};
|
||||
pending.* = .{ .im = self, .what = what };
|
||||
pending.name_len = @min(device_name.len, pending.name_buf.len);
|
||||
@memcpy(pending.name_buf[0..pending.name_len], device_name[0..pending.name_len]);
|
||||
|
||||
self.pending_results.append(self.wm.gpa, pending) catch {
|
||||
self.wm.gpa.destroy(pending);
|
||||
object.destroy();
|
||||
return;
|
||||
};
|
||||
object.setListener(*Result, onResultEvent, pending);
|
||||
}
|
||||
|
||||
fn onResultEvent(
|
||||
object: *river.LibinputResultV1,
|
||||
event: river.LibinputResultV1.Event,
|
||||
pending: *Result,
|
||||
) void {
|
||||
switch (event) {
|
||||
.success => {},
|
||||
.unsupported => log.warn(
|
||||
"{s} does not support {s}; setting ignored",
|
||||
.{ pending.name(), pending.what },
|
||||
),
|
||||
.invalid => log.err(
|
||||
"invalid {s} setting for {s}; setting ignored",
|
||||
.{ pending.what, pending.name() },
|
||||
),
|
||||
}
|
||||
// All three events are destructors, so the object is spent either way.
|
||||
object.destroy();
|
||||
pending.im.forgetResult(pending);
|
||||
}
|
||||
|
||||
fn forgetResult(self: *InputManager, pending: *Result) void {
|
||||
for (self.pending_results.items, 0..) |item, i| {
|
||||
if (item == pending) {
|
||||
_ = self.pending_results.swapRemove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.wm.gpa.destroy(pending);
|
||||
}
|
||||
|
||||
// ─── Keymap ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Compile `config.keymap` and hand it to river.
|
||||
///
|
||||
/// river validates it asynchronously and answers on the keymap object, so
|
||||
/// nothing can be assigned to a keyboard until then; `flush` picks it up once
|
||||
/// `success` arrives.
|
||||
fn createKeymap(self: *InputManager, xkb_config: *river.XkbConfigV1) void {
|
||||
// All-null names are exactly what river compiles by default, so there is
|
||||
// nothing to gain by sending our own.
|
||||
if (comptime config.keymap.isDefault()) return;
|
||||
|
||||
const names: xkb.RuleNames = comptime .{
|
||||
.rules = zeroTerminate(config.keymap.rules),
|
||||
.model = zeroTerminate(config.keymap.model),
|
||||
.layout = zeroTerminate(config.keymap.layout),
|
||||
.variant = zeroTerminate(config.keymap.variant),
|
||||
.options = zeroTerminate(config.keymap.options),
|
||||
};
|
||||
|
||||
const context = xkb.Context.new(.no_flags) orelse {
|
||||
log.err("failed to create an xkb context; keeping river's default keymap", .{});
|
||||
return;
|
||||
};
|
||||
defer xkb_context_unref(context);
|
||||
|
||||
const keymap = xkb.Keymap.newFromNames(context, &names, .no_flags) orelse {
|
||||
log.err("failed to compile keymap (layout {s}, variant {s}, options {s})", .{
|
||||
config.keymap.layout orelse "default",
|
||||
config.keymap.variant orelse "default",
|
||||
config.keymap.options orelse "none",
|
||||
});
|
||||
return;
|
||||
};
|
||||
defer keymap.unref();
|
||||
|
||||
const text = keymap.getAsString(.text_v1) orelse {
|
||||
log.err("failed to serialise the compiled keymap", .{});
|
||||
return;
|
||||
};
|
||||
defer std.c.free(text);
|
||||
|
||||
const fd = keymapFd(std.mem.span(text)) catch |err| {
|
||||
log.err("failed to stage the keymap for river: {s}", .{@errorName(err)});
|
||||
return;
|
||||
};
|
||||
defer sys.close(fd);
|
||||
|
||||
self.keymap = xkb_config.createKeymap(fd, .text_v1) catch |err| {
|
||||
log.err("failed to send the keymap to river: {s}", .{@errorName(err)});
|
||||
return;
|
||||
};
|
||||
self.keymap.?.setListener(*InputManager, onKeymapEvent, self);
|
||||
self.keymap_state = .pending;
|
||||
}
|
||||
|
||||
/// Put a keymap in a sealed memfd for river to mmap.
|
||||
///
|
||||
/// The trailing NUL goes in the file: river sizes the keymap as the file length
|
||||
/// minus one and requires the content to be zero terminated.
|
||||
fn keymapFd(text: []const u8) !sys.fd_t {
|
||||
const fd = try posix.memfd_create(
|
||||
"att_wm-keymap",
|
||||
linux.MFD.CLOEXEC | linux.MFD.ALLOW_SEALING,
|
||||
);
|
||||
errdefer sys.close(fd);
|
||||
|
||||
var written: usize = 0;
|
||||
while (written < text.len) {
|
||||
written += try sys.write(fd, text[written..]);
|
||||
}
|
||||
if (try sys.write(fd, &.{0}) != 1) return error.ShortWrite;
|
||||
|
||||
// Sealing tells river the bytes cannot change under its mmap. Only a
|
||||
// courtesy — it maps the fd read-only and privately either way — so a
|
||||
// kernel that refuses is no reason to give up on the keymap.
|
||||
sys.addSeals(fd, linux.F.SEAL_SHRINK | linux.F.SEAL_GROW |
|
||||
linux.F.SEAL_WRITE | linux.F.SEAL_SEAL) catch {};
|
||||
|
||||
return fd;
|
||||
}
|
||||
|
||||
fn onKeymapEvent(_: *river.XkbKeymapV1, event: river.XkbKeymapV1.Event, self: *InputManager) void {
|
||||
switch (event) {
|
||||
.success => {
|
||||
// Worth saying out loud: a rejected keymap leaves every keyboard on
|
||||
// river's default, and the symptom is simply that the configured
|
||||
// layout is not the one typing produces.
|
||||
log.info("river accepted the keymap (layout {s}, variant {s}, options {s})", .{
|
||||
config.keymap.layout orelse "default",
|
||||
config.keymap.variant orelse "default",
|
||||
config.keymap.options orelse "none",
|
||||
});
|
||||
self.keymap_state = .ready;
|
||||
self.dirty = true;
|
||||
},
|
||||
.failure => |ev| {
|
||||
log.err("river rejected the keymap: {s}", .{std.mem.span(ev.error_msg)});
|
||||
self.keymap_state = .failed;
|
||||
if (self.keymap) |k| k.destroy();
|
||||
self.keymap = null;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Turn a comptime config string into the NUL terminated one xkbcommon wants.
|
||||
fn zeroTerminate(comptime s: ?[]const u8) ?[*:0]const u8 {
|
||||
const value = s orelse return null;
|
||||
return (value ++ "\x00")[0..value.len :0].ptr;
|
||||
}
|
||||
|
||||
/// zig-xkbcommon 0.4.0 aliases `Context.unref` to `xkb_rmlvo_builder_unref`,
|
||||
/// which would hand a context to the wrong destructor. Declared here so we call
|
||||
/// the right one.
|
||||
extern fn xkb_context_unref(context: *xkb.Context) void;
|
||||
+297
@@ -0,0 +1,297 @@
|
||||
//! A logical output, and the window management state that dwm keeps per
|
||||
//! monitor: the visible tag set, the layout, nmaster and mfact.
|
||||
//!
|
||||
//! The arrangement settings are stored per tag rather than per output, as
|
||||
//! dwm's pertag patch does, so switching tags restores the layout that tag was
|
||||
//! last arranged with.
|
||||
|
||||
const Output = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const wayland = @import("wayland");
|
||||
const wl = wayland.client.wl;
|
||||
const river = wayland.client.river;
|
||||
|
||||
const config = @import("config");
|
||||
const action = @import("action");
|
||||
|
||||
const Wm = @import("Wm.zig");
|
||||
const Window = @import("Window.zig");
|
||||
const shm = @import("shm.zig");
|
||||
const color = @import("color.zig");
|
||||
const layout = @import("layout.zig");
|
||||
const Box = layout.Box;
|
||||
|
||||
wm: *Wm,
|
||||
output: *river.OutputV1,
|
||||
layer_output: ?*river.LayerShellOutputV1 = null,
|
||||
|
||||
/// The global name of the corresponding wl_output, used to pair the two up.
|
||||
wl_output_name: u32 = 0,
|
||||
/// Owned by Wm.wl_outputs; holds the human readable output name.
|
||||
wl_output: ?*Wm.WlOutput = null,
|
||||
|
||||
/// Full output area in the compositor's logical coordinate space.
|
||||
box: Box = .{},
|
||||
/// The part of `box` not covered by layer-shell exclusive zones. Windows are
|
||||
/// laid out here so bars are not overlapped.
|
||||
usable: Box = .{},
|
||||
/// Whether `usable` has been reported; before that it tracks `box`.
|
||||
have_usable: bool = false,
|
||||
|
||||
tags: u32 = config.default_tags,
|
||||
prev_tags: u32 = config.default_tags,
|
||||
|
||||
/// One slot per tag, plus slot 0 for views of more than one tag. Indexed
|
||||
/// through `state()`, never directly.
|
||||
tag_state: [action.tag_count + 1]TagState = @splat(.{}),
|
||||
|
||||
removed: bool = false,
|
||||
|
||||
/// Written by the layout pass each manage sequence, read by the render pass.
|
||||
tabbar_box: ?Box = null,
|
||||
/// True when the current layout stacks windows, so only `stack_top` shows.
|
||||
stacked: bool = false,
|
||||
stack_top: ?*Window = null,
|
||||
|
||||
tabbar: TabBar,
|
||||
|
||||
pub fn create(wm: *Wm, output: *river.OutputV1) !*Output {
|
||||
const self = try wm.gpa.create(Output);
|
||||
self.* = .{
|
||||
.wm = wm,
|
||||
.output = output,
|
||||
.tabbar = .{ .wm = wm },
|
||||
};
|
||||
output.setListener(*Output, onEvent, self);
|
||||
|
||||
if (wm.layer_shell) |ls| {
|
||||
self.layer_output = ls.getOutput(output) catch null;
|
||||
if (self.layer_output) |lo| {
|
||||
lo.setListener(*Output, onLayerEvent, self);
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *Output) void {
|
||||
const gpa = self.wm.gpa;
|
||||
self.tabbar.deinit();
|
||||
if (self.layer_output) |lo| lo.destroy();
|
||||
// The wl_output entry is owned by Wm; just break the back reference.
|
||||
if (self.wl_output) |entry| entry.output = null;
|
||||
self.output.destroy();
|
||||
gpa.destroy(self);
|
||||
}
|
||||
|
||||
pub fn displayName(self: *const Output) []const u8 {
|
||||
const entry = self.wl_output orelse return "?";
|
||||
return entry.name orelse "?";
|
||||
}
|
||||
|
||||
/// The area windows are laid out in.
|
||||
pub fn layoutArea(self: *const Output) Box {
|
||||
return if (self.have_usable) self.usable else self.box;
|
||||
}
|
||||
|
||||
/// How one tag is arranged. dwm's pertag patch keeps exactly these four.
|
||||
pub const TagState = struct {
|
||||
layout: action.Layout = config.default_layout,
|
||||
prev_layout: action.Layout = config.default_layout,
|
||||
nmaster: i32 = config.nmaster,
|
||||
mfact: f32 = config.mfact,
|
||||
};
|
||||
|
||||
/// The arrangement settings in force on this output right now, i.e. those of
|
||||
/// the tag being viewed. See `action.tagSlot` for how a view picks its slot.
|
||||
pub fn state(self: *Output) *TagState {
|
||||
return &self.tag_state[action.tagSlot(self.tags)];
|
||||
}
|
||||
|
||||
pub fn setLayout(self: *Output, mode: action.Layout) void {
|
||||
const st = self.state();
|
||||
if (mode == st.layout) return;
|
||||
st.prev_layout = st.layout;
|
||||
st.layout = mode;
|
||||
}
|
||||
|
||||
pub fn setTags(self: *Output, tags: u32) void {
|
||||
const masked = tags & action.all_tags;
|
||||
if (masked == 0 or masked == self.tags) return;
|
||||
self.prev_tags = self.tags;
|
||||
self.tags = masked;
|
||||
}
|
||||
|
||||
fn onEvent(_: *river.OutputV1, event: river.OutputV1.Event, self: *Output) void {
|
||||
switch (event) {
|
||||
.removed => {
|
||||
self.removed = true;
|
||||
self.wm.needsManage();
|
||||
},
|
||||
.position => |ev| {
|
||||
self.box.x = ev.x;
|
||||
self.box.y = ev.y;
|
||||
self.wm.needsManage();
|
||||
},
|
||||
.dimensions => |ev| {
|
||||
self.box.width = ev.width;
|
||||
self.box.height = ev.height;
|
||||
self.wm.needsManage();
|
||||
},
|
||||
.wl_output => |ev| {
|
||||
self.wl_output_name = ev.name;
|
||||
self.wm.attachWlOutput(self);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn onLayerEvent(_: *river.LayerShellOutputV1, event: river.LayerShellOutputV1.Event, self: *Output) void {
|
||||
switch (event) {
|
||||
.non_exclusive_area => |ev| {
|
||||
self.usable = .{ .x = ev.x, .y = ev.y, .width = ev.width, .height = ev.height };
|
||||
self.have_usable = true;
|
||||
self.wm.needsManage();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// The strip of solid colour blocks drawn above the windows in the tabbed
|
||||
/// layout. One block per window, the focused one highlighted; titles are not
|
||||
/// drawn here but are published over IPC for bars that want to render them.
|
||||
pub const TabBar = struct {
|
||||
wm: *Wm,
|
||||
|
||||
surface: ?*wl.Surface = null,
|
||||
shell: ?*river.ShellSurfaceV1 = null,
|
||||
node: ?*river.NodeV1 = null,
|
||||
pool: ?shm.Pool = null,
|
||||
|
||||
/// Currently mapped, i.e. showing a buffer.
|
||||
mapped: bool = false,
|
||||
/// Where the bar is, in global coordinates.
|
||||
box: Box = .{},
|
||||
/// Hit rectangles for the tabs currently drawn, in global coordinates,
|
||||
/// parallel to `windows`.
|
||||
rects: std.ArrayList(Box) = .empty,
|
||||
windows: std.ArrayList(*Window) = .empty,
|
||||
|
||||
pub fn deinit(self: *TabBar) void {
|
||||
const gpa = self.wm.gpa;
|
||||
self.rects.deinit(gpa);
|
||||
self.windows.deinit(gpa);
|
||||
if (self.pool) |*p| p.deinit();
|
||||
if (self.node) |n| n.destroy();
|
||||
if (self.shell) |s| s.destroy();
|
||||
if (self.surface) |s| s.destroy();
|
||||
}
|
||||
|
||||
fn ensureSurface(self: *TabBar) !void {
|
||||
if (self.surface != null) return;
|
||||
|
||||
const compositor = self.wm.compositor orelse return error.NoCompositor;
|
||||
const wm_proxy = self.wm.window_manager orelse return error.NoWindowManager;
|
||||
const wl_shm = self.wm.shm orelse return error.NoShm;
|
||||
|
||||
const surface = try compositor.createSurface();
|
||||
errdefer surface.destroy();
|
||||
|
||||
const shell = try wm_proxy.getShellSurface(surface);
|
||||
errdefer shell.destroy();
|
||||
|
||||
const node = try shell.getNode();
|
||||
|
||||
self.surface = surface;
|
||||
self.shell = shell;
|
||||
self.node = node;
|
||||
self.pool = shm.Pool.init(self.wm.gpa, wl_shm);
|
||||
}
|
||||
|
||||
/// Draw and place the bar. Must be called during a render sequence.
|
||||
pub fn show(self: *TabBar, box: Box, windows: []const *Window, focused: ?*Window) void {
|
||||
self.ensureSurface() catch |err| {
|
||||
std.log.err("tab bar: {s}", .{@errorName(err)});
|
||||
return;
|
||||
};
|
||||
if (box.width <= 0 or box.height <= 0 or windows.len == 0) {
|
||||
self.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
const gpa = self.wm.gpa;
|
||||
const pool = &self.pool.?;
|
||||
|
||||
const buffer = pool.acquire(box.width, box.height) catch |err| {
|
||||
std.log.err("tab bar buffer: {s}", .{@errorName(err)});
|
||||
return;
|
||||
};
|
||||
|
||||
// Recompute the hit rectangles, in buffer-local coordinates first.
|
||||
self.rects.clearRetainingCapacity();
|
||||
self.windows.clearRetainingCapacity();
|
||||
self.rects.ensureTotalCapacity(gpa, windows.len) catch return;
|
||||
self.windows.appendSlice(gpa, windows) catch return;
|
||||
self.rects.resize(gpa, windows.len) catch return;
|
||||
|
||||
const local: Box = .{ .x = 0, .y = 0, .width = box.width, .height = box.height };
|
||||
layout.tabRects(local, windows.len, self.rects.items);
|
||||
|
||||
const sep = color.toArgb8888(config.tab_separator);
|
||||
buffer.fill(local, sep);
|
||||
|
||||
for (self.rects.items, windows) |rect, win| {
|
||||
const is_focused = focused != null and focused.? == win;
|
||||
const argb = color.toArgb8888(if (is_focused) config.tab_focused else config.tab_normal);
|
||||
// Leave a one pixel separator on the right of every tab but the
|
||||
// last, which the background colour shows through.
|
||||
const inner: Box = .{
|
||||
.x = rect.x,
|
||||
.y = rect.y,
|
||||
.width = @max(0, rect.width - 1),
|
||||
.height = rect.height,
|
||||
};
|
||||
buffer.fill(inner, argb);
|
||||
}
|
||||
|
||||
// Translate the hit rectangles into global coordinates for click
|
||||
// handling, now that drawing is done.
|
||||
for (self.rects.items) |*rect| {
|
||||
rect.x += box.x;
|
||||
rect.y += box.y;
|
||||
}
|
||||
|
||||
const surface = self.surface.?;
|
||||
buffer.busy = true;
|
||||
surface.attach(buffer.wl_buffer, 0, 0);
|
||||
surface.damageBuffer(0, 0, box.width, box.height);
|
||||
self.shell.?.syncNextCommit();
|
||||
surface.commit();
|
||||
|
||||
self.node.?.setPosition(box.x, box.y);
|
||||
self.box = box;
|
||||
self.mapped = true;
|
||||
}
|
||||
|
||||
/// Unmap the bar. Must be called during a render sequence if it was
|
||||
/// previously shown.
|
||||
pub fn hide(self: *TabBar) void {
|
||||
if (!self.mapped) return;
|
||||
const surface = self.surface orelse return;
|
||||
surface.attach(null, 0, 0);
|
||||
self.shell.?.syncNextCommit();
|
||||
surface.commit();
|
||||
self.mapped = false;
|
||||
self.rects.clearRetainingCapacity();
|
||||
self.windows.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
/// Which window's tab covers this global coordinate, if any.
|
||||
pub fn windowAt(self: *const TabBar, x: i32, y: i32) ?*Window {
|
||||
if (!self.mapped) return null;
|
||||
for (self.rects.items, self.windows.items) |rect, win| {
|
||||
if (rect.contains(x, y)) return win;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
+461
@@ -0,0 +1,461 @@
|
||||
//! A seat: keyboard focus, key and pointer bindings, and interactive
|
||||
//! move/resize operations.
|
||||
//!
|
||||
//! river delivers binding and pointer events immediately but requires the
|
||||
//! matching protocol requests to be made inside a manage sequence. Every
|
||||
//! handler here therefore records intent in a field and calls `needsManage`;
|
||||
//! `applyManage` is the only place that talks back to the compositor.
|
||||
|
||||
const Seat = @This();
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const wayland = @import("wayland");
|
||||
const wl = wayland.client.wl;
|
||||
const river = wayland.client.river;
|
||||
|
||||
const config = @import("config");
|
||||
const action = @import("action");
|
||||
|
||||
const Wm = @import("Wm.zig");
|
||||
const Window = @import("Window.zig");
|
||||
const Output = @import("Output.zig");
|
||||
const layout = @import("layout.zig");
|
||||
const Box = layout.Box;
|
||||
|
||||
pub const KeyBinding = struct {
|
||||
seat: *Seat,
|
||||
/// Index into config.keys.
|
||||
index: usize,
|
||||
object: *river.XkbBindingV1,
|
||||
};
|
||||
|
||||
pub const PointerBinding = struct {
|
||||
seat: *Seat,
|
||||
/// Index into config.buttons.
|
||||
index: usize,
|
||||
object: *river.PointerBindingV1,
|
||||
};
|
||||
|
||||
pub const Op = struct {
|
||||
kind: enum { move, resize },
|
||||
window: *Window,
|
||||
/// The window's cell when the operation started.
|
||||
start: Box,
|
||||
/// For resize: which corner is being dragged.
|
||||
edges: river.WindowV1.Edges = .{},
|
||||
};
|
||||
|
||||
wm: *Wm,
|
||||
seat: *river.SeatV1,
|
||||
layer_seat: ?*river.LayerShellSeatV1 = null,
|
||||
|
||||
wl_seat_name: u32 = 0,
|
||||
wl_seat: ?*wl.Seat = null,
|
||||
pointer: ?*wl.Pointer = null,
|
||||
|
||||
keys: std.ArrayList(*KeyBinding) = .empty,
|
||||
buttons: std.ArrayList(*PointerBinding) = .empty,
|
||||
/// Set once the bindings have been enabled in a manage sequence.
|
||||
bindings_enabled: bool = false,
|
||||
|
||||
focused: ?*Window = null,
|
||||
/// Focus we want river to apply in the next manage sequence.
|
||||
pending_focus: ?*Window = null,
|
||||
/// True when focus should be cleared rather than moved.
|
||||
pending_clear_focus: bool = false,
|
||||
|
||||
/// A layer surface (a bar, a launcher) holds focus; our focus requests are
|
||||
/// either ignored or would steal it.
|
||||
layer_focus: enum { none, exclusive, non_exclusive } = .none,
|
||||
|
||||
/// Pointer position in the compositor's logical coordinate space.
|
||||
pointer_x: i32 = 0,
|
||||
pointer_y: i32 = 0,
|
||||
|
||||
/// The window the pointer is currently inside.
|
||||
hovered: ?*Window = null,
|
||||
|
||||
op: ?Op = null,
|
||||
/// An operation to start in the next manage sequence.
|
||||
pending_op: ?Op = null,
|
||||
/// The running operation should be ended in the next manage sequence.
|
||||
pending_op_end: bool = false,
|
||||
|
||||
/// One of our own surfaces has pointer focus (the tab bar).
|
||||
pointer_surface: ?*wl.Surface = null,
|
||||
pointer_local_x: f64 = 0,
|
||||
pointer_local_y: f64 = 0,
|
||||
|
||||
removed: bool = false,
|
||||
|
||||
pub fn create(wm: *Wm, seat: *river.SeatV1) !*Seat {
|
||||
const self = try wm.gpa.create(Seat);
|
||||
self.* = .{ .wm = wm, .seat = seat };
|
||||
seat.setListener(*Seat, onEvent, self);
|
||||
|
||||
if (wm.layer_shell) |ls| {
|
||||
self.layer_seat = ls.getSeat(seat) catch null;
|
||||
if (self.layer_seat) |lseat| lseat.setListener(*Seat, onLayerEvent, self);
|
||||
}
|
||||
|
||||
try self.createBindings();
|
||||
|
||||
if (config.cursor_theme) |theme| {
|
||||
var buf: [256]u8 = undefined;
|
||||
const z = std.fmt.bufPrintZ(&buf, "{s}", .{theme}) catch null;
|
||||
if (z) |name| seat.setXcursorTheme(name, config.cursor_size);
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *Seat) void {
|
||||
const gpa = self.wm.gpa;
|
||||
for (self.keys.items) |binding| {
|
||||
binding.object.destroy();
|
||||
gpa.destroy(binding);
|
||||
}
|
||||
for (self.buttons.items) |binding| {
|
||||
binding.object.destroy();
|
||||
gpa.destroy(binding);
|
||||
}
|
||||
self.keys.deinit(gpa);
|
||||
self.buttons.deinit(gpa);
|
||||
if (self.pointer) |p| p.release();
|
||||
if (self.wl_seat) |s| s.release();
|
||||
if (self.layer_seat) |l| l.destroy();
|
||||
self.seat.destroy();
|
||||
gpa.destroy(self);
|
||||
}
|
||||
|
||||
fn createBindings(self: *Seat) !void {
|
||||
const gpa = self.wm.gpa;
|
||||
|
||||
if (self.wm.xkb_bindings) |xkb_bindings| {
|
||||
try self.keys.ensureTotalCapacity(gpa, config.keys.len);
|
||||
for (config.keys, 0..) |key, i| {
|
||||
const mods: river.SeatV1.Modifiers = @bitCast(key.mods);
|
||||
const object = xkb_bindings.getXkbBinding(self.seat, @intFromEnum(key.keysym), mods) catch |err| {
|
||||
std.log.err("failed to bind key {t}: {s}", .{ key.keysym, @errorName(err) });
|
||||
continue;
|
||||
};
|
||||
const binding = try gpa.create(KeyBinding);
|
||||
binding.* = .{ .seat = self, .index = i, .object = object };
|
||||
object.setListener(*KeyBinding, onKeyEvent, binding);
|
||||
self.keys.appendAssumeCapacity(binding);
|
||||
}
|
||||
}
|
||||
|
||||
std.log.info("registered {d}/{d} key bindings", .{ self.keys.items.len, config.keys.len });
|
||||
|
||||
try self.buttons.ensureTotalCapacity(gpa, config.buttons.len);
|
||||
for (config.buttons, 0..) |button, i| {
|
||||
const mods: river.SeatV1.Modifiers = @bitCast(button.mods);
|
||||
const object = self.seat.getPointerBinding(button.button, mods) catch |err| {
|
||||
std.log.err("failed to bind button {d}: {s}", .{ button.button, @errorName(err) });
|
||||
continue;
|
||||
};
|
||||
const binding = try gpa.create(PointerBinding);
|
||||
binding.* = .{ .seat = self, .index = i, .object = object };
|
||||
object.setListener(*PointerBinding, onButtonEvent, binding);
|
||||
self.buttons.appendAssumeCapacity(binding);
|
||||
}
|
||||
|
||||
std.log.info("registered {d}/{d} pointer bindings", .{ self.buttons.items.len, config.buttons.len });
|
||||
}
|
||||
|
||||
/// The output this seat is working on: the one holding the focused window,
|
||||
/// else the one under the pointer.
|
||||
pub fn currentOutput(self: *Seat) ?*Output {
|
||||
if (self.focused) |win| {
|
||||
if (win.output) |out| return out;
|
||||
}
|
||||
return self.wm.outputAt(self.pointer_x, self.pointer_y) orelse self.wm.firstOutput();
|
||||
}
|
||||
|
||||
pub fn focus(self: *Seat, window: ?*Window) void {
|
||||
if (window) |win| {
|
||||
self.pending_focus = win;
|
||||
self.pending_clear_focus = false;
|
||||
} else {
|
||||
self.pending_focus = null;
|
||||
self.pending_clear_focus = true;
|
||||
}
|
||||
self.wm.needsManage();
|
||||
}
|
||||
|
||||
pub fn startMove(self: *Seat, window: *Window) void {
|
||||
if (window.fullscreen) return;
|
||||
self.pending_op = .{ .kind = .move, .window = window, .start = window.cell };
|
||||
self.wm.needsManage();
|
||||
}
|
||||
|
||||
pub fn startResize(self: *Seat, window: *Window, edges: river.WindowV1.Edges) void {
|
||||
if (window.fullscreen) return;
|
||||
self.pending_op = .{
|
||||
.kind = .resize,
|
||||
.window = window,
|
||||
.start = window.cell,
|
||||
.edges = edges,
|
||||
};
|
||||
self.wm.needsManage();
|
||||
}
|
||||
|
||||
/// Issue the requests recorded by the event handlers. Manage sequence only.
|
||||
pub fn applyManage(self: *Seat) void {
|
||||
if (!self.bindings_enabled) {
|
||||
for (self.keys.items) |binding| binding.object.enable();
|
||||
for (self.buttons.items) |binding| binding.object.enable();
|
||||
self.bindings_enabled = true;
|
||||
}
|
||||
|
||||
if (self.pending_op) |op| {
|
||||
// Dragging a tiled window pops it out into floating, as in dwm.
|
||||
if (!op.window.floating) {
|
||||
op.window.floating = true;
|
||||
op.window.floating_forced = true;
|
||||
op.window.float_box = op.window.cell;
|
||||
}
|
||||
self.seat.opStartPointer();
|
||||
self.op = op;
|
||||
self.op.?.start = op.window.cell;
|
||||
self.pending_op = null;
|
||||
op.window.window.informResizeStart();
|
||||
}
|
||||
|
||||
if (self.pending_op_end) {
|
||||
if (self.op) |op| {
|
||||
self.seat.opEnd();
|
||||
if (!op.window.closed) op.window.window.informResizeEnd();
|
||||
}
|
||||
self.op = null;
|
||||
self.pending_op_end = false;
|
||||
}
|
||||
|
||||
// A layer surface with exclusive focus outranks us entirely.
|
||||
if (self.layer_focus == .exclusive) {
|
||||
self.pending_focus = null;
|
||||
self.pending_clear_focus = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.pending_focus) |win| {
|
||||
if (!win.closed and win.mapped) {
|
||||
self.seat.focusWindow(win.window);
|
||||
self.focused = win;
|
||||
self.wm.focus_serial += 1;
|
||||
win.focus_serial = self.wm.focus_serial;
|
||||
self.warpTo(win);
|
||||
self.wm.ipcDirty();
|
||||
}
|
||||
self.pending_focus = null;
|
||||
} else if (self.pending_clear_focus) {
|
||||
self.seat.clearFocus();
|
||||
self.focused = null;
|
||||
self.pending_clear_focus = false;
|
||||
self.wm.ipcDirty();
|
||||
}
|
||||
}
|
||||
|
||||
/// Pull the pointer to the middle of a newly focused window.
|
||||
///
|
||||
/// Skipped when the pointer is already inside it, so keyboard focus following
|
||||
/// the mouse does not yank the cursor out from under the user. Manage sequence
|
||||
/// only.
|
||||
fn warpTo(self: *Seat, win: *Window) void {
|
||||
if (!config.warp_cursor) return;
|
||||
if (self.op != null) return;
|
||||
if (win.cell.contains(self.pointer_x, self.pointer_y)) return;
|
||||
|
||||
self.seat.pointerWarp(
|
||||
win.cell.x + @divTrunc(win.cell.width, 2),
|
||||
win.cell.y + @divTrunc(win.cell.height, 2),
|
||||
);
|
||||
}
|
||||
|
||||
fn onEvent(_: *river.SeatV1, event: river.SeatV1.Event, self: *Seat) void {
|
||||
switch (event) {
|
||||
.removed => {
|
||||
self.removed = true;
|
||||
self.wm.needsManage();
|
||||
},
|
||||
|
||||
.wl_seat => |ev| {
|
||||
self.wl_seat_name = ev.name;
|
||||
self.wm.attachWlSeat(self);
|
||||
},
|
||||
|
||||
.pointer_position => |ev| {
|
||||
self.pointer_x = ev.x;
|
||||
self.pointer_y = ev.y;
|
||||
},
|
||||
|
||||
.pointer_enter => |ev| {
|
||||
const win = Wm.windowFromProxy(ev.window) orelse return;
|
||||
self.hovered = win;
|
||||
if (config.focus_follows_mouse and self.op == null) {
|
||||
if (win.mapped and win.visible) self.focus(win);
|
||||
}
|
||||
},
|
||||
|
||||
.pointer_leave => {
|
||||
self.hovered = null;
|
||||
},
|
||||
|
||||
.window_interaction => |ev| {
|
||||
const win = Wm.windowFromProxy(ev.window) orelse return;
|
||||
// Clicking a window focuses it and, if floating, raises it.
|
||||
self.focus(win);
|
||||
if (win.output) |out| self.wm.focusOutput(out);
|
||||
},
|
||||
|
||||
.shell_surface_interaction => {
|
||||
// Our own tab bar; handled through wl_pointer where we know the
|
||||
// coordinates.
|
||||
},
|
||||
|
||||
.op_delta => |ev| {
|
||||
const op = self.op orelse return;
|
||||
const win = op.window;
|
||||
if (win.closed) return;
|
||||
|
||||
switch (op.kind) {
|
||||
.move => {
|
||||
win.cell.x = op.start.x + ev.dx;
|
||||
win.cell.y = op.start.y + ev.dy;
|
||||
win.float_box = win.cell;
|
||||
},
|
||||
.resize => {
|
||||
var box = op.start;
|
||||
if (op.edges.left) {
|
||||
box.x = op.start.x + ev.dx;
|
||||
box.width = op.start.width - ev.dx;
|
||||
} else {
|
||||
box.width = op.start.width + ev.dx;
|
||||
}
|
||||
if (op.edges.top) {
|
||||
box.y = op.start.y + ev.dy;
|
||||
box.height = op.start.height - ev.dy;
|
||||
} else {
|
||||
box.height = op.start.height + ev.dy;
|
||||
}
|
||||
const min = 2 * config.border_width + 1;
|
||||
box.width = @max(min, box.width);
|
||||
box.height = @max(min, box.height);
|
||||
win.cell = box;
|
||||
win.float_box = box;
|
||||
},
|
||||
}
|
||||
self.wm.needsManage();
|
||||
},
|
||||
|
||||
.op_release => {
|
||||
self.pending_op_end = true;
|
||||
self.wm.needsManage();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn onLayerEvent(_: *river.LayerShellSeatV1, event: river.LayerShellSeatV1.Event, self: *Seat) void {
|
||||
switch (event) {
|
||||
.focus_exclusive => self.layer_focus = .exclusive,
|
||||
.focus_non_exclusive => self.layer_focus = .non_exclusive,
|
||||
.focus_none => {
|
||||
self.layer_focus = .none;
|
||||
// Hand focus back to whatever the user was using.
|
||||
if (self.focused) |win| {
|
||||
if (!win.closed and win.visible) self.focus(win);
|
||||
}
|
||||
},
|
||||
}
|
||||
self.wm.needsManage();
|
||||
}
|
||||
|
||||
fn onKeyEvent(_: *river.XkbBindingV1, event: river.XkbBindingV1.Event, binding: *KeyBinding) void {
|
||||
const self = binding.seat;
|
||||
const key = config.keys[binding.index];
|
||||
switch (event) {
|
||||
.pressed => {
|
||||
self.wm.perform(self, key.action);
|
||||
if (key.shouldRepeat()) self.wm.startRepeat(self, binding.index);
|
||||
},
|
||||
.released, .stop_repeat => {
|
||||
self.wm.stopRepeat(binding.index);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn onButtonEvent(_: *river.PointerBindingV1, event: river.PointerBindingV1.Event, binding: *PointerBinding) void {
|
||||
const self = binding.seat;
|
||||
const button = config.buttons[binding.index];
|
||||
switch (event) {
|
||||
.pressed => {
|
||||
const win = self.hovered orelse self.wm.windowAt(self.pointer_x, self.pointer_y) orelse return;
|
||||
self.focus(win);
|
||||
switch (button.action) {
|
||||
.move => self.startMove(win),
|
||||
.resize => {
|
||||
// Resize from whichever corner the pointer is nearest, so
|
||||
// the drag pulls the expected edge.
|
||||
const mid_x = win.cell.x + @divTrunc(win.cell.width, 2);
|
||||
const mid_y = win.cell.y + @divTrunc(win.cell.height, 2);
|
||||
self.startResize(win, .{
|
||||
.left = self.pointer_x < mid_x,
|
||||
.right = self.pointer_x >= mid_x,
|
||||
.top = self.pointer_y < mid_y,
|
||||
.bottom = self.pointer_y >= mid_y,
|
||||
});
|
||||
},
|
||||
}
|
||||
},
|
||||
.released => {
|
||||
self.pending_op_end = true;
|
||||
self.wm.needsManage();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ─── wl_pointer, used only to click the tab bar ──────────────────────────────
|
||||
|
||||
pub fn onWlSeatEvent(_: *wl.Seat, event: wl.Seat.Event, self: *Seat) void {
|
||||
switch (event) {
|
||||
.capabilities => |ev| {
|
||||
if (ev.capabilities.pointer and self.pointer == null) {
|
||||
self.pointer = self.wl_seat.?.getPointer() catch null;
|
||||
if (self.pointer) |p| p.setListener(*Seat, onPointerEvent, self);
|
||||
}
|
||||
},
|
||||
.name => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn onPointerEvent(_: *wl.Pointer, event: wl.Pointer.Event, self: *Seat) void {
|
||||
switch (event) {
|
||||
.enter => |ev| {
|
||||
self.pointer_surface = ev.surface;
|
||||
self.pointer_local_x = ev.surface_x.toDouble();
|
||||
self.pointer_local_y = ev.surface_y.toDouble();
|
||||
},
|
||||
.leave => {
|
||||
self.pointer_surface = null;
|
||||
},
|
||||
.motion => |ev| {
|
||||
self.pointer_local_x = ev.surface_x.toDouble();
|
||||
self.pointer_local_y = ev.surface_y.toDouble();
|
||||
},
|
||||
.button => |ev| {
|
||||
if (ev.state != .pressed) return;
|
||||
const surface = self.pointer_surface orelse return;
|
||||
const out = self.wm.outputForTabBarSurface(surface) orelse return;
|
||||
|
||||
const gx = out.tabbar.box.x + @as(i32, @intFromFloat(self.pointer_local_x));
|
||||
const gy = out.tabbar.box.y + @as(i32, @intFromFloat(self.pointer_local_y));
|
||||
if (out.tabbar.windowAt(gx, gy)) |win| {
|
||||
self.wm.focusOutput(out);
|
||||
self.focus(win);
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
//! A single managed window.
|
||||
//!
|
||||
//! Event handlers here only ever mutate plain fields. Every protocol request
|
||||
//! that changes window management or rendering state is issued from Wm's
|
||||
//! manage/render sequence handlers, because river only permits those requests
|
||||
//! between manage_start/manage_finish and render_start/render_finish.
|
||||
|
||||
const Window = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const wayland = @import("wayland");
|
||||
const river = wayland.client.river;
|
||||
|
||||
const config = @import("config");
|
||||
|
||||
const Wm = @import("Wm.zig");
|
||||
const Output = @import("Output.zig");
|
||||
const layout = @import("layout.zig");
|
||||
const Box = layout.Box;
|
||||
|
||||
wm: *Wm,
|
||||
window: *river.WindowV1,
|
||||
/// Created lazily: the protocol allows get_node exactly once per window.
|
||||
node: ?*river.NodeV1 = null,
|
||||
|
||||
title: ?[]u8 = null,
|
||||
app_id: ?[]u8 = null,
|
||||
identifier: ?[]u8 = null,
|
||||
parent: ?*Window = null,
|
||||
|
||||
tags: u32 = 0,
|
||||
output: ?*Output = null,
|
||||
|
||||
floating: bool = false,
|
||||
/// Set by rules or by the user; distinguishes "floating because it is a
|
||||
/// dialog" from "floating because it was asked to be".
|
||||
floating_forced: bool = false,
|
||||
|
||||
fullscreen: bool = false,
|
||||
/// What we last told the window, so we only send changes.
|
||||
informed_fullscreen: bool = false,
|
||||
|
||||
/// Target rectangle including the border, computed by the layout.
|
||||
cell: Box = .{},
|
||||
/// Geometry to restore when a floating window stops being fullscreen.
|
||||
float_box: Box = .{},
|
||||
/// The content size river last reported.
|
||||
content_width: i32 = 0,
|
||||
content_height: i32 = 0,
|
||||
/// The last size we proposed, so we do not re-propose every manage sequence.
|
||||
proposed_width: i32 = -1,
|
||||
proposed_height: i32 = -1,
|
||||
|
||||
min_width: i32 = 0,
|
||||
min_height: i32 = 0,
|
||||
max_width: i32 = 0,
|
||||
max_height: i32 = 0,
|
||||
|
||||
/// True once river has sent dimensions, i.e. the window is on screen.
|
||||
mapped: bool = false,
|
||||
/// True once we have sent the one-time setup requests.
|
||||
configured: bool = false,
|
||||
/// Computed each manage sequence.
|
||||
visible: bool = false,
|
||||
/// Whether the window is currently hidden, so we only send changes.
|
||||
hidden: bool = false,
|
||||
/// river has closed this window; it must be reaped and not touched again.
|
||||
closed: bool = false,
|
||||
/// A close was requested. `close` modifies window management state, so it has
|
||||
/// to wait for the next manage sequence like everything else.
|
||||
pending_close: bool = false,
|
||||
|
||||
/// Bumped whenever the window takes focus, giving a cheap "most recently
|
||||
/// focused" ordering without maintaining dwm's second linked list.
|
||||
focus_serial: u64 = 0,
|
||||
/// Cleared once the window has had its one chance at taking focus as it maps.
|
||||
wants_initial_focus: bool = true,
|
||||
|
||||
pub fn create(wm: *Wm, window: *river.WindowV1) !*Window {
|
||||
const self = try wm.gpa.create(Window);
|
||||
self.* = .{ .wm = wm, .window = window };
|
||||
window.setListener(*Window, onEvent, self);
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *Window) void {
|
||||
const gpa = self.wm.gpa;
|
||||
if (self.node) |node| node.destroy();
|
||||
self.window.destroy();
|
||||
if (self.title) |t| gpa.free(t);
|
||||
if (self.app_id) |a| gpa.free(a);
|
||||
if (self.identifier) |i| gpa.free(i);
|
||||
gpa.destroy(self);
|
||||
}
|
||||
|
||||
/// The node is needed to position and stack the window; create it on demand.
|
||||
pub fn getNode(self: *Window) ?*river.NodeV1 {
|
||||
if (self.node) |node| return node;
|
||||
self.node = self.window.getNode() catch |err| {
|
||||
std.log.err("failed to create node for window: {s}", .{@errorName(err)});
|
||||
return null;
|
||||
};
|
||||
return self.node;
|
||||
}
|
||||
|
||||
/// Clamp a proposed size to the window's advertised limits. These are hints,
|
||||
/// but respecting them avoids pointless configure round-trips with windows
|
||||
/// that will refuse the size anyway.
|
||||
pub fn clampSize(self: *const Window, width: i32, height: i32) struct { i32, i32 } {
|
||||
var w = width;
|
||||
var h = height;
|
||||
if (self.min_width > 0) w = @max(w, self.min_width);
|
||||
if (self.min_height > 0) h = @max(h, self.min_height);
|
||||
if (self.max_width > 0) w = @min(w, self.max_width);
|
||||
if (self.max_height > 0) h = @min(h, self.max_height);
|
||||
return .{ @max(1, w), @max(1, h) };
|
||||
}
|
||||
|
||||
/// Apply matching rules from config to a newly created window.
|
||||
pub fn applyRules(self: *Window) void {
|
||||
for (config.rules) |rule| {
|
||||
if (rule.app_id) |want| {
|
||||
const have = self.app_id orelse continue;
|
||||
if (!std.mem.eql(u8, want, have)) continue;
|
||||
}
|
||||
if (rule.title) |want| {
|
||||
const have = self.title orelse continue;
|
||||
if (std.mem.indexOf(u8, have, want) == null) continue;
|
||||
}
|
||||
if (rule.tags) |t| self.tags = t;
|
||||
if (rule.floating) |f| {
|
||||
self.floating = f;
|
||||
self.floating_forced = f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setString(self: *Window, field: *?[]u8, value: ?[*:0]const u8) void {
|
||||
const gpa = self.wm.gpa;
|
||||
if (field.*) |old| gpa.free(old);
|
||||
field.* = null;
|
||||
if (value) |v| {
|
||||
field.* = gpa.dupe(u8, std.mem.span(v)) catch null;
|
||||
}
|
||||
}
|
||||
|
||||
fn onEvent(_: *river.WindowV1, event: river.WindowV1.Event, self: *Window) void {
|
||||
switch (event) {
|
||||
.closed => {
|
||||
self.closed = true;
|
||||
self.wm.needsManage();
|
||||
},
|
||||
|
||||
.dimensions => |ev| {
|
||||
self.content_width = ev.width;
|
||||
self.content_height = ev.height;
|
||||
if (!self.mapped) {
|
||||
self.mapped = true;
|
||||
// Focus and stacking for a new window are settled in the
|
||||
// manage sequence, and only once it is mapped.
|
||||
self.wm.needsManage();
|
||||
self.wm.ipcDirty();
|
||||
}
|
||||
// A window may resize itself; if it is floating its cell must
|
||||
// follow, otherwise the border is drawn around the wrong area.
|
||||
if (self.floating and !self.fullscreen) {
|
||||
const bw = config.border_width;
|
||||
self.cell.width = ev.width + 2 * bw;
|
||||
self.cell.height = ev.height + 2 * bw;
|
||||
self.float_box = self.cell;
|
||||
}
|
||||
},
|
||||
|
||||
.dimensions_hint => |ev| {
|
||||
self.min_width = ev.min_width;
|
||||
self.min_height = ev.min_height;
|
||||
self.max_width = ev.max_width;
|
||||
self.max_height = ev.max_height;
|
||||
},
|
||||
|
||||
.app_id => |ev| {
|
||||
self.setString(&self.app_id, ev.app_id);
|
||||
self.applyRules();
|
||||
self.wm.ipcDirty();
|
||||
},
|
||||
|
||||
.title => |ev| {
|
||||
self.setString(&self.title, ev.title);
|
||||
self.applyRules();
|
||||
self.wm.ipcDirty();
|
||||
},
|
||||
|
||||
.identifier => |ev| {
|
||||
self.setString(&self.identifier, ev.identifier);
|
||||
},
|
||||
|
||||
.parent => |ev| {
|
||||
self.parent = if (ev.parent) |p| Wm.windowFromProxy(p) else null;
|
||||
// Dialogs and file pickers float, as in dwm.
|
||||
if (config.float_children and self.parent != null and !self.floating_forced) {
|
||||
self.floating = true;
|
||||
}
|
||||
},
|
||||
|
||||
.fullscreen_requested => |ev| {
|
||||
self.fullscreen = true;
|
||||
if (ev.output) |o| {
|
||||
if (Wm.outputFromProxy(o)) |out| self.output = out;
|
||||
}
|
||||
self.wm.needsManage();
|
||||
},
|
||||
|
||||
.exit_fullscreen_requested => {
|
||||
self.fullscreen = false;
|
||||
self.wm.needsManage();
|
||||
},
|
||||
|
||||
.pointer_move_requested => |ev| {
|
||||
if (Wm.seatFromProxy(ev.seat)) |seat| seat.startMove(self);
|
||||
},
|
||||
|
||||
.pointer_resize_requested => |ev| {
|
||||
if (Wm.seatFromProxy(ev.seat)) |seat| seat.startResize(self, ev.edges);
|
||||
},
|
||||
|
||||
// We advertise only the fullscreen capability, so these should not
|
||||
// arrive; ignoring them is the documented option either way.
|
||||
.maximize_requested,
|
||||
.unmaximize_requested,
|
||||
.minimize_requested,
|
||||
.show_window_menu_requested,
|
||||
.decoration_hint,
|
||||
.unreliable_pid,
|
||||
.presentation_hint,
|
||||
=> {},
|
||||
}
|
||||
}
|
||||
+1427
File diff suppressed because it is too large
Load Diff
+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;
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
//! Colour conversion.
|
||||
//!
|
||||
//! Config declares colours as the familiar 0xRRGGBBAA with straight alpha.
|
||||
//! The two sinks want something different:
|
||||
//!
|
||||
//! * `river_window_v1.set_borders` takes one full-range u32 per channel —
|
||||
//! river divides each by maxInt(u32) — with premultiplied alpha.
|
||||
//! * wl_shm ARGB8888 wants premultiplied 8-bit channels packed into a u32.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
pub const Rgba = u32;
|
||||
|
||||
pub const Channels = struct {
|
||||
r: u32,
|
||||
g: u32,
|
||||
b: u32,
|
||||
a: u32,
|
||||
};
|
||||
|
||||
fn premul8(c: u8, a: u8) u8 {
|
||||
// Round to nearest rather than truncating, so 0xff at full alpha stays
|
||||
// 0xff instead of drifting down.
|
||||
return @intCast((@as(u32, c) * @as(u32, a) + 127) / 255);
|
||||
}
|
||||
|
||||
/// Expand an 8-bit channel to the full u32 range: 0xff maps exactly to
|
||||
/// 0xffffffff, which is what river treats as 1.0.
|
||||
fn expand(c: u8) u32 {
|
||||
return @as(u32, c) * 0x01010101;
|
||||
}
|
||||
|
||||
fn split(rgba: Rgba) [4]u8 {
|
||||
return .{
|
||||
@intCast((rgba >> 24) & 0xff),
|
||||
@intCast((rgba >> 16) & 0xff),
|
||||
@intCast((rgba >> 8) & 0xff),
|
||||
@intCast(rgba & 0xff),
|
||||
};
|
||||
}
|
||||
|
||||
/// Premultiplied, full-range channels for `set_borders`.
|
||||
pub fn toChannels(rgba: Rgba) Channels {
|
||||
const c = split(rgba);
|
||||
const a = c[3];
|
||||
return .{
|
||||
.r = expand(premul8(c[0], a)),
|
||||
.g = expand(premul8(c[1], a)),
|
||||
.b = expand(premul8(c[2], a)),
|
||||
.a = expand(a),
|
||||
};
|
||||
}
|
||||
|
||||
/// Premultiplied ARGB8888 as a native-endian u32, for wl_shm buffers.
|
||||
pub fn toArgb8888(rgba: Rgba) u32 {
|
||||
const c = split(rgba);
|
||||
const a = c[3];
|
||||
return (@as(u32, a) << 24) |
|
||||
(@as(u32, premul8(c[0], a)) << 16) |
|
||||
(@as(u32, premul8(c[1], a)) << 8) |
|
||||
@as(u32, premul8(c[2], a));
|
||||
}
|
||||
|
||||
test "opaque white survives both conversions intact" {
|
||||
const ch = toChannels(0xffffffff);
|
||||
try std.testing.expectEqual(@as(u32, 0xffffffff), ch.r);
|
||||
try std.testing.expectEqual(@as(u32, 0xffffffff), ch.a);
|
||||
try std.testing.expectEqual(@as(u32, 0xffffffff), toArgb8888(0xffffffff));
|
||||
}
|
||||
|
||||
test "fully transparent premultiplies to zero" {
|
||||
const ch = toChannels(0xffffff00);
|
||||
try std.testing.expectEqual(@as(u32, 0), ch.r);
|
||||
try std.testing.expectEqual(@as(u32, 0), ch.a);
|
||||
try std.testing.expectEqual(@as(u32, 0), toArgb8888(0xffffff00));
|
||||
}
|
||||
|
||||
test "opaque colour keeps its channels in argb order" {
|
||||
// 0xRRGGBBAA -> 0xAARRGGBB
|
||||
try std.testing.expectEqual(@as(u32, 0xff5294e2), toArgb8888(0x5294e2ff));
|
||||
}
|
||||
|
||||
test "half alpha premultiplies channels but not alpha" {
|
||||
const ch = toChannels(0xff000080);
|
||||
try std.testing.expectEqual(@as(u32, 0x80808080), ch.a);
|
||||
// 0xff * 0x80 / 0xff == 0x80
|
||||
try std.testing.expectEqual(@as(u32, 0x80808080), ch.r);
|
||||
try std.testing.expectEqual(@as(u32, 0), ch.g);
|
||||
}
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
//! att_wm configuration, in the spirit of dwm's config.h: edit and rebuild.
|
||||
//!
|
||||
//! Nix users need not patch the source tree — pass a replacement path instead:
|
||||
//!
|
||||
//! zig build -Dconfig=/path/to/my-config.zig
|
||||
//! att_wm.override { config = ./my-config.zig; }
|
||||
|
||||
const action = @import("action");
|
||||
const input = @import("input");
|
||||
const xkb = @import("xkbcommon");
|
||||
|
||||
const Key = action.Key;
|
||||
const Button = action.Button;
|
||||
const Mods = action.Mods;
|
||||
const btn = action.btn;
|
||||
|
||||
/// The dwm "MODKEY". Alt, as dwm ships it; use `Mods.super` if you would
|
||||
/// rather not compete with applications that bind Alt themselves.
|
||||
pub const mod = Mods.alt;
|
||||
|
||||
// ─── Appearance ──────────────────────────────────────────────────────────────
|
||||
|
||||
pub const border_width: i32 = 2;
|
||||
|
||||
/// Gap between adjacent windows. Zero is dwm-faithful.
|
||||
pub const gap: i32 = 0;
|
||||
/// Gap between windows and the edge of the usable area.
|
||||
pub const outer_gap: i32 = 0;
|
||||
|
||||
/// Colours are 0xRRGGBBAA, straight-alpha; they are premultiplied on the way
|
||||
/// to the protocol.
|
||||
pub const border_focused: u32 = 0x5294e2ff;
|
||||
pub const border_normal: u32 = 0x444444ff;
|
||||
|
||||
/// Height of the tab bar drawn in the tabbed layout. Set to 0 to let a bar
|
||||
/// such as quickshell draw the tabs instead, using the IPC `windows` list.
|
||||
pub const tabbar_height: i32 = 22;
|
||||
|
||||
pub const tab_focused: u32 = 0x5294e2ff;
|
||||
pub const tab_normal: u32 = 0x2c2c2cff;
|
||||
/// Drawn as a 1px line between adjacent tabs.
|
||||
pub const tab_separator: u32 = 0x1a1a1aff;
|
||||
|
||||
// ─── Layout ──────────────────────────────────────────────────────────────────
|
||||
|
||||
pub const default_layout = action.Layout.master;
|
||||
|
||||
/// Windows in the master area.
|
||||
pub const nmaster: i32 = 1;
|
||||
/// Fraction of the output width given to the master area.
|
||||
pub const mfact: f32 = 0.55;
|
||||
pub const mfact_min: f32 = 0.05;
|
||||
pub const mfact_max: f32 = 0.95;
|
||||
|
||||
/// Tags visible on a newly connected output.
|
||||
pub const default_tags: u32 = 1;
|
||||
|
||||
/// Names exported over IPC for bars to label tags with.
|
||||
pub const tag_names = [action.tag_count][]const u8{
|
||||
"1", "2", "3", "4", "5", "6", "7", "8", "9",
|
||||
};
|
||||
|
||||
// ─── Behaviour ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// dwm's sloppy focus: moving the pointer over a window focuses it.
|
||||
pub const focus_follows_mouse = false;
|
||||
|
||||
/// Warp the pointer to the centre of a window when focus moves there by
|
||||
/// keyboard. dwm does not do this; it is handy on multi-head setups.
|
||||
pub const warp_cursor = false;
|
||||
|
||||
/// Windows with a parent (dialogs, file pickers) start floating, as in dwm.
|
||||
pub const float_children = true;
|
||||
|
||||
/// How fast a held-down *binding* re-fires, in milliseconds. river reports key
|
||||
/// press and release and leaves repeating to att_wm, so this is what governs
|
||||
/// `Mod+j` held down — not what applications see, which is `repeat` below.
|
||||
pub const binding_repeat_delay: u32 = 300;
|
||||
pub const binding_repeat_interval: u32 = 40;
|
||||
|
||||
pub const cursor_theme: ?[]const u8 = null;
|
||||
pub const cursor_size: u32 = 24;
|
||||
|
||||
/// Commands run once at startup, after the connection to river is up.
|
||||
pub const autostart = [_][]const []const u8{
|
||||
// .{ "quickshell", "-c", "att_wm" },
|
||||
};
|
||||
|
||||
pub const terminal = [_][]const u8{"foot"};
|
||||
pub const menu = [_][]const u8{ "wmenu-run", "-f", "monospace 10" };
|
||||
|
||||
// ─── Input ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/// The xkb layout every keyboard gets, as `setxkbmap` takes it. All-null — the
|
||||
/// default — leaves river's own choice alone, which honours the `XKB_DEFAULT_*`
|
||||
/// environment variables and otherwise gives you `us`.
|
||||
pub const keymap: input.Keymap = .{
|
||||
// .layout = "us,se",
|
||||
// .options = "grp:alt_shift_toggle,caps:escape",
|
||||
};
|
||||
|
||||
/// Key repeat as applications see it — not to be confused with
|
||||
/// `binding_repeat_delay` above, which is how fast a held-down att_wm binding
|
||||
/// re-fires. Per-device overrides go in `input_rules`.
|
||||
///
|
||||
/// These are river's own defaults, so leaving them alone changes nothing.
|
||||
pub const repeat: input.Repeat = .{
|
||||
// Repeats per second. Zero turns key repeat off.
|
||||
.rate = 70,
|
||||
// Milliseconds a key is held before repeating starts.
|
||||
.delay = 150,
|
||||
};
|
||||
|
||||
/// Per-device settings, matched on name and type. `name` is a glob, so `*` does
|
||||
/// the work of writing out "ELAN0501:00 04F3:3060 Touchpad" in full.
|
||||
///
|
||||
/// att_wm logs one line per device as it appears — name and type — which is where
|
||||
/// to find the names; there is no `list-inputs` command because the protocol
|
||||
/// shows input devices to the window manager alone.
|
||||
///
|
||||
/// Every setting defaults to null, meaning "leave libinput's own default". Rules
|
||||
/// are applied in order and a later one overrides an earlier one field by field.
|
||||
pub const input_rules = [_]input.Rule{
|
||||
// A laptop touchpad. Tap to click and ignoring the pad mid-keystroke are
|
||||
// near-universally wanted; scroll direction and click method are matters of
|
||||
// taste, so they are left to you.
|
||||
.{
|
||||
.name = "*Touchpad*",
|
||||
.tap = true,
|
||||
.disable_while_typing = true,
|
||||
// .natural_scroll = true,
|
||||
// .click_method = .clickfinger,
|
||||
},
|
||||
|
||||
// A per-device key repeat, faster than the default above.
|
||||
// .{ .type = .keyboard, .repeat = .{ .rate = 50, .delay = 250 } },
|
||||
|
||||
// Confining a touchscreen or pen to one output is what makes touch follow
|
||||
// display rotation: mapped devices have the output's transform applied to
|
||||
// every event, so `wlr-randr --transform` or rot8 rotates touch with the
|
||||
// screen — no rotation hook, no calibration matrix. It is also what stops a
|
||||
// touchscreen spanning both monitors on a multi-head setup.
|
||||
//
|
||||
// The glob catches both halves of the panel — "Wacom HID 5380 Finger" is the
|
||||
// touchscreen and "... Pen" the stylus, which river reports as `touch` and
|
||||
// `tablet` respectively. `.{ .type = .touch, ... }` and a second rule for
|
||||
// `.tablet` would do the same job without naming the hardware.
|
||||
.{ .name = "Wacom HID 5380*", .map_to_output = "eDP-1" },
|
||||
};
|
||||
|
||||
// ─── Rules ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Matched against a window's app_id and title. A null field matches anything.
|
||||
pub const Rule = struct {
|
||||
app_id: ?[]const u8 = null,
|
||||
title: ?[]const u8 = null,
|
||||
tags: ?u32 = null,
|
||||
floating: ?bool = null,
|
||||
};
|
||||
|
||||
pub const rules = [_]Rule{
|
||||
.{ .app_id = "pavucontrol", .floating = true },
|
||||
.{ .app_id = "org.pulseaudio.pavucontrol", .floating = true },
|
||||
.{ .title = "Picture-in-Picture", .floating = true },
|
||||
};
|
||||
|
||||
// ─── Key bindings ────────────────────────────────────────────────────────────
|
||||
|
||||
pub const keys = tagKeys() ++ [_]Key{
|
||||
.{ .mods = mod | Mods.shift, .keysym = xkb.Keysym.Return, .action = .{ .spawn = &terminal } },
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.p, .action = .{ .spawn = &menu } },
|
||||
.{ .mods = mod | Mods.shift, .keysym = xkb.Keysym.c, .action = .close },
|
||||
.{ .mods = mod | Mods.shift, .keysym = xkb.Keysym.q, .action = .quit },
|
||||
.{ .mods = mod | Mods.ctrl | Mods.shift, .keysym = xkb.Keysym.q, .action = .exit_session },
|
||||
|
||||
// Focus and arrangement.
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.j, .action = .{ .focus = .next } },
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.k, .action = .{ .focus = .prev } },
|
||||
.{ .mods = mod | Mods.shift, .keysym = xkb.Keysym.j, .action = .{ .swap = .next } },
|
||||
.{ .mods = mod | Mods.shift, .keysym = xkb.Keysym.k, .action = .{ .swap = .prev } },
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.Return, .action = .zoom },
|
||||
|
||||
// Master area.
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.h, .action = .{ .mfact = .{ .relative = -0.05 } } },
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.l, .action = .{ .mfact = .{ .relative = 0.05 } } },
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.i, .action = .{ .nmaster = .{ .relative = 1 } } },
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.d, .action = .{ .nmaster = .{ .relative = -1 } } },
|
||||
|
||||
// Layouts.
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.t, .action = .{ .set_layout = .master } },
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.m, .action = .{ .set_layout = .monocle } },
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.u, .action = .{ .set_layout = .tabbed } },
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.space, .action = .toggle_layout },
|
||||
.{ .mods = mod | Mods.shift, .keysym = xkb.Keysym.space, .action = .toggle_float },
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.f, .action = .toggle_fullscreen },
|
||||
|
||||
// Tags.
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.@"0", .action = .{ .view = action.all_tags } },
|
||||
.{ .mods = mod | Mods.shift, .keysym = xkb.Keysym.@"0", .action = .{ .tag = action.all_tags } },
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.Tab, .action = .view_prev },
|
||||
|
||||
// Outputs.
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.comma, .action = .{ .focus_output = .prev } },
|
||||
.{ .mods = mod, .keysym = xkb.Keysym.period, .action = .{ .focus_output = .next } },
|
||||
.{ .mods = mod | Mods.shift, .keysym = xkb.Keysym.comma, .action = .{ .send_to_output = .prev } },
|
||||
.{ .mods = mod | Mods.shift, .keysym = xkb.Keysym.period, .action = .{ .send_to_output = .next } },
|
||||
};
|
||||
|
||||
/// dwm's TAGKEYS macro: Mod+N views, Mod+Shift+N tags, Mod+Ctrl+N toggles the
|
||||
/// view, Mod+Ctrl+Shift+N toggles the window's tag.
|
||||
fn tagKeys() [action.tag_count * 4]Key {
|
||||
// Evaluated at comptime, so the loop costs nothing at runtime.
|
||||
@setEvalBranchQuota(10_000);
|
||||
var out: [action.tag_count * 4]Key = undefined;
|
||||
for (0..action.tag_count) |i| {
|
||||
const mask: u32 = @as(u32, 1) << @intCast(i);
|
||||
const sym: xkb.Keysym = @enumFromInt(@intFromEnum(xkb.Keysym.@"1") + i);
|
||||
out[i * 4 + 0] = .{ .mods = mod, .keysym = sym, .action = .{ .view = mask } };
|
||||
out[i * 4 + 1] = .{ .mods = mod | Mods.shift, .keysym = sym, .action = .{ .tag = mask } };
|
||||
out[i * 4 + 2] = .{ .mods = mod | Mods.ctrl, .keysym = sym, .action = .{ .toggle_view = mask } };
|
||||
out[i * 4 + 3] = .{
|
||||
.mods = mod | Mods.ctrl | Mods.shift,
|
||||
.keysym = sym,
|
||||
.action = .{ .toggle_tag = mask },
|
||||
};
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ─── Pointer bindings ────────────────────────────────────────────────────────
|
||||
|
||||
pub const buttons = [_]Button{
|
||||
.{ .mods = mod, .button = btn.left, .action = .move },
|
||||
.{ .mods = mod, .button = btn.right, .action = .resize },
|
||||
};
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
//! att_wmctl - drive a running att_wm over its IPC socket.
|
||||
|
||||
const std = @import("std");
|
||||
const posix = std.posix;
|
||||
const linux = std.os.linux;
|
||||
const sys = @import("sys.zig");
|
||||
|
||||
const sock = @import("sock.zig");
|
||||
|
||||
const usage =
|
||||
\\att_wmctl - control a running att_wm
|
||||
\\
|
||||
\\Usage: att_wmctl <command> [arguments]
|
||||
\\
|
||||
\\Tags are a 1-based index (3), a mask (0x4 or mask:4), or "all".
|
||||
\\Window ids are the "id" field of each window in the state JSON.
|
||||
\\
|
||||
\\Commands:
|
||||
\\ view <tag> Show only these tags
|
||||
\\ toggle-view <tag> Add or remove tags from the view
|
||||
\\ view-prev Return to the previously viewed tags
|
||||
\\ tag <tag> Move the focused window to these tags
|
||||
\\ toggle-tag <tag> Add or remove tags from the focused window
|
||||
\\
|
||||
\\ focus next|prev Move focus through the visible windows
|
||||
\\ focus-window <id> Focus this window, viewing its tags if need be
|
||||
\\ swap next|prev Move the focused window in the arrangement
|
||||
\\ zoom Promote the focused window to master
|
||||
\\ close Close the focused window
|
||||
\\ close-window <id> Close this window
|
||||
\\
|
||||
\\ layout master|monocle|tabbed
|
||||
\\ cycle-layout [next|prev]
|
||||
\\ toggle-layout Switch to the previous layout
|
||||
\\ nmaster <+1|-1|N> Windows in the master area
|
||||
\\ mfact <+0.05|-0.05|F> Master area width fraction
|
||||
\\
|
||||
\\ toggle-float Float or tile the focused window
|
||||
\\ toggle-fullscreen Fullscreen the focused window
|
||||
\\
|
||||
\\ focus-output next|prev
|
||||
\\ send-to-output next|prev
|
||||
\\
|
||||
\\ spawn <cmd> [args...] Run a command
|
||||
\\ quit Stop att_wm (river keeps running)
|
||||
\\ exit-session End the Wayland session
|
||||
\\
|
||||
\\ state Print the current state as JSON and exit
|
||||
\\ subscribe Stream a JSON state line on every change
|
||||
\\
|
||||
;
|
||||
|
||||
pub fn main(init: std.process.Init) !u8 {
|
||||
const gpa = init.gpa;
|
||||
|
||||
const args = try init.minimal.args.toSlice(init.arena.allocator());
|
||||
|
||||
if (args.len < 2 or isHelp(args[1])) {
|
||||
sys.writeAllBestEffort(1, usage);
|
||||
return if (args.len < 2) 1 else 0;
|
||||
}
|
||||
|
||||
const path = try sock.path(gpa, init.minimal.environ);
|
||||
defer gpa.free(path);
|
||||
|
||||
const fd = sys.socket(linux.AF.UNIX, linux.SOCK.STREAM | linux.SOCK.CLOEXEC, 0) catch |err| {
|
||||
std.log.err("failed to create socket: {s}", .{@errorName(err)});
|
||||
return 1;
|
||||
};
|
||||
defer sys.close(fd);
|
||||
|
||||
const addr = sys.sockaddrUn(path) catch {
|
||||
std.log.err("socket path too long: {s}", .{path});
|
||||
return 1;
|
||||
};
|
||||
sys.connect(fd, @ptrCast(&addr), sys.sockaddrUnLen(&addr)) catch |err| {
|
||||
std.log.err(
|
||||
"cannot reach att_wm at {s}: {s}\nIs att_wm running under this Wayland display?",
|
||||
.{ path, @errorName(err) },
|
||||
);
|
||||
return 1;
|
||||
};
|
||||
|
||||
// Reassemble argv into one newline-terminated line.
|
||||
var line: std.ArrayList(u8) = .empty;
|
||||
defer line.deinit(gpa);
|
||||
for (args[1..], 0..) |arg, i| {
|
||||
if (i > 0) try line.append(gpa, ' ');
|
||||
try line.appendSlice(gpa, arg);
|
||||
}
|
||||
try line.append(gpa, '\n');
|
||||
|
||||
try writeAll(fd, line.items);
|
||||
|
||||
const streaming = std.mem.eql(u8, args[1], "subscribe");
|
||||
return relay(fd, streaming);
|
||||
}
|
||||
|
||||
fn isHelp(arg: []const u8) bool {
|
||||
return std.mem.eql(u8, arg, "-h") or
|
||||
std.mem.eql(u8, arg, "--help") or
|
||||
std.mem.eql(u8, arg, "help");
|
||||
}
|
||||
|
||||
fn writeAll(fd: sys.fd_t, bytes: []const u8) !void {
|
||||
var written: usize = 0;
|
||||
while (written < bytes.len) {
|
||||
written += try sys.write(fd, bytes[written..]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy the reply to stdout. For one-shot commands att_wm closes the connection
|
||||
/// after replying, so this returns; `subscribe` runs until interrupted.
|
||||
fn relay(fd: sys.fd_t, streaming: bool) !u8 {
|
||||
var buf: [8192]u8 = undefined;
|
||||
// Copied out rather than aliased: `buf` is overwritten by later reads.
|
||||
var first: [3]u8 = undefined;
|
||||
var first_len: usize = 0;
|
||||
|
||||
while (true) {
|
||||
const n = sys.read(fd, &buf) catch |err| switch (err) {
|
||||
// att_wm closes the connection after replying to a one-shot
|
||||
// command; a reset here just means it got in first.
|
||||
error.ConnectionReset => break,
|
||||
else => {
|
||||
std.log.err("read failed: {s}", .{@errorName(err)});
|
||||
return 1;
|
||||
},
|
||||
};
|
||||
if (n == 0) break;
|
||||
|
||||
if (first_len == 0 and n > 0) {
|
||||
first_len = @min(n, first.len);
|
||||
@memcpy(first[0..first_len], buf[0..first_len]);
|
||||
}
|
||||
|
||||
// "ok" is the success acknowledgement for a command; printing it would
|
||||
// be noise, so swallow it and let the exit status speak.
|
||||
if (!streaming and std.mem.startsWith(u8, buf[0..n], "ok\n")) {
|
||||
if (n == 3) return 0;
|
||||
}
|
||||
try writeAll(1, buf[0..n]);
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, first[0..first_len], "err")) return 1;
|
||||
return 0;
|
||||
}
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
//! Input device configuration, as declared in config.zig.
|
||||
//!
|
||||
//! Like action.zig this module depends on nothing but the standard library, so
|
||||
//! that config.zig can import it without a cycle back into the window manager.
|
||||
//! `src/InputManager.zig` is what puts these values onto river's protocol
|
||||
//! objects.
|
||||
//!
|
||||
//! Every device setting is optional, and null means "leave it alone" — libinput
|
||||
//! picks per-device defaults that are usually right, so a rule should say only
|
||||
//! what it wants changed.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
/// The kind of device, mirroring `river_input_device_v1.type`.
|
||||
///
|
||||
/// Note that a touchpad reports `pointer`, not `touch`: libinput models it as a
|
||||
/// pointer that happens to support tapping. `touch` is a touchscreen.
|
||||
pub const Type = enum { keyboard, pointer, touch, tablet };
|
||||
|
||||
/// xkb rule names — the RMLVO that `setxkbmap` and every other Wayland
|
||||
/// compositor take. att_wm compiles these into a keymap and hands it to every
|
||||
/// keyboard river reports.
|
||||
///
|
||||
/// A null field is left to xkbcommon, which reads the `XKB_DEFAULT_*`
|
||||
/// environment variables and otherwise falls back to a plain `us` layout. So
|
||||
/// the default of all-null is exactly what you get without this protocol at all.
|
||||
pub const Keymap = struct {
|
||||
/// Rules file, e.g. "evdev". Rarely worth setting.
|
||||
rules: ?[]const u8 = null,
|
||||
model: ?[]const u8 = null,
|
||||
/// One layout, or several separated by commas: "us,se".
|
||||
layout: ?[]const u8 = null,
|
||||
/// Variants, positionally matching `layout`: "dvorak," is dvorak for the
|
||||
/// first layout and the default variant for the second.
|
||||
variant: ?[]const u8 = null,
|
||||
/// Comma separated, e.g. "grp:alt_shift_toggle,caps:escape". With more than
|
||||
/// one layout configured, a `grp:` option is how you switch between them —
|
||||
/// xkb does the switching itself, so att_wm needs no binding for it.
|
||||
options: ?[]const u8 = null,
|
||||
|
||||
/// True when nothing is set, in which case there is no point compiling a
|
||||
/// keymap: river's own default is already what we would produce.
|
||||
pub fn isDefault(self: Keymap) bool {
|
||||
inline for (std.meta.fields(Keymap)) |field| {
|
||||
if (@field(self, field.name) != null) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/// Key repeat as applied by the compositor to the focused client.
|
||||
///
|
||||
/// This is not the same thing as `config.binding_repeat_delay` and
|
||||
/// `config.binding_repeat_interval`, which govern how fast att_wm re-runs a held-down
|
||||
/// *binding*: river reports binding press and release and leaves repeating to
|
||||
/// us. These two are what every other application sees.
|
||||
/// The defaults are river's own, so a config that says nothing about repeat
|
||||
/// leaves keyboards exactly as they would have been.
|
||||
pub const Repeat = struct {
|
||||
/// Repeats per second. Zero disables key repeat entirely.
|
||||
rate: i32 = 40,
|
||||
/// Milliseconds a key must be held before repeating starts.
|
||||
delay: i32 = 400,
|
||||
};
|
||||
|
||||
pub const ButtonMap = enum {
|
||||
/// One finger left, two right, three middle. libinput's default.
|
||||
lrm,
|
||||
/// One finger left, two middle, three right.
|
||||
lmr,
|
||||
};
|
||||
|
||||
pub const DragLock = enum {
|
||||
disabled,
|
||||
/// Lifting the finger keeps the drag alive for a short timeout.
|
||||
timeout,
|
||||
/// Lifting the finger keeps the drag alive until the next tap.
|
||||
sticky,
|
||||
};
|
||||
|
||||
pub const ThreeFingerDrag = enum { disabled, three_finger, four_finger };
|
||||
|
||||
pub const ClickMethod = enum {
|
||||
none,
|
||||
/// Bottom of the touchpad split into left/middle/right zones.
|
||||
button_areas,
|
||||
/// Number of fingers on the pad decides the button.
|
||||
clickfinger,
|
||||
};
|
||||
|
||||
pub const AccelProfile = enum {
|
||||
/// No acceleration: movement maps to pointer travel one to one.
|
||||
none,
|
||||
/// Constant factor, no acceleration.
|
||||
flat,
|
||||
/// Speed-dependent acceleration. libinput's default for most devices.
|
||||
adaptive,
|
||||
};
|
||||
|
||||
pub const ScrollMethod = enum {
|
||||
none,
|
||||
two_finger,
|
||||
edge,
|
||||
/// Moving the device while `scroll_button` is held scrolls.
|
||||
on_button_down,
|
||||
};
|
||||
|
||||
pub const SendEvents = enum {
|
||||
enabled,
|
||||
disabled,
|
||||
/// Useful for a laptop touchpad that should go quiet when a mouse is
|
||||
/// plugged in.
|
||||
disabled_on_external_mouse,
|
||||
};
|
||||
|
||||
/// Matched against the name and type of every input device river reports.
|
||||
///
|
||||
/// att_wm logs one line per device at startup — name and type — which is where
|
||||
/// the names come from; there is no `list-inputs` to run because the protocol
|
||||
/// only shows devices to the window manager itself.
|
||||
pub const Rule = struct {
|
||||
/// Device name to match. `*` matches any run of characters, so
|
||||
/// `"*Touchpad*"` catches the usual "ELAN0501:00 04F3:3060 Touchpad"
|
||||
/// without you having to write it out. Null matches every device.
|
||||
name: ?[]const u8 = null,
|
||||
/// Restrict the rule to one kind of device. Null matches every kind.
|
||||
type: ?Type = null,
|
||||
|
||||
// ─── Keyboards ───
|
||||
|
||||
/// Per-device override of `config.repeat`.
|
||||
repeat: ?Repeat = null,
|
||||
|
||||
// ─── Pointers, touchpads, touchscreens ───
|
||||
|
||||
/// Confine a touchscreen or tablet to one output, named as river names it —
|
||||
/// "eDP-1", the same name the IPC `outputs` list uses.
|
||||
///
|
||||
/// Two reasons to want this. On multiple monitors an unmapped touchscreen
|
||||
/// spans the whole output layout, so touching the left of the panel lands on
|
||||
/// the wrong screen. And it is what makes touch survive **display
|
||||
/// rotation**: a mapped device has the output's transform applied to its
|
||||
/// coordinates on every event, so rotating with `wlr-randr` or rot8 rotates
|
||||
/// touch along with it, with no rotation hook and no calibration matrix.
|
||||
///
|
||||
/// Do not combine with an external calibration matrix for rotation — the two
|
||||
/// transforms compose, and the result is rotated twice.
|
||||
///
|
||||
/// Ignored for keyboards, which have no coordinates to map.
|
||||
map_to_output: ?[]const u8 = null,
|
||||
|
||||
/// Multiplier on scroll distance: 0.5 scrolls half as far, 3.0 three times
|
||||
/// as far. Applied by river rather than libinput, so it works on any
|
||||
/// pointer.
|
||||
scroll_factor: ?f64 = null,
|
||||
|
||||
/// Tap to click.
|
||||
tap: ?bool = null,
|
||||
/// Which button each finger count taps.
|
||||
tap_button_map: ?ButtonMap = null,
|
||||
/// Tap and then drag without a second tap.
|
||||
drag: ?bool = null,
|
||||
/// Whether lifting the finger mid-drag ends it.
|
||||
drag_lock: ?DragLock = null,
|
||||
/// Hold three (or four) fingers to drag.
|
||||
three_finger_drag: ?ThreeFingerDrag = null,
|
||||
|
||||
/// What a physical click on a touchpad means.
|
||||
click_method: ?ClickMethod = null,
|
||||
/// Which button each finger count clicks, under `.clickfinger`.
|
||||
clickfinger_button_map: ?ButtonMap = null,
|
||||
/// Left and right buttons together act as middle click.
|
||||
middle_emulation: ?bool = null,
|
||||
/// Swap left and right buttons.
|
||||
left_handed: ?bool = null,
|
||||
|
||||
/// Content follows the fingers rather than the viewport, as on a phone.
|
||||
natural_scroll: ?bool = null,
|
||||
scroll_method: ?ScrollMethod = null,
|
||||
/// Linux input event code — `input.btn.middle` and friends. Only meaningful
|
||||
/// with `scroll_method = .on_button_down`.
|
||||
scroll_button: ?u32 = null,
|
||||
/// Whether the scroll button must be held, or toggles.
|
||||
scroll_button_lock: ?bool = null,
|
||||
|
||||
accel_profile: ?AccelProfile = null,
|
||||
/// Pointer speed in [-1, 1]; 0 is the device's default.
|
||||
accel_speed: ?f64 = null,
|
||||
|
||||
/// Ignore the touchpad while the keyboard is being typed on.
|
||||
disable_while_typing: ?bool = null,
|
||||
/// Ignore the touchpad while the trackpoint is in use.
|
||||
disable_while_trackpointing: ?bool = null,
|
||||
|
||||
/// Clockwise rotation in degrees, for a device mounted sideways.
|
||||
rotation: ?u32 = null,
|
||||
|
||||
/// Whether the device sends events at all.
|
||||
send_events: ?SendEvents = null,
|
||||
|
||||
/// Fields that select which devices a rule applies to rather than
|
||||
/// configuring them, and so are not merged by `merge`.
|
||||
const selectors = .{ "name", "type" };
|
||||
|
||||
/// Fold `other` on top of `self`: every setting `other` states wins, every
|
||||
/// setting it leaves null keeps the value it had.
|
||||
///
|
||||
/// Rules are applied in the order they are declared, so a broad rule can set
|
||||
/// a house style and a later, narrower one can dissent from it — the same
|
||||
/// last-one-wins that dwm's window rules have.
|
||||
pub fn merge(self: Rule, other: Rule) Rule {
|
||||
var out = self;
|
||||
inline for (std.meta.fields(Rule)) |field| {
|
||||
comptime var is_selector = false;
|
||||
inline for (selectors) |name| {
|
||||
if (comptime std.mem.eql(u8, field.name, name)) is_selector = true;
|
||||
}
|
||||
if (!is_selector) {
|
||||
if (@field(other, field.name)) |v| @field(out, field.name) = v;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/// True if this rule should apply to a device with the given name and type.
|
||||
pub fn matchesDevice(self: Rule, device_name: []const u8, device_type: Type) bool {
|
||||
if (self.type) |t| {
|
||||
if (t != device_type) return false;
|
||||
}
|
||||
if (self.name) |pattern| {
|
||||
if (!matches(pattern, device_name)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/// Glob match supporting `*` as "any run of characters, including none".
|
||||
///
|
||||
/// Deliberately no `?` or character classes: device names are long, noisy and
|
||||
/// full of punctuation, and `*` on either end is all anyone needs to pin one
|
||||
/// down. Iterative with a backtrack point rather than recursive, so a pattern
|
||||
/// like `"*a*a*a*"` cannot blow the stack.
|
||||
pub fn matches(pattern: []const u8, name: []const u8) bool {
|
||||
var p: usize = 0;
|
||||
var n: usize = 0;
|
||||
// Where to resume if the run we are in turns out not to match: the `*` that
|
||||
// let us in, and how far it had consumed.
|
||||
var star: ?usize = null;
|
||||
var star_n: usize = 0;
|
||||
|
||||
while (n < name.len) {
|
||||
if (p < pattern.len and pattern[p] == '*') {
|
||||
star = p;
|
||||
p += 1;
|
||||
star_n = n;
|
||||
} else if (p < pattern.len and pattern[p] == name[n]) {
|
||||
p += 1;
|
||||
n += 1;
|
||||
} else if (star) |s| {
|
||||
// Let the last `*` swallow one more byte and try again.
|
||||
p = s + 1;
|
||||
star_n += 1;
|
||||
n = star_n;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Trailing `*`s can still match the empty remainder.
|
||||
while (p < pattern.len and pattern[p] == '*') p += 1;
|
||||
return p == pattern.len;
|
||||
}
|
||||
|
||||
/// Linux input event codes for the buttons worth binding to scrolling. The same
|
||||
/// values `action.btn` has; duplicated rather than imported so this module keeps
|
||||
/// its single dependency on the standard library.
|
||||
pub const btn = struct {
|
||||
pub const left: u32 = 0x110;
|
||||
pub const right: u32 = 0x111;
|
||||
pub const middle: u32 = 0x112;
|
||||
};
|
||||
+398
@@ -0,0 +1,398 @@
|
||||
//! JSON-lines IPC over a unix socket.
|
||||
//!
|
||||
//! Two things talk to this: bars (quickshell) which send `subscribe` and then
|
||||
//! read a state object every time anything changes, and `att_wmctl` which sends
|
||||
//! one command and reads one reply. Both directions are newline delimited so a
|
||||
//! quickshell `SplitParser` can consume the stream directly.
|
||||
|
||||
const std = @import("std");
|
||||
const posix = std.posix;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const linux = std.os.linux;
|
||||
const sys = @import("sys.zig");
|
||||
|
||||
const act = @import("action");
|
||||
const config = @import("config");
|
||||
|
||||
const Wm = @import("Wm.zig");
|
||||
|
||||
/// Generous, but a runaway subscriber must not be able to make the window
|
||||
/// manager grow without bound.
|
||||
const max_out_buffer = 1 << 20;
|
||||
const max_in_buffer = 64 * 1024;
|
||||
|
||||
const sock = @import("sock.zig");
|
||||
|
||||
const Client = struct {
|
||||
fd: sys.fd_t,
|
||||
/// Receives a state object on every change.
|
||||
subscribed: bool = false,
|
||||
in: std.ArrayList(u8) = .empty,
|
||||
out: std.ArrayList(u8) = .empty,
|
||||
/// Close once the output buffer has drained.
|
||||
closing: bool = false,
|
||||
|
||||
fn deinit(self: *Client, gpa: Allocator) void {
|
||||
self.in.deinit(gpa);
|
||||
self.out.deinit(gpa);
|
||||
sys.close(self.fd);
|
||||
}
|
||||
};
|
||||
|
||||
pub const Ipc = struct {
|
||||
gpa: Allocator,
|
||||
path: []u8,
|
||||
listener: sys.fd_t,
|
||||
clients: std.ArrayList(*Client) = .empty,
|
||||
|
||||
pub fn init(gpa: Allocator, environ: std.process.Environ) !Ipc {
|
||||
const path = try sock.path(gpa, environ);
|
||||
errdefer gpa.free(path);
|
||||
|
||||
// A socket left behind by a crashed instance would block bind(); only
|
||||
// remove it if nothing is listening, so we never kick out a running
|
||||
// window manager.
|
||||
if (isStale(path)) sys.unlink(path);
|
||||
|
||||
const listener = try sys.socket(
|
||||
linux.AF.UNIX,
|
||||
linux.SOCK.STREAM | linux.SOCK.NONBLOCK | linux.SOCK.CLOEXEC,
|
||||
0,
|
||||
);
|
||||
errdefer sys.close(listener);
|
||||
|
||||
const addr = try sys.sockaddrUn(path);
|
||||
try sys.bind(listener, @ptrCast(&addr), sys.sockaddrUnLen(&addr));
|
||||
try sys.listen(listener, 16);
|
||||
|
||||
std.log.info("ipc socket: {s}", .{path});
|
||||
|
||||
return .{ .gpa = gpa, .path = path, .listener = listener };
|
||||
}
|
||||
|
||||
/// True if a socket file is left over from a crashed instance. Connecting
|
||||
/// is the only reliable test: a refused connection means nobody is
|
||||
/// listening, whereas a missing file is not stale at all and a successful
|
||||
/// connection means another att_wm owns it.
|
||||
fn isStale(path: []const u8) bool {
|
||||
const probe = sys.socket(linux.AF.UNIX, linux.SOCK.STREAM | linux.SOCK.CLOEXEC, 0) catch return false;
|
||||
defer sys.close(probe);
|
||||
const addr = sys.sockaddrUn(path) catch return false;
|
||||
sys.connect(probe, @ptrCast(&addr), sys.sockaddrUnLen(&addr)) catch |err| {
|
||||
return err == error.ConnectionRefused;
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Ipc) void {
|
||||
for (self.clients.items) |client| {
|
||||
client.deinit(self.gpa);
|
||||
self.gpa.destroy(client);
|
||||
}
|
||||
self.clients.deinit(self.gpa);
|
||||
sys.close(self.listener);
|
||||
sys.unlink(self.path);
|
||||
self.gpa.free(self.path);
|
||||
}
|
||||
|
||||
/// Append the listener and every client fd, in that order. `handle` expects
|
||||
/// the same slice back.
|
||||
pub fn pollFds(self: *Ipc, fds: *std.ArrayList(posix.pollfd), gpa: Allocator) !void {
|
||||
try fds.append(gpa, .{ .fd = self.listener, .events = posix.POLL.IN, .revents = 0 });
|
||||
for (self.clients.items) |client| {
|
||||
var events: i16 = posix.POLL.IN;
|
||||
if (client.out.items.len > 0) events |= posix.POLL.OUT;
|
||||
try fds.append(gpa, .{ .fd = client.fd, .events = events, .revents = 0 });
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle(self: *Ipc, wm: *Wm, fds: []posix.pollfd) !void {
|
||||
if (fds.len == 0) return;
|
||||
|
||||
if (fds[0].revents & posix.POLL.IN != 0) self.accept();
|
||||
|
||||
// Walk the poll results and look each client up by fd rather than by
|
||||
// position. Dropping a client shifts the list, so index-based pairing
|
||||
// would hand the next client the departed one's revents — and a HUP
|
||||
// from a finished att_wmctl would then disconnect a subscribed bar.
|
||||
for (fds[1..]) |pfd| {
|
||||
const idx = self.indexOfFd(pfd.fd) orelse continue;
|
||||
const client = self.clients.items[idx];
|
||||
var drop = false;
|
||||
|
||||
if (pfd.revents & (posix.POLL.HUP | posix.POLL.ERR | posix.POLL.NVAL) != 0) {
|
||||
drop = true;
|
||||
} else {
|
||||
if (pfd.revents & posix.POLL.IN != 0) drop = !self.read(wm, client);
|
||||
if (!drop and pfd.revents & posix.POLL.OUT != 0) drop = !self.write(client);
|
||||
}
|
||||
|
||||
if (!drop and client.closing and client.out.items.len == 0) drop = true;
|
||||
|
||||
if (drop) {
|
||||
_ = self.clients.orderedRemove(idx);
|
||||
client.deinit(self.gpa);
|
||||
self.gpa.destroy(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn indexOfFd(self: *Ipc, fd: sys.fd_t) ?usize {
|
||||
for (self.clients.items, 0..) |client, i| {
|
||||
if (client.fd == fd) return i;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn accept(self: *Ipc) void {
|
||||
while (true) {
|
||||
const fd = sys.accept4(
|
||||
self.listener,
|
||||
linux.SOCK.NONBLOCK | linux.SOCK.CLOEXEC,
|
||||
) catch return;
|
||||
|
||||
const client = self.gpa.create(Client) catch {
|
||||
sys.close(fd);
|
||||
return;
|
||||
};
|
||||
client.* = .{ .fd = fd };
|
||||
self.clients.append(self.gpa, client) catch {
|
||||
client.deinit(self.gpa);
|
||||
self.gpa.destroy(client);
|
||||
return;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns false if the client should be dropped.
|
||||
fn read(self: *Ipc, wm: *Wm, client: *Client) bool {
|
||||
var buf: [4096]u8 = undefined;
|
||||
while (true) {
|
||||
const n = sys.read(client.fd, &buf) catch |err| switch (err) {
|
||||
error.Again => break,
|
||||
else => return false,
|
||||
};
|
||||
if (n == 0) return false;
|
||||
if (client.in.items.len + n > max_in_buffer) return false;
|
||||
client.in.appendSlice(self.gpa, buf[0..n]) catch return false;
|
||||
}
|
||||
|
||||
while (std.mem.indexOfScalar(u8, client.in.items, '\n')) |idx| {
|
||||
const line = client.in.items[0..idx];
|
||||
self.command(wm, client, line);
|
||||
// Drop the consumed line, including its newline.
|
||||
const rest = client.in.items[idx + 1 ..];
|
||||
std.mem.copyForwards(u8, client.in.items, rest);
|
||||
client.in.shrinkRetainingCapacity(rest.len);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Returns false if the client should be dropped.
|
||||
fn write(self: *Ipc, client: *Client) bool {
|
||||
while (client.out.items.len > 0) {
|
||||
const n = sys.write(client.fd, client.out.items) catch |err| switch (err) {
|
||||
error.Again => return true,
|
||||
else => return false,
|
||||
};
|
||||
const rest = client.out.items[n..];
|
||||
std.mem.copyForwards(u8, client.out.items, rest);
|
||||
client.out.shrinkRetainingCapacity(rest.len);
|
||||
}
|
||||
_ = self;
|
||||
return true;
|
||||
}
|
||||
|
||||
fn send(self: *Ipc, client: *Client, bytes: []const u8) void {
|
||||
if (client.out.items.len + bytes.len > max_out_buffer) {
|
||||
// The peer is not reading. Dropping it beats unbounded growth.
|
||||
client.closing = true;
|
||||
client.out.clearRetainingCapacity();
|
||||
return;
|
||||
}
|
||||
client.out.appendSlice(self.gpa, bytes) catch {
|
||||
client.closing = true;
|
||||
};
|
||||
}
|
||||
|
||||
fn command(self: *Ipc, wm: *Wm, client: *Client, line_raw: []const u8) void {
|
||||
const line = std.mem.trim(u8, line_raw, " \t\r");
|
||||
if (line.len == 0) return;
|
||||
|
||||
var argv: std.ArrayList([]const u8) = .empty;
|
||||
defer argv.deinit(self.gpa);
|
||||
var it = std.mem.tokenizeAny(u8, line, " \t");
|
||||
while (it.next()) |tok| argv.append(self.gpa, tok) catch return;
|
||||
if (argv.items.len == 0) return;
|
||||
|
||||
const cmd = argv.items[0];
|
||||
|
||||
if (std.mem.eql(u8, cmd, "subscribe")) {
|
||||
client.subscribed = true;
|
||||
self.sendState(wm, client);
|
||||
return;
|
||||
}
|
||||
if (std.mem.eql(u8, cmd, "state")) {
|
||||
self.sendState(wm, client);
|
||||
// A subscriber asking for state is refreshing, not saying goodbye.
|
||||
if (!client.subscribed) client.closing = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const action = act.parse(argv.items) catch |err| {
|
||||
var buf: [128]u8 = undefined;
|
||||
const msg = std.fmt.bufPrint(&buf, "err {s}\n", .{@errorName(err)}) catch "err\n";
|
||||
self.send(client, msg);
|
||||
if (!client.subscribed) client.closing = true;
|
||||
return;
|
||||
};
|
||||
|
||||
wm.performIpc(action);
|
||||
self.send(client, "ok\n");
|
||||
// One-shot clients (att_wmctl) are done; subscribers stay connected so a
|
||||
// bar can drive the window manager over the same socket it listens on.
|
||||
if (!client.subscribed) client.closing = true;
|
||||
}
|
||||
|
||||
pub fn broadcast(self: *Ipc, wm: *Wm) !void {
|
||||
if (self.clients.items.len == 0) return;
|
||||
|
||||
var json: std.ArrayList(u8) = .empty;
|
||||
defer json.deinit(self.gpa);
|
||||
try encodeState(wm, self.gpa, &json);
|
||||
|
||||
for (self.clients.items) |client| {
|
||||
if (!client.subscribed or client.closing) continue;
|
||||
self.send(client, json.items);
|
||||
}
|
||||
|
||||
// Push it out now rather than waiting for the next poll, so bars update
|
||||
// in the same frame the change happens.
|
||||
for (self.clients.items) |client| {
|
||||
_ = self.write(client);
|
||||
}
|
||||
}
|
||||
|
||||
fn sendState(self: *Ipc, wm: *Wm, client: *Client) void {
|
||||
var json: std.ArrayList(u8) = .empty;
|
||||
defer json.deinit(self.gpa);
|
||||
encodeState(wm, self.gpa, &json) catch return;
|
||||
self.send(client, json.items);
|
||||
}
|
||||
};
|
||||
|
||||
/// Serialise the whole window manager state as one JSON object followed by a
|
||||
/// newline. Sending everything on every change keeps bars stateless, and the
|
||||
/// payload is small enough that diffing would not pay for itself.
|
||||
fn encodeState(wm: *Wm, gpa: Allocator, out: *std.ArrayList(u8)) !void {
|
||||
var allocating = std.Io.Writer.Allocating.fromArrayList(gpa, out);
|
||||
defer out.* = allocating.toArrayList();
|
||||
const w = &allocating.writer;
|
||||
|
||||
try w.writeAll("{\"tag_count\":");
|
||||
try w.print("{d}", .{act.tag_count});
|
||||
|
||||
try w.writeAll(",\"tag_names\":[");
|
||||
for (config.tag_names, 0..) |name, i| {
|
||||
if (i > 0) try w.writeAll(",");
|
||||
try writeJsonString(w, name);
|
||||
}
|
||||
try w.writeAll("]");
|
||||
|
||||
try w.print(",\"locked\":{s}", .{if (wm.locked) "true" else "false"});
|
||||
|
||||
try w.writeAll(",\"outputs\":[");
|
||||
for (wm.outputs.items, 0..) |output, oi| {
|
||||
if (oi > 0) try w.writeAll(",");
|
||||
|
||||
// A tag is "occupied" if any window carries it, and "urgent" is not
|
||||
// modelled: river-window-management-v1 has no attention-request event.
|
||||
var occupied: u32 = 0;
|
||||
for (wm.windows.items) |win| {
|
||||
if (win.output == output and !win.closed) occupied |= win.tags;
|
||||
}
|
||||
|
||||
// The layout and its knobs belong to the tag being viewed, so what is
|
||||
// published is whatever is in force right now.
|
||||
const st = output.state();
|
||||
|
||||
try w.writeAll("{\"name\":");
|
||||
try writeJsonString(w, output.displayName());
|
||||
try w.print(
|
||||
",\"focused\":{s},\"tags\":{d},\"occupied\":{d},\"layout\":\"{s}\",\"layout_symbol\":",
|
||||
.{
|
||||
if (wm.focused_output == output) "true" else "false",
|
||||
output.tags,
|
||||
occupied & act.all_tags,
|
||||
@tagName(st.layout),
|
||||
},
|
||||
);
|
||||
try writeJsonString(w, st.layout.symbol());
|
||||
try w.print(",\"nmaster\":{d},\"mfact\":{d:.3}", .{ st.nmaster, st.mfact });
|
||||
try w.print(
|
||||
",\"x\":{d},\"y\":{d},\"width\":{d},\"height\":{d}",
|
||||
.{ output.box.x, output.box.y, output.box.width, output.box.height },
|
||||
);
|
||||
// The area left after layer-shell exclusive zones, i.e. where windows
|
||||
// actually get laid out.
|
||||
const usable = output.layoutArea();
|
||||
try w.print(
|
||||
",\"usable\":{{\"x\":{d},\"y\":{d},\"width\":{d},\"height\":{d}}}",
|
||||
.{ usable.x, usable.y, usable.width, usable.height },
|
||||
);
|
||||
|
||||
try w.writeAll(",\"windows\":[");
|
||||
var first = true;
|
||||
for (wm.windows.items) |win| {
|
||||
if (win.output != output or win.closed) continue;
|
||||
if (!first) try w.writeAll(",");
|
||||
first = false;
|
||||
|
||||
try w.writeAll("{\"id\":");
|
||||
try writeJsonString(w, win.identifier orelse "");
|
||||
try w.writeAll(",\"title\":");
|
||||
try writeJsonString(w, win.title orelse "");
|
||||
try w.writeAll(",\"app_id\":");
|
||||
try writeJsonString(w, win.app_id orelse "");
|
||||
try w.print(
|
||||
",\"tags\":{d},\"focused\":{s},\"visible\":{s},\"floating\":{s},\"fullscreen\":{s}",
|
||||
.{
|
||||
win.tags,
|
||||
if (isFocused(wm, win)) "true" else "false",
|
||||
if (win.visible) "true" else "false",
|
||||
if (win.floating) "true" else "false",
|
||||
if (win.fullscreen) "true" else "false",
|
||||
},
|
||||
);
|
||||
try w.writeAll("}");
|
||||
}
|
||||
try w.writeAll("]}");
|
||||
}
|
||||
try w.writeAll("]}\n");
|
||||
}
|
||||
|
||||
fn isFocused(wm: *Wm, win: anytype) bool {
|
||||
for (wm.seats.items) |seat| {
|
||||
if (seat.focused == win) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn writeJsonString(w: *std.Io.Writer, s: []const u8) !void {
|
||||
try w.writeAll("\"");
|
||||
for (s) |c| switch (c) {
|
||||
'"' => try w.writeAll("\\\""),
|
||||
'\\' => try w.writeAll("\\\\"),
|
||||
'\n' => try w.writeAll("\\n"),
|
||||
'\r' => try w.writeAll("\\r"),
|
||||
'\t' => try w.writeAll("\\t"),
|
||||
else => {
|
||||
if (c < 0x20) {
|
||||
try w.print("\\u{x:0>4}", .{c});
|
||||
} else {
|
||||
try w.writeByte(c);
|
||||
}
|
||||
},
|
||||
};
|
||||
try w.writeAll("\"");
|
||||
}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
//! Pure layout geometry.
|
||||
//!
|
||||
//! Nothing here touches Wayland or window manager state: `arrange` is given an
|
||||
//! area and a window count and fills in a slice of cells. That keeps the
|
||||
//! tiling maths unit-testable without a compositor, which matters because the
|
||||
//! master/stack remainder handling is fiddly and easy to get subtly wrong.
|
||||
|
||||
const std = @import("std");
|
||||
const math = std.math;
|
||||
|
||||
const action = @import("action");
|
||||
|
||||
pub const Layout = action.Layout;
|
||||
|
||||
pub const Box = struct {
|
||||
x: i32 = 0,
|
||||
y: i32 = 0,
|
||||
width: i32 = 0,
|
||||
height: i32 = 0,
|
||||
|
||||
pub fn contains(self: Box, x: i32, y: i32) bool {
|
||||
return x >= self.x and x < self.x + self.width and
|
||||
y >= self.y and y < self.y + self.height;
|
||||
}
|
||||
|
||||
/// Shrink by `amount` on every side, never going below zero size.
|
||||
pub fn inset(self: Box, amount: i32) Box {
|
||||
return .{
|
||||
.x = self.x + amount,
|
||||
.y = self.y + amount,
|
||||
.width = @max(0, self.width - 2 * amount),
|
||||
.height = @max(0, self.height - 2 * amount),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Params = struct {
|
||||
/// The area available for tiling: the output minus any layer-shell
|
||||
/// exclusive zones.
|
||||
area: Box,
|
||||
nmaster: u32,
|
||||
mfact: f32,
|
||||
/// Gap between adjacent windows.
|
||||
gap: i32 = 0,
|
||||
/// Gap between the windows and the edge of the usable area.
|
||||
outer_gap: i32 = 0,
|
||||
/// Height of the tab bar strip in the tabbed layout.
|
||||
tabbar_height: i32 = 0,
|
||||
};
|
||||
|
||||
pub const Result = struct {
|
||||
/// Where the tab bar goes, if this layout has one.
|
||||
tabbar: ?Box = null,
|
||||
/// True when the layout stacks all windows in the same place, so only the
|
||||
/// topmost one is worth showing.
|
||||
stacked: bool = false,
|
||||
};
|
||||
|
||||
/// Whether a layout puts every window in the same place, so only the top one
|
||||
/// is rendered. `arrange` reports the same thing after the fact; this answers
|
||||
/// it for callers that need to know before the geometry is computed.
|
||||
pub fn stacks(layout: Layout) bool {
|
||||
return switch (layout) {
|
||||
.master => false,
|
||||
.monocle, .tabbed => true,
|
||||
};
|
||||
}
|
||||
|
||||
/// Fill `cells` with one rectangle per window, in arrangement order.
|
||||
///
|
||||
/// Each cell is the *outer* rectangle including space for the border; the
|
||||
/// caller insets by the border width to get the content geometry to propose.
|
||||
pub fn arrange(layout: Layout, p: Params, cells: []Box) Result {
|
||||
if (cells.len == 0) return .{};
|
||||
|
||||
const area = p.area.inset(p.outer_gap);
|
||||
|
||||
return switch (layout) {
|
||||
.master => tile(p, area, cells),
|
||||
.monocle => stack(p, area, cells, null),
|
||||
.tabbed => blk: {
|
||||
// Reserve the strip at the top for the tab bar. If the area is too
|
||||
// short to give the windows anything, drop the bar rather than
|
||||
// producing zero-height windows.
|
||||
if (area.height <= p.tabbar_height * 2) break :blk stack(p, area, cells, null);
|
||||
const bar: Box = .{
|
||||
.x = area.x,
|
||||
.y = area.y,
|
||||
.width = area.width,
|
||||
.height = p.tabbar_height,
|
||||
};
|
||||
const rest: Box = .{
|
||||
.x = area.x,
|
||||
.y = area.y + p.tabbar_height,
|
||||
.width = area.width,
|
||||
.height = area.height - p.tabbar_height,
|
||||
};
|
||||
break :blk stack(p, rest, cells, bar);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// dwm's tile(): `nmaster` windows share a column of width `mfact`, the rest
|
||||
/// share the remainder. Height is divided by "remaining space / remaining
|
||||
/// windows" so leftover pixels are absorbed rather than accumulating a gap at
|
||||
/// the bottom.
|
||||
fn tile(p: Params, area: Box, cells: []Box) Result {
|
||||
const n: u32 = @intCast(cells.len);
|
||||
const half_gap = @divTrunc(p.gap, 2);
|
||||
|
||||
const nmaster = @min(p.nmaster, n);
|
||||
|
||||
const mw: i32 = if (n > nmaster)
|
||||
(if (nmaster > 0) @as(i32, @intFromFloat(@as(f32, @floatFromInt(area.width)) * p.mfact)) else 0)
|
||||
else
|
||||
area.width;
|
||||
|
||||
var my: i32 = 0;
|
||||
var ty: i32 = 0;
|
||||
|
||||
for (cells, 0..) |*cell, i| {
|
||||
const idx: u32 = @intCast(i);
|
||||
if (idx < nmaster) {
|
||||
const remaining = nmaster - idx;
|
||||
const h = @divTrunc(area.height - my, @as(i32, @intCast(remaining)));
|
||||
cell.* = .{
|
||||
.x = area.x,
|
||||
.y = area.y + my,
|
||||
.width = mw,
|
||||
.height = h,
|
||||
};
|
||||
my += h;
|
||||
} else {
|
||||
const remaining = n - idx;
|
||||
const h = @divTrunc(area.height - ty, @as(i32, @intCast(remaining)));
|
||||
cell.* = .{
|
||||
.x = area.x + mw,
|
||||
.y = area.y + ty,
|
||||
.width = area.width - mw,
|
||||
.height = h,
|
||||
};
|
||||
ty += h;
|
||||
}
|
||||
if (half_gap > 0) cell.* = cell.inset(half_gap);
|
||||
}
|
||||
|
||||
return .{};
|
||||
}
|
||||
|
||||
/// Every window fills the whole area; only the top one is worth rendering.
|
||||
fn stack(p: Params, area: Box, cells: []Box, bar: ?Box) Result {
|
||||
const half_gap = @divTrunc(p.gap, 2);
|
||||
for (cells) |*cell| {
|
||||
cell.* = if (half_gap > 0) area.inset(half_gap) else area;
|
||||
}
|
||||
return .{ .tabbar = bar, .stacked = true };
|
||||
}
|
||||
|
||||
/// Split a tab bar into one rectangle per tab, absorbing the remainder into
|
||||
/// the leftmost tabs so the strip is exactly filled.
|
||||
pub fn tabRects(bar: Box, count: usize, out: []Box) void {
|
||||
std.debug.assert(out.len >= count);
|
||||
if (count == 0) return;
|
||||
|
||||
const n: i32 = @intCast(count);
|
||||
const base = @divTrunc(bar.width, n);
|
||||
var extra = @mod(bar.width, n);
|
||||
|
||||
var x = bar.x;
|
||||
for (out[0..count]) |*rect| {
|
||||
var w = base;
|
||||
if (extra > 0) {
|
||||
w += 1;
|
||||
extra -= 1;
|
||||
}
|
||||
rect.* = .{ .x = x, .y = bar.y, .width = w, .height = bar.height };
|
||||
x += w;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
const std = @import("std");
|
||||
const posix = std.posix;
|
||||
const sys = @import("sys.zig");
|
||||
|
||||
const Wm = @import("Wm.zig");
|
||||
|
||||
pub const std_options: std.Options = .{
|
||||
.log_level = if (@import("builtin").mode == .Debug) .debug else .info,
|
||||
};
|
||||
|
||||
const version = "0.1.0";
|
||||
|
||||
const usage =
|
||||
\\att_wm - a dwm-like window manager for the river Wayland compositor
|
||||
\\
|
||||
\\Usage: att_wm [options]
|
||||
\\
|
||||
\\att_wm is a river-window-management-v1 client and must be started by river
|
||||
\\0.4 or newer:
|
||||
\\
|
||||
\\ river -c att_wm
|
||||
\\
|
||||
\\Options:
|
||||
\\ -h, --help Show this help
|
||||
\\ -v, --version Show the version
|
||||
\\
|
||||
;
|
||||
|
||||
pub fn main(init: std.process.Init) !u8 {
|
||||
const gpa = init.gpa;
|
||||
|
||||
var args = init.minimal.args.iterate();
|
||||
_ = args.next();
|
||||
while (args.next()) |arg| {
|
||||
if (std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help")) {
|
||||
sys.writeAllBestEffort(1, usage);
|
||||
return 0;
|
||||
}
|
||||
if (std.mem.eql(u8, arg, "-v") or std.mem.eql(u8, arg, "--version")) {
|
||||
sys.writeAllBestEffort(1, version ++ "\n");
|
||||
return 0;
|
||||
}
|
||||
std.log.err("unknown argument: {s}", .{arg});
|
||||
sys.writeAllBestEffort(2, usage);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Spawned children are double-forked and reparented to init, so we never
|
||||
// wait on them. Ignoring SIGPIPE keeps a bar disconnecting mid-write from
|
||||
// taking the window manager down with it.
|
||||
const ignore: posix.Sigaction = .{
|
||||
.handler = .{ .handler = posix.SIG.IGN },
|
||||
.mask = posix.sigemptyset(),
|
||||
.flags = 0,
|
||||
};
|
||||
posix.sigaction(posix.SIG.PIPE, &ignore, null);
|
||||
|
||||
const wm = Wm.init(gpa, init.minimal.environ) catch |err| switch (err) {
|
||||
error.NoWindowManagerGlobal => return 1,
|
||||
else => {
|
||||
std.log.err("failed to start: {s}", .{@errorName(err)});
|
||||
return 1;
|
||||
},
|
||||
};
|
||||
defer wm.deinit();
|
||||
|
||||
wm.run() catch |err| {
|
||||
std.log.err("event loop failed: {s}", .{@errorName(err)});
|
||||
return 1;
|
||||
};
|
||||
|
||||
return 0;
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
//! Minimal wl_shm buffer pool for the tab bar.
|
||||
//!
|
||||
//! The tab bar is the only thing att_wm draws itself, and it draws nothing but
|
||||
//! solid rectangles, so this deliberately stops at "memfd, mmap, fill" rather
|
||||
//! than pulling in pixman or a font stack.
|
||||
|
||||
const std = @import("std");
|
||||
const posix = std.posix;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const sys = @import("sys.zig");
|
||||
|
||||
const wayland = @import("wayland");
|
||||
const wl = wayland.client.wl;
|
||||
|
||||
const layout = @import("layout.zig");
|
||||
const Box = layout.Box;
|
||||
|
||||
/// Two buffers is enough: we redraw at most once per render sequence and the
|
||||
/// compositor releases the previous one promptly.
|
||||
const buffer_count = 2;
|
||||
|
||||
pub const Buffer = struct {
|
||||
wl_buffer: *wl.Buffer,
|
||||
data: []align(std.heap.page_size_min) u8,
|
||||
width: i32,
|
||||
height: i32,
|
||||
/// Held by the compositor; must not be drawn into until released.
|
||||
busy: bool = false,
|
||||
|
||||
fn onRelease(_: *wl.Buffer, event: wl.Buffer.Event, self: *Buffer) void {
|
||||
switch (event) {
|
||||
.release => self.busy = false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pixels(self: *Buffer) []u32 {
|
||||
const count: usize = @intCast(self.width * self.height);
|
||||
const ptr: [*]u32 = @ptrCast(@alignCast(self.data.ptr));
|
||||
return ptr[0..count];
|
||||
}
|
||||
|
||||
/// Fill a rectangle, in buffer-local coordinates, clipped to the buffer.
|
||||
pub fn fill(self: *Buffer, rect: Box, argb: u32) void {
|
||||
const x0 = @max(0, rect.x);
|
||||
const y0 = @max(0, rect.y);
|
||||
const x1 = @min(self.width, rect.x + rect.width);
|
||||
const y1 = @min(self.height, rect.y + rect.height);
|
||||
if (x1 <= x0 or y1 <= y0) return;
|
||||
|
||||
const px = self.pixels();
|
||||
const stride: usize = @intCast(self.width);
|
||||
var y: i32 = y0;
|
||||
while (y < y1) : (y += 1) {
|
||||
const row_start = @as(usize, @intCast(y)) * stride;
|
||||
const from = row_start + @as(usize, @intCast(x0));
|
||||
const to = row_start + @as(usize, @intCast(x1));
|
||||
@memset(px[from..to], argb);
|
||||
}
|
||||
}
|
||||
|
||||
fn deinit(self: *Buffer, gpa: Allocator) void {
|
||||
self.wl_buffer.destroy();
|
||||
posix.munmap(self.data);
|
||||
gpa.destroy(self);
|
||||
}
|
||||
};
|
||||
|
||||
pub const Pool = struct {
|
||||
gpa: Allocator,
|
||||
shm: *wl.Shm,
|
||||
buffers: [buffer_count]?*Buffer = @splat(null),
|
||||
|
||||
pub fn init(gpa: Allocator, shm: *wl.Shm) Pool {
|
||||
return .{ .gpa = gpa, .shm = shm };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Pool) void {
|
||||
for (&self.buffers) |*slot| {
|
||||
if (slot.*) |buf| buf.deinit(self.gpa);
|
||||
slot.* = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a buffer of the requested size that the compositor is not
|
||||
/// currently reading from, creating or resizing one as needed.
|
||||
pub fn acquire(self: *Pool, width: i32, height: i32) !*Buffer {
|
||||
if (width <= 0 or height <= 0) return error.InvalidSize;
|
||||
|
||||
// Reuse an idle buffer that is already the right size.
|
||||
for (self.buffers) |maybe| {
|
||||
if (maybe) |buf| {
|
||||
if (!buf.busy and buf.width == width and buf.height == height) return buf;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise take a free slot, evicting an idle wrong-sized buffer.
|
||||
for (&self.buffers) |*slot| {
|
||||
if (slot.* == null) {
|
||||
slot.* = try self.create(width, height);
|
||||
return slot.*.?;
|
||||
}
|
||||
}
|
||||
for (&self.buffers) |*slot| {
|
||||
const buf = slot.*.?;
|
||||
if (!buf.busy) {
|
||||
buf.deinit(self.gpa);
|
||||
slot.* = try self.create(width, height);
|
||||
return slot.*.?;
|
||||
}
|
||||
}
|
||||
|
||||
return error.AllBuffersBusy;
|
||||
}
|
||||
|
||||
fn create(self: *Pool, width: i32, height: i32) !*Buffer {
|
||||
const stride = width * 4;
|
||||
const size: usize = @intCast(stride * height);
|
||||
|
||||
const fd = try posix.memfd_create("att_wm-shm", std.os.linux.MFD.CLOEXEC);
|
||||
defer sys.close(fd);
|
||||
try sys.ftruncate(fd, size);
|
||||
|
||||
const data = try posix.mmap(
|
||||
null,
|
||||
size,
|
||||
.{ .READ = true, .WRITE = true },
|
||||
.{ .TYPE = .SHARED },
|
||||
fd,
|
||||
0,
|
||||
);
|
||||
errdefer posix.munmap(data);
|
||||
|
||||
const shm_pool = try self.shm.createPool(fd, @intCast(size));
|
||||
defer shm_pool.destroy();
|
||||
|
||||
const wl_buffer = try shm_pool.createBuffer(0, width, height, stride, .argb8888);
|
||||
errdefer wl_buffer.destroy();
|
||||
|
||||
const buf = try self.gpa.create(Buffer);
|
||||
buf.* = .{
|
||||
.wl_buffer = wl_buffer,
|
||||
.data = data,
|
||||
.width = width,
|
||||
.height = height,
|
||||
};
|
||||
wl_buffer.setListener(*Buffer, Buffer.onRelease, buf);
|
||||
return buf;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
//! Where the IPC socket lives. Shared by the window manager and att_wmctl, so
|
||||
//! it deliberately imports nothing else.
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Environ = std.process.Environ;
|
||||
|
||||
/// One socket per Wayland display, so nested or parallel river sessions do not
|
||||
/// collide. `ATT_WM_SOCKET` overrides it outright.
|
||||
pub fn path(gpa: Allocator, environ: Environ) ![]u8 {
|
||||
if (environ.getPosix("ATT_WM_SOCKET")) |explicit| {
|
||||
return gpa.dupe(u8, explicit);
|
||||
}
|
||||
const display = environ.getPosix("WAYLAND_DISPLAY") orelse "wayland-0";
|
||||
if (environ.getPosix("XDG_RUNTIME_DIR")) |dir| {
|
||||
return std.fmt.allocPrint(gpa, "{s}/att_wm-{s}.sock", .{ dir, display });
|
||||
}
|
||||
return std.fmt.allocPrint(gpa, "/tmp/att_wm-{d}-{s}.sock", .{ std.os.linux.getuid(), display });
|
||||
}
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
//! Thin typed wrappers over the Linux syscalls att_wm needs.
|
||||
//!
|
||||
//! Zig 0.16 moved most of `std.posix` behind the new `std.Io` interface, which
|
||||
//! is the wrong shape for a window manager: everything here is a raw fd driven
|
||||
//! by a single `poll()` loop, with no allocator and no async runtime. Going
|
||||
//! straight to `std.os.linux` is both simpler and closer to what the code
|
||||
//! actually does.
|
||||
|
||||
const std = @import("std");
|
||||
const linux = std.os.linux;
|
||||
|
||||
/// Must be the linux decoder, not `std.posix.errno`: with libc linked the
|
||||
/// latter expects a libc-style -1 return and reports every raw syscall error
|
||||
/// as success, which then overflows the casts below.
|
||||
const errno = linux.errno;
|
||||
|
||||
pub const fd_t = linux.fd_t;
|
||||
pub const pid_t = linux.pid_t;
|
||||
pub const E = linux.E;
|
||||
|
||||
pub const Error = error{
|
||||
Again,
|
||||
Interrupted,
|
||||
ConnectionReset,
|
||||
AddressInUse,
|
||||
NotFound,
|
||||
PermissionDenied,
|
||||
ConnectionRefused,
|
||||
BrokenPipe,
|
||||
NameTooLong,
|
||||
OutOfMemory,
|
||||
Unexpected,
|
||||
};
|
||||
|
||||
fn check(rc: usize) Error!usize {
|
||||
return switch (errno(rc)) {
|
||||
.SUCCESS => rc,
|
||||
.AGAIN => error.Again,
|
||||
.INTR => error.Interrupted,
|
||||
.ADDRINUSE => error.AddressInUse,
|
||||
.NOENT => error.NotFound,
|
||||
.ACCES, .PERM => error.PermissionDenied,
|
||||
.CONNREFUSED => error.ConnectionRefused,
|
||||
.CONNRESET => error.ConnectionReset,
|
||||
.PIPE => error.BrokenPipe,
|
||||
.NAMETOOLONG => error.NameTooLong,
|
||||
.NOMEM => error.OutOfMemory,
|
||||
else => error.Unexpected,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn read(fd: fd_t, buf: []u8) Error!usize {
|
||||
if (buf.len == 0) return 0;
|
||||
while (true) {
|
||||
return check(linux.read(fd, buf.ptr, buf.len)) catch |err| switch (err) {
|
||||
error.Interrupted => continue,
|
||||
else => err,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write(fd: fd_t, bytes: []const u8) Error!usize {
|
||||
if (bytes.len == 0) return 0;
|
||||
return check(linux.write(fd, bytes.ptr, bytes.len));
|
||||
}
|
||||
|
||||
/// Write everything, retrying short writes. Best effort: errors are swallowed
|
||||
/// because every caller is emitting diagnostics or usage text.
|
||||
pub fn writeAllBestEffort(fd: fd_t, bytes: []const u8) void {
|
||||
var off: usize = 0;
|
||||
while (off < bytes.len) {
|
||||
off += write(fd, bytes[off..]) catch return;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close(fd: fd_t) void {
|
||||
_ = linux.close(fd);
|
||||
}
|
||||
|
||||
pub fn socket(domain: u32, socket_type: u32, protocol: u32) Error!fd_t {
|
||||
return @intCast(try check(linux.socket(domain, socket_type, protocol)));
|
||||
}
|
||||
|
||||
pub fn bind(fd: fd_t, addr: *const linux.sockaddr, len: linux.socklen_t) Error!void {
|
||||
_ = try check(linux.bind(fd, addr, len));
|
||||
}
|
||||
|
||||
pub fn listen(fd: fd_t, backlog: u31) Error!void {
|
||||
_ = try check(linux.listen(fd, backlog));
|
||||
}
|
||||
|
||||
pub fn accept4(fd: fd_t, flags: u32) Error!fd_t {
|
||||
return @intCast(try check(linux.accept4(fd, null, null, flags)));
|
||||
}
|
||||
|
||||
pub fn connect(fd: fd_t, addr: *const linux.sockaddr, len: linux.socklen_t) Error!void {
|
||||
_ = try check(linux.connect(fd, addr, len));
|
||||
}
|
||||
|
||||
pub fn ftruncate(fd: fd_t, length: u64) Error!void {
|
||||
_ = try check(linux.ftruncate(fd, @intCast(length)));
|
||||
}
|
||||
|
||||
/// Make a memfd immutable, so a compositor mapping it cannot have the bytes
|
||||
/// changed underneath it.
|
||||
pub fn addSeals(fd: fd_t, seals: usize) Error!void {
|
||||
_ = try check(linux.fcntl(fd, linux.F.ADD_SEALS, seals));
|
||||
}
|
||||
|
||||
pub fn timerfdCreate(flags: linux.TFD) Error!fd_t {
|
||||
return @intCast(try check(linux.timerfd_create(.MONOTONIC, flags)));
|
||||
}
|
||||
|
||||
pub fn timerfdSetTime(fd: fd_t, spec: *const linux.itimerspec) Error!void {
|
||||
_ = try check(linux.timerfd_settime(fd, .{}, spec, null));
|
||||
}
|
||||
|
||||
pub fn fork() Error!pid_t {
|
||||
return @intCast(try check(linux.fork()));
|
||||
}
|
||||
|
||||
pub fn setsid() void {
|
||||
_ = linux.setsid();
|
||||
}
|
||||
|
||||
pub fn exit(code: u8) noreturn {
|
||||
linux.exit(code);
|
||||
}
|
||||
|
||||
pub fn waitpid(pid: pid_t) void {
|
||||
var status: u32 = undefined;
|
||||
while (true) {
|
||||
const rc = linux.wait4(pid, &status, 0, null);
|
||||
switch (errno(rc)) {
|
||||
.INTR => continue,
|
||||
else => return,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a path. Best effort: the only caller is clearing a stale socket.
|
||||
pub fn unlink(path: []const u8) void {
|
||||
var buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
if (path.len >= buf.len) return;
|
||||
@memcpy(buf[0..path.len], path);
|
||||
buf[path.len] = 0;
|
||||
_ = linux.unlink(@ptrCast(&buf));
|
||||
}
|
||||
|
||||
/// Build a unix socket address. Paths must fit in sun_path with room for the
|
||||
/// terminating NUL.
|
||||
pub fn sockaddrUn(path: []const u8) Error!linux.sockaddr.un {
|
||||
var addr: linux.sockaddr.un = .{ .family = linux.AF.UNIX, .path = undefined };
|
||||
if (path.len >= addr.path.len) return error.NameTooLong;
|
||||
@memset(&addr.path, 0);
|
||||
@memcpy(addr.path[0..path.len], path);
|
||||
return addr;
|
||||
}
|
||||
|
||||
pub fn sockaddrUnLen(addr: *const linux.sockaddr.un) linux.socklen_t {
|
||||
_ = addr;
|
||||
return @sizeOf(linux.sockaddr.un);
|
||||
}
|
||||
|
||||
/// execvp: run `argv[0]`, searching PATH when it contains no slash.
|
||||
///
|
||||
/// Only ever called between fork() and exec in the child, so it must not
|
||||
/// allocate; the candidate path is assembled in a stack buffer.
|
||||
pub fn execvpe(
|
||||
argv: [*:null]const ?[*:0]const u8,
|
||||
envp: [*:null]const ?[*:0]const u8,
|
||||
path_env: ?[]const u8,
|
||||
) Error {
|
||||
const file = std.mem.span(argv[0].?);
|
||||
|
||||
if (std.mem.indexOfScalar(u8, file, '/') != null) {
|
||||
return execErr(linux.execve(argv[0].?, argv, envp));
|
||||
}
|
||||
|
||||
const search = path_env orelse "/usr/local/bin:/usr/bin:/bin";
|
||||
var buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
var last: Error = error.NotFound;
|
||||
|
||||
var it = std.mem.tokenizeScalar(u8, search, ':');
|
||||
while (it.next()) |dir| {
|
||||
if (dir.len + 1 + file.len + 1 > buf.len) continue;
|
||||
@memcpy(buf[0..dir.len], dir);
|
||||
buf[dir.len] = '/';
|
||||
@memcpy(buf[dir.len + 1 ..][0..file.len], file);
|
||||
buf[dir.len + 1 + file.len] = 0;
|
||||
const candidate: [*:0]const u8 = @ptrCast(&buf);
|
||||
|
||||
last = execErr(linux.execve(candidate, argv, envp));
|
||||
// ENOENT just means "not in this directory"; keep looking.
|
||||
if (last != error.NotFound) return last;
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
/// execve only returns on failure, so its result is always an error.
|
||||
fn execErr(rc: usize) Error {
|
||||
_ = check(rc) catch |err| return err;
|
||||
return error.Unexpected;
|
||||
}
|
||||
+425
@@ -0,0 +1,425 @@
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
|
||||
const act = @import("action");
|
||||
const input = @import("input");
|
||||
const layout = @import("layout.zig");
|
||||
const Box = layout.Box;
|
||||
|
||||
comptime {
|
||||
_ = act;
|
||||
_ = input;
|
||||
_ = @import("color.zig");
|
||||
}
|
||||
|
||||
const area: Box = .{ .x = 0, .y = 0, .width = 1000, .height = 600 };
|
||||
|
||||
fn params(nmaster: u32, mfact: f32) layout.Params {
|
||||
return .{ .area = area, .nmaster = nmaster, .mfact = mfact };
|
||||
}
|
||||
|
||||
// ─── master/stack tiling ─────────────────────────────────────────────────────
|
||||
|
||||
test "single window fills the whole area" {
|
||||
var cells: [1]Box = undefined;
|
||||
_ = layout.arrange(.master, params(1, 0.55), &cells);
|
||||
try testing.expectEqual(area, cells[0]);
|
||||
}
|
||||
|
||||
test "two windows split at mfact" {
|
||||
var cells: [2]Box = undefined;
|
||||
_ = layout.arrange(.master, params(1, 0.55), &cells);
|
||||
|
||||
try testing.expectEqual(@as(i32, 550), cells[0].width);
|
||||
try testing.expectEqual(@as(i32, 600), cells[0].height);
|
||||
|
||||
try testing.expectEqual(@as(i32, 550), cells[1].x);
|
||||
try testing.expectEqual(@as(i32, 450), cells[1].width);
|
||||
try testing.expectEqual(@as(i32, 600), cells[1].height);
|
||||
}
|
||||
|
||||
test "stack column divides height with no gap or overlap" {
|
||||
// Three windows, one master: the stack column holds two.
|
||||
var cells: [3]Box = undefined;
|
||||
_ = layout.arrange(.master, params(1, 0.5), &cells);
|
||||
|
||||
try testing.expectEqual(@as(i32, 600), cells[0].height);
|
||||
try testing.expectEqual(cells[1].y + cells[1].height, cells[2].y);
|
||||
try testing.expectEqual(area.height, cells[1].height + cells[2].height);
|
||||
}
|
||||
|
||||
test "odd heights are absorbed rather than leaving a gap at the bottom" {
|
||||
// 600 / 7 does not divide evenly; the last window must still end exactly
|
||||
// at the bottom edge.
|
||||
var cells: [7]Box = undefined;
|
||||
_ = layout.arrange(.master, params(0, 0.55), &cells);
|
||||
|
||||
var y: i32 = area.y;
|
||||
for (cells) |cell| {
|
||||
try testing.expectEqual(y, cell.y);
|
||||
y += cell.height;
|
||||
}
|
||||
try testing.expectEqual(area.y + area.height, y);
|
||||
}
|
||||
|
||||
test "nmaster zero puts every window in the stack column" {
|
||||
var cells: [3]Box = undefined;
|
||||
_ = layout.arrange(.master, params(0, 0.55), &cells);
|
||||
for (cells) |cell| {
|
||||
try testing.expectEqual(@as(i32, 0), cell.x);
|
||||
try testing.expectEqual(area.width, cell.width);
|
||||
}
|
||||
}
|
||||
|
||||
test "windows all fit in master when count does not exceed nmaster" {
|
||||
var cells: [2]Box = undefined;
|
||||
_ = layout.arrange(.master, params(3, 0.55), &cells);
|
||||
// No stack column, so master spans the full width.
|
||||
for (cells) |cell| {
|
||||
try testing.expectEqual(area.width, cell.width);
|
||||
}
|
||||
try testing.expectEqual(area.height, cells[0].height + cells[1].height);
|
||||
}
|
||||
|
||||
test "master column also divides height when nmaster exceeds one" {
|
||||
var cells: [4]Box = undefined;
|
||||
_ = layout.arrange(.master, params(2, 0.5), &cells);
|
||||
|
||||
try testing.expectEqual(@as(i32, 500), cells[0].width);
|
||||
try testing.expectEqual(@as(i32, 500), cells[1].width);
|
||||
try testing.expectEqual(area.height, cells[0].height + cells[1].height);
|
||||
try testing.expectEqual(area.height, cells[2].height + cells[3].height);
|
||||
}
|
||||
|
||||
// ─── stacking layouts ────────────────────────────────────────────────────────
|
||||
|
||||
test "monocle gives every window the full area and reports stacked" {
|
||||
var cells: [3]Box = undefined;
|
||||
const result = layout.arrange(.monocle, params(1, 0.55), &cells);
|
||||
|
||||
try testing.expect(result.stacked);
|
||||
try testing.expect(result.tabbar == null);
|
||||
for (cells) |cell| try testing.expectEqual(area, cell);
|
||||
}
|
||||
|
||||
test "stacks agrees with what arrange reports" {
|
||||
var cells: [2]Box = undefined;
|
||||
var p = params(1, 0.55);
|
||||
p.tabbar_height = 22;
|
||||
for ([_]layout.Layout{ .master, .monocle, .tabbed }) |mode| {
|
||||
try testing.expectEqual(layout.arrange(mode, p, &cells).stacked, layout.stacks(mode));
|
||||
}
|
||||
}
|
||||
|
||||
test "tabbed reserves the bar strip above the windows" {
|
||||
var cells: [3]Box = undefined;
|
||||
var p = params(1, 0.55);
|
||||
p.tabbar_height = 22;
|
||||
const result = layout.arrange(.tabbed, p, &cells);
|
||||
|
||||
try testing.expect(result.stacked);
|
||||
const bar = result.tabbar.?;
|
||||
try testing.expectEqual(@as(i32, 22), bar.height);
|
||||
try testing.expectEqual(area.y, bar.y);
|
||||
|
||||
// Windows start below the bar and the two together cover the area exactly.
|
||||
for (cells) |cell| {
|
||||
try testing.expectEqual(area.y + 22, cell.y);
|
||||
try testing.expectEqual(area.height - 22, cell.height);
|
||||
}
|
||||
}
|
||||
|
||||
test "tabbed drops the bar rather than crushing the windows" {
|
||||
var cells: [2]Box = undefined;
|
||||
var p: layout.Params = .{
|
||||
.area = .{ .x = 0, .y = 0, .width = 400, .height = 30 },
|
||||
.nmaster = 1,
|
||||
.mfact = 0.55,
|
||||
.tabbar_height = 22,
|
||||
};
|
||||
const result = layout.arrange(.tabbed, p, &cells);
|
||||
try testing.expect(result.tabbar == null);
|
||||
try testing.expectEqual(@as(i32, 30), cells[0].height);
|
||||
p.tabbar_height = 0;
|
||||
}
|
||||
|
||||
test "no windows is not a crash" {
|
||||
var cells: [0]Box = undefined;
|
||||
const result = layout.arrange(.master, params(1, 0.55), &cells);
|
||||
try testing.expect(result.tabbar == null);
|
||||
}
|
||||
|
||||
// ─── gaps ────────────────────────────────────────────────────────────────────
|
||||
|
||||
test "outer gap insets the whole area" {
|
||||
var cells: [1]Box = undefined;
|
||||
var p = params(1, 0.55);
|
||||
p.outer_gap = 10;
|
||||
_ = layout.arrange(.master, p, &cells);
|
||||
|
||||
try testing.expectEqual(@as(i32, 10), cells[0].x);
|
||||
try testing.expectEqual(@as(i32, 10), cells[0].y);
|
||||
try testing.expectEqual(@as(i32, 980), cells[0].width);
|
||||
try testing.expectEqual(@as(i32, 580), cells[0].height);
|
||||
}
|
||||
|
||||
test "inner gap separates adjacent windows" {
|
||||
var cells: [2]Box = undefined;
|
||||
var p = params(1, 0.5);
|
||||
p.gap = 10;
|
||||
_ = layout.arrange(.master, p, &cells);
|
||||
|
||||
// Each cell shrinks by half the gap per side, leaving a full gap between.
|
||||
const right_of_master = cells[0].x + cells[0].width;
|
||||
try testing.expect(cells[1].x - right_of_master == 10);
|
||||
}
|
||||
|
||||
// ─── tab rectangles ──────────────────────────────────────────────────────────
|
||||
|
||||
test "tab rects tile the bar exactly with no rounding gap" {
|
||||
const bar: Box = .{ .x = 5, .y = 0, .width = 101, .height = 22 };
|
||||
var rects: [4]Box = undefined;
|
||||
layout.tabRects(bar, 4, &rects);
|
||||
|
||||
try testing.expectEqual(bar.x, rects[0].x);
|
||||
var total: i32 = 0;
|
||||
for (rects, 0..) |rect, i| {
|
||||
total += rect.width;
|
||||
if (i > 0) {
|
||||
try testing.expectEqual(rects[i - 1].x + rects[i - 1].width, rect.x);
|
||||
}
|
||||
}
|
||||
try testing.expectEqual(bar.width, total);
|
||||
try testing.expectEqual(bar.x + bar.width, rects[3].x + rects[3].width);
|
||||
}
|
||||
|
||||
test "single tab spans the bar" {
|
||||
const bar: Box = .{ .x = 0, .y = 0, .width = 300, .height = 22 };
|
||||
var rects: [1]Box = undefined;
|
||||
layout.tabRects(bar, 1, &rects);
|
||||
try testing.expectEqual(@as(i32, 300), rects[0].width);
|
||||
}
|
||||
|
||||
// ─── Box helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
test "inset never produces negative dimensions" {
|
||||
const tiny: Box = .{ .x = 0, .y = 0, .width = 4, .height = 4 };
|
||||
const r = tiny.inset(10);
|
||||
try testing.expectEqual(@as(i32, 0), r.width);
|
||||
try testing.expectEqual(@as(i32, 0), r.height);
|
||||
}
|
||||
|
||||
test "contains is half open on the far edges" {
|
||||
const b: Box = .{ .x = 10, .y = 10, .width = 100, .height = 50 };
|
||||
try testing.expect(b.contains(10, 10));
|
||||
try testing.expect(b.contains(109, 59));
|
||||
try testing.expect(!b.contains(110, 30));
|
||||
try testing.expect(!b.contains(9, 30));
|
||||
}
|
||||
|
||||
// ─── per-tag settings slots ──────────────────────────────────────────────────
|
||||
|
||||
test "each single tag gets its own settings slot" {
|
||||
for (0..act.tag_count) |i| {
|
||||
const mask = @as(u32, 1) << @intCast(i);
|
||||
try testing.expectEqual(i + 1, act.tagSlot(mask));
|
||||
}
|
||||
}
|
||||
|
||||
test "views of more than one tag share the shared slot" {
|
||||
try testing.expectEqual(@as(usize, 0), act.tagSlot(0b11));
|
||||
try testing.expectEqual(@as(usize, 0), act.tagSlot(0b101));
|
||||
try testing.expectEqual(@as(usize, 0), act.tagSlot(act.all_tags));
|
||||
}
|
||||
|
||||
test "an empty view falls back to the shared slot" {
|
||||
try testing.expectEqual(@as(usize, 0), act.tagSlot(0));
|
||||
}
|
||||
|
||||
test "bits above the tag range do not affect the slot" {
|
||||
// A single valid tag stays on its own slot even with junk in the high bits,
|
||||
// so a mask that survived a sloppy IPC caller cannot index past the array.
|
||||
const junk: u32 = ~act.all_tags;
|
||||
try testing.expectEqual(@as(usize, 1), act.tagSlot(0b1 | junk));
|
||||
try testing.expectEqual(act.tag_count, act.tagSlot(@as(u32, 1) << (act.tag_count - 1)));
|
||||
}
|
||||
|
||||
// ─── command parsing ─────────────────────────────────────────────────────────
|
||||
|
||||
fn parseOk(argv: []const []const u8) act.Action {
|
||||
return act.parse(argv) catch unreachable;
|
||||
}
|
||||
|
||||
test "tag arguments accept indices, masks and all" {
|
||||
try testing.expectEqual(@as(u32, 1), parseOk(&.{ "view", "1" }).view);
|
||||
try testing.expectEqual(@as(u32, 4), parseOk(&.{ "view", "3" }).view);
|
||||
try testing.expectEqual(@as(u32, 4), parseOk(&.{ "view", "0x4" }).view);
|
||||
try testing.expectEqual(@as(u32, 4), parseOk(&.{ "view", "mask:4" }).view);
|
||||
try testing.expectEqual(act.all_tags, parseOk(&.{ "view", "all" }).view);
|
||||
}
|
||||
|
||||
test "tag indices outside the range are rejected" {
|
||||
try testing.expectError(error.InvalidArgument, act.parse(&.{ "view", "0" }));
|
||||
try testing.expectError(error.InvalidArgument, act.parse(&.{ "view", "10" }));
|
||||
try testing.expectError(error.InvalidArgument, act.parse(&.{ "view", "nope" }));
|
||||
try testing.expectError(error.MissingArgument, act.parse(&.{"view"}));
|
||||
}
|
||||
|
||||
test "masks are clamped to the valid tag range" {
|
||||
try testing.expectEqual(act.all_tags, parseOk(&.{ "view", "0xffffffff" }).view);
|
||||
}
|
||||
|
||||
test "deltas distinguish relative from absolute" {
|
||||
switch (parseOk(&.{ "mfact", "+0.05" }).mfact) {
|
||||
.relative => |v| try testing.expectApproxEqAbs(@as(f32, 0.05), v, 1e-6),
|
||||
.absolute => return error.TestUnexpectedResult,
|
||||
}
|
||||
switch (parseOk(&.{ "mfact", "0.5" }).mfact) {
|
||||
.absolute => |v| try testing.expectApproxEqAbs(@as(f32, 0.5), v, 1e-6),
|
||||
.relative => return error.TestUnexpectedResult,
|
||||
}
|
||||
switch (parseOk(&.{ "nmaster", "-1" }).nmaster) {
|
||||
.relative => |v| try testing.expectEqual(@as(i32, -1), v),
|
||||
.absolute => return error.TestUnexpectedResult,
|
||||
}
|
||||
}
|
||||
|
||||
test "delta application respects relative and absolute" {
|
||||
const rel = act.Delta(i32){ .relative = -1 };
|
||||
const abs = act.Delta(i32){ .absolute = 3 };
|
||||
try testing.expectEqual(@as(i32, 4), rel.apply(5));
|
||||
try testing.expectEqual(@as(i32, 3), abs.apply(5));
|
||||
}
|
||||
|
||||
test "unknown commands are rejected rather than guessed at" {
|
||||
try testing.expectError(error.UnknownCommand, act.parse(&.{"nonsense"}));
|
||||
try testing.expectError(error.UnknownCommand, act.parse(&.{}));
|
||||
}
|
||||
|
||||
test "spawn keeps its whole argv" {
|
||||
const a = parseOk(&.{ "spawn", "foot", "-e", "htop" });
|
||||
try testing.expectEqual(@as(usize, 3), a.spawn.len);
|
||||
try testing.expectEqualStrings("htop", a.spawn[2]);
|
||||
try testing.expectError(error.MissingArgument, act.parse(&.{"spawn"}));
|
||||
}
|
||||
|
||||
test "directions parse both spellings" {
|
||||
try testing.expectEqual(act.Direction.next, parseOk(&.{ "focus", "next" }).focus);
|
||||
try testing.expectEqual(act.Direction.prev, parseOk(&.{ "focus", "prev" }).focus);
|
||||
try testing.expectEqual(act.Direction.prev, parseOk(&.{ "focus", "previous" }).focus);
|
||||
try testing.expectError(error.InvalidArgument, act.parse(&.{ "focus", "sideways" }));
|
||||
}
|
||||
|
||||
test "window commands keep the identifier verbatim" {
|
||||
try testing.expectEqualStrings("w-17", parseOk(&.{ "focus-window", "w-17" }).focus_window);
|
||||
try testing.expectEqualStrings("w-17", parseOk(&.{ "close-window", "w-17" }).close_window);
|
||||
// An identifier is opaque, so a direction-looking one is still an id.
|
||||
try testing.expectEqualStrings("next", parseOk(&.{ "focus-window", "next" }).focus_window);
|
||||
try testing.expectError(error.MissingArgument, act.parse(&.{"focus-window"}));
|
||||
try testing.expectError(error.InvalidArgument, act.parse(&.{ "focus-window", "" }));
|
||||
}
|
||||
|
||||
test "layouts parse by name" {
|
||||
try testing.expectEqual(act.Layout.monocle, parseOk(&.{ "layout", "monocle" }).set_layout);
|
||||
try testing.expectEqual(act.Layout.tabbed, parseOk(&.{ "layout", "tabbed" }).set_layout);
|
||||
try testing.expectError(error.InvalidArgument, act.parse(&.{ "layout", "spiral" }));
|
||||
}
|
||||
|
||||
test "only navigation-style actions repeat on key hold" {
|
||||
try testing.expect(parseOk(&.{ "focus", "next" }).repeats());
|
||||
try testing.expect(parseOk(&.{ "mfact", "+0.05" }).repeats());
|
||||
try testing.expect(!parseOk(&.{"zoom"}).repeats());
|
||||
try testing.expect(!parseOk(&.{"close"}).repeats());
|
||||
try testing.expect(!parseOk(&.{ "view", "1" }).repeats());
|
||||
}
|
||||
|
||||
// ─── input device rules ──────────────────────────────────────────────────────
|
||||
|
||||
test "device name globs match the way a config author expects" {
|
||||
const touchpad = "ELAN0501:00 04F3:3060 Touchpad";
|
||||
|
||||
try testing.expect(input.matches("*Touchpad*", touchpad));
|
||||
try testing.expect(input.matches("*Touchpad", touchpad));
|
||||
try testing.expect(input.matches("ELAN*", touchpad));
|
||||
try testing.expect(input.matches("*", touchpad));
|
||||
try testing.expect(input.matches(touchpad, touchpad));
|
||||
|
||||
try testing.expect(!input.matches("*Trackpoint*", touchpad));
|
||||
try testing.expect(!input.matches("Touchpad", touchpad));
|
||||
// A literal pattern must match the whole name, not merely a prefix.
|
||||
try testing.expect(!input.matches("ELAN0501", touchpad));
|
||||
}
|
||||
|
||||
test "globs handle empty runs and repeated stars" {
|
||||
try testing.expect(input.matches("", ""));
|
||||
try testing.expect(input.matches("*", ""));
|
||||
try testing.expect(input.matches("***", ""));
|
||||
try testing.expect(!input.matches("a", ""));
|
||||
|
||||
// The backtracking case: each star has to be willing to give ground.
|
||||
try testing.expect(input.matches("*a*b*c*", "xxaxxbxxcxx"));
|
||||
try testing.expect(!input.matches("*a*b*c*", "xxaxxcxxbxx"));
|
||||
// Only the last 'a' lets the rest of the pattern through.
|
||||
try testing.expect(input.matches("*aab", "aaab"));
|
||||
}
|
||||
|
||||
test "rules match on name and type independently" {
|
||||
const rule: input.Rule = .{ .name = "*Touchpad*", .type = .pointer, .tap = true };
|
||||
|
||||
try testing.expect(rule.matchesDevice("Foo Touchpad", .pointer));
|
||||
// Right name, wrong kind of device.
|
||||
try testing.expect(!rule.matchesDevice("Foo Touchpad", .touch));
|
||||
try testing.expect(!rule.matchesDevice("Foo Keyboard", .pointer));
|
||||
|
||||
// A rule with neither selector applies to everything.
|
||||
const catch_all: input.Rule = .{ .natural_scroll = true };
|
||||
try testing.expect(catch_all.matchesDevice("anything", .tablet));
|
||||
try testing.expect(catch_all.matchesDevice("", .keyboard));
|
||||
}
|
||||
|
||||
test "later rules override earlier ones field by field" {
|
||||
const broad: input.Rule = .{ .name = "*", .tap = true, .natural_scroll = true };
|
||||
const narrow: input.Rule = .{ .name = "*Touchpad*", .tap = false, .click_method = .clickfinger };
|
||||
|
||||
const merged = (input.Rule{}).merge(broad).merge(narrow);
|
||||
|
||||
// Stated twice: the later rule wins.
|
||||
try testing.expectEqual(@as(?bool, false), merged.tap);
|
||||
// Stated only by the broad rule: survives.
|
||||
try testing.expectEqual(@as(?bool, true), merged.natural_scroll);
|
||||
// Stated only by the narrow rule: applied.
|
||||
try testing.expectEqual(@as(?input.ClickMethod, .clickfinger), merged.click_method);
|
||||
// Stated by neither: still null, so the device keeps libinput's default.
|
||||
try testing.expectEqual(@as(?bool, null), merged.middle_emulation);
|
||||
}
|
||||
|
||||
test "merging leaves the selectors alone" {
|
||||
// Otherwise a merged rule would claim to be about whichever device matched
|
||||
// last, which is not a thing anything should be able to read back out.
|
||||
const merged = (input.Rule{ .name = "a", .type = .pointer })
|
||||
.merge(.{ .name = "b", .type = .keyboard, .tap = true });
|
||||
|
||||
try testing.expectEqualStrings("a", merged.name.?);
|
||||
try testing.expectEqual(@as(?input.Type, .pointer), merged.type);
|
||||
try testing.expectEqual(@as(?bool, true), merged.tap);
|
||||
}
|
||||
|
||||
test "an unset keymap is recognised as the default" {
|
||||
try testing.expect((input.Keymap{}).isDefault());
|
||||
try testing.expect(!(input.Keymap{ .layout = "us" }).isDefault());
|
||||
try testing.expect(!(input.Keymap{ .options = "caps:escape" }).isDefault());
|
||||
}
|
||||
|
||||
test "map_to_output merges like any other setting" {
|
||||
// It is a string rather than a scalar, so worth pinning that the generic
|
||||
// merge handles it and that a rule silent about it does not clear it.
|
||||
const merged = (input.Rule{})
|
||||
.merge(.{ .type = .touch, .map_to_output = "eDP-1" })
|
||||
.merge(.{ .name = "*", .tap = true });
|
||||
|
||||
try testing.expectEqualStrings("eDP-1", merged.map_to_output.?);
|
||||
|
||||
// And that a later rule naming a different output does win.
|
||||
const moved = merged.merge(.{ .map_to_output = "DP-2" });
|
||||
try testing.expectEqualStrings("DP-2", moved.map_to_output.?);
|
||||
}
|
||||
Reference in New Issue
Block a user