Thread RP_DEBUG-driven logging through input, detection, rendering, and pager decisions so CLI behavior is inspectable without polluting stdout. Add focused coverage for debug wiring, env parsing, and stderr-only output.
102 lines
3.4 KiB
Python
102 lines
3.4 KiB
Python
"""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
|
|
|
|
from rp.debug import DebugLogger, log_debug
|
|
|
|
|
|
def _mouse_capable_less(debug: DebugLogger | None = None) -> 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 <version>`` banner. Any unexpected
|
|
output is treated as unsupported so the caller can fall back safely.
|
|
|
|
Returns:
|
|
The resolved ``less`` executable path when it appears mouse-capable,
|
|
otherwise ``None``.
|
|
"""
|
|
less = shutil.which("less")
|
|
if less is None:
|
|
log_debug(debug, "pager: 'less' was not found")
|
|
return None
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
[less, "--version"], capture_output=True, text=True, timeout=5
|
|
)
|
|
except (subprocess.TimeoutExpired, OSError):
|
|
log_debug(debug, "pager: failed to probe the installed 'less' binary")
|
|
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:
|
|
log_debug(debug, "pager: could not parse the installed 'less' version")
|
|
return None
|
|
|
|
version = int(match.group(1))
|
|
if version < 543:
|
|
log_debug(debug, f"pager: installed 'less' {version} is too old for --mouse")
|
|
return None
|
|
|
|
log_debug(debug, f"pager: using 'less' {version} from {less!r}")
|
|
return less
|
|
|
|
|
|
class MousePager(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 __init__(self, debug: DebugLogger | None = None) -> None:
|
|
"""Initialize the pager with an optional debug logger."""
|
|
self._debug = debug
|
|
|
|
def show(self, content: str) -> None:
|
|
"""Display rendered content in ``less -R --mouse`` when possible.
|
|
|
|
Args:
|
|
content: Fully rendered text from Rich.
|
|
"""
|
|
less = _mouse_capable_less(self._debug)
|
|
if less is None:
|
|
log_debug(self._debug, "pager: falling back to Rich's SystemPager")
|
|
SystemPager().show(content)
|
|
return
|
|
|
|
try:
|
|
log_debug(self._debug, "pager: launching 'less -R --mouse'")
|
|
result = subprocess.run(
|
|
[less, "-R", "--mouse"],
|
|
input=content,
|
|
text=True,
|
|
)
|
|
except OSError:
|
|
log_debug(self._debug, "pager: failed to launch 'less', using SystemPager")
|
|
SystemPager().show(content)
|
|
return
|
|
|
|
if result.returncode in (-signal.SIGINT, 128 + signal.SIGINT):
|
|
log_debug(self._debug, "pager: interrupted by user")
|
|
return
|
|
|
|
if result.returncode != 0:
|
|
log_debug(
|
|
self._debug,
|
|
f"pager: 'less' exited with status {result.returncode}, using SystemPager",
|
|
)
|
|
SystemPager().show(content)
|