Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 54 additions & 5 deletions crates/openlogi-agent-core/src/watchers/gesture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
//! thumb-wheel arming — changes, and dispatches each captured input:
//!
//! - a gesture swipe through the gesture binding map,
//! - a DPI/ModeShift or thumb-wheel-tap press through the button binding map,
//! - a DPI/ModeShift, wheel-tilt, or thumb-wheel-tap press through the button
//! binding map,
//! - thumb-wheel rotation through the [`ButtonId::ThumbwheelScrollUp`] /
//! [`ButtonId::ThumbwheelScrollDown`] bindings — either re-synthesised as
//! continuous, sensitivity-scaled horizontal scroll or accumulated into a
Expand Down Expand Up @@ -129,6 +130,24 @@ fn thumbwheel_armed(hook_maps: &SharedHookMaps, sensitivity: i32) -> bool {
})
}

/// Whether either wheel-tilt control must be diverted over HID++ (which
/// suppresses native horizontal scroll) so we can run a custom action.
///
/// We divert only when a tilt is rebound away from its default horizontal
/// scroll; otherwise the OS scrolls the wheel natively. Mirrors
/// [`thumbwheel_armed`].
fn tilt_armed(hook_maps: &SharedHookMaps) -> bool {
hook_maps.read().ok().is_some_and(|maps| {
[ButtonId::TiltLeft, ButtonId::TiltRight]
.iter()
.any(|&button| {
maps.bindings
.get(&button)
.is_some_and(|action| *action != default_binding(button))
})
})
}

/// Whether a finished capture session should make the manager re-arm.
///
/// `done_epoch` identifies the session that signalled completion; `live_epoch`
Expand All @@ -153,8 +172,8 @@ async fn manage(
receiver_access: ReceiverAccess,
) {
let (tx, mut rx) = mpsc::unbounded_channel::<CapturedInput>();
// (route, capture_thumbwheel, divert_gesture_button)
let mut current: Option<(DeviceRoute, bool, bool)> = None;
// (route, capture_thumbwheel, capture_tilt, divert_gesture_button)
let mut current: Option<(DeviceRoute, bool, bool, bool)> = None;
let mut stop: Option<oneshot::Sender<()>> = None;
let mut ticker = tokio::time::interval(TARGET_POLL);
let mut accumulators = WheelAccumulators::default();
Expand Down Expand Up @@ -201,6 +220,7 @@ async fn manage(
(
t,
thumbwheel_armed(&hook_maps, sensitivity),
tilt_armed(&hook_maps),
divert_gesture,
)
})
Expand All @@ -218,12 +238,17 @@ async fn manage(
current = None;
continue;
}
if let Some((route, capture_thumbwheel, divert_gesture_button)) = want {
if let Some((route, capture_thumbwheel, capture_tilt, divert_gesture_button)) = want {
let Some(receiver_lease) = receiver_access.try_acquire_for_capture() else {
current = None;
continue;
};
current = Some((route.clone(), capture_thumbwheel, divert_gesture_button));
current = Some((
route.clone(),
capture_thumbwheel,
capture_tilt,
divert_gesture_button,
));
let (stop_tx, stop_rx) = oneshot::channel();
let sink = tx.clone();
let slot = Arc::clone(&capture_channel);
Expand All @@ -235,6 +260,7 @@ async fn manage(
if let Err(e) = run_capture_session(
Comment on lines 238 to 260

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Restart Gap Drops Tilt

When a tilt binding changes from default to a custom action, the manager stops the old capture session and waits for the next poll before starting the new one. During that gap the tilt CIDs are restored to native behavior, so a tilt press made immediately after rebinding scrolls horizontally instead of running the new binding.

Fix in Codex Fix in Claude Code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the existing behavior of the shared manage() loop rather than something specific to tilt — thumbwheel_armed and gesture-owner changes have the same ~1s re-arm transient. During the gap the tilt just falls back to native horizontal scroll (no lost input) and self-heals on the next poll. Happy to harden the shared restart timing in a separate follow-up so DPI / thumbwheel / gesture all benefit.

route,
capture_thumbwheel,
capture_tilt,
divert_gesture_button,
sink,
stop_rx,
Expand Down Expand Up @@ -598,4 +624,27 @@ mod tests {
// device is targeted): no target means there is nothing to re-arm.
assert!(!should_rearm(7, 7, false));
}

#[test]
fn tilt_armed_false_at_defaults_true_when_rebound() {
use crate::hook_runtime::HookMaps;
use openlogi_core::binding::Action;
use std::sync::{Arc, RwLock};

let maps: SharedHookMaps = Arc::new(RwLock::new(HookMaps::default()));
assert!(!tilt_armed(&maps), "no bindings → not armed");

// The default binding is not "armed".
if let Ok(mut m) = maps.write() {
m.bindings
.insert(ButtonId::TiltLeft, default_binding(ButtonId::TiltLeft));
}
assert!(!tilt_armed(&maps), "default binding → not armed");

// A non-default binding on either tilt arms capture.
if let Ok(mut m) = maps.write() {
m.bindings.insert(ButtonId::TiltRight, Action::Copy);
}
assert!(tilt_armed(&maps), "a rebound tilt → armed");
}
}
51 changes: 47 additions & 4 deletions crates/openlogi-core/src/binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ use std::time::Instant;

use serde::{Deserialize, Serialize};

/// One of the user-rebindable hotspots on a Logi mouse. The order matches the
/// physical layout from front to side; [`ButtonId::ALL`] is consumed by the
/// default-binding generator and the popover trigger list.
/// One of the user-rebindable hotspots on a Logi mouse. New variants are
/// appended (the identifiers are TOML-stable config keys); [`ButtonId::ALL`]
/// lists them in physical layout order (front to side) and is what the
/// default-binding generator and the popover trigger list consume.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub enum ButtonId {
LeftClick,
Expand All @@ -38,10 +39,18 @@ pub enum ButtonId {
/// The HID++ gesture button on MX-line devices. The press itself
/// fires the bound action; swipe directions are P1.5 territory.
GestureButton,
/// The scroll wheel tilted left — a divertable HID++ `0x1b04` control
/// (CID `0x5b`) on tilt-wheel devices like the MX Ergo. Bound to horizontal
/// scroll by default; captured over HID++ only when rebound (see agent-core
/// `watchers::gesture`), so native tilt-scroll is preserved otherwise.
TiltLeft,
/// The scroll wheel tilted right — divertable HID++ `0x1b04` control
/// (CID `0x5d`). See [`ButtonId::TiltLeft`].
TiltRight,
}

impl ButtonId {
pub const ALL: [ButtonId; 10] = [
pub const ALL: [ButtonId; 12] = [
ButtonId::LeftClick,
ButtonId::RightClick,
ButtonId::MiddleClick,
Expand All @@ -51,6 +60,8 @@ impl ButtonId {
ButtonId::Thumbwheel,
ButtonId::ThumbwheelScrollUp,
ButtonId::ThumbwheelScrollDown,
ButtonId::TiltLeft,
ButtonId::TiltRight,
ButtonId::GestureButton,
];

Expand Down Expand Up @@ -82,6 +93,8 @@ impl ButtonId {
ButtonId::Thumbwheel => "Thumb Wheel",
ButtonId::ThumbwheelScrollUp => "Thumb Wheel Up",
ButtonId::ThumbwheelScrollDown => "Thumb Wheel Down",
ButtonId::TiltLeft => "Tilt Left",
ButtonId::TiltRight => "Tilt Right",
ButtonId::GestureButton => "Gesture Button",
}
}
Expand Down Expand Up @@ -856,6 +869,10 @@ impl Action {
/// hook map, scroll defaults, labels) stay total.
#[must_use]
pub fn default_binding(button: ButtonId) -> Action {
#[allow(
clippy::match_same_arms,
reason = "tilt arms mirror the thumbwheel horizontal-scroll actions but are kept separate and documented per physical control"
)]
match button {
ButtonId::LeftClick => Action::LeftClick,
ButtonId::RightClick => Action::RightClick,
Expand All @@ -871,6 +888,12 @@ pub fn default_binding(button: ButtonId) -> Action {
// would get (see `watchers::gesture`).
ButtonId::ThumbwheelScrollUp => Action::HorizontalScrollRight,
ButtonId::ThumbwheelScrollDown => Action::HorizontalScrollLeft,
// The scroll wheel tilts to scroll horizontally: tilt-left → scroll
// left, tilt-right → scroll right. Dispatched as a discrete per-press
// horizontal scroll (openlogi_inject::execute) via the generic button
// path — see agent-core watchers::gesture.
ButtonId::TiltLeft => Action::HorizontalScrollLeft,
ButtonId::TiltRight => Action::HorizontalScrollRight,
ButtonId::GestureButton => Action::MissionControl,
}
}
Expand Down Expand Up @@ -1407,4 +1430,24 @@ mod tests {
Action::CycleDpiPresets
);
}

#[test]
fn tilt_buttons_default_to_horizontal_scroll() {
assert_eq!(
default_binding(ButtonId::TiltLeft),
Action::HorizontalScrollLeft
);
assert_eq!(
default_binding(ButtonId::TiltRight),
Action::HorizontalScrollRight
);
}

#[test]
fn tilt_buttons_are_in_all_and_have_labels() {
assert!(ButtonId::ALL.contains(&ButtonId::TiltLeft));
assert!(ButtonId::ALL.contains(&ButtonId::TiltRight));
assert_eq!(ButtonId::TiltLeft.label(), "Tilt Left");
assert_eq!(ButtonId::TiltRight.label(), "Tilt Right");
}
}
2 changes: 2 additions & 0 deletions crates/openlogi-gui/locales/da.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ _version: 1
"DPI Toggle": "DPI-skift"
"Thumb Wheel": "Tommelfingerhjul"
"Gesture Button": "Bevægelsesknap"
"Tilt Left": "Vip til venstre"
"Tilt Right": "Vip til højre"
"Up": "Op"
"Down": "Ned"
"Left": "Venstre"
Expand Down
2 changes: 2 additions & 0 deletions crates/openlogi-gui/locales/de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ _version: 1
"DPI Toggle": "DPI-Umschaltung"
"Thumb Wheel": "Daumenrad"
"Gesture Button": "Gestentaste"
"Tilt Left": "Nach links kippen"
"Tilt Right": "Nach rechts kippen"
"Up": "Oben"
"Down": "Unten"
"Left": "Links"
Expand Down
2 changes: 2 additions & 0 deletions crates/openlogi-gui/locales/el.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ _version: 1
"DPI Toggle": "Εναλλαγή DPI"
"Thumb Wheel": "Πλαϊνός τροχός"
"Gesture Button": "Κουμπί χειρονομιών"
"Tilt Left": "Κλίση αριστερά"
"Tilt Right": "Κλίση δεξιά"
"Up": "Πάνω"
"Down": "Κάτω"
"Left": "Αριστερά"
Expand Down
2 changes: 2 additions & 0 deletions crates/openlogi-gui/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ _version: 1
"DPI Toggle": "DPI Toggle"
"Thumb Wheel": "Thumb Wheel"
"Gesture Button": "Gesture Button"
"Tilt Left": "Tilt Left"
"Tilt Right": "Tilt Right"
"Up": "Up"
"Down": "Down"
"Left": "Left"
Expand Down
2 changes: 2 additions & 0 deletions crates/openlogi-gui/locales/es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ _version: 1
"DPI Toggle": "Cambiar DPI"
"Thumb Wheel": "Rueda lateral"
"Gesture Button": "Botón de gestos"
"Tilt Left": "Inclinar a la izquierda"
"Tilt Right": "Inclinar a la derecha"
"Up": "Arriba"
"Down": "Abajo"
"Left": "Izquierda"
Expand Down
2 changes: 2 additions & 0 deletions crates/openlogi-gui/locales/fi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ _version: 1
"DPI Toggle": "DPI-vaihto"
"Thumb Wheel": "Peukalorulla"
"Gesture Button": "Eletoiminto"
"Tilt Left": "Kallista vasemmalle"
"Tilt Right": "Kallista oikealle"
"Up": "Ylös"
"Down": "Alas"
"Left": "Vasen"
Expand Down
2 changes: 2 additions & 0 deletions crates/openlogi-gui/locales/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ _version: 1
"DPI Toggle": "Bascule DPI"
"Thumb Wheel": "Molette latérale"
"Gesture Button": "Bouton de gestes"
"Tilt Left": "Incliner à gauche"
"Tilt Right": "Incliner à droite"
"Up": "Haut"
"Down": "Bas"
"Left": "Gauche"
Expand Down
2 changes: 2 additions & 0 deletions crates/openlogi-gui/locales/it.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ _version: 1
"DPI Toggle": "Cambia DPI"
"Thumb Wheel": "Volante da pollice"
"Gesture Button": "Pulsante gesture"
"Tilt Left": "Inclina a sinistra"
"Tilt Right": "Inclina a destra"
"Up": "Su"
"Down": "Giù"
"Left": "Sinistra"
Expand Down
2 changes: 2 additions & 0 deletions crates/openlogi-gui/locales/ja.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ _version: 1
"DPI Toggle": "DPI 切り替え"
"Thumb Wheel": "サムホイール"
"Gesture Button": "ジェスチャーボタン"
"Tilt Left": "左チルト"
"Tilt Right": "右チルト"
"Up": "上"
"Down": "下"
"Left": "左"
Expand Down
2 changes: 2 additions & 0 deletions crates/openlogi-gui/locales/ko.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ _version: 1
"DPI Toggle": "DPI 전환"
"Thumb Wheel": "썸휠"
"Gesture Button": "제스처 버튼"
"Tilt Left": "왼쪽 틸트"
"Tilt Right": "오른쪽 틸트"
"Up": "위"
"Down": "아래"
"Left": "왼쪽"
Expand Down
2 changes: 2 additions & 0 deletions crates/openlogi-gui/locales/nb.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ _version: 1
"DPI Toggle": "DPI-veksling"
"Thumb Wheel": "Tommelhjul"
"Gesture Button": "Bevegelsesknapp"
"Tilt Left": "Vipp til venstre"
"Tilt Right": "Vipp til høyre"
"Up": "Opp"
"Down": "Ned"
"Left": "Venstre"
Expand Down
2 changes: 2 additions & 0 deletions crates/openlogi-gui/locales/nl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ _version: 1
"DPI Toggle": "DPI wisselen"
"Thumb Wheel": "Duimwiel"
"Gesture Button": "Gebarenknop"
"Tilt Left": "Naar links kantelen"
"Tilt Right": "Naar rechts kantelen"
"Up": "Omhoog"
"Down": "Omlaag"
"Left": "Links"
Expand Down
2 changes: 2 additions & 0 deletions crates/openlogi-gui/locales/pl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ _version: 1
"DPI Toggle": "Przełącznik DPI"
"Thumb Wheel": "Rolka kciukowa"
"Gesture Button": "Przycisk gestów"
"Tilt Left": "Przechył w lewo"
"Tilt Right": "Przechył w prawo"
"Up": "W górę"
"Down": "W dół"
"Left": "W lewo"
Expand Down
2 changes: 2 additions & 0 deletions crates/openlogi-gui/locales/pt-BR.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ _version: 1
"DPI Toggle": "Alternar DPI"
"Thumb Wheel": "Roda do Polegar"
"Gesture Button": "Botão de Gesto"
"Tilt Left": "Inclinar para a esquerda"
"Tilt Right": "Inclinar para a direita"
"Up": "Cima"
"Down": "Baixo"
"Left": "Esquerda"
Expand Down
2 changes: 2 additions & 0 deletions crates/openlogi-gui/locales/pt-PT.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ _version: 1
"DPI Toggle": "Alternar DPI"
"Thumb Wheel": "Roda de polegar"
"Gesture Button": "Botão de gesto"
"Tilt Left": "Inclinar para a esquerda"
"Tilt Right": "Inclinar para a direita"
"Up": "Cima"
"Down": "Baixo"
"Left": "Esquerda"
Expand Down
2 changes: 2 additions & 0 deletions crates/openlogi-gui/locales/ru.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ _version: 1
"DPI Toggle": "Переключение DPI"
"Thumb Wheel": "Колесо под большой палец"
"Gesture Button": "Кнопка жестов"
"Tilt Left": "Наклон влево"
"Tilt Right": "Наклон вправо"
"Up": "Вверх"
"Down": "Вниз"
"Left": "Влево"
Expand Down
2 changes: 2 additions & 0 deletions crates/openlogi-gui/locales/sv.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ _version: 1
"DPI Toggle": "DPI-växling"
"Thumb Wheel": "Tumhjul"
"Gesture Button": "Gestknapp"
"Tilt Left": "Luta åt vänster"
"Tilt Right": "Luta åt höger"
"Up": "Upp"
"Down": "Ned"
"Left": "Vänster"
Expand Down
2 changes: 2 additions & 0 deletions crates/openlogi-gui/locales/zh-CN.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ _version: 1
"DPI Toggle": "灵敏度切换"
"Thumb Wheel": "拇指滚轮"
"Gesture Button": "手势按钮"
"Tilt Left": "向左倾斜"
"Tilt Right": "向右倾斜"
"Up": "上"
"Down": "下"
"Left": "左"
Expand Down
2 changes: 2 additions & 0 deletions crates/openlogi-gui/locales/zh-HK.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ _version: 1
"DPI Toggle": "靈敏度切換"
"Thumb Wheel": "拇指滾輪"
"Gesture Button": "手勢按鈕"
"Tilt Left": "向左傾斜"
"Tilt Right": "向右傾斜"
"Up": "上"
"Down": "下"
"Left": "左"
Expand Down
2 changes: 2 additions & 0 deletions crates/openlogi-gui/locales/zh-TW.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ _version: 1
"DPI Toggle": "靈敏度切換"
"Thumb Wheel": "拇指滾輪"
"Gesture Button": "手勢按鈕"
"Tilt Left": "向左傾斜"
"Tilt Right": "向右傾斜"
"Up": "上"
"Down": "下"
"Left": "左"
Expand Down
Loading