From 3ea5c7446694674c2c8dd932a6fc9e5d41d7b605 Mon Sep 17 00:00:00 2001 From: Chris Birch Date: Wed, 21 Oct 2026 01:40:04 -0400 Subject: [PATCH 1/2] feat(vivaldi): generate a Vivaldi theme and install it on theme apply Each theme gains vivaldi.json, one entry of Vivaldi's vivaldi.themes.user pref. apply_vivaldi upserts it into every profile's Preferences and points vivaldi.themes.current and both OS-schedule slots at it, keeping the user's own themes and every other pref. Vivaldi holds Preferences in memory and writes all of it back on exit, so a write while it runs is lost. An apply that finds it open leaves a detached waiter, which installs whatever theme is current once that Vivaldi's process has exited. The theme shows at the next launch, and the tick says so. No outside route reaches an open window: CSS mods are linked once per window, remote debugging is refused on the default profile, and no policy maps to a vivaldi.* pref. theme browsers reports Vivaldi's installed theme and a pending install. The README no longer claims Chromium is applied, which nothing does. Fixes #1636 Agent-Name: claude-code Agent-Session: 0105027b-1083-4e8b-8f26-8733a784a41b --- CLAUDE.md | 20 +++- README.md | 9 +- bin/theme | 17 ++- lib/generate-all.sh | 1 + lib/generators/vivaldi.sh | 74 ++++++++++++++ lib/lib.sh | 38 +++++++ lib/vivaldi-install-after-exit.sh | 10 ++ lib/vivaldi-preferences.sh | 125 +++++++++++++++++++++++ tests/vivaldi-preferences.bats | 37 +++++++ themes/cendre-medium/vivaldi.json | 25 +++++ themes/cendre-soft/vivaldi.json | 25 +++++ themes/cendre/vivaldi.json | 25 +++++ themes/charcoal-ember/vivaldi.json | 25 +++++ themes/everforest-dark-hard/vivaldi.json | 25 +++++ themes/flexoki-moon-black/vivaldi.json | 25 +++++ themes/flexoki-moon-green/vivaldi.json | 25 +++++ themes/flexoki-moon-purple/vivaldi.json | 25 +++++ themes/flexoki-moon-red/vivaldi.json | 25 +++++ themes/gruvbox/vivaldi.json | 25 +++++ themes/kanagawa/vivaldi.json | 25 +++++ themes/nightfox/vivaldi.json | 25 +++++ themes/nordic/vivaldi.json | 25 +++++ themes/oceanic-next/vivaldi.json | 25 +++++ themes/popping-and-locking/vivaldi.json | 25 +++++ themes/retrobox/vivaldi.json | 25 +++++ themes/rose-pine/vivaldi.json | 25 +++++ themes/solarized-osaka/vivaldi.json | 25 +++++ themes/sora/vivaldi.json | 25 +++++ themes/terafox/vivaldi.json | 25 +++++ 29 files changed, 824 insertions(+), 7 deletions(-) create mode 100755 lib/generators/vivaldi.sh create mode 100755 lib/vivaldi-install-after-exit.sh create mode 100755 lib/vivaldi-preferences.sh create mode 100755 tests/vivaldi-preferences.bats create mode 100644 themes/cendre-medium/vivaldi.json create mode 100644 themes/cendre-soft/vivaldi.json create mode 100644 themes/cendre/vivaldi.json create mode 100644 themes/charcoal-ember/vivaldi.json create mode 100644 themes/everforest-dark-hard/vivaldi.json create mode 100644 themes/flexoki-moon-black/vivaldi.json create mode 100644 themes/flexoki-moon-green/vivaldi.json create mode 100644 themes/flexoki-moon-purple/vivaldi.json create mode 100644 themes/flexoki-moon-red/vivaldi.json create mode 100644 themes/gruvbox/vivaldi.json create mode 100644 themes/kanagawa/vivaldi.json create mode 100644 themes/nightfox/vivaldi.json create mode 100644 themes/nordic/vivaldi.json create mode 100644 themes/oceanic-next/vivaldi.json create mode 100644 themes/popping-and-locking/vivaldi.json create mode 100644 themes/retrobox/vivaldi.json create mode 100644 themes/rose-pine/vivaldi.json create mode 100644 themes/solarized-osaka/vivaldi.json create mode 100644 themes/sora/vivaldi.json create mode 100644 themes/terafox/vivaldi.json diff --git a/CLAUDE.md b/CLAUDE.md index 34e6dce..db14d56 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,9 +59,13 @@ beside the others, and an exported config is self-describing. Nothing is pruned, deliberately. These are also the directories a user's own themes live in, and an installer that deletes what it does not recognize is worse than a few stale kilobytes. -Four apps have no pointer at all. dunst reads `dunstrc.d/` as a directory, firefox and Windows -Terminal have their real file rewritten, and sioyek gets a managed block spliced into its own -config. +These apps have no pointer at all: + +- dunst reads `dunstrc.d/` as a directory. +- firefox and Windows Terminal have their real file rewritten. +- sioyek gets a managed block spliced into its own config. +- Vivaldi gets its theme upserted into each profile's `Preferences`, and only while Vivaldi is + closed. `tests/apply-install.bats` pins it, including the two migration cases off the copy-based scheme: a `current` left as a real file, and a `current.yazi` left as a real directory, which `ln -sfn` @@ -350,6 +354,16 @@ CLI verbs. These generators behave unlike the rest: - `aerc.sh` relies on aerc.conf pinning `styleset-name = current`, so the applied filename is a contract. - `firefox-based.sh` writes one userChrome.css covering Firefox, Zen, Librewolf and Thunderbird. - `vscode.sh` is not wired into `theme apply`; run it directly when needed. +- `vivaldi.sh` emits one entry of Vivaldi's `vivaldi.themes.user` pref, which + `lib/vivaldi-preferences.sh` upserts and selects. Vivaldi holds `Preferences` in memory and + writes all of it back on exit, so a write while it runs is lost. `apply_vivaldi` therefore + installs only into a closed Vivaldi. When Vivaldi is open, it leaves a detached waiter that + installs whatever theme is current once that Vivaldi's process exits. Nothing outside Vivaldi + can change an open window's theme, so the change shows at the next launch: + - CSS mods are linked once per window. + - `--remote-debugging-port` is refused on the default profile. + - No enterprise policy maps to a `vivaldi.*` pref. `BrowserThemeColor` tints Chromium's native + toolbar, which Vivaldi does not draw with. - `glow.sh` emits a glamour style whose code blocks reach the screen quantized to xterm-256. glow never sets glamour's chroma formatter, so only prose keeps true color. `apply_glow` writes `glow.yml`'s `style` as an absolute path, because glamour expands neither `~` nor `$HOME` and diff --git a/README.md b/README.md index f0498fc..4e1db16 100644 --- a/README.md +++ b/README.md @@ -140,11 +140,16 @@ Restart the terminal to see opacity changes. theme browsers # Show detected browsers and theme status ``` -Firefox-based browsers (Zen, Librewolf, Firefox, Thunderbird) and Chromium are +Firefox-based browsers (Zen, Librewolf, Firefox, Thunderbird) and Vivaldi are themed automatically on `theme apply`. Firefox-based browsers require `toolkit.legacyUserProfileCustomizations.stylesheets = true` in `about:config`, then a restart, for `userChrome.css` to take effect. +Vivaldi gets a native Vivaldi theme, selected for both its light and dark +schedule. Vivaldi rewrites its own settings when it quits, so an apply while it +is open waits for it to exit and installs the theme then. The theme shows at its +next launch. + ### Cross-Machine Sync Sync your theme preferences across machines using a GitHub Gist: @@ -197,7 +202,7 @@ Both commands come from [`bashselfupdate`](https://github.com/datapointchris/bas - **yazi** — file manager - **sioyek** — PDF viewer (custom color mode) - **Neovim** — text editor (via colorscheme plugins or generated) -- **Browsers** — Firefox-based (Zen, Librewolf, Firefox, Thunderbird) and Chromium +- **Browsers** — Firefox-based (Zen, Librewolf, Firefox, Thunderbird) and Vivaldi - **Wallpaper** — themed desktop background, set through Finder on macOS, hyprpaper on Hyprland, and the Windows host on WSL diff --git a/bin/theme b/bin/theme index a0968a0..ff22069 100755 --- a/bin/theme +++ b/bin/theme @@ -576,7 +576,7 @@ browsers_usage() { print_section "Description" "blue" cat <<'EOF' Show detected browsers and their theme configuration status. - Supports Firefox-based browsers: Zen, Librewolf, Firefox, Thunderbird. + Supports Firefox-based browsers (Zen, Librewolf, Firefox, Thunderbird) and Vivaldi. EOF print_section "Setup Required" "blue" @@ -600,7 +600,7 @@ Browser Theme Diagnostics Description ----------- Show detected browsers and their theme configuration status. - Supports Firefox-based browsers: Zen, Librewolf, Firefox, Thunderbird. + Supports Firefox-based browsers (Zen, Librewolf, Firefox, Thunderbird) and Vivaldi. Setup Required -------------- @@ -840,6 +840,19 @@ show_browsers() { echo " toolkit.legacyUserProfileCustomizations.stylesheets = true" echo " Then restart the browser." + local vivaldi_dir + vivaldi_dir=$(vivaldi_user_data_dir) + if [[ -f "$vivaldi_dir/Local State" ]]; then + echo "" + echo "Vivaldi:" + local vivaldi_theme + vivaldi_theme=$(jq -r '.vivaldi.themes.current // "none"' "$vivaldi_dir/Default/Preferences" 2>/dev/null || echo "unknown") + echo " Theme: $vivaldi_theme" + if vivaldi_waiter_running; then + echo " Pending: the current theme installs when Vivaldi exits" + fi + fi + # Show chromium status if chromium.theme exists local chromium_theme_file="$HOME/.config/chromium/chromium.theme" if [[ -f "$chromium_theme_file" ]]; then diff --git a/lib/generate-all.sh b/lib/generate-all.sh index 6789b5a..5324cb5 100755 --- a/lib/generate-all.sh +++ b/lib/generate-all.sh @@ -52,6 +52,7 @@ declare -A GENERATOR_OUTPUT=( ["icons"]="icons.theme" ["hyprland-picker"]="hyprland-picker.css" ["chromium"]="chromium.theme" + ["vivaldi"]="vivaldi.json" ["firefox-based"]="userChrome.css" ["bat"]="bat.tmTheme" ["sioyek"]="sioyek.config" diff --git a/lib/generators/vivaldi.sh b/lib/generators/vivaldi.sh new file mode 100755 index 0000000..0ea67e5 --- /dev/null +++ b/lib/generators/vivaldi.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# Generate a Vivaldi theme object from theme.yml +# Usage: vivaldi.sh [output-file] +# +# The object is one entry of Vivaldi's `vivaldi.themes.user` pref, the same +# shape a theme archive's settings.json carries. `theme apply` upserts it into +# the profile's Preferences; see lib/vivaldi-preferences.sh. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/../theme.sh" + +if [[ $# -lt 1 ]]; then + echo "Usage: $0 [output-file]" + exit 1 +fi + +input_file="$1" +output_file="${2:-}" + +eval "$(load_colors "$input_file")" + +UI_ACCENT="${EXTENDED_UI_ACCENT:-$BASE0D}" +UI_SELECTION="${EXTENDED_UI_SELECTION:-$BASE02}" + +# Vivaldi derives every shade, hover and contrast text from these five, so the +# palette's own intermediate slots have nowhere to go. +# +# Vivaldi paints its accent across the whole toolbar row and the active tab, so +# the accent takes the selection shade that firefox-based.sh gives a selected +# tab. The loud UI accent goes to highlight, which marks focus and selection. +generate() { + jq -n \ + --arg id "theme-${THEME_SLUG}" \ + --arg name "$THEME_NAME" \ + --arg bg "$SPECIAL_BG" \ + --arg fg "$SPECIAL_FG" \ + --arg window "$BASE01" \ + --arg accent "$UI_SELECTION" \ + --arg highlight "$UI_ACCENT" \ + '{ + engineVersion: 1, + version: 1, + id: $id, + url: "", + name: $name, + accentFromPage: false, + accentOnWindow: false, + accentSaturationLimit: 1, + backgroundImage: "", + backgroundPosition: "stretch", + backgroundSource: "", + radius: 4, + colorAccentBg: $accent, + colorBg: $bg, + colorFg: $fg, + colorHighlightBg: $highlight, + colorWindowBg: $window, + contrast: 0, + preferSystemAccent: false, + alpha: 1, + blur: 0, + transparencyTabs: false, + transparencyTabBar: false + }' +} + +if [[ -n "$output_file" ]]; then + generate >"$output_file" + echo "Generated: $output_file" +else + generate +fi diff --git a/lib/lib.sh b/lib/lib.sh index 86aacaf..ddaa342 100755 --- a/lib/lib.sh +++ b/lib/lib.sh @@ -16,6 +16,7 @@ THEME_APP_DIR="$(cd "$THEME_LIB_DIR/.." && pwd)" # the other, and on Arch that silently skipped Hyprland, Waybar, ghostty, kitty # and the wallpaper. storage.sh also owns THEME_STATE_DIR and the history paths. source "$THEME_LIB_DIR/storage.sh" +source "$THEME_LIB_DIR/vivaldi-preferences.sh" # Always production, unlike THEME_STATE_DIR: apps watch this directory, so dev # mode must not point them at .dev-data. @@ -45,6 +46,7 @@ THEME_APP_ARTIFACTS=( "sioyek.config:sioyek" "userChrome.css:Firefox-based" "chromium.theme:Chromium" + "vivaldi.json:Vivaldi" "bordersrc:JankyBorders" "hyprland.lua:Hyprland" "hyprlock.conf:Hyprlock" @@ -620,6 +622,30 @@ apply_firefox_based() { return 0 } +# Apply Vivaldi theme (macOS and Arch) +# Prints "at next launch" when Vivaldi is open, and nothing when installed. +apply_vivaldi() { + local theme="$1" + local lib_path + lib_path=$(get_library_path "$theme") + + if [[ -z "$lib_path" ]] || [[ ! -f "$lib_path/vivaldi.json" ]]; then + return 1 + fi + + local dir + dir=$(vivaldi_user_data_dir) + [[ -f "$dir/Local State" ]] || return 1 + + if vivaldi_running_pid "$dir" >/dev/null; then + vivaldi_waiter_running || vivaldi_start_waiter "$dir" || return 1 + echo "at next launch" + return 0 + fi + + vivaldi_install_theme "$dir" "$lib_path/vivaldi.json" +} + # Apply sioyek PDF viewer theme (all platforms) # Splices a marker-delimited managed block into the user's prefs_user.config. # @@ -2121,6 +2147,18 @@ apply_theme_to_apps() { _print_app_status "browsers" "false" fi + # Vivaldi (macOS and Arch) + if [[ "$platform" == "macos" ]] || [[ "$platform" == "arch" ]]; then + local vivaldi_status + if vivaldi_status=$(apply_vivaldi "$theme" 2>/dev/null); then + applied+=("vivaldi") + _print_app_status "vivaldi${vivaldi_status:+ ($vivaldi_status)}" "true" + else + skipped+=("vivaldi") + _print_app_status "vivaldi" "false" + fi + fi + # bat syntax highlighter (all platforms) if apply_bat "$theme" 2>/dev/null; then applied+=("bat") diff --git a/lib/vivaldi-install-after-exit.sh b/lib/vivaldi-install-after-exit.sh new file mode 100755 index 0000000..de5b2f5 --- /dev/null +++ b/lib/vivaldi-install-after-exit.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# Install the current theme into Vivaldi once the Vivaldi holding the given +# user-data dir has exited. Started detached by apply_vivaldi. +# Usage: vivaldi-install-after-exit.sh + +set -euo pipefail + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +vivaldi_install_after_exit "$1" diff --git a/lib/vivaldi-preferences.sh b/lib/vivaldi-preferences.sh new file mode 100755 index 0000000..6504c00 --- /dev/null +++ b/lib/vivaldi-preferences.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# Installs a generated Vivaldi theme into Vivaldi's own profile. +# +# Vivaldi keeps its themes in each profile's Preferences and holds that file in +# memory while it runs, writing all of it back on exit. A write made while it is +# open is therefore overwritten, so a theme is installed only while Vivaldi is +# closed. An apply that finds it open leaves a waiter behind, which installs +# whatever theme is current once that Vivaldi exits. + +VIVALDI_WAITER_PID_FILE="$HOME/.cache/theme/vivaldi-waiter.pid" + +vivaldi_user_data_dir() { + case "$(uname -s)" in + Darwin) echo "$HOME/Library/Application Support/Vivaldi" ;; + *) echo "${XDG_CONFIG_HOME:-$HOME/.config}/vivaldi" ;; + esac +} + +# The pid of the Vivaldi holding this user-data dir, read from Chromium's own +# SingletonLock symlink, whose target is `-`. A crash leaves the link +# naming a dead pid, which counts as not running. +vivaldi_running_pid() { + local dir="$1" + local target + target=$(readlink "$dir/SingletonLock" 2>/dev/null) || return 1 + + local host="${target%-*}" pid="${target##*-}" + [[ "$host" == "$(uname -n)" ]] || return 1 + [[ "$pid" =~ ^[0-9]+$ ]] || return 1 + kill -0 "$pid" 2>/dev/null || return 1 + echo "$pid" +} + +# Upsert one theme object into every profile's Preferences and select it. +# +# Both OS-schedule slots are pointed at it as well as `current`, because +# Vivaldi's default schedule follows the OS light/dark setting and reads those +# slots instead of `current`. Every other user theme stays installed. +vivaldi_install_theme() { + local dir="$1" + local theme_json="$2" + + [[ -f "$dir/Local State" && -f "$theme_json" ]] || return 1 + + local profiles + profiles=$(jq -r '.profile.info_cache // {} | keys[]' "$dir/Local State") || return 1 + + local profile prefs staged + while IFS= read -r profile; do + [[ -n "$profile" ]] || continue + prefs="$dir/$profile/Preferences" + [[ -f "$prefs" ]] || continue + + # Staged beside the file so the mv is a rename; mktemp's 0600 matches + # Chromium's own mode for it. + staged=$(mktemp "$prefs.theme.XXXXXX") || return 1 + if ! jq -c --slurpfile theme "$theme_json" ' + $theme[0] as $t + | .vivaldi.themes.user = ([(.vivaldi.themes.user // [])[] | select(.id != $t.id)] + [$t]) + | .vivaldi.themes.current = $t.id + | .vivaldi.theme.schedule.o_s = ((.vivaldi.theme.schedule.o_s // {}) + {light: $t.id, dark: $t.id}) + ' "$prefs" >"$staged"; then + rm -f "$staged" + return 1 + fi + mv "$staged" "$prefs" || return 1 + done <<<"$profiles" +} + +# Whether an earlier apply already left a waiter, which will install whatever +# theme is current when it fires, so one is enough. +vivaldi_waiter_running() { + local pid + pid=$(cat "$VIVALDI_WAITER_PID_FILE" 2>/dev/null) || return 1 + [[ "$pid" =~ ^[0-9]+$ ]] && kill -0 "$pid" 2>/dev/null +} + +# Detach a waiter from this apply. A scheduled apply runs as a oneshot whose +# cgroup systemd kills on exit, so on Linux the waiter is its own transient +# service. On macOS the launchd agent sets AbandonProcessGroup, so nohup is +# enough there. +vivaldi_start_waiter() { + local dir="$1" + local waiter="$THEME_LIB_DIR/vivaldi-install-after-exit.sh" + + if [[ "$(uname -s)" != "Darwin" ]] && systemctl --user show-environment &>/dev/null; then + systemd-run --user --collect --quiet -E HOME -E PATH -- "$waiter" "$dir" >/dev/null 2>&1 /dev/null 2>&1 "$VIVALDI_WAITER_PID_FILE" + trap 'rm -f "$VIVALDI_WAITER_PID_FILE"' EXIT + + local deadline="" + trap 'deadline=$((SECONDS + 15))' TERM HUP + + local pid + while pid=$(vivaldi_running_pid "$dir"); do + while kill -0 "$pid" 2>/dev/null; do + if [[ -n "$deadline" ]] && ((SECONDS >= deadline)); then + return 1 + fi + sleep 1 + done + done + + local theme + theme=$(cat "$CURRENT_THEME_FILE" 2>/dev/null) || return 1 + vivaldi_install_theme "$dir" "$THEMES_DIR/$theme/vivaldi.json" +} diff --git a/tests/vivaldi-preferences.bats b/tests/vivaldi-preferences.bats new file mode 100755 index 0000000..588cee9 --- /dev/null +++ b/tests/vivaldi-preferences.bats @@ -0,0 +1,37 @@ +#!/usr/bin/env bats +# Installing a theme into Vivaldi's Preferences. +# +# The file is the user's whole browser profile, rewritten in place, so an apply +# must change nothing but the theme: their own themes and every other pref +# survive, a reapplied theme is replaced rather than duplicated, and ours is +# selected under both OS-schedule slots as well as `current`. + +load "$HOME/.local/lib/bats-support/load.bash" +load "$HOME/.local/lib/bats-assert/load.bash" + +setup() { + load "$BATS_TEST_DIRNAME/helpers.bash" + isolate_theme_state + source_theme_libs + + vivaldi="$BATS_TEST_TMPDIR/vivaldi" + mkdir -p "$vivaldi/Default" + printf '{"profile":{"info_cache":{"Default":{}}}}' >"$vivaldi/Local State" + printf '{"browser":{"has_seen_welcome_page":true},"vivaldi":{"theme":{"schedule":{"o_s":{"light":"Vivaldi1","dark":"Vivaldi2"}}},"themes":{"user":[{"id":"mine","name":"Mine"}]}}}' \ + >"$vivaldi/Default/Preferences" + printf '{"id":"theme-a","name":"A"}' >"$BATS_TEST_TMPDIR/a.json" + printf '{"id":"theme-b","name":"B"}' >"$BATS_TEST_TMPDIR/b.json" +} + +@test "an install keeps the user's themes and prefs and selects the theme" { + vivaldi_install_theme "$vivaldi" "$BATS_TEST_TMPDIR/a.json" + vivaldi_install_theme "$vivaldi" "$BATS_TEST_TMPDIR/b.json" + vivaldi_install_theme "$vivaldi" "$BATS_TEST_TMPDIR/b.json" + + run jq -c '[.vivaldi.themes.user[].id], .vivaldi.themes.current, .vivaldi.theme.schedule.o_s, .browser' \ + "$vivaldi/Default/Preferences" + assert_output '["mine","theme-a","theme-b"] +"theme-b" +{"light":"theme-b","dark":"theme-b"} +{"has_seen_welcome_page":true}' +} diff --git a/themes/cendre-medium/vivaldi.json b/themes/cendre-medium/vivaldi.json new file mode 100644 index 0000000..f8dba63 --- /dev/null +++ b/themes/cendre-medium/vivaldi.json @@ -0,0 +1,25 @@ +{ + "engineVersion": 1, + "version": 1, + "id": "theme-cendre-medium", + "url": "", + "name": "Cendre Medium", + "accentFromPage": false, + "accentOnWindow": false, + "accentSaturationLimit": 1, + "backgroundImage": "", + "backgroundPosition": "stretch", + "backgroundSource": "", + "radius": 4, + "colorAccentBg": "#36241d", + "colorBg": "#1d1917", + "colorFg": "#e6d5c2", + "colorHighlightBg": "#ea9875", + "colorWindowBg": "#26211f", + "contrast": 0, + "preferSystemAccent": false, + "alpha": 1, + "blur": 0, + "transparencyTabs": false, + "transparencyTabBar": false +} diff --git a/themes/cendre-soft/vivaldi.json b/themes/cendre-soft/vivaldi.json new file mode 100644 index 0000000..352790d --- /dev/null +++ b/themes/cendre-soft/vivaldi.json @@ -0,0 +1,25 @@ +{ + "engineVersion": 1, + "version": 1, + "id": "theme-cendre-soft", + "url": "", + "name": "Cendre Soft", + "accentFromPage": false, + "accentOnWindow": false, + "accentSaturationLimit": 1, + "backgroundImage": "", + "backgroundPosition": "stretch", + "backgroundSource": "", + "radius": 4, + "colorAccentBg": "#3d2b23", + "colorBg": "#231f1d", + "colorFg": "#e6d5c2", + "colorHighlightBg": "#ea9875", + "colorWindowBg": "#2d2725", + "contrast": 0, + "preferSystemAccent": false, + "alpha": 1, + "blur": 0, + "transparencyTabs": false, + "transparencyTabBar": false +} diff --git a/themes/cendre/vivaldi.json b/themes/cendre/vivaldi.json new file mode 100644 index 0000000..3b42126 --- /dev/null +++ b/themes/cendre/vivaldi.json @@ -0,0 +1,25 @@ +{ + "engineVersion": 1, + "version": 1, + "id": "theme-cendre", + "url": "", + "name": "Cendre", + "accentFromPage": false, + "accentOnWindow": false, + "accentSaturationLimit": 1, + "backgroundImage": "", + "backgroundPosition": "stretch", + "backgroundSource": "", + "radius": 4, + "colorAccentBg": "#2f1e17", + "colorBg": "#171311", + "colorFg": "#e6d5c2", + "colorHighlightBg": "#ea9875", + "colorWindowBg": "#201b19", + "contrast": 0, + "preferSystemAccent": false, + "alpha": 1, + "blur": 0, + "transparencyTabs": false, + "transparencyTabBar": false +} diff --git a/themes/charcoal-ember/vivaldi.json b/themes/charcoal-ember/vivaldi.json new file mode 100644 index 0000000..472f3e9 --- /dev/null +++ b/themes/charcoal-ember/vivaldi.json @@ -0,0 +1,25 @@ +{ + "engineVersion": 1, + "version": 1, + "id": "theme-charcoal-ember", + "url": "", + "name": "Charcoal Ember", + "accentFromPage": false, + "accentOnWindow": false, + "accentSaturationLimit": 1, + "backgroundImage": "", + "backgroundPosition": "stretch", + "backgroundSource": "", + "radius": 4, + "colorAccentBg": "#373b41", + "colorBg": "#1d1f21", + "colorFg": "#c5c8c6", + "colorHighlightBg": "#4eb6fe", + "colorWindowBg": "#2d2d2d", + "contrast": 0, + "preferSystemAccent": false, + "alpha": 1, + "blur": 0, + "transparencyTabs": false, + "transparencyTabBar": false +} diff --git a/themes/everforest-dark-hard/vivaldi.json b/themes/everforest-dark-hard/vivaldi.json new file mode 100644 index 0000000..2100853 --- /dev/null +++ b/themes/everforest-dark-hard/vivaldi.json @@ -0,0 +1,25 @@ +{ + "engineVersion": 1, + "version": 1, + "id": "theme-everforest-dark-hard", + "url": "", + "name": "Everforest Dark Hard", + "accentFromPage": false, + "accentOnWindow": false, + "accentSaturationLimit": 1, + "backgroundImage": "", + "backgroundPosition": "stretch", + "backgroundSource": "", + "radius": 4, + "colorAccentBg": "#4c3743", + "colorBg": "#1e2326", + "colorFg": "#d3c6aa", + "colorHighlightBg": "#a7c080", + "colorWindowBg": "#2e383c", + "contrast": 0, + "preferSystemAccent": false, + "alpha": 1, + "blur": 0, + "transparencyTabs": false, + "transparencyTabBar": false +} diff --git a/themes/flexoki-moon-black/vivaldi.json b/themes/flexoki-moon-black/vivaldi.json new file mode 100644 index 0000000..4594c15 --- /dev/null +++ b/themes/flexoki-moon-black/vivaldi.json @@ -0,0 +1,25 @@ +{ + "engineVersion": 1, + "version": 1, + "id": "theme-flexoki-moon-black", + "url": "", + "name": "Flexoki Moon Black", + "accentFromPage": false, + "accentOnWindow": false, + "accentSaturationLimit": 1, + "backgroundImage": "", + "backgroundPosition": "stretch", + "backgroundSource": "", + "radius": 4, + "colorAccentBg": "#343331", + "colorBg": "#100f0f", + "colorFg": "#cecdc3", + "colorHighlightBg": "#3aa99f", + "colorWindowBg": "#1c1b1a", + "contrast": 0, + "preferSystemAccent": false, + "alpha": 1, + "blur": 0, + "transparencyTabs": false, + "transparencyTabBar": false +} diff --git a/themes/flexoki-moon-green/vivaldi.json b/themes/flexoki-moon-green/vivaldi.json new file mode 100644 index 0000000..f786391 --- /dev/null +++ b/themes/flexoki-moon-green/vivaldi.json @@ -0,0 +1,25 @@ +{ + "engineVersion": 1, + "version": 1, + "id": "theme-flexoki-moon-green", + "url": "", + "name": "Flexoki Moon Green", + "accentFromPage": false, + "accentOnWindow": false, + "accentSaturationLimit": 1, + "backgroundImage": "", + "backgroundPosition": "stretch", + "backgroundSource": "", + "radius": 4, + "colorAccentBg": "#2c3b2d", + "colorBg": "#08120b", + "colorFg": "#ddd8e8", + "colorHighlightBg": "#3aa99f", + "colorWindowBg": "#151f18", + "contrast": 0, + "preferSystemAccent": false, + "alpha": 1, + "blur": 0, + "transparencyTabs": false, + "transparencyTabBar": false +} diff --git a/themes/flexoki-moon-purple/vivaldi.json b/themes/flexoki-moon-purple/vivaldi.json new file mode 100644 index 0000000..8efea45 --- /dev/null +++ b/themes/flexoki-moon-purple/vivaldi.json @@ -0,0 +1,25 @@ +{ + "engineVersion": 1, + "version": 1, + "id": "theme-flexoki-moon-purple", + "url": "", + "name": "Flexoki Moon Purple", + "accentFromPage": false, + "accentOnWindow": false, + "accentSaturationLimit": 1, + "backgroundImage": "", + "backgroundPosition": "stretch", + "backgroundSource": "", + "radius": 4, + "colorAccentBg": "#252330", + "colorBg": "#12101d", + "colorFg": "#ddd8e8", + "colorHighlightBg": "#3aa99f", + "colorWindowBg": "#1c1a25", + "contrast": 0, + "preferSystemAccent": false, + "alpha": 1, + "blur": 0, + "transparencyTabs": false, + "transparencyTabBar": false +} diff --git a/themes/flexoki-moon-red/vivaldi.json b/themes/flexoki-moon-red/vivaldi.json new file mode 100644 index 0000000..4de994c --- /dev/null +++ b/themes/flexoki-moon-red/vivaldi.json @@ -0,0 +1,25 @@ +{ + "engineVersion": 1, + "version": 1, + "id": "theme-flexoki-moon-red", + "url": "", + "name": "Flexoki Moon Red", + "accentFromPage": false, + "accentOnWindow": false, + "accentSaturationLimit": 1, + "backgroundImage": "", + "backgroundPosition": "stretch", + "backgroundSource": "", + "radius": 4, + "colorAccentBg": "#42322e", + "colorBg": "#18090b", + "colorFg": "#f0e8dd", + "colorHighlightBg": "#3aa99f", + "colorWindowBg": "#251a1a", + "contrast": 0, + "preferSystemAccent": false, + "alpha": 1, + "blur": 0, + "transparencyTabs": false, + "transparencyTabBar": false +} diff --git a/themes/gruvbox/vivaldi.json b/themes/gruvbox/vivaldi.json new file mode 100644 index 0000000..c7b03b3 --- /dev/null +++ b/themes/gruvbox/vivaldi.json @@ -0,0 +1,25 @@ +{ + "engineVersion": 1, + "version": 1, + "id": "theme-gruvbox", + "url": "", + "name": "Gruvbox", + "accentFromPage": false, + "accentOnWindow": false, + "accentSaturationLimit": 1, + "backgroundImage": "", + "backgroundPosition": "stretch", + "backgroundSource": "", + "radius": 4, + "colorAccentBg": "#504945", + "colorBg": "#1d2021", + "colorFg": "#ebdbb2", + "colorHighlightBg": "#83a598", + "colorWindowBg": "#3c3836", + "contrast": 0, + "preferSystemAccent": false, + "alpha": 1, + "blur": 0, + "transparencyTabs": false, + "transparencyTabBar": false +} diff --git a/themes/kanagawa/vivaldi.json b/themes/kanagawa/vivaldi.json new file mode 100644 index 0000000..254b8c0 --- /dev/null +++ b/themes/kanagawa/vivaldi.json @@ -0,0 +1,25 @@ +{ + "engineVersion": 1, + "version": 1, + "id": "theme-kanagawa", + "url": "", + "name": "Kanagawa", + "accentFromPage": false, + "accentOnWindow": false, + "accentSaturationLimit": 1, + "backgroundImage": "", + "backgroundPosition": "stretch", + "backgroundSource": "", + "radius": 4, + "colorAccentBg": "#2d4f67", + "colorBg": "#1f1f28", + "colorFg": "#dcd7ba", + "colorHighlightBg": "#7e9cd8", + "colorWindowBg": "#16161d", + "contrast": 0, + "preferSystemAccent": false, + "alpha": 1, + "blur": 0, + "transparencyTabs": false, + "transparencyTabBar": false +} diff --git a/themes/nightfox/vivaldi.json b/themes/nightfox/vivaldi.json new file mode 100644 index 0000000..f8244e7 --- /dev/null +++ b/themes/nightfox/vivaldi.json @@ -0,0 +1,25 @@ +{ + "engineVersion": 1, + "version": 1, + "id": "theme-nightfox", + "url": "", + "name": "Nightfox", + "accentFromPage": false, + "accentOnWindow": false, + "accentSaturationLimit": 1, + "backgroundImage": "", + "backgroundPosition": "stretch", + "backgroundSource": "", + "radius": 4, + "colorAccentBg": "#2b3b51", + "colorBg": "#192330", + "colorFg": "#cdcecf", + "colorHighlightBg": "#719cd6", + "colorWindowBg": "#212e3f", + "contrast": 0, + "preferSystemAccent": false, + "alpha": 1, + "blur": 0, + "transparencyTabs": false, + "transparencyTabBar": false +} diff --git a/themes/nordic/vivaldi.json b/themes/nordic/vivaldi.json new file mode 100644 index 0000000..7140593 --- /dev/null +++ b/themes/nordic/vivaldi.json @@ -0,0 +1,25 @@ +{ + "engineVersion": 1, + "version": 1, + "id": "theme-nordic", + "url": "", + "name": "Nordic", + "accentFromPage": false, + "accentOnWindow": false, + "accentSaturationLimit": 1, + "backgroundImage": "", + "backgroundPosition": "stretch", + "backgroundSource": "", + "radius": 4, + "colorAccentBg": "#3B4252", + "colorBg": "#242933", + "colorFg": "#BBC3D4", + "colorHighlightBg": "#88C0D0", + "colorWindowBg": "#2E3440", + "contrast": 0, + "preferSystemAccent": false, + "alpha": 1, + "blur": 0, + "transparencyTabs": false, + "transparencyTabBar": false +} diff --git a/themes/oceanic-next/vivaldi.json b/themes/oceanic-next/vivaldi.json new file mode 100644 index 0000000..8a6c0d2 --- /dev/null +++ b/themes/oceanic-next/vivaldi.json @@ -0,0 +1,25 @@ +{ + "engineVersion": 1, + "version": 1, + "id": "theme-oceanic-next", + "url": "", + "name": "Oceanic Next", + "accentFromPage": false, + "accentOnWindow": false, + "accentSaturationLimit": 1, + "backgroundImage": "", + "backgroundPosition": "stretch", + "backgroundSource": "", + "radius": 4, + "colorAccentBg": "#4f5b66", + "colorBg": "#162c35", + "colorFg": "#c0c5ce", + "colorHighlightBg": "#6699cc", + "colorWindowBg": "#65737e", + "contrast": 0, + "preferSystemAccent": false, + "alpha": 1, + "blur": 0, + "transparencyTabs": false, + "transparencyTabBar": false +} diff --git a/themes/popping-and-locking/vivaldi.json b/themes/popping-and-locking/vivaldi.json new file mode 100644 index 0000000..3d145e8 --- /dev/null +++ b/themes/popping-and-locking/vivaldi.json @@ -0,0 +1,25 @@ +{ + "engineVersion": 1, + "version": 1, + "id": "theme-popping-and-locking", + "url": "", + "name": "Popping and Locking", + "accentFromPage": false, + "accentOnWindow": false, + "accentSaturationLimit": 1, + "backgroundImage": "", + "backgroundPosition": "stretch", + "backgroundSource": "", + "radius": 4, + "colorAccentBg": "#383637", + "colorBg": "#181921", + "colorFg": "#ebdbb2", + "colorHighlightBg": "#458588", + "colorWindowBg": "#252631", + "contrast": 0, + "preferSystemAccent": false, + "alpha": 1, + "blur": 0, + "transparencyTabs": false, + "transparencyTabBar": false +} diff --git a/themes/retrobox/vivaldi.json b/themes/retrobox/vivaldi.json new file mode 100644 index 0000000..6326ea5 --- /dev/null +++ b/themes/retrobox/vivaldi.json @@ -0,0 +1,25 @@ +{ + "engineVersion": 1, + "version": 1, + "id": "theme-retrobox", + "url": "", + "name": "Retrobox", + "accentFromPage": false, + "accentOnWindow": false, + "accentSaturationLimit": 1, + "backgroundImage": "", + "backgroundPosition": "stretch", + "backgroundSource": "", + "radius": 4, + "colorAccentBg": "#504945", + "colorBg": "#1c1c1c", + "colorFg": "#ebdbb2", + "colorHighlightBg": "#83a598", + "colorWindowBg": "#3c3836", + "contrast": 0, + "preferSystemAccent": false, + "alpha": 1, + "blur": 0, + "transparencyTabs": false, + "transparencyTabBar": false +} diff --git a/themes/rose-pine/vivaldi.json b/themes/rose-pine/vivaldi.json new file mode 100644 index 0000000..2e37bbf --- /dev/null +++ b/themes/rose-pine/vivaldi.json @@ -0,0 +1,25 @@ +{ + "engineVersion": 1, + "version": 1, + "id": "theme-rose-pine", + "url": "", + "name": "Rose Pine", + "accentFromPage": false, + "accentOnWindow": false, + "accentSaturationLimit": 1, + "backgroundImage": "", + "backgroundPosition": "stretch", + "backgroundSource": "", + "radius": 4, + "colorAccentBg": "#403d52", + "colorBg": "#191724", + "colorFg": "#e0def4", + "colorHighlightBg": "#c4a7e7", + "colorWindowBg": "#1f1d2e", + "contrast": 0, + "preferSystemAccent": false, + "alpha": 1, + "blur": 0, + "transparencyTabs": false, + "transparencyTabBar": false +} diff --git a/themes/solarized-osaka/vivaldi.json b/themes/solarized-osaka/vivaldi.json new file mode 100644 index 0000000..055c95e --- /dev/null +++ b/themes/solarized-osaka/vivaldi.json @@ -0,0 +1,25 @@ +{ + "engineVersion": 1, + "version": 1, + "id": "theme-solarized-osaka", + "url": "", + "name": "Solarized Osaka", + "accentFromPage": false, + "accentOnWindow": false, + "accentSaturationLimit": 1, + "backgroundImage": "", + "backgroundPosition": "stretch", + "backgroundSource": "", + "radius": 4, + "colorAccentBg": "#073642", + "colorBg": "#001419", + "colorFg": "#839395", + "colorHighlightBg": "#268bd3", + "colorWindowBg": "#002b36", + "contrast": 0, + "preferSystemAccent": false, + "alpha": 1, + "blur": 0, + "transparencyTabs": false, + "transparencyTabBar": false +} diff --git a/themes/sora/vivaldi.json b/themes/sora/vivaldi.json new file mode 100644 index 0000000..0c9a57c --- /dev/null +++ b/themes/sora/vivaldi.json @@ -0,0 +1,25 @@ +{ + "engineVersion": 1, + "version": 1, + "id": "theme-sora", + "url": "", + "name": "Sora", + "accentFromPage": false, + "accentOnWindow": false, + "accentSaturationLimit": 1, + "backgroundImage": "", + "backgroundPosition": "stretch", + "backgroundSource": "", + "radius": 4, + "colorAccentBg": "#1e2430", + "colorBg": "#0e1018", + "colorFg": "#c8d0e0", + "colorHighlightBg": "#80c8e0", + "colorWindowBg": "#14161e", + "contrast": 0, + "preferSystemAccent": false, + "alpha": 1, + "blur": 0, + "transparencyTabs": false, + "transparencyTabBar": false +} diff --git a/themes/terafox/vivaldi.json b/themes/terafox/vivaldi.json new file mode 100644 index 0000000..91d7b64 --- /dev/null +++ b/themes/terafox/vivaldi.json @@ -0,0 +1,25 @@ +{ + "engineVersion": 1, + "version": 1, + "id": "theme-terafox", + "url": "", + "name": "Terafox", + "accentFromPage": false, + "accentOnWindow": false, + "accentSaturationLimit": 1, + "backgroundImage": "", + "backgroundPosition": "stretch", + "backgroundSource": "", + "radius": 4, + "colorAccentBg": "#293e40", + "colorBg": "#152528", + "colorFg": "#e6eaea", + "colorHighlightBg": "#5a93aa", + "colorWindowBg": "#1d3337", + "contrast": 0, + "preferSystemAccent": false, + "alpha": 1, + "blur": 0, + "transparencyTabs": false, + "transparencyTabBar": false +} From 262425df71d0b6efe8ffb15b6aaa24ed03c0b8b2 Mon Sep 17 00:00:00 2001 From: Chris Birch Date: Wed, 21 Oct 2026 02:04:03 -0400 Subject: [PATCH 2/2] fix(vivaldi): install the staged theme, and trust a pid only while it runs Vivaldi An apply that finds Vivaldi open now stages that theme's JSON in ~/.cache/theme/vivaldi/ for the waiter, rather than leaving the waiter to read the current-theme file when Vivaldi exits. That file is written after every app has applied, so a Vivaldi quit in between got the theme being replaced, and a dev-mode apply's waiter read the production file. The waiter now sources only vivaldi-preferences.sh. A staged theme with no waiter is one that never landed. The next apply warns about it and theme browsers shows it, instead of the failure leaving no trace. A SingletonLock or waiter pid file outlives its writer, so each pid is trusted only while ps names the expected program. The lock's host is no longer compared, because macOS renames the host with the network. One unreadable profile no longer stops the install for the others. theme browsers reads each profile's effective selection, following Vivaldi's schedule mode, instead of `current` from Default alone. start_detached is the one way an apply starts a process that must outlive it, and start_session_daemon falls back to it after Hyprland's dispatcher. Agent-Name: claude-code Agent-Session: 0105027b-1083-4e8b-8f26-8733a784a41b --- CLAUDE.md | 21 ++-- README.md | 4 +- bin/theme | 13 ++- lib/generators/vivaldi.sh | 8 +- lib/lib.sh | 28 +++-- lib/vivaldi-install-after-exit.sh | 4 +- lib/vivaldi-preferences.sh | 171 ++++++++++++++++++++---------- 7 files changed, 164 insertions(+), 85 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index db14d56..44870e5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,9 +17,10 @@ generated data live at: ~/.local/state/theme/schedule.json - Outcome of the last scheduled check ~/.cache/theme/backgrounds/ - Pre-rendered backgrounds per theme ~/.cache/theme/gowall/ - Per-theme gowall palettes, as JSON +~/.cache/theme/vivaldi/ - The theme staged for an open Vivaldi, and its waiter's pid ``` -Generated state belongs in those two cache directories and nowhere else. An app config here is +Generated state belongs in the cache directories above and nowhere else. An app config here is often a symlink into another repo, so writing generated state into it dirties that repo on every apply. @@ -357,13 +358,17 @@ CLI verbs. These generators behave unlike the rest: - `vivaldi.sh` emits one entry of Vivaldi's `vivaldi.themes.user` pref, which `lib/vivaldi-preferences.sh` upserts and selects. Vivaldi holds `Preferences` in memory and writes all of it back on exit, so a write while it runs is lost. `apply_vivaldi` therefore - installs only into a closed Vivaldi. When Vivaldi is open, it leaves a detached waiter that - installs whatever theme is current once that Vivaldi's process exits. Nothing outside Vivaldi - can change an open window's theme, so the change shows at the next launch: - - CSS mods are linked once per window. - - `--remote-debugging-port` is refused on the default profile. - - No enterprise policy maps to a `vivaldi.*` pref. `BrowserThemeColor` tints Chromium's native - toolbar, which Vivaldi does not draw with. + installs only into a closed Vivaldi. When Vivaldi is open, it stages the theme in + `~/.cache/theme/vivaldi/` and leaves a detached waiter, which installs the staged theme once + that Vivaldi's process exits. A staged theme with no waiter is one that never landed, and the + next apply warns about it. Nothing outside Vivaldi can change an open window's theme, so the + change shows at the next launch. Each dead end, with the command that re-checks it on Arch: + - CSS mods are linked once per window: `rg css-mods /opt/vivaldi/resources/vivaldi/window.html`. + - `--remote-debugging-port` is refused on the default profile: + `strings -a /opt/vivaldi/vivaldi-bin | rg 'non-default data directory'`. + - No enterprise policy maps to a `vivaldi.*` pref, so this prints `[]`: + `jq '[.policies[].path | select(startswith("vivaldi"))]' /opt/vivaldi/resources/vivaldi/policy_prefs.json`. + `BrowserThemeColor` tints Chromium's native toolbar, which Vivaldi does not draw with. - `glow.sh` emits a glamour style whose code blocks reach the screen quantized to xterm-256. glow never sets glamour's chroma formatter, so only prose keeps true color. `apply_glow` writes `glow.yml`'s `style` as an absolute path, because glamour expands neither `~` nor `$HOME` and diff --git a/README.md b/README.md index 4e1db16..ecfef36 100644 --- a/README.md +++ b/README.md @@ -140,8 +140,8 @@ Restart the terminal to see opacity changes. theme browsers # Show detected browsers and theme status ``` -Firefox-based browsers (Zen, Librewolf, Firefox, Thunderbird) and Vivaldi are -themed automatically on `theme apply`. Firefox-based browsers require +Firefox-based browsers and Vivaldi are themed automatically on `theme apply`. +Firefox-based browsers require `toolkit.legacyUserProfileCustomizations.stylesheets = true` in `about:config`, then a restart, for `userChrome.css` to take effect. diff --git a/bin/theme b/bin/theme index ff22069..7f6ca07 100755 --- a/bin/theme +++ b/bin/theme @@ -576,7 +576,6 @@ browsers_usage() { print_section "Description" "blue" cat <<'EOF' Show detected browsers and their theme configuration status. - Supports Firefox-based browsers (Zen, Librewolf, Firefox, Thunderbird) and Vivaldi. EOF print_section "Setup Required" "blue" @@ -600,7 +599,6 @@ Browser Theme Diagnostics Description ----------- Show detected browsers and their theme configuration status. - Supports Firefox-based browsers (Zen, Librewolf, Firefox, Thunderbird) and Vivaldi. Setup Required -------------- @@ -845,11 +843,12 @@ show_browsers() { if [[ -f "$vivaldi_dir/Local State" ]]; then echo "" echo "Vivaldi:" - local vivaldi_theme - vivaldi_theme=$(jq -r '.vivaldi.themes.current // "none"' "$vivaldi_dir/Default/Preferences" 2>/dev/null || echo "unknown") - echo " Theme: $vivaldi_theme" - if vivaldi_waiter_running; then - echo " Pending: the current theme installs when Vivaldi exits" + vivaldi_selected_themes "$vivaldi_dir" | sed 's/^/ /' + local vivaldi_stranded + if vivaldi_stranded=$(vivaldi_stranded_theme); then + echo " Pending: $vivaldi_stranded, never installed. The next theme apply replaces it." + elif [[ -f "$VIVALDI_PENDING_THEME" ]]; then + echo " Pending: $(jq -r '.name // .id' "$VIVALDI_PENDING_THEME"), installed when Vivaldi exits" fi fi diff --git a/lib/generators/vivaldi.sh b/lib/generators/vivaldi.sh index 0ea67e5..f5f182d 100755 --- a/lib/generators/vivaldi.sh +++ b/lib/generators/vivaldi.sh @@ -24,12 +24,12 @@ eval "$(load_colors "$input_file")" UI_ACCENT="${EXTENDED_UI_ACCENT:-$BASE0D}" UI_SELECTION="${EXTENDED_UI_SELECTION:-$BASE02}" -# Vivaldi derives every shade, hover and contrast text from these five, so the -# palette's own intermediate slots have nowhere to go. +# Vivaldi derives every shade, hover and contrast text from the colors below, +# so the palette's own intermediate slots have nowhere to go. # # Vivaldi paints its accent across the whole toolbar row and the active tab, so -# the accent takes the selection shade that firefox-based.sh gives a selected -# tab. The loud UI accent goes to highlight, which marks focus and selection. +# the accent takes the quiet UI selection shade. The loud UI accent goes to +# highlight, which marks focus and selection. generate() { jq -n \ --arg id "theme-${THEME_SLUG}" \ diff --git a/lib/lib.sh b/lib/lib.sh index ddaa342..3a44e32 100755 --- a/lib/lib.sh +++ b/lib/lib.sh @@ -638,12 +638,14 @@ apply_vivaldi() { [[ -f "$dir/Local State" ]] || return 1 if vivaldi_running_pid "$dir" >/dev/null; then - vivaldi_waiter_running || vivaldi_start_waiter "$dir" || return 1 + vivaldi_stage_theme "$lib_path/vivaldi.json" || return 1 + vivaldi_waiter_running || start_detached "$THEME_LIB_DIR/vivaldi-install-after-exit.sh" "$dir" echo "at next launch" return 0 fi - vivaldi_install_theme "$dir" "$lib_path/vivaldi.json" + vivaldi_install_theme "$dir" "$lib_path/vivaldi.json" || return 1 + rm -f "$VIVALDI_PENDING_THEME" } # Apply sioyek PDF viewer theme (all platforms) @@ -2149,6 +2151,10 @@ apply_theme_to_apps() { # Vivaldi (macOS and Arch) if [[ "$platform" == "macos" ]] || [[ "$platform" == "arch" ]]; then + local vivaldi_stranded + if vivaldi_stranded=$(vivaldi_stranded_theme); then + apply_warn "Vivaldi never got $vivaldi_stranded: it was still open when its waiter stopped. This apply replaces it." + fi local vivaldi_status if vivaldi_status=$(apply_vivaldi "$theme" 2>/dev/null); then applied+=("vivaldi") @@ -2443,18 +2449,28 @@ reload_dunst() { return 1 } -# Starts a desktop daemon outside the apply's cgroup, which systemd kills when an apply run as a -# service ends. Hyprland's child sits where login started it. A transient scope is the fallback. +# Starts a desktop daemon outside the apply's cgroup. Hyprland's child sits where login started +# it, so the daemon lives as if login had started it. start_detached is the fallback. start_session_daemon() { local program="$1" if command -v hyprctl &>/dev/null \ && [[ "$(hyprctl dispatch "hl.dsp.exec_cmd('$program')" 2>/dev/null)" == "ok" ]]; then return 0 fi + start_detached "$program" +} + +# Starts a command that must outlive the apply. systemd kills a scheduled apply's cgroup when the +# oneshot exits, so where a systemd user manager runs, the command gets its own transient scope. +# Elsewhere it gets its own session. On macOS the launchd agent sets AbandonProcessGroup +# (lib/schedule.sh), which spares it. +start_detached() { if systemctl --user show-environment &>/dev/null; then - systemd-run --user --scope --collect --quiet "$program" >/dev/null 2>&1 /dev/null 2>&1 /dev/null; then + setsid "$@" >/dev/null 2>&1 /dev/null 2>&1 /dev/null 2>&1 set -euo pipefail -source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" +source "$(dirname "${BASH_SOURCE[0]}")/vivaldi-preferences.sh" vivaldi_install_after_exit "$1" diff --git a/lib/vivaldi-preferences.sh b/lib/vivaldi-preferences.sh index 6504c00..84857b9 100755 --- a/lib/vivaldi-preferences.sh +++ b/lib/vivaldi-preferences.sh @@ -1,13 +1,17 @@ #!/usr/bin/env bash -# Installs a generated Vivaldi theme into Vivaldi's own profile. +# Reads and writes Vivaldi's own profile. # # Vivaldi keeps its themes in each profile's Preferences and holds that file in # memory while it runs, writing all of it back on exit. A write made while it is -# open is therefore overwritten, so a theme is installed only while Vivaldi is -# closed. An apply that finds it open leaves a waiter behind, which installs -# whatever theme is current once that Vivaldi exits. +# open is overwritten, so a theme is installed only while Vivaldi is closed. An +# apply that finds it open stages the theme as pending and leaves a waiter, +# which installs the pending theme once that Vivaldi exits. +# +# The waiter sources only this file, so nothing here reads lib.sh. -VIVALDI_WAITER_PID_FILE="$HOME/.cache/theme/vivaldi-waiter.pid" +VIVALDI_STATE_DIR="$HOME/.cache/theme/vivaldi" +VIVALDI_PENDING_THEME="$VIVALDI_STATE_DIR/pending-theme.json" +VIVALDI_WAITER_PID_FILE="$VIVALDI_STATE_DIR/waiter.pid" vivaldi_user_data_dir() { case "$(uname -s)" in @@ -16,26 +20,41 @@ vivaldi_user_data_dir() { esac } +# A lock or pid file outlives the process that wrote it, and kill -0 answers for +# whatever process holds the number now, so a pid is trusted only while it still +# runs the expected program. +vivaldi_pid_is_vivaldi() { + local pid="$1" comm + [[ "$pid" =~ ^[0-9]+$ ]] || return 1 + comm=$(ps -p "$pid" -o comm= 2>/dev/null) || return 1 + comm="${comm##*/}" + [[ "$comm" == vivaldi-bin || "$comm" == Vivaldi ]] +} + # The pid of the Vivaldi holding this user-data dir, read from Chromium's own -# SingletonLock symlink, whose target is `-`. A crash leaves the link -# naming a dead pid, which counts as not running. +# SingletonLock symlink, whose target is `-`. The host is not +# compared, because macOS renames the host with the network while the lock +# keeps the name from launch. vivaldi_running_pid() { - local dir="$1" - local target - target=$(readlink "$dir/SingletonLock" 2>/dev/null) || return 1 - - local host="${target%-*}" pid="${target##*-}" - [[ "$host" == "$(uname -n)" ]] || return 1 - [[ "$pid" =~ ^[0-9]+$ ]] || return 1 - kill -0 "$pid" 2>/dev/null || return 1 + local target pid + target=$(readlink "$1/SingletonLock" 2>/dev/null) || return 1 + pid="${target##*-}" + vivaldi_pid_is_vivaldi "$pid" || return 1 echo "$pid" } +# Each profile directory with its display name, tab-separated, from Local +# State, which is Vivaldi's own record of its profiles. +vivaldi_profiles() { + jq -r '.profile.info_cache // {} | to_entries[] | [.key, (.value.name // .key)] | @tsv' "$1/Local State" +} + # Upsert one theme object into every profile's Preferences and select it. # # Both OS-schedule slots are pointed at it as well as `current`, because # Vivaldi's default schedule follows the OS light/dark setting and reads those -# slots instead of `current`. Every other user theme stays installed. +# slots instead of `current`. Every other user theme stays installed. A profile +# that fails leaves the rest to be installed, and the call returns 1. vivaldi_install_theme() { local dir="$1" local theme_json="$2" @@ -43,66 +62,99 @@ vivaldi_install_theme() { [[ -f "$dir/Local State" && -f "$theme_json" ]] || return 1 local profiles - profiles=$(jq -r '.profile.info_cache // {} | keys[]' "$dir/Local State") || return 1 + profiles=$(vivaldi_profiles "$dir") || return 1 - local profile prefs staged - while IFS= read -r profile; do + local profile name prefs staged failed=0 + while IFS=$'\t' read -r profile name; do [[ -n "$profile" ]] || continue prefs="$dir/$profile/Preferences" [[ -f "$prefs" ]] || continue # Staged beside the file so the mv is a rename; mktemp's 0600 matches # Chromium's own mode for it. - staged=$(mktemp "$prefs.theme.XXXXXX") || return 1 - if ! jq -c --slurpfile theme "$theme_json" ' + if ! staged=$(mktemp "$prefs.theme.XXXXXX"); then + failed=1 + continue + fi + if jq -c --slurpfile theme "$theme_json" ' $theme[0] as $t | .vivaldi.themes.user = ([(.vivaldi.themes.user // [])[] | select(.id != $t.id)] + [$t]) | .vivaldi.themes.current = $t.id | .vivaldi.theme.schedule.o_s = ((.vivaldi.theme.schedule.o_s // {}) + {light: $t.id, dark: $t.id}) - ' "$prefs" >"$staged"; then - rm -f "$staged" - return 1 + ' "$prefs" >"$staged" && mv "$staged" "$prefs"; then + continue fi - mv "$staged" "$prefs" || return 1 + rm -f "$staged" + failed=1 done <<<"$profiles" + + return "$failed" } -# Whether an earlier apply already left a waiter, which will install whatever -# theme is current when it fires, so one is enough. +# What each profile shows, one `: ` line apiece. The +# schedule decides which pref that is: `off` reads `current`, the default +# `system` reads the OS light and dark slots, and `timeline` reads a list of +# times this tool never writes. Absent prefs take Vivaldi's own defaults. +vivaldi_selected_themes() { + local dir="$1" profile name + while IFS=$'\t' read -r profile name; do + [[ -n "$profile" ]] || continue + jq -r --arg profile "$name" ' + ((.vivaldi.themes.user // []) | map({key: .id, value: .name}) | from_entries) as $names + | (.vivaldi.theme.schedule // {}) as $s + | ($s.o_s // {}) as $o + | ($o.light // "Vivaldi1") as $light + | ($o.dark // "Vivaldi2") as $dark + | ($s.enabled // "system") as $mode + | if $mode == "off" or $mode == 0 then ($names[.vivaldi.themes.current // "Vivaldi1"] // (.vivaldi.themes.current // "Vivaldi1")) + elif $mode == "timeline" or $mode == 2 then "a timeline schedule" + elif $light == $dark then ($names[$light] // $light) + else "\($names[$light] // $light) when light, \($names[$dark] // $dark) when dark" + end + | "\($profile): \(.)" + ' "$dir/$profile/Preferences" 2>/dev/null || echo "$name: unreadable" + done < <(vivaldi_profiles "$dir") +} + +# The waiter is trusted only while its pid still runs the waiter. vivaldi_waiter_running() { - local pid + local pid args pid=$(cat "$VIVALDI_WAITER_PID_FILE" 2>/dev/null) || return 1 - [[ "$pid" =~ ^[0-9]+$ ]] && kill -0 "$pid" 2>/dev/null + [[ "$pid" =~ ^[0-9]+$ ]] || return 1 + args=$(ps -p "$pid" -o args= 2>/dev/null) || return 1 + [[ "$args" == *vivaldi-install-after-exit* ]] } -# Detach a waiter from this apply. A scheduled apply runs as a oneshot whose -# cgroup systemd kills on exit, so on Linux the waiter is its own transient -# service. On macOS the launchd agent sets AbandonProcessGroup, so nohup is -# enough there. -vivaldi_start_waiter() { - local dir="$1" - local waiter="$THEME_LIB_DIR/vivaldi-install-after-exit.sh" - - if [[ "$(uname -s)" != "Darwin" ]] && systemctl --user show-environment &>/dev/null; then - systemd-run --user --collect --quiet -E HOME -E PATH -- "$waiter" "$dir" >/dev/null 2>&1 /dev/null 2>&1 /dev/null || echo "a theme" } # The waiter's body. It waits for the whole process to exit rather than for the # lock to go, since Chromium may drop the lock before its last Preferences write. # A Vivaldi relaunched within the poll interval already read the old file, so -# the wait moves on to that instance. +# the wait moves on to that instance. A theme staged during the install is +# installed on the next pass. # # A logout or reboot sends SIGTERM to Vivaldi and to this at once. Vivaldi takes # a moment to write and exit, so the TERM is answered with a bounded wait rather -# than by dying first. +# than by dying first. A failure leaves the theme staged, which the next apply +# reports. vivaldi_install_after_exit() { local dir="$1" + local claimed="$VIVALDI_STATE_DIR/installing-theme.json" - mkdir -p "$(dirname "$VIVALDI_WAITER_PID_FILE")" + mkdir -p "$VIVALDI_STATE_DIR" echo "$$" >"$VIVALDI_WAITER_PID_FILE" trap 'rm -f "$VIVALDI_WAITER_PID_FILE"' EXIT @@ -110,16 +162,23 @@ vivaldi_install_after_exit() { trap 'deadline=$((SECONDS + 15))' TERM HUP local pid - while pid=$(vivaldi_running_pid "$dir"); do - while kill -0 "$pid" 2>/dev/null; do - if [[ -n "$deadline" ]] && ((SECONDS >= deadline)); then - return 1 - fi - sleep 1 + while [[ -f "$VIVALDI_PENDING_THEME" ]]; do + while pid=$(vivaldi_running_pid "$dir"); do + while vivaldi_pid_is_vivaldi "$pid"; do + if [[ -n "$deadline" ]] && ((SECONDS >= deadline)); then + echo "Vivaldi outlasted the session; the staged theme stays pending" >&2 + return 1 + fi + sleep 1 + done done - done - local theme - theme=$(cat "$CURRENT_THEME_FILE" 2>/dev/null) || return 1 - vivaldi_install_theme "$dir" "$THEMES_DIR/$theme/vivaldi.json" + mv "$VIVALDI_PENDING_THEME" "$claimed" || return 1 + if ! vivaldi_install_theme "$dir" "$claimed"; then + [[ -f "$VIVALDI_PENDING_THEME" ]] || mv "$claimed" "$VIVALDI_PENDING_THEME" + echo "Could not install the staged theme into $dir" >&2 + return 1 + fi + rm -f "$claimed" + done }