Ready-made TUI components — inputs, tables, spinners, and more
TextInput, TextArea, ItemList, Table, Tree, Viewport, FilePicker, Progress, Spinner, Cursor, Help, Key, Paginator, Stopwatch, Timer. Everything you'd hand-roll on day one of a TUI project, ready to drop into your Model.
composer require sugarcraft/sugar-widgets
use SugarCraft\Widgets\TextInput\TextInput;
use SugarCraft\Widgets\Spinner\Spinner;
use SugarCraft\Core\{Cmd, Model, Msg, Program, Subscriptions};
use SugarCraft\Core\Msg\KeyMsg;
use SugarCraft\Core\KeyType;
final class Search implements Model {
public function __construct(
public readonly TextInput $ti,
public readonly Spinner $sp,
) {}
public function init(): ?\Closure { return $this->sp->tick(); }
public function subscriptions(): ?Subscriptions { return null; }
public function update(Msg $msg): array {
if ($msg instanceof KeyMsg && $msg->type === KeyType::Enter) {
return [$this, Cmd::quit()];
}
[$ti, $cmd] = $this->ti->update($msg);
[$sp, $cmd2] = $this->sp->update($msg);
return [new self($ti, $sp), Cmd::batch($cmd, $cmd2)];
}
public function view(): string {
return $this->sp->view().' '.$this->ti->view();
}
}
[$input] = TextInput::new()->focus(); // focus() returns [TextInput, ?Cmd]
(new Program(new Search($input, Spinner::new())))->run();
Progress + spring-physics AnimatedProgress driven by SugarSpring.ValidateOn timing control, keystroke restrict pattern filter, line numbers, soft-wrap, dynamic-height growth.styleFunc(row, col) => Style, multi-column sort with withSort() / thenSortBy() / clearSort(), filtering with withFilterable() / withFilter() / withFilterPredicate() (default: case-insensitive substring match on all visible columns), and pagination with withPageSize() / paginator() / withPage() / nextPage() / prevPage() / pageFirst() / pageLast() — integrates with sort and filter.g / G jump.Input\TextEdit is an immutable inline editor with an underline caret (no hardware cursor), grapheme-cluster editing, cell-budget windowing and a digits-only mode; Menu\OptionRow::render() draws btop's two-line options-menu row. Progress::withColorFunc closures also receive the bar width for position-coloured ramps.use SugarCraft\Widgets\Input\TextEdit;
use SugarCraft\Widgets\Menu\OptionRow;
$edit = TextEdit::new('firefox')->home()->right()->right();
echo $edit->view(); // "fi\e[4mr\e[24mefox" — underline caret on "r"
echo OptionRow::render('Update ms', TextEdit::new('2000', numeric: true),
selected: true, editing: true, hasArrows: true, editable: true);
| Class | Method | Description |
|---|---|---|
| TextInput | new() | Create a new text input instance |
| TextInput | update(Msg) | Handle input events |
| TextInput | withValidateOn(ValidateOn) | Set validation timing — None / Blur / Change / Submit |
| TextInput | withRestrict(string $pattern) | PCRE regex filter — only matching keystrokes are accepted |
| ValidateOn | enum | Cases: None / Blur / Change / Submit — controls when validation fires |
| TextArea | new() | Create a multi-line text area |
| Spinner | new(Style) | Create spinner with given style |
| Spinner | tick() | Return tick command for animation |
| Table | new(headers, rows) | Create data table |
| Table | withSort(string $column, SortDirection $dir = Asc) | Set primary sort column and direction — clears prior sort chain |
| Table | thenSortBy(string $column, SortDirection $dir = Asc) | Add secondary (or further) sort tiebreaker |
| Table | clearSort() | Remove all sort criteria, restoring insertion order |
| Table | sortState(): SortState | Return current sort criteria (readonly) |
| Table | withFilterable(bool) | Enable or disable the filter feature |
| Table | withFilter(string $query) | Set filter query string — default: case-insensitive substring match on all visible columns |
| Table | withFilterPredicate(?Closure(list<string>): bool) | Custom filter callable — null restores the default |
| Table | filterable(): bool | Return whether filtering is enabled |
| Table | filter(): string | Return the current filter query string |
| Table | filterPredicate(): ?Closure | Return the current custom filter predicate |
| SortDirection | enum | Cases: Asc / Desc — use toggle() to flip direction |
| SortState | DTO | Immutable list of (column index, SortDirection) pairs |
| Table | withPageSize(int $size) | Set rows per page — 0 disables pagination, ≥1 enables it |
| Table | withPage(int $page) | Navigate to a zero-based page (clamps to valid range) |
| Table | nextPage() | Advance one page — returns new Table |
| Table | prevPage() | Retreat one page — returns new Table |
| Table | pageFirst() | Jump to the first page — returns new Table |
| Table | pageLast() | Jump to the last page — returns new Table |
| Table | pageSize(): int | Return rows per page (0 = pagination disabled) |
| Table | currentPage(): int | Return the current zero-based page |
| Paginator | totalPages(): int | Return total page count (via the table's paginator) |
| Table | paginator(): Paginator | Return a Paginator wired to the table's current page state |
| Tree | new(root) | Create tree view |
| Viewport | new(content) | Create scrollable viewport |
| Progress | new(ratio) | Create progress bar |
| Progress | withColorFunc(?Closure) | Per-filled-cell colour fn(int $i, int $filledCells, float $percent, int $barWidth): Color — the 4th arg (true bar width) enables btop position-coloured ramps; 3-arg closures still work |
| TextEdit | new(text, ?caret, numeric) | Immutable inline editor (btop Draw::TextEdit); underline caret, grapheme-cluster caret |
| TextEdit | left() / right() / home() / end() / withCaret(int) | Caret movement (clamped) |
| TextEdit | insert(string) / backspace() / delete() / clear() | Edits; numeric mode rejects non-digit input |
| TextEdit | withNumeric(bool) | Digits-only mode — strips existing non-digits |
| TextEdit | view(int $limit = 0) | Render with SGR 4 underline caret, windowed to $limit display cells |
| OptionRow | render(name, value, selected, editing, hasArrows, …) | btop options-menu two-line row: bold centred name, ← value → / ↵, live TextEdit while editing |
| FilePicker | new(directory) | Create file picker |
| ItemList | new(items) | Create selectable item list |
VHS-recorded GIFs of every example shipped with the library. Regenerated automatically on every push that touches the source.