Files
browser-cli/browser_cli/commands/rendering.py
T
daniel156161 7cb2a8b618
Testing / remote-protocol-compat (0.9.5) (push) Successful in 1m4s
Testing / test (push) Successful in 1m22s
Testing / remote-protocol-compat (0.9.3) (push) Successful in 1m7s
Package Extension / package-extension (push) Successful in 1m1s
Build & Publish Package / publish (push) Successful in 1m5s
refactor: modularize auth transport and markdown
- Split auth into focused package modules for agent keys, file keys,
  signing, and post-quantum transport helpers while keeping the public
  browser_cli.auth import surface intact.
- Move transport encoding internals into a package with separate codec and
  binary-hoisting helpers, preserving browser_cli.transport compatibility.
- Extract remote TCP auth/socket helpers and serve challenge setup out of the
  runtime paths to make connection handling easier to reason about.
- Move the extension markdown extractor into a dedicated content/markdown
  folder with separate root selection, code normalization, renderer, and utils.
- Centralize CLI Rich rendering helpers for tab/window tree and table output,
  and add rendering tests for the shared builders.
- Remove local typing ignores in SDK/decorator/script plumbing and bump the
  package and extension version to 0.15.3.
2026-06-15 01:23:57 +02:00

188 lines
7.6 KiB
Python

"""Reusable rendering helpers for CLI command modules."""
from __future__ import annotations
import shutil
from collections.abc import Callable, Iterable, Sequence
from typing import Any
from rich.console import Console
from rich.table import Table
from rich.text import Text
from rich.tree import Tree
Column = tuple[str, Callable[[Any], Any]]
def item_value(item: Any, name: str, default: Any = None) -> Any:
"""Read *name* from a dict-like or attribute object."""
if isinstance(item, dict):
return item.get(name, default)
return getattr(item, name, default)
def shorten(value: str | None, limit: int) -> str:
"""Return *value* shortened to *limit* cells-ish, using an ellipsis."""
value = value or ""
return value if len(value) <= limit else value[:max(0, limit - 1)] + ""
def terminal_width(console: Console | None = None, *, fallback: int = 120) -> int:
"""Best-effort terminal width for interactive and redirected output.
Rich falls back to 80 columns when stdout is redirected. browser-cli output is
often piped into files for inspection, so also consult ``shutil``/``COLUMNS``
and prefer the wider value.
"""
rich_width = (console.width if console is not None else 0) or 0
shell_width = shutil.get_terminal_size((fallback, 20)).columns
return max(rich_width, shell_width)
def tree_title_limit(*, console: Console | None = None, show_browser: bool = False, show_urls: bool = False) -> int:
"""Title width for tree labels, reserving space for branches/IDs/metadata."""
reserve = 48 if show_urls else 32
if show_browser:
reserve += 4
return max(50, terminal_width(console) - reserve)
def tree_url_limit(title_limit: int, *, console: Console | None = None) -> int:
"""URL width for tree labels when URLs are displayed."""
return max(35, terminal_width(console) - title_limit - 40)
def no_wrap_text() -> Text:
"""Text configured for one-line tree labels with edge ellipsis."""
return Text(no_wrap=True, overflow="ellipsis")
def tab_tree_label(tab: Any, *, title_limit: int, show_urls: bool = False, url_limit: int = 55) -> Text:
"""Reusable one-line label for a browser tab in tree views."""
label = no_wrap_text()
label.append(f"[{item_value(tab, 'id')}] ", style="dim")
label.append(shorten(item_value(tab, 'title') or "(untitled)", title_limit))
if item_value(tab, "active", False):
label.append(" *", style="green")
url = item_value(tab, "url")
if show_urls and url:
label.append("", style="dim")
label.append(shorten(url, url_limit), style="dim")
return label
def group_tree_label(group_id: int, group: Any, *, title_limit: int) -> Text:
"""Reusable one-line label for a browser tab group in tree views."""
title = item_value(group, "title", "") or f"Group {group_id}"
color = item_value(group, "color", "") or "group"
count = item_value(group, "tab_count", item_value(group, "tabCount", 0)) or 0
collapsed = bool(item_value(group, "collapsed", False))
label = no_wrap_text()
label.append(shorten(title, title_limit), style="bold")
meta = [color]
if count:
meta.append(f"{count} tab" + ("" if count == 1 else "s"))
if collapsed:
meta.append("collapsed")
label.append(" (" + ", ".join(meta) + ")", style="dim")
return label
def tab_sort_key(tab: Any) -> tuple:
"""Stable tab ordering across multi-browser responses."""
group_id = item_value(tab, "group_id", item_value(tab, "groupId"))
return (
item_value(tab, "browser") or "",
item_value(tab, "window_id", item_value(tab, "windowId", 0)),
item_value(tab, "index", 0) or 0,
group_id if group_id is not None else -1,
item_value(tab, "id", 0),
)
def print_tree(tree: Tree, *, console: Console | None = None) -> None:
"""Render a Rich tree using the detected full terminal width."""
Console(width=terminal_width(console)).print(tree)
def print_table_rows(
rows: Sequence[Any],
columns: Sequence[Column],
*,
console: Console,
empty_message: str,
show_header: bool = True,
header_style: str = "bold cyan",
) -> None:
"""Render a small Rich table from arbitrary row objects."""
if not rows:
console.print(empty_message)
return
table = Table(show_header=show_header, header_style=header_style)
for header, _getter in columns:
table.add_column(header)
for row in rows:
table.add_row(*[str(getter(row) or "") for _header, getter in columns])
Console(width=terminal_width(console)).print(table)
def build_tabs_tree(
tabs: Iterable[Any],
groups: Iterable[Any],
*,
console: Console,
show_urls: bool = False,
) -> Tree:
"""Build a browser → window → group/tab tree from tab and group responses."""
tabs = sorted(tabs, key=tab_sort_key)
show_browser = any(item_value(tab, "browser") for tab in tabs)
title_limit = tree_title_limit(console=console, show_browser=show_browser, show_urls=show_urls)
url_limit = tree_url_limit(title_limit, console=console)
group_info = {
(
item_value(group, "browser") or "local",
item_value(group, "window_id", item_value(group, "windowId")),
item_value(group, "id"),
): group
for group in groups
}
root = Tree("[bold]Tabs[/bold]")
browser_nodes: dict[str, Tree] = {}
window_nodes: dict[tuple[str, int], Tree] = {}
group_nodes: dict[tuple[str, int, int], Tree] = {}
for tab in tabs:
browser_key = item_value(tab, "browser") or "local"
browser_node = browser_nodes.get(browser_key)
if browser_node is None:
browser_node = root.add(Text(browser_key, style="bold cyan")) if show_browser else root
browser_nodes[browser_key] = browser_node
window_id = item_value(tab, "window_id", item_value(tab, "windowId", 0))
window_key = (browser_key, window_id)
window_node = window_nodes.get(window_key)
if window_node is None:
window_node = browser_node.add(f"Window {window_id}")
window_nodes[window_key] = window_node
group_id = item_value(tab, "group_id", item_value(tab, "groupId"))
if group_id is None:
window_node.add(tab_tree_label(tab, title_limit=title_limit, show_urls=show_urls, url_limit=url_limit))
continue
group_key = (browser_key, window_id, group_id)
group_node = group_nodes.get(group_key)
if group_node is None:
group = group_info.get(group_key) or group_info.get((browser_key, None, group_id))
group_node = window_node.add(group_tree_label(group_id, group, title_limit=title_limit))
group_nodes[group_key] = group_node
group_node.add(tab_tree_label(tab, title_limit=title_limit, show_urls=show_urls, url_limit=url_limit))
return root
def build_windows_tree(windows: Iterable[dict], tabs: Iterable[Any], *, console: Console) -> Tree:
"""Build a window → tab tree from window and tab responses."""
windows = list(windows)
tabs = list(tabs)
title_limit = tree_title_limit(console=console, show_browser=any("browser" in w for w in windows), show_urls=True)
url_limit = tree_url_limit(title_limit, console=console)
root = Tree("[bold]Windows[/bold]")
for window in sorted(windows, key=lambda item: (item.get("browser", ""), item.get("id", 0))):
window_id = window.get("id")
label = f"Window {window_id}"
if window.get("alias"):
label += f" ({window['alias']})"
if window.get("browser"):
label = f"{window['browser']}: " + label
node = root.add(label)
window_tabs = [
tab for tab in tabs
if item_value(tab, "window_id", item_value(tab, "windowId")) == window_id
and (not window.get("browser") or item_value(tab, "browser") == window.get("browser"))
]
for tab in sorted(window_tabs, key=lambda item: item_value(item, "index", 0) or 0):
node.add(tab_tree_label(tab, title_limit=title_limit, show_urls=True, url_limit=url_limit))
return root