Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

sugarcraft/sugar-bits

Repository files navigation

sugar-bits

SugarBits

CI codecov Packagist Version License PHP

demo

PHP port of charmbracelet/bubbles β€” 15 pre-built TUI components for SugarCraft, including the interactive Tree (mirrors upstream Bubbles #233), dynamic-height TextArea (mirrors #910), and per-cell Table::styleFunc(...) (mirrors #246).

composer require sugarcraft/sugar-bits

TextInput, TextArea, and Help expose short-form aliases for the most-used setters: placeholder / charLimit / width / height / prompt / validator / styles / separator / ellipsis. The upstream-mirroring with* long forms still work side-by-side.

Components

Upstream Bubbles ships 13 components; SugarBits ships those 13 plus AnimatedProgress (the spring-physics variant lives in its own class to keep the static Progress lean).

Component What it does Notable msgs
Cursor\Cursor Animated text cursor BlinkMsg
Help\Help Render short / full key-help footer from a KeyMap; Help::updateWithBinding($msg, $toggle) flips show-all in response to a key β€”
Key\Binding One key + label + help row; Binding::new(...), Binding::withDisabled(...) factories β€”
Spinner\Spinner Animated loading glyph β€” 12 built-in styles (deprecated alias β€” re-exported from SugarCraft\Forms\Spinner in sugarcraft/candy-forms) Spinner\TickMsg
Progress\Progress Static progress bar (gradient fill optional, withColors(...) / withColorFunc(...) / withShowValue(...)) β€”
Progress\AnimatedProgress Spring-physics-animated progress bar (HoneyBounce-driven) SpringTickMsg
Timer\Timer Countdown timer; interval(), timeout(), withInterval(float) Timer\TickMsg, TimeoutMsg
Stopwatch\Stopwatch Elapsed-time counter; interval(), withInterval(float) Stopwatch\TickMsg
TextInput\TextInput Single-line input with autocomplete + validators + ValidateOn timing + restrict pattern + vim mode + placeholder styling + prefix/suffix + Styles β€”
TextArea\TextArea Multi-line editor with line numbers / set-prompt-func / focused() / cursor() / line() / column(); Ctrl+O opens the buffer in $EDITOR (withEditorExtension('.md') to control the syntax-highlight suffix) TextArea\TextAreaEditedMsg
Viewport\Viewport Scrollable text region with mouse-wheel, scrollbar, horizontal scroll, setWidth(int) / setHeight(int) β€”
Paginator\Paginator Dot / arabic page indicator β€”
ItemList\ItemList Selectable / scrollable / filterable list with status messages β€”
Tree\Tree Interactive tree β€” cursor, expand/collapse, viewport scroll. Mirrors upstream Bubbles #233. β€”
Table\Table Selectable data table with Column struct + nav + multi-column sort β€”
Tabs\Tabs Tabbed panel β€” keyboard (Tab/Shift+Tab/1-9) + mouse navigation, wrap/clamp modes, scrollable overflow β€”
FilePicker\FilePicker Directory browser with icons / size / sort modes β€”

Vim mode

TextInput vim mode (Insert/Normal/Visual/VisualLine keybindings) is powered by candy-forms's shared VimKeyHandler β€” the same handler backing sugar-prompt and sugar-readline. Adding a new binding to the VimAction enum benefits all three libs at once. The per-lib opt-in flag withVimMode(true) is preserved; consumers control whether vim mode is enabled.

Msg routing cheat-sheet

Forward these into your model's update() so the embedded component can react: BlinkMsg (Cursor / TextInput), Spinner\TickMsg (Spinner), Timer\TickMsg + Timer\TimeoutMsg, Stopwatch\TickMsg, SpringTickMsg (AnimatedProgress), StartStopMsg (Timer / Stopwatch), TextArea\TextAreaEditedMsg (TextArea's Ctrl+O round-trip). Each component's update() filters by its own id() so multiple instances of the same component coexist on one event loop.

Quickstart β€” TextInput with autocomplete

use SugarCraft\Bits\TextInput\TextInput;
use SugarCraft\Core\{Cmd, Model, Msg, Program};
use SugarCraft\Core\Msg\KeyMsg;
use SugarCraft\Core\KeyType;
final class Search implements Model
{
 public function __construct(public readonly TextInput $ti) {}
 public function init(): ?\Closure { return null; }
 public function update(Msg $msg): array
 {
 if ($msg instanceof KeyMsg && $msg->type === KeyType::Enter) {
 return [$this, Cmd::quit()];
 }
 if ($msg instanceof KeyMsg && $msg->type === KeyType::Tab) {
 return [new self($this->ti->acceptSuggestion()), null];
 }
 [$ti, $cmd] = $this->ti->update($msg);
 return [new self($ti), $cmd];
 }
 public function view(): string
 {
 $body = $this->ti->view();
 if (($s = $this->ti->currentSuggestion()) !== null) {
 $body .= "\n β†’ $s";
 }
 return $body;
 }
}
[$ti, $cmd] = TextInput::new()
 ->withSuggestions(['apple', 'apricot', 'banana', 'cherry'])
 ->showSuggestions()
 ->withValidator(fn(string $v) => strlen($v) >= 2 ? null : 'too short')
 ->focus();
(new Program(new Search($ti)))->run();

Quickstart β€” animated progress bar

use SugarCraft\Bits\Progress\AnimatedProgress;
$bar = AnimatedProgress::new()
 ->withWidth(40)
 ->withDefaultGradient();
[$bar, $cmd] = $bar->setPercent(0.75);
// dispatch $cmd via the Program β€” ticks re-fire from inside update()
// until the bar settles within 5e-4 of the target.

Quickstart β€” TextInput with placeholder styling and prefix/suffix

use SugarCraft\Bits\TextInput\TextInput;
use SugarCraft\Sprinkles\Style;
use SugarCraft\Core\Util\Color;
$ti = TextInput::new()
 ->withPlaceholder('Enter command...')
 ->withPlaceholderStyle(Style::new()->faint()) // default: dim
 ->withPrefix('$ ') // fixed prefix
 ->withSuffix(' <'); // fixed suffix
echo $ti->view();
// $ Enter command... <

TextInput β€” ValidateOn and restrict

TextInput supports deferred and filtered validation via two new builders:

ValidateOn timing control

use SugarCraft\Bits\TextInput\{TextInput, ValidateOn};
$ti = TextInput::new()
 ->withValidateOn(ValidateOn::Blur); // validate when focus leaves
Case When validation fires
ValidateOn::None Never (default β€” use when you drive validation manually)
ValidateOn::Blur When the input loses focus
ValidateOn::Change On every keystroke
ValidateOn::Submit Only on Enter keypress

Keystroke filter (restrict)

use SugarCraft\Bits\TextInput\TextInput;
// Accept only digits
$numeric = TextInput::new()->withRestrict('[0-9]');
// Accept alphanumeric only
$alphanum = TextInput::new()->withRestrict('[a-zA-Z0-9]');

TextInput notable builders

Method What it does
withValidateOn(ValidateOn $timing) Set validation timing (None / Blur / Change / Submit)
withRestrict(string $pattern) Set a PCRE regex β€” only matching characters are accepted (no delimiters)

Table β€” multi-column sort

use SugarCraft\Bits\Table\{Table, SortDirection, SortState};
// Primary sort by Name ascending
$t = $table->withSort('Name');
// Tiebreaker: Age descending
$t = $table->thenSortBy('Age', SortDirection::Desc);
// Reset to insertion order
$t = $table->clearSort();
// Inspect current sort criteria
$state = $t->getSortState(); // SortState
foreach ($state->criteria as [$colIndex, $dir]) {
 // $colIndex is an int, $dir is SortDirection::Asc or SortDirection::Desc
}

SortDirection enum

Case Value Description
SortDirection::Asc 'asc' Sort in ascending order
SortDirection::Desc 'desc' Sort in descending order

SortDirection::toggle() returns the opposite direction.

SortState DTO

Immutable list of sort criteria β€” each entry is a (column index, direction) pair. Applied in order: first entry is primary sort, second is tiebreaker, etc.

Method Returns Description
SortState::empty() SortState Factory for no criteria
SortState->withCriterion(int $col, SortDirection $dir) SortState Append a criterion
SortState->isEmpty() bool True when no criteria are set
SortState->criteria list<array{0:int,1:SortDirection}> Raw criteria list

Table sort builders

Method Description
withSort(string $column, SortDirection $dir = Asc) Set primary sort β€” clears any prior sort chain
thenSortBy(string $column, SortDirection $dir = Asc) Add a secondary (or further) tiebreaker criterion
clearSort() Remove all sort criteria, restoring insertion order
getSortState(): SortState Return the current sort criteria (readonly accessor)

Sorting throws \InvalidArgumentException with message table.sort_unknown_column when the column name is not found. The exception message is localizable.

Table β€” filtering

use SugarCraft\Bits\Table\Table;
// Enable the filter feature (opt-in)
$t = $table->withFilterable(true);
// Set a query string β€” default: case-insensitive substring match across all visible columns
$t = $table->withFilter('foo');
// Custom filter: receives a row (list<string>), returns true to keep
$t = $table->withFilterPredicate(fn(array $row): bool =>
 str_contains(strtolower(implode("\t", $row)), 'foo')
);
// Inspect current filter state
$isFilterable = $t->getFilterable(); // bool
$query = $t->getFilter(); // string
$predicate = $t->getFilterPredicate(); // ?Closure(list<string>): bool

When withFilterPredicate() is set, it overrides the default substring-match behaviour. Pass null to restore the default.

Table filter builders

Method Description
withFilterable(bool $filterable) Enable or disable the filter feature
withFilter(string $query) Set the filter query string (non-empty enables filtering)
withFilterPredicate(?Closure(list<string>): bool $predicate) Custom filter callable β€” null restores the default
getFilterable(): bool Return whether filtering is enabled
getFilter(): string Return the current filter query string
getFilterPredicate(): ?Closure Return the current custom predicate

The default filter applies case-insensitive substring matching across all visible columns.

Table β€” pagination

use SugarCraft\Bits\Table\Table;
// Enable pagination: 10 rows per page
$t = $table->withPageSize(10);
// Navigate pages
$t = $t->withPage(1); // zero-based β€” go to page 1 (second page)
$t = $t->nextPage();
$t = $t->prevPage();
$t = $t->pageFirst();
$t = $t->pageLast();
// Inspect pagination state
$pageSize = $t->getPageSize(); // int β€” rows per page (0 = pagination disabled)
$current = $t->getCurrentPage(); // int β€” zero-based current page
$totalPages = $t->getTotalPages(); // int β€” 1 when pagination is disabled
// Wire a Paginator to the table for UI rendering
$paginator = $t->getPaginator(); // Paginator instance

Table pagination builders

Method Description
withPageSize(int $size) Set rows per page β€” 0 disables pagination; β‰₯1 enables it
withPage(int $page) Navigate to a zero-based page (clamps to valid range)
nextPage() Advance one page
prevPage() Retreat one page
pageFirst() Jump to the first page
pageLast() Jump to the last page
getPageSize(): int Return rows per page (0 = pagination off)
getCurrentPage(): int Return the current zero-based page
getTotalPages(): int Return the total page count (1 when pagination is disabled)
getPaginator(): Paginator Return a Paginator instance wired to the table's current page state

Pagination works with sort and filter: changing the sort order, filter query, or page size automatically re-clamps the cursor to the first row of the current page so the cursor never points to a row outside the current page boundary.

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

cd sugar-bits && composer install && vendor/bin/phpunit

Demos

Cursor

cursor

File picker

file-picker

Help

help

Item list

item-list

Paginator

paginator

Progress

progress

Spinners

spinners

Stopwatch

stopwatch

Tabs

tabs

Table

table

Text area

text-area

Text input

text-input

Text input (enhanced)

text-input

Timer

timer

Tree

tree

Viewport

viewport

Related

About

🧩 PHP port of πŸ“¦ bubbles β€” 14 pre-built TUI widgets: Viewport, TextArea, TextInput, List, Table, Spinner, Progress, FilePicker, Pager & more.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

Contributors

Languages

AltStyle γ«γ‚ˆγ£γ¦ε€‰ζ›γ•γ‚ŒγŸγƒšγƒΌγ‚Έ (->γ‚ͺγƒͺγ‚ΈγƒŠγƒ«) /