1 use crate::app::command::Command;
2 use crate::app::launcher::App;
3 use crate::app::mode::Mode;
4 use crate::app::prompt::OutputType;
5 use crate::app::selection::Selection;
6 use crate::app::tab::Tab;
7 use crate::app::util;
8 use crate::gpg::key::KeyType;
9 use crate::term::tui::Tui;
10 use crate::widget::row::ScrollDirection;
11 use anyhow::Result;
12 use crossterm::event::{KeyCode as Key, KeyEvent, KeyModifiers as Modifiers};
13 use std::str::FromStr;
14 use tui::backend::Backend;
15
16 /// Handles the key events and executes the application command.
handle_events<B: Backend>( key_event: KeyEvent, tui: &mut Tui<B>, app: &mut App, ) -> Result<()>17 pub fn handle_events<B: Backend>(
18 key_event: KeyEvent,
19 tui: &mut Tui<B>,
20 app: &mut App,
21 ) -> Result<()> {
22 handle_command_execution(handle_key_event(key_event, app), tui, app)
23 }
24
25 /// Returns the corresponding application command for a key event.
handle_key_event(key_event: KeyEvent, app: &mut App) -> Command26 fn handle_key_event(key_event: KeyEvent, app: &mut App) -> Command {
27 let mut command = Command::None;
28 if app.prompt.is_enabled() {
29 match key_event.code {
30 Key::Char(c) => {
31 app.prompt.text.push(c);
32 if app.prompt.is_search_enabled() {
33 app.keys_table.reset_state();
34 }
35 }
36 Key::Up => app.prompt.previous(),
37 Key::Down => app.prompt.next(),
38 Key::Tab => {
39 if app.prompt.is_command_input_enabled() {
40 app.prompt.enable_search();
41 } else if app.prompt.is_search_enabled() {
42 app.prompt.enable_command_input();
43 app.keys_table.items = app.keys_table.default_items.clone();
44 }
45 }
46 Key::Backspace => {
47 app.prompt.text.pop();
48 if app.prompt.is_search_enabled() {
49 app.keys_table.reset_state();
50 }
51 }
52 Key::Esc => {
53 app.prompt.clear();
54 if app.prompt.is_search_enabled() {
55 app.keys_table.reset_state();
56 }
57 }
58 Key::Enter => {
59 if app.prompt.is_search_enabled() || app.prompt.text.len() < 2 {
60 app.prompt.clear();
61 } else if let Ok(cmd) = Command::from_str(&app.prompt.text) {
62 app.prompt.history.push(app.prompt.text.clone());
63 app.prompt.clear();
64 command = cmd;
65 } else {
66 app.prompt.set_output((
67 OutputType::Failure,
68 format!(
69 "invalid command: {}",
70 app.prompt.text.replacen(':', "", 1)
71 ),
72 ));
73 }
74 }
75 _ => {}
76 }
77 } else {
78 command = match key_event.code {
79 Key::Char('?') => Command::ShowHelp,
80 Key::Char('q') | Key::Char('Q') => Command::Quit,
81 Key::Esc => {
82 if app.mode != Mode::Normal {
83 Command::SwitchMode(Mode::Normal)
84 } else if app.state.show_options {
85 Command::None
86 } else if app.prompt.command.is_some() {
87 app.prompt.clear();
88 Command::None
89 } else {
90 Command::Quit
91 }
92 }
93 Key::Char('d') | Key::Char('D') | Key::Backspace => {
94 if key_event.modifiers == Modifiers::CONTROL
95 && key_event.code != Key::Backspace
96 {
97 Command::Quit
98 } else {
99 match app.keys_table.selected() {
100 Some(selected_key) => {
101 Command::Confirm(Box::new(Command::DeleteKey(
102 match app.tab {
103 Tab::Keys(key_type) => key_type,
104 _ => KeyType::Public,
105 },
106 selected_key.get_id(),
107 )))
108 }
109 None => Command::ShowOutput(
110 OutputType::Failure,
111 String::from("invalid selection"),
112 ),
113 }
114 }
115 }
116 Key::Char('c') | Key::Char('C') => {
117 if key_event.modifiers == Modifiers::CONTROL {
118 Command::Quit
119 } else {
120 Command::SwitchMode(Mode::Copy)
121 }
122 }
123 Key::Char('v') | Key::Char('V') => {
124 if key_event.modifiers == Modifiers::CONTROL {
125 Command::Paste
126 } else {
127 Command::SwitchMode(Mode::Visual)
128 }
129 }
130 Key::Char('p') | Key::Char('P') => Command::Paste,
131 Key::Char('r') | Key::Char('R') | Key::F(5) => {
132 if key_event.modifiers == Modifiers::CONTROL {
133 Command::RefreshKeys
134 } else {
135 Command::Refresh
136 }
137 }
138 Key::Up | Key::Char('k') | Key::Char('K') => {
139 if key_event.modifiers == Modifiers::CONTROL {
140 Command::Scroll(ScrollDirection::Top, false)
141 } else {
142 Command::Scroll(
143 ScrollDirection::Up(1),
144 key_event.modifiers == Modifiers::ALT,
145 )
146 }
147 }
148 Key::Right | Key::Char('l') | Key::Char('L') => {
149 if key_event.modifiers == Modifiers::ALT {
150 Command::Scroll(ScrollDirection::Right(1), true)
151 } else {
152 Command::NextTab
153 }
154 }
155 Key::Down | Key::Char('j') | Key::Char('J') => {
156 if key_event.modifiers == Modifiers::CONTROL {
157 Command::Scroll(ScrollDirection::Bottom, false)
158 } else {
159 Command::Scroll(
160 ScrollDirection::Down(1),
161 key_event.modifiers == Modifiers::ALT,
162 )
163 }
164 }
165 Key::Left | Key::Char('h') | Key::Char('H') => {
166 if key_event.modifiers == Modifiers::ALT {
167 Command::Scroll(ScrollDirection::Left(1), true)
168 } else {
169 Command::PreviousTab
170 }
171 }
172 Key::PageUp => Command::Scroll(ScrollDirection::Top, false),
173 Key::PageDown => Command::Scroll(ScrollDirection::Bottom, false),
174 Key::Char('t') | Key::Char('T') => Command::ToggleDetail(true),
175 Key::Tab => Command::ToggleDetail(false),
176 Key::Char('`') => Command::Set(
177 String::from("margin"),
178 String::from(if app.keys_table_margin == 1 {
179 "0"
180 } else {
181 "1"
182 }),
183 ),
184 Key::Char('s') | Key::Char('S') => {
185 if key_event.modifiers == Modifiers::CONTROL {
186 Command::ChangeStyle(app.state.style.next())
187 } else {
188 match app.keys_table.selected() {
189 Some(selected_key) => {
190 Command::SignKey(selected_key.get_id())
191 }
192 None => Command::ShowOutput(
193 OutputType::Failure,
194 String::from("invalid selection"),
195 ),
196 }
197 }
198 }
199 Key::Char('e') | Key::Char('E') => {
200 match app.keys_table.selected() {
201 Some(selected_key) => {
202 Command::EditKey(selected_key.get_id())
203 }
204 None => Command::ShowOutput(
205 OutputType::Failure,
206 String::from("invalid selection"),
207 ),
208 }
209 }
210 Key::Char('x') | Key::Char('X') => {
211 if app.mode == Mode::Copy {
212 Command::Copy(Selection::Key)
213 } else {
214 match app.keys_table.selected() {
215 Some(selected_key) => Command::ExportKeys(
216 match app.tab {
217 Tab::Keys(key_type) => key_type,
218 _ => KeyType::Public,
219 },
220 vec![selected_key.get_id()],
221 false,
222 ),
223 None => Command::ShowOutput(
224 OutputType::Failure,
225 String::from("invalid selection"),
226 ),
227 }
228 }
229 }
230 Key::Char('g') | Key::Char('G') => Command::GenerateKey,
231 Key::Char('a') | Key::Char('A') => Command::Set(
232 String::from("armor"),
233 (!app.gpgme.config.armor).to_string(),
234 ),
235 Key::Char('n') | Key::Char('N') => {
236 if app.prompt.command.is_some() {
237 app.prompt.clear();
238 Command::None
239 } else {
240 Command::SwitchMode(Mode::Normal)
241 }
242 }
243 Key::Char('1') => {
244 if app.mode == Mode::Copy {
245 Command::Copy(Selection::TableRow(1))
246 } else {
247 Command::Set(
248 String::from("detail"),
249 String::from("minimum"),
250 )
251 }
252 }
253 Key::Char('2') => {
254 if app.mode == Mode::Copy {
255 Command::Copy(Selection::TableRow(2))
256 } else {
257 Command::Set(
258 String::from("detail"),
259 String::from("standard"),
260 )
261 }
262 }
263 Key::Char('3') => {
264 Command::Set(String::from("detail"), String::from("full"))
265 }
266 Key::Char('i') | Key::Char('I') => {
267 if app.mode == Mode::Copy {
268 Command::Copy(Selection::KeyId)
269 } else {
270 Command::Set(
271 String::from("prompt"),
272 String::from(":import "),
273 )
274 }
275 }
276 Key::Char('f') | Key::Char('F') => {
277 if app.mode == Mode::Copy {
278 Command::Copy(Selection::KeyFingerprint)
279 } else {
280 Command::Set(
281 String::from("prompt"),
282 String::from(":receive "),
283 )
284 }
285 }
286 Key::Char('u') | Key::Char('U') => {
287 if app.mode == Mode::Copy {
288 Command::Copy(Selection::KeyUserId)
289 } else {
290 match app.keys_table.selected() {
291 Some(selected_key) => Command::Confirm(Box::new(
292 Command::SendKey(selected_key.get_id()),
293 )),
294 None => Command::ShowOutput(
295 OutputType::Failure,
296 String::from("invalid selection"),
297 ),
298 }
299 }
300 }
301 Key::Char('m') | Key::Char('M') => Command::ToggleTableSize,
302 Key::Char('y') | Key::Char('Y') => {
303 if let Some(command) = &app.prompt.command {
304 command.clone()
305 } else {
306 Command::None
307 }
308 }
309 Key::Char('o') | Key::Char(' ') | Key::Enter => {
310 if let Some(select_type) = app.state.select {
311 Command::Copy(select_type)
312 } else if app.state.show_options {
313 app.options.selected().cloned().unwrap_or(Command::None)
314 } else {
315 Command::ShowOptions
316 }
317 }
318 Key::Char(':') => Command::EnableInput,
319 Key::Char('/') => Command::Search(None),
320 _ => Command::None,
321 };
322 }
323 command
324 }
325
326 /// Handles the execution of an application command.
327 ///
328 /// It checks the additional conditions for determining
329 /// if the execution of the given command is applicable.
330 /// For example, depending on the command, it toggles the
331 /// [`paused`] state of [`Tui`] or enables/disables the mouse capture.
332 ///
333 /// [`Tui`]: Tui
334 /// [`paused`]: Tui::paused
handle_command_execution<B: Backend>( mut command: Command, tui: &mut Tui<B>, app: &mut App, ) -> Result<()>335 fn handle_command_execution<B: Backend>(
336 mut command: Command,
337 tui: &mut Tui<B>,
338 app: &mut App,
339 ) -> Result<()> {
340 if app.state.show_splash && command != Command::Quit {
341 command = Command::None;
342 }
343 if let Tab::Help = app.tab {
344 match command {
345 Command::ShowOptions
346 | Command::ChangeStyle(_)
347 | Command::Scroll(_, _)
348 | Command::ListKeys(_)
349 | Command::SwitchMode(_)
350 | Command::Paste
351 | Command::EnableInput
352 | Command::NextTab
353 | Command::PreviousTab
354 | Command::Refresh
355 | Command::Quit
356 | Command::None => {}
357 Command::Set(ref option, _) => {
358 if option != "style" {
359 command = Command::None
360 }
361 }
362 _ => command = Command::None,
363 }
364 }
365 let mut toggle_pause = false;
366 match command {
367 Command::SwitchMode(Mode::Normal) | Command::Refresh => {
368 tui.enable_mouse_capture()?
369 }
370 Command::SwitchMode(Mode::Visual) => tui.disable_mouse_capture()?,
371 Command::Set(ref option, ref value) => {
372 if option == "mode" {
373 match Mode::from_str(value) {
374 Ok(Mode::Normal) => tui.enable_mouse_capture()?,
375 Ok(Mode::Visual) => tui.disable_mouse_capture()?,
376 _ => {}
377 }
378 } else if option == "prompt" && value == ":import " {
379 tui.toggle_pause()?;
380 toggle_pause = true;
381 match util::run_os_command(&app.state.file_explorer) {
382 Ok(files) => {
383 command = Command::ImportKeys(files, false);
384 }
385 Err(e) => eprintln!("{:?}", e),
386 }
387 }
388 }
389 Command::ExportKeys(_, _, _)
390 | Command::DeleteKey(_, _)
391 | Command::GenerateKey
392 | Command::RefreshKeys
393 | Command::EditKey(_)
394 | Command::SignKey(_)
395 | Command::ImportKeys(_, true) => {
396 tui.toggle_pause()?;
397 toggle_pause = true;
398 }
399 Command::Copy(Selection::Key) => {
400 if app.gpgme.config.armor {
401 tui.toggle_pause()?;
402 toggle_pause = true;
403 } else {
404 command = Command::ShowOutput(
405 OutputType::Warning,
406 String::from(
407 "enable armored output for copying the exported key(s)",
408 ),
409 );
410 }
411 }
412 _ => {}
413 }
414 app.run_command(command)?;
415 if toggle_pause {
416 tui.toggle_pause()?;
417 }
418 Ok(())
419 }
420
421 #[cfg(feature = "gpg-tests")]
422 #[cfg(test)]
423 mod tests {
424 use super::*;
425 use crate::app::command::Command;
426 use crate::app::style::Style;
427 use crate::args::Args;
428 use crate::gpg::config::GpgConfig;
429 use crate::gpg::context::GpgContext;
430 use pretty_assertions::assert_eq;
431 use std::env;
432 #[test]
test_app_handler() -> Result<()>433 fn test_app_handler() -> Result<()> {
434 env::set_var(
435 "GNUPGHOME",
436 dirs_next::cache_dir()
437 .unwrap()
438 .join(env!("CARGO_PKG_NAME"))
439 .to_str()
440 .unwrap(),
441 );
442 let args = Args::default();
443 let config = GpgConfig::new(&args)?;
444 let mut context = GpgContext::new(config)?;
445 let mut app = App::new(&mut context, &args)?;
446 let key_id = app.gpgme.get_all_keys()?.get(&KeyType::Public).unwrap()
447 [0]
448 .get_id();
449 let test_cases = vec![
450 (
451 Command::Confirm(Box::new(Command::DeleteKey(
452 KeyType::Public,
453 key_id.to_string(),
454 ))),
455 vec![
456 KeyEvent::new(Key::Char('d'), Modifiers::NONE),
457 KeyEvent::new(Key::Backspace, Modifiers::NONE),
458 ],
459 ),
460 (
461 Command::Confirm(Box::new(Command::SendKey(
462 key_id.to_string(),
463 ))),
464 vec![KeyEvent::new(Key::Char('u'), Modifiers::NONE)],
465 ),
466 (
467 Command::ExportKeys(
468 KeyType::Public,
469 vec![key_id.to_string()],
470 false,
471 ),
472 vec![KeyEvent::new(Key::Char('x'), Modifiers::NONE)],
473 ),
474 (
475 Command::EditKey(key_id.to_string()),
476 vec![KeyEvent::new(Key::Char('e'), Modifiers::NONE)],
477 ),
478 (
479 Command::SignKey(key_id),
480 vec![KeyEvent::new(Key::Char('s'), Modifiers::NONE)],
481 ),
482 (
483 Command::ShowHelp,
484 vec![KeyEvent::new(Key::Char('?'), Modifiers::NONE)],
485 ),
486 (
487 Command::ShowOptions,
488 vec![
489 KeyEvent::new(Key::Char('o'), Modifiers::NONE),
490 KeyEvent::new(Key::Char(' '), Modifiers::NONE),
491 KeyEvent::new(Key::Enter, Modifiers::NONE),
492 ],
493 ),
494 (
495 Command::GenerateKey,
496 vec![KeyEvent::new(Key::Char('g'), Modifiers::NONE)],
497 ),
498 (
499 Command::RefreshKeys,
500 vec![KeyEvent::new(Key::Char('r'), Modifiers::CONTROL)],
501 ),
502 (
503 Command::ToggleDetail(true),
504 vec![KeyEvent::new(Key::Char('t'), Modifiers::NONE)],
505 ),
506 (
507 Command::ToggleDetail(false),
508 vec![KeyEvent::new(Key::Tab, Modifiers::NONE)],
509 ),
510 (
511 Command::Scroll(ScrollDirection::Top, false),
512 vec![
513 KeyEvent::new(Key::Up, Modifiers::CONTROL),
514 KeyEvent::new(Key::Char('k'), Modifiers::CONTROL),
515 KeyEvent::new(Key::PageUp, Modifiers::NONE),
516 ],
517 ),
518 (
519 Command::Scroll(ScrollDirection::Up(1), false),
520 vec![
521 KeyEvent::new(Key::Up, Modifiers::NONE),
522 KeyEvent::new(Key::Char('k'), Modifiers::NONE),
523 ],
524 ),
525 (
526 Command::Scroll(ScrollDirection::Right(1), true),
527 vec![
528 KeyEvent::new(Key::Right, Modifiers::ALT),
529 KeyEvent::new(Key::Char('l'), Modifiers::ALT),
530 ],
531 ),
532 (
533 Command::Scroll(ScrollDirection::Bottom, false),
534 vec![
535 KeyEvent::new(Key::Down, Modifiers::CONTROL),
536 KeyEvent::new(Key::Char('j'), Modifiers::CONTROL),
537 KeyEvent::new(Key::PageDown, Modifiers::NONE),
538 ],
539 ),
540 (
541 Command::Scroll(ScrollDirection::Down(1), false),
542 vec![
543 KeyEvent::new(Key::Down, Modifiers::NONE),
544 KeyEvent::new(Key::Char('j'), Modifiers::NONE),
545 ],
546 ),
547 (
548 Command::Scroll(ScrollDirection::Left(1), true),
549 vec![
550 KeyEvent::new(Key::Left, Modifiers::ALT),
551 KeyEvent::new(Key::Char('h'), Modifiers::ALT),
552 ],
553 ),
554 (
555 Command::Set(String::from("margin"), String::from("0")),
556 vec![KeyEvent::new(Key::Char('`'), Modifiers::NONE)],
557 ),
558 (
559 Command::ChangeStyle(Style::Colored),
560 vec![KeyEvent::new(Key::Char('s'), Modifiers::CONTROL)],
561 ),
562 (
563 Command::Set(String::from("armor"), String::from("true")),
564 vec![KeyEvent::new(Key::Char('a'), Modifiers::NONE)],
565 ),
566 (
567 Command::Set(String::from("detail"), String::from("minimum")),
568 vec![KeyEvent::new(Key::Char('1'), Modifiers::NONE)],
569 ),
570 (
571 Command::Set(String::from("detail"), String::from("standard")),
572 vec![KeyEvent::new(Key::Char('2'), Modifiers::NONE)],
573 ),
574 (
575 Command::Set(String::from("detail"), String::from("full")),
576 vec![KeyEvent::new(Key::Char('3'), Modifiers::NONE)],
577 ),
578 (
579 Command::Set(String::from("prompt"), String::from(":import ")),
580 vec![KeyEvent::new(Key::Char('i'), Modifiers::NONE)],
581 ),
582 (
583 Command::Set(String::from("prompt"), String::from(":receive ")),
584 vec![KeyEvent::new(Key::Char('f'), Modifiers::NONE)],
585 ),
586 (
587 Command::ToggleTableSize,
588 vec![KeyEvent::new(Key::Char('m'), Modifiers::NONE)],
589 ),
590 (
591 Command::SwitchMode(Mode::Normal),
592 vec![KeyEvent::new(Key::Char('n'), Modifiers::NONE)],
593 ),
594 (
595 Command::SwitchMode(Mode::Visual),
596 vec![KeyEvent::new(Key::Char('v'), Modifiers::NONE)],
597 ),
598 (
599 Command::SwitchMode(Mode::Copy),
600 vec![KeyEvent::new(Key::Char('c'), Modifiers::NONE)],
601 ),
602 (
603 Command::Paste,
604 vec![KeyEvent::new(Key::Char('v'), Modifiers::CONTROL)],
605 ),
606 (
607 Command::Paste,
608 vec![KeyEvent::new(Key::Char('p'), Modifiers::NONE)],
609 ),
610 (
611 Command::EnableInput,
612 vec![KeyEvent::new(Key::Char(':'), Modifiers::CONTROL)],
613 ),
614 (
615 Command::Search(None),
616 vec![KeyEvent::new(Key::Char('/'), Modifiers::CONTROL)],
617 ),
618 (
619 Command::NextTab,
620 vec![
621 KeyEvent::new(Key::Right, Modifiers::CONTROL),
622 KeyEvent::new(Key::Char('l'), Modifiers::NONE),
623 ],
624 ),
625 (
626 Command::PreviousTab,
627 vec![
628 KeyEvent::new(Key::Left, Modifiers::CONTROL),
629 KeyEvent::new(Key::Char('h'), Modifiers::NONE),
630 ],
631 ),
632 (
633 Command::Refresh,
634 vec![
635 KeyEvent::new(Key::Char('r'), Modifiers::NONE),
636 KeyEvent::new(Key::F(5), Modifiers::NONE),
637 ],
638 ),
639 (
640 Command::Quit,
641 vec![
642 KeyEvent::new(Key::Char('q'), Modifiers::NONE),
643 KeyEvent::new(Key::Esc, Modifiers::NONE),
644 KeyEvent::new(Key::Char('d'), Modifiers::CONTROL),
645 KeyEvent::new(Key::Char('c'), Modifiers::CONTROL),
646 ],
647 ),
648 (
649 Command::None,
650 vec![KeyEvent::new(Key::Char('y'), Modifiers::NONE)],
651 ),
652 (
653 Command::None,
654 vec![KeyEvent::new(Key::Char('ö'), Modifiers::NONE)],
655 ),
656 ];
657 for (command, key_events) in test_cases {
658 for key_event in key_events {
659 assert_eq!(command, handle_key_event(key_event, &mut app));
660 }
661 }
662 app.prompt.enable_command_input();
663 handle_key_event(KeyEvent::new(Key::Esc, Modifiers::NONE), &mut app);
664 assert!(!app.prompt.is_enabled());
665 app.prompt.enable_search();
666 handle_key_event(KeyEvent::new(Key::Tab, Modifiers::NONE), &mut app);
667 for c in String::from("normal-").chars() {
668 handle_key_event(
669 KeyEvent::new(Key::Char(c), Modifiers::NONE),
670 &mut app,
671 );
672 }
673 handle_key_event(
674 KeyEvent::new(Key::Backspace, Modifiers::NONE),
675 &mut app,
676 );
677 assert_eq!(":normal", app.prompt.text);
678 assert_eq!(
679 Command::SwitchMode(Mode::Normal),
680 handle_key_event(
681 KeyEvent::new(Key::Enter, Modifiers::NONE),
682 &mut app,
683 )
684 );
685 app.prompt.enable_command_input();
686 handle_key_event(KeyEvent::new(Key::Down, Modifiers::NONE), &mut app);
687 handle_key_event(KeyEvent::new(Key::Up, Modifiers::NONE), &mut app);
688 assert_eq!(":normal", app.prompt.text);
689 Ok(())
690 }
691 }
692