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