import { webExtApi as api } from '../browser-api'; import { fetchTabHtml, getActiveTab, getAliases, isBrowserErrorUrl, tabInfo } from '../core'; import { CommandGroup } from '../classes/CommandGroup'; import type { CommandEntry } from '../classes/CommandGroup'; import type { TabIdArgs, TabsActiveInWindowArgs, TabsPatternArgs, TabsQueryArgs, TabsWatchUrlArgs } from '../types'; export class TabsQueryCommands extends CommandGroup { readonly namespace = "tabs"; readonly commands: Record = { "tabs.list": () => this.tabsList(), "tabs.active_in_window": (a: TabsActiveInWindowArgs) => this.tabsActiveInWindow(a), "tabs.status": (a: TabIdArgs) => this.tabsStatus(a), "tabs.filter": (a: TabsPatternArgs) => this.tabsFilter(a), "tabs.count": (a: TabsPatternArgs) => this.tabsCount(a), "tabs.query": (a: TabsQueryArgs) => this.tabsQuery(a), "tabs.html": (a: TabIdArgs) => fetchTabHtml(a.tabId), "tabs.watch_url": (a: TabsWatchUrlArgs) => this.tabsWatchUrl(a), }; private async tabsList() { const windows = await api.windows.getAll({ populate: true }); const aliases = await getAliases(); const tabs = []; for (const w of windows) { for (const t of w.tabs) { tabs.push({ ...tabInfo(t), windowAlias: aliases[t.windowId] || null, pinned: t.pinned, favIconUrl: t.favIconUrl, }); } } return tabs; } private async tabsActiveInWindow({ windowId }: TabsActiveInWindowArgs) { const activeTabs = await api.tabs.query({ windowId, active: true }); const tab = activeTabs[0]; if (!tab) { throw new Error(`No active tab found for window ${windowId}`); } return tabInfo(tab); } private async tabsStatus({ tabId }: TabIdArgs) { const tab = tabId ? await api.tabs.get(tabId) : await getActiveTab(); return tabInfo(tab); } private async tabsFilter({ pattern }: TabsPatternArgs) { const all = await api.tabs.query({}); return all.filter(t => t.url && t.url.includes(pattern)).map(tabInfo); } private async tabsCount({ pattern }: TabsPatternArgs) { const all = await api.tabs.query({}); if (pattern) return all.filter(t => t.url && t.url.includes(pattern)).length; return all.length; } private async tabsQuery({ search }: TabsQueryArgs) { const q = search.toLowerCase(); const all = await api.tabs.query({}); return all.filter(t => (t.url && t.url.toLowerCase().includes(q)) || (t.title && t.title.toLowerCase().includes(q)) ).map(tabInfo); } private async tabsWatchUrl({ pattern, timeout = 30000, tabId }: TabsWatchUrlArgs = {}) { const tab = tabId ? await api.tabs.get(tabId) : await getActiveTab(); const deadline = Date.now() + timeout; const regex = new RegExp(pattern); let lastUrl = tab.url || tab.pendingUrl || ""; let lastStatus = tab.status || "unknown"; const matches = (url: string) => { regex.lastIndex = 0; return Boolean(url && regex.test(url)); }; if (matches(lastUrl)) return tabInfo(tab); while (Date.now() < deadline) { const t = await api.tabs.get(tab.id); lastUrl = t.url || t.pendingUrl || ""; lastStatus = t.status || "unknown"; if (matches(t.pendingUrl || "") || matches(t.url || "")) return tabInfo(t); if (isBrowserErrorUrl(t.url || "")) { throw new Error(`Tab ${tab.id} is showing an error page while waiting for URL to match '${pattern}'`); } await new Promise(r => setTimeout(r, 200)); } throw new Error(`Tab ${tab.id} URL did not match '${pattern}' within ${timeout}ms (last URL: '${lastUrl}', status: '${lastStatus}')`); } }