# Prelude
The prelude is Deft's project-level configuration file. It lives at
`.deft/prelude.dft` and is auto-loaded by every runtime in the project —
the scripting binary, the TUI hosts (`retro`/`retra`), and any child
runtimes you spawn. It's a plain Deft data file whose top-level form is
a single `%Prelude{...}` tagged map.
%Prelude{
:mounts %{ "stdext" ".deft/packages/deft/stdext/0.2.0/src"
"launcher" ".deft/packages/deft/stdext/0.2.0/src" }
:pkgs @{ ".deft/packages" }
:registry "https://packages.deft-lang.org"
:themes %{ :default "Dracula" }
:font %{ :family "JetBrains Mono" :size 16 }
:window %{ :width 2200 :height 1200 }
:workspace %{ :default "ide" }
:keys %{ :prefix "ctrl-b" }
}
`deft init` writes a template prelude for you. Edit it to customise
mounts, package roots, registry, theme, and TUI defaults.
## When the Prelude Loads
The host runtime loads the prelude very early — before any user code
runs and before the TUI workspace is built:
1. Runtime starts; `settle()` registers built-in stdlib functions.
2. `loadPrelude()` reads `.deft/prelude.dft` from the current working
directory. If absent or unparseable, the prelude is `nil` and every
field falls back to its built-in default.
3. Mounts from `:mounts` and package roots from `:pkgs` are registered on
the VM.
4. `:registry` configures the package plugin's URL.
5. The config registry autoloads per-package defaults from each
`:pkgs` root, then layers `.deft/config.dft` on top.
6. TUI only: `:window` is read to size the window and `:font` is
resolved to a font file (before the Landlock sandbox applies, so the
font directories can be scanned); `:themes`, `:workspace`, and
`:keys` are stashed for the workspace builder.
7. User code runs (the launched file, REPL input, or workspace spec
eval).
Silent no-op semantics: a missing or malformed prelude is not an error.
The host proceeds with defaults. This means scripts work even when run
outside an initialised project.
The loaded map is validated against the built-in `:Prelude` deftype
(strict, all errors collected): unknown fields and wrong value types
print a `warning: prelude: … schema violation …` line naming the
offending path, but the prelude still applies. See the
Schema page for the field list and the wider vocabulary.
## Fields
### `:mounts` — mount aliases
A map from mount name → target. Each entry registers a mount:
`import "<name>/..."` (and any `:source` path that begins with
`<name>/`) is rewritten to the real path before resolution. Mount
names may be multi-segment — package references like `"deft/stdext"`
work, and imports match the longest mount prefix first.
Value type decides what a mount means:
- string value — an import mount (the classic form): the name maps
to a local directory for `import` and `:source` resolution.
- map value (or `%Local{}` / `%Ssh{}` tagged map) — a VFS
mount: `fs/*` paths whose leading segment(s) name the mount route
to its driver (a local anchor directory or a remote host over ssh).
Never an import mount.
:mounts %{ "deft/stdext" ".deft/packages/deft/stdext/0.2.0/src" "apps" "packages" "themes" "${HOME}/.config/deft/themes" # VFS mounts — fs/* paths under the name route to the driver :docs %Local{ :root "/usr/share/doc" } :prod %{ :kind :ssh :host "10.0.0.5" :root "/srv/app" :user "root" } }
[fs/read "docs/misc.txt"] # → /usr/share/doc/misc.txt [fs/read "prod/etc/hostname"] # → /srv/app/etc/hostname on 10.0.0.5
After the above, both `import "deft/stdext/html"` from Deft code and a
workspace config's `:source "apps/editor/src/editor.dft"` resolve
through the configured directories. VFS mounts instead serve the `fs/`
namespace — see VFS for path semantics, the ssh backend, and
the runtime `[vfs/mount]` API.
Keys may be strings or keywords (`"deft/stdext"` and `:stdext` both
work); values must be strings (import mounts) or maps (VFS mounts).
Path literals are taken as-is — there's no `$HOME` expansion at the
language level, but the host's standard `[fs/expand-path]` runs on
mount registration. VFS mounts seeded from the prelude land in a
process-global table, so app threads that skip the prelude load see
them too; note that `:kind :ssh` mounts need subprocess and network
access, which the Landlock sandbox denies (a warning is printed when
the sandbox is active).
Explicit `:mounts` entries always win over the package auto-scan
(next section) — use them to pin a package to a specific checkout.
`deft init` scaffolds a `launcher` mount (used to resolve the TUI
launcher — see [`:launcher`](#launcher) below); the stdext package
mounts need no prelude entries.
### `:pkgs` — package search roots
A vector of directories to scan for packages. The first entry is
the install target used by `[pkg/install]`. Entries may be relative
(resolved against the current working directory).
:pkgs @{ ".deft/packages" }
A single string is also accepted:
:pkgs ".deft/packages"
At startup every root is scanned and each package found (dev checkout
or installed version) is auto-mounted under `owner/name`, plus
`owner/name@version` pins and the `@latest` alias — no `:mounts`
entries needed. Roots are scanned in listed order; the first root
providing a package wins. This lets you layer a project-local
packages dir over a shared system one. Explicit `:mounts` entries
override the scan.
### `:registry` — package registry URL
When set to an `http://` or `https://` URL, the `pkg/*` stdlib functions
(`pkg/install`, `pkg/fetch`, `pkg/info`, `pkg/available`) talk to that
registry instead of the built-in local virt-pkg tree. Anything other
than an HTTP(S) URL is ignored.
:registry "https://packages.deft-lang.org"
Use this to point a project at a private registry (typically
nginx-terminated, serving static package tarballs).
### `:themes` — TUI theme default
A map with a `:default` key naming the theme to apply when the loaded
workspace config doesn't specify its own `:theme`.
:themes %{ :default "Dracula" }
The TUI host searches the conventional theme locations (user overrides,
stdext defaults, legacy `./themes/`) for a file matching the name.
Workspace-level `:theme` overrides this; see
Workspace configs below and the TUI docs.
### `:window` — TUI initial window size
Pixel dimensions for the TUI window. Honoured only by the `retro`
build (the OpenGL host); `retra` is constrained by the terminal
emulator.
:window %{ :width 2200 :height 1200 }
Both keys are optional; missing dimensions fall back to the binary's
compiled-in default.
### `:font` — TUI font (GL hosts)
The monospaced font behind the GL hosts' workspace cell grid, panel
windows, and surface text (`draw/text-px`). Honoured by `retro` and
`hydra`; `retra` renders through the terminal emulator's
font and ignores this key.
:font %{ :family "JetBrains Mono" :cut :regular :size 16 } # portable :font %{ :path "/home/u/.fonts/FiraCode.ttf" :size 16 } # exact user file :font "Source Code Pro" # family shorthand
- `:family` + `:cut` is the portable form — it names a face without
hard-coding any distro's font tree. `:cut` is `:regular` (default),
`:bold`, `:italic`, or `:bold-italic` (`:oblique` aliases italic;
case/space/hyphen-insensitive). The font directories are discovered
from the system's fontconfig configuration — the `<dir>` entries in
`$FONTCONFIG_FILE` (when set), else `~/.config/fontconfig/fonts.conf`
and `/etc/fonts/fonts.conf` plus its `conf.d/*.conf` snippets — and
the `$XDG_DATA_DIRS` entries, so nonstandard trees (X11 subdirs,
Flatpak exports, the Nix store) resolve too. User dirs
(`$XDG_DATA_HOME/fonts` else `~/.local/share/fonts`, `~/.fonts`) are
searched first; `/usr/local/share/fonts` and `/usr/share/fonts` are
the fixed fallbacks. The scan is recursive
(`.ttf`/`.otf`/`.ttc`) and picks the best match by normalised file
name: the requested cut wins, then `Regular` cuts, then shorter
stems. A family that ships no requested
cut falls back to another face with a warning. `:path` uses an
exact file (relative paths resolve against the working directory) as
the fallback when `:family` is absent or doesn't resolve — it is for
user-specific fonts, not for project or workspace configs.
- Candidates must be effectively monospaced: declared fixed-width,
or with uniform glyph advances (some genuinely mono fonts — Google's
Noto Mono TTFs, for one — don't declare the flag). The probe also
rasterizes probe glyphs and requires each bitmap to fit its cell and
map to real distinct Latin glyphs, so proportional fonts (and
Latin-less script fonts, which would render as boxes) are rejected
with a warning; the cell grid advances by a fixed cell width, so they
would mis-render.
- `:size` is the pixel size, clamped 6–128 (default 18).
- Failures warn on stderr (`font: ...`) and fall back to DejaVu Sans
Mono. A successful resolution logs
`font: using <path> (prelude :font ...)` so the choice is
confirmable from the terminal.
- The CLI flags `--font` / `--size` win per-field — the prelude is the
project default, flags are per-invocation overrides.
Any character, no rebuild. The rasterized repertoire is NOT baked
in: besides the startup set (ASCII + box-drawing + the UI symbol
chrome), every other codepoint rasterizes on demand the first time
it appears — Cyrillic, Greek, CJK, emoji, any symbol — and stays cached
in growable atlas pages. Two optional keys tune that:
:font %{ :family "JetBrains Mono" :size 16 :fallback @{ "/usr/share/fonts/.../NotoSansMonoCJK-VF.ttc" } # faces tried for chars the primary lacks :ranges @{ "0x0400-0x04FF" "0x4E00-0x9FFF" } # pre-rasterize hot scripts }
- `:fallback` (string or vector of family names / file paths,
up to 4) registers faces consulted in order when the primary font
lacks a codepoint — the typical shape is a Latin mono primary plus a
CJK/symbol fallback. The monospace probe does not apply to fallback
paths; only the primary drives the cell grid. Note the family matcher
scores FILE STEMS, not font metadata — CJK collections with
style-suffixed filenames (`NotoSansMonoCJK-VF.ttc`) resolve more
reliably by exact `:path`.
- `:ranges` (string or vector of `first-last` hex with optional
`0x` prefix, or bare codepoints) pre-rasterizes ranges into the
glyph cache at startup so first use doesn't pay a small rasterization
hitch mid-frame. Purely an optimization — everything renders on
demand regardless.
- Both keys always come from the prelude (CLI `--font` picks the
primary face only), and both feed the sandbox's read-only grants.
- East Asian Wide scripts (CJK, kana, Hangul, fullwidth) occupy two
cells per glyph — measure display width with `[width $s]` instead of
`len` when laying out mixed-script text.
Interactive zoom. In `retro` and `hydra`, `Ctrl+=` / `Ctrl+-`
(also keypad `+`/`-`, and `Ctrl+Shift+=` for the `+` glyph) step the
font size, and `Ctrl+0` resets it to the startup size — browser-style,
global over dialogs and apps. `retro` zooms the grid and every
popped-out panel window together; `hydra` zooms the whole instance
(new windows inherit the zoomed size). The zoom is session-only —
persist a size with `:size` here.
Applying from code. Fonts also change live via stdlib verbs (same
resolution and monospace rules as this key):
[tui/set-font "JetBrains Mono" 16] # family (or file path) + size [tui/set-font %{:family "JetBrains Mono" :size 16}] [tui/set-font %{:size 20}] # size-only (keeps the font) [tui/font] # => %{:path "..." :size 16} [tui/font-list] # => @("Adwaita Mono" "Deja Vu Sans Mono" ...)
Resolution and the fixed-width check run immediately (the call returns
`false` when the family doesn't resolve); the rebuild lands on the next
host frame — grid, popped windows, and surface text all follow. Pair
`tui/font-list` with `dialog/search` for a font picker in your own
apps. Like the zoom, applies are session-only — persist a choice with
`:font` here. GL hosts only (`retro`/`hydra`; `retra` renders through
the terminal emulator). `tui/set-font` changes the primary face and
size; `:fallback`/`:ranges` are prelude-configured and survive applies.
### `:workspace` — default workspace (TUI)
When the TUI host starts without an explicit `--workspace` CLI flag, it
looks here for the workspace config to auto-load.
:workspace %{ :default "ide" }
This loads `.deft/workspaces/ide.dft` (see
TUI: App Platform for the workspace spec
format).
### `:keys` — workspace key bindings (TUI)
Key bindings for the TUI hosts. Currently one setting: `:prefix`, the
chord that arms a workspace command (the follow-up key picks it —
`v`/`h` split, `x` close, `t` cycle theme, `d` dock, `p` pop out, …).
:keys %{ :prefix "ctrl-b" } # tmux-style (default) :keys %{ :prefix "ctrl-a" } # screen-style :keys %{ :prefix :ctrl-space } # keyword form :keys %{ :prefix "off" } # disable the prefix chord
Modifiers (`ctrl`, `alt`, `shift`) precede the key name, separated by
`-`, `_`, or `+`; key names are letters (`ctrl-b`), digits (`ctrl-1`),
`space`, `slash`, `tab`, `enter`, `escape`, or `f1`..`f12`. The value
may be a string or keyword. Unknown or missing values keep the built-in
Ctrl+B default.
The live config key `ui/prefix` overrides the prelude — set it with
`[config/set "ui/prefix" :ctrl-a]`, the config app, or
`.deft/config.dft`, and it applies immediately (no restart). The prelude
supplies the project default. Both TUI hosts (`retro` and `retra`)
honour the chord; the follow-up commands are unchanged.
### `:sandbox` — landlock sandbox path lists
When the Linux sandbox is enabled (`DEFT_SANDBOX=1`), the `:sandbox`
map's read-only and writable path lists bound the process's
filesystem access via Landlock:
:sandbox %{ :readonly @{ "/etc" "/usr/bin" "/usr/share" "/proc" } :writable @{ "~/.config" "~/.local" } }
`~` is expanded to `$HOME` when the sandbox is applied. Edit the
lists by hand or with `deft sandbox add/remove/list`. `deft init`
writes a template block. See Sandbox for the full story —
what's always granted, kernel requirements, and limits.
### `:launcher` — launcher app (TUI)
The launcher is the app that opens in newly created panels — splits
(`Ctrl+B v` / `Ctrl+B h`), group adds, and Workspace → New on the
bottom taskbar's Workspace menu. The TUI hosts launch the source path
`launcher/launcher.dft`, resolved through the `launcher` mount in
`:mounts`:
:mounts %{ "stdext" ".deft/packages/deft/stdext/0.2.0/src" "launcher" ".deft/packages/deft/stdext/0.2.0/src" }
`deft init` scaffolds both mounts pointing at the installed stdext, so
the built-in picker is found without further configuration. Resolution
order when the launcher is launched:
1. A top-level `:launcher "path/to/app.dft"` field, when present — a
full source path, resolved through `:mounts` mounts like any other.
2. `launcher/launcher.dft` through the `launcher` mount, when the file
exists.
3. Fallback: `stdext/launcher.dft` through the `stdext` mount (the
built-in picker).
Replacing the launcher. The launcher is itself just a Deft app, so
you can swap it for your own — a domain-specific dashboard, a kiosk
mode, a company-branded picker. Two ways:
# 1. Repoint the launcher mount at your app's directory # (convention: the file is <dir>/launcher.dft) :mounts %{ "launcher" "myapp" } # 2. Or set the explicit field for a full source path :launcher "myapp/picker.dft"
The explicit field wins; the mount is the idiomatic form since the same
alias serves `import "launcher/..."` from Deft code. See
TUI: App Platform for building a custom
launcher.
## Runtime Access
The loaded prelude is available at runtime via the `prelude` stdlib function:
echo [prelude] # => %Prelude{ :mounts {...} :pkgs [...] :registry "..." ... } # Read a single field echo [prelude]~registry # => "https://packages.deft-lang.org" echo [prelude]~themes~default # => "Dracula" # Returns nil when no prelude is loaded (running outside a project) echo [prelude] # => nil
This is the canonical way for a script or app to discover project
configuration without re-reading the file.
## What the Prelude Is Not
- Not user settings. Per-user preferences (theme overrides at the
user level, key bindings, editor options) belong in
`[config/*]` — see Config.
- Not secrets. The prelude is plain Deft data, typically checked
into version control. Don't put API keys or passwords here.
- Not environment-specific. The prelude ships with the project; if
you need machine-specific overrides (CI vs. laptop, prod vs. dev),
express them via `os/env` lookups in your app code, or use the
`config/*` registry layered on top of `config/load`.
## See Also
- Runtimes — every runtime loads the prelude at startup
- VFS — map-valued `:mounts` entries declare VFS mounts
- Config — runtime key/value preferences, layered on top
- Sandbox — the `:sandbox` key configures the Landlock path lists
- TUI: App Platform — TUI workspace configs
- Tooling: Packages — `pkg/*` stdlib functions and
the registry