1#![allow(dead_code)] use std::{collections::HashMap, env, path::PathBuf};
4
5use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
6use directories::ProjectDirs;
7use lazy_static::lazy_static;
8use ratatui::style::{Color, Modifier, Style};
9use serde::{Deserialize, de::Deserializer};
10use tracing::error;
11
12use crate::{action::Action, app::Mode};
13
14const CONFIG: &str = include_str!("../.config/config.json5");
15
16#[derive(Clone, Debug, Deserialize, Default)]
17pub struct AppConfig {
18 #[serde(default)]
19 pub data_dir: PathBuf,
20 #[serde(default)]
21 pub config_dir: PathBuf,
22}
23
24#[derive(Clone, Debug, Deserialize)]
27pub struct NodeConfig {
28 pub name: String,
30 pub url: String,
32 #[serde(default)]
35 pub token: Option<String>,
36 #[serde(default)]
39 pub default: bool,
40}
41
42impl NodeConfig {
43 pub fn resolved_token(&self) -> Option<String> {
46 let raw = self.token.as_deref()?;
47 if let Some(var) = raw.strip_prefix("@env:") {
48 env::var(var).ok()
49 } else {
50 Some(raw.to_string())
51 }
52 }
53}
54
55#[derive(Clone, Debug, Default, Deserialize)]
56pub struct Config {
57 #[serde(default, flatten)]
58 pub config: AppConfig,
59 #[serde(default = "default_nodes")]
60 pub nodes: Vec<NodeConfig>,
61 #[serde(default)]
62 pub keybindings: KeyBindings,
63 #[serde(default)]
64 pub styles: Styles,
65 #[serde(default)]
67 pub ui: UiConfig,
68 #[serde(default)]
72 pub bee: Option<BeeConfig>,
73 #[serde(default)]
78 pub metrics: MetricsConfig,
79 #[serde(default)]
84 pub economics: EconomicsConfig,
85 #[serde(default)]
88 pub alerts: AlertsConfig,
89 #[serde(default)]
94 pub durability: DurabilityConfig,
95 #[serde(default)]
100 pub pubsub: PubsubConfig,
101}
102
103#[derive(Clone, Debug, Deserialize)]
107pub struct BeeConfig {
108 pub bin: PathBuf,
113 pub config: PathBuf,
116 #[serde(default)]
120 pub logs: BeeLogsConfig,
121}
122
123#[derive(Clone, Debug, Deserialize)]
127pub struct BeeLogsConfig {
128 #[serde(default = "default_rotate_size_mb")]
133 pub rotate_size_mb: u64,
134 #[serde(default = "default_keep_files")]
138 pub keep_files: u32,
139}
140
141impl Default for BeeLogsConfig {
142 fn default() -> Self {
143 Self {
144 rotate_size_mb: default_rotate_size_mb(),
145 keep_files: default_keep_files(),
146 }
147 }
148}
149
150fn default_rotate_size_mb() -> u64 {
151 64
152}
153fn default_keep_files() -> u32 {
154 5
155}
156
157#[derive(Clone, Debug, Deserialize)]
161pub struct MetricsConfig {
162 #[serde(default)]
165 pub enabled: bool,
166 #[serde(default = "default_metrics_addr")]
169 pub addr: String,
170}
171
172impl Default for MetricsConfig {
173 fn default() -> Self {
174 Self {
175 enabled: false,
176 addr: default_metrics_addr(),
177 }
178 }
179}
180
181fn default_metrics_addr() -> String {
182 "127.0.0.1:9101".into()
183}
184
185#[derive(Clone, Debug, Default, Deserialize)]
189pub struct EconomicsConfig {
190 #[serde(default)]
195 pub gnosis_rpc_url: Option<String>,
196 #[serde(default)]
201 pub enable_market_tile: bool,
202}
203
204#[derive(Clone, Debug, Deserialize)]
208pub struct DurabilityConfig {
209 #[serde(default)]
216 pub swarmscan_check: bool,
217 #[serde(default = "default_swarmscan_url")]
223 pub swarmscan_url: String,
224}
225
226impl Default for DurabilityConfig {
227 fn default() -> Self {
228 Self {
229 swarmscan_check: false,
230 swarmscan_url: default_swarmscan_url(),
231 }
232 }
233}
234
235fn default_swarmscan_url() -> String {
236 "https://api.swarmscan.io/v1/chunks/{ref}".into()
237}
238
239#[derive(Clone, Debug, Default, Deserialize)]
245pub struct PubsubConfig {
246 #[serde(default)]
253 pub history_file: Option<PathBuf>,
254}
255
256#[derive(Clone, Debug, Deserialize)]
260pub struct AlertsConfig {
261 #[serde(default)]
264 pub webhook_url: Option<String>,
265 #[serde(default = "default_alerts_debounce_secs")]
269 pub debounce_secs: u64,
270}
271
272impl Default for AlertsConfig {
273 fn default() -> Self {
274 Self {
275 webhook_url: None,
276 debounce_secs: default_alerts_debounce_secs(),
277 }
278 }
279}
280
281fn default_alerts_debounce_secs() -> u64 {
282 crate::alerts::DEFAULT_DEBOUNCE_SECS
283}
284
285#[derive(Clone, Debug, Default, Deserialize)]
289pub struct UiConfig {
290 #[serde(default = "default_theme")]
293 pub theme: String,
294 #[serde(default)]
298 pub ascii_fallback: bool,
299 #[serde(default = "default_refresh")]
306 pub refresh: String,
307}
308
309fn default_theme() -> String {
310 "default".into()
311}
312
313fn default_refresh() -> String {
314 "default".into()
315}
316
317impl Config {
318 pub fn active_node(&self) -> Option<&NodeConfig> {
321 self.nodes
322 .iter()
323 .find(|n| n.default)
324 .or_else(|| self.nodes.first())
325 }
326}
327
328fn default_nodes() -> Vec<NodeConfig> {
331 vec![NodeConfig {
332 name: "local".to_string(),
333 url: "http://localhost:1633".to_string(),
334 token: None,
335 default: true,
336 }]
337}
338
339lazy_static! {
340 pub static ref PROJECT_NAME: String = env!("CARGO_CRATE_NAME").to_uppercase().to_string();
341 pub static ref DATA_FOLDER: Option<PathBuf> =
342 env::var(format!("{}_DATA", PROJECT_NAME.clone()))
343 .ok()
344 .map(PathBuf::from);
345 pub static ref CONFIG_FOLDER: Option<PathBuf> =
346 env::var(format!("{}_CONFIG", PROJECT_NAME.clone()))
347 .ok()
348 .map(PathBuf::from);
349}
350
351impl Config {
352 pub fn new() -> color_eyre::Result<Self, config::ConfigError> {
353 let default_config: Config = json5::from_str(CONFIG).unwrap();
354 let data_dir = get_data_dir();
355 let config_dir = get_config_dir();
356 let mut builder = config::Config::builder()
357 .set_default("data_dir", data_dir.to_str().unwrap())?
358 .set_default("config_dir", config_dir.to_str().unwrap())?;
359
360 let config_files = [
361 ("config.json5", config::FileFormat::Json5),
362 ("config.json", config::FileFormat::Json),
363 ("config.yaml", config::FileFormat::Yaml),
364 ("config.toml", config::FileFormat::Toml),
365 ("config.ini", config::FileFormat::Ini),
366 ];
367 let mut found_config = false;
368 for (file, format) in &config_files {
369 let source = config::File::from(config_dir.join(file))
370 .format(*format)
371 .required(false);
372 builder = builder.add_source(source);
373 if config_dir.join(file).exists() {
374 found_config = true
375 }
376 }
377 if !found_config {
378 error!("No configuration file found. Application may not behave as expected");
379 }
380
381 let mut cfg: Self = builder.build()?.try_deserialize()?;
382
383 for (mode, default_bindings) in default_config.keybindings.0.iter() {
384 let user_bindings = cfg.keybindings.0.entry(*mode).or_default();
385 for (key, cmd) in default_bindings.iter() {
386 user_bindings
387 .entry(key.clone())
388 .or_insert_with(|| cmd.clone());
389 }
390 }
391 for (mode, default_styles) in default_config.styles.0.iter() {
392 let user_styles = cfg.styles.0.entry(*mode).or_default();
393 for (style_key, style) in default_styles.iter() {
394 user_styles.entry(style_key.clone()).or_insert(*style);
395 }
396 }
397
398 Ok(cfg)
399 }
400}
401
402pub fn get_data_dir() -> PathBuf {
403 if let Some(s) = DATA_FOLDER.clone() {
404 s
405 } else if let Some(proj_dirs) = project_directory() {
406 proj_dirs.data_local_dir().to_path_buf()
407 } else {
408 PathBuf::from(".").join(".data")
409 }
410}
411
412pub fn get_config_dir() -> PathBuf {
413 if let Some(s) = CONFIG_FOLDER.clone() {
414 s
415 } else if let Some(proj_dirs) = project_directory() {
416 proj_dirs.config_local_dir().to_path_buf()
417 } else {
418 PathBuf::from(".").join(".config")
419 }
420}
421
422fn project_directory() -> Option<ProjectDirs> {
423 ProjectDirs::from("com", "ethswarm-tools", env!("CARGO_PKG_NAME"))
424}
425
426#[derive(Clone, Debug, Default)]
427pub struct KeyBindings(pub HashMap<Mode, HashMap<Vec<KeyEvent>, Action>>);
428
429impl<'de> Deserialize<'de> for KeyBindings {
430 fn deserialize<D>(deserializer: D) -> color_eyre::Result<Self, D::Error>
431 where
432 D: Deserializer<'de>,
433 {
434 let parsed_map = HashMap::<Mode, HashMap<String, Action>>::deserialize(deserializer)?;
435
436 let keybindings = parsed_map
437 .into_iter()
438 .map(|(mode, inner_map)| {
439 let converted_inner_map = inner_map
440 .into_iter()
441 .map(|(key_str, cmd)| (parse_key_sequence(&key_str).unwrap(), cmd))
442 .collect();
443 (mode, converted_inner_map)
444 })
445 .collect();
446
447 Ok(KeyBindings(keybindings))
448 }
449}
450
451fn parse_key_event(raw: &str) -> color_eyre::Result<KeyEvent, String> {
452 let raw_lower = raw.to_ascii_lowercase();
453 let (remaining, modifiers) = extract_modifiers(&raw_lower);
454 parse_key_code_with_modifiers(remaining, modifiers)
455}
456
457fn extract_modifiers(raw: &str) -> (&str, KeyModifiers) {
458 let mut modifiers = KeyModifiers::empty();
459 let mut current = raw;
460
461 loop {
462 match current {
463 rest if rest.starts_with("ctrl-") => {
464 modifiers.insert(KeyModifiers::CONTROL);
465 current = &rest[5..];
466 }
467 rest if rest.starts_with("alt-") => {
468 modifiers.insert(KeyModifiers::ALT);
469 current = &rest[4..];
470 }
471 rest if rest.starts_with("shift-") => {
472 modifiers.insert(KeyModifiers::SHIFT);
473 current = &rest[6..];
474 }
475 _ => break, };
477 }
478
479 (current, modifiers)
480}
481
482fn parse_key_code_with_modifiers(
483 raw: &str,
484 mut modifiers: KeyModifiers,
485) -> color_eyre::Result<KeyEvent, String> {
486 let c = match raw {
487 "esc" => KeyCode::Esc,
488 "enter" => KeyCode::Enter,
489 "left" => KeyCode::Left,
490 "right" => KeyCode::Right,
491 "up" => KeyCode::Up,
492 "down" => KeyCode::Down,
493 "home" => KeyCode::Home,
494 "end" => KeyCode::End,
495 "pageup" => KeyCode::PageUp,
496 "pagedown" => KeyCode::PageDown,
497 "backtab" => {
498 modifiers.insert(KeyModifiers::SHIFT);
499 KeyCode::BackTab
500 }
501 "backspace" => KeyCode::Backspace,
502 "delete" => KeyCode::Delete,
503 "insert" => KeyCode::Insert,
504 "f1" => KeyCode::F(1),
505 "f2" => KeyCode::F(2),
506 "f3" => KeyCode::F(3),
507 "f4" => KeyCode::F(4),
508 "f5" => KeyCode::F(5),
509 "f6" => KeyCode::F(6),
510 "f7" => KeyCode::F(7),
511 "f8" => KeyCode::F(8),
512 "f9" => KeyCode::F(9),
513 "f10" => KeyCode::F(10),
514 "f11" => KeyCode::F(11),
515 "f12" => KeyCode::F(12),
516 "space" => KeyCode::Char(' '),
517 "hyphen" => KeyCode::Char('-'),
518 "minus" => KeyCode::Char('-'),
519 "tab" => KeyCode::Tab,
520 c if c.len() == 1 => {
521 let mut c = c.chars().next().unwrap();
522 if modifiers.contains(KeyModifiers::SHIFT) {
523 c = c.to_ascii_uppercase();
524 }
525 KeyCode::Char(c)
526 }
527 _ => return Err(format!("Unable to parse {raw}")),
528 };
529 Ok(KeyEvent::new(c, modifiers))
530}
531
532pub fn key_event_to_string(key_event: &KeyEvent) -> String {
533 let char;
534 let key_code = match key_event.code {
535 KeyCode::Backspace => "backspace",
536 KeyCode::Enter => "enter",
537 KeyCode::Left => "left",
538 KeyCode::Right => "right",
539 KeyCode::Up => "up",
540 KeyCode::Down => "down",
541 KeyCode::Home => "home",
542 KeyCode::End => "end",
543 KeyCode::PageUp => "pageup",
544 KeyCode::PageDown => "pagedown",
545 KeyCode::Tab => "tab",
546 KeyCode::BackTab => "backtab",
547 KeyCode::Delete => "delete",
548 KeyCode::Insert => "insert",
549 KeyCode::F(c) => {
550 char = format!("f({c})");
551 &char
552 }
553 KeyCode::Char(' ') => "space",
554 KeyCode::Char(c) => {
555 char = c.to_string();
556 &char
557 }
558 KeyCode::Esc => "esc",
559 KeyCode::Null => "",
560 KeyCode::CapsLock => "",
561 KeyCode::Menu => "",
562 KeyCode::ScrollLock => "",
563 KeyCode::Media(_) => "",
564 KeyCode::NumLock => "",
565 KeyCode::PrintScreen => "",
566 KeyCode::Pause => "",
567 KeyCode::KeypadBegin => "",
568 KeyCode::Modifier(_) => "",
569 };
570
571 let mut modifiers = Vec::with_capacity(3);
572
573 if key_event.modifiers.intersects(KeyModifiers::CONTROL) {
574 modifiers.push("ctrl");
575 }
576
577 if key_event.modifiers.intersects(KeyModifiers::SHIFT) {
578 modifiers.push("shift");
579 }
580
581 if key_event.modifiers.intersects(KeyModifiers::ALT) {
582 modifiers.push("alt");
583 }
584
585 let mut key = modifiers.join("-");
586
587 if !key.is_empty() {
588 key.push('-');
589 }
590 key.push_str(key_code);
591
592 key
593}
594
595pub fn parse_key_sequence(raw: &str) -> color_eyre::Result<Vec<KeyEvent>, String> {
596 if raw.chars().filter(|c| *c == '>').count() != raw.chars().filter(|c| *c == '<').count() {
597 return Err(format!("Unable to parse `{}`", raw));
598 }
599 let raw = if !raw.contains("><") {
600 let raw = raw.strip_prefix('<').unwrap_or(raw);
601 raw.strip_prefix('>').unwrap_or(raw)
602 } else {
603 raw
604 };
605 let sequences = raw
606 .split("><")
607 .map(|seq| {
608 if let Some(s) = seq.strip_prefix('<') {
609 s
610 } else if let Some(s) = seq.strip_suffix('>') {
611 s
612 } else {
613 seq
614 }
615 })
616 .collect::<Vec<_>>();
617
618 sequences.into_iter().map(parse_key_event).collect()
619}
620
621#[derive(Clone, Debug, Default)]
622pub struct Styles(pub HashMap<Mode, HashMap<String, Style>>);
623
624impl<'de> Deserialize<'de> for Styles {
625 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
626 where
627 D: Deserializer<'de>,
628 {
629 let parsed_map = HashMap::<Mode, HashMap<String, String>>::deserialize(deserializer)?;
630
631 let styles = parsed_map
632 .into_iter()
633 .map(|(mode, inner_map)| {
634 let converted_inner_map = inner_map
635 .into_iter()
636 .map(|(str, style)| (str, parse_style(&style)))
637 .collect();
638 (mode, converted_inner_map)
639 })
640 .collect();
641
642 Ok(Styles(styles))
643 }
644}
645
646pub fn parse_style(line: &str) -> Style {
647 let (foreground, background) =
648 line.split_at(line.to_lowercase().find("on ").unwrap_or(line.len()));
649 let foreground = process_color_string(foreground);
650 let background = process_color_string(&background.replace("on ", ""));
651
652 let mut style = Style::default();
653 if let Some(fg) = parse_color(&foreground.0) {
654 style = style.fg(fg);
655 }
656 if let Some(bg) = parse_color(&background.0) {
657 style = style.bg(bg);
658 }
659 style = style.add_modifier(foreground.1 | background.1);
660 style
661}
662
663fn process_color_string(color_str: &str) -> (String, Modifier) {
664 let color = color_str
665 .replace("grey", "gray")
666 .replace("bright ", "")
667 .replace("bold ", "")
668 .replace("underline ", "")
669 .replace("inverse ", "");
670
671 let mut modifiers = Modifier::empty();
672 if color_str.contains("underline") {
673 modifiers |= Modifier::UNDERLINED;
674 }
675 if color_str.contains("bold") {
676 modifiers |= Modifier::BOLD;
677 }
678 if color_str.contains("inverse") {
679 modifiers |= Modifier::REVERSED;
680 }
681
682 (color, modifiers)
683}
684
685fn parse_color(s: &str) -> Option<Color> {
686 let s = s.trim_start();
687 let s = s.trim_end();
688 if s.contains("bright color") {
689 let s = s.trim_start_matches("bright ");
690 let c = s
691 .trim_start_matches("color")
692 .parse::<u8>()
693 .unwrap_or_default();
694 Some(Color::Indexed(c.wrapping_shl(8)))
695 } else if s.contains("color") {
696 let c = s
697 .trim_start_matches("color")
698 .parse::<u8>()
699 .unwrap_or_default();
700 Some(Color::Indexed(c))
701 } else if s.contains("gray") {
702 let c = 232
703 + s.trim_start_matches("gray")
704 .parse::<u8>()
705 .unwrap_or_default();
706 Some(Color::Indexed(c))
707 } else if s.contains("rgb") {
708 let red = (s.as_bytes()[3] as char).to_digit(10).unwrap_or_default() as u8;
709 let green = (s.as_bytes()[4] as char).to_digit(10).unwrap_or_default() as u8;
710 let blue = (s.as_bytes()[5] as char).to_digit(10).unwrap_or_default() as u8;
711 let c = 16 + red * 36 + green * 6 + blue;
712 Some(Color::Indexed(c))
713 } else if s == "bold black" {
714 Some(Color::Indexed(8))
715 } else if s == "bold red" {
716 Some(Color::Indexed(9))
717 } else if s == "bold green" {
718 Some(Color::Indexed(10))
719 } else if s == "bold yellow" {
720 Some(Color::Indexed(11))
721 } else if s == "bold blue" {
722 Some(Color::Indexed(12))
723 } else if s == "bold magenta" {
724 Some(Color::Indexed(13))
725 } else if s == "bold cyan" {
726 Some(Color::Indexed(14))
727 } else if s == "bold white" {
728 Some(Color::Indexed(15))
729 } else if s == "black" {
730 Some(Color::Indexed(0))
731 } else if s == "red" {
732 Some(Color::Indexed(1))
733 } else if s == "green" {
734 Some(Color::Indexed(2))
735 } else if s == "yellow" {
736 Some(Color::Indexed(3))
737 } else if s == "blue" {
738 Some(Color::Indexed(4))
739 } else if s == "magenta" {
740 Some(Color::Indexed(5))
741 } else if s == "cyan" {
742 Some(Color::Indexed(6))
743 } else if s == "white" {
744 Some(Color::Indexed(7))
745 } else {
746 None
747 }
748}
749
750#[cfg(test)]
751mod tests {
752 use pretty_assertions::assert_eq;
753
754 use super::*;
755
756 #[test]
757 fn test_parse_style_default() {
758 let style = parse_style("");
759 assert_eq!(style, Style::default());
760 }
761
762 #[test]
763 fn test_parse_style_foreground() {
764 let style = parse_style("red");
765 assert_eq!(style.fg, Some(Color::Indexed(1)));
766 }
767
768 #[test]
769 fn test_parse_style_background() {
770 let style = parse_style("on blue");
771 assert_eq!(style.bg, Some(Color::Indexed(4)));
772 }
773
774 #[test]
775 fn test_parse_style_modifiers() {
776 let style = parse_style("underline red on blue");
777 assert_eq!(style.fg, Some(Color::Indexed(1)));
778 assert_eq!(style.bg, Some(Color::Indexed(4)));
779 }
780
781 #[test]
782 fn test_process_color_string() {
783 let (color, modifiers) = process_color_string("underline bold inverse gray");
784 assert_eq!(color, "gray");
785 assert!(modifiers.contains(Modifier::UNDERLINED));
786 assert!(modifiers.contains(Modifier::BOLD));
787 assert!(modifiers.contains(Modifier::REVERSED));
788 }
789
790 #[test]
791 fn test_parse_color_rgb() {
792 let color = parse_color("rgb123");
793 let expected = 16 + 36 + 2 * 6 + 3;
794 assert_eq!(color, Some(Color::Indexed(expected)));
795 }
796
797 #[test]
798 fn test_parse_color_unknown() {
799 let color = parse_color("unknown");
800 assert_eq!(color, None);
801 }
802
803 #[test]
804 fn test_config() -> color_eyre::Result<()> {
805 let c = Config::new()?;
810 assert_eq!(
811 c.keybindings
812 .0
813 .get(&Mode::Home)
814 .unwrap()
815 .get(&parse_key_sequence("<Ctrl-c>").unwrap_or_default())
816 .unwrap(),
817 &Action::Quit
818 );
819 Ok(())
820 }
821
822 #[test]
823 fn test_simple_keys() {
824 assert_eq!(
825 parse_key_event("a").unwrap(),
826 KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())
827 );
828
829 assert_eq!(
830 parse_key_event("enter").unwrap(),
831 KeyEvent::new(KeyCode::Enter, KeyModifiers::empty())
832 );
833
834 assert_eq!(
835 parse_key_event("esc").unwrap(),
836 KeyEvent::new(KeyCode::Esc, KeyModifiers::empty())
837 );
838 }
839
840 #[test]
841 fn test_with_modifiers() {
842 assert_eq!(
843 parse_key_event("ctrl-a").unwrap(),
844 KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)
845 );
846
847 assert_eq!(
848 parse_key_event("alt-enter").unwrap(),
849 KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT)
850 );
851
852 assert_eq!(
853 parse_key_event("shift-esc").unwrap(),
854 KeyEvent::new(KeyCode::Esc, KeyModifiers::SHIFT)
855 );
856 }
857
858 #[test]
859 fn test_multiple_modifiers() {
860 assert_eq!(
861 parse_key_event("ctrl-alt-a").unwrap(),
862 KeyEvent::new(
863 KeyCode::Char('a'),
864 KeyModifiers::CONTROL | KeyModifiers::ALT
865 )
866 );
867
868 assert_eq!(
869 parse_key_event("ctrl-shift-enter").unwrap(),
870 KeyEvent::new(KeyCode::Enter, KeyModifiers::CONTROL | KeyModifiers::SHIFT)
871 );
872 }
873
874 #[test]
875 fn test_reverse_multiple_modifiers() {
876 assert_eq!(
877 key_event_to_string(&KeyEvent::new(
878 KeyCode::Char('a'),
879 KeyModifiers::CONTROL | KeyModifiers::ALT
880 )),
881 "ctrl-alt-a".to_string()
882 );
883 }
884
885 #[test]
886 fn test_invalid_keys() {
887 assert!(parse_key_event("invalid-key").is_err());
888 assert!(parse_key_event("ctrl-invalid-key").is_err());
889 }
890
891 #[test]
892 fn test_case_insensitivity() {
893 assert_eq!(
894 parse_key_event("CTRL-a").unwrap(),
895 KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)
896 );
897
898 assert_eq!(
899 parse_key_event("AlT-eNtEr").unwrap(),
900 KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT)
901 );
902 }
903}