Compare commits
2
Commits
782a21f864
...
66c2d49ffb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66c2d49ffb | ||
|
|
3ad0b59d8c |
Generated
+1
-1
@@ -605,7 +605,7 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wl-pick"
|
||||
name = "wl-tab"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"cosmic-text",
|
||||
|
||||
@@ -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 crate::capture::{Live, Tile};
|
||||
use crate::config::AltTabMode;
|
||||
use crate::capture::Tile;
|
||||
use crate::overlay;
|
||||
use crate::shm;
|
||||
use crate::target::Target;
|
||||
@@ -55,18 +54,14 @@ use crate::text;
|
||||
use crate::theme::{Layout, Theme};
|
||||
|
||||
/// 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
|
||||
/// `scale` all arrive together and two of them are bare integers.
|
||||
/// live capture to do. `live` is always on for the alt-tab switcher mode.
|
||||
pub struct Settings {
|
||||
pub theme: Theme,
|
||||
pub live: Live,
|
||||
pub fps: u32,
|
||||
/// 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.
|
||||
pub scale: i32,
|
||||
pub output: String,
|
||||
#[allow(dead_code)]
|
||||
pub alt_tab: AltTabMode,
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
@@ -88,7 +83,6 @@ pub struct App {
|
||||
|
||||
pub(crate) theme: Theme,
|
||||
pub(crate) layout: Layout,
|
||||
pub(crate) live: Live,
|
||||
pub(crate) fps: u32,
|
||||
pub(crate) scale: i32,
|
||||
pub(crate) sel: usize,
|
||||
@@ -182,7 +176,6 @@ impl App {
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
let Settings {
|
||||
theme,
|
||||
live,
|
||||
fps,
|
||||
scale,
|
||||
output,
|
||||
@@ -206,7 +199,6 @@ impl App {
|
||||
tiles: targets.into_iter().map(Tile::new).collect(),
|
||||
theme,
|
||||
layout,
|
||||
live,
|
||||
fps,
|
||||
scale,
|
||||
sel,
|
||||
|
||||
+6
-36
@@ -7,12 +7,14 @@
|
||||
//!
|
||||
//! 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
|
||||
//! them again itself.
|
||||
//! them again itself. Live previews are always on.
|
||||
|
||||
use std::error::Error;
|
||||
use std::os::fd::AsFd;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::Target;
|
||||
|
||||
use wayland_client::protocol::{
|
||||
wl_buffer::{self, WlBuffer},
|
||||
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::shm;
|
||||
use crate::target::{Kind, Target};
|
||||
|
||||
/// 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")),
|
||||
}
|
||||
}
|
||||
}
|
||||
use crate::target::Kind;
|
||||
|
||||
/// 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.
|
||||
@@ -202,9 +182,8 @@ impl App {
|
||||
tile.settled = true;
|
||||
continue;
|
||||
}
|
||||
// Only a tile that will be re-captured needs a second buffer, and a
|
||||
// display's is the size of the whole screen.
|
||||
let slots = if self.live == Live::None || tile.target.kind == Kind::Output {
|
||||
// Always 2 buffers for live preview; display gets full-screen buffer.
|
||||
let slots = if tile.target.kind == Kind::Output {
|
||||
1
|
||||
} else {
|
||||
2
|
||||
@@ -301,9 +280,6 @@ impl App {
|
||||
/// Ask for the next frame callback. A commit is needed for the compositor to
|
||||
/// schedule one, and an empty commit is enough.
|
||||
pub fn arm_frame_callback(&mut self, qh: &QueueHandle<Self>) {
|
||||
if self.live == Live::None {
|
||||
return;
|
||||
}
|
||||
if let Some(surface) = self.surface.clone() {
|
||||
surface.frame(qh, ());
|
||||
surface.commit();
|
||||
@@ -314,15 +290,9 @@ impl App {
|
||||
/// the overlay is not being presented.
|
||||
pub fn tick(&mut self, qh: &QueueHandle<Self>) {
|
||||
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 now = Instant::now();
|
||||
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:
|
||||
// refreshing it never settles and costs a whole screen per frame.
|
||||
if self.tiles[i].target.kind == Kind::Output {
|
||||
|
||||
+1
-15
@@ -4,8 +4,7 @@ use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::app::Settings;
|
||||
use crate::capture::Live;
|
||||
use crate::config::{AltTabMode, Config, Length};
|
||||
use crate::config::{Config, Length};
|
||||
use crate::sway::{Display, Order};
|
||||
use crate::target::Format;
|
||||
use crate::theme::Theme;
|
||||
@@ -23,7 +22,6 @@ usage: wl-pick [options]
|
||||
--outputs, --no-outputs include whole displays as tiles [no]
|
||||
--labels, --no-labels a label under each thumbnail [yes]
|
||||
--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]
|
||||
--font FAMILY label font family [the system monospace font]
|
||||
--font-size PX label size in logical px [13.3]
|
||||
@@ -116,11 +114,9 @@ pub struct Args {
|
||||
pub(crate) labels: Option<bool>,
|
||||
pub(crate) font: Option<String>,
|
||||
pub(crate) font_size: Option<f32>,
|
||||
pub(crate) live: Option<Live>,
|
||||
pub(crate) fps: Option<u32>,
|
||||
pub(crate) timeout: Option<Duration>,
|
||||
pub(crate) order: Option<Order>,
|
||||
pub(crate) alt_tab: Option<AltTabMode>,
|
||||
pub(crate) focus: Option<bool>,
|
||||
}
|
||||
|
||||
@@ -204,11 +200,9 @@ impl Args {
|
||||
display: (display.width, display.height),
|
||||
settings: Settings {
|
||||
theme,
|
||||
live: self.live.or(cfg.live).unwrap_or(Live::All),
|
||||
fps: self.fps.or(cfg.fps).unwrap_or(12),
|
||||
scale: display.scale,
|
||||
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")?;
|
||||
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),
|
||||
"--no-focus" => args.focus = Some(false),
|
||||
"--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
|
||||
// is still accepted for whatever it is wired into.
|
||||
"--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" => {
|
||||
let v = it.next().ok_or("--fps needs a number")?;
|
||||
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(&["--no-labels"]).labels, Some(false));
|
||||
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.
|
||||
assert_eq!(args(&[]).outputs, None);
|
||||
assert_eq!(args(&[]).labels, None);
|
||||
|
||||
+1
-40
@@ -14,30 +14,10 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::capture::Live;
|
||||
use crate::sway::Order;
|
||||
use crate::target::Format;
|
||||
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.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Length {
|
||||
@@ -114,12 +94,10 @@ pub struct Config {
|
||||
pub font_size: Option<f32>,
|
||||
pub labels: Option<bool>,
|
||||
pub outputs: Option<bool>,
|
||||
pub live: Option<Live>,
|
||||
pub fps: Option<u32>,
|
||||
pub format: Option<Format>,
|
||||
pub timeout: Option<Duration>,
|
||||
pub order: Option<Order>,
|
||||
pub alt_tab: Option<AltTabMode>,
|
||||
pub focus: Option<bool>,
|
||||
}
|
||||
|
||||
@@ -171,7 +149,7 @@ impl Config {
|
||||
"font-size" => self.font_size = Some(number(value)?),
|
||||
"labels" => self.labels = Some(boolean(value)?),
|
||||
"outputs" => self.outputs = Some(boolean(value)?),
|
||||
"live" => self.live = Some(Live::parse(value)?),
|
||||
|
||||
"fps" => self.fps = Some(number(value)?),
|
||||
"format" => self.format = Some(Format::parse(value)?),
|
||||
// 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));
|
||||
}
|
||||
"order" => self.order = Some(Order::parse(value)?),
|
||||
"alt-tab" => self.alt_tab = Some(AltTabMode::parse(value)?),
|
||||
"focus" => self.focus = Some(boolean(value)?),
|
||||
other => return Err(format!("unknown setting {other:?}")),
|
||||
}
|
||||
@@ -264,12 +241,10 @@ max-width = 70ppt
|
||||
max-columns = 4
|
||||
max-rows = 3
|
||||
|
||||
live = current
|
||||
fps = 30
|
||||
labels = no
|
||||
timeout = 0
|
||||
order = mru
|
||||
alt-tab = yes
|
||||
",
|
||||
)
|
||||
.expect("should parse");
|
||||
@@ -283,8 +258,6 @@ alt-tab = yes
|
||||
assert_eq!(cfg.labels, Some(false));
|
||||
assert_eq!(cfg.timeout, None, "zero means no timeout");
|
||||
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.
|
||||
assert_eq!(cfg.foreground, None);
|
||||
assert_eq!(cfg.max_height, None);
|
||||
@@ -324,16 +297,4 @@ alt-tab = yes
|
||||
unsafe { std::env::set_var("XDG_CONFIG_HOME", "/nonexistent") };
|
||||
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_ENTER: u32 = 28;
|
||||
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_RIGHTSHIFT: u32 = 54;
|
||||
const KEY_LEFTALT: u32 = 56;
|
||||
@@ -59,12 +54,7 @@ const KEY_RIGHTMETA: u32 = 126;
|
||||
fn is_trigger_modifier(code: u32) -> bool {
|
||||
matches!(
|
||||
code,
|
||||
KEY_LEFTALT
|
||||
| KEY_RIGHTALT
|
||||
| KEY_LEFTMETA
|
||||
| KEY_RIGHTMETA
|
||||
| KEY_LEFTCTRL
|
||||
| KEY_RIGHTCTRL
|
||||
KEY_LEFTALT | KEY_RIGHTALT | KEY_LEFTMETA | KEY_RIGHTMETA | KEY_LEFTCTRL | KEY_RIGHTCTRL
|
||||
)
|
||||
}
|
||||
|
||||
@@ -81,10 +71,6 @@ fn is_repeatable_key(code: u32) -> bool {
|
||||
| KEY_END
|
||||
| KEY_PGUP
|
||||
| KEY_PGDN
|
||||
| KEY_H
|
||||
| KEY_J
|
||||
| KEY_K
|
||||
| KEY_L
|
||||
)
|
||||
}
|
||||
|
||||
@@ -358,10 +344,10 @@ impl App {
|
||||
self.ending = Ending::Picked;
|
||||
}
|
||||
KEY_TAB if self.shift => self.move_sel(-1, qh),
|
||||
KEY_TAB | KEY_RIGHT | KEY_L => self.move_sel(1, qh),
|
||||
KEY_LEFT | KEY_H => self.move_sel(-1, qh),
|
||||
KEY_DOWN | KEY_J => self.move_row(1, qh),
|
||||
KEY_UP | KEY_K => self.move_row(-1, qh),
|
||||
KEY_TAB | KEY_RIGHT => self.move_sel(1, qh),
|
||||
KEY_LEFT => self.move_sel(-1, qh),
|
||||
KEY_DOWN => self.move_sel(1, qh),
|
||||
KEY_UP => self.move_sel(-1, qh),
|
||||
KEY_HOME => self.select(0, qh),
|
||||
KEY_END => self.select(self.tiles.len().saturating_sub(1), 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.
|
||||
match code {
|
||||
KEY_TAB if self.shift => self.move_sel(-1, qh),
|
||||
KEY_TAB | KEY_RIGHT | KEY_L => self.move_sel(1, qh),
|
||||
KEY_LEFT | KEY_H => self.move_sel(-1, qh),
|
||||
KEY_DOWN | KEY_J => self.move_row(1, qh),
|
||||
KEY_UP | KEY_K => self.move_row(-1, qh),
|
||||
KEY_TAB | KEY_RIGHT => self.move_sel(1, qh),
|
||||
KEY_LEFT => self.move_sel(-1, qh),
|
||||
KEY_DOWN => self.move_sel(1, qh),
|
||||
KEY_UP => self.move_sel(-1, qh),
|
||||
KEY_HOME => self.select(0, qh),
|
||||
KEY_END => self.select(self.tiles.len().saturating_sub(1), 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()))
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user