add multimonitor
This commit is contained in:
@@ -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
|
`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
|
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
|
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
|
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
|
[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
|
- tags, master / monocle / tabbed layouts, floating and fullscreen windows
|
||||||
- focus cycling, `zoom`, stack reordering, per-tag layout state
|
- 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
|
- the IPC socket, `att_wmctl`, and a quickshell bar that maps as a layer surface
|
||||||
and whose exclusive zone correctly shrinks the tiling area
|
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+j` / `Mod+k` | Focus next / previous window |
|
||||||
| `Mod+Shift+j` / `Mod+Shift+k` | Move focused window down / up the stack |
|
| `Mod+Shift+j` / `Mod+Shift+k` | Move focused window down / up the stack |
|
||||||
| `Mod+Return` | Zoom — promote focused window to master |
|
| `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+i` / `Mod+d` | Increase / decrease windows in master |
|
||||||
| `Mod+t` / `Mod+m` / `Mod+u` | Master / monocle / tabbed layout |
|
| `Mod+t` / `Mod+m` / `Mod+u` | Master / monocle / tabbed layout |
|
||||||
| `Mod+space` | Toggle between current and previous 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+Ctrl+Shift+1..9` | Toggle tag on focused window |
|
||||||
| `Mod+0` / `Mod+Shift+0` | View all tags / put window on all tags |
|
| `Mod+0` / `Mod+Shift+0` | View all tags / put window on all tags |
|
||||||
| `Mod+Tab` / `Mod+Esc` | Back to previously viewed tags |
|
| `Mod+Tab` / `Mod+Esc` | Back to previously viewed tags |
|
||||||
| `Mod+,` / `Mod+.` | Focus previous / next output |
|
| `Mod+h` / `Mod+l` | Focus the screen to the left / right |
|
||||||
| `Mod+Shift+,` / `Mod+Shift+.` | Send window to previous / next output |
|
| `Mod+Shift+h` / `Mod+Shift+l` | Send window to the screen left / right |
|
||||||
| `Mod+Left drag` | Move window (floats it) |
|
| `Mod+Left drag` | Move window (floats it) |
|
||||||
| `Mod+Right drag` | Resize window |
|
| `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 1–5 and the right 6–9; with three
|
||||||
|
they get 1–3, 4–6 and 7–9. 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
|
## Configuration
|
||||||
|
|
||||||
att_wm is configured at compile time, like dwm. Edit `src/config.zig` and rebuild.
|
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",
|
"name": "DP-1",
|
||||||
"focused": true,
|
"focused": true,
|
||||||
"tags": 1,
|
"tags": 1,
|
||||||
|
"owned_tags": 31,
|
||||||
"occupied": 5,
|
"occupied": 5,
|
||||||
"layout": "master",
|
"layout": "master",
|
||||||
"layout_symbol": "[]=",
|
"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
|
`tags`, `owned_tags` and `occupied` are bitmasks. `occupied` is the set of tags
|
||||||
least one window, which is what dwm's bar draws its corner squares from.
|
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
|
`usable` is the area left after layer-shell exclusive zones, i.e. where windows
|
||||||
are actually laid out.
|
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,
|
`AttWm.qml` is a singleton wrapping the socket — reconnecting if att_wm restarts,
|
||||||
exposing `outputs`, `focusedOutput`, `focusedTitle`, `tagActive()` and
|
exposing `outputs`, `focusedOutput`, `focusedTitle`, `tagActive()`,
|
||||||
`tagOccupied()`, plus `send()` for commands. Reuse it in your own bar and ignore
|
`tagOccupied()` and `tagOwned()`, plus `send()` for commands. Reuse it in your
|
||||||
`shell.qml`.
|
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
|
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
|
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/Wm.zig` | Globals, the manage/render sequence state machine, actions, event loop |
|
||||||
| `src/Window.zig` | Per-window state; handlers only mutate fields |
|
| `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/Seat.zig` | Focus, key and pointer bindings, interactive move/resize |
|
||||||
| `src/InputManager.zig` | Input devices: keymaps, key repeat, libinput settings |
|
| `src/InputManager.zig` | Input devices: keymaps, key repeat, libinput settings |
|
||||||
| `src/layout.zig` | Pure tiling geometry — no Wayland, unit tested |
|
| `src/layout.zig` | Pure tiling geometry — no Wayland, unit tested |
|
||||||
|
|||||||
@@ -59,6 +59,16 @@ Singleton {
|
|||||||
return output ? (output.tags & (1 << index)) !== 0 : false;
|
return output ? (output.tags & (1 << index)) !== 0 : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether a tag belongs to this screen at all.
|
||||||
|
///
|
||||||
|
/// With `split_tags` on, each screen owns one slice of the tag set and the
|
||||||
|
/// rest live on the other monitors, so a bar that drew all nine would be
|
||||||
|
/// offering tags its screen cannot show. Every tag is owned when the split
|
||||||
|
/// is off, so the same bar works either way.
|
||||||
|
function tagOwned(output, index) {
|
||||||
|
return output ? (output.owned_tags & (1 << index)) !== 0 : true;
|
||||||
|
}
|
||||||
|
|
||||||
// A Socket that fails to connect cannot be revived: no state change is
|
// 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
|
// 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
|
// socket lives in a Loader and a retry means building a new one. This is
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ ShellRoot {
|
|||||||
readonly property bool active: AttWm.tagActive(bar.output, index)
|
readonly property bool active: AttWm.tagActive(bar.output, index)
|
||||||
readonly property bool occupied: AttWm.tagOccupied(bar.output, index)
|
readonly property bool occupied: AttWm.tagOccupied(bar.output, index)
|
||||||
|
|
||||||
|
// Tags belonging to another screen are that screen's to
|
||||||
|
// draw; a hidden item is out of the layout entirely, so
|
||||||
|
// each bar shows its own slice with no gap.
|
||||||
|
visible: AttWm.tagOwned(bar.output, index)
|
||||||
|
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
implicitWidth: label.implicitWidth + 16
|
implicitWidth: label.implicitWidth + 16
|
||||||
color: active ? "#5294e2" : "transparent"
|
color: active ? "#5294e2" : "transparent"
|
||||||
|
|||||||
+29
-1
@@ -46,6 +46,11 @@ have_usable: bool = false,
|
|||||||
tags: u32 = config.default_tags,
|
tags: u32 = config.default_tags,
|
||||||
prev_tags: u32 = config.default_tags,
|
prev_tags: u32 = config.default_tags,
|
||||||
|
|
||||||
|
/// The tags this output owns. Every tag when `config.split_tags` is off or this
|
||||||
|
/// is the only screen; one slice of the set otherwise. `Wm.assignTagRanges`
|
||||||
|
/// is what divides them up.
|
||||||
|
owned_tags: u32 = action.all_tags,
|
||||||
|
|
||||||
/// One slot per tag, plus slot 0 for views of more than one tag. Indexed
|
/// One slot per tag, plus slot 0 for views of more than one tag. Indexed
|
||||||
/// through `state()`, never directly.
|
/// through `state()`, never directly.
|
||||||
tag_state: [action.tag_count + 1]TagState = @splat(.{}),
|
tag_state: [action.tag_count + 1]TagState = @splat(.{}),
|
||||||
@@ -119,13 +124,36 @@ pub fn setLayout(self: *Output, mode: action.Layout) void {
|
|||||||
st.layout = mode;
|
st.layout = mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Show a set of tags. Tags belonging to another screen are dropped rather than
|
||||||
|
/// shown here, so `Mod+0` means "everything on this screen" and not "everything
|
||||||
|
/// everywhere"; a mask with nothing of this output's in it leaves the view alone.
|
||||||
pub fn setTags(self: *Output, tags: u32) void {
|
pub fn setTags(self: *Output, tags: u32) void {
|
||||||
const masked = tags & action.all_tags;
|
const masked = tags & self.owned_tags;
|
||||||
if (masked == 0 or masked == self.tags) return;
|
if (masked == 0 or masked == self.tags) return;
|
||||||
self.prev_tags = self.tags;
|
self.prev_tags = self.tags;
|
||||||
self.tags = masked;
|
self.tags = masked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Hand this output its slice of the tag set.
|
||||||
|
///
|
||||||
|
/// A view left with nothing it owns falls back to the lowest tag it does, which
|
||||||
|
/// is what settles a screen the moment it is plugged in — it starts on
|
||||||
|
/// `default_tags` like every other, and only here finds out that tag is not
|
||||||
|
/// its to show.
|
||||||
|
pub fn setOwnedTags(self: *Output, mask: u32) void {
|
||||||
|
const owned = mask & action.all_tags;
|
||||||
|
if (owned == 0 or owned == self.owned_tags) return;
|
||||||
|
self.owned_tags = owned;
|
||||||
|
|
||||||
|
self.tags = clampView(self.tags, owned);
|
||||||
|
self.prev_tags = clampView(self.prev_tags, owned);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clampView(tags: u32, owned: u32) u32 {
|
||||||
|
const kept = tags & owned;
|
||||||
|
return if (kept != 0) kept else action.lowestTag(owned);
|
||||||
|
}
|
||||||
|
|
||||||
fn onEvent(_: *river.OutputV1, event: river.OutputV1.Event, self: *Output) void {
|
fn onEvent(_: *river.OutputV1, event: river.OutputV1.Event, self: *Output) void {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.removed => {
|
.removed => {
|
||||||
|
|||||||
+47
-2
@@ -76,6 +76,10 @@ pointer_y: i32 = 0,
|
|||||||
/// The window the pointer is currently inside.
|
/// The window the pointer is currently inside.
|
||||||
hovered: ?*Window = null,
|
hovered: ?*Window = null,
|
||||||
|
|
||||||
|
/// A screen to warp the pointer onto in the next manage sequence. Set when
|
||||||
|
/// focus moves to an output with no window on it to warp to.
|
||||||
|
pending_warp_output: ?*Output = null,
|
||||||
|
|
||||||
op: ?Op = null,
|
op: ?Op = null,
|
||||||
/// An operation to start in the next manage sequence.
|
/// An operation to start in the next manage sequence.
|
||||||
pending_op: ?Op = null,
|
pending_op: ?Op = null,
|
||||||
@@ -165,12 +169,19 @@ fn createBindings(self: *Seat) !void {
|
|||||||
std.log.info("registered {d}/{d} pointer bindings", .{ self.buttons.items.len, config.buttons.len });
|
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,
|
/// The output this seat is working on: the one holding the focused window, else
|
||||||
/// else the one under the pointer.
|
/// the one focus was last moved to, else the one under the pointer.
|
||||||
|
///
|
||||||
|
/// That middle case is what makes an empty screen a place the user can be.
|
||||||
|
/// Moving to a screen with nothing on it clears the focused window, and with
|
||||||
|
/// only the pointer to fall back on the seat would go on reporting the screen it
|
||||||
|
/// came from — so the tag keys, the layout keys and the next window spawned
|
||||||
|
/// would all land back on the monitor just left.
|
||||||
pub fn currentOutput(self: *Seat) ?*Output {
|
pub fn currentOutput(self: *Seat) ?*Output {
|
||||||
if (self.focused) |win| {
|
if (self.focused) |win| {
|
||||||
if (win.output) |out| return out;
|
if (win.output) |out| return out;
|
||||||
}
|
}
|
||||||
|
if (self.wm.focused_output) |out| return out;
|
||||||
return self.wm.outputAt(self.pointer_x, self.pointer_y) orelse self.wm.firstOutput();
|
return self.wm.outputAt(self.pointer_x, self.pointer_y) orelse self.wm.firstOutput();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,6 +267,11 @@ pub fn applyManage(self: *Seat) void {
|
|||||||
self.pending_clear_focus = false;
|
self.pending_clear_focus = false;
|
||||||
self.wm.ipcDirty();
|
self.wm.ipcDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (self.pending_warp_output) |out| {
|
||||||
|
self.warpToOutput(out);
|
||||||
|
self.pending_warp_output = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pull the pointer to the middle of a newly focused window.
|
/// Pull the pointer to the middle of a newly focused window.
|
||||||
@@ -274,6 +290,25 @@ fn warpTo(self: *Seat, win: *Window) void {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pull the pointer onto a screen the keyboard has just moved to, when there is
|
||||||
|
/// no window there to warp to instead.
|
||||||
|
///
|
||||||
|
/// Without it, moving to an empty screen leaves the cursor on the one before —
|
||||||
|
/// and with `focus_follows_mouse` on, the first window the pointer then brushes
|
||||||
|
/// past takes the focus straight back. Manage sequence only.
|
||||||
|
fn warpToOutput(self: *Seat, out: *Output) void {
|
||||||
|
if (!config.warp_cursor) return;
|
||||||
|
if (self.op != null) return;
|
||||||
|
const area = out.layoutArea();
|
||||||
|
if (area.width <= 0 or area.height <= 0) return;
|
||||||
|
if (area.contains(self.pointer_x, self.pointer_y)) return;
|
||||||
|
|
||||||
|
self.seat.pointerWarp(
|
||||||
|
area.x + @divTrunc(area.width, 2),
|
||||||
|
area.y + @divTrunc(area.height, 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn onEvent(_: *river.SeatV1, event: river.SeatV1.Event, self: *Seat) void {
|
fn onEvent(_: *river.SeatV1, event: river.SeatV1.Event, self: *Seat) void {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.removed => {
|
.removed => {
|
||||||
@@ -289,6 +324,16 @@ fn onEvent(_: *river.SeatV1, event: river.SeatV1.Event, self: *Seat) void {
|
|||||||
.pointer_position => |ev| {
|
.pointer_position => |ev| {
|
||||||
self.pointer_x = ev.x;
|
self.pointer_x = ev.x;
|
||||||
self.pointer_y = ev.y;
|
self.pointer_y = ev.y;
|
||||||
|
|
||||||
|
// Sloppy focus crosses screens too, as dwm's motion handler does:
|
||||||
|
// the pointer leaving a monitor is what moves the seat to the next
|
||||||
|
// one, so the tag and layout keys follow the cursor even over a
|
||||||
|
// screen with no window on it to focus.
|
||||||
|
if (config.focus_follows_mouse and self.op == null) {
|
||||||
|
if (self.wm.outputAt(ev.x, ev.y)) |out| {
|
||||||
|
if (self.wm.focused_output != out) self.wm.enterOutput(self, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
.pointer_enter => |ev| {
|
.pointer_enter => |ev| {
|
||||||
|
|||||||
+173
-11
@@ -107,6 +107,8 @@ tab_face_tried: bool = false,
|
|||||||
scratch_windows: std.ArrayList(*Window) = .empty,
|
scratch_windows: std.ArrayList(*Window) = .empty,
|
||||||
scratch_cells: std.ArrayList(Box) = .empty,
|
scratch_cells: std.ArrayList(Box) = .empty,
|
||||||
scratch_order: std.ArrayList(*Window) = .empty,
|
scratch_order: std.ArrayList(*Window) = .empty,
|
||||||
|
/// Outputs in desk order; see `orderedOutputs`.
|
||||||
|
scratch_outputs: std.ArrayList(*Output) = .empty,
|
||||||
|
|
||||||
pub fn init(gpa: Allocator, environ: std.process.Environ) !*Wm {
|
pub fn init(gpa: Allocator, environ: std.process.Environ) !*Wm {
|
||||||
const display = try wl.Display.connect(null);
|
const display = try wl.Display.connect(null);
|
||||||
@@ -261,6 +263,7 @@ pub fn deinit(self: *Wm) void {
|
|||||||
self.scratch_windows.deinit(gpa);
|
self.scratch_windows.deinit(gpa);
|
||||||
self.scratch_cells.deinit(gpa);
|
self.scratch_cells.deinit(gpa);
|
||||||
self.scratch_order.deinit(gpa);
|
self.scratch_order.deinit(gpa);
|
||||||
|
self.scratch_outputs.deinit(gpa);
|
||||||
|
|
||||||
self.ipc.deinit();
|
self.ipc.deinit();
|
||||||
sys.close(self.repeat_fd);
|
sys.close(self.repeat_fd);
|
||||||
@@ -519,6 +522,8 @@ fn manage(self: *Wm) void {
|
|||||||
defer self.in_manage = false;
|
defer self.in_manage = false;
|
||||||
|
|
||||||
self.reap();
|
self.reap();
|
||||||
|
self.assignTagRanges();
|
||||||
|
self.rehomeWindows();
|
||||||
self.assignOutputs();
|
self.assignOutputs();
|
||||||
self.focusNewWindows();
|
self.focusNewWindows();
|
||||||
self.arrangeAll();
|
self.arrangeAll();
|
||||||
@@ -587,6 +592,7 @@ fn reap(self: *Wm) void {
|
|||||||
if (seat.focused) |f| if (f.output == null) {
|
if (seat.focused) |f| if (f.output == null) {
|
||||||
seat.focused = null;
|
seat.focused = null;
|
||||||
};
|
};
|
||||||
|
if (seat.pending_warp_output == out) seat.pending_warp_output = null;
|
||||||
}
|
}
|
||||||
out.destroy();
|
out.destroy();
|
||||||
self.ipcDirty();
|
self.ipcDirty();
|
||||||
@@ -608,6 +614,103 @@ fn reap(self: *Wm) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Divide the tag set among the outputs, left to right.
|
||||||
|
///
|
||||||
|
/// Recomputed every manage sequence rather than only when a screen comes or
|
||||||
|
/// goes, because dragging a monitor to the other side of the desk reorders them
|
||||||
|
/// too and the ranges have to follow. `setOwnedTags` is a no-op when nothing
|
||||||
|
/// has actually changed, which is the common case.
|
||||||
|
fn assignTagRanges(self: *Wm) void {
|
||||||
|
const ordered = self.orderedOutputs();
|
||||||
|
for (ordered, 0..) |out, i| {
|
||||||
|
const mask = if (config.split_tags) act.tagsForOutput(i, ordered.len) else act.all_tags;
|
||||||
|
out.setOwnedTags(mask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move windows to the screen that owns their tags.
|
||||||
|
///
|
||||||
|
/// This is what makes the split work in one direction and heal in the other:
|
||||||
|
/// `Mod+Shift+7` retags a window and the window follows tag 7 to the screen it
|
||||||
|
/// lives on, and unplugging a monitor hands its tags to a neighbour, which the
|
||||||
|
/// windows wearing them follow rather than being stranded on tags their new
|
||||||
|
/// screen cannot show. Plug the monitor back in and they go home.
|
||||||
|
///
|
||||||
|
/// A window keeps the screen it is on for as long as that screen owns any of
|
||||||
|
/// its tags, so putting a window on every tag — `Mod+Shift+0` — does not yank
|
||||||
|
/// it away to whichever screen owns tag 1.
|
||||||
|
fn rehomeWindows(self: *Wm) void {
|
||||||
|
if (!config.split_tags) return;
|
||||||
|
|
||||||
|
for (self.windows.items) |win| {
|
||||||
|
if (win.closed or win.tags == 0) continue;
|
||||||
|
if (win.output) |out| {
|
||||||
|
if (win.tags & out.owned_tags != 0) continue;
|
||||||
|
}
|
||||||
|
const target = self.outputForTags(win.tags) orelse continue;
|
||||||
|
if (win.output == target) continue;
|
||||||
|
|
||||||
|
win.output = target;
|
||||||
|
// The new screen is a different size, so the size we last proposed
|
||||||
|
// says nothing about the size it should have there.
|
||||||
|
win.proposed_width = -1;
|
||||||
|
win.proposed_height = -1;
|
||||||
|
self.windowLeftOutput(win);
|
||||||
|
self.ipcDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Keep a seat where it is when the window it was focusing moves to another
|
||||||
|
/// screen.
|
||||||
|
///
|
||||||
|
/// `Mod+Shift+7` is a way of getting a window off the screen, not of following
|
||||||
|
/// it across to the next one, and `validateFocus` on its own would do the
|
||||||
|
/// opposite: it resolves against the focused window's output, which is now the
|
||||||
|
/// far screen. So the keyboard goes to whatever is left where the user still is.
|
||||||
|
fn windowLeftOutput(self: *Wm, win: *Window) void {
|
||||||
|
const out = self.focused_output orelse return;
|
||||||
|
for (self.seats.items) |seat| {
|
||||||
|
if (seat.focused != win and seat.pending_focus != win) continue;
|
||||||
|
// `win` has already been moved, so it is not a candidate here.
|
||||||
|
if (self.topOnTags(out, out.tags)) |next| seat.focus(next) else seat.focus(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The output owning the lowest tag in a mask.
|
||||||
|
pub fn outputForTags(self: *Wm, tags: u32) ?*Output {
|
||||||
|
const t = act.lowestTag(tags);
|
||||||
|
if (t == 0) return null;
|
||||||
|
for (self.outputs.items) |out| {
|
||||||
|
if (out.owned_tags & t != 0) return out;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The outputs in the order they are arranged on the desk: left to right, then
|
||||||
|
/// top to bottom.
|
||||||
|
///
|
||||||
|
/// This is the order `focus_output` steps through and the order the tag set is
|
||||||
|
/// split in, which is what ties the two together — `Mod+l` moves to the screen
|
||||||
|
/// on the right, and the screen on the right is the one holding the higher
|
||||||
|
/// tags. Connection order would put either wherever the cables happened to go.
|
||||||
|
///
|
||||||
|
/// Returns a scratch buffer, valid until the next call.
|
||||||
|
fn orderedOutputs(self: *Wm) []*Output {
|
||||||
|
self.scratch_outputs.clearRetainingCapacity();
|
||||||
|
// Unsorted is a poor order but a working one; nothing here is worth
|
||||||
|
// failing a manage sequence over.
|
||||||
|
self.scratch_outputs.appendSlice(self.gpa, self.outputs.items) catch return self.outputs.items;
|
||||||
|
// A stable sort, so two screens stacked exactly on top of each other keep
|
||||||
|
// connection order rather than swapping about between frames.
|
||||||
|
std.mem.sort(*Output, self.scratch_outputs.items, {}, lessByPosition);
|
||||||
|
return self.scratch_outputs.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lessByPosition(_: void, a: *Output, b: *Output) bool {
|
||||||
|
if (a.box.x != b.box.x) return a.box.x < b.box.x;
|
||||||
|
return a.box.y < b.box.y;
|
||||||
|
}
|
||||||
|
|
||||||
/// Give new windows an output and a tag set.
|
/// Give new windows an output and a tag set.
|
||||||
fn assignOutputs(self: *Wm) void {
|
fn assignOutputs(self: *Wm) void {
|
||||||
const fallback = self.focused_output orelse self.firstOutput();
|
const fallback = self.focused_output orelse self.firstOutput();
|
||||||
@@ -704,6 +807,57 @@ fn validateFocus(self: *Wm, seat: *Seat) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// View a set of tags, on whichever screen owns them.
|
||||||
|
///
|
||||||
|
/// With the tag set split across displays, `Mod+7` is as much "go to the screen
|
||||||
|
/// tag 7 lives on" as it is "show tag 7" — the two are the same thing, and it is
|
||||||
|
/// what makes the tag keys on their own enough to drive a multi-monitor desk.
|
||||||
|
/// A mask the current screen owns any part of stays where it is, so `Mod+0`
|
||||||
|
/// still means "everything here".
|
||||||
|
fn viewTags(self: *Wm, seat: *Seat, mask: u32) void {
|
||||||
|
const current = seat.currentOutput();
|
||||||
|
const target = blk: {
|
||||||
|
if (!config.split_tags) break :blk current orelse return;
|
||||||
|
if (current) |out| {
|
||||||
|
if (out.owned_tags & mask != 0) break :blk out;
|
||||||
|
}
|
||||||
|
break :blk self.outputForTags(mask) orelse current orelse return;
|
||||||
|
};
|
||||||
|
|
||||||
|
target.setTags(mask);
|
||||||
|
if (target != current) self.enterOutput(seat, target);
|
||||||
|
self.ipcDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the seat to another screen: the keyboard goes to the window most
|
||||||
|
/// recently focused there, and if there is none the pointer is warped instead so
|
||||||
|
/// that an empty screen is still somewhere the user can be.
|
||||||
|
pub fn enterOutput(self: *Wm, seat: *Seat, out: *Output) void {
|
||||||
|
self.focusOutput(out);
|
||||||
|
if (self.topOnTags(out, out.tags)) |win| {
|
||||||
|
seat.focus(win);
|
||||||
|
} else {
|
||||||
|
seat.focus(null);
|
||||||
|
seat.pending_warp_output = out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The most recently focused window a view of `tags` on `out` would show.
|
||||||
|
///
|
||||||
|
/// Unlike `topVisible` this asks the tags rather than the `visible` flags, which
|
||||||
|
/// are only as fresh as the last layout pass — a binding that changes what is
|
||||||
|
/// being viewed and then wants to know what to focus is asking about a view that
|
||||||
|
/// has not been arranged yet.
|
||||||
|
fn topOnTags(self: *Wm, out: *Output, tags: u32) ?*Window {
|
||||||
|
var best: ?*Window = null;
|
||||||
|
for (self.windows.items) |win| {
|
||||||
|
if (win.output != out or win.closed or !win.mapped) continue;
|
||||||
|
if ((win.tags & tags) == 0) continue;
|
||||||
|
if (best == null or win.focus_serial > best.?.focus_serial) best = win;
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
/// The most recently focused visible window on an output.
|
/// The most recently focused visible window on an output.
|
||||||
fn topVisible(self: *Wm, out: *Output) ?*Window {
|
fn topVisible(self: *Wm, out: *Output) ?*Window {
|
||||||
var best: ?*Window = null;
|
var best: ?*Window = null;
|
||||||
@@ -1080,12 +1234,11 @@ pub fn perform(self: *Wm, seat: *Seat, action: act.Action) void {
|
|||||||
.swap => |dir| self.swapWindow(seat, dir),
|
.swap => |dir| self.swapWindow(seat, dir),
|
||||||
.zoom => self.zoom(seat),
|
.zoom => self.zoom(seat),
|
||||||
|
|
||||||
.view => |mask| if (seat.currentOutput()) |out| {
|
.view => |mask| self.viewTags(seat, mask),
|
||||||
out.setTags(mask);
|
|
||||||
self.ipcDirty();
|
|
||||||
},
|
|
||||||
.toggle_view => |mask| if (seat.currentOutput()) |out| {
|
.toggle_view => |mask| if (seat.currentOutput()) |out| {
|
||||||
out.setTags(out.tags ^ mask);
|
// Only this screen's own tags can be toggled into its view; the
|
||||||
|
// rest are somewhere else entirely.
|
||||||
|
out.setTags(out.tags ^ (mask & out.owned_tags));
|
||||||
self.ipcDirty();
|
self.ipcDirty();
|
||||||
},
|
},
|
||||||
.view_prev => if (seat.currentOutput()) |out| {
|
.view_prev => if (seat.currentOutput()) |out| {
|
||||||
@@ -1296,30 +1449,39 @@ fn moveToFront(self: *Wm, win: *Window, before: *Window) void {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Step to the next screen along, or send the focused window there.
|
||||||
|
///
|
||||||
|
/// The step is through `orderedOutputs`, so `prev` is the screen to the left and
|
||||||
|
/// `next` the one to the right whatever order the monitors were plugged in.
|
||||||
fn cycleOutput(self: *Wm, seat: *Seat, dir: act.Direction, send: bool) void {
|
fn cycleOutput(self: *Wm, seat: *Seat, dir: act.Direction, send: bool) void {
|
||||||
if (self.outputs.items.len < 2) return;
|
const ordered = self.orderedOutputs();
|
||||||
|
if (ordered.len < 2) return;
|
||||||
const current = seat.currentOutput() orelse return;
|
const current = seat.currentOutput() orelse return;
|
||||||
|
|
||||||
var idx: usize = 0;
|
var idx: usize = 0;
|
||||||
for (self.outputs.items, 0..) |out, i| {
|
for (ordered, 0..) |out, i| {
|
||||||
if (out == current) idx = i;
|
if (out == current) idx = i;
|
||||||
}
|
}
|
||||||
const n = self.outputs.items.len;
|
const n = ordered.len;
|
||||||
const next = switch (dir) {
|
const next = switch (dir) {
|
||||||
.next => (idx + 1) % n,
|
.next => (idx + 1) % n,
|
||||||
.prev => (idx + n - 1) % n,
|
.prev => (idx + n - 1) % n,
|
||||||
};
|
};
|
||||||
const target = self.outputs.items[next];
|
const target = ordered[next];
|
||||||
|
|
||||||
if (send) {
|
if (send) {
|
||||||
const win = seat.focused orelse return;
|
const win = seat.focused orelse return;
|
||||||
win.output = target;
|
win.output = target;
|
||||||
|
// Onto the tags that screen is showing, which under a split tag set is
|
||||||
|
// also what keeps the window there rather than being rehomed straight
|
||||||
|
// back to where it came from.
|
||||||
win.tags = target.tags;
|
win.tags = target.tags;
|
||||||
win.proposed_width = -1;
|
win.proposed_width = -1;
|
||||||
win.proposed_height = -1;
|
win.proposed_height = -1;
|
||||||
|
// dwm's tagmon leaves you on the monitor you were on, and so does this.
|
||||||
|
self.windowLeftOutput(win);
|
||||||
} else {
|
} else {
|
||||||
self.focusOutput(target);
|
self.enterOutput(seat, target);
|
||||||
if (self.topVisible(target)) |win| seat.focus(win) else seat.focus(null);
|
|
||||||
}
|
}
|
||||||
self.ipcDirty();
|
self.ipcDirty();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,46 @@ pub fn tagSlot(tags: u32) usize {
|
|||||||
return @ctz(t) + 1;
|
return @ctz(t) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The tags belonging to output `index` of `count`, when the tag set is split
|
||||||
|
/// across the displays.
|
||||||
|
///
|
||||||
|
/// The tags are divided into contiguous ranges in the order the outputs are
|
||||||
|
/// arranged on the desk, so with two monitors the left one owns 1–5 and the
|
||||||
|
/// right 6–9. Contiguous rather than interleaved because the keys are what the
|
||||||
|
/// user reaches for: 1–5 under the left hand for the left screen reads as one
|
||||||
|
/// screen's worth of workspaces, 1,3,5,7,9 does not.
|
||||||
|
///
|
||||||
|
/// A lone output owns every tag, which is what makes the split invisible on a
|
||||||
|
/// laptop with nothing plugged in.
|
||||||
|
pub fn tagsForOutput(index: usize, count: usize) u32 {
|
||||||
|
if (count <= 1 or index >= count) return all_tags;
|
||||||
|
|
||||||
|
// More outputs than tags: one each, and the outputs left over share the
|
||||||
|
// last tag rather than getting none. An output owning no tag could show no
|
||||||
|
// window at all, which is worse than two screens showing the same one.
|
||||||
|
if (count >= tag_count) {
|
||||||
|
return @as(u32, 1) << @intCast(@min(index, tag_count - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Earlier outputs take one of the leftover tags each, so the ranges differ
|
||||||
|
// by at most one and it is never the first screen that comes up short.
|
||||||
|
const base = tag_count / count;
|
||||||
|
const rem = tag_count % count;
|
||||||
|
const start = index * base + @min(index, rem);
|
||||||
|
const len = base + @as(usize, @intFromBool(index < rem));
|
||||||
|
|
||||||
|
const ones: u32 = (@as(u32, 1) << @intCast(len)) - 1;
|
||||||
|
return ones << @intCast(start);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The lowest tag in a mask, as a mask of its own. What a view falls back to
|
||||||
|
/// when the tags it was showing have moved to another screen.
|
||||||
|
pub fn lowestTag(tags: u32) u32 {
|
||||||
|
const t = tags & all_tags;
|
||||||
|
if (t == 0) return 0;
|
||||||
|
return @as(u32, 1) << @intCast(@ctz(t));
|
||||||
|
}
|
||||||
|
|
||||||
/// Keyboard modifiers, matching the values of river_seat_v1.modifiers so the
|
/// Keyboard modifiers, matching the values of river_seat_v1.modifiers so the
|
||||||
/// mask can be bit-cast straight into the protocol type.
|
/// mask can be bit-cast straight into the protocol type.
|
||||||
pub const Mods = struct {
|
pub const Mods = struct {
|
||||||
|
|||||||
+24
-9
@@ -60,9 +60,22 @@ pub const mfact: f32 = 0.55;
|
|||||||
pub const mfact_min: f32 = 0.05;
|
pub const mfact_min: f32 = 0.05;
|
||||||
pub const mfact_max: f32 = 0.95;
|
pub const mfact_max: f32 = 0.95;
|
||||||
|
|
||||||
/// Tags visible on a newly connected output.
|
/// Tags visible on a newly connected output. Clamped to the tags that output
|
||||||
|
/// owns when `split_tags` is on, so a second screen starts on the first tag of
|
||||||
|
/// its own range rather than on this one.
|
||||||
pub const default_tags: u32 = 1;
|
pub const default_tags: u32 = 1;
|
||||||
|
|
||||||
|
/// Split the tag set across the connected screens, so that every tag lives on
|
||||||
|
/// exactly one of them: with two monitors the left owns tags 1–5 and the right
|
||||||
|
/// 6–9, `Mod+7` moves to the right-hand screen and shows tag 7 there, and
|
||||||
|
/// `Mod+Shift+7` sends the focused window over to it. Screens are ordered left
|
||||||
|
/// to right by where they sit in the output layout, not by the order they were
|
||||||
|
/// plugged in.
|
||||||
|
///
|
||||||
|
/// Set this false for dwm's model instead, where every screen has a full set of
|
||||||
|
/// nine tags of its own and the tag keys never leave the one you are on.
|
||||||
|
pub const split_tags = true;
|
||||||
|
|
||||||
/// Names exported over IPC for bars to label tags with.
|
/// Names exported over IPC for bars to label tags with.
|
||||||
pub const tag_names = [action.tag_count][]const u8{
|
pub const tag_names = [action.tag_count][]const u8{
|
||||||
"1", "2", "3", "4", "5", "6", "7", "8", "9",
|
"1", "2", "3", "4", "5", "6", "7", "8", "9",
|
||||||
@@ -188,9 +201,10 @@ pub const keys = tagKeys() ++ [_]Key{
|
|||||||
.{ .mods = mod | Mods.shift, .keysym = xkb.Keysym.k, .action = .{ .swap = .prev } },
|
.{ .mods = mod | Mods.shift, .keysym = xkb.Keysym.k, .action = .{ .swap = .prev } },
|
||||||
.{ .mods = mod, .keysym = xkb.Keysym.Return, .action = .zoom },
|
.{ .mods = mod, .keysym = xkb.Keysym.Return, .action = .zoom },
|
||||||
|
|
||||||
// Master area.
|
// Master area. dwm puts these on Mod+h/l; those are the monitor keys here,
|
||||||
.{ .mods = mod, .keysym = xkb.Keysym.h, .action = .{ .mfact = .{ .relative = -0.05 } } },
|
// so the master area takes the pair dwm gives the monitors.
|
||||||
.{ .mods = mod, .keysym = xkb.Keysym.l, .action = .{ .mfact = .{ .relative = 0.05 } } },
|
.{ .mods = mod, .keysym = xkb.Keysym.comma, .action = .{ .mfact = .{ .relative = -0.05 } } },
|
||||||
|
.{ .mods = mod, .keysym = xkb.Keysym.period, .action = .{ .mfact = .{ .relative = 0.05 } } },
|
||||||
.{ .mods = mod, .keysym = xkb.Keysym.i, .action = .{ .nmaster = .{ .relative = 1 } } },
|
.{ .mods = mod, .keysym = xkb.Keysym.i, .action = .{ .nmaster = .{ .relative = 1 } } },
|
||||||
.{ .mods = mod, .keysym = xkb.Keysym.d, .action = .{ .nmaster = .{ .relative = -1 } } },
|
.{ .mods = mod, .keysym = xkb.Keysym.d, .action = .{ .nmaster = .{ .relative = -1 } } },
|
||||||
|
|
||||||
@@ -208,11 +222,12 @@ pub const keys = tagKeys() ++ [_]Key{
|
|||||||
.{ .mods = mod, .keysym = xkb.Keysym.Tab, .action = .view_prev },
|
.{ .mods = mod, .keysym = xkb.Keysym.Tab, .action = .view_prev },
|
||||||
.{ .mods = mod, .keysym = xkb.Keysym.Escape, .action = .view_prev },
|
.{ .mods = mod, .keysym = xkb.Keysym.Escape, .action = .view_prev },
|
||||||
|
|
||||||
// Outputs.
|
// Outputs. Left and right on the same keys as the h/l of vi, since that is
|
||||||
.{ .mods = mod, .keysym = xkb.Keysym.comma, .action = .{ .focus_output = .prev } },
|
// the direction they move in: `prev` is the screen to the left.
|
||||||
.{ .mods = mod, .keysym = xkb.Keysym.period, .action = .{ .focus_output = .next } },
|
.{ .mods = mod, .keysym = xkb.Keysym.h, .action = .{ .focus_output = .prev } },
|
||||||
.{ .mods = mod | Mods.shift, .keysym = xkb.Keysym.comma, .action = .{ .send_to_output = .prev } },
|
.{ .mods = mod, .keysym = xkb.Keysym.l, .action = .{ .focus_output = .next } },
|
||||||
.{ .mods = mod | Mods.shift, .keysym = xkb.Keysym.period, .action = .{ .send_to_output = .next } },
|
.{ .mods = mod | Mods.shift, .keysym = xkb.Keysym.h, .action = .{ .send_to_output = .prev } },
|
||||||
|
.{ .mods = mod | Mods.shift, .keysym = xkb.Keysym.l, .action = .{ .send_to_output = .next } },
|
||||||
};
|
};
|
||||||
|
|
||||||
/// dwm's TAGKEYS macro: Mod+N views, Mod+Shift+N tags, Mod+Ctrl+N toggles the
|
/// dwm's TAGKEYS macro: Mod+N views, Mod+Shift+N tags, Mod+Ctrl+N toggles the
|
||||||
|
|||||||
+5
-1
@@ -319,10 +319,14 @@ fn encodeState(wm: *Wm, gpa: Allocator, out: *std.ArrayList(u8)) !void {
|
|||||||
try w.writeAll("{\"name\":");
|
try w.writeAll("{\"name\":");
|
||||||
try writeJsonString(w, output.displayName());
|
try writeJsonString(w, output.displayName());
|
||||||
try w.print(
|
try w.print(
|
||||||
",\"focused\":{s},\"tags\":{d},\"occupied\":{d},\"layout\":\"{s}\",\"layout_symbol\":",
|
",\"focused\":{s},\"tags\":{d},\"owned_tags\":{d},\"occupied\":{d},\"layout\":\"{s}\",\"layout_symbol\":",
|
||||||
.{
|
.{
|
||||||
if (wm.focused_output == output) "true" else "false",
|
if (wm.focused_output == output) "true" else "false",
|
||||||
output.tags,
|
output.tags,
|
||||||
|
// Which tags belong to this screen at all. Every one of them
|
||||||
|
// unless the tag set is split, in which case a bar wants to
|
||||||
|
// draw its screen's slice and not all nine.
|
||||||
|
output.owned_tags,
|
||||||
occupied & act.all_tags,
|
occupied & act.all_tags,
|
||||||
@tagName(st.layout),
|
@tagName(st.layout),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -244,6 +244,78 @@ test "bits above the tag range do not affect the slot" {
|
|||||||
try testing.expectEqual(act.tag_count, act.tagSlot(@as(u32, 1) << (act.tag_count - 1)));
|
try testing.expectEqual(act.tag_count, act.tagSlot(@as(u32, 1) << (act.tag_count - 1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── splitting the tag set across screens ────────────────────────────────────
|
||||||
|
|
||||||
|
test "a lone screen owns every tag" {
|
||||||
|
try testing.expectEqual(act.all_tags, act.tagsForOutput(0, 1));
|
||||||
|
// No screens at all is not a case callers should have to think about.
|
||||||
|
try testing.expectEqual(act.all_tags, act.tagsForOutput(0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "two screens split the tags into contiguous halves" {
|
||||||
|
// Nine tags do not halve evenly; the left screen takes the extra one.
|
||||||
|
try testing.expectEqual(@as(u32, 0b000011111), act.tagsForOutput(0, 2));
|
||||||
|
try testing.expectEqual(@as(u32, 0b111100000), act.tagsForOutput(1, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "three screens split the tags into thirds" {
|
||||||
|
try testing.expectEqual(@as(u32, 0b000000111), act.tagsForOutput(0, 3));
|
||||||
|
try testing.expectEqual(@as(u32, 0b000111000), act.tagsForOutput(1, 3));
|
||||||
|
try testing.expectEqual(@as(u32, 0b111000000), act.tagsForOutput(2, 3));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "every tag lands on exactly one screen" {
|
||||||
|
// The whole point of the split: no tag is shared and none goes missing,
|
||||||
|
// whatever the screen count.
|
||||||
|
for (1..act.tag_count + 1) |count| {
|
||||||
|
var seen: u32 = 0;
|
||||||
|
for (0..count) |i| {
|
||||||
|
const mask = act.tagsForOutput(i, count);
|
||||||
|
try testing.expect(mask != 0);
|
||||||
|
try testing.expectEqual(@as(u32, 0), seen & mask);
|
||||||
|
seen |= mask;
|
||||||
|
}
|
||||||
|
try testing.expectEqual(act.all_tags, seen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "ranges are contiguous and in screen order" {
|
||||||
|
for (1..act.tag_count + 1) |count| {
|
||||||
|
var next: u32 = 0;
|
||||||
|
for (0..count) |i| {
|
||||||
|
const mask = act.tagsForOutput(i, count);
|
||||||
|
// Contiguous: a run of ones, starting where the last range ended.
|
||||||
|
const lowest = @ctz(mask);
|
||||||
|
const past_highest = 32 - @clz(mask);
|
||||||
|
try testing.expectEqual(next, lowest);
|
||||||
|
try testing.expectEqual(past_highest - lowest, @popCount(mask));
|
||||||
|
next = past_highest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "more screens than tags leaves no screen without one" {
|
||||||
|
// Twelve monitors and nine tags: the ones past the end share the last tag
|
||||||
|
// rather than being handed an empty mask, which could show nothing at all.
|
||||||
|
const count = act.tag_count + 3;
|
||||||
|
for (0..count) |i| {
|
||||||
|
try testing.expect(act.tagsForOutput(i, count) != 0);
|
||||||
|
}
|
||||||
|
try testing.expectEqual(act.tagsForOutput(act.tag_count - 1, count), act.tagsForOutput(count - 1, count));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "a screen index past the end owns everything rather than nothing" {
|
||||||
|
try testing.expectEqual(act.all_tags, act.tagsForOutput(2, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "the lowest tag is what a stranded view falls back to" {
|
||||||
|
try testing.expectEqual(@as(u32, 0b100000), act.lowestTag(0b111100000));
|
||||||
|
try testing.expectEqual(@as(u32, 1), act.lowestTag(act.all_tags));
|
||||||
|
try testing.expectEqual(@as(u32, 0), act.lowestTag(0));
|
||||||
|
// Junk above the tag range is not a tag to fall back to.
|
||||||
|
try testing.expectEqual(@as(u32, 0), act.lowestTag(~act.all_tags));
|
||||||
|
}
|
||||||
|
|
||||||
// ─── command parsing ─────────────────────────────────────────────────────────
|
// ─── command parsing ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn parseOk(argv: []const []const u8) act.Action {
|
fn parseOk(argv: []const []const u8) act.Action {
|
||||||
|
|||||||
Reference in New Issue
Block a user