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 (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#[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
105impl 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}
126impl 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 _ => "", }
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 if let Ok(key) = KeyCombination::from_str(value) {
196 return Ok(Trigger::Key(key));
197 }
198
199 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 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
256pub 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 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 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 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 let trigger0 = Trigger::Mouse(SimpleMouseEvent {
343 kind: MouseEventKind::ScrollDown,
344 modifiers: KeyModifiers::empty(),
345 });
346 bind_map.insert(trigger0.clone(), Actions::default());
347
348 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 assert!(bind_map.contains_key(&from_event));
359
360 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}