Running wl-pick from certain terminals failed outright:
wl-pick: cannot reach sway (No such file or directory (os error 2))
swayipc takes the path from I3SOCK or SWAYSOCK and falls back to asking sway
directly only when neither is set. A variable that is set but stale is used
as it stands, and fails -- which is what happens to every shell descended
from a process that outlived the sway that started it. One long-running
daemon in the ancestry is enough, and nothing about the failure points at
the environment.
The running compositor is the one we want in any case, so look for its
socket in XDG_RUNTIME_DIR when the environment's path does not connect.
Sockets are named sway-ipc.<uid>.<pid>.sock, so the pid says which are worth
trying, checked against /proc for a process that really is a sway, since pids
are reused. Several live compositors is a real situation -- a nested sway --
so that asks for SWAYSOCK rather than guessing.
The environment still wins when it points at something that exists. Its
lookup is no longer delegated to swayipc at all, because that spawns
`sway --get-socketpath` when the variables are unset and lets the child
print "sway socket not detected." over anything we would rather say.
Colours, border and thumbnail size come from ~/.config/wl-pick/config
now, since ten more flags would have made a keybinding unreadable — the
split the rofi setup this replaces already used: look in a file,
behaviour on the command line. Flat `key = value` lines, so no TOML
dependency for something with nothing to nest, and a flag still beats the
file.
Sizes take sway's units. `600px` is absolute; `70ppt` is a percentage of
the display the grid appears on, resolved on every run rather than baked
in, so one config suits monitors of different sizes. That needed the
overlay to know which display it is on, so it now asks sway for the
focused one and maps there explicitly, at that display's scale, instead
of letting the compositor choose and taking the largest scale in use —
which was wrong on any mixed-DPI setup.
tile-width and tile-height are maxima. Given only a width, the height
follows the display's aspect: a 16:9 cell, inherited from a rofi theme
written for a landscape screen, wasted about half of every cell on a
portrait monitor. And if the grid would outgrow the display, tiles now
shrink together, keeping their shape, so thirty windows produce small
tiles rather than a surface larger than the screen. The surface is capped
at the display as a backstop, because on a small screen the padding and
label rows can exceed it no matter how small the tiles get.
Two bugs the tests caught while writing this:
- Stripping comments at the first '#' ate colour values, so
`selection = #d79921 # note` parsed as empty. A comment is now a '#'
followed by whitespace or end of line; a colour is '#' then a hex
digit, so the two cannot collide.
- Twelve tiles at the old fixed size fit a 1280x800 screen, so the first
version of the shrink test proved nothing. It now uses numbers that
genuinely overflow.
Also: failing to reach sway said only "No such file or directory", which
tells a first-time user nothing; it now names sway and what it wanted.
main.rs had grown to 1290 lines holding everything: the CLI, the client
state, the capture engine, the drawing, the input handling and twelve
Dispatch impls. It is now orchestration only, and the concerns live where
you would look for them — cli, app, capture, overlay — with the module
docs saying what each owns. No behaviour changed; the code moved.
Iterating in response to review left residue, now gone:
- Tile::handle was only ever written. Dropping a wayland-rs proxy does
not destroy the object, so nothing needed it held.
- Tile::failed likewise: an earlier captures_settled() read it, and
`settled` is what everything waits on now.
- A blanket #[allow(dead_code)] on Tile hid both of those. Fields are
pub(crate) rather than pub so the lint keeps working.
- Tile's doc comment had drifted onto the Format enum during a patch.
- Three consecutive `if args.verbose` blocks became describe() and
report(), and the loose ticks/releases/starved/pool_bytes counters
became one Stats. `releases` went: frames already imply it.
- quit + quit_why + activate became one Ending enum and picked, so
"closed by the compositor" is a state rather than a string.
- App::new took seven positional arguments, two of them bare integers in
a row; the four that always travel together are now Settings.
- sway::scale and the display listing each called get_outputs; one call
does both.
- scaled(rect, n) became Rect::scaled(n), and Format lives with Target
where render() dispatches on it.
README had drifted too: the usage line still advertised --print, which
no longer exists, and omitted --format and --no-outputs. Its flag list is
now checked against --help, the memory figures are re-measured, and there
is a source layout for anyone arriving cold.
wlgrid described the shape of the thing rather than what it is for, and
the shape is the least interesting part now that it doesn't focus
anything: it shows you what is running and reports which one you pointed
at. wl-pick says that.
The wl- prefix rather than wlr- is deliberate. The capture path is
entirely upstream ext- protocols (ext-image-copy-capture,
ext-image-capture-source, ext-foreign-toplevel-list); the only
wlroots-flavoured piece is layer-shell, which KDE, niri and labwc
implement too. What actually keeps this from running on other
compositors is the sway IPC dependency, not wlroots, so wlr- would
promise a portability that isn't there. Hyphenated because the ecosystem
hyphenates when the suffix is a real word — wl-clipboard, wl-mirror,
wl-screenrec — and reserves the smushed form for coinages like wlsunset.
Also renames the layer-shell namespace and the memfd labels, which show
up in compositor debugging.
--focus and sway::focus are gone. Showing the grid and reporting the
choice is the whole job; deciding what the choice means belongs to
whoever called it, and keeping that decision here only invited more of
it (focus which way? move? swap? scratchpad?). The sway IPC connection
is now scoped to building the list and closed before the overlay maps.
--help no longer just names the three formats, it shows them. Each gets
a real sample line, what every column means, and where the format is
meant to be used: tsv for `IFS=$'\t' read` or cut, json for jq, portal
for xdg-desktop-portal-wlr's simple chooser, with the config stanza to
paste. Plus worked examples of focusing a window, handling either kind
of pick, and screenshotting one with grim -T.
Displays are capture sources too — ext-image-capture-source-v1 makes one
from a wl_output just as it does from a toplevel handle — so they are now
tiles as well, appended after the windows and labelled "NAME · display".
They are snapshot-only: a live display tile contains this overlay, which
contains the display tile, and refreshing that never settles while
costing a whole screen per frame. They also only get one buffer for the
same reason, which is worth ~29MB here.
The bigger change is what wlgrid reports. It was focusing the pick itself
and printing only under --print, which suits a keybinding and nothing
else. Now it is a chooser: it always reports the pick, never acts unless
asked (--focus), exits 1 when cancelled, and can say it three ways.
--format portal emits what xdg-desktop-portal-wlr's simple chooser reads
("Monitor: NAME" / "Window: <foreign-toplevel identifier>"), so wlgrid
can be the picker behind getDisplayMedia, with live previews of windows
and displays. That contract is also why tsv carries both identifiers: the
portal and grim -T want the toplevel identifier, sway scripting wants the
con_id. --format json gives the whole record for jq.
Navigation also takes hjkl, and --help now describes every flag with its
default plus the output formats.
A switcher to replace a wlthumbs + rofi pipeline, with the same look
(gruvbox, ceil(sqrt(n)) columns capped at 4, 16:9 tiles, a yellow
selection filling the element padding) but no thumbnails anywhere.
Each window is captured straight into a wl_shm buffer that is handed to
its own wl_subsurface, with wp_viewporter giving the compositor the
rectangle to scale it into. So there is no PNG encode, no scaler, no
full-resolution bitmap in this process, and the capture buffers are never
even mapped here — the compositor writes those pages and samples them
again for display. Opens in ~65ms for 8 windows (55ms of which is the
compositor reading pixels back out of the GPU) and holds ~9MB of RSS.
All capture sessions are opened before a single roundtrip and every frame
goes in flight together, the same batching wlthumbs uses, because the
readback is bandwidth-bound rather than latency-bound.
sway remains the source of truth: the window list, the con_ids and the
focusing all come from its IPC socket, joined to the Wayland side by
foreign_toplevel_identifier. Navigation reads raw evdev keycodes so it is
layout-independent, which does mean virtual-keyboard clients that invent
their own keymap can't drive it; that resolves when filtering brings xkb.
Labels, type-to-filter and live previews are next.