cosmic_config/
lib.rs

1//! Integrations for cosmic-config — the cosmic configuration system.
2
3use notify::{
4    event::{EventKind, ModifyKind},
5    Watcher,
6};
7use serde::{de::DeserializeOwned, Serialize};
8use std::{
9    fmt, fs,
10    io::Write,
11    path::{Path, PathBuf},
12    sync::Mutex,
13};
14
15#[cfg(feature = "subscription")]
16mod subscription;
17#[cfg(feature = "subscription")]
18pub use subscription::*;
19
20#[cfg(all(feature = "dbus", feature = "subscription"))]
21pub mod dbus;
22
23#[cfg(feature = "macro")]
24pub use cosmic_config_derive;
25
26#[cfg(feature = "calloop")]
27pub mod calloop;
28
29#[derive(Debug)]
30pub enum Error {
31    AtomicWrites(atomicwrites::Error<std::io::Error>),
32    InvalidName(String),
33    Io(std::io::Error),
34    NoConfigDirectory,
35    Notify(notify::Error),
36    NotFound,
37    Ron(ron::Error),
38    RonSpanned(ron::error::SpannedError),
39    GetKey(String, std::io::Error),
40}
41
42impl fmt::Display for Error {
43    #[cold]
44    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
45        match self {
46            Self::AtomicWrites(err) => err.fmt(f),
47            Self::InvalidName(name) => write!(f, "invalid config name '{}'", name),
48            Self::Io(err) => err.fmt(f),
49            Self::NoConfigDirectory => write!(f, "cosmic config directory not found"),
50            Self::Notify(err) => err.fmt(f),
51            Self::NotFound => write!(f, "cosmic config key not configured"),
52            Self::Ron(err) => err.fmt(f),
53            Self::RonSpanned(err) => err.fmt(f),
54            Self::GetKey(key, err) => write!(f, "failed to get key '{}': {}", key, err),
55        }
56    }
57}
58
59impl std::error::Error for Error {}
60
61impl Error {
62    /// Whether the reason for the missing config is caused by an error.
63    ///
64    /// Useful for determining if it is appropriate to log as an error.
65    #[inline]
66    pub fn is_err(&self) -> bool {
67        !matches!(self, Self::NoConfigDirectory | Self::NotFound)
68    }
69}
70
71impl From<atomicwrites::Error<std::io::Error>> for Error {
72    fn from(f: atomicwrites::Error<std::io::Error>) -> Self {
73        Self::AtomicWrites(f)
74    }
75}
76
77impl From<std::io::Error> for Error {
78    fn from(f: std::io::Error) -> Self {
79        Self::Io(f)
80    }
81}
82
83impl From<notify::Error> for Error {
84    fn from(f: notify::Error) -> Self {
85        Self::Notify(f)
86    }
87}
88
89impl From<ron::Error> for Error {
90    fn from(f: ron::Error) -> Self {
91        Self::Ron(f)
92    }
93}
94
95impl From<ron::error::SpannedError> for Error {
96    fn from(f: ron::error::SpannedError) -> Self {
97        Self::RonSpanned(f)
98    }
99}
100
101pub trait ConfigGet {
102    /// Get a configuration value
103    ///
104    /// Fallback to the system default if a local user override is not defined.
105    fn get<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error>;
106
107    /// Get a locally-defined configuration value from the user's local config.
108    fn get_local<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error>;
109
110    /// Get the system-defined default configuration value.
111    fn get_system_default<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error>;
112}
113
114pub trait ConfigSet {
115    /// Set a configuration value
116    fn set<T: Serialize>(&self, key: &str, value: T) -> Result<(), Error>;
117}
118
119#[derive(Clone, Debug)]
120pub struct Config {
121    system_path: Option<PathBuf>,
122    user_path: Option<PathBuf>,
123}
124
125/// Check that the name is relative and doesn't contain . or ..
126fn sanitize_name(name: &str) -> Result<&Path, Error> {
127    let path = Path::new(name);
128    if path
129        .components()
130        .all(|x| matches!(x, std::path::Component::Normal(_)))
131    {
132        Ok(path)
133    } else {
134        Err(Error::InvalidName(name.to_owned()))
135    }
136}
137
138impl Config {
139    /// Get a system config for the given name and config version
140    pub fn system(name: &str, version: u64) -> Result<Self, Error> {
141        let path = sanitize_name(name)?.join(format!("v{version}"));
142        #[cfg(unix)]
143        let system_path = xdg::BaseDirectories::with_prefix("cosmic")
144            .map_err(std::io::Error::from)?
145            .find_data_file(path);
146
147        #[cfg(windows)]
148        let system_path =
149            known_folders::get_known_folder_path(known_folders::KnownFolder::ProgramFilesCommon)
150                .map(|x| x.join("COSMIC").join(&path));
151
152        Ok(Self {
153            system_path,
154            user_path: None,
155        })
156    }
157
158    /// Get config for the given application name and config version
159    // Use folder at XDG config/name for config storage, return Config if successful
160    //TODO: fallbacks for flatpak (HOST_XDG_CONFIG_HOME, xdg-desktop settings proxy)
161    pub fn new(name: &str, version: u64) -> Result<Self, Error> {
162        // Look for [name]/v[version]
163        let path = sanitize_name(name)?.join(format!("v{}", version));
164
165        // Search data file, which provides default (e.g. /usr/share)
166        #[cfg(unix)]
167        let system_path = xdg::BaseDirectories::with_prefix("cosmic")
168            .map_err(std::io::Error::from)?
169            .find_data_file(&path);
170
171        #[cfg(windows)]
172        let system_path =
173            known_folders::get_known_folder_path(known_folders::KnownFolder::ProgramFilesCommon)
174                .map(|x| x.join("COSMIC").join(&path));
175
176        // Get libcosmic user configuration directory
177        let cosmic_user_path = dirs::config_dir()
178            .ok_or(Error::NoConfigDirectory)?
179            .join("cosmic");
180
181        let user_path = cosmic_user_path.join(path);
182        // Create new configuration directory if not found.
183        fs::create_dir_all(&user_path)?;
184
185        // Return Config
186        Ok(Self {
187            system_path,
188            user_path: Some(user_path),
189        })
190    }
191
192    /// Get config for the given application name and config version and custom path.
193    pub fn with_custom_path(name: &str, version: u64, custom_path: PathBuf) -> Result<Self, Error> {
194        // Look for [name]/v[version]
195        let path = sanitize_name(name)?.join(format!("v{version}"));
196
197        let cosmic_user_path = custom_path.join("cosmic");
198
199        let user_path = cosmic_user_path.join(path);
200        // Create new configuration directory if not found.
201        fs::create_dir_all(&user_path)?;
202
203        // Return Config
204        Ok(Self {
205            system_path: None,
206            user_path: Some(user_path),
207        })
208    }
209
210    /// Get state for the given application name and config version. State is meant to be used to
211    /// store items that may need to be exposed to other programs but will change regularly without
212    /// user action
213    // Use folder at XDG config/name for config storage, return Config if successful
214    //TODO: fallbacks for flatpak (HOST_XDG_CONFIG_HOME, xdg-desktop settings proxy)
215    pub fn new_state(name: &str, version: u64) -> Result<Self, Error> {
216        // Look for [name]/v[version]
217        let path = sanitize_name(name)?.join(format!("v{}", version));
218
219        // Get libcosmic user state directory
220        let cosmic_user_path = dirs::state_dir()
221            .ok_or(Error::NoConfigDirectory)?
222            .join("cosmic");
223
224        let user_path = cosmic_user_path.join(path);
225        // Create new state directory if not found.
226        fs::create_dir_all(&user_path)?;
227
228        Ok(Self {
229            system_path: None,
230            user_path: Some(user_path),
231        })
232    }
233
234    // Start a transaction (to set multiple configs at the same time)
235    #[inline]
236    pub fn transaction(&self) -> ConfigTransaction {
237        ConfigTransaction {
238            config: self,
239            updates: Mutex::new(Vec::new()),
240        }
241    }
242
243    // Watch keys for changes, will be triggered once per transaction
244    // This may end up being an mpsc channel instead of a function
245    // See EventHandler in the notify crate: https://docs.rs/notify/latest/notify/trait.EventHandler.html
246    // Having a callback allows for any application abstraction to be used
247    pub fn watch<F>(&self, f: F) -> Result<notify::RecommendedWatcher, Error>
248    // Argument is an array of all keys that changed in that specific transaction
249    //TODO: simplify F requirements
250    where
251        F: Fn(&Self, &[String]) + Send + Sync + 'static,
252    {
253        let watch_config = self.clone();
254        let Some(user_path) = self.user_path.as_ref() else {
255            return Err(Error::NoConfigDirectory);
256        };
257        let user_path_clone = user_path.clone();
258        let mut watcher =
259            notify::recommended_watcher(move |event_res: Result<notify::Event, notify::Error>| {
260                match &event_res {
261                    Ok(event) => {
262                        match &event.kind {
263                            EventKind::Access(_) | EventKind::Modify(ModifyKind::Metadata(_)) => {
264                                // Data not mutated
265                                return;
266                            }
267                            _ => {}
268                        }
269
270                        let mut keys = Vec::new();
271                        for path in &event.paths {
272                            match path.strip_prefix(&user_path_clone) {
273                                Ok(key_path) => {
274                                    if let Some(key) = key_path.to_str() {
275                                        // Skip any .atomicwrite temporary files
276                                        if key.starts_with(".atomicwrite") {
277                                            continue;
278                                        }
279                                        keys.push(key.to_string());
280                                    }
281                                }
282                                Err(_err) => {
283                                    //TODO: handle errors
284                                }
285                            }
286                        }
287                        if !keys.is_empty() {
288                            f(&watch_config, &keys);
289                        }
290                    }
291                    Err(_err) => {
292                        //TODO: handle errors
293                    }
294                }
295            })?;
296        watcher.watch(user_path, notify::RecursiveMode::NonRecursive)?;
297        Ok(watcher)
298    }
299
300    fn default_path(&self, key: &str) -> Result<PathBuf, Error> {
301        let Some(system_path) = self.system_path.as_ref() else {
302            return Err(Error::NoConfigDirectory);
303        };
304
305        Ok(system_path.join(sanitize_name(key)?))
306    }
307
308    /// Get the path of the key in the user's local config directory.
309    fn key_path(&self, key: &str) -> Result<PathBuf, Error> {
310        let Some(user_path) = self.user_path.as_ref() else {
311            return Err(Error::NoConfigDirectory);
312        };
313        Ok(user_path.join(sanitize_name(key)?))
314    }
315}
316
317// Getting any setting is available on a Config object
318impl ConfigGet for Config {
319    //TODO: check for transaction
320    fn get<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error> {
321        match self.get_local(key) {
322            Ok(value) => Ok(value),
323            Err(Error::NotFound) => self.get_system_default(key),
324            Err(why) => Err(why),
325        }
326    }
327
328    fn get_local<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error> {
329        // If key path exists
330        match self.key_path(key) {
331            Ok(key_path) if key_path.is_file() => {
332                // Load user override
333                let data = fs::read_to_string(key_path)
334                    .map_err(|err| Error::GetKey(key.to_string(), err))?;
335
336                Ok(ron::from_str(&data)?)
337            }
338
339            _ => Err(Error::NotFound),
340        }
341    }
342
343    fn get_system_default<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error> {
344        // Load system default
345        let default_path = self.default_path(key)?;
346        let data =
347            fs::read_to_string(default_path).map_err(|err| Error::GetKey(key.to_string(), err))?;
348        Ok(ron::from_str(&data)?)
349    }
350}
351
352// Setting any setting in this way will do one transaction per set call
353impl ConfigSet for Config {
354    fn set<T: Serialize>(&self, key: &str, value: T) -> Result<(), Error> {
355        // Wrap up single key/value sets in a transaction
356        let tx = self.transaction();
357        tx.set(key, value)?;
358        tx.commit()
359    }
360}
361
362#[must_use = "Config transaction must be committed"]
363pub struct ConfigTransaction<'a> {
364    config: &'a Config,
365    //TODO: use map?
366    updates: Mutex<Vec<(PathBuf, String)>>,
367}
368
369impl ConfigTransaction<'_> {
370    /// Apply all pending changes from ConfigTransaction
371    //TODO: apply all changes at once
372    pub fn commit(self) -> Result<(), Error> {
373        let mut updates = self.updates.lock().unwrap();
374        for (key_path, data) in updates.drain(..) {
375            atomicwrites::AtomicFile::new(
376                key_path,
377                atomicwrites::OverwriteBehavior::AllowOverwrite,
378            )
379            .write(|file| file.write_all(data.as_bytes()))?;
380        }
381        Ok(())
382    }
383}
384
385// Setting any setting in this way will do one transaction for all settings
386// when commit finishes that transaction
387impl ConfigSet for ConfigTransaction<'_> {
388    fn set<T: Serialize>(&self, key: &str, value: T) -> Result<(), Error> {
389        //TODO: sanitize key (no slashes, cannot be . or ..)
390        let key_path = self.config.key_path(key)?;
391        let data = ron::ser::to_string_pretty(&value, ron::ser::PrettyConfig::new())?;
392        //TODO: replace duplicates?
393        {
394            let mut updates = self.updates.lock().unwrap();
395            updates.push((key_path, data));
396        }
397        Ok(())
398    }
399}
400
401pub trait CosmicConfigEntry
402where
403    Self: Sized,
404{
405    const VERSION: u64;
406
407    fn write_entry(&self, config: &Config) -> Result<(), crate::Error>;
408    fn get_entry(config: &Config) -> Result<Self, (Vec<crate::Error>, Self)>;
409    /// Returns the keys that were updated
410    fn update_keys<T: AsRef<str>>(
411        &mut self,
412        config: &Config,
413        changed_keys: &[T],
414    ) -> (Vec<crate::Error>, Vec<&'static str>);
415}
416
417#[derive(Debug)]
418pub struct Update<T> {
419    pub errors: Vec<crate::Error>,
420    pub keys: Vec<&'static str>,
421    pub config: T,
422}