Repository navigation
feat(vivaldi): generate a Vivaldi theme and install it on theme apply - #23
Conversation
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
datapointchris
left a comment
There was a problem hiding this comment.
PR review
Arm: Sonnet
Used: Read the diff and ran the installer paths in lib/vivaldi-preferences.sh by reading them against the PR's stated flows. Did not launch Vivaldi.
Standards: Searched the review rules only. No repo rule cited.
Findings: 2 correctness
Correctness
lib/vivaldi-preferences.sh:70—vivaldi_waiter_runningtrusts a pid file with onlykill -0. A waiter killed by SIGKILL, OOM or a crash leaves the file behind because the EXIT trap does not run. After a reboot the pid may belong to an unrelated process. Every later apply then finds a "running waiter", starts none, and printsat next launchwhile nothing will install the theme. Reach: unproven — needs a killed waiter plus pid reuse; a reboot with Vivaldi open at the time is the likely path. Fix: record the waiter's start identity, or check/proc/$pid/cmdline(andps -p $pid -o command=on macOS) namesvivaldi-install-after-exit.sh.bin/theme:849—theme browsersreads only$vivaldi_dir/Default/Preferences.vivaldi_install_themewrites every profile inLocal State. A user whose profile directory isProfile 1seesTheme: unknownalthough the install succeeded. Reach: any Vivaldi user without aDefaultprofile directory. Fix: read the first profilevivaldi_install_themeiterates, or list each profile's selected theme.
This review does not gate merge.
datapointchris
left a comment
There was a problem hiding this comment.
Test review
Arm: Sonnet
Design and architecture
No test should be deleted.
The PR adds one test: tests/vivaldi-preferences.bats "an install keeps the user's themes and prefs and selects the theme".
It runs vivaldi_install_theme on a sandboxed profile.
It checks a computed result: the user's themes survive, a reapplied theme is replaced, and the selection lands in all three slots.
That is behavior a user reaches, and no other test pins it.
It asserts values read back from the written Preferences file, not prose the code declares.
Existing tests that cover changed code are the tests/generators.bats invariants (artifact set, map coverage).
They check structure across all generators, and the new vivaldi.json artifacts satisfy them.
They stay.
datapointchris
left a comment
There was a problem hiding this comment.
PR review
Reviewer: system-design
Arm: Sonnet
Used: read lib/vivaldi-preferences.sh, lib/vivaldi-install-after-exit.sh, apply_vivaldi and its dispatcher block in lib/lib.sh at the PR head. No apply, deploy or login run.
Standards: none cited. No rule searched applies to a pid-file liveness check.
Findings: 1 correctness
Correctness
lib/vivaldi-preferences.sh:67—vivaldi_waiter_runningtrusts a pid file as proof that a waiter is alive, so a stale pid file stops Vivaldi from ever being themed again. Reach: a waiter is started while Vivaldi is open and holds pid N in~/.cache/theme/vivaldi-waiter.pid. The machine reboots or logs out and the waiter is killed with SIGKILL, so its EXIT trap never removes the file. After boot, an unrelated process takes pid N. The nexttheme applyfinds Vivaldi open,kill -0 Nsucceeds, andvivaldi_waiter_runningreturns true. No waiter starts and the tick still reads✓ vivaldi (at next launch). Vivaldi exits and nothing installs the theme. Every later apply repeats this until pid N dies. A long-lived process holding N makes it permanent. The check tests the pid alone, with no check of what the process is. Fix: name the waiter's identity and test that. Either read/proc/$pid/cmdline(orps -p "$pid" -o command=) forvivaldi-install-after-exit.sh, or on Linux start it as a fixed-name transient unit (systemd-run --unit=theme-vivaldi-waiter) and asksystemctl --user is-active. A fixed unit name also removes the file and its race.
This review does not gate merge.
datapointchris
left a comment
There was a problem hiding this comment.
PR review
Reviewer: code-architecture
Arm: Sonnet
Used: read the whole repo at the head; traced each fact the diff decides to its other sites in lib/lib.sh, lib/browser-profiles.sh, lib/storage.sh and bin/theme.
Standards: no rule cited; structural review only.
Findings: 3 design
Design and architecture
-
lib/vivaldi-preferences.sh:84— Detaching a background job from the apply's cgroup is decided twice.vivaldi_start_waiterpicks a mechanism (systemd-run --userservice, elsenohup, keyed onuname -sandsystemctl --user show-environment).start_session_daemonatlib/lib.sh:2448decides the same fact differently (hyprctl exec, thensystemd-run --scope, elsesetsid). The two disagree on the macOS test, the fallback and the systemd unit kind. Reach: the next thing an apply must leave running (a second browser's waiter, another daemon) picks a third copy. Thesystemctlprobe is already written twice. This PR is that change for one case. Fix: onedetach_from_apply <cmd...>inlib/lib.shthat both callers use. The Hyprland dispatch stays a daemon-only option on top. -
lib/vivaldi-preferences.sh:12— Where a browser keeps its data, and which platforms run it, have three owners.get_browser_profiles_dirinlib/browser-profiles.sh:14owns every other browser's path per OS.vivaldi_user_data_dirre-implements it with its ownuname -scase.apply_theme_to_apps(lib/lib.sh:2150) gates ondetect_platformbeingmacosorarch.show_browsers(bin/theme:849) reads$vivaldi_dir/Default/Preferences, whilevivaldi_install_theme(lib/vivaldi-preferences.sh:42) walks every profile inLocal State. The status view and the installer already disagree on which profiles exist. Reach: the next Chromium-family browser (Brave, Chrome, Edge;chromium.themeis already a second Chromium path) copies the directory case, the platform gate and the profile list again. Fix: add Vivaldi toget_browser_profiles_dirand expose one profile-enumeration function. The installer andshow_browsersboth call it. -
lib/vivaldi-preferences.sh:10— The new module reaches up intolib.shfor facts it does not own. It is sourced fromlib/lib.sh:19but readsTHEME_LIB_DIR,THEMES_DIRandCURRENT_THEME_FILE, whichlib.shdefines. It hardcodes$HOME/.cache/themeat line 10, wherelib.sh:1020andlib/generators/background-recolor.sh:86each hardcode it again. The waiter (line 123) builds$THEMES_DIR/$theme/vivaldi.jsonitself, whileapply_vivaldiresolves the same artifact throughget_library_path.lib/vivaldi-install-after-exit.shsources all oflib.shto call one function. Reach: moving the cache dir or the library path layout now needs edits in the module and its entry script, and the module only works when sourced afterlib.sh. Fix: pass the resolved JSON path to the waiter fromapply_vivaldi, which also fixes the divergence withget_library_path. Take the cache dir from oneTHEME_CACHE_DIRvariable.
This review does not gate merge.
datapointchris
left a comment
There was a problem hiding this comment.
PR review
Reviewer: simplest-design
Arm: Sonnet
Used: Read the PR body and diff at head in a detached clone. Read lib/vivaldi-preferences.sh and lib/vivaldi-install-after-exit.sh. Did not run Vivaldi.
Standards: pull-requests.md § "Ask whether the change should exist before looking for faults inside it" and § "The comment carries a version marker and a stable finding grammar".
Findings: 0 correctness · 0 design · 0 prose
The change is for one gap. theme apply leaves Vivaldi on its last theme. Every theme apply caller on Arch and macOS reaches it.
The smallest change the body names is a write to Preferences while Vivaldi is closed, with no waiter. The body rejects it. A browser open all day is open at every scheduled apply, so the theme would land only by chance. That case is advertised, and the smaller change fails it.
The two live routes the body rejects (injected window.html script, DevTools port) fail on stated causes. Neither is smaller.
The waiter's detach follows the repo's existing reasoning in start_session_daemon: a transient systemd unit on Linux, and AbandonProcessGroup on macOS.
No upstream, configuration or shared-library place holds this fix. Vivaldi offers no policy for its own theme prefs.
This review does not gate merge.
datapointchris
left a comment
There was a problem hiding this comment.
PR review
Reviewer: failure-paths
Arm: Sonnet
Used: Cloned the head into the scratchpad. Sourced lib/vivaldi-preferences.sh under a sandbox HOME and ran vivaldi_install_theme against a two-profile tree, one profile with a truncated Preferences.
Standards: code-quality.md § "A failure allowed to degrade costs only the unit that failed". Searches for a silent detached failure found no rule.
Findings: 2 correctness
Correctness
-
lib/vivaldi-preferences.sh:67— One unreadable profile stops the install for every profile after it. The profiles already written keep the new theme. The apply reports Vivaldi as skipped. Reach: any Vivaldi user-data dir with two profiles where one Preferences is truncated or invalid JSON. I ran it withDefaultvalid andProfile 1holding{trunc. It printedjq: parse error, returned 1, leftDefaultwith the theme selected, and leftProfile 1untouched. A profile listed after the bad one would never be tried.code-quality.md§ "A failure allowed to degrade costs only the unit that failed". Fix: invivaldi_install_theme, continue to the next profile on a jq or mv failure, remove the staged file, and return 1 at the end only if no profile was installed. Report the failed profile on stderr. -
lib/vivaldi-preferences.sh:100— The waiter fails with no trace after the apply has already said "at next launch". Reach: Vivaldi is open at apply, then it exits and the install fails. The waiter's stdout and stderr go to/dev/nullinvivaldi_start_waiter. Its failure paths allreturn 1silently: the 15-second deadline after TERM, a missingcurrentfile, and anyvivaldi_install_themeerror such as the case in finding 1. The user sees a success tick and relaunches Vivaldi in the old theme. Nothing records why. I did not run the waiter itself. The silent returns are in the code atvivaldi_install_after_exit. Fix: havevivaldi_install_after_exitappend its failure to$THEME_STATE_DIRor the cache dir (avivaldi-waiter.log), and have the nexttheme applyprint it throughapply_warn. Theapply_warnarray is the route that survives the2>/dev/nullonapply_vivaldi.
This review does not gate merge.
datapointchris
left a comment
There was a problem hiding this comment.
PR review
Reviewer: contracts
Arm: Sonnet
Used: Read the PR head in a clone; traced every changed surface and its readers by search. I did not run the apply path, because it writes live app config.
Standards: No standard cited. The surfaces are a new artifact (vivaldi.json), a new tick, a new pid file and a Preferences write.
Findings: 1 correctness · 1 design
Correctness
lib/vivaldi-preferences.sh:94— A dev-mode apply with Vivaldi open installs the production theme, not the one applied. Reach:vivaldi_start_waiterpasses only-E HOME -E PATHtosystemd-run, so the waiter losesTHEME_ENV=development.lib/storage.sh:15-18then pointsCURRENT_THEME_FILEat~/.local/state/theme/currentinstead of.dev-data. The waiter writes the production current theme'svivaldi.jsoninto the real profile.schedule.sh:270already forwardsTHEME_ENVfor the same reason. Read from code; I did not run it. Fix: add-E THEME_ENVwhen it is set, or pass the theme id to the waiter as an argument.
Design and architecture
bin/theme:850—theme browsersreads onlyDefault/Preferences, while the install writes every profile inLocal State'sinfo_cache. Reach: a user whose Vivaldi runs onProfile 1seesTheme: noneor a stale id after a successful apply, and the next reader trusts it. Fix: report the profiles thatvivaldi_install_themeiterates, or name the profile in the line.
This review does not gate merge.
datapointchris
left a comment
There was a problem hiding this comment.
PR review
Used: ran bats tests/vivaldi-preferences.bats tests/generators.bats on the branch, all passing; read each test beside lib/vivaldi-preferences.sh, lib/lib.sh, lib/generate-all.sh and bin/theme.
Standards: testing.md § "Test behavior the user cares about, not what this commit happened to change"; testing.md § "Never assert on rendered output — assert the value it was built from"; pull-requests.md § "Tests the diff adds are checked for whether they test the program or the change".
Findings: 0 design
No test should be deleted.
tests/vivaldi-preferences.bats "an install keeps the user's themes and prefs and selects the theme" stays. It is the only test of vivaldi_install_theme. It pins three things a user reaches when an apply rewrites their Vivaldi profile. Their own theme mine and an unrelated pref (browser.has_seen_welcome_page) survive. A reapplied theme is replaced, not duplicated. The theme is selected under current and both OS-schedule slots. It asserts the profile's resulting JSON, which is the value itself, not rendered text.
The suite's existing tests of the changed code also stay. These are the tests/generators.bats invariants the new vivaldi map entry and vivaldi.json:Vivaldi artifact row pass through:
- "every theme carries the same artifact set"
- "every artifact a theme carries is one some generator produces"
- "every app artifact the CLI reports on is one a generator produces"
- "every artifact a generator produces is one the CLI reports on"
- the regeneration diff against committed artifacts
Each one is derived from the generator map and the theme tree. None lists Vivaldi by name.
No test asserts the reworded theme browsers help text or the new show_browsers lines.
This review does not gate merge.
datapointchris
left a comment
There was a problem hiding this comment.
PR review
Reviewer: stale-prose
Arm: Sonnet
Used: read every added sentence in the diff beside the head code (lib/vivaldi-preferences.sh, lib/lib.sh, lib/generators/vivaldi.sh, bin/theme, README.md, CLAUDE.md).
Standards: pull-requests.md § "Sentences the diff adds are checked for what goes stale, and the finding carries the rewrite".
Findings: 2 prose
Prose
-
CLAUDE.md:361-365— "Nothing outside Vivaldi can change an open window's theme", "CSS mods are linked once per window.", "--remote-debugging-portis refused on the default profile.", "No enterprise policy maps to avivaldi.*pref.BrowserThemeColortints Chromium's native toolbar, which Vivaldi does not draw with." Will go stale: each is a claim about Vivaldi's behavior that nothing in this repo keeps level with a Vivaldi release. The same shape sits atlib/generators/vivaldi.sh:27("Vivaldi derives every shade, hover and contrast text from these five"),lib/generators/vivaldi.sh:30("Vivaldi paints its accent across the whole toolbar row and the active tab"),lib/vivaldi-preferences.sh:95("Chromium may drop the lock before its last Preferences write") andlib/vivaldi-preferences.sh:80("the launchd agent sets AbandonProcessGroup", a claim about another file the sentence does not name). Reach: Vivaldi updates ship without this repo; the next one that changes any of these leaves the sentence false and the code unchanged. Rewrite: keep the dead ends as a one-line rule with its cause and a way to re-check, e.g. "Only a closed Vivaldi can be themed. Re-test--remote-debugging-port, CSS mods and policies against the installed version before relaxing that." Name the file that setsAbandonProcessGroup. Cut the "derives every shade" and "paints its accent" claims to what the code does: it passes five colors and takes the accent fromUI_SELECTION. -
README.md:143,README.md:205,bin/theme:579,bin/theme:603— "Firefox-based browsers (Zen, Librewolf, Firefox, Thunderbird) and Vivaldi". A value the code holds:lib/browser-profiles.sh:3,lib/generators/firefox-based.sh:3andbin/theme:786already own the browser list, so this is a fourth copy, now with Vivaldi appended to two of them. Reach: adding or dropping a browser means editing four more sentences that nothing checks. Rewrite: "Firefox-based browsers and Vivaldi.theme browserslists the ones found on this machine."
This review does not gate merge.
datapointchris
left a comment
There was a problem hiding this comment.
PR review
Reviewer: fleet-patterns
Arm: Sonnet
Used: Read the diff and lib/vivaldi-preferences.sh at the head. Ran indy search --owned for detaching a process from a systemd oneshot, editing a JSON config atomically, and installing a theme into a browser profile. Read the matching sites in ~/tools/font and in this repo.
Standards: fleet standards search for detach, systemd-run and atomic temp-file writes found no rule that settles either pattern.
Findings: 1 design
Design and architecture
lib/vivaldi-preferences.sh:80-92—vivaldi_start_waiteris a third way to leave a systemd oneshot's cgroup. The fleet already has one:start_session_daemonattheme:lib/lib.sh:2448-2459andfont:lib/arch-apps.sh:105-117. Those two copies carry the same logic. They runsystemd-run --user --scope ... &, thensetsidwhen no user manager exists, thendisown. This PR usessystemd-run --user --collectwithout--scope(a transient service, run in the foreground). Its fallback isnohup ... &, with nosetsid. Reach: the next fix to the cgroup-escape rule lands instart_session_daemonand misses the waiter, or the reverse. The font copy already shows this drift, since its comment differs from the theme copy's. The transient service is the better choice for a waiter that lives for hours, because it gets its own cgroup. That reason is not in the PR body. The non-systemd Linux branch (nohuponly) is the weaker one. The existing helper usessetsidthere. Fix: split the detach out ofstart_session_daemoninto one helper inlib.shthat takes a command and a mode (Hyprland exec, service, or scope). Have bothstart_session_daemonandvivaldi_start_waitercall it. Iffontmust keep identical logic, move the helper to a library both repos source. A "fix both" note does not do that.
This review does not gate merge.
datapointchris
left a comment
There was a problem hiding this comment.
PR review
Reviewer: simplest-design
Used: a scratch HOME with a fake Vivaldi profile and a SingletonLock naming a live sleep, driving lib/vivaldi-preferences.sh at head 3ea5c74.
Standards: pull-requests.md § "Ask whether the change should exist before looking for faults inside it". A search for a detached-waiter rule found none.
Findings: 0 correctness · 0 design
The change is the smallest that does the advertised job. What I checked:
- Reach. Vivaldi is installed here at
/usr/bin/vivaldi. A profile exists at~/.config/vivaldi. Dotfiles declares it ininstall/system.yml. Soapply_vivaldihas a real caller. - The rejected alternative fails. I ran install-only-while-closed against an open fake Vivaldi. It printed
open, skipped. After the process exited,vivaldi.themes.currentwas stillx. The deferred-install case the body advertises needs something to act after exit. - The head's waiter passes the same case.
vivaldi_install_after_exitran against the same setup. After the fake process exited,currentreadtheme-rose-pine. The unrelated keyotherwas unchanged. - A launch wrapper is not smaller. Vivaldi is launched from the app launcher, the macOS Dock and session restore. A wrapper would have to cover each of them. The waiter covers them all with no launch path at all.
- The existing pattern does not fit.
start_session_daemoninlib/lib.shtakes one bare program word. Its fallback issetsid, which macOS lacks. The waiter needs an argument and a macOS path, sovivaldi_start_waiteris not a duplicate of it. - The home is right. The generator sits in
generate-all.sh's map. The install sits beside the otherapply_*functions. Vivaldi exposes no policy or upstream hook that sets avivaldi.*pref.
This review does not gate merge.
datapointchris
left a comment
There was a problem hiding this comment.
PR review
Reviewer: stale-prose
Used: the PR head at 3ea5c74. I ran lib/generators/vivaldi.sh themes/kanagawa/theme.yml and bin/theme browsers --help, and read every added sentence beside its code.
Standards: pull-requests.md § "Sentences the diff adds are checked for what goes stale, and the finding carries the rewrite"; help.md § "Help text never carries a count, version, path or list of what is supported, which the next release makes stale".
Findings: 3 prose
Prose
-
Two added sentences write out a value the code holds.
bin/theme:579andbin/theme:603: "Supports Firefox-based browsers (Zen, Librewolf, Firefox, Thunderbird) and Vivaldi."show_browsersowns which browsers are covered. This list grew by one in this diff, and the next browser has to edit it in both heredocs.help.md§ "Help text never carries a count, version, path or list of what is supported" names exactly this shape. Rewrite: "Shows each detected browser and whether a theme is installed in it."lib/generators/vivaldi.sh:27: "Vivaldi derives every shade, hover and contrast text from these five". Thejqcall below holds the count. The generator emitscolorAccentBg,colorBg,colorFg,colorHighlightBgandcolorWindowBgtoday. A sixth--argmakes the comment false. Rewrite: "Vivaldi derives every shade, hover and contrast text from the colors below, so the palette's own intermediate slots have nowhere to go."
-
lib/generators/vivaldi.sh:31describes a sibling module's behavior: "the accent takes the selection shade that firefox-based.sh gives a selected tab." This is true at the head.firefox-based.sh:197paints.tabbrowser-tab[selected="true"]withUI_SELECTION. Nothing keeps it true whenfirefox-based.shchanges its tab color. The reason this comment needs is Vivaldi-side, and it is already in the comment. Rewrite: "Vivaldi paints its accent across the whole toolbar row and the active tab, so the accent takes the quiet UI selection shade. The loud UI accent goes to highlight, which marks focus and selection." -
lib/vivaldi-preferences.sh:78-81is false on one branch: "on Linux the waiter is its own transient service. On macOS the launchd agent sets AbandonProcessGroup, so nohup is enough there." Theifat line 86 isuname -s != Darwin && systemctl --user show-environment. A Linux box without a systemd user manager takes thenohupbranch, so there the waiter is not a transient service. Rewrite: "A scheduled apply runs as a oneshot whose cgroup systemd kills on exit. Where a systemd user manager runs, the waiter is therefore its own transient service. Elsewhere nohup detaches it. On macOS that is enough, because the launchd agent sets AbandonProcessGroup."
I checked these and found nothing to report:
- The
CLAUDE.md"no pointer at all" list. Its rewrite drops the old count "Four apps". - The
vivaldi.shbullet inCLAUDE.md. - The
README.mdbrowser paragraphs. - The
vivaldi-preferences.shheader, and the comments onvivaldi_running_pid,vivaldi_install_themeandvivaldi_install_after_exit. - The waiter script's header.
- The
tests/vivaldi-preferences.batsheader. - The
show_browsersoutput lines.
None of those narrates the change. Each one matches the code at the head.
This review does not gate merge.
datapointchris
left a comment
There was a problem hiding this comment.
PR review
Reviewer: code-architecture
Used: read the repo at the PR head (3ea5c74) and traced every site that decides how a process escapes the apply, which pref holds Vivaldi's theme, which profiles are read, and how the current theme resolves to an artifact.
Standards: code-quality.md § "Ask whatever owns a fact, the function or enum that computes it, never duplicate its logic at a second site".
Findings: 4 design
Design and architecture
-
lib/vivaldi-preferences.sh:82-92,lib/lib.sh:2448-2460— Two functions decide how a process outlives a scheduled apply.start_session_daemontries Hyprland's exec dispatcher, then a transientsystemd-run --scope, thensetsid.vivaldi_start_waiteruses a transientsystemd-runservice with-E HOME -E PATH, thennohup, and gates Linux onuname -s. Each encodes its own reading of the cgroup rule CLAUDE.md states once. Reach: the next process an apply leaves running.apply_bordersatlib/lib.sh:519-525already restarts a daemon with a bare& disown, andsync_after_actionatlib/sync.sh:378-379backgrounds a push. Each such site must pick one of the two recipes or write a third, and a fix to the cgroup escape lands in one of them only. Fix: onestart_detached <program> [args]inlib.shthat owns the escape.start_session_daemonkeeps only its Hyprland attempt and falls back to it.vivaldi_start_waitercalls it. -
lib/vivaldi-preferences.sh:122-124,lib/vivaldi-preferences.sh:84,lib/vivaldi-install-after-exit.sh:8,lib/lib.sh:19—lib.shsourcesvivaldi-preferences.shas a lower module, and that module reaches back up forlib.sh'sCURRENT_THEME_FILE,THEMES_DIRandTHEME_LIB_DIR. The waiter's entry script then sources all oflib.shto call one function in the lower module. The module's job does not fit one sentence: it rewrites Preferences, detects the running process, owns the waiter's pid file and lifecycle, detaches it, and decides which theme to install. That last fact has two owners.apply_vivaldiresolves the artifact throughget_library_path, which canonicalizes the name. The waiter reads the state file withcatand builds$THEMES_DIR/$theme/vivaldi.jsonitself, bypassing bothget_current_themeandget_library_path. Reach: any change to how a theme id resolves to its directory, such as the dev-mode or installed-copy split CLAUDE.md describes. It reachesapply_vivaldiand misses the waiter. Fix: movevivaldi_install_after_exit's last three lines intolib.shasvivaldi_install_current, built fromget_current_themeandget_library_path. The entry script calls that.vivaldi-preferences.shthen reads Preferences and processes and nothing above it. -
lib/vivaldi-preferences.sh:10,lib/vivaldi-preferences.sh:12-32,lib/vivaldi-preferences.sh:45-46,lib/vivaldi-preferences.sh:102-121,lib/vivaldi-install-after-exit.sh:1-10— The install-while-closed machinery is generic to every Chromium browser but is bound to Vivaldi.SingletonLockpid detection, theLocal Stateprofile list, the staged rename ofPreferences, and the wait-for-exit loop all apply to Chromium, Chrome and Brave unchanged. Only the jq program and the user-data path are Vivaldi's. The pid file, the waiter script and every function carry the Vivaldi name, and one pid file admits one waiting browser. Reach: applyingchromium.theme.lib/generators/chromium.shalready produces it for every theme,bin/theme:858-866still reports it, and this PR's body lists applying it as not done. Chromium also holds its theme inPreferencesand rewrites that file on exit. That browser would copy the module or edit it at the pid file, the entry script, the waiter body and the profile walk in step. Fix: name the generic half for Chromium profiles, and take the user-data dir and the jq program as arguments. Key the pid file and the waiter's arguments by browser. Vivaldi supplies only its path and its pref edit. -
bin/theme:849,lib/vivaldi-preferences.sh:45-60— Two sites decide where Vivaldi's selected theme lives. The installer writes every profile listed inLocal State'sprofile.info_cache, and selects throughvivaldi.themes.currentplus bothvivaldi.theme.schedule.o_sslots.show_browsersreadsDefault/Preferencesalone, and readsvivaldi.themes.currentalone. Under the rule this PR establishes, the effective selection is theo_sslots, so the reader already disagrees with the writer on both the profile set and the pref. Reach: following Vivaldi's timeline schedule, which the PR body lists as not done, moves the selected-theme pref again. It also reaches any second profile. Both sites change in step, and only the writer is tested. Fix: avivaldi_selected_theme <dir>besidevivaldi_install_theme, walking the same profile list and reading the same pref.show_browserscalls it.code-quality.md§ "Ask whatever owns a fact, the function or enum that computes it, never duplicate its logic at a second site".
This review does not gate merge.
datapointchris
left a comment
There was a problem hiding this comment.
PR review
Reviewer: system-design
Used: Read the PR head in a fresh clone. Traced apply_vivaldi, the waiter, apply_theme_to_apps and set_current_theme in lib/lib.sh. Checked install.sh at the tag dotfiles pins, which installs into a git checkout whose path stays the same across updates. Checked dotfiles install/packages.yml and install/system.yml for how Vivaldi is installed. Checked homelab containers/syncthing-lxc/folders-manifest.yml. It syncs no Vivaldi profile and no theme state.
Standards: searched fleet standards search "detached background process race state file read at fire time ordering". No rule reaches this change.
Findings: 1 correctness
Correctness
lib/vivaldi-preferences.sh:123— the waiter installs whatever~/.local/state/theme/currentholds when Vivaldi exits.lib/lib.sh:2255writes that file only at the end ofapply_theme_to_apps, after the Vivaldi block and every app after it. If Vivaldi exits in that window, the waiter installs the previous theme and exits. Vivaldi then launches on the old theme while every other app is on the new one.theme browsersshows nothing pending. Reach: Vivaldi is open on theme A.theme apply B, run by hand or by a scheduled turn, reachesapply_vivaldi. It starts a waiter, or finds one an earlier apply left. It prints✓ vivaldi (at next launch). The apply moves on to bat, the remaining apps and the background. The user quits Vivaldi during those seconds. The waiter sees the pid gone and readscurrent, which still holds A. It writes A intoPreferences, removes its pid file and exits. Thenset_current_theme Bruns. No waiter remains, so B reaches Vivaldi only when a later apply runs with Vivaldi closed. Scheduled turns make this more likely because nobody watches those applies and the user may be using the browser at the time. Found by reading the code. Not run. Fix: writecurrentbeforeapply_theme_to_appsstarts on any app. Or have the waiter re-readcurrentafter its install and install again if it changed.
This review does not gate merge.
datapointchris
left a comment
There was a problem hiding this comment.
PR review
Reviewer: contracts
Used: the PR head in a scratch clone at 3ea5c74. A sandbox HOME held a fake two-profile Vivaldi user-data dir. In it I ran apply_vivaldi kanagawa, bin/theme browsers before and after, and bin/theme info kanagawa. I searched ~/code, ~/tools, ~/webapps, ~/dotfiles, ~/homelab, ~/docs and ~/.claude for callers of vivaldi, theme apply, theme browsers and the APPLIED: line.
Standards: configuration.md § "Every path a tool writes is an XDG base directory" (searched "pid file runtime state location XDG cache state directory"). It reaches no finding here.
Findings: 0 correctness · 1 design
Design and architecture
bin/theme:849—theme browserslabelsvivaldi.themes.currentfrom theDefaultprofile as Vivaldi'sTheme:. The PR states that Vivaldi's default schedule ignorescurrentand readsvivaldi.theme.schedule.o_sinstead. The install also writes every profile inLocal State'sprofile.info_cache, but the report reads onlyDefault. Reach: a user who runstheme browsersto check what Vivaldi shows. In the sandbox,Default/Preferencesheldcurrent: "Vivaldi4"ando_s: {light: "Vivaldi4", dark: "Dark"}. Before any apply,theme browsersprintedTheme: Vivaldi4, while a dark-mode OS showsDark. Afterapply_vivaldi kanagawa, all three keys readtheme-kanagawain both profiles, so the line is correct only once this tool has written it. Fix: print the theme Vivaldi resolves. That iso_s.light/o_s.darkwhen the schedule follows the OS, andcurrentotherwise. Print one line per profile, or rename the fieldcurrent pref (Default)so it claims no more than it reads.
No caller breaks. Nothing outside this repo reads Vivaldi's Preferences, vivaldi.json, the waiter pid file or the new tick line. dotfiles touches Vivaldi only through its extension policy files under /etc/vivaldi/policies and the macOS managed plist. bin/theme parses only APPLIED:, and vivaldi joins it as a space-free name reload_apps ignores. theme info lists Vivaldi under Available. The generated object carries the same keys as a Vivaldi settings.json, checked against tokyonight.nvim's extras/vivaldi. The README and the theme browsers help now name Vivaldi where they claimed Chromium. No code applies chromium.theme.
This review does not gate merge.
datapointchris
left a comment
There was a problem hiding this comment.
PR review
Reviewer: fleet-patterns
Used: PR head 3ea5c74 read in a fresh clone. indy search --owned ran on six phrasings: editing Vivaldi or Chromium Preferences, the Vivaldi theme, detaching from a systemd oneshot, transient units, a pid-file single waiter, and wait-then-rewrite plus atomic JSON rewrite. rg swept ~/tools, ~/code, ~/webapps, ~/dotfiles and ~/homelab for SingletonLock, systemd-run, AbandonProcessGroup, setsid and Hyprland's exec dispatcher. I read font/lib/arch-apps.sh, font/lib/schedule.sh, font/lib/sync.sh, and the Vivaldi rows in dotfiles/install/system.yml and dotfiles/docs/architecture/system-configuration.md.
Standards: none reach it. I searched "atomic write temp file rename", "detached background process pid file single instance lock", "edit another application's config file it owns" and "shared shell library sourced by several bash tools".
Findings: 0 correctness · 1 design · 0 prose
Design and architecture
lib/vivaldi-preferences.sh:82(vivaldi_start_waiter): this is a third way to start a process that outlives the apply's cgroup. The fleet already solves that problem one way, in two places:theme:lib/lib.sh:2448andfont:lib/arch-apps.sh:105. Both definestart_session_daemon, with identical bodies. That recipe usessystemd-run --user --scope, which inherits the caller's whole environment, and falls back tosetsid. The waiter instead uses a transient service with an-E HOME -E PATHallowlist, and falls back tonohupon Linux as well as macOS. The PR body says the waiter must escape the cgroup and carryHOMEandPATH. It gives no reason for choosing a service over the scope that both siblings use. Reach: the next fix to cgroup escape, whether a launchd change, a non-systemd Linux or a new variable the waiter reads, lands in one recipe and is missed in the other. The env allowlist is the concrete hazard. It costs nothing today, becauseCURRENT_THEME_FILEandTHEMES_DIRderive fromHOMEand the script path alone. The first time the waiter readsXDG_*orTHEME_ENV, the service gets nothing, and the scope would have inherited it. The scope recipe is the better one here: it has fewer moving parts, and its environment matches the apply that started it. Its&plusdisownalready detaches. The Hyprland branch is the only part a waiter does not want. Fix: add a cgroup-escape helper besidestart_session_daemonthat takes a command with arguments, usessystemd-run --user --scope --collect, falls back tosetsidon Linux andnohupon macOS, and havestart_session_daemoncall it after its Hyprland attempt. If the service was chosen deliberately, put the reason in the body and in the comment above the function. The copy infontalready disagrees with nothing, so this PR is where the two shapes start to drift. There is no fleet shell library to hold one copy. Moving it across repos stays out of scope until a third tool needs it.
No other divergence found. Vivaldi's extensions go through an enterprise policy in dotfiles:install/system.yml:174. The body's rejection of BrowserThemeColor explains why the theme does not follow that route. No other owned repo reads SingletonLock, writes Chromium Preferences, or keeps a pid-file waiter.
This review does not gate merge.
datapointchris
left a comment
There was a problem hiding this comment.
PR review
Reviewer: failure-paths
Used: The PR head in a scratch clone. A sandboxed HOME and XDG paths held a fake Vivaldi profile. A sleep stood in for the running browser through a hand-made SingletonLock. A stub systemctl forced the nohup waiter path. I drove apply_vivaldi through each failure. No real profile, systemd unit or apply was touched.
Standards: code-quality.md § "Evidence of an effect comes from the effect, never from an exit status or a time window".
Findings: 3 correctness
Correctness
-
lib/vivaldi-preferences.sh:72— A stale waiter pid file stops every later deferred install for good.vivaldi_waiter_runningtrusts any live process holding that pid.apply_vivaldi(lib/lib.sh:641) then starts no waiter and still prints✓ vivaldi (at next launch).theme browsersprintsPending: the current theme installs when Vivaldi exits. Nothing ever installs. Reach: the waiter dies without running its EXIT trap. A power loss, an OOM kill or a SIGKILL at logout does that. The pid file sits in~/.cacheand survives a reboot. Any later user-owned process that takes that pid keeps the state. I wrote a live unrelatedsleep's pid into the file and applied kanagawa against an open fake Vivaldi. It printedat next launchat rc 0. After the browser exited,Preferencesstill read"Vivaldi1".vivaldi_waiter_runningstill answered yes. Fix: prove the pid is the waiter, not just alive. Checkps -p "$pid" -o command=forvivaldi-install-after-exit.shinvivaldi_waiter_running, and remove the pid file when the check fails. -
lib/vivaldi-preferences.sh:87— A deferred install that fails leaves no trace a user can find. The tick✓ vivaldi (at next launch)comes fromsystemd-runornohupstarting a process. It does not come from the install. The waiter's own failures go to/dev/nullon macOS. On Linux they go to the journal of a--collectunit. Those failures are a failedvivaldi_install_theme, the 15-second TERM deadline at:116, and an unreadablecurrentat:123. The pid file is removed on exit either way, sotheme browsersdrops itsPendingline and shows the old theme with no reason. Reach: a profile directory the waiter cannot write when Vivaldi exits, or a Vivaldi that outlasts the deadline at logout. I applied against an open fake Vivaldi, then madeDefault/read-only before the browser exited. The apply printedat next launchat rc 0. After exit,Preferencesstill read"Vivaldi1", and the pid file was gone.code-quality.md§ "Evidence of an effect comes from the effect, never from an exit status or a time window" reaches it. Fix: havevivaldi_install_after_exitwrite its outcome to a state file under~/.cache/theme/, holding the theme id and the failure. Haveshow_browsersand the nextapply_vivaldiread it, so a failed install is reported throughapply_warnon the next apply. -
lib/vivaldi-preferences.sh:28— A host mismatch inSingletonLockreads as a closed browser, so the apply writesPreferencesunder an open Vivaldi. That Vivaldi then overwrites the file on exit. The apply prints a plain✓ vivaldiand starts no waiter. Reach: macOS takes its hostname from the network when none is pinned. A laptop that changes networks while Vivaldi is open has a lock naming the old host. I pointedSingletonLockatold-dhcp-name.local-<pid>with that pid alive. The apply returned rc 0 with no output. It wrotetheme-kanagawainto the open profile'sPreferencesand started no waiter. Fix: invivaldi_running_pid, a live pid whose command is Vivaldi counts as running whatever the host says. Read it withps -p "$pid" -o comm=. Only a dead or non-Vivaldi pid counts as closed.
This review does not gate merge.
datapointchris
left a comment
There was a problem hiding this comment.
PR review
Used: Built a scratch Vivaldi 8.2 profile under a scratch HOME and XDG_CONFIG_HOME, with headless vivaldi-stable --user-data-dir. Ran apply_vivaldi against it closed, then open twice, then sent the browser SIGTERM. Ran the waiter script under env -i HOME PATH, the environment systemd-run -E HOME -E PATH gives it. Ran theme browsers. Regenerated every theme's vivaldi.json and diffed it against the committed copy. Ran bats tests/vivaldi-preferences.bats tests/generators.bats. systemctl was stubbed to fail, so no transient unit reached the live user manager. The real profile was never touched.
Standards: no rule found by fleet standards search "pid file stale process identity runtime directory", "XDG runtime state cache path a tool writes" or "inferring a fact from an artifact lock file".
Findings: 1 correctness · 1 prose
The ordinary flows work. A closed apply installed theme-kanagawa as current and in both o_s slots. Everything outside vivaldi.themes and vivaldi.theme was identical under jq -S. The mode stayed 600. With Vivaldi open, two applies printed at next launch and left one waiter. After SIGTERM the waiter installed the latest theme, theme-rose-pine, and removed its pid file. The waiter ran clean with only HOME and PATH. Every committed vivaldi.json matches its regeneration. Both bats files pass.
Correctness
lib/vivaldi-preferences.sh:22and:72— both liveness checks trust a pid without checking which process holds it. The first isvivaldi_running_pid. It reads the pid out ofSingletonLockand accepts any live process. The second isvivaldi_waiter_running. It does the same with~/.cache/theme/vivaldi-waiter.pid. When a stale lock names a pid an unrelated process now holds, the apply reports✓ vivaldi (at next launch)while no Vivaldi is running.theme browsersthen showsPending. The waiter waits on that unrelated process until it exits. Every later apply that session sees the waiter's pid file and starts no new one. So with Vivaldi open all day, which is the case the PR body says the waiter exists for, the theme never lands that session.
Reach: the stale lock is common. A SIGTERM'd Vivaldi leftSingletonLockbehind in my run (archlinux-3093125still linked after the process exited). The PR body says SIGTERM is what a logout sends. A crash or power loss leaves the lock too, and also the waiter's pid file, because no EXIT trap runs. Pids restart at every boot, and this machine's Vivaldi holds an early-boot pid (archlinux-5982). Reuse is unproven. Runningps -p <old pid>after a reboot, before Vivaldi launches, would settle it.
I pointed the scratch lock at a livesleep 600with no Vivaldi running.apply_vivaldi nordicreturnedat next launch.Preferencesstayed ontheme-rose-pineuntil I killed thesleep, and then the waiter installedtheme-nordic.
Chromium's ownProcessSingletonhandles a stale lock this way: it treats the lock as live only when the pid is a browser process.
Fix: invivaldi_running_pid, afterkill -0, add[[ "$(ps -p "$pid" -o comm=)" == *[Vv]ivaldi* ]] || return 1. On Arch the comm isvivaldi-bin. On macOS it is the bundle'sContents/MacOS/Vivaldipath. Invivaldi_waiter_running, requireps -p "$pid" -o args=to containvivaldi-install-after-exit. On Linux, also move the pid file to${XDG_RUNTIME_DIR}. That directory is emptied when the user's last session ends, so no pid file survives a reboot.
Prose
CLAUDE.md— the list under "State and generated data live at" does not name the new~/.cache/theme/vivaldi-waiter.pid. The paragraph after it says "Generated state belongs in those two cache directories and nowhere else". A reader looking for what an apply writes, or for whytheme browserssaysPending, will not find the file. Rewrite: add a row for the waiter's pid file, at whatever path finding 1 settles on, described as "pid of the waiter that installs the current theme into Vivaldi once it exits". The paragraph's claim is then about generated state only, so it stays true.
This review does not gate merge.
datapointchris
left a comment
There was a problem hiding this comment.
PR review
Arm: Sonnet
Used: Ran vivaldi_install_theme and vivaldi_running_pid against a sandbox HOME with a two-profile Local State (one profile without Preferences), a stale SingletonLock and a live one.
Standards: Read the running-the-review section only. No further rule searches found a rule the diff breaks.
Findings: 1 correctness
Correctness
lib/vivaldi-preferences.sh:vivaldi_waiter_running— a stale waiter pid file can block every later waiter, so an apply made while Vivaldi is open never installs. Reach: unproven — settle it by killing the waiter with SIGKILL (or losing power) while Vivaldi is open, then checking whether the recorded pid has been reused by another process.~/.cache/theme/vivaldi-waiter.pidsurvives a reboot, and only the waiter's own EXIT trap removes it. The check iskill -0 "$pid"on any process. A reused pid makesapply_vivaldiprint "at next launch", skip starting a waiter and install nothing. Sandbox run: installing into a profile, skipping a profile with noPreferences, and the stale/live lock checks all behaved correctly. Fix: verify the pid is the waiter, for example by comparing/proc/$pid/cmdline(orps -p "$pid" -o command=) againstvivaldi-install-after-exit.sh, or hold the pid file under$XDG_RUNTIME_DIRso a reboot clears it.
This review does not gate merge.
Sonnet arm review — mergedArm: Sonnet Findings: 4 correctness · 3 design · 2 prose CorrectnessS1. [G1, G2, system-design] S2. [failure-paths] S3. [failure-paths] S4. [contracts] Design and architectureS5. [G1, contracts, code-architecture, lead] S6. [code-architecture, fleet-patterns] S7. [code-architecture] ProseS8. [stale-prose] S9. [stale-prose] This review does not gate merge. 0 rule(s) proposed, filed into the fleet inbox as proposals. |
datapointchris
left a comment
There was a problem hiding this comment.
PR review
Used: ran headless Vivaldi on a scratch --user-data-dir. Called apply_vivaldi closed, then twice while Vivaldi was open, with systemctl shadowed so the waiter took the nohup path. Sent the browser a SIGTERM, relaunched it, and read Preferences after each step. Ran bats tests/ (255 ok).
Standards: no breach found. Searched configuration.md § "Every path a tool writes is an XDG base directory", where the pid file follows the repo's existing hardcoded ~/.cache/theme. Searched shell.md § "Don't compose an EXIT trap onto one that already exists", where the waiter owns the only EXIT trap in its process.
Findings: 1 correctness · 1 prose
The flows the body describes work. A closed install selected theme-kanagawa in current and in both o_s slots. The file stayed mode 600. With Vivaldi open, gruvbox then rose-pine each printed at next launch, and only one waiter started. After the SIGTERM, the waiter installed theme-rose-pine and removed its pid file. A relaunch kept it.
Correctness
-
lib/vivaldi-preferences.sh:28-30—vivaldi_running_piddecides Vivaldi is running from the lock's host and pid alone, and the waiter then pins its wait to that pid at line 114. That fails in two directions.- Host changed. The lock records the hostname from when Vivaldi launched. macOS changes
uname -nwhen a laptop changes networks, unlessHostNameis set. The next apply then reads an open Vivaldi as closed. It writesPreferencesunder the running browser and prints✓ vivaldi. Vivaldi overwrites the write on exit, and no waiter exists to redo it. - Pid reused. A SIGTERM leaves
SingletonLockbehind. My scratch Vivaldi leftarchlinux-3090623in place after it exited. If another process later takes that pid,apply_vivaldireads a closed Vivaldi as open and starts a waiter. The innerkill -0loop then waits on the unrelated process, and a relaunched Vivaldi does not move it. Every later apply finds that waiter, starts no other, and printsat next launch.theme browserskeeps reportingPending. The theme lands only when the unrelated process exits.
Reach: unproven.
scutil --get HostNameon the Mac settles the first case. Whether a GUI Vivaldi leaves its lock after a logout's SIGTERM settles the second. Headless Vivaldi did. Chromium's ownProcessSingletonguards against pid reuse by checking that the pid's executable is its own browser. Fix: drop the host comparison, because this tool only reads a local profile. Confirm that the pid is a Vivaldi process, with/proc/<pid>/exeon Linux andps -o comm= -pon macOS. In the waiter, re-runvivaldi_running_pidon every poll instead ofkill -0on a pinned pid. - Host changed. The lock records the hostname from when Vivaldi launched. macOS changes
Prose
CLAUDE.md:22— "Generated state belongs in those two cache directories and nowhere else." The branch adds~/.cache/theme/vivaldi-waiter.pid. It sits in neither directory, and the state list above it at lines 10-20 does not name it. A reader debugging a stuckPendingintheme browsershas nowhere to find it. Rewrite: add~/.cache/theme/vivaldi-waiter.pid - The waiter that installs into Vivaldi after it exitsto the list. Then change the sentence to "Generated state belongs in the cache directories above and nowhere else."
This review does not gate merge.
PR review — mergedTen reviewers ran: two general reviewers (G1, G2), the test reviewer, and seven personas. Those were code-architecture, fleet-patterns, system-design, simplest-design, stale-prose, failure-paths and contracts. This comment unions all ten reviews. useless-test and simplest-design posted no findings. It promoted 1 shape. The author's session launched this round through a review lead. Findings: 2 correctness · 5 design · 4 prose Correctness
Design and architecture
Prose
Reviews unioned:
This review does not gate merge. 2 rule(s) proposed, filed into the fleet inbox as proposals. |
… 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
theme applythemes every terminal and desktop app but leaves Vivaldi on whatever theme it last had, so the browser never matches the rest of the desktop. Vivaldi keeps its themes in each profile'sPreferences. It holds that file in memory and writes all of it back on exit, so nothing outside Vivaldi can change an open window's theme.This generates a Vivaldi theme object for every theme and installs it into the profile. A closed Vivaldi gets it at once. An apply while Vivaldi is open stages the theme and leaves a detached waiter, which installs the staged theme once Vivaldi exits. The theme shows at the next launch.
What to look at
vivaldi_install_themeinlib/vivaldi-preferences.sh. Its jq program rewrites the user's wholePreferences. Check that it can change nothing butvivaldi.themes.user,vivaldi.themes.currentandvivaldi.theme.schedule.o_s, and that a failed profile leaves its file in place.vivaldi_install_after_exitin the same file. Check that it waits for the process rather than the lock, and that a theme staged mid-install lands on the next pass. Check that the TERM trap bounds the wait.vivaldi_pid_is_vivaldiandvivaldi_waiter_running. A lock or pid file outlives its writer, so each pid is trusted only whilepsnames the expected program.start_detachedinlib/lib.sh. It is the one way an apply starts a process that must outlive it. Check that it escapes a scheduled oneshot's cgroup.lib/generators/vivaldi.sh.How it was verified
Every run used a scratch Vivaldi 8.2 profile: a temp
--user-data-dir, headless, read over the DevTools protocol. None wrote to a real profile.apply_vivaldi kanagawasettheme-kanagawaascurrentand in both OS-schedule slots.jq -Sof everything outsidevivaldi.themesandvivaldi.themewas byte-identical before and after. The file mode stayed 600. The relaunched UI read--colorBg=#1f1f28and--colorFg=#dcd7ba, matchingthemes/kanagawa/vivaldi.json.at next launch. One waiter ran, in its ownrun-p<pid>.scope, withtheme-rose-pinestaged. After a SIGTERM to Vivaldi, the waiter installed rose-pine and left~/.cache/theme/vivaldi/empty.systemd-run --user --wait -p Type=oneshot. The waiter was still running after the oneshot exited, and installed once Vivaldi stopped.SingletonLocknaming a livesleepread as a closed Vivaldi, so the theme installed at once.sleepwas not trusted.Vivaldi never got Nordic: it was still open when its waiter stopped. This apply replaces it.It then installed terafox and cleared the staged file.theme browsersshowedPending: Nordic, never installed.Profile 1/Preferencestruncated,Defaultstill got terafox. The call returned 1, and no staged file was left behind.theme browserslisted the profile asunreadable.vivaldi_selected_themes, read-only on a real profile, printedWork: Pride Theme Dark when light, Vivaldi2 when dark.colorAccentBgthe UI accent. Vivaldi paints that across the whole toolbar row and the active tab, which made a loud blue bar.bats tests/passed in the commit hook. That includestests/generators.bats' regenerated-artifact diff over the newvivaldi.jsonfiles.What changes
theme applyon Arch and macOS installs the theme into Vivaldi. With Vivaldi open, the tick reads✓ vivaldi (at next launch). Open windows keep their colors until Vivaldi restarts.theme-<id>. Nothing is pruned.theme browsersshows what each Vivaldi profile displays, and any staged theme.✗ vivaldi (error), the way a machine without kitty prints✗ kitty (error).start_session_daemonfalls back tostart_detached, which usesnohupwhere neither systemd norsetsidexists.chromium.theme.Decisions, and what they rejected
window.html. Every Vivaldi upgrade rewrites that file, and macOS would need a patched signed bundle. Reports also disagree on whethervivaldi.prefs.setswitches a theme from injected script.BrowserThemeColortints Chromium's native toolbar, which Vivaldi doesn't draw with. No policy maps to avivaldi.*pref.current. Vivaldi's default schedule follows the OS and reads those slots, socurrentalone changes nothing on a default install. Setting only the slot that matches the theme's variant was rejected. With an OS in light mode, a dark theme would never show.SingletonLockbefore its lastPreferenceswrite. Process exit comes after every write.colorAccentBgtakes the selection shade, andcolorHighlightBgthe UI accent. The selection shade is what a selected tab gets elsewhere.Preferencesonly while Vivaldi is closed, with no waiter. A browser left open all day is open at every scheduled apply, so the theme would land only by chance.Risk and rollback
This writes the user's Vivaldi
Preferences. A revert stops further writes. Themes already installed stay in Vivaldi's list. The schedule slots keep pointing at the last applied theme until they are changed in Vivaldi's settings.What this does not do
currentand the OS slots, andtheme browserssays so.chromium.theme.The review
Opus arm, #23 (comment):
theme browsersshows it.psnames Vivaldi or the waiter. The host comparison is gone, and the waiter re-checks the program on every poll.vivaldi_selected_themeswalks every profile and follows the schedule mode.start_detachedinlib.shis the one recipe.start_session_daemonfalls back to it, and the waiter is a transient scope that inherits the environment.vivaldi-preferences.shand installs the staged file.apply_vivaldialone resolves the artifact, throughget_library_path.chromium.theme. The jq program and user-data dir become parameters when one is.~/.cache/theme/vivaldi/, and the sentence reads "the cache directories above".start_detached's comment names each branch.Sonnet arm, #23 (comment):
THEME_ENV.theme browserssharevivaldi_profiles, andvivaldi_selected_themesfollows the schedule.get_browser_profiles_dirstays Firefox-only, because it returns aprofiles.inidirectory and Vivaldi's user-data dir is not one.lib.sh. NoTHEME_CACHE_DIRwas added, because the waiter sources only this module and could not read one.CLAUDE.mdcarries the command that re-checks it, and theAbandonProcessGroupcomment nameslib/schedule.sh. The generator comments keep the reason for the mapping, which the code cannot show.Opened by Claude Code session 0105027b-1083-4e8b-8f26-8733a784a41b.