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