Skip to main content

bee_tui/
config.rs

1#![allow(dead_code)] // Remove this once you start using the code
2
3use 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/// One configured Bee node. Multiple may coexist; multi-node UX is
25/// targeted at v0.4 but the schema supports it from day one.
26#[derive(Clone, Debug, Deserialize)]
27pub struct NodeConfig {
28    /// Friendly label shown in the UI (e.g. `"prod-1"`, `"local"`).
29    pub name: String,
30    /// Bee API base URL (e.g. `"http://localhost:1633"`).
31    pub url: String,
32    /// Optional bearer token for restricted-mode nodes. Supports the
33    /// `@env:VAR_NAME` indirection — see [`NodeConfig::resolved_token`].
34    #[serde(default)]
35    pub token: Option<String>,
36    /// Marks the default profile loaded on startup. If no entry has
37    /// `default = true`, the first node in the list is used.
38    #[serde(default)]
39    pub default: bool,
40}
41
42impl NodeConfig {
43    /// Resolve `token` to its concrete value: `Some(env_var)` if the
44    /// configured value starts with `@env:`, otherwise the literal.
45    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    /// Pick the active node profile: first entry with `default = true`,
69    /// otherwise the first entry, otherwise [`None`].
70    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
78/// Default node list when the user hasn't configured any: a single
79/// `local` profile pointing at `http://localhost:1633`.
80fn 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, // break out of the loop if no known prefix is detected
226        };
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}