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 mut user_path = dirs::config_dir().ok_or(Error::NoConfigDirectory)?;
174 user_path.push("cosmic");
175 user_path.push(path);
176
177 fs::create_dir_all(&user_path)?;
179
180 Ok(Self {
182 system_path,
183 user_path: Some(user_path),
184 })
185 }
186
187 pub fn with_custom_path(name: &str, version: u64, custom_path: PathBuf) -> Result<Self, Error> {
189 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 fs::create_dir_all(&user_path)?;
197
198 Ok(Self {
200 system_path: None,
201 user_path: Some(user_path),
202 })
203 }
204
205 pub fn new_state(name: &str, version: u64) -> Result<Self, Error> {
211 let path = sanitize_name(name)?.join(format!("v{}", version));
213
214 let mut user_path = dirs::state_dir().ok_or(Error::NoConfigDirectory)?;
216 user_path.push("cosmic");
217 user_path.push(path);
218 fs::create_dir_all(&user_path)?;
220
221 Ok(Self {
222 system_path: None,
223 user_path: Some(user_path),
224 })
225 }
226
227 #[inline]
229 pub fn transaction(&self) -> ConfigTransaction<'_> {
230 ConfigTransaction {
231 config: self,
232 updates: Mutex::new(Vec::new()),
233 }
234 }
235
236 pub fn watch<F>(&self, f: F) -> Result<RecommendedWatcher, Error>
241 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 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 if key.starts_with(".atomicwrite") {
272 continue;
273 }
274 keys.push(key.to_string());
275 }
276 }
277 Err(_err) => {
278 }
280 }
281 }
282 if !keys.is_empty() {
283 f(&watch_config, &keys);
284 }
285 }
286 Err(_err) => {
287 }
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 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
312impl ConfigGet for Config {
314 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 match self.key_path(key) {
326 Ok(key_path) if key_path.is_file() => {
327 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 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
347impl ConfigSet for Config {
349 fn set<T: Serialize>(&self, key: &str, value: T) -> Result<(), Error> {
350 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 updates: Mutex<Vec<(PathBuf, String)>>,
362}
363
364impl ConfigTransaction<'_> {
365 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
380impl ConfigSet for ConfigTransaction<'_> {
383 fn set<T: Serialize>(&self, key: &str, value: T) -> Result<(), Error> {
384 let key_path = self.config.key_path(key)?;
386 let data = ron::ser::to_string_pretty(&value, ron::ser::PrettyConfig::new())?;
387 {
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 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}