Persistent preferences

day::prefs stores small string values between launches, such as a selected theme or volume. It is provided by day-part-prefs and re-exported by day through the default-enabled prefs feature. Apps using the facade do not need a separate dependency.

Use bind to restore a signal’s saved value and persist later changes. For files, use app-local file storage. Neither API is an encrypted store for passwords or tokens.

A direct day-part-prefs dependency remains available. The day::prefs and day_part_prefs paths expose the same API. Disabling the facade’s default features removes its preferences dependency; enable any other default features your app still needs explicitly.

Authoring

day::prefs::set("greeting", "hello");                // persist a value
assert_eq!(day::prefs::get("greeting").as_deref(), Some("hello"));
assert!(day::prefs::contains("greeting"));
day::prefs::remove("greeting");                      // delete it
FunctionBehavior
set(key, value) -> boolPersist value under key, overwriting. true when the write committed.
get(key) -> Option<String>The stored string, or None if absent. A stored "" is Some("").
remove(key) -> boolDelete the value; true only if it existed and was removed.
contains(key) -> boolWhether a value is currently stored under key.
bind(key, signal)Two-way-bind a Signal<T> to a stored value: seed from the store now, persist every later change. T round-trips through FromStr/ToString. Call it right after creating the signal; the write-back stops with the creating scope.
bind_json(key, store)(day::prefs, with the model feature.) Keep a keyed day-model store (Store<Keyed<T>>, T: Serialize + DeserializeOwned) under key as one JSON array: seed it from the saved rows now, then save every later change, a field edit as well as an insert, removal or move. An unreadable value leaves the store as it is and logs a warning. Suits a small list; larger documents belong in day-persistence.
install_nav_store()Install this store as day-core’s navigation-persistence sink, so a nav/nav_stack marked .restore(key) remembers its state across launches (and an Android process death). day::launch calls it when the app has installed no store of its own, so an app calls it only when it boots without the facade. Nav keys are namespaced under day.nav.. See navigation.

Values persist across launches. Platform selection is purely #[cfg(target_os)] (plus #[cfg(target_arch = "wasm32")] for the web), since persistence depends on the OS, not on which widget toolkit is in use. The one cargo feature, memory, swaps every platform store for an in-process map per thread; day’s headless mock backend turns it on, so an app’s cargo test starts with empty preferences and never touches the user’s real ones. A test that restarts its own process to check persistence gives the children DAY_PREFS_DIR=<dir>: the file store in that directory then serves in place of the map, private to that test. The file store (Linux, Windows, HarmonyOS) honors DAY_PREFS_DIR in every build. parts/day-part-prefs/examples/prefs.rs is a plain main that uses it with no Day framework at all (run it twice to watch a value survive the process).

This is a small string store for user settings and lightweight app state, not a database. Keep values modest; large blobs belong in a file.

Per-platform native realization

OSAPIdependency
macOSNSUserDefaults.standardobjc2 + objc2-foundation, shared apple.rs
iOSNSUserDefaults.standardobjc2 + objc2-foundation, shared apple.rs
AndroidSharedPreferences (MODE_PRIVATE) via a Java shimday-android + [package.metadata.day.android]
Linuxper-app file store under $XDG_CONFIG_HOME/day (or ~/.config/day)std only, shared file.rs
Windowsper-app file store under %APPDATA%\daystd only, shared file.rs
HarmonyOSfile store, best-effort in the app sandbox (target_env = "ohos")std only, shared file.rs
WeblocalStorage (per origin, day.pref. namespace) via the day-dom shimthe web-dom host page (docs/web.md), web.rs

What each platform does

  • macOS / iOS: NSUserDefaults.standard is the system’s per-application preferences store (a plist under ~/Library/Preferences on macOS, the app container on iOS). It is toolkit-independent (no NSApplication/UIApplication, run loop, or window), so the crate works in day-qt binaries and plain cargo test processes as well as under day-appkit/day-uikit. setObject:forKey: is the only unsafe objc2 call (the value must be a property-list type, and we always pass a real NSString); writes are immediately readable and flushed to disk by the system. set always returns true.
  • Android: an app-private SharedPreferences file named day_part_prefs, opened with Context.MODE_PRIVATE from day-android’s cached Context (DayBridge.ctx). Writes use Editor.commit() (synchronous), so set/remove return the true commit result. No manifest permission is required; SharedPreferences is private storage. Like the UI pieces, the crate stages its own Java shim through [package.metadata.day.android] and rides the Day runtime (it needs the app’s JVM + Context).
  • Linux / Windows / HarmonyOS: a file-backed store, one flat String -> String map serialized under <config-dir>/day/<app-id>.store, where the app id is Day.toml’s [app].id as day build/day launch export it (DAY_APP_ID; a binary built outside the CLI shares day-part-prefs.store). The file is new per app: nothing is read from the shared file earlier builds wrote, by design (apps started over when the store became per-app, 2026-10). config-dir is $XDG_CONFIG_HOME (else ~/.config) on Linux, %APPDATA% (else %USERPROFILE%\AppData\Roaming) on Windows, and a best-effort app files dir on HarmonyOS. Each entry is a line escaped_key=escaped_value; the escaper removes every raw =, newline, and carriage return (\ → \\, newline → \n, CR → \r, = → \e), so the first raw = on a line is unambiguously the separator and a value may contain anything. Writes are best-effort atomic (write a sibling temp file, then rename over the target); a process-wide mutex serializes load-modify-save cycles. Every read tolerates a missing, unreadable, or corrupt file by treating the store as empty, so a partial write or a hand-edit can never panic a caller. set returns false only when the store file could not be written (e.g. a read-only home). It depends on std only.
  • Web (web-dom): localStorage, reached through the day-dom host shim’s day_dom_pref_* imports under a day.pref. key namespace. Values survive reloads and browser restarts, scoped per origin. localStorage can throw (private browsing, storage pressure); failures report as uncommitted/absent, matching the contract everywhere else. The showcase’s Controls page binds its state through this store, so a reload keeps the counter (docs/web.md). On wasm outside a day-dom host page the imports are unresolved and instantiation fails.
  • Any other platform: a no-op store: set/remove/contains return false, get returns None.

What it shows about the extension system

Like day-part-battery and day-part-network, this is a headless external crate: it has no UI Piece and registers nothing in any backend’s RENDERERS slice. On Android it stages its own Java shim through [package.metadata.day.android] (with no permission this time, since private storage needs none), which day build folds into the app’s Gradle build without touching any core day crate. On every other platform it is fully day-independent (pure FFI on Apple, pure std file I/O on desktop). See docs/extending.md.

Settings and About

pieces/day-piece-settings (docs/settings.md) is the typed, declarative layer over this store: Pref<T> statics, groups and rows, the Settings and About surfaces, and the env-wins rule for DAY_THEME/DAY_LOCALE. Its default Store is this crate. When testing persistence by hand on macOS, unbundled binaries store under the process-name defaults domain (defaults delete <name> clears it; a plist delete alone won’t, because cfprefsd caches).

Binding failures and verification

bind<T> requires FromStr + ToString + Clone + 'static. Invalid saved text leaves the initial signal value unchanged. It persists later watched changes, not the initial default. Its callback ignores set’s boolean result; use explicit writes when the UI must report a failure. Keep UI messages in generated accessors and treat stored strings as data.

The example values above are synthetic fixtures, not UI translations. Compile-designated code is in the storage recipe. Source evidence: preferences API, including the memory feature and its DAY_PREFS_DIR test override. A successful mock round trip does not certify platform durability.