Skip to content

Repository files navigation

LatticeWM

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.

doc/echo-area.png

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.

The one idea to read first

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) and clear (empty it, leave it standing) are two different verbs rather than one verb with a mode;
  • and standing in an empty pane, typing t opens a terminal, e an 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.

What is new here, and what is not

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.

Install

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 first

Once you like it:

./install.sh       # binary, launcher, session entry, man pages, sources, licences

install.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.

The first thing to press

=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.lisp

Keys

Every 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.

Panes

Super+Returna terminal
Super+dsplit horizontally
Super+ssplit vertically
Super+qclose the window
Super+h j k l / arrowsmove focus left, down, up, right
Super+Tabfocus the next pane (Shift for previous)
Shift+Super+h j k lmove the window
Ctrl+Super+h j k lresize
Alt+Super+h j k lswap with the neighbour
Super+eequalize this container (Shift for all)
Shift+Super+qclear the pane

Windows

Super+Spacefloat this window / put it back
Super+`focus the floating layer
Super+ffullscreen
Super+mminimize (Shift+Super+m restores)
Super+wtab this pane (Shift+Super+w untabs)
Super+[ / Super+]previous / next tab
Super+zundo the last layout change (Shift+Super+z redoes)
Super+-show/hide a named scratchpad (Shift+Super+- puts one away)
Super+ttag 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.

The pointer

=Super=+dragmove a window (a tiled one floats first)
=Super=+right-dragresize it, from the corner you grabbed
clickfocus, and raise if it is floating
a titlebar draghonoured, 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.

Workspaces

Super+1Super+0go to workspace 1–10
Shift+Super+1Shift+Super+0send this window there
Super+na new workspace
Super+PageUp / PageDownprevious / next workspace

Workspaces are one-based, so the key and the number on it agree.

More than one monitor

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 / arrowssend 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.

When a window opens

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)))   ; dialogs

Match 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.

Launching

Super+Return*terminal* (default foot)
Super+b*browser* (default firefox)
Super+o*editor* (default emacs)

Asking it things

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+xrun 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+? kwhat does this key do?
Super+? ais there a command for …? — searches every docstring
Super+? cshow a command’s documentation and the keys that run it
Super+? owhat is this setting, what did it ship as, and why
Super+? schange a setting, now, without a file or a restart
Super+? uwhat would undo walk back through?
Super+? twhich windows are tagged what?
Super+? pwhat 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 keymapM-x, with the completion it ships
A chord says what it offersSearch every command by what it does

The lattice

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 coordinatesThe 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. :inheritdirectly 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.

:fitcells stretch to tile the screen exactly. Panning a non-uniform plane resizes windows
:fixeda 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.

Screen capture

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”.

Configuring it

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 command

Anything you can set in the file you can also set live with Super+? s, and Shift+Super+c reloads the file without restarting.

Your keyboard, mouse and touchpad

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 fastest

A 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.

Shift maps, which are a different thing with a similar name

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.

Panels, bars and lockers

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.

Fonts

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.

Extending it

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 when

There 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.

Scripting it

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.

A REPL inside your window manager, one form away

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, exit

Run that before logging out. A configuration error otherwise manifests as a failed session, which is the worst possible moment to find one.

Development

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

The one check that is different in kind

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.

Documentation

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 date
  • Super+/ — 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.

Status

Working, and used on bare metal.

What the build stands on

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.

How far each thing is actually checked

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.

areahow far
keys, minibuffer, spawn, close, splits, floating, empty-pane spawn, persistence, the latticereal hardware, by hand
the protocol: connection, version negotiation, manage and render sequences, the shm overlay path, buffer release, frame callbacksa real headless river, every make check
every keymap key becoming a binding object, and one bound at runtime becoming one morea real river
a client window announced, placed, sized, focused; two windows in disjoint rectanglesa real river
the verbs, driven from outside a protocol sequence the way a script drives thema real river
floating, drag-move, drag-resize, fullscreen, minimize; workspaces, tags, scratchpads, undo, redoa real river
the saved layout, written and read back with a real window’s tags in ita real river
the control socket, one line per forma real river
overlays dismissing on a key bound to nothinga real river — the arming is the half no unit test can see
input configuration: device enumeration, key repeat, compiling and installing a keyboard layouta real river, running nested, which is where devices exist
multi-monitor: one workspace per output, no two the same, surviving repeated workspace switchestwo 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-outputtwo 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 fillsa real wl_shm
the layer-shell exclusive zonea real waybar: usable area went 0,0 1280x6960,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 locka real swaylock over a nested river: the lock is reported, the window manager stays alive, the tree keeps its windows, and commands still run

What is not checked, said plainly

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: wlrctl offers click and 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.

License

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).

About

An infinite-grid window manager for river.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages