Skip to content

feat(vivaldi): generate a Vivaldi theme and install it on theme apply - #23

Merged
datapointchris merged 2 commits into
mainfrom
vivaldi-theme
Oct 21, 2026
Merged

datapointchris merged 2 commits into
mainfrom
vivaldi-theme

Conversation

@datapointchris

@datapointchris datapointchris commented Oct 21, 2026 •

Copy link
Copy Markdown
Owner

theme apply themes 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's Preferences. 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

  1. vivaldi_install_theme in lib/vivaldi-preferences.sh. Its jq program rewrites the user's whole Preferences. Check that it can change nothing but vivaldi.themes.user, vivaldi.themes.current and vivaldi.theme.schedule.o_s, and that a failed profile leaves its file in place.
  2. vivaldi_install_after_exit in 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.
  3. vivaldi_pid_is_vivaldi and vivaldi_waiter_running. A lock or pid file outlives its writer, so each pid is trusted only while ps names the expected program.
  4. start_detached in lib/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.
  5. The color mapping in 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.

  • Closed install. apply_vivaldi kanagawa set theme-kanagawa as current and in both OS-schedule slots. jq -S of everything outside vivaldi.themes and vivaldi.theme was byte-identical before and after. The file mode stayed 600. The relaunched UI read --colorBg=#1f1f28 and --colorFg=#dcd7ba, matching themes/kanagawa/vivaldi.json.
  • Deferred install. With Vivaldi open, gruvbox then rose-pine each printed at next launch. One waiter ran, in its own run-p<pid>.scope, with theme-rose-pine staged. After a SIGTERM to Vivaldi, the waiter installed rose-pine and left ~/.cache/theme/vivaldi/ empty.
  • A scheduled apply. The same deferred apply ran inside systemd-run --user --wait -p Type=oneshot. The waiter was still running after the oneshot exited, and installed once Vivaldi stopped.
  • Stale records.
    • A SingletonLock naming a live sleep read as a closed Vivaldi, so the theme installed at once.
    • A waiter pid file naming a live sleep was not trusted.
  • A stranded theme. With nordic staged and no waiter, the real dispatcher printed 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 browsers showed Pending: Nordic, never installed.
  • A broken second profile. With Profile 1/Preferences truncated, Default still got terafox. The call returned 1, and no staged file was left behind. theme browsers listed the profile as unreadable.
  • The effective selection. vivaldi_selected_themes, read-only on a real profile, printed Work: Pride Theme Dark when light, Vivaldi2 when dark.
  • Screenshots of kanagawa and rose-pine set the mapping. The first draft gave colorAccentBg the 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 includes tests/generators.bats' regenerated-artifact diff over the new vivaldi.json files.

What changes

  • theme apply on 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.
  • The first apply points Vivaldi's OS light/dark schedule at the applied theme, so Vivaldi no longer switches theme with the OS. The user's own themes stay installed and selectable.
  • Every applied theme stays in Vivaldi's theme list as theme-<id>. Nothing is pruned.
  • theme browsers shows what each Vivaldi profile displays, and any staged theme.
  • A staged theme that never landed is warned about at the next apply.
  • A machine with Vivaldi never launched prints ✗ vivaldi (error), the way a machine without kitty prints ✗ kitty (error).
  • start_session_daemon falls back to start_detached, which uses nohup where neither systemd nor setsid exists.
  • The README no longer claims Chromium is themed on apply. No code has ever applied chromium.theme.

Decisions, and what they rejected

  • The theme lands at the next launch. Two live routes were rejected:
    • A script injected into Vivaldi's root-owned window.html. Every Vivaldi upgrade rewrites that file, and macOS would need a patched signed bundle. Reports also disagree on whether vivaldi.prefs.set switches a theme from injected script.
    • The DevTools port. Vivaldi refuses it on the default profile, so the profile would have to move. Any local process could then drive the browser and read its cookies.
  • No enterprise policy. BrowserThemeColor tints Chromium's native toolbar, which Vivaldi doesn't draw with. No policy maps to a vivaldi.* pref.
  • Both OS-schedule slots are set, as well as current. Vivaldi's default schedule follows the OS and reads those slots, so current alone 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.
  • The waiter waits for the process, not the lock. Chromium may drop SingletonLock before its last Preferences write. Process exit comes after every write.
  • The apply stages its own theme for the waiter. Several applies during one Vivaldi session overwrite one staged file, so the last lands. Reading the current-theme file when Vivaldi exits was rejected. That file is written after every app has applied, so a Vivaldi quit in between got the theme being replaced.
  • colorAccentBg takes the selection shade, and colorHighlightBg the UI accent. The selection shade is what a selected tab gets elsewhere.
  • Smallest change that would work is writing Preferences only 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

  • Switch the theme of an open window.
  • Return Vivaldi's windows to their Hyprland workspaces after the restart. Hyprland implements no session-management protocol, so restored windows all open on the active workspace.
  • Follow Vivaldi's timeline schedule. A profile using it ignores both current and the OS slots, and theme browsers says so.
  • Apply chromium.theme.
  • Run on WSL.

The review

Opus arm, #23 (comment):

  1. fixed — the apply stages its own theme, and the waiter installs that file rather than the current-theme file.
  2. fixed — a failure leaves the theme staged. The next apply warns about it, and theme browsers shows it.
  3. fixed — each pid is trusted only while ps names Vivaldi or the waiter. The host comparison is gone, and the waiter re-checks the program on every poll.
  4. fixed — vivaldi_selected_themes walks every profile and follows the schedule mode.
  5. fixed — start_detached in lib.sh is the one recipe. start_session_daemon falls back to it, and the waiter is a transient scope that inherits the environment.
  6. fixed — the waiter sources only vivaldi-preferences.sh and installs the staged file. apply_vivaldi alone resolves the artifact, through get_library_path.
  7. rejected — no second Chromium browser is applied, and nothing plans to apply chromium.theme. The jq program and user-data dir become parameters when one is.
  8. fixed — the state list names ~/.cache/theme/vivaldi/, and the sentence reads "the cache directories above".
  9. fixed — the help's browser list is gone from both copies, and the generator comment says "the colors below".
  10. fixed — start_detached's comment names each branch.
  11. fixed — rewritten as proposed.

Sonnet arm, #23 (comment):

  • S1. fixed — as 3.
  • S2. fixed — as 2.
  • S3. fixed — a failing profile is skipped and its staged file removed. The rest install, and the call returns 1.
  • S4. fixed — the waiter no longer reads the current-theme file, and its scope inherits THEME_ENV.
  • S5. fixed — install and theme browsers share vivaldi_profiles, and vivaldi_selected_themes follows the schedule. get_browser_profiles_dir stays Firefox-only, because it returns a profiles.ini directory and Vivaldi's user-data dir is not one.
  • S6. fixed — as 5.
  • S7. fixed — the waiter is handed the staged file and the module reads nothing from lib.sh. No THEME_CACHE_DIR was added, because the waiter sources only this module and could not read one.
  • S8. fixed — each dead end in CLAUDE.md carries the command that re-checks it, and the AbandonProcessGroup comment names lib/schedule.sh. The generator comments keep the reason for the mapping, which the code cannot show.
  • S9. fixed — the help copies are gone and the README's prose copy is shortened. The README's Supported Applications list stays, as the place a stranger learns which browsers are themed.

Opened by Claude Code session 0105027b-1083-4e8b-8f26-8733a784a41b.

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 datapointchris left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. lib/vivaldi-preferences.sh:70 — vivaldi_waiter_running trusts a pid file with only kill -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 prints at next launch while 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 (and ps -p $pid -o command= on macOS) names vivaldi-install-after-exit.sh.
  2. bin/theme:849 — theme browsers reads only $vivaldi_dir/Default/Preferences. vivaldi_install_theme writes every profile in Local State. A user whose profile directory is Profile 1 sees Theme: unknown although the install succeeded. Reach: any Vivaldi user without a Default profile directory. Fix: read the first profile vivaldi_install_theme iterates, or list each profile's selected theme.

This review does not gate merge.

@datapointchris datapointchris left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 datapointchris left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. lib/vivaldi-preferences.sh:67 — vivaldi_waiter_running trusts 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 next theme apply finds Vivaldi open, kill -0 N succeeds, and vivaldi_waiter_running returns 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 (or ps -p "$pid" -o command=) for vivaldi-install-after-exit.sh, or on Linux start it as a fixed-name transient unit (systemd-run --unit=theme-vivaldi-waiter) and ask systemctl --user is-active. A fixed unit name also removes the file and its race.

This review does not gate merge.

@datapointchris datapointchris left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. lib/vivaldi-preferences.sh:84 — Detaching a background job from the apply's cgroup is decided twice. vivaldi_start_waiter picks a mechanism (systemd-run --user service, else nohup, keyed on uname -s and systemctl --user show-environment). start_session_daemon at lib/lib.sh:2448 decides the same fact differently (hyprctl exec, then systemd-run --scope, else setsid). 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. The systemctl probe is already written twice. This PR is that change for one case. Fix: one detach_from_apply <cmd...> in lib/lib.sh that both callers use. The Hyprland dispatch stays a daemon-only option on top.

  2. lib/vivaldi-preferences.sh:12 — Where a browser keeps its data, and which platforms run it, have three owners. get_browser_profiles_dir in lib/browser-profiles.sh:14 owns every other browser's path per OS. vivaldi_user_data_dir re-implements it with its own uname -s case. apply_theme_to_apps (lib/lib.sh:2150) gates on detect_platform being macos or arch. show_browsers (bin/theme:849) reads $vivaldi_dir/Default/Preferences, while vivaldi_install_theme (lib/vivaldi-preferences.sh:42) walks every profile in Local State. The status view and the installer already disagree on which profiles exist. Reach: the next Chromium-family browser (Brave, Chrome, Edge; chromium.theme is already a second Chromium path) copies the directory case, the platform gate and the profile list again. Fix: add Vivaldi to get_browser_profiles_dir and expose one profile-enumeration function. The installer and show_browsers both call it.

  3. lib/vivaldi-preferences.sh:10 — The new module reaches up into lib.sh for facts it does not own. It is sourced from lib/lib.sh:19 but reads THEME_LIB_DIR, THEMES_DIR and CURRENT_THEME_FILE, which lib.sh defines. It hardcodes $HOME/.cache/theme at line 10, where lib.sh:1020 and lib/generators/background-recolor.sh:86 each hardcode it again. The waiter (line 123) builds $THEMES_DIR/$theme/vivaldi.json itself, while apply_vivaldi resolves the same artifact through get_library_path. lib/vivaldi-install-after-exit.sh sources all of lib.sh to 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 after lib.sh. Fix: pass the resolved JSON path to the waiter from apply_vivaldi, which also fixes the divergence with get_library_path. Take the cache dir from one THEME_CACHE_DIR variable.

This review does not gate merge.

@datapointchris datapointchris left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 datapointchris left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. 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 with Default valid and Profile 1 holding {trunc. It printed jq: parse error, returned 1, left Default with the theme selected, and left Profile 1 untouched. 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: in vivaldi_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.

  2. 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/null in vivaldi_start_waiter. Its failure paths all return 1 silently: the 15-second deadline after TERM, a missing current file, and any vivaldi_install_theme error 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 at vivaldi_install_after_exit. Fix: have vivaldi_install_after_exit append its failure to $THEME_STATE_DIR or the cache dir (a vivaldi-waiter.log), and have the next theme apply print it through apply_warn. The apply_warn array is the route that survives the 2>/dev/null on apply_vivaldi.

This review does not gate merge.

@datapointchris datapointchris left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. lib/vivaldi-preferences.sh:94 — A dev-mode apply with Vivaldi open installs the production theme, not the one applied. Reach: vivaldi_start_waiter passes only -E HOME -E PATH to systemd-run, so the waiter loses THEME_ENV=development. lib/storage.sh:15-18 then points CURRENT_THEME_FILE at ~/.local/state/theme/current instead of .dev-data. The waiter writes the production current theme's vivaldi.json into the real profile. schedule.sh:270 already forwards THEME_ENV for the same reason. Read from code; I did not run it. Fix: add -E THEME_ENV when it is set, or pass the theme id to the waiter as an argument.

Design and architecture

  1. bin/theme:850 — theme browsers reads only Default/Preferences, while the install writes every profile in Local State's info_cache. Reach: a user whose Vivaldi runs on Profile 1 sees Theme: none or a stale id after a successful apply, and the next reader trusts it. Fix: report the profiles that vivaldi_install_theme iterates, or name the profile in the line.

This review does not gate merge.

@datapointchris datapointchris left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 datapointchris left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. CLAUDE.md:361-365 — "Nothing outside Vivaldi can change an open window's theme", "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." 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 at lib/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") and lib/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 sets AbandonProcessGroup. Cut the "derives every shade" and "paints its accent" claims to what the code does: it passes five colors and takes the accent from UI_SELECTION.

  2. 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:3 and bin/theme:786 already 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 browsers lists the ones found on this machine."

This review does not gate merge.

@datapointchris datapointchris left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. lib/vivaldi-preferences.sh:80-92 — vivaldi_start_waiter is a third way to leave a systemd oneshot's cgroup. The fleet already has one: start_session_daemon at theme:lib/lib.sh:2448-2459 and font:lib/arch-apps.sh:105-117. Those two copies carry the same logic. They run systemd-run --user --scope ... &, then setsid when no user manager exists, then disown. This PR uses systemd-run --user --collect without --scope (a transient service, run in the foreground). Its fallback is nohup ... &, with no setsid. Reach: the next fix to the cgroup-escape rule lands in start_session_daemon and 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 (nohup only) is the weaker one. The existing helper uses setsid there. Fix: split the detach out of start_session_daemon into one helper in lib.sh that takes a command and a mode (Hyprland exec, service, or scope). Have both start_session_daemon and vivaldi_start_waiter call it. If font must 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 datapointchris left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 in install/system.yml. So apply_vivaldi has 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.current was still x. The deferred-install case the body advertises needs something to act after exit.
  • The head's waiter passes the same case. vivaldi_install_after_exit ran against the same setup. After the fake process exited, current read theme-rose-pine. The unrelated key other was 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_daemon in lib/lib.sh takes one bare program word. Its fallback is setsid, which macOS lacks. The waiter needs an argument and a macOS path, so vivaldi_start_waiter is not a duplicate of it.
  • The home is right. The generator sits in generate-all.sh's map. The install sits beside the other apply_* functions. Vivaldi exposes no policy or upstream hook that sets a vivaldi.* pref.

This review does not gate merge.

@datapointchris datapointchris left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. Two added sentences write out a value the code holds.

    • bin/theme:579 and bin/theme:603: "Supports Firefox-based browsers (Zen, Librewolf, Firefox, Thunderbird) and Vivaldi." show_browsers owns 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". The jq call below holds the count. The generator emits colorAccentBg, colorBg, colorFg, colorHighlightBg and colorWindowBg today. A sixth --arg makes 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."
  2. lib/generators/vivaldi.sh:31 describes 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:197 paints .tabbrowser-tab[selected="true"] with UI_SELECTION. Nothing keeps it true when firefox-based.sh changes 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."

  3. lib/vivaldi-preferences.sh:78-81 is 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." The if at line 86 is uname -s != Darwin && systemctl --user show-environment. A Linux box without a systemd user manager takes the nohup branch, 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.sh bullet in CLAUDE.md.
  • The README.md browser paragraphs.
  • The vivaldi-preferences.sh header, and the comments on vivaldi_running_pid, vivaldi_install_theme and vivaldi_install_after_exit.
  • The waiter script's header.
  • The tests/vivaldi-preferences.bats header.
  • The show_browsers output lines.

None of those narrates the change. Each one matches the code at the head.

This review does not gate merge.

@datapointchris datapointchris left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. lib/vivaldi-preferences.sh:82-92, lib/lib.sh:2448-2460 — Two functions decide how a process outlives a scheduled apply. start_session_daemon tries Hyprland's exec dispatcher, then a transient systemd-run --scope, then setsid. vivaldi_start_waiter uses a transient systemd-run service with -E HOME -E PATH, then nohup, and gates Linux on uname -s. Each encodes its own reading of the cgroup rule CLAUDE.md states once. Reach: the next process an apply leaves running. apply_borders at lib/lib.sh:519-525 already restarts a daemon with a bare & disown, and sync_after_action at lib/sync.sh:378-379 backgrounds 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: one start_detached <program> [args] in lib.sh that owns the escape. start_session_daemon keeps only its Hyprland attempt and falls back to it. vivaldi_start_waiter calls it.

  2. lib/vivaldi-preferences.sh:122-124, lib/vivaldi-preferences.sh:84, lib/vivaldi-install-after-exit.sh:8, lib/lib.sh:19 — lib.sh sources vivaldi-preferences.sh as a lower module, and that module reaches back up for lib.sh's CURRENT_THEME_FILE, THEMES_DIR and THEME_LIB_DIR. The waiter's entry script then sources all of lib.sh to 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_vivaldi resolves the artifact through get_library_path, which canonicalizes the name. The waiter reads the state file with cat and builds $THEMES_DIR/$theme/vivaldi.json itself, bypassing both get_current_theme and get_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 reaches apply_vivaldi and misses the waiter. Fix: move vivaldi_install_after_exit's last three lines into lib.sh as vivaldi_install_current, built from get_current_theme and get_library_path. The entry script calls that. vivaldi-preferences.sh then reads Preferences and processes and nothing above it.

  3. 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. SingletonLock pid detection, the Local State profile list, the staged rename of Preferences, 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: applying chromium.theme. lib/generators/chromium.sh already produces it for every theme, bin/theme:858-866 still reports it, and this PR's body lists applying it as not done. Chromium also holds its theme in Preferences and 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.

  4. bin/theme:849, lib/vivaldi-preferences.sh:45-60 — Two sites decide where Vivaldi's selected theme lives. The installer writes every profile listed in Local State's profile.info_cache, and selects through vivaldi.themes.current plus both vivaldi.theme.schedule.o_s slots. show_browsers reads Default/Preferences alone, and reads vivaldi.themes.current alone. Under the rule this PR establishes, the effective selection is the o_s slots, 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: a vivaldi_selected_theme <dir> beside vivaldi_install_theme, walking the same profile list and reading the same pref. show_browsers calls 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 datapointchris left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. lib/vivaldi-preferences.sh:123 — the waiter installs whatever ~/.local/state/theme/current holds when Vivaldi exits. lib/lib.sh:2255 writes that file only at the end of apply_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 browsers shows nothing pending. Reach: Vivaldi is open on theme A. theme apply B, run by hand or by a scheduled turn, reaches apply_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 reads current, which still holds A. It writes A into Preferences, removes its pid file and exits. Then set_current_theme B runs. 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: write current before apply_theme_to_apps starts on any app. Or have the waiter re-read current after its install and install again if it changed.

This review does not gate merge.

@datapointchris datapointchris left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. bin/theme:849 — theme browsers labels vivaldi.themes.current from the Default profile as Vivaldi's Theme:. The PR states that Vivaldi's default schedule ignores current and reads vivaldi.theme.schedule.o_s instead. The install also writes every profile in Local State's profile.info_cache, but the report reads only Default. Reach: a user who runs theme browsers to check what Vivaldi shows. In the sandbox, Default/Preferences held current: "Vivaldi4" and o_s: {light: "Vivaldi4", dark: "Dark"}. Before any apply, theme browsers printed Theme: Vivaldi4, while a dark-mode OS shows Dark. After apply_vivaldi kanagawa, all three keys read theme-kanagawa in both profiles, so the line is correct only once this tool has written it. Fix: print the theme Vivaldi resolves. That is o_s.light/o_s.dark when the schedule follows the OS, and current otherwise. Print one line per profile, or rename the field current 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 datapointchris left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. 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:2448 and font:lib/arch-apps.sh:105. Both define start_session_daemon, with identical bodies. That recipe uses systemd-run --user --scope, which inherits the caller's whole environment, and falls back to setsid. The waiter instead uses a transient service with an -E HOME -E PATH allowlist, and falls back to nohup on Linux as well as macOS. The PR body says the waiter must escape the cgroup and carry HOME and PATH. 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, because CURRENT_THEME_FILE and THEMES_DIR derive from HOME and the script path alone. The first time the waiter reads XDG_* or THEME_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 & plus disown already detaches. The Hyprland branch is the only part a waiter does not want. Fix: add a cgroup-escape helper beside start_session_daemon that takes a command with arguments, uses systemd-run --user --scope --collect, falls back to setsid on Linux and nohup on macOS, and have start_session_daemon call 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 in font already 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 datapointchris left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. lib/vivaldi-preferences.sh:72 — A stale waiter pid file stops every later deferred install for good. vivaldi_waiter_running trusts any live process holding that pid. apply_vivaldi (lib/lib.sh:641) then starts no waiter and still prints ✓ vivaldi (at next launch). theme browsers prints Pending: 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 ~/.cache and survives a reboot. Any later user-owned process that takes that pid keeps the state. I wrote a live unrelated sleep's pid into the file and applied kanagawa against an open fake Vivaldi. It printed at next launch at rc 0. After the browser exited, Preferences still read "Vivaldi1". vivaldi_waiter_running still answered yes. Fix: prove the pid is the waiter, not just alive. Check ps -p "$pid" -o command= for vivaldi-install-after-exit.sh in vivaldi_waiter_running, and remove the pid file when the check fails.

  2. lib/vivaldi-preferences.sh:87 — A deferred install that fails leaves no trace a user can find. The tick ✓ vivaldi (at next launch) comes from systemd-run or nohup starting a process. It does not come from the install. The waiter's own failures go to /dev/null on macOS. On Linux they go to the journal of a --collect unit. Those failures are a failed vivaldi_install_theme, the 15-second TERM deadline at :116, and an unreadable current at :123. The pid file is removed on exit either way, so theme browsers drops its Pending line 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 made Default/ read-only before the browser exited. The apply printed at next launch at rc 0. After exit, Preferences still 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: have vivaldi_install_after_exit write its outcome to a state file under ~/.cache/theme/, holding the theme id and the failure. Have show_browsers and the next apply_vivaldi read it, so a failed install is reported through apply_warn on the next apply.

  3. lib/vivaldi-preferences.sh:28 — A host mismatch in SingletonLock reads as a closed browser, so the apply writes Preferences under an open Vivaldi. That Vivaldi then overwrites the file on exit. The apply prints a plain ✓ vivaldi and 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 pointed SingletonLock at old-dhcp-name.local-<pid> with that pid alive. The apply returned rc 0 with no output. It wrote theme-kanagawa into the open profile's Preferences and started no waiter. Fix: in vivaldi_running_pid, a live pid whose command is Vivaldi counts as running whatever the host says. Read it with ps -p "$pid" -o comm=. Only a dead or non-Vivaldi pid counts as closed.

This review does not gate merge.

@datapointchris datapointchris left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. lib/vivaldi-preferences.sh:22 and :72 — both liveness checks trust a pid without checking which process holds it. The first is vivaldi_running_pid. It reads the pid out of SingletonLock and accepts any live process. The second is vivaldi_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 browsers then shows Pending. 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 left SingletonLock behind in my run (archlinux-3093125 still 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. Running ps -p <old pid> after a reboot, before Vivaldi launches, would settle it.
    I pointed the scratch lock at a live sleep 600 with no Vivaldi running. apply_vivaldi nordic returned at next launch. Preferences stayed on theme-rose-pine until I killed the sleep, and then the waiter installed theme-nordic.
    Chromium's own ProcessSingleton handles a stale lock this way: it treats the lock as live only when the pid is a browser process.
    Fix: in vivaldi_running_pid, after kill -0, add [[ "$(ps -p "$pid" -o comm=)" == *[Vv]ivaldi* ]] || return 1. On Arch the comm is vivaldi-bin. On macOS it is the bundle's Contents/MacOS/Vivaldi path. In vivaldi_waiter_running, require ps -p "$pid" -o args= to contain vivaldi-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

  1. 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 why theme browsers says Pending, 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 datapointchris left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. 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.pid survives a reboot, and only the waiter's own EXIT trap removes it. The check is kill -0 "$pid" on any process. A reused pid makes apply_vivaldi print "at next launch", skip starting a waiter and install nothing. Sandbox run: installing into a profile, skipping a profile with no Preferences, and the stale/live lock checks all behaved correctly. Fix: verify the pid is the waiter, for example by comparing /proc/$pid/cmdline (or ps -p "$pid" -o command=) against vivaldi-install-after-exit.sh, or hold the pid file under $XDG_RUNTIME_DIR so a reboot clears it.

This review does not gate merge.

@datapointchris

Copy link
Copy Markdown
Owner Author

Sonnet arm review — merged

Arm: Sonnet
10 reviewers ran (2 general, test, 7 personas), launched by a pr-review-lead on the author's request. This comment unions the 10 Sonnet-arm reviews and promotes 1 shape. The test reviewer and the simplest-design reviewer found nothing to delete or shrink.

Findings: 4 correctness · 3 design · 2 prose

Correctness

S1. [G1, G2, system-design] lib/vivaldi-preferences.sh:67 — vivaldi_waiter_running trusts a pid file with only kill -0. A waiter killed by SIGKILL, OOM or power loss leaves ~/.cache/theme/vivaldi-waiter.pid behind, and the file survives a reboot. A reused pid makes every later apply start no waiter, print at next launch, and install nothing. Reach: unproven. It needs a killed waiter plus pid reuse, and a reboot with Vivaldi open is the likely path. Fix: check the pid's command line names vivaldi-install-after-exit.sh (/proc/$pid/cmdline or ps -p "$pid" -o command=), or keep the file under $XDG_RUNTIME_DIR.

S2. [failure-paths] lib/vivaldi-preferences.sh:100 — A failed deferred install leaves no trace. The waiter's output goes to /dev/null, and its failure paths return silently: the 15-second TERM deadline, a missing current file, a failed vivaldi_install_theme. The apply already printed at next launch. Reach: Vivaldi is open at apply, then exits and the install fails (an unwritable profile directory, or a Vivaldi that outlasts the deadline at logout). Fix: have the waiter write its outcome to a file under ~/.cache/theme/, and have the next apply print it through apply_warn.

S3. [failure-paths] lib/vivaldi-preferences.sh:67 — One unreadable profile stops the install for every profile after it. Profiles written earlier keep the new theme and the apply reports Vivaldi as skipped. Reach: a user-data dir with two profiles where one Preferences is truncated. Reproduced with Default valid and Profile 1 holding {trunc. Fix: 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 installed.

S4. [contracts] lib/vivaldi-preferences.sh:94 — A dev-mode apply with Vivaldi open installs the production theme. vivaldi_start_waiter passes only -E HOME -E PATH to systemd-run, so the waiter loses THEME_ENV=development and reads the production current file. schedule.sh:270 already forwards THEME_ENV for the same reason. Reach: a development-mode apply while Vivaldi runs, read from code, not run. Fix: add -E THEME_ENV when set, or pass the theme id to the waiter as an argument.

Design and architecture

S5. [G1, contracts, code-architecture, lead] bin/theme:849 — Shape: the set of Vivaldi profiles and their data directory is decided at separate sites. vivaldi_install_theme writes every profile in Local State, and show_browsers reads only Default/Preferences. vivaldi_user_data_dir re-implements the per-OS path that get_browser_profiles_dir (lib/browser-profiles.sh:14) owns. show_browsers also reads vivaldi.themes.current alone while the install selects through that and both vivaldi.theme.schedule.o_s slots. Instances: (a) a user on Profile 1 sees Theme: unknown/none after a successful install; (b) before any apply, Theme: shows current while a dark-mode OS shows the o_s.dark theme; (c) the next Chromium-family browser copies the directory case and profile list again. Reach: any Vivaldi user without a Default directory, or whose schedule follows the OS. Fix: add Vivaldi to get_browser_profiles_dir, expose one profile enumeration and one vivaldi_selected_theme beside vivaldi_install_theme, and have show_browsers call them.

S6. [code-architecture, fleet-patterns] lib/vivaldi-preferences.sh:84 — Detaching a job from the apply's cgroup is decided twice. start_session_daemon (lib/lib.sh:2448; same body in font:lib/arch-apps.sh:105) tries Hyprland's exec dispatcher, then systemd-run --user --scope, then setsid. vivaldi_start_waiter uses a transient systemd-run service with -E HOME -E PATH, then nohup on non-systemd Linux and macOS. The PR body gives no reason for the service over the scope. Reach: the next process an apply must leave running picks a third recipe, and the next cgroup-escape fix lands in one copy only. The allowlisted environment already caused S4. Fix: one detach helper in lib/lib.sh taking a command and arguments, used by both callers, with the Hyprland dispatch kept daemon-only. If the service is deliberate, put the reason in the body and above the function.

S7. [code-architecture] lib/vivaldi-preferences.sh:10 — The new module reaches up into lib.sh. It is sourced from lib/lib.sh:19 but reads THEME_LIB_DIR, THEMES_DIR and CURRENT_THEME_FILE, hardcodes $HOME/.cache/theme (also hardcoded at lib.sh:1020 and lib/generators/background-recolor.sh:86), and builds $THEMES_DIR/$theme/vivaldi.json at line 123 while apply_vivaldi resolves the same artifact through get_library_path. The entry script sources all of lib.sh to call one function. Reach: changing how a theme id resolves to a directory (the dev-mode or installed-copy split) reaches apply_vivaldi and misses the waiter. Fix: pass the resolved JSON path to the waiter from apply_vivaldi, and take the cache dir from one THEME_CACHE_DIR variable.

Prose

S8. [stale-prose] CLAUDE.md:361-365 — Claims about Vivaldi's behavior go stale with a Vivaldi release: "Nothing outside Vivaldi can change an open window's theme", "CSS mods are linked once per window", "--remote-debugging-port is refused on the default profile", and the enterprise-policy claim. Same shape at lib/generators/vivaldi.sh:27 ("derives every shade"), lib/generators/vivaldi.sh:30 ("paints its accent across the whole toolbar row"), lib/vivaldi-preferences.sh:95 and lib/vivaldi-preferences.sh:80 (names no file for AbandonProcessGroup). Reach: a Vivaldi update changes any of these and leaves the sentence false. Rewrite: keep the dead ends as one rule with its cause and a way to re-check, name the file that sets AbandonProcessGroup, and cut the generator comments to what the code does.

S9. [stale-prose] README.md:143, README.md:205, bin/theme:579, bin/theme:603 — "Firefox-based browsers (Zen, Librewolf, Firefox, Thunderbird) and Vivaldi" is a fourth copy of a list the code holds in lib/browser-profiles.sh:3, lib/generators/firefox-based.sh:3 and bin/theme:786. Reach: adding a browser means editing four more sentences nothing checks. Rewrite: "Firefox-based browsers and Vivaldi. theme browsers lists the ones found on this machine."

This review does not gate merge.

0 rule(s) proposed, filed into the fleet inbox as proposals.

@datapointchris datapointchris left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. lib/vivaldi-preferences.sh:28-30 — vivaldi_running_pid decides 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 -n when a laptop changes networks, unless HostName is set. The next apply then reads an open Vivaldi as closed. It writes Preferences under the running browser and prints ✓ vivaldi. Vivaldi overwrites the write on exit, and no waiter exists to redo it.
    • Pid reused. A SIGTERM leaves SingletonLock behind. My scratch Vivaldi left archlinux-3090623 in place after it exited. If another process later takes that pid, apply_vivaldi reads a closed Vivaldi as open and starts a waiter. The inner kill -0 loop then waits on the unrelated process, and a relaunched Vivaldi does not move it. Every later apply finds that waiter, starts no other, and prints at next launch. theme browsers keeps reporting Pending. The theme lands only when the unrelated process exits.

    Reach: unproven. scutil --get HostName on 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 own ProcessSingleton guards 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>/exe on Linux and ps -o comm= -p on macOS. In the waiter, re-run vivaldi_running_pid on every poll instead of kill -0 on a pinned pid.

Prose

  1. 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 stuck Pending in theme browsers has nowhere to find it. Rewrite: add ~/.cache/theme/vivaldi-waiter.pid - The waiter that installs into Vivaldi after it exits to the list. Then change the sentence to "Generated state belongs in the cache directories above and nowhere else."

This review does not gate merge.

@datapointchris

Copy link
Copy Markdown
Owner Author

PR review — merged

Ten 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

  1. [system-design] lib/vivaldi-preferences.sh:123 — The waiter can install the theme the apply is replacing. It installs whatever ~/.local/state/theme/current holds when Vivaldi exits. apply_theme_to_apps writes that file at lib/lib.sh:2255, after the Vivaldi block and every app after it. Reach: Vivaldi is open on theme A, and theme apply B runs, by hand or as a scheduled turn. The apply prints ✓ vivaldi (at next launch) and moves on to the remaining apps. The user quits Vivaldi in those seconds. The waiter reads A, writes A, removes its pid file and exits. Then set_current_theme B runs. No waiter remains, so Vivaldi stays on A until a later apply finds it closed. Found by reading the code. Not run. Fix: write current before apply_theme_to_apps applies any app. Or have the waiter re-read current after its install and install again when it changed.

  2. [failure-paths] lib/vivaldi-preferences.sh:87 — A deferred install that fails leaves no trace. The tick ✓ vivaldi (at next launch) reports that systemd-run or nohup started a process. It does not report the install. On macOS the waiter's errors go to /dev/null. On Linux they go to the journal of a --collect unit. These failures land there: a failed vivaldi_install_theme, the 15-second TERM deadline at :116, and an unreadable current at :123. The pid file is removed either way. So theme browsers drops its Pending line 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. failure-paths made Default/ read-only before the fake browser exited. The apply printed at next launch at rc 0. Preferences still read "Vivaldi1" afterwards, 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". Fix: vivaldi_install_after_exit writes its outcome to a state file under ~/.cache/theme/, holding the theme id and any failure. show_browsers reads it. The next apply_vivaldi reads it too, and reports a failure through apply_warn.

Design and architecture

  1. [lead, G1, G2, failure-paths] lib/vivaldi-preferences.sh — Every liveness check trusts a recorded pid or host without confirming which process holds it. Both checks read a file another process wrote and believe it. kill -0 only says some process holds that number. A stale SingletonLock survives a SIGTERM. G1 and G2 each saw headless Vivaldi leave its lock linked after it exited. A stale waiter pid file survives any death that skips the EXIT trap, and it sits in ~/.cache, which survives a reboot. Three instances follow.

    • vivaldi_running_pid reads a closed Vivaldi as open (:22) [G1, G2]. A stale lock names a pid that an unrelated process now holds. The apply prints at next launch and starts a waiter. The waiter pins its kill -0 loop to that pid at :114. A relaunched Vivaldi does not move it, and the theme lands only when the stranger exits. G1 pointed the lock at a live sleep 600 with no Vivaldi running. apply_vivaldi nordic printed at next launch. Preferences stayed on theme-rose-pine until the sleep was killed.
    • vivaldi_running_pid reads an open Vivaldi as closed (:28-30) [G2, failure-paths]. The lock records the hostname from when Vivaldi launched. macOS changes uname -n with the network unless HostName is set. The apply then writes Preferences under the running browser and prints a plain ✓ vivaldi. Vivaldi overwrites the file on exit, and no waiter exists to redo it. failure-paths set the lock to old-dhcp-name.local-<pid> with that pid alive. The apply returned rc 0, wrote theme-kanagawa into the open profile, and started no waiter.
    • vivaldi_waiter_running keeps a dead waiter alive (:72) [G1, failure-paths]. A live unrelated process with the saved pid counts as the waiter. Every later apply starts no waiter and still prints at next launch. theme browsers shows Pending forever. failure-paths wrote a live sleep's pid into the file. After the fake browser exited, Preferences still read "Vivaldi1".

    Reach: the stale lock and the stale pid file are both proven. Pid reuse is unproven. G1 notes this machine's Vivaldi holds an early-boot pid, and pids restart at every boot. ps -p <old pid> after a reboot, before Vivaldi launches, settles it. The host case needs a Mac without a pinned HostName, and scutil --get HostName settles it. Fix: one check confirms a pid is the expected program. Use ps -p "$pid" -o comm= matching Vivaldi, which is vivaldi-bin on Arch and the bundle's Contents/MacOS/Vivaldi on macOS, or use /proc/<pid>/exe on Linux. vivaldi_running_pid drops the host comparison and uses that check. vivaldi_waiter_running requires ps -p "$pid" -o args= to contain vivaldi-install-after-exit, and removes the pid file when it does not. The waiter re-runs vivaldi_running_pid on each poll instead of kill -0 on a pinned pid. G1 suggests ${XDG_RUNTIME_DIR} for the pid file on Linux, because a reboot empties it.

  2. [code-architecture, contracts] bin/theme:849 — theme browsers reports a pref Vivaldi does not read, from one profile of several. The installer writes every profile in Local State's profile.info_cache. It selects through vivaldi.themes.current and both vivaldi.theme.schedule.o_s slots. show_browsers reads only vivaldi.themes.current, and only from Default/Preferences. Under the PR's own rule, the effective selection is the o_s slots. Reach: a user who runs theme browsers to see what Vivaldi shows. contracts found Default holding current: "Vivaldi4" and o_s: {light: "Vivaldi4", dark: "Dark"}. The report printed Theme: Vivaldi4 while a dark-mode OS shows Dark. The line is right only after this tool has written all three keys. Following Vivaldi's timeline schedule, which the body lists as not done, moves the pref again at both sites. Fix: add vivaldi_selected_theme <dir> beside vivaldi_install_theme. It walks the same profile list, reads the o_s slots when the schedule follows the OS and current otherwise, and show_browsers calls it. code-quality.md § "Ask whatever owns a fact, the function or enum that computes it, never duplicate its logic at a second site".

  3. [code-architecture, fleet-patterns] lib/vivaldi-preferences.sh:82-92 — vivaldi_start_waiter is a second recipe for escaping the apply's cgroup. start_session_daemon at lib/lib.sh:2448 already owns one, and font:lib/arch-apps.sh:105 copies it. It uses Hyprland's dispatcher, then systemd-run --user --scope, then setsid. The waiter uses a transient service with an -E HOME -E PATH allowlist, then nohup on Linux and macOS alike. The body gives no reason for a service over a scope. Reach: the next fix to the escape lands in one recipe only. That fix might be a launchd change, a Linux without systemd, or a variable the waiter starts to read. The allowlist is the concrete hazard: the first XDG_* or THEME_ENV read gets nothing under the service, where a scope inherits it. apply_borders at lib/lib.sh:519-525 uses a bare & disown, and sync_after_action at lib/sync.sh:378-379 backgrounds a push. Each must pick a recipe or write a third. Fix: add one helper in lib.sh that runs a command in systemd-run --user --scope --collect, falling back to setsid on Linux and nohup on macOS. start_session_daemon keeps its Hyprland attempt and falls back to that helper. vivaldi_start_waiter calls the helper. If the service was deliberate, the reason goes in the body and the function's comment.

  4. [code-architecture] lib/vivaldi-install-after-exit.sh:8, lib/vivaldi-preferences.sh:122-124 — The waiter resolves the current theme itself, bypassing the functions that own that. apply_vivaldi resolves the artifact through get_library_path. The waiter reads the state file with cat and builds $THEMES_DIR/$theme/vivaldi.json by hand. It skips get_current_theme and get_library_path. That makes the lower module vivaldi-preferences.sh reach up for lib.sh's CURRENT_THEME_FILE, THEMES_DIR and THEME_LIB_DIR. Reach: any change to how a theme id resolves to its directory, such as the dev-mode or installed-copy split. It reaches apply_vivaldi and misses the waiter. Fix: move the last three lines of vivaldi_install_after_exit into lib.sh as vivaldi_install_current, built on get_current_theme and get_library_path. The entry script calls it. vivaldi-preferences.sh then reads Preferences and processes and nothing above it.

  5. [code-architecture] lib/vivaldi-preferences.sh:10-46, :102-121 — The install-while-closed machinery serves any Chromium browser but is bound to Vivaldi. SingletonLock detection, the Local State profile list, the staged rename of Preferences and the wait-for-exit loop all apply unchanged to Chromium, Chrome and Brave. Only the jq program and the user-data path are Vivaldi's. One pid file admits one waiting browser. Reach: applying chromium.theme. lib/generators/chromium.sh produces it for every theme, bin/theme:858-866 reports it, and the body lists applying it as not done. That browser copies the module, or edits the pid file, entry script, waiter body and profile walk in step. Fix: name the generic half for Chromium profiles. It takes the user-data dir and the jq program as arguments, and the pid file and waiter arguments are keyed by browser.

Prose

  1. [G1, G2] CLAUDE.md:10-22 — "Generated state belongs in those two cache directories and nowhere else." The state list above that sentence does not name ~/.cache/theme/vivaldi-waiter.pid, and the file sits in neither cache directory. A reader debugging a stuck Pending in theme browsers has nowhere to find it. Rewrite: add a row at whatever path finding 3 settles on, reading "pid of the waiter that installs the current theme into Vivaldi once it exits". Change the sentence to "Generated state belongs in the cache directories above and nowhere else."

  2. [stale-prose] bin/theme:579, bin/theme:603, lib/generators/vivaldi.sh:27 — Two sentences write out values the code holds. The first is "Supports Firefox-based browsers (Zen, Librewolf, Firefox, Thunderbird) and Vivaldi." show_browsers owns that list, and both heredocs must change for the next browser. help.md § "Help text never carries a count, version, path or list of what is supported, which the next release makes stale". Rewrite: "Shows each detected browser and whether a theme is installed in it." The second is "Vivaldi derives every shade, hover and contrast text from these five". The jq call holds the count, and a sixth --arg makes it 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."

  3. [stale-prose] lib/vivaldi-preferences.sh:78-81 — "on Linux the waiter is its own transient service" is false on one branch. The if at :86 also requires systemctl --user show-environment to succeed. A Linux box without a systemd user manager takes the nohup branch. Finding 5's helper replaces this comment if it lands. 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."

  4. [stale-prose] lib/generators/vivaldi.sh:31 — "the accent takes the selection shade that firefox-based.sh gives a selected tab" describes a sibling module. It is true at the head, where firefox-based.sh:197 paints the selected tab with UI_SELECTION. Nothing keeps it true when that file changes. 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."

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
@datapointchris
datapointchris merged commit 1503131 into main Oct 21, 2026
1 check passed
@datapointchris
datapointchris deleted the vivaldi-theme branch October 21, 2026 06:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant