matchmaker/
binds.rs

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