1use std::path::PathBuf;
10
11use serde::{Deserialize, Serialize};
12
13use crate::error::{ConfigError, Error, Result};
14
15pub fn default_config_path() -> PathBuf {
17 if let Some(home) = dirs::config_dir() {
18 home.join("ntfs-mac").join("config.toml")
19 } else {
20 PathBuf::from("/tmp/ntfs-mac/config.toml")
23 }
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct Config {
28 #[serde(default = "default_mount_base")]
31 pub mount_base: String,
32
33 #[serde(default)]
36 pub mount_options: Vec<String>,
37
38 #[serde(default = "default_fuse_driver")]
42 pub fuse_driver: String,
43
44 #[serde(default = "default_true")]
48 pub require_confirmation: bool,
49
50 #[serde(default = "default_true")]
52 pub color: bool,
53}
54
55impl Default for Config {
56 fn default() -> Self {
57 Self {
58 mount_base: default_mount_base(),
59 mount_options: Vec::new(),
60 fuse_driver: default_fuse_driver(),
61 require_confirmation: default_true(),
62 color: default_true(),
63 }
64 }
65}
66
67fn default_mount_base() -> String {
68 "/Volumes".to_string()
69}
70
71fn default_fuse_driver() -> String {
72 "auto".to_string()
73}
74
75fn default_true() -> bool {
76 true
77}
78
79impl Config {
80 pub fn load() -> Result<Self> {
82 load_config(&default_config_path())
83 }
84
85 pub fn save(&self) -> Result<()> {
87 save_config(self, &default_config_path())
88 }
89
90 #[must_use]
92 pub fn with_mount_base(mut self, base: String) -> Self {
93 self.mount_base = base;
94 self
95 }
96}
97
98pub fn load_config(path: &std::path::Path) -> Result<Config> {
100 let Ok(bytes) = std::fs::read(path) else {
101 return Ok(Config::default());
102 };
103 match toml::from_str(&String::from_utf8_lossy(&bytes)) {
104 Ok(c) => Ok(c),
105 Err(e) => Err(Error::Serde(e.to_string())),
106 }
107}
108
109pub fn save_config(cfg: &Config, path: &std::path::Path) -> Result<()> {
111 if let Some(parent) = path.parent() {
112 std::fs::create_dir_all(parent).map_err(|e| ConfigError::Write {
113 path: path.to_path_buf(),
114 reason: e.to_string(),
115 })?;
116 }
117 let text = toml::to_string_pretty(cfg).map_err(|e| ConfigError::Write {
118 path: path.to_path_buf(),
119 reason: e.to_string(),
120 })?;
121 std::fs::write(path, text).map_err(|e| ConfigError::Write {
122 path: path.to_path_buf(),
123 reason: e.to_string(),
124 })?;
125 Ok(())
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn defaults_are_sensible() {
134 let c = Config::default();
135 assert_eq!(c.mount_base, "/Volumes");
136 assert_eq!(c.fuse_driver, "auto");
137 assert!(c.require_confirmation);
138 assert!(c.color);
139 assert!(c.mount_options.is_empty());
140 }
141
142 #[test]
143 fn round_trip_persists_fields() {
144 let tmp = tempfile::tempdir().unwrap();
145 let path = tmp.path().join("config.toml");
146 let cfg = Config {
147 mount_base: "/tmp/vol".into(),
148 mount_options: vec!["noowners".into()],
149 require_confirmation: false,
150 ..Default::default()
151 };
152 save_config(&cfg, &path).unwrap();
153 let loaded = load_config(&path).unwrap();
154 assert_eq!(loaded.mount_base, "/tmp/vol");
155 assert_eq!(loaded.mount_options, vec!["noowners".to_string()]);
156 assert!(!loaded.require_confirmation);
157 }
158
159 #[test]
160 fn missing_file_returns_defaults() {
161 let cfg = load_config(std::path::Path::new("/nonexistent/path/config.toml")).unwrap();
162 assert_eq!(cfg.mount_base, "/Volumes");
163 }
164
165 #[test]
166 fn unknown_fields_are_ignored() {
167 let tmp = tempfile::tempdir().unwrap();
168 let path = tmp.path().join("config.toml");
169 std::fs::write(
170 &path,
171 r#"
172 mount_base = "/Volumes"
173 totally_unknown_field = 42
174 "#,
175 )
176 .unwrap();
177 let cfg = load_config(&path).unwrap();
178 assert_eq!(cfg.mount_base, "/Volumes");
179 }
180}