PHPackages                             sugarcraft/sugar-prompt - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. sugarcraft/sugar-prompt

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

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.

1573PHP

Since Jun 1Pushed 1w agoCompare

[ Source](https://github.com/sugarcraft/sugar-prompt)[ Packagist](https://packagist.org/packages/sugarcraft/sugar-prompt)[ RSS](/packages/sugarcraft-sugar-prompt/feed)WikiDiscussions master Synced 1w ago

READMEChangelogDependenciesVersions (1)Used By (0)

[![sugar-prompt](.assets/icon.png)](.assets/icon.png)

SugarPrompt
===========

[](#sugarprompt)

[![CI](https://github.com/detain/sugarcraft/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/detain/sugarcraft/actions/workflows/ci.yml)[![codecov](https://camo.githubusercontent.com/f837cff2fef6bced807f8e18cf36fd0f4b55e5d1dc8d232460c5800d52f988a2/68747470733a2f2f636f6465636f762e696f2f67682f64657461696e2f737567617263726166742f6272616e63682f6d61737465722f67726170682f62616467652e7376673f666c61673d73756761722d70726f6d7074)](https://app.codecov.io/gh/detain/sugarcraft?flags%5B0%5D=sugar-prompt)[![Packagist Version](https://camo.githubusercontent.com/f0e5cd7d2bbaa19e7c718f90109f778d7207e8e7c29598ef0cf81748a7201c8b/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f737567617263726166742f73756761722d70726f6d70743f6c6162656c3d7061636b6167697374)](https://packagist.org/packages/sugarcraft/sugar-prompt)[![License](https://camo.githubusercontent.com/7013272bd27ece47364536a221edb554cd69683b68a46fc0ee96881174c4214c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c75652e737667)](LICENSE)[![PHP](https://camo.githubusercontent.com/e78ffc83837c0d12647811a7fd1910c3cbeae04988de94bb4fd5b67e0874696a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d254532253839254135382e312d3838393262662e737667)](https://www.php.net/)

[![demo](.vhs/form.gif)](.vhs/form.gif)

```
composer require sugarcraft/sugar-prompt
```

PHP port of [charmbracelet/huh](https://github.com/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.

Shared foundations
------------------

[](#shared-foundations)

sugar-prompt is built on top of five shared foundation packages:

PackageRole`sugarcraft/candy-async`Async/await engine — drives `withAsyncSuggestions` cancellable fetchers via `Async\Await` + `CancelledException``sugarcraft/candy-buffer`Ring-buffer output renderer — handles SGR sequence batching and viewport sync for the form viewport`sugarcraft/candy-fuzzy`Smith-Waterman local-alignment fuzzy matcher — powers `withFuzzySuggestions()` on `Input` and `Select` fields`sugarcraft/candy-testing`Test harness — provides `ProgramSimulator`, `ScriptedInput`, and golden-file tape helpers for TEA-program tests**Vim keybindings****Via candy-forms `VimKeyHandler`** — `TextInput` vim mode (Insert/Normal/Visual) is shared across all 4 libs; new bindings added to `VimAction` enum benefit sugar-prompt automaticallyField types
-----------

[](#field-types)

FieldDescriptionNotable knobs`Input`Single-line text (wraps `SugarBits\TextInput`)`withPlaceholder`, `withCharLimit`, `withWidth`, `withPrompt`, `withValidator(\Closure)`, `withTitleFunc` / `withDescriptionFunc`, `withPassword(bool, string $echo = '*')`, `withSuggestions(list)`, `withSuggestionsFunc(\Closure(string):list)`, `withFuzzySuggestions(list)`, `withAsyncSuggestions(callable $fetcher, int $debounceMs = 150)``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`, `withFuzzySuggestions(list)`, `withAsyncSuggestions(callable $fetcher, int $debounceMs = 150)`, `withEnum(\BackedEnum::class)``MultiSelect`Multi-choice list (j/k vim keys + space to toggle)`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 `Note`s) are passed over silently.

Forms and groups
----------------

[](#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

[](#form-level-chainables)

MethodWhat 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.`validateAll(): array`Run all field validators and return `[fieldKey => errorMessage]` for fields that failed. Use after `Form::run()` to collect cross-field validation failures that cannot be expressed per-field.### Reading values after submit

[](#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 &amp; accessibility

[](#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

[](#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.

### Built-in validators

[](#built-in-validators)

`SugarCraft\Prompt\Validator` provides five ready-made validators that cover the most common input constraints. All five implement the `Validator` interface (which returns `true` on valid input, an error `string` on invalid). Pass one or more to `Input::withValidator()` — multiple calls chain validators together, each running in sequence with the first error message winning.

```
use SugarCraft\Prompt\Form;
use SugarCraft\Prompt\Field\Input;
use SugarCraft\Prompt\Validator\{Required, Email, MinLength, MaxLength, Pattern};

$form = Form::new(
    Input::new('name')
        ->withTitle('Full name')
        ->withPlaceholder('Ada Lovelace')
        ->withValidator(new Required())
        ->withValidator(new MinLength(2)),
    Input::new('email')
        ->withTitle('Email address')
        ->withPlaceholder('you@example.com')
        ->withValidator(new Required())
        ->withValidator(new Email()),
    Input::new('username')
        ->withTitle('Username')
        ->withPlaceholder('ada_lovelace')
        ->withValidator(new Required())
        ->withValidator(new MinLength(3))
        ->withValidator(new MaxLength(20))
        ->withValidator(new Pattern('/^[a-z0-9_]+$/i', 'Only letters, numbers, and underscores')),
);
```

ClassError messageNotes`Required``Value is required`Fails on empty string only`Email``Must be a valid email address`Skipped when empty; uses `filter_var``MinLength(int $min)``Must be at least N characters`Uses `mb_strlen` (UTF-8 safe)`MaxLength(int $max)``Must be no more than N characters`Uses `mb_strlen` (UTF-8 safe)`Pattern(string $pattern, string $message)``$message`Skipped when empty; uses `preg_match`To create a custom validator, implement `Validator` yourself:

```
use SugarCraft\Prompt\Validator\Validator;

final class NoSpaces implements Validator
{
    public function validate(string $input): true|string
    {
        if (str_contains($input, ' ')) {
            return 'Spaces are not allowed';
        }
        return true;
    }
}
```

### Fuzzy suggestions

[](#fuzzy-suggestions)

`Input` and `Select` fields support `withFuzzySuggestions()` for fuzzy substring matching via Smith-Waterman local alignment scoring. Candidates are ranked by score and filtered to only matches with a positive score.

```
use SugarCraft\Prompt\Form;
use SugarCraft\Prompt\Field\{Input, Select};

$form = Form::new(
    Input::new('language')
        ->withTitle('Pick a language')
        ->withFuzzySuggestions(['PHP', 'Python', 'Go', 'Rust', 'JavaScript', 'TypeScript']),
    Select::new('framework')
        ->withTitle('Pick a framework')
        ->withOptions(['Laravel', 'Symfony', 'Rails', 'Django', 'FastAPI', 'Fiber'])
        ->withFuzzySuggestions(['Laravel', 'Symfony', 'Rails', 'Django', 'FastAPI', 'Fiber']),
);
```

The `fuzzy()` short alias is equivalent:

```
->fuzzy(['PHP', 'Python', 'Go', 'Rust'])
```

For fine-grained control, use `FuzzyMatcher` directly:

```
use SugarCraft\Prompt\Fuzzy\FuzzyMatcher;

$matcher = new FuzzyMatcher();

// Score a single candidate (higher = better match)
$score = $matcher->score('js', 'JavaScript'); // 9

// Rank all candidates — returns list sorted by score desc
$matches = $matcher->match('py', ['Python', 'PHP', 'Ruby', 'JavaScript']);
// [['Python', 8], ['JavaScript', 1]]
```

Scoring constants: match=`+3`, mismatch=`-3`, gap open=`-5`, gap extend=`-1`, adjacent bonus=`+5` for consecutive matches.

### Async suggestions

[](#async-suggestions)

`Input` and `Select` support `withAsyncSuggestions()` for suggestions fetched asynchronously with a debounce delay. The `$fetcher` callable receives the current query string and must return `list`.

The default debounce is 150 ms — tuned to avoid firing on every keystroke while still feeling responsive. A `SuggestionsReadyMsg` is dispatched via the event loop when fresh suggestions are available.

```
use SugarCraft\Prompt\Form;
use SugarCraft\Prompt\Field\Input;
use React\Async\defer;

$form = Form::new(
    Input::new('language')
        ->withTitle('Pick a language')
        ->withAsyncSuggestions(
            defer(fn($query) => fetchFromApi($query)),
            150,
        ),
);
```

The `async()` short alias is equivalent:

```
->async(fn($query) => fetchFromApi($query))
```

Validation
----------

[](#validation)

### Field-level validation with `withValidation`

[](#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`

[](#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.

Snapshot tests
--------------

[](#snapshot-tests)

Render output is covered by golden-file snapshot tests. Fixture files live in `tests/fixtures/` with a `.golden` extension and are compared against actual ANSI byte output via `SugarCraft\Testing\Snapshot\Assertions::assertGoldenAnsi()`. To re-record fixtures after intentional output changes:

```
UPDATE_GOLDENS=1 vendor/bin/phpunit
```

Test
----

[](#test)

```
cd sugar-prompt && composer install && vendor/bin/phpunit
```

Demos
-----

[](#demos)

### Confirm

[](#confirm)

[![confirm](.vhs/confirm.gif)](.vhs/confirm.gif)

### Multi-page form

[](#multi-page-form)

[![form](.vhs/form.gif)](.vhs/form.gif)

### Input

[](#input)

[![input](.vhs/input.gif)](.vhs/input.gif)

### Multi-select

[](#multi-select)

[![multi-select](.vhs/multi-select.gif)](.vhs/multi-select.gif)

### Select

[](#select)

[![select](.vhs/select.gif)](.vhs/select.gif)

### Text (multi-line)

[](#text-multi-line)

[![text](.vhs/text.gif)](.vhs/text.gif)

### Themes

[](#themes)

[![themes](.vhs/themes.gif)](.vhs/themes.gif)

###  Health Score

27

↑

LowBetter than 47% of packages

Maintenance64

Regular maintenance activity

Popularity21

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity11

Early-stage or recently created project

 Bus Factor1

Top contributor holds 99.1% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://www.gravatar.com/avatar/b1036e0717211b8030b83cbe729e8ba6ba442fdbd5285fb97a39d7dcfe339342?d=identicon)[detain](/maintainers/detain)

---

Top Contributors

[![detain](https://avatars.githubusercontent.com/u/1364504?v=4)](https://github.com/detain "detain (116 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (1 commits)")

---

Tags

candycorecheckboxcliconfirmdropdownfile-pickerform-libraryhuhhuh-portinputinteractive-promptsmulti-selectpromptquestionnaireselectterminaltuiuser-inputvalidationwizard

### Embed Badge

![Health badge](/badges/sugarcraft-sugar-prompt/health.svg)

```
[![Health](https://phpackages.com/badges/sugarcraft-sugar-prompt/health.svg)](https://phpackages.com/packages/sugarcraft-sugar-prompt)
```

###  Alternatives

[sitegeist/archaeopteryx

The missing link editor for Neos

2585.0k3](/packages/sitegeist-archaeopteryx)[heptacom/heptaconnect-portal-base

HEPTAconnect base dataset that every other portal is based on

1025.2k15](/packages/heptacom-heptaconnect-portal-base)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
