diff --git a/src/app.rs b/src/app.rs index 837989e..3759c5d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -65,6 +65,7 @@ pub struct Settings { /// 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, } @@ -123,10 +124,7 @@ pub struct App { 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, /// The navigation key currently held down, and when the next repeat fires. pub(crate) repeat_key: Option, @@ -188,15 +186,9 @@ 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; + let sel = if targets.len() > 1 { 1 } else { 0 }; // 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 { @@ -237,10 +229,7 @@ impl App { seat: None, inhibit_mgr: None, inhibitor: None, - alt_tab, - is_alt_tab, latched_modifiers: std::collections::BTreeSet::new(), - initial_stepped: alt_tab == AltTabMode::Yes, repeat_key: None, repeat_next: None, repeat_delay_ms: 600, diff --git a/src/main.rs b/src/main.rs index 0770f82..ae8ec6c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -174,6 +174,9 @@ fn run() -> Result> { // A leave only counts once it has failed to come back, because sway also // cycles focus off and on in a single batch as the pointer crosses us. loop { + if app.finished() { + break; + } // When a navigation key is held, we need to fire repeat events on a // timer rather than blocking indefinitely. Use a timed poll so we // wake up when the next repeat is due without burning the CPU. @@ -189,18 +192,24 @@ fn run() -> Result> { queue.dispatch_pending(&mut app)?; if !app.finished() { conn.flush()?; - let Some(guard) = conn.prepare_read() else { continue }; - let fd = guard.connection_fd(); - let mut fds = [PollFd::new(&fd, PollFlags::IN)]; - let timeout = Timespec { - tv_sec: left.as_secs() as _, - tv_nsec: left.subsec_nanos() as _, - }; - match rustix::event::poll(&mut fds, Some(&timeout)) { - Ok(_) | Err(rustix::io::Errno::INTR) => {} - Err(e) => return Err(Box::new(e)), + if let Some(guard) = conn.prepare_read() { + let fd = guard.connection_fd(); + let mut fds = [PollFd::new(&fd, PollFlags::IN)]; + let timeout = Timespec { + tv_sec: left.as_secs() as _, + tv_nsec: left.subsec_nanos() as _, + }; + match rustix::event::poll(&mut fds, Some(&timeout)) { + Ok(0) => { + // Timeout expired: do NOT call guard.read() because + // nothing is pending on the socket. Dropping guard cancels read. + } + Ok(_) | Err(rustix::io::Errno::INTR) => { + let _ = guard.read(); + } + Err(e) => return Err(Box::new(e)), + } } - guard.read()?; } continue; } @@ -236,6 +245,7 @@ fn run() -> Result> { target::Kind::Window => { if let Some(con_id) = target.con_id { let _ = sway.run_command(format!("[con_id={con_id}] focus")); + sway::record_focus(con_id); } } target::Kind::Output => { diff --git a/src/overlay.rs b/src/overlay.rs index fa00d27..43d4891 100644 --- a/src/overlay.rs +++ b/src/overlay.rs @@ -24,7 +24,6 @@ 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}; @@ -338,7 +337,7 @@ impl App { } fn key(&mut self, code: u32, qh: &QueueHandle) { - if self.is_alt_tab && is_trigger_modifier(code) { + if is_trigger_modifier(code) { self.latched_modifiers.insert(code); } // Arm client-side repeat for navigation keys. @@ -380,10 +379,7 @@ impl App { self.repeat_key = None; self.repeat_next = None; } - if self.is_alt_tab - && self.latched_modifiers.remove(&code) - && self.latched_modifiers.is_empty() - { + if 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; @@ -412,7 +408,7 @@ impl App { } } - fn keyboard_enter(&mut self, keys: Vec, qh: &QueueHandle) { + fn keyboard_enter(&mut self, keys: Vec, _qh: &QueueHandle) { self.focused = true; let held_keys: Vec = keys .chunks_exact(4) @@ -429,41 +425,26 @@ impl App { .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); - } + 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::Auto { - if self.latched_modifiers.is_empty() { - // Auto mode: no modifier held on enter means the mod was - // released before focus arrived — commit immediately. - if self.ending == Ending::Running { - self.picked = self.tiles.get(self.sel).map(|t| t.target.clone()); - self.ending = Ending::Picked; - } - } else { - // Modifier is held: step selection now. - let step = if self.shift { -1 } else { 1 }; - self.move_sel(step, qh); - } - } - } - - // For both Yes and Auto: if we are in alt-tab mode but no modifier is - // currently held, the mod was released before keyboard focus arrived. - // Commit the current selection immediately. - if self.is_alt_tab && self.latched_modifiers.is_empty() && self.ending == Ending::Running { - if self.alt_tab == AltTabMode::Yes - || (self.alt_tab == AltTabMode::Auto && self.initial_stepped) - { + // If no modifier is held on enter, the modifier (and/or Tab) was + // released before focus was acquired: commit selection immediately! + if 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; } + return; + } + + // A modifier is held. If Tab is also held upon enter, arm key-repeat + // immediately so holding Tab cycles through windows. + if held_keys.iter().any(|&k| k == KEY_TAB) { + let delay = std::time::Duration::from_millis(self.repeat_delay_ms as u64); + self.repeat_key = Some(KEY_TAB); + self.repeat_next = Some(std::time::Instant::now() + delay); } } } diff --git a/src/sway.rs b/src/sway.rs index 61cf238..1b65bf2 100644 --- a/src/sway.rs +++ b/src/sway.rs @@ -119,22 +119,79 @@ impl Order { } } +fn history_path() -> PathBuf { + std::env::var_os("XDG_RUNTIME_DIR") + .map(PathBuf::from) + .unwrap_or_else(std::env::temp_dir) + .join("wl-pick-history") +} + +pub fn record_focus(con_id: i64) { + let path = history_path(); + let mut ids: Vec = std::fs::read_to_string(&path) + .ok() + .map(|s| { + s.lines() + .filter_map(|l| l.trim().parse::().ok()) + .collect() + }) + .unwrap_or_default(); + ids.retain(|&id| id != con_id); + ids.insert(0, con_id); + ids.truncate(50); + let content = ids + .iter() + .map(|id| id.to_string()) + .collect::>() + .join("\n"); + let _ = std::fs::write(&path, content); +} + +pub fn read_focus_history() -> Vec { + std::fs::read_to_string(history_path()) + .ok() + .map(|s| { + s.lines() + .filter_map(|l| l.trim().parse::().ok()) + .collect() + }) + .unwrap_or_default() +} + /// 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(); 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); + collect_tree(&tree, &mut out); + + if out.is_empty() { + return Ok(out); + } + + // Record currently focused window into history + if let Some(pos) = out.iter().position(|t| t.focused) { + if let Some(con_id) = out[pos].con_id { + record_focus(con_id); + } + } + + if order == Order::Mru { + let history = read_focus_history(); + out.sort_by_key(|t| { + if t.focused { + return (0, 0, 0); + } + if let Some(con_id) = t.con_id { + if let Some(idx) = history.iter().position(|&id| id == con_id) { + return (1, idx, 0); } } - } - Order::Tree => collect_tree(&tree, &mut out), + if t.visible { + (2, 0, 0) + } else { + (3, 0, 0) + } + }); } Ok(out) } @@ -152,6 +209,7 @@ fn collect_target(node: &Node) -> Option { node.app_id.clone().or(class).unwrap_or_default(), node.name.clone().unwrap_or_default(), node.focused, + node.visible.unwrap_or(true), )) } else { None @@ -167,22 +225,6 @@ fn collect_tree(node: &Node, out: &mut Vec) { } } -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); - } -} - /// One active display: what the overlay needs to size itself against. /// /// The overlay maps on the focused display, so percentages and the buffer scale diff --git a/src/target.rs b/src/target.rs index ed1bcee..23d6d15 100644 --- a/src/target.rs +++ b/src/target.rs @@ -57,10 +57,19 @@ pub struct Target { pub title: String, /// Whether this window was the focused container when sway was queried. pub focused: bool, + /// Whether this window is currently visible on screen. + pub visible: bool, } impl Target { - pub fn window(con_id: i64, ft_id: String, app: String, title: String, focused: bool) -> Self { + pub fn window( + con_id: i64, + ft_id: String, + app: String, + title: String, + focused: bool, + visible: bool, + ) -> Self { Self { kind: Kind::Window, id: con_id.to_string(), @@ -69,6 +78,7 @@ impl Target { app, title, focused, + visible, } } @@ -83,6 +93,7 @@ impl Target { app: "display".to_string(), title: name, focused: false, + visible: true, } } @@ -198,7 +209,14 @@ mod tests { use super::*; fn win() -> Target { - Target::window(42, "abc123".into(), "kitty".into(), "zsh\tin\na tab".into(), false) + Target::window( + 42, + "abc123".into(), + "kitty".into(), + "zsh\tin\na tab".into(), + false, + true, + ) } #[test] @@ -232,7 +250,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(), false); + let anon = Target::window(7, String::new(), "x".into(), "y".into(), false, false); assert_eq!(anon.portal(), None); }