cosmic_config/
lib.rs

1//! Integrations for cosmic-config — the cosmic configuration system.
2
3use notify::{
4    RecommendedWatcher, Watcher,
5    event::{EventKind, ModifyKind, RenameMode},
6};
7use serde::{Serialize, de::DeserializeOwned};
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").find_data_file(path);
144
145        #[cfg(windows)]
146        let system_path =
147            known_folders::get_known_folder_path(known_folders::KnownFolder::ProgramFilesCommon)
148                .map(|x| x.join("COSMIC").join(&path));
149
150        Ok(Self {
151            system_path,
152            user_path: None,
153        })
154    }
155
156    /// Get config for the given application name and config version
157    // Use folder at XDG config/name for config storage, return Config if successful
158    //TODO: fallbacks for flatpak (HOST_XDG_CONFIG_HOME, xdg-desktop settings proxy)
159    pub fn new(name: &str, version: u64) -> Result<Self, Error> {
160        // Look for [name]/v[version]
161        let path = sanitize_name(name)?.join(format!("v{}", version));
162
163        // Search data file, which provides default (e.g. /usr/share)
164        #[cfg(unix)]
165        let system_path = xdg::BaseDirectories::with_prefix("cosmic").find_data_file(&path);
166
167        #[cfg(windows)]
168        let system_path =
169            known_folders::get_known_folder_path(known_folders::KnownFolder::ProgramFilesCommon)
170                .map(|x| x.join("COSMIC").join(&path));
171
172        // Get libcosmic user configuration directory
173        let mut user_path = dirs::config_dir().ok_or(Error::NoConfigDirectory)?;
174        user_path.push("cosmic");
175        user_path.push(path);
176
177        // Create new configuration directory if not found.
178        fs::create_dir_all(&user_path)?;
179
180        // Return Config
181        Ok(Self {
182            system_path,
183            user_path: Some(user_path),
184        })
185    }
186
187    /// Get config for the given application name and config version and custom path.
188    pub fn with_custom_path(name: &str, version: u64, custom_path: PathBuf) -> Result<Self, Error> {
189        // Look for [name]/v[version]
190        let path = sanitize_name(name)?.join(format!("v{version}"));
191
192        let mut user_path = custom_path;
193        user_path.push("cosmic");
194        user_path.push(path);
195        // Create new configuration directory if not found.
196        fs::create_dir_all(&user_path)?;
197
198        // Return Config
199        Ok(Self {
200            system_path: None,
201            user_path: Some(user_path),
202        })
203    }
204
205    /// Get state for the given application name and config version. State is meant to be used to
206    /// store items that may need to be exposed to other programs but will change regularly without
207    /// user action
208    // Use folder at XDG config/name for config storage, return Config if successful
209    //TODO: fallbacks for flatpak (HOST_XDG_CONFIG_HOME, xdg-desktop settings proxy)
210    pub fn new_state(name: &str, version: u64) -> Result<Self, Error> {
211        // Look for [name]/v[version]
212        let path = sanitize_name(name)?.join(format!("v{}", version));
213
214        // Get libcosmic user state directory
215        let mut user_path = dirs::state_dir().ok_or(Error::NoConfigDirectory)?;
216        user_path.push("cosmic");
217        user_path.push(path);
218        // Create new state directory if not found.
219        fs::create_dir_all(&user_path)?;
220
221        Ok(Self {
222            system_path: None,
223            user_path: Some(user_path),
224        })
225    }
226
227    // Start a transaction (to set multiple configs at the same time)
228    #[inline]
229    pub fn transaction(&self) -> ConfigTransaction<'_> {
230        ConfigTransaction {
231            config: self,
232            updates: Mutex::new(Vec::new()),
233        }
234    }
235
236    // Watch keys for changes, will be triggered once per transaction
237    // This may end up being an mpsc channel instead of a function
238    // See EventHandler in the notify crate: https://docs.rs/notify/latest/notify/trait.EventHandler.html
239    // Having a callback allows for any application abstraction to be used
240    pub fn watch<F>(&self, f: F) -> Result<RecommendedWatcher, Error>
241    // Argument is an array of all keys that changed in that specific transaction
242    //TODO: simplify F requirements
243    where
244        F: Fn(&Self, &[String]) + Send + Sync + 'static,
245    {
246        let watch_config = self.clone();
247        let Some(user_path) = self.user_path.as_ref() else {
248            return Err(Error::NoConfigDirectory);
249        };
250        let user_path_clone = user_path.clone();
251        let mut watcher =
252            notify::recommended_watcher(move |event_res: Result<notify::Event, notify::Error>| {
253                match event_res {
254                    Ok(event) => {
255                        match &event.kind {
256                            EventKind::Access(_)
257                            | EventKind::Modify(ModifyKind::Metadata(_))
258                            | EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => {
259                                // Data not mutated
260                                return;
261                            }
262                            _ => {}
263                        }
264
265                        let mut keys = Vec::new();
266                        for path in &event.paths {
267                            match path.strip_prefix(&user_path_clone) {
268                                Ok(key_path) => {
269                                    if let Some(key) = key_path.to_str() {
270                                        // Skip any .atomicwrite temporary files
271                                        if key.starts_with(".atomicwrite") {
272                                            continue;
273                                        }
274                                        keys.push(key.to_string());
275                                    }
276                                }
277                                Err(_err) => {
278                                    //TODO: handle errors
279                                }
280                            }
281                        }
282                        if !keys.is_empty() {
283                            f(&watch_config, &keys);
284                        }
285                    }
286                    Err(_err) => {
287                        //TODO: handle errors
288                    }
289                }
290            })?;
291        watcher.watch(user_path, notify::RecursiveMode::Recursive)?;
292        Ok(watcher)
293    }
294
295    fn default_path(&self, key: &str) -> Result<PathBuf, Error> {
296        let Some(system_path) = self.system_path.as_ref() else {
297            return Err(Error::NoConfigDirectory);
298        };
299
300        Ok(system_path.join(sanitize_name(key)?))
301    }
302
303    /// Get the path of the key in the user's local config directory.
304    fn key_path(&self, key: &str) -> Result<PathBuf, Error> {
305        let Some(user_path) = self.user_path.as_ref() else {
306            return Err(Error::NoConfigDirectory);
307        };
308        Ok(user_path.join(sanitize_name(key)?))
309    }
310}
311
312// Getting any setting is available on a Config object
313impl ConfigGet for Config {
314    //TODO: check for transaction
315    fn get<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error> {
316        match self.get_local(key) {
317            Ok(value) => Ok(value),
318            Err(Error::NotFound) => self.get_system_default(key),
319            Err(why) => Err(why),
320        }
321    }
322
323    fn get_local<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error> {
324        // If key path exists
325        match self.key_path(key) {
326            Ok(key_path) if key_path.is_file() => {
327                // Load user override
328                let data = fs::read_to_string(key_path)
329                    .map_err(|err| Error::GetKey(key.to_string(), err))?;
330
331                Ok(ron::from_str(&data)?)
332            }
333
334            _ => Err(Error::NotFound),
335        }
336    }
337
338    fn get_system_default<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error> {
339        // Load system default
340        let default_path = self.default_path(key)?;
341        let data =
342            fs::read_to_string(default_path).map_err(|err| Error::GetKey(key.to_string(), err))?;
343        Ok(ron::from_str(&data)?)
344    }
345}
346
347// Setting any setting in this way will do one transaction per set call
348impl ConfigSet for Config {
349    fn set<T: Serialize>(&self, key: &str, value: T) -> Result<(), Error> {
350        // Wrap up single key/value sets in a transaction
351        let tx = self.transaction();
352        tx.set(key, value)?;
353        tx.commit()
354    }
355}
356
357#[must_use = "Config transaction must be committed"]
358pub struct ConfigTransaction<'a> {
359    config: &'a Config,
360    //TODO: use map?
361    updates: Mutex<Vec<(PathBuf, String)>>,
362}
363
364impl ConfigTransaction<'_> {
365    /// Apply all pending changes from ConfigTransaction
366    //TODO: apply all changes at once
367    pub fn commit(self) -> Result<(), Error> {
368        let mut updates = self.updates.lock().unwrap();
369        for (key_path, data) in updates.drain(..) {
370            atomicwrites::AtomicFile::new(
371                key_path,
372                atomicwrites::OverwriteBehavior::AllowOverwrite,
373            )
374            .write(|file| file.write_all(data.as_bytes()))?;
375        }
376        Ok(())
377    }
378}
379
380// Setting any setting in this way will do one transaction for all settings
381// when commit finishes that transaction
382impl ConfigSet for ConfigTransaction<'_> {
383    fn set<T: Serialize>(&self, key: &str, value: T) -> Result<(), Error> {
384        //TODO: sanitize key (no slashes, cannot be . or ..)
385        let key_path = self.config.key_path(key)?;
386        let data = ron::ser::to_string_pretty(&value, ron::ser::PrettyConfig::new())?;
387        //TODO: replace duplicates?
388        {
389            let mut updates = self.updates.lock().unwrap();
390            updates.push((key_path, data));
391        }
392        Ok(())
393    }
394}
395
396pub trait CosmicConfigEntry
397where
398    Self: Sized,
399{
400    const VERSION: u64;
401
402    fn write_entry(&self, config: &Config) -> Result<(), crate::Error>;
403    fn get_entry(config: &Config) -> Result<Self, (Vec<crate::Error>, Self)>;
404    /// Returns the keys that were updated
405    fn update_keys<T: AsRef<str>>(
406        &mut self,
407        config: &Config,
408        changed_keys: &[T],
409    ) -> (Vec<crate::Error>, Vec<&'static str>);
410}
411
412#[derive(Debug)]
413pub struct Update<T> {
414    pub errors: Vec<crate::Error>,
415    pub keys: Vec<&'static str>,
416    pub config: T,
417}