# Dialogs (`dialog/*`)
The dialog system is a generic, themed alternative to ad-hoc popups.
Available in both the TUI hosts (`retro`, `retra`) and the scripting
binary (`deft`), with the same stdlib function signatures.
## Kinds
| Function | Result value |
|-----------|--------------|
| `dialog/prompt "Title" ?default` | the entered text |
| `dialog/password "Title" ?default` | the entered text (masked in UI) |
| `dialog/confirm "Title" ?prompt` | `"yes"` or `"no"` |
| `dialog/msgbox "Title" "Message"` | `nil` (dismissed) |
| `dialog/menu "Title" @{a b c} ?sel-idx` | the selected item text |
| `dialog/checklist "Title" @{a b c} ?checked`| newline-joined selected item texts |
| `dialog/search "Title" @{a b c} ?query` | the selected item text (incremental filter) |
| `dialog/file-open "Title" ?start-dir` | absolute path |
| `dialog/file-save "Title" ?start-dir ?fn` | absolute path |
Each stdlib function may also accept a single spec map:
[dialog/prompt %{ :title "Name" :default "Alice" }]
[dialog/menu %{ :title "Pick" :items @{a b c} }]
## Explicit Dialog Ids
Every function accepts an explicit id — a **leading keyword** or the spec
map's `:id` key — so the result event is identifiable without stashing the
returned request id:
[dialog/confirm :save "Save changes?"] [dialog/menu :manual-index %{ :title "Pick" :items @{a b c} }]
Ids are **per-panel scoped**: the result routes to the requesting panel's
own event queue, so two apps may safely reuse the same id (both can use
`:save`). Within one app, the same id may not be queued twice concurrently
— the second call returns `false`.
## Async Contract — Two Modes
### TUI builds (`retro`, `retra`)
The stdlib function returns immediately: `true` when an explicit id was
given, otherwise the auto-assigned numeric **request-id** (`false` when the
pending queue is full or a duplicate id is queued). Pending requests form a
bounded FIFO (8) — one dialog is visible at a time; requests that arrive
while one is showing wait their turn.
When the user submits, a `:message` event is pushed to the panel's
app-thread event queue with topic `"dialog-result"` and a structured
`:data` map:
%{ :type :message :topic "dialog-result"
:data %{ :id :save # your keyword (or the numeric request-id)
:value "yes" # the encoded result ("" when cancelled)
:cancelled false } }
The app's `on-event` handler dispatches on `:data~id`:
[dialog/confirm :save "Save changes?"] def on-event {|ev| if ($ev~topic == "dialog-result" and $ev~data~id == :save) { if $ev~data~cancelled { echo "cancelled" } else { echo "got: " $ev~data~value } } }
Without an explicit id, `:data~id` carries the numeric request-id the call
returned — see `packages/editor/src/editor.dft` for the canonical
structured-id pattern.
### Scripting binary (`deft`)
The stdlib function **blocks synchronously** on stdin, renders the ANSI
dialog, and returns the value directly. No event handler needed (ids are
ignored there).
deft> [dialog/prompt "What is your name?"] Alice "Alice"
The script blocks until the user submits; the return value is the
entered text.
## Why no `tui/popup` / `tui/file-open`?
Those stdlib functions were referenced in old docstrings but never
registered. The unified `dialog/*` namespace supersedes them.
The legacy `tui/file-save` (which only ever opened the
workspace-level Ctrl+Shift+S dialog) is unaffected.
## Spec Map Form
All stdlib functions accept a single spec map in place of positional
args. Supported keys:
| Key | Used by |
|---|---|
| `:id` | All dialogs (explicit result-routing id) |
| `:title` | All dialogs (defaults to "Input", "Password", "Confirm", ...) |
| `:prompt` | `confirm` (sub-text under the title) |
| `:default` | `prompt`, `password` (default text) |
| `:items` | `menu`, `checklist` (list of item strings) |
| `:selected` | `menu` (initial selection index) |
| `:checked` | `checklist` (initial set of selected indices) |
| `:start-dir` | `file-open`, `file-save` (initial directory; default `.`) |
| `:filename` | `file-save` (suggested filename) |
[dialog/file-open %{ :id :open :title "Open File" :start-dir "." }]
[dialog/file-save %{ :title "Save As" :start-dir "." :filename $default-name }]
## Examples
### Confirm before destructive action (TUI)
def cmd-delete {|path| @doc "Delete a file after confirming." set pending-delete $path [dialog/confirm :delete [str "Delete " $path "?"] "This cannot be undone."] # returns true immediately; result handled in on-event } def on-event {|ev| if ($ev~topic == "dialog-result" and $ev~data~id == :delete) { if !$ev~data~cancelled { fs/remove $pending-delete echo "deleted" } } }
### Menu selection (script)
def pick [dialog/menu "Pick a colour" @{red green blue}] echo "You picked: $pick"
### File open (script)
def path [dialog/file-open "Open file" "."] if not(empty? $path) { echo "Selected: $path" }
## Backend Notes
- **TUI build** — the dialog opens as a modal overlay centered on
the calling panel, themed with the workspace's current theme.
- **Script build** — the dialog renders inline using ANSI control
sequences. A C-bridged render function is wired at startup; the
result is returned synchronously when the user submits.
- **Shared spec type** — `src/host/dialog_types.zig` (open deft repo) defines the
dialog kinds and option structs used by both backends and by the
MCP debug plugin.
## Defaults
When no title is provided, the backend picks one: "Input",
"Password", "Confirm", "Message", "Select", "Open File", "Save As".
File dialogs default `:start-dir` to `"."`.
## Cancellation
A cancelled dialog returns:
- **`dialog/prompt` / `dialog/password`** — empty string `""`.
- **`dialog/confirm`** — `"no"`.
- **`dialog/menu` / `dialog/checklist`** — empty string `""`.
- **`dialog/file-open` / `dialog/file-save`** — empty string `""`.
- **`dialog/msgbox`** — `nil` (always; no cancel notion).
In TUI builds, the result arrives as `:data` of the `:message` event with
`:cancelled true` and `:value ""`.
## Quick Reference
| Form | Result |
|---|---|
| `[dialog/prompt "Title" ?default]` | text |
| `[dialog/password "Title" ?default]` | text (masked) |
| `[dialog/confirm "Title" ?prompt]` | `"yes"` or `"no"` |
| `[dialog/msgbox "Title" "Message"]` | `nil` |
| `[dialog/menu "Title" $items ?sel]` | selected text |
| `[dialog/checklist "Title" $items ?checked]` | newline-joined texts |
| `[dialog/file-open "Title" ?dir]` | path |
| `[dialog/file-save "Title" ?dir ?fn]` | path |
| `[dialog/ :my-id …]` | `true` (TUI); result via `:data~id :my-id` |
| Spec map form (`:id` key included) | same |