PHP port of charmbracelet/bubbletea β the Elm-architecture TUI runtime at the heart of the Charmbracelet stack.
use CandyCore\Core\{Cmd, KeyType, Model, Msg, Program};
use CandyCore\Core\Msg\{KeyMsg, WindowSizeMsg};
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->type === KeyType::Char && $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();- PHP 8.1+
mbstring,intl(for grapheme width)pcntl(signal handling β POSIX only)react/event-loop^1.6 (Composer)
Modelβ your app implementsinit(),update(Msg),view().Msgβ marker interface for events. Built-ins:KeyMsg,WindowSizeMsg,QuitMsg.CmdβClosure(): ?Msg. Async work whose result is dispatched as a Msg. Helpers inCmd::quit(),Cmd::batch(),Cmd::send().Programβ orchestrator. Sets up TTY, runs the ReactPHP event loop, dispatches Msgs, drives renders at the configured framerate.InputReaderβ stateful byte-stream parser; handles split escape sequences across reads.Rendererβ minimal cursor-home + erase + write. Diff-based renderer is a follow-up.Util/βAnsi,Color,ColorProfile,Width,Ttyfoundation utilities, shared with CandySprinkles.
- Phase 0 (foundation utilities): π’ complete.
- Phase 3 (runtime): π‘ ~40% β primitives + Program loop landed. Mouse, focus/blur, bracketed paste, function keys, and the diff renderer are upcoming.
See ../CONVERSION.md for the full roadmap.
cd candy-core && composer install && vendor/bin/phpunit
