Claude Code for termios: Python Terminal Attribute Control — Claude Skills 360 Blog
Blog / AI / Claude Code for termios: Python Terminal Attribute Control
AI

Claude Code for termios: Python Terminal Attribute Control

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

Python’s termios module gives direct access to Unix terminal settings — enabling raw mode, disabling echo, changing baud rates, and controlling the line discipline. import termios. tcgetattr: attrs = termios.tcgetattr(fd) → 7-element list [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]. tcsetattr: termios.tcsetattr(fd, when, attrs)when: TCSANOW (immediate), TCSADRAIN (after output), TCSAFLUSH (after output + discard input). Key flags in lflag (index 3): ECHO (echo input), ECHONL (echo newline), ICANON (line buffering — disabling gives char-at-a-time), ISIG (enable Ctrl+C/Ctrl+Z signals), IEXTEN. In iflag (0): IXON (Ctrl+S/Q flow control), IXOFF, ICRNL (CR→NL). cc (index 6) is a list of control chars: cc[VMIN] = min chars to read in raw mode, cc[VTIME] = read timeout in tenths of seconds. cfgetispeed/cfsetispeed: termios.cfgetispeed(attrs), termios.cfsetispeed(attrs, B9600). Speed constants: B9600, B115200, etc. tcsendbreak: termios.tcsendbreak(fd, 0). tcflush: termios.tcflush(fd, TCIOFLUSH). Claude Code generates raw-mode input handlers, password readers, terminal state save/restore, and serial port configurators.

CLAUDE.md for termios

## termios Stack
- Stdlib: import termios, sys
- Get:    attrs = termios.tcgetattr(sys.stdin.fileno())
- Raw:    new = list(attrs); new[3] &= ~(termios.ICANON | termios.ECHO)
          termios.tcsetattr(fd, termios.TCSANOW, new)
- Restore: termios.tcsetattr(fd, termios.TCSAFLUSH, attrs)
- Pattern: old = termios.tcgetattr(fd)
           try: ...raw mode...
           finally: termios.tcsetattr(fd, termios.TCSAFLUSH, old)
- Speed:  termios.cfgetospeed(attrs)  # current baud rate constant

termios Terminal Control Pipeline

# app/termiosutil.py — save/restore, raw, cbreak, echo, char-read, serial, password
from __future__ import annotations

import contextlib
import os
import platform
import sys
from contextlib import contextmanager
from dataclasses import dataclass
from typing import Generator

_TERMIOS_AVAILABLE = platform.system() != "Windows"
if _TERMIOS_AVAILABLE:
    import termios
    import tty


# ─────────────────────────────────────────────────────────────────────────────
# 1. Attribute save / restore
# ─────────────────────────────────────────────────────────────────────────────

def get_attrs(fd: int) -> list:
    """
    Read current terminal attributes.

    Example:
        attrs = get_attrs(sys.stdin.fileno())
    """
    if not _TERMIOS_AVAILABLE:
        raise OSError("termios not available on Windows")
    return termios.tcgetattr(fd)


def set_attrs(fd: int, attrs: list, when: int | None = None) -> None:
    """
    Apply terminal attributes.
    when: termios.TCSANOW (default), TCSADRAIN, or TCSAFLUSH.

    Example:
        set_attrs(fd, saved_attrs)
    """
    if when is None:
        when = termios.TCSAFLUSH
    termios.tcsetattr(fd, when, attrs)


@contextmanager
def save_restore(fd: int) -> Generator[list, None, None]:
    """
    Context manager that saves and restores terminal attributes.

    Example:
        with save_restore(sys.stdin.fileno()) as attrs:
            # modify attrs and apply
            ...
    """
    if not _TERMIOS_AVAILABLE:
        yield []
        return
    saved = termios.tcgetattr(fd)
    try:
        yield saved
    finally:
        termios.tcsetattr(fd, termios.TCSAFLUSH, saved)


# ─────────────────────────────────────────────────────────────────────────────
# 2. Terminal mode context managers
# ─────────────────────────────────────────────────────────────────────────────

@contextmanager
def raw_mode(fd: int) -> Generator[None, None, None]:
    """
    Put the terminal in raw mode: no echo, no line buffering, char-at-a-time.
    Restores original settings on exit.

    Example:
        with raw_mode(sys.stdin.fileno()):
            ch = sys.stdin.read(1)   # returns immediately on each keypress
    """
    with save_restore(fd):
        tty.setraw(fd)
        yield


@contextmanager
def cbreak_mode(fd: int) -> Generator[None, None, None]:
    """
    Put the terminal in cbreak mode: no line buffering but signals still work.
    Ctrl+C still raises KeyboardInterrupt; no echo.

    Example:
        with cbreak_mode(sys.stdin.fileno()):
            ch = sys.stdin.buffer.read(1)
    """
    with save_restore(fd):
        tty.setcbreak(fd)
        yield


@contextmanager
def no_echo(fd: int) -> Generator[None, None, None]:
    """
    Disable echo while keeping line discipline active.
    Useful for password prompts.

    Example:
        with no_echo(sys.stdin.fileno()):
            password = input("Password: ")
    """
    if not _TERMIOS_AVAILABLE:
        yield
        return
    with save_restore(fd):
        attrs = termios.tcgetattr(fd)
        attrs[3] &= ~termios.ECHO
        termios.tcsetattr(fd, termios.TCSANOW, attrs)
        yield


# ─────────────────────────────────────────────────────────────────────────────
# 3. Character-level input readers
# ─────────────────────────────────────────────────────────────────────────────

def read_char(fd: int | None = None) -> str:
    """
    Read a single character from the terminal without waiting for Enter.
    Returns the character string.

    Example:
        ch = read_char()
        print(f"You pressed: {ch!r}")
    """
    if fd is None:
        fd = sys.stdin.fileno()
    with raw_mode(fd):
        return sys.stdin.read(1)


def read_key(fd: int | None = None) -> bytes:
    """
    Read a key including multi-byte escape sequences (arrow keys, F-keys).
    Returns raw bytes.

    Example:
        key = read_key()
        if key == b"\\x1b[A": print("Up arrow")
    """
    if fd is None:
        fd = sys.stdin.fileno()

    import select as _select
    with raw_mode(fd):
        first = os.read(fd, 1)
        if first == b"\x1b":
            # Check for more bytes (escape sequence)
            r, _, _ = _select.select([fd], [], [], 0.05)
            if r:
                rest = os.read(fd, 8)
                return first + rest
        return first


def prompt_password(prompt: str = "Password: ", fd: int | None = None) -> str:
    """
    Prompt for a password without echoing input.

    Example:
        pwd = prompt_password("Enter password: ")
    """
    if not _TERMIOS_AVAILABLE:
        import getpass
        return getpass.getpass(prompt)

    real_fd = fd if fd is not None else sys.stdin.fileno()
    with no_echo(real_fd):
        sys.stdout.write(prompt)
        sys.stdout.flush()
        try:
            value = sys.stdin.readline().rstrip("\n")
        finally:
            sys.stdout.write("\n")
            sys.stdout.flush()
    return value


# ─────────────────────────────────────────────────────────────────────────────
# 4. Terminal attribute inspector
# ─────────────────────────────────────────────────────────────────────────────

@dataclass
class TerminalInfo:
    is_tty:      bool
    echo:        bool
    canonical:   bool   # line buffering (ICANON)
    signals:     bool   # Ctrl+C enabled (ISIG)
    flow_ctrl:   bool   # Ctrl+S/Q (IXON)
    input_speed:  int   # baud rate constant
    output_speed: int
    vmin:        int    # min chars (raw mode)
    vtime:       int    # read timeout (raw mode, tenths of seconds)

    def __str__(self) -> str:
        flags = []
        if self.echo:      flags.append("echo")
        if self.canonical: flags.append("canonical")
        if self.signals:   flags.append("signals")
        if self.flow_ctrl: flags.append("flow-ctrl")
        mode = ",".join(flags) if flags else "raw"
        return (f"tty={self.is_tty}  mode=[{mode}]  "
                f"ispeed={self.input_speed}  ospeed={self.output_speed}  "
                f"VMIN={self.vmin}  VTIME={self.vtime}")


def describe_terminal(fd: int | None = None) -> TerminalInfo:
    """
    Return a TerminalInfo snapshot of the current terminal state.

    Example:
        info = describe_terminal()
        print(info)
    """
    if fd is None:
        fd = sys.stdin.fileno()

    is_tty = os.isatty(fd)
    if not _TERMIOS_AVAILABLE or not is_tty:
        return TerminalInfo(is_tty=is_tty, echo=True, canonical=True,
                            signals=True, flow_ctrl=False,
                            input_speed=0, output_speed=0, vmin=1, vtime=0)

    attrs = termios.tcgetattr(fd)
    iflag, oflag, cflag, lflag, ispeed, ospeed, cc = attrs

    return TerminalInfo(
        is_tty=is_tty,
        echo=bool(lflag & termios.ECHO),
        canonical=bool(lflag & termios.ICANON),
        signals=bool(lflag & termios.ISIG),
        flow_ctrl=bool(iflag & termios.IXON),
        input_speed=ispeed,
        output_speed=ospeed,
        vmin=cc[termios.VMIN] if isinstance(cc[termios.VMIN], int) else ord(cc[termios.VMIN]),
        vtime=cc[termios.VTIME] if isinstance(cc[termios.VTIME], int) else ord(cc[termios.VTIME]),
    )


# ─────────────────────────────────────────────────────────────────────────────
# 5. Serial port configuration helper
# ─────────────────────────────────────────────────────────────────────────────

# Baud rate constant lookup
_BAUD_MAP: dict[int, int] = {}
if _TERMIOS_AVAILABLE:
    for _name in ["B50","B75","B110","B134","B150","B200","B300","B600",
                  "B1200","B1800","B2400","B4800","B9600","B19200","B38400",
                  "B57600","B115200","B230400"]:
        _val = getattr(termios, _name, None)
        if _val is not None:
            _BAUD_MAP[int(_name[1:])] = _val


def baud_constant(rate: int) -> int:
    """
    Look up the termios baud rate constant for an integer rate.
    Raises ValueError if not found.

    Example:
        B = baud_constant(115200)   # termios.B115200
    """
    if rate not in _BAUD_MAP:
        raise ValueError(f"Unknown baud rate: {rate}. Available: {sorted(_BAUD_MAP)}")
    return _BAUD_MAP[rate]


def configure_serial(
    fd: int,
    baud: int = 9600,
    bits: int = 8,
    parity: str = "N",
    stop_bits: int = 1,
    vmin: int = 1,
    vtime: int = 0,
) -> None:
    """
    Configure a file descriptor (e.g. /dev/ttyUSB0) for serial communication.

    baud: baud rate (e.g. 9600, 115200)
    bits: data bits (5, 6, 7, 8)
    parity: "N" (none), "E" (even), "O" (odd)
    stop_bits: 1 or 2
    vmin: VMIN — minimum characters to read
    vtime: VTIME — read timeout in tenths of seconds

    Example:
        with open("/dev/ttyUSB0", "rb+", buffering=0) as port:
            configure_serial(port.fileno(), baud=115200)
            port.write(b"AT\r\n")
            print(port.read(32))
    """
    if not _TERMIOS_AVAILABLE:
        raise OSError("termios not available on Windows")

    attrs = termios.tcgetattr(fd)
    iflag, oflag, cflag, lflag, ispeed, ospeed, cc = attrs

    # Input/output flags: raw mode
    iflag &= ~(termios.IGNBRK | termios.BRKINT | termios.PARMRK | termios.ISTRIP |
               termios.INLCR  | termios.IGNCR  | termios.ICRNL  | termios.IXON)
    oflag &= ~termios.OPOST
    lflag &= ~(termios.ECHO | termios.ECHONL | termios.ICANON | termios.ISIG | termios.IEXTEN)
    cflag &= ~(termios.CSIZE | termios.PARENB | termios.CSTOPB)

    # Data bits
    cflag |= {5: termios.CS5, 6: termios.CS6, 7: termios.CS7, 8: termios.CS8}.get(bits, termios.CS8)

    # Parity
    if parity == "E":
        cflag |= termios.PARENB
    elif parity == "O":
        cflag |= termios.PARENB | termios.PARODD

    # Stop bits
    if stop_bits == 2:
        cflag |= termios.CSTOPB

    # Enable read
    cflag |= termios.CREAD | termios.CLOCAL

    # Baud rate
    B = baud_constant(baud)

    # VMIN / VTIME
    cc[termios.VMIN]  = vmin
    cc[termios.VTIME] = vtime

    new_attrs = [iflag, oflag, cflag, lflag, B, B, cc]
    termios.tcsetattr(fd, termios.TCSANOW, new_attrs)


# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────

if __name__ == "__main__":
    print("=== termios demo ===")

    if not _TERMIOS_AVAILABLE:
        print("  termios not available on Windows — skipping")
        raise SystemExit(0)

    fd = sys.stdin.fileno()
    is_tty = os.isatty(fd)
    print(f"  stdin is a tty: {is_tty}")

    # ── describe_terminal ──────────────────────────────────────────────────────
    print("\n--- describe_terminal ---")
    info = describe_terminal(fd)
    print(f"  {info}")

    # ── save_restore + raw_mode ────────────────────────────────────────────────
    print("\n--- save_restore round-trip ---")
    if is_tty:
        original = get_attrs(fd)
        with save_restore(fd) as saved:
            # Enter raw mode inside the context
            termios.tcsetattr(fd, termios.TCSANOW, saved)
            attrs_in = get_attrs(fd)
        restored = get_attrs(fd)
        print(f"  original lflag: {original[3]:#010x}")
        print(f"  restored lflag: {restored[3]:#010x}")
        print(f"  match: {original[3] == restored[3]}")
    else:
        print("  (not a tty — skipping live test)")

    # ── baud_constant ──────────────────────────────────────────────────────────
    print("\n--- baud_constant ---")
    for rate in [9600, 115200, 230400]:
        try:
            B = baud_constant(rate)
            print(f"  {rate:7d} bps → constant {B}")
        except ValueError as e:
            print(f"  {rate}: {e}")

    # ── describe mode inside raw_mode (non-interactive, needs tty) ─────────────
    print("\n--- mode inside raw_mode (if tty) ---")
    if is_tty:
        with raw_mode(fd):
            info_raw = describe_terminal(fd)
        print(f"  raw:     {info_raw}")
        info_normal = describe_terminal(fd)
        print(f"  restored: {info_normal}")
    else:
        print("  (not a tty — skipping)")

    print("\n=== done ===")

For the tty alternative — tty.setraw(fd) and tty.setcbreak(fd) are convenience wrappers around termios.tcsetattr() that set the most common raw and cbreak modes in one call — use tty for the two most common mode switches; use termios directly when you need fine-grained control over individual flag bits (e.g. keep signals enabled but disable echo, or set custom VMIN/VTIME for a specific read timeout). For the curses alternative — curses.initscr() initializes a full-screen terminal UI and manages raw mode, keypad decoding, and window rendering automatically; curses.noecho(), curses.cbreak(), curses.keypad() are the equivalent termios flag manipulations wrapped in a high-level API — use curses when building interactive TUI applications with windows, colors, and keyboard navigation; use termios directly when you need terminal control outside of a curses session, such as in a streaming data consumer, a custom line editor, or a serial port driver. The Claude Skills 360 bundle includes termios skill sets covering get_attrs()/set_attrs()/save_restore() attribute management, raw_mode()/cbreak_mode()/no_echo() context managers, read_char()/read_key()/prompt_password() input readers, TerminalInfo with describe_terminal() inspector, and baud_constant()/configure_serial() serial port helper. Start with the free tier to try terminal control patterns and termios 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