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