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