Skip to main content

gpg_tui/app/
command.rs

1use crate::app::mode::Mode;
2use crate::app::prompt::OutputType;
3use crate::app::selection::Selection;
4use crate::app::style::Style;
5use crate::gpg::key::KeyType;
6use crate::widget::row::ScrollDirection;
7use clap::ValueEnum;
8use crossterm::event::KeyCode as Key;
9use std::fmt::{Display, Formatter, Result as FmtResult};
10use std::str::FromStr;
11use tui_logger::TuiWidgetEvent;
12
13/// Possible logger widget commands.
14#[derive(Clone, Debug, PartialEq)]
15pub struct LoggerCommand(pub TuiWidgetEvent);
16
17impl Eq for LoggerCommand {}
18
19impl LoggerCommand {
20	/// Parses a logger command from the given key.
21	pub fn parse(key: Key) -> Option<Self> {
22		match key {
23			Key::Char(' ') => Some(Self(TuiWidgetEvent::SpaceKey)),
24			Key::Esc => Some(Self(TuiWidgetEvent::EscapeKey)),
25			Key::PageUp => Some(Self(TuiWidgetEvent::PrevPageKey)),
26			Key::PageDown => Some(Self(TuiWidgetEvent::NextPageKey)),
27			Key::Up => Some(Self(TuiWidgetEvent::UpKey)),
28			Key::Down => Some(Self(TuiWidgetEvent::DownKey)),
29			Key::Left => Some(Self(TuiWidgetEvent::LeftKey)),
30			Key::Right => Some(Self(TuiWidgetEvent::RightKey)),
31			Key::Char('+') => Some(Self(TuiWidgetEvent::PlusKey)),
32			Key::Char('-') => Some(Self(TuiWidgetEvent::MinusKey)),
33			Key::Char('h') => Some(Self(TuiWidgetEvent::HideKey)),
34			Key::Char('f') => Some(Self(TuiWidgetEvent::FocusKey)),
35			_ => None,
36		}
37	}
38}
39
40/// Command to run on rendering process.
41///
42/// It specifies the main operation to perform on [`App`].
43///
44/// [`App`]: crate::app::launcher::App
45#[derive(Clone, Debug, PartialEq)]
46pub enum Command {
47	/// Confirm the execution of a command.
48	Confirm(Box<Command>),
49	/// Show help.
50	ShowHelp,
51	/// Change application style.
52	ChangeStyle(Style),
53	/// Show application output.
54	ShowOutput(OutputType, String),
55	/// Show popup for options menu.
56	ShowOptions,
57	/// List the public/secret keys.
58	ListKeys(KeyType),
59	/// Import public/secret keys from files or a keyserver.
60	ImportKeys(Vec<String>, bool),
61	/// Import public/secret keys from clipboard.
62	ImportClipboard,
63	/// Export the public/secret keys.
64	ExportKeys(KeyType, Vec<String>, bool),
65	/// Delete the public/secret key.
66	DeleteKey(KeyType, String),
67	/// Send the key to the default keyserver.
68	SendKey(String),
69	/// Edit a key.
70	EditKey(String),
71	/// Sign a key.
72	SignKey(String),
73	/// Generate a new key pair.
74	GenerateKey,
75	/// Refresh the keyring.
76	RefreshKeys,
77	/// Copy a property to clipboard.
78	Copy(Selection),
79	/// Toggle the detail level.
80	ToggleDetail(bool),
81	/// Toggle the table size.
82	ToggleTableSize,
83	/// Scroll the current widget.
84	Scroll(ScrollDirection, bool),
85	/// Set the value of an option.
86	Set(String, String),
87	/// Get the value of an option.
88	Get(String),
89	/// Switch the application mode.
90	SwitchMode(Mode),
91	/// Paste the clipboard contents.
92	Paste,
93	/// Enable command input.
94	EnableInput,
95	/// Search for a value.
96	Search(Option<String>),
97	/// Select the next tab.
98	NextTab,
99	/// Select the previous tab.
100	PreviousTab,
101	/// Show logs.
102	Logs,
103	/// Logger event.
104	LoggerEvent(LoggerCommand),
105	/// Refresh the application.
106	Refresh,
107	/// Quit the application.
108	Quit,
109	/// Do nothing.
110	None,
111}
112
113impl Display for Command {
114	fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
115		write!(
116			f,
117			"{}",
118			match self {
119				Command::None => String::from("close menu"),
120				Command::Refresh => String::from("refresh application"),
121				Command::RefreshKeys => String::from("refresh the keyring"),
122				Command::ShowHelp => String::from("show help"),
123				Command::ChangeStyle(style) => {
124					match style {
125						Style::Plain => String::from("disable colors"),
126						Style::Colored => String::from("enable colors"),
127					}
128				}
129				Command::ListKeys(key_type) => {
130					format!(
131						"list {} keys",
132						format!("{key_type:?}").to_lowercase()
133					)
134				}
135				Command::ImportClipboard => {
136					String::from("import key(s) from clipboard")
137				}
138				Command::ExportKeys(key_type, patterns, ref export_subkeys) => {
139					if patterns.is_empty() {
140						format!("export all the keys ({key_type})")
141					} else if *export_subkeys {
142						format!("export the selected subkeys ({key_type})")
143					} else {
144						format!("export the selected key ({key_type})")
145					}
146				}
147				Command::DeleteKey(key_type, _) =>
148					format!("delete the selected key ({key_type})"),
149				Command::SendKey(_) =>
150					String::from("send key to the keyserver"),
151				Command::EditKey(_) => String::from("edit the selected key"),
152				Command::SignKey(_) => String::from("sign the selected key"),
153				Command::GenerateKey => String::from("generate a new key pair"),
154				Command::Copy(copy_type) =>
155					format!("copy {}", copy_type.to_string().to_lowercase()),
156				Command::Paste => String::from("paste from clipboard"),
157				Command::ToggleDetail(all) => format!(
158					"toggle detail ({})",
159					if *all { "all" } else { "selected" }
160				),
161				Command::ToggleTableSize => String::from("toggle table size"),
162				Command::Set(option, ref value) => {
163					let action =
164						if value == "true" { "enable" } else { "disable" };
165					match option.as_ref() {
166						"armor" => format!("{action} armored output"),
167						"signer" => String::from("set as the signing key"),
168						"margin" => String::from("toggle table margin"),
169						"prompt" => {
170							if value == ":import " {
171								String::from("import key(s) from a file")
172							} else if value == ":receive " {
173								String::from("receive key(s) from keyserver")
174							} else {
175								format!("set prompt text to {value}")
176							}
177						}
178						_ => format!("set {option} to {value}"),
179					}
180				}
181				Command::SwitchMode(mode) => format!(
182					"switch to {} mode",
183					format!("{mode:?}").to_lowercase()
184				),
185				Command::Quit => String::from("quit application"),
186				Command::Confirm(command) => (*command).to_string(),
187				Command::Logs => String::from("show logs"),
188				_ => format!("{self:?}"),
189			}
190		)
191	}
192}
193
194impl FromStr for Command {
195	type Err = ();
196	fn from_str(s: &str) -> Result<Self, Self::Err> {
197		let mut values = s
198			.replacen(':', "", 1)
199			.to_lowercase()
200			.split_whitespace()
201			.map(String::from)
202			.collect::<Vec<String>>();
203		let command = values.first().cloned().unwrap_or_default();
204		let args = values.drain(1..).collect::<Vec<String>>();
205		match command.as_str() {
206			"confirm" => Ok(Command::Confirm(Box::new(if args.is_empty() {
207				Command::None
208			} else {
209				Command::from_str(&args.join(" "))?
210			}))),
211			"help" | "h" => Ok(Command::ShowHelp),
212			"style" => Ok(Command::ChangeStyle(
213				Style::from_str(
214					&args.first().cloned().unwrap_or_default(),
215					true,
216				)
217				.unwrap_or_default(),
218			)),
219			"output" | "out" => {
220				if !args.is_empty() {
221					Ok(Command::ShowOutput(
222						OutputType::from(
223							args.first().cloned().unwrap_or_default(),
224						),
225						args[1..].join(" "),
226					))
227				} else {
228					Err(())
229				}
230			}
231			"options" | "opt" => Ok(Command::ShowOptions),
232			"list" | "ls" => Ok(Command::ListKeys(KeyType::from_str(
233				&args.first().cloned().unwrap_or_else(|| String::from("pub")),
234			)?)),
235			"import" | "receive" => Ok(Command::ImportKeys(
236				s.replacen(':', "", 1)
237					.split_whitespace()
238					.map(String::from)
239					.skip(1)
240					.collect(),
241				command.as_str() == "receive",
242			)),
243			"import-clipboard" => Ok(Command::ImportClipboard),
244			"export" | "exp" => {
245				let mut patterns = if !args.is_empty() {
246					args[1..].to_vec()
247				} else {
248					Vec::new()
249				};
250				let export_subkeys =
251					patterns.last() == Some(&String::from("subkey"));
252				if export_subkeys {
253					patterns.truncate(patterns.len() - 1)
254				}
255				Ok(Command::ExportKeys(
256					KeyType::from_str(
257						&args
258							.first()
259							.cloned()
260							.unwrap_or_else(|| String::from("pub")),
261					)?,
262					patterns,
263					export_subkeys,
264				))
265			}
266			"delete" | "del" => {
267				let key_id = args.get(1).cloned().unwrap_or_default();
268				Ok(Command::DeleteKey(
269					KeyType::from_str(
270						&args
271							.first()
272							.cloned()
273							.unwrap_or_else(|| String::from("pub")),
274					)?,
275					if let Some(key) = key_id.strip_prefix("0x") {
276						format!("0x{}", key.to_string().to_uppercase())
277					} else {
278						key_id
279					},
280				))
281			}
282			"send" => Ok(Command::SendKey(args.first().cloned().ok_or(())?)),
283			"edit" => Ok(Command::EditKey(args.first().cloned().ok_or(())?)),
284			"sign" => Ok(Command::SignKey(args.first().cloned().ok_or(())?)),
285			"generate" | "gen" => Ok(Command::GenerateKey),
286			"copy" | "c" => {
287				if let Some(arg) = args.first().cloned() {
288					Ok(Command::Copy(
289						Selection::from_str(&arg, true).map_err(|_| ())?,
290					))
291				} else {
292					Ok(Command::SwitchMode(Mode::Copy))
293				}
294			}
295			"toggle" | "t" => {
296				if args.first() == Some(&String::from("detail")) {
297					Ok(Command::ToggleDetail(
298						args.get(1) == Some(&String::from("all")),
299					))
300				} else {
301					Ok(Command::ToggleTableSize)
302				}
303			}
304			"scroll" => {
305				let scroll_row = args.first() == Some(&String::from("row"));
306				Ok(Command::Scroll(
307					ScrollDirection::from_str(&if scroll_row {
308						args[1..].join(" ")
309					} else {
310						args.join(" ")
311					})
312					.unwrap_or(ScrollDirection::Down(1)),
313					scroll_row,
314				))
315			}
316			"set" | "s" => Ok(Command::Set(
317				args.first().cloned().unwrap_or_default(),
318				args.get(1).cloned().unwrap_or_default(),
319			)),
320			"get" | "g" => {
321				Ok(Command::Get(args.first().cloned().unwrap_or_default()))
322			}
323			"mode" | "m" => Ok(Command::SwitchMode(Mode::from_str(
324				&args.first().cloned().ok_or(())?,
325			)?)),
326			"normal" | "n" => Ok(Command::SwitchMode(Mode::Normal)),
327			"visual" | "v" => Ok(Command::SwitchMode(Mode::Visual)),
328			"paste" | "p" => Ok(Command::Paste),
329			"input" => Ok(Command::EnableInput),
330			"search" => Ok(Command::Search(args.first().cloned())),
331			"next" => Ok(Command::NextTab),
332			"previous" | "prev" => Ok(Command::PreviousTab),
333			"refresh" | "r" => {
334				if args.first() == Some(&String::from("keys")) {
335					Ok(Command::RefreshKeys)
336				} else {
337					Ok(Command::Refresh)
338				}
339			}
340			"quit" | "q" | "q!" => Ok(Command::Quit),
341			"logs" | "l" => Ok(Command::Logs),
342			"none" => Ok(Command::None),
343			_ => Err(()),
344		}
345	}
346}
347
348#[cfg(test)]
349mod tests {
350	use super::*;
351	use pretty_assertions::assert_eq;
352	#[test]
353	fn test_app_command() -> Result<(), ()> {
354		assert_eq!(
355			Command::Confirm(Box::new(Command::None)),
356			Command::from_str(":confirm none")?
357		);
358		assert_eq!(Command::ShowHelp, Command::from_str(":help")?);
359		assert_eq!(
360			Command::ShowOutput(
361				OutputType::Success,
362				String::from("operation successful"),
363			),
364			Command::from_str(":out success operation successful")?
365		);
366		assert_eq!(
367			Command::ChangeStyle(Style::Colored),
368			Command::from_str(":style colored")?
369		);
370		assert_eq!(
371			Command::ChangeStyle(Style::Plain),
372			Command::from_str(":style plain")?
373		);
374		assert_eq!(Command::ShowOptions, Command::from_str(":options")?);
375		for cmd in &[":list", ":list pub", ":ls", ":ls pub"] {
376			let command = Command::from_str(cmd)?;
377			assert_eq!(Command::ListKeys(KeyType::Public), command);
378		}
379		for cmd in &[":list sec", ":ls sec"] {
380			let command = Command::from_str(cmd)?;
381			assert_eq!(Command::ListKeys(KeyType::Secret), command);
382		}
383		assert_eq!(
384			Command::ImportKeys(
385				vec![
386					String::from("Test1"),
387					String::from("Test2"),
388					String::from("tesT3")
389				],
390				false
391			),
392			Command::from_str(":import Test1 Test2 tesT3")?
393		);
394		assert_eq!(
395			Command::ImportKeys(vec![String::from("Test"),], true),
396			Command::from_str(":receive Test")?
397		);
398		assert_eq!(
399			Command::ImportClipboard,
400			Command::from_str(":import-clipboard")?
401		);
402		for cmd in &[":export", ":export pub", ":exp", ":exp pub"] {
403			let command = Command::from_str(cmd)?;
404			assert_eq!(
405				Command::ExportKeys(KeyType::Public, Vec::new(), false),
406				command
407			);
408		}
409		assert_eq!(
410			Command::ExportKeys(
411				KeyType::Public,
412				vec![String::from("test1"), String::from("test2")],
413				false
414			),
415			Command::from_str(":export pub test1 test2")?
416		);
417		assert_eq!(
418			Command::ExportKeys(
419				KeyType::Secret,
420				vec![String::from("test3"), String::from("test4")],
421				true
422			),
423			Command::from_str(":export sec test3 test4 subkey")?
424		);
425		for cmd in &[":export sec", ":exp sec"] {
426			let command = Command::from_str(cmd)?;
427			assert_eq!(
428				Command::ExportKeys(KeyType::Secret, Vec::new(), false),
429				command
430			);
431		}
432		assert_eq!(
433			Command::ExportKeys(
434				KeyType::Secret,
435				vec![
436					String::from("test1"),
437					String::from("test2"),
438					String::from("test3")
439				],
440				false
441			),
442			Command::from_str(":export sec test1 test2 test3")?
443		);
444		for cmd in &[":delete pub xyz", ":del pub xyz"] {
445			let command = Command::from_str(cmd)?;
446			assert_eq!(
447				Command::DeleteKey(KeyType::Public, String::from("xyz")),
448				command
449			);
450		}
451		assert_eq!(
452			Command::SendKey(String::from("test")),
453			Command::from_str(":send test")?
454		);
455		assert_eq!(
456			Command::EditKey(String::from("test")),
457			Command::from_str(":edit test")?
458		);
459		assert_eq!(
460			Command::SignKey(String::from("test")),
461			Command::from_str(":sign test")?
462		);
463		assert_eq!(Command::GenerateKey, Command::from_str(":generate")?);
464		assert_eq!(Command::RefreshKeys, Command::from_str(":refresh keys")?);
465		for cmd in &[":toggle detail all", ":t detail all"] {
466			let command = Command::from_str(cmd)?;
467			assert_eq!(Command::ToggleDetail(true), command);
468		}
469		assert_eq!(Command::ToggleTableSize, Command::from_str(":toggle")?);
470		for cmd in &[":scroll up 1", ":scroll u 1"] {
471			let command = Command::from_str(cmd)?;
472			assert_eq!(Command::Scroll(ScrollDirection::Up(1), false), command);
473		}
474		for cmd in &[":set armor true", ":s armor true"] {
475			let command = Command::from_str(cmd)?;
476			assert_eq!(
477				Command::Set(String::from("armor"), String::from("true")),
478				command
479			);
480		}
481		for cmd in &[":get armor", ":g armor"] {
482			let command = Command::from_str(cmd)?;
483			assert_eq!(Command::Get(String::from("armor")), command);
484		}
485		assert_eq!(
486			Command::Set(String::from("test"), String::from("_")),
487			Command::from_str(":set test _")?
488		);
489		for cmd in &[":normal", ":n"] {
490			let command = Command::from_str(cmd)?;
491			assert_eq!(Command::SwitchMode(Mode::Normal), command);
492		}
493		for cmd in &[":visual", ":v"] {
494			let command = Command::from_str(cmd)?;
495			assert_eq!(Command::SwitchMode(Mode::Visual), command);
496		}
497		for cmd in &[":copy", ":c"] {
498			let command = Command::from_str(cmd)?;
499			assert_eq!(Command::SwitchMode(Mode::Copy), command);
500		}
501		for cmd in &[":paste", ":p"] {
502			let command = Command::from_str(cmd)?;
503			assert_eq!(Command::Paste, command);
504		}
505		assert_eq!(
506			Command::Search(Some(String::from("q"))),
507			Command::from_str(":search q")?
508		);
509		assert_eq!(Command::EnableInput, Command::from_str(":input")?);
510		assert_eq!(Command::NextTab, Command::from_str(":next")?);
511		assert_eq!(Command::PreviousTab, Command::from_str(":prev")?);
512		assert_eq!(Command::Refresh, Command::from_str(":refresh")?);
513		for cmd in &[":quit", ":q", ":q!"] {
514			let command = Command::from_str(cmd)?;
515			assert_eq!(Command::Quit, command);
516		}
517		assert_eq!(Command::None, Command::from_str(":none")?);
518		assert!(Command::from_str("test").is_err());
519		assert_eq!(Command::Logs, Command::from_str(":logs")?);
520
521		assert_eq!("close menu", Command::None.to_string());
522		assert_eq!("show help", Command::ShowHelp.to_string());
523		assert_eq!(
524			"disable colors",
525			Command::ChangeStyle(Style::Plain).to_string()
526		);
527		assert_eq!(
528			"enable colors",
529			Command::ChangeStyle(Style::Colored).to_string()
530		);
531		assert_eq!("refresh application", Command::Refresh.to_string());
532		assert_eq!("refresh the keyring", Command::RefreshKeys.to_string());
533		assert_eq!(
534			"list public keys",
535			Command::ListKeys(KeyType::Public).to_string()
536		);
537		assert_eq!(
538			"export all the keys (sec)",
539			Command::ExportKeys(KeyType::Secret, Vec::new(), false).to_string()
540		);
541		assert_eq!(
542			"export the selected subkeys (sec)",
543			Command::ExportKeys(KeyType::Secret, vec![String::new()], true)
544				.to_string()
545		);
546		assert_eq!(
547			"export the selected key (pub)",
548			Command::ExportKeys(KeyType::Public, vec![String::new()], false)
549				.to_string()
550		);
551		assert_eq!(
552			"delete the selected key (pub)",
553			Command::DeleteKey(KeyType::Public, String::new()).to_string()
554		);
555		assert_eq!(
556			"send key to the keyserver",
557			Command::SendKey(String::new()).to_string()
558		);
559		assert_eq!(
560			"edit the selected key",
561			Command::EditKey(String::new()).to_string()
562		);
563		assert_eq!(
564			"sign the selected key",
565			Command::SignKey(String::new()).to_string()
566		);
567		assert_eq!("generate a new key pair", Command::GenerateKey.to_string());
568		assert_eq!(
569			"copy exported key",
570			Command::Copy(Selection::Key).to_string()
571		);
572		assert_eq!("paste from clipboard", Command::Paste.to_string());
573		assert_eq!(
574			"toggle detail (all)",
575			Command::ToggleDetail(true).to_string()
576		);
577		assert_eq!(
578			"toggle detail (selected)",
579			Command::ToggleDetail(false).to_string()
580		);
581		assert_eq!("toggle table size", Command::ToggleTableSize.to_string());
582		assert_eq!(
583			"disable armored output",
584			Command::Set(String::from("armor"), String::from("false"))
585				.to_string()
586		);
587		assert_eq!(
588			"set style to colored",
589			Command::Set(String::from("style"), String::from("colored"))
590				.to_string()
591		);
592		assert_eq!(
593			"toggle table margin",
594			Command::Set(String::from("margin"), String::new()).to_string()
595		);
596		assert_eq!(
597			"import key(s) from a file",
598			Command::Set(String::from("prompt"), String::from(":import "))
599				.to_string()
600		);
601		assert_eq!(
602			"import key(s) from clipboard",
603			Command::ImportClipboard.to_string()
604		);
605		assert_eq!(
606			"receive key(s) from keyserver",
607			Command::Set(String::from("prompt"), String::from(":receive "))
608				.to_string()
609		);
610		assert_eq!(
611			"set prompt text to xyz",
612			Command::Set(String::from("prompt"), String::from("xyz"))
613				.to_string()
614		);
615		assert_eq!(
616			"set x to y",
617			Command::Set(String::from("x"), String::from("y")).to_string()
618		);
619		assert_eq!(
620			"switch to visual mode",
621			Command::SwitchMode(Mode::Visual).to_string()
622		);
623		assert_eq!(
624			"refresh application",
625			Command::Confirm(Box::new(Command::Refresh)).to_string()
626		);
627		assert_eq!("quit application", Command::Quit.to_string());
628		assert_eq!("NextTab", Command::NextTab.to_string());
629		assert_eq!("show logs", Command::Logs.to_string());
630		Ok(())
631	}
632}