feat(cli): improve tab and window tree rendering

- Add shared rendering helpers for width-aware tree labels, truncation, and no-wrap Rich text.
- Preserve tab index and group window metadata through the extension and SDK factories.
- Render tab trees in browser/window/index order with grouped tab details and optional shortened URLs.
- Reuse the tab tree labels in window trees to keep output compact and consistent.
- Cover legacy missing-index responses, grouped/collapsed tabs, URL display, and rendering helpers with tests.
This commit is contained in:
2026-06-15 01:04:02 +02:00
parent 657b1b0923
commit 0b43408a8d
9 changed files with 409 additions and 181 deletions
+55
View File
@@ -0,0 +1,55 @@
"""Reusable rendering helpers for CLI command modules."""
from __future__ import annotations
import shutil
from rich.console import Console
from rich.text import Text
from rich.tree import Tree
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, *, 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"[{tab.id}] ", style="dim")
label.append(shorten(tab.title or "(untitled)", title_limit))
if tab.active:
label.append(" *", style="green")
if show_urls and tab.url:
label.append("", style="dim")
label.append(shorten(tab.url, url_limit), style="dim")
return label
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)
+168 -129
View File
@@ -2,78 +2,117 @@ import base64
import binascii
import click
from browser_cli.commands import client_from_ctx, gentle_mode_option, handle_errors, print_counts, tab_option
from browser_cli.commands.rendering import no_wrap_text, print_tree, shorten, tab_tree_label, tree_title_limit, tree_url_limit
from rich.console import Console
from rich.table import Table
from rich.text import Text
from rich.tree import Tree
console = Console()
def _group_tree_label(group_id: int, group, *, title_limit: int) -> Text:
title = getattr(group, "title", "") or f"Group {group_id}"
color = getattr(group, "color", "") or "group"
count = getattr(group, "tab_count", 0) or 0
collapsed = bool(getattr(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):
return (
tab.browser or "",
tab.window_id,
getattr(tab, "index", 0),
tab.group_id if tab.group_id is not None else -1,
tab.id,
)
def _print_tabs(tabs, *, show_browser: bool = False) -> None:
if not tabs:
console.print("[yellow]No tabs found[/yellow]")
return
table = Table(show_header=True, header_style="bold cyan")
if show_browser:
table.add_column("Browser", no_wrap=True)
table.add_column("ID", style="dim", no_wrap=True)
table.add_column("Window", no_wrap=True)
table.add_column("Active", width=7)
table.add_column("Muted", width=7)
table.add_column("Title")
table.add_column("URL")
for t in tabs:
active = "[green]✓[/green]" if t.active else ""
muted = "[yellow]✓[/yellow]" if t.muted else ""
row = [
(t.browser or "") if show_browser else None,
str(t.id),
str(t.window_id),
active,
muted,
(t.title or "")[:60],
(t.url or "")[:80],
]
table.add_row(*[value for value in row if value is not None])
console.print(table)
if not tabs:
console.print("[yellow]No tabs found[/yellow]")
return
table = Table(show_header=True, header_style="bold cyan")
if show_browser:
table.add_column("Browser", no_wrap=True)
table.add_column("ID", style="dim", no_wrap=True)
table.add_column("Window", no_wrap=True)
table.add_column("Active", width=7)
table.add_column("Muted", width=7)
table.add_column("Title")
table.add_column("URL")
for t in tabs:
active = "[green]✓[/green]" if t.active else ""
muted = "[yellow]✓[/yellow]" if t.muted else ""
row = [
(t.browser or "") if show_browser else None,
str(t.id),
str(t.window_id),
active,
muted,
(t.title or "")[:60],
(t.url or "")[:80],
]
table.add_row(*[value for value in row if value is not None])
console.print(table)
@click.group("tabs")
def tabs_group():
"""Manage browser tabs."""
"""Manage browser tabs."""
@tabs_group.command("list")
@handle_errors
def tabs_list():
"""List all open tabs across all windows."""
tabs = client_from_ctx().tabs.list()
_print_tabs(tabs, show_browser=any(t.browser for t in tabs))
"""List all open tabs across all windows."""
tabs = client_from_ctx().tabs.list()
_print_tabs(tabs, show_browser=any(t.browser for t in tabs))
@tabs_group.command("tree")
@click.option("--urls", "show_urls", is_flag=True, help="Show shortened URLs next to tab titles")
@handle_errors
def tabs_tree():
"""Show tabs grouped as a window/group tree."""
tabs = sorted(client_from_ctx().tabs.list(), key=lambda t: ((t.browser or ""), t.window_id, t.group_id if t.group_id is not None else -1, t.index))
root = Tree("[bold]Tabs[/bold]")
browsers = {}
windows = {}
groups = {}
show_browser = any(t.browser for t in tabs)
for tab in tabs:
browser_key = tab.browser or "local"
browser_node = browsers.setdefault(browser_key, root.add(f"[bold cyan]{browser_key}[/bold cyan]") if show_browser else root)
win_key = (browser_key, tab.window_id)
win_node = windows.get(win_key)
if win_node is None:
win_node = browser_node.add(f"Window {tab.window_id}")
windows[win_key] = win_node
group_label = f"Group {tab.group_id}" if tab.group_id is not None else "Ungrouped"
group_key = (browser_key, tab.window_id, group_label)
group_node = groups.get(group_key)
if group_node is None:
group_node = win_node.add(group_label)
groups[group_key] = group_node
active = " [green]*[/green]" if tab.active else ""
group_node.add(f"[{tab.id}] {tab.title or '(untitled)'}{active} — [dim]{tab.url or ''}[/dim]")
console.print(root)
def tabs_tree(show_urls):
"""Show tabs grouped as a window/group tree."""
client = client_from_ctx()
tabs = sorted(client.tabs.list(), key=_tab_sort_key)
title_limit = tree_title_limit(console=console, show_browser=any(t.browser for t in tabs), show_urls=show_urls)
url_limit = tree_url_limit(title_limit, console=console)
group_info = {
(group.browser or "local", group.id): group
for group in client.groups.list()
}
root = Tree("[bold]Tabs[/bold]")
browsers = {}
windows = {}
groups = {}
show_browser = any(t.browser for t in tabs)
for tab in tabs:
browser_key = tab.browser or "local"
browser_node = browsers.get(browser_key)
if browser_node is None:
browser_node = root.add(Text(browser_key, style="bold cyan")) if show_browser else root
browsers[browser_key] = browser_node
win_key = (browser_key, tab.window_id)
win_node = windows.get(win_key)
if win_node is None:
win_node = browser_node.add(f"Window {tab.window_id}")
windows[win_key] = win_node
if tab.group_id is None:
win_node.add(tab_tree_label(tab, title_limit=title_limit, show_urls=show_urls, url_limit=url_limit))
continue
group_key = (browser_key, tab.window_id, tab.group_id)
group_node = groups.get(group_key)
group = group_info.get((browser_key, tab.group_id))
if group_node is None:
group_node = win_node.add(_group_tree_label(tab.group_id, group, title_limit=title_limit))
groups[group_key] = group_node
group_node.add(tab_tree_label(tab, title_limit=title_limit, show_urls=show_urls, url_limit=url_limit))
print_tree(root, console=console)
@tabs_group.command("close")
@click.argument("tab_id", type=int, required=False)
@@ -82,9 +121,9 @@ def tabs_tree():
@gentle_mode_option("Throttle mode for large close operations.")
@handle_errors
def tabs_close(tab_id, inactive, duplicates, gentle_mode):
"""Close a tab, all inactive tabs, or all duplicate tabs."""
count = client_from_ctx().tabs.close(tab_id, inactive=inactive, duplicates=duplicates, gentle_mode=gentle_mode)
console.print(f"[green]Closed {count} tab(s)[/green]")
"""Close a tab, all inactive tabs, or all duplicate tabs."""
count = client_from_ctx().tabs.close(tab_id, inactive=inactive, duplicates=duplicates, gentle_mode=gentle_mode)
console.print(f"[green]Closed {count} tab(s)[/green]")
@tabs_group.command("move")
@click.argument("tab_id", type=int)
@@ -97,123 +136,123 @@ def tabs_close(tab_id, inactive, duplicates, gentle_mode):
@click.option("--index", type=int, default=None, help="Absolute position index in target")
@handle_errors
def tabs_move(tab_id, forward, backward, group_id, window_id, index):
"""Move a tab. Use --forward/--backward or --right/--left for relative movement."""
client_from_ctx().tabs.move(
tab_id, forward=forward, backward=backward,
group_id=group_id, window_id=window_id, index=index,
)
console.print("[green]Tab moved[/green]")
"""Move a tab. Use --forward/--backward or --right/--left for relative movement."""
client_from_ctx().tabs.move(
tab_id, forward=forward, backward=backward,
group_id=group_id, window_id=window_id, index=index,
)
console.print("[green]Tab moved[/green]")
@tabs_group.command("active")
@click.argument("tab_id", type=int)
@handle_errors
def tabs_active(tab_id):
"""Switch browser focus to a tab."""
client_from_ctx().tabs.activate(tab_id)
console.print(f"[green]Switched to tab {tab_id}[/green]")
"""Switch browser focus to a tab."""
client_from_ctx().tabs.activate(tab_id)
console.print(f"[green]Switched to tab {tab_id}[/green]")
@tabs_group.command("status")
@click.argument("tab_id", type=int, required=False)
@handle_errors
def tabs_status(tab_id):
"""Show status for the active tab or a specific tab."""
tab = client_from_ctx().tabs.status(tab_id)
table = Table(show_header=False)
table.add_column("Field", style="bold cyan")
table.add_column("Value")
table.add_row("ID", str(tab.id))
table.add_row("Window", str(tab.window_id))
table.add_row("Active", "yes" if tab.active else "no")
table.add_row("Muted", "yes" if tab.muted else "no")
table.add_row("Title", tab.title or "")
table.add_row("URL", tab.url or "")
console.print(table)
"""Show status for the active tab or a specific tab."""
tab = client_from_ctx().tabs.status(tab_id)
table = Table(show_header=False)
table.add_column("Field", style="bold cyan")
table.add_column("Value")
table.add_row("ID", str(tab.id))
table.add_row("Window", str(tab.window_id))
table.add_row("Active", "yes" if tab.active else "no")
table.add_row("Muted", "yes" if tab.muted else "no")
table.add_row("Title", tab.title or "")
table.add_row("URL", tab.url or "")
console.print(table)
@tabs_group.command("filter")
@click.argument("pattern")
@handle_errors
def tabs_filter(pattern):
"""List tabs whose URL contains PATTERN."""
_print_tabs(client_from_ctx().tabs.filter(pattern))
"""List tabs whose URL contains PATTERN."""
_print_tabs(client_from_ctx().tabs.filter(pattern))
@tabs_group.command("count")
@click.argument("pattern", required=False)
@handle_errors
def tabs_count(pattern):
"""Count open tabs, optionally filtered by URL PATTERN."""
label = f" matching '{pattern}'" if pattern else ""
print_counts(client_from_ctx().tabs.count(pattern), "tab", single_suffix=label)
"""Count open tabs, optionally filtered by URL PATTERN."""
label = f" matching '{pattern}'" if pattern else ""
print_counts(client_from_ctx().tabs.count(pattern), "tab", single_suffix=label)
@tabs_group.command("query")
@click.argument("search")
@handle_errors
def tabs_query(search):
"""Search tabs by URL or title."""
_print_tabs(client_from_ctx().tabs.query(search))
"""Search tabs by URL or title."""
_print_tabs(client_from_ctx().tabs.query(search))
@tabs_group.command("html")
@click.argument("tab_id", type=int, required=False)
@handle_errors
def tabs_html(tab_id):
"""Print the full HTML of a tab."""
console.print(client_from_ctx().tabs.html(tab_id))
"""Print the full HTML of a tab."""
console.print(client_from_ctx().tabs.html(tab_id))
@tabs_group.command("dedupe")
@gentle_mode_option("Throttle mode for large dedupe operations.")
@handle_errors
def tabs_dedupe(gentle_mode):
"""Close duplicate tabs (keep the first occurrence of each URL)."""
count = client_from_ctx().tabs.dedupe(gentle_mode=gentle_mode)
console.print(f"[green]Closed {count} duplicate tab(s)[/green]")
"""Close duplicate tabs (keep the first occurrence of each URL)."""
count = client_from_ctx().tabs.dedupe(gentle_mode=gentle_mode)
console.print(f"[green]Closed {count} duplicate tab(s)[/green]")
@tabs_group.command("sort")
@click.option("--by", type=click.Choice(["domain", "title", "time"]), default="domain", show_default=True)
@gentle_mode_option("Throttle mode for large sort operations.")
@handle_errors
def tabs_sort(by, gentle_mode):
"""Sort tabs within each window."""
client_from_ctx().tabs.sort(by=by, gentle_mode=gentle_mode)
console.print(f"[green]Tabs sorted by {by}[/green]")
"""Sort tabs within each window."""
client_from_ctx().tabs.sort(by=by, gentle_mode=gentle_mode)
console.print(f"[green]Tabs sorted by {by}[/green]")
@tabs_group.command("merge-windows")
@gentle_mode_option("Throttle mode for large merge operations.")
@handle_errors
def tabs_merge_windows(gentle_mode):
"""Move all tabs into the focused window."""
count = client_from_ctx().tabs.merge_windows(gentle_mode=gentle_mode)
console.print(f"[green]Merged — moved {count} tab(s) into current window[/green]")
"""Move all tabs into the focused window."""
count = client_from_ctx().tabs.merge_windows(gentle_mode=gentle_mode)
console.print(f"[green]Merged — moved {count} tab(s) into current window[/green]")
@tabs_group.command("mute")
@click.argument("tab_id", type=int, required=False)
@handle_errors
def tabs_mute(tab_id):
"""Mute the active tab or a specific tab."""
target = client_from_ctx().tabs.mute(tab_id)
console.print(f"[green]Muted tab {target}[/green]")
"""Mute the active tab or a specific tab."""
target = client_from_ctx().tabs.mute(tab_id)
console.print(f"[green]Muted tab {target}[/green]")
@tabs_group.command("unmute")
@click.argument("tab_id", type=int, required=False)
@handle_errors
def tabs_unmute(tab_id):
"""Unmute the active tab or a specific tab."""
target = client_from_ctx().tabs.unmute(tab_id)
console.print(f"[green]Unmuted tab {target}[/green]")
"""Unmute the active tab or a specific tab."""
target = client_from_ctx().tabs.unmute(tab_id)
console.print(f"[green]Unmuted tab {target}[/green]")
@tabs_group.command("pin")
@click.argument("tab_id", type=int, required=False)
@handle_errors
def tabs_pin(tab_id):
"""Pin the active tab or a specific tab."""
target = client_from_ctx().tabs.pin(tab_id)
console.print(f"[green]Pinned tab {target}[/green]")
"""Pin the active tab or a specific tab."""
target = client_from_ctx().tabs.pin(tab_id)
console.print(f"[green]Pinned tab {target}[/green]")
@tabs_group.command("unpin")
@click.argument("tab_id", type=int, required=False)
@handle_errors
def tabs_unpin(tab_id):
"""Unpin the active tab or a specific tab."""
target = client_from_ctx().tabs.unpin(tab_id)
console.print(f"[green]Unpinned tab {target}[/green]")
"""Unpin the active tab or a specific tab."""
target = client_from_ctx().tabs.unpin(tab_id)
console.print(f"[green]Unpinned tab {target}[/green]")
@tabs_group.command("watch-url")
@click.argument("pattern")
@@ -221,9 +260,9 @@ def tabs_unpin(tab_id):
@click.option("--timeout", type=float, default=30.0, show_default=True, help="Max seconds to wait")
@handle_errors
def tabs_watch_url(pattern, tab_id, timeout):
"""Wait until the active (or specified) tab URL matches regex PATTERN."""
tab = client_from_ctx().tabs.watch_url(pattern, tab_id=tab_id, timeout=timeout)
console.print(f"[green]URL matched:[/green] {tab.url}")
"""Wait until the active (or specified) tab URL matches regex PATTERN."""
tab = client_from_ctx().tabs.watch_url(pattern, tab_id=tab_id, timeout=timeout)
console.print(f"[green]URL matched:[/green] {tab.url}")
@tabs_group.command("screenshot")
@click.argument("output", required=False, metavar="FILE")
@@ -232,21 +271,21 @@ def tabs_watch_url(pattern, tab_id, timeout):
@click.option("--quality", type=int, default=None, help="JPEG quality 0-100")
@handle_errors
def tabs_screenshot(output, tab_id, fmt, quality):
"""Capture a screenshot of the active (or specified) tab.
"""Capture a screenshot of the active (or specified) tab.
Saves to FILE if given, otherwise prints the base64 data URL.
"""
data_url = client_from_ctx().tabs.screenshot(tab_id, format=fmt, quality=quality)
if output:
header = f"data:image/{fmt};base64,"
if not data_url.startswith(header):
raise click.ClickException("Empty or unexpected screenshot response (incognito/protected tab?)")
try:
raw = base64.b64decode(data_url[len(header):])
except binascii.Error as e:
raise click.ClickException(f"Failed to decode screenshot data: {e}")
with open(output, "wb") as f:
f.write(raw)
console.print(f"[green]Screenshot saved:[/green] {output}")
else:
console.print(data_url)
Saves to FILE if given, otherwise prints the base64 data URL.
"""
data_url = client_from_ctx().tabs.screenshot(tab_id, format=fmt, quality=quality)
if output:
header = f"data:image/{fmt};base64,"
if not data_url.startswith(header):
raise click.ClickException("Empty or unexpected screenshot response (incognito/protected tab?)")
try:
raw = base64.b64decode(data_url[len(header):])
except binascii.Error as e:
raise click.ClickException(f"Failed to decode screenshot data: {e}")
with open(output, "wb") as f:
f.write(raw)
console.print(f"[green]Screenshot saved:[/green] {output}")
else:
console.print(data_url)
+53 -51
View File
@@ -1,5 +1,6 @@
import click
from browser_cli.commands import client_from_ctx, handle_errors
from browser_cli.commands.rendering import print_tree, tab_tree_label, tree_title_limit, tree_url_limit
from rich.console import Console
from rich.table import Table
from rich.tree import Tree
@@ -7,81 +8,82 @@ from rich.tree import Tree
console = Console()
def _print_windows(windows: list[dict], *, show_browser: bool = False) -> None:
if not windows:
console.print("[yellow]No windows found[/yellow]")
return
table = Table(show_header=True, header_style="bold cyan")
if show_browser:
table.add_column("Browser")
table.add_column("ID", style="dim", no_wrap=True)
table.add_column("Alias", width=20)
table.add_column("Tabs", width=6)
table.add_column("State", width=12)
for w in windows:
row = [
w.get("browser", "") if show_browser else None,
str(w.get("id", "")),
w.get("alias") or "",
str(w.get("tabCount", "")),
w.get("state") or "",
]
table.add_row(*[value for value in row if value is not None])
console.print(table)
if not windows:
console.print("[yellow]No windows found[/yellow]")
return
table = Table(show_header=True, header_style="bold cyan")
if show_browser:
table.add_column("Browser")
table.add_column("ID", style="dim", no_wrap=True)
table.add_column("Alias", width=20)
table.add_column("Tabs", width=6)
table.add_column("State", width=12)
for w in windows:
row = [
w.get("browser", "") if show_browser else None,
str(w.get("id", "")),
w.get("alias") or "",
str(w.get("tabCount", "")),
w.get("state") or "",
]
table.add_row(*[value for value in row if value is not None])
console.print(table)
@click.group("windows")
def windows_group():
"""Manage browser windows."""
"""Manage browser windows."""
@windows_group.command("list")
@handle_errors
def windows_list():
"""List all browser windows."""
windows = client_from_ctx().windows.list()
_print_windows(windows, show_browser=any("browser" in w for w in windows))
"""List all browser windows."""
windows = client_from_ctx().windows.list()
_print_windows(windows, show_browser=any("browser" in w for w in windows))
@windows_group.command("tree")
@handle_errors
def windows_tree():
"""Show windows and their tabs as a tree."""
client = client_from_ctx()
windows = client.windows.list()
tabs = client.tabs.list()
root = Tree("[bold]Windows[/bold]")
for w in sorted(windows, key=lambda item: (item.get("browser", ""), item.get("id", 0))):
wid = w.get("id")
label = f"Window {wid}"
if w.get("alias"):
label += f" ({w['alias']})"
if w.get("browser"):
label = f"{w['browser']}: " + label
node = root.add(label)
for tab in sorted([t for t in tabs if t.window_id == wid and (not w.get("browser") or t.browser == w.get("browser"))], key=lambda t: t.index):
active = " [green]*[/green]" if tab.active else ""
node.add(f"[{tab.id}] {tab.title or '(untitled)'}{active} — [dim]{tab.url or ''}[/dim]")
console.print(root)
"""Show windows and their tabs as a tree."""
client = client_from_ctx()
windows = client.windows.list()
tabs = client.tabs.list()
root = Tree("[bold]Windows[/bold]")
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)
for w in sorted(windows, key=lambda item: (item.get("browser", ""), item.get("id", 0))):
wid = w.get("id")
label = f"Window {wid}"
if w.get("alias"):
label += f" ({w['alias']})"
if w.get("browser"):
label = f"{w['browser']}: " + label
node = root.add(label)
for tab in sorted([t for t in tabs if t.window_id == wid and (not w.get("browser") or t.browser == w.get("browser"))], key=lambda t: getattr(t, "index", 0)):
node.add(tab_tree_label(tab, title_limit=title_limit, show_urls=True, url_limit=url_limit))
print_tree(root, console=console)
@windows_group.command("rename")
@click.argument("window_id", type=int)
@click.argument("name")
@handle_errors
def windows_rename(window_id, name):
"""Give a window a local alias NAME (stored in native host)."""
client_from_ctx().windows.rename(window_id, name)
console.print(f"[green]Window {window_id} aliased as '{name}'[/green]")
"""Give a window a local alias NAME (stored in native host)."""
client_from_ctx().windows.rename(window_id, name)
console.print(f"[green]Window {window_id} aliased as '{name}'[/green]")
@windows_group.command("close")
@click.argument("window_id", type=int)
@handle_errors
def windows_close(window_id):
"""Close a browser window."""
client_from_ctx().windows.close(window_id)
console.print(f"[green]Window {window_id} closed[/green]")
"""Close a browser window."""
client_from_ctx().windows.close(window_id)
console.print(f"[green]Window {window_id} closed[/green]")
@windows_group.command("open")
@click.argument("url", required=False)
@handle_errors
def windows_open(url):
"""Open a new browser window."""
result = client_from_ctx().windows.open(url)
wid = result.get("id") if isinstance(result, dict) else result
console.print(f"[green]Opened new window[/green] (id: {wid})" + (f" with {url}" if url else ""))
"""Open a new browser window."""
result = client_from_ctx().windows.open(url)
wid = result.get("id") if isinstance(result, dict) else result
console.print(f"[green]Opened new window[/green] (id: {wid})" + (f" with {url}" if url else ""))