Fix #822: add browser button to toggle superignore

Ideally this would inherit the user's theme but I don't think
I care enough to do that
This commit is contained in:
Oliver Blanthorn 2026-07-25 12:41:07 +02:00
parent d0739c51d9
commit 3387a4f864
No known key found for this signature in database
GPG key ID: 2BB8C36BB504BFF3
11 changed files with 301 additions and 2 deletions

View file

@ -1,6 +1,6 @@
const esbuild = require('esbuild')
for (let f of ["content", "background", "help", "newtab", "reader", "commandline_frame", "qrCodeGenerator"]) {
for (let f of ["content", "background", "help", "newtab", "reader", "commandline_frame", "qrCodeGenerator", "browser_action_popup"]) {
esbuild.build({
entryPoints: [`src/${f}.ts`],
bundle: true,

View file

@ -28,6 +28,7 @@ import * as omnibox from "@src/background/omnibox"
import * as R from "ramda"
import * as webrequests from "@src/background/webrequests"
import * as commands from "@src/background/commands"
import * as browser_action from "@src/background/browser_action"
import * as meta from "@src/background/meta"
import * as Logging from "@src/lib/logging"
import * as Proxy from "@src/lib/proxy"
@ -244,6 +245,10 @@ browser.tabs.onCreated.addListener(aucon.tabCreatedListener)
// An object to collect all of our statistics in one place.
const statsLogger: perf.StatsLogger = new perf.StatsLogger()
const messages = {
browser_action_background: {
getState: browser_action.getState,
toggle: browser_action.toggle,
},
config_background: {
clear: config.clear,
pull: config.pull,
@ -288,6 +293,7 @@ omnibox.init()
// }}}
commands.updateListener()
browser_action.init()
// {{{ Obey Mozilla's orders https://github.com/tridactyl/tridactyl/issues/1800

View file

@ -0,0 +1,43 @@
import * as config from "@src/lib/config"
import { getState, init, toggle } from "@src/background/browser_action"
jest.mock("@src/lib/config", () => {
const userconfig = { superignore: "false" }
return {
DEFAULTS: { superignore: "false" },
USERCONFIG: userconfig,
getAsync: jest.fn().mockResolvedValue(undefined),
set: jest.fn((_key, value) => {
userconfig.superignore = value
return Promise.resolve()
}),
addChangeListener: jest.fn(),
}
})
test("browser action toggles superignore without reloading tabs", async () => {
await config.set("superignore", "false")
init()
await expect(getState()).resolves.toBe("false")
await expect(Promise.all([toggle(), toggle()])).resolves.toEqual([
"true",
"false",
])
expect(config.USERCONFIG.superignore).toBe("false")
expect(browser.browserAction.setBadgeText).toHaveBeenCalledWith({
text: "OFF",
})
expect(browser.tabs.reload).not.toHaveBeenCalled()
expect(browser.browserAction.onClicked.addListener).not.toHaveBeenCalled()
expect(browser.browserAction.setTitle).toHaveBeenLastCalledWith({
title: "Tridactyl enabled",
})
jest.mocked(config.set).mockImplementationOnce(async (_key, value) => {
config.USERCONFIG.superignore = value as "true" | "false"
throw new Error("write failed")
})
await expect(toggle()).rejects.toThrow("write failed")
expect(config.USERCONFIG.superignore).toBe("false")
})

View file

@ -0,0 +1,48 @@
import * as config from "@src/lib/config"
const ready = config.getAsync()
const superignore = () =>
config.USERCONFIG.superignore ?? config.DEFAULTS.superignore
function updateButton(value) {
const disabled = value === "true"
return Promise.all([
browser.browserAction.setBadgeText({ text: disabled ? "OFF" : "" }),
browser.browserAction.setTitle({
title: disabled ? "Tridactyl disabled" : "Tridactyl enabled",
}),
])
}
export async function getState() {
await ready
return superignore()
}
let toggleQueue: Promise<unknown> = Promise.resolve()
export function toggle() {
const pending = toggleQueue
.catch(() => undefined)
.then(async () => {
const previous = config.USERCONFIG.superignore
const value = (await getState()) === "true" ? "false" : "true"
try {
await config.set("superignore", value)
} catch (error) {
if (previous === undefined) delete config.USERCONFIG.superignore
else config.USERCONFIG.superignore = previous
throw error
}
updateButton(value).catch(console.error)
return value
})
toggleQueue = pending
return pending
}
export function init() {
config.addChangeListener("superignore", (_, value) =>
updateButton(value).catch(console.error),
)
getState().then(updateButton).catch(console.error)
}

View file

@ -0,0 +1,55 @@
jest.mocked(browser.runtime.sendMessage)
.mockResolvedValueOnce("false")
.mockResolvedValueOnce("true")
const flush = () => new Promise(resolve => setTimeout(resolve))
test("popup separates toggling from reloading the active tab", async () => {
document.body.innerHTML = `
<p id="state"></p>
<button id="toggle" disabled></button>
<button id="reload" disabled></button>`
jest.mocked(browser.tabs.query).mockResolvedValue([
{ id: 7 } as browser.tabs.Tab,
])
jest.mocked(browser.tabs.reload).mockResolvedValue(undefined)
const close = jest.spyOn(window, "close").mockImplementation()
await import("@src/browser_action_popup")
await flush()
expect(document.querySelector("#state").textContent).toBe(
"Enabled globally",
)
expect(browser.runtime.sendMessage).toHaveBeenCalledWith({
type: "browser_action_background",
command: "getState",
args: [],
})
document.querySelector<HTMLElement>("#toggle").click()
expect(document.querySelector<HTMLButtonElement>("#reload").disabled).toBe(
true,
)
await flush()
expect(browser.runtime.sendMessage).toHaveBeenCalledWith({
type: "browser_action_background",
command: "toggle",
args: [],
})
expect(browser.tabs.reload).not.toHaveBeenCalled()
expect(document.querySelector("#state").textContent).toBe(
"Disabled globally",
)
document.querySelector<HTMLElement>("#reload").click()
expect(document.querySelector<HTMLButtonElement>("#toggle").disabled).toBe(
true,
)
await flush()
expect(browser.tabs.query).toHaveBeenCalledWith({
active: true,
currentWindow: true,
})
expect(browser.tabs.reload).toHaveBeenCalledWith(7)
expect(close).toHaveBeenCalled()
})

View file

@ -0,0 +1,63 @@
export {}
function message(command: "getState" | "toggle") {
return browser.runtime.sendMessage({
type: "browser_action_background",
command,
args: [],
})
}
const state = document.querySelector<HTMLElement>("#state")
const toggleButton = document.querySelector<HTMLButtonElement>("#toggle")
const reloadButton = document.querySelector<HTMLButtonElement>("#reload")
function showState(value) {
const disabled = value === "true"
state.textContent = disabled ? "Disabled globally" : "Enabled globally"
toggleButton.textContent = disabled
? "Enable Tridactyl"
: "Disable Tridactyl"
}
function showError(error) {
state.textContent = `Error: ${error instanceof Error ? error.message : error}`
}
function setBusy(busy) {
toggleButton.disabled = busy
reloadButton.disabled = busy
}
async function run(action: () => Promise<void>) {
setBusy(true)
try {
await action()
} catch (error) {
showError(error)
} finally {
setBusy(false)
}
}
message("getState")
.then(showState)
.then(() => setBusy(false), showError)
toggleButton.addEventListener("click", () =>
run(async () => {
showState(await message("toggle"))
}),
)
reloadButton.addEventListener("click", () =>
run(async () => {
const [tab] = await browser.tabs.query({
active: true,
currentWindow: true,
})
if (tab?.id === undefined) throw new Error("No active tab found")
await browser.tabs.reload(tab.id)
window.close()
}),
)

View file

@ -248,7 +248,7 @@ export class default_config {
*
* You are usually better off using `blacklistadd` and `seturl [url] noiframe true` as you can then still use some Tridactyl binds, e.g. `shift-insert` for exiting ignore mode.
*
* NB: you should only use this with `seturl`. If you get trapped with Tridactyl disabled everywhere just run `tri unset superignore` in the Firefox address bar. If that still doesn't fix things, you can totally reset Tridactyl by running `tri help superignore` in the Firefox address bar, scrolling to the bottom of that page and then clicking "Reset Tridactyl config".
* Use the toolbar popup to toggle this globally. If you get trapped with Tridactyl disabled everywhere just run `tri unset superignore` in the Firefox address bar. If that still doesn't fix things, you can totally reset Tridactyl by running `tri help superignore` in the Firefox address bar, scrolling to the bottom of that page and then clicking "Reset Tridactyl config".
*/
superignore: "true" | "false" = "false"

View file

@ -20,6 +20,7 @@ export type TabMessageType =
| "commandline_frame_ready_to_receive_messages"
export type NonTabMessageType =
| "browser_action_background"
| "owntab_background"
| "excmd_background"
| "controller_background"

View file

@ -13,6 +13,12 @@
"background.js"
]
},
"browser_action": {
"default_area": "navbar",
"default_icon": "static/logo/Tridactyl_32px.png",
"default_popup": "static/browser_action_popup.html",
"default_title": "Tridactyl controls"
},
"chrome_url_overrides": {
"newtab": "static/newtab.html"
},

View file

@ -0,0 +1,21 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Tridactyl controls</title>
<link rel="stylesheet" href="css/browser_action_popup.css" />
</head>
<body>
<h1>Tridactyl</h1>
<p id="state" class="state" aria-live="polite">Loading...</p>
<button id="toggle" type="button" disabled>Loading...</button>
<p class="help">
This setting applies globally. Reload pages to apply it.
</p>
<button id="reload" class="secondary" type="button" disabled>
Reload this tab
</button>
<script src="../browser_action_popup.js"></script>
</body>
</html>

View file

@ -0,0 +1,56 @@
@import url("../themes/auto/auto.css");
body {
box-sizing: border-box;
width: 18rem;
margin: 0;
padding: 1rem;
border-top: 0.25rem solid var(--tridactyl-url-fg);
color: var(--tridactyl-fg);
background: var(--tridactyl-bg);
font-family: var(--tridactyl-font-family-sans);
font-size: var(--tridactyl-font-size);
}
h1 {
margin: 0 0 0.75rem;
font-family: var(--tridactyl-font-family);
font-size: 1.25rem;
}
.state {
margin: 0 0 1rem;
font-weight: bold;
}
button {
box-sizing: border-box;
width: 100%;
padding: 0.65rem;
border: 1px solid var(--tridactyl-fg);
border-radius: var(--tridactyl-status-border-radius);
color: var(--tridactyl-bg);
background: var(--tridactyl-fg);
font: inherit;
cursor: pointer;
}
button:focus-visible {
outline: 2px solid var(--tridactyl-url-fg);
outline-offset: 2px;
}
button:disabled {
cursor: wait;
opacity: 0.55;
}
.secondary {
margin-top: 0.25rem;
color: var(--tridactyl-fg);
background: var(--tridactyl-bg);
}
.help {
font-size: var(--tridactyl-small-font-size);
}