sugarcraft / candy-shell
PHP port of charmbracelet/gum — composer-installable CLI of SugarCraft TUI primitives. 13 subcommands: choose, confirm, file, filter, format, input, join, log, pager, spin, style, table, write.
Requires
- php: ^8.1
- sugarcraft/candy-core: @dev
- sugarcraft/candy-shine: @dev
- sugarcraft/candy-sprinkles: @dev
- sugarcraft/sugar-bits: @dev
- sugarcraft/sugar-prompt: @dev
- symfony/console: ^6.4 || ^7.0
Requires (Dev)
- phpunit/phpunit: ^10.5
This package is auto-updated.
Last update: 2026-05-19 05:48:08 UTC
README
CandyShell
composer require sugarcraft/candy-shell
PHP port of charmbracelet/gum — a composer-installable CLI of SugarCraft TUI primitives, useful for shell scripts.
# Apply styling. candyshell style --foreground "#ff5f87" --bold "Hello, candy!" # Pick one item. choice=$(candyshell choose Pizza Burger Salad) # Read a single line. name=$(candyshell input --placeholder "Your name?") # Confirm a destructive action. candyshell confirm "Really delete $file?" && rm "$file"
Subcommands
All 13 gum subcommands ship. Run candyshell <cmd> --help for the full
flag list per command.
Auto-discovery
CandyShell uses PHP attributes to auto-discover commands at runtime.
Mark any class extending Symfony\Component\Console\Command\Command with
#[Command] and it will be picked up by Application::scan().
Help attributes
Two additional attributes enrich the --help output of your commands:
#[Alias('name')]— registers an alternative name for the command (e.g.choose→cho). Multiple aliases are supported via repeated attributes.#[Example('usage', 'description')]— adds an example line to the command's help block. Thedescriptionparameter is optional. Multiple examples are supported via repeated attributes.
The HelpFormatter class renders these automatically when --help is
invoked. It reads #[Alias] and #[Example] attributes via
ReflectionClass::getAttributes() and formats them alongside the
standard Symfony description and help text.
Typo suggestion
When a user types an unknown command name, Application::find() runs it
through a TypoSuggester that computes Levenshtein distance against all
registered command names. If a match is found within distance ≤ 2, the
error message suggests the nearest alternative (e.g. "Did you mean
choose?"). Beyond distance 2 the original error propagates silently.
use SugarCraft\Shell\Attribute\Command; use SugarCraft\Shell\Attribute\Flag; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; #[Command(name: 'mycmd', description: 'Does something useful.', descriptionSection: 'Longer help text.')] final class MyCommand extends Command { #[Flag(name: 'format', short: 'f', description: 'Output format.', enum: FormatType::class)] protected function configure(): void { } protected function execute(InputInterface $input, OutputInterface $output): int { $format = $input->getOption('format'); // ... return self::SUCCESS; } } enum FormatType: string { case Json = 'json'; case Yaml = 'yaml'; }
Register the namespace in your bootstrap:
$app = new \SugarCraft\Shell\Application(); $app->scan('My\\Namespace\\'); // discovers all #[Command] classes $app->run();
Application::scan($namespace) iterates all already-loaded classes
under that namespace, picks those bearing #[Command], and registers
them into the application. Classes must be autoloaded (or required)
before scan() can find them — the scanner uses get_declared_classes()
and does not trigger autoloading.
| Command | Role |
|---|---|
choose |
Pick one or many items from a list. |
completion |
Emit shell completion script (bash · zsh · fish). |
confirm |
Yes/no prompt — exit 0 on confirm, 1 on cancel. |
file |
Interactive file picker. |
filter |
Fuzzy filter over stdin lines (single- or multi-select). |
format |
Render Markdown / code / template / emoji to the terminal. |
input |
Read a single line; supports --password masking. |
join |
Concatenate two styled fragments side-by-side or stacked. |
log |
Levelled / structured log output (text · json · logfmt). |
pager |
Scrollable viewer for long input. |
spin |
Run an external command behind a spinner. |
style |
Apply Sprinkles styling to argv (or stdin). |
table |
Render a CSV / TSV table. |
write |
Multi-line text editor. |
Flag reference (selected highlights)
The audit lists upstream-gum flags that are not yet wired in CandyShell. The shipped surface today covers the 80 % case for shell scripts; see AUDIT_2026_05_06.md for the full delta. Common flags across commands:
--limit N/--no-limit/--ordered/--selected="a,b"— multi-select onchooseandfilter.--header "Email:"/--prompt "> "/--value "$LAST"/--char-limit N/--width N/--max-lines N/--show-line-numbersoninput/write.--affirmative "Yes"/--negative "No"/--default=yes|no/--show-outputonconfirm.--show-output/--show-errorplus 12 spinner styles (dot·line·pulse·globe·points·monkey·moon·meter·mini-dot·hamburger·ellipsis·jump) onspin.--min-level info/--prefix/--time RFC3339/--file out.log/--formatter text|json|logfmt/--structuredonlog.--border/--border-foreground "#ff0"/--height N/--trimonstyle.
Environment
CandyShell respects standard CLI colour conventions:
NO_COLOR=1disables every SGR escape — output is plain ASCII.CLICOLOR=0disables colour when stdout is not a TTY (otherwise defaults to colour).CLICOLOR_FORCE=1keeps colour even when stdout is piped or redirected.FORCE_COLOR=1|2|3forces a specific tier (16 / 256 / TrueColor).
CANDYSHELL_* env var fallbacks
Any command option can be given via an environment variable using the
CANDYSHELL_ prefix followed by the option name in uppercase with
non-alphanumeric characters replaced by _. For example:
# Equivalent to: candyshell style --foreground=#ff0000 --bold "Hello" CANDYSHELL_FOREGROUND=#ff0000 CANDYSHELL_BOLD=1 candyshell style "Hello"
The fallback is applied when no explicit CLI option is provided. An explicit flag on the command line always takes precedence over the env var.
Shell completion
candyshell completion --shell=bash candyshell completion --shell=zsh candyshell completion --shell=fish
Emit a shell completion script for bash, zsh, or fish. Source the output directly or drop it into the appropriate completion directory.
Version
candyshell --version reports the version read from the monorepo root
composer.json via Application::versionFromComposer(). The version is
discovered by walking up from the package directory to find the nearest
composer.json with a non-empty version field.
Exit codes
0— normal completion. Forconfirm, this means the user picked the affirmative answer.1—confirmdeclined; or non-zero exit forwarded from the external command run byspin.130— interrupted (Ctrl-C / SIGINT). Matches POSIX shell convention.
Porting from gum
Most gum X invocations work as candyshell X verbatim. Known
behavioural differences (also see
AUDIT_2026_05_06.md):
formataccepts-t/--type(markdown,code,template,emoji) alongside--theme. Template support is the lightweight{{VAR}}expansion — Go template-function helpers are not implemented.--styleflags using the gum dotted form (--header.foreground,--cursor.foreground, …) are not yet wired across every command.--timeout,--show-help,--strip-ansi, and the--cursor-modeflags now accept their gum-equivalent values on every command. Where a flag is meaningless to a non-interactive command (format,join,log,style,table) it is still accepted for parity but treated as a no-op.confirm --default=yes|nois the form to use; the older--default-yesalias is preserved.
Theming and customization
Almost every interactive subcommand accepts a --style flag in
<element>.<property>=<value> form. Repeat the flag to layer
properties or target multiple elements:
candyshell choose --style "cursor.foreground=212" \ --style "selected.bold=true" \ --style "header.foreground=99" \ --header "Pick a colour" red green blue
Available properties on every element: foreground, background,
bold, italic, underline, strikethrough, faint, blink,
reverse. Element names are documented inline in each subcommand's
--help output (choose exposes cursor, header, selected,
unselected; confirm exposes prompt, selected, unselected;
input/write expose prompt, placeholder, cursor, header,
lineNumber).
Colour values accept the same surface as
CandySprinkles: hex (#ff8800),
ANSI 0–15 (9 for bright red), 8-bit (212), and named CSS colours
(coral, slategray).
Themes for format ride on
CandyShine: pass --theme dracula,
--theme tokyo-night, --theme dark, --theme light, --theme pink,
--theme ascii, or --theme notty to swap renderer presets without
authoring a Style yourself. --type code --language=go reuses the
markdown pipeline for syntax-only rendering, while --type emoji
expands the built-in :smile: shortcode set (unknown shortcodes pass
through verbatim).
The same Style rules apply to the style subcommand, which is the
canonical way to compose lipgloss-style boxes from a script:
candyshell style --foreground=212 --bold --border rounded \
--padding "1 4" --margin "0 2" "Welcome aboard"
Test
cd candy-shell && composer install && vendor/bin/phpunit
Demos
choose
Confirm
file
filter
format
Input
join
log
pager
spin
Style
Table
write
Related
- SugarCraft monorepo
- Upstream: charmbracelet/gum












