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 return Ok(Trigger::Semantic(s.to_string()));
251 }
252
253 if let Ok(key) = KeyCombination::from_str(value) {
255 return Ok(Trigger::Key(key));
256 }
257
258 let parts: Vec<&str> = value.split('+').collect();
260 if let Some(last) = parts.last()
261 && let Some(kind) = match last.to_lowercase().as_str() {
262 "left" => Some(MouseEventKind::Down(MouseButton::Left)),
263 "middle" => Some(MouseEventKind::Down(MouseButton::Middle)),
264 "right" => Some(MouseEventKind::Down(MouseButton::Right)),
265 "scrolldown" => Some(MouseEventKind::ScrollDown),
266 "scrollup" => Some(MouseEventKind::ScrollUp),
267 "scrollleft" => Some(MouseEventKind::ScrollLeft),
268 "scrollright" => Some(MouseEventKind::ScrollRight),
269 _ => None,
270 }
271 {
272 let mut modifiers = KeyModifiers::empty();
273 for m in &parts[..parts.len() - 1] {
274 match m.to_lowercase().as_str() {
275 "shift" => modifiers |= KeyModifiers::SHIFT,
276 "ctrl" => modifiers |= KeyModifiers::CONTROL,
277 "alt" => modifiers |= KeyModifiers::ALT,
278 "super" => modifiers |= KeyModifiers::SUPER,
279 "hyper" => modifiers |= KeyModifiers::HYPER,
280 "meta" => modifiers |= KeyModifiers::META,
281 "none" => {}
282 unknown => return Err(format!("Unknown modifier: {}", unknown)),
283 }
284 }
285
286 return Ok(Trigger::Mouse(SimpleMouseEvent { kind, modifiers }));
287 }
288
289 if let Ok(event) = value.parse::<Event>() {
290 return Ok(Trigger::Event(event));
291 }
292
293 Err(format!("failed to parse trigger from '{}'", value))
294 }
295}
296
297impl<'de> serde::Deserialize<'de> for Trigger {
298 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
299 where
300 D: Deserializer<'de>,
301 {
302 struct TriggerVisitor;
303
304 impl<'de> Visitor<'de> for TriggerVisitor {
305 type Value = Trigger;
306
307 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
308 write!(f, "a string representing a Trigger")
309 }
310
311 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
312 where
313 E: de::Error,
314 {
315 value.parse::<Trigger>().map_err(E::custom)
316 }
317 }
318
319 deserializer.deserialize_str(TriggerVisitor)
320 }
321}
322
323use ratatui::style::Style;
324use ratatui::text::{Line, Span, Text};
325
326pub fn display_binds<A: ActionExt + Display>(
327 binds: &BindMap<A>,
328 cfg: Option<&HelpColorConfig>,
329) -> Text<'static> {
330 let mut entries: Vec<(String, String)> = binds
332 .iter()
333 .map(|(trigger, actions)| {
334 let value_str = if actions.len() == 1 {
335 actions[0].to_string()
336 } else {
337 let inner = actions
338 .iter()
339 .map(|a| a.to_string())
340 .collect::<Vec<_>>()
341 .join(", ");
342 format!("[{inner}]")
343 };
344 (trigger.to_string(), value_str)
345 })
346 .collect();
347
348 entries.sort_by(|a, b| a.1.cmp(&b.1));
350
351 let Some(cfg) = cfg else {
353 let mut text = Text::default();
355 for (trigger, value) in entries {
356 text.extend(Text::from(format!("{trigger} = {value}\n")));
357 }
358 return text;
359 };
360
361 let mut text = Text::default();
362
363 for (trigger, value) in entries {
364 let mut spans = vec![];
365
366 spans.push(Span::styled(trigger, Style::default().fg(cfg.key)));
368 spans.push(Span::raw(" = "));
369
370 if value.starts_with('[') {
372 spans.push(Span::raw("["));
374 let inner = &value[1..value.len() - 1];
375 for (i, item) in inner.split(", ").enumerate() {
376 if i > 0 {
377 spans.push(Span::raw(", "));
378 }
379 spans.push(Span::styled(
380 item.to_string(),
381 Style::default().fg(cfg.value),
382 ));
383 }
384 spans.push(Span::raw("]"));
385 } else {
386 spans.push(Span::styled(value, Style::default().fg(cfg.value)));
387 }
388
389 spans.push(Span::raw("\n"));
390 text.extend(Text::from(Line::from(spans)));
391 }
392
393 text
394}
395
396#[cfg(test)]
397mod test {
398 use super::*;
399 use crossterm::event::MouseEvent;
400
401 #[test]
402 fn test_bindmap_trigger() {
403 let mut bind_map: BindMap = BindMap::new();
404
405 let trigger0 = Trigger::Mouse(SimpleMouseEvent {
407 kind: MouseEventKind::ScrollDown,
408 modifiers: KeyModifiers::empty(),
409 });
410 bind_map.insert(trigger0.clone(), Actions::default());
411
412 let mouse_event = MouseEvent {
414 kind: MouseEventKind::ScrollDown,
415 column: 0,
416 row: 0,
417 modifiers: KeyModifiers::empty(),
418 };
419 let from_event: Trigger = mouse_event.into();
420
421 assert!(bind_map.contains_key(&from_event));
423
424 let shift_trigger = Trigger::Mouse(SimpleMouseEvent {
426 kind: MouseEventKind::ScrollDown,
427 modifiers: KeyModifiers::SHIFT,
428 });
429 assert!(!bind_map.contains_key(&shift_trigger));
430 }
431
432 #[test]
433 fn test_check_cycles() {
434 use crate::bindmap;
435 let bind_map: BindMap = bindmap!(
436 Trigger::Semantic("a".into()) => Action::Semantic("b".into()),
437 Trigger::Semantic("b".into()) => Action::Semantic("a".into()),
438 );
439 assert!(bind_map.check_cycles().is_err());
440
441 let bind_map_no_cycle: BindMap = bindmap!(
442 Trigger::Semantic("a".into()) => Action::Semantic("b".into()),
443 Trigger::Semantic("b".into()) => Action::Print("ok".into()),
444 );
445 assert!(bind_map_no_cycle.check_cycles().is_ok());
446
447 let bind_map_self_cycle: BindMap = bindmap!(
448 Trigger::Semantic("a".into()) => Action::Semantic("a".into()),
449 );
450 assert!(bind_map_self_cycle.check_cycles().is_err());
451
452 let bind_map_indirect_cycle: BindMap = bindmap!(
453 key!(a) => Action::Semantic("foo".into()),
454 Trigger::Semantic("foo".into()) => Action::Semantic("foo".into()),
455 );
456 assert!(bind_map_indirect_cycle.check_cycles().is_err());
457 }
458}