Skip to main content

teksilo_settings/
lib.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! `teksilo-settings` — persistent, reactive user preferences for Teksilo.
5//!
6//! Two persistence shapes share one storage backbone:
7//!
8//! * [`SettingsStore`] — dynamic, dotted-key K/V for **scalar** values
9//!   (numbers, strings, bools, arrays of those), surfaced as
10//!   `Signal<T>`. The QSettings analogue. Struct values aren't supported
11//!   here because TOML serializes them as tables, indistinguishable from
12//!   nested key paths; use [`SettingsFile<T>`] for those instead.
13//! * [`SettingsFile<T>`] — typed single-struct persistence with
14//!   migrations.
15//! * [`PersistedListModel<T>`] — a reactive, [`Keyed`]-item ordered
16//!   collection persisted to a single file, merging by **op** (upsert /
17//!   remove / clear) rather than by whole-document snapshot — so a peer
18//!   process's concurrent addition survives.
19//! * [`MruList<T>`] — generic dedupe + pin + cap list over an
20//!   app-defined item type implementing [`MruEntry`]. Apps register
21//!   their own via `TeksiloAppBuilder::app_state(handle)`.
22//! * [`WindowStateService`] — per-window geometry persistence. When
23//!   registered via `TeksiloAppBuilder::settings(...)`, every
24//!   `WindowConfig` carrying an `id(...)` is automatically restored
25//!   on creation (sanitized against the current monitor) and recorded
26//!   on every change by `teksilo-app`'s window manager. No widget-side
27//!   wiring required.
28//!
29//! All disk writes are atomic (write-temp + rename). Every write **merges**
30//! against the document read fresh off disk under an exclusive lock —
31//! never a blind whole-document overwrite — so two processes sharing a
32//! file (Skribisto's one-process-per-project model shares `general.toml`,
33//! `recents.toml`, `window_state.toml` across every open project) cannot
34//! silently clobber each other. [`Reloadable`] is the matching read-side
35//! contract: a hook a file-system watcher calls to push a peer's write into
36//! this process's live signals/models — and [`SettingsWatcher`] +
37//! [`SettingsRegistry`] are the actual watcher: a `notify`-based directory
38//! watch (parent-directory, not file, so an atomic rename-over doesn't
39//! invalidate it) plus the path → [`Reloadable`] lookup a changed-file
40//! event dispatches through. `TeksiloAppBuilder::settings(...)` wires one
41//! up automatically (opt out via `.settings_watch(false)`); every service
42//! [`SettingsBundle::open`] opens is pre-registered into
43//! [`OpenedSettings::registry`], and application code can register its
44//! own ad hoc `SettingsFile` / `PersistedListModel` / `MruList` handles
45//! into that same registry.
46//!
47//! ```
48//! use teksilo_settings::{AppPaths, SettingsStore, SettingsKey};
49//!
50//! const FONT_SIZE: SettingsKey<f32> =
51//!     SettingsKey::new("editor.font_size", || 14.0);
52//!
53//! // Tests / docs use `for_testing(...)` against a tempdir so they
54//! // never touch the user's real config tree. Production apps use
55//! // `AppPaths::new(qualifier, organization, application)`.
56//! let tmp = tempfile::tempdir().unwrap();
57//! let paths = AppPaths::for_testing(tmp.path());
58//! let store = SettingsStore::open(paths.config_file("general")).unwrap();
59//!
60//! let font_size = store.signal_for(&FONT_SIZE);
61//! font_size.set(18.0); // persisted on the next debounce tick
62//! ```
63
64mod bundle;
65mod collection;
66mod ext;
67mod file;
68mod flush;
69mod lock;
70mod migration;
71mod mru;
72mod path;
73mod reload;
74mod store;
75mod watch;
76mod window_state;
77
78pub use crate::bundle::{OpenedSettings, SettingsBundle, SettingsBundleError};
79pub use crate::collection::list::{Keyed, ListFile, ListOp, PersistedListModel};
80pub use crate::ext::SettingsExt;
81pub use crate::file::{SettingsFile, SettingsFileError};
82pub use crate::flush::{
83    DebouncedWriter, FlushError, LandedStamp, WriteFailureSink, WriteLandedSink,
84    set_write_failure_sink,
85};
86pub use crate::migration::{MigrationError, Migrator, Versioned};
87pub use crate::mru::{MruEntry, MruList};
88pub use crate::path::AppPaths;
89pub use crate::reload::Reloadable;
90pub use crate::store::{
91    DEFAULT_DEBOUNCE, SettingsKey, SettingsStore, SettingsStoreError, TEXT_SCALE_KEY,
92};
93pub use crate::watch::{SettingsRegistry, SettingsReloadSink, SettingsWatcher};
94pub use crate::window_state::{PerWindowState, WindowStateService};