This commit is contained in:
2026-09-09 11:30:58 +02:00
parent 78631065e2
commit 1367b03b97
8 changed files with 315 additions and 35 deletions
+10 -3
View File
@@ -37,6 +37,7 @@ is the one thing the rofi version had that this doesn't — see the roadmap.
```
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]
```
@@ -46,9 +47,13 @@ wl-pick [--format tsv|json|portal] [--live all|current|none] [--fps N]
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
on). Both directions exist so either can override the config file
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`)
@@ -109,7 +114,7 @@ grid still works.
| `→` `←` / `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` | pick the selection |
| `Enter` / release modifier | pick the selection (release Alt/Super in Alt+Tab mode) |
| `Escape` / `q` | cancel |
| click | pick that tile |
| scroll | next / previous tile |
@@ -171,7 +176,9 @@ max-rows = 4
font = monospace
font-size = 13.3
labels = yes
outputs = 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
+43 -2
View File
@@ -37,12 +37,17 @@ use wayland_protocols::wp::cursor_shape::v1::client::{
wp_cursor_shape_device_v1::WpCursorShapeDeviceV1,
wp_cursor_shape_manager_v1::WpCursorShapeManagerV1,
};
use wayland_protocols::wp::keyboard_shortcuts_inhibit::zv1::client::{
zwp_keyboard_shortcuts_inhibit_manager_v1::ZwpKeyboardShortcutsInhibitManagerV1,
zwp_keyboard_shortcuts_inhibitor_v1::ZwpKeyboardShortcutsInhibitorV1,
};
use wayland_protocols::wp::viewporter::client::{
wp_viewport::WpViewport, wp_viewporter::WpViewporter,
};
use wayland_protocols_wlr::layer_shell::v1::client::zwlr_layer_shell_v1::ZwlrLayerShellV1;
use crate::capture::{Live, Tile};
use crate::config::AltTabMode;
use crate::overlay;
use crate::shm;
use crate::target::Target;
@@ -60,6 +65,7 @@ pub struct Settings {
/// the overlay maps there rather than wherever the compositor would put it.
pub scale: i32,
pub output: String,
pub alt_tab: AltTabMode,
}
pub struct App {
@@ -113,6 +119,14 @@ pub struct App {
pub(crate) focused: bool,
pub(crate) picked: Option<Target>,
pub(crate) stats: Stats,
pub(crate) seat: Option<WlSeat>,
pub(crate) inhibit_mgr: Option<ZwpKeyboardShortcutsInhibitManagerV1>,
pub(crate) inhibitor: Option<ZwpKeyboardShortcutsInhibitorV1>,
pub(crate) alt_tab: AltTabMode,
pub(crate) is_alt_tab: bool,
pub(crate) latched_modifiers: std::collections::BTreeSet<u32>,
pub(crate) initial_stepped: bool,
}
/// Counters worth reporting with --verbose. Live capture is easy to get subtly
@@ -166,7 +180,15 @@ impl App {
fps,
scale,
output,
alt_tab,
} = settings;
let focused_idx = targets.iter().position(|t| t.focused).unwrap_or(0);
let sel = if alt_tab == AltTabMode::Yes && targets.len() > 1 {
(focused_idx + 1) % targets.len()
} else {
focused_idx
};
let is_alt_tab = alt_tab == AltTabMode::Yes;
// Bind everything up front so a compositor missing a protocol fails
// here, with a name, rather than halfway through a capture.
let mut app = Self {
@@ -187,7 +209,7 @@ impl App {
live,
fps,
scale,
sel: 0,
sel,
scroll: 0,
shift: false,
hover: None,
@@ -204,6 +226,13 @@ impl App {
focused: false,
picked: None,
stats: Stats::default(),
seat: None,
inhibit_mgr: None,
inhibitor: None,
alt_tab,
is_alt_tab,
latched_modifiers: std::collections::BTreeSet::new(),
initial_stepped: alt_tab == AltTabMode::Yes,
};
let _: ExtForeignToplevelListV1 = globals.bind(qh, 1..=1, ())?;
// One wl_output per display, bound at v4 so it tells us its name.
@@ -216,7 +245,9 @@ impl App {
}
}
}
let _: WlSeat = globals.bind(qh, 1..=7, ())?;
let seat: WlSeat = globals.bind(qh, 1..=7, ())?;
app.seat = Some(seat);
app.inhibit_mgr = globals.bind(qh, 1..=1, ()).ok();
Ok(app)
}
@@ -387,3 +418,13 @@ delegate_noop!(App: ignore WlSurface);
delegate_noop!(App: WpCursorShapeManagerV1);
delegate_noop!(App: WpCursorShapeDeviceV1);
delegate_noop!(App: ignore WlBuffer);
delegate_noop!(App: ZwpKeyboardShortcutsInhibitManagerV1);
delegate_noop!(App: ignore ZwpKeyboardShortcutsInhibitorV1);
impl Drop for App {
fn drop(&mut self) {
if let Some(inhibitor) = self.inhibitor.take() {
inhibitor.destroy();
}
}
}
+41 -8
View File
@@ -5,8 +5,8 @@ use std::time::Duration;
use crate::app::Settings;
use crate::capture::Live;
use crate::config::{Config, Length};
use crate::sway::Display;
use crate::config::{AltTabMode, Config, Length};
use crate::sway::{Display, Order};
use crate::target::Format;
use crate::theme::Theme;
@@ -20,8 +20,11 @@ usage: wl-pick [options]
--live all|current|none which tiles keep updating live [all]
(displays are always a single snapshot)
--fps N cap on live updates per tile per second [12]
--outputs, --no-outputs include whole displays as tiles [yes]
--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]
--timeout SECS exit anyway after SECS, in case the keyboard
@@ -30,7 +33,8 @@ usage: wl-pick [options]
-h, --help this
keys: arrows, hjkl or Tab/Shift+Tab move; PgUp/PgDn and Home/End jump;
Enter picks; Escape or q cancels
Enter picks; Escape or q cancels; in Alt+Tab mode, releasing
the modifier (Alt/Super) picks the selection.
mouse: click a tile to pick it, scroll to move. Hovering does not move the
selection, and a click outside a tile does nothing.
@@ -60,7 +64,10 @@ config:
font = monospace # also --font
font-size = 13.3
labels = yes
outputs = yes # include whole displays as tiles
outputs = no # include whole displays as tiles
order = mru # mru or tree
alt-tab = auto # auto, yes or no
focus = no # focus picked target in sway directly
live = all
fps = 12
format = tsv
@@ -89,6 +96,8 @@ formats:
focusing on sway:
wl-pick --focus or:
IFS=$'\\t' read -r type id toplevel app title < <(wl-pick) &&
case $type in
window) swaymsg \"[con_id=$id] focus\" ;;
@@ -110,6 +119,9 @@ pub struct Args {
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>,
}
/// Every setting resolved, with sizes turned into pixels for the display the
@@ -119,6 +131,8 @@ pub struct Options {
pub format: Format,
pub outputs: bool,
pub timeout: Option<Duration>,
pub order: Order,
pub focus: bool,
/// The logical size of the display the grid will be laid out for.
pub display: (i32, i32),
pub settings: Settings,
@@ -183,8 +197,10 @@ impl Args {
Options {
verbose: self.verbose,
format: self.format.or(cfg.format).unwrap_or(Format::Tsv),
outputs: self.outputs.or(cfg.outputs).unwrap_or(true),
outputs: self.outputs.or(cfg.outputs).unwrap_or(false),
timeout: self.timeout.or(cfg.timeout),
order: self.order.or(cfg.order).unwrap_or(Order::Mru),
focus: self.focus.or(cfg.focus).unwrap_or(false),
display: (display.width, display.height),
settings: Settings {
theme,
@@ -192,6 +208,7 @@ impl Args {
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),
},
}
}
@@ -212,6 +229,14 @@ fn parse(it: impl Iterator<Item = String>) -> Result<Args, String> {
}
"--outputs" => args.outputs = Some(true),
"--no-outputs" => args.outputs = Some(false),
"--order" => {
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" => {
args.config = Some(PathBuf::from(it.next().ok_or("--config needs a path")?))
}
@@ -282,6 +307,8 @@ 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);
@@ -300,8 +327,14 @@ mod tests {
assert!(!off.outputs);
assert!(!off.settings.theme.labels);
// With no flag the file decides, and with no file either, the default.
// With no flag the file decides, and with no file either, the default (outputs: false).
assert!(!args(&[]).resolve(&file(false), &display()).outputs);
assert!(args(&[]).resolve(&Config::default(), &display()).outputs);
assert!(!args(&[]).resolve(&Config::default(), &display()).outputs);
assert!(args(&["--outputs"]).resolve(&Config::default(), &display()).outputs);
assert_eq!(args(&[]).resolve(&Config::default(), &display()).order, Order::Mru);
assert_eq!(
args(&["--order", "tree"]).resolve(&Config::default(), &display()).order,
Order::Tree
);
}
}
+42
View File
@@ -15,9 +15,29 @@ 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 {
@@ -98,6 +118,9 @@ pub struct Config {
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>,
}
impl Config {
@@ -157,6 +180,9 @@ impl Config {
let secs: f64 = number(value)?;
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:?}")),
}
Ok(())
@@ -242,6 +268,8 @@ live = current
fps = 30
labels = no
timeout = 0
order = mru
alt-tab = yes
",
)
.expect("should parse");
@@ -254,6 +282,8 @@ timeout = 0
assert_eq!(cfg.fps, Some(30));
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);
@@ -294,4 +324,16 @@ timeout = 0
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());
}
}
+15 -1
View File
@@ -82,7 +82,7 @@ fn run() -> Result<ExitCode, Box<dyn Error>> {
let displays = sway::displays(&mut sway)?;
let display = sway::focused(&displays).ok_or("sway reports no active display")?;
let opts = args.resolve(&config, display);
let mut targets = sway::windows(&mut sway)?;
let mut targets = sway::windows(&mut sway, opts.order)?;
if opts.outputs {
// Displays go last, after the windows, so window positions are
// stable as windows come and go.
@@ -198,6 +198,20 @@ fn run() -> Result<ExitCode, Box<dyn Error>> {
let Some(target) = app.picked() else {
return Ok(ExitCode::FAILURE); // cancelled: nothing on stdout
};
if opts.focus {
if let Ok(mut sway) = sway::connect() {
match target.kind {
target::Kind::Window => {
if let Some(con_id) = target.con_id {
let _ = sway.run_command(format!("[con_id={con_id}] focus"));
}
}
target::Kind::Output => {
let _ = sway.run_command(format!("focus output {}", target.id));
}
}
}
}
match target.render(opts.format) {
Some(line) => println!("{line}"),
// Only the portal format can fail to name something: it identifies a
+85 -8
View File
@@ -24,6 +24,7 @@ use wayland_protocols_wlr::layer_shell::v1::client::{
use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape;
use crate::app::{App, Ending};
use crate::config::AltTabMode;
use crate::shm;
use crate::theme::{Rect, fit_centred};
@@ -32,23 +33,41 @@ use crate::theme::{Rect, fit_centred};
const KEY_ESC: u32 = 1;
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_ENTER: u32 = 28;
const KEY_LEFTSHIFT: u32 = 42;
const KEY_RIGHTSHIFT: u32 = 54;
const KEY_LEFTALT: u32 = 56;
const KEY_KPENTER: u32 = 96;
const KEY_RIGHTCTRL: u32 = 97;
const KEY_RIGHTALT: u32 = 100;
const KEY_HOME: u32 = 102;
const KEY_UP: u32 = 103;
const KEY_PGUP: u32 = 104;
const KEY_LEFT: u32 = 105;
const KEY_RIGHT: u32 = 106;
const KEY_END: u32 = 107;
const KEY_DOWN: u32 = 108;
const KEY_PGUP: u32 = 104;
const KEY_PGDN: u32 = 109;
const KEY_LEFTMETA: u32 = 125;
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
)
}
/// evdev button code, as wl_pointer reports it.
const BTN_LEFT: u32 = 0x110;
@@ -105,6 +124,9 @@ impl App {
}
pool.destroy();
self.chrome = Some(shm::Chrome::new(&file, pw, ph)?);
if let (Some(mgr), Some(seat)) = (&self.inhibit_mgr, &self.seat) {
self.inhibitor = Some(mgr.inhibit_shortcuts(&surface, seat, qh, ()));
}
self.surface = Some(surface);
Ok(())
}
@@ -296,6 +318,9 @@ impl App {
}
fn key(&mut self, code: u32, qh: &QueueHandle<Self>) {
if self.is_alt_tab && is_trigger_modifier(code) {
self.latched_modifiers.insert(code);
}
match code {
KEY_LEFTSHIFT | KEY_RIGHTSHIFT => self.shift = true,
KEY_ESC | KEY_Q => self.ending = Ending::Cancelled,
@@ -315,6 +340,62 @@ impl App {
_ => {}
}
}
fn key_up(&mut self, code: u32) {
if code == KEY_LEFTSHIFT || code == KEY_RIGHTSHIFT {
self.shift = false;
}
if self.is_alt_tab
&& self.latched_modifiers.remove(&code)
&& self.latched_modifiers.is_empty()
{
if self.ending == Ending::Running {
self.picked = self.tiles.get(self.sel).map(|t| t.target.clone());
self.ending = Ending::Picked;
}
}
}
fn keyboard_enter(&mut self, keys: Vec<u8>, qh: &QueueHandle<Self>) {
self.focused = true;
let held_keys: Vec<u32> = keys
.chunks_exact(4)
.map(|chunk| u32::from_ne_bytes(chunk.try_into().unwrap()))
.collect();
if held_keys.iter().any(|&k| k == KEY_LEFTSHIFT || k == KEY_RIGHTSHIFT) {
self.shift = true;
}
let held_modifiers: Vec<u32> = held_keys
.iter()
.copied()
.filter(|&k| is_trigger_modifier(k))
.collect();
if !held_modifiers.is_empty() && self.alt_tab != AltTabMode::No {
self.is_alt_tab = true;
for &m in &held_modifiers {
self.latched_modifiers.insert(m);
}
}
if self.is_alt_tab && !self.initial_stepped {
self.initial_stepped = true;
if self.alt_tab == AltTabMode::Yes && self.latched_modifiers.is_empty() {
// In explicit alt-tab mode, if no modifier was held on enter,
// the modifier was released before focus was acquired: commit immediately!
if self.ending == Ending::Running {
self.picked = self.tiles.get(self.sel).map(|t| t.target.clone());
self.ending = Ending::Picked;
}
} else if self.alt_tab == AltTabMode::Auto {
// In auto mode, step selection now that we know a modifier was held:
let step = if self.shift { -1 } else { 1 };
self.move_sel(step, qh);
}
}
}
}
// --- event plumbing -------------------------------------------------------
@@ -379,18 +460,14 @@ impl Dispatch<WlKeyboard, ()> for App {
match event {
wl_keyboard::Event::Key { key, state, .. } => match state {
WEnum::Value(wl_keyboard::KeyState::Pressed) => app.key(key, qh),
WEnum::Value(wl_keyboard::KeyState::Released)
if key == KEY_LEFTSHIFT || key == KEY_RIGHTSHIFT =>
{
app.shift = false
}
WEnum::Value(wl_keyboard::KeyState::Released) => app.key_up(key),
_ => {}
},
// Focus is only tracked here. sway sends leave immediately
// followed by enter on the same surface when the pointer crosses
// it, so whether the grab is really gone is decided by the main
// loop, once the event batch has been dispatched.
wl_keyboard::Event::Enter { .. } => app.focused = true,
wl_keyboard::Event::Enter { keys, .. } => app.keyboard_enter(keys, qh),
wl_keyboard::Event::Leave { .. } => app.focused = false,
_ => {}
}
+72 -10
View File
@@ -99,32 +99,87 @@ fn live_sockets() -> Vec<PathBuf> {
found
}
/// Every view in the tree, in tree order (the same traversal the jq filter did,
/// so the grid keeps the ordering the muscle memory expects).
pub fn windows(conn: &mut Connection) -> Result<Vec<Target>, swayipc::Error> {
/// How to order windows in the grid.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum Order {
/// Most-Recently-Used (MRU) focus order: current window first, then previous, etc.
#[default]
Mru,
/// Traversal of sway's layout tree (workspace by workspace).
Tree,
}
impl Order {
pub fn parse(s: &str) -> Result<Self, String> {
match s.trim() {
"mru" => Ok(Order::Mru),
"tree" => Ok(Order::Tree),
other => Err(format!("{other:?} is not mru or tree")),
}
}
}
/// Views in the tree, either in MRU focus order or tree layout order.
pub fn windows(conn: &mut Connection, order: Order) -> Result<Vec<Target>, swayipc::Error> {
let mut out = Vec::new();
collect(&conn.get_tree()?, &mut out);
let tree = conn.get_tree()?;
match order {
Order::Mru => {
collect_mru(&tree, &mut out);
// Ensure the currently focused window is at index 0
if let Some(pos) = out.iter().position(|t| t.focused) {
if pos > 0 {
let focused = out.remove(pos);
out.insert(0, focused);
}
}
}
Order::Tree => collect_tree(&tree, &mut out),
}
Ok(out)
}
fn collect(node: &Node, out: &mut Vec<Target>) {
fn collect_target(node: &Node) -> Option<Target> {
let is_con = matches!(node.node_type, NodeType::Con | NodeType::FloatingCon);
let class = node
.window_properties
.as_ref()
.and_then(|p| p.class.clone());
if is_con && (node.app_id.is_some() || class.is_some()) {
// A view with no identifier can't be captured, but it still belongs in
// the list: it gets a tile with no thumbnail.
out.push(Target::window(
Some(Target::window(
node.id,
node.foreign_toplevel_identifier.clone().unwrap_or_default(),
node.app_id.clone().or(class).unwrap_or_default(),
node.name.clone().unwrap_or_default(),
));
node.focused,
))
} else {
None
}
}
fn collect_tree(node: &Node, out: &mut Vec<Target>) {
if let Some(target) = collect_target(node) {
out.push(target);
}
for child in node.nodes.iter().chain(node.floating_nodes.iter()) {
collect(child, out);
collect_tree(child, out);
}
}
fn collect_mru(node: &Node, out: &mut Vec<Target>) {
if let Some(target) = collect_target(node) {
out.push(target);
}
let mut children: Vec<&Node> = node.nodes.iter().chain(node.floating_nodes.iter()).collect();
children.sort_by_key(|child| {
node.focus
.iter()
.position(|&id| id == child.id)
.unwrap_or(usize::MAX)
});
for child in children {
collect_mru(child, out);
}
}
@@ -184,4 +239,11 @@ mod tests {
assert_eq!(socket_pid("sway-ipc.1000..sock"), None);
assert_eq!(socket_pid("sway-ipc.1000.notapid.sock"), None);
}
#[test]
fn order_parses() {
assert_eq!(super::Order::parse("mru"), Ok(super::Order::Mru));
assert_eq!(super::Order::parse("tree"), Ok(super::Order::Tree));
assert!(super::Order::parse("invalid").is_err());
}
}
+7 -3
View File
@@ -55,10 +55,12 @@ pub struct Target {
pub ft_id: String,
pub app: String,
pub title: String,
/// Whether this window was the focused container when sway was queried.
pub focused: bool,
}
impl Target {
pub fn window(con_id: i64, ft_id: String, app: String, title: String) -> Self {
pub fn window(con_id: i64, ft_id: String, app: String, title: String, focused: bool) -> Self {
Self {
kind: Kind::Window,
id: con_id.to_string(),
@@ -66,6 +68,7 @@ impl Target {
ft_id,
app,
title,
focused,
}
}
@@ -79,6 +82,7 @@ impl Target {
ft_id: String::new(),
app: "display".to_string(),
title: name,
focused: false,
}
}
@@ -194,7 +198,7 @@ mod tests {
use super::*;
fn win() -> Target {
Target::window(42, "abc123".into(), "kitty".into(), "zsh\tin\na tab".into())
Target::window(42, "abc123".into(), "kitty".into(), "zsh\tin\na tab".into(), false)
}
#[test]
@@ -228,7 +232,7 @@ mod tests {
Some("Monitor: DP-1")
);
// No identifier means the portal cannot be told about this window.
let anon = Target::window(7, String::new(), "x".into(), "y".into());
let anon = Target::window(7, String::new(), "x".into(), "y".into(), false);
assert_eq!(anon.portal(), None);
}