chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
"""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 0x40–0x7E 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"]
|
||||
Reference in New Issue
Block a user