1use std::{
2 cmp::Ordering,
3 collections::HashMap,
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> = 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 ser::Serialize for Trigger {
155 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
156 where
157 S: ser::Serializer,
158 {
159 match self {
160 Trigger::Key(key) => serializer.serialize_str(&key.to_string()),
161 Trigger::Mouse(event) => {
162 let mut s = String::new();
163 if event.modifiers.contains(KeyModifiers::SHIFT) {
164 s.push_str("shift+");
165 }
166 if event.modifiers.contains(KeyModifiers::CONTROL) {
167 s.push_str("ctrl+");
168 }
169 if event.modifiers.contains(KeyModifiers::ALT) {
170 s.push_str("alt+");
171 }
172 if event.modifiers.contains(KeyModifiers::SUPER) {
173 s.push_str("super+");
174 }
175 if event.modifiers.contains(KeyModifiers::HYPER) {
176 s.push_str("hyper+");
177 }
178 if event.modifiers.contains(KeyModifiers::META) {
179 s.push_str("meta+");
180 }
181 s.push_str(mouse_event_kind_as_str(event.kind));
182 serializer.serialize_str(&s)
183 }
184 Trigger::Event(event) => serializer.serialize_str(&event.to_string()),
185 Trigger::Semantic(alias) => serializer.serialize_str(&format!("::{alias}")),
186 }
187 }
188}
189
190pub fn mouse_event_kind_as_str(kind: MouseEventKind) -> &'static str {
191 match kind {
192 MouseEventKind::Down(MouseButton::Left) => "left",
193 MouseEventKind::Down(MouseButton::Middle) => "middle",
194 MouseEventKind::Down(MouseButton::Right) => "right",
195 MouseEventKind::ScrollDown => "scrolldown",
196 MouseEventKind::ScrollUp => "scrollup",
197 MouseEventKind::ScrollLeft => "scrollleft",
198 MouseEventKind::ScrollRight => "scrollright",
199 _ => "", }
201}
202
203impl FromStr for Trigger {
204 type Err = String;
205
206 fn from_str(value: &str) -> Result<Self, Self::Err> {
207 if let Some(s) = value.strip_prefix("::") {
208 return Ok(Trigger::Semantic(s.to_string()));
209 }
210 if let Ok(key) = KeyCombination::from_str(value) {
212 return Ok(Trigger::Key(key));
213 }
214
215 let parts: Vec<&str> = value.split('+').collect();
217 if let Some(last) = parts.last()
218 && let Some(kind) = match last.to_lowercase().as_str() {
219 "left" => Some(MouseEventKind::Down(MouseButton::Left)),
220 "middle" => Some(MouseEventKind::Down(MouseButton::Middle)),
221 "right" => Some(MouseEventKind::Down(MouseButton::Right)),
222 "scrolldown" => Some(MouseEventKind::ScrollDown),
223 "scrollup" => Some(MouseEventKind::ScrollUp),
224 "scrollleft" => Some(MouseEventKind::ScrollLeft),
225 "scrollright" => Some(MouseEventKind::ScrollRight),
226 _ => None,
227 }
228 {
229 let mut modifiers = KeyModifiers::empty();
230 for m in &parts[..parts.len() - 1] {
231 match m.to_lowercase().as_str() {
232 "shift" => modifiers |= KeyModifiers::SHIFT,
233 "ctrl" => modifiers |= KeyModifiers::CONTROL,
234 "alt" => modifiers |= KeyModifiers::ALT,
235 "super" => modifiers |= KeyModifiers::SUPER,
236 "hyper" => modifiers |= KeyModifiers::HYPER,
237 "meta" => modifiers |= KeyModifiers::META,
238 "none" => {}
239 unknown => {
240 return Err(format!("Unknown modifier: {}", unknown));
241 }
242 }
243 }
244
245 return Ok(Trigger::Mouse(SimpleMouseEvent { kind, modifiers }));
246 }
247
248 if let Ok(evt) = value.parse::<Event>() {
250 return Ok(Trigger::Event(evt));
251 }
252
253 Err(format!("failed to parse trigger from '{}'", value))
254 }
255}
256
257impl<'de> serde::Deserialize<'de> for Trigger {
258 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
259 where
260 D: Deserializer<'de>,
261 {
262 struct TriggerVisitor;
263
264 impl<'de> Visitor<'de> for TriggerVisitor {
265 type Value = Trigger;
266
267 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
268 write!(f, "a string representing a Trigger")
269 }
270
271 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
272 where
273 E: de::Error,
274 {
275 value.parse::<Trigger>().map_err(E::custom)
276 }
277 }
278
279 deserializer.deserialize_str(TriggerVisitor)
280 }
281}
282
283use ratatui::style::Style;
284use ratatui::text::{Line, Span, Text};
285use regex::Regex;
286
287pub fn display_binds<A: ActionExt + Display>(
289 binds: &BindMap<A>,
290 cfg: Option<&TomlColorConfig>,
291) -> Text<'static> {
292 let toml_string = toml::to_string(&BindFmtWrapper { binds }).unwrap();
293
294 let Some(cfg) = cfg else {
295 return Text::from(toml_string);
296 };
297
298 let section_re = Regex::new(r"^\s*\[.*\]").unwrap();
299 let key_re = Regex::new(r"^(\s*[\w_-]+)(\s*=\s*)").unwrap();
300 let string_re = Regex::new(r#""[^"]*""#).unwrap();
301 let number_re = Regex::new(r"\b\d+(\.\d+)?\b").unwrap();
302
303 let mut text = Text::default();
304
305 for line in toml_string.lines() {
306 if section_re.is_match(line) {
307 let mut style = Style::default().fg(cfg.section);
308 if cfg.section_bold {
309 style = style.add_modifier(ratatui::style::Modifier::BOLD);
310 }
311 text.extend(Text::from(Span::styled(line.to_string(), style)));
312 } else {
313 let mut spans = vec![];
314 let mut remainder = line.to_string();
315
316 if let Some(cap) = key_re.captures(&remainder) {
318 let key = &cap[1];
319 let eq = &cap[2];
320 spans.push(Span::styled(key.to_string(), Style::default().fg(cfg.key)));
321 spans.push(Span::raw(eq.to_string()));
322 remainder = remainder[cap[0].len()..].to_string();
323 }
324
325 let mut last_idx = 0;
327 for m in string_re.find_iter(&remainder) {
328 if m.start() > last_idx {
329 spans.push(Span::raw(remainder[last_idx..m.start()].to_string()));
330 }
331 spans.push(Span::styled(
332 m.as_str().to_string(),
333 Style::default().fg(cfg.string),
334 ));
335 last_idx = m.end();
336 }
337
338 let remainder = &remainder[last_idx..];
340 let mut last_idx = 0;
341 for m in number_re.find_iter(remainder) {
342 if m.start() > last_idx {
343 spans.push(Span::raw(remainder[last_idx..m.start()].to_string()));
344 }
345 spans.push(Span::styled(
346 m.as_str().to_string(),
347 Style::default().fg(cfg.number),
348 ));
349 last_idx = m.end();
350 }
351
352 if last_idx < remainder.len() {
353 spans.push(Span::raw(remainder[last_idx..].to_string()));
354 }
355
356 text.extend(Text::from(Line::from(spans)));
357 }
358 }
359
360 text
361}
362
363struct BindFmtWrapper<'a, A: ActionExt + Display> {
364 binds: &'a BindMap<A>,
365}
366
367use serde::ser::{SerializeMap, Serializer};
368
369impl<'a, A> Serialize for BindFmtWrapper<'a, A>
370where
371 A: ActionExt + Display,
372{
373 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
374 where
375 S: Serializer,
376 {
377 let mut entries: Vec<_> = self.binds.iter().collect();
378
379 entries.sort_by(|(_, v1), (_, v2)| {
381 v1.0.iter()
382 .map(ToString::to_string)
383 .cmp(v2.0.iter().map(ToString::to_string))
384 });
385
386 let mut map = serializer.serialize_map(Some(entries.len()))?;
387 for (k, v) in entries {
388 map.serialize_entry(k, v)?;
389 }
390 map.end()
391 }
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}