modkey
This commit is contained in:
+12
@@ -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.
|
||||||
|
|||||||
+33
-1
@@ -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 {
|
||||||
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()
|
if !app.finished()
|
||||||
&& !app.focused
|
&& !app.focused
|
||||||
&& !pump_for(
|
&& !pump_for(
|
||||||
|
|||||||
+99
-11
@@ -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,17 +438,31 @@ 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
|
||||||
if self.ending == Ending::Running {
|
// released before focus arrived — commit immediately.
|
||||||
self.picked = self.tiles.get(self.sel).map(|t| t.target.clone());
|
if self.ending == Ending::Running {
|
||||||
self.ending = Ending::Picked;
|
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<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;
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user