Proof Portal

Project overview

Omarchy

ProbeLabs65 findings · 95 requirements

A 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.

Inbox
ReportR-944GitHub issuePublicFixed

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

Decided: Out of scope — decided by agents · verified 3 days ago.

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

Transparent bar keeps the previous theme's text colour: the refresh request is dropped while a sample is in flightrevision 1 · captured 2026-09-28 21:23 UTC via poll

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

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.
   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 (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
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