diff --git a/browser_cli/__init__.py b/browser_cli/__init__.py index 00d9607..aeee79b 100644 --- a/browser_cli/__init__.py +++ b/browser_cli/__init__.py @@ -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,16 @@ class BrowserCLI: """Switch browser focus to a tab by ID.""" self._cmd("tabs.active", {"tabId": tab_id}) + 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}) diff --git a/browser_cli/commands/tabs.py b/browser_cli/commands/tabs.py index df36538..d7053b4 100644 --- a/browser_cli/commands/tabs.py +++ b/browser_cli/commands/tabs.py @@ -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], ] @@ -198,3 +201,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]") diff --git a/browser_cli/models.py b/browser_cli/models.py index 0f18224..066a924 100644 --- a/browser_cli/models.py +++ b/browser_cli/models.py @@ -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}) diff --git a/extension/background.js b/extension/background.js index ce6f4b3..3057264 100644 --- a/extension/background.js +++ b/extension/background.js @@ -141,6 +141,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(); @@ -439,8 +441,27 @@ async function tabsMergeWindows() { return { moved }; } +async function tabsMute({ tabId }) { + const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab(); + await chrome.tabs.update(tab.id, { muted: true }); + return { tabId: tab.id, muted: true }; +} + +async function tabsUnmute({ tabId }) { + const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab(); + 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 ──────────────────────────────────────────────────────────────────── diff --git a/extension/manifest.json b/extension/manifest.json index 18dce74..d804061 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "browser-cli", - "version": "0.5.8", + "version": "0.5.10", "description": "Control your browser from the terminal via browser-cli", "permissions": [ "tabs", diff --git a/pyproject.toml b/pyproject.toml index 54867c8..7d68acc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browser-cli" -version = "0.5.8" +version = "0.5.10" description = "Control your real running browser from the terminal via a Chrome extension" requires-python = ">=3.10" dependencies = [ diff --git a/tests/test_tabs.py b/tests/test_tabs.py index 669b82c..45d5410 100644 --- a/tests/test_tabs.py +++ b/tests/test_tabs.py @@ -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): @@ -114,3 +115,21 @@ 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