"""Shared helpers for the Click CLI command modules. Every CLI command is a thin presentation layer over the Python SDK: it builds a :class:`~browser_cli.BrowserCLI` from the global ``--browser/--remote/--key`` options, calls the matching SDK namespace method, and renders the result. The SDK is the single source of truth for command strings, argument shapes, and multi-browser routing. """ import functools import click from rich.console import Console from rich.table import Table from browser_cli import BrowserCLI, BrowserCounts from browser_cli.client import BrowserNotConnected from browser_cli.constants import GENTLE_MODES _console = Console() # Reusable ``--tab`` option: select a tab by ID (default: the active tab). tab_option = click.option("--tab", "tab_id", type=int, default=None, help="Tab ID (default: active tab)") def gentle_mode_option(help_text: str): """Reusable ``--gentle-mode`` Click option (throttle mode for large operations).""" return click.option( "--gentle-mode", type=click.Choice(GENTLE_MODES), default="auto", show_default=True, help=help_text, ) def print_counts(result, noun: str, *, single_suffix: str = "") -> None: """Render a count result. In multi-browser mode (*result* is a :class:`~browser_cli.BrowserCounts`) print a per-browser table with a Total row; otherwise print a single ``N noun(s)`` line. """ if isinstance(result, BrowserCounts): table = Table(show_header=True, header_style="bold cyan") table.add_column("Browser", no_wrap=True) table.add_column(f"{noun.capitalize()}s", justify="right") rendered_groups: set[str] = set() for name, count in result.by_browser.items(): group = result.browser_groups.get(name) if group: if group not in rendered_groups: group_total = sum( browser_count for browser_name, browser_count in result.by_browser.items() if result.browser_groups.get(browser_name) == group ) table.add_row(f"[bold]{group}[/bold]", str(group_total)) rendered_groups.add(group) display_name = name.removeprefix(f"{group}:") table.add_row(f" {display_name}", str(count)) else: table.add_row(name, str(count)) table.add_row("Total", str(result.total)) _console.print(table) else: _console.print(f"[bold]{result}[/bold] {noun}(s){single_suffix}") def client_from_ctx() -> BrowserCLI: """Build a BrowserCLI from the root context's global options. Reads ``browser``/``remote``/``key`` set by the top-level ``main`` group. Falls back to an unconfigured client when a command group is invoked standalone (e.g. in unit tests). """ obj = click.get_current_context().find_root().obj or {} return BrowserCLI(browser=obj.get("browser"), remote=obj.get("remote"), key=obj.get("key")) def handle_errors(fn): """Decorate a CLI command so SDK exceptions become clean errors + exit(1). Apply as the innermost decorator (directly above ``def``) so Click's option decorators attach their params to the wrapper. """ @functools.wraps(fn) def wrapper(*args, **kwargs): try: return fn(*args, **kwargs) except BrowserNotConnected as e: _console.print(f"[red]Error:[/red] {e}") raise SystemExit(1) except PermissionError as e: _console.print(f"[red]Blocked:[/red] {e}") raise SystemExit(1) except RuntimeError as e: _console.print(f"[red]Browser error:[/red] {e}") raise SystemExit(1) return wrapper