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