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.