Sticky Backgrounds
An Omarchy shell plugin that remembers the wallpaper you last chose for each theme, and puts it back when you return to that theme.
Out of the box, switching away from a theme and back always lands on that theme's first wallpaper, whatever you had picked. This is basecamp/omarchy#7668.
Install
omarchy plugin add https://github.com/whiskeytuesday/omarchy-sticky-backgrounds.git --enable
Remove it with omarchy plugin disable whiskeytuesday.sticky-backgrounds, which restores
the built-in renderer, or omarchy plugin remove whiskeytuesday.sticky-backgrounds.
Requirements and dependencies
Omarchy Quattro, which supplies everything this uses: omarchy-shell
(Quickshell) and the stock omarchy.background plugin it wraps out of
$OMARCHY_PATH. The helper script calls only bash, mkdir, printf, cat
and ln, all from coreutils. No third-party dependencies, no network access,
no privileged operations.
Enabling this plugin writes omarchy.background into disabledPlugins in
~/.config/omarchy/shell.json. That is Omarchy's own clonedFrom mechanism
doing it, not this plugin editing your config behind your back, and
omarchy plugin disable reverses it and restores the built-in renderer.
How it works
The plugin does not fork Omarchy's background renderer. Its manifest declares:
"omarchy": { "clonedFrom": "omarchy.background" }
which makes the shell disable the built-in background plugin on enable and
restore it on disable, and route calls addressed to omarchy.background here.
The plugin then loads the stock Background.qml out of $OMARCHY_PATH as a
child object and forwards every IPC call to it, so all of the rendering — the
reveal animation, the per-monitor Variants, the double-click handlers — stays
upstream's and keeps working across an omarchy update.
Quickshell registers IPC targets first-come-first-served. The wrapper's handler
is declarative and registers during its own completion, before the child is
created in Component.onCompleted, so the wrapper's handler wins and the
child's identical handler is inert. Expect one benign warning in the log:
QML IpcHandler at .../background/Background.qml[131:3]: Handler was registered
but will not be used because another handler is registered for target background
No flash
omarchy-theme-set writes the incoming theme into theme.name before it
sends the themeTransition IPC call, and rewrites the background symlink
after. Intercepting that call means the substitution happens before a single
frame is drawn — the wrong wallpaper is never rendered, not merely covered up.
What gets recorded
Only deliberate selections. Every user-made choice — the background switcher,
omarchy theme bg next, omarchy theme bg set — reaches the shell through
omarchy-theme-bg-set, which sends the background set IPC call. That is the
only writer of the symlink besides omarchy-theme-set itself, so it is the only
place a selection needs recording. A theme you have never picked a wallpaper for
keeps stock behavior.
State lives in ~/.local/state/omarchy/sticky-backgrounds/<theme-slug>, one
path per file.
Resolution order
- The remembered path, if it still exists. This is the case essentially always:
a theme's memory is only ever read back while that theme is staged, and
current/theme/backgrounds/is rebuilt with identical filenames on every switch, so the recorded path is alive exactly when it is consulted. It also covers user wallpapers under~/.config/omarchy/backgrounds/<theme>/and wallpapers chosen from outside any theme directory. - The same basename inside that theme's background directories. A belt-and-braces fallback for a wallpaper that moved between directories but kept its name. It does not rescue a theme update that renames files, since the basename changes too.
- Nothing — Omarchy's own choice stands.
Known wart
omarchy-theme-set rewrites the background symlink to its pick as soon as the
IPC call returns, so the plugin cannot win that write; it repairs the symlink on
a 400 ms timer instead. The symlink has to end up correct because
omarchy theme bg next derives its cycle position from it and the lock screen
reads it for the blurred wallpaper. Losing the race is cosmetic and
self-correcting — the rendered wallpaper is right either way, and the next
selection rewrites the link — but it is a race, and it is the main reason this
belongs upstream rather than in a plugin.
Testing
./test/run exercises the resolution rules directly, with no Quickshell and no
live desktop:
$ ./test/run
resolution:
ok unknown theme resolves to nothing
ok existing path is returned verbatim
ok vanished staged path resolves to nothing while unstaged
ok vanished staged path re-resolves by basename once restaged
ok basename fallback prefers the user dir
ok external path survives
safety:
ok traversal theme name writes nothing
ok traversal theme name resolves to nothing
ok empty theme name resolves to nothing
filenames:
ok glob metacharacters in filename
ok spaces in path
Status — this plugin expects to be made obsolete
It is a deliberate stopgap. The correct fix is in omarchy-theme-set itself,
where the wallpaper is chosen before any of this machinery runs, and
basecamp/omarchy#7718 proposes
exactly that. If that PR merges, uninstall this plugin.
Upstream's version is strictly better: it is flash-free by construction rather than by interception, and it needs no symlink repair because the symlink is written from the corrected choice. See the wart above.
The two do not corrupt each other in the meantime. State lives in
~/.local/state/omarchy/sticky-backgrounds/, deliberately not the
theme-backgrounds/ directory that PR introduces. And if both were ever active
at once they would agree rather than fight: omarchy-theme-bg-set resolves the
path with realpath before sending it, that PR records it with readlink -f,
so both remember the same file. choose_theme_background would then pass the
remembered wallpaper as finalPath, this plugin would find its own record
identical, and it would forward the call unchanged without substituting or
repairing anything.
That is precisely why it should still be removed: it degrades to a pass-through
that keeps the built-in renderer disabled for no benefit, and any upstream
change to Background.qml's function signatures would then break forwarding for
nothing gained.
License
MIT