1use 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 #[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").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 pub fn new(name: &str, version: u64) -> Result<Self, Error> {
160 let path = sanitize_name(name)?.join(format!("v{}", version));
162
163 #[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 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 fs::create_dir_all(&user_path)?;
180
181 Ok(Self {
183 system_path,
184 user_path: Some(user_path),
185 })
186 }
187
188 pub fn with_custom_path(name: &str, version: u64, custom_path: PathBuf) -> Result<Self, Error> {
190 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 fs::create_dir_all(&user_path)?;
198
199 Ok(Self {
201 system_path: None,
202 user_path: Some(user_path),
203 })
204 }
205
206 pub fn new_state(name: &str, version: u64) -> Result<Self, Error> {
212 let path = sanitize_name(name)?.join(format!("v{}", version));
214
215 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 fs::create_dir_all(&user_path)?;
223
224 Ok(Self {
225 system_path: None,
226 user_path: Some(user_path),
227 })
228 }
229
230 #[inline]
232 pub fn transaction(&self) -> ConfigTransaction {
233 ConfigTransaction {
234 config: self,
235 updates: Mutex::new(Vec::new()),
236 }
237 }
238
239 pub fn watch<F>(&self, f: F) -> Result<RecommendedWatcher, Error>
244 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 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 if key.starts_with(".atomicwrite") {
275 continue;
276 }
277 keys.push(key.to_string());
278 }
279 }
280 Err(_err) => {
281 }
283 }
284 }
285 if !keys.is_empty() {
286 f(&watch_config, &keys);
287 }
288 }
289 Err(_err) => {
290 }
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 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
315impl ConfigGet for Config {
317 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 match self.key_path(key) {
329 Ok(key_path) if key_path.is_file() => {
330 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 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
350impl ConfigSet for Config {
352 fn set<T: Serialize>(&self, key: &str, value: T) -> Result<(), Error> {
353 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 updates: Mutex<Vec<(PathBuf, String)>>,
365}
366
367impl ConfigTransaction<'_> {
368 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
383impl ConfigSet for ConfigTransaction<'_> {
386 fn set<T: Serialize>(&self, key: &str, value: T) -> Result<(), Error> {
387 let key_path = self.config.key_path(key)?;
389 let data = ron::ser::to_string_pretty(&value, ron::ser::PrettyConfig::new())?;
390 {
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 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}