项目文件夹

文件
2026-07-13 13:22:34 +08:00

95 行
3.0 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
from __future__ import annotations
import os
import select
import sys
import click
if sys.platform != "win32":
import termios
import tty
def _read_key() -> str:
"""Read a single keystroke (or escape sequence) from stdin in raw mode."""
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
try:
tty.setraw(fd)
# `os.read` bypasses Python's stdin buffer; otherwise the BufferedReader
# would slurp the rest of an arrow-key sequence on the first read(1) and
# `select.select(fd)` would never see the pending bytes.
ch = os.read(fd, 1).decode("utf-8", errors="replace")
# Read the rest of the escape sequence only if more bytes are pending;
# a bare Esc keypress would otherwise block here waiting for two more chars.
if ch == "\x1b" and select.select([fd], [], [], 0.05)[0]:
ch += os.read(fd, 2).decode("utf-8", errors="replace")
return ch
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
def arrow_select(prompt_text: str, options: list[str]) -> int:
"""Render `options` and let the user pick one via arrow keys; return its index.
Falls back to a numeric prompt on Windows or when stdio isn't a TTY.
"""
if sys.platform == "win32" or not (sys.stdin.isatty() and sys.stderr.isatty()):
click.secho(prompt_text, bold=True, err=True)
for i, opt in enumerate(options, 1):
click.echo(f" {click.style(str(i), fg='cyan')}. {opt}", err=True)
choice = click.prompt(
click.style("Select", fg="cyan", bold=True),
type=click.IntRange(1, len(options)),
default=1,
err=True,
)
return choice - 1
click.secho(
f"{prompt_text} (↑/↓ to navigate, Enter to select)",
fg="cyan",
bold=True,
err=True,
)
idx = 0
n = len(options)
def render() -> None:
for i, opt in enumerate(options):
if i == idx:
click.secho(f" {opt}", fg="cyan", err=True)
else:
click.echo(f" {opt}", err=True)
def rewind() -> None:
sys.stderr.write(f"\x1b[{n}A\x1b[J")
sys.stderr.flush()
sys.stderr.write("\x1b[?25l") # hide cursor
sys.stderr.flush()
try:
render()
while True:
key = _read_key()
match key:
case "\r" | "\n":
rewind()
click.secho(f" {options[idx]}", fg="green", err=True)
return idx
case "\x03":
rewind()
raise click.Abort()
case "\x1b[A" | "\x1bOA" | "k":
idx = (idx - 1) % n
case "\x1b[B" | "\x1bOB" | "j":
idx = (idx + 1) % n
case _:
continue
rewind()
render()
finally:
sys.stderr.write("\x1b[?25h") # show cursor
sys.stderr.flush()