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+F6 cycling, programmatic focus, :focused state 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.