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