Skip to main content

matchmaker/
binds.rs

1use std::{
2    cmp::Ordering,
3    collections::HashMap,
4    fmt::{self, Display},
5    str::FromStr,
6};
7
8use serde::{
9    Deserializer,
10    de::{self, Visitor},
11    ser,
12};
13
14use crate::{
15    action::{Action, ActionExt, Actions, NullActionExt},
16    config::HelpColorConfig,
17    message::Event,
18};
19
20pub use crate::bindmap;
21pub use crokey::{KeyCombination, key};
22pub use crossterm::event::{KeyModifiers, MouseButton, MouseEventKind};
23
24#[allow(type_alias_bounds)]
25pub type BindMap<A: ActionExt = NullActionExt> = HashMap<Trigger, Actions<A>>;
26
27#[easy_ext::ext(BindMapExt)]
28impl<A: ActionExt> BindMap<A> {
29    #[allow(unused_mut)]
30    pub fn default_binds() -> Self {
31        let mut ret = bindmap!(
32            key!(ctrl-c) => Action::Quit(1),
33            key!(esc) => Action::Quit(1),
34            key!(up) => Action::Up(1),
35            key!(down) => Action::Down(1),
36            key!(enter) => Action::Accept,
37            key!(right) => Action::ForwardChar,
38            key!(left) => Action::BackwardChar,
39            key!(backspace) => Action::DeleteChar,
40            key!(ctrl-right) => Action::ForwardWord,
41            key!(ctrl-left) => Action::BackwardWord,
42            key!(ctrl-h) => Action::DeleteWord,
43            key!(ctrl-u) => Action::Cancel,
44            key!(alt-a) => Action::QueryPos(0),
45            key!(alt-h) => Action::Help("".to_string()),
46            key!(ctrl-'[') => Action::ToggleWrap,
47            key!(ctrl-']') => Action::TogglePreviewWrap,
48            key!(shift-right) => Action::HScroll(1),
49            key!(shift-left) => Action::HScroll(-1),
50            key!(PageDown) => Action::PageDown,
51            key!(PageUp) => Action::PageUp,
52            key!(Home) => Action::Pos(0),
53            key!(End) => Action::Pos(-1),
54            key!(shift-PageDown) => Action::PreviewHalfPageDown,
55            key!(shift-PageUp) => Action::PreviewHalfPageUp,
56            key!(shift-Home) => Action::PreviewJump,
57            key!(shift-End) => Action::PreviewJump,
58            key!('?') => Action::SwitchPreview(None)
59        );
60
61        #[cfg(target_os = "macos")]
62        {
63            let ext = bindmap!(
64                key!(alt-left) => Action::ForwardWord,
65                key!(alt-right) => Action::BackwardWord,
66                key!(alt-backspace) => Action::DeleteWord,
67            );
68            ret.extend(ext);
69        }
70
71        ret
72    }
73
74    /// Check for infinite loops in semantic actions.
75    pub fn check_cycles(&self) -> Result<(), String> {
76        for actions in self.values() {
77            for action in actions {
78                if let Action::Semantic(s) = action {
79                    let mut path = Vec::new();
80                    self.dfs_semantic(s, &mut path)?;
81                }
82            }
83        }
84        Ok(())
85    }
86
87    pub fn dfs_semantic(&self, current: &str, path: &mut Vec<String>) -> Result<(), String> {
88        if path.contains(&current.to_string()) {
89            return Err(format!(
90                "Infinite loop detected in semantic actions: {} -> {}",
91                path.join(" -> "),
92                current
93            ));
94        }
95
96        path.push(current.to_string());
97        if let Some(actions) = self.get(&Trigger::Semantic(current.to_string())) {
98            for action in actions {
99                if let Action::Semantic(next) = action {
100                    self.dfs_semantic(next, path)?;
101                }
102            }
103        }
104        path.pop();
105
106        Ok(())
107    }
108}
109
110#[derive(Debug, Hash, PartialEq, Eq, Clone)]
111pub enum Trigger {
112    Key(KeyCombination),
113    Mouse(SimpleMouseEvent),
114    Event(Event),
115    /// A "semantic" trigger, such as `Open`, which should be resolved or rejected before starting the picker.
116    /// This is serialized/deserialized with a `::` prefix, such as "::Open" = "Execute(open {})"
117    Semantic(String),
118}
119
120// impl Ord for Trigger {
121//     fn cmp(&self, other: &Self) -> Ordering {
122//         use Trigger::*;
123
124//         match (self, other) {
125//             (Key(a), Key(b)) => a.to_string().cmp(&b.to_string()),
126//             (Mouse(a), Mouse(b)) => a.cmp(b),
127//             (Event(a), Event(b)) => a.cmp(b),
128
129//             // define variant order
130//             (Key(_), _) => Ordering::Less,
131//             (Mouse(_), Key(_)) => Ordering::Greater,
132//             (Mouse(_), Event(_)) => Ordering::Less,
133//             (Event(_), _) => Ordering::Greater,
134//         }
135//     }
136// }
137
138// impl PartialOrd for Trigger {
139//     fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
140//         Some(self.cmp(other))
141//     }
142// }
143
144/// Crossterm mouse event without location
145#[derive(Debug, Eq, Clone, PartialEq, Hash)]
146pub struct SimpleMouseEvent {
147    pub kind: MouseEventKind,
148    pub modifiers: KeyModifiers,
149}
150
151impl Ord for SimpleMouseEvent {
152    fn cmp(&self, other: &Self) -> Ordering {
153        match self.kind.partial_cmp(&other.kind) {
154            Some(Ordering::Equal) | None => self.modifiers.bits().cmp(&other.modifiers.bits()),
155            Some(o) => o,
156        }
157    }
158}
159
160impl PartialOrd for SimpleMouseEvent {
161    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
162        Some(self.cmp(other))
163    }
164}
165
166// ---------- BOILERPLATE
167impl From<crossterm::event::MouseEvent> for Trigger {
168    fn from(e: crossterm::event::MouseEvent) -> Self {
169        Trigger::Mouse(SimpleMouseEvent {
170            kind: e.kind,
171            modifiers: e.modifiers,
172        })
173    }
174}
175
176impl From<KeyCombination> for Trigger {
177    fn from(key: KeyCombination) -> Self {
178        Trigger::Key(key)
179    }
180}
181
182impl From<Event> for Trigger {
183    fn from(event: Event) -> Self {
184        Trigger::Event(event)
185    }
186}
187// ------------ SERDE
188
189impl Display for Trigger {
190    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191        match self {
192            Trigger::Key(key) => write!(f, "{}", key),
193            Trigger::Mouse(event) => {
194                if event.modifiers.contains(KeyModifiers::SHIFT) {
195                    write!(f, "shift+")?;
196                }
197                if event.modifiers.contains(KeyModifiers::CONTROL) {
198                    write!(f, "ctrl+")?;
199                }
200                if event.modifiers.contains(KeyModifiers::ALT) {
201                    write!(f, "alt+")?;
202                }
203                if event.modifiers.contains(KeyModifiers::SUPER) {
204                    write!(f, "super+")?;
205                }
206                if event.modifiers.contains(KeyModifiers::HYPER) {
207                    write!(f, "hyper+")?;
208                }
209                if event.modifiers.contains(KeyModifiers::META) {
210                    write!(f, "meta+")?;
211                }
212                write!(f, "{}", mouse_event_kind_as_str(event.kind))
213            }
214            Trigger::Event(event) => write!(f, "{}", event),
215            Trigger::Semantic(alias) => write!(f, "::{alias}"),
216        }
217    }
218}
219
220impl ser::Serialize for Trigger {
221    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
222    where
223        S: ser::Serializer,
224    {
225        serializer.serialize_str(&self.to_string())
226    }
227}
228
229pub fn mouse_event_kind_as_str(kind: MouseEventKind) -> &'static str {
230    match kind {
231        MouseEventKind::Down(MouseButton::Left) => "left",
232        MouseEventKind::Down(MouseButton::Middle) => "middle",
233        MouseEventKind::Down(MouseButton::Right) => "right",
234        MouseEventKind::ScrollDown => "scrolldown",
235        MouseEventKind::ScrollUp => "scrollup",
236        MouseEventKind::ScrollLeft => "scrollleft",
237        MouseEventKind::ScrollRight => "scrollright",
238        _ => "", // Other kinds are not handled in deserialize
239    }
240}
241
242impl FromStr for Trigger {
243    type Err = String;
244
245    fn from_str(value: &str) -> Result<Self, Self::Err> {
246        // try semantic
247        if let Some(s) = value.strip_prefix("::") {
248            return Ok(Trigger::Semantic(s.to_string()));
249        }
250
251        // 1. Try KeyCombination
252        if let Ok(key) = KeyCombination::from_str(value) {
253            return Ok(Trigger::Key(key));
254        }
255
256        // 2. Try MouseEvent
257        let parts: Vec<&str> = value.split('+').collect();
258        if let Some(last) = parts.last()
259            && let Some(kind) = match last.to_lowercase().as_str() {
260                "left" => Some(MouseEventKind::Down(MouseButton::Left)),
261                "middle" => Some(MouseEventKind::Down(MouseButton::Middle)),
262                "right" => Some(MouseEventKind::Down(MouseButton::Right)),
263                "scrolldown" => Some(MouseEventKind::ScrollDown),
264                "scrollup" => Some(MouseEventKind::ScrollUp),
265                "scrollleft" => Some(MouseEventKind::ScrollLeft),
266                "scrollright" => Some(MouseEventKind::ScrollRight),
267                _ => None,
268            }
269        {
270            let mut modifiers = KeyModifiers::empty();
271            for m in &parts[..parts.len() - 1] {
272                match m.to_lowercase().as_str() {
273                    "shift" => modifiers |= KeyModifiers::SHIFT,
274                    "ctrl" => modifiers |= KeyModifiers::CONTROL,
275                    "alt" => modifiers |= KeyModifiers::ALT,
276                    "super" => modifiers |= KeyModifiers::SUPER,
277                    "hyper" => modifiers |= KeyModifiers::HYPER,
278                    "meta" => modifiers |= KeyModifiers::META,
279                    "none" => {}
280                    unknown => return Err(format!("Unknown modifier: {}", unknown)),
281                }
282            }
283
284            return Ok(Trigger::Mouse(SimpleMouseEvent { kind, modifiers }));
285        }
286
287        if let Ok(event) = value.parse::<Event>() {
288            return Ok(Trigger::Event(event));
289        }
290
291        Err(format!("failed to parse trigger from '{}'", value))
292    }
293}
294
295impl<'de> serde::Deserialize<'de> for Trigger {
296    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
297    where
298        D: Deserializer<'de>,
299    {
300        struct TriggerVisitor;
301
302        impl<'de> Visitor<'de> for TriggerVisitor {
303            type Value = Trigger;
304
305            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
306                write!(f, "a string representing a Trigger")
307            }
308
309            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
310            where
311                E: de::Error,
312            {
313                value.parse::<Trigger>().map_err(E::custom)
314            }
315        }
316
317        deserializer.deserialize_str(TriggerVisitor)
318    }
319}
320
321use ratatui::style::Style;
322use ratatui::text::{Line, Span, Text};
323
324pub fn display_binds<A: ActionExt + Display>(
325    binds: &BindMap<A>,
326    cfg: Option<&HelpColorConfig>,
327) -> Text<'static> {
328    // Collect trigger and action strings
329    let mut entries: Vec<(String, String)> = binds
330        .iter()
331        .map(|(trigger, actions)| {
332            let value_str = if actions.len() == 1 {
333                actions[0].to_string()
334            } else {
335                let inner = actions
336                    .iter()
337                    .map(|a| a.to_string())
338                    .collect::<Vec<_>>()
339                    .join(", ");
340                format!("[{inner}]")
341            };
342            (trigger.to_string(), value_str)
343        })
344        .collect();
345
346    // Sort by trigger string
347    entries.sort_by(|a, b| a.1.cmp(&b.1));
348
349    // Build output
350    let Some(cfg) = cfg else {
351        // fallback plain text
352        let mut text = Text::default();
353        for (trigger, value) in entries {
354            text.extend(Text::from(format!("{trigger} = {value}\n")));
355        }
356        return text;
357    };
358
359    let mut text = Text::default();
360
361    for (trigger, value) in entries {
362        let mut spans = vec![];
363
364        // Trigger
365        spans.push(Span::styled(trigger, Style::default().fg(cfg.key)));
366        spans.push(Span::raw(" = "));
367
368        // Value
369        if value.starts_with('[') {
370            // multi-action list: color each item
371            spans.push(Span::raw("["));
372            let inner = &value[1..value.len() - 1];
373            for (i, item) in inner.split(", ").enumerate() {
374                if i > 0 {
375                    spans.push(Span::raw(", "));
376                }
377                spans.push(Span::styled(
378                    item.to_string(),
379                    Style::default().fg(cfg.value),
380                ));
381            }
382            spans.push(Span::raw("]"));
383        } else {
384            spans.push(Span::styled(value, Style::default().fg(cfg.value)));
385        }
386
387        spans.push(Span::raw("\n"));
388        text.extend(Text::from(Line::from(spans)));
389    }
390
391    text
392}
393
394#[cfg(test)]
395mod test {
396    use super::*;
397    use crossterm::event::MouseEvent;
398
399    #[test]
400    fn test_bindmap_trigger() {
401        let mut bind_map: BindMap = BindMap::new();
402
403        // Insert trigger with default actions
404        let trigger0 = Trigger::Mouse(SimpleMouseEvent {
405            kind: MouseEventKind::ScrollDown,
406            modifiers: KeyModifiers::empty(),
407        });
408        bind_map.insert(trigger0.clone(), Actions::default());
409
410        // Construct via From<MouseEvent>
411        let mouse_event = MouseEvent {
412            kind: MouseEventKind::ScrollDown,
413            column: 0,
414            row: 0,
415            modifiers: KeyModifiers::empty(),
416        };
417        let from_event: Trigger = mouse_event.into();
418
419        // Should be retrievable
420        assert!(bind_map.contains_key(&from_event));
421
422        // Shift-modified trigger should NOT be found
423        let shift_trigger = Trigger::Mouse(SimpleMouseEvent {
424            kind: MouseEventKind::ScrollDown,
425            modifiers: KeyModifiers::SHIFT,
426        });
427        assert!(!bind_map.contains_key(&shift_trigger));
428    }
429
430    #[test]
431    fn test_check_cycles() {
432        use crate::bindmap;
433        let bind_map: BindMap = bindmap!(
434            Trigger::Semantic("a".into()) => Action::Semantic("b".into()),
435            Trigger::Semantic("b".into()) => Action::Semantic("a".into()),
436        );
437        assert!(bind_map.check_cycles().is_err());
438
439        let bind_map_no_cycle: BindMap = bindmap!(
440            Trigger::Semantic("a".into()) => Action::Semantic("b".into()),
441            Trigger::Semantic("b".into()) => Action::Print("ok".into()),
442        );
443        assert!(bind_map_no_cycle.check_cycles().is_ok());
444
445        let bind_map_self_cycle: BindMap = bindmap!(
446            Trigger::Semantic("a".into()) => Action::Semantic("a".into()),
447        );
448        assert!(bind_map_self_cycle.check_cycles().is_err());
449
450        let bind_map_indirect_cycle: BindMap = bindmap!(
451            key!(a) => Action::Semantic("foo".into()),
452            Trigger::Semantic("foo".into()) => Action::Semantic("foo".into()),
453        );
454        assert!(bind_map_indirect_cycle.check_cycles().is_err());
455    }
456}