matchmaker/
binds.rs

1use crokey::KeyCombination;
2use crossterm::event::{KeyModifiers, MouseEventKind};
3use serde::{Deserializer, Serialize, ser};
4use std::cmp::Ordering;
5use std::collections::BTreeMap;
6use std::{fmt, str::FromStr};
7
8use crossterm::event::MouseButton;
9use serde::de::{self, Visitor};
10
11use crate::config::TomlColorConfig;
12use crate::{action::Actions, message::Event};
13
14pub type BindMap = BTreeMap<Trigger, Actions>;
15
16#[derive(Debug, Hash, PartialEq, Eq, Clone)]
17pub enum Trigger {
18    Key(KeyCombination),
19    Mouse(MouseEvent),
20    Event(Event),
21}
22
23impl Ord for Trigger {
24    fn cmp(&self, other: &Self) -> Ordering {
25        use Trigger::*;
26
27        match (self, other) {
28            (Key(a), Key(b)) => a.to_string().cmp(&b.to_string()),
29            (Mouse(a), Mouse(b)) => mouse_event_kind_as_str(a.kind).cmp(mouse_event_kind_as_str(b.kind)),
30            (Event(a), Event(b)) => a.to_string().cmp(&b.to_string()),
31
32            // define variant order
33            (Key(_), _) => Ordering::Less,
34            (Mouse(_), Key(_)) => Ordering::Greater,
35            (Mouse(_), Event(_)) => Ordering::Less,
36            (Event(_), _) => Ordering::Greater,
37        }
38    }
39}
40
41impl PartialOrd for Trigger {
42    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
43        Some(self.cmp(other))
44    }
45}
46
47#[derive(Debug, Hash, PartialEq, Eq, Clone)]
48pub struct MouseEvent {
49    pub kind: MouseEventKind,
50    pub modifiers: KeyModifiers,
51}
52
53// ---------- BOILERPLATE
54impl From<crossterm::event::MouseEvent> for Trigger {
55    fn from(e: crossterm::event::MouseEvent) -> Self {
56        Trigger::Mouse(MouseEvent {
57            kind: e.kind,
58            modifiers: e.modifiers,
59        })
60    }
61}
62
63impl From<KeyCombination> for Trigger {
64    fn from(key: KeyCombination) -> Self {
65        Trigger::Key(key)
66    }
67}
68
69impl From<Event> for Trigger {
70    fn from(event: Event) -> Self {
71        Trigger::Event(event)
72    }
73}
74// ------------ SERDE
75
76impl ser::Serialize for Trigger {
77    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
78    where
79    S: ser::Serializer,
80    {
81        match self {
82            Trigger::Key(key) => serializer.serialize_str(&key.to_string()),
83            Trigger::Mouse(event) => {
84                let mut s = String::new();
85                if event.modifiers.contains(KeyModifiers::SHIFT) {
86                    s.push_str("shift+");
87                }
88                if event.modifiers.contains(KeyModifiers::CONTROL) {
89                    s.push_str("ctrl+");
90                }
91                if event.modifiers.contains(KeyModifiers::ALT) {
92                    s.push_str("alt+");
93                }
94                if event.modifiers.contains(KeyModifiers::SUPER) {
95                    s.push_str("super+");
96                }
97                if event.modifiers.contains(KeyModifiers::HYPER) {
98                    s.push_str("hyper+");
99                }
100                if event.modifiers.contains(KeyModifiers::META) {
101                    s.push_str("meta+");
102                }
103                s.push_str(mouse_event_kind_as_str(event.kind));
104                serializer.serialize_str(&s)
105            }
106            Trigger::Event(event) => serializer.serialize_str(&event.to_string()),
107        }
108    }
109}
110
111pub fn mouse_event_kind_as_str(kind: MouseEventKind) -> &'static str {
112    match kind {
113        MouseEventKind::Down(MouseButton::Left) => "left",
114        MouseEventKind::Down(MouseButton::Middle) => "middle",
115        MouseEventKind::Down(MouseButton::Right) => "right",
116        MouseEventKind::ScrollDown => "scrolldown",
117        MouseEventKind::ScrollUp => "scrollup",
118        MouseEventKind::ScrollLeft => "scrollleft",
119        MouseEventKind::ScrollRight => "scrollright",
120        _ => "", // Other kinds are not handled in deserialize
121    }
122}
123
124impl<'de> serde::Deserialize<'de> for Trigger {
125    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
126    where
127    D: Deserializer<'de>,
128    {
129        struct TriggerVisitor;
130
131        impl<'de> Visitor<'de> for TriggerVisitor {
132            type Value = Trigger;
133
134            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
135                write!(f, "a string representing a Trigger")
136            }
137
138            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
139            where
140            E: de::Error,
141            {
142                // 1. Try KeyCombination
143                if let Ok(key) = KeyCombination::from_str(value) {
144                    return Ok(Trigger::Key(key));
145                }
146
147                // 2. Try MouseEvent: modifiers split by '+', last = mouse button
148                let parts: Vec<&str> = value.split('+').collect();
149                if let Some(last) = parts.last()
150                && let Some(kind) = match last.to_lowercase().as_str() {
151                    "left" => Some(MouseEventKind::Down(MouseButton::Left)),
152                    "middle" => Some(MouseEventKind::Down(MouseButton::Middle)),
153                    "right" => Some(MouseEventKind::Down(MouseButton::Right)),
154                    "scrolldown" => Some(MouseEventKind::ScrollDown),
155                    "scrollup" => Some(MouseEventKind::ScrollUp),
156                    "scrollleft" => Some(MouseEventKind::ScrollLeft),
157                    "scrollright" => Some(MouseEventKind::ScrollRight),
158                    _ => None,
159                } {
160                    let mut modifiers = KeyModifiers::empty();
161                    for m in &parts[..parts.len() - 1] {
162                        match m.to_lowercase().as_str() {
163                            "shift" => modifiers |= KeyModifiers::SHIFT,
164                            "ctrl" => modifiers |= KeyModifiers::CONTROL,
165                            "alt" => modifiers |= KeyModifiers::ALT,
166                            "super" => modifiers |= KeyModifiers::SUPER,
167                            "hyper" => modifiers |= KeyModifiers::HYPER,
168                            "meta" => modifiers |= KeyModifiers::META,
169                            "none" => {}
170                            unknown => {
171                                return Err(E::custom(format!(
172                                    "Unknown modifier: {}",
173                                    unknown
174                                )));
175                            }
176                        }
177                    }
178                    return Ok(Trigger::Mouse(MouseEvent { kind, modifiers }));
179                }
180
181                // 3. Try Event
182                if let Ok(evt) = value.parse::<Event>() {
183                    return Ok(Trigger::Event(evt));
184                }
185
186                Err(E::custom(format!(
187                    "failed to parse trigger from '{}'",
188                    value
189                )))
190            }
191        }
192
193        deserializer.deserialize_str(TriggerVisitor)
194    }
195}
196
197#[derive(Serialize)]
198struct BindFmtWrapper<'a> {
199    binds: &'a BindMap
200}
201use ratatui::style::{Style};
202use ratatui::text::{Line, Span, Text};
203use regex::Regex;
204
205
206
207// random ai toml coloring cuz i dont wanna use bat just for this
208pub fn display_binds(binds: &BindMap, cfg: Option<&TomlColorConfig>) -> Text<'static> {
209    let toml_string = toml::to_string(&BindFmtWrapper { binds }).unwrap();
210
211    let Some(cfg) = cfg else {
212        return Text::from(toml_string);
213    };
214
215    let section_re = Regex::new(r"^\s*\[.*\]").unwrap();
216    let key_re = Regex::new(r"^(\s*[\w_-]+)(\s*=\s*)").unwrap();
217    let string_re = Regex::new(r#""[^"]*""#).unwrap();
218    let number_re = Regex::new(r"\b\d+(\.\d+)?\b").unwrap();
219
220    let mut text = Text::default();
221
222    for line in toml_string.lines() {
223        if section_re.is_match(line) {
224            let mut style = Style::default().fg(cfg.section);
225            if cfg.section_bold {
226                style = style.add_modifier(ratatui::style::Modifier::BOLD);
227            }
228            text.extend(Text::from(Span::styled(line.to_string(), style)));
229        } else {
230            let mut spans = vec![];
231            let mut remainder = line.to_string();
232
233            // Highlight key
234            if let Some(cap) = key_re.captures(&remainder) {
235                let key = &cap[1];
236                let eq = &cap[2];
237                spans.push(Span::styled(key.to_string(), Style::default().fg(cfg.key)));
238                spans.push(Span::raw(eq.to_string()));
239                remainder = remainder[cap[0].len()..].to_string();
240            }
241
242            // Highlight strings
243            let mut last_idx = 0;
244            for m in string_re.find_iter(&remainder) {
245                if m.start() > last_idx {
246                    spans.push(Span::raw(remainder[last_idx..m.start()].to_string()));
247                }
248                spans.push(Span::styled(
249                    m.as_str().to_string(),
250                    Style::default().fg(cfg.string),
251                ));
252                last_idx = m.end();
253            }
254
255            // Highlight numbers
256            let remainder = &remainder[last_idx..];
257            let mut last_idx = 0;
258            for m in number_re.find_iter(remainder) {
259                if m.start() > last_idx {
260                    spans.push(Span::raw(remainder[last_idx..m.start()].to_string()));
261                }
262                spans.push(Span::styled(
263                    m.as_str().to_string(),
264                    Style::default().fg(cfg.number),
265                ));
266                last_idx = m.end();
267            }
268
269            if last_idx < remainder.len() {
270                spans.push(Span::raw(remainder[last_idx..].to_string()));
271            }
272
273            text.extend(Text::from(Line::from(spans)));
274        }
275    }
276
277    text
278}