Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
edf9056430
|
|||
|
c494e76fe2
|
@@ -77,6 +77,7 @@ class BrowserCLI:
|
||||
id=data["id"],
|
||||
window_id=data.get("windowId", 0),
|
||||
active=data.get("active", False),
|
||||
muted=data.get("muted", False),
|
||||
title=data.get("title") or "",
|
||||
url=data.get("url") or "",
|
||||
group_id=data.get("groupId") or None,
|
||||
@@ -172,6 +173,23 @@ class BrowserCLI:
|
||||
"""Switch browser focus to a tab by ID."""
|
||||
self._cmd("tabs.active", {"tabId": tab_id})
|
||||
|
||||
def tabs_status(self, tab_id: int | None = None) -> Tab:
|
||||
"""Return status for the active tab or a specific tab."""
|
||||
data = self._cmd("tabs.status", {"tabId": tab_id})
|
||||
if not isinstance(data, dict) or "id" not in data:
|
||||
raise RuntimeError("No tab status returned")
|
||||
return self._make_tab(data)
|
||||
|
||||
def tabs_mute(self, tab_id: int | None = None) -> int:
|
||||
"""Mute the active tab or a specific tab. Returns the target tab ID."""
|
||||
result = self._cmd("tabs.mute", {"tabId": tab_id})
|
||||
return result.get("tabId", tab_id) if isinstance(result, dict) else int(tab_id or 0)
|
||||
|
||||
def tabs_unmute(self, tab_id: int | None = None) -> int:
|
||||
"""Unmute the active tab or a specific tab. Returns the target tab ID."""
|
||||
result = self._cmd("tabs.unmute", {"tabId": tab_id})
|
||||
return result.get("tabId", tab_id) if isinstance(result, dict) else int(tab_id or 0)
|
||||
|
||||
def window_active_tab(self, window_id: int) -> Tab:
|
||||
"""Return active tab for a specific browser window."""
|
||||
data = self._cmd("tabs.active_in_window", {"windowId": window_id})
|
||||
|
||||
@@ -44,15 +44,18 @@ def _print_tabs(tabs: list[dict], *, show_browser: bool = False) -> None:
|
||||
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.get("active") else ""
|
||||
muted = "[yellow]✓[/yellow]" if t.get("muted") else ""
|
||||
row = [
|
||||
t.get("browser", "") if show_browser else None,
|
||||
str(t.get("id", "")),
|
||||
str(t.get("windowId", "")),
|
||||
active,
|
||||
muted,
|
||||
(t.get("title") or "")[:60],
|
||||
(t.get("url") or "")[:80],
|
||||
]
|
||||
@@ -122,6 +125,23 @@ def tabs_active(tab_id):
|
||||
console.print(f"[green]Switched to tab {tab_id}[/green]")
|
||||
|
||||
|
||||
@tabs_group.command("status")
|
||||
@click.argument("tab_id", type=int, required=False)
|
||||
def tabs_status(tab_id):
|
||||
"""Show status for the active tab or a specific tab."""
|
||||
tab = _handle("tabs.status", {"tabId": tab_id}) or {}
|
||||
table = Table(show_header=False)
|
||||
table.add_column("Field", style="bold cyan")
|
||||
table.add_column("Value")
|
||||
table.add_row("ID", str(tab.get("id", "")))
|
||||
table.add_row("Window", str(tab.get("windowId", "")))
|
||||
table.add_row("Active", "yes" if tab.get("active") else "no")
|
||||
table.add_row("Muted", "yes" if tab.get("muted") else "no")
|
||||
table.add_row("Title", tab.get("title") or "")
|
||||
table.add_row("URL", tab.get("url") or "")
|
||||
console.print(table)
|
||||
|
||||
|
||||
@tabs_group.command("filter")
|
||||
@click.argument("pattern")
|
||||
def tabs_filter(pattern):
|
||||
@@ -198,3 +218,21 @@ def tabs_merge_windows():
|
||||
result = _handle("tabs.merge_windows")
|
||||
count = result.get("moved", 0) if isinstance(result, dict) else 0
|
||||
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)
|
||||
def tabs_mute(tab_id):
|
||||
"""Mute the active tab or a specific tab."""
|
||||
result = _handle("tabs.mute", {"tabId": tab_id})
|
||||
target = result.get("tabId", tab_id) if isinstance(result, dict) else tab_id
|
||||
console.print(f"[green]Muted tab {target}[/green]")
|
||||
|
||||
|
||||
@tabs_group.command("unmute")
|
||||
@click.argument("tab_id", type=int, required=False)
|
||||
def tabs_unmute(tab_id):
|
||||
"""Unmute the active tab or a specific tab."""
|
||||
result = _handle("tabs.unmute", {"tabId": tab_id})
|
||||
target = result.get("tabId", tab_id) if isinstance(result, dict) else tab_id
|
||||
console.print(f"[green]Unmuted tab {target}[/green]")
|
||||
|
||||
@@ -29,6 +29,7 @@ class Tab:
|
||||
id: int
|
||||
window_id: int
|
||||
active: bool
|
||||
muted: bool
|
||||
title: str
|
||||
url: str
|
||||
group_id: int | None = None
|
||||
@@ -48,6 +49,14 @@ class Tab:
|
||||
"""Switch browser focus to this tab."""
|
||||
self._b()._cmd("tabs.active", {"tabId": self.id})
|
||||
|
||||
def mute(self) -> None:
|
||||
"""Mute this tab."""
|
||||
self._b()._cmd("tabs.mute", {"tabId": self.id})
|
||||
|
||||
def unmute(self) -> None:
|
||||
"""Unmute this tab."""
|
||||
self._b()._cmd("tabs.unmute", {"tabId": self.id})
|
||||
|
||||
def reload(self) -> None:
|
||||
"""Reload this tab."""
|
||||
self._b()._cmd("navigate.reload", {"tabId": self.id})
|
||||
|
||||
+42
-7
@@ -134,6 +134,7 @@ async function dispatch(command, args) {
|
||||
case "tabs.move": return tabsMove(args);
|
||||
case "tabs.active": return tabsActive(args);
|
||||
case "tabs.active_in_window": return tabsActiveInWindow(args);
|
||||
case "tabs.status": return tabsStatus(args);
|
||||
case "tabs.filter": return tabsFilter(args);
|
||||
case "tabs.count": return tabsCount(args);
|
||||
case "tabs.query": return tabsQuery(args);
|
||||
@@ -141,6 +142,8 @@ async function dispatch(command, args) {
|
||||
case "tabs.dedupe": return tabsDedupe();
|
||||
case "tabs.sort": return tabsSort(args);
|
||||
case "tabs.merge_windows": return tabsMergeWindows();
|
||||
case "tabs.mute": return tabsMute(args);
|
||||
case "tabs.unmute": return tabsUnmute(args);
|
||||
|
||||
// ── Groups ────────────────────────────────────────────────────────────
|
||||
case "group.list": return groupList();
|
||||
@@ -273,15 +276,10 @@ async function tabsList() {
|
||||
for (const w of windows) {
|
||||
for (const t of w.tabs) {
|
||||
tabs.push({
|
||||
id: t.id,
|
||||
windowId: t.windowId,
|
||||
...tabInfo(t),
|
||||
windowAlias: aliases[t.windowId] || null,
|
||||
active: t.active,
|
||||
pinned: t.pinned,
|
||||
title: t.title,
|
||||
url: t.url,
|
||||
favIconUrl: t.favIconUrl,
|
||||
groupId: t.groupId >= 0 ? t.groupId : null,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -344,6 +342,11 @@ async function tabsActiveInWindow({ windowId }) {
|
||||
return tabInfo(tab);
|
||||
}
|
||||
|
||||
async function tabsStatus({ tabId }) {
|
||||
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
|
||||
return tabInfo(tab);
|
||||
}
|
||||
|
||||
async function tabsFilter({ pattern }) {
|
||||
const all = await chrome.tabs.query({});
|
||||
return all.filter(t => t.url && t.url.includes(pattern)).map(tabInfo);
|
||||
@@ -439,8 +442,27 @@ async function tabsMergeWindows() {
|
||||
return { moved };
|
||||
}
|
||||
|
||||
async function tabsMute({ tabId }) {
|
||||
const tab = await resolveTabForDirectAction(tabId, "mute");
|
||||
await chrome.tabs.update(tab.id, { muted: true });
|
||||
return { tabId: tab.id, muted: true };
|
||||
}
|
||||
|
||||
async function tabsUnmute({ tabId }) {
|
||||
const tab = await resolveTabForDirectAction(tabId, "unmute");
|
||||
await chrome.tabs.update(tab.id, { muted: false });
|
||||
return { tabId: tab.id, muted: false };
|
||||
}
|
||||
|
||||
function tabInfo(t) {
|
||||
return { id: t.id, windowId: t.windowId, active: t.active, title: t.title, url: t.url };
|
||||
return {
|
||||
id: t.id,
|
||||
windowId: t.windowId,
|
||||
active: t.active,
|
||||
muted: Boolean(t.mutedInfo && t.mutedInfo.muted),
|
||||
title: t.title,
|
||||
url: t.url,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Groups ────────────────────────────────────────────────────────────────────
|
||||
@@ -1222,6 +1244,19 @@ async function getActiveTab() {
|
||||
|| activeTabs[0];
|
||||
}
|
||||
|
||||
async function resolveTabForDirectAction(tabId, actionName) {
|
||||
if (tabId != null) {
|
||||
return chrome.tabs.get(tabId);
|
||||
}
|
||||
const allTabs = await chrome.tabs.query({});
|
||||
if (allTabs.length !== 1) {
|
||||
throw new Error(
|
||||
`Refusing to ${actionName} without explicit tab ID when ${allTabs.length} tabs are open`
|
||||
);
|
||||
}
|
||||
return allTabs[0];
|
||||
}
|
||||
|
||||
async function resolveGroupId(nameOrId) {
|
||||
const asInt = parseInt(nameOrId);
|
||||
if (!isNaN(asInt)) return asInt;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "browser-cli",
|
||||
"version": "0.5.8",
|
||||
"version": "0.5.12",
|
||||
"description": "Control your browser from the terminal via browser-cli",
|
||||
"permissions": [
|
||||
"tabs",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "browser-cli"
|
||||
version = "0.5.8"
|
||||
version = "0.5.12"
|
||||
description = "Control your real running browser from the terminal via a Chrome extension"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
|
||||
@@ -12,6 +12,7 @@ def test_tabs_list(browser):
|
||||
assert "windowId" in first
|
||||
assert "url" in first
|
||||
assert "title" in first
|
||||
assert "muted" in first
|
||||
|
||||
|
||||
def test_tabs_count(browser):
|
||||
@@ -51,6 +52,13 @@ def test_tabs_active_in_window(browser):
|
||||
assert result["windowId"] == active["windowId"]
|
||||
|
||||
|
||||
def test_tabs_status(browser):
|
||||
result = browser("tabs.status", {})
|
||||
assert isinstance(result, dict)
|
||||
assert "id" in result
|
||||
assert "muted" in result
|
||||
|
||||
|
||||
def test_tabs_html(browser, http_tab):
|
||||
html = browser("tabs.html", {"tabId": http_tab["id"]})
|
||||
assert isinstance(html, str)
|
||||
@@ -114,3 +122,32 @@ def test_tabs_merge_windows_no_crash(browser):
|
||||
result = browser("tabs.merge_windows")
|
||||
assert isinstance(result, dict)
|
||||
assert "moved" in result
|
||||
|
||||
|
||||
def test_tabs_mute_and_unmute(browser, http_tab):
|
||||
muted = browser("tabs.mute", {"tabId": http_tab["id"]})
|
||||
assert isinstance(muted, dict)
|
||||
assert muted["tabId"] == http_tab["id"]
|
||||
assert muted["muted"] is True
|
||||
listed = browser("tabs.list")
|
||||
listed_tab = next(t for t in listed if t["id"] == http_tab["id"])
|
||||
assert listed_tab["muted"] is True
|
||||
|
||||
unmuted = browser("tabs.unmute", {"tabId": http_tab["id"]})
|
||||
assert isinstance(unmuted, dict)
|
||||
assert unmuted["tabId"] == http_tab["id"]
|
||||
assert unmuted["muted"] is False
|
||||
listed = browser("tabs.list")
|
||||
listed_tab = next(t for t in listed if t["id"] == http_tab["id"])
|
||||
assert listed_tab["muted"] is False
|
||||
status = browser("tabs.status", {"tabId": http_tab["id"]})
|
||||
assert status["muted"] is False
|
||||
|
||||
|
||||
def test_tabs_mute_requires_explicit_tab_when_multiple_tabs_open(browser):
|
||||
opened = browser("navigate.open", {"url": "https://example.com", "background": True})
|
||||
try:
|
||||
with pytest.raises(RuntimeError, match="Refusing to mute without explicit tab ID"):
|
||||
browser("tabs.mute", {})
|
||||
finally:
|
||||
browser("tabs.close", {"tabId": opened["id"]})
|
||||
|
||||
Reference in New Issue
Block a user