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