Skip to main content

matchmaker/
binds.rs

1use std::{
2    cmp::Ordering,
3    collections::BTreeMap,
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> = BTreeMap<Trigger, Actions<A>>;
26
27#[easy_ext::ext(BindMapExt)]
28impl<A: ActionExt> BindMap<A> {
29    pub fn default_binds() -> Self {
30        bindmap!(
31            key!(ctrl-c) => Action::Quit(1),
32            key!(esc) => Action::Quit(1),
33            key!(up) => Action::Up(1),
34            key!(down) => Action::Down(1),
35            key!(enter) => Action::Accept,
36            key!(right) => Action::ForwardChar,
37            key!(left) => Action::BackwardChar,
38            key!(ctrl-right) => Action::ForwardWord,
39            key!(ctrl-left) => Action::BackwardWord,
40            key!(backspace) => Action::DeleteChar,
41            key!(ctrl-h) => Action::DeleteWord,
42            key!(ctrl-u) => Action::Cancel,
43            key!(alt-h) => Action::Help("".to_string()),
44            key!(ctrl-'[') => Action::ToggleWrap,
45            key!(ctrl-']') => Action::ToggleWrapPreview,
46            key!(shift-right) => Action::HScroll(1),
47            key!(shift-left) => Action::HScroll(-1),
48        )
49    }
50}
51
52#[derive(Debug, Hash, PartialEq, Eq, Clone)]
53pub enum Trigger {
54    Key(KeyCombination),
55    Mouse(SimpleMouseEvent),
56    Event(Event),
57}
58
59impl Ord for Trigger {
60    fn cmp(&self, other: &Self) -> Ordering {
61        use Trigger::*;
62
63        match (self, other) {
64            (Key(a), Key(b)) => a.to_string().cmp(&b.to_string()),
65            (Mouse(a), Mouse(b)) => a.cmp(b),
66            (Event(a), Event(b)) => a.cmp(b),
67
68            // define variant order
69            (Key(_), _) => Ordering::Less,
70            (Mouse(_), Key(_)) => Ordering::Greater,
71            (Mouse(_), Event(_)) => Ordering::Less,
72            (Event(_), _) => Ordering::Greater,
73        }
74    }
75}
76
77impl PartialOrd for Trigger {
78    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
79        Some(self.cmp(other))
80    }
81}
82
83/// Crossterm mouse event without location
84#[derive(Debug, Eq, Clone, PartialEq, Hash)]
85pub struct SimpleMouseEvent {
86    pub kind: MouseEventKind,
87    pub modifiers: KeyModifiers,
88}
89
90impl Ord for SimpleMouseEvent {
91    fn cmp(&self, other: &Self) -> Ordering {
92        match self.kind.partial_cmp(&other.kind) {
93            Some(Ordering::Equal) | None => self.modifiers.bits().cmp(&other.modifiers.bits()),
94            Some(o) => o,
95        }
96    }
97}
98
99impl PartialOrd for SimpleMouseEvent {
100    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
101        Some(self.cmp(other))
102    }
103}
104
105// ---------- BOILERPLATE
106impl From<crossterm::event::MouseEvent> for Trigger {
107    fn from(e: crossterm::event::MouseEvent) -> Self {
108        Trigger::Mouse(SimpleMouseEvent {
109            kind: e.kind,
110            modifiers: e.modifiers,
111        })
112    }
113}
114
115impl From<KeyCombination> for Trigger {
116    fn from(key: KeyCombination) -> Self {
117        Trigger::Key(key)
118    }
119}
120
121impl From<Event> for Trigger {
122    fn from(event: Event) -> Self {
123        Trigger::Event(event)
124    }
125}
126// ------------ SERDE
127
128impl ser::Serialize for Trigger {
129    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
130    where
131        S: ser::Serializer,
132    {
133        match self {
134            Trigger::Key(key) => serializer.serialize_str(&key.to_string()),
135            Trigger::Mouse(event) => {
136                let mut s = String::new();
137                if event.modifiers.contains(KeyModifiers::SHIFT) {
138                    s.push_str("shift+");
139                }
140                if event.modifiers.contains(KeyModifiers::CONTROL) {
141                    s.push_str("ctrl+");
142                }
143                if event.modifiers.contains(KeyModifiers::ALT) {
144                    s.push_str("alt+");
145                }
146                if event.modifiers.contains(KeyModifiers::SUPER) {
147                    s.push_str("super+");
148                }
149                if event.modifiers.contains(KeyModifiers::HYPER) {
150                    s.push_str("hyper+");
151                }
152                if event.modifiers.contains(KeyModifiers::META) {
153                    s.push_str("meta+");
154                }
155                s.push_str(mouse_event_kind_as_str(event.kind));
156                serializer.serialize_str(&s)
157            }
158            Trigger::Event(event) => serializer.serialize_str(&event.to_string()),
159        }
160    }
161}
162
163pub fn mouse_event_kind_as_str(kind: MouseEventKind) -> &'static str {
164    match kind {
165        MouseEventKind::Down(MouseButton::Left) => "left",
166        MouseEventKind::Down(MouseButton::Middle) => "middle",
167        MouseEventKind::Down(MouseButton::Right) => "right",
168        MouseEventKind::ScrollDown => "scrolldown",
169        MouseEventKind::ScrollUp => "scrollup",
170        MouseEventKind::ScrollLeft => "scrollleft",
171        MouseEventKind::ScrollRight => "scrollright",
172        _ => "", // Other kinds are not handled in deserialize
173    }
174}
175
176impl<'de> serde::Deserialize<'de> for Trigger {
177    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
178    where
179        D: Deserializer<'de>,
180    {
181        struct TriggerVisitor;
182
183        impl<'de> Visitor<'de> for TriggerVisitor {
184            type Value = Trigger;
185
186            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
187                write!(f, "a string representing a Trigger")
188            }
189
190            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
191            where
192                E: de::Error,
193            {
194                // 1. Try KeyCombination
195                if let Ok(key) = KeyCombination::from_str(value) {
196                    return Ok(Trigger::Key(key));
197                }
198
199                // 2. Try MouseEvent: modifiers split by '+', last = mouse button
200                let parts: Vec<&str> = value.split('+').collect();
201                if let Some(last) = parts.last()
202                    && let Some(kind) = match last.to_lowercase().as_str() {
203                        "left" => Some(MouseEventKind::Down(MouseButton::Left)),
204                        "middle" => Some(MouseEventKind::Down(MouseButton::Middle)),
205                        "right" => Some(MouseEventKind::Down(MouseButton::Right)),
206                        "scrolldown" => Some(MouseEventKind::ScrollDown),
207                        "scrollup" => Some(MouseEventKind::ScrollUp),
208                        "scrollleft" => Some(MouseEventKind::ScrollLeft),
209                        "scrollright" => Some(MouseEventKind::ScrollRight),
210                        _ => None,
211                    }
212                {
213                    let mut modifiers = KeyModifiers::empty();
214                    for m in &parts[..parts.len() - 1] {
215                        match m.to_lowercase().as_str() {
216                            "shift" => modifiers |= KeyModifiers::SHIFT,
217                            "ctrl" => modifiers |= KeyModifiers::CONTROL,
218                            "alt" => modifiers |= KeyModifiers::ALT,
219                            "super" => modifiers |= KeyModifiers::SUPER,
220                            "hyper" => modifiers |= KeyModifiers::HYPER,
221                            "meta" => modifiers |= KeyModifiers::META,
222                            "none" => {}
223                            unknown => {
224                                return Err(E::custom(format!("Unknown modifier: {}", unknown)));
225                            }
226                        }
227                    }
228                    return Ok(Trigger::Mouse(SimpleMouseEvent { kind, modifiers }));
229                }
230
231                // 3. Try Event
232                if let Ok(evt) = value.parse::<Event>() {
233                    return Ok(Trigger::Event(evt));
234                }
235
236                Err(E::custom(format!(
237                    "failed to parse trigger from '{}'",
238                    value
239                )))
240            }
241        }
242
243        deserializer.deserialize_str(TriggerVisitor)
244    }
245}
246
247#[derive(Serialize)]
248#[serde(bound(serialize = "",))]
249struct BindFmtWrapper<'a, A: ActionExt + Display> {
250    binds: &'a BindMap<A>,
251}
252use ratatui::style::Style;
253use ratatui::text::{Line, Span, Text};
254use regex::Regex;
255
256// random ai toml coloring cuz i dont wanna use bat just for this
257pub fn display_binds<A: ActionExt + Display>(
258    binds: &BindMap<A>,
259    cfg: Option<&TomlColorConfig>,
260) -> Text<'static> {
261    let toml_string = toml::to_string(&BindFmtWrapper { binds }).unwrap();
262
263    let Some(cfg) = cfg else {
264        return Text::from(toml_string);
265    };
266
267    let section_re = Regex::new(r"^\s*\[.*\]").unwrap();
268    let key_re = Regex::new(r"^(\s*[\w_-]+)(\s*=\s*)").unwrap();
269    let string_re = Regex::new(r#""[^"]*""#).unwrap();
270    let number_re = Regex::new(r"\b\d+(\.\d+)?\b").unwrap();
271
272    let mut text = Text::default();
273
274    for line in toml_string.lines() {
275        if section_re.is_match(line) {
276            let mut style = Style::default().fg(cfg.section);
277            if cfg.section_bold {
278                style = style.add_modifier(ratatui::style::Modifier::BOLD);
279            }
280            text.extend(Text::from(Span::styled(line.to_string(), style)));
281        } else {
282            let mut spans = vec![];
283            let mut remainder = line.to_string();
284
285            // Highlight key
286            if let Some(cap) = key_re.captures(&remainder) {
287                let key = &cap[1];
288                let eq = &cap[2];
289                spans.push(Span::styled(key.to_string(), Style::default().fg(cfg.key)));
290                spans.push(Span::raw(eq.to_string()));
291                remainder = remainder[cap[0].len()..].to_string();
292            }
293
294            // Highlight strings
295            let mut last_idx = 0;
296            for m in string_re.find_iter(&remainder) {
297                if m.start() > last_idx {
298                    spans.push(Span::raw(remainder[last_idx..m.start()].to_string()));
299                }
300                spans.push(Span::styled(
301                    m.as_str().to_string(),
302                    Style::default().fg(cfg.string),
303                ));
304                last_idx = m.end();
305            }
306
307            // Highlight numbers
308            let remainder = &remainder[last_idx..];
309            let mut last_idx = 0;
310            for m in number_re.find_iter(remainder) {
311                if m.start() > last_idx {
312                    spans.push(Span::raw(remainder[last_idx..m.start()].to_string()));
313                }
314                spans.push(Span::styled(
315                    m.as_str().to_string(),
316                    Style::default().fg(cfg.number),
317                ));
318                last_idx = m.end();
319            }
320
321            if last_idx < remainder.len() {
322                spans.push(Span::raw(remainder[last_idx..].to_string()));
323            }
324
325            text.extend(Text::from(Line::from(spans)));
326        }
327    }
328
329    text
330}
331
332#[cfg(test)]
333mod test {
334    use super::*;
335    use crossterm::event::MouseEvent;
336
337    #[test]
338    fn test_bindmap_trigger() {
339        let mut bind_map: BindMap = BindMap::new();
340
341        // Insert trigger with default actions
342        let trigger0 = Trigger::Mouse(SimpleMouseEvent {
343            kind: MouseEventKind::ScrollDown,
344            modifiers: KeyModifiers::empty(),
345        });
346        bind_map.insert(trigger0.clone(), Actions::default());
347
348        // Construct via From<MouseEvent>
349        let mouse_event = MouseEvent {
350            kind: MouseEventKind::ScrollDown,
351            column: 0,
352            row: 0,
353            modifiers: KeyModifiers::empty(),
354        };
355        let from_event: Trigger = mouse_event.into();
356
357        // Should be retrievable
358        assert!(bind_map.contains_key(&from_event));
359
360        // Shift-modified trigger should NOT be found
361        let shift_trigger = Trigger::Mouse(SimpleMouseEvent {
362            kind: MouseEventKind::ScrollDown,
363            modifiers: KeyModifiers::SHIFT,
364        });
365        assert!(!bind_map.contains_key(&shift_trigger));
366    }
367}