Overview of Text-Based User Interfaces (TUIs) Building applications for the terminal often feels like returning to the roots of computing, but modern tools have transformed the command line into a canvas for rich, interactive experiences. A Text-Based User Interface, or **TUI**, uses alphanumeric characters and ANSI%20escape%20codes to construct visual elements like tables, progress bars, and navigation menus. Unlike a standard Command Line Interface (CLI) that simply outputs text and terminates, a TUI creates an immersive environment where the user can interact with the state of the application in real-time. Developing these interfaces matters because it bridges the gap between simple scripts and complex web applications. It allows developers to create high-utility internal tools, dashboards, or even experimental games—like Pong—that live entirely within the developer's native habitat: the terminal. By mastering TUIs, you gain total control over the terminal buffer, enabling you to manipulate pixels (characters) to suit your specific workflow needs. Prerequisites and Toolkit To follow this tutorial, you should possess a solid grasp of PHP and the Laravel ecosystem. Familiarity with Object-Oriented Programming (OOP) is essential, as we will be extending base classes to manage application state and rendering logic. Key Libraries & Tools * Laravel%20Prompts: A first-party Laravel package developed by Jess%20Archer that provides beautiful, pre-styled UI components for CLI apps. * **Chewy**: A helper package created by Joe%20Tannenbaum that acts as "glue" between Laravel Prompts and custom TUIs, offering utilities for keypress listening and screen management. * **Terminal App**: Any modern terminal emulator (iTerm2, Windows Terminal, etc.) that supports ANSI colors and alternate screen buffers. The Fundamental Rule: Nothing for Free When you move from the browser to the terminal, you leave behind the luxuries of the DOM. In a web browser, an `<input>` tag handles focus, cursor positioning, and text deletion automatically. In the terminal, you get **nothing for free**. If you want a cursor to move when a user types, you must track the index of that cursor and render an inverted-color character at that specific position. If the user hits backspace, you must manually manipulate the string in your application state and re-render the entire view. This manual nature requires a shift in mindset. You are no longer just sending data to a renderer; you are managing a loop of state, input, and output. Every interaction—from a simple keypress to a complex window resize—is a problem you must explicitly solve. Building a Speaker Directory App Let's walk through building a functional Speaker Directory. This app features a list of names on the left and a detailed bio on the right. 1. Defining Application State Your state class must extend the base `Prompt` class. This is where you store your data, such as a collection of speakers and the index of the currently selected item. ```php class SpeakerDirectory extends Prompt { public Collection $speakers; public int $selected = 0; public function __construct() { $this->speakers = collect(json_decode(file_get_contents('speakers.json'))); } public function value(): mixed { return null; // We are building an app, not returning a single value } } ``` 2. Implementing Keypress Listeners To make the list navigable, we use the `KeyPressListener`. This captures input and updates the state. Note how we use `min` and `max` to prevent the selection from going out of bounds. ```php // Inside your state class (new KeyPressListener($this)) ->on(Key::Down, fn() => $this->selected = min($this->selected + 1, $this->speakers->count() - 1)) ->on(Key::Up, fn() => $this->selected = max($this->selected - 1, 0)) ->listen(); ``` 3. The Renderer Logic The renderer is an invocable class that transforms your state into a string. To show columns side-by-side, we use a "zip" technique. Since the terminal prints line-by-line, you must concatenate the first line of the left column with the first line of the right column, and so on. ```php class SpeakerRenderer extends Renderer { public function __invoke(SpeakerDirectory $state): string { $leftColumn = $state->speakers->map(fn($s, $i) => $i === $state->selected ? "\e[42m {$s->name} \e[0m" : " {$s->name} " ); $rightColumn = explode("\n", wordwrap($state->speakers[$state->selected]->bio, 40)); // Zip columns together and return as string return $this->zipColumns($leftColumn, $rightColumn); } } ``` Syntax Notes and Terminal Constraints A critical technical hurdle in TUI development is the **Line Wrap Trap**. If your output string is wider than the terminal window, the terminal will automatically wrap the text to the next line. This breaks the `Laravel Prompts` rendering engine because it no longer knows exactly how many lines to erase before re-drawing the UI. Always use the `terminal()->cols()` helper to calculate available width and use `wordwrap()` to manually break your strings before the terminal does it for you. Another advanced feature is the **Alternate Screen Buffer**. By sending a specific escape code, you can tell the terminal to open a fresh, empty canvas that doesn't mess with the user's scrollback history. When the app exits, the terminal restores the previous screen as if nothing happened. This makes your TUI feel like a standalone application rather than a messy script output. Practical Examples and Tips Beyond simple directories, you can extend these concepts to build Kanban boards, system monitors, or even photo booths that use ASCII art to represent camera feeds. **Best Practices:** * **Buffer your measurements:** Always subtract 1 or 2 characters from the terminal width to account for edge-case rendering bugs. * **Use Monospace Awareness:** Use `mb_strwidth()` instead of `strlen()` to accurately calculate how much space a string occupies, especially if using Unicode characters. * **Contextual Hotkeys:** Display a footer with available commands (e.g., "Press T to Tweet") to guide the user through the interface.
DOM
Products
- Sep 4, 2024
- Sep 3, 2024