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