Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

SettingsReloadSink

Live cross-process settings sync: a notify-based directory watcher plus the registry that lets a changed path be dispatched to the in-memory Reloadable handle that owns it.

This is the read-side counterpart to the write-side cross-process safety documented in flush.rs / reload.rs: every write in this crate already merges safely against a peer's concurrent write, but a process that never looks again will not notice a peer's write until it happens to touch the same key itself. SettingsWatcher is what makes it look again, automatically, the moment a peer's write lands on disk.

Shape, mirrored from teksilo-i18n's FtlFileWatcher

SettingsWatcher owns a notify::RecommendedWatcher background thread and a type-erased sink Arc<dyn Fn(PathBuf) + Send + Sync>. Exactly like FtlFileWatcher, it watches directories, not files: atomic writers (this crate's own write_atomic included) write a temp file and rename it over the target, which invalidates an inode-level watch on the file itself. Unlike FtlFileWatcher — which watches a fixed, already-existing set of .ftl files and derives their parents — SettingsWatcher watches the settings directories (AppPaths::config_dir() / AppPaths::data_dir()) directly, because the set of settings files living there is open-ended and some of them (e.g. window_state.toml) may not exist yet at watch-construction time.

The sink receives the changed path (not yet filtered against anything this process cares about); SettingsRegistry::dispatch is what decides whether the path names something registered and, if so, calls its Reloadable::reload_from_disk. A path with no registered owner (a .lock sidecar, a .tmp write-in-progress, an unrelated file a peer dropped in the same directory) is a harmless no-op.

The registry

SettingsRegistry maps a canonical path to a Weak<dyn Reloadable>. It never holds a strong reference itself: whoever opens a persisted service (SettingsBundle::open, or application code opening its own ad hoc SettingsFile<T> / PersistedListModel<T> / MruList<T>) wraps it in an Rc<dyn Reloadable>, registers a weak clone via SettingsRegistry::register, and keeps the returned Rc alive for as long as it wants peer writes to be picked up. When that Rc (and every clone of it) is dropped, the registry's entry can no longer be upgraded — SettingsRegistry::dispatch then quietly prunes it and reports nothing happened. Nothing leaks and nothing is ever called on a service that no longer exists.

API reference

📖 Full rustdoc API for this module

pub type SettingsReloadSink

Sink type invoked on the notify worker thread whenever a watched settings directory reports a create/modify event. Implementations must be thread-safe; teksilo-app's implementation posts the path through the winit EventLoopProxy as AppEvent::SettingsReload, which hops back onto the UI thread where the (single-threaded, Rc-based) SettingsRegistry actually lives.

#![allow(unused)]
fn main() {
pub type SettingsReloadSink = Arc<dyn Fn(PathBuf) + Send + Sync + 'static>;
}

pub struct SettingsWatcher

Active directory watcher over one or more settings directories. One per TeksiloAppBuilder::run invocation (when a settings bundle with watching enabled is configured).

Owns the notify::RecommendedWatcher background thread for its whole lifetime; dropping the SettingsWatcher stops the watcher and cleans up. Kept alive by the caller for as long as live reload is wanted — teksilo-app stores it on its window-loop handler, exactly like teksilo-i18n's FtlFileWatcher.

#![allow(unused)]
fn main() {
pub struct SettingsWatcher { /* fields */ }
}

Methods

pub fn new(dirs: Vec<PathBuf>, sink: SettingsReloadSink) -> Result<Self, notify::Error>

Build a watcher over dirs (deduplicated by canonical path, so passing the same directory twice — e.g. AppPaths::for_testing, whose config_dir() and data_dir() are the same tempdir — never double-watches or double-fires) and a sink callback.

A directory that does not exist (or can't be canonicalized for any other reason) is logged and skipped — not fatal — since a freshly-installed app may not have created its data directory yet when this is called. As long as at least the config directory exists (which AppPaths implies by the time SettingsBundle has successfully opened anything in it), watching still works for the files that matter.

pub struct SettingsRegistry

Registry mapping a canonical settings path to the live Reloadable handle that owns it, so a file-watcher event naming that path can be dispatched to the right in-memory state.

Clone is cheap (an Rc bump) — every clone shares the same underlying map, matching the rest of this crate's handle types. Holds only Weak references: see the module docs' "the registry" section for the full ownership contract.

#![allow(unused)]
fn main() {
pub struct SettingsRegistry { /* fields */ }
}

Methods

pub fn new() -> Self

A fresh, empty registry.

pub fn register(&self, reloadable: Rc<dyn Reloadable>) -> Rc<dyn Reloadable>

Register reloadable under its canonical path and return it back unchanged, so a caller can register and retain in one expression:

use teksilo_settings::{SettingsRegistry, SettingsFile, Migrator, Versioned};
use serde::{Serialize, Deserialize};
use std::rc::Rc;

#[derive(Serialize, Deserialize, Default, Clone, PartialEq)]
struct Prefs { version: u32 }
impl Versioned for Prefs {
    const CURRENT_VERSION: u32 = 1;
    fn version(&self) -> u32 { self.version }
    fn set_version(&mut self, v: u32) { self.version = v; }
}

let dir = tempfile::tempdir().unwrap();
let file: SettingsFile<Prefs> =
    SettingsFile::load(dir.path().join("prefs.toml"), Migrator::new()).unwrap();

let registry = SettingsRegistry::new();
// Keep `handle` alive for as long as reload should keep working.
let handle = registry.register(Rc::new(file.clone()));
drop(handle); // dropping it deregisters: no leak, no dangling call.

The caller is responsible for keeping the returned Rc alive — only a Weak is retained internally, by design (see the module docs). Registering a second Reloadable under the same canonical path replaces the first entry.

pub fn dispatch(&self, changed_path: &Path) -> Result<bool, SettingsFileError>

Look up changed_path's registered owner and call Reloadable::reload_from_disk on it.

Returns Ok(true) if the owner's in-memory state actually changed, Ok(false) if nothing needed to change (including: the path names nothing registered, or its owner has been dropped — in the latter case the dead entry is pruned from the map so it doesn't accumulate forever).

pub fn registered_paths(&self) -> Vec<PathBuf>

The canonical paths currently registered (including entries whose owner has since been dropped but not yet pruned by a dispatch call). Exposed for tests and diagnostics.

pub fn live_count(&self) -> usize

Number of live (upgradeable) entries. Exposed for tests.