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.
This commit is contained in:
Milad Alizadeh
2026-08-23 18:35:27 +01:00
parent 71ba77c989
commit 5b98615c41
5 changed files with 27 additions and 27 deletions
Generated
+1 -1
View File
@@ -605,7 +605,7 @@ dependencies = [
] ]
[[package]] [[package]]
name = "wlgrid" name = "wl-pick"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"cosmic-text", "cosmic-text",
+1 -1
View File
@@ -1,5 +1,5 @@
[package] [package]
name = "wlgrid" name = "wl-pick"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
description = "Live window switcher for wlroots compositors: a thumbnail grid with the look of a rofi theme" description = "Live window switcher for wlroots compositors: a thumbnail grid with the look of a rofi theme"
+8 -8
View File
@@ -1,4 +1,4 @@
# wlgrid # wl-pick
A window switcher for wlroots compositors: a grid overlay of **live** window A window switcher for wlroots compositors: a grid overlay of **live** window
previews that looks like a rofi theme, and tells you which one you picked. previews that looks like a rofi theme, and tells you which one you picked.
@@ -35,7 +35,7 @@ is the one thing the rofi version had that this doesn't — see the roadmap.
## Usage ## Usage
``` ```
wlgrid [--print] [--verbose] [--hide-labels] [--font FAMILY] [--font-size PX] wl-pick [--print] [--verbose] [--hide-labels] [--font FAMILY] [--font-size PX]
[--live all|current|none] [--fps N] [--timeout SECS] [--live all|current|none] [--fps N] [--timeout SECS]
``` ```
@@ -48,17 +48,17 @@ wlgrid [--print] [--verbose] [--hide-labels] [--font FAMILY] [--font-size PX]
- `--timeout SECS` exits after a deadline (an escape hatch: the overlay takes an - `--timeout SECS` exits after a deadline (an escape hatch: the overlay takes an
exclusive keyboard grab) exclusive keyboard grab)
wlgrid is a chooser: it reports what you picked and leaves acting on it to the wl-pick is a chooser: it reports what you picked and leaves acting on it to the
caller. The pick goes to stdout, nothing does if you cancel, and the exit status caller. The pick goes to stdout, nothing does if you cancel, and the exit status
is 0 for a pick and 1 for a cancel. is 0 for a pick and 1 for a cancel.
wlgrid never acts on the choice — it has no idea what you want to do with it. wl-pick never acts on the choice — it has no idea what you want to do with it.
Focusing on sway looks like this: Focusing on sway looks like this:
```sh ```sh
#!/usr/bin/env bash #!/usr/bin/env bash
# ~/.local/bin/winmenu, bound to $mod+Tab # ~/.local/bin/winmenu, bound to $mod+Tab
IFS=$'\t' read -r type id toplevel app title < <(wlgrid) || exit 0 IFS=$'\t' read -r type id toplevel app title < <(wl-pick) || exit 0
case $type in case $type in
window) swaymsg "[con_id=$id] focus" ;; window) swaymsg "[con_id=$id] focus" ;;
output) swaymsg "focus output $id" ;; output) swaymsg "focus output $id" ;;
@@ -68,7 +68,7 @@ esac
Windows only, as a one-liner: Windows only, as a one-liner:
```sh ```sh
swaymsg "[con_id=$(wlgrid --no-outputs | cut -f2)] focus" swaymsg "[con_id=$(wl-pick --no-outputs | cut -f2)] focus"
``` ```
Three formats, because the identifiers different consumers need differ: Three formats, because the identifiers different consumers need differ:
@@ -80,13 +80,13 @@ Three formats, because the identifiers different consumers need differ:
| `portal` | `Monitor: NAME` or `Window: TOPLEVEL_ID` | | `portal` | `Monitor: NAME` or `Window: TOPLEVEL_ID` |
`portal` is exactly what xdg-desktop-portal-wlr's `simple` chooser reads, so `portal` is exactly what xdg-desktop-portal-wlr's `simple` chooser reads, so
wlgrid can be the picker for `getDisplayMedia` and friends — with live previews wl-pick can be the picker for `getDisplayMedia` and friends — with live previews
of both windows and displays: of both windows and displays:
```ini ```ini
[screencast] [screencast]
chooser_type=simple chooser_type=simple
chooser_cmd=wlgrid --format portal chooser_cmd=wl-pick --format portal
``` ```
| key | | | key | |
+16 -16
View File
@@ -1,4 +1,4 @@
//! wlgrid shows a live grid of every window and display as a layer-shell overlay //! wl-pick shows a live grid of every window and display as a layer-shell overlay
//! and reports which one you picked. That is all it does: acting on the choice //! and reports which one you picked. That is all it does: acting on the choice
//! belongs to whatever called it. It replaces a wlthumbs + rofi pipeline, so the //! belongs to whatever called it. It replaces a wlthumbs + rofi pipeline, so the
//! look comes straight from that rofi theme (see theme.rs). //! look comes straight from that rofi theme (see theme.rs).
@@ -385,7 +385,7 @@ impl App {
self.pool_bytes = total; self.pool_bytes = total;
// Note: no mmap. The compositor writes these pages and samples them // Note: no mmap. The compositor writes these pages and samples them
// again for display; mapping them here would only cost us the faults. // again for display; mapping them here would only cost us the faults.
let file = shm::memfd("wlgrid-capture", total)?; let file = shm::memfd("wl-pick-capture", total)?;
let pool = self.shm.create_pool(file.as_fd(), total as i32, qh, ()); let pool = self.shm.create_pool(file.as_fd(), total as i32, qh, ());
for (i, slot_offsets) in offsets.iter().enumerate() { for (i, slot_offsets) in offsets.iter().enumerate() {
let (w, h, format) = { let (w, h, format) = {
@@ -515,7 +515,7 @@ impl App {
&surface, &surface,
None, // let the compositor place it on the active output None, // let the compositor place it on the active output
Layer::Overlay, Layer::Overlay,
"wlgrid".to_string(), "wl-pick".to_string(),
qh, qh,
(), (),
); );
@@ -526,7 +526,7 @@ impl App {
let (pw, ph) = (lw * self.scale, lh * self.scale); let (pw, ph) = (lw * self.scale, lh * self.scale);
let len = shm::Chrome::slot_len(pw, ph) * shm::Chrome::SLOTS; let len = shm::Chrome::slot_len(pw, ph) * shm::Chrome::SLOTS;
let file = shm::memfd("wlgrid-chrome", len)?; let file = shm::memfd("wl-pick-chrome", len)?;
let pool = self.shm.create_pool(file.as_fd(), len as i32, qh, ()); let pool = self.shm.create_pool(file.as_fd(), len as i32, qh, ());
for slot in 0..shm::Chrome::SLOTS { for slot in 0..shm::Chrome::SLOTS {
self.chrome_buffers.push(pool.create_buffer( self.chrome_buffers.push(pool.create_buffer(
@@ -724,9 +724,9 @@ fn pump(
} }
const HELP: &str = "\ const HELP: &str = "\
wlgrid — a live grid of window and display previews, for picking one wl-pick — a live grid of window and display previews, for picking one
usage: wlgrid [options] usage: wl-pick [options]
--format tsv|json|portal how to report the pick [tsv] --format tsv|json|portal how to report the pick [tsv]
--live all|current|none which tiles keep updating live [all] --live all|current|none which tiles keep updating live [all]
@@ -766,11 +766,11 @@ formats:
[screencast] [screencast]
chooser_type=simple chooser_type=simple
chooser_cmd=wlgrid --format portal chooser_cmd=wl-pick --format portal
focusing on sway: focusing on sway:
IFS=$'\\t' read -r type id toplevel app title < <(wlgrid) && IFS=$'\\t' read -r type id toplevel app title < <(wl-pick) &&
case $type in case $type in
window) swaymsg \"[con_id=$id] focus\" ;; window) swaymsg \"[con_id=$id] focus\" ;;
output) swaymsg \"focus output $id\" ;; output) swaymsg \"focus output $id\" ;;
@@ -852,7 +852,7 @@ fn main() -> ExitCode {
match run() { match run() {
Ok(code) => code, Ok(code) => code,
Err(e) => { Err(e) => {
eprintln!("wlgrid: {e}"); eprintln!("wl-pick: {e}");
ExitCode::FAILURE ExitCode::FAILURE
} }
} }
@@ -865,7 +865,7 @@ fn run() -> Result<ExitCode, Box<dyn Error>> {
if let Some(d) = args.timeout { if let Some(d) = args.timeout {
std::thread::spawn(move || { std::thread::spawn(move || {
std::thread::sleep(d); std::thread::sleep(d);
eprintln!("wlgrid: timeout"); eprintln!("wl-pick: timeout");
std::process::exit(2); std::process::exit(2);
}); });
} }
@@ -955,7 +955,7 @@ fn run() -> Result<ExitCode, Box<dyn Error>> {
); );
} }
eprintln!( eprintln!(
"wlgrid: {} window(s), {matched} matched, {ready} captured; \ "wl-pick: {} window(s), {matched} matched, {ready} captured; \
grid {}x{}, surface {}x{} logical at scale {}, {} MB of capture buffers", grid {}x{}, surface {}x{} logical at scale {}, {} MB of capture buffers",
app.tiles.len(), app.tiles.len(),
app.layout.cols, app.layout.cols,
@@ -980,7 +980,7 @@ fn run() -> Result<ExitCode, Box<dyn Error>> {
let frames: u32 = app.tiles.iter().map(|t| t.frames).sum(); let frames: u32 = app.tiles.iter().map(|t| t.frames).sum();
let live_for = start.elapsed().as_secs_f64(); let live_for = start.elapsed().as_secs_f64();
eprintln!( eprintln!(
"wlgrid: {frames} frame(s) over {live_for:.1}s = {:.1}/s, {} tick(s), \ "wl-pick: {frames} frame(s) over {live_for:.1}s = {:.1}/s, {} tick(s), \
{} release(s), {} blocked; per tile: {}", {} release(s), {} blocked; per tile: {}",
frames as f64 / live_for, frames as f64 / live_for,
app.ticks, app.ticks,
@@ -994,10 +994,10 @@ fn run() -> Result<ExitCode, Box<dyn Error>> {
); );
} }
// wlgrid is a chooser: it reports the pick, and what that means is the // wl-pick is a chooser: it reports the pick, and what that means is the
// caller's business. // caller's business.
if args.verbose { if args.verbose {
eprintln!("wlgrid: {}", app.quit_why); eprintln!("wl-pick: {}", app.quit_why);
} }
let Some(target) = app.activate else { let Some(target) = app.activate else {
return Ok(ExitCode::FAILURE); // cancelled: nothing on stdout return Ok(ExitCode::FAILURE); // cancelled: nothing on stdout
@@ -1010,7 +1010,7 @@ fn run() -> Result<ExitCode, Box<dyn Error>> {
None => { None => {
// The portal can only name a window by its foreign-toplevel // The portal can only name a window by its foreign-toplevel
// identifier, and this one has none; silence means declined. // identifier, and this one has none; silence means declined.
eprintln!("wlgrid: {:?} has no toplevel identifier", target.title); eprintln!("wl-pick: {:?} has no toplevel identifier", target.title);
return Ok(ExitCode::FAILURE); return Ok(ExitCode::FAILURE);
} }
}, },
@@ -1126,7 +1126,7 @@ impl Dispatch<ExtImageCopyCaptureFrameV1, usize> for App {
// frame yet leaves the tile without a thumbnail. // frame yet leaves the tile without a thumbnail.
if tile.frames == 0 { if tile.frames == 0 {
eprintln!( eprintln!(
"wlgrid: capture failed for {:?} ({reason:?})", "wl-pick: capture failed for {:?} ({reason:?})",
tile.target.title tile.target.title
); );
tile.failed = true; tile.failed = true;
+1 -1
View File
@@ -2,7 +2,7 @@
//! Wayland side only supplies pixels. The join between the two is //! Wayland side only supplies pixels. The join between the two is
//! `foreign_toplevel_identifier`, which sway reports per view. //! `foreign_toplevel_identifier`, which sway reports per view.
//! //!
//! Acting on the choice is deliberately not here: wlgrid reports what was picked //! Acting on the choice is deliberately not here: wl-pick reports what was picked
//! and the caller decides what that means. //! and the caller decides what that means.
use swayipc::{Connection, Node, NodeType}; use swayipc::{Connection, Node, NodeType};