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.
|
/// the overlay maps there rather than wherever the compositor would put it.
|
||||||
pub scale: i32,
|
pub scale: i32,
|
||||||
pub output: String,
|
pub output: String,
|
||||||
|
#[allow(dead_code)]
|
||||||
pub alt_tab: AltTabMode,
|
pub alt_tab: AltTabMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,10 +124,7 @@ pub struct App {
|
|||||||
pub(crate) seat: Option<WlSeat>,
|
pub(crate) seat: Option<WlSeat>,
|
||||||
pub(crate) inhibit_mgr: Option<ZwpKeyboardShortcutsInhibitManagerV1>,
|
pub(crate) inhibit_mgr: Option<ZwpKeyboardShortcutsInhibitManagerV1>,
|
||||||
pub(crate) inhibitor: Option<ZwpKeyboardShortcutsInhibitorV1>,
|
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) latched_modifiers: std::collections::BTreeSet<u32>,
|
||||||
pub(crate) initial_stepped: bool,
|
|
||||||
|
|
||||||
/// The navigation key currently held down, and when the next repeat fires.
|
/// The navigation key currently held down, and when the next repeat fires.
|
||||||
pub(crate) repeat_key: Option<u32>,
|
pub(crate) repeat_key: Option<u32>,
|
||||||
@@ -188,15 +186,9 @@ impl App {
|
|||||||
fps,
|
fps,
|
||||||
scale,
|
scale,
|
||||||
output,
|
output,
|
||||||
alt_tab,
|
..
|
||||||
} = settings;
|
} = settings;
|
||||||
let focused_idx = targets.iter().position(|t| t.focused).unwrap_or(0);
|
let sel = if targets.len() > 1 { 1 } else { 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
|
// Bind everything up front so a compositor missing a protocol fails
|
||||||
// here, with a name, rather than halfway through a capture.
|
// here, with a name, rather than halfway through a capture.
|
||||||
let mut app = Self {
|
let mut app = Self {
|
||||||
@@ -237,10 +229,7 @@ impl App {
|
|||||||
seat: None,
|
seat: None,
|
||||||
inhibit_mgr: None,
|
inhibit_mgr: None,
|
||||||
inhibitor: None,
|
inhibitor: None,
|
||||||
alt_tab,
|
|
||||||
is_alt_tab,
|
|
||||||
latched_modifiers: std::collections::BTreeSet::new(),
|
latched_modifiers: std::collections::BTreeSet::new(),
|
||||||
initial_stepped: alt_tab == AltTabMode::Yes,
|
|
||||||
repeat_key: None,
|
repeat_key: None,
|
||||||
repeat_next: None,
|
repeat_next: None,
|
||||||
repeat_delay_ms: 600,
|
repeat_delay_ms: 600,
|
||||||
|
|||||||
+13
-3
@@ -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
|
// 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.
|
// cycles focus off and on in a single batch as the pointer crosses us.
|
||||||
loop {
|
loop {
|
||||||
|
if app.finished() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
// When a navigation key is held, we need to fire repeat events on a
|
// 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
|
// timer rather than blocking indefinitely. Use a timed poll so we
|
||||||
// wake up when the next repeat is due without burning the CPU.
|
// 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)?;
|
queue.dispatch_pending(&mut app)?;
|
||||||
if !app.finished() {
|
if !app.finished() {
|
||||||
conn.flush()?;
|
conn.flush()?;
|
||||||
let Some(guard) = conn.prepare_read() else { continue };
|
if let Some(guard) = conn.prepare_read() {
|
||||||
let fd = guard.connection_fd();
|
let fd = guard.connection_fd();
|
||||||
let mut fds = [PollFd::new(&fd, PollFlags::IN)];
|
let mut fds = [PollFd::new(&fd, PollFlags::IN)];
|
||||||
let timeout = Timespec {
|
let timeout = Timespec {
|
||||||
@@ -197,10 +200,16 @@ fn run() -> Result<ExitCode, Box<dyn Error>> {
|
|||||||
tv_nsec: left.subsec_nanos() as _,
|
tv_nsec: left.subsec_nanos() as _,
|
||||||
};
|
};
|
||||||
match rustix::event::poll(&mut fds, Some(&timeout)) {
|
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)),
|
Err(e) => return Err(Box::new(e)),
|
||||||
}
|
}
|
||||||
guard.read()?;
|
}
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -236,6 +245,7 @@ fn run() -> Result<ExitCode, Box<dyn Error>> {
|
|||||||
target::Kind::Window => {
|
target::Kind::Window => {
|
||||||
if let Some(con_id) = target.con_id {
|
if let Some(con_id) = target.con_id {
|
||||||
let _ = sway.run_command(format!("[con_id={con_id}] focus"));
|
let _ = sway.run_command(format!("[con_id={con_id}] focus"));
|
||||||
|
sway::record_focus(con_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
target::Kind::Output => {
|
target::Kind::Output => {
|
||||||
|
|||||||
+12
-31
@@ -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 wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape;
|
||||||
|
|
||||||
use crate::app::{App, Ending};
|
use crate::app::{App, Ending};
|
||||||
use crate::config::AltTabMode;
|
|
||||||
use crate::shm;
|
use crate::shm;
|
||||||
use crate::theme::{Rect, fit_centred};
|
use crate::theme::{Rect, fit_centred};
|
||||||
|
|
||||||
@@ -338,7 +337,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn key(&mut self, code: u32, qh: &QueueHandle<Self>) {
|
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);
|
self.latched_modifiers.insert(code);
|
||||||
}
|
}
|
||||||
// Arm client-side repeat for navigation keys.
|
// Arm client-side repeat for navigation keys.
|
||||||
@@ -380,10 +379,7 @@ impl App {
|
|||||||
self.repeat_key = None;
|
self.repeat_key = None;
|
||||||
self.repeat_next = None;
|
self.repeat_next = None;
|
||||||
}
|
}
|
||||||
if self.is_alt_tab
|
if self.latched_modifiers.remove(&code) && self.latched_modifiers.is_empty() {
|
||||||
&& self.latched_modifiers.remove(&code)
|
|
||||||
&& self.latched_modifiers.is_empty()
|
|
||||||
{
|
|
||||||
if self.ending == Ending::Running {
|
if self.ending == Ending::Running {
|
||||||
self.picked = self.tiles.get(self.sel).map(|t| t.target.clone());
|
self.picked = self.tiles.get(self.sel).map(|t| t.target.clone());
|
||||||
self.ending = Ending::Picked;
|
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;
|
self.focused = true;
|
||||||
let held_keys: Vec<u32> = keys
|
let held_keys: Vec<u32> = keys
|
||||||
.chunks_exact(4)
|
.chunks_exact(4)
|
||||||
@@ -429,41 +425,26 @@ impl App {
|
|||||||
.filter(|&k| is_trigger_modifier(k))
|
.filter(|&k| is_trigger_modifier(k))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if !held_modifiers.is_empty() && self.alt_tab != AltTabMode::No {
|
|
||||||
self.is_alt_tab = true;
|
|
||||||
for &m in &held_modifiers {
|
for &m in &held_modifiers {
|
||||||
self.latched_modifiers.insert(m);
|
self.latched_modifiers.insert(m);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if self.is_alt_tab && !self.initial_stepped {
|
// If no modifier is held on enter, the modifier (and/or Tab) was
|
||||||
self.initial_stepped = true;
|
// released before focus was acquired: commit selection immediately!
|
||||||
if self.alt_tab == AltTabMode::Auto {
|
|
||||||
if self.latched_modifiers.is_empty() {
|
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 {
|
if self.ending == Ending::Running {
|
||||||
self.picked = self.tiles.get(self.sel).map(|t| t.target.clone());
|
self.picked = self.tiles.get(self.sel).map(|t| t.target.clone());
|
||||||
self.ending = Ending::Picked;
|
self.ending = Ending::Picked;
|
||||||
}
|
}
|
||||||
} else {
|
return;
|
||||||
// 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
|
// A modifier is held. If Tab is also held upon enter, arm key-repeat
|
||||||
// currently held, the mod was released before keyboard focus arrived.
|
// immediately so holding Tab cycles through windows.
|
||||||
// Commit the current selection immediately.
|
if held_keys.iter().any(|&k| k == KEY_TAB) {
|
||||||
if self.is_alt_tab && self.latched_modifiers.is_empty() && self.ending == Ending::Running {
|
let delay = std::time::Duration::from_millis(self.repeat_delay_ms as u64);
|
||||||
if self.alt_tab == AltTabMode::Yes
|
self.repeat_key = Some(KEY_TAB);
|
||||||
|| (self.alt_tab == AltTabMode::Auto && self.initial_stepped)
|
self.repeat_next = Some(std::time::Instant::now() + delay);
|
||||||
{
|
|
||||||
self.picked = self.tiles.get(self.sel).map(|t| t.target.clone());
|
|
||||||
self.ending = Ending::Picked;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+66
-24
@@ -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.
|
/// 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> {
|
pub fn windows(conn: &mut Connection, order: Order) -> Result<Vec<Target>, swayipc::Error> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
let tree = conn.get_tree()?;
|
let tree = conn.get_tree()?;
|
||||||
match order {
|
collect_tree(&tree, &mut out);
|
||||||
Order::Mru => {
|
|
||||||
collect_mru(&tree, &mut out);
|
if out.is_empty() {
|
||||||
// Ensure the currently focused window is at index 0
|
return Ok(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record currently focused window into history
|
||||||
if let Some(pos) = out.iter().position(|t| t.focused) {
|
if let Some(pos) = out.iter().position(|t| t.focused) {
|
||||||
if pos > 0 {
|
if let Some(con_id) = out[pos].con_id {
|
||||||
let focused = out.remove(pos);
|
record_focus(con_id);
|
||||||
out.insert(0, focused);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
Ok(out)
|
||||||
}
|
}
|
||||||
@@ -152,6 +209,7 @@ fn collect_target(node: &Node) -> Option<Target> {
|
|||||||
node.app_id.clone().or(class).unwrap_or_default(),
|
node.app_id.clone().or(class).unwrap_or_default(),
|
||||||
node.name.clone().unwrap_or_default(),
|
node.name.clone().unwrap_or_default(),
|
||||||
node.focused,
|
node.focused,
|
||||||
|
node.visible.unwrap_or(true),
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
None
|
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.
|
/// One active display: what the overlay needs to size itself against.
|
||||||
///
|
///
|
||||||
/// The overlay maps on the focused display, so percentages and the buffer scale
|
/// 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,
|
pub title: String,
|
||||||
/// Whether this window was the focused container when sway was queried.
|
/// Whether this window was the focused container when sway was queried.
|
||||||
pub focused: bool,
|
pub focused: bool,
|
||||||
|
/// Whether this window is currently visible on screen.
|
||||||
|
pub visible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Target {
|
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 {
|
Self {
|
||||||
kind: Kind::Window,
|
kind: Kind::Window,
|
||||||
id: con_id.to_string(),
|
id: con_id.to_string(),
|
||||||
@@ -69,6 +78,7 @@ impl Target {
|
|||||||
app,
|
app,
|
||||||
title,
|
title,
|
||||||
focused,
|
focused,
|
||||||
|
visible,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +93,7 @@ impl Target {
|
|||||||
app: "display".to_string(),
|
app: "display".to_string(),
|
||||||
title: name,
|
title: name,
|
||||||
focused: false,
|
focused: false,
|
||||||
|
visible: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +209,14 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn win() -> Target {
|
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]
|
#[test]
|
||||||
@@ -232,7 +250,7 @@ mod tests {
|
|||||||
Some("Monitor: DP-1")
|
Some("Monitor: DP-1")
|
||||||
);
|
);
|
||||||
// No identifier means the portal cannot be told about this window.
|
// 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);
|
assert_eq!(anon.portal(), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user