diff --git a/README.md b/README.md index bc3d086..c67dbbd 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # wlgrid 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 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] ``` -- `--print` writes the selected sway `con_id` to stdout instead of focusing it - `--verbose` prints phase timings and how many windows were captured - `--hide-labels` draws an icon-only grid - `--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 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 -# Simplest: let wlgrid do the focusing, for a bare keybinding. -bindsym $mod+Tab exec wlgrid --focus - -# 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. +#!/usr/bin/env bash +# ~/.local/bin/winmenu, bound to $mod+Tab IFS=$'\t' read -r type id toplevel app title < <(wlgrid) || exit 0 case $type in window) swaymsg "[con_id=$id] focus" ;; @@ -68,8 +65,11 @@ case $type in esac ``` -`--focus` runs exactly those two commands for you. (Focusing a display only does -something visible when you have more than one.) +Windows only, as a one-liner: + +```sh +swaymsg "[con_id=$(wlgrid --no-outputs | cut -f2)] focus" +``` 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-foreign-toplevel-list-v1`, `wlr-layer-shell-unstable-v1` and `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 -for focusing, over its IPC socket. +only sway is tested. sway is also the source of truth for the window list, over +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 blurry on **fractionally scaled** outputs diff --git a/src/main.rs b/src/main.rs index b426f3a..db4b877 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ -//! wlgrid shows a thumbnail grid of every open window as a layer-shell overlay -//! and focuses the one you pick. It replaces a wlthumbs + rofi pipeline, so it -//! keeps that pipeline's contract: sway owns the window list and the focusing, -//! and the look comes straight from the rofi theme (see theme.rs). +//! wlgrid 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 +//! belongs to whatever called it. It replaces a wlthumbs + rofi pipeline, so the +//! look comes straight from that rofi theme (see theme.rs). //! //! 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 @@ -728,8 +728,7 @@ wlgrid — a live grid of window and display previews, for picking one usage: wlgrid [options] - --format tsv|json|portal how to report the pick [tsv] - --focus also focus the pick, via sway [off] + --format tsv|json|portal how to report the pick [tsv]; see FORMATS --live all|current|none which tiles keep updating live [all] (display tiles are always a single snapshot) --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 pick and 1 for a cancel. - tsv TYPEIDTOPLEVEL_IDAPPTITLE - TYPE is \"window\" or \"output\". ID is the sway con_id, or the - output name for a display. TOPLEVEL_ID is the - ext-foreign-toplevel-list-v1 identifier, which is what tools like - grim -T capture by; it is empty for displays. - json the same record, every key always present, for jq - portal \"Monitor: NAME\" or \"Window: TOPLEVEL_ID\", i.e. exactly what - xdg-desktop-portal-wlr's simple chooser reads: +FORMATS + + tsv One line of tab-separated columns; the default, meant for + `IFS=$'\\t' read` or cut(1): + + TYPEIDTOPLEVEL_IDAPPTITLE + + 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] chooser_type=simple chooser_cmd=wlgrid --format portal -examples: - swaymsg \"[con_id=$(wlgrid | cut -f2)] focus\" - wlgrid --format json | jq -r .title +EXAMPLES, on sway + + 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 { format: Format, - focus: bool, outputs: bool, verbose: bool, hide_labels: bool, @@ -783,7 +816,6 @@ struct Args { fn parse_args() -> Result { let mut args = Args { format: Format::Tsv, - focus: false, outputs: true, verbose: false, hide_labels: false, @@ -796,7 +828,6 @@ fn parse_args() -> Result { let mut it = std::env::args().skip(1); while let Some(arg) = it.next() { match arg.as_str() { - "--focus" => args.focus = true, "--format" => { args.format = match it.next().ok_or("--format needs tsv|json|portal")?.as_str() { "tsv" => Format::Tsv, @@ -865,15 +896,21 @@ fn run() -> Result> { let start = Instant::now(); let mut phases = Phases::new(args.verbose); - let mut sway_conn = swayipc::Connection::new()?; - let mut targets = sway::windows(&mut sway_conn)?; - let scale = sway::scale(&mut sway_conn)?; - if args.outputs { - // Displays go last, after the windows, so window positions stay stable. - for output in sway_conn.get_outputs()?.iter().filter(|o| o.active) { - targets.push(Target::output(output.name.clone())); + // The IPC connection is only needed to build the list, so it is closed again + // before the overlay maps. + let (targets, scale) = { + let mut sway = swayipc::Connection::new()?; + let mut targets = sway::windows(&mut sway)?; + let scale = sway::scale(&mut sway)?; + 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() { return Ok(ExitCode::SUCCESS); } @@ -981,8 +1018,8 @@ fn run() -> Result> { ); } - // wlgrid is a chooser: it reports the pick and leaves acting on it to the - // caller (--focus is a convenience for a bare keybinding). + // wlgrid is a chooser: it reports the pick, and what that means is the + // caller's business. if args.verbose { eprintln!("wlgrid: {}", app.quit_why); } @@ -1002,9 +1039,6 @@ fn run() -> Result> { } }, } - if args.focus { - sway::focus(&mut sway_conn, &target)?; - } Ok(ExitCode::SUCCESS) } diff --git a/src/sway.rs b/src/sway.rs index b4939a6..ab7f3e0 100644 --- a/src/sway.rs +++ b/src/sway.rs @@ -1,7 +1,9 @@ -//! sway stays the source of truth for the window list and for focusing, exactly -//! as the shell script this replaces did (`swaymsg -t get_tree` + `[con_id=N] -//! focus`). The Wayland side only supplies pixels; the join between the two is +//! sway is the source of truth for the window list (`swaymsg -t get_tree`); the +//! Wayland side only supplies pixels. The join between the two is //! `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}; @@ -47,15 +49,3 @@ pub fn scale(conn: &mut Connection) -> Result { .unwrap_or(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(()) -}