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::{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
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!("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
81pub 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 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 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, };
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 app_data.save(Some(test_path.clone()))?;
817
818 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}