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::TogglePreviewWrap,
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 FromStr for Trigger {
177    type Err = String;
178
179    fn from_str(value: &str) -> Result<Self, Self::Err> {
180        // 1. Try KeyCombination
181        if let Ok(key) = KeyCombination::from_str(value) {
182            return Ok(Trigger::Key(key));
183        }
184
185        // 2. Try MouseEvent
186        let parts: Vec<&str> = value.split('+').collect();
187        if let Some(last) = parts.last()
188            && let Some(kind) = match last.to_lowercase().as_str() {
189                "left" => Some(MouseEventKind::Down(MouseButton::Left)),
190                "middle" => Some(MouseEventKind::Down(MouseButton::Middle)),
191                "right" => Some(MouseEventKind::Down(MouseButton::Right)),
192                "scrolldown" => Some(MouseEventKind::ScrollDown),
193                "scrollup" => Some(MouseEventKind::ScrollUp),
194                "scrollleft" => Some(MouseEventKind::ScrollLeft),
195                "scrollright" => Some(MouseEventKind::ScrollRight),
196                _ => None,
197            }
198        {
199            let mut modifiers = KeyModifiers::empty();
200            for m in &parts[..parts.len() - 1] {
201                match m.to_lowercase().as_str() {
202                    "shift" => modifiers |= KeyModifiers::SHIFT,
203                    "ctrl" => modifiers |= KeyModifiers::CONTROL,
204                    "alt" => modifiers |= KeyModifiers::ALT,
205                    "super" => modifiers |= KeyModifiers::SUPER,
206                    "hyper" => modifiers |= KeyModifiers::HYPER,
207                    "meta" => modifiers |= KeyModifiers::META,
208                    "none" => {}
209                    unknown => {
210                        return Err(format!("Unknown modifier: {}", unknown));
211                    }
212                }
213            }
214
215            return Ok(Trigger::Mouse(SimpleMouseEvent { kind, modifiers }));
216        }
217
218        // 3. Try Event
219        if let Ok(evt) = value.parse::<Event>() {
220            return Ok(Trigger::Event(evt));
221        }
222
223        Err(format!("failed to parse trigger from '{}'", value))
224    }
225}
226
227impl<'de> serde::Deserialize<'de> for Trigger {
228    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
229    where
230        D: Deserializer<'de>,
231    {
232        struct TriggerVisitor;
233
234        impl<'de> Visitor<'de> for TriggerVisitor {
235            type Value = Trigger;
236
237            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
238                write!(f, "a string representing a Trigger")
239            }
240
241            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
242            where
243                E: de::Error,
244            {
245                value.parse::<Trigger>().map_err(E::custom)
246            }
247        }
248
249        deserializer.deserialize_str(TriggerVisitor)
250    }
251}
252
253#[derive(Serialize)]
254#[serde(bound(serialize = "",))]
255struct BindFmtWrapper<'a, A: ActionExt + Display> {
256    binds: &'a BindMap<A>,
257}
258use ratatui::style::Style;
259use ratatui::text::{Line, Span, Text};
260use regex::Regex;
261
262// random ai toml coloring cuz i dont wanna use bat just for this
263pub fn display_binds<A: ActionExt + Display>(
264    binds: &BindMap<A>,
265    cfg: Option<&TomlColorConfig>,
266) -> Text<'static> {
267    let toml_string = toml::to_string(&BindFmtWrapper { binds }).unwrap();
268
269    let Some(cfg) = cfg else {
270        return Text::from(toml_string);
271    };
272
273    let section_re = Regex::new(r"^\s*\[.*\]").unwrap();
274    let key_re = Regex::new(r"^(\s*[\w_-]+)(\s*=\s*)").unwrap();
275    let string_re = Regex::new(r#""[^"]*""#).unwrap();
276    let number_re = Regex::new(r"\b\d+(\.\d+)?\b").unwrap();
277
278    let mut text = Text::default();
279
280    for line in toml_string.lines() {
281        if section_re.is_match(line) {
282            let mut style = Style::default().fg(cfg.section);
283            if cfg.section_bold {
284                style = style.add_modifier(ratatui::style::Modifier::BOLD);
285            }
286            text.extend(Text::from(Span::styled(line.to_string(), style)));
287        } else {
288            let mut spans = vec![];
289            let mut remainder = line.to_string();
290
291            // Highlight key
292            if let Some(cap) = key_re.captures(&remainder) {
293                let key = &cap[1];
294                let eq = &cap[2];
295                spans.push(Span::styled(key.to_string(), Style::default().fg(cfg.key)));
296                spans.push(Span::raw(eq.to_string()));
297                remainder = remainder[cap[0].len()..].to_string();
298            }
299
300            // Highlight strings
301            let mut last_idx = 0;
302            for m in string_re.find_iter(&remainder) {
303                if m.start() > last_idx {
304                    spans.push(Span::raw(remainder[last_idx..m.start()].to_string()));
305                }
306                spans.push(Span::styled(
307                    m.as_str().to_string(),
308                    Style::default().fg(cfg.string),
309                ));
310                last_idx = m.end();
311            }
312
313            // Highlight numbers
314            let remainder = &remainder[last_idx..];
315            let mut last_idx = 0;
316            for m in number_re.find_iter(remainder) {
317                if m.start() > last_idx {
318                    spans.push(Span::raw(remainder[last_idx..m.start()].to_string()));
319                }
320                spans.push(Span::styled(
321                    m.as_str().to_string(),
322                    Style::default().fg(cfg.number),
323                ));
324                last_idx = m.end();
325            }
326
327            if last_idx < remainder.len() {
328                spans.push(Span::raw(remainder[last_idx..].to_string()));
329            }
330
331            text.extend(Text::from(Line::from(spans)));
332        }
333    }
334
335    text
336}
337
338#[cfg(test)]
339mod test {
340    use super::*;
341    use crossterm::event::MouseEvent;
342
343    #[test]
344    fn test_bindmap_trigger() {
345        let mut bind_map: BindMap = BindMap::new();
346
347        // Insert trigger with default actions
348        let trigger0 = Trigger::Mouse(SimpleMouseEvent {
349            kind: MouseEventKind::ScrollDown,
350            modifiers: KeyModifiers::empty(),
351        });
352        bind_map.insert(trigger0.clone(), Actions::default());
353
354        // Construct via From<MouseEvent>
355        let mouse_event = MouseEvent {
356            kind: MouseEventKind::ScrollDown,
357            column: 0,
358            row: 0,
359            modifiers: KeyModifiers::empty(),
360        };
361        let from_event: Trigger = mouse_event.into();
362
363        // Should be retrievable
364        assert!(bind_map.contains_key(&from_event));
365
366        // Shift-modified trigger should NOT be found
367        let shift_trigger = Trigger::Mouse(SimpleMouseEvent {
368            kind: MouseEventKind::ScrollDown,
369            modifiers: KeyModifiers::SHIFT,
370        });
371        assert!(!bind_map.contains_key(&shift_trigger));
372    }
373}