Settings: declared once, a page for free
An app declares its settings as typed structs; architect stores them (per device or synced to the person), and builds the Settings page — T3 Code's design — with search, reset and restore.
Status: the registry and the page landed (2026-11-01) in
architect-settings(features/settings). Settings are data (features/settings/src/model.rsSection,Setting,Control,Value), so an extension registers its own at run time (features/settings/src/registry.rsRegistry::register,Registry::remove_owner;features/settings/src/ui.rsuse_section) as the app does at launch;Valueschecks, resets and restores them;DeviceValues(featurestore) keeps the device’s;SettingsPage(featureui) is T3 Code’s page, opened by theAPP_SETTINGScommand (⌘,).The derive, the scopes and search to the row landed (2026-11-01).
#[derive(Settings)](features/settings/src/derive.rsSettings) makes a facet struct a section, anduse_settings::<T>()(features/settings/src/ui.rsuse_settings) its typed value;ScopedValues(features/settings/src/values.rs) holds values by scope over an org’sOrgSettings;ProjectValues(features/settings/src/store.rs) andDocValues(features/settings/src/synced.rs, featuresynced) store theProjectandSyncedscopes; a search hit opens its section, scrolls to its row and pulses it (SettingsHandle::show_setting). Still to come: Task’s and Studio’s panels as structs;Option<T>andKeyChordfields; which org’s settings apply (the current org, with organizations’ scoping); an admin’s page to edit them.
Every app has a Settings page and every app writes it by hand: Task’s six
panels, Studio’s and Session’s SettingsPanels, T3 Code’s twenty
sections — and in each, every row repeats what the type already says, and
the “restore defaults” list repeats every key again. Here an app declares
its settings once; the store, the page, its search, the reset arrows and
“Restore defaults” all come from the declaration.
Declaring#
/// Appearance
#[derive(Facet, Settings, Default)]
#[settings(section = "Appearance", icon = "palette", scope = Device)]
pub struct Appearance {
/// Theme
/// Follow the system, or always light or dark.
#[setting(control = cards)]
pub theme: ThemeMode, // an enum: its variants are the choices
/// Text size
#[setting(control = slider(80..=140, step = 5), unit = "%")]
pub text_size: u32,
/// Reduce motion
/// Drawers and menus appear without moving.
pub reduce_motion: bool, // a bool is a switch
}
/// Audio
#[derive(Facet, Settings, Default)]
#[settings(section = "Audio", group = "Studio", icon = "audio-lines", scope = Device)]
pub struct Audio {
/// Buffer size
/// Smaller is quicker to play, and harder for the computer.
#[setting(control = select, choices = [64, 128, 256, 512, 1024])]
pub buffer: u32,
}
/// Editing (follows the person to every device)
#[derive(Facet, Settings, Default)]
#[settings(section = "Editing", scope = Synced)]
pub struct Editing { /* … */ }
- The words come from the doc comment: its first line is the title, the rest the description; a struct’s is its section’s title.
- The control comes from the type unless
controlsays otherwise:bool→ switch; an enum → select (orsegmented,cards,list); numbers → a stepper field (orslider(range));String→ text;Option<T>→ T with an off; aKeyChord→ a shortcut recorder; a struct → a nested group. - Scope is where a value lives:
Device(this device:Store<T>,DeviceValues),Synced(the person’s, on every device:DocValues, below), orProject(the open project’s:ProjectValuesin the project’s directory, or aDocValuesover the project’s own document). - The derive also gives typed access:
use_settings::<Audio>()returns a memo of the value andupdate(|a| a.buffer = 256); each field that changed is stored in its scope and seen by everything reading it. Outside the UI,Audio::read(®istry, &values)is the same value (the audio engine reads its buffer that way, before any page exists). - Today the derive takes
bool, integers, floats,String, enums of unit variants and one level of nested struct; a field of any other type is left off the page and keeps its default.
Scopes, synced, and the org#
ScopedValues holds the three scopes’ values and the org’s
OrgSettings under them. A value reads, in order: the org’s lock, its
own scope’s value, the org’s default, the declared default. A locked
row stays on the page, dimmed, saying “Set by your organization”;
setting it is refused (ValueError::Locked). Which org’s settings
apply is the app’s to say (SettingsHandle::set_org when the window’s
org changes).
A synced setting can be kept on one device instead
(ScopedValues::set_local, the cloud and laptop toggle on its row,
Notebook Navigator’s per-setting sync mode): the device’s values list it
in Values::local, and it is read and written there, starting from the
person’s value; given back, the person’s value holds again.
ScopedValues::scope_of says where a setting is kept now.
Every change reaches the provider’s on_change as a SettingsChange
(the scope, its values, the keys that changed), so the app sends it to
that scope’s store. The Synced store is a CRDT document: DocValues
keeps the values in its root map, one entry per key, so two devices
that changed different settings offline both keep theirs, and a key
changed on both takes the later write. Every device opens the person’s
document under the same id (synced_doc_id(app, person)) with
crdt::docs’ Lineage::Merge, syncs it like any app document, writes
only the changed keys (save_keys), and on on_remote_change puts
load() back with SettingsHandle::replace.
- An app adds a hand-written section where a generated one cannot do
(Account, About, a theme library):
Section::custom("Account", icon, || rsx! { … }), in the same page and search.
The page (T3 Code’s)#
On a desk: a modal over the window (rounded, shadowed, up to 80rem by
60rem; ⌘, opens it, Escape closes). Its left column lists the sections in
groups (a group’s heading small and muted; each section an icon and a
name), a search field at its top (/ or ⌘F). Its right column: a title
row with the breadcrumb (Settings › Appearance) and the page’s actions
(“Restore defaults”), then the section’s groups, at most 56rem wide.
On a phone: the iOS grouped list. The root lists the sections in the same groups (rounded cards, 24pt corners; a row 56pt: an icon, the name, its value muted on the right, a chevron); a section pushes its own page of grouped rows. The search filters sections. On an iPad the root is the sidebar and the section beside it.
A row: the title (medium weight), its description under it (small, muted), the control on the right; under a narrow width the control drops below. Beside the title, two small slots: a dot when the value is not the default, and a reset arrow (on hover; always on touch) that puts the default back. A row that cannot be changed now (signed out, no device) stays, dimmed, saying why.
The page’s actions: “Restore defaults” lists every changed setting
in the section and asks before resetting them. Search finds a setting by
its title, its description and any aliases, opens its section and
scrolls to the row, which pulses.
The controls: a switch; a select (a small trigger, the choices in a menu); a segmented control; a slider with its value in a chip (“65%”); cards for a few visual choices (Light / Dark / System); a stepper field; a text field (saved on blur when it parses); a shortcut recorder (press to record, chips for keys).
Where it opens#
- The account row at the bottom left of the app shell and the phone’s drawer have Settings.
- ⌘, on a Mac (“Settings…” in the app menu: app menus).
- An action,
app.settings { section? }, so the palette and a model can open it at a section.
From what exists#
architect::settings::Store<T>stores theDevicescope as it is.architect::schemaalready walks a facet struct’s fields (names, docs, defaults) for the MCP schema; the derive reads the same, plus its attributes.- Task’s
UserPrefsentity andPrefsServiceare the pattern for theSyncedscope, generalised so any app’s settings structs can sync. - architect-form’s
Fieldgives the text and number controls their parse-and-validate behaviour.
Decided#
- Synced settings’ storage: one per-person CRDT document per app
(
DocValues), synced as an app document; no service per app. - Per-project values: a setting declared
Projectlives in the project’s store, not as an override of a device value. - Settings an app reads before the UI exists:
T::readover the values loaded from the stores, the same type the page uses.
Linked from
- The Architect tourA guided tour of every system, in the sandbox: one chapter each, over the live app, built from a guide scenario.
- The UI primitivesWhat architect ships to draw an app with: architect-ui's components and theme tokens, the shell's page parts, every kind of tab and pane in the workspace, the commands overlays, the settings page, and architect-story to browse them.
- SettingsDeclare settings once as a typed struct; architect stores each in its scope (this device, the person's, the project's) under an org's defaults and locks, and draws the settings page with search to the row.
- SettingsSettings declared once (a struct, or data an extension registers), held by scope over an org's defaults and locks, stored on the device, the project or the person's synced document, and drawn as one settings page.
- Organizations: the spine everything is scoped byThe organization (workspace) is architect's, not an app's: files, documents, settings, extensions, presence, permissions and the shell's switcher all know which org they are in, so every app gets multi-org behaviour without writing it.
- Actions everywhere: bindings, ⌘K and ⌘OREAPER's Action List, taken further: everything any UI does is an action, any action binds to any number of keys, sequences, held-letter mouse tools, MIDI or OSC, per context; which-key, a ⌘K palette and a ⌘O open-anything come free.
- Extensions and scriptsAdding to an app at runtime, without rebuilding it: data-only extensions, WASM components over a versioned WIT world drawn from the Action List, Luau scripts for macros, out-of-process plugins on a desk — with capabilities, and views as frame documents.
- ThemingRuntime themes: a small palette resolved into every semantic token, a cascade of layers from the app's default to a transient preview, theme files that hot-reload, THEME_* actions, and how the shell, architect-ui and view kinds follow one store.