hermes-agent/hermes_cli/secret_prompt.py

126 lines
3.1 KiB
Python

"""Secret input prompts with masked typing feedback."""
from __future__ import annotations
import getpass
import os
import sys
from collections.abc import Callable
_BACKSPACE_CHARS = {"\b", "\x7f"}
_ENTER_CHARS = {"\r", "\n"}
_EOF_CHARS = {"\x04", "\x1a"}
def _collect_masked_input(
read_char: Callable[[], str],
write: Callable[[str], object],
prompt: str,
*,
mask: str = "*",
) -> str:
"""Read one secret line while writing a mask character per typed char."""
value: list[str] = []
write(prompt)
while True:
ch = read_char()
if ch == "":
write("\n")
raise EOFError
if ch in _ENTER_CHARS:
write("\n")
return "".join(value)
if ch == "\x03":
write("\n")
raise KeyboardInterrupt
if ch in _EOF_CHARS:
write("\n")
raise EOFError
if ch in _BACKSPACE_CHARS:
if value:
value.pop()
write("\b \b")
continue
if ch == "\x1b":
# Ignore escape itself. Terminals commonly send escape-prefixed
# navigation/delete sequences; they should not become secret text.
continue
value.append(ch)
if mask:
write(mask)
def masked_secret_prompt(prompt: str, *, mask: str = "*") -> str:
"""Prompt for a secret while showing masked typing feedback.
Falls back to ``getpass.getpass`` when stdin/stdout are not interactive or
when raw terminal handling is unavailable.
"""
stdin = sys.stdin
stdout = sys.stdout
if not _stream_is_tty(stdin) or not _stream_is_tty(stdout):
return getpass.getpass(prompt)
if os.name == "nt":
try:
return _masked_secret_prompt_windows(prompt, mask=mask)
except (KeyboardInterrupt, EOFError):
raise
except Exception:
return getpass.getpass(prompt)
try:
return _masked_secret_prompt_posix(prompt, mask=mask)
except (KeyboardInterrupt, EOFError):
raise
except Exception:
return getpass.getpass(prompt)
def _stream_is_tty(stream) -> bool:
try:
return bool(stream.isatty())
except Exception:
return False
def _masked_secret_prompt_windows(prompt: str, *, mask: str) -> str:
import msvcrt
def read_char() -> str:
ch = msvcrt.getwch()
if ch in {"\x00", "\xe0"}:
msvcrt.getwch()
return "\x1b"
return ch
def write(text: str) -> None:
sys.stdout.write(text)
sys.stdout.flush()
return _collect_masked_input(read_char, write, prompt, mask=mask)
def _masked_secret_prompt_posix(prompt: str, *, mask: str) -> str:
import termios
import tty
fd = sys.stdin.fileno()
old_attrs = termios.tcgetattr(fd)
def read_char() -> str:
return sys.stdin.read(1)
def write(text: str) -> None:
sys.stdout.write(text)
sys.stdout.flush()
try:
tty.setraw(fd)
return _collect_masked_input(read_char, write, prompt, mask=mask)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_attrs)