Skip to main content

qframe/storage/
preferences.rs

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