xocdr/tui

Terminal UI framework for PHP

Maintainers

Details

github.com/xocdr/tui

Source

Issues

Installs: 18

Dependents: 1

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/xocdr/tui

0.2.9 2025-12-30 22:35 UTC

This package is auto-updated.

Last update: 2025-12-30 22:35:36 UTC


README

xocdr/tui

xocdr/tui

A Terminal UI framework for PHP. Build beautiful, interactive terminal applications with a component-based architecture and hooks for state management.

Features

  • ๐ŸŽจ Component-based - Build UIs with composable components (Box, Text, etc.)
  • โšก Hooks - state, onRender, memo, onInput, and more
  • ๐Ÿ“ฆ Flexbox layout - Powered by Yoga layout engine via ext-tui
  • ๐ŸŽฏ Focus management - Tab navigation and focus tracking
  • ๐Ÿ”Œ Event system - Priority-based event dispatching with propagation control
  • ๐Ÿงช Testable - Interface-based design with mock implementations

Requirements

  • PHP 8.4+
  • ext-tui (C extension)

Installation

composer require xocdr/tui

Quick Start

<?php

use Xocdr\Tui\Tui;
use Xocdr\Tui\Components\Box;
use Xocdr\Tui\Components\Text;
use Xocdr\Tui\Hooks\Hooks;

$app = function () {
    $hooks = new Hooks(Tui::getApplication());

    [$count, $setCount] = $hooks->state(0);
    ['exit' => $exit] = $hooks->app();

    $hooks->onInput(function ($key) use ($setCount, $exit) {
        if ($key === 'q') {
            $exit();
        }
        if ($key === ' ') {
            $setCount(fn($c) => $c + 1);
        }
    });

    return Box::create()
        ->flexDirection('column')
        ->padding(1)
        ->border('round')
        ->children([
            Text::create("Count: {$count}")->bold(),
            Text::create('Press SPACE to increment, Q to quit')->dim(),
        ]);
};

Tui::render($app)->waitUntilExit();

Components

Box

Flexbox container for layout:

use Xocdr\Tui\Components\Box;

Box::create()
    ->flexDirection('column')  // 'row' | 'column'
    ->alignItems('center')     // 'flex-start' | 'center' | 'flex-end'
    ->justifyContent('center') // 'flex-start' | 'center' | 'flex-end' | 'space-between'
    ->padding(1)
    ->paddingX(2)
    ->margin(1)
    ->gap(1)
    ->width(50)
    ->height(10)
    ->aspectRatio(16/9)        // Width/height ratio
    ->direction('ltr')         // 'ltr' | 'rtl' layout direction
    ->border('single')         // 'single' | 'double' | 'round' | 'bold'
    ->borderColor('blue')
    ->children([...]);

// Shortcuts
Box::column([...]); // flexDirection('column')
Box::row([...]);    // flexDirection('row')

// Tailwind-like utility classes
Box::create()
    ->styles('border border-round border-blue-500')   // Border style + color
    ->styles('bg-slate-900 p-2')                      // Background + padding
    ->styles('flex-col items-center gap-1')           // Layout utilities
    ->styles(fn() => $hasBorder ? 'border' : '');     // Conditional

Text

Styled text content:

use Xocdr\Tui\Components\Text;

Text::create('Hello World')
    ->bold()
    ->italic()
    ->underline()
    ->strikethrough()
    ->dim()
    ->inverse()
    ->color('#ff0000')        // Hex color
    ->bgColor('#0000ff')      // Background color
    ->color('blue', 500)      // Tailwind palette + shade
    ->bgColor('slate', 100)   // Background palette + shade
    ->wrap('word');           // 'word' | 'none'

// Color shortcuts
Text::create('Error')->red();
Text::create('Success')->green();
Text::create('Info')->blue()->bold();

// Unified color API (accepts Color enum, hex, or palette name with shade)
use Xocdr\Tui\Ext\Color;
Text::create('Palette')->color('red', 500);           // Palette name + shade
Text::create('Palette')->color(Color::Red, 500);      // Color enum + shade
Text::create('Palette')->color(Color::Coral);         // CSS color via enum

// Tailwind-like utility classes
Text::create('Hello')
    ->styles('bold text-green-500')                   // Multiple utilities
    ->styles('text-red bg-slate-900 underline');      // Colors + styles

// Bare colors as text color shorthand
Text::create('Error')->styles('red');                 // Same as text-red
Text::create('Success')->styles('green-500');         // Same as text-green-500

// Dynamic styles with callables
Text::create('Status')
    ->styles(fn() => $active ? 'green' : 'red')       // Conditional styling
    ->styles('bold', ['italic', 'underline']);        // Mixed arguments

Other Components

use Xocdr\Tui\Components\Fragment;
use Xocdr\Tui\Components\Spacer;
use Xocdr\Tui\Components\Newline;
use Xocdr\Tui\Components\Static_;

// Fragment - group without extra node
Fragment::create([
    Text::create('Line 1'),
    Text::create('Line 2'),
]);

// Spacer - fills available space (flexGrow: 1)
Box::row([
    Text::create('Left'),
    Spacer::create(),
    Text::create('Right'),
]);

// Newline - line breaks
Newline::create(2); // Two line breaks

// Static - non-rerendering content (logs, history)
Static_::create($logItems);

Hooks

The Hooks class provides state management and side effects for components.

use Xocdr\Tui\Hooks\Hooks;

$hooks = new Hooks($instance);

state

Manage component state:

[$count, $setCount] = $hooks->state(0);

// Direct value
$setCount(5);

// Functional update
$setCount(fn($prev) => $prev + 1);

onRender

Run side effects:

$hooks->onRender(function () {
    // Effect runs when deps change
    $timer = startTimer();

    // Return cleanup function
    return fn() => $timer->stop();
}, [$dependency]);

memo / callback

Memoize values and callbacks:

$expensive = $hooks->memo(fn() => computeExpensiveValue($data), [$data]);
$handler = $hooks->callback(fn($e) => handleEvent($e), [$dependency]);

ref

Create mutable references:

$ref = $hooks->ref(null);
$ref->current = 'new value'; // Doesn't trigger re-render

reducer

Complex state with reducer pattern:

$reducer = fn($state, $action) => match($action['type']) {
    'increment' => $state + 1,
    'decrement' => $state - 1,
    default => $state,
};

[$count, $dispatch] = $hooks->reducer($reducer, 0);
$dispatch(['type' => 'increment']);

onInput

Handle keyboard input:

$hooks->onInput(function ($key, $nativeKey) {
    if ($key === 'q') {
        // Handle quit
    }
    if ($nativeKey->upArrow) {
        // Handle arrow key
    }
    if ($nativeKey->ctrl && $key === 'c') {
        // Handle Ctrl+C
    }
}, ['isActive' => true]);

app

Access application controls:

['exit' => $exit] = $hooks->app();
$exit(0); // Exit with code 0

focus / focusManager

Manage focus:

// Check focus state
['isFocused' => $isFocused, 'focus' => $focus] = $hooks->focus([
    'autoFocus' => true,
]);

// Navigate focus
['focusNext' => $next, 'focusPrevious' => $prev] = $hooks->focusManager();

stdout

Get terminal info:

['columns' => $cols, 'rows' => $rows, 'write' => $write] = $hooks->stdout();

HooksAware Trait

For components, use the HooksAwareTrait for convenient access:

use Xocdr\Tui\Contracts\HooksAwareInterface;
use Xocdr\Tui\Hooks\HooksAwareTrait;

class MyComponent implements HooksAwareInterface
{
    use HooksAwareTrait;

    public function render(): mixed
    {
        [$count, $setCount] = $this->hooks()->state(0);
        // ...
    }
}

Events

Listen to events on the application:

$app = Tui::render($myApp);

// Input events
$app->onInput(function ($key, $nativeKey) {
    echo "Key pressed: $key";
}, priority: 10);

// Resize events
$app->onResize(function ($event) {
    echo "New size: {$event->width}x{$event->height}";
});

// Focus events
$app->onFocus(function ($event) {
    echo "Focus changed to: {$event->currentId}";
});

// Remove handler
$handlerId = $app->onInput($handler);
$app->off($handlerId);

Advanced Usage

Application Builder

Configure with fluent API:

use Xocdr\Tui\Tui;

$app = Tui::builder()
    ->component($myApp)
    ->fullscreen(true)
    ->exitOnCtrlC(true)
    ->eventDispatcher($customDispatcher)
    ->hookContext($customHooks)
    ->renderer($customRenderer)
    ->start();

Dependency Injection

For testing or custom configurations:

use Xocdr\Tui\Application;
use Xocdr\Tui\Terminal\Events\EventDispatcher;
use Xocdr\Tui\Hooks\HookContext;
use Xocdr\Tui\Rendering\Render\ComponentRenderer;
use Xocdr\Tui\Rendering\Render\ExtensionRenderTarget;

$app = new Application(
    $component,
    ['fullscreen' => true],
    new EventDispatcher(),
    new HookContext(),
    new ComponentRenderer(new ExtensionRenderTarget())
);

Testing Without C Extension

Use mock implementations:

use Xocdr\Tui\Tests\Mocks\MockRenderTarget;
use Xocdr\Tui\Rendering\Render\ComponentRenderer;

$target = new MockRenderTarget();
$renderer = new ComponentRenderer($target);

$node = $renderer->render($component);

// Inspect created nodes
$this->assertCount(2, $target->createdNodes);

Style Utilities

Style Builder

use Xocdr\Tui\Styling\Style\Style;

$style = Style::create()
    ->bold()
    ->color('#ff0000')
    ->bgColor('#000000')
    ->toArray();

Color Utilities

use Xocdr\Tui\Styling\Style\Color;

// Conversions
$rgb = Color::hexToRgb('#ff0000'); // ['r' => 255, 'g' => 0, 'b' => 0]
$hex = Color::rgbToHex(255, 0, 0); // '#ff0000'
$lerped = Color::lerp('#000000', '#ffffff', 0.5); // '#808080'

// CSS Named Colors (141 colors via ext-tui Color enum)
$hex = Color::css('coral');       // '#ff7f50'
$hex = Color::css('dodgerblue');  // '#1e90ff'
Color::isCssColor('salmon');      // true
$names = Color::cssNames();       // All 141 color names

// Tailwind Palette
$blue500 = Color::palette('blue', 500);  // '#3b82f6'

// Universal resolver
$hex = Color::resolve('coral');           // CSS name
$hex = Color::resolve('#ff0000');         // Hex passthrough
$hex = Color::resolve('red-500');         // Tailwind palette

// Custom color aliases
Color::defineColor('dusty-orange', 'orange', 700);  // From palette + shade
Color::defineColor('brand-primary', '#3498db');      // From hex
Color::defineColor('accent', 'coral');               // From CSS name

// Use custom colors anywhere
Text::create('Hello')->styles('dusty-orange');
Box::create()->styles('bg-brand-primary border-accent');
$hex = Color::custom('dusty-orange');                // Get hex value
Color::isCustomColor('brand-primary');               // true

// Custom palettes (auto-generates shades 50-950)
Color::define('brand', '#3498db');                   // Base color becomes 500
Text::create('Hello')->color('brand', 300);          // Use lighter shade

Gradients

use Xocdr\Tui\Styling\Animation\Gradient;
use Xocdr\Tui\Ext\Color;

// Create gradient between colors (hex, palette, or Color enum)
$gradient = Gradient::between('#ff0000', '#0000ff', 10);
$gradient = Gradient::between(['red', 500], ['blue', 500], 10);
$gradient = Gradient::between(Color::Red, Color::Blue, 10);

// Fluent builder API
$gradient = Gradient::from('red', 500)
    ->to('blue', 300)
    ->steps(10)
    ->hsl()        // Use HSL interpolation (default: RGB)
    ->circular()   // Make gradient loop back
    ->build();

// Get colors from gradient
$colors = $gradient->getColors();  // Array of hex strings
$color = $gradient->at(0.5);       // Color at position (0-1)

Border Styles

use Xocdr\Tui\Styling\Style\Border;

Border::SINGLE;  // โ”Œโ”€โ”โ”‚โ””โ”€โ”˜
Border::DOUBLE;  // โ•”โ•โ•—โ•‘โ•šโ•โ•
Border::ROUND;   // โ•ญโ”€โ•ฎโ”‚โ•ฐโ”€โ•ฏ
Border::BOLD;    // โ”โ”โ”“โ”ƒโ”—โ”โ”›

$chars = Border::getChars('round');

Terminal Control

Access terminal features via TerminalManager:

$app = Tui::render($myComponent);
$terminal = $app->getTerminalManager();

// Window title
$terminal->setTitle('My TUI App');
$terminal->resetTitle();

// Cursor control
$terminal->hideCursor();
$terminal->showCursor();
$terminal->setCursorShape('bar');  // 'block', 'underline', 'bar', etc.

// Terminal capabilities
$terminal->supportsTrueColor();    // 24-bit color support
$terminal->supportsHyperlinks();   // OSC 8 support
$terminal->supportsMouse();        // Mouse input
$terminal->getTerminalType();      // 'kitty', 'iterm2', 'wezterm', etc.
$terminal->getColorDepth();        // 8, 256, or 16777216

Scrolling

SmoothScroller

Spring physics-based smooth scrolling:

use Xocdr\Tui\Scroll\SmoothScroller;

// Create with default spring physics
$scroller = SmoothScroller::create();

// Or with custom settings
$scroller = new SmoothScroller(stiffness: 170.0, damping: 26.0);

// Preset configurations
$scroller = SmoothScroller::fast();   // Quick animations
$scroller = SmoothScroller::slow();   // Smooth, slow animations
$scroller = SmoothScroller::bouncy(); // Bouncy effect

// Set target position
$scroller->setTarget(0.0, 100.0);

// Or scroll by delta
$scroller->scrollBy(0, 10);

// In render loop
while ($scroller->isAnimating()) {
    $scroller->update(1.0 / 60.0);  // 60 FPS
    $pos = $scroller->getPosition();
    // Render at $pos['y']
}

VirtualList

Efficient rendering for large lists (windowing/virtualization):

use Xocdr\Tui\Scroll\VirtualList;

// Create for 100,000 items with 1-row height, 20-row viewport
$vlist = VirtualList::create(
    itemCount: 100000,
    viewportHeight: 20,
    itemHeight: 1,
    overscan: 5
);

// Get visible range (only render these!)
$range = $vlist->getVisibleRange();
for ($i = $range['start']; $i < $range['end']; $i++) {
    $offset = $vlist->getItemOffset($i);
    // Render item at Y = $offset
}

// Navigation
$vlist->scrollItems(1);     // Arrow down
$vlist->scrollItems(-1);    // Arrow up
$vlist->pageDown();         // Page down
$vlist->pageUp();           // Page up
$vlist->scrollToTop();      // Home
$vlist->scrollToBottom();   // End
$vlist->ensureVisible($i);  // Scroll to make item visible

Architecture

The package follows SOLID principles with a clean separation of concerns:

src/
โ”œโ”€โ”€ Application/          # Manager classes for Application
โ”‚   โ”œโ”€โ”€ TimerManager.php  # Timer and interval management
โ”‚   โ”œโ”€โ”€ OutputManager.php # Terminal output operations
โ”‚   โ””โ”€โ”€ TerminalManager.php # Cursor, title, capabilities
โ”œโ”€โ”€ Scroll/               # Scrolling utilities
โ”‚   โ”œโ”€โ”€ SmoothScroller.php # Spring physics scrolling
โ”‚   โ””โ”€โ”€ VirtualList.php   # Virtual list for large datasets
โ”œโ”€โ”€ Components/           # UI components
โ”‚   โ”œโ”€โ”€ Component.php     # Base interface
โ”‚   โ”œโ”€โ”€ Box.php           # Flexbox container
โ”‚   โ”œโ”€โ”€ Text.php          # Styled text
โ”‚   โ””โ”€โ”€ ...
โ”œโ”€โ”€ Contracts/            # Interfaces for loose coupling
โ”‚   โ”œโ”€โ”€ NodeInterface.php
โ”‚   โ”œโ”€โ”€ RenderTargetInterface.php
โ”‚   โ”œโ”€โ”€ RendererInterface.php
โ”‚   โ”œโ”€โ”€ EventDispatcherInterface.php
โ”‚   โ”œโ”€โ”€ HookContextInterface.php
โ”‚   โ”œโ”€โ”€ InstanceInterface.php
โ”‚   โ”œโ”€โ”€ TimerManagerInterface.php
โ”‚   โ”œโ”€โ”€ OutputManagerInterface.php
โ”‚   โ”œโ”€โ”€ InputManagerInterface.php
โ”‚   โ””โ”€โ”€ TerminalManagerInterface.php
โ”œโ”€โ”€ Hooks/                # State management hooks
โ”‚   โ”œโ”€โ”€ HookContext.php
โ”‚   โ”œโ”€โ”€ HookRegistry.php
โ”‚   โ”œโ”€โ”€ Hooks.php         # Primary hooks API
โ”‚   โ””โ”€โ”€ HooksAwareTrait.php
โ”œโ”€โ”€ Rendering/            # Rendering subsystem
โ”‚   โ”œโ”€โ”€ Lifecycle/        # Application lifecycle
โ”‚   โ”œโ”€โ”€ Render/           # Component rendering
โ”‚   โ””โ”€โ”€ Focus/            # Focus management
โ”œโ”€โ”€ Styling/              # Styling subsystem
โ”‚   โ”œโ”€โ”€ Style/            # Colors, styles, borders
โ”‚   โ”œโ”€โ”€ Animation/        # Easing, gradients, tweens
โ”‚   โ”œโ”€โ”€ Drawing/          # Canvas, buffer, sprites
โ”‚   โ””โ”€โ”€ Text/             # Text utilities
โ”œโ”€โ”€ Support/              # Support utilities
โ”‚   โ”œโ”€โ”€ Exceptions/       # Exception classes
โ”‚   โ”œโ”€โ”€ Testing/          # Mock implementations
โ”‚   โ”œโ”€โ”€ Debug/            # Runtime inspection
โ”‚   โ””โ”€โ”€ Telemetry/        # Performance metrics
โ”œโ”€โ”€ Terminal/             # Terminal subsystem
โ”‚   โ”œโ”€โ”€ Input/            # Keyboard input (InputManager, Key, Modifier)
โ”‚   โ”œโ”€โ”€ Events/           # Event system
โ”‚   โ””โ”€โ”€ Capabilities.php  # Terminal feature detection
โ”œโ”€โ”€ Widgets/              # Pre-built widgets
โ”œโ”€โ”€ Container.php         # DI container
โ”œโ”€โ”€ Application.php       # Application wrapper with manager getters
โ”œโ”€โ”€ InstanceBuilder.php   # Fluent builder
โ””โ”€โ”€ Tui.php               # Main entry point

Development

# Install dependencies
composer install

# Run tests
composer test

# Format code (PSR-12)
composer format

# Check formatting
composer format:check

# Static analysis
composer analyse

License

MIT

Related