41 Commits
Author SHA1 Message Date
N0VA 0ce2854d72 Port fixes from wl-pick 2026-09-10 09:36:55 +02:00
N0VA e1d8760a43 Update README.adoc 2026-09-10 09:12:31 +02:00
N0VA f78fd7ee41 Update Cargo.toml 2026-09-10 08:55:25 +02:00
N0VA 6559956c3c Update cli.rs 2026-09-10 08:55:06 +02:00
N0VA 77efdf03e9 rename 2026-09-10 08:53:44 +02:00
N0VA 314e474341 colours 2026-09-10 08:50:07 +02:00
N0VA 0afdcf4c83 Update cli.rs 2026-09-10 07:37:21 +02:00
N0VA 21cbb5450e help text 2026-09-10 07:33:57 +02:00
N0VA 3abf610fbf metadata 2026-09-10 07:29:39 +02:00
N0VA 66c2d49ffb cleanup 2026-09-09 23:40:21 +02:00
N0VA 3ad0b59d8c Update Cargo.lock 2026-09-09 21:55:36 +02:00
N0VA 782a21f864 Update Cargo.toml 2026-09-09 19:16:07 +02:00
N0VA 01c1b31636 horizontal 2026-09-09 15:23:53 +02:00
N0VA 53cebf14f3 fix: alt-tab focus history MRU ordering, repeat handling, and quick release 2026-09-09 12:39:10 +02:00
N0VA 50caed0110 modkey 2026-09-09 11:44:23 +02:00
N0VA 1367b03b97 alt-tab 2026-09-09 11:30:58 +02:00
Milad Alizadeh 78631065e2 Release 0.4.0
--outputs and --labels turn those settings back on from the command line.
Only the negatives existed before, so a config file that switched either off
could not be overridden for a single run. --no-labels is the matching
negative; --hide-labels still works.
2026-09-07 18:40:23 +01:00
Milad Alizadeh 6bd4a3ee76 Let a flag turn a boolean setting back on
Both booleans could only be switched one way from the command line. There
was a --no-outputs but no --outputs in the help, and no --labels at all, so
a config file saying `outputs = no` could not be overridden for a single
run: the only way back was to edit the file. "A flag always beats the file"
was true of everything that takes a value and half true of the rest.

So --outputs and --labels are the counterparts, --no-labels is the negative
that matches them, and --hide-labels stays accepted for whatever it is
already wired into. --outputs turned out to be parsed already and merely
undocumented, which is its own kind of missing.

Argument parsing moves behind parse(), taking the arguments as an iterator
rather than reading the environment, so precedence is testable. The tests
pin both directions and the fall-through to file then default, which is the
part that quietly went wrong.
2026-09-07 18:25:47 +01:00
Milad Alizadeh a99de502e2 Release 0.3.1
Two ways wl-pick could fail to appear at all, both fixed.

A second instance hung with no window and had to be killed: sway answers a
capture request for a toplevel another client is already capturing with
silence, and the wait for it was unbounded. Every wait before the overlay is
interactive now has a deadline, and a tile that never arrives is drawn as a
bare label. Losing the keyboard grab for good also ends a run, so a second
wl-pick replaces the first rather than stranding it on screen.

Starting wl-pick from a shell with a stale SWAYSOCK failed outright. The
running compositor's socket is now found in XDG_RUNTIME_DIR when the
environment's path does not connect.
2026-09-07 17:41:37 +01:00
Milad Alizadeh 8e317682a1 Find sway's socket when the environment is stale
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.
2026-09-07 17:37:48 +01:00
Milad Alizadeh c607fc9a1c Bound every wait before the overlay is interactive
Running wl-pick twice left the second instance hung with no window at all
and no way to end it but kill.

The hang was in the capture phase, not the overlay. sway answers a capture
request for a toplevel that another client is already capturing with
silence: no frame, no failed, no stopped. captures_settled() waits for every
tile to reach one of those three, so it waited forever, holding its buffers,
having never created a layer surface. Any concurrent capture client will do
this, not just a second wl-pick.

So the phases before the overlay is interactive now have a deadline. pump_for
polls the connection with one, and a tile that never arrives is drawn as a
bare label, exactly as an outright capture failure already was.
report_unsettled names those tiles on stderr, which is the diagnostic whose
absence made this hard to find.

The first instance had a second problem: sway hands the keyboard to the new
overlay and sends the old one wl_keyboard.leave, but the dispatcher only
handled Key, so the loser sat on screen holding a grab it no longer had,
deaf to every key. That is the process that stays around. Losing the grab
for good now ends the run, so a second wl-pick started from the same
keybinding replaces the first rather than stranding it.

For good, because sway also sends leave followed immediately by enter on the
same surface -- microseconds apart -- as a focus refresh when the pointer
crosses the overlay. Treating a bare leave as terminal made a lone instance
quit itself after less than a second. Focus is tracked in the dispatcher and
the main loop only gives up once it has failed to come back.

pump had no callers left after that: every wait is now either budgeted or
focus-aware.
2026-09-07 11:47:41 +01:00
Milad Alizadeh 34548fe4f6 Release 0.3.0
Sizing is now specified as caps rather than as a thumbnail size: max-width
and max-height bound the overlay, max-columns and max-rows bound the grid
inside it, and a thumbnail is simply the one divided by the other. A
thumbnail is therefore the same size whether one window is open or thirty.

Rows past max-rows scroll, with a scrollbar, PgUp/PgDn, and the selection
kept in view. Tiles scrolled out of sight are unmapped, so live capture
skips them.

timeout = 0 now means no timeout instead of an immediate deadline.
2026-09-06 11:13:09 +01:00
Milad Alizadeh 1f5b735541 Tidy up after the sizing rework
The caps model landed in pieces, and the pieces left seams. This joins them
up, and fixes two things the reread turned up.

Deferred subsurface syncing is gone. `needs_tiles` existed because `select`
had no queue handle to sync with, so main.rs grew a bespoke event loop to
notice the flag afterwards. The dispatch handlers are handed a handle
already: pass it down and let `select` do the work itself. main.rs is back
to one uniform `pump`.

The layout is built once, in run(), and passed to App::new, rather than
built there and again inside it. `display` moves from Settings, which is
what App needs, to Options, which is what the caller needs it for.

`timeout = 0` meant an immediate deadline, so uncommenting the line in the
shipped config would have made wl-pick exit before you saw it. Zero now
means no timeout, which is what the comment beside it always claimed.

`timeout` was also settable but documented nowhere -- not in --help, not in
the README. Both now list every key config.rs accepts.

Theme::default's max-width/max-height were placeholder pixel counts that
happened to match one monitor. They are i32::MAX now: no cap of their own,
with Layout clamping to the display.
2026-09-06 11:01:11 +01:00
Milad Alizadeh 86319aa309 Size the grid by caps, not by tile size
tile-width and tile-height are gone. In their place the config states a
box and a grid, all four settings caps of the same kind:

    max-width   = 90ppt      # the box the grid may fill
    max-height  = 90ppt
    max-columns = 4          # the grid inside it
    max-rows    = 4

A thumbnail is that box divided by those caps. The property that buys is
that a thumbnail's size no longer depends on how many windows are open:
one window gets the same thumbnail as thirty, in a smaller overlay,
because the overlay hugs whatever is actually there. Rows past max-rows
still scroll.

It also removes two things that were hard to explain. tile-height used to
default to the display's aspect through a rule you could only learn from
the documentation — the shape now falls out of the box and the grid.
And ppt meant "of the display width" on one key and "of the display
height" on another; the two remaining lengths take the axis their name
implies.

The 90% fill constant went with them: it was an invisible cap doing the
job max-width now does out loud, and its old value is the default.

Turning labels off now gives that row to the thumbnails rather than
shrinking the window, which follows from the box being what you asked
for. Verified on a 1280x1440 display: 90ppt gives a 1149x1293 overlay,
60ppt gives 765x861, and caps of 2x2 in the same box give larger
thumbnails that scroll after two rows.
2026-09-06 10:46:15 +01:00
Milad Alizadeh 13c0252cef Add max-rows
Scrolling left the overlay's height entirely derived — as many rows as fit
in 90% of the display — so with enough windows it is always nearly
full-height, and there was no way to ask for a compact strip instead.
max-rows caps the viewport and scrolls the rest, which makes it the
symmetric partner to max-columns; rows are also the natural unit when
tiles are a fixed size, where a height in ppt would flip the row count
about as the tile size changes.

Measured on a 1280x1440 display with 14 tiles at 18ppt: uncapped gives a
981px surface showing 3 of 4 rows, max-rows = 2 gives 657px, and 1 gives
333px — each a row's pitch apart.

The test I wrote for it was wrong before the code was: four tiles make a
2x2 grid under the ceil(sqrt(n)) rule, not the 4x1 I had assumed.
2026-08-31 18:27:11 +01:00
Milad Alizadeh 94d8145079 Release 0.2.0
Minor, not patch: colours and sizing move into a config file and the grid
scrolls, so behaviour people could depend on has changed shape.

- ~/.config/wl-pick/config sets colours, border, thumbnail size and the
  flags, as flat `key = value` lines; a flag still beats the file.
- Sizes take sway's units. 70ppt is a percentage of the display the grid
  appears on, resolved per run, so one config suits monitors of different
  sizes. The overlay maps on the focused display explicitly, at that
  display's scale, rather than taking the largest scale in use.
- Give only tile-width and the height follows the display's aspect, which
  is roughly the shape of the windows on it.
- More rows than fit now scroll, with a scrollbar, PgUp/PgDn, and the
  selection always revealed. Tiles out of view are unmapped and skipped
  by live capture.

Also corrects rust-version, which said 1.88 while cosmic-text has needed
1.89 all along: cargo was resolving 0.1.1 against a floor its own
dependency could not meet, and said so if you looked. It now locks
cleanly.
2026-08-31 18:22:57 +01:00
Milad Alizadeh 30c478872a Scroll when there are more tiles than fit
Shrinking the grid to fit meant a configured tile-width was quietly
ignored the moment enough windows were open — a setting that silently
does not apply is a bug, not a policy. Tiles are now the size the config
asks for and the extra rows scroll.

The layout became a viewport: it reports how many rows fit, elem/tile/
label take a scroll offset and return nothing for tiles above or below
the fold, and hit-testing follows the offset so a click lands on what is
under the cursor rather than what used to be there. Any keyboard move
goes through select(), which scrolls the least that keeps the selection
visible; PgUp/PgDn jump a screen. A scrollbar appears in the right margin
only when there is something to scroll, so it reads as a hint rather than
furniture.

Tiles scrolled out of sight get a null buffer, which unmaps their
subsurface, and the live clock skips them — a long list no longer spends
readback bandwidth on pixels nobody can see. Re-placing the subsurfaces
happens once per dispatch in the event loop rather than inside the key
handler, so holding an arrow key coalesces.

Shrinking survives for one case only: a tile too large for even a single
row or column, where otherwise nothing could be drawn. The old
shrink-to-fit test was rewritten around that, since its premise (twenty
tiles at full size cannot fit) is now answered by scrolling instead.

Verified against the session: 16 tiles at tile-width = 40ppt gives a 2x8
grid with 2 rows visible; three Downs scroll to row 2 and Enter returns
index 6, matching its own list; 82% of the viewport changes across the
scroll; and the scrollbar thumb measures 634px tall at y=24 unscrolled
and y=658 at scroll 2, both what the geometry predicts.
2026-08-31 18:20:03 +01:00
Milad Alizadeh 487acb8b6a Add a config file, and size the grid to the display
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.
2026-08-31 17:29:06 +01:00
Milad Alizadeh c22e3cdd31 Release 0.1.1
The published 0.1.0 defaults the label font to Berkeley Mono, which falls
back to a non-monospaced face on any machine that does not have it. That
is a defect for every user but one, so it is a patch release rather than a
new feature: no flag was added, removed or renamed.

Also silences a lint that arrived with Rust 1.98. Clippy suggests
`as_chunks` in place of `chunks_exact`, but that API is newer than the
1.88 this crate declares as its minimum, so taking the suggestion would
break the builds the manifest promises to support.
2026-08-28 10:49:14 +01:00
Milad Alizadeh 11dc30ddc1 Default to the system monospace font
The default was Berkeley Mono, which is one machine's licensed font and
reads as configuration rather than a default. It is now the system's
monospace font, so a fresh install looks right anywhere and --font is
there for anyone who wants their own.

Getting "the system monospace font" is less direct than it sounds.
Family::Name("monospace") resolves to Noto Sans — not monospaced at all.
cosmic-text's generic Family::Monospace goes through fontdb's built-in
preference, "FreeMono", which is usually absent, and then lands on an
arbitrary face (Adwaita Mono here). Enabling cosmic-text's fontconfig
feature changes nothing, because fontdb's config parser does not pick up
the alias files a distribution actually ships.

So fontconfig is asked directly: `fc-match -f %{family} monospace`, which
is the same answer every other application on the system gets — Noto Sans
Mono here. It costs 10ms on the worker thread that is already waiting for
the compositor, so nothing in wall clock. Without the fontconfig tools a
short list of common distribution defaults is tried instead.

Weight and stretch overrides went too: they were tuned for Berkeley Mono
Medium SemiCondensed, and a family name carries that anyway — the full
name resolves to exactly that face, verified by rendering both.

--verbose now reports the family the labels were shaped with, since the
default legitimately differs from machine to machine.
2026-08-24 12:37:55 +01:00
Milad Alizadeh 15b74d311d Add the metadata crates.io needs
repository, readme, keywords and categories, so the crate page says what
this is and is findable; the MIT text the license field was already
claiming; and rust-version, because let-chains mean 1.88 and an older
toolchain should say so rather than fail to parse.

`cargo package` builds the packaged tarball cleanly: 17 files, nothing
stray, and the release binary links nothing beyond libc, so installing it
needs no system development packages.
2026-08-24 12:03:40 +01:00
Milad Alizadeh 25d90bdcec Pick with the mouse
Click a tile to pick it, scroll to move the selection. Hovering does not
move the selection: the keyboard keeps that, and a click acts on whatever
is under the cursor instead. A click on the margin, on a gap, or on an
empty cell of a ragged last row does nothing.

Tiles are already subsurfaces, so a click on a thumbnail identifies its
tile by which surface the event arrived on — no hit-testing needed. Only
the chrome around them (padding, labels, gaps) needs Layout::hit, which
inverts the same maths elem() lays out with, and is tested against it.

Picking waits for press and release on the same tile, so sliding off a
tile before letting go is not a pick.

wp_cursor_shape_v1 sets the pointer shape, which spares us a cursor theme
and libwayland-cursor; without it the cursor keeps whatever shape the
window underneath gave it. It is optional — a compositor without it just
gets whatever shape was already there.
2026-08-23 20:25:57 +01:00
Milad Alizadeh 8d8a27c4c1 Split the client into modules, and clear out what iteration left behind
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.
2026-08-23 19:17:04 +01:00
Milad Alizadeh 5b98615c41 Rename to wl-pick
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.
2026-08-23 18:35:27 +01:00
Milad Alizadeh 71ba77c989 Tighten --help
75 lines to 51. The format section had grown into prose that explained
the same thing twice, and two of the three examples were variations on
one idea, so one worked example survives.

The samples were also taken verbatim from the machine this was developed
on — a real toplevel identifier, a real output name, real app ids — which
reads like configuration rather than illustration. They are generic now.
2026-08-23 16:29:50 +01:00
Milad Alizadeh 3d54b368d2 Make wlgrid a chooser only, and say what each format is
--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.
2026-08-23 14:07:11 +01:00
Milad Alizadeh 6d8ae655b9 Say why the overlay closed, and document focusing both kinds of tile
--verbose now reports whether the overlay was picked from, cancelled, or
closed by the compositor. Chasing a "keys do nothing" symptom that turned
out to be a locked session, that distinction was the piece of information
I kept lacking: an exit code of 1 cannot tell a cancel from a surface the
compositor took away.

README now shows the two commands a caller needs — [con_id=N] focus for a
window, focus output NAME for a display — and that --focus runs them.
2026-08-23 13:26:54 +01:00
Milad Alizadeh 5b1f0f74e8 Add display tiles, three output formats, and hjkl
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.
2026-08-23 12:33:25 +01:00
Milad Alizadeh 33d75222cf Make the previews live
Capture sessions stay open and tiles refresh, so the grid shows what the
windows are actually doing rather than a snapshot from when it opened.
--live all|current|none picks how much of that happens, --fps caps it.

Three things keep it cheap:

The protocol is damage-driven. After a session's first frame the
compositor only produces another once the content changes, so a request
left outstanding on an idle window costs nothing. Measured with one
animating window out of ten: 52,52,1,1,1,1,1,1,13,1 frames over 4s — the
static windows delivered their first frame and then nothing.

The overlay's own wl_surface.frame callbacks are the clock, so refreshes
stop when it isn't being presented and no timer or poll loop is needed.
Per-tile throttling on top of that measured 12.1/s at --fps 12.

Each window gets two buffers, since a capture must not write into one the
compositor is reading, and wl_buffer.release says which is free. That
release is the whole contract: with wl_shm the compositor copies the
pixels at commit and hands the buffer straight back, so the slot on
screen is usually free too. Waiting for it to stop being the displayed
slot instead — which is what this first did — deadlocks after two frames,
with both slots stuck busy (687 blocked attempts, 2 frames per tile).

Live mode doubles the shm handed to the compositor (110MB for ten windows
here, against 55MB with --live none); our own RSS is unaffected because
those pages are still never mapped. --verbose now reports frames, ticks,
releases, blocked attempts and pool size, which is what localised the
release bug.
2026-08-23 10:54:06 +01:00
Milad Alizadeh 2cd076699d Label the tiles
"title · app" centred under each thumbnail, in the same font the rofi
theme used (Berkeley Mono at pango's "small"), with the selected tile's
label inverted onto the yellow the way rofi's element background did.
Long titles are ellipsised to the cell width.

Text comes from cosmic-text, which brings real shaping. The catch is
cost: building a font system and rasterising the first glyphs takes
~55ms, nearly doubling a 65ms startup. But the capture phase is ~55ms of
sitting blocked while the compositor copies pixels, so labels are shaped
on a worker thread started before the captures and joined after them.
The measured labels phase is now 0.0ms — it costs nothing in wall clock.

Two thirds of that font cost was FontSystem::new() scanning all 1793
system faces, which even new_with_fonts() does. So the database is built
by hand: the user's own font directories first, since they are small,
and the full system scan only when the family isn't found there. An
unknown family still resolves, because that fallback is exactly the
system scan (verified: --font "No Such Font" renders in Noto).

--hide-labels restores the icon-only grid, and --font/--font-size make
the family and size settable. Adds a test that a label actually puts
pixels on the surface, one for ellipsising, and one pinning the label row
into the element geometry.
2026-08-23 10:39:37 +01:00
Milad Alizadeh dbfc06519a Add wlgrid: a window-thumbnail grid overlay for wlroots
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.
2026-08-23 10:07:09 +01:00