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, Serialize,
10    de::{self, Visitor},
11    ser,
12};
13
14use crate::{
15    action::{Action, ActionExt, Actions, NullActionExt},
16    config::TomlColorConfig,
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 ser::Serialize for Trigger {
155    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
156    where
157        S: ser::Serializer,
158    {
159        match self {
160            Trigger::Key(key) => serializer.serialize_str(&key.to_string()),
161            Trigger::Mouse(event) => {
162                let mut s = String::new();
163                if event.modifiers.contains(KeyModifiers::SHIFT) {
164                    s.push_str("shift+");
165                }
166                if event.modifiers.contains(KeyModifiers::CONTROL) {
167                    s.push_str("ctrl+");
168                }
169                if event.modifiers.contains(KeyModifiers::ALT) {
170                    s.push_str("alt+");
171                }
172                if event.modifiers.contains(KeyModifiers::SUPER) {
173                    s.push_str("super+");
174                }
175                if event.modifiers.contains(KeyModifiers::HYPER) {
176                    s.push_str("hyper+");
177                }
178                if event.modifiers.contains(KeyModifiers::META) {
179                    s.push_str("meta+");
180                }
181                s.push_str(mouse_event_kind_as_str(event.kind));
182                serializer.serialize_str(&s)
183            }
184            Trigger::Event(event) => serializer.serialize_str(&event.to_string()),
185            Trigger::Semantic(alias) => serializer.serialize_str(&format!("::{alias}")),
186        }
187    }
188}
189
190pub fn mouse_event_kind_as_str(kind: MouseEventKind) -> &'static str {
191    match kind {
192        MouseEventKind::Down(MouseButton::Left) => "left",
193        MouseEventKind::Down(MouseButton::Middle) => "middle",
194        MouseEventKind::Down(MouseButton::Right) => "right",
195        MouseEventKind::ScrollDown => "scrolldown",
196        MouseEventKind::ScrollUp => "scrollup",
197        MouseEventKind::ScrollLeft => "scrollleft",
198        MouseEventKind::ScrollRight => "scrollright",
199        _ => "", // Other kinds are not handled in deserialize
200    }
201}
202
203impl FromStr for Trigger {
204    type Err = String;
205
206    fn from_str(value: &str) -> Result<Self, Self::Err> {
207        if let Some(s) = value.strip_prefix("::") {
208            return Ok(Trigger::Semantic(s.to_string()));
209        }
210        // 1. Try KeyCombination
211        if let Ok(key) = KeyCombination::from_str(value) {
212            return Ok(Trigger::Key(key));
213        }
214
215        // 2. Try MouseEvent
216        let parts: Vec<&str> = value.split('+').collect();
217        if let Some(last) = parts.last()
218            && let Some(kind) = match last.to_lowercase().as_str() {
219                "left" => Some(MouseEventKind::Down(MouseButton::Left)),
220                "middle" => Some(MouseEventKind::Down(MouseButton::Middle)),
221                "right" => Some(MouseEventKind::Down(MouseButton::Right)),
222                "scrolldown" => Some(MouseEventKind::ScrollDown),
223                "scrollup" => Some(MouseEventKind::ScrollUp),
224                "scrollleft" => Some(MouseEventKind::ScrollLeft),
225                "scrollright" => Some(MouseEventKind::ScrollRight),
226                _ => None,
227            }
228        {
229            let mut modifiers = KeyModifiers::empty();
230            for m in &parts[..parts.len() - 1] {
231                match m.to_lowercase().as_str() {
232                    "shift" => modifiers |= KeyModifiers::SHIFT,
233                    "ctrl" => modifiers |= KeyModifiers::CONTROL,
234                    "alt" => modifiers |= KeyModifiers::ALT,
235                    "super" => modifiers |= KeyModifiers::SUPER,
236                    "hyper" => modifiers |= KeyModifiers::HYPER,
237                    "meta" => modifiers |= KeyModifiers::META,
238                    "none" => {}
239                    unknown => {
240                        return Err(format!("Unknown modifier: {}", unknown));
241                    }
242                }
243            }
244
245            return Ok(Trigger::Mouse(SimpleMouseEvent { kind, modifiers }));
246        }
247
248        // 3. Try Event
249        if let Ok(evt) = value.parse::<Event>() {
250            return Ok(Trigger::Event(evt));
251        }
252
253        Err(format!("failed to parse trigger from '{}'", value))
254    }
255}
256
257impl<'de> serde::Deserialize<'de> for Trigger {
258    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
259    where
260        D: Deserializer<'de>,
261    {
262        struct TriggerVisitor;
263
264        impl<'de> Visitor<'de> for TriggerVisitor {
265            type Value = Trigger;
266
267            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
268                write!(f, "a string representing a Trigger")
269            }
270
271            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
272            where
273                E: de::Error,
274            {
275                value.parse::<Trigger>().map_err(E::custom)
276            }
277        }
278
279        deserializer.deserialize_str(TriggerVisitor)
280    }
281}
282
283use ratatui::style::Style;
284use ratatui::text::{Line, Span, Text};
285use regex::Regex;
286
287// random ai toml coloring cuz i dont wanna use bat just for this
288pub fn display_binds<A: ActionExt + Display>(
289    binds: &BindMap<A>,
290    cfg: Option<&TomlColorConfig>,
291) -> Text<'static> {
292    let toml_string = toml::to_string(&BindFmtWrapper { binds }).unwrap();
293
294    let Some(cfg) = cfg else {
295        return Text::from(toml_string);
296    };
297
298    let section_re = Regex::new(r"^\s*\[.*\]").unwrap();
299    let key_re = Regex::new(r"^(\s*[\w_-]+)(\s*=\s*)").unwrap();
300    let string_re = Regex::new(r#""[^"]*""#).unwrap();
301    let number_re = Regex::new(r"\b\d+(\.\d+)?\b").unwrap();
302
303    let mut text = Text::default();
304
305    for line in toml_string.lines() {
306        if section_re.is_match(line) {
307            let mut style = Style::default().fg(cfg.section);
308            if cfg.section_bold {
309                style = style.add_modifier(ratatui::style::Modifier::BOLD);
310            }
311            text.extend(Text::from(Span::styled(line.to_string(), style)));
312        } else {
313            let mut spans = vec![];
314            let mut remainder = line.to_string();
315
316            // Highlight key
317            if let Some(cap) = key_re.captures(&remainder) {
318                let key = &cap[1];
319                let eq = &cap[2];
320                spans.push(Span::styled(key.to_string(), Style::default().fg(cfg.key)));
321                spans.push(Span::raw(eq.to_string()));
322                remainder = remainder[cap[0].len()..].to_string();
323            }
324
325            // Highlight strings
326            let mut last_idx = 0;
327            for m in string_re.find_iter(&remainder) {
328                if m.start() > last_idx {
329                    spans.push(Span::raw(remainder[last_idx..m.start()].to_string()));
330                }
331                spans.push(Span::styled(
332                    m.as_str().to_string(),
333                    Style::default().fg(cfg.string),
334                ));
335                last_idx = m.end();
336            }
337
338            // Highlight numbers
339            let remainder = &remainder[last_idx..];
340            let mut last_idx = 0;
341            for m in number_re.find_iter(remainder) {
342                if m.start() > last_idx {
343                    spans.push(Span::raw(remainder[last_idx..m.start()].to_string()));
344                }
345                spans.push(Span::styled(
346                    m.as_str().to_string(),
347                    Style::default().fg(cfg.number),
348                ));
349                last_idx = m.end();
350            }
351
352            if last_idx < remainder.len() {
353                spans.push(Span::raw(remainder[last_idx..].to_string()));
354            }
355
356            text.extend(Text::from(Line::from(spans)));
357        }
358    }
359
360    text
361}
362
363struct BindFmtWrapper<'a, A: ActionExt + Display> {
364    binds: &'a BindMap<A>,
365}
366
367use serde::ser::{SerializeMap, Serializer};
368
369impl<'a, A> Serialize for BindFmtWrapper<'a, A>
370where
371    A: ActionExt + Display,
372{
373    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
374    where
375        S: Serializer,
376    {
377        let mut entries: Vec<_> = self.binds.iter().collect();
378
379        // Sort by value.to_string()
380        entries.sort_by(|(_, v1), (_, v2)| {
381            v1.0.iter()
382                .map(ToString::to_string)
383                .cmp(v2.0.iter().map(ToString::to_string))
384        });
385
386        let mut map = serializer.serialize_map(Some(entries.len()))?;
387        for (k, v) in entries {
388            map.serialize_entry(k, v)?;
389        }
390        map.end()
391    }
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}