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.
Menu cursor jumps to top of Apps submenu ~1s after navigation, with flicker from continuous app-list rebuilds
Reported by mightywomble · filed Sep 16, 2026Closed upstream on 2026-09-16 by its own reporter mightywomble, as not planned. Closed by its own reporter — not by itself evidence of a fix.
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
What happened
When navigating the Omarchy menu's Apps submenu (Super+Space → Enter into Apps → Up/Down to move the cursor), the selection snaps back to the top of the list approximately 1 second after the last key press. The list also flickers visibly while open.
What was expected
The cursor should stay where the user placed it. The list should not rebuild/flicker unless the application set actually changed.
Steps to reproduce
- Open the menu: Super+Space
- Enter the Apps submenu: press Enter
- Move cursor to the bottom: press Up (wraps to last item)
- Wait 1-2 seconds without touching anything
- Cursor jumps back to the top of the list on its own
Root cause
Two issues, both in the Omarchy shell menu plugin (/usr/share/omarchy/shell/plugins/menu/Menu.qml):
1. `settleCursor()` doesn't clamp `selectedIndex` when the list shrinks
DesktopEntries.applications.onValuesChanged fires in continuous bursts (8+ events every ~2 seconds) even when no .desktop files have actually changed (confirmed via inotifywait — only read events, zero modify/create/delete events). Each burst triggers appLibrary.appsChanged → mergeAppRows() → rebuildDisplay() → settleCursor().
During these rebuilds the app count oscillates (observed 22–25 apps across rebuilds). When the count drops below the current selectedIndex, nextSelectable(from, 1) applies modulo arithmetic: ((selectedIndex % count) + count) % count wraps an out-of-bounds index back to 0, snapping the cursor to the top.
2. No debounce or change-detection on `onAppsChanged`
Each of the 8+ spurious onValuesChanged events per burst calls mergeAppRows() → rebuildDisplay(), which does displayModel.clear() then re-appends every row. This causes visible flicker even when the app set is identical.
Fix (applied locally via cloned plugin)
Three changes in the menu plugin:
- Clamp `selectedIndex` in `settleCursor()` — before calling
nextSelectable, clamp todisplayModel.count - 1so an out-of-bounds index doesn't wrap to 0:
function settleCursor() {
var from = Math.min(root.selectedIndex, displayModel.count - 1)
if (from < 0) from = 0
var target = root.nextSelectable(from, 1)
root.selectedIndex = target >= 0 ? target : 0
root.cursorActive = target >= 0
}- Debounce `onAppsChanged` — coalesce the burst into a single rebuild with a 150ms Timer:
Connections {
target: root.appLibrary
function onAppsChanged() {
appsChangedDebounce.restart()
}
}
Timer {
id: appsChangedDebounce
interval: 150
onTriggered: {
if (root.providersLoaded["apps"]) root.mergeAppRows()
}
}- Skip rebuild when app set is unchanged — compute a signature of app IDs and skip
mergeAppRows()/rebuildDisplay()when it matches the last signature, eliminating flicker from spurious rebuilds:
function mergeAppRows() {
if (!root.appLibrary) return
var rows = root.appLibrary.sortedEntries("")
var sig = ""
for (var s = 0; s < rows.length; s++) {
sig += String((rows[s].entry && rows[s].entry.id) || "") + "\n"
}
if (sig === root.lastAppSignature && root.opened) return
root.lastAppSignature = sig
// ... rest of mergeAppRows unchanged
}System details
- Omarchy: 4.0.3rc4-1
- Machine: MacBookPro18,4 (Apple M1 Pro, J314c)
- Kernel: 7.1.13-3-1-ARCH (linux-asahi)
- Hyprland: 0.56.2
- GPU: agx-t6001 (asahi driver)
- OS: Arch Linux ARM (Asahi)
Capture
Screen recording attached showing the cursor jumping to the top after navigation.
View raw text
## What happened
When navigating the Omarchy menu's Apps submenu (Super+Space → Enter into Apps → Up/Down to move the cursor), the selection snaps back to the top of the list approximately 1 second after the last key press. The list also flickers visibly while open.
## What was expected
The cursor should stay where the user placed it. The list should not rebuild/flicker unless the application set actually changed.
## Steps to reproduce
1. Open the menu: Super+Space
2. Enter the Apps submenu: press Enter
3. Move cursor to the bottom: press Up (wraps to last item)
4. Wait 1-2 seconds without touching anything
5. Cursor jumps back to the top of the list on its own
## Root cause
Two issues, both in the Omarchy shell menu plugin (`/usr/share/omarchy/shell/plugins/menu/Menu.qml`):
**1. `settleCursor()` doesn't clamp `selectedIndex` when the list shrinks**
`DesktopEntries.applications.onValuesChanged` fires in continuous bursts (8+ events every ~2 seconds) even when no `.desktop` files have actually changed (confirmed via `inotifywait` — only read events, zero modify/create/delete events). Each burst triggers `appLibrary.appsChanged` → `mergeAppRows()` → `rebuildDisplay()` → `settleCursor()`.
During these rebuilds the app count oscillates (observed 22–25 apps across rebuilds). When the count drops below the current `selectedIndex`, `nextSelectable(from, 1)` applies modulo arithmetic: `((selectedIndex % count) + count) % count` wraps an out-of-bounds index back to 0, snapping the cursor to the top.
**2. No debounce or change-detection on `onAppsChanged`**
Each of the 8+ spurious `onValuesChanged` events per burst calls `mergeAppRows()` → `rebuildDisplay()`, which does `displayModel.clear()` then re-appends every row. This causes visible flicker even when the app set is identical.
## Fix (applied locally via cloned plugin)
Three changes in the menu plugin:
1. **Clamp `selectedIndex` in `settleCursor()`** — before calling `nextSelectable`, clamp to `displayModel.count - 1` so an out-of-bounds index doesn't wrap to 0:
```qml
function settleCursor() {
var from = Math.min(root.selectedIndex, displayModel.count - 1)
if (from < 0) from = 0
var target = root.nextSelectable(from, 1)
root.selectedIndex = target >= 0 ? target : 0
root.cursorActive = target >= 0
}
```
2. **Debounce `onAppsChanged`** — coalesce the burst into a single rebuild with a 150ms Timer:
```qml
Connections {
target: root.appLibrary
function onAppsChanged() {
appsChangedDebounce.restart()
}
}
Timer {
id: appsChangedDebounce
interval: 150
onTriggered: {
if (root.providersLoaded["apps"]) root.mergeAppRows()
}
}
```
3. **Skip rebuild when app set is unchanged** — compute a signature of app IDs and skip `mergeAppRows()`/`rebuildDisplay()` when it matches the last signature, eliminating flicker from spurious rebuilds:
```qml
function mergeAppRows() {
if (!root.appLibrary) return
var rows = root.appLibrary.sortedEntries("")
var sig = ""
for (var s = 0; s < rows.length; s++) {
sig += String((rows[s].entry && rows[s].entry.id) || "") + "\n"
}
if (sig === root.lastAppSignature && root.opened) return
root.lastAppSignature = sig
// ... rest of mergeAppRows unchanged
}
```
## System details
- **Omarchy:** 4.0.3rc4-1
- **Machine:** MacBookPro18,4 (Apple M1 Pro, J314c)
- **Kernel:** 7.1.13-3-1-ARCH (linux-asahi)
- **Hyprland:** 0.56.2
- **GPU:** agx-t6001 (asahi driver)
- **OS:** Arch Linux ARM (Asahi)
## Capture
Screen recording attached showing the cursor jumping to the top after navigation.
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
- Related PRpr/7413· 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.
No decision has been recorded yet.