| 1 | // Background worker. |
| 2 | // |
| 3 | // Its one real job is the element picker's keyboard shortcut. `activeTab` — the |
| 4 | // permission that lets us touch a page without asking for <all_urls> — is only |
| 5 | // granted on specific user gestures: a toolbar-action click, a context menu |
| 6 | // item, or a keyboard command. A click inside the side panel is not one of |
| 7 | // them, so the in-panel button can fail while the shortcut always works. Both |
| 8 | // paths exist; this is the reliable one. |
| 9 | |
| 10 | // One of the two always exists — this file only ever runs as an extension |
| 11 | // background script — but neither is declared unconditionally, so say which. |
| 12 | const api = /** @type {TbExtensionApi} */ (globalThis.browser ?? globalThis.chrome); |
| 13 | |
| 14 | // Chrome's service worker starts empty; Firefox loads picker.js alongside this |
| 15 | // file via background.scripts, so there it is already defined. |
| 16 | if (typeof tbPickElement === "undefined" && typeof importScripts === "function") { |
| 17 | importScripts("picker.js", "lib/shot.js"); |
| 18 | } |
| 19 | |
| 20 | // Chrome only: make the toolbar button open the side panel. |
| 21 | if (api.sidePanel?.setPanelBehavior) { |
| 22 | api.sidePanel |
| 23 | .setPanelBehavior({ openPanelOnActionClick: true }) |
| 24 | .catch((e) => console.error(e)); |
| 25 | } |
| 26 | |
| 27 | const SKIP = /^(chrome|about|edge|moz-extension|chrome-extension|view-source|devtools):/; |
| 28 | |
| 29 | async function runPicker() { |
| 30 | const [tab] = await api.tabs.query({ active: true, currentWindow: true }); |
| 31 | if (!tab || SKIP.test(tab.url ?? "")) { |
| 32 | console.warn("termbridge: cannot pick on this page", tab?.url); |
| 33 | return; |
| 34 | } |
| 35 | |
| 36 | let value = null; |
| 37 | try { |
| 38 | const results = await api.scripting.executeScript({ |
| 39 | target: { tabId: tab.id }, |
| 40 | func: tbPickElement, |
| 41 | }); |
| 42 | value = results?.[0]?.result ?? null; |
| 43 | } catch (e) { |
| 44 | console.error("termbridge: pick failed", e); |
| 45 | return; |
| 46 | } |
| 47 | if (!value) return; |
| 48 | |
| 49 | // Screenshot here rather than in the sidebar: this path's page access comes |
| 50 | // from the activeTab grant the keyboard command just minted, and that grant |
| 51 | // belongs to the worker. |
| 52 | const shot = await Shot.copyPickedShot(api, tab.id, value); |
| 53 | if (shot !== true) console.warn("termbridge: no screenshot", shot); |
| 54 | |
| 55 | // The sidebar may not be open. Try to hand it over directly, and fall back to |
| 56 | // storage so the pick isn't lost — the sidebar drains it when it next opens. |
| 57 | try { |
| 58 | await api.runtime.sendMessage({ type: "picked", value, shot: shot === true }); |
| 59 | } catch { |
| 60 | await api.storage.local.set({ pendingPick: value }); |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | api.commands?.onCommand.addListener((command) => { |
| 65 | if (command === "pick-element") runPicker(); |
| 66 | }); |