Lab: TUI log viewer
Build a Textual TUI that displays a live-updating log table, supports keyboard navigation, and exports the current view to a file.
- Build a DataTable log viewer that populates from an in-memory buffer
- Use set_interval to append a new log entry and refresh the table each second
- Bind the E key to export the current table rows to a text file
- Add a Footer widget that lists available keybindings
This lab ties together everything from the TUI module: a DataTable populated
from an in-memory buffer, a one-second refresh timer, a keybinding that exports
the current view to disk, and a Footer that keeps the user oriented. By the
end you will have a self-contained log viewer you can adapt for any structured
data source.
Textual requires a real terminal to display correctly. The runnable cells below
use pip install textual and print the app structure. Copy the final app code
to a .py file and run it locally with python log_viewer.py.
The data model
The viewer simulates a growing log buffer — a plain Python list that acts as an in-memory ring buffer. In production this list would be fed by a background thread reading from a file, a subprocess pipe, or a network socket.
The buffer grows without bound here. A real viewer would cap it with
_log_buffer = _log_buffer[-500:] to keep memory stable across long sessions.
Building the DataTable view
The DataTable widget shows the buffer contents. Columns are added once in
on_mount; the refresh callback clears and repopulates rows from the current
buffer state.
A few decisions worth noting:
table.clear()+add_row()on each refresh is the right approach here because the rolling-window truncation can remove rows from the top, making keyed row updates unreliable. The table is small enough that a full rebuild does not flicker noticeably.self._buffer = self._buffer[-200:]keeps memory bounded. Adjust the window to match the expected scroll depth.self.notify(...)shows a brief toast message in the bottom-right corner when the export completes — useful feedback without interrupting the view.
The export action
The action_export method writes the buffer to a plain-text file. The naming
convention matters: a method named action_<name> is automatically callable
via BINDINGS — the ("e", "export", "Export") binding calls action_export
without any extra wiring.
Putting it together
Run the complete checklist locally:
pip install textualin a virtual environment.- Save the app code (from the second runnable above) to
log_viewer.py. - Run
python log_viewer.py. The table should appear and gain one new row per second. - Use the arrow keys and Page Down to scroll through older entries.
- Press
E— a toast notification confirms the export. Checklog_export.txtin the same directory. - Press
Qto quit cleanly.
Add BINDINGS = [..., ("c", "clear_buffer", "Clear")] and implement
action_clear_buffer to empty self._buffer and rebuild the table. It is a
common request in real log viewers — operators want to clear noise before an
operation they are watching.
What you practised
You combined three patterns from the TUI module:
- DataTable with a clear-and-rebuild refresh cycle for a variable-length buffer.
- set_interval triggering a data ingestion step and a table redraw each second.
- Action bindings wiring a key directly to a Python method with no boilerplate.
The resulting viewer handles live data, keyboard navigation, and file export in under 60 lines of application logic. That is the payoff for Textual's separation between layout declaration and event handling.
Where to go next
Next module: Plugin Systems — extending Python tools with discoverable,
installable plugins using entry points and importlib.metadata.