fix: alt-tab focus history MRU ordering, repeat handling, and quick release
This commit is contained in:
+3
-14
@@ -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,
|
||||
|
||||
+21
-11
@@ -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,18 +192,24 @@ 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 };
|
||||
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<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 => {
|
||||
|
||||
+18
-37
@@ -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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+68
-26
@@ -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
|
||||
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<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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user