1use ratatui::style::{Color, Modifier, Style};
2use ratatui::text::{Line, Span, Text};
3use ratatui::widgets::ListItem;
4use std::fmt::{Display, Formatter, Result as FmtResult};
5
6pub const KEY_BINDINGS: &[KeyBinding] = &[
8 KeyBinding {
9 key: "?",
10 action: "show help",
11 description: r#"
12 Use arrow keys / hjkl to navigate through the key bindings.
13 Corresponding commands and additional information will be shown here.
14 :help
15 "#,
16 },
17 KeyBinding {
18 key: "o,space,enter",
19 action: "show options",
20 description: r#"
21 Shows the options menu for the current tab.
22 :options
23 "#,
24 },
25 KeyBinding {
26 key: "hjkl,arrows,pgkeys",
27 action: "navigate",
28 description: r#"
29 Scrolls the current widget or selects the next/previous tab.
30 M-<key>: scroll the table rows
31 C-<key>,pgup,pgdown: scroll to top/bottom
32 :scroll (row) up/down/left/right <amount>
33 "#,
34 },
35 KeyBinding {
36 key: "n",
37 action: "switch to normal mode",
38 description: r#"
39 Resets the application mode.
40 :normal
41 "#,
42 },
43 KeyBinding {
44 key: "v",
45 action: "switch to visual mode",
46 description: r#"
47 Disables the mouse capture.
48 :visual
49 "#,
50 },
51 KeyBinding {
52 key: "c",
53 action: "switch to copy mode",
54 description: r#"
55 x: Copy the exported key
56 i: Copy the key id
57 f: Copy the key fingerprint
58 u: Copy the user id
59 1,2: Copy the content of the row
60 :copy
61 "#,
62 },
63 KeyBinding {
64 key: "p,C-v",
65 action: "paste from clipboard",
66 description: ":paste",
67 },
68 KeyBinding {
69 key: "x",
70 action: "export key",
71 description: r#"
72 Exports the key to "$GNUPGHOME/out" or specified path via `--outdir`
73 :export <pub/sec> <keyids>
74 "#,
75 },
76 KeyBinding {
77 key: "s",
78 action: "sign key",
79 description: r#"
80 Signs the key with the default secret key.
81 Same as `gpg --sign-key`
82 :sign <keyid>
83 "#,
84 },
85 KeyBinding {
86 key: "e",
87 action: "edit key",
88 description: r#"
89 Presents a menu for key management.
90 Same as `gpg --edit-key`
91 :edit <keyid>
92 "#,
93 },
94 KeyBinding {
95 key: "i",
96 action: "import key(s)",
97 description: r#"
98 Imports the keys from given files.
99 :import <file1> <file2>
100 "#,
101 },
102 KeyBinding {
103 key: "f",
104 action: "receive key",
105 description: r#"
106 Imports the keys with the given key IDs from default keyserver.
107 Same as `gpg --receive-keys`
108 :receive <keyids>
109 "#,
110 },
111 KeyBinding {
112 key: "u",
113 action: "send key",
114 description: r#"
115 Sends the key to the default keyserver.
116 :send <keyid>
117 "#,
118 },
119 KeyBinding {
120 key: "g",
121 action: "generate key",
122 description: r#"
123 Generates a new key pair with dialogs for all options.
124 Same as `gpg --full-generate-key`
125 :generate
126 "#,
127 },
128 KeyBinding {
129 key: "d,backspace",
130 action: "delete key",
131 description: r#"
132 Removes the public/secret key from the keyring.
133 :delete <pub/sec> <keyid>
134 "#,
135 },
136 KeyBinding {
137 key: "C-r",
138 action: "refresh keys",
139 description: r#"
140 Requests updates for keys on the local keyring.
141 Same as `gpg --refresh-keys`
142 :refresh keys
143 "#,
144 },
145 KeyBinding {
146 key: "a",
147 action: "toggle armored output",
148 description: r#"
149 Toggles ASCII armored output.
150 The default is to create the binary OpenPGP format.
151 :set armor <true/false>
152 "#,
153 },
154 KeyBinding {
155 key: "1,2,3",
156 action: "set detail level",
157 description: r#"
158 1: Minimum
159 2: Standard
160 3: Full
161 :set detail <level>
162 "#,
163 },
164 KeyBinding {
165 key: "t,tab",
166 action: "toggle detail (all/selected)",
167 description: ":toggle detail (all)",
168 },
169 KeyBinding {
170 key: "`",
171 action: "toggle table margin",
172 description: ":set margin <0/1>",
173 },
174 KeyBinding {
175 key: "m",
176 action: "toggle table size",
177 description: ":toggle",
178 },
179 KeyBinding {
180 key: "C-s",
181 action: "toggle style",
182 description: ":set colored <true/false>",
183 },
184 KeyBinding {
185 key: "/",
186 action: "search",
187 description: ":search <query>",
188 },
189 KeyBinding {
190 key: ":",
191 action: "run command",
192 description: "Switches to command mode for running commands.",
193 },
194 KeyBinding {
195 key: "ctrl-l,f2",
196 action: "show logs",
197 description: ":logs",
198 },
199 KeyBinding {
200 key: "r,f5",
201 action: "refresh application",
202 description: ":refresh",
203 },
204 KeyBinding {
205 key: "q,C-c/d,escape",
206 action: "quit application",
207 description: ":quit",
208 },
209];
210
211#[derive(Clone, Copy, Debug)]
213pub struct KeyBinding<'a> {
214 key: &'a str,
216 action: &'a str,
218 pub description: &'a str,
220}
221
222impl<'a> Display for KeyBinding<'a> {
223 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
224 write!(
225 f,
226 "{}\n └─{}\n ",
227 self.key
228 .split(',')
229 .fold(String::new(), |acc, v| format!("{acc}[{v}] ")),
230 self.action
231 )
232 }
233}
234
235impl<'a> KeyBinding<'a> {
236 pub fn new(key: &'a str, action: &'a str, description: &'a str) -> Self {
238 Self {
239 key,
240 action,
241 description,
242 }
243 }
244
245 pub fn get_description_text(&self, command_style: Style) -> Text<'a> {
247 let mut lines = Vec::new();
248 for line in self.description.lines().map(|v| format!("{}\n", v.trim()))
249 {
250 lines.push(if line.starts_with(':') {
251 Line::from(Span::styled(line, command_style))
252 } else {
253 Line::from(line)
254 })
255 }
256 Text::from(lines)
257 }
258
259 pub fn as_list_item(
261 &self,
262 colored: bool,
263 highlighted: bool,
264 ) -> ListItem<'a> {
265 let highlight_style = if highlighted {
266 Style::default().fg(Color::Reset)
267 } else {
268 Style::default()
269 };
270 ListItem::new(if colored {
271 Text::from(vec![
272 Line::from(self.key.split(',').fold(
273 Vec::new(),
274 |mut keys, key| {
275 keys.push(Span::styled("[", highlight_style));
276 keys.push(Span::styled(
277 key,
278 Style::default()
279 .fg(Color::Green)
280 .add_modifier(Modifier::BOLD),
281 ));
282 keys.push(Span::styled("] ", highlight_style));
283 keys
284 },
285 )),
286 Line::from(vec![
287 Span::styled(" └─", Style::default().fg(Color::DarkGray)),
288 Span::styled(self.action, highlight_style),
289 ]),
290 Line::default(),
291 ])
292 } else {
293 Text::raw(self.to_string())
294 })
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301 use pretty_assertions::assert_eq;
302 use std::borrow::Cow::Borrowed;
303 #[test]
304 fn test_app_keys() {
305 let key_binding =
306 KeyBinding::new("q,esc", "quit", "quits the application\n:quit");
307 assert_eq!("quits the application\n:quit", key_binding.description);
308 assert_eq!(
309 Text {
310 lines: vec![
311 Line {
312 spans: vec![Span {
313 content: Borrowed("quits the application"),
314 style: Style::default(),
315 }],
316 ..Default::default()
317 },
318 Line {
319 spans: vec![Span {
320 content: Borrowed(":quit\n"),
321 style: Style::default().fg(Color::Red),
322 }],
323 ..Default::default()
324 },
325 ],
326 ..Default::default()
327 },
328 key_binding.get_description_text(Style::default().fg(Color::Red))
329 );
330 assert_eq!(
331 ListItem::new(Text {
332 lines: vec![
333 Line {
334 spans: vec![Span {
335 content: Borrowed("[q] [esc] "),
336 style: Style::default(),
337 }],
338 ..Default::default()
339 },
340 Line {
341 spans: vec![Span {
342 content: Borrowed(" └─quit"),
343 style: Style::default(),
344 }],
345 ..Default::default()
346 },
347 Line {
348 spans: vec![Span {
349 content: Borrowed(" "),
350 style: Style::default(),
351 }],
352 ..Default::default()
353 },
354 ],
355 ..Default::default()
356 }),
357 key_binding.as_list_item(false, false)
358 );
359 assert_eq!(
360 ListItem::new(Text {
361 lines: vec![
362 Line {
363 spans: vec![
364 Span {
365 content: Borrowed("["),
366 style: Style {
367 fg: Some(Color::Reset),
368 ..Style::default()
369 },
370 },
371 Span {
372 content: Borrowed("q"),
373 style: Style {
374 fg: Some(Color::Green),
375 bg: None,
376 add_modifier: Modifier::BOLD,
377 sub_modifier: Modifier::empty(),
378 underline_color: None,
379 },
380 },
381 Span {
382 content: Borrowed("] "),
383 style: Style {
384 fg: Some(Color::Reset),
385 ..Style::default()
386 },
387 },
388 Span {
389 content: Borrowed("["),
390 style: Style {
391 fg: Some(Color::Reset),
392 ..Style::default()
393 },
394 },
395 Span {
396 content: Borrowed("esc"),
397 style: Style {
398 fg: Some(Color::Green),
399 bg: None,
400 add_modifier: Modifier::BOLD,
401 sub_modifier: Modifier::empty(),
402 underline_color: None,
403 },
404 },
405 Span {
406 content: Borrowed("] "),
407 style: Style {
408 fg: Some(Color::Reset),
409 ..Style::default()
410 },
411 },
412 ],
413 ..Default::default()
414 },
415 Line {
416 spans: vec![
417 Span {
418 content: Borrowed(" └─"),
419 style: Style {
420 fg: Some(Color::DarkGray),
421 ..Style::default()
422 },
423 },
424 Span {
425 content: Borrowed("quit"),
426 style: Style {
427 fg: Some(Color::Reset),
428 ..Style::default()
429 },
430 },
431 ],
432 ..Default::default()
433 },
434 Line::default(),
435 ],
436 ..Default::default()
437 }),
438 key_binding.as_list_item(true, true)
439 );
440 }
441}