Fix #5049: ga picks last audible tab

Also, add tri.webext.getLastAudibleTab() for use in scripts
This commit is contained in:
Oliver Blanthorn 2026-07-25 08:34:29 +02:00
parent 8ed0f11d59
commit 51775ac49f
No known key found for this signature in database
GPG key ID: 2BB8C36BB504BFF3
4 changed files with 49 additions and 7 deletions

View file

@ -161,7 +161,7 @@ If you want to use Firefox's default `<C-b>` binding to open the bookmarks sideb
- `u` — undo the last tab/window closure - `u` — undo the last tab/window closure
- `gt`/`gT` — go to the next/previous tab - `gt`/`gT` — go to the next/previous tab
- `g^ OR g0`/`g$` — go to the first/last tab - `g^ OR g0`/`g$` — go to the first/last tab
- `ga` — go to the tab currently playing audio - `ga` — go to the tab currently playing audio, or the one that most recently stopped
- `<C-^>` — go to the last active tab - `<C-^>` — go to the last active tab
- `b` — bring up a list of open tabs in the current window; you can type the tab ID or part of the title or URL to choose a tab - `b` — bring up a list of open tabs in the current window; you can type the tab ID or part of the title or URL to choose a tab

View file

@ -1,4 +1,5 @@
import { queryAndURLwrangler } from "@src/lib/webext" import { queryAndURLwrangler } from "@src/lib/webext"
import * as webext from "@src/lib/webext"
import * as config from "@src/lib/config" import * as config from "@src/lib/config"
import * as Native from "@src/lib/native" import * as Native from "@src/lib/native"
@ -49,6 +50,7 @@ Object.defineProperty(browser, "sessions", {
value: { getTabValue: jest.fn(), setTabValue: jest.fn() }, value: { getTabValue: jest.fn(), setTabValue: jest.fn() },
}) })
webext.initLastAudibleTabTracking()
const backgroundExcmds = require("@src/.excmds_background.generated") const backgroundExcmds = require("@src/.excmds_background.generated")
const { jsb, nativeopen, quickmarkremove, set, tabopen, winopen } = const { jsb, nativeopen, quickmarkremove, set, tabopen, winopen } =
backgroundExcmds backgroundExcmds
@ -138,6 +140,25 @@ test("`winopen` creates a neutral tab before navigating it", async () => {
}) })
}) })
test("`getLastAudibleTab` prioritises current audio, falls back, and forgets closed tabs", async () => {
const currentTab = { id: 1, windowId: 10 } as browser.tabs.Tab
const previousTab = { id: 2, windowId: 20 } as browser.tabs.Tab
const onUpdated = browser.tabs.onUpdated.addListener as jest.Mock
const onRemoved = browser.tabs.onRemoved.addListener as jest.Mock
onUpdated.mock.calls[0][0](previousTab.id, { audible: false }, previousTab)
jest.mocked(browser.tabs.query).mockResolvedValue([])
jest.mocked(browser.tabs.query).mockResolvedValueOnce([currentTab])
await expect(webext.getLastAudibleTab()).resolves.toBe(currentTab)
jest.mocked(browser.tabs.get).mockResolvedValueOnce(previousTab)
await expect(webext.getLastAudibleTab()).resolves.toBe(previousTab)
jest.mocked(browser.tabs.get).mockRejectedValueOnce(new Error())
await expect(webext.getLastAudibleTab()).resolves.toBeUndefined()
onRemoved.mock.calls[0][0](previousTab.id)
jest.mocked(browser.tabs.get).mockClear()
await webext.getLastAudibleTab()
expect(browser.tabs.get).not.toHaveBeenCalled()
})
test("`nativeopen` targets the running macOS Firefox application", async () => { test("`nativeopen` targets the running macOS Firefox application", async () => {
jest.mocked(browser.runtime.getPlatformInfo).mockResolvedValue({ jest.mocked(browser.runtime.getPlatformInfo).mockResolvedValue({
arch: "x86-64", arch: "x86-64",

View file

@ -76,7 +76,7 @@
// Shared // Shared
import * as Messaging from "@src/lib/messaging" import * as Messaging from "@src/lib/messaging"
import { ownWinTriIndex, getTriVersion, getTriVersionName, browserBg, activeTab, activeTabOnWindow, activeTabId, activeTabContainerId, openInNewTab, openInNewWindow, openInTab, queryAndURLwrangler, goToTab, getSortedTabs, prevActiveTab } from "@src/lib/webext" import { ownWinTriIndex, getTriVersion, getTriVersionName, browserBg, activeTab, activeTabOnWindow, activeTabId, activeTabContainerId, openInNewTab, openInNewWindow, openInTab, queryAndURLwrangler, goToTab, getSortedTabs, prevActiveTab, getLastAudibleTab } from "@src/lib/webext"
import * as Container from "@src/lib/containers" import * as Container from "@src/lib/containers"
import state from "@src/state" import state from "@src/state"
import * as State from "@src/state" import * as State from "@src/state"
@ -2821,13 +2821,13 @@ export async function tabpush(windowId?: number) {
} }
} }
/** Switch to the tab currently playing audio, if any. */ /** Switch to a tab playing audio, or the one that most recently stopped. */
//#background //#background
export async function tabaudio() { export async function tabaudio() {
const tabs = await browser.tabs.query({ audible: true }) const tab = await getLastAudibleTab()
if (tabs.length > 0) { if (tab) {
await browser.windows.update(tabs[0].windowId, { focused: true }) await browser.windows.update(tab.windowId, { focused: true })
return browser.tabs.update(tabs[0].id, { active: true }) return browser.tabs.update(tab.id, { active: true })
} }
} }

View file

@ -72,6 +72,27 @@ export function getContext() {
// Make this library work for both content and background. // Make this library work for both content and background.
export const browserBg = inContentScript() ? browserProxy : browser export const browserBg = inContentScript() ? browserProxy : browser
let lastAudibleTabId: number | undefined
export function initLastAudibleTabTracking() {
browser.tabs.onUpdated.addListener(
(tabId, changeInfo) => {
if (changeInfo.audible === false) lastAudibleTabId = tabId
},
{ properties: ["audible"] },
)
browser.tabs.onRemoved.addListener(tabId => {
if (tabId === lastAudibleTabId) lastAudibleTabId = undefined
})
}
if (getContext() === "background") initLastAudibleTabTracking()
/** Return a currently audible tab, or the one that most recently stopped. */
export async function getLastAudibleTab() {
const [tab] = await browserBg.tabs.query({ audible: true })
if (tab || lastAudibleTabId === undefined) return tab
return browserBg.tabs.get(lastAudibleTabId).catch(() => undefined)
}
/** The first active tab in the currentWindow. /** The first active tab in the currentWindow.
* *
* TODO: Highlander theory: Can there ever be more than one? * TODO: Highlander theory: Can there ever be more than one?