Skip to main content

Configuration

A form is a tree of panels, each holding fields, built fluently. Rules are named-argument spec objects, so the IDE completes them and a typo fails at declaration time:

use DrevOps\Tui\Builder\Form;
use DrevOps\Tui\Builder\PanelBuilder;
use DrevOps\Tui\Condition\Condition;
use DrevOps\Tui\Derive\Derive;

$form = Form::create('My form')
->panel('general', 'General', function (PanelBuilder $p): void {
// text | select | suggest | confirm | toggle | number | calendar
// textarea | password | search | reorder | filepicker | pause
// select, search and filepicker collect a list with ->multiple()
$p->text('name', 'Produce name')->required();

// Compute one field from others.
$p->text('slug', 'Slug')->derive(new Derive('{{name}}', transform: 'machine'));

$p->select('category', 'Category')
->default('fruit')
->options(['fruit' => 'Fruit', 'vegetable' => 'Vegetable']);

// Shown only when the condition holds; compose with Condition::all()/any()/not().
$p->text('variety', 'Variety')->when(new Condition('category', eq: 'vegetable'));
});

Each field builder chains ->description(), ->default(), ->required(), ->weight(), ->options() / ->option() (with per-option descriptions and optional disabled state), ->heading() / ->separator() (non-selectable option-list structure), ->when(new Condition(...)), ->derive(new Derive(...)), ->discover(...), ->validate(...) and ->transform(...).

The form declares its own chrome: ->banner() sets a start banner and ->buttons() controls the submit/cancel buttons. The global TUI runtime - describing the terminal, not the questionnaire - is configured on the Tui facade instead: ->theme() names a theme, auto-detected from the terminal background when unset (see Themes), ->keys() sets the key bindings (see Key bindings), ->footer() toggles the key-hint footer, ->clearOnExit() keeps or clears the final frame, ->color() / ->unicode() force a display mode, and ->translator() presents chrome and questions in another language (see Translations).

Derived values

A Derive computes a field from other answers: a template with {{field}} placeholders plus a transform. A transform is any str2name conversion (machine, kebab, pascal, ...) plus host, lower, upper and initials - an unknown name throws when the form is declared. Chains of derives settle to a fixpoint:

$p->text('slug', 'Slug')->derive(new Derive('{{name}}', 'machine'));
$p->text('label', 'Label')->derive(new Derive('{{category}}/{{slug}}', 'lower'));
$p->text('code', 'Code')->derive(new Derive('{{name}}', 'constant'));

Conditional fields

A ->when() rule shows or hides a field based on other answers, with operators eq / ne / in / contains, composable with Condition::all(), Condition::any() and Condition::not():

$p->text('heat_level', 'Heat level')->default('mild')->when(new Condition('category', eq: 'vegetable'));
$p->confirm('warning_label', 'Add a heat warning?')->when(Condition::all(new Condition('category', eq: 'vegetable'), new Condition('heat_level', ne: 'mild')));

A form-level ->fixup(new Fixup(set: ..., to: ..., when: ...)) reconciles dependent answers on every settle pass, so an answer that no longer makes sense after another change is corrected instead of leaking through.

Derived values, conditional fields and fix-ups each have a runnable script in playground/06-form-logic/.