Elm-architecture TUI runtime
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.
composer require sugarcraft/candy-core
use SugarCraft\Core\{Cmd, KeyType, Model, Msg, Program, Subscriptions};
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 subscriptions(): ?Subscriptions { 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();
init(), update(Msg), view().Cmd::quit(), Cmd::batch(), Cmd::send() — async work scheduled, never inlined.MAX_PASTE_BYTES (1 MiB) surfaces in chunks, and an envelope whose end marker never arrives is closed after PASTE_IDLE_TIMEOUT (1 s) of silence so the keyboard comes back. Pasted content is sanitized by default.CLICOLOR_FORCE / FORCE_COLOR is set, a non-tty output is NoTty first (so NO_COLOR on a pipe emits no escapes at all), then TERM=dumb → NoTty; next NO_COLOR → Ascii (colour off, bold/faint kept), then force → TrueColor (so a forced pipe still gets colour), then COLORTERM, TERM_PROGRAM and TERM.RootModelWithScreenStack + ScreenStackCapable delegate init/update/view to the active screen automatically.Component interface adds onMount()/onUnmount(); Composite owns children and drains lifecycle Cmds during reconcile.WorkerCmd::run($pool, $task) dispatches a global function name or serializable callable and yields a WorkerResultMsg on completion. A Closure cannot cross the process boundary, so dispatching one returns an already-rejected promise.VHS-recorded GIFs of every example shipped with the library. Regenerated automatically on every push that touches the source.