diff --git a/README.md b/README.md index 3655fc4..e5edb4c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/app.rs b/src/app.rs index f572296..67f2aca 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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, pub(crate) stats: Stats, + + pub(crate) seat: Option, + pub(crate) inhibit_mgr: Option, + pub(crate) inhibitor: Option, + pub(crate) alt_tab: AltTabMode, + pub(crate) is_alt_tab: bool, + pub(crate) latched_modifiers: std::collections::BTreeSet, + 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(); + } + } +} diff --git a/src/cli.rs b/src/cli.rs index 7b2733a..fececca 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -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, pub(crate) fps: Option, pub(crate) timeout: Option, + pub(crate) order: Option, + pub(crate) alt_tab: Option, + pub(crate) focus: Option, } /// 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, + 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) -> Result { } "--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 + ); } } diff --git a/src/config.rs b/src/config.rs index 48ea06e..f047652 100644 --- a/src/config.rs +++ b/src/config.rs @@ -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 { + 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, pub format: Option, pub timeout: Option, + pub order: Option, + pub alt_tab: Option, + pub focus: Option, } 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()); + } } diff --git a/src/main.rs b/src/main.rs index 317d1f9..d4e74d8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -82,7 +82,7 @@ fn run() -> Result> { 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> { 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 diff --git a/src/overlay.rs b/src/overlay.rs index fab55c9..a160927 100644 --- a/src/overlay.rs +++ b/src/overlay.rs @@ -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) { + 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, qh: &QueueHandle) { + self.focused = true; + let held_keys: Vec = 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 = 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 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, _ => {} } diff --git a/src/sway.rs b/src/sway.rs index ae7857a..61cf238 100644 --- a/src/sway.rs +++ b/src/sway.rs @@ -99,32 +99,87 @@ fn live_sockets() -> Vec { 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, 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 { + 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, 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) { +fn collect_target(node: &Node) -> Option { 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) { + 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) { + 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()); + } } diff --git a/src/target.rs b/src/target.rs index 750418d..ed1bcee 100644 --- a/src/target.rs +++ b/src/target.rs @@ -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); }