Revert WhatsApp theme bridge
The native messaging host, extension shim, and lifecycle machinery are too complicated for the value this integration provides.
This commit is contained in:
@@ -1,103 +0,0 @@
|
||||
// MV3 service worker for the Omarchy WhatsApp theme extension.
|
||||
//
|
||||
// Holds the long-lived native-messaging port to the shared Omarchy theme bridge
|
||||
// (com.omarchy.theme / omarchy-chromium-theme-host), which pushes the active
|
||||
// theme on connect and on every omarchy theme-set. We rebroadcast pushes to
|
||||
// WhatsApp tabs and answer the content script's theme requests. We never write
|
||||
// to the port — the host is push-only.
|
||||
|
||||
const HOST = "com.omarchy.theme";
|
||||
const WHATSAPP_URL_PATTERNS = ["*://web.whatsapp.com/*"];
|
||||
|
||||
let port = null;
|
||||
let reconnectTimer = null;
|
||||
|
||||
function connect() {
|
||||
// The service worker can reach this from three directions at once: the
|
||||
// module-level call below, onInstalled, and onStartup. Without this guard each
|
||||
// one opens its own port, and every port spawns a separate long-lived native
|
||||
// host — so the theme-set refresh then signals N hosts and every theme change
|
||||
// gets broadcast to the same tabs N times.
|
||||
if (port) return;
|
||||
try {
|
||||
port = chrome.runtime.connectNative(HOST);
|
||||
console.log("[omarchy] native port connected");
|
||||
} catch (e) {
|
||||
console.warn("[omarchy] connectNative threw:", e);
|
||||
scheduleReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
port.onMessage.addListener((theme) => {
|
||||
if (!theme || theme.error) {
|
||||
console.warn("[omarchy] native host error:", theme && theme.error);
|
||||
return;
|
||||
}
|
||||
console.log("[omarchy] theme pushed by native host:", theme.theme_name, theme.bg);
|
||||
chrome.storage.local.set({ theme });
|
||||
broadcast(theme);
|
||||
});
|
||||
|
||||
port.onDisconnect.addListener(() => {
|
||||
const err = chrome.runtime.lastError;
|
||||
console.warn("[omarchy] native host disconnected:", err && err.message);
|
||||
port = null;
|
||||
scheduleReconnect();
|
||||
});
|
||||
|
||||
// No request needed: the host is push-only. It emits the current theme as soon
|
||||
// as it starts, then again on every theme change (driven by omarchy's
|
||||
// theme-set refresh). We never write to the port.
|
||||
}
|
||||
|
||||
function scheduleReconnect() {
|
||||
if (reconnectTimer) return;
|
||||
reconnectTimer = setTimeout(() => {
|
||||
reconnectTimer = null;
|
||||
connect();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function broadcast(theme) {
|
||||
chrome.tabs.query({ url: WHATSAPP_URL_PATTERNS }, (tabs) => {
|
||||
console.log("[omarchy] broadcasting theme to", tabs.length, "whatsapp tab(s)");
|
||||
for (const t of tabs) {
|
||||
chrome.tabs.sendMessage(t.id, { type: "omarchy-theme", theme }).catch(() => {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
if (msg && msg.type === "request-theme") {
|
||||
chrome.storage.local.get("theme").then(({ theme }) => sendResponse(theme || null));
|
||||
return true;
|
||||
}
|
||||
// Kept for parity with the shared engine, which asks for a guaranteed-current
|
||||
// theme right before it would drive an app's own color mode. Storage is
|
||||
// already current: omarchy fires its theme-set refresh after the new theme's
|
||||
// files are final, the host pushes immediately, and we write storage on that
|
||||
// push — all before the content script gets the broadcast that makes it ask.
|
||||
if (msg && msg.type === "request-fresh-theme") {
|
||||
chrome.storage.local.get("theme").then(({ theme }) => sendResponse(theme || null));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// The WhatsApp host permission is what grants tab urls here and lets the query
|
||||
// above filter by url, so the `tabs` permission — which would hand over every
|
||||
// tab's url and title — is not needed. Tabs we have no permission for arrive
|
||||
// with url unset and fall out on the guard below. If a url ever went missing on
|
||||
// a tab we do own, content.js still asks for the theme itself at document_start,
|
||||
// so this listener is the redundant path rather than the only one.
|
||||
chrome.tabs.onUpdated.addListener((tabId, info, tab) => {
|
||||
if (info.status !== "complete") return;
|
||||
if (!tab.url || !tab.url.includes("web.whatsapp.com")) return;
|
||||
chrome.storage.local.get("theme").then(({ theme }) => {
|
||||
if (theme) chrome.tabs.sendMessage(tabId, { type: "omarchy-theme", theme }).catch(() => {});
|
||||
});
|
||||
});
|
||||
|
||||
chrome.runtime.onInstalled.addListener(connect);
|
||||
chrome.runtime.onStartup.addListener(connect);
|
||||
|
||||
connect();
|
||||
@@ -1,60 +0,0 @@
|
||||
// Follows the active Omarchy theme's light/dark for WhatsApp Web.
|
||||
//
|
||||
// The shared theme bridge (com.omarchy.theme) pushes the current theme to the
|
||||
// service worker, which broadcasts it here. We decide dark vs. light from the
|
||||
// WCAG relative luminance of the terminal background — not the theme's day/night
|
||||
// name — then hand that to the MAIN-world prefers-color-scheme shim
|
||||
// (omarchy-prefers-color-scheme.js). The shim flips window.matchMedia, so
|
||||
// WhatsApp Web's "System default" theme repaints live with no reload.
|
||||
//
|
||||
// That's the whole extension: WhatsApp themes itself, so there's no CSS to
|
||||
// inject. Matching WhatsApp's surfaces to the exact omarchy palette would be a
|
||||
// separate, heavier layer and is intentionally left out of this version.
|
||||
|
||||
// sRGB is gamma-encoded, so the WCAG weights only mean anything once each
|
||||
// channel is linearized — the same two steps the shell uses in
|
||||
// Panel.qml's colorChannelLuminance. Weighting the raw bytes instead lands
|
||||
// mid-tone backgrounds on the wrong side: #808080 reads 0.502 that way and
|
||||
// 0.216 once linearized. Every shipped theme is far enough from the middle to
|
||||
// classify the same either way; a custom one need not be.
|
||||
function channelLuminance(byte) {
|
||||
const channel = byte / 255;
|
||||
return channel <= 0.03928
|
||||
? channel / 12.92
|
||||
: Math.pow((channel + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
|
||||
function isDarkTheme(theme) {
|
||||
const hex = ((theme && theme.bg) || "").replace(/^#/, "");
|
||||
if (hex.length < 6) return null;
|
||||
const r = parseInt(hex.slice(0, 2), 16);
|
||||
const g = parseInt(hex.slice(2, 4), 16);
|
||||
const b = parseInt(hex.slice(4, 6), 16);
|
||||
if ([r, g, b].some(Number.isNaN)) return null;
|
||||
const luminance =
|
||||
0.2126 * channelLuminance(r) +
|
||||
0.7152 * channelLuminance(g) +
|
||||
0.0722 * channelLuminance(b);
|
||||
return luminance < 0.5;
|
||||
}
|
||||
|
||||
let lastDark = null;
|
||||
|
||||
function applyTheme(theme) {
|
||||
const dark = isDarkTheme(theme);
|
||||
if (dark === null || dark === lastDark) return;
|
||||
lastDark = dark;
|
||||
document.documentElement.style.colorScheme = dark ? "dark" : "light";
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("omarchy:set-color-scheme", { detail: { dark } })
|
||||
);
|
||||
}
|
||||
|
||||
// Themes arrive two ways: pushed live by the service worker on an omarchy
|
||||
// theme-set, and fetched once on load.
|
||||
chrome.runtime.onMessage.addListener((msg) => {
|
||||
if (msg && msg.type === "omarchy-theme") applyTheme(msg.theme);
|
||||
});
|
||||
chrome.runtime.sendMessage({ type: "request-theme" }, (theme) => {
|
||||
if (theme) applyTheme(theme);
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "WhatsApp Omarchy Theme",
|
||||
"version": "1.0",
|
||||
"description": "Make WhatsApp Web follow your current Omarchy light/dark theme, this extension is installed by Omarchy",
|
||||
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApWxRYnzXjhKFK8oDs721taskS9MbEid55l4V5qVpDTdm4Idrmj6NTdvm1Z2SW/lbGOSk95kxhyEH35tVo6QW2GC9K+0V6lvAzZv9nqMcfeDowQsyPPBopoh5soGBANw2lyHduADPUmbVT9J0/C+erYWx4QM7v08R6aZIF6j2KgOLnCo7aWLpVA40fA2eDHj1CLtcnAnSefRYBB4b5ac8Ir3Mjh4ojEROYQ51kEgxAZJF9HhvFbMFuN9AqD/wV2HdHSflMhxpNocGkhkVNAkXNIlcjQKrVo2B0EBB16Q4/QbYol93vnJ+rojWnds0tGbnLgYsdK7uacYh8l+4dKu6OQIDAQAB",
|
||||
"permissions": ["nativeMessaging", "storage"],
|
||||
"host_permissions": ["*://web.whatsapp.com/*"],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["*://web.whatsapp.com/*"],
|
||||
"js": ["omarchy-prefers-color-scheme.js"],
|
||||
"run_at": "document_start",
|
||||
"all_frames": false,
|
||||
"world": "MAIN"
|
||||
},
|
||||
{
|
||||
"matches": ["*://web.whatsapp.com/*"],
|
||||
"js": ["content.js"],
|
||||
"run_at": "document_start",
|
||||
"all_frames": false
|
||||
}
|
||||
],
|
||||
"action": {
|
||||
"default_title": "WhatsApp Omarchy Theme"
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
// Omarchy web-app theming — prefers-color-scheme shim (app-agnostic).
|
||||
//
|
||||
// Runs in the page's MAIN world at document_start. Spoofs
|
||||
// window.matchMedia('(prefers-color-scheme: ...)') so a web app's "sync with
|
||||
// system" appearance follows the active Omarchy theme instead of the real OS
|
||||
// setting. The engine (omarchy-runtime.js, isolated world) dispatches an
|
||||
// `omarchy:set-color-scheme` event on every theme apply; we flip the spoofed
|
||||
// value here and notify the app's registered media-query listeners, so an app
|
||||
// on "System default" repaints live with no reload.
|
||||
|
||||
(function () {
|
||||
if (window.__omarchyPCSInstalled) return;
|
||||
window.__omarchyPCSInstalled = true;
|
||||
|
||||
const orig = window.matchMedia.bind(window);
|
||||
let isDark = orig("(prefers-color-scheme: dark)").matches;
|
||||
const listeners = new Set();
|
||||
const owners = new Set();
|
||||
|
||||
function makeProxy(query) {
|
||||
const wantsDark = /dark/i.test(query);
|
||||
const wantsLight = /light/i.test(query);
|
||||
const target = orig(query);
|
||||
// Registrations live in one shared Set, so they have to record which
|
||||
// MediaQueryList they came from. Without that, an app that hands the same
|
||||
// callback to both the dark and the light query and later detaches one
|
||||
// would silently detach the other too, and the query it still holds would
|
||||
// stop hearing theme changes. `addListener` is a legacy alias of
|
||||
// `addEventListener("change")`, so the two share a registration space and
|
||||
// either remover cancels either add.
|
||||
const owner = {};
|
||||
let onchange = null;
|
||||
let proxy = null;
|
||||
|
||||
function captureFrom(options) {
|
||||
return typeof options === "boolean" ? options : !!options?.capture;
|
||||
}
|
||||
|
||||
function has(cb, capture) {
|
||||
for (const e of listeners)
|
||||
if (e.owner === owner && e.cb === cb && e.capture === capture) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function add(cb, options = false) {
|
||||
// Native listeners dedupe on identity; adding twice must not fire twice.
|
||||
const capture = captureFrom(options);
|
||||
if (
|
||||
(typeof cb !== "function" && typeof cb?.handleEvent !== "function") ||
|
||||
options?.signal?.aborted ||
|
||||
has(cb, capture)
|
||||
)
|
||||
return;
|
||||
const entry = {
|
||||
owner,
|
||||
cb,
|
||||
capture,
|
||||
once: !!options?.once,
|
||||
signal: options?.signal,
|
||||
wantsDark,
|
||||
wantsLight,
|
||||
};
|
||||
listeners.add(entry);
|
||||
if (entry.signal) {
|
||||
entry.abort = () => listeners.delete(entry);
|
||||
entry.signal.addEventListener("abort", entry.abort, { once: true });
|
||||
}
|
||||
}
|
||||
|
||||
function remove(cb, options = false) {
|
||||
const capture = captureFrom(options);
|
||||
for (const e of listeners) {
|
||||
if (e.owner !== owner || e.cb !== cb || e.capture !== capture) continue;
|
||||
listeners.delete(e);
|
||||
if (e.signal) e.signal.removeEventListener("abort", e.abort);
|
||||
}
|
||||
}
|
||||
|
||||
proxy = new Proxy(target, {
|
||||
get(_t, prop) {
|
||||
if (prop === "matches") {
|
||||
if (wantsDark) return isDark;
|
||||
if (wantsLight) return !isDark;
|
||||
return target.matches;
|
||||
}
|
||||
if (prop === "media") return query;
|
||||
if (prop === "onchange") return onchange;
|
||||
if (prop === "addEventListener") {
|
||||
return (evt, cb, options) => {
|
||||
if (evt === "change") add(cb, options);
|
||||
};
|
||||
}
|
||||
if (prop === "removeEventListener") {
|
||||
return (evt, cb, options) => {
|
||||
if (evt === "change") remove(cb, options);
|
||||
};
|
||||
}
|
||||
if (prop === "addListener") {
|
||||
// deprecated API — single callback arg
|
||||
return (cb) => add(cb);
|
||||
}
|
||||
if (prop === "removeListener") {
|
||||
return (cb) => remove(cb);
|
||||
}
|
||||
const v = target[prop];
|
||||
return typeof v === "function" ? v.bind(target) : v;
|
||||
},
|
||||
set(_t, prop, value) {
|
||||
if (prop === "onchange") {
|
||||
onchange = typeof value === "function" ? value : null;
|
||||
if (onchange) owners.add(owner);
|
||||
else owners.delete(owner);
|
||||
return true;
|
||||
}
|
||||
return Reflect.set(target, prop, value);
|
||||
},
|
||||
});
|
||||
owner.proxy = proxy;
|
||||
owner.onchange = () => onchange;
|
||||
owner.wantsDark = wantsDark;
|
||||
owner.wantsLight = wantsLight;
|
||||
return proxy;
|
||||
}
|
||||
|
||||
window.matchMedia = function (query) {
|
||||
if (typeof query === "string" && /prefers-color-scheme/i.test(query)) {
|
||||
return makeProxy(query);
|
||||
}
|
||||
return orig(query);
|
||||
};
|
||||
|
||||
document.addEventListener("omarchy:set-color-scheme", (ev) => {
|
||||
const next = !!(ev.detail && ev.detail.dark);
|
||||
if (next === isDark) return;
|
||||
isDark = next;
|
||||
for (const entry of listeners) {
|
||||
const { owner, cb, wantsDark, wantsLight } = entry;
|
||||
const matches = wantsDark ? isDark : wantsLight ? !isDark : false;
|
||||
const media = wantsDark
|
||||
? "(prefers-color-scheme: dark)"
|
||||
: wantsLight
|
||||
? "(prefers-color-scheme: light)"
|
||||
: "";
|
||||
try {
|
||||
if (entry.once) listeners.delete(entry);
|
||||
if (entry.signal) entry.signal.removeEventListener("abort", entry.abort);
|
||||
// Shaped like a MediaQueryListEvent; apps read .matches. The legacy
|
||||
// addListener callback takes the same argument, so there is nothing to
|
||||
// branch on here.
|
||||
const event = { matches, media, target: owner.proxy, currentTarget: owner.proxy };
|
||||
if (typeof cb === "function") cb.call(owner.proxy, event);
|
||||
else cb.handleEvent(event);
|
||||
} catch (_) {}
|
||||
}
|
||||
for (const owner of owners) {
|
||||
const cb = owner.onchange();
|
||||
if (!cb) continue;
|
||||
const matches = owner.wantsDark ? isDark : owner.wantsLight ? !isDark : false;
|
||||
const media = owner.wantsDark
|
||||
? "(prefers-color-scheme: dark)"
|
||||
: owner.wantsLight
|
||||
? "(prefers-color-scheme: light)"
|
||||
: "";
|
||||
try {
|
||||
cb.call(owner.proxy, {
|
||||
matches,
|
||||
media,
|
||||
target: owner.proxy,
|
||||
currentTarget: owner.proxy,
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"name": "com.omarchy.theme",
|
||||
"description": "Omarchy theme bridge host — pushes the active theme to web-app extensions",
|
||||
"path": "__HOST_PATH__",
|
||||
"type": "stdio",
|
||||
"allowed_origins": [
|
||||
"chrome-extension://ndpkabodcpddojgepdideonokpblpeln/"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user