I've been writing Python CLI tools for years the same way: print() statements, maybe some argparse, and if I was feeling fancy, rich for colored output. Then I actually sat down and built something with Textual, and it changed how I think about terminal apps.
If you haven't heard of it, Textual is a Python framework (from the same team behind rich) for building full TUI (Terminal User Interface) apps --- think dashboards, forms, file browsers, all running inside your terminal, but with actual layout, widgets, mouse support, and CSS-like styling.
Why I picked it up
I wanted a quick internal tool to monitor some background jobs --- nothing worth a full web dashboard, but plain log output wasn't cutting it either. Textual felt like the middle ground I didn't know I needed.
So instead of just reading docs, I built something real with it: a command-logging utility --- a TUI where you can log commands you run (with context/notes attached) without breaking flow to open a separate notes app. Code's on GitHub.
Setting it up
pip install textual textual-dev
That's it. No extra system dependencies, no weird terminal config. textual-dev gives you a live console for debugging, which honestly saved me a lot of guesswork and the devtools lets you serve an app launched directly from a Python file.
The part that surprised me: CSS
Yes, actual CSS. You style widgets with .tcss files:
Screen {
background: $surface;
}
#sidebar {
width: 30;
border: solid green;
}
Coming from years of manually padding strings to "layout" a CLI, this felt like cheating. Widgets snap into place, you get flexbox-like sizing, and you're not fighting with \n and spaces to align things.
A minimal example
from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, Static
class DemoApp(App):
CSS_PATH = "demo.tcss"
def compose(self) -> ComposeResult:
yield Header()
yield Static("Hello from Textual", id="body")
yield Footer()
if __name__ == "__main__":
DemoApp().run()
Run it, and you already get a header, footer with keybindings, and a styled body. No boilerplate hell.
What actually impressed me
- Reactive attributes : change a variable, and the UI updates automatically. No manual redraw logic.
- Built-in widgets : DataTable, Tree, ListView, Input, all ready to use. I didn't have to build a table renderer from scratch.
- Works over SSH : since it's still a terminal app, no port forwarding or browser needed. That alone makes it worth it for remote/server tooling.
-
Hot reload :
textual run --dev app.pyand CSS changes reflect instantly without restarting.
Where it got tricky
- Debugging layout issues (widgets not sizing how you expect) took some trial and error — the docs help, but you'll spend time in the CSS live-reload loop.
- Testing TUI apps isn't as straightforward as testing a normal script; Textual has a test harness (
Pilot), but it's a different mental model. - Quitting the application (Ctrl+q) is sometimes tricky due to some similar key bindings.
The project: a command-logging utility
The idea was simple; I run a lot of one-off commands (debugging, ops, random scripts) and kept losing track of why I ran them. So I built a small Textual app that lets you:
- Log a command + a quick note in one input
- Browse past entries in a scrollable list
- Delete entries without leaving the terminal
Nothing groundbreaking, but it's exactly the kind of tool that's annoying to build with plain print() and perfect for Textual's widget set: Input for capture, and reactive state to keep the list in sync as you add entries.
Should you use it?
If you're building anything more than a script that prints and exits — a monitoring tool, an admin utility, a game --- Textual is worth the few hours it takes to get comfortable. It's actively maintained, the community's responsive, and it makes terminal apps feel like a legitimate UI surface instead of an afterthought.
Note: I'm not affiliated with Rich/Textual in any way and these are my personal opinions.
Top comments (0)