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