sugarcraft / sugar-prompt
PHP port of charmbracelet/huh — interactive form library (Note, Input, Confirm, Select, MultiSelect, Text, FilePicker) with multi-page Group support, 6 stock themes, and form-level KeyMap override per binding.
Requires
- php: ^8.1
- sugarcraft/candy-core: @dev
- sugarcraft/candy-sprinkles: @dev
- sugarcraft/sugar-bits: @dev
Requires (Dev)
- phpunit/phpunit: ^10.5
This package is auto-updated.
Last update: 2026-05-18 22:49:23 UTC
README
SugarPrompt
composer require sugarcraft/sugar-prompt
PHP port of charmbracelet/huh — interactive form library built on top of SugarCraft + SugarBits.
use SugarCraft\Prompt\Form; use SugarCraft\Prompt\Field\{Input, Confirm, Select, Note}; $form = Form::new( Note::new('welcome')->title('Onboarding')->desc('A few quick questions.'), Input::new('name')->title('Your name?')->placeholder('Ada Lovelace'), Confirm::new('newsletter')->title('Subscribe to the newsletter?'), Select::new('lang')->title('Favorite language?')->options('PHP', 'Go', 'Rust', 'Python'), ); // $form is a SugarCraft Model — drop it into a Program.
Every field exposes short-form aliases (
title,desc,placeholder,width,height,validator,options,min,max, …). The upstream-mirroring long forms (withTitle,withDescription, …) work identically — pick whichever reads better at the call site.
Field types
| Field | Description | Notable knobs |
|---|---|---|
Input |
Single-line text (wraps SugarBits\TextInput) |
withPlaceholder, withCharLimit, withWidth, withPrompt, withValidator(\Closure), withTitleFunc / withDescriptionFunc, withPassword(bool, string $echo = '*'), withSuggestions(list<string>), withSuggestionsFunc(\Closure(string):list<string>) |
Text |
Multi-line text editor | withCharLimit, withMaxLines, withShowLineNumbers, withValidator |
Confirm |
Yes/no boolean | withAffirmative/withNegative, withValidator(\Closure(bool):?string), withTitleFunc, withDescriptionFunc |
Select |
Single-choice list (wraps SugarBits\ItemList) |
withOptions(...), withTitleFunc, withDescriptionFunc |
MultiSelect |
Multi-choice list | withOptions(...), withLimit(int) |
Note |
Read-only paragraph; skipped by tab navigation | withTitle, withDescription, withHeight(int), withNext(bool), withNextLabel(string) (turns it into an interactive button page) |
FilePicker |
Filesystem picker (wraps SugarBits\FileTree) |
withCwd, withAllowDirs, withAllowFiles, withShowSize, withShowHidden |
All fields share a common navigation contract: Tab / ↓ advances,
Shift+Tab / ↑ retreats, Enter on the last interactive field
submits, Esc / Ctrl+C aborts. Skippable fields (e.g. plain
Notes) are passed over silently.
Forms and groups
Form::new(...$fields) is a single-page form. For multi-page flows
build with Form::groups(Group::new(...$fields), …). Each group
carries its own title / description / hide-predicate / theme override:
Form::groups( Group::new( Input::new('name')->withTitle('Your name?'), Confirm::new('proceed')->withTitle('Continue?'), )->withTitle('Step 1'), Group::new( Note::new('done')->withTitle('Thanks!')->withNext()->withNextLabel('Finish'), ) ->withTheme(Theme::dracula()) ->withShowHelp(false) ->withHideFunc(fn (array $v) => $v['proceed'] !== true), );
Form-level chainables
| Method | What it does |
|---|---|
withTheme(Theme) |
Switch the colour palette. |
withAccessible(bool) |
Render plain label: value text — for screen readers / dumb terminals. |
withShowHelp(bool) |
Toggle the help footer. |
withShowErrors(bool) |
Toggle the inline ! error line on validation failures. |
withWidth(int), withHeight(int) |
Pin the rendered geometry. |
withTimeout(int $ms) |
Auto-abort after $ms of wall clock. |
keyMap(KeyMap) / withKeyMap(KeyMap) |
Override the bindings for Next / Prev / Submit / Quit (and per-field nav) on a single form. Mirrors upstream huh #272. |
Reading values after submit
values() returns every visible field keyed by key(). For typed
access call getString, getInt, getBool, getArray. For
inspecting validation state during a run use errors(),
hasErrors(), getFocusedField(), keyBinds(), help().
Themes & accessibility
Stock themes ship as static factories on SugarCraft\Prompt\Theme:
ansi() (default), plain(), charm(), dracula(), catppuccin(),
base16(). Pass one to Form::withTheme(...). The accessibility
mode flips the entire form to plain-text rendering — useful when you
detect NO_COLOR=1 or TERM=dumb.
Validators and dynamic labels
Every value-producing field supports withValidator(\Closure). The
closure runs on every keystroke (or every value flip for Confirm)
and returns null for valid or an error string. The error renders
inline beneath the field and shows up in Form::errors().
Use withTitleFunc(\Closure(): string) / withDescriptionFunc(...)
on any field to compute labels lazily — handy when the label depends
on values from a previous group.
Validation
Field-level validation with withValidation
Input and Text fields support withValidation() for predicate-based
validation — a cleaner alternative to withValidator:
$input = Input::new('email') ->withTitle('Email address') ->withPlaceholder('you@example.com') ->withValidation(fn($v) => filter_var($v, FILTER_VALIDATE_EMAIL), 'Must be a valid email'); $text = Text::new('bio') ->withTitle('Biography') ->withValidation(fn($v) => mb_strlen($v) >= 10, 'Must be at least 10 characters');
The predicate receives the field value and must return true for valid
or false for invalid. The error message renders inline beneath the field
and is collected into Form::errors().
Error summary with withErrorSummary
Enable withErrorSummary() on a Form to display all validation errors
at the end when submission fails:
$form = Form::new( Input::new('name') ->withTitle('Name') ->withValidation(fn($v) => !empty(trim($v)), 'Name is required'), Input::new('email') ->withTitle('Email') ->withValidation(fn($v) => filter_var($v, FILTER_VALIDATE_EMAIL), 'Must be a valid email'), )->withErrorSummary(true);
When enabled and the form is submitted with errors, an error summary renders above the form listing every failed field and its error message.
Test
cd sugar-prompt && composer install && vendor/bin/phpunit






