Skip to Content
Course content

352: Building Terminal UIs with Rich or Textual

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When you first start playing with Rich, it feels like magic. You wrap a string in a Panel or throw some data into a Table, and suddenly your terminal looks like a professional dashboard. But there's a specific point where most developers hit a wall: the moment they try to make that UI dynamic.

I see this all the time. Someone wants a progress tracker or a live-updating status board, and they try to combine a Live display with standard printing. It usually looks something like this:

from rich.live import Live
from rich.table import Table
from rich.console import Console
import time

console = Console()
table = Table(title="System Monitor")
table.add_column("Metric")
table.add_column("Value")

# We want this table to update every second
with Live(table, refresh_per_second=4) as live:
    while True:
        # The mistake: adding rows inside the loop without clearing
        table.add_row("CPU Usage", "15%")
        table.add_row("Memory Usage", "42%")
        time.sleep(1)

The Infinite Scrolling Table Problem

If you run the code above, you'll notice something frustrating. Instead of the "CPU Usage" and "Memory Usage" values updating in place, the table just grows and grows. Every single second, two new rows are appended to the bottom, and your terminal scrolls infinitely until the original header is long gone. You aren't building a dashboard; you're building a very organized list of historical data.

The issue here is a misunderstanding of how Live works. Live doesn't "magic away" the contents of the object you give it; it simply re-renders the current state of that object to the screen. Since you're calling table.add_row() inside the loop, you're mutating the table by making it larger every iteration.

Reconstructing State for a Clean Refresh

To fix this, you have to stop thinking about "adding" to the UI and start thinking about "defining" the UI for that specific moment in time. I usually handle this by moving the table creation inside the loop or by clearing the table before adding the new data.

from rich.live import Live
from rich.table import Table
import time
import random

def generate_status_table():
    # We create a fresh table object every time we want to render
    table = Table(title="System Monitor")
    table.add_column("Metric")
    table.add_column("Value")
    
    table.add_row("CPU Usage", f"{random.randint(10, 90)}%")
    table.add_row("Memory Usage", f"{random.randint(30, 70)}%")
    return table

with Live(generate_status_table(), refresh_per_second=4) as live:
    while True:
        time.sleep(1)
        # We update the live display with a brand new table instance
        live.update(generate_status_table())

By using live.update() with a fresh table, we replace the previous render entirely. This is the core philosophy of modern UI development—similar to how React works—where you describe what the UI should look like based on the current state, rather than manually manipulating individual elements.

Organizing Complex Screens with Layout

Once you move past simple tables, you'll realize that a single object isn't enough for a real tool. You might want a header, a main content area, and a footer. This is where rich.layout.Layout comes in. I treat Layout like a CSS Grid for the terminal. You split the screen into regions, and then you assign a "renderable" (like a Table or a Panel) to each region.

I recommend defining your layout structure once at the start and then only updating the update() method of the specific panels that actually change. It prevents the screen from flickering and makes your code much easier to manage as the project grows.

Transitioning to Textual for Interactivity

Here is the honest truth: Rich is amazing for output, but it is terrible for input. If you want a user to be able to click buttons, scroll through a list, or type into a text box, stop using Rich and move to Textual.

Textual is built on top of Rich, so all your knowledge of Panels and Tables carries over. The big shift is that Textual is an event-driven framework. Instead of a while True loop, you create a class that inherits from App, define your widgets in a compose method, and handle interactions via messages. It's the difference between writing a script that prints things and building a full-blown application that lives in the terminal.




📋 Practical Task

Build a Real-Time Log Monitor with a Status Header

Your task is to create a terminal utility that simulates a log monitor. The screen should be split into two sections: a top "Status Header" and a bottom "Log Feed".

  • The Header: Use a rich.panel.Panel that displays the current timestamp and a randomly generated "System Health" status (e.g., "Healthy", "Warning", or "Critical"). This should update every second.
  • The Log Feed: Use a rich.table.Table or a rich.text.Text block that displays the last 5 simulated log entries. As new logs arrive, the oldest ones should disappear (do not let the table grow infinitely).
  • The Implementation: Use rich.layout.Layout to divide the screen and rich.live.Live to handle the updates.

Requirements: Ensure that the header and the log feed update independently without causing the entire terminal to scroll. The final result should look like a fixed-position dashboard.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.