Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
+614
View File
@@ -0,0 +1,614 @@
#[macro_use]
pub mod macros;
pub mod manager;
pub mod schema;
// Re-export commonly used types and traits
pub use macros::SettingSection;
pub use manager::SettingsManager;
// Re-export crates used by macro expansions in downstream crates.
#[doc(hidden)]
pub use inventory as _inventory;
#[doc(hidden)]
pub use schemars as _schemars;
#[doc(hidden)]
pub use settings_value as _settings_value;
pub use settings_value::SettingsValue;
use std::fmt::Debug;
use std::ops::Deref;
use std::sync::atomic::{AtomicBool, Ordering};
/// 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 serde::{Serialize, de::DeserializeOwned};
use warpui::{AppContext, Entity, ModelContext};
use warpui_extras::user_preferences::UserPreferences;
/// Whether the TOML-backed settings file is active.
///
/// Set once during startup via [`set_settings_file_enabled`]. When `false`,
/// public settings fall back to the private (platform-native) backend so
/// that all settings share a single instance.
static SETTINGS_FILE_ENABLED: AtomicBool = AtomicBool::new(false);
/// Records whether the TOML-backed settings file feature is active.
///
/// Call this once during startup after checking `FeatureFlag::SettingsFile`.
/// The value is read by [`Setting::preferences_for_setting`] and
/// [`SettingsManager::read_local_setting_value`] to decide which backend
/// to use for public settings.
pub fn set_settings_file_enabled(enabled: bool) {
SETTINGS_FILE_ENABLED.store(enabled, Ordering::Relaxed);
}
/// Returns whether the TOML-backed settings file is currently active.
pub fn is_settings_file_enabled() -> bool {
SETTINGS_FILE_ENABLED.load(Ordering::Relaxed)
}
/// 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<dyn UserPreferences>);
impl PublicPreferences {
pub fn new(prefs: Box<dyn UserPreferences>) -> 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<(), warpui_extras::user_preferences::Error> {
self.0.reload_from_disk()
}
}
impl warpui::Entity for PublicPreferences {
type Event = ();
}
impl warpui::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<dyn UserPreferences>);
impl PrivatePreferences {
pub fn new(prefs: Box<dyn UserPreferences>) -> Self {
Self(prefs)
}
}
impl Deref for PrivatePreferences {
type Target = dyn UserPreferences;
fn deref(&self) -> &Self::Target {
self.0.as_ref()
}
}
impl warpui::Entity for PrivatePreferences {
type Event = ();
}
impl warpui::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<SupportedPlatforms>, Box<SupportedPlatforms>),
}
/// 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"), target_os = "linux"))
}
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::Value>) -> 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<u32> {
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<Self::Group>) -> 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<Self::Group>,
) -> 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 warpui::ModelContext<Self::Group>,
) -> anyhow::Result<()>;
/// Sets the value of the setting persisting it to storage.
fn set_value(
&mut self,
new_value: Self::Value,
ctx: &mut ModelContext<Self::Group>,
) -> 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 warpui::ModelContext<Self::Group>,
) -> 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 warpui::SingletonEntity;
if Self::is_private() {
<PrivatePreferences as SingletonEntity>::as_ref(ctx).deref()
} else if is_settings_file_enabled() {
<PublicPreferences as SingletonEntity>::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.
<PrivatePreferences as SingletonEntity>::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<Self::Value> {
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::<serde_json::Value>(&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 <Self::Value as SettingsValue>::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<bool> {
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 = <Self::Value as SettingsValue>::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::<serde_json::Value>(stored).ok()?;
return <Self::Value as SettingsValue>::from_file_value(&json_value);
}
serde_json::from_str::<Self::Value>(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;
}
/// 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<<Self as Setting>::Group>,
) -> Result<<Self as Setting>::Value>;
}
impl<T, S> ToggleableSetting for S
where
T: std::ops::Not<Output = T> + Copy + Debug,
S: Setting<Value = T>,
{
fn toggle_and_save_value(
&mut self,
ctx: &mut ModelContext<<Self as Setting>::Group>,
) -> Result<<Self as Setting>::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;
+955
View File
@@ -0,0 +1,955 @@
//! This module defines a set of macros to standardize and simplify the process
//! of defining new settings within Warp.
//!
//! Settings are defined as enums or structs that implement [`Setting`], and are
//! organized into groups in singleton models which contain one or more settings
//! and automatically emit an event notifying interested listeners when a
//! setting changes (specifying which setting was updated). A setting can hold
//! any type which has a default value, supports equality checks, and can be
//! both serialized and deserialized.
//!
//! # Defining settings
//!
//! ## Defining a new setting group
//!
//! This shows the simplest usage of these macros - creating a group of settings
//! where each setting has its implementation automatically generated for you.
//!
//! ```
//! # use settings::*;
//! # use settings::macros::*;
//! define_settings_group!(ExampleGroup, settings: [
//! bool_setting: BoolSetting {
//! type: bool,
//! default: false,
//! supported_platforms: SupportedPlatforms::ALL,
//! sync_to_cloud: SyncToCloud::Never,
//! private: false,
//! toml_path: "example.bool_setting",
//! },
//! float_setting: FloatSetting {
//! type: f32,
//! default: 3.14,
//! supported_platforms: SupportedPlatforms::ALL,
//! sync_to_cloud: SyncToCloud::Never,
//! private: false,
//! toml_path: "example.float_setting",
//! },
//! ]);
//! ```
//!
//! The macro also generates an `Event` type that is passed to subscribers of the
//! setting group model. The name of the type is created by appended 'ChangedEvent'
//! to the name of the setting group:
//!
//! ```
//! pub enum ExampleGroupChangedEvent {
//! BoolSetting,
//! FloatSetting
//! }
//! ```
//!
//! Note that this event type must be explicitly included in `use` statements to
//! bring it into scope.
//!
//! # Turning an existing enum into a setting
//!
//! You can also "upgrade" an existing enum into a setting. As with
//! primitive-based settings, you'll need to make sure your enum implements
//! [`Default`], [`PartialEq`], [`serde::Serialize`], and
//! [`serde::Deserialize`].
//!
//! ```
//! # use schemars::JsonSchema;
//! # use serde::{Deserialize, Serialize};
//! # use settings::macros::*;
//! # use settings::*;
//! #[derive(Default, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
//! enum MyEnum {
//! #[default]
//! Unit,
//! Tuple(bool),
//! Struct { inner: f32 },
//! }
//!
//! impl settings_value::SettingsValue for MyEnum {}
//!
//! implement_setting_for_enum!(MyEnum, EnumSettingsGroup, SupportedPlatforms::ALL, SyncToCloud::Never, private: false, toml_path: "example.my_enum");
//!
//! define_settings_group!(EnumSettingsGroup, settings: [
//! my_enum: MyEnum,
//! ]);
//! ```
//!
//! ## Syncing a setting to the cloud.
//!
//! It's easy to declare a setting as being synced to the cloud by
//! setting the sync_to_cloud field to either Global or PerPlatform.
//! For either syncing option you can specify whether the setting
//! should be synced regardless of the current state of
//! CloudPreferencesSettings.
//!
//! ```
//! # use settings::macros::*;
//! # use settings::*;
//! define_settings_group!(OverrideSettingsGroup, settings: [
//! to_override: ToOverride {
//! type: bool,
//! default: false,
//! supported_platforms: SupportedPlatforms::ALL,
//! sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes),
//! private: false,
//! toml_path: "example.to_override",
//! },
//! ]);
//! ```
//!
//! # Using settings
//!
//! Once you've defined a setting, usage is straightforward:
//!
//! ```
//! # use warpui::*;
//! # use settings::macros::*;
//! # use settings::manager::SettingsManager;
//! # use settings::*;
//! # use warpui_extras::user_preferences;
//! define_settings_group!(ExampleGroup, settings: [
//! bool_setting: BoolSetting {
//! type: bool,
//! default: false,
//! supported_platforms: SupportedPlatforms::ALL,
//! sync_to_cloud: SyncToCloud::Never,
//! private: false,
//! toml_path: "example.bool_setting",
//! },
//! ]);
//!
//! App::test((), |mut app| async move {
//! // Initialize the underlying user preferences system.
//! app.add_singleton_model(move |_ctx| {
//! PublicPreferences::new(Box::<user_preferences::in_memory::InMemoryPreferences>::default())
//! });
//!
//! app.add_singleton_model(move |_ctx| {
//! PrivatePreferences::new(Box::<user_preferences::in_memory::InMemoryPreferences>::default())
//! });
//!
//! app.add_singleton_model(|_ctx| SettingsManager::default());
//!
//! // Register the settings group singleton model with the application.
//! ExampleGroup::register(&mut app);
//!
//! // Read the value:
//! app.read(|ctx| {
//! let value = ExampleGroup::handle(ctx)
//! .as_ref(ctx)
//! .bool_setting
//! .value();
//! });
//!
//! // Update the value:
//! app.update(|ctx| {
//! let _ = ExampleGroup::handle(ctx)
//! .update(ctx, |example_group, ctx| {
//! example_group.bool_setting.set_value(true, ctx);
//! });
//! });
//! });
//!
//! // Subscribe to the value changes from a view:
//! impl MyView {
//! pub fn new(ctx: &mut ViewContext<Self>) -> Self {
//! let handle = ExampleGroup::handle(ctx);
//! ctx.subscribe_to_model(&handle, |me, _handle, event, _ctx| {
//! match event {
//! ExampleGroupChangedEvent::BoolSetting { .. } => {
//! me.handle_changed_bool_setting();
//! }
//! }
//! });
//!
//! Self {}
//! }
//! }
//!
//! struct MyView {}
//!
//! impl Entity for MyView {
//! type Event = ();
//! }
//!
//! impl View for MyView {
//! fn ui_name() -> &'static str {
//! "MyView"
//! }
//!
//! fn render(&self, app_ctx: &AppContext) -> Box<dyn Element> {
//! elements::Rect::new().finish()
//! }
//! }
//!
//! impl MyView {
//! fn handle_changed_bool_setting(&self) {
//! println!("Bool setting changed.");
//! }
//! }
//! ```
pub use ::concat_idents::concat_idents;
#[macro_export]
macro_rules! define_setting {
// Convenience arm: with storage_key + toml_path + max_table_depth
($name:ident: $type:ty, default: $default:tt, supported_platforms: $supported_platforms: expr, group: $group:path, storage_key: $storage_key:expr, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr, toml_path: $toml_path:expr, max_table_depth: $mtd:literal $(, description: $desc:literal)? $(, feature_flag: $flag:path)?) => {
$crate::macros::define_setting!(@base $name: $type, default: $default, supported_platforms: $supported_platforms, group: $group, sync_to_cloud: $sync_to_cloud, private: $private, storage_key: $storage_key, toml_path_value: Some($toml_path), max_table_depth_value: $mtd $(, description: $desc)? $(, feature_flag: $flag)?);
};
// Convenience arm: with toml_path + max_table_depth (no explicit storage_key)
($name:ident: $type:ty, default: $default:tt, supported_platforms: $supported_platforms: expr, group: $group:path, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr, toml_path: $toml_path:expr, max_table_depth: $mtd:literal $(, description: $desc:literal)? $(, feature_flag: $flag:path)?) => {
$crate::macros::define_setting!(@base $name: $type, default: $default, supported_platforms: $supported_platforms, group: $group, sync_to_cloud: $sync_to_cloud, private: $private, storage_key: stringify!($name), toml_path_value: Some($toml_path), max_table_depth_value: $mtd $(, description: $desc)? $(, feature_flag: $flag)?);
};
// Convenience arm: with storage_key + toml_path
($name:ident: $type:ty, default: $default:tt, supported_platforms: $supported_platforms: expr, group: $group:path, storage_key: $storage_key:expr, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr, toml_path: $toml_path:expr $(, description: $desc:literal)? $(, feature_flag: $flag:path)?) => {
$crate::macros::define_setting!(@base $name: $type, default: $default, supported_platforms: $supported_platforms, group: $group, sync_to_cloud: $sync_to_cloud, private: $private, storage_key: $storage_key, toml_path_value: Some($toml_path) $(, description: $desc)? $(, feature_flag: $flag)?);
};
// Convenience arm: with toml_path (no explicit storage_key)
($name:ident: $type:ty, default: $default:tt, supported_platforms: $supported_platforms: expr, group: $group:path, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr, toml_path: $toml_path:expr $(, description: $desc:literal)? $(, feature_flag: $flag:path)?) => {
$crate::macros::define_setting!(@base $name: $type, default: $default, supported_platforms: $supported_platforms, group: $group, sync_to_cloud: $sync_to_cloud, private: $private, storage_key: stringify!($name), toml_path_value: Some($toml_path) $(, description: $desc)? $(, feature_flag: $flag)?);
};
// Convenience arm: without toml_path (private settings with explicit storage_key)
($name:ident: $type:ty, default: $default:tt, supported_platforms: $supported_platforms: expr, group: $group:path, storage_key: $storage_key:expr, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr $(, description: $desc:literal)? $(, feature_flag: $flag:path)?) => {
$crate::macros::define_setting!(@base $name: $type, default: $default, supported_platforms: $supported_platforms, group: $group, sync_to_cloud: $sync_to_cloud, private: $private, storage_key: $storage_key, toml_path_value: None::<&str> $(, description: $desc)? $(, feature_flag: $flag)?);
};
// Convenience arm: without toml_path (private settings with default storage_key)
($name:ident: $type:ty, default: $default:tt, supported_platforms: $supported_platforms: expr, group: $group:path, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr $(, description: $desc:literal)? $(, feature_flag: $flag:path)?) => {
$crate::macros::define_setting!(@base $name: $type, default: $default, supported_platforms: $supported_platforms, group: $group, sync_to_cloud: $sync_to_cloud, private: $private, storage_key: stringify!($name), toml_path_value: None::<&str> $(, description: $desc)? $(, feature_flag: $flag)?);
};
// Base arm: generates the struct and Setting impl
(@base $name:ident: $type:ty, default: $default:tt, supported_platforms: $supported_platforms: expr, group: $group:path, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr, storage_key: $storage_key:expr, toml_path_value: $toml_path_value:expr $(, max_table_depth_value: $mtd:literal)? $(, description: $desc:literal)? $(, feature_flag: $flag:path)?) => {
pub struct $name {
inner: $type,
is_explicitly_set: bool,
}
const _: () = {
let toml_path: Option<&str> = $toml_path_value;
if !$private && toml_path.is_none() {
panic!("non-private settings must specify a toml_path");
}
};
impl $crate::Setting for $name {
type Value = $type;
type Group = $group;
/// Creates a new setting with the given value, if provided, otherwise
/// uses the default value. Also tracks whether the setting was explicitly
/// set or not.
fn new(value: Option<Self::Value>) -> Self {
match value {
Some(v) => Self {
inner: v,
is_explicitly_set: true,
},
None => {
let default_value = Self::default_value();
log::debug!(
"Initializing {} to default value: {:?}",
Self::setting_name(),
default_value
);
Self {
inner: default_value,
is_explicitly_set: false,
}
}
}
}
fn setting_name() -> &'static str {
stringify!($name)
}
fn toml_path() -> Option<&'static str> {
$toml_path_value
}
fn storage_key() -> &'static str {
$storage_key
}
fn toml_key() -> &'static str {
const KEY: &str = match $toml_path_value {
Some(path) => $crate::toml_path_storage_key(path),
None => $storage_key,
};
KEY
}
fn hierarchy() -> Option<&'static str> {
const HIER: Option<&str> = match $toml_path_value {
Some(path) => $crate::toml_path_hierarchy(path),
None => None,
};
HIER
}
fn sync_to_cloud() -> $crate::SyncToCloud {
$sync_to_cloud
}
fn is_private() -> bool {
$private
}
fn supported_platforms() -> SupportedPlatforms {
$supported_platforms
}
fn value(&self) -> &Self::Value {
&self.inner
}
fn clear_value(
&mut self,
ctx: &mut warpui::ModelContext<Self::Group>,
) -> anyhow::Result<()> {
use $crate::ChangeEventReason;
Self::clear_from_preferences(Self::preferences_for_setting(ctx))?;
self.inner = self.validate(Self::default_value());
self.is_explicitly_set = false;
ctx.emit($crate::macros::concat_idents!(EventName = $group, ChangedEvent { EventName::$name {
change_event_reason: ChangeEventReason::Clear,
}}));
Ok(())
}
fn set_value_from_cloud_sync(
&mut self,
new_value: Self::Value,
ctx: &mut warpui::ModelContext<Self::Group>,
) -> anyhow::Result<()> {
use $crate::ChangeEventReason;
let changed_in_storage =
Self::write_to_preferences(&new_value, Self::preferences_for_setting(ctx))?;
if self.value() != &new_value || changed_in_storage {
self.inner = self.validate(new_value);
self.is_explicitly_set = true;
ctx.emit($crate::macros::concat_idents!(EventName = $group, ChangedEvent { EventName::$name {
change_event_reason: ChangeEventReason::CloudSync,
}}));
}
Ok(())
}
fn set_value(
&mut self,
new_value: Self::Value,
ctx: &mut warpui::ModelContext<Self::Group>,
) -> anyhow::Result<()> {
use $crate::ChangeEventReason;
let changed_in_storage =
Self::write_to_preferences(&new_value, Self::preferences_for_setting(ctx))?;
if self.value() != &new_value || changed_in_storage {
self.inner = self.validate(new_value);
self.is_explicitly_set = true;
ctx.emit($crate::macros::concat_idents!(EventName = $group, ChangedEvent { EventName::$name {
change_event_reason: ChangeEventReason::LocalChange,
}}));
}
Ok(())
}
fn load_value(
&mut self,
new_value: Self::Value,
explicitly_set: bool,
ctx: &mut warpui::ModelContext<Self::Group>,
) -> anyhow::Result<()> {
use $crate::ChangeEventReason;
let validated = self.validate(new_value);
if self.value() != &validated || self.is_explicitly_set != explicitly_set {
self.inner = validated;
self.is_explicitly_set = explicitly_set;
ctx.emit($crate::macros::concat_idents!(EventName = $group, ChangedEvent { EventName::$name {
change_event_reason: ChangeEventReason::LocalChange,
}}));
}
Ok(())
}
fn default_value() -> Self::Value {
$default
}
fn is_supported_on_current_platform(&self) -> bool {
$supported_platforms.matches_current_platform()
}
fn is_value_explicitly_set(&self) -> bool {
self.is_explicitly_set
}
$(
fn max_table_depth() -> Option<u32> {
Some($mtd)
}
)?
}
impl std::ops::Deref for $name {
type Target = $type;
fn deref(&self) -> &Self::Target {
use $crate::Setting;
self.value()
}
}
$crate::submit_schema_entry!(
private: $private,
description: $crate::_schema_default_description!($($desc)?),
toml_path_value: $toml_path_value,
fallback_storage_key: $storage_key,
supported_platforms: $supported_platforms,
feature_flag: $crate::_schema_default_flag!($($flag)?),
max_table_depth: $crate::_schema_default_max_table_depth!($($mtd)?),
default: $default,
value_type: $type
);
};
}
pub use define_setting;
#[macro_export]
macro_rules! maybe_define_setting {
// storage_key + toml_path + max_table_depth
($setting:ident, group: $group:path, { type: $value_type:ty, default: $default:expr, supported_platforms: $supported_platforms:expr, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr, storage_key: $key:expr, toml_path: $toml_path:expr, max_table_depth: $mtd:literal $(, description: $desc:literal)? $(, feature_flag: $flag:path)? $(,)? }) => {
$crate::macros::define_setting!(
$setting: $value_type,
default: $default,
supported_platforms: $supported_platforms,
group: $group,
storage_key: $key,
sync_to_cloud: $sync_to_cloud,
private: $private,
toml_path: $toml_path,
max_table_depth: $mtd
$(, description: $desc)?
$(, feature_flag: $flag)?
);
};
// toml_path + max_table_depth (no explicit storage_key)
($setting:ident, group: $group:path, { type: $value_type:ty, default: $default:expr, supported_platforms: $supported_platforms:expr, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr, toml_path: $toml_path:expr, max_table_depth: $mtd:literal $(, description: $desc:literal)? $(, feature_flag: $flag:path)? $(,)? }) => {
$crate::macros::define_setting!(
$setting: $value_type,
default: $default,
supported_platforms: $supported_platforms,
group: $group,
sync_to_cloud: $sync_to_cloud,
private: $private,
toml_path: $toml_path,
max_table_depth: $mtd
$(, description: $desc)?
$(, feature_flag: $flag)?
);
};
// storage_key + toml_path
($setting:ident, group: $group:path, { type: $value_type:ty, default: $default:expr, supported_platforms: $supported_platforms:expr, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr, storage_key: $key:expr, toml_path: $toml_path:expr $(, description: $desc:literal)? $(, feature_flag: $flag:path)? $(,)? }) => {
$crate::macros::define_setting!(
$setting: $value_type,
default: $default,
supported_platforms: $supported_platforms,
group: $group,
storage_key: $key,
sync_to_cloud: $sync_to_cloud,
private: $private,
toml_path: $toml_path
$(, description: $desc)?
$(, feature_flag: $flag)?
);
};
// toml_path only (no explicit storage_key)
($setting:ident, group: $group:path, { type: $value_type:ty, default: $default:expr, supported_platforms: $supported_platforms:expr, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr, toml_path: $toml_path:expr $(, description: $desc:literal)? $(, feature_flag: $flag:path)? $(,)? }) => {
$crate::macros::define_setting!(
$setting: $value_type,
default: $default,
supported_platforms: $supported_platforms,
group: $group,
sync_to_cloud: $sync_to_cloud,
private: $private,
toml_path: $toml_path
$(, description: $desc)?
$(, feature_flag: $flag)?
);
};
// storage_key only, no toml_path (private settings with custom key)
($setting:ident, group: $group:path, { type: $value_type:ty, default: $default:expr, supported_platforms: $supported_platforms:expr, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr, storage_key: $key:expr $(, description: $desc:literal)? $(, feature_flag: $flag:path)? $(,)? }) => {
$crate::macros::define_setting!(
$setting: $value_type,
default: $default,
supported_platforms: $supported_platforms,
group: $group,
storage_key: $key,
sync_to_cloud: $sync_to_cloud,
private: $private
$(, description: $desc)?
$(, feature_flag: $flag)?
);
};
// neither toml_path nor storage_key (private settings with default key)
($setting:ident, group: $group:path, { type: $value_type:ty, default: $default:expr, supported_platforms: $supported_platforms:expr, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr $(, description: $desc:literal)? $(, feature_flag: $flag:path)? $(,)? }) => {
$crate::macros::define_setting!(
$setting: $value_type,
default: $default,
supported_platforms: $supported_platforms,
group: $group,
sync_to_cloud: $sync_to_cloud,
private: $private
$(, description: $desc)?
$(, feature_flag: $flag)?
);
};
($setting:ident, group: $group:path) => {};
}
pub use maybe_define_setting;
#[macro_export]
macro_rules! implement_setting_for_enum {
// Base arm with all parameters
(@base $name:ident, $group:path, $supported_platforms:expr, $sync_to_cloud:expr, private: $private:expr, storage_key: $storage_key:expr, toml_path_value: $toml_path_value:expr $(, max_table_depth_value: $mtd:literal)? $(, description: $desc:literal)? $(, feature_flag: $flag:path)?) => {
const _: () = {
let toml_path: Option<&str> = $toml_path_value;
if !$private && toml_path.is_none() {
panic!("non-private settings must specify a toml_path");
}
};
impl $crate::Setting for $name {
type Value = $name;
type Group = $group;
fn new(value: Option<Self::Value>) -> Self {
match value {
Some(v) => v,
None => Self::default_value()
}
}
fn setting_name() -> &'static str {
stringify!($name)
}
fn toml_path() -> Option<&'static str> {
$toml_path_value
}
fn storage_key() -> &'static str {
$storage_key
}
fn toml_key() -> &'static str {
const KEY: &str = match $toml_path_value {
Some(path) => $crate::toml_path_storage_key(path),
None => $storage_key,
};
KEY
}
fn hierarchy() -> Option<&'static str> {
const HIER: Option<&str> = match $toml_path_value {
Some(path) => $crate::toml_path_hierarchy(path),
None => None,
};
HIER
}
fn sync_to_cloud() -> $crate::SyncToCloud {
$sync_to_cloud
}
fn is_private() -> bool {
$private
}
fn supported_platforms() -> SupportedPlatforms {
$supported_platforms
}
fn value(&self) -> &Self::Value {
&self
}
fn clear_value(
&mut self,
ctx: &mut warpui::ModelContext<Self::Group>,
) -> anyhow::Result<()> {
use $crate::ChangeEventReason;
Self::clear_from_preferences(Self::preferences_for_setting(ctx))?;
*self = self.validate(Self::default_value());
ctx.emit($crate::macros::concat_idents!(EventName = $group, ChangedEvent { EventName::$name {
change_event_reason: ChangeEventReason::Clear,
}}));
Ok(())
}
fn set_value_from_cloud_sync(
&mut self,
new_value: Self::Value,
ctx: &mut warpui::ModelContext<Self::Group>,
) -> anyhow::Result<()> {
use $crate::ChangeEventReason;
let changed_in_storage =
Self::write_to_preferences(&new_value, Self::preferences_for_setting(ctx))?;
if self.value() != &new_value || changed_in_storage {
*self = self.validate(new_value);
ctx.emit($crate::macros::concat_idents!(EventName = $group, ChangedEvent { EventName::$name {
change_event_reason: ChangeEventReason::CloudSync,
}}));
}
Ok(())
}
fn set_value(
&mut self,
new_value: Self::Value,
ctx: &mut warpui::ModelContext<Self::Group>,
) -> anyhow::Result<()> {
use $crate::ChangeEventReason;
let changed_in_storage =
Self::write_to_preferences(&new_value, Self::preferences_for_setting(ctx))?;
if self.value() != &new_value || changed_in_storage {
*self = self.validate(new_value);
ctx.emit($crate::macros::concat_idents!(EventName = $group, ChangedEvent { EventName::$name {
change_event_reason: ChangeEventReason::LocalChange,
}}));
}
Ok(())
}
fn load_value(
&mut self,
new_value: Self::Value,
_explicitly_set: bool,
ctx: &mut warpui::ModelContext<Self::Group>,
) -> anyhow::Result<()> {
use $crate::ChangeEventReason;
let validated = self.validate(new_value);
if self.value() != &validated {
*self = validated;
ctx.emit($crate::macros::concat_idents!(EventName = $group, ChangedEvent { EventName::$name {
change_event_reason: ChangeEventReason::LocalChange,
}}));
}
Ok(())
}
fn default_value() -> Self::Value {
Self::default()
}
fn is_supported_on_current_platform(&self) -> bool {
$supported_platforms.matches_current_platform()
}
fn is_value_explicitly_set(&self) -> bool {
// For enums using implement_setting_for_enum, we don't track explicit setting
// TODO(advait): deprecate this in favour of struct settings in a follow-up PR.
true
}
$(
fn max_table_depth() -> Option<u32> {
Some($mtd)
}
)?
}
$crate::submit_schema_entry!(
private: $private,
description: $crate::_schema_default_description!($($desc)?),
toml_path_value: $toml_path_value,
fallback_storage_key: $storage_key,
supported_platforms: $supported_platforms,
feature_flag: $crate::_schema_default_flag!($($flag)?),
max_table_depth: $crate::_schema_default_max_table_depth!($($mtd)?),
default: { <$name as Default>::default() },
value_type: $name
);
};
// toml_path + max_table_depth
($name:ident, $group:path, $supported_platforms:expr, $sync_to_cloud:expr, private: $private:expr, toml_path: $toml_path:expr, max_table_depth: $mtd:literal $(, description: $desc:literal)? $(, feature_flag: $flag:path)? $(,)?) => {
$crate::macros::implement_setting_for_enum!(@base $name, $group, $supported_platforms, $sync_to_cloud, private: $private, storage_key: stringify!($name), toml_path_value: Some($toml_path), max_table_depth_value: $mtd $(, description: $desc)? $(, feature_flag: $flag)?);
};
// toml_path only
($name:ident, $group:path, $supported_platforms:expr, $sync_to_cloud:expr, private: $private:expr, toml_path: $toml_path:expr $(, description: $desc:literal)? $(, feature_flag: $flag:path)? $(,)?) => {
$crate::macros::implement_setting_for_enum!(@base $name, $group, $supported_platforms, $sync_to_cloud, private: $private, storage_key: stringify!($name), toml_path_value: Some($toml_path) $(, description: $desc)? $(, feature_flag: $flag)?);
};
// neither (private settings)
($name:ident, $group:path, $supported_platforms:expr, $sync_to_cloud:expr, private: $private:expr $(, description: $desc:literal)? $(, feature_flag: $flag:path)? $(,)?) => {
$crate::macros::implement_setting_for_enum!(@base $name, $group, $supported_platforms, $sync_to_cloud, private: $private, storage_key: stringify!($name), toml_path_value: None::<&str> $(, description: $desc)? $(, feature_flag: $flag)?);
};
}
pub use implement_setting_for_enum;
/// By defining a trait that the settings groups implement, we're able to call
/// methods like `is_supported_on_current_platform()` without knowing the exact settings
/// group we're operating on at compile time.
pub trait SettingSection {
fn is_supported_on_current_platform(&self) -> bool;
}
#[macro_export]
macro_rules! define_settings_group {
($group:ident, settings: [$($var:ident: $setting:ident $({ type: $value_type:ty, default: $default:expr, supported_platforms: $supported_platforms:expr, sync_to_cloud: $sync_to_cloud:expr, private: $private:expr $(, storage_key: $storage_key:literal)? $(, toml_path: $toml_path:literal)? $(, max_table_depth: $mtd:literal)? $(, description: $desc:literal)? $(, feature_flag: $flag:path)? $(,)? })? $(,)? )*]) => {
$(
$crate::macros::maybe_define_setting!($setting, group: $group $(, { type: $value_type, default: $default, supported_platforms: $supported_platforms, sync_to_cloud: $sync_to_cloud, private: $private $(, storage_key: $storage_key)? $(, toml_path: $toml_path)? $(, max_table_depth: $mtd)? $(, description: $desc)? $(, feature_flag: $flag)? })?);
)*
pub struct $group {
$(
pub $var: $setting,
)*
}
impl $group {
#[allow(dead_code)]
fn new_from_storage(ctx: &mut warpui::ModelContext<Self>) -> Self {
use $crate::Setting;
Self {
$(
$var: <$setting>::new_from_storage(ctx),
)*
}
}
#[cfg(any(test, feature = "integration_tests"))]
#[allow(dead_code)]
pub fn new_with_defaults(_ctx: &mut warpui::ModelContext<Self>) -> Self {
use $crate::Setting;
Self {
$(
$var: <$setting>::new(None),
)*
}
}
#[allow(dead_code)]
pub fn register(ctx: &mut (impl warpui::GetSingletonModelHandle + warpui::AddSingletonModel + warpui::UpdateModel)) -> warpui::ModelHandle<Self> {
let settings_group = ctx.add_singleton_model(|ctx| {
Self::new_from_storage(ctx)
});
// Wire up settings event update functions for all settings
$(
$crate::macros::register_settings_events!(
$group,
$var,
$setting,
settings_group.clone(),
ctx
);
)*
settings_group
}
}
impl $crate::macros::SettingSection for $group {
/// If any of the settings in the setting group are supported, then the group is supported.
/// If none of the settings in the group are supported, then the group is not supported.
fn is_supported_on_current_platform(&self) -> bool {
use $crate::Setting;
$(
if self.$var.is_supported_on_current_platform() {
return true;
}
)*
false
}
}
$crate::macros::concat_idents!(EventName = $group, ChangedEvent {
use $crate::ChangeEventReason;
#[derive(Debug)]
#[allow(clippy::enum_variant_names)]
pub enum EventName {
$(
$setting {
#[allow(dead_code)]
change_event_reason: ChangeEventReason,
},
)*
}
impl warpui::Entity for $group {
type Event = EventName;
}
});
impl warpui::SingletonEntity for $group {}
};
}
pub use define_settings_group;
/// Registers listeners for settings events that get piped through the
/// SettingsManager. These events allow for anyone to listen to settings
/// changes based on storage key rather than individual settings models.
#[macro_export]
macro_rules! register_settings_events {
( $group:ident, $var:ident, $setting:ident, $handle:expr, $ctx:expr ) => {{
$crate::macros::generate_settings_event_fn!($group, $var, $setting);
concat_idents::concat_idents!(fn_name = register_events_for_, $setting, {
fn_name($handle, $ctx);
});
}};
}
pub use register_settings_events;
/// Generates a function that can be used to register event handlers for a
/// for letting the SettingsManager know when a setting has been updated.
/// Used for managing the flow of events for local and cloud settings.
#[macro_export]
macro_rules! generate_settings_event_fn {
( $group:ident, $var:ident, $setting:ident ) => {
concat_idents::concat_idents!(fn_name = register_events_for_, $setting, {
#[allow(dead_code)]
#[allow(non_snake_case)]
fn fn_name(
settings_group: warpui::ModelHandle<$group>,
ctx: &mut (
impl warpui::GetSingletonModelHandle
+ warpui::AddSingletonModel
+ warpui::UpdateModel
),
) {
use anyhow::anyhow;
use serde_json;
use warpui::SingletonEntity;
use $crate::Setting as _;
use $crate::manager::{SettingsEvent, SettingsManager};
SettingsManager::handle(ctx).update(ctx, |manager, ctx| {
// Propagate per settings change events through the SettingsManager
ctx.subscribe_to_model(&settings_group, |_manager, _, ctx| {
ctx.emit(SettingsEvent::LocalPreferencesUpdated {
storage_key: $setting::storage_key().to_string(),
sync_to_cloud: $setting::sync_to_cloud(),
});
});
// Register callbacks for updating individual settings model by storage key
let settings_group_update_clone = settings_group.clone();
let settings_group_reset_clone = settings_group.clone();
let settings_group_load_clone = settings_group.clone();
let settings_group_is_syncable_clone = settings_group.clone();
let serialized_default_value =
serde_json::to_string(&$setting::default_value())
.expect("default should serialize");
let file_serialized_default_value = {
use $crate::_settings_value::SettingsValue as _;
let file_value = $setting::default_value().to_file_value();
serde_json::to_string(&file_value)
.expect("default file value should serialize")
};
manager.register_setting(
$setting::storage_key(),
$setting::sync_to_cloud(),
$setting::supported_platforms(),
serialized_default_value,
file_serialized_default_value,
$setting::hierarchy(),
$setting::toml_key(),
$setting::max_table_depth(),
$setting::is_private(),
move |value, from_cloud_sync, ctx| {
use $crate::_settings_value::SettingsValue as _;
// Try SettingsValue first (handles snake_case enums etc.),
// then fall back to serde for cloud sync values.
let value = serde_json::from_str::<serde_json::Value>(&value)
.ok()
.and_then(|json_val| {
<$setting as $crate::Setting>::Value::from_file_value(&json_val)
})
.or_else(|| serde_json::from_str(&value).ok());
let Some(value) = value else {
return Err(anyhow!(
"Failed to parse updated value for setting {}: Not updating",
$setting::storage_key()
));
};
settings_group_update_clone.update(ctx, |settings_group, ctx| {
if from_cloud_sync {
settings_group.$var.set_value_from_cloud_sync(value, ctx)
} else {
settings_group.$var.set_value(value, ctx)
}
})
},
move |ctx| {
settings_group_reset_clone.update(ctx, |settings_group, ctx| {
if settings_group
.$var
.is_setting_syncable_on_current_platform(true)
{
log::debug!(
"Clearing cloud synced setting from local storage: {}",
$setting::storage_key()
);
settings_group.$var.clear_value(ctx)
} else {
Ok(())
}
})
},
move |value, explicitly_set, ctx| {
use $crate::_settings_value::SettingsValue as _;
let value = serde_json::from_str::<serde_json::Value>(&value)
.ok()
.and_then(|json_val| {
<$setting as $crate::Setting>::Value::from_file_value(&json_val)
})
.or_else(|| serde_json::from_str(&value).ok());
let Some(value) = value else {
return Err(anyhow!(
"Failed to parse loaded value for setting {}: Not loading",
$setting::storage_key()
));
};
settings_group_load_clone.update(ctx, |settings_group, ctx| {
settings_group.$var.load_value(value, explicitly_set, ctx)
})
},
|left, right| {
use $crate::_settings_value::SettingsValue as _;
let parse =
|s: &str| -> anyhow::Result<<$setting as $crate::Setting>::Value> {
let json_val = serde_json::from_str::<serde_json::Value>(s)?;
<$setting as $crate::Setting>::Value::from_file_value(&json_val)
.or_else(|| serde_json::from_str(s).ok())
.ok_or_else(|| {
anyhow!(
"Failed to parse value for {}",
$setting::storage_key()
)
})
};
let left_setting = $setting::new(Some(parse(left)?));
let right_setting = $setting::new(Some(parse(right)?));
Ok(left_setting.value() == right_setting.value())
},
move |ctx| {
settings_group_is_syncable_clone
.as_ref(ctx)
.$var
.current_value_is_syncable()
},
);
});
}
});
};
}
pub use generate_settings_event_fn;
#[cfg(test)]
#[path = "macros_tests.rs"]
mod macros_tests;
File diff suppressed because it is too large Load Diff
+435
View File
@@ -0,0 +1,435 @@
use std::collections::HashMap;
use std::ops::Deref;
use anyhow::{Result, anyhow};
use warpui::{AppContext, Entity, ModelContext, SingletonEntity};
use warpui_extras::user_preferences::UserPreferences;
use super::PrivatePreferences;
use super::{RespectUserSyncSetting, SupportedPlatforms, SyncToCloud};
type UpdateFn = Box<dyn FnMut(String, bool, &mut AppContext) -> Result<()>>;
type ClearFn = Box<dyn FnMut(&mut AppContext) -> Result<()>>;
/// Loads a value into memory without persisting. Parameters: (serialized_value, explicitly_set, ctx).
type LoadFn = Box<dyn FnMut(String, bool, &mut AppContext) -> Result<()>>;
type EqualsFn = Box<dyn Fn(&str, &str) -> Result<bool>>;
type IsSyncableFn = Box<dyn Fn(&AppContext) -> bool>;
/// Intermediate data collected for each setting during reload, before
/// calling the mutable `load_fns`.
struct SettingReloadEntry {
storage_key: String,
read_value: Option<String>,
serialized_default: String,
toml_key: &'static str,
hierarchy: Option<&'static str>,
}
#[derive(Debug)]
struct SettingsInfo {
sync_to_cloud: SyncToCloud,
supported_platforms: SupportedPlatforms,
serialized_default_value: String,
/// The default value serialized using the `SettingsValue` trait
/// for the settings file.
file_serialized_default_value: String,
hierarchy: Option<&'static str>,
/// The key used in the TOML settings file (last segment of `toml_path`).
/// For settings without a `toml_path`, this equals the storage key.
toml_key: &'static str,
/// The maximum number of TOML section-table levels to use when rendering
/// this setting's value in the settings file. `None` means unlimited.
max_table_depth: Option<u32>,
/// Whether this setting is private (not shown in the user-visible settings file).
is_private: bool,
}
/// Provides an interface for listening for settings events based on
/// storage key and also for updating settings based on storage key.
///
/// Practically speaking this struct is used for keeping local and
/// cloud preferences in sync with each other without creating a direct
/// dependency between the define_settings_group macros and the
/// cloud preferences syncing machinery.
#[derive(Default)]
pub struct SettingsManager {
/// Settings info by storage key
settings: HashMap<String, SettingsInfo>,
/// Functions for updating settings by storage key
update_fns: HashMap<String, UpdateFn>,
/// Functions for clearing settings from local storage (which also effectively resets them to their default value)
clear_fns: HashMap<String, ClearFn>,
/// Functions for loading a value into memory without persisting to storage.
/// Used during hot-reload to avoid write-back loops with the file watcher.
load_fns: HashMap<String, LoadFn>,
/// Functions for checking whether two serialized settings
/// with the same storage key have equal values. Note that
/// we need this because we can't just compare the deserialized
/// or raw json values for equality. This fails because things
/// like HashSet serialize to ordered json arrays, but don't have
/// a defined order.
equals_fns: HashMap<String, EqualsFn>,
/// Functions for checking whether a setting is currently syncable
/// based on its value. Settings that want custom logic here should define
/// the current_value_is_syncable method.
is_syncable_fns: HashMap<String, IsSyncableFn>,
}
pub enum SettingsEvent {
LocalPreferencesUpdated {
storage_key: String,
sync_to_cloud: SyncToCloud,
},
}
impl SettingsManager {
/// Registers a function that updates a a setting with the given storage key
/// to have a new value. Also tracks whether that storage key is for a cloud-synced
/// setting and what platforms it's supported on.
#[allow(clippy::too_many_arguments)]
pub fn register_setting(
&mut self,
storage_key: &str,
sync_to_cloud: SyncToCloud,
supported_platforms: SupportedPlatforms,
serialized_default_value: String,
file_serialized_default_value: String,
hierarchy: Option<&'static str>,
toml_key: &'static str,
max_table_depth: Option<u32>,
is_private: bool,
update_fn: impl FnMut(String, bool, &mut AppContext) -> Result<()> + 'static,
clear_fn: impl FnMut(&mut AppContext) -> Result<()> + 'static,
load_fn: impl FnMut(String, bool, &mut AppContext) -> Result<()> + 'static,
equals_fn: impl Fn(&str, &str) -> Result<bool> + 'static,
is_syncable_fn: impl Fn(&AppContext) -> bool + 'static,
) {
self.update_fns
.insert(storage_key.to_owned(), Box::new(update_fn));
self.clear_fns
.insert(storage_key.to_owned(), Box::new(clear_fn));
self.load_fns
.insert(storage_key.to_owned(), Box::new(load_fn));
self.equals_fns
.insert(storage_key.to_owned(), Box::new(equals_fn));
self.is_syncable_fns
.insert(storage_key.to_owned(), Box::new(is_syncable_fn));
self.settings.insert(
storage_key.to_owned(),
SettingsInfo {
supported_platforms,
sync_to_cloud,
serialized_default_value,
file_serialized_default_value,
hierarchy,
toml_key,
max_table_depth,
is_private,
},
);
}
/// Clears all cloud synced settings from the user defaults. Does not affect their cloud state.
/// Typically called when a user logs out. Note that the caller is responsible for ensuring that
/// cloud preferences are enabled before calling this.
pub fn clear_cloud_settings_local_state(
&mut self,
ctx: &mut ModelContext<Self>,
) -> Vec<anyhow::Error> {
self.clear_fns
.values_mut()
.filter_map(|clear_fn| clear_fn(ctx).err())
.collect::<Vec<anyhow::Error>>()
}
/// Returns all registered storage keys.
pub fn all_storage_keys(&self) -> impl Iterator<Item = &String> {
self.settings.keys()
}
/// Returns the storage keys for all public (non-private) settings.
pub fn public_storage_keys(&self) -> impl Iterator<Item = &str> + '_ {
self.settings
.iter()
.filter(|(_, info)| !info.is_private)
.map(|(key, _)| key.as_str())
}
/// Returns whether the setting with the given storage key should be synced even if the
/// user has disabled syncing.
pub fn sync_regardless_of_users_syncing_setting(&self, storage_key: &str) -> bool {
self.settings
.get(storage_key)
.map(|info| {
matches!(
info.sync_to_cloud,
SyncToCloud::Globally(RespectUserSyncSetting::No)
| SyncToCloud::PerPlatform(RespectUserSyncSetting::No)
)
})
.unwrap_or(false)
}
/// Returns whether the setting with the given storage key has a value that is currently
/// syncable to the cloud.
pub fn is_current_value_syncable(&self, storage_key: &str, app: &AppContext) -> Result<bool> {
self.is_syncable_fns
.get(storage_key)
.map(|cb| Ok(cb(app)))
.unwrap_or_else(|| {
Err(anyhow!(
"no is_syncable fn registered for storage key {}",
storage_key
))
})
}
/// Returns the cloud_syncing_mode for the given storage key.
pub fn cloud_syncing_mode_for_storage_key(&self, storage_key: &str) -> Option<SyncToCloud> {
self.settings
.get(storage_key)
.map(|info| info.sync_to_cloud)
}
/// Returns the supported platforms for this storage key.
pub fn supported_platforms_for_storage_key(
&self,
storage_key: &str,
) -> Option<&SupportedPlatforms> {
self.settings
.get(storage_key)
.map(|info| &info.supported_platforms)
}
/// Returns whether the setting with the given storage key is private.
pub fn is_private_for_storage_key(&self, storage_key: &str) -> bool {
self.settings
.get(storage_key)
.map(|info| info.is_private)
.unwrap_or(false)
}
/// Reads a setting's current local value from the correct preferences
/// backend, routing private settings to the private store and public
/// settings to the main (potentially TOML-backed) store.
pub fn read_local_setting_value(
&self,
storage_key: &str,
ctx: &AppContext,
) -> Result<Option<String>> {
let private: &dyn UserPreferences =
<PrivatePreferences as SingletonEntity>::as_ref(ctx).deref();
let prefs: &dyn UserPreferences = if self.is_private_for_storage_key(storage_key) {
private
} else if super::is_settings_file_enabled() {
<super::PublicPreferences as SingletonEntity>::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.
private
};
let info = self.settings.get(storage_key);
let key = if prefs.is_settings_file() {
info.map_or(storage_key, |i| i.toml_key)
} else {
storage_key
};
let hierarchy = info.and_then(|i| i.hierarchy);
prefs
.read_value_with_hierarchy(key, hierarchy)
.map_err(|e| anyhow!("failed to read setting {storage_key}: {e}"))
}
/// Updates the setting with the given storage key to a new value, returning
/// a result indicating whether the update was successful.
pub fn update_setting_with_storage_key(
&mut self,
storage_key: &str,
new_value: String,
from_cloud_sync: bool,
ctx: &mut AppContext,
) -> Result<()> {
self.update_fns
.get_mut(storage_key)
.map(|update_fn| update_fn(new_value, from_cloud_sync, ctx))
.unwrap_or_else(|| {
Err(anyhow!(
"no update fn registered for storage key {}",
storage_key
))
})
}
/// Returns whether the two serialized settings with the given storage key
/// have equal values. This isn't a direct string comparison, or even a comparison
/// of JSON values, but a comparison using the Setting.value()'s equality method.
pub fn are_equal_settings(&self, storage_key: &str, left: &str, right: &str) -> Result<bool> {
self.equals_fns
.get(storage_key)
.map(|equality_fn| equality_fn(left, right))
.unwrap_or_else(|| {
Err(anyhow!(
"no equals fn registered for storage key {}",
storage_key
))
})
}
pub fn default_values(&self) -> impl Iterator<Item = (String, String)> + '_ {
self.settings
.iter()
.map(|(key, info)| (key.clone(), info.serialized_default_value.clone()))
}
/// Loads a setting value into memory without persisting to storage.
///
/// `explicitly_set` indicates whether the value came from the file (`true`)
/// or is a default for an absent key (`false`).
pub fn load_setting(
&mut self,
storage_key: &str,
value: String,
explicitly_set: bool,
ctx: &mut AppContext,
) -> Result<()> {
self.load_fns
.get_mut(storage_key)
.map(|load_fn| load_fn(value, explicitly_set, ctx))
.unwrap_or_else(|| {
Err(anyhow!(
"no load fn registered for storage key {}",
storage_key
))
})
}
/// Reloads all public (non-private) settings from the preferences backend.
///
/// Call this after the backing store has been refreshed from disk (e.g.
/// via [`UserPreferences::reload_from_disk`]) so that every in-memory
/// setting picks up the new values.
///
/// Uses [`load_setting`](Self::load_setting) to update in-memory values
/// without writing back to the preferences backend, avoiding write-back
/// loops with the file watcher. Keys present in the file are loaded with
/// `explicitly_set = true`; absent keys are reset to their default with
/// `explicitly_set = false`.
/// Returns the storage keys of any settings that failed to load.
pub fn reload_all_public_settings(&mut self, ctx: &mut AppContext) -> Vec<String> {
let prefs = <super::PublicPreferences as SingletonEntity>::as_ref(ctx).as_preferences();
// Read every non-private setting from the (now-reloaded) preferences,
// collecting them up-front to release the immutable borrow on
// `self.settings` before calling the mutable `load_fns`.
let updates: Vec<SettingReloadEntry> = self
.settings
.iter()
.filter(|(_, info)| !info.is_private)
.map(|(key, info)| {
let read_value =
match prefs.read_value_with_hierarchy(info.toml_key, info.hierarchy) {
Ok(v) => v,
Err(err) => {
log::warn!("Failed to read setting {key} during reload: {err}");
None
}
};
SettingReloadEntry {
storage_key: key.clone(),
read_value,
serialized_default: info.serialized_default_value.clone(),
toml_key: info.toml_key,
hierarchy: info.hierarchy,
}
})
.collect();
let mut failed_keys = Vec::new();
for entry in updates {
let (effective_value, explicitly_set) = match entry.read_value {
Some(v) => (v, true),
None => (entry.serialized_default, false),
};
if let Err(err) =
self.load_setting(&entry.storage_key, effective_value, explicitly_set, ctx)
{
log::warn!("Failed to reload setting {}: {err}", entry.storage_key);
// Re-inhibit this key so writes don't overwrite the
// user's broken-but-fixable value in the file.
let prefs =
<super::PublicPreferences as SingletonEntity>::as_ref(ctx).as_preferences();
prefs.inhibit_writes_for_key(entry.toml_key, entry.hierarchy);
failed_keys.push(entry.toml_key.to_string());
}
}
failed_keys
}
/// Validates all public settings by reading each from the preferences
/// backend and attempting deserialization. Returns the storage keys of
/// any settings whose stored value cannot be deserialized.
///
/// This is a read-only check — it does not modify in-memory state.
/// Call after [`register_all_settings`] on startup to detect invalid
/// values in the settings file.
pub fn validate_all_public_settings(&self, ctx: &AppContext) -> Vec<String> {
let prefs = <super::PublicPreferences as SingletonEntity>::as_ref(ctx).as_preferences();
self.settings
.iter()
.filter(|(_, info)| !info.is_private)
.filter_map(|(key, info)| {
let value = prefs
.read_value_with_hierarchy(info.toml_key, info.hierarchy)
.ok()
.flatten()?;
// Try deserializing through the equals_fn — if serde_json
// can't parse both sides, the value is invalid.
if let Some(equals_fn) = self.equals_fns.get(key)
&& equals_fn(&value, &value).is_err()
{
return Some(info.toml_key.to_string());
}
None
})
.collect()
}
/// Returns all registered settings with their toml key, serialized
/// default value (in file format), hierarchy path, and max table depth,
/// for use when writing the user-visible settings file.
pub fn default_values_for_settings_file(
&self,
) -> impl Iterator<Item = (&str, &str, Option<&'static str>, Option<u32>)> + '_ {
self.settings
.iter()
.filter(|(_, info)| !info.is_private)
.map(|(_, info)| {
(
info.toml_key,
info.file_serialized_default_value.as_str(),
info.hierarchy,
info.max_table_depth,
)
})
}
}
impl Entity for SettingsManager {
type Event = SettingsEvent;
}
/// Mark SettingsManager as global application state.
impl SingletonEntity for SettingsManager {}
+635
View File
@@ -0,0 +1,635 @@
use warpui::SingletonEntity;
use crate::manager::SettingsManager;
use crate::{Setting, SupportedPlatforms, SyncToCloud};
use crate::*;
define_settings_group!(TestSettings, settings: [
never_sync_setting: SimpleSetting {
type: bool,
default: false,
supported_platforms: SupportedPlatforms::ALL,
sync_to_cloud: SyncToCloud::Never,
private: false,
toml_path: "test.simple_setting",
},
global_setting: GlobalSetting {
type: bool,
default: false,
supported_platforms: SupportedPlatforms::ALL,
sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes),
private: false,
toml_path: "test.global_setting",
},
global_setting_no_respect: GlobalSettingNoRespect {
type: bool,
default: false,
supported_platforms: SupportedPlatforms::ALL,
sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::No),
private: false,
toml_path: "test.global_setting_no_respect",
},
per_platform_setting: PerPlatformSetting {
type: bool,
default: false,
supported_platforms: SupportedPlatforms::MAC,
sync_to_cloud: SyncToCloud::PerPlatform(RespectUserSyncSetting::Yes),
private: false,
toml_path: "test.per_platform_setting",
},
per_platform_setting_no_respect: PerPlatformSettingNoRespect {
type: bool,
default: false,
supported_platforms: SupportedPlatforms::MAC,
sync_to_cloud: SyncToCloud::PerPlatform(RespectUserSyncSetting::No),
private: false,
toml_path: "test.per_platform_setting_no_respect",
},
]);
pub fn init_and_register_user_preferences(ctx: &mut AppContext) {
ctx.add_singleton_model(move |_| {
crate::PublicPreferences::new(Box::<
warpui_extras::user_preferences::in_memory::InMemoryPreferences,
>::default())
});
ctx.add_singleton_model(move |_| {
crate::PrivatePreferences::new(Box::<
warpui_extras::user_preferences::in_memory::InMemoryPreferences,
>::default())
});
}
#[test]
fn test_is_setting_syncable_on_current_platform() {
warpui::App::test((), |mut app| async move {
app.update(init_and_register_user_preferences);
app.add_singleton_model(|_| SettingsManager::default());
// Register our TestSettings settings group with the app.
TestSettings::register(&mut app);
app.read(|app| {
let settings = TestSettings::as_ref(app);
assert!(
!settings
.never_sync_setting
.is_setting_syncable_on_current_platform(true)
);
assert!(
!settings
.never_sync_setting
.is_setting_syncable_on_current_platform(false)
);
assert!(
settings
.global_setting
.is_setting_syncable_on_current_platform(true)
);
assert!(
!settings
.global_setting
.is_setting_syncable_on_current_platform(false)
);
assert!(
settings
.global_setting_no_respect
.is_setting_syncable_on_current_platform(true)
);
assert!(
settings
.global_setting_no_respect
.is_setting_syncable_on_current_platform(false)
);
if cfg!(target_os = "macos") {
assert!(
settings
.per_platform_setting
.is_setting_syncable_on_current_platform(true)
);
assert!(
!settings
.per_platform_setting
.is_setting_syncable_on_current_platform(false)
);
assert!(
settings
.per_platform_setting_no_respect
.is_setting_syncable_on_current_platform(true)
);
assert!(
settings
.per_platform_setting_no_respect
.is_setting_syncable_on_current_platform(false)
);
} else {
assert!(
!settings
.per_platform_setting
.is_setting_syncable_on_current_platform(true)
);
assert!(
!settings
.per_platform_setting
.is_setting_syncable_on_current_platform(false)
);
assert!(
!settings
.per_platform_setting_no_respect
.is_setting_syncable_on_current_platform(true)
);
assert!(
!settings
.per_platform_setting_no_respect
.is_setting_syncable_on_current_platform(false)
);
}
});
});
}
mod reload_all_public_settings_tests {
use warpui::SingletonEntity;
use crate::manager::SettingsManager;
use crate::{Setting, SupportedPlatforms, SyncToCloud};
use crate::*;
define_settings_group!(ReloadTestSettings, settings: [
public_flag: PublicFlag {
type: bool,
default: false,
supported_platforms: SupportedPlatforms::ALL,
sync_to_cloud: SyncToCloud::Never,
private: false,
toml_path: "test.public_flag",
},
private_flag: PrivateFlag {
type: bool,
default: false,
supported_platforms: SupportedPlatforms::ALL,
sync_to_cloud: SyncToCloud::Never,
private: true,
},
]);
fn init_prefs(ctx: &mut AppContext) {
ctx.add_singleton_model(move |_| -> crate::PublicPreferences {
crate::PublicPreferences::new(Box::<
warpui_extras::user_preferences::in_memory::InMemoryPreferences,
>::default())
});
ctx.add_singleton_model(move |_| -> crate::PrivatePreferences {
crate::PrivatePreferences(Box::<
warpui_extras::user_preferences::in_memory::InMemoryPreferences,
>::default())
});
}
/// Verifies that `reload_all_public_settings` picks up values present
/// in the preferences backend.
#[test]
fn test_loads_present_keys() {
warpui::App::test((), |mut app| async move {
app.update(init_prefs);
app.add_singleton_model(|_| SettingsManager::default());
ReloadTestSettings::register(&mut app);
// Write a non-default value directly to the public backend.
app.update(|ctx| {
let public =
<crate::PublicPreferences as SingletonEntity>::as_ref(ctx).as_preferences();
public
.write_value("public_flag", "true".to_string())
.unwrap();
});
// Reload — the in-memory value should update.
app.update(|ctx| {
SettingsManager::handle(ctx).update(ctx, |manager, ctx| {
manager.reload_all_public_settings(ctx);
});
});
app.read(|ctx| {
assert!(
*ReloadTestSettings::as_ref(ctx).public_flag.value(),
"reload should load present key from preferences"
);
});
});
}
/// Verifies that absent keys are reset to their default values during
/// reload (the key-deletion scenario).
#[test]
fn test_resets_absent_keys_to_defaults() {
warpui::App::test((), |mut app| async move {
app.update(init_prefs);
app.add_singleton_model(|_| SettingsManager::default());
ReloadTestSettings::register(&mut app);
// Set a non-default value via set_value (updates both in-memory and storage).
app.update(|ctx| {
ReloadTestSettings::handle(ctx).update(ctx, |settings, ctx| {
settings.public_flag.set_value(true, ctx).unwrap();
});
});
// Remove the key from storage (simulates the user deleting a key from
// the settings file, then reload_from_disk picking up the deletion).
app.update(|ctx| {
let public =
<crate::PublicPreferences as SingletonEntity>::as_ref(ctx).as_preferences();
public.remove_value("public_flag").unwrap();
});
// Reload — the in-memory value should reset to default.
app.update(|ctx| {
SettingsManager::handle(ctx).update(ctx, |manager, ctx| {
manager.reload_all_public_settings(ctx);
});
});
app.read(|ctx| {
assert_eq!(
*ReloadTestSettings::as_ref(ctx).public_flag.value(),
PublicFlag::default_value(),
"absent key should be reset to default"
);
});
});
}
/// Verifies that reload does NOT write absent keys back to storage.
/// This is the property that prevents the infinite watcher loop.
#[test]
fn test_absent_keys_are_not_written_back() {
warpui::App::test((), |mut app| async move {
app.update(init_prefs);
app.add_singleton_model(|_| SettingsManager::default());
ReloadTestSettings::register(&mut app);
// Set a non-default value, then remove from storage.
app.update(|ctx| {
ReloadTestSettings::handle(ctx).update(ctx, |settings, ctx| {
settings.public_flag.set_value(true, ctx).unwrap();
});
});
app.update(|ctx| {
let public =
<crate::PublicPreferences as SingletonEntity>::as_ref(ctx).as_preferences();
public.remove_value("public_flag").unwrap();
});
// Reload.
app.update(|ctx| {
SettingsManager::handle(ctx).update(ctx, |manager, ctx| {
manager.reload_all_public_settings(ctx);
});
});
// Storage should still be empty — reload must not write back.
app.read(|ctx| {
let public =
<crate::PublicPreferences as SingletonEntity>::as_ref(ctx).as_preferences();
let stored = public.read_value("public_flag").unwrap();
assert!(
stored.is_none(),
"reload should not write absent keys back to storage"
);
});
});
}
/// Verifies that `reload_all_public_settings` returns the storage keys
/// of settings that fail to deserialize (invalid value in file).
#[test]
fn test_reload_returns_failed_keys_for_invalid_values() {
warpui::App::test((), |mut app| async move {
app.update(init_prefs);
app.add_singleton_model(|_| SettingsManager::default());
ReloadTestSettings::register(&mut app);
// Write an invalid value for the public bool setting.
app.update(|ctx| {
let public =
<crate::PublicPreferences as SingletonEntity>::as_ref(ctx).as_preferences();
public
.write_value("public_flag", "not_a_bool".to_string())
.unwrap();
});
// Reload — should return the failed key.
let failed_keys = app.update(|ctx| {
SettingsManager::handle(ctx)
.update(ctx, |manager, ctx| manager.reload_all_public_settings(ctx))
});
assert_eq!(
failed_keys,
vec!["public_flag".to_string()],
"reload should return the key that failed to deserialize"
);
// The setting should remain at its default (not crash).
app.read(|ctx| {
assert_eq!(
*ReloadTestSettings::as_ref(ctx).public_flag.value(),
PublicFlag::default_value(),
"setting should remain at default after failed reload"
);
});
});
}
/// Verifies that `reload_all_public_settings` returns an empty vec
/// when all values are valid.
#[test]
fn test_reload_returns_empty_vec_on_success() {
warpui::App::test((), |mut app| async move {
app.update(init_prefs);
app.add_singleton_model(|_| SettingsManager::default());
ReloadTestSettings::register(&mut app);
// Write a valid value.
app.update(|ctx| {
let public =
<crate::PublicPreferences as SingletonEntity>::as_ref(ctx).as_preferences();
public
.write_value("public_flag", "true".to_string())
.unwrap();
});
let failed_keys = app.update(|ctx| {
SettingsManager::handle(ctx)
.update(ctx, |manager, ctx| manager.reload_all_public_settings(ctx))
});
assert!(
failed_keys.is_empty(),
"reload should return empty vec when all values are valid"
);
});
}
/// Verifies that `validate_all_public_settings` detects invalid stored
/// values without modifying in-memory state.
#[test]
fn test_validate_detects_invalid_values() {
warpui::App::test((), |mut app| async move {
crate::set_settings_file_enabled(true);
app.update(init_prefs);
app.add_singleton_model(|_| SettingsManager::default());
ReloadTestSettings::register(&mut app);
// Write an invalid value directly to the public preferences.
app.update(|ctx| {
let public =
<crate::PublicPreferences as SingletonEntity>::as_ref(ctx).as_preferences();
public
.write_value("public_flag", "not_valid_json_bool".to_string())
.unwrap();
});
let invalid_keys =
app.read(|ctx| SettingsManager::as_ref(ctx).validate_all_public_settings(ctx));
assert_eq!(
invalid_keys,
vec!["public_flag".to_string()],
"validate should detect the invalid key"
);
// In-memory value should be unchanged (validate is read-only).
app.read(|ctx| {
assert_eq!(
*ReloadTestSettings::as_ref(ctx).public_flag.value(),
PublicFlag::default_value(),
"validate should not modify in-memory state"
);
});
});
}
/// Verifies that `validate_all_public_settings` returns empty when all
/// stored values are valid.
#[test]
fn test_validate_returns_empty_when_all_valid() {
warpui::App::test((), |mut app| async move {
crate::set_settings_file_enabled(true);
app.update(init_prefs);
app.add_singleton_model(|_| SettingsManager::default());
ReloadTestSettings::register(&mut app);
// Write a valid value.
app.update(|ctx| {
let public =
<crate::PublicPreferences as SingletonEntity>::as_ref(ctx).as_preferences();
public
.write_value("public_flag", "true".to_string())
.unwrap();
});
let invalid_keys =
app.read(|ctx| SettingsManager::as_ref(ctx).validate_all_public_settings(ctx));
assert!(
invalid_keys.is_empty(),
"validate should return empty when all values are valid"
);
});
}
}
mod write_to_preferences_tests {
use crate::*;
#[derive(
Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
)]
pub struct StructWithOptionals {
required_field: String,
optional_field: Option<String>,
nested: NestedStruct,
}
impl SettingsValue for StructWithOptionals {}
impl Default for StructWithOptionals {
fn default() -> Self {
Self {
required_field: "hello".to_string(),
optional_field: None,
nested: NestedStruct::default(),
}
}
}
#[derive(
Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
)]
pub struct NestedStruct {
width: u32,
height: u32,
}
impl Default for NestedStruct {
fn default() -> Self {
Self {
width: 100,
height: 50,
}
}
}
define_settings_group!(StructTestSettings, settings: [
struct_setting: StructSetting {
type: StructWithOptionals,
default: StructWithOptionals::default(),
supported_platforms: SupportedPlatforms::ALL,
sync_to_cloud: SyncToCloud::Never,
private: false,
toml_path: "test.struct_setting",
},
]);
/// Verifies that `write_to_preferences` does NOT write back when the stored
/// JSON differs in formatting (missing null fields, different key ordering)
/// but is semantically equal to the new value (in-memory backend).
#[test]
fn test_no_spurious_write_with_format_differences() {
let prefs =
Box::<warpui_extras::user_preferences::in_memory::InMemoryPreferences>::default();
// Simulate what a TOML backend produces after a round-trip:
// - null fields (optional_field) are stripped
// - key ordering may differ (nested before required_field)
let stored_json_without_nulls =
r#"{"nested":{"width":100,"height":50},"required_field":"hello"}"#;
prefs
.write_value("StructSetting", stored_json_without_nulls.to_string())
.unwrap();
// The Rust value has optional_field: None, which serde_json serializes as
// `"optional_field":null` with a different key order. The stored JSON
// doesn't have that field at all and has different key ordering.
let value = StructWithOptionals::default();
let canonical_json = serde_json::to_string(&value).unwrap();
assert_ne!(
canonical_json, stored_json_without_nulls,
"precondition: the JSON strings should differ"
);
// write_to_preferences should detect they're semantically equal and NOT write.
let changed = StructSetting::write_to_preferences(&value, prefs.as_ref()).unwrap();
assert!(
!changed,
"write_to_preferences should not report a change for semantically equal values"
);
}
/// Test with HashMap fields (non-deterministic key order) and missing
/// Option fields — reproduces the exact QuakeModeSettings scenario.
#[test]
fn test_no_spurious_write_with_hashmap_and_missing_options() {
use std::collections::HashMap;
use warpui_extras::user_preferences::toml_backed::TomlBackedUserPreferences;
#[derive(
Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
)]
pub struct QuakeLike {
pub keybinding: Option<String>,
pub active_pin_position: String,
pub sizes: HashMap<String, u32>,
pub pin_screen: Option<u32>,
pub hide_when_unfocused: bool,
}
impl SettingsValue for QuakeLike {}
impl Default for QuakeLike {
fn default() -> Self {
let mut sizes = HashMap::new();
sizes.insert("top".to_string(), 30);
sizes.insert("bottom".to_string(), 30);
Self {
keybinding: None,
active_pin_position: "Top".to_string(),
sizes,
pin_screen: None,
hide_when_unfocused: true,
}
}
}
define_settings_group!(QuakeLikeGroup, settings: [
quake_setting: QuakeLikeSetting {
type: QuakeLike,
default: QuakeLike::default(),
supported_platforms: SupportedPlatforms::ALL,
sync_to_cloud: SyncToCloud::Never,
private: false,
toml_path: "test.quake_like_setting",
},
]);
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("settings.toml");
let (prefs, _) = TomlBackedUserPreferences::new(file_path.clone());
let value = QuakeLike::default();
// First write.
let changed = QuakeLikeSetting::write_to_preferences(&value, &prefs).unwrap();
assert!(changed, "first write should report a change");
// Second write of same value should NOT report a change.
let changed_again = QuakeLikeSetting::write_to_preferences(&value, &prefs).unwrap();
assert!(
!changed_again,
"second write of same value should not report a change on TOML backend"
);
}
/// Same test but using the real TOML backend
/// null-stripping and key-reordering happens.
#[test]
fn test_no_spurious_write_with_toml_backend() {
use warpui_extras::user_preferences::toml_backed::TomlBackedUserPreferences;
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("settings.toml");
let (prefs, _) = TomlBackedUserPreferences::new(file_path.clone());
let value = StructWithOptionals::default();
// First write stores the value. The TOML backend will strip the null
// `optional_field` and may reorder keys.
let changed = StructSetting::write_to_preferences(&value, &prefs).unwrap();
assert!(changed, "first write should report a change");
// Verify the TOML file doesn't contain the null field.
let contents = std::fs::read_to_string(&file_path).unwrap();
assert!(
!contents.contains("optional_field"),
"TOML should strip null fields, but file contains: {contents}"
);
// Second write of the same value should NOT report a change,
// even though the JSON serialization differs from what's stored.
let changed_again = StructSetting::write_to_preferences(&value, &prefs).unwrap();
assert!(
!changed_again,
"second write of same value should not report a change on TOML backend"
);
}
}
+137
View File
@@ -0,0 +1,137 @@
//! Schema metadata for settings, used by the JSON Schema generator.
use schemars::Schema;
use schemars::SchemaGenerator;
use crate::SupportedPlatforms;
/// Metadata about a single setting, collected via `inventory` for schema generation.
///
/// Each setting registered with `define_setting!` or `implement_setting_for_enum!`
/// emits an `inventory::submit!` call that registers one of these entries. The
/// generator binary iterates all entries to produce a JSON Schema document.
pub struct SettingSchemaEntry {
/// The storage key for this setting (last segment of toml_path).
pub storage_key: &'static str,
/// User-facing description of what this setting does.
pub description: &'static str,
/// The TOML section path (everything before the last segment of toml_path).
pub hierarchy: Option<&'static str>,
/// Whether this setting is private (excluded from user-facing schema).
pub is_private: bool,
/// Feature flag gating this setting.
/// If Some, the setting is only included in the schema when the flag
/// is active for the target build channel.
pub feature_flag: Option<warp_features::FeatureFlag>,
/// Returns which platforms this setting applies to.
pub supported_platforms_fn: fn() -> SupportedPlatforms,
/// Returns the default value serialized as JSON.
pub default_value_fn: fn() -> String,
/// Returns the JSON Schema for this setting's value type.
pub schema_fn: fn(&mut SchemaGenerator) -> Schema,
/// Returns the default value serialized using `SettingsValue::to_file_value`.
pub file_default_value_fn: fn() -> String,
/// The maximum number of TOML section-table levels to use when rendering
/// this setting's value in the settings file. Mirrors `Setting::max_table_depth`.
/// `None` means unlimited depth.
pub max_table_depth: Option<u32>,
}
inventory::collect!(SettingSchemaEntry);
/// Submits a [`SettingSchemaEntry`] to the `inventory` registry.
#[macro_export]
macro_rules! submit_schema_entry {
(
private: $private:expr,
description: $desc:expr,
toml_path_value: $toml_path:expr,
fallback_storage_key: $fallback_key:expr,
supported_platforms: $plat:expr,
feature_flag: $flag:expr,
max_table_depth: $mtd:expr,
default: $default:tt,
value_type: $type:ty $(,)?
) => {
$crate::_inventory::submit! {
$crate::schema::SettingSchemaEntry {
storage_key: {
const KEY: &str = match $toml_path {
Some(path) => $crate::toml_path_storage_key(path),
None => $fallback_key,
};
KEY
},
description: $desc,
hierarchy: {
const HIER: Option<&str> = match $toml_path {
Some(path) => $crate::toml_path_hierarchy(path),
None => None,
};
HIER
},
is_private: $private,
feature_flag: $flag,
supported_platforms_fn: || $plat,
default_value_fn: || {
let val: $type = $default;
serde_json::to_string(&val).expect("default value should serialize")
},
schema_fn: <$type as $crate::_settings_value::SettingsValue>::file_schema,
file_default_value_fn: || {
use $crate::_settings_value::SettingsValue as _;
let val: $type = $default;
let file_value = val.to_file_value();
serde_json::to_string(&file_value).expect("default file value should serialize")
},
max_table_depth: $mtd,
}
}
};
}
/// Helper: produces a `&'static str` description, defaulting to `""` when omitted.
#[macro_export]
macro_rules! _schema_default_description {
() => {
""
};
($desc:literal) => {
$desc
};
}
/// Helper: produces `Option<FeatureFlag>` for a feature flag, defaulting to `None`.
#[macro_export]
macro_rules! _schema_default_flag {
() => {
None
};
($flag:path) => {
Some($flag)
};
}
/// Helper: produces `Option<u32>` for a max-table-depth literal, defaulting to `None`.
#[macro_export]
macro_rules! _schema_default_max_table_depth {
() => {
None
};
($mtd:literal) => {
Some($mtd)
};
}
#[cfg(test)]
#[path = "schema_tests.rs"]
mod tests;
+283
View File
@@ -0,0 +1,283 @@
use std::collections::HashSet;
use schemars::JsonSchema;
use schemars::SchemaGenerator;
use crate::schema::SettingSchemaEntry;
// ---------------------------------------------------------------------------
// Fixture settings used for per-type schema validation tests
// ---------------------------------------------------------------------------
fn entries() -> Vec<&'static SettingSchemaEntry> {
inventory::iter::<SettingSchemaEntry>.into_iter().collect()
}
// ---------------------------------------------------------------------------
// Invariant tests (run over all registered entries)
// ---------------------------------------------------------------------------
#[test]
fn at_least_one_entry_exists() {
assert!(
!entries().is_empty(),
"Expected at least one SettingSchemaEntry to be registered"
);
}
#[test]
fn no_empty_storage_keys() {
for entry in entries() {
assert!(
!entry.storage_key.is_empty(),
"Found entry with empty storage_key"
);
}
}
#[test]
fn all_defaults_produce_valid_json() {
for entry in entries() {
let json = (entry.default_value_fn)();
assert!(
serde_json::from_str::<serde_json::Value>(&json).is_ok(),
"default_value_fn for '{}' produced invalid JSON: {json}",
entry.storage_key
);
}
}
#[test]
fn all_schemas_are_serializable() {
let mut schema_gen = SchemaGenerator::default();
for entry in entries() {
let schema = (entry.schema_fn)(&mut schema_gen);
assert!(
serde_json::to_string(&schema).is_ok(),
"schema_fn for '{}' produced a non-serializable schema",
entry.storage_key
);
}
}
#[test]
fn no_duplicate_storage_keys() {
// NOTE: when run in the settings crate alone, test modules may
// register fixture settings with overlapping keys (e.g. both
// mod_tests and macros_tests define "SimpleSetting"). This test
// is most useful when run from the app crate where real settings
// are registered. We still check for duplicates but collect them
// all before reporting, to give a clear picture.
let mut seen = HashSet::new();
let mut duplicates = Vec::new();
for entry in entries() {
if !seen.insert(entry.storage_key) {
duplicates.push(entry.storage_key);
}
}
// In the app crate context (no test-only fixtures), there should
// be zero duplicates.
#[cfg(not(test))]
assert!(
duplicates.is_empty(),
"Duplicate storage_keys: {duplicates:?}"
);
}
#[test]
fn all_supported_platforms_fn_succeed() {
for entry in entries() {
// Just call it to ensure it doesn't panic.
let _ = (entry.supported_platforms_fn)();
}
}
#[test]
fn hierarchy_values_are_well_formed() {
for entry in entries() {
if let Some(h) = entry.hierarchy {
assert!(
!h.starts_with('.') && !h.ends_with('.'),
"Hierarchy for '{}' has leading/trailing dots: '{h}'",
entry.storage_key
);
assert!(
!h.contains(".."),
"Hierarchy for '{}' contains consecutive dots: '{h}'",
entry.storage_key
);
}
}
}
// ---------------------------------------------------------------------------
// $ref resolution test
// ---------------------------------------------------------------------------
#[test]
fn all_refs_resolve_to_definitions() {
let mut schema_gen = SchemaGenerator::default();
// Process all entries through the shared generator
for entry in entries() {
let _schema = (entry.schema_fn)(&mut schema_gen);
}
// Collect all $ref pointers from the schemas
fn collect_refs(value: &serde_json::Value, refs: &mut HashSet<String>) {
match value {
serde_json::Value::Object(map) => {
if let Some(serde_json::Value::String(r)) = map.get("$ref") {
refs.insert(r.clone());
}
for v in map.values() {
collect_refs(v, refs);
}
}
serde_json::Value::Array(arr) => {
for v in arr {
collect_refs(v, refs);
}
}
_ => {}
}
}
// Re-process to collect refs from the output
let mut schema_gen2 = SchemaGenerator::default();
let mut all_refs = HashSet::new();
for entry in entries() {
let schema = (entry.schema_fn)(&mut schema_gen2);
let value = serde_json::to_value(&schema).unwrap();
collect_refs(&value, &mut all_refs);
}
let defs = schema_gen2.definitions();
for r in &all_refs {
// schemars 1.x uses "#/$defs/TypeName"
if let Some(type_name) = r.strip_prefix("#/$defs/") {
assert!(
defs.contains_key(type_name),
"$ref '{r}' does not resolve to any definition. Available: {:?}",
defs.keys().collect::<Vec<_>>()
);
}
}
}
// ---------------------------------------------------------------------------
// Per-type schema validation (using fixture settings)
// ---------------------------------------------------------------------------
/// Helper to generate a schema for a type and return it as a JSON value.
fn schema_value_for<T: JsonSchema>() -> serde_json::Value {
let mut schema_gen = SchemaGenerator::default();
let schema = T::json_schema(&mut schema_gen);
schema.to_value()
}
#[test]
fn bool_schema() {
let v = schema_value_for::<bool>();
assert_eq!(v.get("type").and_then(|t| t.as_str()), Some("boolean"));
}
#[test]
fn string_schema() {
let v = schema_value_for::<String>();
assert_eq!(v.get("type").and_then(|t| t.as_str()), Some("string"));
}
#[test]
fn simple_enum_schema() {
#[derive(JsonSchema)]
#[allow(dead_code)]
enum SimpleEnum {
A,
B,
C,
}
let v = schema_value_for::<SimpleEnum>();
let variants = v.get("enum").and_then(|e| e.as_array());
assert!(variants.is_some(), "Expected enum array in schema");
let names: Vec<&str> = variants
.unwrap()
.iter()
.filter_map(|v| v.as_str())
.collect();
assert_eq!(names, vec!["A", "B", "C"]);
}
#[test]
fn optional_field_schema() {
let v = schema_value_for::<Option<String>>();
// schemars 0.8 may represent Option<T> as:
// - {"anyOf": [...]}
// - {"type": ["string", "null"]}
let has_any_of = v.get("anyOf").is_some();
let has_one_of = v.get("oneOf").is_some();
let has_nullable_type = v
.get("type")
.and_then(|t| t.as_array())
.is_some_and(|arr| arr.iter().any(|t| t.as_str() == Some("null")));
assert!(
has_any_of || has_one_of || has_nullable_type,
"Expected anyOf, oneOf, or nullable type array for Option<String>, got: {v}"
);
}
#[test]
fn struct_schema() {
#[derive(JsonSchema)]
#[allow(dead_code)]
struct MyStruct {
name: String,
count: u32,
}
let v = schema_value_for::<MyStruct>();
assert_eq!(v.get("type").and_then(|t| t.as_str()), Some("object"));
let props = v.get("properties").and_then(|p| p.as_object());
assert!(props.is_some(), "Expected properties in struct schema");
let props = props.unwrap();
assert!(props.contains_key("name"));
assert!(props.contains_key("count"));
}
#[test]
fn nested_struct_ref_resolves() {
#[derive(JsonSchema)]
#[allow(dead_code)]
struct Inner {
value: i32,
}
#[derive(JsonSchema)]
#[allow(dead_code)]
struct Outer {
inner: Inner,
}
let mut schema_gen = SchemaGenerator::default();
let schema = Outer::json_schema(&mut schema_gen);
let v = serde_json::to_value(schema).unwrap();
// The inner field should be a $ref
let inner_prop = v
.pointer("/properties/inner")
.expect("Expected inner property");
let has_ref = inner_prop.get("$ref").is_some() || inner_prop.get("allOf").is_some();
assert!(
has_ref,
"Expected $ref or allOf for nested struct, got: {inner_prop}"
);
// The definition should exist
let defs = schema_gen.definitions();
assert!(
defs.contains_key("Inner"),
"Expected 'Inner' in definitions"
);
}
+43
View File
@@ -0,0 +1,43 @@
use super::*;
#[test]
fn storage_key_single_segment() {
assert_eq!(toml_path_storage_key("font_name"), "font_name");
}
#[test]
fn storage_key_two_segments() {
assert_eq!(toml_path_storage_key("font.font_name"), "font_name");
}
#[test]
fn storage_key_three_segments() {
assert_eq!(
toml_path_storage_key("appearance.text.font_name"),
"font_name"
);
}
#[test]
fn hierarchy_single_segment() {
assert_eq!(toml_path_hierarchy("font_name"), None);
}
#[test]
fn hierarchy_two_segments() {
assert_eq!(toml_path_hierarchy("font.font_name"), Some("font"));
}
#[test]
fn hierarchy_three_segments() {
assert_eq!(
toml_path_hierarchy("appearance.text.font_name"),
Some("appearance.text")
);
}
// Verify const evaluation works at compile time.
const _: () = {
assert!(matches!(toml_path_storage_key("a.b.c").as_bytes(), b"c"));
assert!(matches!(toml_path_storage_key("key").as_bytes(), b"key"));
};