1use 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
23pub 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 #[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
84pub 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 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 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, };
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 app_data.save(Some(test_path.clone()))?;
819
820 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}