cleanup
This commit is contained in:
@@ -1,279 +0,0 @@
|
|||||||
# wl-pick
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
It replaces a `wlthumbs | rofi` pipeline, and doubles as a screencast source
|
|
||||||
picker for the desktop portal. The difference is that no thumbnails
|
|
||||||
exist: each window is captured straight into a `wl_shm` buffer that is handed to
|
|
||||||
its own `wl_subsurface`, and `wp_viewporter` tells the compositor which rectangle
|
|
||||||
to scale it into. There is no image encoding, no scaler, and no full-resolution
|
|
||||||
bitmap in this process — which is also why it appears in about 60 ms and holds
|
|
||||||
~18 MB of RSS however many windows are open.
|
|
||||||
|
|
||||||
```
|
|
||||||
sway-tree 0.6ms window list + con_ids over sway IPC
|
|
||||||
toplevels 0.2ms ext-foreign-toplevel-list handles
|
|
||||||
constraints 1.5ms every capture session's buffer size, in one roundtrip
|
|
||||||
capture 52.5ms 8 windows, all frames in flight at once
|
|
||||||
labels 0.0ms shaped on a worker thread while the captures ran
|
|
||||||
mapped 4.9ms layer surface + subsurfaces on screen
|
|
||||||
```
|
|
||||||
|
|
||||||
The capture phase is the compositor reading full-resolution window pixels out of
|
|
||||||
the GPU. It is bandwidth-bound (~1.1 GB/s here) and unaffected by how large the
|
|
||||||
thumbnails are — which also makes it a free window to do other work in. Loading
|
|
||||||
a font and rasterising its first glyphs costs ~20ms, so labels are shaped on a
|
|
||||||
worker thread started before the captures and joined after them, and cost
|
|
||||||
nothing in wall clock.
|
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
Working: a labelled grid of live previews with keyboard navigation. Type-to-filter
|
|
||||||
is the one thing the rofi version had that this doesn't — see the roadmap.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```
|
|
||||||
wl-pick [--format tsv|json|portal] [--live all|current|none] [--fps N]
|
|
||||||
[--outputs|--no-outputs] [--labels|--no-labels]
|
|
||||||
[--order mru|tree] [--alt-tab|--no-alt-tab]
|
|
||||||
[--font FAMILY] [--font-size PX]
|
|
||||||
[--timeout SECS] [--verbose]
|
|
||||||
```
|
|
||||||
|
|
||||||
- `--format tsv|json|portal` how to report the pick (default `tsv`)
|
|
||||||
- `--live all|current|none` which tiles keep updating (default `all`; displays
|
|
||||||
are always a single snapshot)
|
|
||||||
- `--fps N` cap on live updates per tile per second (default 12)
|
|
||||||
- `--outputs` / `--no-outputs` whether whole displays are tiles too (default
|
|
||||||
off). Both directions exist so either can override the config file
|
|
||||||
- `--labels` / `--no-labels` whether a label is drawn under each thumbnail
|
|
||||||
(default on); `--hide-labels` is the old spelling and still works
|
|
||||||
- `--order mru|tree` window ordering: Most-Recently-Used focus order or tree
|
|
||||||
layout order (default `mru`)
|
|
||||||
- `--alt-tab` / `--no-alt-tab` switcher mode: automatically pick on modifier
|
|
||||||
release (default `auto`, enabled whenever a modifier was held on enter)
|
|
||||||
- `--font FAMILY` label font family (default: the system monospace font)
|
|
||||||
- `--font-size PX` label size in logical px
|
|
||||||
- `--config PATH` config file (default `~/.config/wl-pick/config`)
|
|
||||||
- `--timeout SECS` exits after a deadline, in case the keyboard grab ever traps
|
|
||||||
you
|
|
||||||
- `--verbose` phase timings, the tile list, and capture stats
|
|
||||||
|
|
||||||
wl-pick is a chooser: the pick goes to stdout, nothing does if you cancel, and
|
|
||||||
the exit status is 0 for a pick and 1 for a cancel. It never acts on the choice —
|
|
||||||
it has no idea what you want to do with it. Focusing on sway looks like this:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
# ~/.local/bin/winmenu, bound to $mod+Tab
|
|
||||||
IFS=$'\t' read -r type id toplevel app title < <(wl-pick) || exit 0
|
|
||||||
case $type in
|
|
||||||
window) swaymsg "[con_id=$id] focus" ;;
|
|
||||||
output) swaymsg "focus output $id" ;;
|
|
||||||
esac
|
|
||||||
```
|
|
||||||
|
|
||||||
Windows only, as a one-liner:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
swaymsg "[con_id=$(wl-pick --no-outputs | cut -f2)] focus"
|
|
||||||
```
|
|
||||||
|
|
||||||
Three formats, because the identifiers different consumers need differ:
|
|
||||||
|
|
||||||
| `--format` | output |
|
|
||||||
|---|---|
|
|
||||||
| `tsv` (default) | `TYPE⇥ID⇥TOPLEVEL_ID⇥APP⇥TITLE` — `ID` is the sway `con_id`, or the output name for a display; `TOPLEVEL_ID` is the ext-foreign-toplevel-list-v1 identifier that `grim -T` and the portal capture by |
|
|
||||||
| `json` | the same record with every key always present, for `jq` |
|
|
||||||
| `portal` | `Monitor: NAME` or `Window: TOPLEVEL_ID` |
|
|
||||||
|
|
||||||
`portal` is exactly what xdg-desktop-portal-wlr's `simple` chooser reads, so
|
|
||||||
wl-pick can be the picker for `getDisplayMedia` and friends — with live previews
|
|
||||||
of both windows and displays:
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[screencast]
|
|
||||||
chooser_type=simple
|
|
||||||
chooser_cmd=wl-pick --format portal
|
|
||||||
```
|
|
||||||
|
|
||||||
**Starting a second wl-pick replaces the first.** The new overlay takes the
|
|
||||||
keyboard grab, and the one that loses it exits without printing anything — so
|
|
||||||
hitting the keybinding twice leaves you with one overlay, not a stranded
|
|
||||||
process. The catch is that sway answers a capture request for a toplevel
|
|
||||||
another client is already capturing with silence — no frame, no failure — so
|
|
||||||
the replacement's thumbnails are mostly blank until the first instance has
|
|
||||||
gone. Every wait before the overlay is interactive is capped at two seconds
|
|
||||||
for that reason: a tile that never arrives is drawn as a bare label, and the
|
|
||||||
grid still works.
|
|
||||||
|
|
||||||
| key | |
|
|
||||||
|---|---|
|
|
||||||
| `→` `←` / `l` `h` / `Tab` `Shift+Tab` | next / previous tile |
|
|
||||||
| `↓` `↑` / `j` `k` | move a row |
|
|
||||||
| `Home` `End` / `PgUp` `PgDn` | first / last, or a screen at a time |
|
|
||||||
| `Enter` / release modifier | pick the selection (release Alt/Super in Alt+Tab mode) |
|
|
||||||
| `Escape` / `q` | cancel |
|
|
||||||
| click | pick that tile |
|
|
||||||
| scroll | next / previous tile |
|
|
||||||
|
|
||||||
Hovering deliberately does not move the selection — the keyboard keeps it, and a
|
|
||||||
click acts on whatever is under the cursor. Clicking the margin, a gap, or an
|
|
||||||
empty cell of a ragged last row does nothing. Tiles are subsurfaces, so a click
|
|
||||||
on a thumbnail identifies its tile by surface; only clicks on the chrome around
|
|
||||||
them need hit-testing.
|
|
||||||
|
|
||||||
Navigation reads raw evdev keycodes, so it is layout-independent — but it also
|
|
||||||
means virtual-keyboard clients such as `wtype` (which invent their own keymap)
|
|
||||||
cannot drive it. That goes away with xkb support, which filtering needs anyway.
|
|
||||||
|
|
||||||
## Live previews
|
|
||||||
|
|
||||||
Capture sessions stay open, so a tile can be refreshed. Three things keep that
|
|
||||||
from being expensive:
|
|
||||||
|
|
||||||
- **It is damage-driven.** After a session's first frame the compositor only
|
|
||||||
produces another once the window content changes, so a request left
|
|
||||||
outstanding on an idle window costs nothing. Measured over 4s with one
|
|
||||||
animating window out of ten: `52,52,1,1,1,1,1,1,13,1` frames — the static
|
|
||||||
windows delivered exactly their first frame and nothing more.
|
|
||||||
- **Frame callbacks are the clock.** Re-captures are driven by the overlay's own
|
|
||||||
`wl_surface.frame` callbacks, so they stop when it isn't being presented, and
|
|
||||||
`--fps` throttles per tile on top of that (12 fps measured as 12.1).
|
|
||||||
- **Two buffers per window, alternating.** A capture must not write into a buffer
|
|
||||||
the compositor is reading, so each window gets two and `wl_buffer.release`
|
|
||||||
decides which is free. Note that release is the entire contract: with `wl_shm`
|
|
||||||
the compositor copies the pixels out at commit and hands the buffer straight
|
|
||||||
back, so the slot on screen is usually free too — waiting for it to stop being
|
|
||||||
displayed instead deadlocks after two frames.
|
|
||||||
|
|
||||||
The cost is memory and bandwidth: two full-resolution buffers per window (138 MB
|
|
||||||
of shm for eight windows and a display here, against 83 MB with `--live none`)
|
|
||||||
and a readback per refreshed frame. `--live current` refreshes only the selected tile,
|
|
||||||
which is much cheaper and still reads as alive.
|
|
||||||
|
|
||||||
## Config
|
|
||||||
|
|
||||||
`~/.config/wl-pick/config`, or `--config PATH`. Flat `key = value` lines with
|
|
||||||
`#` comments, everything optional, and a flag always beats the file. No TOML
|
|
||||||
dependency, because there is nothing to nest.
|
|
||||||
|
|
||||||
```ini
|
|
||||||
background = #282828 # the grid's backdrop
|
|
||||||
foreground = #ebdbb2 # label text
|
|
||||||
selection = #d79921 # the highlighted tile
|
|
||||||
selection-text = #282828 # its label
|
|
||||||
border = #d79921
|
|
||||||
border-width = 2px
|
|
||||||
|
|
||||||
max-width = 90ppt # the box the grid may fill
|
|
||||||
max-height = 90ppt
|
|
||||||
max-columns = 4 # thumbnails are that box divided by these
|
|
||||||
max-rows = 4
|
|
||||||
|
|
||||||
font = monospace
|
|
||||||
font-size = 13.3
|
|
||||||
labels = yes
|
|
||||||
outputs = no # include whole displays as tiles (default no)
|
|
||||||
order = mru # mru (default) or tree
|
|
||||||
alt-tab = auto # auto (default), yes or no
|
|
||||||
live = all
|
|
||||||
fps = 12
|
|
||||||
format = tsv
|
|
||||||
timeout = 0 # seconds; 0 means none
|
|
||||||
```
|
|
||||||
|
|
||||||
Sizes take sway's units: `600px` is absolute, `90ppt` a percentage — and the
|
|
||||||
percentage resolves against **the display the grid actually appears on**, every
|
|
||||||
time it runs. On a mixed setup one file gives 90% of a 1280-wide laptop panel and
|
|
||||||
90% of a 3840-wide monitor, rather than a pixel count that suits one and looks
|
|
||||||
wrong on the other. The overlay maps explicitly on that display, at its scale, so
|
|
||||||
mixed-DPI renders crisply either way.
|
|
||||||
|
|
||||||
All four sizing settings are **caps**:
|
|
||||||
|
|
||||||
- `max-width` and `max-height` bound the overlay.
|
|
||||||
- `max-columns` and `max-rows` bound the grid inside it.
|
|
||||||
|
|
||||||
A thumbnail is simply that box divided by those caps, which means **its size
|
|
||||||
never 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` scroll, with a scrollbar in the right
|
|
||||||
margin, `PgUp`/`PgDn`, and the selection always kept in view. Tiles scrolled out
|
|
||||||
of sight are unmapped, so live capture skips them too.
|
|
||||||
|
|
||||||
Turning labels off gives that row back to the thumbnails rather than shrinking
|
|
||||||
the window, since the box is what you asked for either way.
|
|
||||||
|
|
||||||
## Look
|
|
||||||
|
|
||||||
The defaults come from the rofi theme this replaces: gruvbox dark, a yellow
|
|
||||||
selection filling the element padding, `ceil(sqrt(n))` columns capped at 4,
|
|
||||||
`title · app` centred underneath. Padding, gaps and margins are still fixed, in
|
|
||||||
`src/theme.rs`.
|
|
||||||
|
|
||||||
The label font defaults to the system monospace font — whatever `fc-match
|
|
||||||
monospace` answers, which is what the rest of the desktop uses. (cosmic-text's
|
|
||||||
own generic resolves through a built-in preference that is usually not
|
|
||||||
installed, and then lands on an arbitrary face, so it is asked directly
|
|
||||||
instead; if fontconfig isn't available, a short list of common distribution
|
|
||||||
defaults is tried.) `--font` names a family instead, and `--verbose` reports
|
|
||||||
which family the labels were actually shaped with.
|
|
||||||
|
|
||||||
Naming a family scans your own font directories first because they are small;
|
|
||||||
the full system scan (~37ms) happens only if it isn't found there. An unknown
|
|
||||||
family falls back to whatever cosmic-text picks rather than failing. Long titles
|
|
||||||
are ellipsised to the cell.
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
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, 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).
|
|
||||||
|
|
||||||
The socket is found from `SWAYSOCK`/`I3SOCK` when those point at something that
|
|
||||||
exists, and otherwise by looking for the running sway's socket in
|
|
||||||
`$XDG_RUNTIME_DIR`. Inheriting a stale path is easy — any process that outlives
|
|
||||||
the sway that started it hands one to every shell it spawns — and a picker on a
|
|
||||||
keybinding should not be the thing that notices.
|
|
||||||
|
|
||||||
Known upstream issue: holding per-toplevel capture sessions open makes windows
|
|
||||||
blurry on **fractionally scaled** outputs
|
|
||||||
([sway#9113](https://github.com/swaywm/sway/issues/9113)). Integer scales are
|
|
||||||
unaffected. It matters more once previews are live.
|
|
||||||
|
|
||||||
## Roadmap
|
|
||||||
|
|
||||||
- type-to-filter with fzf-quality fuzzy matching (and the xkb keyboard input it
|
|
||||||
needs, which would also let virtual-keyboard clients drive the overlay)
|
|
||||||
- dmabuf capture, so the pixels never leave the GPU at all — and live previews
|
|
||||||
stop costing a readback per frame
|
|
||||||
|
|
||||||
## Source layout
|
|
||||||
|
|
||||||
```
|
|
||||||
main.rs orchestration: list, capture, map, report the pick
|
|
||||||
cli.rs flags, defaults, and the help text that documents them
|
|
||||||
sway.rs the window list and display names, over sway's IPC socket
|
|
||||||
target.rs what a tile stands for, and the three output formats
|
|
||||||
app.rs the Wayland client state every event dispatches into
|
|
||||||
capture.rs capture sessions, their buffers, and the live clock
|
|
||||||
overlay.rs the layer surface, the drawing, and the keyboard
|
|
||||||
theme.rs colours, grid geometry, aspect fitting
|
|
||||||
text.rs label shaping on a worker thread
|
|
||||||
shm.rs memfd allocation and the ARGB painter
|
|
||||||
```
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
```
|
|
||||||
cargo build --release
|
|
||||||
cargo test # grid geometry and hit-testing, ellipsising, output
|
|
||||||
# formats, glyph output
|
|
||||||
```
|
|
||||||
+2
-10
@@ -46,8 +46,7 @@ use wayland_protocols::wp::viewporter::client::{
|
|||||||
};
|
};
|
||||||
use wayland_protocols_wlr::layer_shell::v1::client::zwlr_layer_shell_v1::ZwlrLayerShellV1;
|
use wayland_protocols_wlr::layer_shell::v1::client::zwlr_layer_shell_v1::ZwlrLayerShellV1;
|
||||||
|
|
||||||
use crate::capture::{Live, Tile};
|
use crate::capture::Tile;
|
||||||
use crate::config::AltTabMode;
|
|
||||||
use crate::overlay;
|
use crate::overlay;
|
||||||
use crate::shm;
|
use crate::shm;
|
||||||
use crate::target::Target;
|
use crate::target::Target;
|
||||||
@@ -55,18 +54,14 @@ use crate::text;
|
|||||||
use crate::theme::{Layout, Theme};
|
use crate::theme::{Layout, Theme};
|
||||||
|
|
||||||
/// What the caller decided before any of this started: the look, and how much
|
/// What the caller decided before any of this started: the look, and how much
|
||||||
/// live capture to do. Passed as one value because `theme`, `live`, `fps` and
|
/// live capture to do. `live` is always on for the alt-tab switcher mode.
|
||||||
/// `scale` all arrive together and two of them are bare integers.
|
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
pub theme: Theme,
|
pub theme: Theme,
|
||||||
pub live: Live,
|
|
||||||
pub fps: u32,
|
pub fps: u32,
|
||||||
/// Integer scale of the display the overlay renders on, and its name, so
|
/// Integer scale of the display the overlay renders on, and its name, so
|
||||||
/// the overlay maps there rather than wherever the compositor would put it.
|
/// the overlay maps there rather than wherever the compositor would put it.
|
||||||
pub scale: i32,
|
pub scale: i32,
|
||||||
pub output: String,
|
pub output: String,
|
||||||
#[allow(dead_code)]
|
|
||||||
pub alt_tab: AltTabMode,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
@@ -88,7 +83,6 @@ pub struct App {
|
|||||||
|
|
||||||
pub(crate) theme: Theme,
|
pub(crate) theme: Theme,
|
||||||
pub(crate) layout: Layout,
|
pub(crate) layout: Layout,
|
||||||
pub(crate) live: Live,
|
|
||||||
pub(crate) fps: u32,
|
pub(crate) fps: u32,
|
||||||
pub(crate) scale: i32,
|
pub(crate) scale: i32,
|
||||||
pub(crate) sel: usize,
|
pub(crate) sel: usize,
|
||||||
@@ -182,7 +176,6 @@ impl App {
|
|||||||
) -> Result<Self, Box<dyn Error>> {
|
) -> Result<Self, Box<dyn Error>> {
|
||||||
let Settings {
|
let Settings {
|
||||||
theme,
|
theme,
|
||||||
live,
|
|
||||||
fps,
|
fps,
|
||||||
scale,
|
scale,
|
||||||
output,
|
output,
|
||||||
@@ -206,7 +199,6 @@ impl App {
|
|||||||
tiles: targets.into_iter().map(Tile::new).collect(),
|
tiles: targets.into_iter().map(Tile::new).collect(),
|
||||||
theme,
|
theme,
|
||||||
layout,
|
layout,
|
||||||
live,
|
|
||||||
fps,
|
fps,
|
||||||
scale,
|
scale,
|
||||||
sel,
|
sel,
|
||||||
|
|||||||
+6
-36
@@ -7,12 +7,14 @@
|
|||||||
//!
|
//!
|
||||||
//! The pixels are never mapped into this process. A capture buffer goes straight
|
//! The pixels are never mapped into this process. A capture buffer goes straight
|
||||||
//! to a subsurface for display, so the compositor writes those pages and samples
|
//! to a subsurface for display, so the compositor writes those pages and samples
|
||||||
//! them again itself.
|
//! them again itself. Live previews are always on.
|
||||||
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::os::fd::AsFd;
|
use std::os::fd::AsFd;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use crate::Target;
|
||||||
|
|
||||||
use wayland_client::protocol::{
|
use wayland_client::protocol::{
|
||||||
wl_buffer::{self, WlBuffer},
|
wl_buffer::{self, WlBuffer},
|
||||||
wl_callback, wl_output, wl_shm,
|
wl_callback, wl_output, wl_shm,
|
||||||
@@ -30,29 +32,7 @@ use wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport;
|
|||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::shm;
|
use crate::shm;
|
||||||
use crate::target::{Kind, Target};
|
use crate::target::Kind;
|
||||||
|
|
||||||
/// Which tiles keep updating after the first frame.
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
||||||
pub enum Live {
|
|
||||||
/// Every tile.
|
|
||||||
All,
|
|
||||||
/// Only the selected tile: much cheaper, and still reads as alive.
|
|
||||||
Current,
|
|
||||||
/// Nothing: one snapshot each, a picker rather than an expose.
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Live {
|
|
||||||
pub fn parse(s: &str) -> Result<Self, String> {
|
|
||||||
match s.trim() {
|
|
||||||
"all" => Ok(Live::All),
|
|
||||||
"current" => Ok(Live::Current),
|
|
||||||
"none" => Ok(Live::None),
|
|
||||||
other => Err(format!("{other:?} is not all, current or none")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One capture buffer. `busy` means the compositor still holds it — either it is
|
/// One capture buffer. `busy` means the compositor still holds it — either it is
|
||||||
/// on screen or a capture is writing into it — so we must not scribble over it.
|
/// on screen or a capture is writing into it — so we must not scribble over it.
|
||||||
@@ -202,9 +182,8 @@ impl App {
|
|||||||
tile.settled = true;
|
tile.settled = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Only a tile that will be re-captured needs a second buffer, and a
|
// Always 2 buffers for live preview; display gets full-screen buffer.
|
||||||
// display's is the size of the whole screen.
|
let slots = if tile.target.kind == Kind::Output {
|
||||||
let slots = if self.live == Live::None || tile.target.kind == Kind::Output {
|
|
||||||
1
|
1
|
||||||
} else {
|
} else {
|
||||||
2
|
2
|
||||||
@@ -301,9 +280,6 @@ impl App {
|
|||||||
/// Ask for the next frame callback. A commit is needed for the compositor to
|
/// Ask for the next frame callback. A commit is needed for the compositor to
|
||||||
/// schedule one, and an empty commit is enough.
|
/// schedule one, and an empty commit is enough.
|
||||||
pub fn arm_frame_callback(&mut self, qh: &QueueHandle<Self>) {
|
pub fn arm_frame_callback(&mut self, qh: &QueueHandle<Self>) {
|
||||||
if self.live == Live::None {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if let Some(surface) = self.surface.clone() {
|
if let Some(surface) = self.surface.clone() {
|
||||||
surface.frame(qh, ());
|
surface.frame(qh, ());
|
||||||
surface.commit();
|
surface.commit();
|
||||||
@@ -314,15 +290,9 @@ impl App {
|
|||||||
/// the overlay is not being presented.
|
/// the overlay is not being presented.
|
||||||
pub fn tick(&mut self, qh: &QueueHandle<Self>) {
|
pub fn tick(&mut self, qh: &QueueHandle<Self>) {
|
||||||
self.stats.ticks += 1;
|
self.stats.ticks += 1;
|
||||||
if self.live == Live::None {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let interval = Duration::from_secs_f64(1.0 / self.fps.max(1) as f64);
|
let interval = Duration::from_secs_f64(1.0 / self.fps.max(1) as f64);
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
for i in 0..self.tiles.len() {
|
for i in 0..self.tiles.len() {
|
||||||
if self.live == Live::Current && i != self.sel {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// A display tile shows this overlay, which shows the display tile:
|
// A display tile shows this overlay, which shows the display tile:
|
||||||
// refreshing it never settles and costs a whole screen per frame.
|
// refreshing it never settles and costs a whole screen per frame.
|
||||||
if self.tiles[i].target.kind == Kind::Output {
|
if self.tiles[i].target.kind == Kind::Output {
|
||||||
|
|||||||
+1
-15
@@ -4,8 +4,7 @@ use std::path::PathBuf;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::app::Settings;
|
use crate::app::Settings;
|
||||||
use crate::capture::Live;
|
use crate::config::{Config, Length};
|
||||||
use crate::config::{AltTabMode, Config, Length};
|
|
||||||
use crate::sway::{Display, Order};
|
use crate::sway::{Display, Order};
|
||||||
use crate::target::Format;
|
use crate::target::Format;
|
||||||
use crate::theme::Theme;
|
use crate::theme::Theme;
|
||||||
@@ -23,7 +22,6 @@ usage: wl-pick [options]
|
|||||||
--outputs, --no-outputs include whole displays as tiles [no]
|
--outputs, --no-outputs include whole displays as tiles [no]
|
||||||
--labels, --no-labels a label under each thumbnail [yes]
|
--labels, --no-labels a label under each thumbnail [yes]
|
||||||
--order mru|tree window ordering: mru or layout tree [mru]
|
--order mru|tree window ordering: mru or layout tree [mru]
|
||||||
--alt-tab, --no-alt-tab alt-tab switcher mode (commit on release) [auto]
|
|
||||||
--focus, --no-focus focus the picked target in sway directly [no]
|
--focus, --no-focus focus the picked target in sway directly [no]
|
||||||
--font FAMILY label font family [the system monospace font]
|
--font FAMILY label font family [the system monospace font]
|
||||||
--font-size PX label size in logical px [13.3]
|
--font-size PX label size in logical px [13.3]
|
||||||
@@ -116,11 +114,9 @@ pub struct Args {
|
|||||||
pub(crate) labels: Option<bool>,
|
pub(crate) labels: Option<bool>,
|
||||||
pub(crate) font: Option<String>,
|
pub(crate) font: Option<String>,
|
||||||
pub(crate) font_size: Option<f32>,
|
pub(crate) font_size: Option<f32>,
|
||||||
pub(crate) live: Option<Live>,
|
|
||||||
pub(crate) fps: Option<u32>,
|
pub(crate) fps: Option<u32>,
|
||||||
pub(crate) timeout: Option<Duration>,
|
pub(crate) timeout: Option<Duration>,
|
||||||
pub(crate) order: Option<Order>,
|
pub(crate) order: Option<Order>,
|
||||||
pub(crate) alt_tab: Option<AltTabMode>,
|
|
||||||
pub(crate) focus: Option<bool>,
|
pub(crate) focus: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,11 +200,9 @@ impl Args {
|
|||||||
display: (display.width, display.height),
|
display: (display.width, display.height),
|
||||||
settings: Settings {
|
settings: Settings {
|
||||||
theme,
|
theme,
|
||||||
live: self.live.or(cfg.live).unwrap_or(Live::All),
|
|
||||||
fps: self.fps.or(cfg.fps).unwrap_or(12),
|
fps: self.fps.or(cfg.fps).unwrap_or(12),
|
||||||
scale: display.scale,
|
scale: display.scale,
|
||||||
output: display.name.clone(),
|
output: display.name.clone(),
|
||||||
alt_tab: self.alt_tab.or(cfg.alt_tab).unwrap_or(AltTabMode::Auto),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,8 +227,6 @@ fn parse(it: impl Iterator<Item = String>) -> Result<Args, String> {
|
|||||||
let v = it.next().ok_or("--order needs mru|tree")?;
|
let v = it.next().ok_or("--order needs mru|tree")?;
|
||||||
args.order = Some(Order::parse(&v)?);
|
args.order = Some(Order::parse(&v)?);
|
||||||
}
|
}
|
||||||
"--alt-tab" => args.alt_tab = Some(AltTabMode::Yes),
|
|
||||||
"--no-alt-tab" => args.alt_tab = Some(AltTabMode::No),
|
|
||||||
"--focus" => args.focus = Some(true),
|
"--focus" => args.focus = Some(true),
|
||||||
"--no-focus" => args.focus = Some(false),
|
"--no-focus" => args.focus = Some(false),
|
||||||
"--config" => {
|
"--config" => {
|
||||||
@@ -245,10 +237,6 @@ fn parse(it: impl Iterator<Item = String>) -> Result<Args, String> {
|
|||||||
// --hide-labels was the only spelling before --labels existed, and
|
// --hide-labels was the only spelling before --labels existed, and
|
||||||
// is still accepted for whatever it is wired into.
|
// is still accepted for whatever it is wired into.
|
||||||
"--no-labels" | "--hide-labels" => args.labels = Some(false),
|
"--no-labels" | "--hide-labels" => args.labels = Some(false),
|
||||||
"--live" => {
|
|
||||||
let v = it.next().ok_or("--live needs all|current|none")?;
|
|
||||||
args.live = Some(Live::parse(&v)?);
|
|
||||||
}
|
|
||||||
"--fps" => {
|
"--fps" => {
|
||||||
let v = it.next().ok_or("--fps needs a number")?;
|
let v = it.next().ok_or("--fps needs a number")?;
|
||||||
args.fps = Some(v.parse().map_err(|_| format!("bad --fps: {v}"))?);
|
args.fps = Some(v.parse().map_err(|_| format!("bad --fps: {v}"))?);
|
||||||
@@ -307,8 +295,6 @@ mod tests {
|
|||||||
assert_eq!(args(&["--labels"]).labels, Some(true));
|
assert_eq!(args(&["--labels"]).labels, Some(true));
|
||||||
assert_eq!(args(&["--no-labels"]).labels, Some(false));
|
assert_eq!(args(&["--no-labels"]).labels, Some(false));
|
||||||
assert_eq!(args(&["--hide-labels"]).labels, Some(false), "old spelling");
|
assert_eq!(args(&["--hide-labels"]).labels, Some(false), "old spelling");
|
||||||
assert_eq!(args(&["--alt-tab"]).alt_tab, Some(AltTabMode::Yes));
|
|
||||||
assert_eq!(args(&["--no-alt-tab"]).alt_tab, Some(AltTabMode::No));
|
|
||||||
// Unset is what lets the file have its say.
|
// Unset is what lets the file have its say.
|
||||||
assert_eq!(args(&[]).outputs, None);
|
assert_eq!(args(&[]).outputs, None);
|
||||||
assert_eq!(args(&[]).labels, None);
|
assert_eq!(args(&[]).labels, None);
|
||||||
|
|||||||
+1
-40
@@ -14,30 +14,10 @@
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::capture::Live;
|
|
||||||
use crate::sway::Order;
|
use crate::sway::Order;
|
||||||
use crate::target::Format;
|
use crate::target::Format;
|
||||||
use crate::theme::Argb;
|
use crate::theme::Argb;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
|
||||||
pub enum AltTabMode {
|
|
||||||
#[default]
|
|
||||||
Auto,
|
|
||||||
Yes,
|
|
||||||
No,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AltTabMode {
|
|
||||||
pub fn parse(s: &str) -> Result<Self, String> {
|
|
||||||
match s.trim() {
|
|
||||||
"auto" => Ok(AltTabMode::Auto),
|
|
||||||
"yes" | "true" | "on" | "1" => Ok(AltTabMode::Yes),
|
|
||||||
"no" | "false" | "off" | "0" => Ok(AltTabMode::No),
|
|
||||||
other => Err(format!("{other:?} is not auto, yes or no")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A size, either absolute or relative to the display it will be shown on.
|
/// A size, either absolute or relative to the display it will be shown on.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub enum Length {
|
pub enum Length {
|
||||||
@@ -114,12 +94,10 @@ pub struct Config {
|
|||||||
pub font_size: Option<f32>,
|
pub font_size: Option<f32>,
|
||||||
pub labels: Option<bool>,
|
pub labels: Option<bool>,
|
||||||
pub outputs: Option<bool>,
|
pub outputs: Option<bool>,
|
||||||
pub live: Option<Live>,
|
|
||||||
pub fps: Option<u32>,
|
pub fps: Option<u32>,
|
||||||
pub format: Option<Format>,
|
pub format: Option<Format>,
|
||||||
pub timeout: Option<Duration>,
|
pub timeout: Option<Duration>,
|
||||||
pub order: Option<Order>,
|
pub order: Option<Order>,
|
||||||
pub alt_tab: Option<AltTabMode>,
|
|
||||||
pub focus: Option<bool>,
|
pub focus: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +149,7 @@ impl Config {
|
|||||||
"font-size" => self.font_size = Some(number(value)?),
|
"font-size" => self.font_size = Some(number(value)?),
|
||||||
"labels" => self.labels = Some(boolean(value)?),
|
"labels" => self.labels = Some(boolean(value)?),
|
||||||
"outputs" => self.outputs = Some(boolean(value)?),
|
"outputs" => self.outputs = Some(boolean(value)?),
|
||||||
"live" => self.live = Some(Live::parse(value)?),
|
|
||||||
"fps" => self.fps = Some(number(value)?),
|
"fps" => self.fps = Some(number(value)?),
|
||||||
"format" => self.format = Some(Format::parse(value)?),
|
"format" => self.format = Some(Format::parse(value)?),
|
||||||
// Zero is how you say "no timeout"; an immediate deadline would
|
// Zero is how you say "no timeout"; an immediate deadline would
|
||||||
@@ -181,7 +159,6 @@ impl Config {
|
|||||||
self.timeout = (secs > 0.0).then(|| Duration::from_secs_f64(secs));
|
self.timeout = (secs > 0.0).then(|| Duration::from_secs_f64(secs));
|
||||||
}
|
}
|
||||||
"order" => self.order = Some(Order::parse(value)?),
|
"order" => self.order = Some(Order::parse(value)?),
|
||||||
"alt-tab" => self.alt_tab = Some(AltTabMode::parse(value)?),
|
|
||||||
"focus" => self.focus = Some(boolean(value)?),
|
"focus" => self.focus = Some(boolean(value)?),
|
||||||
other => return Err(format!("unknown setting {other:?}")),
|
other => return Err(format!("unknown setting {other:?}")),
|
||||||
}
|
}
|
||||||
@@ -264,12 +241,10 @@ max-width = 70ppt
|
|||||||
max-columns = 4
|
max-columns = 4
|
||||||
max-rows = 3
|
max-rows = 3
|
||||||
|
|
||||||
live = current
|
|
||||||
fps = 30
|
fps = 30
|
||||||
labels = no
|
labels = no
|
||||||
timeout = 0
|
timeout = 0
|
||||||
order = mru
|
order = mru
|
||||||
alt-tab = yes
|
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
.expect("should parse");
|
.expect("should parse");
|
||||||
@@ -283,8 +258,6 @@ alt-tab = yes
|
|||||||
assert_eq!(cfg.labels, Some(false));
|
assert_eq!(cfg.labels, Some(false));
|
||||||
assert_eq!(cfg.timeout, None, "zero means no timeout");
|
assert_eq!(cfg.timeout, None, "zero means no timeout");
|
||||||
assert_eq!(cfg.order, Some(Order::Mru));
|
assert_eq!(cfg.order, Some(Order::Mru));
|
||||||
assert_eq!(cfg.alt_tab, Some(AltTabMode::Yes));
|
|
||||||
assert!(cfg.live.is_some());
|
|
||||||
// Untouched settings stay unset, so defaults survive.
|
// Untouched settings stay unset, so defaults survive.
|
||||||
assert_eq!(cfg.foreground, None);
|
assert_eq!(cfg.foreground, None);
|
||||||
assert_eq!(cfg.max_height, None);
|
assert_eq!(cfg.max_height, None);
|
||||||
@@ -324,16 +297,4 @@ alt-tab = yes
|
|||||||
unsafe { std::env::set_var("XDG_CONFIG_HOME", "/nonexistent") };
|
unsafe { std::env::set_var("XDG_CONFIG_HOME", "/nonexistent") };
|
||||||
assert!(Config::load(None).is_ok());
|
assert!(Config::load(None).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alt_tab_mode_parses() {
|
|
||||||
assert_eq!(AltTabMode::parse("auto"), Ok(AltTabMode::Auto));
|
|
||||||
assert_eq!(AltTabMode::parse("yes"), Ok(AltTabMode::Yes));
|
|
||||||
assert_eq!(AltTabMode::parse("true"), Ok(AltTabMode::Yes));
|
|
||||||
assert_eq!(AltTabMode::parse("1"), Ok(AltTabMode::Yes));
|
|
||||||
assert_eq!(AltTabMode::parse("no"), Ok(AltTabMode::No));
|
|
||||||
assert_eq!(AltTabMode::parse("false"), Ok(AltTabMode::No));
|
|
||||||
assert_eq!(AltTabMode::parse("0"), Ok(AltTabMode::No));
|
|
||||||
assert!(AltTabMode::parse("maybe").is_err());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-24
@@ -34,11 +34,6 @@ const KEY_TAB: u32 = 15;
|
|||||||
const KEY_Q: u32 = 16;
|
const KEY_Q: u32 = 16;
|
||||||
const KEY_ENTER: u32 = 28;
|
const KEY_ENTER: u32 = 28;
|
||||||
const KEY_LEFTCTRL: u32 = 29;
|
const KEY_LEFTCTRL: u32 = 29;
|
||||||
// hjkl, by physical position: the same keys as vim on a qwerty layout.
|
|
||||||
const KEY_H: u32 = 35;
|
|
||||||
const KEY_J: u32 = 36;
|
|
||||||
const KEY_K: u32 = 37;
|
|
||||||
const KEY_L: u32 = 38;
|
|
||||||
const KEY_LEFTSHIFT: u32 = 42;
|
const KEY_LEFTSHIFT: u32 = 42;
|
||||||
const KEY_RIGHTSHIFT: u32 = 54;
|
const KEY_RIGHTSHIFT: u32 = 54;
|
||||||
const KEY_LEFTALT: u32 = 56;
|
const KEY_LEFTALT: u32 = 56;
|
||||||
@@ -59,12 +54,7 @@ const KEY_RIGHTMETA: u32 = 126;
|
|||||||
fn is_trigger_modifier(code: u32) -> bool {
|
fn is_trigger_modifier(code: u32) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
code,
|
code,
|
||||||
KEY_LEFTALT
|
KEY_LEFTALT | KEY_RIGHTALT | KEY_LEFTMETA | KEY_RIGHTMETA | KEY_LEFTCTRL | KEY_RIGHTCTRL
|
||||||
| KEY_RIGHTALT
|
|
||||||
| KEY_LEFTMETA
|
|
||||||
| KEY_RIGHTMETA
|
|
||||||
| KEY_LEFTCTRL
|
|
||||||
| KEY_RIGHTCTRL
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,10 +71,6 @@ fn is_repeatable_key(code: u32) -> bool {
|
|||||||
| KEY_END
|
| KEY_END
|
||||||
| KEY_PGUP
|
| KEY_PGUP
|
||||||
| KEY_PGDN
|
| KEY_PGDN
|
||||||
| KEY_H
|
|
||||||
| KEY_J
|
|
||||||
| KEY_K
|
|
||||||
| KEY_L
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,10 +344,10 @@ impl App {
|
|||||||
self.ending = Ending::Picked;
|
self.ending = Ending::Picked;
|
||||||
}
|
}
|
||||||
KEY_TAB if self.shift => self.move_sel(-1, qh),
|
KEY_TAB if self.shift => self.move_sel(-1, qh),
|
||||||
KEY_TAB | KEY_RIGHT | KEY_L => self.move_sel(1, qh),
|
KEY_TAB | KEY_RIGHT => self.move_sel(1, qh),
|
||||||
KEY_LEFT | KEY_H => self.move_sel(-1, qh),
|
KEY_LEFT => self.move_sel(-1, qh),
|
||||||
KEY_DOWN | KEY_J => self.move_row(1, qh),
|
KEY_DOWN => self.move_sel(1, qh),
|
||||||
KEY_UP | KEY_K => self.move_row(-1, qh),
|
KEY_UP => self.move_sel(-1, qh),
|
||||||
KEY_HOME => self.select(0, qh),
|
KEY_HOME => self.select(0, qh),
|
||||||
KEY_END => self.select(self.tiles.len().saturating_sub(1), qh),
|
KEY_END => self.select(self.tiles.len().saturating_sub(1), qh),
|
||||||
KEY_PGUP => self.move_row(-self.layout.visible_rows, qh),
|
KEY_PGUP => self.move_row(-self.layout.visible_rows, qh),
|
||||||
@@ -396,10 +382,10 @@ impl App {
|
|||||||
// Re-run the navigation action without re-arming the delay.
|
// Re-run the navigation action without re-arming the delay.
|
||||||
match code {
|
match code {
|
||||||
KEY_TAB if self.shift => self.move_sel(-1, qh),
|
KEY_TAB if self.shift => self.move_sel(-1, qh),
|
||||||
KEY_TAB | KEY_RIGHT | KEY_L => self.move_sel(1, qh),
|
KEY_TAB | KEY_RIGHT => self.move_sel(1, qh),
|
||||||
KEY_LEFT | KEY_H => self.move_sel(-1, qh),
|
KEY_LEFT => self.move_sel(-1, qh),
|
||||||
KEY_DOWN | KEY_J => self.move_row(1, qh),
|
KEY_DOWN => self.move_sel(1, qh),
|
||||||
KEY_UP | KEY_K => self.move_row(-1, qh),
|
KEY_UP => self.move_sel(-1, qh),
|
||||||
KEY_HOME => self.select(0, qh),
|
KEY_HOME => self.select(0, qh),
|
||||||
KEY_END => self.select(self.tiles.len().saturating_sub(1), qh),
|
KEY_END => self.select(self.tiles.len().saturating_sub(1), qh),
|
||||||
KEY_PGUP => self.move_row(-self.layout.visible_rows, qh),
|
KEY_PGUP => self.move_row(-self.layout.visible_rows, qh),
|
||||||
@@ -415,7 +401,10 @@ impl App {
|
|||||||
.map(|chunk| u32::from_ne_bytes(chunk.try_into().unwrap()))
|
.map(|chunk| u32::from_ne_bytes(chunk.try_into().unwrap()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if held_keys.iter().any(|&k| k == KEY_LEFTSHIFT || k == KEY_RIGHTSHIFT) {
|
if held_keys
|
||||||
|
.iter()
|
||||||
|
.any(|&k| k == KEY_LEFTSHIFT || k == KEY_RIGHTSHIFT)
|
||||||
|
{
|
||||||
self.shift = true;
|
self.shift = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user