use std::collections::BTreeMap;
use crate::binding::{
Action, Binding, ButtonId, GestureDirection, default_binding, default_binding_for,
};
use crate::config::Config;
#[must_use]
pub fn bindings_for(
config: &Config,
config_key: Option<&str>,
app_bundle: Option<&str>,
) -> BTreeMap<ButtonId, Action> {
let stored = config_key
.map(|key| config.effective_bindings(key, app_bundle))
.unwrap_or_default();
let mut bindings: BTreeMap<ButtonId, Action> = ButtonId::ALL
.iter()
.copied()
.map(|b| (b, default_binding(b)))
.collect();
for (k, binding) in stored {
if binding.is_gesture() && binding.direction_action(GestureDirection::Click).is_none() {
continue;
}
bindings.insert(k, binding.click_action());
}
bindings
}
#[must_use]
pub fn hidpp_gesture_maps_for(
config: &Config,
config_key: Option<&str>,
) -> BTreeMap<ButtonId, BTreeMap<GestureDirection, Action>> {
let Some(key) = config_key else {
return BTreeMap::new();
};
let stored = config.bindings_for(key);
ButtonId::ALL
.iter()
.copied()
.filter(|button| button.is_hidpp_gesture_source())
.filter_map(|button| {
let mut binding = stored
.get(&button)
.cloned()
.unwrap_or_else(|| default_binding_for(button));
binding.fill_gesture_defaults();
match binding {
Binding::Gesture(map) => Some((button, map)),
Binding::Single(_) => None,
}
})
.collect()
}
#[must_use]
pub fn oshook_gestures_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();
};
config
.effective_bindings(key, app_bundle)
.into_iter()
.filter(|(id, _)| id.is_os_hook_button())
.filter_map(|(id, binding)| match binding {
Binding::Gesture(map) => Some((id, map)),
Binding::Single(_) => None,
})
.collect()
}
#[cfg(test)]
#[allow(clippy::expect_used, reason = "expect/unwrap are idiomatic in tests")]
mod tests {
use crate::binding::default_gesture_binding;
use super::*;
#[test]
fn click_less_gesture_keeps_default_click_in_projection() {
let mut cfg = Config::default();
let mut map = BTreeMap::new();
map.insert(GestureDirection::Up, Action::Copy);
cfg.set_binding("2b042", ButtonId::GestureButton, Binding::Gesture(map));
let projected = bindings_for(&cfg, Some("2b042"), None);
assert_eq!(
projected.get(&ButtonId::GestureButton),
Some(&default_binding(ButtonId::GestureButton)),
"a Click-less gesture must keep the default click, not None"
);
}
#[test]
fn explicit_gesture_click_overrides_default_in_projection() {
let mut cfg = Config::default();
let mut map = BTreeMap::new();
map.insert(GestureDirection::Click, Action::Paste);
cfg.set_binding("2b042", ButtonId::GestureButton, Binding::Gesture(map));
let projected = bindings_for(&cfg, Some("2b042"), None);
assert_eq!(
projected.get(&ButtonId::GestureButton),
Some(&Action::Paste)
);
}
#[test]
fn oshook_gestures_collects_only_os_hook_gesture_buttons() {
let mut cfg = Config::default();
cfg.set_binding(
"2b042",
ButtonId::Back,
Binding::Gesture(BTreeMap::from([(GestureDirection::Up, Action::Copy)])),
);
cfg.set_binding("2b042", ButtonId::MiddleClick, Action::MiddleClick.into());
cfg.set_binding(
"2b042",
ButtonId::GestureButton,
Binding::Gesture(BTreeMap::from([(
GestureDirection::Up,
Action::MissionControl,
)])),
);
let oshook = oshook_gestures_for(&cfg, Some("2b042"), None);
assert_eq!(oshook.len(), 1, "only the gesture-mode Back belongs here");
assert_eq!(
oshook.get(&ButtonId::Back),
Some(&BTreeMap::from([(GestureDirection::Up, Action::Copy)]))
);
assert!(!oshook.contains_key(&ButtonId::MiddleClick));
assert!(!oshook.contains_key(&ButtonId::GestureButton));
}
#[test]
fn oshook_gestures_includes_every_gesture_mode_button() {
let mut cfg = Config::default();
cfg.set_gesture_mode("2b042", ButtonId::Back, true);
cfg.set_gesture_mode("2b042", ButtonId::MiddleClick, true);
let oshook = oshook_gestures_for(&cfg, Some("2b042"), None);
assert!(oshook.contains_key(&ButtonId::Back), "got: {oshook:?}");
assert!(
oshook.contains_key(&ButtonId::MiddleClick),
"got: {oshook:?}"
);
}
#[test]
fn hidpp_gesture_maps_includes_every_gesture_mode_source() {
let mut cfg = Config::default();
cfg.set_gesture_mode("2b042", ButtonId::HapticPanel, true);
let maps = hidpp_gesture_maps_for(&cfg, Some("2b042"));
let dedicated = maps
.get(&ButtonId::GestureButton)
.expect("the dedicated button's default gesture mode must survive");
assert_eq!(
dedicated.get(&GestureDirection::Up),
Some(&default_gesture_binding(GestureDirection::Up))
);
let panel = maps
.get(&ButtonId::HapticPanel)
.expect("a gesture-mode panel must dispatch");
for dir in GestureDirection::ALL {
assert!(panel.contains_key(&dir), "unseeded panel arm {dir:?}");
}
}
#[test]
fn per_app_override_drops_the_owner_from_the_oshook_gesture_set() {
let mut cfg = Config::default();
cfg.set_gesture_mode("2b042", ButtonId::Back, true);
assert!(
oshook_gestures_for(&cfg, Some("2b042"), None).contains_key(&ButtonId::Back),
"Back gestures globally"
);
cfg.set_per_app_binding(
"2b042",
"com.apple.Safari",
ButtonId::Back,
Some(Action::NextTab),
);
assert!(
oshook_gestures_for(&cfg, Some("2b042"), Some("com.apple.Safari")).is_empty(),
"a per-app override of the owner removes it from the gesture set"
);
assert!(
oshook_gestures_for(&cfg, Some("2b042"), Some("com.other.App"))
.contains_key(&ButtonId::Back)
);
}
#[test]
fn hidpp_maps_silent_for_a_demoted_dedicated_button() {
let mut cfg = Config::default();
let maps = hidpp_gesture_maps_for(&cfg, Some("2b042"));
assert_eq!(
maps.get(&ButtonId::GestureButton)
.and_then(|m| m.get(&GestureDirection::Up)),
Some(&default_gesture_binding(GestureDirection::Up)),
"the dedicated button gestures by default, seeded"
);
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(),
"a demoted dedicated button must dispatch nothing over HID++"
);
}
}