3e3b8d529c
- Change nav open and open-wait to avoid activating newly created tabs unless --focus is explicitly requested. - Send background=true for default opens so older or remote extensions also avoid stealing focus even if they ignore the new focus flag. - Remove the redundant --bg flag from navigation and search CLI commands now that no-focus/background behavior is the default. - Thread focus support through the sync SDK, async SDK, tab helpers, and workflow decorators. - Update README and demo usage to document the new default and --focus opt-in. - Bump package and extension metadata to 0.12.3. - Add regression coverage for CLI help, wire payloads, and extension behavior.
214 lines
7.8 KiB
Python
214 lines
7.8 KiB
Python
"""Tabs namespace: ``b.tabs.*``."""
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable, Iterable
|
|
|
|
from browser_cli.models import BrowserCounts, Tab
|
|
from browser_cli.sdk.base import Namespace
|
|
|
|
class TabsNS(Namespace):
|
|
"""List, open, close, move, and inspect browser tabs."""
|
|
|
|
def 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.
|
|
"""
|
|
return self.multi_list("tabs.list", {}, self.tab_from_target)
|
|
|
|
def open(
|
|
self,
|
|
url: str,
|
|
*,
|
|
wait: bool = False,
|
|
timeout: float = 30.0,
|
|
background: bool = False,
|
|
focus: bool = False,
|
|
window: str | None = None,
|
|
group: str | None = None,
|
|
) -> Tab:
|
|
"""Open *url* in a new tab and return a bound :class:`Tab`.
|
|
|
|
Set ``wait=True`` to block until the page reaches ``readyState=complete``.
|
|
Pass ``focus=True`` to explicitly bring the created tab/window forward.
|
|
"""
|
|
if wait:
|
|
return self._c.nav.open_wait(url, timeout=timeout, background=background, focus=focus, window=window, group=group)
|
|
return self.require_tab(
|
|
self.command("navigate.open", {"url": url, "background": background or not focus, "focus": focus, "window": window, "group": group}),
|
|
"navigate.open returned unexpected data",
|
|
)
|
|
|
|
def get(self, tab_id: int) -> Tab:
|
|
"""Return a specific tab by ID."""
|
|
return self.status(tab_id)
|
|
|
|
def active(self) -> Tab:
|
|
"""Return the active tab."""
|
|
return self.status()
|
|
|
|
def query(self, search: str) -> list[Tab]:
|
|
"""Search tabs by URL or title."""
|
|
return [self.tab_from(t) for t in (self.command("tabs.query", {"search": search}) or [])]
|
|
|
|
def first(self, search: str) -> Tab | None:
|
|
"""Return the first tab matching *search*, or ``None``."""
|
|
matches = self.query(search)
|
|
return matches[0] if matches else None
|
|
|
|
def close(
|
|
self,
|
|
tab_id: int | None = None,
|
|
*,
|
|
tab_ids: Iterable[int | Tab] | None = None,
|
|
inactive: bool = False,
|
|
duplicates: bool = False,
|
|
gentle_mode: str = "auto",
|
|
) -> int:
|
|
"""Close tab(s). Returns the number of tabs closed.
|
|
|
|
Pass ``tab_ids`` to close many tabs in a single round-trip. Accepts tab
|
|
IDs or :class:`Tab` objects. ``gentle_mode`` (auto/normal/gentle/ultra)
|
|
controls throttling of large close operations.
|
|
"""
|
|
ids = None
|
|
if tab_ids is not None:
|
|
ids = [t.id if isinstance(t, Tab) else t for t in tab_ids]
|
|
result = self.command("tabs.close", {
|
|
"tabId": tab_id,
|
|
"tabIds": ids,
|
|
"inactive": inactive,
|
|
"duplicates": duplicates,
|
|
"gentleMode": gentle_mode,
|
|
})
|
|
return self.field(result, "closed", 1)
|
|
|
|
def close_inactive(self) -> int:
|
|
"""Close all inactive tabs. Returns count closed."""
|
|
return self.field(self.command("tabs.close", {"inactive": True}), "closed", 0)
|
|
|
|
def close_duplicates(self) -> int:
|
|
"""Close duplicate tabs. Returns count closed."""
|
|
return self.field(self.command("tabs.close", {"duplicates": True}), "closed", 0)
|
|
|
|
def 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.command("tabs.move", {
|
|
"tabId": tab_id, "forward": forward, "backward": backward,
|
|
"groupId": group_id, "windowId": window_id, "index": index,
|
|
})
|
|
|
|
def activate(self, tab_id: int) -> None:
|
|
"""Switch browser focus to a tab by ID."""
|
|
self.command("tabs.active", {"tabId": tab_id})
|
|
|
|
def status(self, tab_id: int | None = None) -> Tab:
|
|
"""Return status for the active tab or a specific tab."""
|
|
return self.require_tab(self.command("tabs.status", {"tabId": tab_id}), "No tab status returned")
|
|
|
|
def mute(self, tab_id: int | None = None) -> int:
|
|
"""Mute the active tab or a specific tab. Returns the target tab ID."""
|
|
return self.toggle_tab("tabs.mute", tab_id)
|
|
|
|
def unmute(self, tab_id: int | None = None) -> int:
|
|
"""Unmute the active tab or a specific tab. Returns the target tab ID."""
|
|
return self.toggle_tab("tabs.unmute", tab_id)
|
|
|
|
def pin(self, tab_id: int | None = None) -> int:
|
|
"""Pin the active tab or a specific tab. Returns the target tab ID."""
|
|
return self.toggle_tab("tabs.pin", tab_id)
|
|
|
|
def unpin(self, tab_id: int | None = None) -> int:
|
|
"""Unpin the active tab or a specific tab. Returns the target tab ID."""
|
|
return self.toggle_tab("tabs.unpin", tab_id)
|
|
|
|
def watch_url(self, pattern: str, *, tab_id: int | None = None, timeout: float = 30.0) -> Tab:
|
|
"""Block until the tab URL matches regex pattern. Returns the Tab."""
|
|
return self.require_tab(
|
|
self.command("tabs.watch_url", {"pattern": pattern, "tabId": tab_id, "timeout": int(timeout * 1000)}),
|
|
"tabs.watch_url returned unexpected data",
|
|
)
|
|
|
|
def wait_for_load(
|
|
self,
|
|
tab_id: int | None = None,
|
|
*,
|
|
timeout: float = 30.0,
|
|
ready_state: str = "complete",
|
|
) -> Tab:
|
|
"""Block until the tab finishes loading. Returns the Tab when ready.
|
|
|
|
Args:
|
|
tab_id: Tab to watch. Defaults to the active tab.
|
|
timeout: Max seconds to wait before raising ``RuntimeError``.
|
|
ready_state: ``"complete"`` (default) or ``"interactive"``.
|
|
"""
|
|
return self.require_tab(
|
|
self.command("navigate.wait", {
|
|
"tabId": tab_id,
|
|
"timeout": int(timeout * 1000),
|
|
"readyState": ready_state,
|
|
}),
|
|
"navigate.wait returned unexpected data",
|
|
)
|
|
|
|
def screenshot(
|
|
self,
|
|
tab_id: int | None = None,
|
|
*,
|
|
format: str = "png",
|
|
quality: int | None = None,
|
|
) -> str:
|
|
"""Capture the visible area of a tab. Returns a base64 data URL.
|
|
|
|
Args:
|
|
tab_id: Tab to capture. Defaults to the active tab.
|
|
format: ``"png"`` (default) or ``"jpeg"``.
|
|
quality: JPEG quality 0-100 (ignored for PNG).
|
|
"""
|
|
result = self.command("tabs.screenshot", {"tabId": tab_id, "format": format, "quality": quality})
|
|
return self.field(result, "dataUrl", "", fallback=str(result))
|
|
|
|
def active_in_window(self, window_id: int) -> Tab:
|
|
"""Return active tab for a specific browser window."""
|
|
return self.require_tab(
|
|
self.command("tabs.active_in_window", {"windowId": window_id}),
|
|
f"No active tab found for window {window_id}",
|
|
)
|
|
|
|
def filter(
|
|
self,
|
|
pattern_or_filter: str | Callable[[Tab], bool] | Callable[[list[Tab]], Iterable[Tab]],
|
|
) -> list[Tab]:
|
|
"""Return tabs filtered by URL pattern or a Python callable."""
|
|
if isinstance(pattern_or_filter, str):
|
|
return [self.tab_from(t) for t in (self.command("tabs.filter", {"pattern": pattern_or_filter}) or [])]
|
|
return self.apply_tab_filter(pattern_or_filter)
|
|
|
|
def count(self, pattern: str | None = None) -> "int | BrowserCounts":
|
|
"""Count open tabs, optionally filtered by URL pattern.
|
|
|
|
Returns ``BrowserCounts`` in implicit multi-browser mode.
|
|
"""
|
|
return self.multi_count("tabs.count", {"pattern": pattern})
|
|
|
|
def html(self, tab_id: int | None = None) -> str:
|
|
"""Return the full HTML source of the active (or specified) tab."""
|
|
return self.command("tabs.html", {"tabId": tab_id}) or ""
|
|
|
|
def dedupe(self, *, gentle_mode: str = "auto") -> int:
|
|
"""Close duplicate tabs (keep the first occurrence of each URL). Returns count closed."""
|
|
return self.field(self.command("tabs.dedupe", {"gentleMode": gentle_mode}), "closed", 0)
|
|
|
|
def sort(self, by: str = "domain", *, gentle_mode: str = "auto") -> None:
|
|
"""Sort tabs within each window. *by* is one of 'domain', 'title', 'time'."""
|
|
self.command("tabs.sort", {"by": by, "gentleMode": gentle_mode})
|
|
|
|
def merge_windows(self, *, gentle_mode: str = "auto") -> int:
|
|
"""Move all tabs into the focused window. Returns count moved."""
|
|
return self.field(self.command("tabs.merge_windows", {"gentleMode": gentle_mode}), "moved", 0)
|