libcosmic/cosmic-config/src/lib.rs

425 lines
14 KiB
Rust
Raw Normal View History

//! Integrations for cosmic-config — the cosmic configuration system.
use notify::{
event::{EventKind, ModifyKind, RenameMode},
2025-06-23 17:50:28 +02:00
RecommendedWatcher, Watcher,
};
2023-03-05 10:44:04 -07:00
use serde::{de::DeserializeOwned, Serialize};
use std::{
fmt, fs,
2023-03-05 10:44:04 -07:00
io::Write,
path::{Path, PathBuf},
2025-06-23 17:50:28 +02:00
sync::Mutex,
2023-03-05 10:44:04 -07:00
};
#[cfg(feature = "subscription")]
mod subscription;
2024-04-09 16:17:40 -04:00
#[cfg(feature = "subscription")]
pub use subscription::*;
#[cfg(all(feature = "dbus", feature = "subscription"))]
pub mod dbus;
Cosmic advanced text (#103) * wip: update to use cosmic-advanced-text * use cosmic-advanced-text branch of iced * fix: line height and spacing for segmented button and update to get svg fix * fix: spin button styling & spacing * update iced to fix segmented button border radius * feat: example improvements * feat: helper for loading fonts * feat: add focus style to button * fix: slider height and iced fixed * feat: hash icon width and height * cleanup * update ci * refactor: always use lazy feature of iced * update iced * update iced * cleanup & update iced * update iced: new slider & tiny-skia quad updates * update iced: fixes for tiny-skia quad rendering with edge case border radius * re-export iced_runtime & iced_widget * merge master * udpate iced * update iced * update iced * update iced * fix: make rectangle_tracker subscription only return update if there is some * feat: derive macro for loading a cosmic-config * feat (cosmic-config): iced subscription * fix (example): update to rectangle tracker subscription * fix (cosmic-config) * refactor(cosmic-config-derive): add support for types with generic parameters * fix (cosmic-config): feature gate updates for subscription helpers * feat: support for custom & system themes + move cosmic-theme to libcosmic * feat: sorta hacky way of creating header bars for libcosmic + update iced to get support for resizable windows in iced-sctk * update iced * update and reexport sctk * fix: applet border radius * feat (cosmic-theme): add id and name methods * fix(cosmic-theme): reexport palette from cosmic-theme * fix(cosmic-config-derive): allow use with reexported cosmic-config * feat: update iced with fix and refactor applet env vars * update iced
2023-05-30 12:03:15 -04:00
#[cfg(feature = "macro")]
pub use cosmic_config_derive;
#[cfg(feature = "calloop")]
pub mod calloop;
2023-03-05 10:44:04 -07:00
#[derive(Debug)]
pub enum Error {
AtomicWrites(atomicwrites::Error<std::io::Error>),
InvalidName(String),
Io(std::io::Error),
NoConfigDirectory,
Notify(notify::Error),
NotFound,
2023-03-05 10:44:04 -07:00
Ron(ron::Error),
RonSpanned(ron::error::SpannedError),
2024-01-18 19:01:11 -05:00
GetKey(String, std::io::Error),
2023-03-05 10:44:04 -07:00
}
impl fmt::Display for Error {
#[cold]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::AtomicWrites(err) => err.fmt(f),
Self::InvalidName(name) => write!(f, "invalid config name '{}'", name),
Self::Io(err) => err.fmt(f),
Self::NoConfigDirectory => write!(f, "cosmic config directory not found"),
Self::Notify(err) => err.fmt(f),
Self::NotFound => write!(f, "cosmic config key not configured"),
Self::Ron(err) => err.fmt(f),
Self::RonSpanned(err) => err.fmt(f),
2024-01-18 19:01:11 -05:00
Self::GetKey(key, err) => write!(f, "failed to get key '{}': {}", key, err),
}
}
}
impl std::error::Error for Error {}
impl Error {
/// Whether the reason for the missing config is caused by an error.
///
/// Useful for determining if it is appropriate to log as an error.
#[inline]
pub fn is_err(&self) -> bool {
!matches!(self, Self::NoConfigDirectory | Self::NotFound)
}
}
2023-03-05 10:44:04 -07:00
impl From<atomicwrites::Error<std::io::Error>> for Error {
fn from(f: atomicwrites::Error<std::io::Error>) -> Self {
Self::AtomicWrites(f)
}
}
impl From<std::io::Error> for Error {
fn from(f: std::io::Error) -> Self {
Self::Io(f)
}
}
impl From<notify::Error> for Error {
fn from(f: notify::Error) -> Self {
Self::Notify(f)
}
}
impl From<ron::Error> for Error {
fn from(f: ron::Error) -> Self {
Self::Ron(f)
}
}
impl From<ron::error::SpannedError> for Error {
fn from(f: ron::error::SpannedError) -> Self {
Self::RonSpanned(f)
}
}
pub trait ConfigGet {
/// Get a configuration value
///
/// Fallback to the system default if a local user override is not defined.
2023-03-05 10:44:04 -07:00
fn get<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error>;
/// Get a locally-defined configuration value from the user's local config.
fn get_local<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error>;
/// Get the system-defined default configuration value.
fn get_system_default<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error>;
2023-03-05 10:44:04 -07:00
}
pub trait ConfigSet {
/// Set a configuration value
fn set<T: Serialize>(&self, key: &str, value: T) -> Result<(), Error>;
}
#[derive(Clone, Debug)]
pub struct Config {
system_path: Option<PathBuf>,
user_path: Option<PathBuf>,
2023-03-05 10:44:04 -07:00
}
/// Check that the name is relative and doesn't contain . or ..
fn sanitize_name(name: &str) -> Result<&Path, Error> {
let path = Path::new(name);
if path
.components()
.all(|x| matches!(x, std::path::Component::Normal(_)))
{
Ok(path)
} else {
Err(Error::InvalidName(name.to_owned()))
}
}
2023-03-05 10:44:04 -07:00
impl Config {
/// Get a system config for the given name and config version
pub fn system(name: &str, version: u64) -> Result<Self, Error> {
let path = sanitize_name(name)?.join(format!("v{version}"));
#[cfg(unix)]
let system_path = xdg::BaseDirectories::with_prefix("cosmic")
.map_err(std::io::Error::from)?
.find_data_file(path);
#[cfg(windows)]
let system_path =
known_folders::get_known_folder_path(known_folders::KnownFolder::ProgramFilesCommon)
.map(|x| x.join("COSMIC").join(&path));
Ok(Self {
system_path,
user_path: None,
})
}
2023-03-05 10:44:04 -07:00
/// Get config for the given application name and config version
// Use folder at XDG config/name for config storage, return Config if successful
//TODO: fallbacks for flatpak (HOST_XDG_CONFIG_HOME, xdg-desktop settings proxy)
pub fn new(name: &str, version: u64) -> Result<Self, Error> {
// Look for [name]/v[version]
let path = sanitize_name(name)?.join(format!("v{}", version));
// Search data file, which provides default (e.g. /usr/share)
#[cfg(unix)]
let system_path = xdg::BaseDirectories::with_prefix("cosmic")
.map_err(std::io::Error::from)?
.find_data_file(&path);
#[cfg(windows)]
let system_path =
known_folders::get_known_folder_path(known_folders::KnownFolder::ProgramFilesCommon)
.map(|x| x.join("COSMIC").join(&path));
2023-03-05 10:44:04 -07:00
// Get libcosmic user configuration directory
let cosmic_user_path = dirs::config_dir()
.ok_or(Error::NoConfigDirectory)?
.join("cosmic");
let user_path = cosmic_user_path.join(path);
// Create new configuration directory if not found.
fs::create_dir_all(&user_path)?;
// Return Config
Ok(Self {
system_path,
user_path: Some(user_path),
})
2023-03-05 10:44:04 -07:00
}
2023-10-12 13:36:42 +02:00
2024-04-09 16:17:40 -04:00
/// Get config for the given application name and config version and custom path.
pub fn with_custom_path(name: &str, version: u64, custom_path: PathBuf) -> Result<Self, Error> {
// Look for [name]/v[version]
let path = sanitize_name(name)?.join(format!("v{version}"));
let cosmic_user_path = custom_path.join("cosmic");
let user_path = cosmic_user_path.join(path);
// Create new configuration directory if not found.
fs::create_dir_all(&user_path)?;
// Return Config
Ok(Self {
system_path: None,
user_path: Some(user_path),
})
}
/// Get state for the given application name and config version. State is meant to be used to
/// store items that may need to be exposed to other programs but will change regularly without
/// user action
// Use folder at XDG config/name for config storage, return Config if successful
//TODO: fallbacks for flatpak (HOST_XDG_CONFIG_HOME, xdg-desktop settings proxy)
pub fn new_state(name: &str, version: u64) -> Result<Self, Error> {
// Look for [name]/v[version]
let path = sanitize_name(name)?.join(format!("v{}", version));
// Get libcosmic user state directory
let cosmic_user_path = dirs::state_dir()
.ok_or(Error::NoConfigDirectory)?
.join("cosmic");
let user_path = cosmic_user_path.join(path);
// Create new state directory if not found.
fs::create_dir_all(&user_path)?;
Ok(Self {
system_path: None,
user_path: Some(user_path),
})
}
2023-03-05 10:44:04 -07:00
// Start a transaction (to set multiple configs at the same time)
#[inline]
pub fn transaction(&self) -> ConfigTransaction {
2023-03-05 10:44:04 -07:00
ConfigTransaction {
config: self,
updates: Mutex::new(Vec::new()),
}
}
// Watch keys for changes, will be triggered once per transaction
// This may end up being an mpsc channel instead of a function
// See EventHandler in the notify crate: https://docs.rs/notify/latest/notify/trait.EventHandler.html
// Having a callback allows for any application abstraction to be used
2025-06-23 11:20:48 -06:00
pub fn watch<F>(&self, f: F) -> Result<RecommendedWatcher, Error>
2023-03-05 10:44:04 -07:00
// Argument is an array of all keys that changed in that specific transaction
//TODO: simplify F requirements
where
F: Fn(&Self, &[String]) + Send + Sync + 'static,
{
let watch_config = self.clone();
let Some(user_path) = self.user_path.as_ref() else {
return Err(Error::NoConfigDirectory);
};
let user_path_clone = user_path.clone();
2025-06-25 17:52:03 -04:00
let mut watcher =
notify::recommended_watcher(move |event_res: Result<notify::Event, notify::Error>| {
match event_res {
2025-06-23 11:20:48 -06:00
Ok(event) => {
match &event.kind {
EventKind::Access(_)
| EventKind::Modify(ModifyKind::Metadata(_))
| EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => {
2025-06-23 11:20:48 -06:00
// Data not mutated
return;
}
2025-06-23 11:20:48 -06:00
_ => {}
}
2025-06-23 11:20:48 -06:00
let mut keys = Vec::new();
for path in &event.paths {
match path.strip_prefix(&user_path_clone) {
Ok(key_path) => {
if let Some(key) = key_path.to_str() {
// Skip any .atomicwrite temporary files
if key.starts_with(".atomicwrite") {
continue;
2023-03-05 10:44:04 -07:00
}
2025-06-23 11:20:48 -06:00
keys.push(key.to_string());
2025-06-23 17:50:28 +02:00
}
2023-03-05 10:44:04 -07:00
}
2025-06-23 11:20:48 -06:00
Err(_err) => {
//TODO: handle errors
}
2023-03-05 10:44:04 -07:00
}
2025-06-23 11:20:48 -06:00
}
if !keys.is_empty() {
f(&watch_config, &keys);
}
2023-03-05 10:44:04 -07:00
}
2025-06-23 11:20:48 -06:00
Err(_err) => {
2023-03-05 10:44:04 -07:00
//TODO: handle errors
}
}
2025-06-25 17:52:03 -04:00
})?;
2025-06-23 11:20:48 -06:00
watcher.watch(user_path, notify::RecursiveMode::Recursive)?;
2023-03-05 10:44:04 -07:00
Ok(watcher)
}
fn default_path(&self, key: &str) -> Result<PathBuf, Error> {
let Some(system_path) = self.system_path.as_ref() else {
return Err(Error::NoConfigDirectory);
};
Ok(system_path.join(sanitize_name(key)?))
2023-03-05 10:44:04 -07:00
}
/// Get the path of the key in the user's local config directory.
2023-03-05 10:44:04 -07:00
fn key_path(&self, key: &str) -> Result<PathBuf, Error> {
let Some(user_path) = self.user_path.as_ref() else {
return Err(Error::NoConfigDirectory);
};
Ok(user_path.join(sanitize_name(key)?))
2023-03-05 10:44:04 -07:00
}
}
// Getting any setting is available on a Config object
impl ConfigGet for Config {
//TODO: check for transaction
fn get<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error> {
match self.get_local(key) {
Ok(value) => Ok(value),
Err(Error::NotFound) => self.get_system_default(key),
Err(why) => Err(why),
}
}
fn get_local<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error> {
2023-03-05 10:44:04 -07:00
// If key path exists
match self.key_path(key) {
Ok(key_path) if key_path.is_file() => {
// Load user override
let data = fs::read_to_string(key_path)
.map_err(|err| Error::GetKey(key.to_string(), err))?;
Ok(ron::from_str(&data)?)
}
_ => Err(Error::NotFound),
}
}
fn get_system_default<T: DeserializeOwned>(&self, key: &str) -> Result<T, Error> {
// Load system default
let default_path = self.default_path(key)?;
let data =
fs::read_to_string(default_path).map_err(|err| Error::GetKey(key.to_string(), err))?;
Ok(ron::from_str(&data)?)
2023-03-05 10:44:04 -07:00
}
}
// Setting any setting in this way will do one transaction per set call
impl ConfigSet for Config {
fn set<T: Serialize>(&self, key: &str, value: T) -> Result<(), Error> {
// Wrap up single key/value sets in a transaction
let tx = self.transaction();
tx.set(key, value)?;
tx.commit()
}
}
#[must_use = "Config transaction must be committed"]
pub struct ConfigTransaction<'a> {
config: &'a Config,
//TODO: use map?
updates: Mutex<Vec<(PathBuf, String)>>,
}
impl ConfigTransaction<'_> {
2023-03-05 10:44:04 -07:00
/// Apply all pending changes from ConfigTransaction
//TODO: apply all changes at once
pub fn commit(self) -> Result<(), Error> {
let mut updates = self.updates.lock().unwrap();
for (key_path, data) in updates.drain(..) {
atomicwrites::AtomicFile::new(
key_path,
atomicwrites::OverwriteBehavior::AllowOverwrite,
)
.write(|file| file.write_all(data.as_bytes()))?;
}
Ok(())
}
}
// Setting any setting in this way will do one transaction for all settings
// when commit finishes that transaction
impl ConfigSet for ConfigTransaction<'_> {
2023-03-05 10:44:04 -07:00
fn set<T: Serialize>(&self, key: &str, value: T) -> Result<(), Error> {
//TODO: sanitize key (no slashes, cannot be . or ..)
let key_path = self.config.key_path(key)?;
let data = ron::ser::to_string_pretty(&value, ron::ser::PrettyConfig::new())?;
2023-03-05 10:44:04 -07:00
//TODO: replace duplicates?
{
let mut updates = self.updates.lock().unwrap();
updates.push((key_path, data));
}
Ok(())
}
}
Cosmic advanced text (#103) * wip: update to use cosmic-advanced-text * use cosmic-advanced-text branch of iced * fix: line height and spacing for segmented button and update to get svg fix * fix: spin button styling & spacing * update iced to fix segmented button border radius * feat: example improvements * feat: helper for loading fonts * feat: add focus style to button * fix: slider height and iced fixed * feat: hash icon width and height * cleanup * update ci * refactor: always use lazy feature of iced * update iced * update iced * cleanup & update iced * update iced: new slider & tiny-skia quad updates * update iced: fixes for tiny-skia quad rendering with edge case border radius * re-export iced_runtime & iced_widget * merge master * udpate iced * update iced * update iced * update iced * fix: make rectangle_tracker subscription only return update if there is some * feat: derive macro for loading a cosmic-config * feat (cosmic-config): iced subscription * fix (example): update to rectangle tracker subscription * fix (cosmic-config) * refactor(cosmic-config-derive): add support for types with generic parameters * fix (cosmic-config): feature gate updates for subscription helpers * feat: support for custom & system themes + move cosmic-theme to libcosmic * feat: sorta hacky way of creating header bars for libcosmic + update iced to get support for resizable windows in iced-sctk * update iced * update and reexport sctk * fix: applet border radius * feat (cosmic-theme): add id and name methods * fix(cosmic-theme): reexport palette from cosmic-theme * fix(cosmic-config-derive): allow use with reexported cosmic-config * feat: update iced with fix and refactor applet env vars * update iced
2023-05-30 12:03:15 -04:00
pub trait CosmicConfigEntry
where
Self: Sized,
{
const VERSION: u64;
Cosmic advanced text (#103) * wip: update to use cosmic-advanced-text * use cosmic-advanced-text branch of iced * fix: line height and spacing for segmented button and update to get svg fix * fix: spin button styling & spacing * update iced to fix segmented button border radius * feat: example improvements * feat: helper for loading fonts * feat: add focus style to button * fix: slider height and iced fixed * feat: hash icon width and height * cleanup * update ci * refactor: always use lazy feature of iced * update iced * update iced * cleanup & update iced * update iced: new slider & tiny-skia quad updates * update iced: fixes for tiny-skia quad rendering with edge case border radius * re-export iced_runtime & iced_widget * merge master * udpate iced * update iced * update iced * update iced * fix: make rectangle_tracker subscription only return update if there is some * feat: derive macro for loading a cosmic-config * feat (cosmic-config): iced subscription * fix (example): update to rectangle tracker subscription * fix (cosmic-config) * refactor(cosmic-config-derive): add support for types with generic parameters * fix (cosmic-config): feature gate updates for subscription helpers * feat: support for custom & system themes + move cosmic-theme to libcosmic * feat: sorta hacky way of creating header bars for libcosmic + update iced to get support for resizable windows in iced-sctk * update iced * update and reexport sctk * fix: applet border radius * feat (cosmic-theme): add id and name methods * fix(cosmic-theme): reexport palette from cosmic-theme * fix(cosmic-config-derive): allow use with reexported cosmic-config * feat: update iced with fix and refactor applet env vars * update iced
2023-05-30 12:03:15 -04:00
fn write_entry(&self, config: &Config) -> Result<(), crate::Error>;
fn get_entry(config: &Config) -> Result<Self, (Vec<crate::Error>, Self)>;
/// Returns the keys that were updated
fn update_keys<T: AsRef<str>>(
&mut self,
config: &Config,
changed_keys: &[T],
) -> (Vec<crate::Error>, Vec<&'static str>);
Cosmic advanced text (#103) * wip: update to use cosmic-advanced-text * use cosmic-advanced-text branch of iced * fix: line height and spacing for segmented button and update to get svg fix * fix: spin button styling & spacing * update iced to fix segmented button border radius * feat: example improvements * feat: helper for loading fonts * feat: add focus style to button * fix: slider height and iced fixed * feat: hash icon width and height * cleanup * update ci * refactor: always use lazy feature of iced * update iced * update iced * cleanup & update iced * update iced: new slider & tiny-skia quad updates * update iced: fixes for tiny-skia quad rendering with edge case border radius * re-export iced_runtime & iced_widget * merge master * udpate iced * update iced * update iced * update iced * fix: make rectangle_tracker subscription only return update if there is some * feat: derive macro for loading a cosmic-config * feat (cosmic-config): iced subscription * fix (example): update to rectangle tracker subscription * fix (cosmic-config) * refactor(cosmic-config-derive): add support for types with generic parameters * fix (cosmic-config): feature gate updates for subscription helpers * feat: support for custom & system themes + move cosmic-theme to libcosmic * feat: sorta hacky way of creating header bars for libcosmic + update iced to get support for resizable windows in iced-sctk * update iced * update and reexport sctk * fix: applet border radius * feat (cosmic-theme): add id and name methods * fix(cosmic-theme): reexport palette from cosmic-theme * fix(cosmic-config-derive): allow use with reexported cosmic-config * feat: update iced with fix and refactor applet env vars * update iced
2023-05-30 12:03:15 -04:00
}
2024-01-18 19:01:11 -05:00
2024-10-16 20:36:46 -04:00
#[derive(Debug)]
2024-01-18 19:01:11 -05:00
pub struct Update<T> {
pub errors: Vec<crate::Error>,
pub keys: Vec<&'static str>,
pub config: T,
}