# 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