diff --git a/src/rp/pager.py b/src/rp/pager.py index f2aa2b1..6f5331a 100644 --- a/src/rp/pager.py +++ b/src/rp/pager.py @@ -1,53 +1,81 @@ -"""Custom pager with mouse scroll support.""" +"""Less-based pager integration with mouse support and fallback.""" from __future__ import annotations +import re +import signal import shutil import subprocess from rich.pager import Pager, SystemPager -def _is_mouse_capable_less() -> bool: - """Check whether the system has a usable ``less`` executable. +def _mouse_capable_less() -> str | None: + """Return a ``less`` path that likely supports ``--mouse``. + + Mouse support was added in ``less`` 543. This probe is best-effort: it + checks that ``less`` exists, asks for its version string, and extracts the + version number from the leading ``less `` banner. Any unexpected + output is treated as unsupported so the caller can fall back safely. Returns: - True if ``less`` is installed and responds to ``--version``. + The resolved ``less`` executable path when it appears mouse-capable, + otherwise ``None``. """ less = shutil.which("less") if less is None: - return False + return None + try: result = subprocess.run( [less, "--version"], capture_output=True, text=True, timeout=5 ) - version_str = result.stdout or result.stderr or "" - return "GNU" in version_str or "less" in version_str.lower() except (subprocess.TimeoutExpired, OSError): - return False + return None + + version_str = result.stdout or result.stderr or "" + match = re.search(r"\bless\s+(\d+)\b", version_str, re.IGNORECASE) + if match is None: + return None + + if int(match.group(1)) < 543: + return None + + return less class MousePager(Pager): - """Pager that prefers ``less --mouse`` and falls back to the system pager.""" + """Pager that prefers ``less -R --mouse`` for mouse scrolling. + + ``less`` is the prerequisite for mouse-enabled paging in this project. If + ``less`` is missing, too old, cannot be launched, or exits with a non-zero + status, we fall back to Rich's ``SystemPager``. A user interrupt does not + trigger a second pager session. + """ def show(self, content: str) -> None: - """Display content with mouse-scroll support when available. - - Uses ``less --mouse`` when available; otherwise falls back to the system - pager. + """Display rendered content in ``less -R --mouse`` when possible. Args: - content: Content to display in pager. + content: Fully rendered text from Rich. """ - if _is_mouse_capable_less(): - try: - subprocess.run( - ["less", "-R", "--mouse"], - input=content, - text=True, - check=True, - ) - return - except (subprocess.CalledProcessError, FileNotFoundError): - pass - SystemPager().show(content) + less = _mouse_capable_less() + if less is None: + SystemPager().show(content) + return + + try: + result = subprocess.run( + [less, "-R", "--mouse"], + input=content, + text=True, + ) + except OSError: + SystemPager().show(content) + return + + if result.returncode in (-signal.SIGINT, 128 + signal.SIGINT): + return + + if result.returncode != 0: + SystemPager().show(content)