xiaohongshu
xiaohongshu
building-tui-apps
Use when building interactive terminal dashboards or full-screen terminal applications; when implementing keyboard navigation, live data updates, or multi-panel layouts; when TUI is flickering, slow, or unresponsive; when handling terminal resize events
Full skill instructions
TUIs are reactive terminal interfaces. Unlike CLIs (single operation → exit), TUIs maintain state, handle events, and update displays continuously. Think of them as web apps for the terminal.
digraph decision {
rankdir=TB;
"Need persistent display?" [shape=diamond];
"Multiple views/panels?" [shape=diamond];
"Real-time updates?" [shape=diamond];
"CLI with progress" [shape=box, style=filled, fillcolor=lightblue];
"Full TUI" [shape=box, style=filled, fillcolor=lightgreen];
"CLI" [shape=box, style=filled, fillcolor=lightyellow];
"Need persistent display?" -> "CLI" [label="no"];
"Need persistent display?" -> "Multiple views/panels?" [label="yes"];
"Multiple views/panels?" -> "Full TUI" [label="yes"];
"Multiple views/panels?" -> "Real-time updates?" [label="no"];
"Real-time updates?" -> "Full TUI" [label="yes"];
"Real-time updates?" -> "CLI with progress" [label="no"];
}
TUI is right when: Dashboard monitoring, file browsers, log viewers, interactive data exploration, multi-step wizards with navigation CLI is better when: Single operation, piping output, scripting, simple progress display
| Language | Full TUI Framework | Simple Interactive |
|---|---|---|
| Python | textual (modern, reactive) | rich (tables, progress, prompts) |
| TypeScript | ink (React-like) or blessed | inquirer (prompts only) |
| C# | Terminal.Gui (full widgets) | Spectre.Console (tables, prompts) |
digraph library {
rankdir=TB;
"Need full-screen app?" [shape=diamond];
"Python or TS?" [shape=diamond];
"C#?" [shape=diamond];
"Modern reactive?" [shape=diamond];
"textual" [shape=box, style=filled, fillcolor=lightgreen];
"ink" [shape=box, style=filled, fillcolor=lightblue];
"blessed" [shape=box, style=filled, fillcolor=lightblue];
"Terminal.Gui" [shape=box, style=filled, fillcolor=lightyellow];
"rich/Spectre" [shape=box, style=filled, fillcolor=lightgray];
"Need full-screen app?" -> "Python or TS?" [label="yes"];
"Need full-screen app?" -> "rich/Spectre" [label="no, just prompts/tables"];
"Python or TS?" -> "textual" [label="Python"];
"Python or TS?" -> "Modern reactive?" [label="TypeScript"];
"Modern reactive?" -> "ink" [label="yes, React-like"];
"Modern reactive?" -> "blessed" [label="no, traditional"];
"Python or TS?" -> "C#?" [label="neither"];
"C#?" -> "Terminal.Gui" [label="yes"];
}
┌─────────────────────────────────────────────────────────┐
│ App │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
│ │ State │→ │ Widgets │→ │ Render │ │
│ │ (reactive) │ │ (compose) │ │ (on change) │ │
│ └─────────────┘ └─────────────┘ └─────────────────┘ │
│ ↑ │ │
│ └────────── Events ←─────────────────┘ │
└─────────────────────────────────────────────────────────┘
All modern TUI frameworks use this reactive pattern:
from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, DataTable, Static
from textual.reactive import reactive
from textual.containers import Horizontal, Vertical
class DashboardApp(App):
"""Main TUI application."""
CSS = """
#sidebar { width: 30; }
#main { width: 1fr; }
"""
BINDINGS = [
("q", "quit", "Quit"),
("r", "refresh", "Refresh"),
("enter", "select", "Select"),
]
# Reactive state - changes trigger UI updates
selected_id: reactive[str | None] = reactive(None)
items: reactive[list] = reactive([])
def compose(self) -> ComposeResult:
"""Build the UI tree."""
yield Header()
with Horizontal():
yield DataTable(id="table")
yield Static(id="detail")
yield Footer()
def on_mount(self) -> None:
"""Called when app starts."""
self.load_data()
def watch_selected_id(self, new_id: str | None) -> None:
"""Called automatically when selected_id changes."""
self.update_detail_panel(new_id)
def action_refresh(self) -> None:
"""Handle 'r' key."""
self.load_data()
async def load_data(self) -> None:
"""Load data without blocking UI."""
self.items = await self.fetch_items()
Workers for async operations:
from textual.worker import Worker
class MyApp(App):
@work(exclusive=True)
async def fetch_data(self) -> None:
"""Run in background, won't block UI."""
result = await api.get_items()
self.items = result
def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
"""Handle worker completion."""
if event.state == WorkerState.SUCCESS:
self.refresh_table()
Custom widgets:
from textual.widget import Widget
from textual.message import Message
class NoticeCard(Widget):
"""Custom widget with message passing."""
class Selected(Message):
def __init__(self, notice_id: str) -> None:
self.notice_id = notice_id
super().__init__()
def on_click(self) -> None:
self.post_message(self.Selected(self.notice_id))
import React, { useState, useEffect } from 'react';
import { render, Box, Text, useInput, useApp } from 'ink';
const Dashboard = () => {
const [items, setItems] = useState<Item[]>([]);
const [selectedIndex, setSelectedIndex] = useState(0);
const { exit } = useApp();
// Handle keyboard input
useInput((input, key) => {
if (input === 'q') exit();
if (key.upArrow) setSelectedIndex(i => Math.max(0, i - 1));
if (key.downArrow) setSelectedIndex(i => Math.min(items.length - 1, i + 1));
if (key.return) handleSelect(items[selectedIndex]);
});
// Load data on mount
useEffect(() => {
loadItems().then(setItems);
}, []);
return (
<Box flexDirection="column">
<Box borderStyle="single" padding={1}>
<Text bold>Dashboard</Text>
</Box>
<Box flexDirection="row">
<ItemList items={items} selected={selectedIndex} />
<DetailPanel item={items[selectedIndex]} />
</Box>
</Box>
);
};
render(<Dashboard />);
Reactive updates:
import { useEffect, useState } from 'react';
const LiveStatus = () => {
const [status, setStatus] = useState('loading');
useEffect(() => {
const interval = setInterval(async () => {
const data = await fetchStatus();
setStatus(data);
}, 1000);
return () => clearInterval(interval);
}, []);
return <Text color={status === 'ok' ? 'green' : 'red'}>{status}</Text>;
};
using Terminal.Gui;
class Program
{
static void Main()
{
Application.Init();
var top = Application.Top;
var win = new Window("Dashboard")
{
X = 0, Y = 1,
Width = Dim.Fill(),
Height = Dim.Fill()
};
var listView = new ListView(items)
{
X = 0, Y = 0,
Width = Dim.Percent(30),
Height = Dim.Fill()
};
var detailView = new TextView()
{
X = Pos.Right(listView) + 1,
Y = 0,
Width = Dim.Fill(),
Height = Dim.Fill()
};
listView.SelectedItemChanged += (args) => {
detailView.Text = GetDetails(items[listView.SelectedItem]);
};
win.Add(listView, detailView);
top.Add(win);
Application.Run();
Application.Shutdown();
}
}
Handle terminal resize gracefully:
# Textual - automatic with CSS
CSS = """
#sidebar {
width: 30;
}
@media (width < 80) {
#sidebar { display: none; }
}
"""
// Ink - useStdout hook
import { useStdout } from 'ink';
const ResponsiveLayout = () => {
const { stdout } = useStdout();
const width = stdout.columns;
return (
<Box flexDirection={width < 80 ? 'column' : 'row'}>
{width >= 80 && <Sidebar />}
<MainContent />
</Box>
);
};
┌────────────────────────────────┐ ┌────────────────────────────────┐
│ Header │ │ Sidebar │ Main │
├──────────┬─────────────────────┤ │ │ │
│ Sidebar │ Main │ │ ────── │ │
│ │ │ │ Item 1 │ Detail View │
│ Nav │ Content │ │ Item 2 │ │
│ │ │ │ Item 3 │ │
├──────────┴─────────────────────┤ │ │ │
│ Footer │ └─────────────┴──────────────────┘
└────────────────────────────────┘
Master-Detail Sidebar + Content
digraph state {
rankdir=LR;
"User Input" [shape=ellipse];
"Event Handler" [shape=box];
"State Update" [shape=box];
"Re-render" [shape=box];
"Display" [shape=ellipse];
"User Input" -> "Event Handler";
"Event Handler" -> "State Update";
"State Update" -> "Re-render";
"Re-render" -> "Display";
"Display" -> "User Input" [style=dashed, label="next input"];
}
Rules:
# Bad - triggers re-render per item
for item in items:
self.items.append(item) # Each append triggers render!
# Good - single update
self.items = new_items # One render
# Textual DataTable handles this automatically
# For custom widgets, only render visible items
def render_visible(self):
viewport_start = self.scroll_offset
viewport_end = viewport_start + self.height
visible_items = self.items[viewport_start:viewport_end]
# Only render visible_items
from textual.timer import Timer
class LiveDashboard(App):
def __init__(self):
self._pending_updates = []
self._update_timer: Timer | None = None
def queue_update(self, data):
self._pending_updates.append(data)
if not self._update_timer:
self._update_timer = self.set_timer(0.1, self._flush_updates)
def _flush_updates(self):
# Process all pending updates at once
self.process_batch(self._pending_updates)
self._pending_updates = []
self._update_timer = None
| Key | Action |
|---|---|
↑/↓ or j/k | Navigate items |
Enter | Select/confirm |
Escape | Cancel/back |
q | Quit |
? | Help |
/ | Search |
Tab | Next panel |
# Textual
class MyApp(App):
def action_next_panel(self) -> None:
self.screen.focus_next()
def action_prev_panel(self) -> None:
self.screen.focus_previous()
Critical rule: Never block the main thread. TUIs freeze if you make synchronous network/file calls.
from textual.app import App
from textual.worker import Worker, WorkerState
class DashboardApp(App):
def on_mount(self) -> None:
# Start worker - doesn't block UI
self.run_worker(self.fetch_data())
async def fetch_data(self) -> None:
"""Runs in background thread."""
result = await api.get_items() # Network call
self.items = result # Update state when done
def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
if event.state == WorkerState.ERROR:
self.show_error(str(event.worker.error))
const Dashboard = () => {
const [data, setData] = useState<Data | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Async in useEffect - doesn't block render
(async () => {
const result = await fetchData();
setData(result);
setLoading(false);
})();
}, []);
if (loading) return <Text>Loading...</Text>;
return <DataView data={data} />;
};
// Use Application.MainLoop.Invoke for thread-safe UI updates
Task.Run(async () => {
var data = await FetchDataAsync();
Application.MainLoop.Invoke(() => {
listView.SetSource(data); // Update UI on main thread
});
});
# Textual - use semantic widgets
from textual.widgets import Button, Label
# Bad - visual only
yield Static("[bold red]Error![/]")
# Good - semantic + visual
yield Label("Error: File not found", id="error", classes="error")
| Anti-Pattern | Problem | Fix |
|---|---|---|
| Blocking main thread | UI freezes | Use workers/async |
| Manual screen clear | Flicker | Use framework's render |
| Global state mutations | Race conditions | Use reactive state |
| Not handling resize | Broken layout | Test with small terminals |
| Hardcoded dimensions | Not portable | Use relative sizing (Dim.Fill, percentages) |
| No keyboard shortcuts | Mouse-dependent | Add BINDINGS/useInput |
| Polling in render | CPU spin | Use timers, events |
from textual.testing import AppTest
async def test_dashboard():
async with AppTest(DashboardApp()) as app:
# Wait for mount
await app.wait_for_loaded()
# Check initial state
table = app.query_one("#table", DataTable)
assert table.row_count > 0
# Simulate key press
await app.press("down")
await app.press("enter")
# Check result
detail = app.query_one("#detail", Static)
assert "selected" in detail.render()
my_tui/
├── app.py # Main App class
├── screens/ # Full-screen views
│ ├── main.py
│ └── detail.py
├── widgets/ # Reusable components
│ ├── sidebar.py
│ └── status_bar.py
├── state/ # State management
│ └── store.py
├── api/ # Backend communication
│ └── client.py
├── styles.css # Textual CSS (if using)
└── tests/
└── test_app.py
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience