1use std::fs;
5use std::io;
6use std::path::Path;
7
8use super::ecosystem::file_name;
9use super::preferences::hold_folder;
10use super::{Ecosystem, SettingValue, Settings, Shared};
11use crate::diagnostics::Severity;
12use crate::icons::IconMode;
13
14impl Ecosystem {
15 pub fn settle(&self, app: &str) -> io::Result<Vec<Shared>> {
55 match self.config_dir() {
56 Some(dir) => self.settle_in(&dir, app),
57 None => Err(io::Error::new(io::ErrorKind::NotFound, "no config directory found")),
58 }
59 }
60
61 pub fn settle_in(&self, config_dir: &Path, app: &str) -> io::Result<Vec<Shared>> {
82 fs::create_dir_all(config_dir)?;
83 let _held = hold_folder(config_dir)?;
84 let mut own = Settings::open(config_dir.join(file_name(app))).member_of(self);
85 if let Some(problem) = own.diagnostics().iter().find(|problem| problem.severity == Severity::Error) {
86 return Err(io::Error::new(io::ErrorKind::InvalidData, problem.to_string()));
87 }
88 if own.get::<bool>(Settings::SHARED_CHECKED) == Some(true) {
89 return Ok(Vec::new());
90 }
91 let shared_path = config_dir.join(file_name(self.id()));
92 let shared = shared_path.is_file().then(|| Settings::open(&shared_path));
93 let mut following = Vec::new();
94 for key in Shared::ALL {
95 let fixed = key.read(&own).filter(|text| *text != self.id());
96 let common = shared
97 .as_ref()
98 .and_then(|shared| key.read(shared))
99 .or_else(|| (key == Shared::ReducedMotion && shared.is_some()).then(|| false.to_string()));
100 if let (Some(fixed), Some(common)) = (fixed, common)
101 && common != self.id()
102 && same(key, &fixed, &common)
103 {
104 own.store(key.key(), SettingValue::Text(self.id().to_owned()));
105 following.push(key);
106 }
107 }
108 own.store(Settings::SHARED_CHECKED, SettingValue::Bool(true));
109 own.save()?;
110 Ok(following)
111 }
112}
113
114fn same(key: Shared, fixed: &str, common: &str) -> bool {
117 match key {
118 Shared::Icons => IconMode::from_name(fixed).is_some_and(|mode| IconMode::from_name(common) == Some(mode)),
119 Shared::ReducedMotion => fixed.parse::<bool>().is_ok_and(|reduced| common.parse() == Ok(reduced)),
120 Shared::Language | Shared::Theme => {
121 let fixed = fixed.trim();
122 !fixed.is_empty() && fixed == common.trim()
123 }
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use std::path::PathBuf;
130
131 use super::*;
132
133 fn folder(name: &str) -> PathBuf {
135 let path = std::env::temp_dir().join(format!("quvyta-settle-{name}-{}", std::process::id()));
136 let _ = fs::remove_dir_all(&path);
137 fs::create_dir_all(&path).expect("the folder");
138 path
139 }
140
141 fn read(folder: &Path, name: &str) -> String {
142 fs::read_to_string(folder.join(name)).unwrap_or_default()
143 }
144
145 const SHARED: &str = "language = \"tr\"\ntheme = \"nordic\"\nicons = \"unicode\"\n";
146
147 #[test]
148 fn a_value_equal_to_the_shared_one_follows_the_ecosystem() {
149 let folder = folder("equal");
150 fs::write(folder.join("quvyta.conf"), SHARED).expect("shared");
151 fs::write(
152 folder.join("desktop.conf"),
153 "language = \"tr\"\ntheme = \"nordic\"\nicons = \"unicode\"\ndock = 3\n",
154 )
155 .expect("own");
156 let following = Ecosystem::QUVYTA.settle_in(&folder, "desktop").expect("settle");
157 assert_eq!(following, [Shared::Language, Shared::Theme, Shared::Icons]);
158 assert_eq!(
159 read(&folder, "desktop.conf"),
160 "language = \"quvyta\"\ntheme = \"quvyta\"\nicons = \"quvyta\"\ndock = 3\nshared-checked = true\n"
161 );
162 assert_eq!(read(&folder, "quvyta.conf"), SHARED, "the shared file is only read");
163 let _ = fs::remove_dir_all(&folder);
164 }
165
166 #[test]
167 fn a_value_other_than_the_shared_one_stays() {
168 let folder = folder("different");
169 fs::write(folder.join("quvyta.conf"), SHARED).expect("shared");
170 fs::write(folder.join("code.conf"), "language = \"en\"\ntheme = \"nordic\"\nicons = \"nerd\"\n").expect("own");
171 let following = Ecosystem::QUVYTA.settle_in(&folder, "code").expect("settle");
172 assert_eq!(following, [Shared::Theme]);
173 let own = read(&folder, "code.conf");
174 assert!(own.contains("language = \"en\""), "the person's language stays: {own}");
175 assert!(own.contains("icons = \"nerd\""), "and so do the icons: {own}");
176 assert!(own.contains("theme = \"quvyta\""), "{own}");
177 let _ = fs::remove_dir_all(&folder);
178 }
179
180 #[test]
181 fn reduced_motion_kept_off_follows_and_reduced_stays_when_the_shared_file_never_held_it() {
182 let folder = folder("motion");
183 fs::write(folder.join("quvyta.conf"), SHARED).expect("shared");
184 fs::write(folder.join("code.conf"), "reduced-motion = false\n").expect("code");
185 fs::write(folder.join("focus.conf"), "reduced-motion = true\n").expect("focus");
186 assert_eq!(Ecosystem::QUVYTA.settle_in(&folder, "code").expect("code"), [Shared::ReducedMotion]);
187 assert_eq!(read(&folder, "code.conf"), "reduced-motion = \"quvyta\"\nshared-checked = true\n");
188 assert!(Ecosystem::QUVYTA.settle_in(&folder, "focus").expect("focus").is_empty());
189 assert!(read(&folder, "focus.conf").contains("reduced-motion = true"), "the person's need stays");
190 fs::write(folder.join("quvyta.conf"), format!("{SHARED}reduced-motion = true\n")).expect("shared");
192 fs::write(folder.join("focus.conf"), "reduced-motion = true\n").expect("focus");
193 assert_eq!(Ecosystem::QUVYTA.settle_in(&folder, "focus").expect("again"), [Shared::ReducedMotion]);
194 let _ = fs::remove_dir_all(&folder);
195 }
196
197 #[test]
198 fn the_second_run_changes_nothing() {
199 let folder = folder("twice");
200 fs::write(folder.join("quvyta.conf"), SHARED).expect("shared");
201 fs::write(folder.join("code.conf"), "theme = \"iris\"\n").expect("own");
202 assert!(Ecosystem::QUVYTA.settle_in(&folder, "code").expect("first").is_empty());
203 let mut own = Settings::open(folder.join("code.conf")).member_of(&Ecosystem::QUVYTA);
205 own.set("theme", "nordic".to_owned());
206 own.save().expect("the choice");
207 let before = read(&folder, "code.conf");
208 assert!(Ecosystem::QUVYTA.settle_in(&folder, "code").expect("second").is_empty());
209 assert_eq!(read(&folder, "code.conf"), before, "the choice is not undone");
210 assert!(before.contains("theme = \"nordic\""), "{before}");
211 let _ = fs::remove_dir_all(&folder);
212 }
213
214 #[test]
215 fn a_missing_file_takes_only_the_mark() {
216 let folder = folder("missing");
217 fs::write(folder.join("quvyta.conf"), SHARED).expect("shared");
218 assert!(Ecosystem::QUVYTA.settle_in(&folder, "code").expect("settle").is_empty());
219 assert_eq!(read(&folder, "code.conf"), "shared-checked = true\n");
220 let bare = folder.join("bare");
221 assert!(Ecosystem::QUVYTA.settle_in(&bare, "code").expect("no shared file either").is_empty());
222 assert_eq!(read(&bare, "code.conf"), "shared-checked = true\n");
223 let _ = fs::remove_dir_all(&folder);
224 }
225
226 #[test]
227 fn a_broken_file_is_left_as_it_was() {
228 let folder = folder("broken");
229 fs::write(folder.join("quvyta.conf"), SHARED).expect("shared");
230 fs::write(folder.join("code.conf"), "theme = \nicons = \"nerd\"\n").expect("own");
231 let error = Ecosystem::QUVYTA.settle_in(&folder, "code").expect_err("broken");
232 assert_eq!(error.kind(), io::ErrorKind::InvalidData);
233 assert_eq!(read(&folder, "code.conf"), "theme = \nicons = \"nerd\"\n");
234 let _ = fs::remove_dir_all(&folder);
235 }
236
237 #[test]
238 fn the_mark_survives_a_member_s_self_healing() {
239 let text = "theme = \"nordic\"\nshared-checked = true\n";
240 let healed = Settings::parse_str("code.conf", text)
241 .member_of(&Ecosystem::QUVYTA)
242 .schema(super::super::Schema::builtin())
243 .self_heal(true);
244 assert_eq!(healed.get::<bool>(Settings::SHARED_CHECKED), Some(true));
245 assert!(healed.diagnostics().is_empty(), "{:?}", healed.diagnostics());
246 }
247}