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