1use notify::{
4 event::{EventKind, ModifyKind, RenameMode},
5 RecommendedWatcher, 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 #[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 fn get<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error>;
106
107 fn get_local<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error>;
109
110 fn get_system_default<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error>;
112}
113
114pub trait ConfigSet {
115 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
125fn 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 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 pub fn new(name: &str, version: u64) -> Result<Self, Error> {
162 let path = sanitize_name(name)?.join(format!("v{}", version));
164
165 #[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 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 fs::create_dir_all(&user_path)?;
184
185 Ok(Self {
187 system_path,
188 user_path: Some(user_path),
189 })
190 }
191
192 pub fn with_custom_path(name: &str, version: u64, custom_path: PathBuf) -> Result<Self, Error> {
194 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 fs::create_dir_all(&user_path)?;
202
203 Ok(Self {
205 system_path: None,
206 user_path: Some(user_path),
207 })
208 }
209
210 pub fn new_state(name: &str, version: u64) -> Result<Self, Error> {
216 let path = sanitize_name(name)?.join(format!("v{}", version));
218
219 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 fs::create_dir_all(&user_path)?;
227
228 Ok(Self {
229 system_path: None,
230 user_path: Some(user_path),
231 })
232 }
233
234 #[inline]
236 pub fn transaction(&self) -> ConfigTransaction {
237 ConfigTransaction {
238 config: self,
239 updates: Mutex::new(Vec::new()),
240 }
241 }
242
243 pub fn watch<F>(&self, f: F) -> Result<RecommendedWatcher, Error>
248 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(_)
264 | EventKind::Modify(ModifyKind::Metadata(_))
265 | EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => {
266 return;
268 }
269 _ => {}
270 }
271
272 let mut keys = Vec::new();
273 for path in &event.paths {
274 match path.strip_prefix(&user_path_clone) {
275 Ok(key_path) => {
276 if let Some(key) = key_path.to_str() {
277 if key.starts_with(".atomicwrite") {
279 continue;
280 }
281 keys.push(key.to_string());
282 }
283 }
284 Err(_err) => {
285 }
287 }
288 }
289 if !keys.is_empty() {
290 f(&watch_config, &keys);
291 }
292 }
293 Err(_err) => {
294 }
296 }
297 })?;
298 watcher.watch(user_path, notify::RecursiveMode::Recursive)?;
299 Ok(watcher)
300 }
301
302 fn default_path(&self, key: &str) -> Result<PathBuf, Error> {
303 let Some(system_path) = self.system_path.as_ref() else {
304 return Err(Error::NoConfigDirectory);
305 };
306
307 Ok(system_path.join(sanitize_name(key)?))
308 }
309
310 fn key_path(&self, key: &str) -> Result<PathBuf, Error> {
312 let Some(user_path) = self.user_path.as_ref() else {
313 return Err(Error::NoConfigDirectory);
314 };
315 Ok(user_path.join(sanitize_name(key)?))
316 }
317}
318
319impl ConfigGet for Config {
321 fn get<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error> {
323 match self.get_local(key) {
324 Ok(value) => Ok(value),
325 Err(Error::NotFound) => self.get_system_default(key),
326 Err(why) => Err(why),
327 }
328 }
329
330 fn get_local<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error> {
331 match self.key_path(key) {
333 Ok(key_path) if key_path.is_file() => {
334 let data = fs::read_to_string(key_path)
336 .map_err(|err| Error::GetKey(key.to_string(), err))?;
337
338 Ok(ron::from_str(&data)?)
339 }
340
341 _ => Err(Error::NotFound),
342 }
343 }
344
345 fn get_system_default<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error> {
346 let default_path = self.default_path(key)?;
348 let data =
349 fs::read_to_string(default_path).map_err(|err| Error::GetKey(key.to_string(), err))?;
350 Ok(ron::from_str(&data)?)
351 }
352}
353
354impl ConfigSet for Config {
356 fn set<T: Serialize>(&self, key: &str, value: T) -> Result<(), Error> {
357 let tx = self.transaction();
359 tx.set(key, value)?;
360 tx.commit()
361 }
362}
363
364#[must_use = "Config transaction must be committed"]
365pub struct ConfigTransaction<'a> {
366 config: &'a Config,
367 updates: Mutex<Vec<(PathBuf, String)>>,
369}
370
371impl ConfigTransaction<'_> {
372 pub fn commit(self) -> Result<(), Error> {
375 let mut updates = self.updates.lock().unwrap();
376 for (key_path, data) in updates.drain(..) {
377 atomicwrites::AtomicFile::new(
378 key_path,
379 atomicwrites::OverwriteBehavior::AllowOverwrite,
380 )
381 .write(|file| file.write_all(data.as_bytes()))?;
382 }
383 Ok(())
384 }
385}
386
387impl ConfigSet for ConfigTransaction<'_> {
390 fn set<T: Serialize>(&self, key: &str, value: T) -> Result<(), Error> {
391 let key_path = self.config.key_path(key)?;
393 let data = ron::ser::to_string_pretty(&value, ron::ser::PrettyConfig::new())?;
394 {
396 let mut updates = self.updates.lock().unwrap();
397 updates.push((key_path, data));
398 }
399 Ok(())
400 }
401}
402
403pub trait CosmicConfigEntry
404where
405 Self: Sized,
406{
407 const VERSION: u64;
408
409 fn write_entry(&self, config: &Config) -> Result<(), crate::Error>;
410 fn get_entry(config: &Config) -> Result<Self, (Vec<crate::Error>, Self)>;
411 fn update_keys<T: AsRef<str>>(
413 &mut self,
414 config: &Config,
415 changed_keys: &[T],
416 ) -> (Vec<crate::Error>, Vec<&'static str>);
417}
418
419#[derive(Debug)]
420pub struct Update<T> {
421 pub errors: Vec<crate::Error>,
422 pub keys: Vec<&'static str>,
423 pub config: T,
424}