Skip to content

Keep background refreshes from scrolling an open select menu - #7413

Open
melonamin wants to merge 1 commit into
basecamp:quattrofrom
melonamin:keep-menu-scroll-position
Open

Keep background refreshes from scrolling an open select menu#7413
melonamin wants to merge 1 commit into
basecamp:quattrofrom
melonamin:keep-menu-scroll-position

Conversation

@melonamin

Copy link
Copy Markdown

Leave the Shortcuts menu (Super+K) open for a few seconds and it snaps back to the top while you're reading. Anything that wakes the desktop-entry watcher triggers it — mise stamps ~/.local/share on every shell activation, so on a machine with mise the menu resets every few seconds.

The chain: DesktopEntries rescans → AppLibrary fires appsChanged → the menu plugin's mergeAppRows() ends with rebuildDisplay() on the open menu (armed once the apps provider has loaded during the shell's lifetime). In dmenu mode the rows come solely from dmenuOptions, so the rebuild clears and re-appends an identical model, resetting the ListView's scroll; revealCursor() then scrolls back to the cursor, which wheel scrolling never moved off row 0. Provider exits, guard batches, and menu source reloads hit the same path.

Fix: skip the display rebuild in the four async completion paths when a dmenu is active. Menu mode is unchanged — those rebuilds still run there, where rows genuinely depend on the refreshed data.

Testing:

  • test/shell.d/menu-test.sh now asserts all four root.opened-guarded rebuildDisplay() sites carry !root.dmenuActive; ./test/shell passes.
  • Reproduced and verified in a QEMU VM via omarchy-iso: open the Shortcuts menu, wheel-scroll down, drop a new .desktop file to force a rescan. Stock 4.0.0 and pre-fix HEAD snap back to the top (after-trigger frame pixel-identical to the opening frame); with this change the scrolled position holds (pixel-identical to the scrolled frame).

Desktop-entry rescans (e.g. mise touching ~/.local/share on every shell
activation), provider exits, guard batches, and menu source reloads all
called rebuildDisplay() on the open menu. In dmenu mode the rows come
solely from dmenuOptions, so the rebuild only cleared and re-appended an
identical model — resetting the ListView scroll to the cursor row at the
top while the user was reading. Skip the rebuild when a dmenu is active.
Copilot AI balanced review requested due to automatic review settings August 18, 2026 18:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Prevents background refreshes from rebuilding an actively open dmenu (avoiding scroll position resets) and adds a regression test to enforce the new guard.

Changes:

  • Gate root.rebuildDisplay() behind !root.dmenuActive during background refresh paths.
  • Simplify provider reload logic by removing the inner if (!root.dmenuActive) block (now implied by the new outer condition).
  • Add a test asserting background rebuild sites include the !root.dmenuActive guard.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
test/shell.d/menu-test.sh Adds assertions to ensure background refresh rebuilds are guarded to avoid touching an open dmenu.
shell/plugins/menu/Menu.qml Adds !root.dmenuActive guards to prevent rebuildDisplay during background refreshes while dmenu is active.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread test/shell.d/menu-test.sh
Comment on lines +589 to +592
assertEqual(
backgroundRebuilds.length,
4,
'menu rebuilds the open display from app, provider, guard, and source refreshes'
Comment on lines +258 to 262
if (root.opened && !root.dmenuActive) {
root.rebuildDisplay()
if (!root.dmenuActive) {
if (root.filterText.trim()) root.loadProvidersForSearch()
else root.loadProviderForMenu(root.activeMenu)
}
if (root.filterText.trim()) root.loadProvidersForSearch()
else root.loadProviderForMenu(root.activeMenu)
}
root.items = merged.items
root.itemOrder = merged.itemOrder
if (root.opened) root.rebuildDisplay()
if (root.opened && !root.dmenuActive) root.rebuildDisplay()
root.items = merged.items
root.itemOrder = merged.itemOrder
if (root.opened) root.rebuildDisplay()
if (root.opened && !root.dmenuActive) root.rebuildDisplay()
root.checkedResults = nextChecked
root.disabledResults = nextDisabled
if (root.opened) root.rebuildDisplay()
if (root.opened && !root.dmenuActive) root.rebuildDisplay()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants