A Terminal UI framework for PHP. Build beautiful, interactive terminal applications with a component-based architecture and hooks for state management.
- 🎨 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
- PHP 8.4+
- ext-tui (C extension)
composer require xocdr/tui<?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();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' : ''); // ConditionalStyled 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 argumentsuse 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);The Hooks class provides state management and side effects for components.
use Xocdr\Tui\Hooks\Hooks;
$hooks = new Hooks($instance);Manage component state:
[$count, $setCount] = $hooks->state(0);
// Direct value
$setCount(5);
// Functional update
$setCount(fn($prev) => $prev + 1);Run side effects:
$hooks->onRender(function () {
// Effect runs when deps change
$timer = startTimer();
// Return cleanup function
return fn() => $timer->stop();
}, [$dependency]);Memoize values and callbacks:
$expensive = $hooks->memo(fn() => computeExpensiveValue($data), [$data]);
$handler = $hooks->callback(fn($e) => handleEvent($e), [$dependency]);Create mutable references:
$ref = $hooks->ref(null);
$ref->current = 'new value'; // Doesn't trigger re-renderComplex 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']);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]);Access application controls:
['exit' => $exit] = $hooks->app();
$exit(0); // Exit with code 0Manage focus:
// Check focus state
['isFocused' => $isFocused, 'focus' => $focus] = $hooks->focus([
'autoFocus' => true,
]);
// Navigate focus
['focusNext' => $next, 'focusPrevious' => $prev] = $hooks->focusManager();Get terminal info:
['columns' => $cols, 'rows' => $rows, 'write' => $write] = $hooks->stdout();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);
// ...
}
}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);Configure with fluent API:
use Xocdr\Tui\Tui;
$app = Tui::builder()
->component($myApp)
->fullscreen(true)
->exitOnCtrlC(true)
->eventDispatcher($customDispatcher)
->hookContext($customHooks)
->renderer($customRenderer)
->start();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())
);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);use Xocdr\Tui\Styling\Style\Style;
$style = Style::create()
->bold()
->color('#ff0000')
->bgColor('#000000')
->toArray();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 shadeuse 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)use Xocdr\Tui\Styling\Style\Border;
Border::SINGLE; // ┌─┐│└─┘
Border::DOUBLE; // ╔═╗║╚═╝
Border::ROUND; // ╭─╮│╰─╯
Border::BOLD; // ┏━┓┃┗━┛
$chars = Border::getChars('round');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 16777216Spring 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']
}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 visibleThe 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
# Install dependencies
composer install
# Run tests
composer test
# Format code (PSR-12)
composer format
# Check formatting
composer format:check
# Static analysis
composer analyseMIT
- xocdr/ext-tui - Required C extension