1use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
4use std::fmt::Write as _;
5
6#[must_use]
8pub fn format_key(ke: &KeyEvent) -> String {
9 let mut s = String::new();
10 if ke.modifiers.contains(KeyModifiers::CONTROL) {
11 s.push_str("Ctrl-");
12 }
13 if ke.modifiers.contains(KeyModifiers::ALT) {
14 s.push_str("Alt-");
15 }
16 if ke.modifiers.contains(KeyModifiers::SHIFT) {
17 if let KeyCode::Char(c) = ke.code {
18 if !c.is_ascii_uppercase() {
19 s.push_str("Shift-");
20 }
21 } else {
22 s.push_str("Shift-");
23 }
24 }
25
26 match ke.code {
27 KeyCode::Char(c) => s.push(c),
28 KeyCode::Enter => s.push_str("Enter"),
29 KeyCode::Tab => s.push_str("Tab"),
30 KeyCode::BackTab => s.push_str("BackTab"),
31 KeyCode::Backspace => s.push_str("Backspace"),
32 KeyCode::Esc => s.push_str("Esc"),
33 KeyCode::Up => s.push_str("Up"),
34 KeyCode::Down => s.push_str("Down"),
35 KeyCode::Left => s.push_str("Left"),
36 KeyCode::Right => s.push_str("Right"),
37 KeyCode::PageUp => s.push_str("PageUp"),
38 KeyCode::PageDown => s.push_str("PageDown"),
39 KeyCode::Home => s.push_str("Home"),
40 KeyCode::End => s.push_str("End"),
41 KeyCode::Delete => s.push_str("Delete"),
42 KeyCode::Insert => s.push_str("Insert"),
43 KeyCode::F(n) => {
44 let _ = write!(s, "F{n}");
45 }
46 KeyCode::Null => s.push_str("Null"),
47 _ => s.push_str("Unknown"),
48 }
49 s
50}
51
52#[must_use]
56pub fn format_help_table_from_entries<'a, I>(entries: I) -> String
57where
58 I: IntoIterator<Item = (&'a KeyEvent, &'a str)>,
59{
60 let mut groups: Vec<(&'a str, Vec<String>)> = Vec::new();
61 for (key, desc) in entries {
62 let trimmed_desc = desc.trim();
63 if trimmed_desc.is_empty() {
64 continue;
65 }
66 let key_str = format_key(key);
67 if let Some((_, keys)) = groups.iter_mut().find(|(d, _)| *d == trimmed_desc) {
68 if !keys.contains(&key_str) {
69 keys.push(key_str);
70 }
71 } else {
72 groups.push((trimmed_desc, vec![key_str]));
73 }
74 }
75
76 if groups.is_empty() {
77 return String::new();
78 }
79
80 let mut out = String::new();
81 out.push_str(" COMMAND SUMMARY\n\n");
82 out.push_str(" Key(s) Action\n");
83 out.push_str(" ------ ------\n");
84
85 for (desc, keys) in groups {
86 let keys_str = keys.join(", ");
87 let _ = writeln!(out, " {keys_str:<30} {desc}");
88 }
89
90 out
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use crossterm::event::KeyEventState;
97
98 #[test]
99 fn test_format_key() {
100 let k1 = KeyEvent {
101 code: KeyCode::Char('q'),
102 modifiers: KeyModifiers::NONE,
103 kind: crossterm::event::KeyEventKind::Press,
104 state: KeyEventState::NONE,
105 };
106 assert_eq!(format_key(&k1), "q");
107
108 let k2 = KeyEvent {
109 code: KeyCode::Char('c'),
110 modifiers: KeyModifiers::CONTROL,
111 kind: crossterm::event::KeyEventKind::Press,
112 state: KeyEventState::NONE,
113 };
114 assert_eq!(format_key(&k2), "Ctrl-c");
115
116 let k3 = KeyEvent {
117 code: KeyCode::Up,
118 modifiers: KeyModifiers::ALT,
119 kind: crossterm::event::KeyEventKind::Press,
120 state: KeyEventState::NONE,
121 };
122 assert_eq!(format_key(&k3), "Alt-Up");
123 }
124
125 #[test]
126 fn test_format_help_table_from_entries() {
127 let k1 = KeyEvent {
128 code: KeyCode::Char('q'),
129 modifiers: KeyModifiers::NONE,
130 kind: crossterm::event::KeyEventKind::Press,
131 state: KeyEventState::NONE,
132 };
133 let k2 = KeyEvent {
134 code: KeyCode::Char('c'),
135 modifiers: KeyModifiers::CONTROL,
136 kind: crossterm::event::KeyEventKind::Press,
137 state: KeyEventState::NONE,
138 };
139 let k3 = KeyEvent {
140 code: KeyCode::Char('x'),
141 modifiers: KeyModifiers::NONE,
142 kind: crossterm::event::KeyEventKind::Press,
143 state: KeyEventState::NONE,
144 };
145
146 let entries = vec![(&k1, "quit"), (&k2, "quit"), (&k3, "")];
147 let table = format_help_table_from_entries(entries);
148 assert!(table.contains("COMMAND SUMMARY"));
149 assert!(table.contains("q, Ctrl-c"));
150 assert!(table.contains("quit"));
151 assert!(!table.contains(" x "));
152
153 let empty_table = format_help_table_from_entries(Vec::<(&KeyEvent, &str)>::new());
154 assert!(empty_table.is_empty());
155 }
156}