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:
David Heinemeier Hansson
2026-08-01 12:44:40 -07:00
parent 237405215d
commit 66427571bc
15 changed files with 1 additions and 964 deletions
-273
View File
@@ -1,273 +0,0 @@
#!/bin/bash
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
export PATH="$ROOT/bin:$PATH"
TMPDIR=""
cleanup() {
[[ -n $TMPDIR && -d $TMPDIR ]] && rm -rf "$TMPDIR"
}
trap cleanup EXIT
require_command jq
require_command node
EXT_DIR="$ROOT/default/chromium/extensions/whatsapp-theme"
# The manifest key pins the extension id so the native host manifest's
# allowed_origins can be hardcoded. Derive it the same way Chromium does.
theme_id=$(node - <<'JS' "$EXT_DIR/manifest.json"
const crypto = require('crypto')
const fs = require('fs')
const manifest = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'))
const hash = crypto.createHash('sha256').update(Buffer.from(manifest.key, 'base64')).digest()
const alphabet = 'abcdefghijklmnop'
let id = ''
for (const byte of hash.subarray(0, 16)) {
id += alphabet[byte >> 4]
id += alphabet[byte & 0x0f]
}
process.stdout.write(id)
JS
)
[[ $theme_id == "ndpkabodcpddojgepdideonokpblpeln" ]] ||
fail "whatsapp-theme extension manifest has the stable id" "$theme_id"
pass "whatsapp-theme extension manifest has the stable id"
jq -e '
.manifest_version == 3 and
(.permissions | index("nativeMessaging")) and
(.host_permissions | index("*://web.whatsapp.com/*")) and
.background.service_worker == "background.js" and
(.content_scripts | map(.world) | index("MAIN")) and
(.content_scripts | map(.js) | add | index("omarchy-prefers-color-scheme.js")) and
(.content_scripts | map(.js) | add | index("content.js"))
' "$EXT_DIR/manifest.json" >/dev/null ||
fail "whatsapp-theme extension declares its host, content script, and prefers-color-scheme shim"
grep -q 'connectNative(HOST)' "$EXT_DIR/background.js" &&
grep -q '"com.omarchy.theme"' "$EXT_DIR/background.js" ||
fail "whatsapp-theme extension connects to the com.omarchy.theme bridge"
pass "whatsapp-theme extension follows the theme over native messaging"
# The WhatsApp host permission already grants tab urls and lets tabs.query filter
# by url, so `tabs` would only add every other tab's url and title.
jq -e '(.permissions | index("tabs")) | not' "$EXT_DIR/manifest.json" >/dev/null ||
fail "whatsapp-theme extension asks for no broader tab access than it needs"
pass "whatsapp-theme extension asks for no broader tab access than it needs"
# Light/dark comes from WCAG relative luminance, which needs each channel
# linearized first. Mid-tones are where weighting the raw bytes diverges.
scheme_check=$(node - "$EXT_DIR/content.js" <<'JS'
const fs = require('fs')
const source = fs.readFileSync(process.argv[2], 'utf8')
const match = source.match(/function channelLuminance[\s\S]*?\n}\n[\s\S]*?function isDarkTheme[\s\S]*?\n}\n/)
if (!match) {
process.stdout.write('no-classifier')
process.exit(0)
}
const isDarkTheme = new Function(match[0] + '; return isDarkTheme')()
const cases = [
['#1e1e2e', true], // catppuccin, unambiguously dark
['#eff1f5', false], // catppuccin-latte, unambiguously light
['#000000', true],
['#ffffff', false],
['#808080', true], // gamma-encoded weighting calls this light
]
const bad = cases.filter(([bg, want]) => isDarkTheme({ bg }) !== want).map(([bg]) => bg)
if (isDarkTheme({ bg: 'nope' }) !== null || isDarkTheme({}) !== null) bad.push('malformed')
process.stdout.write(bad.length ? 'wrong:' + bad.join(',') : 'ok')
JS
)
[[ $scheme_check == "ok" ]] ||
fail "whatsapp-theme classifies light and dark by linearized luminance" "$scheme_check"
pass "whatsapp-theme classifies light and dark by linearized luminance"
# The shim must preserve the MediaQueryList listener shapes web apps can use,
# including EventListener objects and the onchange property.
listener_check=$(node - "$EXT_DIR/omarchy-prefers-color-scheme.js" <<'JS'
const fs = require('fs')
const vm = require('vm')
let themeChange
const context = {
window: {
matchMedia(query) {
return { matches: query.includes('dark'), media: query }
},
},
document: {
addEventListener(type, cb) {
if (type === 'omarchy:set-color-scheme') themeChange = cb
},
},
}
vm.runInNewContext(fs.readFileSync(process.argv[2], 'utf8'), context)
const query = context.window.matchMedia('(prefers-color-scheme: dark)')
const calls = []
query.addEventListener('change', { handleEvent: (event) => calls.push(['object', event.matches]) })
query.addEventListener('change', (event) => calls.push(['once', event.matches]), { once: true })
const controller = new AbortController()
query.addEventListener('change', () => calls.push(['aborted']), { signal: controller.signal })
controller.abort()
query.onchange = function (event) {
calls.push(['onchange', event.matches, this === query])
}
themeChange({ detail: { dark: false } })
themeChange({ detail: { dark: true } })
process.stdout.write(JSON.stringify(calls))
JS
)
[[ $listener_check == '[["object",false],["once",false],["onchange",false,true],["object",true],["onchange",true,true]]' ]] ||
fail "prefers-color-scheme shim supports listener objects and onchange" "$listener_check"
pass "prefers-color-scheme shim supports listener objects and onchange"
TMPDIR=$(mktemp -d)
test_home="$TMPDIR/home"
native_manifest="$test_home/.config/chromium/NativeMessagingHosts/com.omarchy.theme.json"
HOME="$test_home" OMARCHY_PATH="$ROOT" omarchy-install-chromium-theme
[[ -f $native_manifest ]] || fail "theme native host installer creates fresh Chromium profile root"
jq -e --arg path "$ROOT/bin/omarchy-chromium-theme-host" '
.name == "com.omarchy.theme" and
.path == $path and
(.allowed_origins | index("chrome-extension://ndpkabodcpddojgepdideonokpblpeln/"))
' "$native_manifest" >/dev/null || fail "theme native host manifest uses Omarchy host path and extension id"
pass "theme native host installer registers the stable extension id"
# Chromium ships in the base packages, so a first install marks every migration
# as already applied; the user install has to register the host itself.
grep -q 'omarchy-install-chromium-theme' "$ROOT/install/user/chromium.sh" ||
fail "user install runs the theme native messaging host setup"
fresh_home="$TMPDIR/fresh-install"
HOME="$fresh_home" OMARCHY_PATH="$ROOT" PATH="$ROOT/bin:$PATH" \
bash -euo pipefail -c 'source "$ROOT/install/user/chromium.sh"'
[[ -f $fresh_home/.config/chromium/NativeMessagingHosts/com.omarchy.theme.json ]] ||
fail "fresh install registers the theme native messaging host"
pass "fresh install registers the theme native messaging host"
# The host reads the active theme and frames it for the extension.
theme_home="$TMPDIR/theme-home"
current="$theme_home/.local/state/omarchy/current"
mkdir -p "$current/theme"
printf 'tokyo-night' >"$current/theme.name"
printf '[colors.primary]\nbackground = "#1a1b26"\n' >"$current/theme/alacritty.toml"
printf 'accent = "#7aa2f7"\n' >"$current/theme/colors.toml"
theme_json=$(HOME="$theme_home" XDG_RUNTIME_DIR="$TMPDIR/run" \
bash -c 'source "$1"; build_state' bash "$ROOT/bin/omarchy-chromium-theme-host")
jq -e '.theme_name == "tokyo-night" and .bg == "#1a1b26" and .accent == "#7aa2f7"' <<<"$theme_json" >/dev/null ||
fail "theme host reads the active Omarchy theme" "$theme_json"
pass "theme host reads the active Omarchy theme"
framed=$(XDG_RUNTIME_DIR="$TMPDIR/run" \
bash -c 'source "$1"; emit "hi"' bash "$ROOT/bin/omarchy-chromium-theme-host" |
od -An -v -tx1 | tr -d ' \n')
[[ $framed == "020000006869" ]] ||
fail "theme host frames messages with a little-endian length prefix" "$framed"
pass "theme host frames messages with a little-endian length prefix"
# omarchy-theme-set calls the refresh to SIGUSR1 every running host; stale
# pidfiles are pruned.
run_dir="$TMPDIR/run/omarchy-theme"
mkdir -p "$run_dir"
echo 999999 >"$run_dir/999999.pid"
# A host can be killed before its EXIT trap removes the pidfile. If Linux later
# reuses that PID, the refresh must not send SIGUSR1 to the unrelated process.
echo "$$ 0" >"$run_dir/reused.pid"
marker="$TMPDIR/partial-pid-signalled"
MARKER="$marker" bash -c 'trap "touch \"$MARKER\"" USR1; while :; do sleep 1; done' &
partial_pid=$!
echo "$partial_pid" >"$run_dir/partial.pid"
# Drive the real host rather than a stand-in sleeper: a synthetic process with
# its own USR1 trap would keep this green even if the host's trap, its watchdog
# wait, or its second framed write regressed. Hold stdin open through a FIFO so
# the watchdog does not see EOF and exit.
host_out="$TMPDIR/host.out"
host_in="$TMPDIR/host.in"
mkfifo "$host_in"
: >"$host_out"
HOME="$theme_home" XDG_RUNTIME_DIR="$TMPDIR/run" \
omarchy-chromium-theme-host <"$host_in" >"$host_out" 2>/dev/null &
host_pid=$!
# Keeping a writer attached is what holds the FIFO open.
sleep 300 >"$host_in" &
host_writer=$!
frame_count() {
node - "$host_out" <<'JS'
const fs = require('fs')
const buf = fs.readFileSync(process.argv[2])
let at = 0
let frames = 0
while (at + 4 <= buf.length) {
const len = buf.readUInt32LE(at)
if (at + 4 + len > buf.length) break
JSON.parse(buf.subarray(at + 4, at + 4 + len).toString('utf8'))
frames++
at += 4 + len
}
process.stdout.write(String(frames))
JS
}
# The host pushes once on connect, before it arms the watchdog.
for _ in $(seq 1 100); do [[ $(frame_count) -ge 1 ]] && break; sleep 0.05; done
[[ $(frame_count) -ge 1 ]] ||
fail "theme host pushes the current theme on connect" "$(od -An -tx1 "$host_out" | head -2)"
[[ -s $run_dir/$host_pid.pid ]] ||
fail "theme host writes its pidfile where the refresh looks" "$(ls "$run_dir")"
watchdog_pid=$(pgrep -P "$host_pid")
[[ -n $watchdog_pid && -z $(pgrep -P "$watchdog_pid") ]] ||
fail "theme host watchdog does not spawn a child that can be orphaned"
XDG_RUNTIME_DIR="$TMPDIR/run" omarchy-chromium-theme-refresh
for _ in $(seq 1 100); do [[ $(frame_count) -ge 2 ]] && break; sleep 0.05; done
frames=$(frame_count)
# Read this before killing the host, which cleans its own pidfile up on exit.
kept_pidfile=$([[ -s $run_dir/$host_pid.pid ]] && echo yes || echo no)
kill "$host_pid" "$host_writer" 2>/dev/null
wait "$host_pid" 2>/dev/null
kill "$partial_pid" 2>/dev/null
wait "$partial_pid" 2>/dev/null
(( frames >= 2 )) ||
fail "theme-set refresh makes the running host push again" "frames=$frames"
# The second frame has to be a usable theme, not just bytes on the pipe.
second=$(node - "$host_out" <<'JS'
const fs = require('fs')
const buf = fs.readFileSync(process.argv[2])
const out = []
let at = 0
while (at + 4 <= buf.length) {
const len = buf.readUInt32LE(at)
if (at + 4 + len > buf.length) break
out.push(JSON.parse(buf.subarray(at + 4, at + 4 + len).toString('utf8')))
at += 4 + len
}
process.stdout.write(JSON.stringify(out[1] || null))
JS
)
jq -e '.theme_name == "tokyo-night" and .bg == "#1a1b26"' <<<"$second" >/dev/null ||
fail "theme host re-pushes a usable theme on refresh" "$second"
[[ ! -f $run_dir/999999.pid ]] || fail "theme-set refresh prunes stale pidfiles"
[[ ! -f $run_dir/reused.pid ]] || fail "theme-set refresh prunes reused pidfiles"
[[ ! -f $run_dir/partial.pid && ! -f $marker ]] ||
fail "theme-set refresh rejects pidfiles without process start time"
# The refresh ran while the host was alive, so its own pidfile had to survive;
# the host removes it through its EXIT trap once we kill it above.
[[ $kept_pidfile == "yes" ]] || fail "theme-set refresh keeps live pidfiles" "$(ls "$run_dir")"
pass "theme-set refresh signals running hosts and prunes stale pidfiles"