From 83d1758cd24af1e9cbfd89cb0259826f039142cb Mon Sep 17 00:00:00 2001 From: George Nick Gorzynski Date: Sun, 5 Jul 2026 14:09:10 +0100 Subject: [PATCH 1/6] feat(binding): add TiltLeft/TiltRight buttons defaulting to horizontal scroll --- crates/openlogi-core/src/binding.rs | 44 ++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/crates/openlogi-core/src/binding.rs b/crates/openlogi-core/src/binding.rs index 1463492a..3cef2bd4 100644 --- a/crates/openlogi-core/src/binding.rs +++ b/crates/openlogi-core/src/binding.rs @@ -38,10 +38,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, @@ -51,6 +59,8 @@ impl ButtonId { ButtonId::Thumbwheel, ButtonId::ThumbwheelScrollUp, ButtonId::ThumbwheelScrollDown, + ButtonId::TiltLeft, + ButtonId::TiltRight, ButtonId::GestureButton, ]; @@ -82,6 +92,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", } } @@ -856,6 +868,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, @@ -871,6 +887,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, } } @@ -1407,4 +1429,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"); + } } From 004b0f1defd1d9642a32905461a3a58e89638af6 Mon Sep 17 00:00:00 2001 From: George Nick Gorzynski Date: Sun, 5 Jul 2026 14:18:51 +0100 Subject: [PATCH 2/6] feat(hid): capture MX Ergo wheel tilt (CIDs 0x5b/0x5d) as diverted buttons --- .../src/watchers/gesture.rs | 1 + crates/openlogi-hid/src/gesture.rs | 61 ++++++++++++++++--- crates/openlogi-hid/src/gesture/tests.rs | 37 +++++++++++ crates/openlogi-hid/src/reprog_controls.rs | 9 +++ 4 files changed, 98 insertions(+), 10 deletions(-) diff --git a/crates/openlogi-agent-core/src/watchers/gesture.rs b/crates/openlogi-agent-core/src/watchers/gesture.rs index 80659682..7eb301f8 100644 --- a/crates/openlogi-agent-core/src/watchers/gesture.rs +++ b/crates/openlogi-agent-core/src/watchers/gesture.rs @@ -235,6 +235,7 @@ async fn manage( if let Err(e) = run_capture_session( route, capture_thumbwheel, + false, divert_gesture_button, sink, stop_rx, diff --git a/crates/openlogi-hid/src/gesture.rs b/crates/openlogi-hid/src/gesture.rs index 1783494b..4b28c9e9 100644 --- a/crates/openlogi-hid/src/gesture.rs +++ b/crates/openlogi-hid/src/gesture.rs @@ -1,6 +1,6 @@ //! Live control capture for one device: divert the MX dedicated gesture button, the -//! DPI/ModeShift button, and the thumb wheel over HID++ and turn their events -//! into [`CapturedInput`] the GUI can dispatch. +//! DPI/ModeShift button, the wheel-tilt buttons, and the thumb wheel over HID++ +//! and turn their events into [`CapturedInput`] the GUI can dispatch. //! //! [`run_capture_session`] holds a single HID++ channel open for one device, //! enables diversion on whichever of those controls it exposes, registers one @@ -75,11 +75,15 @@ struct CaptureAccum { /// Whether any DPI/ModeShift control was held in the last event — for /// rising-edge press detection. dpi_down: bool, + /// Whether the left tilt control was held in the last event. + tilt_left_down: bool, + /// Whether the right tilt control was held in the last event. + tilt_right_down: bool, } -/// Capture the gesture button, DPI/ModeShift button, and (when -/// `capture_thumbwheel`) the thumb wheel on `route` until `shutdown` resolves, -/// forwarding each event to `sink`. +/// Capture the gesture button, DPI/ModeShift button, (when `capture_tilt`) +/// the wheel-tilt buttons, and (when `capture_thumbwheel`) the thumb wheel on +/// `route` until `shutdown` resolves, forwarding each event to `sink`. /// /// The dedicated gesture button (raw-XY) is diverted only when `divert_gesture_button` — /// i.e. it is the device's gesture owner. When the user moves the gesture role @@ -95,6 +99,7 @@ struct CaptureAccum { pub async fn run_capture_session( route: DeviceRoute, capture_thumbwheel: bool, + capture_tilt: bool, divert_gesture_button: bool, sink: mpsc::UnboundedSender, shutdown: oneshot::Receiver<()>, @@ -108,6 +113,7 @@ pub async fn run_capture_session( &chan, device_index, capture_thumbwheel, + capture_tilt, divert_gesture_button, ) .await?; @@ -156,6 +162,7 @@ pub async fn run_capture_session( index = device_index, gesture = armed.gesture_diverted, dpi_buttons = armed.dpi_cids.len(), + tilt_buttons = armed.tilt_cids.len(), thumbwheel = armed.thumb.is_some(), "control capture active" ); @@ -179,6 +186,8 @@ struct ArmedControls { gesture_diverted: bool, /// DPI/ModeShift CIDs diverted as plain buttons. dpi_cids: Vec, + /// Wheel tilt CIDs diverted as plain buttons. + tilt_cids: Vec, /// `0x2150` accessor + feature index, present when the thumb wheel is /// diverted. thumb: Option<(Thumbwheel, u8)>, @@ -197,6 +206,9 @@ impl ArmedControls { for &cid in &self.dpi_cids { restore(rc.set_cid_reporting(cid, false, false).await, "DPI button"); } + for &cid in &self.tilt_cids { + restore(rc.set_cid_reporting(cid, false, false).await, "tilt button"); + } } if let Some((tw, _)) = self.thumb.as_ref() { restore(tw.set_reporting(false, false).await, "thumb wheel"); @@ -205,14 +217,16 @@ impl ArmedControls { } /// Resolve features off the device's root and divert the controls we capture: -/// the gesture button (raw-XY) and DPI/ModeShift buttons over `0x1b04`, and — -/// when `capture_thumbwheel` and the wheel reports a single tap — the thumb -/// wheel over `0x2150`. The root-feature lookup mirrors `write::open_feature`, -/// since hidpp 0.2's registry doesn't carry the features OpenLogi reimplements. +/// the gesture button (raw-XY), DPI/ModeShift buttons, and (when +/// `capture_tilt`) the wheel-tilt buttons, all over `0x1b04`, and — when +/// `capture_thumbwheel` and the wheel reports a single tap — the thumb wheel +/// over `0x2150`. The root-feature lookup mirrors `write::open_feature`, since +/// hidpp 0.2's registry doesn't carry the features OpenLogi reimplements. async fn arm_controls( chan: &Arc, slot: u8, capture_thumbwheel: bool, + capture_tilt: bool, divert_gesture_button: bool, ) -> Result { let device = Device::new(Arc::clone(chan), slot) @@ -222,6 +236,7 @@ async fn arm_controls( let mut reprog: Option<(ReprogControlsV4, u8)> = None; let mut gesture_diverted = false; let mut dpi_cids: Vec = Vec::new(); + let mut tilt_cids: Vec = Vec::new(); if let Some(info) = device .root() .get_feature(reprog_controls::FEATURE_ID) @@ -251,6 +266,19 @@ async fn arm_controls( dpi_cids.push(cid); } } + if capture_tilt { + for &cid in &[ + reprog_controls::TILT_LEFT_CID, + reprog_controls::TILT_RIGHT_CID, + ] { + if controls.iter().any(|c| c.cid == cid && c.is_divertable()) { + rc.set_cid_reporting(cid, true, false) + .await + .map_err(|e| GestureError::Hidpp(format!("{e:?}")))?; + tilt_cids.push(cid); + } + } + } reprog = Some((rc, info.index)); } @@ -283,13 +311,14 @@ async fn arm_controls( } } - if !gesture_diverted && dpi_cids.is_empty() && thumb.is_none() { + if !gesture_diverted && dpi_cids.is_empty() && tilt_cids.is_empty() && thumb.is_none() { debug!(slot, "no capturable controls — idle session"); } Ok(ArmedControls { reprog, gesture_diverted, dpi_cids, + tilt_cids, thumb, }) } @@ -349,6 +378,18 @@ fn handle_reprog( let _ = sink.send(CapturedInput::ButtonPressed(ButtonId::DpiToggle)); } acc.dpi_down = dpi_down; + + let tilt_left_down = cids.contains(&reprog_controls::TILT_LEFT_CID); + if tilt_left_down && !acc.tilt_left_down { + let _ = sink.send(CapturedInput::ButtonPressed(ButtonId::TiltLeft)); + } + acc.tilt_left_down = tilt_left_down; + + let tilt_right_down = cids.contains(&reprog_controls::TILT_RIGHT_CID); + if tilt_right_down && !acc.tilt_right_down { + let _ = sink.send(CapturedInput::ButtonPressed(ButtonId::TiltRight)); + } + acc.tilt_right_down = tilt_right_down; } RawControlEvent::RawXy { dx, dy } => { // Commit the instant a clean direction emerges (mid-swipe, once per diff --git a/crates/openlogi-hid/src/gesture/tests.rs b/crates/openlogi-hid/src/gesture/tests.rs index 035f2cda..6a9febaa 100644 --- a/crates/openlogi-hid/src/gesture/tests.rs +++ b/crates/openlogi-hid/src/gesture/tests.rs @@ -102,3 +102,40 @@ fn a_dpi_button_re_presses_after_a_release() { ); assert!(rx.try_recv().is_err()); } + +#[test] +fn a_held_tilt_left_presses_once_on_the_rising_edge() { + let (tx, mut rx) = mpsc::unbounded_channel(); + let mut acc = CaptureAccum::default(); + let down = RawControlEvent::DivertedButtons([reprog_controls::TILT_LEFT_CID, 0, 0, 0]); + + handle_reprog(&mut acc, down, &[], &tx); + handle_reprog(&mut acc, down, &[], &tx); + + assert_eq!( + rx.try_recv(), + Ok(CapturedInput::ButtonPressed(ButtonId::TiltLeft)) + ); + assert!(rx.try_recv().is_err(), "a held tilt presses once"); +} + +#[test] +fn tilt_left_and_right_emit_independent_presses() { + let (tx, mut rx) = mpsc::unbounded_channel(); + let mut acc = CaptureAccum::default(); + let left = RawControlEvent::DivertedButtons([reprog_controls::TILT_LEFT_CID, 0, 0, 0]); + let right = RawControlEvent::DivertedButtons([reprog_controls::TILT_RIGHT_CID, 0, 0, 0]); + + handle_reprog(&mut acc, left, &[], &tx); + handle_reprog(&mut acc, right, &[], &tx); + + assert_eq!( + rx.try_recv(), + Ok(CapturedInput::ButtonPressed(ButtonId::TiltLeft)) + ); + assert_eq!( + rx.try_recv(), + Ok(CapturedInput::ButtonPressed(ButtonId::TiltRight)) + ); + assert!(rx.try_recv().is_err()); +} diff --git a/crates/openlogi-hid/src/reprog_controls.rs b/crates/openlogi-hid/src/reprog_controls.rs index c25d4e3b..f100d6b5 100644 --- a/crates/openlogi-hid/src/reprog_controls.rs +++ b/crates/openlogi-hid/src/reprog_controls.rs @@ -51,6 +51,15 @@ pub const GESTURE_BUTTON_CID: u16 = 0x00c3; /// cross-checked against Solaar `special_keys.py`. pub const DPI_MODE_SHIFT_CIDS: [u16; 3] = [0x00c4, 0x00ed, 0x00fd]; +/// Control IDs of the scroll wheel's horizontal tilt on tilt-wheel devices +/// (e.g. MX Ergo): left = `0x5b`, right = `0x5d`. These are classic mouse CIDs +/// (below `0xB8`, so absent from `control_ids.rs`); values cross-checked against +/// Solaar `special_keys.py` and the MX Ergo asset depot slotIds (`c91`/`c93`). +/// Their default on-device task is horizontal scroll left/right. +pub const TILT_LEFT_CID: u16 = 0x005b; +/// See [`TILT_LEFT_CID`]. +pub const TILT_RIGHT_CID: u16 = 0x005d; + /// Identity and capabilities of one reprogrammable control, as returned by /// `getCtrlIdInfo`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] From c7250f626ab3f9ab8ff05cc8ff07064c363707c5 Mon Sep 17 00:00:00 2001 From: George Nick Gorzynski Date: Sun, 5 Jul 2026 14:25:52 +0100 Subject: [PATCH 3/6] feat(agent): divert MX Ergo tilt only when rebound (mirrors thumbwheel) --- .../src/watchers/gesture.rs | 57 +++++++++++++++++-- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/crates/openlogi-agent-core/src/watchers/gesture.rs b/crates/openlogi-agent-core/src/watchers/gesture.rs index 7eb301f8..c1f797fd 100644 --- a/crates/openlogi-agent-core/src/watchers/gesture.rs +++ b/crates/openlogi-agent-core/src/watchers/gesture.rs @@ -129,6 +129,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` @@ -153,8 +171,8 @@ async fn manage( receiver_access: ReceiverAccess, ) { let (tx, mut rx) = mpsc::unbounded_channel::(); - // (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> = None; let mut ticker = tokio::time::interval(TARGET_POLL); let mut accumulators = WheelAccumulators::default(); @@ -201,6 +219,7 @@ async fn manage( ( t, thumbwheel_armed(&hook_maps, sensitivity), + tilt_armed(&hook_maps), divert_gesture, ) }) @@ -218,12 +237,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); @@ -235,7 +259,7 @@ async fn manage( if let Err(e) = run_capture_session( route, capture_thumbwheel, - false, + capture_tilt, divert_gesture_button, sink, stop_rx, @@ -599,4 +623,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"); + } } From 13e4a4a2e408fd7954acab5260fa1ec854bc632a Mon Sep 17 00:00:00 2001 From: George Nick Gorzynski Date: Sun, 5 Jul 2026 14:31:55 +0100 Subject: [PATCH 4/6] feat(gui): map MX Ergo SLOT_NAME_SCROLL_LEFT/RIGHT to tilt hotspots --- crates/openlogi-gui/src/mouse_model/geometry.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/openlogi-gui/src/mouse_model/geometry.rs b/crates/openlogi-gui/src/mouse_model/geometry.rs index ec709edc..b75080cb 100644 --- a/crates/openlogi-gui/src/mouse_model/geometry.rs +++ b/crates/openlogi-gui/src/mouse_model/geometry.rs @@ -218,6 +218,8 @@ fn map_slot_name(name: &str) -> Option { "SLOT_NAME_LEFT_BUTTON" => Some(ButtonId::LeftClick), "SLOT_NAME_RIGHT_BUTTON" => Some(ButtonId::RightClick), "SLOT_NAME_MIDDLE_BUTTON" => Some(ButtonId::MiddleClick), + "SLOT_NAME_SCROLL_LEFT" => Some(ButtonId::TiltLeft), + "SLOT_NAME_SCROLL_RIGHT" => Some(ButtonId::TiltRight), "SLOT_NAME_BACK_BUTTON" => Some(ButtonId::Back), "SLOT_NAME_FORWARD_BUTTON" => Some(ButtonId::Forward), "SLOT_NAME_MODESHIFT_BUTTON" => Some(ButtonId::DpiToggle), @@ -297,4 +299,16 @@ mod tests { ys.dedup(); assert_eq!(ys.len(), labels.len(), "each label gets a distinct slot"); } + + #[test] + fn scroll_slots_map_to_tilt_buttons() { + assert_eq!( + map_slot_name("SLOT_NAME_SCROLL_LEFT"), + Some(ButtonId::TiltLeft) + ); + assert_eq!( + map_slot_name("SLOT_NAME_SCROLL_RIGHT"), + Some(ButtonId::TiltRight) + ); + } } From 01110995cad4ce712644afb06fd0fd583c381d6f Mon Sep 17 00:00:00 2001 From: George Nick Gorzynski Date: Sun, 5 Jul 2026 14:41:38 +0100 Subject: [PATCH 5/6] feat(i18n): localize Tilt Left/Tilt Right button labels across all 20 locales --- crates/openlogi-gui/locales/da.yml | 2 ++ crates/openlogi-gui/locales/de.yml | 2 ++ crates/openlogi-gui/locales/el.yml | 2 ++ crates/openlogi-gui/locales/en.yml | 2 ++ crates/openlogi-gui/locales/es.yml | 2 ++ crates/openlogi-gui/locales/fi.yml | 2 ++ crates/openlogi-gui/locales/fr.yml | 2 ++ crates/openlogi-gui/locales/it.yml | 2 ++ crates/openlogi-gui/locales/ja.yml | 2 ++ crates/openlogi-gui/locales/ko.yml | 2 ++ crates/openlogi-gui/locales/nb.yml | 2 ++ crates/openlogi-gui/locales/nl.yml | 2 ++ crates/openlogi-gui/locales/pl.yml | 2 ++ crates/openlogi-gui/locales/pt-BR.yml | 2 ++ crates/openlogi-gui/locales/pt-PT.yml | 2 ++ crates/openlogi-gui/locales/ru.yml | 2 ++ crates/openlogi-gui/locales/sv.yml | 2 ++ crates/openlogi-gui/locales/zh-CN.yml | 2 ++ crates/openlogi-gui/locales/zh-HK.yml | 2 ++ crates/openlogi-gui/locales/zh-TW.yml | 2 ++ 20 files changed, 40 insertions(+) diff --git a/crates/openlogi-gui/locales/da.yml b/crates/openlogi-gui/locales/da.yml index 2e2f041f..2572bd63 100644 --- a/crates/openlogi-gui/locales/da.yml +++ b/crates/openlogi-gui/locales/da.yml @@ -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" diff --git a/crates/openlogi-gui/locales/de.yml b/crates/openlogi-gui/locales/de.yml index 99480ebe..8d37c304 100644 --- a/crates/openlogi-gui/locales/de.yml +++ b/crates/openlogi-gui/locales/de.yml @@ -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" diff --git a/crates/openlogi-gui/locales/el.yml b/crates/openlogi-gui/locales/el.yml index 3b96ae1c..39c4389d 100644 --- a/crates/openlogi-gui/locales/el.yml +++ b/crates/openlogi-gui/locales/el.yml @@ -150,6 +150,8 @@ _version: 1 "DPI Toggle": "Εναλλαγή DPI" "Thumb Wheel": "Πλαϊνός τροχός" "Gesture Button": "Κουμπί χειρονομιών" +"Tilt Left": "Κλίση αριστερά" +"Tilt Right": "Κλίση δεξιά" "Up": "Πάνω" "Down": "Κάτω" "Left": "Αριστερά" diff --git a/crates/openlogi-gui/locales/en.yml b/crates/openlogi-gui/locales/en.yml index 7b567aa7..bd744d10 100644 --- a/crates/openlogi-gui/locales/en.yml +++ b/crates/openlogi-gui/locales/en.yml @@ -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" diff --git a/crates/openlogi-gui/locales/es.yml b/crates/openlogi-gui/locales/es.yml index 9374362f..c1d6bb45 100644 --- a/crates/openlogi-gui/locales/es.yml +++ b/crates/openlogi-gui/locales/es.yml @@ -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" diff --git a/crates/openlogi-gui/locales/fi.yml b/crates/openlogi-gui/locales/fi.yml index 44fe3b8c..17259ee4 100644 --- a/crates/openlogi-gui/locales/fi.yml +++ b/crates/openlogi-gui/locales/fi.yml @@ -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" diff --git a/crates/openlogi-gui/locales/fr.yml b/crates/openlogi-gui/locales/fr.yml index 1317cd65..7400c2f1 100644 --- a/crates/openlogi-gui/locales/fr.yml +++ b/crates/openlogi-gui/locales/fr.yml @@ -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" diff --git a/crates/openlogi-gui/locales/it.yml b/crates/openlogi-gui/locales/it.yml index 08b290b7..606aaa4e 100644 --- a/crates/openlogi-gui/locales/it.yml +++ b/crates/openlogi-gui/locales/it.yml @@ -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" diff --git a/crates/openlogi-gui/locales/ja.yml b/crates/openlogi-gui/locales/ja.yml index 61c9d9d0..bed9217e 100644 --- a/crates/openlogi-gui/locales/ja.yml +++ b/crates/openlogi-gui/locales/ja.yml @@ -150,6 +150,8 @@ _version: 1 "DPI Toggle": "DPI 切り替え" "Thumb Wheel": "サムホイール" "Gesture Button": "ジェスチャーボタン" +"Tilt Left": "左チルト" +"Tilt Right": "右チルト" "Up": "上" "Down": "下" "Left": "左" diff --git a/crates/openlogi-gui/locales/ko.yml b/crates/openlogi-gui/locales/ko.yml index c4504c0e..39704f20 100644 --- a/crates/openlogi-gui/locales/ko.yml +++ b/crates/openlogi-gui/locales/ko.yml @@ -150,6 +150,8 @@ _version: 1 "DPI Toggle": "DPI 전환" "Thumb Wheel": "썸휠" "Gesture Button": "제스처 버튼" +"Tilt Left": "왼쪽 틸트" +"Tilt Right": "오른쪽 틸트" "Up": "위" "Down": "아래" "Left": "왼쪽" diff --git a/crates/openlogi-gui/locales/nb.yml b/crates/openlogi-gui/locales/nb.yml index 2f0e55c4..ba1bff90 100644 --- a/crates/openlogi-gui/locales/nb.yml +++ b/crates/openlogi-gui/locales/nb.yml @@ -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" diff --git a/crates/openlogi-gui/locales/nl.yml b/crates/openlogi-gui/locales/nl.yml index 51f32483..4904eb0b 100644 --- a/crates/openlogi-gui/locales/nl.yml +++ b/crates/openlogi-gui/locales/nl.yml @@ -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" diff --git a/crates/openlogi-gui/locales/pl.yml b/crates/openlogi-gui/locales/pl.yml index fbec6b45..c5dd4987 100644 --- a/crates/openlogi-gui/locales/pl.yml +++ b/crates/openlogi-gui/locales/pl.yml @@ -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" diff --git a/crates/openlogi-gui/locales/pt-BR.yml b/crates/openlogi-gui/locales/pt-BR.yml index 4508e5ae..ae2f7bdd 100644 --- a/crates/openlogi-gui/locales/pt-BR.yml +++ b/crates/openlogi-gui/locales/pt-BR.yml @@ -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" diff --git a/crates/openlogi-gui/locales/pt-PT.yml b/crates/openlogi-gui/locales/pt-PT.yml index 0a13f08c..e39ab45e 100644 --- a/crates/openlogi-gui/locales/pt-PT.yml +++ b/crates/openlogi-gui/locales/pt-PT.yml @@ -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" diff --git a/crates/openlogi-gui/locales/ru.yml b/crates/openlogi-gui/locales/ru.yml index 32e1be75..837c416c 100644 --- a/crates/openlogi-gui/locales/ru.yml +++ b/crates/openlogi-gui/locales/ru.yml @@ -150,6 +150,8 @@ _version: 1 "DPI Toggle": "Переключение DPI" "Thumb Wheel": "Колесо под большой палец" "Gesture Button": "Кнопка жестов" +"Tilt Left": "Наклон влево" +"Tilt Right": "Наклон вправо" "Up": "Вверх" "Down": "Вниз" "Left": "Влево" diff --git a/crates/openlogi-gui/locales/sv.yml b/crates/openlogi-gui/locales/sv.yml index cc6cf27f..fb8e46b5 100644 --- a/crates/openlogi-gui/locales/sv.yml +++ b/crates/openlogi-gui/locales/sv.yml @@ -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" diff --git a/crates/openlogi-gui/locales/zh-CN.yml b/crates/openlogi-gui/locales/zh-CN.yml index aa217b46..8c8335f9 100644 --- a/crates/openlogi-gui/locales/zh-CN.yml +++ b/crates/openlogi-gui/locales/zh-CN.yml @@ -150,6 +150,8 @@ _version: 1 "DPI Toggle": "灵敏度切换" "Thumb Wheel": "拇指滚轮" "Gesture Button": "手势按钮" +"Tilt Left": "向左倾斜" +"Tilt Right": "向右倾斜" "Up": "上" "Down": "下" "Left": "左" diff --git a/crates/openlogi-gui/locales/zh-HK.yml b/crates/openlogi-gui/locales/zh-HK.yml index e11ad21a..bfc47f6f 100644 --- a/crates/openlogi-gui/locales/zh-HK.yml +++ b/crates/openlogi-gui/locales/zh-HK.yml @@ -150,6 +150,8 @@ _version: 1 "DPI Toggle": "靈敏度切換" "Thumb Wheel": "拇指滾輪" "Gesture Button": "手勢按鈕" +"Tilt Left": "向左傾斜" +"Tilt Right": "向右傾斜" "Up": "上" "Down": "下" "Left": "左" diff --git a/crates/openlogi-gui/locales/zh-TW.yml b/crates/openlogi-gui/locales/zh-TW.yml index 5d51239b..820b5dc8 100644 --- a/crates/openlogi-gui/locales/zh-TW.yml +++ b/crates/openlogi-gui/locales/zh-TW.yml @@ -150,6 +150,8 @@ _version: 1 "DPI Toggle": "靈敏度切換" "Thumb Wheel": "拇指滾輪" "Gesture Button": "手勢按鈕" +"Tilt Left": "向左傾斜" +"Tilt Right": "向右傾斜" "Up": "上" "Down": "下" "Left": "左" From a8d9ff4f2cb2b74ce3d6e0a738bd94f2edfc7f39 Mon Sep 17 00:00:00 2001 From: George Nick Gorzynski Date: Sun, 5 Jul 2026 15:53:43 +0100 Subject: [PATCH 6/6] docs(tilt): note wheel-tilt buttons in capture/dispatch doc comments Address code-review: the module docs mentioned wheel tilt but the CapturedInput::ButtonPressed variant doc, the agent-core watcher module doc, and the ButtonId doc still described the pre-tilt set. Also clarify that ButtonId variants are appended (TOML-stable) while ButtonId::ALL carries the physical layout order. --- crates/openlogi-agent-core/src/watchers/gesture.rs | 3 ++- crates/openlogi-core/src/binding.rs | 7 ++++--- crates/openlogi-hid/src/gesture.rs | 3 ++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/openlogi-agent-core/src/watchers/gesture.rs b/crates/openlogi-agent-core/src/watchers/gesture.rs index c1f797fd..e0d79653 100644 --- a/crates/openlogi-agent-core/src/watchers/gesture.rs +++ b/crates/openlogi-agent-core/src/watchers/gesture.rs @@ -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 diff --git a/crates/openlogi-core/src/binding.rs b/crates/openlogi-core/src/binding.rs index 3cef2bd4..c4d27d53 100644 --- a/crates/openlogi-core/src/binding.rs +++ b/crates/openlogi-core/src/binding.rs @@ -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, diff --git a/crates/openlogi-hid/src/gesture.rs b/crates/openlogi-hid/src/gesture.rs index 4b28c9e9..b306dcac 100644 --- a/crates/openlogi-hid/src/gesture.rs +++ b/crates/openlogi-hid/src/gesture.rs @@ -40,7 +40,8 @@ pub enum CapturedInput { /// A completed gesture-button swipe. Gesture(GestureDirection), /// A diverted button was pressed — the DPI/ModeShift button - /// ([`ButtonId::DpiToggle`]) or the thumb-wheel single tap + /// ([`ButtonId::DpiToggle`]), a wheel-tilt button ([`ButtonId::TiltLeft`] / + /// [`ButtonId::TiltRight`]), or the thumb-wheel single tap /// ([`ButtonId::Thumbwheel`]). ButtonPressed(ButtonId), /// Thumb-wheel rotation to re-synthesise as horizontal scroll, in the