import { getActiveTab, getAliases, isBrowserErrorUrl, resolveGroupId, tabInfo } from '../core'; import { CommandGroup } from '../classes/CommandGroup'; import type { CommandEntry } from '../classes/CommandGroup'; import type { NavOpenArgs, NavToArgs, NavTabArgs, NavFocusArgs, NavWaitArgs, NavOpenWaitArgs } from '../types'; export class NavigationCommands extends CommandGroup { readonly namespace = "navigate"; readonly commands: Record = { "navigate.open": (a: NavOpenArgs) => this.navOpen(a), "navigate.to": (a: NavToArgs) => this.navTo(a), "navigate.reload": (a: NavTabArgs) => this.navReload(a, false), "navigate.hard_reload": (a: NavTabArgs) => this.navReload(a, true), "navigate.back": (a: NavTabArgs) => this.navBack(a), "navigate.forward": (a: NavTabArgs) => this.navForward(a), "navigate.focus": (a: NavFocusArgs) => this.navFocus(a), "navigate.wait": (a: NavWaitArgs) => this.navWait(a), "navigate.open_wait": (a: NavOpenWaitArgs) => this.navOpenWait(a), }; private async navOpen({ url, background, window: windowName, windowId: explicitWindowId, group: groupNameOrId }: NavOpenArgs) { let windowId: number | undefined; if (explicitWindowId != null) { windowId = explicitWindowId; } else if (windowName) { const aliases = await getAliases(); const entry = Object.entries(aliases).find(([, v]) => v === windowName); if (entry) windowId = parseInt(entry[0]); } const tab = await chrome.tabs.create({ url, active: !background, windowId }); if (groupNameOrId != null) { let groupId; try { groupId = await resolveGroupId(groupNameOrId); // Close any blank placeholder tabs that were created when the group was made const groupTabs = await chrome.tabs.query({ groupId }); const placeholders = groupTabs.filter(t => t.id !== tab.id && (t.url === "chrome://newtab/" || t.url === "about:blank" || t.pendingUrl === "chrome://newtab/") ); await chrome.tabs.group({ tabIds: [tab.id], groupId }); if (placeholders.length) await chrome.tabs.remove(placeholders.map(t => t.id)); } catch (e) { if (!(e instanceof Error) || !e.message.startsWith("No tab group found")) throw e; // Group doesn't exist — create it with the tab already in it groupId = await chrome.tabs.group({ tabIds: [tab.id] }); await chrome.tabGroups.update(groupId, { title: String(groupNameOrId) }); } } return { id: tab.id, url: tab.url }; } private async navTo({ tabId, url }: NavToArgs) { const tab = await chrome.tabs.update(tabId, { url }); return { id: tab.id, url: tab.url || url }; } private async navReload({ tabId }: NavTabArgs, bypassCache: boolean) { const tab = tabId ? { id: tabId } : await getActiveTab(); await chrome.tabs.reload(tab.id, { bypassCache }); return { tabId: tab.id }; } private async navBack({ tabId }: NavTabArgs) { const tab = tabId ? { id: tabId } : await getActiveTab(); await chrome.tabs.goBack(tab.id); return { tabId: tab.id }; } private async navForward({ tabId }: NavTabArgs) { const tab = tabId ? { id: tabId } : await getActiveTab(); await chrome.tabs.goForward(tab.id); return { tabId: tab.id }; } private async navFocus({ pattern }: NavFocusArgs) { // If pattern is a plain integer, treat it as a tab ID const asInt = parseInt(pattern); let match: chrome.tabs.Tab | undefined; if (!isNaN(asInt) && String(asInt) === String(pattern)) { match = await chrome.tabs.get(asInt); } else { const all = await chrome.tabs.query({}); match = all.find(t => (t.url && t.url.includes(pattern)) || (t.pendingUrl && t.pendingUrl.includes(pattern))); } if (!match) return null; await chrome.windows.update(match.windowId, { focused: true }); await chrome.tabs.update(match.id, { active: true }); return { id: match.id, url: match.url || match.pendingUrl, title: match.title }; } private async navWait({ tabId, timeout = 30000, readyState = "complete" }: NavWaitArgs = {}) { const tab = tabId ? { id: tabId } : await getActiveTab(); const deadline = Date.now() + timeout; const interval = 200; while (Date.now() < deadline) { const t = await chrome.tabs.get(tab.id); const currentUrl = t.url || t.pendingUrl || ""; if (isBrowserErrorUrl(currentUrl)) { throw new Error(`Tab ${tab.id} is showing an error page while waiting for load (${currentUrl})`); } if (readyState === "complete" ? t.status === "complete" : t.status !== "loading") { return tabInfo(t); } await new Promise(r => setTimeout(r, interval)); } throw new Error(`Tab ${tab.id} did not reach status '${readyState}' within ${timeout}ms`); } private async navOpenWait({ url, timeout = 30000, background, window: windowName, group }: NavOpenWaitArgs = {}) { const opened = await this.navOpen({ url, background, window: windowName, group }); return await this.navWait({ tabId: opened.id, timeout }); } }