use crate::app::command::Command; use crate::app::keys::{KeyBinding, KEY_BINDINGS}; use crate::app::mode::Mode; use crate::app::prompt::{OutputType, Prompt, COMMAND_PREFIX, SEARCH_PREFIX}; use crate::app::selection::Selection; use crate::app::splash::{SplashConfig, SplashScreen}; use crate::app::state::State; use crate::app::style::Style; use crate::app::tab::Tab; use crate::args::Args; use crate::gpg::context::GpgContext; use crate::gpg::key::{GpgKey, KeyDetail, KeyType}; use crate::widget::list::StatefulList; use crate::widget::row::ScrollDirection; use crate::widget::style::Color as WidgetColor; use crate::widget::table::{StatefulTable, TableSize, TableState}; use anyhow::{anyhow, Error as AnyhowError, Result}; use colorsys::Rgb; use copypasta_ext::display::DisplayServer as ClipboardDisplayServer; use copypasta_ext::prelude::ClipboardProvider; use std::collections::HashMap; use std::path::Path; use std::process::Command as OsCommand; use std::str; use std::str::FromStr; use std::time::Instant; use tui::style::Color; /// Max duration of prompt messages. const MESSAGE_DURATION: u128 = 1750; /// Splash screen config. static SPLASH_CONFIG: SplashConfig = SplashConfig { image_path: "splash.jpg", sha256_hash: Some(hex_literal::hex!( "dffd680d649a4a4b5df25dc1afaa722186c3bf38469a9156bad33f1719574e2a" )), render_steps: 12, }; /// Main application. /// /// It is responsible for running the commands /// for changing the state of the interface. pub struct App<'a> { /// Application state. pub state: State, /// Application mode. pub mode: Mode, /// Prompt manager. pub prompt: Prompt, /// Current tab. pub tab: Tab, /// Content of the options menu. pub options: StatefulList, /// Splash screen of the application. pub splash_screen: SplashScreen, /// Content of the key bindings list. pub key_bindings: StatefulList>, /// Public/secret keys. pub keys: HashMap>, /// Table of public/secret keys. pub keys_table: StatefulTable, /// States of the keys table. pub keys_table_states: HashMap, /// Level of detail to show for keys table. pub keys_table_detail: KeyDetail, /// Bottom margin value of the keys table. pub keys_table_margin: u16, /// Clipboard context. pub clipboard: Option>, /// GPGME context. pub gpgme: &'a mut GpgContext, } impl<'a> App<'a> { /// Constructs a new instance of `App`. pub fn new(gpgme: &'a mut GpgContext, args: &'a Args) -> Result { let keys = gpgme.get_all_keys()?; let keys_table = StatefulTable::with_items( keys.get(&KeyType::Public) .expect("failed to get public keys") .to_vec(), ); let state = State::from(args); Ok(Self { mode: Mode::Normal, prompt: if state.select.is_some() { Prompt { output_type: OutputType::Action, text: String::from("-- select --"), clock: Some(Instant::now()), ..Prompt::default() } } else { Prompt::default() }, state, tab: Tab::Keys(KeyType::Public), options: StatefulList::with_items(Vec::new()), splash_screen: SplashScreen::new(SPLASH_CONFIG)?, key_bindings: StatefulList::with_items(KEY_BINDINGS.to_vec()), keys, keys_table, keys_table_states: HashMap::new(), keys_table_detail: KeyDetail::Minimum, keys_table_margin: 1, clipboard: match ClipboardDisplayServer::select().try_context() { None => { eprintln!("failed to initialize clipboard, no suitable clipboard provider found"); None } clipboard => clipboard, }, gpgme, }) } /// Resets the application state. pub fn refresh(&mut self) -> Result<()> { self.state.refresh(); self.mode = Mode::Normal; self.prompt.clear(); self.options.state.select(Some(0)); self.keys = self.gpgme.get_all_keys()?; self.keys_table_states.clear(); self.keys_table_detail = KeyDetail::Minimum; self.keys_table_margin = 1; match self.tab { Tab::Keys(key_type) => { self.keys_table = StatefulTable::with_items( self.keys .get(&key_type) .unwrap_or_else(|| { panic!("failed to get {} keys", key_type) }) .to_vec(), ) } Tab::Help => {} }; Ok(()) } /// Handles the tick event of the application. /// /// It is currently used to flush the prompt messages. pub fn tick(&mut self) { if let Some(clock) = self.prompt.clock { if clock.elapsed().as_millis() > MESSAGE_DURATION && self.prompt.command.is_none() { self.prompt.clear() } } } /// Runs the given command which is used to specify /// the widget to render or action to perform. pub fn run_command(&mut self, command: Command) -> Result<()> { let mut show_options = false; if let Command::Confirm(ref cmd) = command { self.prompt.set_command(*cmd.clone()) } else if self.prompt.command.is_some() { self.prompt.clear(); } match command { Command::ShowHelp => { self.tab = Tab::Help; if self.key_bindings.state.selected().is_none() { self.key_bindings.state.select(Some(0)); } } Command::ChangeStyle(style) => { self.state.style = style; self.prompt.set_output(( OutputType::Success, format!("style: {}", self.state.style), )) } Command::ShowOutput(output_type, message) => { self.prompt.set_output((output_type, message)) } Command::ShowOptions => { let prev_selection = self.options.state.selected(); let prev_item_count = self.options.items.len(); self.options = StatefulList::with_items(match self.tab { Tab::Keys(key_type) => { if let Some(selected_key) = &self.keys_table.selected() { vec![ Command::None, Command::ShowHelp, Command::Refresh, Command::RefreshKeys, Command::Set( String::from("prompt"), String::from(":import "), ), Command::ImportClipboard, Command::Set( String::from("prompt"), String::from(":receive "), ), Command::ExportKeys( key_type, vec![selected_key.get_id()], false, ), if key_type == KeyType::Secret { Command::ExportKeys( key_type, vec![selected_key.get_id()], true, ) } else { Command::None }, Command::ExportKeys( key_type, Vec::new(), false, ), Command::Confirm(Box::new(Command::DeleteKey( key_type, selected_key.get_id(), ))), Command::Confirm(Box::new(Command::SendKey( selected_key.get_id(), ))), Command::EditKey(selected_key.get_id()), if key_type == KeyType::Secret { Command::Set( String::from("signer"), selected_key.get_id(), ) } else { Command::None }, Command::SignKey(selected_key.get_id()), Command::GenerateKey, Command::Set( String::from("armor"), (!self.gpgme.config.armor).to_string(), ), Command::Copy(Selection::Key), Command::Copy(Selection::KeyId), Command::Copy(Selection::KeyFingerprint), Command::Copy(Selection::KeyUserId), Command::Copy(Selection::TableRow(1)), Command::Copy(Selection::TableRow(2)), Command::Paste, Command::ToggleDetail(false), Command::ToggleDetail(true), Command::Set( String::from("margin"), String::from( if self.keys_table_margin == 1 { "0" } else { "1" }, ), ), Command::ToggleTableSize, Command::ChangeStyle(self.state.style.next()), if self.mode == Mode::Visual { Command::SwitchMode(Mode::Normal) } else { Command::SwitchMode(Mode::Visual) }, Command::Quit, ] .into_iter() .enumerate() .filter(|(i, c)| { if c == &Command::None { *i == 0 } else { true } }) .map(|(_, c)| c) .collect() } else { vec![ Command::None, Command::ShowHelp, Command::Refresh, Command::RefreshKeys, Command::Set( String::from("prompt"), String::from(":import "), ), Command::ImportClipboard, Command::Set( String::from("prompt"), String::from(":receive "), ), Command::GenerateKey, Command::Paste, Command::ChangeStyle(self.state.style.next()), if self.mode == Mode::Visual { Command::SwitchMode(Mode::Normal) } else { Command::SwitchMode(Mode::Visual) }, Command::Quit, ] .into_iter() .enumerate() .filter(|(i, c)| { if c == &Command::None { *i == 0 } else { true } }) .map(|(_, c)| c) .collect() } } Tab::Help => { vec![ Command::None, Command::ListKeys(KeyType::Public), Command::ListKeys(KeyType::Secret), Command::ChangeStyle(self.state.style.next()), if self.mode == Mode::Visual { Command::SwitchMode(Mode::Normal) } else { Command::SwitchMode(Mode::Visual) }, Command::Refresh, Command::Quit, ] } }); if prev_item_count == 0 || self.options.items.len() == prev_item_count { self.options.state.select(prev_selection.or(Some(0))); } else { self.options.state.select(Some(0)); } show_options = true; } Command::ListKeys(key_type) => { if let Tab::Keys(previous_key_type) = self.tab { self.keys_table_states.insert( previous_key_type, self.keys_table.state.clone(), ); self.keys.insert( previous_key_type, self.keys_table.default_items.clone(), ); } self.keys_table = StatefulTable::with_items( self.keys .get(&key_type) .unwrap_or_else(|| { panic!("failed to get {} keys", key_type) }) .to_vec(), ); if let Some(state) = self.keys_table_states.get(&key_type) { self.keys_table.state = state.clone(); } self.tab = Tab::Keys(key_type); } Command::ImportKeys(_, false) | Command::ImportClipboard => { let mut keys = Vec::new(); let mut import_error = String::from("no files given"); if let Command::ImportKeys(ref key_files, _) = command { keys = key_files.clone(); } else if let Some(clipboard) = self.clipboard.as_mut() { match clipboard.get_contents() { Ok(content) => { keys = vec![content]; } Err(e) => { import_error = e.to_string(); } } } if keys.is_empty() { self.prompt.set_output(( OutputType::Failure, format!("import error: {}", import_error), )) } else { match self .gpgme .import_keys(keys, command != Command::ImportClipboard) { Ok(key_count) => { self.refresh()?; self.prompt.set_output(( OutputType::Success, format!("{} key(s) imported", key_count), )) } Err(e) => self.prompt.set_output(( OutputType::Failure, format!("import error: {}", e), )), } } } Command::ExportKeys(key_type, ref patterns, false) => { self.prompt.set_output( match self .gpgme .export_keys(key_type, Some(patterns.to_vec())) { Ok(path) => { (OutputType::Success, format!("export: {}", path)) } Err(e) => ( OutputType::Failure, format!("export error: {}", e), ), }, ); } Command::DeleteKey(key_type, ref key_id) => { match self.gpgme.delete_key(key_type, key_id.to_string()) { Ok(_) => { self.refresh()?; } Err(e) => self.prompt.set_output(( OutputType::Failure, format!("delete error: {}", e), )), } } Command::SendKey(key_id) => { self.prompt.set_output(match self.gpgme.send_key(key_id) { Ok(key_id) => ( OutputType::Success, format!("key sent to the keyserver: 0x{}", key_id), ), Err(e) => { (OutputType::Failure, format!("send error: {}", e)) } }); } Command::GenerateKey | Command::RefreshKeys | Command::EditKey(_) | Command::SignKey(_) | Command::ImportKeys(_, true) | Command::ExportKeys(_, _, true) => { let mut success_msg = None; let mut os_command = OsCommand::new("gpg"); os_command .arg("--homedir") .arg(self.gpgme.config.home_dir.as_os_str()); if self.gpgme.config.armor { os_command.arg("--armor"); } if let Some(default_key) = &self.gpgme.config.default_key { os_command.arg("--default-key").arg(default_key); } let os_command = match command { Command::EditKey(ref key) => { os_command.arg("--edit-key").arg(key) } Command::SignKey(ref key) => { os_command.arg("--sign-key").arg(key) } Command::ImportKeys(ref keys, _) => { os_command.arg("--receive-keys").args(keys) } Command::ExportKeys(key_type, ref keys, true) => { let path = self .gpgme .get_output_file(key_type, keys.to_vec())?; success_msg = Some(format!("export: {}", path.to_string_lossy())); os_command .arg("--output") .arg(path) .arg("--export-secret-subkeys") .args(keys) } Command::RefreshKeys => os_command.arg("--refresh-keys"), _ => os_command.arg("--full-gen-key"), }; match os_command.spawn() { Ok(mut child) => { child.wait()?; self.refresh()?; if let Some(msg) = success_msg { self.prompt.set_output((OutputType::Success, msg)) } } Err(e) => self.prompt.set_output(( OutputType::Failure, format!("execution error: {}", e), )), } } Command::ToggleDetail(true) => { self.keys_table_detail.increase(); for key in self.keys_table.items.iter_mut() { key.detail = self.keys_table_detail; } for key in self.keys_table.default_items.iter_mut() { key.detail = self.keys_table_detail; } } Command::ToggleDetail(false) => { if let Some(index) = self.keys_table.state.tui.selected() { if let Some(key) = self.keys_table.items.get_mut(index) { key.detail.increase() } if self.keys_table.items.len() == self.keys_table.default_items.len() { if let Some(key) = self.keys_table.default_items.get_mut(index) { key.detail.increase() } } } } Command::ToggleTableSize => { self.keys_table.state.minimize_threshold = 0; self.keys_table.state.size = self.keys_table.state.size.next(); self.prompt.set_output(( OutputType::Success, format!( "table size: {}", format!("{:?}", self.keys_table.state.size) .to_lowercase() ), )); } Command::Scroll(direction, false) => match direction { ScrollDirection::Down(_) => { if self.state.show_options { self.options.next(); show_options = true; } else if Tab::Help == self.tab { self.key_bindings.next(); } else { self.keys_table.next(); } } ScrollDirection::Up(_) => { if self.state.show_options { self.options.previous(); show_options = true; } else if Tab::Help == self.tab { self.key_bindings.previous(); } else { self.keys_table.previous(); } } ScrollDirection::Top => { if self.state.show_options { self.options.state.select(Some(0)); show_options = true; } else if Tab::Help == self.tab { self.key_bindings.state.select(Some(0)); } else { self.keys_table.state.tui.select(Some(0)); } } ScrollDirection::Bottom => { if self.state.show_options { self.options.state.select(Some( self.options .items .len() .checked_sub(1) .unwrap_or_default(), )); show_options = true; } else if Tab::Help == self.tab { self.key_bindings .state .select(Some(KEY_BINDINGS.len() - 1)); } else { self.keys_table.state.tui.select(Some( self.keys_table .items .len() .checked_sub(1) .unwrap_or_default(), )); } } _ => {} }, Command::Scroll(direction, true) => { self.keys_table.scroll_row(direction); } Command::Set(option, value) => { if option == *"prompt" && (value.starts_with(COMMAND_PREFIX) | value.starts_with(SEARCH_PREFIX)) { self.prompt.clear(); self.prompt.text = value; } else { self.prompt.set_output(match option.as_str() { "output" => { let path = Path::new(&value); if path.exists() { self.gpgme.config.output_dir = path.to_path_buf(); ( OutputType::Success, format!( "output directory: {:?}", self.gpgme.config.output_dir ), ) } else { ( OutputType::Failure, String::from("path does not exist"), ) } } "mode" => { if let Ok(mode) = Mode::from_str(&value) { self.mode = mode; ( OutputType::Success, format!( "mode: {}", format!("{:?}", mode).to_lowercase() ), ) } else { ( OutputType::Failure, String::from("invalid mode"), ) } } "armor" => { if let Ok(value) = FromStr::from_str(&value) { self.gpgme.config.armor = value; self.gpgme.apply_config(); ( OutputType::Success, format!("armor: {}", value), ) } else { ( OutputType::Failure, String::from( "usage: set armor ", ), ) } } "signer" => { self.gpgme.config.default_key = Some(value.to_string()); (OutputType::Success, format!("signer: {}", value)) } "minimize" => { self.keys_table.state.minimize_threshold = value.parse().unwrap_or_default(); ( OutputType::Success, format!( "minimize threshold: {}", self.keys_table.state.minimize_threshold ), ) } "detail" => { if let Ok(detail_level) = KeyDetail::from_str(&value) { if let Some(index) = self.keys_table.state.tui.selected() { if let Some(key) = self.keys_table.items.get_mut(index) { key.detail = detail_level; } if self.keys_table.items.len() == self.keys_table.default_items.len() { if let Some(key) = self .keys_table .default_items .get_mut(index) { key.detail = detail_level; } } } ( OutputType::Success, format!("detail: {}", detail_level), ) } else { ( OutputType::Failure, String::from("usage: set detail "), ) } } "margin" => { self.keys_table_margin = value.parse().unwrap_or_default(); ( OutputType::Success, format!( "table margin: {}", self.keys_table_margin ), ) } "style" => match Style::from_str(&value) { Ok(style) => { self.state.style = style; ( OutputType::Success, format!("style: {}", self.state.style), ) } Err(_) => ( OutputType::Failure, String::from("usage: set style