composer require sugarcraft/candy-shellPHP 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"All 13 gum subcommands ship. Run candyshell <cmd> --help for the full
flag list per command.
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().
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.
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. |
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.
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).
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.
candyshell completion --shell=bash
candyshell completion --shell=zsh
candyshell completion --shell=fishEmit a shell completion script for bash, zsh, or fish. Source the output directly or drop it into the appropriate completion directory.
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.
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.
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.
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 blueAvailable 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"cd candy-shell && composer install && vendor/bin/phpunit- SugarCraft monorepo
- Upstream: charmbracelet/gum












