Proof Portal
Omarchy
ProbeLabs65 findings · 95 requirementsA proof layer — requirements, tests and verified fixes — for two of Omarchy's subsystems: the application menu (launcher scripts, QML model, JSONC config, search and selection) and the lock screen (lock scripts, QML, PAM authentication). Scope is deliberately limited to those components of omacom/omarchy; the rest of the distribution is not covered.
Transparent bar keeps the previous theme's text colour: the refresh request is dropped while a sample is in flight
Reported by 9r0j3c70521 · filed Sep 5, 2026 · open at source
At a glance
- 1 · What is claimed?
- Only the report text below — no validator has restated it as checkable claims yet.
- 2 · What does it affect?
- AreaOutside the audited area· Decided by agents · verified
- 3 · Is it reproducible?
- No validation has been run yet.
- 4 · What remains unclear?
- Not validated yet: no claims, no reproducer.
- 5 · What happens next?
- Decided by agents and verified by a second, independent agent — see the decision.
Original report
System details
Intel N97, Intel UHD Graphics (Alder Lake-N), Omarchy 4.0.2-1 (quattro), Quickshell 0.3.1, Hyprland 0.56.2, ImageMagick 7.1.2-30, single 2560x1440 monitor @ scale 1
What's wrong?
What happens
With bar.transparent = true, switching themes leaves the bar text in the previous theme's colour. Everything else (background, borders, panels, widget accents) flips correctly — only the bar foreground is stale, and it stays stale indefinitely.
Double-clicking the bar to toggle transparency off and on again immediately produces the correct colour, so the value that gets computed is right; the refresh just never happens during the theme switch.
Switching from a light theme to a dark one is where it bites: the old dark foreground lands on the new dark wallpaper and the bar becomes close to unreadable.
Steps to reproduce
omarchy bar transparent trueomarchy theme set rose-pine(light), thenomarchy theme set tokyo-night(dark)- The bar text keeps rose-pine's
#575279on tokyo-night's purple wallpaper - Double-click the bar twice (transparency off, then on) — the colour corrects itself
Evidence
Polling /proc for omarchy-bar-text-color launches during the switch in step 2, with argv and ms offsets:
75 ms omarchy-bar-text-color top 30 #575279 #faf4ed <- rose-pine's palette
95 ms omarchy-bar-text-color top 30 #575279 #faf4ed
...nothing for the remaining 12 s of the traceIt is never launched with tokyo-night's #a9b1d6 / #1a1b26. Toggling transparency afterwards launches it correctly:
210 ms omarchy-bar-text-color top 30 #a9b1d6 #1a1b26
231 ms omarchy-bar-text-color top 30 #a9b1d6 #1a1b26omarchy-bar-text-color takes ~570 ms here (2560x1440 screen, 5120x2880 PNG wallpaper):
563 ms
573 ms
573 msCause
Refresh requests are dropped while a sample is in flight, and nothing re-arms them.
shell/plugins/bar/Bar.qml:830
function refreshTransparentForeground() {
if (!requestedTransparent || transparentForegroundProc.running) return // <-- dropped, never retried
...
}Every trigger goes through scheduleTransparentForegroundRefresh(), which restarts a 120 ms timer (Bar.qml:850). The timer coalesces triggers, but once it has fired and the magick process is running, any later request hits the early return above and is lost. There is no onExited re-run and no pending flag.
A theme switch reliably produces triggers on both sides of that window:
omarchy-theme-setdoesmv "$NEXT_THEME_PATH" "$CURRENT_THEME_PATH". TheFileViewon~/.local/state/omarchy/current(Bar.qml:874) fires immediately and starts the 120 ms timer. At this point the shell has not received the new palette yet, andln -nsf … current/backgroundhas not run either, so the sample is launched with the oldthemeForeground/themeContrastForegroundagainst the old wallpaper. That is the 75 ms / 95 ms pair in the trace.- The palette arrives afterwards over IPC.
Background.qmldefers applying it:applyPendingTheme()runs onstartReveal()(once the incoming wallpaper image is ready) or via the 300 mspendingThemeFallbackTimer. Only then doesColor.loadColors()run,themeForegroundchange, andonThemeForegroundChangedrestart the timer.
That second timer fires while the ~570 ms sample from step 1 is still running, so it takes the early return and is dropped. transparentForeground keeps whatever step 1 computed, which is a colour chosen for the old palette.
Two related things:
- The stdout handler (
Bar.qml:858) accepts the result unconditionally. It cannot tell that the value it is applying was computed for a palette that is no longer current — there is no generation check against thethemeForegroundthe command was built with. - Even a single well-timed run can sample the wrong wallpaper, because
omarchy-theme-setrepointscurrent/backgroundafter the theme IPC, not before.
Toggling transparency recovers because setRequestedTransparency(false) resets transparentForeground = themeForeground (Bar.qml:802) and turning it back on schedules a fresh sample, by which time both the palette and the wallpaper link have settled.
Fix
Two changes, both in Bar.qml:
- Re-arm instead of dropping. When
transparentForegroundProc.running, recordpendingand re-run from the process'sonExited(viaQt.callLater, to keep the relaunch out of the exit handler) — the same shape #8307 uses for itsreadlinkprobe. - Version the result. Record the
themeForegroundthe command was built for insampledFor; in the stdout handler, discard a value whosesampledForno longer matches the current foreground and letonExitedrun the sample the current palette asked for.
function refreshTransparentForeground() {
- if (!requestedTransparent || transparentForegroundProc.running) return
+ if (!requestedTransparent) return
+ // A sample already in flight was started for the palette of the moment it
+ // was launched. Dropping the request outright loses the newer palette for
+ // good: nothing re-arms it, so a theme switch that lands mid-sample leaves
+ // the bar wearing the previous theme's foreground until transparency is
+ // toggled. Remember the request and run it when the sample returns.
+ if (transparentForegroundProc.running) {
+ transparentForegroundProc.pending = true
+ return
+ }
+ transparentForegroundProc.pending = false
+ transparentForegroundProc.sampledFor = colorHex(root.themeForeground)
transparentForegroundProc.command = [
"omarchy-bar-text-color",
root.position,
@@
Process {
id: transparentForegroundProc
+ // Set when a refresh arrived while this sample was running.
+ property bool pending: false
+ // The theme foreground the running command was built for.
+ property string sampledFor: ""
+
stdout: SplitParser {
onRead: function(line) {
var value = String(line || "").trim()
if (!/^#[0-9A-Fa-f]{6}$/.test(value)) return
+ // The palette moved on while this sample ran, so the colour it picked
+ // was weighed against a foreground the bar no longer wears. Drop it and
+ // let onExited run the sample the current palette asked for.
+ if (transparentForegroundProc.sampledFor !== root.colorHex(root.themeForeground)) {
+ transparentForegroundProc.pending = true
+ return
+ }
+
root.foregroundAnimationEnabled = false
root.transparentForeground = value
if (root.requestedTransparent) {
@@
root.restoreForegroundAnimation()
}
}
+
+ onExited: {
+ if (!pending) return
+ pending = false
+ // Qt.callLater keeps the relaunch out of the process's own exit handler.
+ Qt.callLater(function() { root.refreshTransparentForeground() })
+ }
}Verified on the running shell
Same trace as above, patched, rose-pine → tokyo-night:
126 ms omarchy-bar-text-color top 30 #575279 #faf4ed <- old palette, as before
146 ms omarchy-bar-text-color top 30 #575279 #faf4ed
1597 ms omarchy-bar-text-color top 30 #a9b1d6 #1a1b26 <- the deferred re-run
1649 ms omarchy-bar-text-color top 30 #a9b1d6 #1a1b26The bar now reaches the same foreground it used to need a manual transparency toggle to get.
Reverse direction (tokyo-night → catppuccin-latte) re-runs with #4c4f69 #eff1f5 and then goes quiet for the rest of a 14 s trace — no resampling loop.
Rapid switching (tokyo-night → gruvbox → everforest back to back) converges on the last palette and stops:
2439 ms ... #a9b1d6 #1a1b26 (tokyo-night)
5897 ms ... #d4be98 #282828 (gruvbox)
9735 ms ... #d3c6aa #2d353b (everforest, final)qmllint shell/plugins/bar/Bar.qml is clean before and after.
Trade-offs
- The correction is not instant. The deferred re-run lands ~1.6 s into the switch and takes another ~570 ms, so the bar briefly wears the old foreground before settling. Far better than never, and #8307's ~40x cheaper sampling would mostly close that gap.
- Rapid switching runs a few extra samples (gruvbox is sampled four times in the trace above). Clearing
pendingwhen a result is applied would remove them, but it would also drop the case this mechanism exists for — the wallpaper changing while the palette stays put — so the extra sample seemed the better side to err on.
Happy to open a PR with this if the approach looks right.
Relation to #8307
#8307 (still open) would hide this in practice: it gates the refresh on readlink -f current/background actually changing — so the early, pre-palette run in step 1 stops happening — and it cuts sampling cost by ~40x, which shrinks the race window. But it does not touch the early return, so the dropped-request behaviour itself survives that PR, and any trigger that arrives during a sample is still lost.
Environment
- Omarchy
4.0.2-1(Arch package,OMARCHY_PATH=/usr/share/omarchy),versionfile4.0.0.alpha,quattro shell/plugins/bar/Bar.qmlbyte-identical toquattroHEAD- Quickshell 0.3.1 (Arch), Hyprland 0.56.2
- ImageMagick 7.1.2-30 Q16-HDRI
- Single monitor, 2560x1440 @ scale 1;
bar.position = "top",bar.transparent = true, bar size 30 - Themes involved: rose-pine (dawn variant,
#575279/#faf4ed), tokyo-night (#a9b1d6/#1a1b26), catppuccin-latte, gruvbox, everforest
View raw text
### System details
Intel N97, Intel UHD Graphics (Alder Lake-N), Omarchy 4.0.2-1 (`quattro`), Quickshell 0.3.1, Hyprland 0.56.2, ImageMagick 7.1.2-30, single 2560x1440 monitor @ scale 1
### What's wrong?
#### What happens
With `bar.transparent = true`, switching themes leaves the bar text in the **previous** theme's colour. Everything else (background, borders, panels, widget accents) flips correctly — only the bar foreground is stale, and it stays stale indefinitely.
Double-clicking the bar to toggle transparency off and on again immediately produces the correct colour, so the value that gets computed is right; the refresh just never happens during the theme switch.
Switching from a light theme to a dark one is where it bites: the old dark foreground lands on the new dark wallpaper and the bar becomes close to unreadable.
#### Steps to reproduce
1. `omarchy bar transparent true`
2. `omarchy theme set rose-pine` (light), then `omarchy theme set tokyo-night` (dark)
3. The bar text keeps rose-pine's `#575279` on tokyo-night's purple wallpaper
4. Double-click the bar twice (transparency off, then on) — the colour corrects itself
#### Evidence
Polling `/proc` for `omarchy-bar-text-color` launches during the switch in step 2, with argv and ms offsets:
```
75 ms omarchy-bar-text-color top 30 #575279 #faf4ed <- rose-pine's palette
95 ms omarchy-bar-text-color top 30 #575279 #faf4ed
...nothing for the remaining 12 s of the trace
```
It is never launched with tokyo-night's `#a9b1d6` / `#1a1b26`. Toggling transparency afterwards launches it correctly:
```
210 ms omarchy-bar-text-color top 30 #a9b1d6 #1a1b26
231 ms omarchy-bar-text-color top 30 #a9b1d6 #1a1b26
```
`omarchy-bar-text-color` takes ~570 ms here (2560x1440 screen, 5120x2880 PNG wallpaper):
```
563 ms
573 ms
573 ms
```
#### Cause
Refresh requests are dropped while a sample is in flight, and nothing re-arms them.
`shell/plugins/bar/Bar.qml:830`
```qml
function refreshTransparentForeground() {
if (!requestedTransparent || transparentForegroundProc.running) return // <-- dropped, never retried
...
}
```
Every trigger goes through `scheduleTransparentForegroundRefresh()`, which restarts a 120 ms timer (`Bar.qml:850`). The timer coalesces triggers, but once it has fired and the `magick` process is running, any later request hits the early return above and is lost. There is no `onExited` re-run and no pending flag.
A theme switch reliably produces triggers on both sides of that window:
1. `omarchy-theme-set` does `mv "$NEXT_THEME_PATH" "$CURRENT_THEME_PATH"`. The `FileView` on `~/.local/state/omarchy/current` (`Bar.qml:874`) fires immediately and starts the 120 ms timer. At this point the shell has **not** received the new palette yet, and `ln -nsf … current/background` has not run either, so the sample is launched with the **old** `themeForeground` / `themeContrastForeground` against the **old** wallpaper. That is the 75 ms / 95 ms pair in the trace.
2. The palette arrives afterwards over IPC. `Background.qml` defers applying it: `applyPendingTheme()` runs on `startReveal()` (once the incoming wallpaper image is ready) or via the 300 ms `pendingThemeFallbackTimer`. Only then does `Color.loadColors()` run, `themeForeground` change, and `onThemeForegroundChanged` restart the timer.
That second timer fires while the ~570 ms sample from step 1 is still running, so it takes the early return and is dropped. `transparentForeground` keeps whatever step 1 computed, which is a colour chosen for the old palette.
Two related things:
- The stdout handler (`Bar.qml:858`) accepts the result unconditionally. It cannot tell that the value it is applying was computed for a palette that is no longer current — there is no generation check against the `themeForeground` the command was built with.
- Even a single well-timed run can sample the wrong wallpaper, because `omarchy-theme-set` repoints `current/background` *after* the theme IPC, not before.
Toggling transparency recovers because `setRequestedTransparency(false)` resets `transparentForeground = themeForeground` (`Bar.qml:802`) and turning it back on schedules a fresh sample, by which time both the palette and the wallpaper link have settled.
#### Fix
Two changes, both in `Bar.qml`:
- **Re-arm instead of dropping.** When `transparentForegroundProc.running`, record `pending` and re-run from the process's `onExited` (via `Qt.callLater`, to keep the relaunch out of the exit handler) — the same shape #8307 uses for its `readlink` probe.
- **Version the result.** Record the `themeForeground` the command was built for in `sampledFor`; in the stdout handler, discard a value whose `sampledFor` no longer matches the current foreground and let `onExited` run the sample the current palette asked for.
```diff
function refreshTransparentForeground() {
- if (!requestedTransparent || transparentForegroundProc.running) return
+ if (!requestedTransparent) return
+ // A sample already in flight was started for the palette of the moment it
+ // was launched. Dropping the request outright loses the newer palette for
+ // good: nothing re-arms it, so a theme switch that lands mid-sample leaves
+ // the bar wearing the previous theme's foreground until transparency is
+ // toggled. Remember the request and run it when the sample returns.
+ if (transparentForegroundProc.running) {
+ transparentForegroundProc.pending = true
+ return
+ }
+ transparentForegroundProc.pending = false
+ transparentForegroundProc.sampledFor = colorHex(root.themeForeground)
transparentForegroundProc.command = [
"omarchy-bar-text-color",
root.position,
@@
Process {
id: transparentForegroundProc
+ // Set when a refresh arrived while this sample was running.
+ property bool pending: false
+ // The theme foreground the running command was built for.
+ property string sampledFor: ""
+
stdout: SplitParser {
onRead: function(line) {
var value = String(line || "").trim()
if (!/^#[0-9A-Fa-f]{6}$/.test(value)) return
+ // The palette moved on while this sample ran, so the colour it picked
+ // was weighed against a foreground the bar no longer wears. Drop it and
+ // let onExited run the sample the current palette asked for.
+ if (transparentForegroundProc.sampledFor !== root.colorHex(root.themeForeground)) {
+ transparentForegroundProc.pending = true
+ return
+ }
+
root.foregroundAnimationEnabled = false
root.transparentForeground = value
if (root.requestedTransparent) {
@@
root.restoreForegroundAnimation()
}
}
+
+ onExited: {
+ if (!pending) return
+ pending = false
+ // Qt.callLater keeps the relaunch out of the process's own exit handler.
+ Qt.callLater(function() { root.refreshTransparentForeground() })
+ }
}
```
##### Verified on the running shell
Same trace as above, patched, rose-pine → tokyo-night:
```
126 ms omarchy-bar-text-color top 30 #575279 #faf4ed <- old palette, as before
146 ms omarchy-bar-text-color top 30 #575279 #faf4ed
1597 ms omarchy-bar-text-color top 30 #a9b1d6 #1a1b26 <- the deferred re-run
1649 ms omarchy-bar-text-color top 30 #a9b1d6 #1a1b26
```
The bar now reaches the same foreground it used to need a manual transparency toggle to get.
Reverse direction (tokyo-night → catppuccin-latte) re-runs with `#4c4f69 #eff1f5` and then goes quiet for the rest of a 14 s trace — no resampling loop.
Rapid switching (tokyo-night → gruvbox → everforest back to back) converges on the last palette and stops:
```
2439 ms ... #a9b1d6 #1a1b26 (tokyo-night)
5897 ms ... #d4be98 #282828 (gruvbox)
9735 ms ... #d3c6aa #2d353b (everforest, final)
```
`qmllint shell/plugins/bar/Bar.qml` is clean before and after.
##### Trade-offs
- **The correction is not instant.** The deferred re-run lands ~1.6 s into the switch and takes another ~570 ms, so the bar briefly wears the old foreground before settling. Far better than never, and #8307's ~40x cheaper sampling would mostly close that gap.
- **Rapid switching runs a few extra samples** (gruvbox is sampled four times in the trace above). Clearing `pending` when a result is applied would remove them, but it would also drop the case this mechanism exists for — the wallpaper changing while the palette stays put — so the extra sample seemed the better side to err on.
Happy to open a PR with this if the approach looks right.
#### Relation to #8307
[#8307](https://github.com/omacom/omarchy/pull/8307) (still open) would hide this in practice: it gates the refresh on `readlink -f current/background` actually changing — so the early, pre-palette run in step 1 stops happening — and it cuts sampling cost by ~40x, which shrinks the race window. But it does not touch the early return, so the dropped-request behaviour itself survives that PR, and any trigger that arrives during a sample is still lost.
#### Environment
- Omarchy `4.0.2-1` (Arch package, `OMARCHY_PATH=/usr/share/omarchy`), `version` file `4.0.0.alpha`, `quattro`
- `shell/plugins/bar/Bar.qml` byte-identical to `quattro` HEAD
- Quickshell 0.3.1 (Arch), Hyprland 0.56.2
- ImageMagick 7.1.2-30 Q16-HDRI
- Single monitor, 2560x1440 @ scale 1; `bar.position = "top"`, `bar.transparent = true`, bar size 30
- Themes involved: rose-pine (dawn variant, `#575279` / `#faf4ed`), tokyo-night (`#a9b1d6` / `#1a1b26`), catppuccin-latte, gruvbox, everforest
Validation
Not checked yet. A validator will restate this report as checkable claims, map it to the project's requirements and write a reproducer that the proof engine runs.
Linked
- Fixed bypr/9312· confirmed
- Fixed bypr/12446· confirmed
Decision
Decisions are recorded by our staff — or, where this project allows it, by agents: checked by a second, independent agent, or by a single agent where the project's policy says so. Any decision reached so far is shown below.
- Out of scopeDecided by agents · verified · 2026-10-09 10:50 UTC