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}
90
91#[derive(Clone, Debug, Deserialize)]
95pub struct BeeConfig {
96 pub bin: PathBuf,
101 pub config: PathBuf,
104 #[serde(default)]
108 pub logs: BeeLogsConfig,
109}
110
111#[derive(Clone, Debug, Deserialize)]
115pub struct BeeLogsConfig {
116 #[serde(default = "default_rotate_size_mb")]
121 pub rotate_size_mb: u64,
122 #[serde(default = "default_keep_files")]
126 pub keep_files: u32,
127}
128
129impl Default for BeeLogsConfig {
130 fn default() -> Self {
131 Self {
132 rotate_size_mb: default_rotate_size_mb(),
133 keep_files: default_keep_files(),
134 }
135 }
136}
137
138fn default_rotate_size_mb() -> u64 {
139 64
140}
141fn default_keep_files() -> u32 {
142 5
143}
144
145#[derive(Clone, Debug, Deserialize)]
149pub struct MetricsConfig {
150 #[serde(default)]
153 pub enabled: bool,
154 #[serde(default = "default_metrics_addr")]
157 pub addr: String,
158}
159
160impl Default for MetricsConfig {
161 fn default() -> Self {
162 Self {
163 enabled: false,
164 addr: default_metrics_addr(),
165 }
166 }
167}
168
169fn default_metrics_addr() -> String {
170 "127.0.0.1:9101".into()
171}
172
173#[derive(Clone, Debug, Default, Deserialize)]
177pub struct EconomicsConfig {
178 #[serde(default)]
183 pub gnosis_rpc_url: Option<String>,
184 #[serde(default)]
189 pub enable_market_tile: bool,
190}
191
192#[derive(Clone, Debug, Deserialize)]
196pub struct AlertsConfig {
197 #[serde(default)]
200 pub webhook_url: Option<String>,
201 #[serde(default = "default_alerts_debounce_secs")]
205 pub debounce_secs: u64,
206}
207
208impl Default for AlertsConfig {
209 fn default() -> Self {
210 Self {
211 webhook_url: None,
212 debounce_secs: default_alerts_debounce_secs(),
213 }
214 }
215}
216
217fn default_alerts_debounce_secs() -> u64 {
218 crate::alerts::DEFAULT_DEBOUNCE_SECS
219}
220
221#[derive(Clone, Debug, Default, Deserialize)]
225pub struct UiConfig {
226 #[serde(default = "default_theme")]
229 pub theme: String,
230 #[serde(default)]
234 pub ascii_fallback: bool,
235 #[serde(default = "default_refresh")]
242 pub refresh: String,
243}
244
245fn default_theme() -> String {
246 "default".into()
247}
248
249fn default_refresh() -> String {
250 "default".into()
251}
252
253impl Config {
254 pub fn active_node(&self) -> Option<&NodeConfig> {
257 self.nodes
258 .iter()
259 .find(|n| n.default)
260 .or_else(|| self.nodes.first())
261 }
262}
263
264fn default_nodes() -> Vec<NodeConfig> {
267 vec![NodeConfig {
268 name: "local".to_string(),
269 url: "http://localhost:1633".to_string(),
270 token: None,
271 default: true,
272 }]
273}
274
275lazy_static! {
276 pub static ref PROJECT_NAME: String = env!("CARGO_CRATE_NAME").to_uppercase().to_string();
277 pub static ref DATA_FOLDER: Option<PathBuf> =
278 env::var(format!("{}_DATA", PROJECT_NAME.clone()))
279 .ok()
280 .map(PathBuf::from);
281 pub static ref CONFIG_FOLDER: Option<PathBuf> =
282 env::var(format!("{}_CONFIG", PROJECT_NAME.clone()))
283 .ok()
284 .map(PathBuf::from);
285}
286
287impl Config {
288 pub fn new() -> color_eyre::Result<Self, config::ConfigError> {
289 let default_config: Config = json5::from_str(CONFIG).unwrap();
290 let data_dir = get_data_dir();
291 let config_dir = get_config_dir();
292 let mut builder = config::Config::builder()
293 .set_default("data_dir", data_dir.to_str().unwrap())?
294 .set_default("config_dir", config_dir.to_str().unwrap())?;
295
296 let config_files = [
297 ("config.json5", config::FileFormat::Json5),
298 ("config.json", config::FileFormat::Json),
299 ("config.yaml", config::FileFormat::Yaml),
300 ("config.toml", config::FileFormat::Toml),
301 ("config.ini", config::FileFormat::Ini),
302 ];
303 let mut found_config = false;
304 for (file, format) in &config_files {
305 let source = config::File::from(config_dir.join(file))
306 .format(*format)
307 .required(false);
308 builder = builder.add_source(source);
309 if config_dir.join(file).exists() {
310 found_config = true
311 }
312 }
313 if !found_config {
314 error!("No configuration file found. Application may not behave as expected");
315 }
316
317 let mut cfg: Self = builder.build()?.try_deserialize()?;
318
319 for (mode, default_bindings) in default_config.keybindings.0.iter() {
320 let user_bindings = cfg.keybindings.0.entry(*mode).or_default();
321 for (key, cmd) in default_bindings.iter() {
322 user_bindings
323 .entry(key.clone())
324 .or_insert_with(|| cmd.clone());
325 }
326 }
327 for (mode, default_styles) in default_config.styles.0.iter() {
328 let user_styles = cfg.styles.0.entry(*mode).or_default();
329 for (style_key, style) in default_styles.iter() {
330 user_styles.entry(style_key.clone()).or_insert(*style);
331 }
332 }
333
334 Ok(cfg)
335 }
336}
337
338pub fn get_data_dir() -> PathBuf {
339 if let Some(s) = DATA_FOLDER.clone() {
340 s
341 } else if let Some(proj_dirs) = project_directory() {
342 proj_dirs.data_local_dir().to_path_buf()
343 } else {
344 PathBuf::from(".").join(".data")
345 }
346}
347
348pub fn get_config_dir() -> PathBuf {
349 if let Some(s) = CONFIG_FOLDER.clone() {
350 s
351 } else if let Some(proj_dirs) = project_directory() {
352 proj_dirs.config_local_dir().to_path_buf()
353 } else {
354 PathBuf::from(".").join(".config")
355 }
356}
357
358fn project_directory() -> Option<ProjectDirs> {
359 ProjectDirs::from("com", "ethswarm-tools", env!("CARGO_PKG_NAME"))
360}
361
362#[derive(Clone, Debug, Default)]
363pub struct KeyBindings(pub HashMap<Mode, HashMap<Vec<KeyEvent>, Action>>);
364
365impl<'de> Deserialize<'de> for KeyBindings {
366 fn deserialize<D>(deserializer: D) -> color_eyre::Result<Self, D::Error>
367 where
368 D: Deserializer<'de>,
369 {
370 let parsed_map = HashMap::<Mode, HashMap<String, Action>>::deserialize(deserializer)?;
371
372 let keybindings = parsed_map
373 .into_iter()
374 .map(|(mode, inner_map)| {
375 let converted_inner_map = inner_map
376 .into_iter()
377 .map(|(key_str, cmd)| (parse_key_sequence(&key_str).unwrap(), cmd))
378 .collect();
379 (mode, converted_inner_map)
380 })
381 .collect();
382
383 Ok(KeyBindings(keybindings))
384 }
385}
386
387fn parse_key_event(raw: &str) -> color_eyre::Result<KeyEvent, String> {
388 let raw_lower = raw.to_ascii_lowercase();
389 let (remaining, modifiers) = extract_modifiers(&raw_lower);
390 parse_key_code_with_modifiers(remaining, modifiers)
391}
392
393fn extract_modifiers(raw: &str) -> (&str, KeyModifiers) {
394 let mut modifiers = KeyModifiers::empty();
395 let mut current = raw;
396
397 loop {
398 match current {
399 rest if rest.starts_with("ctrl-") => {
400 modifiers.insert(KeyModifiers::CONTROL);
401 current = &rest[5..];
402 }
403 rest if rest.starts_with("alt-") => {
404 modifiers.insert(KeyModifiers::ALT);
405 current = &rest[4..];
406 }
407 rest if rest.starts_with("shift-") => {
408 modifiers.insert(KeyModifiers::SHIFT);
409 current = &rest[6..];
410 }
411 _ => break, };
413 }
414
415 (current, modifiers)
416}
417
418fn parse_key_code_with_modifiers(
419 raw: &str,
420 mut modifiers: KeyModifiers,
421) -> color_eyre::Result<KeyEvent, String> {
422 let c = match raw {
423 "esc" => KeyCode::Esc,
424 "enter" => KeyCode::Enter,
425 "left" => KeyCode::Left,
426 "right" => KeyCode::Right,
427 "up" => KeyCode::Up,
428 "down" => KeyCode::Down,
429 "home" => KeyCode::Home,
430 "end" => KeyCode::End,
431 "pageup" => KeyCode::PageUp,
432 "pagedown" => KeyCode::PageDown,
433 "backtab" => {
434 modifiers.insert(KeyModifiers::SHIFT);
435 KeyCode::BackTab
436 }
437 "backspace" => KeyCode::Backspace,
438 "delete" => KeyCode::Delete,
439 "insert" => KeyCode::Insert,
440 "f1" => KeyCode::F(1),
441 "f2" => KeyCode::F(2),
442 "f3" => KeyCode::F(3),
443 "f4" => KeyCode::F(4),
444 "f5" => KeyCode::F(5),
445 "f6" => KeyCode::F(6),
446 "f7" => KeyCode::F(7),
447 "f8" => KeyCode::F(8),
448 "f9" => KeyCode::F(9),
449 "f10" => KeyCode::F(10),
450 "f11" => KeyCode::F(11),
451 "f12" => KeyCode::F(12),
452 "space" => KeyCode::Char(' '),
453 "hyphen" => KeyCode::Char('-'),
454 "minus" => KeyCode::Char('-'),
455 "tab" => KeyCode::Tab,
456 c if c.len() == 1 => {
457 let mut c = c.chars().next().unwrap();
458 if modifiers.contains(KeyModifiers::SHIFT) {
459 c = c.to_ascii_uppercase();
460 }
461 KeyCode::Char(c)
462 }
463 _ => return Err(format!("Unable to parse {raw}")),
464 };
465 Ok(KeyEvent::new(c, modifiers))
466}
467
468pub fn key_event_to_string(key_event: &KeyEvent) -> String {
469 let char;
470 let key_code = match key_event.code {
471 KeyCode::Backspace => "backspace",
472 KeyCode::Enter => "enter",
473 KeyCode::Left => "left",
474 KeyCode::Right => "right",
475 KeyCode::Up => "up",
476 KeyCode::Down => "down",
477 KeyCode::Home => "home",
478 KeyCode::End => "end",
479 KeyCode::PageUp => "pageup",
480 KeyCode::PageDown => "pagedown",
481 KeyCode::Tab => "tab",
482 KeyCode::BackTab => "backtab",
483 KeyCode::Delete => "delete",
484 KeyCode::Insert => "insert",
485 KeyCode::F(c) => {
486 char = format!("f({c})");
487 &char
488 }
489 KeyCode::Char(' ') => "space",
490 KeyCode::Char(c) => {
491 char = c.to_string();
492 &char
493 }
494 KeyCode::Esc => "esc",
495 KeyCode::Null => "",
496 KeyCode::CapsLock => "",
497 KeyCode::Menu => "",
498 KeyCode::ScrollLock => "",
499 KeyCode::Media(_) => "",
500 KeyCode::NumLock => "",
501 KeyCode::PrintScreen => "",
502 KeyCode::Pause => "",
503 KeyCode::KeypadBegin => "",
504 KeyCode::Modifier(_) => "",
505 };
506
507 let mut modifiers = Vec::with_capacity(3);
508
509 if key_event.modifiers.intersects(KeyModifiers::CONTROL) {
510 modifiers.push("ctrl");
511 }
512
513 if key_event.modifiers.intersects(KeyModifiers::SHIFT) {
514 modifiers.push("shift");
515 }
516
517 if key_event.modifiers.intersects(KeyModifiers::ALT) {
518 modifiers.push("alt");
519 }
520
521 let mut key = modifiers.join("-");
522
523 if !key.is_empty() {
524 key.push('-');
525 }
526 key.push_str(key_code);
527
528 key
529}
530
531pub fn parse_key_sequence(raw: &str) -> color_eyre::Result<Vec<KeyEvent>, String> {
532 if raw.chars().filter(|c| *c == '>').count() != raw.chars().filter(|c| *c == '<').count() {
533 return Err(format!("Unable to parse `{}`", raw));
534 }
535 let raw = if !raw.contains("><") {
536 let raw = raw.strip_prefix('<').unwrap_or(raw);
537 raw.strip_prefix('>').unwrap_or(raw)
538 } else {
539 raw
540 };
541 let sequences = raw
542 .split("><")
543 .map(|seq| {
544 if let Some(s) = seq.strip_prefix('<') {
545 s
546 } else if let Some(s) = seq.strip_suffix('>') {
547 s
548 } else {
549 seq
550 }
551 })
552 .collect::<Vec<_>>();
553
554 sequences.into_iter().map(parse_key_event).collect()
555}
556
557#[derive(Clone, Debug, Default)]
558pub struct Styles(pub HashMap<Mode, HashMap<String, Style>>);
559
560impl<'de> Deserialize<'de> for Styles {
561 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
562 where
563 D: Deserializer<'de>,
564 {
565 let parsed_map = HashMap::<Mode, HashMap<String, String>>::deserialize(deserializer)?;
566
567 let styles = parsed_map
568 .into_iter()
569 .map(|(mode, inner_map)| {
570 let converted_inner_map = inner_map
571 .into_iter()
572 .map(|(str, style)| (str, parse_style(&style)))
573 .collect();
574 (mode, converted_inner_map)
575 })
576 .collect();
577
578 Ok(Styles(styles))
579 }
580}
581
582pub fn parse_style(line: &str) -> Style {
583 let (foreground, background) =
584 line.split_at(line.to_lowercase().find("on ").unwrap_or(line.len()));
585 let foreground = process_color_string(foreground);
586 let background = process_color_string(&background.replace("on ", ""));
587
588 let mut style = Style::default();
589 if let Some(fg) = parse_color(&foreground.0) {
590 style = style.fg(fg);
591 }
592 if let Some(bg) = parse_color(&background.0) {
593 style = style.bg(bg);
594 }
595 style = style.add_modifier(foreground.1 | background.1);
596 style
597}
598
599fn process_color_string(color_str: &str) -> (String, Modifier) {
600 let color = color_str
601 .replace("grey", "gray")
602 .replace("bright ", "")
603 .replace("bold ", "")
604 .replace("underline ", "")
605 .replace("inverse ", "");
606
607 let mut modifiers = Modifier::empty();
608 if color_str.contains("underline") {
609 modifiers |= Modifier::UNDERLINED;
610 }
611 if color_str.contains("bold") {
612 modifiers |= Modifier::BOLD;
613 }
614 if color_str.contains("inverse") {
615 modifiers |= Modifier::REVERSED;
616 }
617
618 (color, modifiers)
619}
620
621fn parse_color(s: &str) -> Option<Color> {
622 let s = s.trim_start();
623 let s = s.trim_end();
624 if s.contains("bright color") {
625 let s = s.trim_start_matches("bright ");
626 let c = s
627 .trim_start_matches("color")
628 .parse::<u8>()
629 .unwrap_or_default();
630 Some(Color::Indexed(c.wrapping_shl(8)))
631 } else if s.contains("color") {
632 let c = s
633 .trim_start_matches("color")
634 .parse::<u8>()
635 .unwrap_or_default();
636 Some(Color::Indexed(c))
637 } else if s.contains("gray") {
638 let c = 232
639 + s.trim_start_matches("gray")
640 .parse::<u8>()
641 .unwrap_or_default();
642 Some(Color::Indexed(c))
643 } else if s.contains("rgb") {
644 let red = (s.as_bytes()[3] as char).to_digit(10).unwrap_or_default() as u8;
645 let green = (s.as_bytes()[4] as char).to_digit(10).unwrap_or_default() as u8;
646 let blue = (s.as_bytes()[5] as char).to_digit(10).unwrap_or_default() as u8;
647 let c = 16 + red * 36 + green * 6 + blue;
648 Some(Color::Indexed(c))
649 } else if s == "bold black" {
650 Some(Color::Indexed(8))
651 } else if s == "bold red" {
652 Some(Color::Indexed(9))
653 } else if s == "bold green" {
654 Some(Color::Indexed(10))
655 } else if s == "bold yellow" {
656 Some(Color::Indexed(11))
657 } else if s == "bold blue" {
658 Some(Color::Indexed(12))
659 } else if s == "bold magenta" {
660 Some(Color::Indexed(13))
661 } else if s == "bold cyan" {
662 Some(Color::Indexed(14))
663 } else if s == "bold white" {
664 Some(Color::Indexed(15))
665 } else if s == "black" {
666 Some(Color::Indexed(0))
667 } else if s == "red" {
668 Some(Color::Indexed(1))
669 } else if s == "green" {
670 Some(Color::Indexed(2))
671 } else if s == "yellow" {
672 Some(Color::Indexed(3))
673 } else if s == "blue" {
674 Some(Color::Indexed(4))
675 } else if s == "magenta" {
676 Some(Color::Indexed(5))
677 } else if s == "cyan" {
678 Some(Color::Indexed(6))
679 } else if s == "white" {
680 Some(Color::Indexed(7))
681 } else {
682 None
683 }
684}
685
686#[cfg(test)]
687mod tests {
688 use pretty_assertions::assert_eq;
689
690 use super::*;
691
692 #[test]
693 fn test_parse_style_default() {
694 let style = parse_style("");
695 assert_eq!(style, Style::default());
696 }
697
698 #[test]
699 fn test_parse_style_foreground() {
700 let style = parse_style("red");
701 assert_eq!(style.fg, Some(Color::Indexed(1)));
702 }
703
704 #[test]
705 fn test_parse_style_background() {
706 let style = parse_style("on blue");
707 assert_eq!(style.bg, Some(Color::Indexed(4)));
708 }
709
710 #[test]
711 fn test_parse_style_modifiers() {
712 let style = parse_style("underline red on blue");
713 assert_eq!(style.fg, Some(Color::Indexed(1)));
714 assert_eq!(style.bg, Some(Color::Indexed(4)));
715 }
716
717 #[test]
718 fn test_process_color_string() {
719 let (color, modifiers) = process_color_string("underline bold inverse gray");
720 assert_eq!(color, "gray");
721 assert!(modifiers.contains(Modifier::UNDERLINED));
722 assert!(modifiers.contains(Modifier::BOLD));
723 assert!(modifiers.contains(Modifier::REVERSED));
724 }
725
726 #[test]
727 fn test_parse_color_rgb() {
728 let color = parse_color("rgb123");
729 let expected = 16 + 36 + 2 * 6 + 3;
730 assert_eq!(color, Some(Color::Indexed(expected)));
731 }
732
733 #[test]
734 fn test_parse_color_unknown() {
735 let color = parse_color("unknown");
736 assert_eq!(color, None);
737 }
738
739 #[test]
740 fn test_config() -> color_eyre::Result<()> {
741 let c = Config::new()?;
746 assert_eq!(
747 c.keybindings
748 .0
749 .get(&Mode::Home)
750 .unwrap()
751 .get(&parse_key_sequence("<Ctrl-c>").unwrap_or_default())
752 .unwrap(),
753 &Action::Quit
754 );
755 Ok(())
756 }
757
758 #[test]
759 fn test_simple_keys() {
760 assert_eq!(
761 parse_key_event("a").unwrap(),
762 KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())
763 );
764
765 assert_eq!(
766 parse_key_event("enter").unwrap(),
767 KeyEvent::new(KeyCode::Enter, KeyModifiers::empty())
768 );
769
770 assert_eq!(
771 parse_key_event("esc").unwrap(),
772 KeyEvent::new(KeyCode::Esc, KeyModifiers::empty())
773 );
774 }
775
776 #[test]
777 fn test_with_modifiers() {
778 assert_eq!(
779 parse_key_event("ctrl-a").unwrap(),
780 KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)
781 );
782
783 assert_eq!(
784 parse_key_event("alt-enter").unwrap(),
785 KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT)
786 );
787
788 assert_eq!(
789 parse_key_event("shift-esc").unwrap(),
790 KeyEvent::new(KeyCode::Esc, KeyModifiers::SHIFT)
791 );
792 }
793
794 #[test]
795 fn test_multiple_modifiers() {
796 assert_eq!(
797 parse_key_event("ctrl-alt-a").unwrap(),
798 KeyEvent::new(
799 KeyCode::Char('a'),
800 KeyModifiers::CONTROL | KeyModifiers::ALT
801 )
802 );
803
804 assert_eq!(
805 parse_key_event("ctrl-shift-enter").unwrap(),
806 KeyEvent::new(KeyCode::Enter, KeyModifiers::CONTROL | KeyModifiers::SHIFT)
807 );
808 }
809
810 #[test]
811 fn test_reverse_multiple_modifiers() {
812 assert_eq!(
813 key_event_to_string(&KeyEvent::new(
814 KeyCode::Char('a'),
815 KeyModifiers::CONTROL | KeyModifiers::ALT
816 )),
817 "ctrl-alt-a".to_string()
818 );
819 }
820
821 #[test]
822 fn test_invalid_keys() {
823 assert!(parse_key_event("invalid-key").is_err());
824 assert!(parse_key_event("ctrl-invalid-key").is_err());
825 }
826
827 #[test]
828 fn test_case_insensitivity() {
829 assert_eq!(
830 parse_key_event("CTRL-a").unwrap(),
831 KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)
832 );
833
834 assert_eq!(
835 parse_key_event("AlT-eNtEr").unwrap(),
836 KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT)
837 );
838 }
839}