An extensible window manager for the river Wayland compositor, written in Common Lisp, whose layout model is a replaceable policy layer rather than the program itself.
It tiles the way you expect — recursive splits, tabs, workspaces, floating, multi-monitor, pointer drag-and-resize, panels and status bars — and then lets you change any of it while it is running, without a restart and without a rebuild.
A Lisp window manager is not a new idea, and this one does not pretend to be one. StumpWM has carried it on X11 for two decades, and Emacs made the case for a live, self-documenting system long before that. LatticeWM takes the idea to Wayland — to river specifically, whose window-management protocol hands layout to an outside program — and pushes on one thing the others leave fixed: the layout model itself is replaceable, from outside, without editing the program.
A pane may deliberately hold no window, and that is not a degenerate case — it is how you place slack.
In the tiling window managers most people arrive from — i3, sway, dwm, Hyprland — a pane is where a window is, so an empty one is a transient state on the way to being filled rather than something you can stand in. Here it is a thing you place on purpose:
- you split a pane and leave one side empty, and the window occupies the rest. That is the resize idiom;
- focus is a place, not a window. It can rest on an empty pane, and Wayland keyboard focus is derived from where it rests rather than stored;
- so
close(take the pane with it) andclear(empty it, leave it standing) are two different verbs rather than one verb with a mode; - and standing in an empty pane, typing
topens a terminal,ean editor. The empty pane is a spawn menu with no menu.
Everything else in the model follows from that, including a few things that look surprising until you have read it. It is worth the thirty seconds.
Worth being exact about, because “written in Lisp” describes the implementation and is not on its own a reason to use anything.
Not new. Tiling, workspaces, tabs, floating, a live REPL attached to the running window manager, keybindings you change without restarting, commands carrying their own documentation. StumpWM has had all of that for years, and this project is downstream of that tradition rather than beside it.
What is actually different is one structural decision, and the rest of this README follows from it: *the layout model is a policy layer with a published surface, and the shipped tiling behaviour is one implementation of that surface rather than the program itself.* Sixty-nine generic functions decide what happens; the shipped methods are the defaults; specialising one is how you change behaviour, live, without touching the core.
The test of that is lattice/ — an infinite two-dimensional plane of cells,
which is a different layout model with a container kind the core has never heard
of. It lives in a separate ASDF system, depends only on the exported policy
package, and edits nothing under =src/=. Two gates hold it there on every
build: gate 3 fails if the extension reaches into the core, gate 4 fails if the
core cannot load, start and lay out with the extension absent.
That is the falsifiable version of “extensible”. It is also why this section
does not tell you how extensible the program is: gate 6 counts how many policy
generics anybody outside src/ has actually specialised and prints the number
on every build, including the less flattering version with the flagship
extension excluded.
You need river 0.4.5 or newer and SBCL. Newer is genuinely fine, including rivers released after this build: LatticeWM binds the window management protocol at its own version, so a river that has moved ahead speaks the older dialect back and you lose only the features nobody has vendored yet. Below the floor it refuses and names both numbers, because it would be sending requests that river does not have — INSTALL.org has the reasoning. Any Linux; nix is one supported route, not a requirement. Per-distribution package names are in INSTALL.org.
git clone https://github.com/SYKhayyat/LatticeWM && cd LatticeWM
./bootstrap.sh # fetch the Lisp dependencies into ./.deps/ (or: nix-shell)
make # build, twenty-two gates, and the unit suite
make check # the above, plus a real river driven headlessly -- and it
# fails rather than skips when river or a terminal is absent
make run # river nested in the session you are already in — try it here firstOnce you like it:
./install.sh # binary, launcher, session entry, man pages, sources, licencesinstall.sh adds a wayland-sessions desktop entry, so LatticeWM appears at
your login screen like any other desktop. --prefix /usr/local installs
system-wide; --uninstall takes it back out. It never overwrites an existing
init.lisp. It is also the only installer — nix build runs this same script,
so there is one list of what ships rather than two that drift.
In a session you are already running river in: river -c latticewm.
=Super+/= draws the entire keymap on screen, with what each binding does — built from the live bindings and each command’s own docstring, so it includes anything you bound five minutes ago and it cannot go stale.
You will also get a short welcome overlay the first time it starts, listing
the half-dozen keys that matter. Any key dismisses it, it never appears again,
and M-x welcome brings it back. To stop it appearing at all:
(setf *welcome-on-first-run* nil) ; in ~/.config/latticewm/init.lispEvery binding below is generated from *modifier*, which defaults to Super.
Change that one value and the whole keymap moves with it.
Both the vi letters and the arrow keys are always bound. That is deliberate rather than indecisive: the arrows are what you use on your first day and the letters are what you use on your hundredth.
Super+Return | a terminal |
Super+d | split horizontally |
Super+s | split vertically |
Super+q | close the window |
Super+h j k l / arrows | move focus left, down, up, right |
Super+Tab | focus the next pane (Shift for previous) |
Shift+Super+h j k l | move the window |
Ctrl+Super+h j k l | resize |
Alt+Super+h j k l | swap with the neighbour |
Super+e | equalize this container (Shift for all) |
Shift+Super+q | clear the pane |
Super+Space | float this window / put it back |
Super+` | focus the floating layer |
Super+f | fullscreen |
Super+m | minimize (Shift+Super+m restores) |
Super+w | tab this pane (Shift+Super+w untabs) |
Super+[ / Super+] | previous / next tab |
Super+z | undo the last layout change (Shift+Super+z redoes) |
Super+- | show/hide a named scratchpad (Shift+Super+- puts one away) |
Super+t | tag this window (Shift+Super+t jumps to a tag) |
Minimize leaves the tiling tree rather than collapsing it, so restoring puts the window back where it was. Floating and unfloating do the same, so looking at something and putting it back is symmetric.
Undo is a layout undo. It restores the arrangement, and it cannot un-quit an
application — a window that has closed since is simply absent from the tree
that comes back. Super+? u lists what it would walk through.
It covers every door that changes the layout — the keys, Super+;, the control
socket and SLIME — because it is taken at the moment the tree settles rather than
hung off any one command path.
| =Super=+drag | move a window (a tiled one floats first) |
| =Super=+right-drag | resize it, from the corner you grabbed |
| click | focus, and raise if it is floating |
| a titlebar drag | honoured, for clients that draw their own |
No unmodified button is bound, deliberately: a window manager that grabs plain left-click cannot be used with any application.
Super+1 … Super+0 | go to workspace 1–10 |
Shift+Super+1 … Shift+Super+0 | send this window there |
Super+n | a new workspace |
Super+PageUp / PageDown | previous / next workspace |
Workspaces are one-based, so the key and the number on it agree.
Each screen shows its own workspace and keeps it across an undock. Two things are worth knowing and neither needs configuring.
Motion crosses the screen boundary. Walk off the right-hand edge of the left
monitor with Super+Right and you are on the right-hand one — no chord, no
mode, and you arrive where you last were on that screen rather than at its
first pane. Motion in this window manager is continuous across every boundary
and the screen is simply the last of them. With one monitor there is nothing
in any direction and nothing changes.
Ctrl+Shift+Super+h j k l / arrows | send this window to the next screen |
M-x focus-output moves the cursor to another screen without walking there,
for when you are in the middle of one. It has no binding on purpose: the arrow
keys already do it.
By default it splits the focused pane, along whichever side is longer so panes tend towards square. Three settings and one table cover almost everything people want to change:
(setf *spawn-mode* :fill-first) ; :split (default), :fill-first, :stack
(setf *split-axis* :horizontal) ; :longer (default), :horizontal, :vertical
(setf *window-rules*
'(((:app-id "pavucontrol") :float t)
((:app-id "firefox") :workspace 2)
((:title-contains "Picture-in") :float t)
((:parent t) :float t))) ; dialogsMatch on :app-id, :title, either of those as a substring, or :parent for
what river reports as a child window; the first rule that matches wins. To find
out what to write, ask the running program — (mapcar #'window-app-id
(all-windows)) at Super+; is the same list a rule is matched against.
examples/02-window-rules.lisp is the worked version, including the method to
write when a table is not enough, and latticewm --list-options prints every
override key with what it does.
There is no limit and there is no configured count. The workspace list is a
stack at the root of the layout tree, and a stack grows: asking for one that is
not there makes it, and every one before it. Super+x workspace 40 works on a
machine that has three. What a new workspace contains is a setting
(*new-workspace*) and, more usefully, a policy decision — which is how the
lattice makes every workspace a plane rather than only the ones that existed
when it loaded.
Super+Return | *terminal* (default foot) |
Super+b | *browser* (default firefox) |
Super+o | *editor* (default emacs) |
The half of Emacs worth copying is not the keybindings — it is that you can always ask, and the answer comes from the running system rather than from a manual. Every command here is named, documented and reachable, and every screen below is built by interrogating the live image:
Super+/ | the whole keymap, drawn from the live bindings |
Super+x | run any command by name — including ones that take arguments, which it asks for |
Super+; | evaluate a Lisp form inside the running window manager |
Super+. | do that again, with the same arguments |
Super+? k | what does this key do? |
Super+? a | is there a command for …? — searches every docstring |
Super+? c | show a command’s documentation and the keys that run it |
Super+? o | what is this setting, what did it ship as, and why |
Super+? s | change a setting, now, without a file or a restart |
Super+? u | what would undo walk back through? |
Super+? t | which windows are tagged what? |
Super+? p | what is on the named scratchpads? |
Pressing a chord’s first key lists what the second can be. Completion is
prefix, then substring, then subsequence — so wsp finds send-to-workspace.
The prompt has a point, a history ring and the readline keys, because a prompt
you can only backspace out of is one you stop using.
| Super+/, the keymap | M-x, with the completion it ships |
| A chord says what it offers | Search every command by what it does |
Optional, and off by default. Add to ~/.config/latticewm/init.lisp:
(load-extension "lattice")
(lattice:enable)Each workspace becomes an infinite plane of cells addressed by integer coordinate. Zoom is the choice of how many cells you can see — 1, 2, 4, 6, 8 — and past that it becomes a drawn map. Motion runs straight through a cell boundary as though it were not there.
| Cells and coordinates | The drawn map at deep zoom |
Infinitely many planes, one behind another. Every workspace is a plane — not
just the ones open when you enabled it — so the workspace keys are a third axis
through a stack of infinite planes, and Super+PageUp / PageDown walk it.
Four settings say what that feels like:
*new-workspace-zoom* | a new plane’s zoom. :inherit — the one you were on |
*new-workspace-origin* | where its view starts. :inherit — directly behind the plane you left |
*new-workspace-cells* | cells it is born with. nil — one, in view |
*workspace-entry* | which cell you land on: :remembered (where you left that plane), :aligned (the coordinate you are on now), :origin, :occupied |
The defaults read the phrase literally: same zoom, same window of coordinates,
and you keep your place. Set *workspace-entry* to :aligned and
*new-workspace-origin* to :inherit for pure Z-axis movement — X and Y never
change, only which plane is under you.
Two ways for zoom to make room, and it matters once you resize a column.
*zoom-mode* is :fit by default: the visible cells stretch to fill the screen
exactly. That is free while every column is the same width, and the moment one
is not, panning changes the width of every window on screen — which is what
:fixed is for.
:fit | cells stretch to tile the screen exactly. Panning a non-uniform plane resizes windows |
:fixed | a cell is *cell-width* by *cell-height* pixels wherever it sits; the plane scrolls under the screen and the trailing cell is cropped at the edge, border redrawn there by the compositor |
Under :fixed a column you widen is that much wider in pixels, always, and
panning resizes nothing. The crop is river’s set_content_clip_box and it is
the reason the partial cell at the edge reads as a cleanly cut cell rather than
a window sliced in half.
The lattice is the project’s own experiment on itself: it is a separate ASDF system that depends on the policy package and nothing else, and a CI gate asserts on every build that it touches no core file. What that cost is written up in FINDINGS.org — read that first if you want to know whether the premise holds.
The one thing you cannot see by looking at your own screen is that somebody else can see it too. A screen share you forgot to stop looks exactly like a screen share you never started.
River reports how many capture sessions are reading each window and each
monitor, so LatticeWM can tell you: it says so in the echo area when one starts
or stops, the shipped status line carries a standing REC for as long as one
lasts — because an announcement scrolls away and the fact does not — and M-x
captures lists what is being recorded, including a window you minimized and
can no longer see. :capture-changed is the hook for anything else: a status
bar, a light on a keyboard, a script that mutes a microphone.
Nothing about placement changes. A window being recorded is still tiled,
hidden, minimized and closed exactly as it was; this is information, and it is
deliberately only information. Set *announce-capture* to nil for the status
line without a line in the echo area every time OBS adds a source.
This needs a river that carries interface version 5 — 0.4.6 or newer. Against an older one the count is simply never reported, which the model keeps distinct from “reported as zero”.
The configuration file is Common Lisp, but configuring it is not programming:
the common case is a list of one-line settings like (setf *gaps* 8), and if
you have edited a dotfile you have the whole skill. Never written Lisp?
doc/EXTENDING.org opens with a five-minute primer that makes every snippet here
readable.
latticewm --write-config # a commented starter ~/.config/latticewm/init.lisp
latticewm --list-options # every setting, its value, its default, and why
latticewm --list-keys # every binding
latticewm --list-commands # every commandAnything you can set in the file you can also set live with Super+? s, and
Shift+Super+c reloads the file without restarting.
On river there is nothing else on the machine that configures input — no
settings daemon, no xinput, no compositor config file. The window manager
owns it, so this is where tap-to-click and your keyboard layout live.
(setf *xkb-layout* "de") ; what the keys do, for every application
(setf *xkb-options* "ctrl:nocaps") ; caps lock becomes control
(setf *tap-to-click* t) ; on by default
(setf *natural-scroll* t)
(setf *repeat-rate* 50) ; key repeats per second
(setf *repeat-delay* 300) ; ms held before repeating starts
(setf *accel-speed* 0.3d0) ; -1 slowest, 0 default, 1 fastestA laptop has a touchpad and a mouse and they want opposite things, so rules
match per device. Later rules win, which makes a rule for t a default and a
rule naming a device an override:
(setf *input-rules*
'(("Touchpad" :natural-scroll t :click-method :clickfinger)
("TrackPoint" :scroll-method :on-button-down :scroll-button 274)
("Logitech" :accel-profile :flat)))(list-inputs) prints every device with its name, its kind, everything
settable on it and what is currently in force — which is where those names come
from. (set-input "Touchpad" :natural-scroll t) tries one without writing it
down. (keyboard-layout "fr") switches layout now;
(next-keyboard-layout) cycles through the layouts of a multi-layout keymap
and can go on any key you like.
Setting *xkb-layout* needs xkbcli, which ships with libxkbcommon and is
therefore already installed on any machine running river. Without it the layout
stays as river started it, and one log line says so.
River does the xkb work and does not tell us the shifted keysym, so the
window manager cannot derive what Shift+9 produces — it has to be declared
per layout, or its own prompt inserts American punctuation on a German
keyboard.
Setting *xkb-layout* adopts the matching table automatically, so in the
ordinary case there is nothing to do. To set one by hand:
(setf *keyboard-layout* "de") ; us, uk, de, fr, dvorak, colemak(shift-map-names) lists what is registered, and register-shift-map adds one
in three lines.
Anything using wlr-layer-shell works: waybar, yambar, mako, swaybg, swaync.
LatticeWM binds river’s layer-shell protocol and lays windows out inside the
area panels have left themselves.
Measured against a real waybar on a 1280x720 output: the usable area goes from
0,0 1280x696 to 0,30 1280x666 when a 30-pixel bar appears, the tiled window
under it moves by exactly that, and both go back when the bar exits. The number
comes from the compositor — river subtracts every layer surface’s exclusive
zone and tells us what is left — so this works for any panel, on any edge, at
any size, with nothing to configure. *honour-exclusive-zones* turns it off.
Lockers reach the screen two different ways, and LatticeWM handles both.
Modern lockers — swaylock, gtklock — use ext-session-lock-v1, a protocol the
compositor implements: river owns the lock surface and its input, LatticeWM is
told (river sends session_locked, and it is recorded on the world) and has
nothing else to do. The window manager stays alive under the lock, the tree
keeps its windows, and killing the locker does not unlock the session — the
protocol working as designed.
Older lockers instead take the keyboard exclusively through a layer surface. LatticeWM handles that too: it stops sending focus requests and stops drawing a focused border on a window that does not have focus, so it does not re-focus a window underneath the lock screen on every manage sequence.
It ships with Terminus, and it will use any PSF console font — which means the
few dozen already under /usr/share/kbd/consolefonts/ on your machine, with
nothing to find, convert or install.
M-x load-font /usr/share/kbd/consolefonts/ter-124n.psf.gz
That takes effect immediately. To make it stick, or to use different fonts in different places:
(register-font (load-psf "/usr/share/kbd/consolefonts/ter-124n.psf.gz"))
(setf *ui-font* "ter-124n")
;; Or per role — :echo, :minibuffer, :help, :hint, :overlay, :map.
(defmethod font-for ((policy conventional-policy) (role (eql :map)))
(find-font "ter-112n"))Widths up to sixteen pixels and any height. (font-names) lists what is
registered.
The point of the project. Changing behaviour means writing one defmethod in
your own file — no core edit, no rebuild, and it takes effect immediately:
(defmethod p:gaps ((policy p:conventional-policy) container) 8)latticewm --extension-surface # every policy generic you can specialize
latticewm --container-surface # every generic a new container kind must answer
latticewm --hooks # every hook, what it is called with, and whenThere are three surfaces, and they answer different questions. The extension surface is what the window manager decides; the container surface is what it can hold; the hooks are what it notices. All three are generated from the running image, so none of them can drift, and the first two list the methods you added — which is how you check that your configuration file was loaded. Gate 2 requires every member of both protocols to be documented.
The container surface matters because a new container kind that answers only the obvious members of the protocol loads without complaint and then breaks on copy, save and focus repair — the failure modes are silent, so the surface is generated from the image and gate 2 requires every member documented rather than left to be discovered the hard way.
The hook surface is the mechanism for noticing — a status bar, a recording indicator, a script that starts something per screen. A hook’s contract is the arguments its functions receive and the moment they receive them; gate 14 requires every declared hook to be attached to and watched firing by the unit suite, the integration run or the session recorder, whichever tier can produce the event at all, so the list and its argument lists cannot go stale.
Four worked examples are in examples/, including a whole alternate layout
model in one method and a new container kind — niri’s scrolling strip — in
sixty lines. doc/EXTENDING.org walks through them.
There is a control socket — a Unix socket in $XDG_RUNTIME_DIR, mode 0600 —
and the binary is its own client:
latticewm --eval '(workspace 3)'
latticewm --eval '(setf *gaps* 8)' --eval '(relayout :force t)'
latticewm --eval '(mapcar #'window-app-id (all-windows))'Forms run in the window manager’s own thread, so they may touch the compositor. The protocol is write a form, read a line, so anything that can open a socket can drive it.
Worth being exact about how little this costs, because it is the capability the
rest of the design rests on. Press Super+; and type:
(start-swank 4005)Then M-x slime-connect to localhost 4005, and you are at a REPL inside
the running window manager: C-c C-c on a defmethod and the windows move.
No restart, no configuration file, and nothing listening until you ask.
This is the StumpWM development loop, which is where the idea comes from, with the loading already done: SWANK is built into the shipped image, so reaching a REPL is one form rather than a Quicklisp install at an eval prompt.
It is off by default, and it stays that way: a REPL on a TCP port is
arbitrary code execution with no authentication step, in a program that starts
as your session before you have done anything. Put (setf *swank-port* 4005)
in your init.lisp, or pass --swank-port 4005, if you would rather have it
at every startup; it binds to loopback either way. Anything that only wants to
send a form wants the control socket above, which needs no flag and is
already on.
latticewm --check-config # load your init.lisp, report problems, exitRun that before logging out. A configuration error otherwise manifests as a failed session, which is the worst possible moment to find one.
Everything CI runs is reachable from your machine:
make # build, the twenty-two gates, and the unit suite
make test # the unit suite alone
make integration # a real river, headless, driven for a few seconds
make check # all of the above -- and what CI runs
make run # river nested inside your current session
make image # dump ./latticewm -- 13 MB; make image-fast for the dev loop
make surface # regenerate the six documents under doc/Twenty-two gates run on every build, and on every push. They are not style checks: each one exists because something in this project rotted silently once, and each prints the argument for its own existence before it prints what is wrong with your branch.
The argument for each gate lives in the gate — as the comment above it in
tools/gates.lisp, which is the one place it cannot drift away from the check
it describes. This README does not repeat it: a second copy is exactly the kind
of drift the gates exist to catch.
Where to go instead:
- CONTRIBUTING.md — what will refuse your branch, as a table, before you hit it
- doc/ONBOARDING.org — the map of the tree, the five instruments and what each one structurally cannot see, and what each kind of change obliges you to do
tools/gates.lisp— the reasoning, per gate, beside the gate
make integration runs river on a headless backend, connects to it as the
ordinary Wayland client this program is, opens windows, drives the real commands
from outside a protocol sequence, and asserts on what came back. Every other
check in this project constructs the state it examines, and the bugs that
survive live exactly where constructed state cannot reach.
It earns its place because those bugs are real. The kind it catches and a
constructed test cannot: pointer bindings enabled through the keyboard
interface — a type error inside a generated marshaller, caught, logged, and
otherwise completely silent, so Super+drag did nothing and no test that built
its own world could tell. The model was right; the compositor was the only
witness to the screen being wrong. doc/FINDINGS.org has the rest.
If you are here to use it:
- INSTALL.org — installing on any distribution, and the two version ranges that matter
- doc/TROUBLESHOOTING.org — symptom first, and how far each answer is actually checked
latticewm --list-keys,--list-options,--list-commands— generated from the running image, so they cannot be out of dateSuper+/— the whole keymap, on screen, built from the live keymap
If you are here to change your own window manager:
- doc/EXTENDING.org — the extension guide, in four tiers, with worked examples
- examples/ — five worked extensions, each loaded and exercised on every build
latticewm --extension-surface— every generic you can specialise, what its shipped method reads, and what overrides it
If you are here to change this repository:
- CONTRIBUTING.md — the short version: what will fail your branch, and why
- doc/ONBOARDING.org — the long version: the map, the five instruments, and what each kind of change obliges you to do
The record:
- FINDINGS.org — what building the lattice from outside the core actually cost
- DESIGN.org — the full design record and the reasoning behind every decision
- PLAN.org — the execution plan and the log of what actually happened
The last three are historical and deliberately not edited into agreement with
what shipped. DESIGN.org contains a row reading “Overturned by D22, in this
document”, which is what a record that has not been laundered looks like.
Working, and used on bare metal.
The whole of make build gates test surface is green on SBCL 2.2.6 — the
declared floor — and on 2.2.9 and 2.6.6, and the six generated documents
come out byte-identical on all three. The integration suite is green against
river at both ends of the supported range: 0.4.5, which carries interface
version 4 and is the floor, and 0.4.6, which carries 5 and is what the vendored
protocol was generated from.
INSTALL.org has the full table; src/protocol/PINNED has
the protocol half.
The distinction that matters here is not tested/untested but what kind of thing did the checking, because a suite that builds its own world can only ever confirm the builder.
| area | how far |
|---|---|
| keys, minibuffer, spawn, close, splits, floating, empty-pane spawn, persistence, the lattice | real hardware, by hand |
| the protocol: connection, version negotiation, manage and render sequences, the shm overlay path, buffer release, frame callbacks | a real headless river, every make check |
| every keymap key becoming a binding object, and one bound at runtime becoming one more | a real river |
| a client window announced, placed, sized, focused; two windows in disjoint rectangles | a real river |
| the verbs, driven from outside a protocol sequence the way a script drives them | a real river |
| floating, drag-move, drag-resize, fullscreen, minimize; workspaces, tags, scratchpads, undo, redo | a real river |
| the saved layout, written and read back with a real window’s tags in it | a real river |
| the control socket, one line per form | a real river |
| overlays dismissing on a key bound to nothing | a real river — the arming is the half no unit test can see |
| input configuration: device enumeration, key repeat, compiling and installing a keyboard layout | a real river, running nested, which is where devices exist |
| multi-monitor: one workspace per output, no two the same, surviving repeated workspace switches | two real outputs, live — river’s Wayland backend takes WLR_WL_OUTPUTS=2 — plus unit tests that build a two-output world |
motion crossing the screen boundary, focus-output, send-to-output | two real outputs, live: focus :right moved the cursor from WL-1 to WL-2 and back |
| HiDPI arithmetic: a 2x canvas at twice the device size, logical coordinates for text and fills | a real wl_shm |
| the layer-shell exclusive zone | a real waybar: usable area went 0,0 1280x696 → 0,30 1280x666 for a 30-pixel bar, the window under it moved by exactly that, and both returned when the bar exited |
a screen locker holding an ext-session-lock-v1 lock | a real swaylock over a nested river: the lock is reported, the window manager stays alive, the tree keeps its windows, and commands still run |
The integration suite names these in its own output on every run rather than passing quietly over them, and a headless backend is why:
- a physically scaled output — the backend is 1x, so scaling is exercised as arithmetic and never as a display;
- libinput hardware — tap-to-click, acceleration, click and scroll method are written and unit-tested as decisions and cannot be exercised without a device behind them. This is the one that needs a machine rather than a better harness;
- a pointer press — the two pointer bindings river makes are counted live and
the drag arithmetic is unit-tested, but no button is ever pressed. Synthesised
input gets close and not there:
wlrctloffersclickand no separate press/release, so the displacement half of a drag cannot be produced at all, and a Super+click through a virtual pointer produced nothing observable — which does not distinguish “the binding did not fire” from “the synthetic press never reached the matcher”, so it is reported as untested rather than as a defect; - a layer-shell locker taking exclusive keyboard focus — the branch exists and
is described above, and the lockers that would exercise it have moved to
ext-session-lock-v1, which is checked instead.
PLAN.org §log7 is the current list of which is which.
The reliability work behind that confidence — an orphaned window manager that
spun a core when river went away, a flaky quit check, two monitors that could
show the same workspace — is written up in FINDINGS.org,
each finding beside how it was reproduced and fixed. This is where to look if you
want the evidence rather than the claim.
GPL-3.0-or-later. See LICENSE.
The bundled bitmap font is Terminus, under the SIL Open Font License 1.1 (doc/OFL-TERMINUS.txt).
