Skip to content

Enable Async ​

Phui does not own an event loop — frame() is just a function you call, so a TUI can run inside whatever loop you already have.

Overview ​

Schedule frame() on a periodic timer in Revolt, ReactPHP or any loop, and Phui renders one frame per call, owning no loop itself. Its async runs on PHP fibers — the same mechanism those loops use — so a handler can await() work on your loop directly, no bridging code. Data arriving from outside Phui still needs wrapping so the next frame picks it up, and everything stays single-threaded.

Run frame() on your loop ​

Start the screen, tick frame() at your frame rate, and stop when the app quits. The shape is the same on any loop.

Drive from Revolt ​

php
use Phui\Exceptions\QuitException;
use Phui\Screen;
use Revolt\EventLoop;

$screen = Screen::mount(App::class)->start();

EventLoop::repeat(1 / 60, function (string $timerId) use ($screen) {
    try {
        $screen->frame();
    } catch (QuitException) {
        EventLoop::cancel($timerId);
    }
});

EventLoop::run();

$screen->stop();

Drive from ReactPHP ​

The same shape on ReactPHP's loop:

php
use Phui\Exceptions\QuitException;
use Phui\Screen;
use React\EventLoop\Loop;

$screen = Screen::mount(App::class)->start();

Loop::addPeriodicTimer(1 / 60, function ($timer) use ($screen) {
    try {
        $screen->frame();
    } catch (QuitException) {
        Loop::cancelTimer($timer);
        Loop::stop();
    }
});

Loop::run();

$screen->stop();

Handle async state ​

The waiting is your async library's job — an await(), ideally fiber-based like Amp's Future::await(). Phui runs inside those same fibers, so awaiting never blocks the render. It does leave a tracking gap, though: Phui only sees state changes made in code it drives, so anything that resolves outside a handler needs $this->async->track() to trigger a rebuild.

Await inside a handler ​

When a handler, timer or lifecycle method calls that await(), Phui doesn't block on it: the frame completes while the fiber is suspended, the UI keeps rendering, and the loop resumes your handler between frames — so awaiting your loop's work is the simplest way to load data on a keypress:

php
Binding::keypress(Keypress::r, function () {
    $this->status = 'loading…';
    $this->rows = $this->get(Api::class)->fetch()->await();
    $this->status = 'done';
});

State written before the await renders immediately; state written after the resume lands on the next frame. The same works in onMount to load initial data behind a spinner.

Don't await() in onRebuild: state written after the resume re-dirties the component, which rebuilds and runs onRebuild again — an endless loop. Fetch in onMount or a handler instead.

While a handler is suspended ​

A few things hold between the suspend and the resume:

  • A suspended handler counts as handled, so the event stops bubbling straight away — even though the handler hasn't finished.
  • If the component unmounts before the work resolves, the rest of the handler still runs, but its state writes are ignored.
  • A complete() or an exception after the resume surfaces on the next frame, exactly as if it had run synchronously.
  • A scheduler tick that calls frame() while another frame is still in flight is dropped, so a frame never re-enters mid-run.

Update from an external callback ​

When work resolves through a callback instead of an await() — a promise's then(), a socket's data event — Phui isn't driving that code, so it misses the state change. Wrap the callback in $this->async->track() and it won't:

php
public function onMount(): void
{
    $this->get(TaskStore::class)->onData($this->async->track(function () {
        $this->latestValue = $this->get(TaskStore::class)->read();
    }));
}

track() returns a closure to hand to the external system; when it fires, your callback runs inside Phui's change tracking and the next frame reflects it.

Stay single-threaded ​

Keep frame() and all component code in one thread and fiber context. Phui's state tracking assumes single-threaded access — interleaved async callbacks are fine, parallelism is not.

Released under the MIT License.