Claude Code for tty: Python Terminal Mode Switching — Claude Skills 360 Blog
Blog / AI / Claude Code for tty: Python Terminal Mode Switching
AI

Claude Code for tty: Python Terminal Mode Switching

Published: October 20, 2028
Read time: 5 min read
By: Claude Skills 360

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.

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