Files
MoFin/venv/lib/python3.12/site-packages/huggingface_hub/utils/_terminal.py
T
知微 fa45d8aa5f fix: 小果地址统一node122(兼容LAN+EasyTier)
- health_checklist.json: 192.168.1.122→node122
- ocr_client.py: docstring IP→node122
- docs/market-data-requirements.md: IP→node122
- 所有API调用通过ProxyHandler({})绕过系统代理
  Privoxy对node122:18003返回500,直连正常
2026-06-30 02:56:35 +08:00

263 lines
8.8 KiB
Python

# Copyright 2025 The HuggingFace Team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Contains utilities to print stuff to the terminal (styling, helpers)."""
import ctypes
import os
import shutil
import sys
from contextlib import contextmanager
from ._detect_agent import is_agent
if sys.platform == "win32":
import msvcrt
else:
import select
import termios
import tty
class StatusLine:
"""Minimal TTY status line for sync progress (stderr, single-line overwrite)."""
def __init__(self, enabled: bool = True):
self._active = enabled and sys.stderr.isatty()
def update(self, msg: str) -> None:
if not self._active:
return
width = shutil.get_terminal_size().columns
if len(msg) > width - 1:
msg = msg[: width - 4] + "..."
sys.stderr.write(f"\r\033[K\033[90m{msg}\033[0m")
sys.stderr.flush()
def done(self, msg: str) -> None:
if not self._active:
return
width = shutil.get_terminal_size().columns
if len(msg) > width - 1:
msg = msg[: width - 4] + "..."
sys.stderr.write(f"\r\033[K\033[90m{msg}\033[0m\n")
sys.stderr.flush()
class ANSI:
"""
Helper for en.wikipedia.org/wiki/ANSI_escape_code
"""
_blue = "\u001b[34m"
_bold = "\u001b[1m"
_gray = "\u001b[90m"
_green = "\u001b[32m"
_red = "\u001b[31m"
_reset = "\u001b[0m"
_underline = "\u001b[4m"
_yellow = "\u001b[33m"
@classmethod
def blue(cls, s: str) -> str:
return cls._format(s, cls._blue)
@classmethod
def bold(cls, s: str) -> str:
return cls._format(s, cls._bold)
@classmethod
def gray(cls, s: str) -> str:
return cls._format(s, cls._gray)
@classmethod
def green(cls, s: str) -> str:
return cls._format(s, cls._green)
@classmethod
def red(cls, s: str) -> str:
return cls._format(s, cls._bold + cls._red)
@classmethod
def underline(cls, s: str) -> str:
return cls._format(s, cls._underline)
@classmethod
def yellow(cls, s: str) -> str:
return cls._format(s, cls._yellow)
@classmethod
def _format(cls, s: str, code: str) -> str:
if os.environ.get("NO_COLOR") or is_agent():
# See https://no-color.org/
return s
return f"{code}{s}{cls._reset}"
def select_choice(prompt: str, choices: list[str]) -> int:
"""Single-choice interactive prompt. Returns the index of the selected choice.
On a TTY, renders an arrow-key menu (Up/Down to move, Enter to confirm, 1-9 to pick
directly, Ctrl+C to abort). Falls back to a numbered `input()` prompt when raw
keyboard input is not available. Callers are responsible for not prompting at all in
non-interactive contexts.
"""
if not choices:
raise ValueError("select_choice() requires at least one choice.")
if _supports_raw_keyboard():
return _select_with_arrows(prompt, choices)
return _select_with_numbers(prompt, choices)
def _supports_raw_keyboard() -> bool:
if not (sys.stdin and sys.stdin.isatty() and sys.stdout.isatty()):
return False
if sys.platform == "win32":
return _enable_windows_vt_processing()
return True
def _enable_windows_vt_processing() -> bool:
"""Enable VT escape-sequence processing on the Windows console (off by default in legacy
cmd.exe/PowerShell windows, where the menu would otherwise render as literal `←[K` garbage)."""
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
STD_OUTPUT_HANDLE = -11
try:
kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined]
handle = kernel32.GetStdHandle(STD_OUTPUT_HANDLE)
mode = ctypes.c_uint32()
if not kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
return False
return bool(kernel32.SetConsoleMode(handle, mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING))
except Exception:
return False
@contextmanager
def _raw_terminal():
"""Put the terminal in cbreak mode (read keypresses without Enter). No-op on Windows."""
if sys.platform == "win32":
yield
return
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setcbreak(fd)
yield
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
def _read_key() -> str:
"""Read one keypress, normalizing arrow keys to "up"/"down"."""
if sys.platform == "win32":
char = msvcrt.getwch()
if char in ("\x00", "\xe0"): # arrow keys come as a two-character sequence
return {"H": "up", "P": "down"}.get(msvcrt.getwch(), "")
return char
# Read the file descriptor directly: `sys.stdin.read(1)` would buffer the whole escape
# sequence internally, making the fd look empty to `select()` below.
fd = sys.stdin.fileno()
char = os.read(fd, 1)
# Disambiguate a bare Escape from an escape sequence (e.g. "\x1b[A" for Up).
if char == b"\x1b" and select.select([fd], [], [], 0.05)[0]:
if os.read(fd, 1) == b"[" and select.select([fd], [], [], 0.05)[0]:
return {b"A": "up", b"B": "down"}.get(os.read(fd, 1), "")
return ""
return char.decode(errors="replace")
def _select_with_arrows(prompt: str, choices: list[str]) -> int:
selected = 0
print(ANSI.bold(f"? {prompt}") + ANSI.gray(" [Use arrows, Enter to confirm]"))
def render() -> None:
for i, choice in enumerate(choices):
line = ANSI.green("> ") + ANSI.bold(choice) if i == selected else " " + choice
sys.stdout.write(f"\r\x1b[K{line}\n")
sys.stdout.flush()
try:
sys.stdout.write("\x1b[?25l") # hide cursor
render()
with _raw_terminal():
while True:
key = _read_key()
if key == "\x03":
# Ctrl+C: POSIX cbreak keeps ISIG so it never reaches here, but on Windows
# msvcrt.getwch() returns the raw character instead of raising.
raise KeyboardInterrupt
if key == "up":
selected = (selected - 1) % len(choices)
elif key == "down":
selected = (selected + 1) % len(choices)
elif key.isdecimal() and 1 <= int(key) <= len(choices):
selected = int(key) - 1
break
elif key in ("\r", "\n"):
break
else:
continue
sys.stdout.write(f"\x1b[{len(choices)}A") # move back to the first option line
render()
finally:
sys.stdout.write("\x1b[?25h") # show cursor
sys.stdout.flush()
# Collapse the menu into a single "? prompt answer" summary line, like gh does.
sys.stdout.write(f"\x1b[{len(choices) + 1}A\r\x1b[J")
print(ANSI.bold(f"? {prompt} ") + ANSI.green(choices[selected]))
return selected
def _select_with_numbers(prompt: str, choices: list[str]) -> int:
print(f"? {prompt}")
for i, choice in enumerate(choices, start=1):
print(f" {i}. {choice}")
while True:
raw = input("Choice [1]: ").strip()
if not raw:
return 0
if raw.isdecimal() and 1 <= int(raw) <= len(choices):
return int(raw) - 1
print(f"Invalid choice. Enter a number between 1 and {len(choices)}.")
def tabulate(
rows: list[list[str | int]],
headers: list[str],
alignments: dict[str, str] | None = None,
) -> str:
"""
Inspired by:
- stackoverflow.com/a/8356620/593036
- stackoverflow.com/questions/9535954/printing-lists-as-tabular-data
"""
_ALIGN_MAP = {"left": "<", "right": ">"}
for row in rows:
if len(row) < len(headers):
raise IndexError(f"Row has {len(row)} values but expected {len(headers)} (headers: {headers})")
col_widths = [max(len(str(x)) for x in col) for col in zip(*rows, headers)]
col_aligns = [_ALIGN_MAP.get((alignments or {}).get(h, "left"), "<") for h in headers]
row_format = " ".join(f"{{:{a}{w}}}" for a, w in zip(col_aligns, col_widths))
lines = []
lines.append(row_format.format(*headers))
lines.append(row_format.format(*["-" * w for w in col_widths]))
for row in rows:
lines.append(row_format.format(*row))
return "\n".join(lines)