1use std::{
2 cmp::Ordering,
3 collections::HashMap,
4 fmt::{self, Display},
5 str::FromStr,
6};
7
8use serde::{
9 Deserializer,
10 de::{self, Visitor},
11 ser,
12};
13
14use crate::{
15 action::{Action, ActionExt, Actions, NullActionExt},
16 config::HelpColorConfig,
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> = HashMap<Trigger, Actions<A>>;
26
27#[easy_ext::ext(BindMapExt)]
28impl<A: ActionExt> BindMap<A> {
29 #[allow(unused_mut)]
30 pub fn default_binds() -> Self {
31 let mut ret = bindmap!(
32 key!(ctrl-c) => Action::Quit(1),
33 key!(esc) => Action::Quit(1),
34 key!(up) => Action::Up(1),
35 key!(down) => Action::Down(1),
36 key!(enter) => Action::Accept,
37 key!(right) => Action::ForwardChar,
38 key!(left) => Action::BackwardChar,
39 key!(backspace) => Action::DeleteChar,
40 key!(ctrl-right) => Action::ForwardWord,
41 key!(ctrl-left) => Action::BackwardWord,
42 key!(ctrl-h) => Action::DeleteWord,
43 key!(ctrl-u) => Action::Cancel,
44 key!(alt-a) => Action::QueryPos(0),
45 key!(alt-h) => Action::Help("".to_string()),
46 key!(ctrl-'[') => Action::ToggleWrap,
47 key!(ctrl-']') => Action::TogglePreviewWrap,
48 key!(ctrl-shift-right) => Action::HScroll(1),
49 key!(ctrl-shift-left) => Action::HScroll(-1),
50 key!(ctrl-shift-up) => Action::VScroll(1),
51 key!(ctrl-shift-down) => Action::VScroll(-1),
52 key!(PageDown) => Action::HalfPageDown,
53 key!(PageUp) => Action::HalfPageUp,
54 key!(Home) => Action::Pos(0),
55 key!(End) => Action::Pos(-1),
56 key!(shift-PageDown) => Action::PreviewHalfPageDown,
57 key!(shift-PageUp) => Action::PreviewHalfPageUp,
58 key!(shift-Home) => Action::PreviewJump,
59 key!(shift-End) => Action::PreviewJump,
60 key!('?') => Action::SwitchPreview(None),
61 );
62
63 #[cfg(target_os = "macos")]
64 {
65 let ext = bindmap!(
66 key!(alt-left) => Action::ForwardWord,
67 key!(alt-right) => Action::BackwardWord,
68 key!(alt-backspace) => Action::DeleteWord,
69 );
70 ret.extend(ext);
71 }
72
73 ret
74 }
75
76 pub fn check_cycles(&self) -> Result<(), String> {
78 for actions in self.values() {
79 for action in actions {
80 if let Action::Semantic(s) = action {
81 let mut path = Vec::new();
82 self.dfs_semantic(s, &mut path)?;
83 }
84 }
85 }
86 Ok(())
87 }
88
89 pub fn dfs_semantic(&self, current: &str, path: &mut Vec<String>) -> Result<(), String> {
90 if path.contains(¤t.to_string()) {
91 return Err(format!(
92 "Infinite loop detected in semantic actions: {} -> {}",
93 path.join(" -> "),
94 current
95 ));
96 }
97
98 path.push(current.to_string());
99 if let Some(actions) = self.get(&Trigger::Semantic(current.to_string())) {
100 for action in actions {
101 if let Action::Semantic(next) = action {
102 self.dfs_semantic(next, path)?;
103 }
104 }
105 }
106 path.pop();
107
108 Ok(())
109 }
110}
111
112#[derive(Debug, Hash, PartialEq, Eq, Clone)]
113pub enum Trigger {
114 Key(KeyCombination),
115 Mouse(SimpleMouseEvent),
116 Event(Event),
117 Semantic(String),
120}
121
122#[derive(Debug, Eq, Clone, PartialEq, Hash)]
148pub struct SimpleMouseEvent {
149 pub kind: MouseEventKind,
150 pub modifiers: KeyModifiers,
151}
152
153impl Ord for SimpleMouseEvent {
154 fn cmp(&self, other: &Self) -> Ordering {
155 match self.kind.partial_cmp(&other.kind) {
156 Some(Ordering::Equal) | None => self.modifiers.bits().cmp(&other.modifiers.bits()),
157 Some(o) => o,
158 }
159 }
160}
161
162impl PartialOrd for SimpleMouseEvent {
163 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
164 Some(self.cmp(other))
165 }
166}
167
168impl From<crossterm::event::MouseEvent> for Trigger {
170 fn from(e: crossterm::event::MouseEvent) -> Self {
171 Trigger::Mouse(SimpleMouseEvent {
172 kind: e.kind,
173 modifiers: e.modifiers,
174 })
175 }
176}
177
178impl From<KeyCombination> for Trigger {
179 fn from(key: KeyCombination) -> Self {
180 Trigger::Key(key)
181 }
182}
183
184impl From<Event> for Trigger {
185 fn from(event: Event) -> Self {
186 Trigger::Event(event)
187 }
188}
189impl Display for Trigger {
192 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193 match self {
194 Trigger::Key(key) => write!(f, "{}", key),
195 Trigger::Mouse(event) => {
196 if event.modifiers.contains(KeyModifiers::SHIFT) {
197 write!(f, "shift+")?;
198 }
199 if event.modifiers.contains(KeyModifiers::CONTROL) {
200 write!(f, "ctrl+")?;
201 }
202 if event.modifiers.contains(KeyModifiers::ALT) {
203 write!(f, "alt+")?;
204 }
205 if event.modifiers.contains(KeyModifiers::SUPER) {
206 write!(f, "super+")?;
207 }
208 if event.modifiers.contains(KeyModifiers::HYPER) {
209 write!(f, "hyper+")?;
210 }
211 if event.modifiers.contains(KeyModifiers::META) {
212 write!(f, "meta+")?;
213 }
214 write!(f, "{}", mouse_event_kind_as_str(event.kind))
215 }
216 Trigger::Event(event) => write!(f, "{}", event),
217 Trigger::Semantic(alias) => write!(f, "@{alias}"),
218 }
219 }
220}
221
222impl ser::Serialize for Trigger {
223 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
224 where
225 S: ser::Serializer,
226 {
227 serializer.serialize_str(&self.to_string())
228 }
229}
230
231pub fn mouse_event_kind_as_str(kind: MouseEventKind) -> &'static str {
232 match kind {
233 MouseEventKind::Down(MouseButton::Left) => "left",
234 MouseEventKind::Down(MouseButton::Middle) => "middle",
235 MouseEventKind::Down(MouseButton::Right) => "right",
236 MouseEventKind::ScrollDown => "scrolldown",
237 MouseEventKind::ScrollUp => "scrollup",
238 MouseEventKind::ScrollLeft => "scrollleft",
239 MouseEventKind::ScrollRight => "scrollright",
240 _ => "", }
242}
243
244impl FromStr for Trigger {
245 type Err = String;
246
247 fn from_str(value: &str) -> Result<Self, Self::Err> {
248 if let Some(s) = value.strip_prefix("@")
250 && !s.is_empty()
251 {
252 return Ok(Trigger::Semantic(s.to_string()));
253 }
254
255 if let Ok(key) = KeyCombination::from_str(value) {
257 return Ok(Trigger::Key(key));
258 }
259
260 let parts: Vec<&str> = value.split('+').collect();
262 if let Some(last) = parts.last()
263 && let Some(kind) = match last.to_lowercase().as_str() {
264 "left" => Some(MouseEventKind::Down(MouseButton::Left)),
265 "middle" => Some(MouseEventKind::Down(MouseButton::Middle)),
266 "right" => Some(MouseEventKind::Down(MouseButton::Right)),
267 "scrolldown" => Some(MouseEventKind::ScrollDown),
268 "scrollup" => Some(MouseEventKind::ScrollUp),
269 "scrollleft" => Some(MouseEventKind::ScrollLeft),
270 "scrollright" => Some(MouseEventKind::ScrollRight),
271 _ => None,
272 }
273 {
274 let mut modifiers = KeyModifiers::empty();
275 for m in &parts[..parts.len() - 1] {
276 match m.to_lowercase().as_str() {
277 "shift" => modifiers |= KeyModifiers::SHIFT,
278 "ctrl" => modifiers |= KeyModifiers::CONTROL,
279 "alt" => modifiers |= KeyModifiers::ALT,
280 "super" => modifiers |= KeyModifiers::SUPER,
281 "hyper" => modifiers |= KeyModifiers::HYPER,
282 "meta" => modifiers |= KeyModifiers::META,
283 "none" => {}
284 unknown => return Err(format!("Unknown modifier: {}", unknown)),
285 }
286 }
287
288 return Ok(Trigger::Mouse(SimpleMouseEvent { kind, modifiers }));
289 }
290
291 if let Ok(event) = value.parse::<Event>() {
292 return Ok(Trigger::Event(event));
293 }
294
295 Err(format!("failed to parse trigger from '{}'", value))
296 }
297}
298
299impl<'de> serde::Deserialize<'de> for Trigger {
300 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
301 where
302 D: Deserializer<'de>,
303 {
304 struct TriggerVisitor;
305
306 impl<'de> Visitor<'de> for TriggerVisitor {
307 type Value = Trigger;
308
309 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
310 write!(f, "a string representing a Trigger")
311 }
312
313 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
314 where
315 E: de::Error,
316 {
317 value.parse::<Trigger>().map_err(E::custom)
318 }
319 }
320
321 deserializer.deserialize_str(TriggerVisitor)
322 }
323}
324
325use ratatui::style::Style;
326use ratatui::text::{Line, Span, Text};
327
328pub fn display_binds<A: ActionExt + Display>(
329 binds: &BindMap<A>,
330 cfg: Option<&HelpColorConfig>,
331) -> Text<'static> {
332 let mut entries: Vec<(String, String)> = binds
334 .iter()
335 .map(|(trigger, actions)| {
336 let value_str = if actions.len() == 1 {
337 actions[0].to_string()
338 } else {
339 let inner = actions
340 .iter()
341 .map(|a| a.to_string())
342 .collect::<Vec<_>>()
343 .join(", ");
344 format!("[{inner}]")
345 };
346 (trigger.to_string(), value_str)
347 })
348 .collect();
349
350 entries.sort_by(|a, b| a.1.cmp(&b.1));
352
353 let Some(cfg) = cfg else {
355 let mut text = Text::default();
357 for (trigger, value) in entries {
358 text.extend(Text::from(format!("{trigger} = {value}\n")));
359 }
360 return text;
361 };
362
363 let mut text = Text::default();
364
365 for (trigger, value) in entries {
366 let mut spans = vec![];
367
368 spans.push(Span::styled(trigger, Style::default().fg(cfg.key)));
370 spans.push(Span::raw(" = "));
371
372 if value.starts_with('[') {
374 spans.push(Span::raw("["));
376 let inner = &value[1..value.len() - 1];
377 for (i, item) in inner.split(", ").enumerate() {
378 if i > 0 {
379 spans.push(Span::raw(", "));
380 }
381 spans.push(Span::styled(
382 item.to_string(),
383 Style::default().fg(cfg.value),
384 ));
385 }
386 spans.push(Span::raw("]"));
387 } else {
388 spans.push(Span::styled(value, Style::default().fg(cfg.value)));
389 }
390
391 spans.push(Span::raw("\n"));
392 text.extend(Text::from(Line::from(spans)));
393 }
394
395 text
396}
397
398#[cfg(test)]
399mod test {
400 use super::*;
401 use crossterm::event::MouseEvent;
402
403 #[test]
404 fn test_bindmap_trigger() {
405 let mut bind_map: BindMap = BindMap::new();
406
407 let trigger0 = Trigger::Mouse(SimpleMouseEvent {
409 kind: MouseEventKind::ScrollDown,
410 modifiers: KeyModifiers::empty(),
411 });
412 bind_map.insert(trigger0.clone(), Actions::default());
413
414 let mouse_event = MouseEvent {
416 kind: MouseEventKind::ScrollDown,
417 column: 0,
418 row: 0,
419 modifiers: KeyModifiers::empty(),
420 };
421 let from_event: Trigger = mouse_event.into();
422
423 assert!(bind_map.contains_key(&from_event));
425
426 let shift_trigger = Trigger::Mouse(SimpleMouseEvent {
428 kind: MouseEventKind::ScrollDown,
429 modifiers: KeyModifiers::SHIFT,
430 });
431 assert!(!bind_map.contains_key(&shift_trigger));
432 }
433
434 #[test]
435 fn test_semantic_parsing() {
436 assert_eq!(
437 Trigger::from_str("@foo").unwrap(),
438 Trigger::Semantic("foo".into())
439 );
440 let trigger = Trigger::from_str("@").unwrap();
441 assert!(matches!(trigger, Trigger::Key(_)));
443
444 assert_eq!(
445 Action::<NullActionExt>::from_str("@foo").unwrap(),
446 Action::Semantic("foo".into())
447 );
448 assert!(Action::<NullActionExt>::from_str("@").is_err());
449 }
450
451 #[test]
452 fn test_check_cycles() {
453 use crate::bindmap;
454 let bind_map: BindMap = bindmap!(
455 Trigger::Semantic("a".into()) => Action::Semantic("b".into()),
456 Trigger::Semantic("b".into()) => Action::Semantic("a".into()),
457 );
458 assert!(bind_map.check_cycles().is_err());
459
460 let bind_map_no_cycle: BindMap = bindmap!(
461 Trigger::Semantic("a".into()) => Action::Semantic("b".into()),
462 Trigger::Semantic("b".into()) => Action::Print("ok".into()),
463 );
464 assert!(bind_map_no_cycle.check_cycles().is_ok());
465
466 let bind_map_self_cycle: BindMap = bindmap!(
467 Trigger::Semantic("a".into()) => Action::Semantic("a".into()),
468 );
469 assert!(bind_map_self_cycle.check_cycles().is_err());
470
471 let bind_map_indirect_cycle: BindMap = bindmap!(
472 key!(a) => Action::Semantic("foo".into()),
473 Trigger::Semantic("foo".into()) => Action::Semantic("foo".into()),
474 );
475 assert!(bind_map_indirect_cycle.check_cycles().is_err());
476 }
477}