#!/bin/bash # omarchy:summary=Native messaging host: push the active Omarchy theme to Chromium web apps # omarchy:hidden=true # Native messaging host for the Omarchy theme bridge. # # Reads the active Omarchy theme and reports background / foreground / accent / # selection / browser-chrome colors to a web-app extension as length-prefixed # JSON on stdout. It's a shared bridge keyed by name (com.omarchy.theme): any # bundled web-app extension that wants to follow the theme connects to it and # lists its ID in the host manifest's allowed_origins. # # This host is PUSH-ONLY: it never parses inbound messages. That's deliberate. # Reading Chromium's length-prefixed framing in bash means blocking in # `head -c4`, which a trap cannot interrupt — so a signal-driven wakeup would # need a background reader plus a FIFO. Instead we let omarchy's theme-set hook # signal us with SIGUSR1 (see bin/omarchy-theme-set) and drop inbound # stdio entirely. The extension gets a push on connect and a push on every # theme change, which is all it ever needs. # # This differs from Omarchy's other bundled hosts (omarchy-chromium-copy-url-host, # -ytdlp-host): those are one-shot — read a frame, act, exit. Theme-following is # the inverse direction (system -> extension), so we stay alive to push; only # their framing idioms carry over, not their structure. # # Because it ships in-tree, `omarchy-theme-set` runs the refresh command on every # switch, so no polling fallback is needed (the standalone AUR build of this host # carries one for old Omarchy). set -uo pipefail # Omarchy 4+ keeps the active theme state here; theme/ is a directory Omarchy # replaces wholesale on every switch (same layout `omarchy-theme-set` writes). CURRENT="$HOME/.local/state/omarchy/current" # Pidfiles live here so the theme-set.d hook can SIGUSR1 every running host. RUNDIR="$XDG_RUNTIME_DIR/omarchy-theme" PIDFILE="$RUNDIR/$$.pid" WATCHDOG_PID="" # ---------------------------------------------------------------- lifecycle -- cleanup() { rm -f "$PIDFILE" [[ -n $WATCHDOG_PID ]] && kill "$WATCHDOG_PID" 2>/dev/null return 0 } # ------------------------------------------------------------------ parsing -- trim() { local s="$1" s="${s#"${s%%[![:space:]]*}"}" s="${s%"${s##*[![:space:]]}"}" printf '%s' "$s" } read_text() { [[ -f $1 ]] || return 0 local text text=$(<"$1") || return 0 trim "$text" } # The terminal background lives under [colors.primary] in the theme's # alacritty.toml. Only that section counts — other sections define their own # `background` keys that would otherwise match. parse_alacritty_bg() { local file="$1" line s in_primary=0 [[ -f $file ]] || return 0 while IFS= read -r line || [[ -n $line ]]; do s=$(trim "$line") if [[ $s == \[* ]]; then if [[ $s == "[colors.primary]" ]]; then in_primary=1; else in_primary=0; fi continue fi if ((in_primary)) && [[ $s =~ ^background[[:space:]]*=[[:space:]]*\"(#[0-9a-fA-F]{6,8})\" ]]; then printf '%s' "${BASH_REMATCH[1]}" return 0 fi done <"$file" } # colors.toml is a flat list of `name = "#rrggbb"` pairs. Populates COLORS. declare -A COLORS=() parse_colors_toml() { local file="$1" line s COLORS=() [[ -f $file ]] || return 0 while IFS= read -r line || [[ -n $line ]]; do s=$(trim "$line") if [[ $s =~ ^([A-Za-z0-9_]+)[[:space:]]*=[[:space:]]*\"(#[0-9a-fA-F]{6,8})\" ]]; then COLORS["${BASH_REMATCH[1]}"]="${BASH_REMATCH[2]}" fi done <"$file" } # omarchy ships the browser chrome color as 'r,g,b' decimal CSV in chromium.theme. # Most stock themes omit the file — omarchy-theme-set-browser then falls back to # #1c2027, but we return empty and let the extension pick its own fallback so it # can be theme-aware about the choice. parse_chromium_theme() { local file="$1" text r g b extra c [[ -f $file ]] || return 0 text=$(<"$file") || return 0 text=$(trim "$text") IFS=, read -r r g b extra <<<"$text" [[ -n ${extra:-} ]] && return 0 r=$(trim "${r:-}") g=$(trim "${g:-}") b=$(trim "${b:-}") for c in "$r" "$g" "$b"; do [[ $c =~ ^[0-9]+$ ]] || return 0 ((c >= 0 && c <= 255)) || return 0 done printf '#%02x%02x%02x' "$r" "$g" "$b" } # --------------------------------------------------------------------- JSON -- json_escape() { local s="$1" s="${s//\\/\\\\}" s="${s//\"/\\\"}" s="${s//$'\n'/\\n}" s="${s//$'\r'/\\r}" s="${s//$'\t'/\\t}" printf '%s' "$s" } # Emit a JSON string, or bare null when empty, so the extension can tell "theme # didn't define this color" from "empty string". json_value() { if [[ -z ${1:-} ]]; then printf 'null' else printf '"%s"' "$(json_escape "$1")" fi } build_state() { local name bg fg accent selection chrome name=$(read_text "$CURRENT/theme.name") bg=$(parse_alacritty_bg "$CURRENT/theme/alacritty.toml") parse_colors_toml "$CURRENT/theme/colors.toml" chrome=$(parse_chromium_theme "$CURRENT/theme/chromium.theme") [[ -z $bg ]] && bg="${COLORS[background]:-}" [[ -z $bg ]] && bg="#1e1e2e" # last-resort fallback fg="${COLORS[foreground]:-}" accent="${COLORS[accent]:-}" selection="${COLORS[selection_background]:-}" # No day/night flag by design — the extension decides dark vs. light purely # from the WCAG relative luminance of bg. printf '{"theme_name":%s,"bg":%s,"fg":%s,"accent":%s,"selection_bg":%s,"chrome":%s}' \ "$(json_value "$name")" \ "$(json_value "$bg")" \ "$(json_value "$fg")" \ "$(json_value "$accent")" \ "$(json_value "$selection")" \ "$(json_value "$chrome")" } # ------------------------------------------------------------------ framing -- # Chromium expects each message prefixed with its byte length as a native-endian # (little, on every platform we target) uint32. emit() { local json="$1" len len=$(LC_ALL=C printf '%s' "$json" | wc -c) printf '%b%s' \ "\\x$(printf '%02x' $((len & 0xff)))\\x$(printf '%02x' $((len >> 8 & 0xff)))\\x$(printf '%02x' $((len >> 16 & 0xff)))\\x$(printf '%02x' $((len >> 24 & 0xff)))" \ "$json" || exit 0 # stdout closed: the browser is gone } emit_theme() { local state state=$(build_state) || return 0 emit "$state" } # --------------------------------------------------------------------- main -- main() { local starttime trap cleanup EXIT # Arm signal handlers before publishing the pidfile so refresh cannot signal # the host during a window where USR1 still has its default fatal disposition. trap 'exit 0' TERM INT trap 'emit_theme' USR1 mkdir -p "$RUNDIR" starttime=$(awk '{print $22}' "/proc/$$/stat") [[ -n $starttime ]] || exit 1 printf '%s %s\n' "$$" "$starttime" >"$PIDFILE" # Push the current theme immediately on connect, before arming the watchdog — # if stdin is already at EOF the watchdog fires at once, and we'd otherwise be # killed before saying anything. emit_theme # The browser talks to us by closing the pipe, not by sending anything we care # about. Drain stdin so its writes never block, and exit when it hangs up. # # The `<&3` is load-bearing. Bash gives every background job /dev/null as stdin # unless it's redirected explicitly, so a bare read loop here would see EOF # instantly and kill us right after the first push — leaving the extension in a # reconnect loop. Duplicating the real stdin onto fd 3 overrides the default. exec 3<&0 (while IFS= read -r; do :; done <&3; kill -TERM "$$" 2>/dev/null) & WATCHDOG_PID=$! # Stay alive until the browser closes the pipe. A SIGUSR1 interrupts `wait`, # runs the USR1 trap to push, and we resume waiting; when the watchdog exits # (browser gone) `wait` succeeds and we fall through to the EXIT trap. while ! wait "$WATCHDOG_PID"; do :; done } if [[ ${BASH_SOURCE[0]} == "$0" ]]; then main "$@" fi