## How to Build Beautiful Terminal User Interfaces in Python
To build a beautiful and maintainable Terminal User Interface (TUI) in Python, combine the `rich` library for vibrant presentation and the `questionary` library for interactive prompts. The key is to create a centralized theme class for styling (colors, icons, layout) and a base UI class with reusable components, ensuring a consistent and professional look across your entire application.
This guide breaks down the architecture used by the `tng-python` CLI to create its powerful interactive interface.
## What are `rich` and `questionary`?
The foundation of a modern Python TUI rests on two key libraries that handle presentation and interaction separately.
- **`rich`**: A library for writing rich text and beautiful formatting to the terminal. It's used for rendering panels, tables, styled text, progress bars, and more. It handles the *output*.
- **`questionary`**: A library for building interactive command-line prompts. It's used for asking questions, creating menus, and getting user input. It handles the *input*.
By combining them, you get a full-featured, app-like experience directly in the terminal.
## How to Structure a Theming System for TUIs
The most critical architectural decision for a scalable TUI is to **centralize all styling**. In the `tng-python` project, this is handled by a single `TngTheme` class in `theme.py`. This class acts as a single source of truth for all visual elements.
A well-structured theme class should contain nested classes for different aspects of styling:
1. **Colors**: Define all color names and styles (e.g., `PRIMARY`, `SUCCESS`, `TEXT_MUTED`).
2. **Icons**: Keep all Unicode icons/emojis in one place (e.g., `BACK`, `SUCCESS`, `FILE`).
3. **Layout**: Specify dimensions like padding, widths, and box styles.
4. **TextStyles**: Define semantic text styles (e.g., `TITLE`, `HEADER`, `INSTRUCTION`).
```python
class SomeClass:
"""Centralized theme configuration forPython UI"""
# ==================== COLORS ====================
class Colors:
PRIMARY = "cyan"
SUCCESS = "bold green"
WARNING = "bold yellow"
TEXT_MUTED = "dim white"
BORDER_DEFAULT = "cyan"
# ==================== ICONS ====================
class Icons:
BACK = "←"
SUCCESS = "✅"
FILE = "📄"
LIGHTBULB = "💡"
# ==================== LAYOUT ====================
class Layout:
PANEL_PADDING = (1, 2)
PANEL_BOX_STYLE = DOUBLE # from rich.box
# ==================== TEXT STYLES ====================
class TextStyles:
TITLE = "bold cyan"
HEADER = "bold white"
INSTRUCTION = "dim"
```
**Our unique insight:** The most effective TUIs separate presentation logic (the 'what') from styling (the 'how'). A centralized theme class is the architectural pattern that enables this separation, making complex UIs maintainable and easy to re-brand.
## How to Create Reusable UI Components
To avoid repeating code, create a `BaseUI` class that all your UI "screens" can inherit from. This base class, seen in `base_ui.py`, should:
1. Initialize the `Console` from `rich` and your `TngTheme`.
2. Provide helper methods for creating common UI elements like styled panels.
The `create_centered_panel` method is a perfect example. It takes content and a title, applies consistent styling from the theme, and returns a `Panel` object ready to be displayed.
```python
from rich.console import Console
from rich.panel import Panel
from rich.align import Align
from .theme import TngTheme
class BaseUI:
def __init__(self):
self.console = Console()
self.theme = TngTheme()
def create_centered_panel(self, content, title, border_style=None):
"""Create a centered panel with consistent styling"""
if border_style is None:
border_style = self.theme.Colors.BORDER_DEFAULT
panel = Panel(
Align.center(content),
title=title,
border_style=border_style,
padding=self.theme.Layout.PANEL_PADDING,
box=self.theme.Layout.PANEL_BOX_STYLE
)
return panel
```
By using this helper, every panel in the application looks the same, reinforcing a professional user experience.
## How to Build Interactive Prompts and Menus
`questionary` makes it easy to build interactive menus. The key is to integrate your theme with `questionary`'s styling system. You can create a method in your theme class that returns a `questionary.Style` object.
```python
# In theme.py
import questionary
class Theme:
# ... (other classes) ...
@classmethod
def get_questionary_style(cls):
"""Get questionary style configuration"""
return questionary.Style([
('question', cls.TextStyles.QUESTION), # "bold cyan"
('answer', cls.TextStyles.ANSWER), # "bold green"
('pointer', cls.Colors.POINTER), # "bold yellow"
('highlighted', cls.Colors.HIGHLIGHTED), # "bold green"
('selected', cls.Colors.SELECTED), # "bold green"
])
```
Then, in your UI screens, you pass this style to your prompts.
```python
# In any UI screen class
import questionary
from .base_ui import BaseUI
class MyScreen(BaseUI):
def show_menu(self):
action = questionary.select(
"Select an option:",
choices=["Generate Tests", "View Stats", "Exit"],
style=self.theme.get_questionary_style() # Use the centralized theme
).ask()
return action
```
This ensures even interactive elements match your application's brand.
## How to Display Rich Content like Tables
The `rich` library excels at displaying structured data. The `Table` class lets you create beautiful, formatted tables with headers, titles, and custom styles drawn directly from your theme.
```python
# A simplified example from help_ui.py
from rich.table import Table
class HelpUI(BaseUI):
def show(self):
table = Table(
title="Available Commands",
show_header=True,
header_style=self.theme.TextStyles.HEADER,
box=self.theme.Layout.PANEL_BOX_STYLE
)
table.add_column("Command", style=self.theme.Colors.PRIMARY)
table.add_column("Description", style=self.theme.Colors.TEXT_PRIMARY)
table.add_row("tng", "Start interactive test generation mode")
table.add_row("tng-init", "Generate TNG configuration file")
self.console.print(table)
```
## FAQ for Building Python TUIs
### What are the best libraries for Python TUIs in 2025?
For CLI applications, the combination of **`rich`** (for display) and **`questionary`** (for prompts) is a powerful, modern, and easy-to-use stack. For full-screen, app-like experiences with more complex layouts and widgets, **`textual`** (from the creator of `rich`) is the leading choice.
### Is `rich` better than the built-in `curses` library?
Yes, for most use cases. `curses` is a low-level library that requires manual handling of screen state, positioning, and colors, which is complex and error-prone. `rich` provides a high-level, declarative API that handles all the hard parts for you, making it significantly faster to develop and maintain beautiful TUIs.
### How do you handle terminal width and responsiveness?
The `rich.console.Console` object can automatically detect the terminal width. You can build responsive layouts by creating `Panel` and `Table` objects whose dimensions are based on the console width, as demonstrated in the `TngTheme`'s `center_text` and `calculate_box_width` helper methods.
### Can you use emojis and icons in the terminal?
Yes. Modern terminals fully support Unicode, including emojis. Storing them in a central `Icons` class within your theme (like in `theme.py`) makes them easy to manage and use consistently. They add significant visual appeal and clarity to a TUI.
### What's the difference between `rich` and `textual`?
`rich` is a library for rendering rich content in the terminal. You print something, and it's done. `textual` is a full application framework for building TUIs. It runs an event loop, manages state, and has a widget-based system similar to a GUI framework. Use `rich` for CLI *output*; use `textual` for CLI *apps*. `tng-python` uses `rich` and `questionary` for its interactive prompts, which is a common pattern.
### How do you manage colors and styles consistently?
The best practice is to define all colors and semantic styles (like "title" or "error message") in a single, centralized `theme.py` file. In your UI code, always reference these theme variables (e.g., `self.theme.Colors.SUCCESS`) instead of hardcoding color strings like `"green"`. This allows you to change your entire application's look from one file.
From our repo: [https://tng.sh](https://tng.sh)