Introducing the Symfony Tui Component
I'm thrilled to announce a brand new Symfony component: Tui, a PHP library for building rich, interactive terminal user interfaces.
For 15 years, the Console component has been one of the most used Symfony components, powering many CLI tools in the ecosystem. It does a lot: commands, arguments, output formatting, questions, tables, progress bars... But over time, I realized that two very different responsibilities had grown intertwined: structuring CLI applications, and building interactive terminal experiences.
What if we split them?
Console stays focused on commands, arguments, and output. And Tui takes over everything related to rich terminal interaction: widgets, layouts, styling, input handling, mouse support, and real-time rendering.
Today, I'm opening the pull request to add Tui to Symfony.
A Full Widget Toolkit for the Terminal
Tui ships with a complete set of widgets:
- TextWidget for labels, headings, and FIGlet ASCII art banners
- InputWidget for single-line text fields with cursor, scrolling, and paste support
- EditorWidget, a full multi-line text editor with word wrap, undo/redo, a kill ring, and autocomplete
- SelectListWidget for scrollable, filterable pick lists
- SettingsListWidget for preference panels with value cycling and submenus
- TabsWidget for multi-view interfaces with horizontal or vertical headers (follow-up PR)
- MarkdownWidget with full CommonMark support and syntax-highlighted code blocks
- ImageWidget and AnimatedImageWidget for inline images (via the Kitty graphics protocol) and animated GIF playback as ASCII art (follow-up PR)
- OverlayWidget for modal dialogs, dropdowns, and floating panels (follow-up PR)
- LoaderWidget, CancellableLoaderWidget, and ProgressBarWidget for background operations
Every widget supports padding, borders (with 9 built-in patterns like
rounded, double, and block styles), backgrounds, text decoration, and
alignment. Widgets compose into trees using ContainerWidget with vertical or
horizontal layout, gaps, and vertical expansion.
CSS-like Styling
Tui's styling system draws heavily from CSS. Styles are immutable value objects
with properties for colors (ANSI, 256-palette, true color RGB with mix(),
tint(), and shade()), text formatting, padding, borders, layout direction,
gaps, and alignment.
There are three ways to style a widget:
Stylesheet rules with CSS-like selectors (universal, FQCN, class, state, sub-elements, responsive breakpoints):
1 2 3 4 5 6 7
$stylesheet->addRule('.sidebar:focused', new Style(
border: Border::all(1, 'rounded', 'cyan'),
));
$stylesheet->addRule(MarkdownWidget::class.'::code-block-border', new Style(
color: 'gray',
));
Tailwind-like utility classes for quick, composable styling:
1 2 3 4
$widget->addStyleClass('p-2');
$widget->addStyleClass('bg-emerald-500');
$widget->addStyleClass('bold');
$widget->addStyleClass('border-rounded');
The full Tailwind shade palette is supported: text-blue-700,
bg-rose-100, border-cyan-400, and so on.
Inline styles for one-off overrides with the highest cascade priority.
The cascade merges these layers exactly like CSS: stylesheet rules, then utility
classes, then inline styles. Responsive breakpoints let you switch layouts based
on terminal width, just like @media queries.
Declarative Templates with Twig (follow-up PR)
Tui includes a template system that lets you describe widget trees using pseudo-HTML and Twig. Structure lives in templates, styling in stylesheets, and behavior in PHP:
1 2 3 4 5 6 7 8 9 10
{% extends "layout.html.twig" %}
{% block content %}
<text>Pick a language:</text>
<select-list id="list" max-visible="5">
<item value="php" label="PHP" description="Hypertext Preprocessor"></item>
<item value="python" label="Python" description="Simple and powerful"></item>
<item value="rust" label="Rust" description="Memory safety without GC"></item>
</select-list>
{% endblock %}
Templates support Twig inheritance, blocks, loops, conditionals, and custom widget tags via namespace prefixes. You get the same separation of concerns you're used to in web development: Twig for structure, stylesheets for presentation, PHP for behavior.
Powered by PHP Fibers
Under the hood, Tui runs on PHP Fibers and the Revolt event loop. This means the entire application is single-threaded but fully concurrent, with no extensions required: pure PHP 8.4+.
What does this enable? Animations keep running while you type. The loader spinner keeps spinning during HTTP requests. Input is never blocked by rendering. Multiple async operations run in parallel. The Amp ecosystem provides non-blocking replacements for HTTP, processes, and timers that integrate seamlessly since they share the same event loop.
Smart Rendering
The rendering pipeline is engineered for performance:
- Dirty tracking: widgets self-invalidate; only dirty subtrees are re-rendered
- Render cache: unchanged widgets return cached output, skipping style resolution, layout, chrome, and content rendering entirely
- Screen diffing: only changed cells are written to the terminal, minimizing I/O
- Compositing: multiple layers (transparent or opaque) can be merged, useful for animated backgrounds with text on top
The result: smooth rendering, efficient enough for real-time games.
Mouse, Focus, Keybindings, Events
Tui supports the full range of interaction patterns you'd expect from a modern UI toolkit:
- Mouse support (follow-up PR): click to focus, click to place cursors, scroll wheels, drag, click-outside dismissal for overlays. Hold Shift to bypass mouse tracking for native text selection.
- Focus management:
F6/Shift+F6cycling, programmatic focus,:focusedstate for conditional styling. - Customizable keybindings: a three-layer merge system (widget defaults, app-level, per-widget overrides) so you can remap any action without subclassing.
- Event system: built on Symfony EventDispatcher with per-widget listeners,
global listeners, and typed events (
SubmitEvent,SelectEvent,ChangeEvent,CancelEvent,TabChangeEvent,FocusEvent, and more).
FIGlet Fonts
Tui bundles five FIGlet fonts (big, small, slant, standard, mini) and
makes it trivial to register your own via FontRegistry. Apply a font with a
Tailwind utility class (font-big) or a stylesheet rule, and TextWidget
renders your text as large ASCII art. For the keynote where I first presented
Tui, I created custom FIGlet fonts (chisel, VGA, minecraft, rounded, slanted)
to make the slides look great, all rendered live in the terminal.
Real-World: Already in Production
Tui isn't a toy. It already powers my very own AI coding agent built entirely on the component: an editor widget for code input, markdown rendering for AI responses, streaming output via fibers, overlays for model selection, tabs for multiple conversations, mouse support, customizable keybindings, and themes.
The Tui integrates naturally with Symfony Console: create a Tui inside a
command's execute() method, run it, and return the exit code. After run()
returns, the terminal is restored and you can use the Console output object
normally.
What's Next
I've just opened the pull request on github.com/symfony/symfony. I can't
wait to see what the community builds with it.
The terminal has always been my favorite playground. With Tui, I hope it becomes yours too.
Super excited to see this new component coming to live! Can't wait to try it when it gets merged 🎉
Cool!
I'm so glad this is happening. I'll try to build a TUI with what the PR currenlty proposes to see how far we can go with it (and provide feedback where necessary).
An excellent addition to the Symfony Family :)
We don't need no stinkin' GUI - this looks so cool :)