# Config
`config/*` is a global, persistent, reactive key/value store — Deft's
equivalent of dconf or the Windows Registry. It sits at the same
layer as `pub/*` (on the Runtime, not the TUI Workspace), so it is
available in **both** the standalone `deft` binary and the TUI hosts.
Keys are hierarchical strings using `/` as the separator, prefixed
with the owning app for namespacing: `log/max-lines`,
`editor/font`, `config/auto-save`. Cross-cutting keys with no owning
app live at the top level: `ui/theme`, `window/width`.
## `config/*` Stdlib functions
| Function | Returns | Purpose |
|---|---|---|
| `[config/get "key"]` | value or nil | Read |
| `[config/set "key" value]` | stored value | Write + publish change |
| `[config/unset "key"]` | bool | Remove; publishes with `:value nil` |
| `[config/scan "prefix"]` | map | All keys starting with prefix |
| `[config/keys "prefix"]` | list | Matching key names |
| `[config/count]` | number | Total key count |
| `[config/load ?path]` | bool | Load `.deft/config.dft` |
| `[config/save ?path]` | bool | Write entire registry |
| `[config/save-overrides ?path]` | count written | Diff against defaults; write only changed |
| `[config/save-map $map ?path]` | bool | Write a given map |
## Prefixing — Manual, Not Automatic
Apps declare their prefix once and reuse it. No runtime magic — the
"which app am I in?" question is genuinely ambiguous across CLI
scripts, TUI panels hosting multiple apps, and child runtimes, so
the registry never tries to answer it.
def cfg "log/" # Explicit prefix at each call site — no hidden resolution config/set ($cfg + "max-lines") 1000 echo [config/get ($cfg + "max-lines")] # Or a thin wrapper when call-site noise matters def set-cfg {|k v| config/set ($cfg + $k) $v]} def get-cfg {|k| config/get ($cfg + $k)]} set-cfg "max-lines" 1000
Cross-app reads stay explicit (`[config/get "editor/font"]` from
inside `log`), and cross-cutting keys (`ui/theme`) live at the top
level without contortion.
## Reactivity
`config/set` and `config/unset` publish a change event on the
`"config"` pub/sub topic. Subscribers receive it in the standard
event map under `:data`:
%{ :type :message :topic "config"
:data %{ :key "log/max-lines" :value 1000 :old 500 } }
Subscribe and filter by key prefix in the handler:
def on-config {|ev| @doc "React to log config changes." if [starts-with? $ev~data~key "log/"] { echo "log config changed:" $ev~data~key "->" $ev~data~value [reload-logger] | } } pub/subscribe "config" "on-config"
See [Pubsub](pubsub) for the broker contract.
## Values — Stringifiable Data Only
Registry values must round-trip through `deft/stringify` /
`deft/parse`:
- Scalars (numbers, strings, booleans, nil)
- Keywords
- Lists, maps, sets
- Tagged maps
Function values are stored as **names** (strings) and resolved at
the call site via `[apply $name ...]`. `apply` accepts either a
string/keyword name (resolved through the namespace chain) or a
direct callable value (closure, native-func, keyword, map,
tagged-map, set, vector).
## Persistence
`config/save` writes the entire registry to a flat map file (default
`.deft/config.dft`) as `%{ "key1" v1 "key2" v2 }`. `config/load`
reads such a file and replaces registry contents. The file is plain
Deft data — diff-friendly, hand-editable, and version-control-
friendly.
# Per-package defaults ship as small data files (keyword keys are idiomatic): %{ :log/max-lines 1000 :log/wrap true :log/severity :all }
`config/save-overrides` diffs against loaded defaults and writes
only changed keys — this is the canonical save path for the config
app. Load accepts both string and keyword keys; save emits string
keys.
## Loading Defaults at Startup
A typical package's `on-init` loads defaults if the registry is
empty:
def on-init {|| @doc "Load log defaults on first run." if (empty? [config/scan "log/"]) { config/load "packages/log/defaults.dft" | } pub/subscribe "config" "on-config" }
## The Config App
The TUI ships with a config app (see `packages/config/src/config.dft`)
that:
- Reads `[config/scan $prefix]` to list keys.
- Lets the user toggle booleans, edit strings/numbers.
- Writes back via `config/set`.
- Persists changes via `config/save-overrides` — only the keys the
user explicitly changed, not the full registry.
## What Lives Where
- `tui/get` / `tui/set` — **ephemeral per-session UI state** (cursor
position, selection, scroll). TUI-only, never persisted. Different
concern; don't conflate.
- `config/*` — **persistent user preferences**. All hosts. Disk-backed
via `config/load` / `config/save`.
## Examples
### A simple preference
config/set "editor/font-size" 14 echo [config/get "editor/font-size"]] # => 14 config/unset "editor/font-size" echo [config/get "editor/font-size"]] # => nil
### Scanning for a namespace
def log-config [config/scan "log/"] # => %{ :log/max-lines 1000 :log/wrap true :log/severity :all } for k [keys $log-config] { echo "$k = $log-config~$k" }
### Reactive theme switching
def apply-theme {|ev| if ($ev~data~key == "ui/theme") { echo "switching theme to $ev~data~value" [load-theme $ev~data~value] | } } pub/subscribe "config" "apply-theme" # Anywhere: config/set "ui/theme" "catppuccin-mocha"
## Quick Reference
| Form | Purpose |
|---|---|
| `[config/get "key"]` | Read |
| `[config/set "key" value]` | Write + publish |
| `[config/unset "key"]` | Remove + publish |
| `[config/scan "prefix"]` | All matching keys |
| `[config/keys "prefix"]` | Matching key names |
| `[config/count]` | Total keys |
| `[config/load ?path]` | Load from disk |
| `[config/save ?path]` | Write all to disk |
| `[config/save-overrides ?path]` | Write only diff from defaults |
| `[config/save-map $map ?path]` | Write a given map |