#[macro_use] pub mod macros; pub mod manager; pub mod schema; // Re-export commonly used types and traits use std::fmt::Debug; use std::ops::Deref; // Re-export galaxyui_core for use by macros. pub use galaxyui_core; // Re-export crates used by macro expansions in downstream crates. #[doc(hidden)] pub use inventory as _inventory; pub use macros::SettingSection; pub use manager::SettingsManager; #[doc(hidden)] pub use schemars as _schemars; #[doc(hidden)] pub use settings_value as _settings_value; pub use settings_value::SettingsValue; /// Extracts the storage key (last segment after the final `.`) from a toml_path. /// /// # Examples /// - `"appearance.text.font_name"` → `"font_name"` /// - `"font_name"` → `"font_name"` pub const fn toml_path_storage_key(path: &str) -> &str { let bytes = path.as_bytes(); let mut i = path.len(); while i > 0 { i -= 1; if bytes[i] == b'.' { let (_, suffix) = path.split_at(i + 1); return suffix; } } path } /// Extracts the hierarchy (everything before the final `.`) from a toml_path. /// /// Returns `None` when the path contains no dot (the path is just a key). /// /// # Examples /// - `"appearance.text.font_name"` → `Some("appearance.text")` /// - `"font_name"` → `None` pub const fn toml_path_hierarchy(path: &str) -> Option<&str> { let bytes = path.as_bytes(); let mut i = path.len(); while i > 0 { i -= 1; if bytes[i] == b'.' { let (prefix, _) = path.split_at(i); return Some(prefix); } } None } use anyhow::{Context, Result}; use galaxy_features::FeatureFlag; use galaxyui_core::{AppContext, Entity, ModelContext}; use galaxyui_extras::secure_storage::{self, AppContextExt as _}; use galaxyui_extras::user_preferences::UserPreferences; use serde::Serialize; use serde::de::DeserializeOwned; /// A newtype wrapper for the public preferences backend. /// /// Public settings (those marked `private: false` in `define_settings_group!`) /// are stored in the user-visible settings file (TOML) when the `SettingsFile` /// feature flag is enabled, otherwise in the platform-native store. /// /// The inner field is private and only accessible within the settings crate via /// [`as_preferences`](Self::as_preferences). This prevents external code from /// bypassing the settings macros to read/write public preferences directly. pub struct PublicPreferences(Box); impl PublicPreferences { pub fn new(prefs: Box) -> Self { Self(prefs) } /// Returns the underlying preferences backend. /// /// This is intentionally `pub(crate)` so that only the settings /// infrastructure (macros, `Setting` trait, `SettingsManager`) can access /// the raw backend. External code must go through typed settings groups /// produced by `define_settings_group!`. pub(crate) fn as_preferences(&self) -> &dyn UserPreferences { self.0.as_ref() } /// Returns whether this backend is the user-visible settings file. pub fn is_settings_file(&self) -> bool { self.0.is_settings_file() } /// Reloads the backing store from disk. pub fn reload_from_disk(&self) -> Result<(), galaxyui_extras::user_preferences::Error> { self.0.reload_from_disk() } } impl galaxyui_core::Entity for PublicPreferences { type Event = (); } impl galaxyui_core::SingletonEntity for PublicPreferences {} /// A newtype wrapper for the private preferences backend. /// /// Private settings (those marked `private: true` in `define_settings_group!`) /// are stored here instead of in the user-visible settings file. This always /// uses the platform-native store (e.g. UserDefaults on macOS, JSON file on /// Linux, registry on Windows). pub struct PrivatePreferences(Box); impl PrivatePreferences { pub fn new(prefs: Box) -> Self { Self(prefs) } } impl Deref for PrivatePreferences { type Target = dyn UserPreferences; fn deref(&self) -> &Self::Target { self.0.as_ref() } } impl galaxyui_core::Entity for PrivatePreferences { type Event = (); } impl galaxyui_core::SingletonEntity for PrivatePreferences {} /// An enum representing the different platforms a setting could apply to. #[derive(Debug, Clone)] pub enum SupportedPlatforms { ALL, DESKTOP, /* Refers to running on device, not web-based, such as Mac, Linux, and Windows */ MAC, LINUX, WINDOWS, WEB, OR(Box, Box), } /// An enum representing the different ways a setting can be synced to the cloud. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SyncToCloud { /// The setting is synced to the cloud as a single global value that applies to on all supported platforms. Globally(RespectUserSyncSetting), /// The setting is synced to the cloud as a value that is unique to each platform. PerPlatform(RespectUserSyncSetting), /// The setting is not synced to the cloud. Never, } /// Whether for this setting we respect the user toggle for settings sync. /// There are some cases we want to sync settings regardless of the user setting, /// such as for the value of whether cloud syncing is enabled, whether telemetry is enabled, etc. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RespectUserSyncSetting { /// Only sync if the user has settings sync enabled Yes, /// Sync regardless of the user's setting No, } impl SupportedPlatforms { pub fn matches_current_platform(&self) -> bool { match self { SupportedPlatforms::ALL => true, SupportedPlatforms::DESKTOP => { cfg!(not(target_family = "wasm")) } SupportedPlatforms::MAC => { cfg!(all(not(target_family = "wasm"), target_os = "macos")) } SupportedPlatforms::LINUX => { cfg!(all( not(target_family = "wasm"), any(target_os = "linux", target_os = "freebsd") )) } SupportedPlatforms::WINDOWS => { cfg!(all(not(target_family = "wasm"), target_os = "windows")) } SupportedPlatforms::WEB => { cfg!(target_family = "wasm") } SupportedPlatforms::OR(first, second) => { first.matches_current_platform() || second.matches_current_platform() } } } } /// An enum representing the reason for a change event. #[derive(Debug, Clone, Copy)] pub enum ChangeEventReason { /// The change was initiated from a cloud sync CloudSync, /// The change was initiated from a local setting change LocalChange, /// The change was initiated from a clear operation Clear, } /// A representation of a setting which can be loaded from and persisted to some /// sort of durable storage. pub trait Setting { type Group: Entity; type Value: Serialize + DeserializeOwned + PartialEq + Debug + SettingsValue; /// Constructs this setting object with the given initial value. /// If value is None, uses the default value and marks as not explicitly set. /// If value is Some, uses that value and marks as explicitly set. fn new(value: Option) -> Self where Self: Sized; /// Returns the name of the setting. fn setting_name() -> &'static str; /// Returns the key underwhich this setting should be stored. Should be /// distinct from all other settings, and should not change over time. fn storage_key() -> &'static str; /// Returns the full TOML path for this setting, if any. /// /// The toml_path is a dot-separated path that includes both the hierarchy /// (section) and the storage key as the last segment. For example, /// `"appearance.text.font_name"` means the setting lives under /// `[appearance.text]` with key `font_name`. fn toml_path() -> Option<&'static str> { None } /// Returns the key used in the TOML settings file. /// /// For settings with a `toml_path`, this is the last segment (e.g. /// `"font_name"` from `"appearance.text.font_name"`). For settings /// without a `toml_path`, falls back to `storage_key()`. fn toml_key() -> &'static str { Self::storage_key() } /// Returns the hierarchy path for this setting, if any. /// /// When set, hierarchy-aware preferences backends use this to organize /// settings into logical groups. For example, a hierarchy of `"font"` /// places the setting under a `font` section in the backing store. fn hierarchy() -> Option<&'static str> { None } /// Returns the maximum number of TOML section-table levels to use when /// rendering this setting's value in the settings file. /// /// - `None` (default) — unlimited depth; nested objects become section /// tables (`[section.subsection]`) all the way down. /// - `Some(0)` — the value itself is rendered as an inline table /// (`key = { ... }`). Used for enum settings whose shape changes between /// variants. /// - `Some(1)` — the setting gets its own section header, but any nested /// objects within it are rendered inline. Useful for struct settings /// that contain maps or nested structs. fn max_table_depth() -> Option { None } /// Returns the platforms that this setting is supported on. fn supported_platforms() -> SupportedPlatforms; /// Returns whether and how this setting is synced to the cloud via Warp Drive. fn sync_to_cloud() -> SyncToCloud; /// Returns whether this setting is private (not shown in the user-visible settings file). /// /// Private settings are persisted to the platform-native store (e.g. UserDefaults on /// macOS) rather than the TOML settings file, ensuring they never appear in the /// user-editable file. fn is_private() -> bool; /// Returns whether the current value of this setting should be synced. /// Only applies if sync_to_cloud() returns a value other than SyncToCloud::Never. /// Specific settings can implement this to filter which values should be synced. fn current_value_is_syncable(&self) -> bool { true } /// Returns whether the current value of this setting is syncable on the current platform, /// given the user's settings sync preference. fn is_setting_syncable_on_current_platform(&self, settings_sync_enabled: bool) -> bool { if !self.current_value_is_syncable() { return false; } match (Self::sync_to_cloud(), settings_sync_enabled) { (SyncToCloud::Never, _) => false, (SyncToCloud::Globally(RespectUserSyncSetting::No), _) => true, (SyncToCloud::Globally(RespectUserSyncSetting::Yes), true) => true, (SyncToCloud::Globally(RespectUserSyncSetting::Yes), false) => false, (SyncToCloud::PerPlatform(RespectUserSyncSetting::No), _) => { self.is_supported_on_current_platform() } (SyncToCloud::PerPlatform(RespectUserSyncSetting::Yes), true) => { self.is_supported_on_current_platform() } (SyncToCloud::PerPlatform(RespectUserSyncSetting::Yes), false) => false, } } /// Returns the current value of the setting. This may be different from /// the value persisted in storage. fn value(&self) -> &Self::Value; /// Validates whether the new value is valid and returns a valid value to /// use. If the provided value is valid, the expectation is that this will /// return the provided value. If not, it is up to the implementation /// whether the current value of the setting is returned or whether some /// other value is returned. fn validate(&self, new_value: Self::Value) -> Self::Value { new_value } /// Clears the value of the setting from persistent storage and fires a change event /// indicating that the value was cleared. fn clear_value(&mut self, ctx: &mut ModelContext) -> anyhow::Result<()>; /// Loads a value into memory without persisting it to storage. /// /// Used during hot-reload to sync in-memory state with the file on disk. /// Unlike [`set_value`](Self::set_value), this never writes to the /// preferences backend, avoiding write-back loops with the file watcher. fn load_value( &mut self, new_value: Self::Value, explicitly_set: bool, ctx: &mut ModelContext, ) -> anyhow::Result<()>; /// Sets the value of the setting persisting it to storage. The change event indicates /// that the update was initiated from a cloud sync. fn set_value_from_cloud_sync( &mut self, new_value: Self::Value, ctx: &mut galaxyui_core::ModelContext, ) -> anyhow::Result<()>; /// Sets the value of the setting persisting it to storage. fn set_value( &mut self, new_value: Self::Value, ctx: &mut ModelContext, ) -> Result<()>; /// Returns the default value of the setting. fn default_value() -> Self::Value; /// Sets the value of the setting to its default and persists it to storage. fn set_value_to_default( &mut self, ctx: &mut galaxyui_core::ModelContext, ) -> anyhow::Result<()> { self.set_value(Self::default_value(), ctx) } /// Returns the appropriate preferences backend for this setting. /// /// Private settings use the platform-native store; public settings use /// the main preferences backend (which may be the TOML settings file). fn preferences_for_setting(ctx: &AppContext) -> &dyn UserPreferences { use galaxyui_core::SingletonEntity; if Self::is_private() { ::as_ref(ctx).deref() } else if FeatureFlag::SettingsFile.is_enabled() { ::as_ref(ctx).as_preferences() } else { // When the settings file is disabled, fall back to the private // backend so both paths share a single instance. ::as_ref(ctx).deref() } } /// Constructs a new instance of the setting, populating its initial value /// based on any previously-stored value, falling back to /// `Self::default_value()` if no value was stored or it could not be parsed /// successfully. fn new_from_storage(ctx: &mut AppContext) -> Self where Self: Sized, { Self::new(Self::read_from_preferences(Self::preferences_for_setting( ctx, ))) } /// Reads the setting's value from the provided preferences, returning None /// if the value is not set or could not be parsed successfully. fn read_from_preferences(preferences: &dyn UserPreferences) -> Option { let key = if preferences.is_settings_file() { Self::toml_key() } else { Self::storage_key() }; let value = preferences .read_value_with_hierarchy(key, Self::hierarchy()) .unwrap_or_default()?; // For the settings file, use the SettingsValue trait. if preferences.is_settings_file() { let json_value = match serde_json::from_str::(&value) { Ok(v) => v, Err(err) => { log::error!( "Failed to parse JSON for setting {}: {err:?}", Self::storage_key() ); preferences.inhibit_writes_for_key(Self::toml_key(), Self::hierarchy()); return None; } }; match ::from_file_value(&json_value) { Some(val) => { log::debug!( "Loaded {} from settings file; value: {:?}", Self::setting_name(), val ); return Some(val); } None => { log::error!( "Failed to parse file value for setting {}", Self::storage_key() ); preferences.inhibit_writes_for_key(Self::toml_key(), Self::hierarchy()); return None; } } } match serde_json::from_str(&value) { Ok(val) => { log::debug!( "Loaded {} from user defaults; value: {:?}", Self::setting_name(), val ); Some(val) } Err(err) => { log::error!( "Failed to parse stored value for setting {}: {err:?}", Self::storage_key() ); None } } } /// Persists the current value of the setting in some form of durable /// storage. Returns whether the value was changed from what was currently /// stored. fn write_to_preferences( new_value: &Self::Value, preferences: &dyn UserPreferences, ) -> Result { let key = if preferences.is_settings_file() { Self::toml_key() } else { Self::storage_key() }; // For the settings file, use the SettingsValue trait. let value = if preferences.is_settings_file() { let file_value = ::to_file_value(new_value); serde_json::to_string(&file_value).context(format!( "Failed to write {} to storage", Self::storage_key() ))? } else { serde_json::to_string(new_value).context(format!( "Failed to write {} to storage", Self::storage_key() ))? }; // Compare semantically by deserializing the stored value back into // the typed value rather than comparing JSON strings. This avoids // spurious writes caused by serialization differences (key ordering, // null-vs-missing fields, formatting) that don't represent actual // value changes. let stored_value_matches = preferences .read_value_with_hierarchy(key, Self::hierarchy())? .as_deref() .and_then(|stored| { if preferences.is_settings_file() { let json_value = serde_json::from_str::(stored).ok()?; return ::from_file_value(&json_value); } serde_json::from_str::(stored).ok() }) .is_some_and(|stored_val| &stored_val == new_value); if !stored_value_matches { log::debug!( "Writing new value of {} to storage; key: {}; value: {:?}", Self::setting_name(), key, value ); let _ = preferences.write_value_with_hierarchy( key, value, Self::hierarchy(), Self::max_table_depth(), ); Ok(true) } else { Ok(false) } } /// Clears the setting from the given durable storage and returns whether the setting was cleared. fn clear_from_preferences(preferences: &dyn UserPreferences) -> Result<()> { let key = if preferences.is_settings_file() { Self::toml_key() } else { Self::storage_key() }; log::debug!( "Clearing setting {} with key {} from preferences", Self::setting_name(), key, ); preferences.remove_value_with_hierarchy(key, Self::hierarchy())?; Ok(()) } /// Returns true if this setting is supported on the current platform (e.g., Web, Linux, Mac). For example, /// Background opacity is supported on Mac and Linux, not Web. fn is_supported_on_current_platform(&self) -> bool; /// Returns true if this setting was explicitly set by the user (i.e., not using the default value). fn is_value_explicitly_set(&self) -> bool; } /// Shared persistence operations for typed settings backed by secure storage. /// /// Implementors remain responsible for routing their [`Setting`] lifecycle /// methods through this trait and for keeping the setting private and /// non-synced when the value must not be exposed through ordinary settings /// storage. pub trait SecureSetting: Setting { /// Writes this setting's serialized value through its selected secure-storage path. fn write_secure_storage_value( storage: &dyn secure_storage::SecureStorage, key: &str, value: &str, ) -> Result<(), secure_storage::Error> { storage.write_value(key, value) } /// Reads and deserializes this setting from secure storage. /// /// Missing, unreadable, or malformed values return `None`, allowing the /// setting to fail closed to its default value. fn read_from_secure_storage(ctx: &AppContext) -> Option { let value = match ctx.secure_storage().read_value(Self::storage_key()) { Ok(value) => value, Err(secure_storage::Error::NotFound) => return None, Err(err) => { log::error!( "Failed to read {} from secure storage: {err:#}", Self::setting_name() ); return None; } }; match serde_json::from_str(&value) { Ok(value) => Some(value), Err(err) => { log::error!( "Failed to deserialize {} from secure storage: {err:#}", Self::setting_name() ); None } } } /// Persists this setting to secure storage if its typed value changed. fn write_to_secure_storage(new_value: &Self::Value, ctx: &AppContext) -> Result { let stored_value_matches = match ctx.secure_storage().read_value(Self::storage_key()) { Ok(stored) => serde_json::from_str::(&stored) .is_ok_and(|stored| stored == *new_value), Err(secure_storage::Error::NotFound) => false, Err(err) => { return Err(anyhow::anyhow!(err)).context(format!( "Failed to read existing {} from secure storage", Self::setting_name() )); } }; if stored_value_matches { return Ok(false); } let serialized = serde_json::to_string(new_value).context(format!( "Failed to serialize {} for secure storage", Self::setting_name() ))?; Self::write_secure_storage_value(ctx.secure_storage(), Self::storage_key(), &serialized) .context(format!( "Failed to write {} to secure storage", Self::setting_name() ))?; Ok(true) } /// Removes this setting from secure storage. fn clear_from_secure_storage(ctx: &AppContext) -> Result<()> { match ctx.secure_storage().remove_value(Self::storage_key()) { Ok(()) | Err(secure_storage::Error::NotFound) => Ok(()), Err(err) => Err(anyhow::anyhow!(err)).context(format!( "Failed to clear {} from secure storage", Self::setting_name() )), } } } /// A trait for settings that can be toggled between two values. pub trait ToggleableSetting: Setting { /// Toggles the value of the setting and persists it to storage, returning /// the new value upon success. fn toggle_and_save_value( &mut self, ctx: &mut ModelContext<::Group>, ) -> Result<::Value>; } impl ToggleableSetting for S where T: std::ops::Not + Copy + Debug, S: Setting, { fn toggle_and_save_value( &mut self, ctx: &mut ModelContext<::Group>, ) -> Result<::Value> { let current_value = *self.value(); let new_value = !current_value; log::debug!( "Toggling value of {} from {:?} to {:?}", Self::setting_name(), current_value, new_value ); self.set_value(new_value, ctx)?; Ok(new_value) } } #[cfg(test)] #[path = "toml_path_tests.rs"] mod toml_path_tests; #[cfg(test)] #[path = "mod_tests.rs"] mod mod_tests;