Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

102 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

TUI logo

Terminal user interfaces for PHP

GitHub Issues GitHub Pull Requests Test PHP codecov GitHub release (latest by date) LICENSE Renovate


Terminal form with a rounded border

drevops/tui is a PHP library for panel-based terminal forms: keyboard-driven questionnaires that collect a set of answers and hand them back to your code as typed values.

  • Declarative form model. A form is declared with a fluent builder (Form / PanelBuilder / FieldBuilder): panels of typed fields, each with its own options, conditions, derivation rules and behavior.
  • Two collection modes, one declaration. The same form runs as a full-screen interactive TUI on a terminal, or resolves non-interactively from a JSON payload, per-field environment variables, discovery rules and defaults.
  • Application-agnostic. The library doesn't know (or care) what application it serves; questions and handlers live in your code, and applying the collected answers is your job. It collects; you apply.
  • Dependency-light. The runtime dependency surface is a single string-transform package.

The padded rounded border above is the default look. The same form explicitly opted out of the frame (border none, normal spacing):

Terminal form without a border

πŸ“– Documentation

Full documentation lives at phptui.dev. The in-development build, rebuilt from main ahead of each release, is previewed at tui-docs.netlify.app.

Core concepts

A screen is built from four levels, and each owns a fixed set of capabilities. When something does not obviously fit, the question is never "where does this go" but which level owns the capability it needs.

Screen        the root; occupies the terminal, or fits its contents
└─ Layout     arranges; reusable by name
   └─ Region  holds blocks and flows them; declares whether it scrolls
      └─ Block   drawn in a region

One kind of block - a panel - contains a layout, which starts the chain again. That is where depth comes from, rather than from a fifth level. Seven kinds of block exist: Panel, Field, Markup, Breadcrumb, Legend, Actions and Progress. Only a field collects, so only a field reaches the answers; everything else shows, focuses or activates.

Three things follow, and they are what the rest of the library is shaped by:

  • One tree. The builder writes blocks directly, so $form->root() is what the interactive screen draws, what the headless collector reads, and what the JSON schema describes.
  • Blocks say what they can do. Each declares its capabilities as interfaces, so a driver asks "does this bind keys, does it collect" rather than "which class is this".
  • Themes say how it looks. A block asks the theme for one element at a time and hands it a plain string; order and spacing belong to the block, color and glyph to the theme.

The whole model is written out at phptui.dev/specification.

Features

Every feature has a reference page and a runnable, self-contained example in playground/:

Feature Summary Docs Example
🧭 Full-screen TUI Scrollable panel browser: hubs drill into sub-panels to any depth, contextual key-hint footer, ? help overlay panels 03-panels-*
πŸͺŸ Modal panels A panel marked ->modal() opens as a centered dialog over its dimmed parent, with its own submit/cancel buttons panels 03-panels-*
🧱 Panel grids ->layout(1, 2) arranges sub-panels as a grid of side-by-side preview columns - rows of any width, recursively per level, with spatial arrow navigation panels 03-panels-*
🧭 Layouts ->layout('two-column') arranges a screen or a panel into named regions, each with its own size, flow and scrolling; register a layout class of your own and pick it by name layouts 20-layouts-*
πŸ–₯️ Fullscreen mode ->fullscreen() stretches the frame to the whole terminal; halign/valign anchor the content and min/max size options guard small or very wide terminals panels 03-panels-*
⚑ Inline editing A field's editor opens in place on the panel row; ->standalone() opts a field out to full-screen panels 04-inline-editing
🧩 Fields 15 field types: text, template, number, rating, calendar, textarea, password, select, reorder, suggest, search, file picker, confirm, toggle, pause - plus markup and progress blocks, which collect nothing fields 02-fields-*
πŸ—οΈ Builder-driven The form is declared in PHP with a fluent builder; the common cases need no code configuration 01-quickstart
πŸŽ›οΈ Interactive or unattended run() picks the mode: keyboard on a terminal, otherwise JSON payload + TUI_<ID> environment variables headless collection 08-headless-*
πŸ”— Derived values Fields computed from other answers via {{field}} templates and str2name transforms, settling to a fixpoint configuration 05-form-logic-*
πŸ”€ Conditional fields ->when() conditions (eq/ne/in/contains, composable with all/any/not) drive visibility; form-level fix-ups reconcile answers, and an opt-in indent steps each field in from the answer that reveals it configuration 05-form-logic-*
βš™οΈ Declared behavior ->required() rejects an empty value with a label-derived or declared message; dynamic defaults, validation and transforms as field closures, or as per-field handler classes resolved by naming convention field behavior 06-field-behaviour-*
πŸ” Discovery Update mode detects defaults from an existing directory: dotenv keys, JSON dot-paths, path checks, directory scans discovery 07-discovery
⏳ Progress A progress() primitive wraps slow work: a spinner when the length is unknown, a determinate bar when known - theme-drawn, animating on a TTY, degrading to a plain line when piped or headless progress 15-progress-*
🎯 Answer-driven options An ->options() callback that asks for the run context resolves a choice field's list from the answers collected so far, so one field narrows by another - re-resolved as they change, honored by the panel, headless collection, the schema and the validator alike, and a choice the narrowed list drops does not survive in the answers, while one supplied headlessly is reported rather than dropped options from the answers 19-dynamic-options
🌐 Remote-backed options ->optionsFrom() resolves a search or suggest field's candidates from the live query - a themed Loading… while it runs, a typing burst settling into one call, a per-query cache, and ->minQuery() holding it back until the query is worth sending options from a query 17-query-options
🧾 Output An output() primitive draws the chrome around a form: boxes and cards, tables, five status lines, definition lists, wrapped text, rules and a banner - theme-drawn, dropping their color when piped or redirected output 18-output-*
πŸ“¦ Self-describing answers Answers carry provenance; toSummary() renders a badged, panel-grouped report and toJson() the machine result; schema(), validate() and agentHelp() describe the form itself self-describing answers 08-headless-*
🎨 Themes Six built-ins selected by name; a custom theme is a DefaultTheme subclass repainting a handful of voices, and ->theme(fn(ThemeBuilder $t) => ...) patches individual elements with no class at all themes 09-themes-*
⌨️ Key bindings Presets (default, vim, or a class) plus per-binding overrides scoped to navigation or a field type; conflicts throw at setup key bindings 10-key-bindings-*
✨ Display modes Dark/light follows the terminal background, glyphs follow the locale, color honors NO_COLOR; all three can be forced display modes 11-display-modes-*
πŸ§ͺ Test harness TuiTester drives a whole form from scripted keystrokes and ScreenTester one screen frame by frame, no TTY; assert on answers, output and every frame drawn testing 13-testing
🌍 Translations Bundled chrome catalogs load automatically; a directory, a single catalog file or an inline map layers your own strings and chrome overrides on top, falling back to English translations 12-translations

Installation

composer require drevops/tui

Quick start

Declare a form with the Form builder, then drive it through the Tui facade - the one class that wires up collection, the input resolver, the schema tools and the interactive screen for you:

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

$form = Form::create('Quick start')
  ->panel('order', 'New order', function (PanelBuilder $p): void {
    // A required single-line text field.
    $p->text('name', 'Order name')->required();

    // A single choice, starting on "Banana".
    $p->select('fruit', 'Fruit')->default('banana')->options([
      'apple' => 'Apple',
      'banana' => 'Banana',
      'cherry' => 'Cherry',
    ]);

    // A multi-select, with one option pre-checked.
    $p->select('veg', 'Vegetables')->multiple()->default(['carrot'])->options([
      'carrot' => 'Carrot',
      'tomato' => 'Tomato',
      'spinach' => 'Spinach',
    ]);

    // An integer bounded to a sensible quantity.
    $p->number('quantity', 'Quantity')->min(1)->max(99)->default(6);

    // A yes/no gate.
    $p->confirm('organic', 'Organic only?')->default(FALSE);
  });

$tui = new Tui($form, handler_namespaces: ['App\\Handler']);

$answers = $tui->run();

The facade's surface:

Call Purpose
run($prompts, $version, $directory, $interactive) Collect answers; interactive on a TTY, headless otherwise (or forced via $interactive)
collect($prompts, $directory, $update, $version) Headless collection from JSON + environment; $update enables discovery
interact() The interactive panel TUI, explicitly
progress($total, $caption, $work) Show slow work running around the form: a spinner with no total, a determinate bar with one - a theme-drawn primitive
output() Draw the chrome around the form: boxes and cards, tables, status lines, definition lists, text, rules and a banner - theme-drawn primitives
schema() / validate($answers) / agentHelp() Describe the questions as structured metadata, validate an answer payload, emit the agent-facing answer schema
theme($theme, $options) Select the theme by name or class, or pass a closure to patch individual elements
layout($layout) / keys($preset, $overrides) Arrange the screen into named regions; select the key bindings
color($bool) / unicode($bool) / markdown($bool) / fullscreen($bool) / footer($bool) / clearOnExit($bool) / translator($t) Display and runtime switches
root() / registry() The declared block tree, and the handler registry - for finer control

Read the full guide at phptui.dev, and browse playground/ for complete, runnable examples - the numbered scripts for each feature in the table above.

Fields

There's a field for most things you'd want to ask: text entry, numbers and dates, single and multiple choice, fuzzy search, filesystem browsing, and simple gates. Each one links to its full reference on phptui.dev, and every card below plays back the real interaction in whichever color scheme - light or dark - your reader is using.

Calendar field Calendar
A month calendar returning a normalized ISO YYYY-MM-DD; arrows move by day and week.
Confirm field Confirm
Yes/No toggle; arrows or Space switch, y/n set the choice directly, Enter accepts.
File picker field File picker
Browse the filesystem for a path; arrows move, β†’ enters a directory and ← returns to its parent. Add ->multiple() for several paths.
Number field Number
Integer entry (digits with an optional leading minus) accepted as an int, with optional min, max and step.
Password field Password
Text rendered as a mask in the editor, the field row and the summary; the accepted value stays plain for your code, and can be made revealable.
Pause field Pause
An acknowledgment gate; Enter or Space accepts. Unattended runs auto-acknowledge it, so it never blocks automation.
Progress row Progress
A panel row that runs its work when activated, filling a bar or ticking a spinner in the row itself; it collects no value.
Rating field Rating
A graded answer picked from a scale of points, accepted as an int; arrows walk the scale, a digit jumps to its point, and each point can carry a caption.
Reorder field Reorder
Rank a list by moving items into the order you want; Space picks an item up, arrows carry it through the list, Enter accepts.
Search field Search
Single choice with a visible filter line; typing fuzzy-matches and ranks the labels, exact and prefix matches leading.
Select field Select
Single choice from a list; arrows move, Enter accepts the highlighted option, long lists page around the cursor.
Suggest field Suggest
Free text with autocomplete over a fixed option set: as you type, suggestions are fuzzy-matched and ranked by relevance.
Template field Template
Fill the named slots of a fixed shape; the fixed text is context, Tab steps between slots and each one validates on its own.
Text field Text
Single-line input with a movable caret; type to insert, arrows move, Backspace deletes, Enter accepts.
Textarea field Textarea
Multi-line input; Enter inserts a newline, arrows move between lines, Tab accepts, with an external-editor handoff.
Toggle field Toggle
An inline switch between two labeled values; arrows or Space flip, the first letter of each label sets it directly.

Themes

Six themes ship built-in, selected by name on the Tui facade. Dark or light is a separate mode display option auto-detected from the terminal background, so every adaptive theme serves both:

$tui = (new Tui($form))->theme('midnight');
Name Palette
default Cyan accents on an auto-detected dark or light base - the out-of-the-box look.
midnight Violet accents, green values, pink highlights.
frost Arctic frost-blue accents, sage values, sand highlights.
ember Burnt-orange accents, olive values, gold highlights.
mono Hue-free - bold weight, gray levels and reverse video for maximum compatibility.
dos Retro MS-DOS: the bright white/cyan/yellow CGA palette in a double-line window, made for a blue terminal background.

Each renders across every field and degrades to plain text without ANSI. Here the dark palette (left) and the light palette (right); the themes docs also show every theme inside the rounded border frame:

midnight

midnight theme, dark mode midnight theme, light mode

frost

frost theme, dark mode frost theme, light mode

ember

ember theme, dark mode ember theme, light mode

mono

mono theme, dark mode mono theme, light mode

dos - the CGA blue screen, painted regardless of the terminal background

dos theme, dark terminal dos theme, light terminal

Write your own by subclassing DefaultTheme and repainting just the voices a palette needs - see the theming guide and the playground's OceanTheme. To change a handful of glyphs and nothing else, skip the class: ->theme(fn(ThemeBuilder $t) => $t->field(fn(FieldOverrides $f) => $f->selector('β–Ά', '=>'))) patches the selected theme in place, and anything it does not name keeps that theme's own answer.

Contributing

See the Contributing guide for the development workflow, quality gates and how the documentation and SVG assets are built.


This repository was created using the Scaffold project template

About

DO NOT USE. API IS NOT STABLE! Terminal user interfaces for PHP

Topics

Resources

Contributing

Security policy

Stars

Watchers

Forks

Releases

Sponsor this project

Contributors

Languages