Python’s tty module provides two convenience functions for switching a terminal file descriptor into raw or cbreak mode using termios under the hood. import tty. setraw: tty.setraw(fd, when=termios.TCSAFLUSH) — raw mode: disables ICANON (line buffering), ECHO, ISIG (Ctrl+C/Z), IXON (flow control), clears most input/output processing, sets VMIN=1, VTIME=0; every keystroke arrives immediately without echo. setcbreak: tty.setcbreak(fd, when=termios.TCSAFLUSH) — cbreak mode: disables ICANON and ECHO but keeps ISIG active (Ctrl+C still raises SIGINT); characters arrive one at a time. Both accept an optional when argument: termios.TCSANOW, TCSADRAIN, or TCSAFLUSH (default). Always save attributes first: old = termios.tcgetattr(fd) then termios.tcsetattr(fd, termios.TCSAFLUSH, old) in a finally block; tty does not restore for you. fd is typically sys.stdin.fileno(). Windows: not available — use msvcrt.getwch() instead. Claude Code generates single-keypress readers, arrow-key decoders, interactive menus, and game input handlers.
CLAUDE.md for tty
## tty Stack
- Stdlib: import tty, termios, sys, os
- Raw: old = termios.tcgetattr(sys.stdin.fileno())
try:
tty.setraw(sys.stdin.fileno())
ch = sys.stdin.buffer.read(1) # immediate, no echo
finally:
termios.tcsetattr(fd, termios.TCSAFLUSH, old)
- Cbreak: tty.setcbreak(fd) # same but Ctrl+C still works
- Note: tty only switches mode — YOU must save/restore with termios
tty Terminal Mode Pipeline
# app/ttyutil.py — mode switch, getch, arrow keys, menu, game input, msvcrt fallback
from __future__ import annotations
import contextlib
import os
import platform
import sys
from contextlib import contextmanager
from dataclasses import dataclass
from typing import Callable, Generator
_UNIX = platform.system() != "Windows"
if _UNIX:
import termios
import tty
else:
import msvcrt # type: ignore[import]
# ─────────────────────────────────────────────────────────────────────────────
# 1. Cross-platform getch
# ─────────────────────────────────────────────────────────────────────────────
def getch() -> str:
"""
Read a single character immediately without echoing or line buffering.
Works on Unix (via tty.setraw) and Windows (via msvcrt.getwch).
Example:
ch = getch()
print(f"pressed: {ch!r}")
"""
if not _UNIX:
return msvcrt.getwch()
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
try:
tty.setraw(fd)
ch = sys.stdin.buffer.read(1)
return ch.decode("utf-8", errors="replace")
finally:
termios.tcsetattr(fd, termios.TCSAFLUSH, old)
def getche() -> str:
"""Read a single character and echo it."""
ch = getch()
sys.stdout.write(ch)
sys.stdout.flush()
return ch
def read_raw_bytes(n: int = 1) -> bytes:
"""
Read n raw bytes from stdin in raw mode.
Useful for reading escape sequences and multi-byte keys.
Example:
data = read_raw_bytes(3) # read up to 3 bytes
"""
if not _UNIX:
return msvcrt.getch() # type: ignore[return-value]
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
try:
tty.setraw(fd)
return sys.stdin.buffer.read(n)
finally:
termios.tcsetattr(fd, termios.TCSAFLUSH, old)
# ─────────────────────────────────────────────────────────────────────────────
# 2. Context managers
# ─────────────────────────────────────────────────────────────────────────────
@contextmanager
def raw_mode(fd: int | None = None) -> Generator[None, None, None]:
"""
Context manager: put terminal into raw mode, restore on exit.
Example:
with raw_mode():
while (ch := sys.stdin.buffer.read(1)) != b"q":
print(repr(ch))
"""
if not _UNIX:
yield
return
real_fd = fd if fd is not None else sys.stdin.fileno()
if not os.isatty(real_fd):
yield
return
old = termios.tcgetattr(real_fd)
try:
tty.setraw(real_fd)
yield
finally:
termios.tcsetattr(real_fd, termios.TCSAFLUSH, old)
@contextmanager
def cbreak_mode(fd: int | None = None) -> Generator[None, None, None]:
"""
Context manager: put terminal into cbreak mode, restore on exit.
Signals (Ctrl+C) still work in cbreak mode.
Example:
with cbreak_mode():
ch = sys.stdin.buffer.read(1)
"""
if not _UNIX:
yield
return
real_fd = fd if fd is not None else sys.stdin.fileno()
if not os.isatty(real_fd):
yield
return
old = termios.tcgetattr(real_fd)
try:
tty.setcbreak(real_fd)
yield
finally:
termios.tcsetattr(real_fd, termios.TCSAFLUSH, old)
# ─────────────────────────────────────────────────────────────────────────────
# 3. Key decoder
# ─────────────────────────────────────────────────────────────────────────────
# Escape sequence → human-readable key name
_ESCAPE_SEQUENCES: dict[bytes, str] = {
b"\x1b[A": "UP",
b"\x1b[B": "DOWN",
b"\x1b[C": "RIGHT",
b"\x1b[D": "LEFT",
b"\x1b[H": "HOME",
b"\x1b[F": "END",
b"\x1b[2~": "INSERT",
b"\x1b[3~": "DELETE",
b"\x1b[5~": "PAGE_UP",
b"\x1b[6~": "PAGE_DOWN",
b"\x1bOP": "F1",
b"\x1bOQ": "F2",
b"\x1bOR": "F3",
b"\x1bOS": "F4",
b"\x1b[15~": "F5",
b"\x1b[17~": "F6",
b"\x1b[18~": "F7",
b"\x1b[19~": "F8",
b"\x1b[20~": "F9",
b"\x1b[21~": "F10",
b"\x1b[23~": "F11",
b"\x1b[24~": "F12",
}
# Control characters
_CTRL_NAMES: dict[bytes, str] = {
b"\x01": "CTRL+A", b"\x02": "CTRL+B", b"\x03": "CTRL+C",
b"\x04": "CTRL+D", b"\x05": "CTRL+E", b"\x06": "CTRL+F",
b"\x08": "BACKSPACE", b"\x09": "TAB", b"\x0a": "ENTER",
b"\x0d": "ENTER", b"\x1b": "ESC", b"\x7f": "BACKSPACE",
}
@dataclass
class KeyEvent:
raw: bytes
name: str
char: str # printable char or ""
is_escape: bool
is_ctrl: bool
def __str__(self) -> str:
return f"KeyEvent({self.name!r} raw={self.raw!r})"
def read_key() -> KeyEvent:
"""
Read a key event, decoding escape sequences for arrow/function keys.
Returns a KeyEvent with the key name.
Example:
ev = read_key()
if ev.name == "UP":
...
elif ev.name == "ENTER":
...
"""
if not _UNIX:
# Windows: read via msvcrt
ch = msvcrt.getch()
if ch in (b"\x00", b"\xe0"):
ch2 = msvcrt.getch()
raw = ch + ch2
name = {
b"\xe0H": "UP", b"\xe0P": "DOWN",
b"\xe0K": "LEFT", b"\xe0M": "RIGHT",
b"\xe0G": "HOME", b"\xe0O": "END",
b"\xe0Q": "PAGE_DOWN", b"\xe0I": "PAGE_UP",
b"\xe0S": "DELETE",
}.get(raw, f"SPECIAL({raw.hex()})")
return KeyEvent(raw=raw, name=name, char="", is_escape=True, is_ctrl=False)
raw = ch
name = _CTRL_NAMES.get(raw, raw.decode("utf-8", errors="replace"))
is_ctrl = raw[0] < 32 or raw[0] == 127
return KeyEvent(raw=raw, name=name, char=name if len(name) == 1 else "", is_escape=False, is_ctrl=is_ctrl)
import select as _select
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
try:
tty.setraw(fd)
first = os.read(fd, 1)
if first == b"\x1b":
r, _, _ = _select.select([fd], [], [], 0.05)
if r:
rest = os.read(fd, 8)
raw = first + rest
else:
raw = first
else:
raw = first
finally:
termios.tcsetattr(fd, termios.TCSAFLUSH, old)
if raw in _ESCAPE_SEQUENCES:
name = _ESCAPE_SEQUENCES[raw]
return KeyEvent(raw=raw, name=name, char="", is_escape=True, is_ctrl=False)
if raw in _CTRL_NAMES:
name = _CTRL_NAMES[raw]
return KeyEvent(raw=raw, name=name, char="", is_escape=raw == b"\x1b", is_ctrl=True)
# Printable
char = raw.decode("utf-8", errors="replace")
return KeyEvent(raw=raw, name=char, char=char, is_escape=False, is_ctrl=False)
# ─────────────────────────────────────────────────────────────────────────────
# 4. Simple interactive menu
# ─────────────────────────────────────────────────────────────────────────────
def select_menu(
options: list[str],
title: str = "Select:",
selected_marker: str = "> ",
unselected_marker: str = " ",
) -> int | None:
"""
Display an interactive arrow-key selection menu.
Returns the index of the selected option, or None if Ctrl+C/ESC.
Only works when attached to a real tty.
Example:
choice = select_menu(["Option A", "Option B", "Option C"])
if choice is not None:
print(f"You chose: {options[choice]}")
"""
if not os.isatty(sys.stdout.fileno()):
print(title)
for i, opt in enumerate(options):
print(f" {i+1}. {opt}")
try:
val = int(input("Enter number: ")) - 1
return val if 0 <= val < len(options) else None
except (ValueError, EOFError):
return None
current = 0
n = len(options)
def _render() -> None:
# Move cursor up to overwrite previous render
sys.stdout.write(f"\033[{n}A\033[J")
for i, opt in enumerate(options):
marker = selected_marker if i == current else unselected_marker
if i == current:
line = f"\033[7m{marker}{opt}\033[0m" # reverse video
else:
line = f"{marker}{opt}"
sys.stdout.write(line + "\n")
sys.stdout.flush()
print(title)
for i, opt in enumerate(options):
marker = selected_marker if i == current else unselected_marker
print(f"{marker}{opt}")
sys.stdout.flush()
with cbreak_mode():
while True:
ev = read_key()
if ev.name == "UP":
current = (current - 1) % n
elif ev.name == "DOWN":
current = (current + 1) % n
elif ev.name in ("ENTER", "\r", "\n"):
sys.stdout.write("\n")
return current
elif ev.name in ("ESC", "CTRL+C"):
sys.stdout.write("\n")
return None
_render()
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== tty demo ===")
if not os.isatty(sys.stdin.fileno()):
print(" (stdin is not a tty — running non-interactive demo only)")
# Demonstrate that the module loads and mode functions exist
print("\n--- module functions available ---")
fns = ["getch", "getche", "read_raw_bytes", "raw_mode",
"cbreak_mode", "read_key", "select_menu"]
for fn in fns:
exists = fn in dir(sys.modules[__name__])
# Access from current module globals
import sys as _sys
import importlib
mod = importlib.import_module(__name__)
exists = hasattr(mod, fn)
print(f" {fn}: defined={exists}")
print("\n--- key decoder table (escape sequences) ---")
for seq, name in list(_ESCAPE_SEQUENCES.items())[:8]:
print(f" {seq!r:20s}: {name}")
raise SystemExit(0)
print("\n--- getch (press any key) ---")
sys.stdout.write(" Press any key: ")
sys.stdout.flush()
ch = getch()
print(f" got {ch!r}")
print("\n--- read_key (press a key, try arrow keys) ---")
sys.stdout.write(" Press a key (including arrows): ")
sys.stdout.flush()
ev = read_key()
print(f" {ev}")
print("\n--- select_menu ---")
choice = select_menu(
["Python tutorial", "stdlib deep dive", "Cancel"],
title="Choose an action:",
)
if choice is not None:
print(f" Selected: index={choice}")
else:
print(" Cancelled")
print("\n=== done ===")
For the termios alternative — termios is the low-level module that tty wraps; termios.tcgetattr() and termios.tcsetattr() give access to all terminal flags individually (ICANON, ECHO, ISIG, IXON, baud rates, VMIN, VTIME) — use termios directly when you need fine-grained flag manipulation beyond what tty.setraw() and tty.setcbreak() provide, such as enabling signals in raw mode, setting a specific read timeout, or configuring a serial port. For the curses alternative — curses.wrapper(main) manages raw mode, keypad initialization, and restoration automatically; stdscr.getch() returns integer key codes including curses.KEY_UP, curses.KEY_F1, etc. — use curses when building a full-screen TUI with panels, colors, and window management; use tty + read_key() when you only need keyboard input handling without a full screen context, such as in a scrolling log viewer, a pause-play controller, or a prompt with arrow-key history. The Claude Skills 360 bundle includes tty skill sets covering cross-platform getch()/getche()/read_raw_bytes(), raw_mode()/cbreak_mode() context managers, KeyEvent with read_key() escape-sequence decoder (arrows, F-keys, Ctrl+ chords), and select_menu() arrow-key interactive menu. Start with the free tier to try terminal mode patterns and tty pipeline code generation.