# How OpenLogi Captures and Remaps Gestures with Per-Direction Bindings and Live Capture

> Learn how OpenLogi captures and remaps gestures using per-direction bindings and live capture. Divert controls, accumulate motion, and map gestures to custom actions efficiently.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: how-to-guide
- Published: 2026-09-02

---

**OpenLogi captures gestures through a single HID++ device session that diverts controls, accumulates raw-XY motion into directional swipes, and maps each `GestureDirection` to user-configured actions via per-button binding tables.**

OpenLogi provides **live gesture capture and remapping** for Logitech devices by implementing a unified device-session architecture. The system intercepts raw input at the HID++ protocol layer, transforms motion data into directional gestures, and routes the results to a configurable binding system. This article walks through the complete pipeline, from channel establishment to per-direction action execution, using the actual source implementation in `AprilNEA/OpenLogi`.

## The Capture Session Architecture

OpenLogi centralizes gesture handling in `run_capture_session` within [[`crates/openlogi-device/src/session/gesture.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-device/src/session/gesture.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-device/src/session/gesture.rs). This function opens a **single HID++ channel** per device rather than splitting input across multiple streams, preventing race conditions and simplifying state management.

The session begins with a **CaptureSpec** that declares which controls to divert:

```rust
pub struct CaptureSpec {
    pub capture_thumbwheel: bool,           // Thumb-wheel rotation and tap
    pub divert_gesture_sources: Vec<Cid>,   // Dedicated gesture button or Haptic panel (raw-XY)
    pub divert_gesture_buttons: Vec<Cid>,   // Standard buttons with raw-XY support
    pub divert_buttons: Vec<Cid>,           // Plain-press diversion for non-default bindings
}

```

Each flag corresponds to a specific hardware capability. The `capture_thumbwheel` field handles devices like the MX Master series with horizontal scroll wheels, while `divert_gesture_sources` targets the dedicated gesture button or Haptic touch panel found on premium Logitech mice.

## Arming Controls for Diverted Reporting

Before receiving input, the session must **arm** the selected controls. The `arm_controls` function (via `arm_controls_into`) writes to the device's **ReprogControlsV4** table to enable **diverted reporting mode**.

Diverted reporting instructs the device to send raw HID++ messages instead of standard HID reports, allowing OpenLogi to intercept and reinterpret the data. For controls supporting **raw-XY**, the arming process also requests absolute motion deltas.

The original reporting state is preserved in an `ArmedReporting` structure:

```rust
// From reprog_controls.rs - stores pre-diversion state for restoration
pub struct ArmedReporting {
    pub cid: Cid,
    pub original_flags: ControlFlags,
    pub supports_raw_xy: bool,
}

```

This enables clean shutdown: when the session ends (normally or via error), `finish_capture` → `restore_after_stop` reverts every control to its original state using the logic in [[`capture_restore.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/capture_restore.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-device/src/session/capture_restore.rs).

## Parsing Incoming HID++ Reports

Once armed, the session installs a **message listener** that classifies every `v20::Message` into one of three processing paths:

### Path 1: Reprogrammable Control Events

 decoded by `reprog_controls::decode_event`. The `handle_reprog_with_gesture_buttons` function updates a shared `CaptureAccum` and emits `CapturedInput` variants:

- `CapturedInput::ButtonDown` / `CapturedInput::ButtonUp` — discrete press/release
- `CapturedInput::DpiSwitch` — DPI toggle button activation
- `CapturedInput::Gesture(button, direction)` — completed swipe with directional classification

### Path 2: Thumb-Wheel Events

decoded by `thumbwheel::decode_event` in [[`thumbwheel.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/thumbwheel.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-device/src/thumbwheel.rs). The `thumbwheel_input` function produces:

- `CapturedInput::Scroll { increments, resolution }` — rotational movement
- `CapturedInput::ButtonPulse` — wheel tap/click

### Path 3: Raw-XY Motion

fed into `handle_raw_xy` when a gesture button is held. Motion deltas accumulate until they cross the gesture threshold.

## Swipe Accumulation and Direction Detection

The [`SwipeAccumulator`](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-core/src/swipe.rs) transforms raw motion into semantic directions. Located in [`crates/openlogi-core/src/swipe.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/swipe.rs), this component:

1. Accumulates `(dx, dy)` deltas while `HoldState::Holding` is active
2. Compares cumulative displacement against `GESTURE_SWIPE_THRESHOLD`
3. Once exceeded, determines the dominant axis and sign
4. Yields a `GestureDirection`: `Up`, `Down`, `Left`, `Right`, or `Click` (for taps without motion)

The accumulator implements **minimum distance gating** to prevent accidental triggers from micro-adjustments, while still maintaining responsive feel for intentional swipes.

## Per-Direction Binding Resolution

`CapturedInput` events traverse an **unbounded Tokio channel** (`mpsc::unbounded_channel`) to the GUI process in `openlogi-desktop`. The binding resolution occurs in [[`crates/openlogi-desktop/src/state/bindings.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/state/bindings.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-desktop/src/state/bindings.rs):

```rust
// bindings.rs - per-button, per-direction action mapping
pub struct GestureBindings {
    // ButtonId → {GestureDirection → Action}
    inner: BTreeMap<ButtonId, BTreeMap<GestureDirection, Action>>,
}

```

Each `ButtonId` (defined in [[`binding.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/binding.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-core/src/binding.rs)) maintains its own `BTreeMap` from direction to action. This structure enables **complex chorded configurations**:

- **GestureButton** + `Up` → Copy
- **GestureButton** + `Down` → Paste  
- **GestureButton** + `Left` → Undo
- **GestureButton** + `Right` → Redo
- **GestureButton** + `Click` (tap, no direction) → Middle-click

The `GestureDirection` enum in [`binding.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/binding.rs) provides exhaustive pattern matching for all cases:

```rust
pub enum GestureDirection {
    Up,
    Down,
    Left,
    Right,
    Click,  // No motion detected—treated as tap
}

```

## Live Capture Implementation Example

The following demonstrates establishing a complete capture session with per-direction handling:

```rust
use openlogi_device::session::{run_capture_session, CaptureSpec};
use openlogi_core::binding::{ButtonId, GestureDirection};
use openlogi_core::action::Action;
use tokio::sync::mpsc;

// Configure which hardware features to divert
let spec = CaptureSpec {
    capture_thumbwheel: true,
    divert_gesture_sources: vec![reprog_controls::GESTURE_BUTTON_CID],
    divert_gesture_buttons: vec![],
    divert_buttons: vec![],
    ..Default::default()
};

// Channel for live capture streaming
let (tx, mut rx) = mpsc::unbounded_channel::<CapturedInput>();

// Spawn the capture session
let session_handle = tokio::spawn(async move {
    run_capture_session(
        backend,      // HidBackend implementation
        device_route, // DeviceRoute identifying the mouse
        spec,
        tx,           // Event sink
        shutdown_rx,  // Graceful termination signal
        Default::default(),
        Default::default(),
    ).await
});

// Process captured inputs with per-direction binding map
while let Some(input) = rx.recv().await {
    match input {
        CapturedInput::Gesture(btn, dir) => {
            let action = resolve_gesture_binding(btn, dir);
            inject_action(action).await;
        }
        CapturedInput::Scroll { increments, resolution } => {
            synthesize_scroll(increments, resolution).await;
        }
        CapturedInput::ButtonPulse => {
            // Thumb-wheel tap—handle as configured
        }
        _ => {}
    }
}

fn resolve_gesture_binding(btn: ButtonId, dir: GestureDirection) -> Action {
    // Lookup in BTreeMap<GestureDirection, Action>
    GESTURE_BINDINGS.get(&btn)
        .and_then(|m| m.get(&dir))
        .copied()
        .unwrap_or(Action::NoOp)
}

```

## Session Liveness and Auto-Recovery

The `CaptureLiveness` watchdog ensures **uninterrupted capture**. It periodically emits `root.ping(0x5a)` heartbeat messages; if the channel falls silent, the watchdog triggers automatic session restart. This prevents "stuck" states from device sleep/wake cycles or USB transient errors.

On any termination path, the session guarantees **state restoration**:

```rust
// gesture.rs - cleanup sequence
async fn finish_capture(&mut self) {
    self.restore_after_stop().await;  // Return controls to original mode
    self.clear_shared_channel_slot(); // Release for next session
}

```

## Component Reference

| File | Purpose |
|------|---------|
| [[`gesture.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/gesture.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-device/src/session/gesture.rs) | Core session: channel management, arming, event routing |
| [[`swipe.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/swipe.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-core/src/swipe.rs) | `SwipeAccumulator` for delta-to-direction conversion |
| [[`bindings.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/bindings.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-desktop/src/state/bindings.rs) | Per-button/per-direction action maps |
| [[`binding.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/binding.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-core/src/binding.rs) | `ButtonId`, `GestureDirection` type definitions |
| [[`reprog_controls.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/reprog_controls.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-device/src/reprog_controls.rs) | `ReprogControlsV4` table operations |
| [[`thumbwheel.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/thumbwheel.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-device/src/thumbwheel.rs) | Wheel-specific event decoding |
| [[`capture_restore.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/capture_restore.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-device/src/session/capture_restore.rs) | State preservation and restoration |

## Summary

- **Single HID++ channel** prevents input stream fragmentation and race conditions
- **CaptureSpec** declaratively configures which hardware features to divert
- **Arming process** writes `ReprogControlsV4` flags and preserves original state in `ArmedReporting`
- **Three report paths** handle reprogrammable controls, thumb-wheel events, and raw-XY motion
- **SwipeAccumulator** applies threshold-based direction detection from accumulated deltas
- **Per-direction bindings** use `BTreeMap<ButtonId, BTreeMap<GestureDirection, Action>>` for flexible configuration
- **Liveness watchdog** ensures automatic recovery from channel failures
- **Guaranteed restoration** returns devices to original state on session end

## Frequently Asked Questions

### What hardware does OpenLogi gesture capture support?

OpenLogi targets Logitech devices implementing the **HID++ 2.0 protocol**, particularly those with reprogrammable controls and raw-XY capable buttons. This includes the MX Master series (gesture button, thumb-wheel), MX Anywhere, and devices with Haptic touch panels. The `divert_gesture_sources` and `divert_gesture_buttons` fields in `CaptureSpec` allow selective enablement based on detected hardware capabilities.

### How does OpenLogi distinguish between a tap and a directional swipe?

The `SwipeAccumulator` maintains a `HoldState` tracking whether motion has exceeded `GESTURE_SWIPE_THRESHOLD`. If the button releases before threshold crossing, the session emits `GestureDirection::Click`. If threshold is exceeded, the dominant axis determines `Up`/`Down`/`Left`/`Right`. This occurs in `handle_raw_xy` within the gesture session before `CapturedInput` reaches the binding layer.

### Can the same physical button have different actions for different swipe directions?

Yes—this is the core design of the binding system. The `GestureBindings` structure in [`bindings.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/bindings.rs) stores a `BTreeMap<GestureDirection, Action>` for each `ButtonId`, enabling independent configuration per direction. A single gesture button can trigger Copy (up), Paste (down), Undo (left), Redo (right), and Middle-click (tap) simultaneously.

### What happens if the USB connection drops during active capture?

The `CaptureLiveness` watchdog detects silence via missed ping responses and initiates automatic session restart. The restoration logic in [`capture_restore.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/capture_restore.rs) first attempts to return the device to original reporting state; if this fails (device unplugged), the error is logged and the shared channel slot is cleared for re-acquisition when the device reconnects.