← All libraries

SugarCraft

🍬 SugarCraft

Elm-architecture TUI runtime

port of bubbletea runtime elm-architecture react-php fibers ssh

Model / Msg / Cmd / Program. The runtime everything else builds on. Includes the cursed cell-diff renderer ported from Bubble Tea v2 — meaningful SSH bandwidth savings for slow-changing UIs.

Install

composer require sugarcraft/candy-core

Quickstart

use SugarCraft\Core\{Cmd, KeyType, Model, Msg, Program};
use SugarCraft\Core\Msg\KeyMsg;

final class Counter implements Model {
    public function __construct(public readonly int $count = 0) {}
    public function init(): ?\Closure { return null; }

    public function update(Msg $msg): array {
        if ($msg instanceof KeyMsg) {
            return match (true) {
                $msg->rune === 'q'              => [$this, Cmd::quit()],
                $msg->type === KeyType::Up      => [new self($this->count + 1), null],
                $msg->type === KeyType::Down    => [new self($this->count - 1), null],
                default                         => [$this, null],
            };
        }
        return [$this, null];
    }

    public function view(): string {
        return "count: $this->count\n(↑/↓ to change, q to quit)";
    }
}

(new Program(new Counter()))->run();

What's in the box

Model interfaceThree pure methods: init(), update(Msg), view().
Cmd helpersCmd::quit(), Cmd::batch(), Cmd::send() — async work scheduled, never inlined.
InputReaderStateful escape-sequence parser. Bracketed paste, mouse, function keys, OSC, DEC private modes.
Cursed rendererCell-diff rendering ported from Bubble Tea v2. ≥15% bandwidth savings on a 50-tick test.
ReactPHP loopMirrors goroutine semantics for input, signals, render tick, command execution.
Signal handlingCtrl+C, SIGTERM, SIGWINCH (resize) all surface as Msgs in your update().
Screen / ScreenStackImmutable stack for modal / drill-down flows. RootModelWithScreenStack + ScreenStackCapable delegate init/update/view to the active screen automatically.
Component / CompositeParent-child lifecycle model. Component interface adds onMount()/onUnmount(); Composite owns children and drains lifecycle Cmds during reconcile.
WorkerPoolBounded-concurrency pool (default 4 workers) for CPU-bound tasks. WorkerCmd::run($pool, $task) dispatches a callable and yields a WorkerResultMsg on completion.

Source & demos

Try the quickstart →

Demos.

VHS-recorded GIFs of every example shipped with the library. Regenerated automatically on every push that touches the source.

Counter Model

Counter Model

↑ / ↓ to count, q to quit. Demonstrates the full Model / Msg / Cmd loop.
Timer

Timer

Ticking timer driven by a Cmd-scheduled tick message.