Python’s curses.textpad module provides a simple interactive text-editing widget for curses TUI applications. from curses.textpad import Textbox, rectangle. Textbox(win, insert_mode=False) — wraps a curses window as an editable text field; insert_mode=True enables character-insert mode (otherwise overwrite). box.edit(validator=None) — enter the interactive editing loop; the optional validator callable receives each keystroke and returns the (possibly modified) key to process; the loop ends when the validator returns a non-truthy value or the user presses Ctrl+G (default terminator). box.gather() — return the buffer contents as a str, stripping trailing spaces per line if box.stripspaces is True (default). box.do_command(ch) — dispatch a single character command; useful for programmatic input or testing. rectangle(win, uly, ulx, lry, lrx) — draw a rectangular border using ACS_* box-drawing characters. Default keybindings: Ctrl+A = start-of-line; Ctrl+B = left; Ctrl+D = delete char; Ctrl+E = end-of-line; Ctrl+F = right; Ctrl+G = terminate; Ctrl+H = backspace; Ctrl+J = newline/terminate; Ctrl+K = kill-to-EOL; Ctrl+L = refresh; Ctrl+N = down; Ctrl+O = insert blank line; Ctrl+P = up. Claude Code generates terminal form fields, inline editors, search bars, confirmation dialogs, multi-line text editors, and interactive TUI input pipelines.
CLAUDE.md for curses.textpad
## curses.textpad Stack
- Stdlib: from curses.textpad import Textbox, rectangle
- Create: box = Textbox(win, insert_mode=False)
- Edit: text = box.edit(validator) # blocks; Ctrl+G to end
- Gather: text = box.gather() # returns buffer as str
- Frame: rectangle(stdscr, y1, x1, y2, x2) # border around field
- Keys: Ctrl+G = done; Ctrl+H/^? = backspace; Ctrl+A/E = home/end
- Ctrl+B/F = left/right; Ctrl+N/P = down/up
- Ctrl+D = del char; Ctrl+K = kill to EOL
- validator(ch) → int: return 0/False to stop, return ch to process normally
curses.textpad Terminal Input Pipeline
# app/cursestextpadutil.py — single-line, multi-line, validator, search, form
from __future__ import annotations
import curses
import curses.ascii
from curses.textpad import Textbox, rectangle
from dataclasses import dataclass, field
from typing import Any, Callable
# ─────────────────────────────────────────────────────────────────────────────
# 1. Validator factories
# ─────────────────────────────────────────────────────────────────────────────
def make_enter_validator() -> Callable[[int], int]:
"""
Return a validator that accepts Enter (Ctrl+J or Ctrl+M) to finish editing.
By default Textbox uses Ctrl+G; this adds Enter as a terminator.
Example:
box = Textbox(win)
text = box.edit(make_enter_validator())
"""
def validator(ch: int) -> int:
if ch in (curses.ascii.ctrl(ord('J')), # Ctrl+J = \n
curses.ascii.ctrl(ord('M'))): # Ctrl+M = \r
return curses.ascii.BEL # BEL = Ctrl+G terminates
return ch
return validator
def make_maxlen_validator(maxlen: int) -> Callable[[int], int]:
"""
Return a validator that prevents input beyond maxlen printable characters.
Example:
box = Textbox(win)
text = box.edit(make_maxlen_validator(20))
"""
_count: list[int] = [0]
def validator(ch: int) -> int:
if curses.ascii.isprint(ch):
if _count[0] >= maxlen:
return 0 # ignore — suppress the character
_count[0] += 1
elif ch in (curses.KEY_BACKSPACE if hasattr(curses, 'KEY_BACKSPACE') else 263,
curses.ascii.ctrl(ord('H'))): # backspace
_count[0] = max(0, _count[0] - 1)
return ch
return validator
def make_numeric_validator() -> Callable[[int], int]:
"""
Return a validator that only allows digit characters and backspace/delete.
Example:
box = Textbox(win)
amount_str = box.edit(make_numeric_validator())
"""
def validator(ch: int) -> int:
if curses.ascii.isdigit(ch):
return ch
if ch in (curses.ascii.ctrl(ord('H')), # Ctrl+H = backspace
curses.ascii.ctrl(ord('G')), # terminator
curses.ascii.ctrl(ord('J')), # newline
curses.ascii.ctrl(ord('A')), # home
curses.ascii.ctrl(ord('E')), # end
curses.ascii.ctrl(ord('B')), # left
curses.ascii.ctrl(ord('F'))): # right
return ch
return 0 # swallow non-digit printable chars
return validator
# ─────────────────────────────────────────────────────────────────────────────
# 2. Framed input field helpers
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class InputField:
label: str
y: int # top-left row of the frame
x: int # top-left column of the frame
width: int # inner editing width
height: int = 1 # inner editing height (1 = single line)
def draw_field(stdscr: Any, field: InputField) -> Textbox:
"""
Draw a labelled, framed input field on stdscr and return a Textbox.
Framing adds 2 to width and height (border).
Example:
box = draw_field(stdscr, InputField("Name:", 2, 2, 30))
text = box.edit(make_enter_validator()).strip()
"""
# Label
stdscr.addstr(field.y, field.x, field.label)
label_len = len(field.label) + 1
# Rectangle border
uly = field.y
ulx = field.x + label_len
lry = field.y + field.height + 1
lrx = field.x + label_len + field.width + 1
rectangle(stdscr, uly, ulx, lry, lrx)
stdscr.refresh()
# Inner editing window
edit_win = curses.newwin(field.height, field.width,
field.y + 1, field.x + label_len + 1)
return Textbox(edit_win, insert_mode=True)
# ─────────────────────────────────────────────────────────────────────────────
# 3. Simple prompted input (wraps curses.wrapper)
# ─────────────────────────────────────────────────────────────────────────────
def prompt_input(prompt: str,
width: int = 40,
validator: "Callable[[int], int] | None" = None,
y: int = 2,
x: int = 2) -> str:
"""
Display a prompted input field in a curses session and return the entered text.
Blocks until the user finishes (Ctrl+G or Enter via enter_validator).
Example:
name = prompt_input("Enter your name: ", width=30)
print(f"Hello, {name}!")
"""
result: list[str] = [""]
def _inner(stdscr: Any) -> None:
curses.curs_set(1)
stdscr.clear()
field = InputField(label=prompt, y=y, x=x, width=width)
box = draw_field(stdscr, field)
val = validator or make_enter_validator()
raw = box.edit(val)
result[0] = raw.strip()
curses.wrapper(_inner)
return result[0]
# ─────────────────────────────────────────────────────────────────────────────
# 4. Multi-field form
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class FormField:
name: str
label: str
width: int = 30
numeric: bool = False
maxlen: int = 0 # 0 = unlimited
def run_form(fields: list[FormField]) -> dict[str, str]:
"""
Display a multi-field curses form and return a dict of field_name → entered_text.
Each field is stacked vertically. Press Enter to advance between fields.
Example:
data = run_form([
FormField("name", "Full name:", width=30),
FormField("age", "Age: ", width=5, numeric=True, maxlen=3),
FormField("city", "City: ", width=20),
])
print(data)
"""
results: dict[str, str] = {}
def _inner(stdscr: Any) -> None:
curses.curs_set(1)
stdscr.clear()
y = 1
for f in fields:
field_obj = InputField(label=f.label, y=y, x=2, width=f.width)
box = draw_field(stdscr, field_obj)
if f.numeric:
val_fn: Callable[[int], int] = make_numeric_validator()
elif f.maxlen > 0:
val_fn = make_maxlen_validator(f.maxlen)
else:
val_fn = make_enter_validator()
raw = box.edit(val_fn)
results[f.name] = raw.strip()
y += 3 # spacing between fields
curses.wrapper(_inner)
return results
# ─────────────────────────────────────────────────────────────────────────────
# 5. Programmatic Textbox testing helper
# ─────────────────────────────────────────────────────────────────────────────
def simulate_input(text: str,
win_height: int = 3,
win_width: int = 40) -> str:
"""
Simulate typing text into a Textbox and return the gathered result.
Uses do_command() to inject keystrokes without a live terminal.
Useful for unit-testing Textbox behaviour.
Example:
result = simulate_input("hello world")
assert result.strip() == "hello world"
"""
result: list[str] = [""]
def _inner(stdscr: Any) -> None:
win = curses.newwin(win_height, win_width, 0, 0)
box = Textbox(win, insert_mode=True)
for ch in text:
box.do_command(ord(ch))
# Terminate
box.do_command(curses.ascii.BEL)
result[0] = box.gather()
curses.wrapper(_inner)
return result[0]
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== curses.textpad demo ===")
print()
print("This module requires a live terminal. Run the examples below")
print("in an interactive Python session or a real terminal:")
print()
print(" # 1. Single prompt (Enter to confirm):")
print(" from cursestextpadutil import prompt_input")
print(" name = prompt_input('Your name: ', width=20)")
print(" print(f'Hello, {name}!')")
print()
print(" # 2. Multi-field form:")
print(" from cursestextpadutil import run_form, FormField")
print(" data = run_form([")
print(" FormField('name', 'Full name:', width=30),")
print(" FormField('age', 'Age: ', width=5, numeric=True, maxlen=3),")
print(" ])")
print(" print(data)")
print()
print(" # 3. Simulate input (no terminal needed):")
print(" from cursestextpadutil import simulate_input")
print(" result = simulate_input('hello world')")
print(" print(repr(result.strip()))")
print()
# ── Demonstrate validators without a terminal ─────────────────────────
print("--- validator demo (no terminal) ---")
# max-length validator: characters beyond limit return 0
maxlen_val = make_maxlen_validator(5)
print(" maxlen_val (5 chars):")
for ch in "hello world":
out = maxlen_val(ord(ch))
print(f" input={ch!r:3s} output={out:3d} {'accepted' if out else 'rejected'}")
# numeric validator: non-digits rejected
numeric_val = make_numeric_validator()
print("\n numeric_val:")
for ch in "12a3b4":
out = numeric_val(ord(ch))
print(f" input={ch!r:3s} output={out:3d} {'accepted' if out else 'rejected'}")
print("\n=== done ===")
For the urwid (PyPI) alternative — urwid.Edit(caption="Name: ") and urwid.IntEdit() provide richer text-input widgets with Unicode support, focus handling, and a signal system in the urwid TUI framework — use urwid for full-featured TUI applications with multiple widgets, focus traversal, scrolling lists, and pop-up dialogs; use curses.textpad.Textbox for lightweight, zero-dependency text input fields embedded in curses applications or scripts. For the prompt_toolkit (PyPI) alternative — prompt_toolkit.prompt("Enter name: ") provides readline-like input with history, auto-completion, syntax highlighting, and multi-line editing in both synchronous and asyncio contexts — use prompt_toolkit for REPL-style input at the command line; use curses.textpad for form-style input embedded inside a full-screen curses TUI. The Claude Skills 360 bundle includes curses.textpad skill sets covering make_enter_validator()/make_maxlen_validator()/make_numeric_validator() validator factories, InputField/draw_field() framed field builder, prompt_input() single-prompt helper, FormField/run_form() multi-field form runner, and simulate_input() programmatic testing helper. Start with the free tier to try terminal input patterns and curses.textpad pipeline code generation.