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
75#[derive(Debug, Hash, PartialEq, Eq, Clone)]
76pub enum Trigger {
77    Key(KeyCombination),
78    Mouse(SimpleMouseEvent),
79    Event(Event),
80    /// A "semantic" trigger, such as `Open`, which should be resolved or rejected before starting the picker.
81    /// This is serialized/deserialized with a `::` prefix, such as "::Open" = "Execute(open {})"
82    Semantic(String),
83}
84
85// impl Ord for Trigger {
86//     fn cmp(&self, other: &Self) -> Ordering {
87//         use Trigger::*;
88
89//         match (self, other) {
90//             (Key(a), Key(b)) => a.to_string().cmp(&b.to_string()),
91//             (Mouse(a), Mouse(b)) => a.cmp(b),
92//             (Event(a), Event(b)) => a.cmp(b),
93
94//             // define variant order
95//             (Key(_), _) => Ordering::Less,
96//             (Mouse(_), Key(_)) => Ordering::Greater,
97//             (Mouse(_), Event(_)) => Ordering::Less,
98//             (Event(_), _) => Ordering::Greater,
99//         }
100//     }
101// }
102
103// impl PartialOrd for Trigger {
104//     fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
105//         Some(self.cmp(other))
106//     }
107// }
108
109/// Crossterm mouse event without location
110#[derive(Debug, Eq, Clone, PartialEq, Hash)]
111pub struct SimpleMouseEvent {
112    pub kind: MouseEventKind,
113    pub modifiers: KeyModifiers,
114}
115
116impl Ord for SimpleMouseEvent {
117    fn cmp(&self, other: &Self) -> Ordering {
118        match self.kind.partial_cmp(&other.kind) {
119            Some(Ordering::Equal) | None => self.modifiers.bits().cmp(&other.modifiers.bits()),
120            Some(o) => o,
121        }
122    }
123}
124
125impl PartialOrd for SimpleMouseEvent {
126    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
127        Some(self.cmp(other))
128    }
129}
130
131// ---------- BOILERPLATE
132impl From<crossterm::event::MouseEvent> for Trigger {
133    fn from(e: crossterm::event::MouseEvent) -> Self {
134        Trigger::Mouse(SimpleMouseEvent {
135            kind: e.kind,
136            modifiers: e.modifiers,
137        })
138    }
139}
140
141impl From<KeyCombination> for Trigger {
142    fn from(key: KeyCombination) -> Self {
143        Trigger::Key(key)
144    }
145}
146
147impl From<Event> for Trigger {
148    fn from(event: Event) -> Self {
149        Trigger::Event(event)
150    }
151}
152// ------------ SERDE
153
154impl Display for Trigger {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        match self {
157            Trigger::Key(key) => write!(f, "{}", key),
158            Trigger::Mouse(event) => {
159                if event.modifiers.contains(KeyModifiers::SHIFT) {
160                    write!(f, "shift+")?;
161                }
162                if event.modifiers.contains(KeyModifiers::CONTROL) {
163                    write!(f, "ctrl+")?;
164                }
165                if event.modifiers.contains(KeyModifiers::ALT) {
166                    write!(f, "alt+")?;
167                }
168                if event.modifiers.contains(KeyModifiers::SUPER) {
169                    write!(f, "super+")?;
170                }
171                if event.modifiers.contains(KeyModifiers::HYPER) {
172                    write!(f, "hyper+")?;
173                }
174                if event.modifiers.contains(KeyModifiers::META) {
175                    write!(f, "meta+")?;
176                }
177                write!(f, "{}", mouse_event_kind_as_str(event.kind))
178            }
179            Trigger::Event(event) => write!(f, "{}", event),
180            Trigger::Semantic(alias) => write!(f, "::{alias}"),
181        }
182    }
183}
184
185impl ser::Serialize for Trigger {
186    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
187    where
188        S: ser::Serializer,
189    {
190        serializer.serialize_str(&self.to_string())
191    }
192}
193
194pub fn mouse_event_kind_as_str(kind: MouseEventKind) -> &'static str {
195    match kind {
196        MouseEventKind::Down(MouseButton::Left) => "left",
197        MouseEventKind::Down(MouseButton::Middle) => "middle",
198        MouseEventKind::Down(MouseButton::Right) => "right",
199        MouseEventKind::ScrollDown => "scrolldown",
200        MouseEventKind::ScrollUp => "scrollup",
201        MouseEventKind::ScrollLeft => "scrollleft",
202        MouseEventKind::ScrollRight => "scrollright",
203        _ => "", // Other kinds are not handled in deserialize
204    }
205}
206
207impl FromStr for Trigger {
208    type Err = String;
209
210    fn from_str(value: &str) -> Result<Self, Self::Err> {
211        if let Some(s) = value.strip_prefix("::") {
212            return Ok(Trigger::Semantic(s.to_string()));
213        }
214        // 1. Try KeyCombination
215        if let Ok(key) = KeyCombination::from_str(value) {
216            return Ok(Trigger::Key(key));
217        }
218
219        // 2. Try MouseEvent
220        let parts: Vec<&str> = value.split('+').collect();
221        if let Some(last) = parts.last()
222            && let Some(kind) = match last.to_lowercase().as_str() {
223                "left" => Some(MouseEventKind::Down(MouseButton::Left)),
224                "middle" => Some(MouseEventKind::Down(MouseButton::Middle)),
225                "right" => Some(MouseEventKind::Down(MouseButton::Right)),
226                "scrolldown" => Some(MouseEventKind::ScrollDown),
227                "scrollup" => Some(MouseEventKind::ScrollUp),
228                "scrollleft" => Some(MouseEventKind::ScrollLeft),
229                "scrollright" => Some(MouseEventKind::ScrollRight),
230                _ => None,
231            }
232        {
233            let mut modifiers = KeyModifiers::empty();
234            for m in &parts[..parts.len() - 1] {
235                match m.to_lowercase().as_str() {
236                    "shift" => modifiers |= KeyModifiers::SHIFT,
237                    "ctrl" => modifiers |= KeyModifiers::CONTROL,
238                    "alt" => modifiers |= KeyModifiers::ALT,
239                    "super" => modifiers |= KeyModifiers::SUPER,
240                    "hyper" => modifiers |= KeyModifiers::HYPER,
241                    "meta" => modifiers |= KeyModifiers::META,
242                    "none" => {}
243                    unknown => {
244                        return Err(format!("Unknown modifier: {}", unknown));
245                    }
246                }
247            }
248
249            return Ok(Trigger::Mouse(SimpleMouseEvent { kind, modifiers }));
250        }
251
252        // 3. Try Event
253        if let Ok(evt) = value.parse::<Event>() {
254            return Ok(Trigger::Event(evt));
255        }
256
257        Err(format!("failed to parse trigger from '{}'", value))
258    }
259}
260
261impl<'de> serde::Deserialize<'de> for Trigger {
262    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
263    where
264        D: Deserializer<'de>,
265    {
266        struct TriggerVisitor;
267
268        impl<'de> Visitor<'de> for TriggerVisitor {
269            type Value = Trigger;
270
271            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
272                write!(f, "a string representing a Trigger")
273            }
274
275            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
276            where
277                E: de::Error,
278            {
279                value.parse::<Trigger>().map_err(E::custom)
280            }
281        }
282
283        deserializer.deserialize_str(TriggerVisitor)
284    }
285}
286
287use ratatui::style::Style;
288use ratatui::text::{Line, Span, Text};
289
290pub fn display_binds<A: ActionExt + Display>(
291    binds: &BindMap<A>,
292    cfg: Option<&HelpColorConfig>,
293) -> Text<'static> {
294    // Collect trigger and action strings
295    let mut entries: Vec<(String, String)> = binds
296        .iter()
297        .map(|(trigger, actions)| {
298            let value_str = if actions.len() == 1 {
299                actions[0].to_string()
300            } else {
301                let inner = actions
302                    .iter()
303                    .map(|a| a.to_string())
304                    .collect::<Vec<_>>()
305                    .join(", ");
306                format!("[{inner}]")
307            };
308            (trigger.to_string(), value_str)
309        })
310        .collect();
311
312    // Sort by trigger string
313    entries.sort_by(|a, b| a.1.cmp(&b.1));
314
315    // Build output
316    let Some(cfg) = cfg else {
317        // fallback plain text
318        let mut text = Text::default();
319        for (trigger, value) in entries {
320            text.extend(Text::from(format!("{trigger} = {value}\n")));
321        }
322        return text;
323    };
324
325    let mut text = Text::default();
326
327    for (trigger, value) in entries {
328        let mut spans = vec![];
329
330        // Trigger
331        spans.push(Span::styled(trigger, Style::default().fg(cfg.key)));
332        spans.push(Span::raw(" = "));
333
334        // Value
335        if value.starts_with('[') {
336            // multi-action list: color each item
337            spans.push(Span::raw("["));
338            let inner = &value[1..value.len() - 1];
339            for (i, item) in inner.split(", ").enumerate() {
340                if i > 0 {
341                    spans.push(Span::raw(", "));
342                }
343                spans.push(Span::styled(
344                    item.to_string(),
345                    Style::default().fg(cfg.value),
346                ));
347            }
348            spans.push(Span::raw("]"));
349        } else {
350            spans.push(Span::styled(value, Style::default().fg(cfg.value)));
351        }
352
353        spans.push(Span::raw("\n"));
354        text.extend(Text::from(Line::from(spans)));
355    }
356
357    text
358}
359
360#[cfg(test)]
361mod test {
362    use super::*;
363    use crossterm::event::MouseEvent;
364
365    #[test]
366    fn test_bindmap_trigger() {
367        let mut bind_map: BindMap = BindMap::new();
368
369        // Insert trigger with default actions
370        let trigger0 = Trigger::Mouse(SimpleMouseEvent {
371            kind: MouseEventKind::ScrollDown,
372            modifiers: KeyModifiers::empty(),
373        });
374        bind_map.insert(trigger0.clone(), Actions::default());
375
376        // Construct via From<MouseEvent>
377        let mouse_event = MouseEvent {
378            kind: MouseEventKind::ScrollDown,
379            column: 0,
380            row: 0,
381            modifiers: KeyModifiers::empty(),
382        };
383        let from_event: Trigger = mouse_event.into();
384
385        // Should be retrievable
386        assert!(bind_map.contains_key(&from_event));
387
388        // Shift-modified trigger should NOT be found
389        let shift_trigger = Trigger::Mouse(SimpleMouseEvent {
390            kind: MouseEventKind::ScrollDown,
391            modifiers: KeyModifiers::SHIFT,
392        });
393        assert!(!bind_map.contains_key(&shift_trigger));
394    }
395}