Skip to main content

gpg_tui/app/
keys.rs

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
6/// Key bindings of the application.
7pub 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/// Representation of an individual key binding.
212#[derive(Clone, Copy, Debug)]
213pub struct KeyBinding<'a> {
214	/// Key binding.
215	key: &'a str,
216	/// Brief description of the key binding action.
217	action: &'a str,
218	/// Full description of the action along with the commands.
219	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	/// Constructs a new instance of `KeyBinding`.
237	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	/// Returns the description text of the key binding.
246	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	/// Returns the key binding as a list item.
260	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}