qframe/storage/preferences.rs
1//! The preferences every application of a family shares: language, theme and icons.
2//!
3//! The family's shared file holds one value of each, and each application's own file either
4//! names its own value or the family's id, which means "use the shared one":
5//!
6//! ```toml
7//! # quvyta.conf
8//! language = "tr"
9//! theme = "monochrome"
10//! icons = "nerd"
11//!
12//! # code.conf
13//! language = "quvyta"
14//! theme = "nordic"
15//! ```
16//!
17//! [`Family::preferences`] resolves each key on its own, in this order:
18//!
19//! 1. the application's value, when it is anything but the family's id;
20//! 2. the shared file's value, when the application's value is the family's id or the key is
21//! missing from the application's file;
22//! 3. the value detected on this machine, when the shared file does not hold one either.
23//!
24//! [`Family::set`] changes one key for the whole family or for one application, and
25//! [`Family::follow`] puts one application back on the family's value without touching it.
26
27use std::fs;
28use std::io;
29use std::path::{Path, PathBuf};
30
31use super::{Family, SettingValue, Settings, atomic_write};
32use crate::diagnostics::{Diagnostic, Severity};
33use crate::i18n::I18n;
34use crate::icons::{GlyphMode, IconMode, default_font_dirs, detect_glyph_mode};
35use crate::runtime::Command;
36
37/// The theme a machine starts with: every theme is dark, so there is nothing to detect.
38const DETECTED_THEME: &str = "monochrome";
39
40/// The language when the system names none the application speaks.
41const FALLBACK_LANGUAGE: &str = "en";
42
43/// A preference every application of a family shares.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45pub enum Shared {
46 /// The language, a locale code such as `tr`.
47 Language,
48 /// The colour theme, a theme id such as `nordic`.
49 Theme,
50 /// The icon mode: `auto`, `nerd`, `unicode` or `ascii`.
51 Icons,
52}
53
54impl Shared {
55 /// Every shared preference, in the order a settings screen lists them.
56 pub const ALL: [Self; 3] = [Self::Language, Self::Theme, Self::Icons];
57
58 /// The key the preference is written under, in the shared file and in each application's.
59 #[must_use]
60 pub fn key(self) -> &'static str {
61 match self {
62 Self::Language => Settings::LANGUAGE,
63 Self::Theme => Settings::THEME,
64 Self::Icons => Settings::ICONS,
65 }
66 }
67}
68
69/// Where [`Family::set`] writes a change: the "In every Quvyta application" choice of a settings
70/// screen.
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
72pub enum Scope {
73 /// The shared file takes the value and the application follows it again, so every
74 /// application that follows the family changes with it. Applications that chose their own
75 /// value keep it.
76 Family,
77 /// Only the application's own file takes the value; the shared file is left alone.
78 App,
79}
80
81/// Where a resolved preference came from, so a settings screen can say "follows every Quvyta
82/// application" or "only here".
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
84pub enum Source {
85 /// The application's own file names it.
86 App,
87 /// The family's shared file holds it and the application follows it.
88 Family,
89 /// Neither file holds it; it was detected on this machine.
90 Detected,
91}
92
93/// A preference's value together with where it came from.
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct Resolved<T> {
96 /// The value to use.
97 pub value: T,
98 /// Where the value came from.
99 pub source: Source,
100}
101
102/// The shared preferences as one application sees them, from [`Family::preferences`].
103///
104/// ```
105/// use qframe::i18n::I18n;
106/// use qframe::storage::{Family, Source};
107///
108/// # let folder = std::env::temp_dir().join(format!("quvyta-preferences-doc-{}", std::process::id()));
109/// // An application calls `Family::QUVYTA.preferences("code", &i18n)`; the example keeps to a
110/// // folder of its own.
111/// let prefs = Family::QUVYTA.preferences_in(&folder, "code", &I18n::builtin());
112/// let theme = prefs.theme();
113/// assert_eq!((theme.value.as_str(), theme.source), ("monochrome", Source::Detected), "never detected");
114/// assert!(folder.join("quvyta.conf").is_file(), "the first start writes the shared file");
115/// # std::fs::remove_dir_all(&folder).ok();
116/// ```
117#[derive(Debug, Clone, PartialEq)]
118pub struct Preferences {
119 language: Resolved<String>,
120 theme: Resolved<String>,
121 icons: Resolved<IconMode>,
122 update_notice: bool,
123 diagnostics: Vec<Diagnostic>,
124}
125
126impl Preferences {
127 /// Whether the family's applications say when a newer version of themselves is out; see
128 /// [`Family::update_notice`]. One switch for the whole family, on unless it was turned off.
129 #[must_use]
130 pub fn update_notice(&self) -> bool {
131 self.update_notice
132 }
133
134 /// Records that the update notice is now `on`, after a change written with
135 /// [`Family::set_update_notice`].
136 pub(crate) fn record_update_notice(&mut self, on: bool) {
137 self.update_notice = on;
138 }
139
140 /// The locale code to speak.
141 #[must_use]
142 pub fn language(&self) -> &Resolved<String> {
143 &self.language
144 }
145
146 /// The theme id to draw with.
147 #[must_use]
148 pub fn theme(&self) -> &Resolved<String> {
149 &self.theme
150 }
151
152 /// The icon mode to draw with.
153 #[must_use]
154 pub fn icons(&self) -> &Resolved<IconMode> {
155 &self.icons
156 }
157
158 /// Where `key` came from.
159 #[must_use]
160 pub fn source(&self, key: Shared) -> Source {
161 match key {
162 Shared::Language => self.language.source,
163 Shared::Theme => self.theme.source,
164 Shared::Icons => self.icons.source,
165 }
166 }
167
168 /// Records that `key` now holds `value`, which came from `source`, after a change written
169 /// with [`Family::set`].
170 pub(crate) fn record(&mut self, key: Shared, value: &str, source: Source) {
171 match key {
172 Shared::Language => self.language = Resolved { value: value.to_owned(), source },
173 Shared::Theme => self.theme = Resolved { value: value.to_owned(), source },
174 Shared::Icons => {
175 let mode = IconMode::from_name(value).unwrap_or(self.icons.value);
176 self.icons = Resolved { value: mode, source };
177 }
178 }
179 }
180
181 /// The value of `key` as it is written in a file.
182 pub(crate) fn text(&self, key: Shared) -> String {
183 match key {
184 Shared::Language => self.language.value.clone(),
185 Shared::Theme => self.theme.value.clone(),
186 Shared::Icons => self.icons.value.name().to_owned(),
187 }
188 }
189
190 /// Problems found on the way: a broken line in the shared file, a shared file that could not
191 /// be written. Each is located where a file is to blame; the key it concerns fell back to the
192 /// detected value. Problems in the application's own file are reported by the application's
193 /// own [`Settings`] load and are not repeated here.
194 #[must_use]
195 pub fn diagnostics(&self) -> &[Diagnostic] {
196 &self.diagnostics
197 }
198
199 /// Commands that switch the running application to the resolved language, theme and icons,
200 /// for use after a change in `update`. At start give the preferences to
201 /// [`Runtime::preferences`](crate::runtime::Runtime::preferences) instead, so the first
202 /// frame is already drawn with them.
203 #[must_use]
204 pub fn apply<Msg: Send + 'static>(&self) -> Command<Msg> {
205 Command::batch([
206 Command::set_theme(self.theme.value.clone()),
207 Command::set_locale(self.language.value.clone()),
208 Command::set_icon_mode(self.icons.value),
209 ])
210 }
211}
212
213/// What a resolution does about a shared file that is not there.
214#[derive(Debug, Clone, Copy, PartialEq, Eq)]
215enum Missing {
216 /// Write it with the detected values, so the next application finds them.
217 Create,
218 /// Leave it missing and detect its keys, so nothing is written before a setup wizard finishes.
219 Leave,
220}
221
222/// What this machine would choose for each shared preference.
223struct Detected {
224 language: String,
225 icons: IconMode,
226}
227
228impl Detected {
229 fn on_this_machine(i18n: &I18n, lookup: impl Fn(&str) -> Option<String>, font_dirs: &[PathBuf]) -> Self {
230 let language = i18n.detect(&lookup).unwrap_or_else(|| FALLBACK_LANGUAGE.to_owned());
231 let icons = match detect_glyph_mode(IconMode::Auto, &lookup, font_dirs) {
232 GlyphMode::Nerd => IconMode::Nerd,
233 GlyphMode::Unicode => IconMode::Unicode,
234 GlyphMode::Ascii => IconMode::Ascii,
235 };
236 Self { language, icons }
237 }
238
239 /// The detected value of `key` as it is written in a file.
240 fn text(&self, key: Shared) -> String {
241 match key {
242 Shared::Language => self.language.clone(),
243 Shared::Theme => DETECTED_THEME.to_owned(),
244 Shared::Icons => self.icons.name().to_owned(),
245 }
246 }
247
248 /// The shared file as it is first written: the detected value of every key.
249 fn file(&self) -> String {
250 let mut settings = Settings::in_memory();
251 for key in Shared::ALL {
252 settings.set(key.key(), self.text(key));
253 }
254 settings.to_toml()
255 }
256}
257
258impl Family {
259 /// Resolves the shared preferences of application `app`: for each of language, theme and
260 /// icons, the application's own value when its file names one other than the family's id,
261 /// else the value in the [shared file](Self::shared_file), else the value detected on this
262 /// machine. A key missing from the application's file follows the family, as the family's id
263 /// does, so a file written by hand before the family shared anything follows it too.
264 ///
265 /// Detection reads the environment: the language as [`I18n::detect`] finds it among the
266 /// languages `i18n` knows (English when it knows none of the system's), the theme always
267 /// `monochrome`, the icons as the strongest set the terminal and the installed fonts allow
268 /// ([`detect_glyph_mode`]).
269 ///
270 /// When the shared file does not exist it is created with the detected values, so the next
271 /// application that starts finds them. A broken line never stops anything: that key falls
272 /// back to the detected value and the reason, located at file, line and column, is in
273 /// [`Preferences::diagnostics`]. Without a home folder nothing is read or written and every
274 /// value is detected.
275 ///
276 /// The rest of the application's settings are read as before, with
277 /// [`Settings::load_member`]; this only resolves the keys [`Shared`] names.
278 #[must_use]
279 pub fn preferences(&self, app: &str, i18n: &I18n) -> Preferences {
280 let lookup = |name: &str| std::env::var(name).ok();
281 let font_dirs = default_font_dirs(lookup);
282 let detected = Detected::on_this_machine(i18n, lookup, &font_dirs);
283 match self.config_dir() {
284 Some(dir) => self.resolve(&dir, app, &detected, Missing::Create),
285 None => {
286 let mut prefs = resolved_from(&detected, |_| None, |_| None);
287 prefs
288 .diagnostics
289 .push(Diagnostic::warning(None, "no config directory found; preferences are not saved"));
290 prefs
291 }
292 }
293 }
294
295 /// [`preferences`](Self::preferences) with `config_dir` as the family's folder instead of
296 /// this platform's, for a test or a demo that must leave the user's own files alone.
297 #[must_use]
298 pub fn preferences_in(&self, config_dir: &Path, app: &str, i18n: &I18n) -> Preferences {
299 let lookup = |name: &str| std::env::var(name).ok();
300 let detected = Detected::on_this_machine(i18n, lookup, &default_font_dirs(lookup));
301 self.resolve(config_dir, app, &detected, Missing::Create)
302 }
303
304 /// [`preferences`](Self::preferences) without writing anything: a missing shared file is left
305 /// missing and its keys are detected instead.
306 ///
307 /// For an application whose first start shows a [setup wizard](crate::widgets::Setup): a
308 /// wizard closed half-way leaves the user's settings folder as empty as it found it, and the
309 /// wizard's Finish writes both files. An application without a wizard uses
310 /// [`preferences`](Self::preferences), so the first application to start leaves the shared
311 /// file for the next one.
312 #[must_use]
313 pub fn preferences_without_saving(&self, app: &str, i18n: &I18n) -> Preferences {
314 let lookup = |name: &str| std::env::var(name).ok();
315 let font_dirs = default_font_dirs(lookup);
316 let detected = Detected::on_this_machine(i18n, lookup, &font_dirs);
317 match self.config_dir() {
318 Some(dir) => self.resolve(&dir, app, &detected, Missing::Leave),
319 None => resolved_from(&detected, |_| None, |_| None),
320 }
321 }
322
323 /// [`preferences_without_saving`](Self::preferences_without_saving) with `config_dir` as the
324 /// family's folder instead of this platform's, for a test or a demo.
325 #[must_use]
326 pub fn preferences_without_saving_in(&self, config_dir: &Path, app: &str, i18n: &I18n) -> Preferences {
327 let lookup = |name: &str| std::env::var(name).ok();
328 let detected = Detected::on_this_machine(i18n, lookup, &default_font_dirs(lookup));
329 self.resolve(config_dir, app, &detected, Missing::Leave)
330 }
331
332 /// [`preferences_in`](Self::preferences_in) with the machine's detection read through
333 /// `lookup` and `font_dirs` instead of the process environment, for tests.
334 #[cfg(test)]
335 fn preferences_detecting(
336 &self,
337 config_dir: &Path,
338 app: &str,
339 i18n: &I18n,
340 lookup: impl Fn(&str) -> Option<String>,
341 ) -> Preferences {
342 self.resolve(config_dir, app, &Detected::on_this_machine(i18n, lookup, &[]), Missing::Create)
343 }
344
345 /// Changes shared preference `key` of application `app` to `value`, for the whole family or
346 /// for the application alone:
347 ///
348 /// | `scope` | shared file | application's file |
349 /// |---|---|---|
350 /// | [`Scope::Family`] | `key = value` | `key = "<family id>"` |
351 /// | [`Scope::App`] | unchanged | `key = value` |
352 ///
353 /// Each file is read from disk right before it is written and only `key` changes in it, so
354 /// two applications changing preferences at the same time both keep their change instead
355 /// of one writing back what it read earlier. On Unix systems the family's folder is held
356 /// with an advisory lock from the reading to the writing, so even two changes in the same
357 /// instant follow one another; elsewhere the window between them is a few microseconds. Files are written with [`atomic_write`]; the
358 /// other keys stay as they were, though comments do not survive, as with
359 /// [`Settings::save`]. The running application is not switched; use
360 /// [`Preferences::apply`] or the matching [`Command`] for that.
361 ///
362 /// # Errors
363 ///
364 /// Returns an error of kind [`io::ErrorKind::InvalidInput`] when `value` is not a valid
365 /// value of `key` (an unknown icon mode, the family's own id, an empty text), of kind
366 /// [`io::ErrorKind::NotFound`] when there is no home folder, and any error from writing.
367 pub fn set(&self, app: &str, key: Shared, value: &str, scope: Scope) -> io::Result<()> {
368 match self.config_dir() {
369 Some(dir) => self.set_in(&dir, app, key, value, scope),
370 None => Err(io::Error::new(io::ErrorKind::NotFound, "no config directory found")),
371 }
372 }
373
374 /// [`set`](Self::set) with `config_dir` as the family's folder instead of this platform's.
375 ///
376 /// # Errors
377 ///
378 /// As [`set`](Self::set), except that there is always a folder.
379 pub fn set_in(&self, config_dir: &Path, app: &str, key: Shared, value: &str, scope: Scope) -> io::Result<()> {
380 let value = self.checked(key, value)?;
381 fs::create_dir_all(config_dir)?;
382 let _held = hold_folder(config_dir)?;
383 let app_file = config_dir.join(super::family::file_name(app));
384 match scope {
385 Scope::Family => {
386 let shared_file = config_dir.join(super::family::file_name(self.id()));
387 rewrite(&shared_file, key.key(), SettingValue::Text(value), None)?;
388 rewrite(&app_file, key.key(), SettingValue::Text(self.id().to_owned()), Some(self))
389 }
390 Scope::App => rewrite(&app_file, key.key(), SettingValue::Text(value), Some(self)),
391 }
392 }
393
394 /// Puts application `app` back on the family's value of `key`: its own file says the family's
395 /// id and the [shared file](Self::shared_file) is neither read nor written, so the next
396 /// resolution answers the shared value with [`Source::Family`] and no other application
397 /// changes. The one way back from a value of an application's own, for the settings screen
398 /// that lists every member of the family: "follow the shared setting" on one member's cell
399 /// must not change what the whole family draws with.
400 ///
401 /// The file is read from disk right before it is written and only `key` changes in it, as
402 /// [`set`](Self::set) does, with the family's folder held by an advisory lock on Unix. A
403 /// missing file is created holding that one key. A key that already follows the family is
404 /// left alone, file and all. Comments do not survive a change, as with [`Settings::save`].
405 /// The running application is not switched; use [`Preferences::apply`] for that.
406 ///
407 /// # Errors
408 ///
409 /// Returns an error of kind [`io::ErrorKind::NotFound`] when there is no home folder, of kind
410 /// [`io::ErrorKind::InvalidData`] when the application's file cannot be read as settings,
411 /// and any error from writing. A file that could not be read is left exactly as it was.
412 pub fn follow(&self, app: &str, key: Shared) -> io::Result<()> {
413 match self.config_dir() {
414 Some(dir) => self.follow_in(&dir, app, key),
415 None => Err(io::Error::new(io::ErrorKind::NotFound, "no config directory found")),
416 }
417 }
418
419 /// [`follow`](Self::follow) with `config_dir` as the family's folder instead of this
420 /// platform's, for a test or a demo that must leave the user's own files alone.
421 ///
422 /// ```
423 /// use qframe::storage::{Family, Shared};
424 ///
425 /// # let folder = std::env::temp_dir().join(format!("quvyta-follow-doc-{}", std::process::id()));
426 /// # std::fs::create_dir_all(&folder).expect("folder");
427 /// std::fs::write(folder.join("code.conf"), "theme = \"amber\"\n").expect("the file");
428 /// Family::QUVYTA.follow_in(&folder, "code", Shared::Theme).expect("follow");
429 /// assert_eq!(std::fs::read_to_string(folder.join("code.conf")).expect("read"), "theme = \"quvyta\"\n");
430 /// assert!(!folder.join("quvyta.conf").exists(), "the shared file is left alone");
431 /// # std::fs::remove_dir_all(&folder).ok();
432 /// ```
433 ///
434 /// # Errors
435 ///
436 /// As [`follow`](Self::follow), except that there is always a folder.
437 pub fn follow_in(&self, config_dir: &Path, app: &str, key: Shared) -> io::Result<()> {
438 fs::create_dir_all(config_dir)?;
439 let _held = hold_folder(config_dir)?;
440 let path = config_dir.join(super::family::file_name(app));
441 let mut settings = Settings::open(&path).member_of(self);
442 if let Some(problem) = settings.diagnostics().iter().find(|problem| problem.severity == Severity::Error) {
443 return Err(io::Error::new(io::ErrorKind::InvalidData, problem.to_string()));
444 }
445 let value = SettingValue::Text(self.id().to_owned());
446 if settings.value(key.key()) == Some(&value) && path.exists() {
447 return Ok(());
448 }
449 settings.store(key.key(), value);
450 settings.save()
451 }
452
453 /// Changes `key` in application `app`'s own file in `config_dir` to `value`, the way
454 /// [`set_in`](Self::set_in) changes a shared key: read right before writing, only that key,
455 /// with the folder held. For the application's settings that sit beside the shared ones on an
456 /// appearance screen, such as reduced motion.
457 pub(crate) fn set_own_in(&self, config_dir: &Path, app: &str, key: &str, value: SettingValue) -> io::Result<()> {
458 fs::create_dir_all(config_dir)?;
459 let _held = hold_folder(config_dir)?;
460 rewrite(&config_dir.join(super::family::file_name(app)), key, value, Some(self))
461 }
462
463 /// `value` as it is written under `key`, or why it cannot be.
464 fn checked(&self, key: Shared, value: &str) -> io::Result<String> {
465 let invalid = |why: String| io::Error::new(io::ErrorKind::InvalidInput, why);
466 let value = value.trim();
467 if value.is_empty() {
468 return Err(invalid(format!("`{}` cannot be empty", key.key())));
469 }
470 if value == self.id() {
471 return Err(invalid(format!("`{}` cannot be set to the family's own id `{value}`", key.key())));
472 }
473 match key {
474 Shared::Icons => IconMode::from_name(value)
475 .map(|mode| mode.name().to_owned())
476 .ok_or_else(|| invalid(format!("`{value}` is not an icon mode; use auto, nerd, unicode or ascii"))),
477 Shared::Language | Shared::Theme => Ok(value.to_owned()),
478 }
479 }
480
481 /// Resolves the preferences of `app` from the files in `config_dir`, creating the shared file
482 /// with the `detected` values when it is missing and `missing` says to.
483 fn resolve(&self, config_dir: &Path, app: &str, detected: &Detected, missing: Missing) -> Preferences {
484 let mut diagnostics = Vec::new();
485 let shared_path = config_dir.join(super::family::file_name(self.id()));
486 let shared = if shared_path.exists() {
487 let shared = Settings::open(&shared_path);
488 diagnostics.extend(shared.diagnostics().iter().cloned());
489 Some(shared)
490 } else {
491 if missing == Missing::Create
492 && let Err(error) = create(&shared_path, &detected.file())
493 {
494 diagnostics.push(Diagnostic::error(
495 None,
496 format!("{}: shared preferences not saved: {error}", shared_path.display()),
497 ));
498 }
499 None
500 };
501 let own = Settings::open(config_dir.join(super::family::file_name(app))).member_of(self);
502 let family_value = |key: Shared| -> Option<String> {
503 let shared = shared.as_ref()?;
504 let text = shared.get::<String>(key.key()).filter(|text| valid(key, text))?;
505 (text != self.id()).then_some(text)
506 };
507 let app_value = |key: Shared| -> Option<String> {
508 own.get::<String>(key.key()).filter(|text| text != self.id() && valid(key, text))
509 };
510 if let Some(shared) = &shared {
511 for key in Shared::ALL {
512 if shared.get::<String>(key.key()).is_some_and(|text| text == self.id()) {
513 diagnostics.push(Diagnostic::warning(
514 shared.origin(key.key()),
515 format!(
516 "`{}` cannot follow the family in the family's own file; the detected value is used",
517 key.key()
518 ),
519 ));
520 }
521 }
522 }
523 let mut prefs = resolved_from(detected, app_value, family_value);
524 prefs.update_notice = shared.as_ref().is_none_or(super::update_notice::from_shared);
525 prefs.diagnostics = diagnostics;
526 prefs
527 }
528}
529
530/// Whether `text` is a usable value of `key`; what is not was already reported by the settings
531/// load that read it.
532fn valid(key: Shared, text: &str) -> bool {
533 match key {
534 Shared::Icons => IconMode::from_name(text).is_some(),
535 Shared::Language | Shared::Theme => !text.trim().is_empty(),
536 }
537}
538
539/// Each key from the application's value, the family's value or the detected one, in that order.
540fn resolved_from(
541 detected: &Detected,
542 app_value: impl Fn(Shared) -> Option<String>,
543 family_value: impl Fn(Shared) -> Option<String>,
544) -> Preferences {
545 let text = |key: Shared| -> Resolved<String> {
546 if let Some(value) = app_value(key) {
547 Resolved { value, source: Source::App }
548 } else if let Some(value) = family_value(key) {
549 Resolved { value, source: Source::Family }
550 } else {
551 Resolved { value: detected.text(key), source: Source::Detected }
552 }
553 };
554 let icons = text(Shared::Icons);
555 Preferences {
556 language: text(Shared::Language),
557 theme: text(Shared::Theme),
558 icons: Resolved { value: IconMode::from_name(&icons.value).unwrap_or(detected.icons), source: icons.source },
559 update_notice: true,
560 diagnostics: Vec::new(),
561 }
562}
563
564/// Holds the family's folder with an advisory lock while it lives, so no other writer reads a file
565/// between this writer's reading and writing it. Locking the folder rather than a file of its own
566/// leaves nothing behind in the user's settings. Unix only: elsewhere the framework has no
567/// advisory lock, as for [`AppLock`](super::AppLock).
568#[cfg(unix)]
569pub(super) fn hold_folder(dir: &Path) -> io::Result<Option<fs::File>> {
570 let folder = fs::File::open(dir)?;
571 folder.lock()?;
572 Ok(Some(folder))
573}
574
575#[cfg(not(unix))]
576pub(super) fn hold_folder(_dir: &Path) -> io::Result<Option<fs::File>> {
577 Ok(None)
578}
579
580/// Writes the first shared file, creating its folder.
581fn create(path: &Path, text: &str) -> io::Result<()> {
582 if let Some(dir) = path.parent() {
583 fs::create_dir_all(dir)?;
584 }
585 atomic_write(path, text.as_bytes())
586}
587
588/// Reads the file at `path` as it is on disk now, changes only `key` to `value` and writes it
589/// back. `family` marks an application's file, whose own values follow the family.
590pub(super) fn rewrite(path: &Path, key: &str, value: SettingValue, family: Option<&Family>) -> io::Result<()> {
591 let mut settings = Settings::open(path);
592 if let Some(family) = family {
593 settings = settings.member_of(family);
594 }
595 if settings.value(key) == Some(&value) && path.exists() {
596 return Ok(());
597 }
598 settings.store(key, value);
599 settings.save()
600}
601
602#[cfg(test)]
603#[path = "preferences_tests.rs"]
604mod tests;