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 (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 FromStr for Trigger {
177 type Err = String;
178
179 fn from_str(value: &str) -> Result<Self, Self::Err> {
180 if let Ok(key) = KeyCombination::from_str(value) {
182 return Ok(Trigger::Key(key));
183 }
184
185 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 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
262pub 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 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 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 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 let trigger0 = Trigger::Mouse(SimpleMouseEvent {
349 kind: MouseEventKind::ScrollDown,
350 modifiers: KeyModifiers::empty(),
351 });
352 bind_map.insert(trigger0.clone(), Actions::default());
353
354 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 assert!(bind_map.contains_key(&from_event));
365
366 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}