# 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" }
}
`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` and `:workspace` 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.
## Fields
### `:mounts` — mount aliases
A map from mount name → target. Each entry registers a **mount**:
`import "/..."` (and any `:source` path that begins with
`/`) 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](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](#workspace) 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`,
`retrs`, and `hydra`; `retra` renders through the terminal emulator's
font and ignores this key.
:font %{ :family "JetBrains Mono" :size 16 } # resolve by family name :font %{ :path "/home/u/.fonts/FiraCode.ttf" :size 16 } # exact file :font "Source Code Pro" # family shorthand
- **`:family`** scans the conventional font directories
(`$XDG_DATA_HOME/fonts` — else `~/.local/share/fonts` — then
`~/.fonts`, `/usr/local/share/fonts`, `/usr/share/fonts`; recursive,
`.ttf`/`.otf`/`.ttc`) and picks the best match by normalised file
name, preferring `Regular` cuts. **`:path`** uses an exact file
(relative paths resolve against the working directory) and wins over
`:family` when both are present.
- 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 (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](../06-TUI/app-platform) for the workspace spec
format).
### `: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](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](../06-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](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](runtimes) — every runtime loads the prelude at startup
- [VFS](vfs) — map-valued `:mounts` entries declare VFS mounts
- [Config](config) — runtime key/value preferences, layered on top
- [Sandbox](sandbox) — the `:sandbox` key configures the Landlock path lists
- [TUI: App Platform](../06-TUI/app-platform) — TUI workspace configs
- [Tooling: Packages](../07-Tooling/packages) — `pkg/*` stdlib functions and
the registry