node_launchpad/
config.rs

1// Copyright 2024 MaidSafe.net limited.
2//
3// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3.
4// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed
5// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
6// KIND, either express or implied. Please review the Licences for the specific language governing
7// permissions and limitations relating to use of the SAFE Network Software.
8
9use crate::connection_mode::ConnectionMode;
10use crate::system::get_primary_mount_point;
11use crate::{action::Action, mode::Scene};
12use ant_node_manager::config::is_running_as_root;
13use color_eyre::eyre::{eyre, Result};
14use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
15use derive_deref::{Deref, DerefMut};
16use ratatui::style::{Color, Modifier, Style};
17use serde::{de::Deserializer, Deserialize, Serialize};
18use std::collections::HashMap;
19use std::path::PathBuf;
20
21const CONFIG: &str = include_str!("../.config/config.json5");
22
23/// Where to store the Nodes data.
24///
25/// If `base_dir` is the primary mount point, we store in "<base_dir>/$HOME/user_data_dir/autonomi/node".
26///
27/// if not we store in "<base_dir>/autonomi/node".
28///
29/// If should_create is true, the directory will be created if it doesn't exists.
30pub fn get_launchpad_nodes_data_dir_path(
31    base_dir: &PathBuf,
32    should_create: bool,
33) -> Result<PathBuf> {
34    let mut mount_point = PathBuf::new();
35    let is_root = is_running_as_root();
36
37    let data_directory: PathBuf = if *base_dir == get_primary_mount_point() {
38        if is_root {
39            // The root's data directory isn't accessible to the user `ant`, so we are using an
40            // alternative default path that `ant` can access.
41            #[cfg(unix)]
42            {
43                let default_data_dir_path = PathBuf::from("/var/antctl/services");
44                debug!("Running as root; using default path {:?} for nodes data directory instead of primary mount point", default_data_dir_path);
45                default_data_dir_path
46            }
47            #[cfg(windows)]
48            get_user_data_dir()?
49        } else {
50            get_user_data_dir()?
51        }
52    } else {
53        base_dir.clone()
54    };
55    mount_point.push(data_directory);
56    mount_point.push("autonomi");
57    mount_point.push("node");
58    if should_create {
59        debug!("Creating nodes data dir: {:?}", mount_point.as_path());
60        match std::fs::create_dir_all(mount_point.as_path()) {
61            Ok(_) => debug!("Nodes {:?} data dir created successfully", mount_point),
62            Err(e) => {
63                error!(
64                    "Failed to create nodes data dir in {:?}: {:?}",
65                    mount_point, e
66                );
67                return Err(eyre!(
68                    "Failed to create nodes data dir in {:?}",
69                    mount_point
70                ));
71            }
72        }
73    }
74    Ok(mount_point)
75}
76
77fn get_user_data_dir() -> Result<PathBuf> {
78    dirs_next::data_dir().ok_or_else(|| eyre!("User data directory is not obtainable",))
79}
80
81/// Where to store the Launchpad config & logs.
82///
83pub fn get_launchpad_data_dir_path() -> Result<PathBuf> {
84    let mut home_dirs =
85        dirs_next::data_dir().ok_or_else(|| eyre!("Data directory is not obtainable"))?;
86    home_dirs.push("autonomi");
87    home_dirs.push("launchpad");
88    std::fs::create_dir_all(home_dirs.as_path())?;
89    Ok(home_dirs)
90}
91
92pub fn get_config_dir() -> Result<PathBuf> {
93    // TODO: consider using dirs_next::config_dir. Configuration and data are different things.
94    let config_dir = get_launchpad_data_dir_path()?.join("config");
95    std::fs::create_dir_all(&config_dir)?;
96    Ok(config_dir)
97}
98
99#[cfg(windows)]
100pub async fn configure_winsw() -> Result<()> {
101    let data_dir_path = get_launchpad_data_dir_path()?;
102    ant_node_manager::helpers::configure_winsw(
103        &data_dir_path.join("winsw.exe"),
104        ant_node_manager::VerbosityLevel::Minimal,
105    )
106    .await?;
107    Ok(())
108}
109
110#[cfg(not(windows))]
111pub async fn configure_winsw() -> Result<()> {
112    Ok(())
113}
114
115#[derive(Clone, Debug, Deserialize, Serialize)]
116pub struct AppData {
117    pub discord_username: String,
118    pub nodes_to_start: usize,
119    pub storage_mountpoint: Option<PathBuf>,
120    pub storage_drive: Option<String>,
121    pub connection_mode: Option<ConnectionMode>,
122    pub port_from: Option<u32>,
123    pub port_to: Option<u32>,
124}
125
126impl Default for AppData {
127    fn default() -> Self {
128        Self {
129            discord_username: "".to_string(),
130            nodes_to_start: 1,
131            storage_mountpoint: None,
132            storage_drive: None,
133            connection_mode: None,
134            port_from: None,
135            port_to: None,
136        }
137    }
138}
139
140impl AppData {
141    pub fn load(custom_path: Option<PathBuf>) -> Result<Self> {
142        let config_path = if let Some(path) = custom_path {
143            path
144        } else {
145            get_config_dir()
146                .map_err(|_| color_eyre::eyre::eyre!("Could not obtain config dir"))?
147                .join("app_data.json")
148        };
149
150        if !config_path.exists() {
151            return Ok(Self::default());
152        }
153
154        let data = std::fs::read_to_string(&config_path).map_err(|e| {
155            error!("Failed to read app data file: {}", e);
156            color_eyre::eyre::eyre!("Failed to read app data file: {}", e)
157        })?;
158
159        let mut app_data: AppData = serde_json::from_str(&data).map_err(|e| {
160            error!("Failed to parse app data: {}", e);
161            color_eyre::eyre::eyre!("Failed to parse app data: {}", e)
162        })?;
163
164        // Don't allow the manual setting to HomeNetwork anymore
165        if let Some(ConnectionMode::HomeNetwork) = app_data.connection_mode {
166            app_data.connection_mode = Some(ConnectionMode::Automatic);
167        }
168
169        Ok(app_data)
170    }
171
172    pub fn save(&self, custom_path: Option<PathBuf>) -> Result<()> {
173        let config_path = if let Some(path) = custom_path {
174            path
175        } else {
176            get_config_dir()
177                .map_err(|_| config::ConfigError::Message("Could not obtain data dir".to_string()))?
178                .join("app_data.json")
179        };
180
181        let serialized_config = serde_json::to_string_pretty(&self)?;
182        std::fs::write(config_path, serialized_config)?;
183
184        Ok(())
185    }
186}
187
188#[derive(Clone, Debug, Default, Deserialize, Serialize)]
189pub struct Config {
190    #[serde(default)]
191    pub keybindings: KeyBindings,
192    #[serde(default)]
193    pub styles: Styles,
194}
195
196impl Config {
197    pub fn new() -> Result<Self, config::ConfigError> {
198        let default_config: Config = json5::from_str(CONFIG).unwrap();
199        let data_dir = get_launchpad_data_dir_path()
200            .map_err(|_| config::ConfigError::Message("Could not obtain data dir".to_string()))?;
201        let config_dir = get_config_dir()
202            .map_err(|_| config::ConfigError::Message("Could not obtain data dir".to_string()))?;
203        let mut builder = config::Config::builder()
204            .set_default("_data_dir", data_dir.to_str().unwrap())?
205            .set_default("_config_dir", config_dir.to_str().unwrap())?;
206
207        let config_files = [
208            ("config.json5", config::FileFormat::Json5),
209            ("config.json", config::FileFormat::Json),
210            ("config.yaml", config::FileFormat::Yaml),
211            ("config.toml", config::FileFormat::Toml),
212            ("config.ini", config::FileFormat::Ini),
213        ];
214        let mut found_config = false;
215        for (file, format) in &config_files {
216            builder = builder.add_source(
217                config::File::from(config_dir.join(file))
218                    .format(*format)
219                    .required(false),
220            );
221            if config_dir.join(file).exists() {
222                found_config = true
223            }
224        }
225        if !found_config {
226            log::error!("No configuration file found. Application may not behave as expected");
227        }
228
229        let mut cfg: Self = builder.build()?.try_deserialize()?;
230
231        for (mode, default_bindings) in default_config.keybindings.iter() {
232            let user_bindings = cfg.keybindings.entry(*mode).or_default();
233            for (key, cmd) in default_bindings.iter() {
234                user_bindings
235                    .entry(key.clone())
236                    .or_insert_with(|| cmd.clone());
237            }
238        }
239        for (mode, default_styles) in default_config.styles.iter() {
240            let user_styles = cfg.styles.entry(*mode).or_default();
241            for (style_key, style) in default_styles.iter() {
242                user_styles
243                    .entry(style_key.clone())
244                    .or_insert_with(|| *style);
245            }
246        }
247
248        Ok(cfg)
249    }
250}
251
252#[derive(Clone, Debug, Default, Deref, DerefMut, Serialize)]
253pub struct KeyBindings(pub HashMap<Scene, HashMap<Vec<KeyEvent>, Action>>);
254
255impl<'de> Deserialize<'de> for KeyBindings {
256    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
257    where
258        D: Deserializer<'de>,
259    {
260        let parsed_map = HashMap::<Scene, HashMap<String, Action>>::deserialize(deserializer)?;
261
262        let keybindings = parsed_map
263            .into_iter()
264            .map(|(mode, inner_map)| {
265                let converted_inner_map = inner_map
266                    .into_iter()
267                    .map(|(key_str, cmd)| (parse_key_sequence(&key_str).unwrap(), cmd))
268                    .collect();
269                (mode, converted_inner_map)
270            })
271            .collect();
272
273        Ok(KeyBindings(keybindings))
274    }
275}
276
277fn parse_key_event(raw: &str) -> Result<KeyEvent, String> {
278    let raw_lower = raw.to_ascii_lowercase();
279    let (remaining, modifiers) = extract_modifiers(&raw_lower);
280    parse_key_code_with_modifiers(remaining, modifiers)
281}
282
283fn extract_modifiers(raw: &str) -> (&str, KeyModifiers) {
284    let mut modifiers = KeyModifiers::empty();
285    let mut current = raw;
286
287    loop {
288        match current {
289            rest if rest.starts_with("ctrl-") => {
290                modifiers.insert(KeyModifiers::CONTROL);
291                current = &rest[5..];
292            }
293            rest if rest.starts_with("alt-") => {
294                modifiers.insert(KeyModifiers::ALT);
295                current = &rest[4..];
296            }
297            rest if rest.starts_with("shift-") => {
298                modifiers.insert(KeyModifiers::SHIFT);
299                current = &rest[6..];
300            }
301            _ => break, // break out of the loop if no known prefix is detected
302        };
303    }
304
305    (current, modifiers)
306}
307
308fn parse_key_code_with_modifiers(
309    raw: &str,
310    mut modifiers: KeyModifiers,
311) -> Result<KeyEvent, String> {
312    let c = match raw {
313        "esc" => KeyCode::Esc,
314        "enter" => KeyCode::Enter,
315        "left" => KeyCode::Left,
316        "right" => KeyCode::Right,
317        "up" => KeyCode::Up,
318        "down" => KeyCode::Down,
319        "home" => KeyCode::Home,
320        "end" => KeyCode::End,
321        "pageup" => KeyCode::PageUp,
322        "pagedown" => KeyCode::PageDown,
323        "backtab" => {
324            modifiers.insert(KeyModifiers::SHIFT);
325            KeyCode::BackTab
326        }
327        "backspace" => KeyCode::Backspace,
328        "delete" => KeyCode::Delete,
329        "insert" => KeyCode::Insert,
330        "f1" => KeyCode::F(1),
331        "f2" => KeyCode::F(2),
332        "f3" => KeyCode::F(3),
333        "f4" => KeyCode::F(4),
334        "f5" => KeyCode::F(5),
335        "f6" => KeyCode::F(6),
336        "f7" => KeyCode::F(7),
337        "f8" => KeyCode::F(8),
338        "f9" => KeyCode::F(9),
339        "f10" => KeyCode::F(10),
340        "f11" => KeyCode::F(11),
341        "f12" => KeyCode::F(12),
342        "space" => KeyCode::Char(' '),
343        "hyphen" => KeyCode::Char('-'),
344        "minus" => KeyCode::Char('-'),
345        "tab" => KeyCode::Tab,
346        c if c.len() == 1 => {
347            let mut c = c.chars().next().unwrap();
348            if modifiers.contains(KeyModifiers::SHIFT) {
349                c = c.to_ascii_uppercase();
350            }
351            KeyCode::Char(c)
352        }
353        _ => return Err(format!("Unable to parse {raw}")),
354    };
355    Ok(KeyEvent::new(c, modifiers))
356}
357
358pub fn key_event_to_string(key_event: &KeyEvent) -> String {
359    let char;
360    let key_code = match key_event.code {
361        KeyCode::Backspace => "backspace",
362        KeyCode::Enter => "enter",
363        KeyCode::Left => "left",
364        KeyCode::Right => "right",
365        KeyCode::Up => "up",
366        KeyCode::Down => "down",
367        KeyCode::Home => "home",
368        KeyCode::End => "end",
369        KeyCode::PageUp => "pageup",
370        KeyCode::PageDown => "pagedown",
371        KeyCode::Tab => "tab",
372        KeyCode::BackTab => "backtab",
373        KeyCode::Delete => "delete",
374        KeyCode::Insert => "insert",
375        KeyCode::F(c) => {
376            char = format!("f({c})");
377            &char
378        }
379        KeyCode::Char(' ') => "space",
380        KeyCode::Char(c) => {
381            char = c.to_string();
382            &char
383        }
384        KeyCode::Esc => "esc",
385        KeyCode::Null => "",
386        KeyCode::CapsLock => "",
387        KeyCode::Menu => "",
388        KeyCode::ScrollLock => "",
389        KeyCode::Media(_) => "",
390        KeyCode::NumLock => "",
391        KeyCode::PrintScreen => "",
392        KeyCode::Pause => "",
393        KeyCode::KeypadBegin => "",
394        KeyCode::Modifier(_) => "",
395    };
396
397    let mut modifiers = Vec::with_capacity(3);
398
399    if key_event.modifiers.intersects(KeyModifiers::CONTROL) {
400        modifiers.push("ctrl");
401    }
402
403    if key_event.modifiers.intersects(KeyModifiers::SHIFT) {
404        modifiers.push("shift");
405    }
406
407    if key_event.modifiers.intersects(KeyModifiers::ALT) {
408        modifiers.push("alt");
409    }
410
411    let mut key = modifiers.join("-");
412
413    if !key.is_empty() {
414        key.push('-');
415    }
416    key.push_str(key_code);
417
418    key
419}
420
421pub fn parse_key_sequence(raw: &str) -> Result<Vec<KeyEvent>, String> {
422    if raw.chars().filter(|c| *c == '>').count() != raw.chars().filter(|c| *c == '<').count() {
423        return Err(format!("Unable to parse `{}`", raw));
424    }
425    let raw = if !raw.contains("><") {
426        let raw = raw.strip_prefix('<').unwrap_or(raw);
427        let raw = raw.strip_prefix('>').unwrap_or(raw);
428        raw
429    } else {
430        raw
431    };
432    let sequences = raw
433        .split("><")
434        .map(|seq| {
435            if let Some(s) = seq.strip_prefix('<') {
436                s
437            } else if let Some(s) = seq.strip_suffix('>') {
438                s
439            } else {
440                seq
441            }
442        })
443        .collect::<Vec<_>>();
444
445    sequences.into_iter().map(parse_key_event).collect()
446}
447
448#[derive(Clone, Debug, Default, Deref, DerefMut, Serialize)]
449pub struct Styles(pub HashMap<Scene, HashMap<String, Style>>);
450
451impl<'de> Deserialize<'de> for Styles {
452    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
453    where
454        D: Deserializer<'de>,
455    {
456        let parsed_map = HashMap::<Scene, HashMap<String, String>>::deserialize(deserializer)?;
457
458        let styles = parsed_map
459            .into_iter()
460            .map(|(mode, inner_map)| {
461                let converted_inner_map = inner_map
462                    .into_iter()
463                    .map(|(str, style)| (str, parse_style(&style)))
464                    .collect();
465                (mode, converted_inner_map)
466            })
467            .collect();
468
469        Ok(Styles(styles))
470    }
471}
472
473pub fn parse_style(line: &str) -> Style {
474    let (foreground, background) =
475        line.split_at(line.to_lowercase().find("on ").unwrap_or(line.len()));
476    let foreground = process_color_string(foreground);
477    let background = process_color_string(&background.replace("on ", ""));
478
479    let mut style = Style::default();
480    if let Some(fg) = parse_color(&foreground.0) {
481        style = style.fg(fg);
482    }
483    if let Some(bg) = parse_color(&background.0) {
484        style = style.bg(bg);
485    }
486    style = style.add_modifier(foreground.1 | background.1);
487    style
488}
489
490fn process_color_string(color_str: &str) -> (String, Modifier) {
491    let color = color_str
492        .replace("grey", "gray")
493        .replace("bright ", "")
494        .replace("bold ", "")
495        .replace("underline ", "")
496        .replace("inverse ", "");
497
498    let mut modifiers = Modifier::empty();
499    if color_str.contains("underline") {
500        modifiers |= Modifier::UNDERLINED;
501    }
502    if color_str.contains("bold") {
503        modifiers |= Modifier::BOLD;
504    }
505    if color_str.contains("inverse") {
506        modifiers |= Modifier::REVERSED;
507    }
508
509    (color, modifiers)
510}
511
512fn parse_color(s: &str) -> Option<Color> {
513    let s = s.trim_start();
514    let s = s.trim_end();
515    if s.contains("bright color") {
516        let s = s.trim_start_matches("bright ");
517        let c = s
518            .trim_start_matches("color")
519            .parse::<u8>()
520            .unwrap_or_default();
521        Some(Color::Indexed(c.wrapping_shl(8)))
522    } else if s.contains("color") {
523        let c = s
524            .trim_start_matches("color")
525            .parse::<u8>()
526            .unwrap_or_default();
527        Some(Color::Indexed(c))
528    } else if s.contains("gray") {
529        let c = 232
530            + s.trim_start_matches("gray")
531                .parse::<u8>()
532                .unwrap_or_default();
533        Some(Color::Indexed(c))
534    } else if s.contains("rgb") {
535        let red = (s.as_bytes()[3] as char).to_digit(10).unwrap_or_default() as u8;
536        let green = (s.as_bytes()[4] as char).to_digit(10).unwrap_or_default() as u8;
537        let blue = (s.as_bytes()[5] as char).to_digit(10).unwrap_or_default() as u8;
538        let c = 16 + red * 36 + green * 6 + blue;
539        Some(Color::Indexed(c))
540    } else if s == "bold black" {
541        Some(Color::Indexed(8))
542    } else if s == "bold red" {
543        Some(Color::Indexed(9))
544    } else if s == "bold green" {
545        Some(Color::Indexed(10))
546    } else if s == "bold yellow" {
547        Some(Color::Indexed(11))
548    } else if s == "bold blue" {
549        Some(Color::Indexed(12))
550    } else if s == "bold magenta" {
551        Some(Color::Indexed(13))
552    } else if s == "bold cyan" {
553        Some(Color::Indexed(14))
554    } else if s == "bold white" {
555        Some(Color::Indexed(15))
556    } else if s == "black" {
557        Some(Color::Indexed(0))
558    } else if s == "red" {
559        Some(Color::Indexed(1))
560    } else if s == "green" {
561        Some(Color::Indexed(2))
562    } else if s == "yellow" {
563        Some(Color::Indexed(3))
564    } else if s == "blue" {
565        Some(Color::Indexed(4))
566    } else if s == "magenta" {
567        Some(Color::Indexed(5))
568    } else if s == "cyan" {
569        Some(Color::Indexed(6))
570    } else if s == "white" {
571        Some(Color::Indexed(7))
572    } else {
573        None
574    }
575}
576
577#[cfg(test)]
578mod tests {
579    use pretty_assertions::assert_eq;
580    use tempfile::tempdir;
581
582    use super::*;
583
584    #[test]
585    fn test_parse_style_default() {
586        let style = parse_style("");
587        assert_eq!(style, Style::default());
588    }
589
590    #[test]
591    fn test_parse_style_foreground() {
592        let style = parse_style("red");
593        assert_eq!(style.fg, Some(Color::Indexed(1)));
594    }
595
596    #[test]
597    fn test_parse_style_background() {
598        let style = parse_style("on blue");
599        assert_eq!(style.bg, Some(Color::Indexed(4)));
600    }
601
602    #[test]
603    fn test_parse_style_modifiers() {
604        let style = parse_style("underline red on blue");
605        assert_eq!(style.fg, Some(Color::Indexed(1)));
606        assert_eq!(style.bg, Some(Color::Indexed(4)));
607    }
608
609    #[test]
610    fn test_process_color_string() {
611        let (color, modifiers) = process_color_string("underline bold inverse gray");
612        assert_eq!(color, "gray");
613        assert!(modifiers.contains(Modifier::UNDERLINED));
614        assert!(modifiers.contains(Modifier::BOLD));
615        assert!(modifiers.contains(Modifier::REVERSED));
616    }
617
618    #[test]
619    fn test_parse_color_rgb() {
620        let color = parse_color("rgb123");
621        let expected = 16 + 36 + 2 * 6 + 3;
622        assert_eq!(color, Some(Color::Indexed(expected)));
623    }
624
625    #[test]
626    fn test_parse_color_unknown() {
627        let color = parse_color("unknown");
628        assert_eq!(color, None);
629    }
630
631    #[test]
632    fn test_config() -> Result<()> {
633        let c = Config::new()?;
634        assert_eq!(
635            c.keybindings
636                .get(&Scene::Status)
637                .unwrap()
638                .get(&parse_key_sequence("<q>").unwrap_or_default())
639                .unwrap(),
640            &Action::Quit
641        );
642        Ok(())
643    }
644
645    #[test]
646    fn test_simple_keys() {
647        assert_eq!(
648            parse_key_event("a").unwrap(),
649            KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())
650        );
651
652        assert_eq!(
653            parse_key_event("enter").unwrap(),
654            KeyEvent::new(KeyCode::Enter, KeyModifiers::empty())
655        );
656
657        assert_eq!(
658            parse_key_event("esc").unwrap(),
659            KeyEvent::new(KeyCode::Esc, KeyModifiers::empty())
660        );
661    }
662
663    #[test]
664    fn test_with_modifiers() {
665        assert_eq!(
666            parse_key_event("ctrl-a").unwrap(),
667            KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)
668        );
669
670        assert_eq!(
671            parse_key_event("alt-enter").unwrap(),
672            KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT)
673        );
674
675        assert_eq!(
676            parse_key_event("shift-esc").unwrap(),
677            KeyEvent::new(KeyCode::Esc, KeyModifiers::SHIFT)
678        );
679    }
680
681    #[test]
682    fn test_multiple_modifiers() {
683        assert_eq!(
684            parse_key_event("ctrl-alt-a").unwrap(),
685            KeyEvent::new(
686                KeyCode::Char('a'),
687                KeyModifiers::CONTROL | KeyModifiers::ALT
688            )
689        );
690
691        assert_eq!(
692            parse_key_event("ctrl-shift-enter").unwrap(),
693            KeyEvent::new(KeyCode::Enter, KeyModifiers::CONTROL | KeyModifiers::SHIFT)
694        );
695    }
696
697    #[test]
698    fn test_reverse_multiple_modifiers() {
699        assert_eq!(
700            key_event_to_string(&KeyEvent::new(
701                KeyCode::Char('a'),
702                KeyModifiers::CONTROL | KeyModifiers::ALT
703            )),
704            "ctrl-alt-a".to_string()
705        );
706    }
707
708    #[test]
709    fn test_invalid_keys() {
710        assert!(parse_key_event("invalid-key").is_err());
711        assert!(parse_key_event("ctrl-invalid-key").is_err());
712    }
713
714    #[test]
715    fn test_case_insensitivity() {
716        assert_eq!(
717            parse_key_event("CTRL-a").unwrap(),
718            KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)
719        );
720
721        assert_eq!(
722            parse_key_event("AlT-eNtEr").unwrap(),
723            KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT)
724        );
725    }
726
727    #[test]
728    fn test_app_data_file_does_not_exist() -> Result<()> {
729        let temp_dir = tempdir()?;
730        let non_existent_path = temp_dir.path().join("non_existent_app_data.json");
731
732        let app_data = AppData::load(Some(non_existent_path))?;
733
734        assert_eq!(app_data.discord_username, "");
735        assert_eq!(app_data.nodes_to_start, 1);
736        assert_eq!(app_data.storage_mountpoint, None);
737        assert_eq!(app_data.storage_drive, None);
738        assert_eq!(app_data.connection_mode, None);
739        assert_eq!(app_data.port_from, None);
740        assert_eq!(app_data.port_to, None);
741
742        Ok(())
743    }
744
745    #[test]
746    fn test_app_data_partial_info() -> Result<()> {
747        let temp_dir = tempdir()?;
748        let partial_data_path = temp_dir.path().join("partial_app_data.json");
749
750        let partial_data = r#"
751        {
752            "discord_username": "test_user",
753            "nodes_to_start": 3
754        }
755        "#;
756
757        std::fs::write(&partial_data_path, partial_data)?;
758
759        let app_data = AppData::load(Some(partial_data_path))?;
760
761        assert_eq!(app_data.discord_username, "test_user");
762        assert_eq!(app_data.nodes_to_start, 3);
763        assert_eq!(app_data.storage_mountpoint, None);
764        assert_eq!(app_data.storage_drive, None);
765        assert_eq!(app_data.connection_mode, None);
766        assert_eq!(app_data.port_from, None);
767        assert_eq!(app_data.port_to, None);
768
769        Ok(())
770    }
771
772    #[test]
773    fn test_app_data_missing_mountpoint() -> Result<()> {
774        let temp_dir = tempdir()?;
775        let missing_mountpoint_path = temp_dir.path().join("missing_mountpoint_app_data.json");
776
777        let missing_mountpoint_data = r#"
778        {
779            "discord_username": "test_user",
780            "nodes_to_start": 3,
781            "storage_drive": "C:"
782        }
783        "#;
784
785        std::fs::write(&missing_mountpoint_path, missing_mountpoint_data)?;
786
787        let app_data = AppData::load(Some(missing_mountpoint_path))?;
788
789        assert_eq!(app_data.discord_username, "test_user");
790        assert_eq!(app_data.nodes_to_start, 3);
791        assert_eq!(app_data.storage_mountpoint, None);
792        assert_eq!(app_data.storage_drive, Some("C:".to_string()));
793        assert_eq!(app_data.connection_mode, None);
794        assert_eq!(app_data.port_from, None);
795        assert_eq!(app_data.port_to, None);
796
797        Ok(())
798    }
799
800    #[test]
801    fn test_app_data_save_and_load() -> Result<()> {
802        let temp_dir = tempdir()?;
803        let test_path = temp_dir.path().join("test_app_data.json");
804
805        let mut app_data = AppData::default();
806        let var_name = &"save_load_user";
807        app_data.discord_username = var_name.to_string();
808        app_data.nodes_to_start = 4;
809        app_data.storage_mountpoint = Some(PathBuf::from("/mnt/test"));
810        app_data.storage_drive = Some("E:".to_string());
811        app_data.connection_mode = Some(ConnectionMode::CustomPorts);
812        app_data.port_from = Some(12000);
813        app_data.port_to = Some(13000);
814
815        // Save to custom path
816        app_data.save(Some(test_path.clone()))?;
817
818        // Load from custom path
819        let loaded_data = AppData::load(Some(test_path))?;
820
821        assert_eq!(loaded_data.discord_username, "save_load_user");
822        assert_eq!(loaded_data.nodes_to_start, 4);
823        assert_eq!(
824            loaded_data.storage_mountpoint,
825            Some(PathBuf::from("/mnt/test"))
826        );
827        assert_eq!(loaded_data.storage_drive, Some("E:".to_string()));
828        assert_eq!(
829            loaded_data.connection_mode,
830            Some(ConnectionMode::CustomPorts)
831        );
832        assert_eq!(loaded_data.port_from, Some(12000));
833        assert_eq!(loaded_data.port_to, Some(13000));
834
835        Ok(())
836    }
837}