Saved preferences
Contracts and encoding
clock-core::Preferences is an editable Settings draft. validate checks 6–24
integer pt and the format string, normalizing CRLF. ValidatedPreferences has
private fields and carries the compiled format. Runtime consumers borrow it;
draft() returns a separate editable copy. Initial preferences are primary,
9 pt, system theme, automatic update checks off and %H:%M\n%g%V.%u.
clock-windows::preferences_store::PreferenceSession loads safe defaults on
missing data or an error. load_error() is the recoverable Settings status.
apply() validates and saves before publishing the new active value. A failed
save leaves active preferences and first-run completion unchanged. The caller
retains its editable draft. reset_draft() only stages defaults, and does not
reset first-run state. complete_first_run() persists internal onboarding state;
login startup is deliberately absent and remains owned by Windows (startup).
The production store resolves FOLDERID_LocalAppData with
SHGetKnownFolderPath, then uses a dedicated ElevenClock-rs directory, which holds preferences.dat and
the bounded logs. It
never consults legacy .elevenclock preferences, HOME, command arguments or an
environment override. The directory inherits the current user’s LocalAppData
ACLs; the app is standard-user and makes no cross-user store. This is private
application storage, not a security boundary against other processes of the same
user. The filename and encoding are implementation details, not a supported
manual-edit interface.
Maintained Serde + serde_json provide a compact internal UTF-8 encoding with a
version envelope. Reads are capped at 64 KiB; typed payload validation rejects
unknown fields, missing fields, invalid enums, bounds and formats. Version 2
is the current writer; version 1 remains readable. No legacy import is supported. Version 1 migration copies and flushes the original before atomically
committing version 2. It retains format, monitors, font, theme and onboarding;
update_check_enabled is false and the update history starts empty. A failed
preservation or commit leaves the original intact, retains the old clock values
with checks off, and blocks background writes until recovery in Settings.
Unsupported versions (including newer versions) are detected before decoding
version-specific fields, preserved and blocked from all save/recovery actions.
Damaged/invalid data stays untouched on load and ordinary Apply. Settings must
present an explicit recovery action calling recover(draft): it validates the
draft, copies and flushes the original into a uniquely named preservation file,
then saves. Backups are never silently deleted. If preservation or save fails,
the old disk data and active value remain. Recovery state clears only after
success. StoreError distinguishes I/O, damaged data, invalid preference fields,
unsupported version and required recovery. Its Display messages omit paths.
Saves use create-new same-directory temporary files, write_all, sync_all and
close before MoveFileExW(REPLACE_EXISTING | WRITE_THROUGH). This documented
Windows operation handles first creation and existing-target replacement on the
same volume without a truncate-in-place window. Failures remove the temporary
file best-effort. A process kill may leave an ignored temporary file; it cannot
be mistaken for committed preferences. There is no watcher, reload operation or
per-monitor override. Presentation-only sharing and the separate named-preset library
are described in templates; they do not export this internal preference file.
The runtime’s single-instance coordinator
serializes app access; this store does not claim multi-writer transactions.
The public safe storage trait supports in-memory tests; filesystem path injection is private to unit tests.
Tests
The Windows job in rust-ci.yml runs the real persistence tests, including first create/replacement,
locked-target (delete sharing denied), read-only denied-target and injected
pre-replacement failure. These tests need no interactive desktop. Portable tests cover defaults, invalid stored
values, unsupported versions, corrupt/oversized data, preservation/recovery,
active rollback, reset draft and first-run persistence.
Compatibility and retained fixtures
Retained synthetic files live in crates/clock-windows/tests/fixtures/preferences/.
v1-defaults.json records initial onboarding and defaults; v1-custom.json records
non-default values and completed onboarding. Portable store tests decode them and
compare encoded values with their retained JSON structure, independently of field
order and whitespace. Keep old fixtures unchanged when changing the writer: do not
regenerate compatibility inputs from the encoder under test. Fixtures contain no
real user data. Add a fixture when extending the persisted contract, including
relevant enum/optional-field cases; ordinary same-format UI changes need none.
Version 2 adds the editable update_check_enabled flag and private update history:
last_update_check (UTC epoch seconds or none), last_notified_version (or none),
and backoff_until (UTC epoch seconds). The v2-defaults.json and v2-custom.json
fixtures record the expected migrated values; v2-updates-enabled.json records
opt-in with nonempty attempt, notification and backoff state. The UI thread serializes background
history commits and Settings Apply through the same session, preserving unsaved
drafts and the latest committed opt-in. Attempt time is saved before dispatch;
notification version is saved before the tray call. Failed saves prevent those
actions. A future attempt time defers checks after clock rollback. Managed mode
ignores the flag and keeps it intact, including when other Settings are applied.
An older version-1 executable treats version 2 as unsupported and cannot save it; returning to this executable retains the version-2 data. A migration must preserve its original input before committing and publish active values only after a successful save. Retain fixtures for every supported source version and expected target, with tests for upgrade, interrupted/failed commit, reopening and rollback expectations. If older files become unsupported, document that explicitly rather than treating them as corrupt or silently resetting them.
Downgrade safety today means an older app uses safe defaults and refuses to write
an unsupported version, including Apply, recovery and onboarding completion. It
must not strip unfamiliar fields or convert the file to its own schema. A user can
return to the compatible executable with the original bytes intact. Existing
unsupported_version_cannot_be_overwritten tests cover the store/session boundary;
damaged_data_is_preserved_and_recovery_is_explicit covers malformed originals and
byte-for-byte preservation. Keep these protections when adding migrations, without
replicating them in a separate framework.
Optional managed inventory format read
The managed launcher’s administrator-controlled include_clock_format setting can
include only the committed format source in inventory. saved_clock_format reads
through the existing bounded decoder without writing/migrating/recovering any file.
A missing file means the built-in default; read/validation/version failures remain
unavailable instead of being reported as a user-selected default. Unsaved drafts,
rendered date/time text and other preference fields are not exported. Collection
runs only in the opted-in managed helper, not in the app or Settings callbacks; see
managed inventory.