1use std::collections::BTreeMap;
8
9use crate::binding::{
10 Action, Binding, ButtonId, GestureDirection, default_binding, default_binding_for,
11};
12use crate::config::Config;
13
14#[must_use]
23pub fn bindings_for(
24 config: &Config,
25 config_key: Option<&str>,
26 app_bundle: Option<&str>,
27) -> BTreeMap<ButtonId, Action> {
28 let stored = config_key
29 .map(|key| config.effective_bindings(key, app_bundle))
30 .unwrap_or_default();
31 let mut bindings: BTreeMap<ButtonId, Action> = ButtonId::ALL
32 .iter()
33 .copied()
34 .map(|b| (b, default_binding(b)))
35 .collect();
36 for (k, binding) in stored {
37 if binding.is_gesture() && binding.direction_action(GestureDirection::Click).is_none() {
43 continue;
44 }
45 bindings.insert(k, binding.click_action());
46 }
47 bindings
48}
49
50#[must_use]
57pub fn hidpp_gesture_maps_for(
58 config: &Config,
59 config_key: Option<&str>,
60) -> BTreeMap<ButtonId, BTreeMap<GestureDirection, Action>> {
61 let Some(key) = config_key else {
62 return BTreeMap::new();
63 };
64 let stored = config.bindings_for(key);
65 ButtonId::ALL
66 .iter()
67 .copied()
68 .filter(|button| button.is_hidpp_gesture_source())
69 .filter_map(|button| {
70 let mut binding = stored
73 .get(&button)
74 .cloned()
75 .unwrap_or_else(|| default_binding_for(button));
76 binding.fill_gesture_defaults();
77 match binding {
78 Binding::Gesture(map) => Some((button, map)),
79 Binding::Single(_) => None,
80 }
81 })
82 .collect()
83}
84
85#[must_use]
107pub fn oshook_gestures_for(
108 config: &Config,
109 config_key: Option<&str>,
110 app_bundle: Option<&str>,
111) -> BTreeMap<ButtonId, BTreeMap<GestureDirection, Action>> {
112 let Some(key) = config_key else {
113 return BTreeMap::new();
114 };
115 config
118 .effective_bindings(key, app_bundle)
119 .into_iter()
120 .filter(|(id, _)| id.is_os_hook_button())
121 .filter_map(|(id, binding)| match binding {
122 Binding::Gesture(map) => Some((id, map)),
123 Binding::Single(_) => None,
124 })
125 .collect()
126}
127
128#[cfg(test)]
129#[allow(clippy::expect_used, reason = "expect/unwrap are idiomatic in tests")]
130mod tests {
131 use crate::binding::default_gesture_binding;
132
133 use super::*;
134
135 #[test]
136 fn click_less_gesture_keeps_default_click_in_projection() {
137 let mut cfg = Config::default();
141 let mut map = BTreeMap::new();
142 map.insert(GestureDirection::Up, Action::Copy);
143 cfg.set_binding("2b042", ButtonId::GestureButton, Binding::Gesture(map));
144
145 let projected = bindings_for(&cfg, Some("2b042"), None);
146 assert_eq!(
147 projected.get(&ButtonId::GestureButton),
148 Some(&default_binding(ButtonId::GestureButton)),
149 "a Click-less gesture must keep the default click, not None"
150 );
151 }
152
153 #[test]
154 fn explicit_gesture_click_overrides_default_in_projection() {
155 let mut cfg = Config::default();
157 let mut map = BTreeMap::new();
158 map.insert(GestureDirection::Click, Action::Paste);
159 cfg.set_binding("2b042", ButtonId::GestureButton, Binding::Gesture(map));
160
161 let projected = bindings_for(&cfg, Some("2b042"), None);
162 assert_eq!(
163 projected.get(&ButtonId::GestureButton),
164 Some(&Action::Paste)
165 );
166 }
167
168 #[test]
169 fn oshook_gestures_collects_only_os_hook_gesture_buttons() {
170 let mut cfg = Config::default();
171 cfg.set_binding(
173 "2b042",
174 ButtonId::Back,
175 Binding::Gesture(BTreeMap::from([(GestureDirection::Up, Action::Copy)])),
176 );
177 cfg.set_binding("2b042", ButtonId::MiddleClick, Action::MiddleClick.into());
179 cfg.set_binding(
182 "2b042",
183 ButtonId::GestureButton,
184 Binding::Gesture(BTreeMap::from([(
185 GestureDirection::Up,
186 Action::MissionControl,
187 )])),
188 );
189
190 let oshook = oshook_gestures_for(&cfg, Some("2b042"), None);
191 assert_eq!(oshook.len(), 1, "only the gesture-mode Back belongs here");
192 assert_eq!(
193 oshook.get(&ButtonId::Back),
194 Some(&BTreeMap::from([(GestureDirection::Up, Action::Copy)]))
195 );
196 assert!(!oshook.contains_key(&ButtonId::MiddleClick));
197 assert!(!oshook.contains_key(&ButtonId::GestureButton));
198 }
199
200 #[test]
201 fn oshook_gestures_includes_every_gesture_mode_button() {
202 let mut cfg = Config::default();
205 cfg.set_gesture_mode("2b042", ButtonId::Back, true);
206 cfg.set_gesture_mode("2b042", ButtonId::MiddleClick, true);
207
208 let oshook = oshook_gestures_for(&cfg, Some("2b042"), None);
209 assert!(oshook.contains_key(&ButtonId::Back), "got: {oshook:?}");
210 assert!(
211 oshook.contains_key(&ButtonId::MiddleClick),
212 "got: {oshook:?}"
213 );
214 }
215
216 #[test]
217 fn hidpp_gesture_maps_includes_every_gesture_mode_source() {
218 let mut cfg = Config::default();
221 cfg.set_gesture_mode("2b042", ButtonId::HapticPanel, true);
222
223 let maps = hidpp_gesture_maps_for(&cfg, Some("2b042"));
224 let dedicated = maps
226 .get(&ButtonId::GestureButton)
227 .expect("the dedicated button's default gesture mode must survive");
228 assert_eq!(
229 dedicated.get(&GestureDirection::Up),
230 Some(&default_gesture_binding(GestureDirection::Up))
231 );
232 let panel = maps
234 .get(&ButtonId::HapticPanel)
235 .expect("a gesture-mode panel must dispatch");
236 for dir in GestureDirection::ALL {
237 assert!(panel.contains_key(&dir), "unseeded panel arm {dir:?}");
238 }
239 }
240
241 #[test]
242 fn per_app_override_drops_the_owner_from_the_oshook_gesture_set() {
243 let mut cfg = Config::default();
245 cfg.set_gesture_mode("2b042", ButtonId::Back, true);
246 assert!(
247 oshook_gestures_for(&cfg, Some("2b042"), None).contains_key(&ButtonId::Back),
248 "Back gestures globally"
249 );
250
251 cfg.set_per_app_binding(
255 "2b042",
256 "com.apple.Safari",
257 ButtonId::Back,
258 Some(Action::NextTab),
259 );
260 assert!(
261 oshook_gestures_for(&cfg, Some("2b042"), Some("com.apple.Safari")).is_empty(),
262 "a per-app override of the owner removes it from the gesture set"
263 );
264 assert!(
266 oshook_gestures_for(&cfg, Some("2b042"), Some("com.other.App"))
267 .contains_key(&ButtonId::Back)
268 );
269 }
270
271 #[test]
272 fn hidpp_maps_silent_for_a_demoted_dedicated_button() {
273 let mut cfg = Config::default();
276 let maps = hidpp_gesture_maps_for(&cfg, Some("2b042"));
277 assert_eq!(
278 maps.get(&ButtonId::GestureButton)
279 .and_then(|m| m.get(&GestureDirection::Up)),
280 Some(&default_gesture_binding(GestureDirection::Up)),
281 "the dedicated button gestures by default, seeded"
282 );
283
284 cfg.set_gesture_mode("2b042", ButtonId::GestureButton, false);
287 cfg.set_gesture_mode("2b042", ButtonId::Back, true);
288 assert!(
289 hidpp_gesture_maps_for(&cfg, Some("2b042")).is_empty(),
290 "a demoted dedicated button must dispatch nothing over HID++"
291 );
292 }
293}