lesavka: preserve quick modifier chords

This commit is contained in:
Brad Stein 2026-04-16 15:32:15 -03:00
parent 20cb355aa0
commit a3e84c5c15
2 changed files with 68 additions and 11 deletions

View File

@ -473,15 +473,32 @@ impl InputAggregator {
fn process_keyboard_updates(&mut self) {
for index in 0..self.keyboards.len() {
let mut keyboard_shadow: HashSet<KeyCode> = self.keyboards[index]
.pressed_keys_snapshot()
.into_iter()
.collect();
let other_pressed: HashSet<KeyCode> = self
.keyboards
.iter()
.enumerate()
.filter(|(other_index, _)| *other_index != index)
.flat_map(|(_, keyboard)| keyboard.pressed_keys_snapshot())
.collect();
let updates = {
let keyboard = &mut self.keyboards[index];
keyboard.drain_key_updates()
};
for update in updates {
update_shadow_pressed_keys(&mut keyboard_shadow, update.code, update.value);
if update.swallowed || !self.keyboard_capture_enabled() {
continue;
}
let report = self.build_combined_keyboard_report();
let report = build_keyboard_report(
other_pressed
.iter()
.copied()
.chain(keyboard_shadow.iter().copied()),
);
if report == self.last_keyboard_report {
continue;
}
@ -497,16 +514,6 @@ impl InputAggregator {
.any(KeyboardAggregator::sending_enabled)
}
fn build_combined_keyboard_report(&self) -> [u8; 8] {
let mut pressed = HashSet::new();
for keyboard in &self.keyboards {
for key in keyboard.pressed_keys_snapshot() {
pressed.insert(key);
}
}
build_keyboard_report(pressed.into_iter())
}
fn quick_toggle_active(&mut self) -> bool {
self.quick_toggle_key.is_some_and(|key| {
self.keyboards
@ -696,6 +703,14 @@ fn should_ignore_keyboard_device(dev: &Device) -> bool {
|| name.contains("codex-persistent-kbd")
}
fn update_shadow_pressed_keys(pressed_keys: &mut HashSet<KeyCode>, code: KeyCode, value: i32) {
if value == 0 {
pressed_keys.remove(&code);
} else if value > 0 {
pressed_keys.insert(code);
}
}
/// Resolves the quick-toggle key from env, defaulting to Pause/Break.
fn quick_toggle_key_from_env() -> Option<KeyCode> {
match std::env::var("LESAVKA_INPUT_TOGGLE_KEY") {

View File

@ -153,6 +153,48 @@ mod inputs_contract_extra {
);
}
#[test]
#[serial]
fn process_keyboard_updates_keeps_quick_shift_chord_when_full_tap_lands_in_one_poll_cycle() {
let Some((mut vdev, dev)) = build_keyboard_pair_with_keys(
"lesavka-input-shift-batch",
&[evdev::KeyCode::KEY_LEFTSHIFT, evdev::KeyCode::KEY_A],
) else {
return;
};
let (kbd_tx, mut rx) = tokio::sync::broadcast::channel(32);
let (mou_tx, _) = tokio::sync::broadcast::channel(16);
let keyboard = KeyboardAggregator::new(dev, false, kbd_tx.clone(), None);
let mut agg = InputAggregator::new(false, kbd_tx, mou_tx, None);
agg.keyboards.push(keyboard);
vdev.emit(&[
evdev::InputEvent::new(evdev::EventType::KEY.0, evdev::KeyCode::KEY_LEFTSHIFT.0, 1),
evdev::InputEvent::new(evdev::EventType::KEY.0, evdev::KeyCode::KEY_A.0, 1),
evdev::InputEvent::new(evdev::EventType::KEY.0, evdev::KeyCode::KEY_A.0, 0),
evdev::InputEvent::new(evdev::EventType::KEY.0, evdev::KeyCode::KEY_LEFTSHIFT.0, 0),
])
.expect("emit quick shifted tap");
thread::sleep(std::time::Duration::from_millis(25));
temp_env::with_var("LESAVKA_LIVE_MODIFIER_DELAY_MS", Some("0"), || {
agg.process_keyboard_updates();
});
let reports: Vec<Vec<u8>> =
std::iter::from_fn(|| rx.try_recv().ok().map(|pkt| pkt.data)).collect();
assert!(
reports.contains(&vec![0x02, 0, 0x04, 0, 0, 0, 0, 0]),
"expected shifted A report from one-batch chord, got {reports:?}"
);
assert!(
reports.contains(&vec![0; 8]),
"expected final empty report after one-batch chord, got {reports:?}"
);
}
#[test]
#[serial]
fn process_keyboard_updates_keeps_overlapping_plain_keys_from_sticking_across_keyboards() {