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