matchmaker/
binds.rs

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