Files
browser-cli/browser_cli/__init__.py
T
daniel156161 61b774a7a4
Package Extension / package-extension (push) Successful in 12s
Build & Publish Package / publish (push) Successful in 22s
add multi browser mode to arragate data from all browsers by tabs list, tabs count, group list, group count and windows list
remove (unnamed) into the group names just leave it a empty string, remove Focused on windows how should the browser know what windows are focused
2026-04-10 12:49:51 +02:00

384 lines
17 KiB
Python

"""
browser_cli — Python API for controlling your running browser.
Usage:
from browser_cli import BrowserCLI
b = BrowserCLI()
tabs = b.tabs_list() # list[Tab]
tabs[0].close()
tabs[0].move(forward=True)
groups = b.group_list() # list[Group]
groups[0].tabs()
groups[0].add_tab("https://example.com")
# When multiple browser instances are active, pass the alias:
b = BrowserCLI(browser="brave")
"""
from collections.abc import Callable, Iterable
from dataclasses import dataclass
from browser_cli.client import BrowserNotConnected, active_browser_targets, send_command
from browser_cli.models import Group, Tab
__all__ = ["BrowserCLI", "BrowserCounts", "BrowserNotConnected", "Tab", "Group"]
@dataclass(frozen=True)
class BrowserCounts:
"""Aggregated per-browser counts returned in implicit multi-browser mode."""
total: int
by_browser: dict[str, int]
class BrowserCLI:
def __init__(self, browser: str | None = None):
"""
Args:
browser: Profile alias to target. Required when multiple browser
instances are active. Equivalent to ``--browser`` on the CLI.
"""
self._browser = browser
def _cmd(self, command: str, args: dict | None = None):
return send_command(command, args, profile=self._browser)
def _multi_browser_targets(self):
if self._browser is not None:
return []
targets = active_browser_targets()
if len(targets) <= 1:
return []
return targets
def _collect_multi_browser(self, command: str, args: dict | None = None):
results = []
for target in self._multi_browser_targets():
try:
data = send_command(command, args, profile=target.profile)
except (BrowserNotConnected, RuntimeError):
continue
results.append((target, data))
if results:
return results
if self._multi_browser_targets():
raise BrowserNotConnected(
"Cannot resolve a browser socket automatically.\n"
"Make sure the browser is running with the browser-cli extension enabled,\n"
"or pass --browser <alias> / set BROWSER_CLI_PROFILE to a known alias."
)
return []
# ── Internal factories ────────────────────────────────────────────────
def _make_tab(self, data: dict, *, browser_profile: str | None = None, browser_name: str | None = None) -> Tab:
tab = Tab(
id=data["id"],
window_id=data.get("windowId", 0),
active=data.get("active", False),
title=data.get("title") or "",
url=data.get("url") or "",
group_id=data.get("groupId") or None,
browser=browser_name,
)
tab._browser = self if browser_profile is None else BrowserCLI(browser=browser_profile)
return tab
def _make_group(self, data: dict, *, browser_profile: str | None = None, browser_name: str | None = None) -> Group:
group = Group(
id=data["id"],
title=data.get("title") or "",
color=data.get("color") or "",
collapsed=data.get("collapsed", False),
tab_count=data.get("tabCount", 0),
browser=browser_name,
)
group._browser = self if browser_profile is None else BrowserCLI(browser=browser_profile)
return group
# ── Navigation ────────────────────────────────────────────────────────
def open(self, url: str, *, background: bool = False, window: str | None = None, group: str | None = None) -> None:
self._cmd("navigate.open", {"url": url, "background": background, "window": window, "group": group})
def reload(self, tab_id: int | None = None) -> None:
self._cmd("navigate.reload", {"tabId": tab_id})
def hard_reload(self, tab_id: int | None = None) -> None:
self._cmd("navigate.hard_reload", {"tabId": tab_id})
def back(self, tab_id: int | None = None) -> None:
self._cmd("navigate.back", {"tabId": tab_id})
def forward(self, tab_id: int | None = None) -> None:
self._cmd("navigate.forward", {"tabId": tab_id})
def focus_url(self, pattern: str) -> None:
self._cmd("navigate.focus", {"pattern": pattern})
# ── Search ────────────────────────────────────────────────────────────
def search(
self, engine: str, query: str, *,
background: bool = False, window: str | None = None, group: str | None = None,
) -> None:
"""Open a search query in the given engine (e.g. 'google', 'youtube', 'ddg')."""
from urllib.parse import quote_plus
from browser_cli.commands.search import ENGINES
template = ENGINES.get(engine)
if template is None:
raise ValueError(f"Unknown search engine '{engine}'. Available: {', '.join(ENGINES)}")
url = template.format(query=quote_plus(query))
self._cmd("navigate.open", {"url": url, "background": background, "window": window, "group": group})
# ── Tabs ──────────────────────────────────────────────────────────────
def tabs_list(self) -> list[Tab]:
"""Return all open tabs across all windows.
When multiple browsers are active and no browser was specified, each Tab
includes ``tab.browser`` naming its source browser.
"""
multi_results = self._collect_multi_browser("tabs.list", {})
if multi_results:
return [
self._make_tab(tab, browser_profile=target.profile, browser_name=target.display_name)
for target, tabs in multi_results
for tab in (tabs or [])
]
return [self._make_tab(t) for t in (self._cmd("tabs.list", {}) or [])]
def tabs_close(self, tab_id: int | None = None, *, inactive: bool = False, duplicates: bool = False) -> int:
"""Close tab(s). Returns the number of tabs closed."""
result = self._cmd("tabs.close", {"tabId": tab_id, "inactive": inactive, "duplicates": duplicates})
return result.get("closed", 1) if isinstance(result, dict) else 1
def tabs_move(
self, tab_id: int, *,
forward: bool = False, backward: bool = False,
group_id: int | None = None, window_id: int | None = None, index: int | None = None,
) -> None:
self._cmd("tabs.move", {
"tabId": tab_id, "forward": forward, "backward": backward,
"groupId": group_id, "windowId": window_id, "index": index,
})
def tabs_active(self, tab_id: int) -> None:
"""Switch browser focus to a tab by ID."""
self._cmd("tabs.active", {"tabId": tab_id})
def tabs_filter(self, pattern_or_filter: str | Callable[[Tab], bool] | Callable[[list[Tab]], Iterable[Tab]]) -> list[Tab]:
"""Return tabs filtered by pattern or a Python callable."""
if isinstance(pattern_or_filter, str):
return [self._make_tab(t) for t in (self._cmd("tabs.filter", {"pattern": pattern_or_filter}) or [])]
return self._apply_tab_filter(pattern_or_filter)
def tabs_count(self, pattern: str | None = None) -> int | BrowserCounts:
"""Count open tabs, optionally filtered by URL pattern.
Returns ``BrowserCounts`` in implicit multi-browser mode.
"""
multi_results = self._collect_multi_browser("tabs.count", {"pattern": pattern})
if multi_results:
by_browser = {target.display_name: int(count or 0) for target, count in multi_results}
return BrowserCounts(total=sum(by_browser.values()), by_browser=by_browser)
return self._cmd("tabs.count", {"pattern": pattern})
def tabs_query(self, search: str) -> list[Tab]:
"""Search tabs by URL or title."""
return [self._make_tab(t) for t in (self._cmd("tabs.query", {"search": search}) or [])]
def tabs_html(self, tab_id: int | None = None) -> str:
"""Return the full HTML source of the active (or specified) tab."""
return self._cmd("tabs.html", {"tabId": tab_id}) or ""
def tabs_dedupe(self) -> int:
"""Close duplicate tabs (keep the first occurrence of each URL). Returns count closed."""
result = self._cmd("tabs.dedupe", {})
return result.get("closed", 0) if isinstance(result, dict) else 0
def tabs_sort(self, by: str = "domain") -> None:
"""Sort tabs within each window. *by* is one of 'domain', 'title', 'time'."""
self._cmd("tabs.sort", {"by": by})
def tabs_merge_windows(self) -> int:
"""Move all tabs into the focused window. Returns count moved."""
result = self._cmd("tabs.merge_windows", {})
return result.get("moved", 0) if isinstance(result, dict) else 0
def tabs_close_inactive(self) -> int:
"""Close all inactive tabs. Returns count closed."""
result = self._cmd("tabs.close", {"inactive": True})
return result.get("closed", 0) if isinstance(result, dict) else 0
def tabs_close_duplicates(self) -> int:
"""Close duplicate tabs. Returns count closed."""
result = self._cmd("tabs.close", {"duplicates": True})
return result.get("closed", 0) if isinstance(result, dict) else 0
# ── Tab Groups ────────────────────────────────────────────────────────
def group_list(self) -> list[Group]:
"""Return all tab groups.
When multiple browsers are active and no browser was specified, each Group
includes ``group.browser`` naming its source browser.
"""
multi_results = self._collect_multi_browser("group.list", {})
if multi_results:
return [
self._make_group(group, browser_profile=target.profile, browser_name=target.display_name)
for target, groups in multi_results
for group in (groups or [])
]
return [self._make_group(g) for g in (self._cmd("group.list", {}) or [])]
def group_tabs(self, group_id: int) -> list[Tab]:
"""Return all tabs inside a group."""
return [self._make_tab(t) for t in (self._cmd("group.tabs", {"groupId": group_id}) or [])]
def group_count(self) -> int | BrowserCounts:
"""Return the number of tab groups.
Returns ``BrowserCounts`` in implicit multi-browser mode.
"""
multi_results = self._collect_multi_browser("group.count", {})
if multi_results:
by_browser = {target.display_name: int(count or 0) for target, count in multi_results}
return BrowserCounts(total=sum(by_browser.values()), by_browser=by_browser)
return self._cmd("group.count", {})
def group_query(self, search: str) -> list[Group]:
"""Search groups by name."""
return [self._make_group(g) for g in (self._cmd("group.query", {"search": search}) or [])]
def group_close(self, group_id: int) -> None:
"""Ungroup (and close) a tab group by ID."""
self._cmd("group.close", {"groupId": group_id})
def group_create(self, name: str) -> Group:
"""Create a new tab group with *name*. Returns the created Group."""
data = self._cmd("group.open", {"name": name})
return self._make_group(data) if isinstance(data, dict) else Group(id=data, title=name, color="", collapsed=False, tab_count=0)
def group_add_tab(self, group: str | int, url: str | None = None) -> int | None:
"""Open a new tab (optionally at URL) inside a group. Returns the new tab ID."""
result = self._cmd("group.add_tab", {"group": str(group), "url": url})
return result.get("tabId") if isinstance(result, dict) else result
def group_move(self, group: str | int, *, forward: bool = False, backward: bool = False) -> None:
"""Move a tab group forward or backward."""
self._cmd("group.move", {"group": str(group), "forward": forward, "backward": backward})
# ── Windows ───────────────────────────────────────────────────────────
def windows_list(self) -> list[dict]:
"""Return browser windows.
In implicit multi-browser mode each window dict includes a ``browser`` key.
"""
multi_results = self._collect_multi_browser("windows.list", {})
if multi_results:
return [
{**window, "browser": target.display_name}
for target, windows in multi_results
for window in (windows or [])
]
return self._cmd("windows.list", {})
def windows_rename(self, window_id: int, name: str) -> None:
self._cmd("windows.rename", {"windowId": window_id, "name": name})
def windows_close(self, window_id: int) -> None:
self._cmd("windows.close", {"windowId": window_id})
def windows_open(self, profile: str | None = None) -> dict:
return self._cmd("windows.open", {"profile": profile})
# ── DOM ───────────────────────────────────────────────────────────────
def dom_query(self, selector: str) -> list[dict]:
return self._cmd("dom.query", {"selector": selector})
def dom_click(self, selector: str) -> None:
self._cmd("dom.click", {"selector": selector})
def dom_type(self, selector: str, text: str) -> None:
self._cmd("dom.type", {"selector": selector, "text": text})
def dom_attr(self, selector: str, attr: str) -> list[str]:
return self._cmd("dom.attr", {"selector": selector, "attr": attr})
def dom_text(self, selector: str) -> list[str]:
return self._cmd("dom.text", {"selector": selector})
def dom_exists(self, selector: str) -> bool:
return self._cmd("dom.exists", {"selector": selector})
# ── Extract ───────────────────────────────────────────────────────────
def extract_links(self) -> list[dict]:
return self._cmd("extract.links", {})
def extract_images(self) -> list[dict]:
return self._cmd("extract.images", {})
def extract_text(self) -> str:
return self._cmd("extract.text", {})
def extract_json(self, selector: str):
return self._cmd("extract.json", {"selector": selector})
def extract_markdown(self, selector: str | None = None) -> str:
return self._cmd("extract.markdown", {"selector": selector}) or ""
# ── Session ───────────────────────────────────────────────────────────
def session_save(self, name: str) -> None:
self._cmd("session.save", {"name": name})
def session_load(self, name: str) -> None:
self._cmd("session.load", {"name": name})
def session_diff(self, name_a: str, name_b: str) -> dict:
return self._cmd("session.diff", {"nameA": name_a, "nameB": name_b})
def session_list(self) -> list[dict]:
return self._cmd("session.list", {})
def session_remove(self, name: str) -> None:
self._cmd("session.remove", {"name": name})
def session_auto_save(self, enabled: bool) -> None:
self._cmd("session.auto_save", {"enabled": enabled})
# ── Misc ──────────────────────────────────────────────────────────────
def clients(self) -> list[dict]:
return self._cmd("clients.list", {})
def _apply_tab_filter(self, filter_fn: Callable[[Tab], bool] | Callable[[list[Tab]], Iterable[Tab]]) -> list[Tab]:
tabs = self.tabs_list()
try:
transformed = filter_fn(tabs)
except Exception:
transformed = None
if isinstance(transformed, list):
return transformed
if isinstance(transformed, tuple):
return list(transformed)
if isinstance(transformed, set):
return list(transformed)
if transformed is tabs:
return tabs
if isinstance(transformed, bool):
return [tab for tab in tabs if filter_fn(tab)]
try:
return list(transformed)
except TypeError:
return [tab for tab in tabs if filter_fn(tab)]