This commit is contained in:
2026-09-09 11:44:23 +02:00
parent 1367b03b97
commit 50caed0110
3 changed files with 144 additions and 12 deletions
+12
View File
@@ -127,6 +127,14 @@ pub struct App {
pub(crate) is_alt_tab: bool, 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, pub(crate) initial_stepped: bool,
/// The navigation key currently held down, and when the next repeat fires.
pub(crate) repeat_key: Option<u32>,
pub(crate) repeat_next: Option<std::time::Instant>,
/// Milliseconds before the first repeat fires. From wl_keyboard::RepeatInfo.
pub(crate) repeat_delay_ms: u32,
/// Milliseconds between subsequent repeats. From wl_keyboard::RepeatInfo.
pub(crate) repeat_rate_ms: u32,
} }
/// Counters worth reporting with --verbose. Live capture is easy to get subtly /// Counters worth reporting with --verbose. Live capture is easy to get subtly
@@ -233,6 +241,10 @@ impl App {
is_alt_tab, is_alt_tab,
latched_modifiers: std::collections::BTreeSet::new(), latched_modifiers: std::collections::BTreeSet::new(),
initial_stepped: alt_tab == AltTabMode::Yes, initial_stepped: alt_tab == AltTabMode::Yes,
repeat_key: None,
repeat_next: None,
repeat_delay_ms: 600,
repeat_rate_ms: 25,
}; };
let _: ExtForeignToplevelListV1 = globals.bind(qh, 1..=1, ())?; let _: ExtForeignToplevelListV1 = globals.bind(qh, 1..=1, ())?;
// One wl_output per display, bound at v4 so it tells us its name. // One wl_output per display, bound at v4 so it tells us its name.
+32
View File
@@ -174,7 +174,39 @@ 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 {
// 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.
if let Some(next) = app.repeat_next {
let now = Instant::now();
if now >= next {
let qh = queue.handle();
app.fire_repeat(&qh);
conn.flush()?;
} else {
// Poll for events with a timeout set to when the next repeat fires.
let left = next - now;
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)),
}
guard.read()?;
}
continue;
}
} else {
queue.blocking_dispatch(&mut app)?; queue.blocking_dispatch(&mut app)?;
}
if !app.finished() if !app.finished()
&& !app.focused && !app.focused
&& !pump_for( && !pump_for(
+94 -6
View File
@@ -69,6 +69,26 @@ fn is_trigger_modifier(code: u32) -> bool {
) )
} }
/// Keys that should fire repeatedly while held.
fn is_repeatable_key(code: u32) -> bool {
matches!(
code,
KEY_TAB
| KEY_RIGHT
| KEY_LEFT
| KEY_DOWN
| KEY_UP
| KEY_HOME
| KEY_END
| KEY_PGUP
| KEY_PGDN
| KEY_H
| KEY_J
| KEY_K
| KEY_L
)
}
/// evdev button code, as wl_pointer reports it. /// evdev button code, as wl_pointer reports it.
const BTN_LEFT: u32 = 0x110; const BTN_LEFT: u32 = 0x110;
@@ -321,6 +341,16 @@ impl App {
if self.is_alt_tab && is_trigger_modifier(code) { if self.is_alt_tab && is_trigger_modifier(code) {
self.latched_modifiers.insert(code); self.latched_modifiers.insert(code);
} }
// Arm client-side repeat for navigation keys.
if is_repeatable_key(code) {
let delay = std::time::Duration::from_millis(self.repeat_delay_ms as u64);
self.repeat_key = Some(code);
self.repeat_next = Some(std::time::Instant::now() + delay);
} else {
// Non-repeating key clears any held repeat.
self.repeat_key = None;
self.repeat_next = None;
}
match code { match code {
KEY_LEFTSHIFT | KEY_RIGHTSHIFT => self.shift = true, KEY_LEFTSHIFT | KEY_RIGHTSHIFT => self.shift = true,
KEY_ESC | KEY_Q => self.ending = Ending::Cancelled, KEY_ESC | KEY_Q => self.ending = Ending::Cancelled,
@@ -345,6 +375,11 @@ impl App {
if code == KEY_LEFTSHIFT || code == KEY_RIGHTSHIFT { if code == KEY_LEFTSHIFT || code == KEY_RIGHTSHIFT {
self.shift = false; self.shift = false;
} }
// Clear repeat if this is the key that was held.
if self.repeat_key == Some(code) {
self.repeat_key = None;
self.repeat_next = None;
}
if self.is_alt_tab if self.is_alt_tab
&& self.latched_modifiers.remove(&code) && self.latched_modifiers.remove(&code)
&& self.latched_modifiers.is_empty() && self.latched_modifiers.is_empty()
@@ -356,6 +391,27 @@ impl App {
} }
} }
/// Called by the main loop when the key-repeat timer fires. Fires the
/// currently held navigation action, then arms the next repeat tick.
pub fn fire_repeat(&mut self, qh: &QueueHandle<Self>) {
let Some(code) = self.repeat_key else { return };
let rate = std::time::Duration::from_millis(self.repeat_rate_ms as u64);
self.repeat_next = Some(std::time::Instant::now() + rate);
// Re-run the navigation action without re-arming the delay.
match code {
KEY_TAB if self.shift => self.move_sel(-1, qh),
KEY_TAB | KEY_RIGHT | KEY_L => self.move_sel(1, qh),
KEY_LEFT | KEY_H => self.move_sel(-1, qh),
KEY_DOWN | KEY_J => self.move_row(1, qh),
KEY_UP | KEY_K => self.move_row(-1, qh),
KEY_HOME => self.select(0, qh),
KEY_END => self.select(self.tiles.len().saturating_sub(1), qh),
KEY_PGUP => self.move_row(-self.layout.visible_rows, qh),
KEY_PGDN => self.move_row(self.layout.visible_rows, qh),
_ => {}
}
}
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
@@ -382,20 +438,34 @@ impl App {
if self.is_alt_tab && !self.initial_stepped { if self.is_alt_tab && !self.initial_stepped {
self.initial_stepped = true; self.initial_stepped = true;
if self.alt_tab == AltTabMode::Yes && self.latched_modifiers.is_empty() { if self.alt_tab == AltTabMode::Auto {
// In explicit alt-tab mode, if no modifier was held on enter, if self.latched_modifiers.is_empty() {
// the modifier was released before focus was acquired: commit immediately! // 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 if self.alt_tab == AltTabMode::Auto { } else {
// In auto mode, step selection now that we know a modifier was held: // Modifier is held: step selection now.
let step = if self.shift { -1 } else { 1 }; let step = if self.shift { -1 } else { 1 };
self.move_sel(step, qh); 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)
{
self.picked = self.tiles.get(self.sel).map(|t| t.target.clone());
self.ending = Ending::Picked;
}
}
}
} }
// --- event plumbing ------------------------------------------------------- // --- event plumbing -------------------------------------------------------
@@ -463,12 +533,30 @@ impl Dispatch<WlKeyboard, ()> for App {
WEnum::Value(wl_keyboard::KeyState::Released) => app.key_up(key), WEnum::Value(wl_keyboard::KeyState::Released) => app.key_up(key),
_ => {} _ => {}
}, },
// Store compositor key-repeat settings for our client-side timer.
wl_keyboard::Event::RepeatInfo { rate, delay } => {
// rate == 0 means repeat is disabled.
if rate > 0 {
app.repeat_rate_ms = (1000 / rate as u32).max(1);
app.repeat_delay_ms = delay as u32;
} else {
app.repeat_key = None;
app.repeat_next = None;
app.repeat_delay_ms = 0;
app.repeat_rate_ms = 0;
}
}
// Focus is only tracked here. sway sends leave immediately // Focus is only tracked here. sway sends leave immediately
// followed by enter on the same surface when the pointer crosses // followed by enter on the same surface when the pointer crosses
// it, so whether the grab is really gone is decided by the main // it, so whether the grab is really gone is decided by the main
// loop, once the event batch has been dispatched. // loop, once the event batch has been dispatched.
wl_keyboard::Event::Enter { keys, .. } => app.keyboard_enter(keys, qh), wl_keyboard::Event::Enter { keys, .. } => app.keyboard_enter(keys, qh),
wl_keyboard::Event::Leave { .. } => app.focused = false, wl_keyboard::Event::Leave { .. } => {
app.focused = false;
// Clear any held repeat — we no longer have the keyboard.
app.repeat_key = None;
app.repeat_next = None;
}
_ => {} _ => {}
} }
} }