modkey
This commit is contained in:
+12
@@ -127,6 +127,14 @@ pub struct App {
|
||||
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>,
|
||||
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
|
||||
@@ -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.
|
||||
|
||||
+32
@@ -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
|
||||
// cycles focus off and on in a single batch as the pointer crosses us.
|
||||
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)?;
|
||||
}
|
||||
if !app.finished()
|
||||
&& !app.focused
|
||||
&& !pump_for(
|
||||
|
||||
+94
-6
@@ -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<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>) {
|
||||
self.focused = true;
|
||||
let held_keys: Vec<u32> = keys
|
||||
@@ -382,20 +438,34 @@ 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.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 if self.alt_tab == AltTabMode::Auto {
|
||||
// In auto mode, step selection now that we know a modifier was held:
|
||||
} 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)
|
||||
{
|
||||
self.picked = self.tiles.get(self.sel).map(|t| t.target.clone());
|
||||
self.ending = Ending::Picked;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- event plumbing -------------------------------------------------------
|
||||
@@ -463,12 +533,30 @@ impl Dispatch<WlKeyboard, ()> 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;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user