fix: alt-tab focus history MRU ordering, repeat handling, and quick release

This commit is contained in:
2026-09-09 12:39:10 +02:00
parent 50caed0110
commit 53cebf14f3
5 changed files with 131 additions and 91 deletions
+3 -14
View File
@@ -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<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,
/// The navigation key currently held down, and when the next repeat fires.
pub(crate) repeat_key: Option<u32>,
@@ -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,
+13 -3
View File
@@ -174,6 +174,9 @@ fn run() -> Result<ExitCode, Box<dyn Error>> {
// 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,7 +192,7 @@ fn run() -> Result<ExitCode, Box<dyn Error>> {
queue.dispatch_pending(&mut app)?;
if !app.finished() {
conn.flush()?;
let Some(guard) = conn.prepare_read() else { continue };
if let Some(guard) = conn.prepare_read() {
let fd = guard.connection_fd();
let mut fds = [PollFd::new(&fd, PollFlags::IN)];
let timeout = Timespec {
@@ -197,10 +200,16 @@ fn run() -> Result<ExitCode, Box<dyn Error>> {
tv_nsec: left.subsec_nanos() as _,
};
match rustix::event::poll(&mut fds, Some(&timeout)) {
Ok(_) | Err(rustix::io::Errno::INTR) => {}
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<ExitCode, Box<dyn Error>> {
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 => {
+12 -31
View File
@@ -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<Self>) {
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<u8>, qh: &QueueHandle<Self>) {
fn keyboard_enter(&mut self, keys: Vec<u8>, _qh: &QueueHandle<Self>) {
self.focused = true;
let held_keys: Vec<u32> = 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);
}
}
if self.is_alt_tab && !self.initial_stepped {
self.initial_stepped = true;
if self.alt_tab == AltTabMode::Auto {
// 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() {
// 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);
}
}
return;
}
// 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)
{
self.picked = self.tiles.get(self.sel).map(|t| t.target.clone());
self.ending = Ending::Picked;
}
// 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);
}
}
}
+66 -24
View File
@@ -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<i64> = std::fs::read_to_string(&path)
.ok()
.map(|s| {
s.lines()
.filter_map(|l| l.trim().parse::<i64>().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::<Vec<_>>()
.join("\n");
let _ = std::fs::write(&path, content);
}
pub fn read_focus_history() -> Vec<i64> {
std::fs::read_to_string(history_path())
.ok()
.map(|s| {
s.lines()
.filter_map(|l| l.trim().parse::<i64>().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<Vec<Target>, 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
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 pos > 0 {
let focused = out.remove(pos);
out.insert(0, 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);
}
Order::Tree => collect_tree(&tree, &mut out),
if let Some(con_id) = t.con_id {
if let Some(idx) = history.iter().position(|&id| id == con_id) {
return (1, idx, 0);
}
}
if t.visible {
(2, 0, 0)
} else {
(3, 0, 0)
}
});
}
Ok(out)
}
@@ -152,6 +209,7 @@ fn collect_target(node: &Node) -> Option<Target> {
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<Target>) {
}
}
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);
}
}
/// One active display: what the overlay needs to size itself against.
///
/// The overlay maps on the focused display, so percentages and the buffer scale
+21 -3
View File
@@ -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);
}