Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

224 lines
8.0 KiB
Python

"""Interactive prompt helpers (Escape to cancel, etc.)."""
from __future__ import annotations
import asyncio
import sys
import time
from collections.abc import Callable
from pathlib import Path
from typing import Any
import questionary.question
from prompt_toolkit.key_binding import KeyBindings, KeyBindingsBase, merge_key_bindings
from prompt_toolkit.keys import Keys
from rich.console import Console
from platform.terminal.theme import DIM
_escape_patch_installed: list[bool] = [False]
_ctrl_c_patch_installed: list[bool] = [False]
_last_ctrl_c: list[float | None] = [None]
_handling_ctrl_c: list[bool] = [False]
CTRL_C_DOUBLE_PRESS_WINDOW_S: float = 2.0
_CTRL_C_EXIT_WINDOW: float = CTRL_C_DOUBLE_PRESS_WINDOW_S
class _HardQuitInterrupt(KeyboardInterrupt):
"""Raised by explicit quit keys (Ctrl+Q) to bypass the Ctrl+C double-exit guard."""
def _with_escape_cancel(question: questionary.question.Question) -> questionary.question.Question:
"""Prepend Escape handling so it wins over questionary's catch-all bindings."""
extra = KeyBindings()
@extra.add(Keys.Escape, eager=True)
def _escape(event: Any) -> None:
event.app.exit(result=None)
app = question.application
existing: KeyBindingsBase = app.key_bindings or KeyBindings()
app.key_bindings = merge_key_bindings([extra, existing])
return question
def _wrap_question_prompt(
orig: Callable[..., questionary.question.Question],
) -> Callable[..., questionary.question.Question]:
def wrapped(*args: Any, **kwargs: Any) -> questionary.question.Question:
return _with_escape_cancel(orig(*args, **kwargs))
wrapped.__name__ = orig.__name__
wrapped.__doc__ = orig.__doc__
wrapped.__qualname__ = getattr(orig, "__qualname__", orig.__name__)
return wrapped
def install_questionary_escape_cancel() -> None:
"""Make Escape cancel questionary prompts (returns None), consistent across the CLI."""
if _escape_patch_installed[0]:
return
import questionary
import questionary.prompts.checkbox as checkbox_mod
import questionary.prompts.confirm as confirm_mod
import questionary.prompts.password as password_mod
import questionary.prompts.path as path_mod
import questionary.prompts.select as select_mod
import questionary.prompts.text as text_mod
select_mod.select = _wrap_question_prompt(select_mod.select)
checkbox_mod.checkbox = _wrap_question_prompt(checkbox_mod.checkbox)
confirm_mod.confirm = _wrap_question_prompt(confirm_mod.confirm)
text_mod.text = _wrap_question_prompt(text_mod.text)
path_mod.path = _wrap_question_prompt(path_mod.path)
password_mod.password = _wrap_question_prompt(password_mod.password)
questionary.select = select_mod.select
questionary.checkbox = checkbox_mod.checkbox
questionary.confirm = confirm_mod.confirm
questionary.text = text_mod.text
questionary.path = path_mod.path
questionary.password = password_mod.password
_escape_patch_installed[0] = True
def handle_ctrl_c_press() -> None:
"""Handle Ctrl+C from the SIGINT signal handler (between prompts)."""
if _handling_ctrl_c[0]:
return
_handling_ctrl_c[0] = True
try:
now = time.monotonic()
if _last_ctrl_c[0] is not None and now - _last_ctrl_c[0] <= _CTRL_C_EXIT_WINDOW:
print("\nGoodbye!", flush=True)
sys.exit(0)
_last_ctrl_c[0] = now
print("\n(Press Ctrl+C again to exit)", flush=True)
finally:
_handling_ctrl_c[0] = False
def _with_ctrl_c_double_exit(
question: questionary.question.Question,
) -> questionary.question.Question:
"""Add Ctrl+C double-exit handling to a questionary prompt."""
def _patched_ask(*args: Any, **kwargs: Any) -> Any:
while True:
try:
try:
asyncio.get_running_loop()
in_event_loop = True
except RuntimeError:
in_event_loop = False
if in_event_loop:
result = question.application.run(in_thread=True)
else:
result = question.unsafe_ask(*args, **kwargs)
_last_ctrl_c[0] = None
return result
except KeyboardInterrupt as exc:
if isinstance(exc, _HardQuitInterrupt):
raise
now = time.monotonic()
if _last_ctrl_c[0] is not None and now - _last_ctrl_c[0] <= _CTRL_C_EXIT_WINDOW:
print("\nGoodbye!", flush=True)
sys.exit(0)
_last_ctrl_c[0] = now
print("\n(Press Ctrl+C again to exit)", flush=True)
except (OSError, KeyError) as exc:
import logging
logging.getLogger(__name__).debug(
"interactive prompt selector error: %s", exc, exc_info=True
)
print(
"\nThe interactive prompt could not be displayed due to a "
"terminal I/O error on this platform.",
flush=True,
)
sys.exit(1)
question.ask = _patched_ask # type: ignore[method-assign]
return question
def _wrap_question_ctrl_c(
orig: Callable[..., questionary.question.Question],
) -> Callable[..., questionary.question.Question]:
def wrapped(*args: Any, **kwargs: Any) -> questionary.question.Question:
return _with_ctrl_c_double_exit(orig(*args, **kwargs))
wrapped.__name__ = orig.__name__
wrapped.__doc__ = orig.__doc__
wrapped.__qualname__ = getattr(orig, "__qualname__", orig.__name__)
return wrapped
def repl_reset_ctrl_c_gate() -> None:
_last_ctrl_c[0] = None
def cli_invocation_name() -> str:
"""Return the basename of the current CLI launcher (for example ``opensre`` or ``o``)."""
argv0 = sys.argv[0].strip() if sys.argv else ""
if not argv0:
return "opensre"
name = Path(argv0).name
return name or "opensre"
def print_session_resume_hint(console: Console, session_id: str) -> None:
"""Print REPL and CLI commands that restore ``session_id``."""
cli_name = cli_invocation_name()
console.print(f"[{DIM}]Resume this session with:[/]")
console.print(f"[{DIM}]/resume {session_id}[/]")
console.print(f"[{DIM}]{cli_name} --resume {session_id}[/]")
def repl_prompt_note_ctrl_c(console: Console, session_id: str | None = None) -> bool:
now = time.monotonic()
if _last_ctrl_c[0] is not None and now - _last_ctrl_c[0] <= _CTRL_C_EXIT_WINDOW:
console.print()
if session_id:
print_session_resume_hint(console, session_id)
console.print(f"[{DIM}]Goodbye![/]")
_last_ctrl_c[0] = None
return True
_last_ctrl_c[0] = now
console.print(f"[{DIM}](Press Ctrl+C again to exit)[/]")
return False
def install_questionary_ctrl_c_double_exit() -> None:
"""Make Ctrl+C show a hint on first press and exit on second press within 2 s."""
if _ctrl_c_patch_installed[0]:
return
import questionary
import questionary.prompts.checkbox as checkbox_mod
import questionary.prompts.confirm as confirm_mod
import questionary.prompts.password as password_mod
import questionary.prompts.path as path_mod
import questionary.prompts.select as select_mod
import questionary.prompts.text as text_mod
select_mod.select = _wrap_question_ctrl_c(select_mod.select)
checkbox_mod.checkbox = _wrap_question_ctrl_c(checkbox_mod.checkbox)
confirm_mod.confirm = _wrap_question_ctrl_c(confirm_mod.confirm)
text_mod.text = _wrap_question_ctrl_c(text_mod.text)
path_mod.path = _wrap_question_ctrl_c(path_mod.path)
password_mod.password = _wrap_question_ctrl_c(password_mod.password)
questionary.select = select_mod.select
questionary.checkbox = checkbox_mod.checkbox
questionary.confirm = confirm_mod.confirm
questionary.text = text_mod.text
questionary.path = path_mod.path
questionary.password = password_mod.password
_ctrl_c_patch_installed[0] = True