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-5055GitHub issuePublicReceived

[Omarchy shell] "Launching <app>…" OSD can get permanently stuck

Reported by WilsonCheee · filed Sep 12, 2026Closed upstream on 2026-09-21 by bjarneo via an automated message, as a duplicate of #8242 (R-1462). GitHub close reason: completed.

Received and recorded verbatim. Nothing has been checked yet.

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?
Not yet mapped to any requirement or component.
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?
Our staff decide whether the report is valid. Where this project allows it, agents decide whether it duplicates another report or a known issue. No decision has been recorded yet.

Original report

[Omarchy shell] "Launching <app>…" OSD can get permanently stuckrevision 1 · captured 2026-10-04 12:10 UTC via poll

Summary

The "Launching X…" on-screen display shown when an app is started from the
Omarchy menu can stay on screen forever. It is created with duration: 0
(no auto-hide) and is only ever taken down by
AppLibrary.closeLaunchFeedback(). Under a timing race between the
launchDelay timer that opens it and maybeFinishLaunchFeedback() that closes
it, the osd close call is delivered before the osd show call, the OSD
ends up visible while launchOsdOpen is false, and no timer or code path
remains that can ever close it.

Reproduced on the reporter's machine at least once (OSD still open many
minutes after the app was launched and closed). Root cause below is derived by
reading the shipped sources; see "Reproduction status".

Environment

  • Omarchy 4.0.3-1 (upgraded 2026-09-08 from 4.0.0-1)
  • Quickshell 0.3.1-1 (quickshell -n -p /usr/share/omarchy/shell)
  • Hyprland 0.56.2 (Wayland), SDDM autologin
  • Kernel 7.2.3-arch1-3, Apple MacBookAir5,2
  • Shell uptime when observed: ~1 day 18 h

Steps to reproduce (as observed)

  1. Open the Omarchy menu and launch an app whose first window takes roughly

~2 s to appear (observed with a Chromium web-app / X).
2. The app's window registers at almost exactly the same moment as the 2 s
launchDelay timer fires.
3. The "Launching X…" OSD appears.
4. Close the app (e.g. its window/tab). The OSD does not disappear — not after
15 s, not after minutes.

Expected

The launch OSD should disappear when the launched window appears, when focus
changes, or at the latest 15 s later (the launchTimeout backstop).

Actual

The OSD stays on screen indefinitely. omarchy-shell osd state keeps returning
open long after the 15 s backstop should have fired.

Evidence

  • Screenshot of the stuck OSD (rocket glyph + Launching X…) captured

2026-09-12 16:05: /tmp/pi-clipboard-b7c1b33e-f31b-4fa8-81c9-2b77e7e72c2b.png
- omarchy-shell osd state → open several minutes later; only
omarchy-shell osd close cleared it.

Root cause analysis

All paths are from the installed package (read-only), Omarchy 4.0.3-1.

The OSD is designed to never auto-hide

/usr/share/omarchy/shell/plugins/osd/Osd.qml (lines 57-70, 80-86):

function show(iconName, rawMessage, rawValue, rawMax, rawProgressText, rawDuration) {
  ...
  duration = next.duration
  opened = true
  if (duration > 0) hideTimer.restart()
  else hideTimer.stop()          // duration == 0  -> never auto-hides
}

function close() { opened = false }

The launch feedback deliberately uses duration: 0
(/usr/share/omarchy/shell/services/AppLibrary.qml, line 245):

Quickshell.execDetached(["omarchy-shell", "osd", "show",
  JSON.stringify({ icon: "󱓞", message: root.launchOsdMessage, duration: 0 })])

So the only way to close it is closeLaunchFeedback() (lines 169-177):

function closeLaunchFeedback(serial) {
  if (serial !== root.launchSerial) return
  launchDelay.stop()
  launchTimeout.stop()
  if (root.launchOsdOpen) {
    Quickshell.execDetached(["omarchy-shell", "osd", "close"])
    root.launchOsdOpen = false
  }
}

There are exactly two callers, both passing the current serial:

  • launchDelay timer, 2000 ms (lines 239-247) — sets launchOsdOpen = true

and spawns osd show when no toplevel change was seen;
- launchTimeout timer, 15000 ms (lines 249-253) — the backstop;
plus maybeFinishLaunchFeedback() (lines 179-183), wired to
ToplevelManager.toplevels.onValuesChanged and
ToplevelManager.onActiveToplevelChanged (lines 229-237).

The race

Both osd show and osd close are spawned as **separate asynchronous
processes** via Quickshell.execDetached, so their arrival order at the shell's
IPC is not guaranteed. When the launched window registers at almost the same
instant as the 2 s launchDelay fires:

  1. launchDelay.onTriggered sees no toplevel change yet → sets

launchOsdOpen = true, spawns omarchy-shell osd show … duration: 0.
2. Milliseconds later the toplevel registers →
maybeFinishLaunchFeedback() → closeLaunchFeedback(), which stops both
timers, sets launchOsdOpen = false, and spawns omarchy-shell osd close.
3. If the close process reaches the shell's IPC before the show
process, the OSD is opened after the close, with duration: 0.

Why it can never recover

After step 3:

  • launchOsdOpen === false, so every later closeLaunchFeedback() skips the

osd close because of the if (root.launchOsdOpen) guard;
- both launchDelay and launchTimeout were stopped in the same call, so the
15 s backstop is gone.

This is the only reachable state that leaves the OSD visible with nothing
scheduled to close it, which matches the observed permanent stick. (The OSD can
also be left visible if the osd close IPC is simply lost, which is the same
class of problem: a single fire-and-forget close with no retry or timeout.)

Suggested fix

Pick one (smallest first):

  1. Give the launch OSD a non-zero duration as a backstop, e.g.

duration: 15000 instead of 0. The launch feedback already treats 15 s as
the maximum wait, and closeLaunchFeedback() still closes it earlier. This
alone makes a lost/raced close self-heal. (One-line change in
AppLibrary.qml line 245.)
2. Stop ordering the OSD through two detached CLI processes. Call the OSD
plugin's show()/close() in-process (or through one ordered channel) so
show/close cannot be reordered.
3. Track a generation/serial in the OSD plugin, or have closeLaunchFeedback()
always send osd close (drop the launchOsdOpen guard) and schedule a
delayed re-close, so a close that overtakes a show is corrected.

Option 1 is the pragmatic fix for the bug as reported; option 2 removes the
whole class.

Workaround

omarchy-shell osd close     # dismiss it immediately
omarchy-shell osd state     # open | closed

Diagnostics

Attach the debug log (generated, 902 lines, on the reporter's machine at
/tmp/omarchy-debug-print.txt):

omarchy-debug --no-sudo --print > /tmp/omarchy-debug.log
# or run `omarchy debug` interactively to upload it to logs.omarchy.org

Relevant excerpts: Omarchy Package: omarchy 4.0.3-1, `Desktop Hyprland
v 0.56.2, Machine Apple MacBookAir5,2`.

Reproduction status

Observed once and captured (screenshot + omarchy-shell osd state == open).
Not yet deterministically reproduced, because the trigger is a sub-second race;
the analysis above is from reading the shipped 4.0.3-1 sources and by
elimination of every other code path that could leave the duration-0 OSD
visible. Happy to help validate a fix by re-testing a patched shell build.

View raw text
## Summary

The "Launching *X*…" on-screen display shown when an app is started from the
Omarchy menu can stay on screen **forever**. It is created with `duration: 0`
(no auto-hide) and is only ever taken down by
`AppLibrary.closeLaunchFeedback()`. Under a timing race between the
`launchDelay` timer that opens it and `maybeFinishLaunchFeedback()` that closes
it, the `osd close` call is delivered **before** the `osd show` call, the OSD
ends up visible while `launchOsdOpen` is `false`, and no timer or code path
remains that can ever close it.

Reproduced on the reporter's machine at least once (OSD still `open` many
minutes after the app was launched and closed). Root cause below is derived by
reading the shipped sources; see "Reproduction status".

## Environment

- Omarchy **4.0.3-1** (upgraded 2026-09-08 from 4.0.0-1)
- Quickshell **0.3.1-1** (`quickshell -n -p /usr/share/omarchy/shell`)
- Hyprland **0.56.2** (Wayland), SDDM autologin
- Kernel 7.2.3-arch1-3, Apple MacBookAir5,2
- Shell uptime when observed: ~1 day 18 h

## Steps to reproduce (as observed)

1. Open the Omarchy menu and launch an app whose first window takes roughly
   ~2 s to appear (observed with a Chromium web-app / `X`).
2. The app's window registers at almost exactly the same moment as the 2 s
   `launchDelay` timer fires.
3. The "Launching *X*…" OSD appears.
4. Close the app (e.g. its window/tab). The OSD does not disappear — not after
   15 s, not after minutes.

## Expected

The launch OSD should disappear when the launched window appears, when focus
changes, or at the latest 15 s later (the `launchTimeout` backstop).

## Actual

The OSD stays on screen indefinitely. `omarchy-shell osd state` keeps returning
`open` long after the 15 s backstop should have fired.

### Evidence

- Screenshot of the stuck OSD (rocket glyph + `Launching X…`) captured
  2026-09-12 16:05: `/tmp/pi-clipboard-b7c1b33e-f31b-4fa8-81c9-2b77e7e72c2b.png`
- `omarchy-shell osd state` → `open` several minutes later; only
  `omarchy-shell osd close` cleared it.

## Root cause analysis

All paths are from the installed package (read-only), Omarchy 4.0.3-1.

### The OSD is designed to never auto-hide

`/usr/share/omarchy/shell/plugins/osd/Osd.qml` (lines 57-70, 80-86):

```qml
function show(iconName, rawMessage, rawValue, rawMax, rawProgressText, rawDuration) {
  ...
  duration = next.duration
  opened = true
  if (duration > 0) hideTimer.restart()
  else hideTimer.stop()          // duration == 0  -> never auto-hides
}

function close() { opened = false }
```

The launch feedback deliberately uses `duration: 0`
(`/usr/share/omarchy/shell/services/AppLibrary.qml`, line 245):

```qml
Quickshell.execDetached(["omarchy-shell", "osd", "show",
  JSON.stringify({ icon: "󱓞", message: root.launchOsdMessage, duration: 0 })])
```

So the **only** way to close it is `closeLaunchFeedback()` (lines 169-177):

```qml
function closeLaunchFeedback(serial) {
  if (serial !== root.launchSerial) return
  launchDelay.stop()
  launchTimeout.stop()
  if (root.launchOsdOpen) {
    Quickshell.execDetached(["omarchy-shell", "osd", "close"])
    root.launchOsdOpen = false
  }
}
```

There are exactly two callers, both passing the current serial:

- `launchDelay` timer, 2000 ms (lines 239-247) — sets `launchOsdOpen = true`
  and spawns `osd show` when no toplevel change was seen;
- `launchTimeout` timer, 15000 ms (lines 249-253) — the backstop;
  plus `maybeFinishLaunchFeedback()` (lines 179-183), wired to
  `ToplevelManager.toplevels.onValuesChanged` and
  `ToplevelManager.onActiveToplevelChanged` (lines 229-237).

### The race

Both `osd show` and `osd close` are spawned as **separate asynchronous
processes** via `Quickshell.execDetached`, so their arrival order at the shell's
IPC is not guaranteed. When the launched window registers at almost the same
instant as the 2 s `launchDelay` fires:

1. `launchDelay.onTriggered` sees no toplevel change yet → sets
   `launchOsdOpen = true`, spawns `omarchy-shell osd show … duration: 0`.
2. Milliseconds later the toplevel registers →
   `maybeFinishLaunchFeedback()` → `closeLaunchFeedback()`, which stops both
   timers, sets `launchOsdOpen = false`, and spawns `omarchy-shell osd close`.
3. If the `close` process reaches the shell's IPC **before** the `show`
   process, the OSD is opened *after* the close, with `duration: 0`.

### Why it can never recover

After step 3:

- `launchOsdOpen === false`, so every later `closeLaunchFeedback()` skips the
  `osd close` because of the `if (root.launchOsdOpen)` guard;
- both `launchDelay` and `launchTimeout` were stopped in the same call, so the
  15 s backstop is gone.

This is the *only* reachable state that leaves the OSD visible with nothing
scheduled to close it, which matches the observed permanent stick. (The OSD can
also be left visible if the `osd close` IPC is simply lost, which is the same
class of problem: a single fire-and-forget close with no retry or timeout.)

## Suggested fix

Pick one (smallest first):

1. **Give the launch OSD a non-zero duration as a backstop**, e.g.
   `duration: 15000` instead of `0`. The launch feedback already treats 15 s as
   the maximum wait, and `closeLaunchFeedback()` still closes it earlier. This
   alone makes a lost/raced close self-heal. (One-line change in
   `AppLibrary.qml` line 245.)
2. **Stop ordering the OSD through two detached CLI processes.** Call the OSD
   plugin's `show()`/`close()` in-process (or through one ordered channel) so
   show/close cannot be reordered.
3. Track a generation/serial in the OSD plugin, or have `closeLaunchFeedback()`
   always send `osd close` (drop the `launchOsdOpen` guard) *and* schedule a
   delayed re-close, so a close that overtakes a show is corrected.

Option 1 is the pragmatic fix for the bug as reported; option 2 removes the
whole class.

## Workaround

```bash
omarchy-shell osd close     # dismiss it immediately
omarchy-shell osd state     # open | closed
```

## Diagnostics

Attach the debug log (generated, 902 lines, on the reporter's machine at
`/tmp/omarchy-debug-print.txt`):

```bash
omarchy-debug --no-sudo --print > /tmp/omarchy-debug.log
# or run `omarchy debug` interactively to upload it to logs.omarchy.org
```

Relevant excerpts: `Omarchy Package: omarchy 4.0.3-1`, `Desktop Hyprland
v 0.56.2`, `Machine Apple MacBookAir5,2`.

## Reproduction status

Observed once and captured (screenshot + `omarchy-shell osd state == open`).
Not yet deterministically reproduced, because the trigger is a sub-second race;
the analysis above is from reading the shipped 4.0.3-1 sources and by
elimination of every other code path that could leave the duration-0 OSD
visible. Happy to help validate a fix by re-testing a patched shell build.

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

Not linked to any known issue, other report or change yet.

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.

No decision has been recorded yet.