1use std::path::PathBuf;
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
9#[serde(rename_all = "snake_case")]
10pub enum SourceSide {
11 Base,
12 Target,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
16#[serde(rename_all = "snake_case")]
17pub enum DeleteBehavior {
18 Trash,
19 Delete,
20 Ignore,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct DriveId {
25 pub label: Option<String>,
26 pub uuid: Option<String>,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct SoundConfig {
31 pub sync_start: Option<PathBuf>,
32 pub sync_done: Option<PathBuf>,
33 pub sync_error: Option<PathBuf>,
34}
35
36impl Default for SoundConfig {
37 fn default() -> Self {
38 Self {
39 sync_start: None,
40 sync_done: None,
41 sync_error: None,
42 }
43 }
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct PairConfig {
48 pub name: String,
49 pub base: PathBuf,
50 pub target: PathBuf,
51 pub source: SourceSide,
52 pub drive_id: Option<DriveId>,
53 pub ignore: Vec<String>,
54 pub delete_behavior: DeleteBehavior,
55 pub sounds: SoundConfig,
56 pub created_at: DateTime<Utc>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct AppConfig {
61 pub version: u32,
62 pub pairs: Vec<PairConfig>,
63}
64
65impl Default for AppConfig {
66 fn default() -> Self {
67 Self {
68 version: 1,
69 pairs: Vec::new(),
70 }
71 }
72}
73
74pub fn get_config_path() -> Result<PathBuf, String> {
77 config_path()
78}
79
80pub fn reset_config() -> Result<(), String> {
81 let path = config_path()?;
82 if path.exists() {
83 std::fs::remove_file(&path)
84 .map_err(|e| format!("Failed to delete config: {}", e))?;
85 }
86 Ok(())
87}
88
89fn config_path() -> Result<PathBuf, String> {
90 let dir = dirs::config_dir()
91 .ok_or_else(|| "Could not determine config directory".to_string())?
92 .join("hard-sync");
93 Ok(dir.join("config.json"))
94}
95
96pub fn load_config() -> Result<AppConfig, String> {
99 let path = config_path()?;
100 if !path.exists() {
101 return Ok(AppConfig::default());
102 }
103 let raw = std::fs::read_to_string(&path)
104 .map_err(|e| format!("Failed to read config: {}", e))?;
105 serde_json::from_str(&raw)
106 .map_err(|e| format!("Failed to parse config: {}", e))
107}
108
109pub fn save_config(config: &AppConfig) -> Result<(), String> {
110 let path = config_path()?;
111 if let Some(parent) = path.parent() {
112 std::fs::create_dir_all(parent)
113 .map_err(|e| format!("Failed to create config directory: {}", e))?;
114 }
115 let raw = serde_json::to_string_pretty(config)
116 .map_err(|e| format!("Failed to serialize config: {}", e))?;
117 std::fs::write(&path, raw)
118 .map_err(|e| format!("Failed to write config: {}", e))
119}
120
121pub fn add_pair(pair: PairConfig) -> Result<(), String> {
124 let mut config = load_config()?;
125 if config.pairs.iter().any(|p| p.name == pair.name) {
126 return Err(format!("A pair named \"{}\" already exists", pair.name));
127 }
128 config.pairs.push(pair);
129 save_config(&config)
130}
131
132pub fn get_pair(name: &str) -> Result<PairConfig, String> {
133 let config = load_config()?;
134 config
135 .pairs
136 .into_iter()
137 .find(|p| p.name == name)
138 .ok_or_else(|| format!("No pair named \"{}\"", name))
139}
140
141pub fn list_pairs() -> Result<Vec<PairConfig>, String> {
142 let config = load_config()?;
143 Ok(config.pairs)
144}
145
146pub fn remove_pair(name: &str) -> Result<(), String> {
147 let mut config = load_config()?;
148 let before = config.pairs.len();
149 config.pairs.retain(|p| p.name != name);
150 if config.pairs.len() == before {
151 return Err(format!("No pair named \"{}\"", name));
152 }
153 save_config(&config)
154}
155
156pub fn set_source(name: &str, source: SourceSide) -> Result<(), String> {
157 let mut config = load_config()?;
158 let pair = config
159 .pairs
160 .iter_mut()
161 .find(|p| p.name == name)
162 .ok_or_else(|| format!("No pair named \"{}\"", name))?;
163 pair.source = source;
164 save_config(&config)
165}
166
167pub fn update_pair(updated: PairConfig) -> Result<(), String> {
168 let mut config = load_config()?;
169 let pair = config
170 .pairs
171 .iter_mut()
172 .find(|p| p.name == updated.name)
173 .ok_or_else(|| format!("No pair named \"{}\"", updated.name))?;
174 *pair = updated;
175 save_config(&config)
176}