| 16 | |
| 17 | |
| 18 | class TerminalPrompter: |
| 19 | def __init__( |
| 20 | self, |
| 21 | *, |
| 22 | stdin: TextIO | None = None, |
| 23 | stdout: TextIO | None = None, |
| 24 | env: dict[str, str] | None = None, |
| 25 | ) -> None: |
| 26 | self.stdin = stdin or sys.stdin |
| 27 | self.stdout = stdout or sys.stdout |
| 28 | self.env = env or dict(os.environ) |
| 29 | self._interactive = ( |
| 30 | bool(getattr(self.stdin, "isatty", lambda: False)()) |
| 31 | and bool(getattr(self.stdout, "isatty", lambda: False)()) |
| 32 | and self.env.get("TERM", "").lower() != "dumb" |
| 33 | and not self.env.get("CI") |
| 34 | ) |
| 35 | |
| 36 | @property |
| 37 | def interactive(self) -> bool: |
| 38 | return self._interactive |
| 39 | |
| 40 | def choose( |
| 41 | self, |
| 42 | prompt: str, |
| 43 | options: list[MenuOption], |
| 44 | *, |
| 45 | default: int = 0, |
| 46 | ) -> MenuOption: |
| 47 | if not options: |
| 48 | raise ValueError("at least one option is required") |
| 49 | if self._interactive: |
| 50 | return self._choose_tty(prompt, options, default=default) |
| 51 | return self._choose_numbered(prompt, options, default=default) |
| 52 | |
| 53 | def confirm(self, prompt: str, *, default: bool = True) -> bool: |
| 54 | suffix = "[Y/n]" if default else "[y/N]" |
| 55 | while True: |
| 56 | self.stdout.write(f"{prompt} {suffix} ") |
| 57 | self.stdout.flush() |
| 58 | answer = self.stdin.readline() |
| 59 | if answer == "": |
| 60 | return default |
| 61 | clean = answer.strip().lower() |
| 62 | if not clean: |
| 63 | return default |
| 64 | if clean in {"y", "yes"}: |
| 65 | return True |
| 66 | if clean in {"n", "no"}: |
| 67 | return False |
| 68 | self.stdout.write("Please answer y or n.\n") |
| 69 | |
| 70 | def ask( |
| 71 | self, |
| 72 | prompt: str, |
| 73 | *, |
| 74 | default: str = "", |
| 75 | secret: bool = False, |