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