Files
craft-libs/docs/settings-storage.md
T
Alejandro Londono 6006abb757 feat(settings): add shared config storage for Craft apps
Add shared config.toml storage while keeping settings types, defaults and UI policy in each app.

Includes isolated in-memory operation, configurable portable paths, atomic saves with private backups, damaged-file preservation, schema migrations, and documented best-effort handling of external edits.

Validation: native macOS formatting, strict workspace clippy, 172 workspace tests and the craft-settings WASM check. Independent backup-permission and failed external-migration repros fail before the fixes and pass afterward.

Hosted CI: all seven UI and printing jobs pass across macOS, Linux, Windows and UI WASM.

Refs #12
2026-10-12 15:40:50 -06:00

103 lines
6.3 KiB
Markdown

# Settings storage across the Craft apps
How every Craft app stores its settings, and what goes where. The shared code is
[`craft-settings`](../crates/settings/README.md); this page is the policy it implements.
Discussion: [storytold/craft-libs#12](https://github.com/storytold/craft-libs/issues/12).
## What goes where
| Kind | Where | Why |
|---|---|---|
| Settings people might edit | `config.toml` in the config folder | One readable file with comments, atomic saves, backup, recovery, migrations |
| Window and UI state | `ui.ron` / `app.ron` beside `config.toml` | Changes every session; not worth comments or hand edits |
| Caches and models | Platform cache folder (`paths::cache_dir`); `%LOCALAPPDATA%` on Windows so it does not roam | Disposable, large |
| Recovery and autosave files | Platform data folder (`paths::data_dir`) | Not settings; must survive a settings reset |
| Settings that belong to a library or project (LightCraft) | Inside that folder, via `SettingsStore::open_at` | Travels with the library |
| API keys and tokens | The OS keychain | Never in a plain-text file |
## The config folder
First match wins. The store reports which rule applied, so apps can show the path in their
about or settings screen.
1. An override passed by the app, such as a `--config-dir` flag
2. `<APP>_CONFIG_DIR`, for example `PHOTOCRAFT_CONFIG_DIR` (one per app, so redirecting one
app does not move the others). Set but **empty** means isolated: nothing is read or written
anywhere, the same as DesignCraft's existing behaviour, so tests and agents can never touch a
user's real settings
3. Portable mode: a marker file next to the executable (next to the `.app` on macOS, since
writing inside a signed bundle breaks its signature). The default is a file named `portable`
with settings in `config/` beside it; apps that already ship portable builds keep their own
names (PhotoCraft: `portable.txt` or `PhotoCraft.portable`, folder `PhotoCraftData`)
4. The platform default:
Apps can also open a store with `Location::Memory` (for a switch like `DESIGNCRAFT_NO_PREFS`):
settings work in memory and nothing touches the disk.
| Platform | Folder |
|---|---|
| Linux / BSD | `$XDG_CONFIG_HOME/<app>/`, or `~/.config/<app>/` if unset |
| macOS | `~/Library/Application Support/<app>/` |
| Windows | `%APPDATA%\<app>\` (Roaming) |
`<app>` is the app's lowercase id (`photocraft`, `designcraft`, …) on every platform, macOS
included (decided in #12). Existing Linux folder names do not change; the Launcher moves from
`ai.storyteller.artcraft-launcher` to `artcraft-launcher`.
If the folder cannot be created or written (a portable marker in `/Applications`, an AppImage
mount), the store opens read-only and says so at startup.
## Files in the folder
- `config.toml`: starts with `schema_version = N`, then one `[section]` per settings area.
Written on first launch so people can find it. `0600` on Unix.
- `config.toml.bak`: the previous version, refreshed before every save.
- `config.broken-<unix-ts>.toml`: a file that was not valid TOML, moved aside and never deleted.
## Saving and recovery
- Saves happen during use, half a second after the last change, atomically through
`craft-atomic`. A failed save leaves the old file intact and is retried with backoff; the app
shows the error.
- A file with a TOML syntax error is moved aside and defaults are used. A file that parses but
has a value that no longer fits (a renamed theme, a typo in a hand edit) is not treated as
broken: only that `[section]` falls back to defaults and the app is told which one.
- A file written by a newer build is shown but never overwritten.
- Comments, key order and keys the app does not know survive saves. If two instances of an app
are open, each re-reads the file before writing so those survive; known keys are
last-writer-wins.
## Migration
- Schema changes: `Settings::migrate` upgrades the document one version at a time before it is
parsed. A failed step leaves the file untouched and the store read-only.
- Older per-app files (`prefs.json`, `ui.json`, `*.ron`): imported on the first run that finds no
`config.toml`, using the app's existing parser. The old file is **left in place**, so rolling
back to an older build keeps working; apps can remove it a few releases later.
## Where the apps are today
From the audit in #12 (Linux defaults, from source; overrides and portable mode change them).
Before an app moves over, check where it saves on macOS and Windows so the importer finds the old
files there too.
| App | Today | Moving to `craft-settings` |
|---|---|---|
| PhotoCraft | `~/.config/photocraft/preferences.json` + eframe `ui.ron`; atomic saves with retries; portable via `portable.txt`/`PhotoCraft.portable` → `PhotoCraftData/` | Import `preferences.json`; keep `ui.ron`; keep its portable names via `PORTABLE_MARKERS`/`PORTABLE_DIR` |
| DesignCraft | `~/.config/designcraft/prefs.json` + `ui.json`; saves on exit, ignores failures; empty `DESIGNCRAFT_CONFIG_DIR` and `DESIGNCRAFT_NO_PREFS` touch nothing | Import `prefs.json`; keep `ui.json`; gain save-during-use and error reporting; `DESIGNCRAFT_NO_PREFS` → `Location::Memory` |
| EffectCraft | `~/.config/effectcraft/prefs.json`, `shortcuts.json`, `script_settings.json`; atomic | Import all three into sections of one `config.toml` |
| FilmCraft | `~/.local/share/filmcraft/preferences.json`; atomic | Import from the data folder; settings move to the config folder |
| GridCraft | `~/.config/gridcraft/prefs.json` + `ui.json`; saves on exit, ignores failures | Same as DesignCraft |
| LightCraft | global `ui.json`; library `prefs.json`, `presets.json`, `view.json`; atomic, backups, overwrite protection | Global settings via `open`; library settings via `open_at(library_dir)` |
| VectorCraft | `~/.config/vectorcraft/ui.json` with engine preferences inside; atomic, ignores failures | Split engine preferences out into `config.toml`; keep UI state in `ui.json` |
| ArtCraft Launcher | `~/.local/share/ai.storyteller.artcraft-launcher/app.ron` (eframe) | Import from `app.ron`; needs its own tick timer while in the tray |
Adoption order: PhotoCraft and DesignCraft first (closest to and furthest from the target
behaviour), then the rest.
## Not yet
- Reloading settings when the file changes on disk while the app runs
- Removing imported legacy files
- Settings on the web (`wasm32`), where only the trait, document helpers and error types exist