1 use crate::app::command::Command;
2 use crate::app::keys::{KeyBinding, KEY_BINDINGS};
3 use crate::app::mode::Mode;
4 use crate::app::prompt::{OutputType, Prompt, COMMAND_PREFIX, SEARCH_PREFIX};
5 use crate::app::selection::Selection;
6 use crate::app::splash::{SplashConfig, SplashScreen};
7 use crate::app::state::State;
8 use crate::app::style::Style;
9 use crate::app::tab::Tab;
10 use crate::args::Args;
11 use crate::gpg::context::GpgContext;
12 use crate::gpg::key::{GpgKey, KeyDetail, KeyType};
13 use crate::widget::list::StatefulList;
14 use crate::widget::row::ScrollDirection;
15 use crate::widget::style::Color as WidgetColor;
16 use crate::widget::table::{StatefulTable, TableSize, TableState};
17 use anyhow::{anyhow, Error as AnyhowError, Result};
18 use colorsys::Rgb;
19 use copypasta_ext::display::DisplayServer as ClipboardDisplayServer;
20 use copypasta_ext::prelude::ClipboardProvider;
21 use std::collections::HashMap;
22 use std::path::Path;
23 use std::process::Command as OsCommand;
24 use std::str;
25 use std::str::FromStr;
26 use std::time::Instant;
27 use tui::style::Color;
28 
29 /// Max duration of prompt messages.
30 const MESSAGE_DURATION: u128 = 1750;
31 
32 /// Splash screen config.
33 static SPLASH_CONFIG: SplashConfig = SplashConfig {
34 	image_path: "splash.jpg",
35 	sha256_hash: Some(hex_literal::hex!(
36 		"dffd680d649a4a4b5df25dc1afaa722186c3bf38469a9156bad33f1719574e2a"
37 	)),
38 	render_steps: 12,
39 };
40 
41 /// Main application.
42 ///
43 /// It is responsible for running the commands
44 /// for changing the state of the interface.
45 pub struct App<'a> {
46 	/// Application state.
47 	pub state: State,
48 	/// Application mode.
49 	pub mode: Mode,
50 	/// Prompt manager.
51 	pub prompt: Prompt,
52 	/// Current tab.
53 	pub tab: Tab,
54 	/// Content of the options menu.
55 	pub options: StatefulList<Command>,
56 	/// Splash screen of the application.
57 	pub splash_screen: SplashScreen,
58 	/// Content of the key bindings list.
59 	pub key_bindings: StatefulList<KeyBinding<'a>>,
60 	/// Public/secret keys.
61 	pub keys: HashMap<KeyType, Vec<GpgKey>>,
62 	/// Table of public/secret keys.
63 	pub keys_table: StatefulTable<GpgKey>,
64 	/// States of the keys table.
65 	pub keys_table_states: HashMap<KeyType, TableState>,
66 	/// Level of detail to show for keys table.
67 	pub keys_table_detail: KeyDetail,
68 	/// Bottom margin value of the keys table.
69 	pub keys_table_margin: u16,
70 	/// Clipboard context.
71 	pub clipboard: Option<Box<dyn ClipboardProvider>>,
72 	/// GPGME context.
73 	pub gpgme: &'a mut GpgContext,
74 }
75 
76 impl<'a> App<'a> {
77 	/// Constructs a new instance of `App`.
new(gpgme: &'a mut GpgContext, args: &'a Args) -> Result<Self>78 	pub fn new(gpgme: &'a mut GpgContext, args: &'a Args) -> Result<Self> {
79 		let keys = gpgme.get_all_keys()?;
80 		let keys_table = StatefulTable::with_items(
81 			keys.get(&KeyType::Public)
82 				.expect("failed to get public keys")
83 				.to_vec(),
84 		);
85 		let state = State::from(args);
86 		Ok(Self {
87 			mode: Mode::Normal,
88 			prompt: if state.select.is_some() {
89 				Prompt {
90 					output_type: OutputType::Action,
91 					text: String::from("-- select --"),
92 					clock: Some(Instant::now()),
93 					..Prompt::default()
94 				}
95 			} else {
96 				Prompt::default()
97 			},
98 			state,
99 			tab: Tab::Keys(KeyType::Public),
100 			options: StatefulList::with_items(Vec::new()),
101 			splash_screen: SplashScreen::new(SPLASH_CONFIG)?,
102 			key_bindings: StatefulList::with_items(KEY_BINDINGS.to_vec()),
103 			keys,
104 			keys_table,
105 			keys_table_states: HashMap::new(),
106 			keys_table_detail: KeyDetail::Minimum,
107 			keys_table_margin: 1,
108 			clipboard: match ClipboardDisplayServer::select().try_context() {
109 				None => {
110 					eprintln!("failed to initialize clipboard, no suitable clipboard provider found");
111 					None
112 				}
113 				clipboard => clipboard,
114 			},
115 			gpgme,
116 		})
117 	}
118 
119 	/// Resets the application state.
refresh(&mut self) -> Result<()>120 	pub fn refresh(&mut self) -> Result<()> {
121 		self.state.refresh();
122 		self.mode = Mode::Normal;
123 		self.prompt.clear();
124 		self.options.state.select(Some(0));
125 		self.keys = self.gpgme.get_all_keys()?;
126 		self.keys_table_states.clear();
127 		self.keys_table_detail = KeyDetail::Minimum;
128 		self.keys_table_margin = 1;
129 		match self.tab {
130 			Tab::Keys(key_type) => {
131 				self.keys_table = StatefulTable::with_items(
132 					self.keys
133 						.get(&key_type)
134 						.unwrap_or_else(|| {
135 							panic!("failed to get {} keys", key_type)
136 						})
137 						.to_vec(),
138 				)
139 			}
140 			Tab::Help => {}
141 		};
142 		Ok(())
143 	}
144 
145 	/// Handles the tick event of the application.
146 	///
147 	/// It is currently used to flush the prompt messages.
tick(&mut self)148 	pub fn tick(&mut self) {
149 		if let Some(clock) = self.prompt.clock {
150 			if clock.elapsed().as_millis() > MESSAGE_DURATION
151 				&& self.prompt.command.is_none()
152 			{
153 				self.prompt.clear()
154 			}
155 		}
156 	}
157 
158 	/// Runs the given command which is used to specify
159 	/// the widget to render or action to perform.
run_command(&mut self, command: Command) -> Result<()>160 	pub fn run_command(&mut self, command: Command) -> Result<()> {
161 		let mut show_options = false;
162 		if let Command::Confirm(ref cmd) = command {
163 			self.prompt.set_command(*cmd.clone())
164 		} else if self.prompt.command.is_some() {
165 			self.prompt.clear();
166 		}
167 		match command {
168 			Command::ShowHelp => {
169 				self.tab = Tab::Help;
170 				if self.key_bindings.state.selected().is_none() {
171 					self.key_bindings.state.select(Some(0));
172 				}
173 			}
174 			Command::ChangeStyle(style) => {
175 				self.state.style = style;
176 				self.prompt.set_output((
177 					OutputType::Success,
178 					format!("style: {}", self.state.style),
179 				))
180 			}
181 			Command::ShowOutput(output_type, message) => {
182 				self.prompt.set_output((output_type, message))
183 			}
184 			Command::ShowOptions => {
185 				let prev_selection = self.options.state.selected();
186 				let prev_item_count = self.options.items.len();
187 				self.options = StatefulList::with_items(match self.tab {
188 					Tab::Keys(key_type) => {
189 						if let Some(selected_key) = &self.keys_table.selected()
190 						{
191 							vec![
192 								Command::None,
193 								Command::ShowHelp,
194 								Command::Refresh,
195 								Command::RefreshKeys,
196 								Command::Set(
197 									String::from("prompt"),
198 									String::from(":import "),
199 								),
200 								Command::ImportClipboard,
201 								Command::Set(
202 									String::from("prompt"),
203 									String::from(":receive "),
204 								),
205 								Command::ExportKeys(
206 									key_type,
207 									vec![selected_key.get_id()],
208 									false,
209 								),
210 								if key_type == KeyType::Secret {
211 									Command::ExportKeys(
212 										key_type,
213 										vec![selected_key.get_id()],
214 										true,
215 									)
216 								} else {
217 									Command::None
218 								},
219 								Command::ExportKeys(
220 									key_type,
221 									Vec::new(),
222 									false,
223 								),
224 								Command::Confirm(Box::new(Command::DeleteKey(
225 									key_type,
226 									selected_key.get_id(),
227 								))),
228 								Command::Confirm(Box::new(Command::SendKey(
229 									selected_key.get_id(),
230 								))),
231 								Command::EditKey(selected_key.get_id()),
232 								if key_type == KeyType::Secret {
233 									Command::Set(
234 										String::from("signer"),
235 										selected_key.get_id(),
236 									)
237 								} else {
238 									Command::None
239 								},
240 								Command::SignKey(selected_key.get_id()),
241 								Command::GenerateKey,
242 								Command::Set(
243 									String::from("armor"),
244 									(!self.gpgme.config.armor).to_string(),
245 								),
246 								Command::Copy(Selection::Key),
247 								Command::Copy(Selection::KeyId),
248 								Command::Copy(Selection::KeyFingerprint),
249 								Command::Copy(Selection::KeyUserId),
250 								Command::Copy(Selection::TableRow(1)),
251 								Command::Copy(Selection::TableRow(2)),
252 								Command::Paste,
253 								Command::ToggleDetail(false),
254 								Command::ToggleDetail(true),
255 								Command::Set(
256 									String::from("margin"),
257 									String::from(
258 										if self.keys_table_margin == 1 {
259 											"0"
260 										} else {
261 											"1"
262 										},
263 									),
264 								),
265 								Command::ToggleTableSize,
266 								Command::ChangeStyle(self.state.style.next()),
267 								if self.mode == Mode::Visual {
268 									Command::SwitchMode(Mode::Normal)
269 								} else {
270 									Command::SwitchMode(Mode::Visual)
271 								},
272 								Command::Quit,
273 							]
274 							.into_iter()
275 							.enumerate()
276 							.filter(|(i, c)| {
277 								if c == &Command::None {
278 									*i == 0
279 								} else {
280 									true
281 								}
282 							})
283 							.map(|(_, c)| c)
284 							.collect()
285 						} else {
286 							vec![
287 								Command::None,
288 								Command::ShowHelp,
289 								Command::Refresh,
290 								Command::RefreshKeys,
291 								Command::Set(
292 									String::from("prompt"),
293 									String::from(":import "),
294 								),
295 								Command::ImportClipboard,
296 								Command::Set(
297 									String::from("prompt"),
298 									String::from(":receive "),
299 								),
300 								Command::GenerateKey,
301 								Command::Paste,
302 								Command::ChangeStyle(self.state.style.next()),
303 								if self.mode == Mode::Visual {
304 									Command::SwitchMode(Mode::Normal)
305 								} else {
306 									Command::SwitchMode(Mode::Visual)
307 								},
308 								Command::Quit,
309 							]
310 							.into_iter()
311 							.enumerate()
312 							.filter(|(i, c)| {
313 								if c == &Command::None {
314 									*i == 0
315 								} else {
316 									true
317 								}
318 							})
319 							.map(|(_, c)| c)
320 							.collect()
321 						}
322 					}
323 					Tab::Help => {
324 						vec![
325 							Command::None,
326 							Command::ListKeys(KeyType::Public),
327 							Command::ListKeys(KeyType::Secret),
328 							Command::ChangeStyle(self.state.style.next()),
329 							if self.mode == Mode::Visual {
330 								Command::SwitchMode(Mode::Normal)
331 							} else {
332 								Command::SwitchMode(Mode::Visual)
333 							},
334 							Command::Refresh,
335 							Command::Quit,
336 						]
337 					}
338 				});
339 				if prev_item_count == 0
340 					|| self.options.items.len() == prev_item_count
341 				{
342 					self.options.state.select(prev_selection.or(Some(0)));
343 				} else {
344 					self.options.state.select(Some(0));
345 				}
346 				show_options = true;
347 			}
348 			Command::ListKeys(key_type) => {
349 				if let Tab::Keys(previous_key_type) = self.tab {
350 					self.keys_table_states.insert(
351 						previous_key_type,
352 						self.keys_table.state.clone(),
353 					);
354 					self.keys.insert(
355 						previous_key_type,
356 						self.keys_table.default_items.clone(),
357 					);
358 				}
359 				self.keys_table = StatefulTable::with_items(
360 					self.keys
361 						.get(&key_type)
362 						.unwrap_or_else(|| {
363 							panic!("failed to get {} keys", key_type)
364 						})
365 						.to_vec(),
366 				);
367 				if let Some(state) = self.keys_table_states.get(&key_type) {
368 					self.keys_table.state = state.clone();
369 				}
370 				self.tab = Tab::Keys(key_type);
371 			}
372 			Command::ImportKeys(_, false) | Command::ImportClipboard => {
373 				let mut keys = Vec::new();
374 				let mut import_error = String::from("no files given");
375 				if let Command::ImportKeys(ref key_files, _) = command {
376 					keys = key_files.clone();
377 				} else if let Some(clipboard) = self.clipboard.as_mut() {
378 					match clipboard.get_contents() {
379 						Ok(content) => {
380 							keys = vec![content];
381 						}
382 						Err(e) => {
383 							import_error = e.to_string();
384 						}
385 					}
386 				}
387 				if keys.is_empty() {
388 					self.prompt.set_output((
389 						OutputType::Failure,
390 						format!("import error: {}", import_error),
391 					))
392 				} else {
393 					match self
394 						.gpgme
395 						.import_keys(keys, command != Command::ImportClipboard)
396 					{
397 						Ok(key_count) => {
398 							self.refresh()?;
399 							self.prompt.set_output((
400 								OutputType::Success,
401 								format!("{} key(s) imported", key_count),
402 							))
403 						}
404 						Err(e) => self.prompt.set_output((
405 							OutputType::Failure,
406 							format!("import error: {}", e),
407 						)),
408 					}
409 				}
410 			}
411 			Command::ExportKeys(key_type, ref patterns, false) => {
412 				self.prompt.set_output(
413 					match self
414 						.gpgme
415 						.export_keys(key_type, Some(patterns.to_vec()))
416 					{
417 						Ok(path) => {
418 							(OutputType::Success, format!("export: {}", path))
419 						}
420 						Err(e) => (
421 							OutputType::Failure,
422 							format!("export error: {}", e),
423 						),
424 					},
425 				);
426 			}
427 			Command::DeleteKey(key_type, ref key_id) => {
428 				match self.gpgme.delete_key(key_type, key_id.to_string()) {
429 					Ok(_) => {
430 						self.refresh()?;
431 					}
432 					Err(e) => self.prompt.set_output((
433 						OutputType::Failure,
434 						format!("delete error: {}", e),
435 					)),
436 				}
437 			}
438 			Command::SendKey(key_id) => {
439 				self.prompt.set_output(match self.gpgme.send_key(key_id) {
440 					Ok(key_id) => (
441 						OutputType::Success,
442 						format!("key sent to the keyserver: 0x{}", key_id),
443 					),
444 					Err(e) => {
445 						(OutputType::Failure, format!("send error: {}", e))
446 					}
447 				});
448 			}
449 			Command::GenerateKey
450 			| Command::RefreshKeys
451 			| Command::EditKey(_)
452 			| Command::SignKey(_)
453 			| Command::ImportKeys(_, true)
454 			| Command::ExportKeys(_, _, true) => {
455 				let mut success_msg = None;
456 				let mut os_command = OsCommand::new("gpg");
457 				os_command
458 					.arg("--homedir")
459 					.arg(self.gpgme.config.home_dir.as_os_str());
460 				if self.gpgme.config.armor {
461 					os_command.arg("--armor");
462 				}
463 				if let Some(default_key) = &self.gpgme.config.default_key {
464 					os_command.arg("--default-key").arg(default_key);
465 				}
466 				let os_command = match command {
467 					Command::EditKey(ref key) => {
468 						os_command.arg("--edit-key").arg(key)
469 					}
470 					Command::SignKey(ref key) => {
471 						os_command.arg("--sign-key").arg(key)
472 					}
473 					Command::ImportKeys(ref keys, _) => {
474 						os_command.arg("--receive-keys").args(keys)
475 					}
476 					Command::ExportKeys(key_type, ref keys, true) => {
477 						let path = self
478 							.gpgme
479 							.get_output_file(key_type, keys.to_vec())?;
480 						success_msg =
481 							Some(format!("export: {}", path.to_string_lossy()));
482 						os_command
483 							.arg("--output")
484 							.arg(path)
485 							.arg("--export-secret-subkeys")
486 							.args(keys)
487 					}
488 					Command::RefreshKeys => os_command.arg("--refresh-keys"),
489 					_ => os_command.arg("--full-gen-key"),
490 				};
491 				match os_command.spawn() {
492 					Ok(mut child) => {
493 						child.wait()?;
494 						self.refresh()?;
495 						if let Some(msg) = success_msg {
496 							self.prompt.set_output((OutputType::Success, msg))
497 						}
498 					}
499 					Err(e) => self.prompt.set_output((
500 						OutputType::Failure,
501 						format!("execution error: {}", e),
502 					)),
503 				}
504 			}
505 			Command::ToggleDetail(true) => {
506 				self.keys_table_detail.increase();
507 				for key in self.keys_table.items.iter_mut() {
508 					key.detail = self.keys_table_detail;
509 				}
510 				for key in self.keys_table.default_items.iter_mut() {
511 					key.detail = self.keys_table_detail;
512 				}
513 			}
514 			Command::ToggleDetail(false) => {
515 				if let Some(index) = self.keys_table.state.tui.selected() {
516 					if let Some(key) = self.keys_table.items.get_mut(index) {
517 						key.detail.increase()
518 					}
519 					if self.keys_table.items.len()
520 						== self.keys_table.default_items.len()
521 					{
522 						if let Some(key) =
523 							self.keys_table.default_items.get_mut(index)
524 						{
525 							key.detail.increase()
526 						}
527 					}
528 				}
529 			}
530 			Command::ToggleTableSize => {
531 				self.keys_table.state.minimize_threshold = 0;
532 				self.keys_table.state.size = self.keys_table.state.size.next();
533 				self.prompt.set_output((
534 					OutputType::Success,
535 					format!(
536 						"table size: {}",
537 						format!("{:?}", self.keys_table.state.size)
538 							.to_lowercase()
539 					),
540 				));
541 			}
542 			Command::Scroll(direction, false) => match direction {
543 				ScrollDirection::Down(_) => {
544 					if self.state.show_options {
545 						self.options.next();
546 						show_options = true;
547 					} else if Tab::Help == self.tab {
548 						self.key_bindings.next();
549 					} else {
550 						self.keys_table.next();
551 					}
552 				}
553 				ScrollDirection::Up(_) => {
554 					if self.state.show_options {
555 						self.options.previous();
556 						show_options = true;
557 					} else if Tab::Help == self.tab {
558 						self.key_bindings.previous();
559 					} else {
560 						self.keys_table.previous();
561 					}
562 				}
563 				ScrollDirection::Top => {
564 					if self.state.show_options {
565 						self.options.state.select(Some(0));
566 						show_options = true;
567 					} else if Tab::Help == self.tab {
568 						self.key_bindings.state.select(Some(0));
569 					} else {
570 						self.keys_table.state.tui.select(Some(0));
571 					}
572 				}
573 				ScrollDirection::Bottom => {
574 					if self.state.show_options {
575 						self.options.state.select(Some(
576 							self.options
577 								.items
578 								.len()
579 								.checked_sub(1)
580 								.unwrap_or_default(),
581 						));
582 						show_options = true;
583 					} else if Tab::Help == self.tab {
584 						self.key_bindings
585 							.state
586 							.select(Some(KEY_BINDINGS.len() - 1));
587 					} else {
588 						self.keys_table.state.tui.select(Some(
589 							self.keys_table
590 								.items
591 								.len()
592 								.checked_sub(1)
593 								.unwrap_or_default(),
594 						));
595 					}
596 				}
597 				_ => {}
598 			},
599 			Command::Scroll(direction, true) => {
600 				self.keys_table.scroll_row(direction);
601 			}
602 			Command::Set(option, value) => {
603 				if option == *"prompt"
604 					&& (value.starts_with(COMMAND_PREFIX)
605 						| value.starts_with(SEARCH_PREFIX))
606 				{
607 					self.prompt.clear();
608 					self.prompt.text = value;
609 				} else {
610 					self.prompt.set_output(match option.as_str() {
611 						"output" => {
612 							let path = Path::new(&value);
613 							if path.exists() {
614 								self.gpgme.config.output_dir =
615 									path.to_path_buf();
616 								(
617 									OutputType::Success,
618 									format!(
619 										"output directory: {:?}",
620 										self.gpgme.config.output_dir
621 									),
622 								)
623 							} else {
624 								(
625 									OutputType::Failure,
626 									String::from("path does not exist"),
627 								)
628 							}
629 						}
630 						"mode" => {
631 							if let Ok(mode) = Mode::from_str(&value) {
632 								self.mode = mode;
633 								(
634 									OutputType::Success,
635 									format!(
636 										"mode: {}",
637 										format!("{:?}", mode).to_lowercase()
638 									),
639 								)
640 							} else {
641 								(
642 									OutputType::Failure,
643 									String::from("invalid mode"),
644 								)
645 							}
646 						}
647 						"armor" => {
648 							if let Ok(value) = FromStr::from_str(&value) {
649 								self.gpgme.config.armor = value;
650 								self.gpgme.apply_config();
651 								(
652 									OutputType::Success,
653 									format!("armor: {}", value),
654 								)
655 							} else {
656 								(
657 									OutputType::Failure,
658 									String::from(
659 										"usage: set armor <true/false>",
660 									),
661 								)
662 							}
663 						}
664 						"signer" => {
665 							self.gpgme.config.default_key =
666 								Some(value.to_string());
667 							(OutputType::Success, format!("signer: {}", value))
668 						}
669 						"minimize" => {
670 							self.keys_table.state.minimize_threshold =
671 								value.parse().unwrap_or_default();
672 							(
673 								OutputType::Success,
674 								format!(
675 									"minimize threshold: {}",
676 									self.keys_table.state.minimize_threshold
677 								),
678 							)
679 						}
680 						"detail" => {
681 							if let Ok(detail_level) =
682 								KeyDetail::from_str(&value)
683 							{
684 								if let Some(index) =
685 									self.keys_table.state.tui.selected()
686 								{
687 									if let Some(key) =
688 										self.keys_table.items.get_mut(index)
689 									{
690 										key.detail = detail_level;
691 									}
692 									if self.keys_table.items.len()
693 										== self.keys_table.default_items.len()
694 									{
695 										if let Some(key) = self
696 											.keys_table
697 											.default_items
698 											.get_mut(index)
699 										{
700 											key.detail = detail_level;
701 										}
702 									}
703 								}
704 								(
705 									OutputType::Success,
706 									format!("detail: {}", detail_level),
707 								)
708 							} else {
709 								(
710 									OutputType::Failure,
711 									String::from("usage: set detail <level>"),
712 								)
713 							}
714 						}
715 						"margin" => {
716 							self.keys_table_margin =
717 								value.parse().unwrap_or_default();
718 							(
719 								OutputType::Success,
720 								format!(
721 									"table margin: {}",
722 									self.keys_table_margin
723 								),
724 							)
725 						}
726 						"style" => match Style::from_str(&value) {
727 							Ok(style) => {
728 								self.state.style = style;
729 								(
730 									OutputType::Success,
731 									format!("style: {}", self.state.style),
732 								)
733 							}
734 							Err(_) => (
735 								OutputType::Failure,
736 								String::from("usage: set style <style>"),
737 							),
738 						},
739 						"color" => {
740 							self.state.color =
741 								WidgetColor::from(value.as_ref()).get();
742 							(
743 								OutputType::Success,
744 								format!(
745 									"color: {}",
746 									match self.state.color {
747 										Color::Rgb(r, g, b) =>
748 											Rgb::from((r, g, b)).to_hex_string(),
749 										_ => format!("{:?}", self.state.color)
750 											.to_lowercase(),
751 									}
752 								),
753 							)
754 						}
755 						_ => (
756 							OutputType::Failure,
757 							if !option.is_empty() {
758 								format!("unknown option: {}", option)
759 							} else {
760 								String::from("usage: set <option> <value>")
761 							},
762 						),
763 					})
764 				}
765 			}
766 			Command::Get(option) => {
767 				self.prompt.set_output(match option.as_str() {
768 					"output" => (
769 						OutputType::Success,
770 						format!(
771 							"output directory: {:?}",
772 							self.gpgme.config.output_dir.as_os_str()
773 						),
774 					),
775 					"mode" => (
776 						OutputType::Success,
777 						format!(
778 							"mode: {}",
779 							format!("{:?}", self.mode).to_lowercase()
780 						),
781 					),
782 					"armor" => (
783 						OutputType::Success,
784 						format!("armor: {}", self.gpgme.config.armor),
785 					),
786 					"signer" => (
787 						OutputType::Success,
788 						match &self.gpgme.config.default_key {
789 							Some(key) => format!("signer: {}", key),
790 							None => String::from("signer key is not specified"),
791 						},
792 					),
793 					"minimize" => (
794 						OutputType::Success,
795 						format!(
796 							"minimize threshold: {}",
797 							self.keys_table.state.minimize_threshold
798 						),
799 					),
800 					"detail" => {
801 						if let Some(index) =
802 							self.keys_table.state.tui.selected()
803 						{
804 							if let Some(key) = self.keys_table.items.get(index)
805 							{
806 								(
807 									OutputType::Success,
808 									format!("detail: {}", key.detail),
809 								)
810 							} else {
811 								(
812 									OutputType::Failure,
813 									String::from("invalid selection"),
814 								)
815 							}
816 						} else {
817 							(
818 								OutputType::Failure,
819 								String::from("unknown selection"),
820 							)
821 						}
822 					}
823 					"margin" => (
824 						OutputType::Success,
825 						format!("table margin: {}", self.keys_table_margin),
826 					),
827 					"style" => (
828 						OutputType::Success,
829 						format!("style: {}", self.state.style),
830 					),
831 					"color" => (
832 						OutputType::Success,
833 						format!(
834 							"color: {}",
835 							match self.state.color {
836 								Color::Rgb(r, g, b) =>
837 									Rgb::from((r, g, b)).to_hex_string(),
838 								_ => format!("{:?}", self.state.color)
839 									.to_lowercase(),
840 							}
841 						),
842 					),
843 					_ => (
844 						OutputType::Failure,
845 						if !option.is_empty() {
846 							format!("unknown option: {}", option)
847 						} else {
848 							String::from("usage: get <option>")
849 						},
850 					),
851 				})
852 			}
853 			Command::SwitchMode(mode) => {
854 				if !(mode == Mode::Copy && self.keys_table.items.is_empty()) {
855 					self.mode = mode;
856 					self.prompt
857 						.set_output((OutputType::Action, mode.to_string()))
858 				}
859 			}
860 			Command::Copy(copy_type) => {
861 				let selected_key =
862 					&self.keys_table.selected().expect("invalid selection");
863 				let content = match copy_type {
864 					Selection::TableRow(1) => Ok(selected_key
865 						.get_subkey_info(
866 							self.gpgme.config.default_key.as_deref(),
867 							self.keys_table.state.size != TableSize::Normal,
868 						)
869 						.join("\n")),
870 					Selection::TableRow(2) => Ok(selected_key
871 						.get_user_info(
872 							self.keys_table.state.size == TableSize::Minimized,
873 						)
874 						.join("\n")),
875 					Selection::TableRow(_) => {
876 						Err(anyhow!("invalid row number"))
877 					}
878 					Selection::Key => {
879 						match self.gpgme.get_exported_keys(
880 							match self.tab {
881 								Tab::Keys(key_type) => key_type,
882 								_ => KeyType::Public,
883 							},
884 							Some(vec![selected_key.get_id()]),
885 						) {
886 							Ok(key) => str::from_utf8(&key)
887 								.map(|v| v.to_string())
888 								.map_err(AnyhowError::from),
889 							Err(e) => Err(e),
890 						}
891 					}
892 					Selection::KeyId => Ok(selected_key.get_id()),
893 					Selection::KeyFingerprint => {
894 						Ok(selected_key.get_fingerprint())
895 					}
896 					Selection::KeyUserId => Ok(selected_key.get_user_id()),
897 				};
898 				match content {
899 					Ok(content) => {
900 						if self.state.select.is_some() {
901 							self.state.exit_message = Some(content);
902 							self.run_command(Command::Quit)?;
903 						} else if let Some(clipboard) = self.clipboard.as_mut()
904 						{
905 							self.prompt.set_output(
906 								match clipboard.set_contents(content) {
907 									Ok(_) => (
908 										OutputType::Success,
909 										format!(
910 											"{} copied to clipboard",
911 											copy_type
912 										),
913 									),
914 									Err(e) => (
915 										OutputType::Failure,
916 										format!("clipboard error: {}", e),
917 									),
918 								},
919 							);
920 						} else {
921 							self.prompt.set_output((
922 								OutputType::Failure,
923 								String::from("clipboard not available"),
924 							));
925 						}
926 					}
927 					Err(e) => {
928 						self.prompt.set_output((
929 							OutputType::Failure,
930 							format!("selection error: {}", e),
931 						));
932 					}
933 				}
934 				self.mode = Mode::Normal;
935 			}
936 			Command::Paste => {
937 				if let Some(clipboard) = self.clipboard.as_mut() {
938 					match clipboard.get_contents() {
939 						Ok(content) => {
940 							self.prompt.clear();
941 							self.prompt.text = format!(":{}", content);
942 						}
943 						Err(e) => {
944 							self.prompt.set_output((
945 								OutputType::Failure,
946 								format!("clipboard error: {}", e),
947 							));
948 						}
949 					}
950 				} else {
951 					self.prompt.set_output((
952 						OutputType::Failure,
953 						String::from("clipboard not available"),
954 					));
955 				}
956 			}
957 			Command::EnableInput => self.prompt.enable_command_input(),
958 			Command::Search(query) => {
959 				self.prompt.text = format!("/{}", query.unwrap_or_default());
960 				self.prompt.enable_search();
961 				self.keys_table.items = self.keys_table.default_items.clone();
962 			}
963 			Command::NextTab => {
964 				self.run_command(self.tab.next().get_command())?
965 			}
966 			Command::PreviousTab => {
967 				self.run_command(self.tab.previous().get_command())?
968 			}
969 			Command::Refresh => self.refresh()?,
970 			Command::Quit => self.state.running = false,
971 			Command::Confirm(_) | Command::None => {}
972 		}
973 		self.state.show_options = show_options;
974 		Ok(())
975 	}
976 }
977 
978 #[cfg(test)]
979 mod tests {
980 	use super::*;
981 	use crate::gpg::config::GpgConfig;
982 	use pretty_assertions::assert_eq;
983 	use std::convert::TryInto;
984 	use std::thread;
985 	use std::time::Duration;
986 	#[test]
test_app_launcher() -> Result<()>987 	fn test_app_launcher() -> Result<()> {
988 		let args = Args::default();
989 		let config = GpgConfig::new(&args)?;
990 		let mut context = GpgContext::new(config)?;
991 		let mut app = App::new(&mut context, &args)?;
992 		app.run_command(Command::Refresh)?;
993 
994 		app.run_command(Command::ShowHelp)?;
995 		assert_eq!(Tab::Help, app.tab);
996 		app.run_command(Command::ShowOptions)?;
997 		assert!(app.state.show_options);
998 
999 		app.run_command(Command::ChangeStyle(Style::Colored))?;
1000 		assert_eq!(Style::Colored, app.state.style);
1001 		app.run_command(Command::ChangeStyle(Style::Plain))?;
1002 		assert_eq!(Style::Plain, app.state.style);
1003 
1004 		app.run_command(Command::ListKeys(KeyType::Public))?;
1005 		app.run_command(Command::ToggleDetail(false))?;
1006 		let mut detail = app.keys_table_detail.clone();
1007 		detail.increase();
1008 		app.run_command(Command::ToggleDetail(true))?;
1009 		assert_eq!(detail, app.keys_table_detail);
1010 
1011 		let prompt_text = format!("{}test", COMMAND_PREFIX);
1012 		app.run_command(Command::Set(
1013 			String::from("prompt"),
1014 			prompt_text.to_string(),
1015 		))?;
1016 		assert_eq!(prompt_text, app.prompt.text);
1017 
1018 		let home_dir =
1019 			dirs_next::home_dir().unwrap().to_str().unwrap().to_string();
1020 		app.run_command(Command::Set(
1021 			String::from("output"),
1022 			home_dir.to_string(),
1023 		))?;
1024 		app.run_command(Command::Get(String::from("output")))?;
1025 		assert!(app.prompt.text.contains(&home_dir));
1026 
1027 		let mut test_values = vec![
1028 			("output", "/tmp"),
1029 			("mode", "normal"),
1030 			("armor", "true"),
1031 			("signer", "0x0"),
1032 			("minimize", "10"),
1033 			("margin", "2"),
1034 			("style", "plain"),
1035 			("color", "#123123"),
1036 		];
1037 		if cfg!(feature = "gpg-tests") {
1038 			test_values.push(("detail", "full"));
1039 		}
1040 		for (option, value) in test_values {
1041 			app.run_command(Command::Set(
1042 				option.to_string(),
1043 				value.to_string(),
1044 			))?;
1045 			app.run_command(Command::Get(option.to_string()))?;
1046 			assert!(
1047 				(app.prompt.text == format!("{}: {}", option, value))
1048 					|| app.prompt.text.contains(value)
1049 			);
1050 		}
1051 
1052 		app.mode = Mode::Normal;
1053 		app.run_command(Command::SwitchMode(Mode::Visual))?;
1054 		assert_eq!(Mode::Visual, app.mode);
1055 
1056 		app.run_command(Command::EnableInput)?;
1057 		assert!(app.prompt.is_command_input_enabled());
1058 		assert_eq!(COMMAND_PREFIX.to_string(), app.prompt.text);
1059 
1060 		app.run_command(Command::Search(Some(String::from("x"))))?;
1061 		assert!(app.prompt.is_search_enabled());
1062 		assert_eq!(format!("{}x", SEARCH_PREFIX), app.prompt.text);
1063 
1064 		app.tab = Tab::Keys(KeyType::Public);
1065 		app.run_command(Command::NextTab)?;
1066 		assert_eq!(Tab::Keys(KeyType::Secret), app.tab);
1067 		app.run_command(Command::NextTab)?;
1068 		assert_eq!(Tab::Keys(KeyType::Public), app.tab);
1069 
1070 		app.tick();
1071 		app.run_command(Command::ShowOutput(
1072 			OutputType::Success,
1073 			String::from("test"),
1074 		))?;
1075 		assert_eq!("test", app.prompt.text);
1076 		thread::sleep(Duration::from_millis(
1077 			(MESSAGE_DURATION + 10).try_into().unwrap(),
1078 		));
1079 		app.tick();
1080 		assert_eq!("", app.prompt.text);
1081 
1082 		app.run_command(Command::Quit)?;
1083 		assert!(!app.state.running);
1084 
1085 		app.run_command(Command::None)
1086 	}
1087 }
1088