1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
|
# ext-window-tiling — baseline design (slice 7)
> **Status: BASELINE DECIDED — build off this.** The agreed starting point for
> the tiling extension. Layout catalogue to grow into later lives in
> `notes/tiling-layouts-reference.md`. Open questions at the bottom are to be
> resolved as we build.
## Naming (replaces dwm's "master")
dwm's "master/stack" → we use:
- **primary** — the window that gets the most space / most attention.
- **stack** — every other tiled window.
Rationale: "master" is unclear (and carries master/slave baggage). "primary" is
plain and position-agnostic (it works whether the big window is left or right).
Considered alternatives: `hero`, `main` (fine), `focus` (REJECTED — collides
with keyboard focus), `stage` (REJECTED — taken by the stage dock).
**Pending: final user sign-off + `GLOSSARY.md` entry** for `primary` / `stack`
(no-synonym-coinage rule).
## The baseline layout
A single default tiling layout. Mirror of dwm's `tile`; the primary column sits
on the **right by default but the side is configurable** (`primary_side`), so it
can be flipped to a left primary + right stack.
- **New windows auto-tile** — opening a window drops it into the layout
automatically; no manual placement step. Where it lands in the stack (top or
bottom) is **configurable** (`new_window`).
- Window-count behaviour:
- **1 window** → fills the whole area (fullscreen).
- **2 windows** → split 50/50: primary right half, the one stack window left
half.
- **3+ windows** → primary takes the right portion (`split_ratio` of the
width); the remaining windows share the LEFT portion, stacked vertically and
sized evenly.
```
1 window 2 windows 3+ windows
┌──────────┐ ┌─────┬─────┐ ┌────┬────────┐
│ │ │ │ │ │ S1 │ │
│ P │ │ S │ P │ ├────┤ P │
│ │ │ │ │ │ S2 │ │
└──────────┘ └─────┴─────┘ ├────┤ │
│ S3 │ │
└────┴────────┘
P = primary (right) S = stack (left, vertical)
```
## The ordering model (decided)
One **ordered list** of tiled windows. The **head of the list is the primary**;
the remaining elements are the **stack**, rendered top-to-bottom in list order.
- **New windows enter at the BOTTOM of the stack** (append to the list end) —
they do NOT steal the primary slot.
- **The top of the stack is promoted to primary** when the primary slot is
vacated (the primary window closes or is moved out). Because the primary is
just the head of the list, removing the head makes the next element (the old
top-of-stack) the new primary automatically.
- Net effect of the two defaults: opening windows never disturbs your current
primary; closing the primary hands the big slot to the longest-waiting stack
window (the one at the top).
## Config (`unbox.toml`, `[tiling]` table)
```toml
[tiling]
primary_side = "right" # "right" (default) or "left"
new_window = "bottom" # "bottom" (default) or "top" of the stack
split_ratio = 0.55 # fraction of width given to the primary column
primary_count = 1 # windows in the primary area (baseline 1)
```
- **`primary_side`** — which side the primary column sits on. Default **right**.
- **`new_window`** — where a newly-opened window enters the stack. Default
**bottom**. (`top` makes the newest window the first in the stack and thus the
next in line for promotion to primary.)
- **`split_ratio`** (dwm `mfact`) — primary column width fraction. **Static
config for now** (no runtime keybind/drag yet — add when friction demands).
- **`primary_count`** (dwm `nmaster`) — baseline **1**; model allows >1 later
without redesign.
### Hot-reloadable (all four)
All four values **live-reload** — edit `unbox.toml`, save, and the change applies
with no restart. This reuses the existing kernel primitive
`Host::watch_file(path, cb) -> FileWatch` that already backs ext-keybindings'
config reload (RAII, coalesced, editor-save/create-safe, error-isolated; one
session inotify is shared). ext-window-tiling watches the same `unbox.toml` and
re-parses its own `[tiling]` table.
On a save:
- Re-read + re-parse `[tiling]` (pure). **Keep-old-on-bad:** a malformed /
mid-edit file keeps the current values and logs one warning — never drops a
working layout (same contract as the keybindings reload).
- **`primary_side`, `split_ratio`, `primary_count`** take effect immediately:
swap the live values and **re-arrange every output's tiled set** that frame.
- **`new_window`** updates the stored value but only affects windows opened
*after* the change (it governs insertion order, not existing windows) — no
re-arrange needed.
The watcher holds no tiling state; it just hands new parsed values to the same
pure core, so reload is a value swap + a re-arrange call (no re-subscribe, no
window churn).
## The pure core (slice-7 contract shape)
Two pure pieces, both wlroots-free and 100% doctest-coverable (the slice-7 "pure
decision core" rule):
```
# geometry: ordered window list -> one Box each, in list order
arrange(area: Box, n: int, primary_count: int, split_ratio: float,
primary_side: Side) -> [Box]
# list management: where a new window is inserted (top|bottom) and primary
# promotion on removal — pure operations over the ordered list of opaque tokens
```
- `area` = the usable region (already minus the status bar / any reserved zones).
- `arrange` returns one `Box` per window in list order (head = primary).
- `new_window` (top/bottom) and promotion-on-remove govern the LIST order, not
the geometry — keep them separate from `arrange`.
- Effects (assigning rects to real toplevels via the scene) live in thin glue.
## Decided
- **Primary side:** configurable (`primary_side`), default **right**.
- **New-window insertion:** configurable (`new_window`), default **bottom** of
the stack.
- **Primary promotion:** when the primary leaves, the **top of the stack** is
promoted to primary (automatic — it's the new list head).
## Scope discipline
Keep the baseline minimal and **fully automatic**: **NO new keybindings or
gestures for tiling at this stage.** Windows auto-tile on open; the primary is
the list head; that's it. Everything below is **deferred until real-use friction
makes it the next thing to fix** — the WM is being dogfooded on the CF-AX3 and
features get added incrementally, largest friction first.
## Deferred until needed (NOT in baseline)
- **Manual primary swap / move-in-stack** — promoting an arbitrary stack window
or reordering. (Needs a keybind/gesture → out of scope for now.)
- **Runtime `split_ratio` / `primary_count` changes** — config-only for now.
- **Focus + cycling across primary/stack** — existing ext-keybindings focus ring
(Alt+Tab) already moves focus; no tiling-specific focus controls added now.
- **Floating exceptions** — dialogs / fixed-size / transient windows staying
floating. Confirm + implement when a real app needs it.
- **Gaps** (dwm `vanitygaps`) — inner/outer gaps as a parameter (not a layout).
- **Touch ergonomics (CF-AX3):** many stack windows → thin left column; cap stack
count or collapse to deck/monocle past N (a future second layout, not baseline).
- **Interaction with the rest of unbox:** minimize-to-stage-dock removing a
window from the tiled set and restoring re-inserting it; home-screen /
fullscreen-app vs the tiled set.
- **Per-output / per-workspace scope** — what tiling state is keyed to (workspace
model is itself a deferred decision — see plan.md §7).
## Not in baseline (build off this later)
See `notes/tiling-layouts-reference.md` — deck, monocle, bstack, centeredmaster,
spiral/dwindle, grids, flextile-deluxe-style configurability, cfacts weights.
|