add multimonitor

This commit is contained in:
2026-08-18 21:30:51 +02:00
parent 7d117f4ae5
commit f60dd06e7e
10 changed files with 470 additions and 34 deletions
+65 -10
View File
@@ -6,7 +6,8 @@ river 0.4 is *non-monolithic*: it ships no window management policy of its own
`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.
layouts, and keybindings compiled into the binary. On more than one monitor the
tags are split across the screens, so every tag lives on exactly one of them.
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
@@ -22,6 +23,9 @@ 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
- multiple monitors, with the tag set split across them: verified on a headless
river with two outputs, driven over IPC — the split, moving between screens,
windows following their tags across, and views clamped to the screen's own tags
- the IPC socket, `att_wmctl`, and a quickshell bar that maps as a layer surface
and whose exclusive zone correctly shrinks the tiling area
@@ -149,7 +153,7 @@ if you would rather not compete with applications that bind Alt themselves.
| `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+,` / `Mod+.` | 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 |
@@ -161,8 +165,8 @@ if you would rather not compete with applications that bind Alt themselves.
| `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` / `Mod+Esc` | Back to previously viewed tags |
| `Mod+,` / `Mod+.` | Focus previous / next output |
| `Mod+Shift+,` / `Mod+Shift+.` | Send window to previous / next output |
| `Mod+h` / `Mod+l` | Focus the screen to the left / right |
| `Mod+Shift+h` / `Mod+Shift+l` | Send window to the screen left / right |
| `Mod+Left drag` | Move window (floats it) |
| `Mod+Right drag` | Resize window |
@@ -212,6 +216,48 @@ keep theirs for the way back.
---
## Multiple monitors
The tag set is **split across the screens**: every tag lives on exactly one of
them. With two monitors the left owns tags 15 and the right 69; with three
they get 13, 46 and 79. Screens are ordered by where they sit in the output
layout, left to right and then top to bottom — not by the order they were
plugged in — so the arrangement follows the monitors on the desk rather than the
cables behind it.
| | |
|---|---|
| `Mod+h` / `Mod+l` | Move to the screen on the left / right |
| `Mod+Shift+h` / `Mod+Shift+l` | Send the focused window there |
| `Mod+7` | Go to the screen tag 7 lives on and show tag 7 |
| `Mod+Shift+7` | Send the focused window to tag 7, wherever that is |
Because a tag names a screen as well as a workspace, `Mod+7` and `Mod+h` are two
ways of doing the same thing, and the tag keys alone are enough to drive the
whole desk. Sending a window away leaves you where you are, as dwm's `tagmon`
does; the keyboard goes to whatever is left on the screen you are still on.
`Mod+0` means everything on *this* screen — a view is always clamped to the tags
its screen owns, so no screen can be made to show another's tag.
Unplugging a monitor hands its tags to the screens that remain, and the windows
wearing those tags follow them there rather than being stranded on a tag nothing
can show. Plug it back in and they go home. A laptop with nothing attached owns
all nine tags and behaves exactly as it did before, which is why none of this is
visible until there is a second screen.
Set `split_tags = false` in `config.zig` for dwm's model instead: every screen
gets a full set of nine tags of its own, and the tag keys never leave the screen
you are on. `Mod+h`/`Mod+l` and `Mod+Shift+h`/`Mod+Shift+l` still move between
screens and are then the only way to.
Layer surfaces (bars) that do not name an output land on the focused screen.
`warp_cursor` is worth turning on here: it pulls the pointer along when the
keyboard moves to another screen, including onto an empty one, where there is no
window to warp to and the cursor would otherwise be left behind.
---
## Configuration
att_wm is configured at compile time, like dwm. Edit `src/config.zig` and rebuild.
@@ -390,6 +436,7 @@ quickshell config uses one socket for both, avoiding a process spawn per click.
"name": "DP-1",
"focused": true,
"tags": 1,
"owned_tags": 31,
"occupied": 5,
"layout": "master",
"layout_symbol": "[]=",
@@ -411,8 +458,12 @@ quickshell config uses one socket for both, avoiding a process spawn per click.
}
```
`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.
`tags`, `owned_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. `owned_tags` is the slice of the tag set this screen owns — see
[Multiple monitors](#multiple-monitors) — so a bar can draw its own screen's
tags and leave the rest to the bar on the screen they belong to. It is all nine
when `split_tags` is off, so a bar that honours it works either way.
`usable` is the area left after layer-shell exclusive zones, i.e. where windows
are actually laid out.
@@ -464,9 +515,13 @@ 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`.
exposing `outputs`, `focusedOutput`, `focusedTitle`, `tagActive()`,
`tagOccupied()` and `tagOwned()`, plus `send()` for commands. Reuse it in your
own bar and ignore `shell.qml`.
One bar is created per screen and each draws only the tags its screen owns, via
`tagOwned()` — with the tag set split across monitors the other tags belong to
the bar next door.
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
@@ -489,7 +544,7 @@ river's identifier is shared between them.
| --- | --- |
| `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/Output.zig` | Output tags and which of them it owns, 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 |