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
75#[derive(Debug, Hash, PartialEq, Eq, Clone)]
76pub enum Trigger {
77 Key(KeyCombination),
78 Mouse(SimpleMouseEvent),
79 Event(Event),
80 Semantic(String),
83}
84
85#[derive(Debug, Eq, Clone, PartialEq, Hash)]
111pub struct SimpleMouseEvent {
112 pub kind: MouseEventKind,
113 pub modifiers: KeyModifiers,
114}
115
116impl Ord for SimpleMouseEvent {
117 fn cmp(&self, other: &Self) -> Ordering {
118 match self.kind.partial_cmp(&other.kind) {
119 Some(Ordering::Equal) | None => self.modifiers.bits().cmp(&other.modifiers.bits()),
120 Some(o) => o,
121 }
122 }
123}
124
125impl PartialOrd for SimpleMouseEvent {
126 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
127 Some(self.cmp(other))
128 }
129}
130
131impl From<crossterm::event::MouseEvent> for Trigger {
133 fn from(e: crossterm::event::MouseEvent) -> Self {
134 Trigger::Mouse(SimpleMouseEvent {
135 kind: e.kind,
136 modifiers: e.modifiers,
137 })
138 }
139}
140
141impl From<KeyCombination> for Trigger {
142 fn from(key: KeyCombination) -> Self {
143 Trigger::Key(key)
144 }
145}
146
147impl From<Event> for Trigger {
148 fn from(event: Event) -> Self {
149 Trigger::Event(event)
150 }
151}
152impl Display for Trigger {
155 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156 match self {
157 Trigger::Key(key) => write!(f, "{}", key),
158 Trigger::Mouse(event) => {
159 if event.modifiers.contains(KeyModifiers::SHIFT) {
160 write!(f, "shift+")?;
161 }
162 if event.modifiers.contains(KeyModifiers::CONTROL) {
163 write!(f, "ctrl+")?;
164 }
165 if event.modifiers.contains(KeyModifiers::ALT) {
166 write!(f, "alt+")?;
167 }
168 if event.modifiers.contains(KeyModifiers::SUPER) {
169 write!(f, "super+")?;
170 }
171 if event.modifiers.contains(KeyModifiers::HYPER) {
172 write!(f, "hyper+")?;
173 }
174 if event.modifiers.contains(KeyModifiers::META) {
175 write!(f, "meta+")?;
176 }
177 write!(f, "{}", mouse_event_kind_as_str(event.kind))
178 }
179 Trigger::Event(event) => write!(f, "{}", event),
180 Trigger::Semantic(alias) => write!(f, "::{alias}"),
181 }
182 }
183}
184
185impl ser::Serialize for Trigger {
186 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
187 where
188 S: ser::Serializer,
189 {
190 serializer.serialize_str(&self.to_string())
191 }
192}
193
194pub fn mouse_event_kind_as_str(kind: MouseEventKind) -> &'static str {
195 match kind {
196 MouseEventKind::Down(MouseButton::Left) => "left",
197 MouseEventKind::Down(MouseButton::Middle) => "middle",
198 MouseEventKind::Down(MouseButton::Right) => "right",
199 MouseEventKind::ScrollDown => "scrolldown",
200 MouseEventKind::ScrollUp => "scrollup",
201 MouseEventKind::ScrollLeft => "scrollleft",
202 MouseEventKind::ScrollRight => "scrollright",
203 _ => "", }
205}
206
207impl FromStr for Trigger {
208 type Err = String;
209
210 fn from_str(value: &str) -> Result<Self, Self::Err> {
211 if let Some(s) = value.strip_prefix("::") {
212 return Ok(Trigger::Semantic(s.to_string()));
213 }
214 if let Ok(key) = KeyCombination::from_str(value) {
216 return Ok(Trigger::Key(key));
217 }
218
219 let parts: Vec<&str> = value.split('+').collect();
221 if let Some(last) = parts.last()
222 && let Some(kind) = match last.to_lowercase().as_str() {
223 "left" => Some(MouseEventKind::Down(MouseButton::Left)),
224 "middle" => Some(MouseEventKind::Down(MouseButton::Middle)),
225 "right" => Some(MouseEventKind::Down(MouseButton::Right)),
226 "scrolldown" => Some(MouseEventKind::ScrollDown),
227 "scrollup" => Some(MouseEventKind::ScrollUp),
228 "scrollleft" => Some(MouseEventKind::ScrollLeft),
229 "scrollright" => Some(MouseEventKind::ScrollRight),
230 _ => None,
231 }
232 {
233 let mut modifiers = KeyModifiers::empty();
234 for m in &parts[..parts.len() - 1] {
235 match m.to_lowercase().as_str() {
236 "shift" => modifiers |= KeyModifiers::SHIFT,
237 "ctrl" => modifiers |= KeyModifiers::CONTROL,
238 "alt" => modifiers |= KeyModifiers::ALT,
239 "super" => modifiers |= KeyModifiers::SUPER,
240 "hyper" => modifiers |= KeyModifiers::HYPER,
241 "meta" => modifiers |= KeyModifiers::META,
242 "none" => {}
243 unknown => {
244 return Err(format!("Unknown modifier: {}", unknown));
245 }
246 }
247 }
248
249 return Ok(Trigger::Mouse(SimpleMouseEvent { kind, modifiers }));
250 }
251
252 if let Ok(evt) = value.parse::<Event>() {
254 return Ok(Trigger::Event(evt));
255 }
256
257 Err(format!("failed to parse trigger from '{}'", value))
258 }
259}
260
261impl<'de> serde::Deserialize<'de> for Trigger {
262 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
263 where
264 D: Deserializer<'de>,
265 {
266 struct TriggerVisitor;
267
268 impl<'de> Visitor<'de> for TriggerVisitor {
269 type Value = Trigger;
270
271 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
272 write!(f, "a string representing a Trigger")
273 }
274
275 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
276 where
277 E: de::Error,
278 {
279 value.parse::<Trigger>().map_err(E::custom)
280 }
281 }
282
283 deserializer.deserialize_str(TriggerVisitor)
284 }
285}
286
287use ratatui::style::Style;
288use ratatui::text::{Line, Span, Text};
289
290pub fn display_binds<A: ActionExt + Display>(
291 binds: &BindMap<A>,
292 cfg: Option<&HelpColorConfig>,
293) -> Text<'static> {
294 let mut entries: Vec<(String, String)> = binds
296 .iter()
297 .map(|(trigger, actions)| {
298 let value_str = if actions.len() == 1 {
299 actions[0].to_string()
300 } else {
301 let inner = actions
302 .iter()
303 .map(|a| a.to_string())
304 .collect::<Vec<_>>()
305 .join(", ");
306 format!("[{inner}]")
307 };
308 (trigger.to_string(), value_str)
309 })
310 .collect();
311
312 entries.sort_by(|a, b| a.1.cmp(&b.1));
314
315 let Some(cfg) = cfg else {
317 let mut text = Text::default();
319 for (trigger, value) in entries {
320 text.extend(Text::from(format!("{trigger} = {value}\n")));
321 }
322 return text;
323 };
324
325 let mut text = Text::default();
326
327 for (trigger, value) in entries {
328 let mut spans = vec![];
329
330 spans.push(Span::styled(trigger, Style::default().fg(cfg.key)));
332 spans.push(Span::raw(" = "));
333
334 if value.starts_with('[') {
336 spans.push(Span::raw("["));
338 let inner = &value[1..value.len() - 1];
339 for (i, item) in inner.split(", ").enumerate() {
340 if i > 0 {
341 spans.push(Span::raw(", "));
342 }
343 spans.push(Span::styled(
344 item.to_string(),
345 Style::default().fg(cfg.value),
346 ));
347 }
348 spans.push(Span::raw("]"));
349 } else {
350 spans.push(Span::styled(value, Style::default().fg(cfg.value)));
351 }
352
353 spans.push(Span::raw("\n"));
354 text.extend(Text::from(Line::from(spans)));
355 }
356
357 text
358}
359
360#[cfg(test)]
361mod test {
362 use super::*;
363 use crossterm::event::MouseEvent;
364
365 #[test]
366 fn test_bindmap_trigger() {
367 let mut bind_map: BindMap = BindMap::new();
368
369 let trigger0 = Trigger::Mouse(SimpleMouseEvent {
371 kind: MouseEventKind::ScrollDown,
372 modifiers: KeyModifiers::empty(),
373 });
374 bind_map.insert(trigger0.clone(), Actions::default());
375
376 let mouse_event = MouseEvent {
378 kind: MouseEventKind::ScrollDown,
379 column: 0,
380 row: 0,
381 modifiers: KeyModifiers::empty(),
382 };
383 let from_event: Trigger = mouse_event.into();
384
385 assert!(bind_map.contains_key(&from_event));
387
388 let shift_trigger = Trigger::Mouse(SimpleMouseEvent {
390 kind: MouseEventKind::ScrollDown,
391 modifiers: KeyModifiers::SHIFT,
392 });
393 assert!(!bind_map.contains_key(&shift_trigger));
394 }
395}