Claude Code for curses: Python Terminal UI Programming — Claude Skills 360 Blog
Blog / AI / Claude Code for curses: Python Terminal UI Programming
AI

Claude Code for curses: Python Terminal UI Programming

Published: September 30, 2028
Read time: 5 min read
By: Claude Skills 360

Python’s curses module provides an interface to the ncurses terminal control library for building text-based UIs. import curses. wrapper: curses.wrapper(fn) — initializes curses, calls fn(stdscr), restores terminal on exit (recommended over manual initscr). initscr: stdscr = curses.initscr() — manual init; always pair with curses.endwin(). newwin: win = curses.newwin(height, width, begin_y, begin_x) — create a window; 0 = full terminal. getmaxyx: h, w = stdscr.getmaxyx(). addstr: win.addstr(y, x, "text", attr) — write at position; attr e.g. curses.A_BOLD | curses.color_pair(1). getch: ch = win.getch() — blocking key read; win.nodelay(True) for non-blocking (-1 = no key). getkey: key = win.getkey() — returns str key name. refresh: win.refresh() — write to terminal. box: win.box() — draw border. start_color / init_pair: curses.start_color(); curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK). color_pair: curses.color_pair(1) — attribute. noecho: curses.noecho() — don’t print typed chars. cbreak: curses.cbreak() — no line buffering. Attributes: A_BOLD, A_REVERSE, A_UNDERLINE, A_DIM, A_BLINK. curses.KEY_UP/DOWN/LEFT/RIGHT/ENTER/BACKSPACE. KEY_RESIZE — terminal resize event. curses.panel — layered windows. Claude Code generates terminal dashboards, file pickers, progress UIs, and interactive menus.

CLAUDE.md for curses

## curses Stack
- Stdlib: import curses
- Entry:  curses.wrapper(main)      # safest: restores terminal on crash
- Dims:   h, w = stdscr.getmaxyx()
- Write:  win.addstr(y, x, text, curses.A_BOLD | curses.color_pair(1))
- Read:   ch = win.getch()          # blocking; -1 if nodelay
- Color:  curses.start_color(); curses.init_pair(1, curses.COLOR_CYAN, -1)
- Flush:  win.refresh()             # or: noutrefresh + doupdate for flicker-free

curses Terminal UI Pipeline

# app/cursesutil.py — safe init, color helper, menu, progress bar, dashboard panel
from __future__ import annotations

import curses
import curses.panel
import time
from contextlib import contextmanager
from dataclasses import dataclass, field
from typing import Callable, Generator, Any


# ─────────────────────────────────────────────────────────────────────────────
# 1. Safe initialization helpers
# ─────────────────────────────────────────────────────────────────────────────

def run(fn: Callable) -> Any:
    """
    Run fn(stdscr) inside curses.wrapper — restores the terminal on any exit.
    Use instead of calling curses.initscr() manually.

    Example:
        def my_app(stdscr):
            stdscr.addstr(0, 0, "Hello curses!")
            stdscr.getch()
        run(my_app)
    """
    return curses.wrapper(fn)


@contextmanager
def curses_session() -> Generator[Any, None, None]:
    """
    Context manager that initializes curses and yields stdscr.
    Restores terminal on exit even if an exception is raised.

    Example:
        with curses_session() as stdscr:
            stdscr.addstr(0, 0, "Running...")
            stdscr.getch()
    """
    stdscr = curses.initscr()
    curses.noecho()
    curses.cbreak()
    stdscr.keypad(True)
    try:
        yield stdscr
    finally:
        stdscr.keypad(False)
        curses.echo()
        curses.nocbreak()
        curses.endwin()


# ─────────────────────────────────────────────────────────────────────────────
# 2. Color helpers
# ─────────────────────────────────────────────────────────────────────────────

# Default color pair assignments
_COLOR_PAIRS: dict[str, int] = {}
_NEXT_PAIR_ID = 1


def setup_colors(
    pairs: list[tuple[str, int, int]] | None = None,
) -> dict[str, int]:
    """
    Initialize color pairs. pairs = [(name, fg, bg), ...].
    Returns dict of name → curses.color_pair() value.
    Uses -1 for default terminal background (requires use_default_colors).

    Example:
        colors = setup_colors([
            ("header",  curses.COLOR_CYAN,  -1),
            ("error",   curses.COLOR_RED,   -1),
            ("success", curses.COLOR_GREEN, -1),
        ])
        win.addstr(0, 0, "Header", colors["header"] | curses.A_BOLD)
    """
    global _NEXT_PAIR_ID
    curses.start_color()
    try:
        curses.use_default_colors()
    except Exception:
        pass

    default_pairs = [
        ("normal",  curses.COLOR_WHITE,  -1),
        ("header",  curses.COLOR_CYAN,   -1),
        ("error",   curses.COLOR_RED,    -1),
        ("success", curses.COLOR_GREEN,  -1),
        ("warning", curses.COLOR_YELLOW, -1),
        ("dim",     curses.COLOR_WHITE,  -1),
    ]
    all_pairs = (pairs or default_pairs)
    result: dict[str, int] = {}
    for i, (name, fg, bg) in enumerate(all_pairs, 1):
        curses.init_pair(i, fg, bg)
        result[name] = curses.color_pair(i)
        _NEXT_PAIR_ID = i + 1
    return result


# ─────────────────────────────────────────────────────────────────────────────
# 3. Drawing primitives
# ─────────────────────────────────────────────────────────────────────────────

def safe_addstr(win: Any, y: int, x: int, text: str, attr: int = 0) -> None:
    """
    Write text to window at (y, x), silently ignoring out-of-bounds errors.

    Example:
        safe_addstr(stdscr, 0, 0, "Status: OK", curses.A_BOLD)
    """
    try:
        win.addstr(y, x, text, attr)
    except curses.error:
        pass


def center_text(win: Any, y: int, text: str, attr: int = 0) -> None:
    """
    Write text centered horizontally on row y.

    Example:
        center_text(stdscr, 0, "─── My App ───", curses.A_BOLD)
    """
    _, w = win.getmaxyx()
    x = max(0, (w - len(text)) // 2)
    safe_addstr(win, y, x, text[:w], attr)


def draw_box_title(win: Any, title: str, attr: int = 0) -> None:
    """
    Draw a border and place title at the top edge.

    Example:
        popup = curses.newwin(10, 40, 5, 10)
        draw_box_title(popup, " My Popup ")
        popup.refresh()
    """
    win.box()
    h, w = win.getmaxyx()
    if title and w > 4:
        safe_addstr(win, 0, (w - len(title)) // 2, title[:w - 2], attr)


def fill_row(win: Any, y: int, char: str = " ", attr: int = 0) -> None:
    """Fill an entire row with char (useful for highlight bars)."""
    _, w = win.getmaxyx()
    safe_addstr(win, y, 0, (char * w)[:w], attr)


# ─────────────────────────────────────────────────────────────────────────────
# 4. Interactive menu
# ─────────────────────────────────────────────────────────────────────────────

@dataclass
class MenuItem:
    label: str
    value: Any = None
    shortcut: str = ""


def popup_menu(
    stdscr: Any,
    items: list[MenuItem],
    title: str = " Select ",
    y: int | None = None,
    x: int | None = None,
) -> Any | None:
    """
    Display a modal selection menu. Returns the selected item's value or None.
    Arrow keys / j/k to navigate, Enter to select, q/Escape to cancel.

    Example:
        result = popup_menu(stdscr, [
            MenuItem("Option A", "a"),
            MenuItem("Option B", "b"),
            MenuItem("Cancel",   None),
        ], title=" Choose ")
    """
    h_max, w_max = stdscr.getmaxyx()
    width  = max(len(title), *(len(i.label) + 4 for i in items)) + 4
    height = len(items) + 4
    width  = min(width, w_max - 2)
    height = min(height, h_max - 2)
    by = y if y is not None else (h_max - height) // 2
    bx = x if x is not None else (w_max - width) // 2

    win = curses.newwin(height, width, by, bx)
    curses.curs_set(0)
    selected = 0

    while True:
        win.erase()
        draw_box_title(win, title)
        for i, item in enumerate(items):
            attr = curses.A_REVERSE if i == selected else 0
            row = i + 2
            text = f"  {item.label}"
            if item.shortcut:
                text += f"  [{item.shortcut}]"
            safe_addstr(win, row, 1, text[: width - 2], attr)
        win.refresh()

        ch = stdscr.getch()
        if ch in (curses.KEY_UP, ord("k")):
            selected = (selected - 1) % len(items)
        elif ch in (curses.KEY_DOWN, ord("j")):
            selected = (selected + 1) % len(items)
        elif ch in (curses.KEY_ENTER, 10, 13):
            return items[selected].value
        elif ch in (ord("q"), 27):  # q or ESC
            return None
        else:
            # Shortcut key
            for i, item in enumerate(items):
                if item.shortcut and ch == ord(item.shortcut):
                    return item.value


# ─────────────────────────────────────────────────────────────────────────────
# 5. Progress bar
# ─────────────────────────────────────────────────────────────────────────────

@dataclass
class ProgressBar:
    """
    A simple progress bar drawn inside a curses window.

    Example:
        def main(stdscr):
            bar = ProgressBar(stdscr, y=5, x=2, width=40)
            for i in range(101):
                bar.update(i / 100, f"Step {i}/100")
                time.sleep(0.02)
        curses.wrapper(main)
    """
    win: Any
    y: int
    x: int
    width: int = 40

    def update(self, fraction: float, label: str = "") -> None:
        """Update progress (fraction 0.0–1.0) and optional label."""
        fraction = max(0.0, min(1.0, fraction))
        filled = int(self.width * fraction)
        bar = "█" * filled + "░" * (self.width - filled)
        pct = f"{fraction * 100:5.1f}%"
        text = f"{bar} {pct}"
        if label:
            text += f"  {label}"
        safe_addstr(self.win, self.y, self.x, text[:self.width + 20])
        self.win.refresh()


# ─────────────────────────────────────────────────────────────────────────────
# Demo (runs only in a real terminal)
# ─────────────────────────────────────────────────────────────────────────────

def _demo_main(stdscr: Any) -> None:
    """Demo that renders a simple dashboard with a menu."""
    colors = setup_colors()
    curses.curs_set(0)
    stdscr.clear()

    h, w = stdscr.getmaxyx()
    header = f" curses demo — {w}×{h} terminal "
    center_text(stdscr, 0, header, colors["header"] | curses.A_BOLD)

    # Status panel
    panel_win = curses.newwin(8, w - 4, 2, 2)
    draw_box_title(panel_win, " Status ", colors["header"])
    safe_addstr(panel_win, 2, 2, "Python curses dashboard", curses.A_BOLD)
    safe_addstr(panel_win, 3, 2, f"Terminal size: {w}×{h}")
    safe_addstr(panel_win, 4, 2, "Press any key for menu...", colors["dim"])
    panel_win.refresh()

    stdscr.getch()

    # Menu
    menu_items = [
        MenuItem("Show info",    "info",    "i"),
        MenuItem("Show progress","progress","p"),
        MenuItem("Quit",         "quit",    "q"),
    ]
    choice = popup_menu(stdscr, menu_items, title=" Demo Menu ")

    stdscr.erase()
    if choice == "progress":
        center_text(stdscr, 1, "Running progress demo...", curses.A_BOLD)
        bar = ProgressBar(stdscr, y=3, x=4, width=min(50, w - 10))
        for i in range(101):
            bar.update(i / 100, f"step {i}")
            time.sleep(0.01)
        safe_addstr(stdscr, 5, 4, "Done! Press any key.", colors["success"])
    else:
        center_text(stdscr, h // 2, f"Choice: {choice!r}  Press any key to exit", curses.A_BOLD)

    stdscr.getch()


if __name__ == "__main__":
    import os
    import sys

    print("=== curses demo ===")
    if not sys.stdout.isatty() or os.environ.get("CI"):
        print("  (skipping interactive demo: not a TTY)")
        print("  To run interactively: python -c "
              '"import cursesutil; cursesutil.run(cursesutil._demo_main)"')
    else:
        run(_demo_main)
        print("Demo complete.")

For the rich alternative — rich (PyPI) provides high-level terminal rendering (tables, syntax highlighting, progress bars, panels, markdown, tracebacks) using ANSI escape codes without the low-level ncurses session model; it works on any terminal without calling initscr or managing a curses event loop — use rich for styled output during normal script execution (logging, reports, progress); use curses when you need full-screen interactive applications with cursor positioning, keyboard event loops, window layers, or any UI where the entire terminal is taken over and text is repositioned arbitrarily. For the urwid alternative — urwid (PyPI) wraps curses in a widget toolkit with layouts, list boxes, edit widgets, and signal-based event handling similar to GUI toolkits; it handles terminal resize, palette management, and widget focus automatically — use urwid for complex TUIs with forms, scroll views, and multi-widget layouts where writing raw curses primitives would be tedious; use curses directly when your TUI is simple enough to not need a widget framework, or when you want zero third-party dependencies. The Claude Skills 360 bundle includes curses skill sets covering run()/curses_session() safe initialization, setup_colors() palette builder, safe_addstr()/center_text()/draw_box_title()/fill_row() drawing primitives, MenuItem/popup_menu() interactive selection menu, and ProgressBar terminal progress widget. Start with the free tier to try terminal UI patterns and curses pipeline code generation.

Keep Reading

AI

Claude Code for email.contentmanager: Python Email Content Accessors

Read and write EmailMessage body content with Python's email.contentmanager module and Claude Code — email contentmanager ContentManager for the class that maps content types to get and set handler functions allowing EmailMessage to support get_content and set_content with type-specific behaviour, email contentmanager raw_data_manager for the ContentManager instance that handles raw bytes and str payloads without any conversion, email contentmanager content_manager for the standard ContentManager instance used by email.policy.default that intelligently handles text plain text html multipart and binary content types, email contentmanager get_content_text for the handler that returns the decoded text payload of a text-star message part as a str, email contentmanager get_content_binary for the handler that returns the raw decoded bytes payload of a non-text message part, email contentmanager get_data_manager for the get-handler lookup used by EmailMessage get_content to find the right reader function for the content type, email contentmanager set_content text for the handler that creates and sets a text part correctly choosing charset and transfer encoding, email contentmanager set_content bytes for the handler that creates and sets a binary part with base64 encoding and optional filename Content-Disposition, email contentmanager EmailMessage get_content for the method that reads the message body using the registered content manager handlers, email contentmanager EmailMessage set_content for the method that sets the message body and MIME headers in one call, email contentmanager EmailMessage make_alternative make_mixed make_related for the methods that convert a simple message into a multipart container, email contentmanager EmailMessage add_attachment for the method that attaches a file or bytes to a multipart message, and email contentmanager integration with email.message and email.policy and email.mime and io for building high-level email readers attachment extractors text body accessors HTML readers and policy-aware MIME construction pipelines.

5 min read Feb 12, 2029
AI

Claude Code for email.charset: Python Email Charset Encoding

Control header and body encoding for international email with Python's email.charset module and Claude Code — email charset Charset for the class that wraps a character set name with the encoding rules for header encoding and body encoding describing how to encode text for that charset in email messages, email charset Charset header_encoding for the attribute specifying whether headers using this charset should use QP quoted-printable encoding BASE64 encoding or no encoding, email charset Charset body_encoding for the attribute specifying the Content-Transfer-Encoding to use for message bodies in this charset such as QP or BASE64, email charset Charset output_codec for the attribute giving the Python codec name used to encode the string to bytes for the wire format, email charset Charset input_codec for the attribute giving the Python codec name used to decode incoming bytes to str, email charset Charset get_output_charset for returning the output charset name, email charset Charset header_encode for encoding a header string using the charset's header_encoding method, email charset Charset body_encode for encoding body content using the charset's body_encoding, email charset Charset convert for converting a string from the input_codec to the output_codec, email charset add_charset for registering a new charset with custom encoding rules in the global charset registry, email charset add_alias for adding an alias name that maps to an existing registered charset, email charset add_codec for registering a codec name mapping for use by the charset machinery, and email charset integration with email.message and email.mime and email.policy and email.encoders for building international email senders non-ASCII header encoders Content-Transfer-Encoding selectors charset-aware message constructors and MIME encoding pipelines.

5 min read Feb 11, 2029
AI

Claude Code for email.utils: Python Email Address and Header Utilities

Parse and format RFC 2822 email addresses and dates with Python's email.utils module and Claude Code — email utils parseaddr for splitting a display-name plus angle-bracket address string into a realname and email address tuple, email utils formataddr for combining a realname and address string into a properly quoted RFC 2822 address with angle brackets, email utils getaddresses for parsing a list of raw address header strings each potentially containing multiple comma-separated addresses into a list of realname address tuples, email utils parsedate for parsing an RFC 2822 date string into a nine-tuple compatible with time.mktime, email utils parsedate_tz for parsing an RFC 2822 date string into a ten-tuple that includes the UTC offset timezone in seconds, email utils parsedate_to_datetime for parsing an RFC 2822 date string into an aware datetime object with timezone, email utils formatdate for formatting a POSIX timestamp or the current time as an RFC 2822 date string with optional usegmt and localtime flags, email utils format_datetime for formatting a datetime object as an RFC 2822 date string, email utils make_msgid for generating a globally unique Message-ID string with optional idstring and domain components, email utils decode_rfc2231 for decoding an RFC 2231 encoded parameter value into a tuple of charset language and value, email utils encode_rfc2231 for encoding a string as an RFC 2231 encoded parameter value, email utils collapse_rfc2231_value for collapsing a decoded RFC 2231 tuple to a Unicode string, and email utils integration with email.message and email.headerregistry and datetime and time for building address parsers date formatters message-id generators header extractors and RFC-compliant email construction utilities.

5 min read Feb 10, 2029

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free