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.
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
# wlgrid
|
# wlgrid
|
||||||
|
|
||||||
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 focuses the window you pick.
|
previews that looks like a rofi theme, and tells you which one you picked.
|
||||||
|
|
||||||
It replaces a `wlthumbs | rofi` pipeline, and doubles as a screencast source
|
It replaces a `wlthumbs | rofi` pipeline, and doubles as a screencast source
|
||||||
picker for the desktop portal. The difference is that no thumbnails
|
picker for the desktop portal. The difference is that no thumbnails
|
||||||
@@ -39,7 +39,6 @@ wlgrid [--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]
|
||||||
```
|
```
|
||||||
|
|
||||||
- `--print` writes the selected sway `con_id` to stdout instead of focusing it
|
|
||||||
- `--verbose` prints phase timings and how many windows were captured
|
- `--verbose` prints phase timings and how many windows were captured
|
||||||
- `--hide-labels` draws an icon-only grid
|
- `--hide-labels` draws an icon-only grid
|
||||||
- `--font FAMILY` label font family (default `Berkeley Mono`)
|
- `--font FAMILY` label font family (default `Berkeley Mono`)
|
||||||
@@ -53,14 +52,12 @@ wlgrid 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.
|
||||||
|
Focusing on sway looks like this:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Simplest: let wlgrid do the focusing, for a bare keybinding.
|
#!/usr/bin/env bash
|
||||||
bindsym $mod+Tab exec wlgrid --focus
|
# ~/.local/bin/winmenu, bound to $mod+Tab
|
||||||
|
|
||||||
# Windows only, in a script:
|
|
||||||
swaymsg "[con_id=$(wlgrid --no-outputs | cut -f2)] focus"
|
|
||||||
|
|
||||||
# Both windows and displays: the TYPE column says which command to use.
|
|
||||||
IFS=$'\t' read -r type id toplevel app title < <(wlgrid) || exit 0
|
IFS=$'\t' read -r type id toplevel app title < <(wlgrid) || exit 0
|
||||||
case $type in
|
case $type in
|
||||||
window) swaymsg "[con_id=$id] focus" ;;
|
window) swaymsg "[con_id=$id] focus" ;;
|
||||||
@@ -68,8 +65,11 @@ case $type in
|
|||||||
esac
|
esac
|
||||||
```
|
```
|
||||||
|
|
||||||
`--focus` runs exactly those two commands for you. (Focusing a display only does
|
Windows only, as a one-liner:
|
||||||
something visible when you have more than one.)
|
|
||||||
|
```sh
|
||||||
|
swaymsg "[con_id=$(wlgrid --no-outputs | cut -f2)] focus"
|
||||||
|
```
|
||||||
|
|
||||||
Three formats, because the identifiers different consumers need differ:
|
Three formats, because the identifiers different consumers need differ:
|
||||||
|
|
||||||
@@ -145,8 +145,9 @@ A wlroots compositor advertising `ext-image-copy-capture-v1`,
|
|||||||
`ext-image-capture-source-v1` (with the foreign-toplevel source manager),
|
`ext-image-capture-source-v1` (with the foreign-toplevel source manager),
|
||||||
`ext-foreign-toplevel-list-v1`, `wlr-layer-shell-unstable-v1` and
|
`ext-foreign-toplevel-list-v1`, `wlr-layer-shell-unstable-v1` and
|
||||||
`wp_viewporter` — sway 1.11+, and in principle Hyprland, labwc and jay, though
|
`wp_viewporter` — sway 1.11+, and in principle Hyprland, labwc and jay, though
|
||||||
only sway is tested. sway is also the source of truth for the window list and
|
only sway is tested. sway is also the source of truth for the window list, over
|
||||||
for focusing, over its IPC socket.
|
its IPC socket, which is the one thing that would need replacing to run
|
||||||
|
elsewhere (`ext-foreign-toplevel-list-v1` already reports app id and title).
|
||||||
|
|
||||||
Known upstream issue: holding per-toplevel capture sessions open makes windows
|
Known upstream issue: holding per-toplevel capture sessions open makes windows
|
||||||
blurry on **fractionally scaled** outputs
|
blurry on **fractionally scaled** outputs
|
||||||
|
|||||||
+67
-33
@@ -1,7 +1,7 @@
|
|||||||
//! wlgrid shows a thumbnail grid of every open window as a layer-shell overlay
|
//! wlgrid shows a live grid of every window and display as a layer-shell overlay
|
||||||
//! and focuses the one you pick. It replaces a wlthumbs + rofi pipeline, so it
|
//! and reports which one you picked. That is all it does: acting on the choice
|
||||||
//! keeps that pipeline's contract: sway owns the window list and the focusing,
|
//! belongs to whatever called it. It replaces a wlthumbs + rofi pipeline, so the
|
||||||
//! and the look comes straight from the rofi theme (see theme.rs).
|
//! look comes straight from that rofi theme (see theme.rs).
|
||||||
//!
|
//!
|
||||||
//! The pixels never pass through this process. Each window is captured into an
|
//! The pixels never pass through this process. Each window is captured into an
|
||||||
//! shm buffer handed straight to a subsurface, with wp_viewporter telling the
|
//! shm buffer handed straight to a subsurface, with wp_viewporter telling the
|
||||||
@@ -728,8 +728,7 @@ wlgrid — a live grid of window and display previews, for picking one
|
|||||||
|
|
||||||
usage: wlgrid [options]
|
usage: wlgrid [options]
|
||||||
|
|
||||||
--format tsv|json|portal how to report the pick [tsv]
|
--format tsv|json|portal how to report the pick [tsv]; see FORMATS
|
||||||
--focus also focus the pick, via sway [off]
|
|
||||||
--live all|current|none which tiles keep updating live [all]
|
--live all|current|none which tiles keep updating live [all]
|
||||||
(display tiles are always a single snapshot)
|
(display tiles are always a single snapshot)
|
||||||
--fps N cap on live updates per tile per second [12]
|
--fps N cap on live updates per tile per second [12]
|
||||||
@@ -749,27 +748,61 @@ keys: arrows, hjkl, or Tab/Shift+Tab move; Home/End jump; Enter picks;
|
|||||||
The pick goes to stdout, nothing does if you cancel; exit status is 0 for a
|
The pick goes to stdout, nothing does if you cancel; exit status is 0 for a
|
||||||
pick and 1 for a cancel.
|
pick and 1 for a cancel.
|
||||||
|
|
||||||
tsv TYPE<TAB>ID<TAB>TOPLEVEL_ID<TAB>APP<TAB>TITLE
|
FORMATS
|
||||||
TYPE is \"window\" or \"output\". ID is the sway con_id, or the
|
|
||||||
output name for a display. TOPLEVEL_ID is the
|
tsv One line of tab-separated columns; the default, meant for
|
||||||
ext-foreign-toplevel-list-v1 identifier, which is what tools like
|
`IFS=$'\\t' read` or cut(1):
|
||||||
grim -T capture by; it is empty for displays.
|
|
||||||
json the same record, every key always present, for jq
|
TYPE<TAB>ID<TAB>TOPLEVEL_ID<TAB>APP<TAB>TITLE
|
||||||
portal \"Monitor: NAME\" or \"Window: TOPLEVEL_ID\", i.e. exactly what
|
|
||||||
xdg-desktop-portal-wlr's simple chooser reads:
|
window 68 8e38849641c86be05bb1a68c163a1c41 dev.zed.Zed jobchi
|
||||||
|
output DP-1 display DP-1
|
||||||
|
|
||||||
|
TYPE is \"window\" or \"output\", so a caller knows which kind of
|
||||||
|
thing it got. ID is the one to act on: a sway con_id for a
|
||||||
|
window, the name for a display. TOPLEVEL_ID is the
|
||||||
|
ext-foreign-toplevel-list-v1 identifier, which is what capture
|
||||||
|
tools address a window by (grim -T, the desktop portal); it is
|
||||||
|
empty for a display. APP is the app id, or \"display\".
|
||||||
|
|
||||||
|
json The same record as one object, with every key always present so
|
||||||
|
jq can rely on it:
|
||||||
|
|
||||||
|
{\"type\":\"window\",\"con_id\":68,\"toplevel_id\":\"8e3884...\",
|
||||||
|
\"output\":null,\"app\":\"dev.zed.Zed\",\"title\":\"jobchi\"}
|
||||||
|
|
||||||
|
portal What xdg-desktop-portal-wlr's \"simple\" chooser reads, which lets
|
||||||
|
wlgrid be the picker a screencast pops up (getDisplayMedia in a
|
||||||
|
browser, for instance) with live previews of both windows and
|
||||||
|
whole displays:
|
||||||
|
|
||||||
|
Window: 8e38849641c86be05bb1a68c163a1c41
|
||||||
|
Monitor: DP-1
|
||||||
|
|
||||||
|
Put this in ~/.config/xdg-desktop-portal-wlr/config:
|
||||||
|
|
||||||
[screencast]
|
[screencast]
|
||||||
chooser_type=simple
|
chooser_type=simple
|
||||||
chooser_cmd=wlgrid --format portal
|
chooser_cmd=wlgrid --format portal
|
||||||
|
|
||||||
examples:
|
EXAMPLES, on sway
|
||||||
swaymsg \"[con_id=$(wlgrid | cut -f2)] focus\"
|
|
||||||
wlgrid --format json | jq -r .title
|
Focus a window:
|
||||||
|
swaymsg \"[con_id=$(wlgrid --no-outputs | cut -f2)] focus\"
|
||||||
|
|
||||||
|
Focus a window or move to a display, whichever was picked:
|
||||||
|
IFS=$'\\t' read -r type id toplevel app title < <(wlgrid) &&
|
||||||
|
case $type in
|
||||||
|
window) swaymsg \"[con_id=$id] focus\" ;;
|
||||||
|
output) swaymsg \"focus output $id\" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
Screenshot whatever you pick:
|
||||||
|
grim -T \"$(wlgrid --no-outputs | cut -f3)\" shot.png
|
||||||
";
|
";
|
||||||
|
|
||||||
struct Args {
|
struct Args {
|
||||||
format: Format,
|
format: Format,
|
||||||
focus: bool,
|
|
||||||
outputs: bool,
|
outputs: bool,
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
hide_labels: bool,
|
hide_labels: bool,
|
||||||
@@ -783,7 +816,6 @@ struct Args {
|
|||||||
fn parse_args() -> Result<Args, String> {
|
fn parse_args() -> Result<Args, String> {
|
||||||
let mut args = Args {
|
let mut args = Args {
|
||||||
format: Format::Tsv,
|
format: Format::Tsv,
|
||||||
focus: false,
|
|
||||||
outputs: true,
|
outputs: true,
|
||||||
verbose: false,
|
verbose: false,
|
||||||
hide_labels: false,
|
hide_labels: false,
|
||||||
@@ -796,7 +828,6 @@ fn parse_args() -> Result<Args, String> {
|
|||||||
let mut it = std::env::args().skip(1);
|
let mut it = std::env::args().skip(1);
|
||||||
while let Some(arg) = it.next() {
|
while let Some(arg) = it.next() {
|
||||||
match arg.as_str() {
|
match arg.as_str() {
|
||||||
"--focus" => args.focus = true,
|
|
||||||
"--format" => {
|
"--format" => {
|
||||||
args.format = match it.next().ok_or("--format needs tsv|json|portal")?.as_str() {
|
args.format = match it.next().ok_or("--format needs tsv|json|portal")?.as_str() {
|
||||||
"tsv" => Format::Tsv,
|
"tsv" => Format::Tsv,
|
||||||
@@ -865,15 +896,21 @@ fn run() -> Result<ExitCode, Box<dyn Error>> {
|
|||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let mut phases = Phases::new(args.verbose);
|
let mut phases = Phases::new(args.verbose);
|
||||||
let mut sway_conn = swayipc::Connection::new()?;
|
// The IPC connection is only needed to build the list, so it is closed again
|
||||||
let mut targets = sway::windows(&mut sway_conn)?;
|
// before the overlay maps.
|
||||||
let scale = sway::scale(&mut sway_conn)?;
|
let (targets, scale) = {
|
||||||
if args.outputs {
|
let mut sway = swayipc::Connection::new()?;
|
||||||
// Displays go last, after the windows, so window positions stay stable.
|
let mut targets = sway::windows(&mut sway)?;
|
||||||
for output in sway_conn.get_outputs()?.iter().filter(|o| o.active) {
|
let scale = sway::scale(&mut sway)?;
|
||||||
targets.push(Target::output(output.name.clone()));
|
if args.outputs {
|
||||||
|
// Displays go last, after the windows, so window positions are
|
||||||
|
// stable as windows come and go.
|
||||||
|
for output in sway.get_outputs()?.iter().filter(|o| o.active) {
|
||||||
|
targets.push(Target::output(output.name.clone()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
(targets, scale)
|
||||||
|
};
|
||||||
if targets.is_empty() {
|
if targets.is_empty() {
|
||||||
return Ok(ExitCode::SUCCESS);
|
return Ok(ExitCode::SUCCESS);
|
||||||
}
|
}
|
||||||
@@ -981,8 +1018,8 @@ fn run() -> Result<ExitCode, Box<dyn Error>> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// wlgrid is a chooser: it reports the pick and leaves acting on it to the
|
// wlgrid is a chooser: it reports the pick, and what that means is the
|
||||||
// caller (--focus is a convenience for a bare keybinding).
|
// caller's business.
|
||||||
if args.verbose {
|
if args.verbose {
|
||||||
eprintln!("wlgrid: {}", app.quit_why);
|
eprintln!("wlgrid: {}", app.quit_why);
|
||||||
}
|
}
|
||||||
@@ -1002,9 +1039,6 @@ fn run() -> Result<ExitCode, Box<dyn Error>> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if args.focus {
|
|
||||||
sway::focus(&mut sway_conn, &target)?;
|
|
||||||
}
|
|
||||||
Ok(ExitCode::SUCCESS)
|
Ok(ExitCode::SUCCESS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-15
@@ -1,7 +1,9 @@
|
|||||||
//! sway stays the source of truth for the window list and for focusing, exactly
|
//! sway is the source of truth for the window list (`swaymsg -t get_tree`); the
|
||||||
//! as the shell script this replaces did (`swaymsg -t get_tree` + `[con_id=N]
|
//! Wayland side only supplies pixels. The join between the two is
|
||||||
//! focus`). The 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
|
||||||
|
//! and the caller decides what that means.
|
||||||
|
|
||||||
use swayipc::{Connection, Node, NodeType};
|
use swayipc::{Connection, Node, NodeType};
|
||||||
|
|
||||||
@@ -47,15 +49,3 @@ pub fn scale(conn: &mut Connection) -> Result<i32, swayipc::Error> {
|
|||||||
.unwrap_or(1)
|
.unwrap_or(1)
|
||||||
.max(1))
|
.max(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn focus(conn: &mut Connection, target: &Target) -> Result<(), swayipc::Error> {
|
|
||||||
let cmd = match target.con_id {
|
|
||||||
Some(con_id) => format!("[con_id={con_id}] focus"),
|
|
||||||
// Picking a display means going to it.
|
|
||||||
None => format!("focus output {}", target.id),
|
|
||||||
};
|
|
||||||
for res in conn.run_command(cmd)? {
|
|
||||||
res?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user