|
|
|
@@ -1,224 +0,0 @@
|
|
|
|
|
#!/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
|