Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

153 lines
5.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Low-level terminal key reader for TTY-first interactive menus.
Shared between :mod:`choice_menu` (REPL inline picker) and
:mod:`feedback` (post-investigation rating prompt) so the raw-mode
terminal I/O lives in one place.
Return values from :func:`read_key_unix` / :func:`read_key_windows`:
``"up"``, ``"down"``, ``"enter"``, ``"cancel"``, ``"tab"``,
``"right"``, ``"left"``, ``"eof"``, ``"ignore"``.
"""
from __future__ import annotations
import contextlib
import os
import sys
def flush_stdin_unix() -> None:
"""Discard pending stdin bytes before raw-mode reading."""
with contextlib.suppress(Exception):
import termios
termios.tcflush(sys.stdin.fileno(), termios.TCIFLUSH) # type: ignore[attr-defined]
def restore_stdin_terminal() -> None:
"""Return stdin to canonical echo mode after Live/raw investigation UI.
Investigation progress uses a background Ctrl+O watcher that puts stdin in
non-canonical mode without echo. If nested watchers restore the wrong
snapshot, the shell prompt appears to accept input but characters are not
echoed. Call this after investigation UI teardown and before line prompts.
"""
if os.name == "nt" or not sys.stdin.isatty():
return
import termios
with contextlib.suppress(Exception):
fd = sys.stdin.fileno()
attrs = termios.tcgetattr(fd) # type: ignore[attr-defined]
# Restore cooked-mode flags a raw menu clears: ICRNL so Enter (CR) submits,
# OPOST for output newlines, ICANON/ECHO/ISIG for line editing and signals.
attrs[0] |= termios.BRKINT | termios.ICRNL | termios.IXON # type: ignore[attr-defined]
attrs[1] |= termios.OPOST # type: ignore[attr-defined]
attrs[3] |= termios.ICANON | termios.ECHO | termios.ISIG # type: ignore[attr-defined]
if hasattr(termios, "IEXTEN"):
attrs[3] |= termios.IEXTEN # type: ignore[attr-defined]
termios.tcsetattr(fd, termios.TCSADRAIN, attrs) # type: ignore[attr-defined]
termios.tcflush(fd, termios.TCIFLUSH) # type: ignore[attr-defined]
def read_key_unix(*, also_cancel: tuple[bytes, ...] = ()) -> str:
"""Read one logical keypress in raw mode; return a normalised key name.
Possible return values: ``"up"``, ``"down"``, ``"enter"``,
``"cancel"``, ``"tab"``, ``"right"``, ``"left"``, ``"eof"``,
``"ignore"``.
``also_cancel`` treats additional single-byte keys as ``"cancel"`` (e.g.
``(b"s", b"S")`` for an explicit skip shortcut).
"""
import select as _sel
import termios
import tty
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd) # type: ignore[attr-defined]
try:
tty.setraw(fd) # type: ignore[attr-defined]
ch = os.read(fd, 1)
if not ch:
return "eof"
b = ch[0]
if b in (3, 4) or ch in also_cancel: # Ctrl-C / Ctrl-D / caller shortcuts
return "cancel"
if b in (10, 13, 32): # LF / CR / Space
return "enter"
if b == 9: # Tab
return "tab"
if ch in (b"j", b"J"):
return "down"
if ch in (b"k", b"K"):
return "up"
if ch in (b"q", b"Q"):
return "cancel"
if b == 27: # ESC or arrow-key prefix
if _sel.select([fd], [], [], 0.1)[0]:
nxt = os.read(fd, 1)
if nxt == b"[" and _sel.select([fd], [], [], 0.1)[0]:
arr = os.read(fd, 1)
if arr == b"A":
return "up"
if arr == b"B":
return "down"
if arr == b"C":
return "right"
if arr == b"D":
return "left"
# Not an arrow key — drain the rest of the CSI sequence so
# bytes like "0;1R" from a CPR (ESC[row;colR) don't leak into
# the next read or the prompt buffer as literal characters.
# The VT/xterm spec defines 0x400x7E as valid CSI final bytes.
while arr and not (0x40 <= arr[0] <= 0x7E):
if not _sel.select([fd], [], [], 0)[0]:
break
arr = os.read(fd, 1)
return "cancel"
return "ignore"
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old) # type: ignore[attr-defined]
def read_key_windows(*, also_cancel: tuple[bytes, ...] = ()) -> str:
"""Read one logical keypress on Windows; return a normalised key name.
Possible return values: ``"up"``, ``"down"``, ``"enter"``,
``"cancel"``, ``"tab"``, ``"right"``, ``"left"``, ``"eof"``,
``"ignore"``.
``also_cancel`` treats additional single-byte keys as ``"cancel"``.
"""
import msvcrt # type: ignore[import,attr-defined]
ch = msvcrt.getch() # type: ignore[attr-defined]
if ch in (b"\x03", b"\x1b") or ch in also_cancel:
return "cancel"
if ch in (b"\r", b"\n", b" "):
return "enter"
if ch == b"\t":
return "tab"
if ch in (b"j", b"J"):
return "down"
if ch in (b"k", b"K"):
return "up"
if ch in (b"q", b"Q"):
return "cancel"
if ch in (b"\xe0", b"\x00"):
ch2 = msvcrt.getch() # type: ignore[attr-defined]
if ch2 == b"H":
return "up"
if ch2 == b"P":
return "down"
if ch2 == b"M":
return "right"
if ch2 == b"K":
return "left"
return "ignore"
return "ignore"
__all__ = ["flush_stdin_unix", "read_key_unix", "read_key_windows", "restore_stdin_terminal"]