urwid is a Python TUI framework for building terminal applications. pip install urwid. Text: from urwid import Text; t = Text("Hello world"). Attr: AttrMap(Text("styled"), "heading"). Edit: from urwid import Edit; e = Edit("Name: "). get_edit_text: e.get_edit_text(). Button: from urwid import Button; b = Button("OK"); urwid.connect_signal(b, "click", callback). CheckBox: from urwid import CheckBox; cb = CheckBox("Enable", state=False). RadioButton: group-based selects. Pile: from urwid import Pile; p = Pile([widget1, widget2]). Columns: from urwid import Columns; c = Columns([w1, w2, w3]). ListBox: from urwid import ListBox, SimpleListWalker; lb = ListBox(SimpleListWalker([Text("item1"), Text("item2")])). Frame: from urwid import Frame; f = Frame(body=lb, header=Text("Header"), footer=Text("Footer")). Padding: from urwid import Padding; Padding(widget, align="center", width=40). Filler: from urwid import Filler; Filler(widget, valign="top"). Canvas: widget.render((width,), focus=False). Palette: palette = [("heading","white","dark blue"),("body","black","white")]. MainLoop: from urwid import MainLoop; loop = MainLoop(widget, palette, unhandled_input=handle_input). run: loop.run(). ExitMainLoop: raise urwid.ExitMainLoop(). set_alarm_in: loop.set_alarm_in(5.0, callback). draw_screen: loop.draw_screen(). Divider: from urwid import Divider; Divider("-"). ProgressBar: from urwid import ProgressBar; pb = ProgressBar("normal","done"). LineBox: from urwid import LineBox; LineBox(widget). SelectableIcon: click target. For asyncio: AsyncioEventLoop. Claude Code generates urwid dashboards, menus, forms, and monitoring panels.
CLAUDE.md for urwid
## urwid Stack
- Version: urwid >= 2.6 | pip install urwid
- Text: Text("str") | AttrMap(widget, "palette_key")
- Input: Edit("prompt> ") | Button("OK") | CheckBox("opt") | RadioButton(group, "opt")
- Layout: Pile([w1,w2]) | Columns([w1,w2]) | ListBox(SimpleListWalker([...]))
- Frame: Frame(body=lb, header=Text("H"), footer=Text("F"))
- Loop: MainLoop(top_widget, palette).run() | raise ExitMainLoop()
urwid TUI Pipeline
# app/tui.py — urwid widgets, layout, palette, Frame, MainLoop, dialogs
from __future__ import annotations
import urwid
from urwid import (
AttrMap,
Button,
CheckBox,
Columns,
Divider,
Edit,
Filler,
Frame,
LineBox,
ListBox,
MainLoop,
Padding,
Pile,
ProgressBar,
RadioButton,
SelectableIcon,
SimpleListWalker,
Text,
connect_signal,
)
# ─────────────────────────────────────────────────────────────────────────────
# 1. Palette
# ─────────────────────────────────────────────────────────────────────────────
PALETTE = [
# (name, fg, bg, mono)
("header", "white", "dark blue", "bold"),
("footer", "light gray", "dark gray", ""),
("body", "light gray", "black", ""),
("focus", "black", "light gray", "bold"),
("heading", "yellow", "black", "bold"),
("success", "light green", "black", ""),
("error", "light red", "black", "bold"),
("warning", "yellow", "black", ""),
("button", "black", "light gray", ""),
("button_focus", "white", "dark blue", "bold"),
("progress_done", "black", "light green",""),
("progress_norm", "black", "dark gray", ""),
("line", "dark gray", "black", ""),
]
# ─────────────────────────────────────────────────────────────────────────────
# 2. Widget helpers
# ─────────────────────────────────────────────────────────────────────────────
def title_text(text: str) -> AttrMap:
"""Styled heading text."""
return AttrMap(Text(text, align="center"), "heading")
def styled(widget, attr: str) -> AttrMap:
"""Wrap a widget with a palette attribute."""
return AttrMap(widget, attr)
def make_button(
label: str,
on_click,
user_data=None,
attr: str = "button",
focus_attr: str = "button_focus",
) -> AttrMap:
"""
Create a styled button with a click handler.
Example:
btn = make_button(" OK ", lambda btn, data: handle_ok())
"""
btn = Button(label)
connect_signal(btn, "click", on_click, user_data)
return AttrMap(btn, attr, focus_attr)
def make_edit(
caption: str = "",
default: str = "",
multiline: bool = False,
mask: str | None = None,
) -> Edit:
"""
Create an edit widget.
Example:
username = make_edit("Username: ")
password = make_edit("Password: ", mask="*")
"""
kwargs = {"caption": caption, "edit_text": default, "multiline": multiline}
if mask:
kwargs["mask"] = mask
return Edit(**kwargs)
def hline(char: str = "─") -> AttrMap:
"""Horizontal divider line."""
return AttrMap(Divider(char), "line")
def vspace(rows: int = 1) -> Text:
"""Empty vertical padding."""
return Text("\n" * (rows - 1))
def button_row(buttons: list[AttrMap], align: str = "center") -> Padding:
"""
Lay out buttons in a horizontal row.
Example:
row = button_row([
make_button(" OK ", on_ok),
make_button(" Cancel ", on_cancel),
])
"""
col_items = [(12, b) for b in buttons]
cols = Columns(col_items, dividechars=2)
return Padding(cols, align=align, width="clip")
# ─────────────────────────────────────────────────────────────────────────────
# 3. Layouts
# ─────────────────────────────────────────────────────────────────────────────
def make_frame(
body,
header_text: str = "",
footer_text: str = " q:quit tab:next enter:select",
) -> Frame:
"""
Wrap a body widget in a Frame with styled header and footer.
Example:
frame = make_frame(list_widget, header_text=" My App", footer_text=" q:quit")
"""
header = AttrMap(Text((" " + header_text).ljust(80), align="left"), "header")
footer = AttrMap(Text(footer_text, align="left"), "footer")
return Frame(AttrMap(body, "body"), header=header, footer=footer)
def centered(widget, width: int = 60, height: int | None = None, valign: str = "middle"):
"""
Center a widget horizontally and optionally vertically.
"""
padded = Padding(widget, align="center", width=width)
if height:
return Filler(padded, valign=valign, height=height)
return Filler(padded, valign=valign)
def in_box(widget, title: str = "") -> LineBox:
"""Wrap a widget in a box with optional title."""
return LineBox(widget, title=title, title_align="left")
# ─────────────────────────────────────────────────────────────────────────────
# 4. Scrollable list view
# ─────────────────────────────────────────────────────────────────────────────
class SelectableItem(urwid.WidgetWrap):
"""A selectable list item."""
def __init__(self, label: str, value=None) -> None:
self.value = value
self._text = SelectableIcon(f" {label}", 0)
super().__init__(AttrMap(self._text, "body", "focus"))
def selectable(self) -> bool:
return True
def keypress(self, size, key: str) -> str | None:
return key
def make_list_view(
items: list[str],
on_select=None,
title: str = "",
) -> ListBox:
"""
Create a scrollable list of selectable items.
Example:
lv = make_list_view(
["Option A", "Option B", "Option C"],
on_select=lambda label, value: handle_select(label),
)
"""
widgets = []
for item in items:
si = SelectableItem(item, value=item)
if on_select:
urwid.connect_signal(si._text, "click", lambda btn, val=item: on_select(btn, val))
widgets.append(si)
walker = SimpleListWalker(widgets)
lb = ListBox(walker)
return in_box(lb, title=title) if title else lb
# ─────────────────────────────────────────────────────────────────────────────
# 5. Dialog overlay
# ─────────────────────────────────────────────────────────────────────────────
def make_dialog(
message: str,
on_ok=None,
on_cancel=None,
title: str = "Confirm",
width: int = 40,
height: int = 8,
):
"""
Create a confirmation dialog widget.
Use with urwid.Overlay to show on top of another widget.
Example:
dialog = make_dialog("Are you sure?", on_ok=handle_ok, on_cancel=handle_cancel)
overlay = urwid.Overlay(
dialog, main_widget,
align="center", width=40,
valign="middle", height=8,
)
"""
body = Pile([
Text(message, align="center"),
vspace(1),
button_row([
make_button(" OK ", on_ok or (lambda b: None)),
make_button(" Cancel ", on_cancel or (lambda b: None)),
]),
])
return in_box(centered(body, width=width - 4, height=height - 2), title=title)
# ─────────────────────────────────────────────────────────────────────────────
# 6. Progress bar demo
# ─────────────────────────────────────────────────────────────────────────────
class ProgressView:
"""
A simple progress bar screen.
Example:
pv = ProgressView("Building...", steps=100)
pv.run()
"""
def __init__(self, title: str = "Progress", steps: int = 100) -> None:
self.title = title
self.steps = steps
self._current = 0
self._pb = ProgressBar("progress_norm", "progress_done", done=steps)
self._status = Text("Starting...", align="center")
self._loop: MainLoop | None = None
body = Pile([
vspace(1),
title_text(title),
vspace(1),
Padding(self._pb, align="center", width=60),
vspace(1),
self._status,
])
self._frame = make_frame(Filler(body, valign="top"), header_text=f" {title}")
def update(self, step: int, status: str = "") -> None:
self._current = step
self._pb.set_completion(step)
if status:
self._status.set_text(status)
if self._loop:
self._loop.draw_screen()
def done(self) -> None:
self._pb.set_completion(self.steps)
self._status.set_text("Complete!")
if self._loop:
self._loop.set_alarm_in(1.5, lambda loop, _: (_ for _ in ()).throw(urwid.ExitMainLoop()))
def run(self) -> None:
self._loop = MainLoop(self._frame, PALETTE)
self._loop.run()
# ─────────────────────────────────────────────────────────────────────────────
# 7. Full demo application
# ─────────────────────────────────────────────────────────────────────────────
def build_demo_app() -> tuple[Frame, MainLoop]:
"""
Build a demo TUI app with a menu and form.
Example:
frame, loop = build_demo_app()
loop.run()
"""
log_messages = ["App started", "Waiting for input..."]
log_walker = SimpleListWalker([Text(m) for m in log_messages])
# Form fields
name_edit = make_edit("Name: ")
email_edit = make_edit("Email: ")
pass_edit = make_edit("Password:", mask="*")
notify_cb = CheckBox("Email notifications", state=True)
log_lb = ListBox(log_walker)
status_text = [Text("Status: ready", align="center")]
def log(msg: str) -> None:
log_walker.contents.append(Text(f" {msg}"))
log_lb.set_focus(len(log_walker.contents) - 1)
def on_submit(btn) -> None:
name = name_edit.get_edit_text()
email = email_edit.get_edit_text()
log(f"Submitted: name={name!r} email={email!r}")
status_text[0].set_text(f"Submitted: {name}")
def on_reset(btn) -> None:
name_edit.set_edit_text("")
email_edit.set_edit_text("")
pass_edit.set_edit_text("")
notify_cb.set_state(True)
log("Form reset.")
status_text[0].set_text("Status: reset")
def handle_input(key: str) -> None:
if key in ("q", "Q", "esc"):
raise urwid.ExitMainLoop()
form = Pile([
title_text(" User Registration"),
hline(),
AttrMap(name_edit, "body", "focus"),
AttrMap(email_edit, "body", "focus"),
AttrMap(pass_edit, "body", "focus"),
vspace(1),
notify_cb,
vspace(1),
button_row([
make_button(" Submit ", on_submit),
make_button(" Reset ", on_reset),
]),
hline(),
status_text[0],
])
left = Padding(in_box(form, title="Form"), left=1, right=1)
right = in_box(log_lb, title="Log")
cols = Columns([(60, left), right], dividechars=1)
frame = make_frame(cols, header_text=" urwid Demo", footer_text=" q:quit Tab:next Enter:select")
loop = MainLoop(frame, PALETTE, unhandled_input=handle_input)
return frame, loop
if __name__ == "__main__":
print("Launching urwid demo TUI (press 'q' or Esc to quit)...")
_, loop = build_demo_app()
loop.run()
For the textual alternative — Textual is a modern async TUI framework with CSS-style layouts, reactive state, mouse support, and a rich widget library; urwid is a mature, stable, lower-level framework with a direct widget tree model and signal-based event handling — use Textual for new TUI projects where you want a CSS-like declarative styling system and async support, urwid for existing projects or when you need fine-grained widget control with minimal dependencies. For the curses stdlib alternative — Python’s curses module provides direct terminal manipulation at the character/attribute level; urwid builds a complete widget framework on top of curses with Pile/Columns layouts, focus management, and signal-based interactivity — use curses when you need raw terminal control or are building custom display primitives, urwid when you want a widget system with layout management and focus handling. The Claude Skills 360 bundle includes urwid skill sets covering PALETTE definition, title_text()/styled()/make_button()/make_edit()/hline()/vspace()/button_row() helpers, make_frame()/centered()/in_box() layouts, SelectableItem/make_list_view() scrollable lists, make_dialog() overlay dialogs, ProgressView class, and build_demo_app() full form+log demo. Start with the free tier to try terminal UI and TUI dashboard code generation.