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