"""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)