From 50caed0110e01f3263061a2a976f0fd00f050c8f Mon Sep 17 00:00:00 2001 From: "N0\\A" Date: Wed, 9 Sep 2026 11:44:23 +0200 Subject: [PATCH] modkey --- src/app.rs | 12 ++++++ src/main.rs | 34 ++++++++++++++- src/overlay.rs | 110 ++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 144 insertions(+), 12 deletions(-) diff --git a/src/app.rs b/src/app.rs index 67f2aca..837989e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -127,6 +127,14 @@ pub struct App { 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, + pub(crate) repeat_next: Option, + /// 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 @@ -233,6 +241,10 @@ impl App { 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, + repeat_rate_ms: 25, }; let _: ExtForeignToplevelListV1 = globals.bind(qh, 1..=1, ())?; // One wl_output per display, bound at v4 so it tells us its name. diff --git a/src/main.rs b/src/main.rs index d4e74d8..0770f82 100644 --- a/src/main.rs +++ b/src/main.rs @@ -174,7 +174,39 @@ 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 { - queue.blocking_dispatch(&mut app)?; + // 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)?; + } if !app.finished() && !app.focused && !pump_for( diff --git a/src/overlay.rs b/src/overlay.rs index a160927..fa00d27 100644 --- a/src/overlay.rs +++ b/src/overlay.rs @@ -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. const BTN_LEFT: u32 = 0x110; @@ -321,6 +341,16 @@ impl App { if self.is_alt_tab && is_trigger_modifier(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 { KEY_LEFTSHIFT | KEY_RIGHTSHIFT => self.shift = true, KEY_ESC | KEY_Q => self.ending = Ending::Cancelled, @@ -345,6 +375,11 @@ impl App { if code == KEY_LEFTSHIFT || code == KEY_RIGHTSHIFT { 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 && self.latched_modifiers.remove(&code) && 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) { + 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, qh: &QueueHandle) { self.focused = true; let held_keys: Vec = keys @@ -382,17 +438,31 @@ impl App { if self.is_alt_tab && !self.initial_stepped { self.initial_stepped = true; - if self.alt_tab == AltTabMode::Yes && self.latched_modifiers.is_empty() { - // In explicit alt-tab mode, if no modifier was held on enter, - // the modifier was released before focus was acquired: commit immediately! - if self.ending == Ending::Running { - self.picked = self.tiles.get(self.sel).map(|t| t.target.clone()); - self.ending = Ending::Picked; + 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); } - } else if self.alt_tab == AltTabMode::Auto { - // In auto mode, step selection now that we know a modifier was held: - 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) + { + self.picked = self.tiles.get(self.sel).map(|t| t.target.clone()); + self.ending = Ending::Picked; } } } @@ -463,12 +533,30 @@ impl Dispatch for App { 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 // followed by enter on the same surface when the pointer crosses // it, so whether the grab is really gone is decided by the main // loop, once the event batch has been dispatched. 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; + } _ => {} } }