Docs menuExtensions
On this page5 sections
Extensions in a shell
The config names Chrome extensions, and the engine installs them into the browser windows' profile. Content scripts and network rules need nothing from the shell. The shell draws their toolbar buttons (actions) and popups.
Part of How a shell works. Design:
EXTENSIONS.md. Manganese's tray button:
extensions/ExtensionAction.tsx.
The config
{ "extensions": { "web_store": ["ddkjiahejlhfcafbddmgiahcphecmpfh"], "unpacked": ["~/src/my-extension"] }}web_store: Chrome Web Store ids (this one is uBlock Origin Lite).unpacked: directories, absolute or under~.- Listed extensions install without a prompt and get every permission their manifest declares. Camera, microphone and the other site permissions are asked for like a site's.
- Removing one from the list uninstalls it on reload.
The list
domicile.extensions.list is the full list; extensions.on("change")
reports changes:
const Tray = () => { const { extensions } = useDomicile(); const list = useWatch(extensions, "change", () => extensions.list) ?? []; return list .filter(({ enabled }) => enabled) .map((extension) => <TrayIcon extension={extension} key={extension.id} />);};| Field | What |
|---|---|
id | What extensions.activate takes |
name | The extension's name |
title | The action's tooltip and the icon's accessible name |
icon | A data:image/png URL at the page's device pixel ratio |
badgeText, badgeColor | The badge. badgeColor is #rrggbbaa, transparent when unset |
popup | The popup's chrome-extension:// URL, or null |
enabled | false after action.disable() |
The row type is DomicileExtension. extensionSchema, from
@domicile-desktop/sdk/extension, parses one into an Extension.
A click
Call domicile.extensions.activate(id) on every click, with or without a popup.
This grants activeTab on the focused browser window, as Chrome's toolbar
does, so scripting.executeScript and tab.url work there. Then:
- No
popup: done. The engine firesaction.onClicked. - A
popup: open a<webview extensionpopup>at that URL in a panel under the icon.
An extension can also ask for its popup with action.openPopup(). Phantom
does this from action.onClicked, so its row has no popup.
extensions.on("popup", ({ id, popup }) => …) reports the request: open
popup under id's icon as for a click. The request carries the URL because
the extension may clear its row's popup straight after.
import { WEBVIEW_CLOSE_EVENT } from "@domicile-desktop/sdk/webview-element";const Popup = ({ extension, onClose }) => { const viewRef = useRef<HTMLElement>(null); useEffect(() => { const view = viewRef.current; view?.addEventListener(WEBVIEW_CLOSE_EVENT, onClose); return () => view?.removeEventListener(WEBVIEW_CLOSE_EVENT, onClose); }, [onClose]); return <webview extensionpopup="" ref={viewRef} src={extension.popup} />;};extensionpopup: set it before appending. The engine reads it once. Without it the popup loads as a tab, and extensions that lay out by context (for example, Bitwarden) render as a full page.- Size: size the view to
contentWidthandcontentHeight(0 until laid out), announced indomicile-content-size-change. The page sizes itself as in Chrome, between 25x25 and 800x600, and scrolls past that. - Showing: show the panel once the page has sized itself and
loadingis false, as Chrome does. While it loads it is still growing. - Keyboard: take it with
windows.focusShellwhile the panel is open, and give it back on close (Who gets the keyboard). - Closing: close the panel on
domicile-close(the popup calledwindow.close()), on a press outside it, and on Escape. Escape reaches you only while your page has focus, not while the popup's page does (The keyboard). - Permissions: the popup fires
domicile-permission-requestlike a browser window (Permissions). Unclaimed, the extension gets nothing. A choice is stored for the extension'schrome-extension://origin, so it holds in all its pages.
Tabs and windows
Every <webview> is a tab, and the desktop is one chrome.windows window.
The active tab is the view that last took focus. So view.focus() sets what
tabs.query({active: true, currentWindow: true}) returns, and the tray shows
that tab's state.
The browser carries out some extension calls. Others fire an event on a
<webview>:
| Extension call | What happens |
|---|---|
tabs.create({url}) | A browser window opens and appears in the next browserwindowschanged |
windows.create({type: "popup", url}) | A browser window opens as that popup window's tab (below) |
tabs.remove(id) | That browser window closes. A view of your own fires domicile-close (Close requests) |
tabs.update(id, {active: true}), windows.update(id, {focused: true}) | domicile-focus-request (WEBVIEW_FOCUS_REQUEST_EVENT): raise that window |
tabs.setZoom(id, factor) | domicile-zoom-change: already applied |
action.openPopup() | extensions.on("popup"): open that popup (A click) |
sidePanel.open() | A popup window showing the side panel, with no size (0 by 0), in browserwindowschanged: tile it; a second call sends domicile-focus-request for it. sidePanel.close() closes it |
Calls with no desktop meaning (tabs.move, tabs.group, tabs.discard, …)
fail with not supported on a Domicile desk.
Extension popup windows
chrome.windows.create({type: "popup", url}) (for example, Bitwarden's
"Unlock") opens an ordinary browser window
(The engine owns browser windows):
- It is listed with
popupWindow(itschrome.windowsid) and the requestedwidthandheight(0 if unset). Placement is yours. Manganese floats one with a size and tiles one without, such as a side panel. - It shows an extension's page, so draw it with no address bar, as Chrome does.
windows.remove(popupWindow)closes it.windows.update(popupWindow, {focused: true})firesdomicile-focus-requeston its view.