This commit is contained in:
2026-09-09 23:40:21 +02:00
parent 3ad0b59d8c
commit 66c2d49ffb
6 changed files with 23 additions and 404 deletions
+2 -10
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}