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
2 changes: 1 addition & 1 deletion crates/openlogi-agent-core/src/capture_plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ pub fn plan_for_device(
// One direction map per HID++ source in gesture mode — several may
// gesture at once, each armed with its own raw-XY divert (the watcher
// derives the CIDs to divert from this map's keys).
let gesture_bindings = hidpp_gesture_maps_for(config, Some(config_key));
let gesture_bindings = hidpp_gesture_maps_for(config, Some(config_key), app);
// The HID++ gesture sources never reach the OS hook, so a non-default
// single binding on one is deliverable only via a plain HID++ divert — but
// only while the source is NOT in gesture mode (the raw-XY gesture divert
Expand Down
22 changes: 21 additions & 1 deletion crates/openlogi-agent-core/src/orchestrator/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use super::{
any_device_needs_capture_rearm, build_devices, configured_wheel_mode, host_switch_links,
pick_current, plan_reapply, reapply_targets,
};
use openlogi_core::binding::{Action, ButtonId};
use openlogi_core::binding::{Action, ButtonId, GestureDirection};
use openlogi_core::config::{Config, LightSettings, ScrollResolution};
use openlogi_core::device::{
Capabilities, DeviceInventory, DeviceKind, DeviceModelInfo, DeviceTransports,
Expand Down Expand Up @@ -745,6 +745,18 @@ fn published_back_binding(orch: &Orchestrator) -> Option<Action> {
})
}

/// The published dedicated HID++ gesture button's plain-click action.
fn published_gesture_click(orch: &Orchestrator) -> Option<Action> {
orch.shared.capture_plans.read().ok().and_then(|plans| {
plans.first().and_then(|plan| {
plan.gesture_bindings
.get(&ButtonId::GestureButton)
.and_then(|map| map.get(&GestureDirection::Click))
.cloned()
})
})
}

#[test]
fn app_switch_republishes_capture_plans() {
// HID++ dispatch reads `plan.bindings` at event time, so a
Expand All @@ -758,6 +770,12 @@ fn app_switch_republishes_capture_plans() {
ButtonId::Back,
Some(Action::Undo),
);
config.set_per_app_binding(
"a",
"com.example.editor",
ButtonId::GestureButton,
Some(Action::BrowserBack),
);
let mut orch = orchestrator(config);
orch.devices = vec![dev("a", 1, true)];
orch.rebuild();
Expand All @@ -766,6 +784,8 @@ fn app_switch_republishes_capture_plans() {
Some(Action::Undo),
"no per-app overlay while no app is in front"
);
assert_ne!(published_gesture_click(&orch), Some(Action::BrowserBack));
orch.set_current_app(Some("com.example.editor".into()));
assert_eq!(published_back_binding(&orch), Some(Action::Undo));
assert_eq!(published_gesture_click(&orch), Some(Action::BrowserBack));
}
61 changes: 56 additions & 5 deletions crates/openlogi-core/src/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,20 @@ pub fn bindings_for(
/// keyed by the button its captured swipes dispatch as. Each map is seeded
/// via [`Binding::fill_gesture_defaults`] — the one canonical seeding rule —
/// so the watcher always dispatches the full five-direction set the GUI
/// shows. Empty when no HID++ source gestures (or `config_key` is `None`).
/// shows. A per-app single-action override replaces only that source's Click
/// arm; its global swipe arms and gesture mode remain intact. Empty when no
/// HID++ source gestures (or `config_key` is `None`).
#[must_use]
pub fn hidpp_gesture_maps_for(
config: &Config,
config_key: Option<&str>,
app_bundle: Option<&str>,
) -> BTreeMap<ButtonId, BTreeMap<GestureDirection, Action>> {
let Some(key) = config_key else {
return BTreeMap::new();
};
let stored = config.bindings_for(key);
let effective = app_bundle.map(|app| config.effective_bindings(key, Some(app)));
ButtonId::ALL
.iter()
.copied()
Expand All @@ -75,7 +79,17 @@ pub fn hidpp_gesture_maps_for(
.unwrap_or_else(|| default_binding_for(button));
binding.fill_gesture_defaults();
match binding {
Binding::Gesture(map) => Some((button, map)),
Binding::Gesture(mut map) => {
// HID++ gesture sources never reach the OS hook. Keep the
// global gesture shape armed and apply an app's single
// override to the plain press only, preserving swipes.
if let Some(Binding::Single(action)) =
effective.as_ref().and_then(|map| map.get(&button))
{
map.insert(GestureDirection::Click, action.clone());
}
Some((button, map))
}
Binding::Single(_) => None,
}
})
Expand Down Expand Up @@ -220,7 +234,7 @@ mod tests {
let mut cfg = Config::default();
cfg.set_gesture_mode("2b042", ButtonId::HapticPanel, true);

let maps = hidpp_gesture_maps_for(&cfg, Some("2b042"));
let maps = hidpp_gesture_maps_for(&cfg, Some("2b042"), None);
// The dedicated button gestures by default...
let dedicated = maps
.get(&ButtonId::GestureButton)
Expand Down Expand Up @@ -273,7 +287,7 @@ mod tests {
// Default device: the dedicated HID++ gesture button gestures, with its
// defaults seeded.
let mut cfg = Config::default();
let maps = hidpp_gesture_maps_for(&cfg, Some("2b042"));
let maps = hidpp_gesture_maps_for(&cfg, Some("2b042"), None);
assert_eq!(
maps.get(&ButtonId::GestureButton)
.and_then(|m| m.get(&GestureDirection::Up)),
Expand All @@ -286,8 +300,45 @@ mod tests {
cfg.set_gesture_mode("2b042", ButtonId::GestureButton, false);
cfg.set_gesture_mode("2b042", ButtonId::Back, true);
assert!(
hidpp_gesture_maps_for(&cfg, Some("2b042")).is_empty(),
hidpp_gesture_maps_for(&cfg, Some("2b042"), None).is_empty(),
"a demoted dedicated button must dispatch nothing over HID++"
);
}

#[test]
fn hidpp_source_applies_per_app_click_and_preserves_swipes() {
let mut cfg = Config::default();
cfg.set_per_app_binding(
"2b042",
"com.apple.Safari",
ButtonId::GestureButton,
Some(Action::BrowserBack),
);

let global = hidpp_gesture_maps_for(&cfg, Some("2b042"), None);
let safari = hidpp_gesture_maps_for(&cfg, Some("2b042"), Some("com.apple.Safari"));
let global_map = global
.get(&ButtonId::GestureButton)
.expect("default HID++ gesture map");
let safari_map = safari
.get(&ButtonId::GestureButton)
.expect("app override must keep gesture mode armed");

assert_eq!(
safari_map.get(&GestureDirection::Click),
Some(&Action::BrowserBack)
);
for direction in [
GestureDirection::Up,
GestureDirection::Down,
GestureDirection::Left,
GestureDirection::Right,
] {
assert_eq!(
safari_map.get(&direction),
global_map.get(&direction),
"per-app click override changed {direction:?}"
);
}
}
}
2 changes: 1 addition & 1 deletion crates/openlogi-desktop/src/state/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ impl AppState {
// so the menus can never drift from what the agent actually does:
// HID++ sources seeded like the gesture watcher, OS-hook buttons raw
// like the hook (global view — no per-app overlay here).
let mut maps = hidpp_gesture_maps_for(&self.config, Some(key));
let mut maps = hidpp_gesture_maps_for(&self.config, Some(key), None);
maps.extend(oshook_gestures_for(&self.config, Some(key), None));
maps
}
Expand Down