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,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"
|
||||
Reference in New Issue
Block a user