Make the theme picker open instantly (#13403)

* Run menu summon actions in-process

A menu action that only summons another shell plugin spawned bash and a
qs ipc client to ask this same shell to do it, about 60ms of the path.
Call shell.summon directly instead, and fall back to bash when the call
is refused or the action is anything more than a bare summon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Open the theme picker from rows held in the shell

Opening the theme picker ran omarchy-theme-switcher to rebuild its index
and then made a second IPC call, about 170ms before the picker mapped.
The picker now holds the theme rows itself, opens from them at once, and
refreshes them behind the open via omarchy-theme-switcher --print-rows.
It applies the chosen theme with omarchy-theme-set directly, so
omarchy-theme-set no longer preloads the picker.

From the keybinding to the overlay mapped drops from ~245ms to ~83ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep the image picker surface mapped between opens

Each open mapped a fresh surface, which rendered its first frames before
Hyprland sent its fractional scale: the pixel ratio stepped 2, 1, then
1.6, so the picker flashed blurry for ~130ms and re-uploaded every
thumbnail texture. Keep the surface and park it transparent and
input-less on the bottom layer while closed, since anything on the
overlay layer blocks direct scanout for fullscreen apps. It follows the
focused monitor on each open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier HanssonandClaude Opus 5.5 authored and GitHub committed 2026-09-27 15:10:49 +02:00
1 parent e558224acf
commit 24f1243120
14 files changed
+232 -27

No files matched your search

+3 -2
View File
@@ -7,8 +7,9 @@ const fs = require('fs')
const backgroundQml = fs.readFileSync(path.join(root, 'shell/plugins/background/Background.qml'), 'utf8')
assert(
/theme=\$\(omarchy-theme-switcher\); \[\[ -n \$theme \]\] && omarchy-theme-set \\"\$theme\\" >\/dev\/null 2>&1 &/.test(backgroundQml),
'background theme switcher starts theme application asynchronously after selection'
/function openThemeSwitcher\(\) \{[\s\S]*if \(!root\.shell \|\| !root\.shell\.summon\("omarchy\.image-picker", payload\)\)\s*Util\.execArgv\(\["omarchy-shell", "shell", "summon", "omarchy\.image-picker", payload\]\)/.test(backgroundQml) &&
!backgroundQml.includes('omarchy-theme-switcher'),
'background opens the in-shell theme picker instead of spawning the switcher script'
)
assert(
+32
View File
@@ -47,6 +47,38 @@ assert(
/function preloadRows[\s\S]*if \(opened \|\| requestActive\) return/.test(imagePickerQml),
'image picker ignores cache preloads while a request is visible'
)
assert(
/if \(args\.source === "themes"\) \{\s*openThemes\(\)/.test(imagePickerQml) &&
/function openThemes\(\) \{\s*if \(themeRows\) \{\s*openThemeRows\(\)[\s\S]*refreshThemeRows\(\)/.test(imagePickerQml),
'image picker opens themes from held rows before refreshing them'
)
assert(
/command: \[root\.omarchyPath \+ "\/bin\/omarchy-theme-switcher", "--print-rows"\]/.test(imagePickerQml),
'image picker refreshes theme rows from the theme switcher'
)
assert(
/if \(themeMode\) \{[\s\S]*Util\.execArgv\(\["omarchy-theme-set", nameForPath\(path\)\]\)/.test(imagePickerQml),
'image picker applies a chosen theme itself'
)
assert(
/function cancel\(\) \{\s*themeOpenPending = false/.test(imagePickerQml) &&
/function closeSelector\(nextDoneFile\) \{\s*requestSerial \+= 1\s*themeOpenPending = false/.test(imagePickerQml),
'image picker drops a pending theme open once dismissed'
)
assert(
/function openSelector[\s\S]*?themeMode = false/.test(imagePickerQml),
'image picker leaves theme mode when another caller opens it'
)
assert(
/PanelWindow \{[\s\S]*?visible: true[\s\S]*?mask: root\.opened \? null : closedMask[\s\S]*?WlrLayershell\.layer: root\.opened \? WlrLayer\.Overlay : WlrLayer\.Bottom/.test(imagePickerQml) &&
/Region \{ id: closedMask \}/.test(imagePickerQml),
'image picker keeps its surface mapped, parked input-less below windows while closed'
)
assert(
/function openSelector[\s\S]*?targetScreen = focusedScreen\(\) \|\| targetScreen/.test(imagePickerQml) &&
/screen: root\.targetScreen/.test(imagePickerQml),
'image picker follows the focused monitor on each open'
)
assert(
/source: item\.sourceActivated && item\.thumbnailPath \? Util\.fileUrl\(item\.thumbnailPath\) : ""[\s\S]*asynchronous: false/.test(imagePickerQml),
'image picker loads activated thumbnails synchronously to avoid carousel flicker'
+12
View File
@@ -139,3 +139,15 @@ PATH="$stub_bin:$PATH" XDG_CACHE_HOME="$cache_home" VIPSTHUMBNAIL_CALLS_FILE="$t
(( $(wc -l <"$tmp/calls") == 6 )) || fail "image menu releases thumbnail locks after generation"
pass "image menu owns locks for exactly one generator lifetime"
rm -rf "$cache_home"
mkdir -p "$cache_home"
rows=$(PATH="$stub_bin:$PATH" XDG_CACHE_HOME="$cache_home" \
"$ROOT/bin/omarchy-menu-images" --print-rows "$images")
(( $(wc -l <<<"$rows") == 3 )) || fail "image menu prints one row per image"
while IFS=$'\t' read -r row_image row_thumbnail; do
[[ $row_image == "$images"/* && -f $row_thumbnail ]] ||
fail "image menu prints each image with its generated thumbnail"
done <<<"$rows"
pass "image menu prints its rows for the shell to hold"
+18
View File
@@ -10,6 +10,24 @@ const menu = requireFromRoot('shell/plugins/menu/MenuModel.js')
const menuQml = fs.readFileSync(path.join(root, 'shell/plugins/menu/Menu.qml'), 'utf8')
const defaultMenuJsonc = fs.readFileSync(path.join(root, 'default/omarchy/omarchy-menu.jsonc'), 'utf8')
assertDeepEqual(
menu.summonAction("omarchy-shell shell summon omarchy.speedtest"),
{ id: 'omarchy.speedtest', payload: '{}' },
'menu runs a bare summon action in-process'
)
assertDeepEqual(
menu.summonAction(`omarchy-shell shell summon omarchy.image-picker '{"source":"themes"}'`),
{ id: 'omarchy.image-picker', payload: '{"source":"themes"}' },
'menu keeps a single-quoted summon payload'
)
assertEqual(menu.summonAction("omarchy-shell shell summon omarchy.speedtest && echo done"), null, 'menu leaves compound summon commands to bash')
assertEqual(menu.summonAction(`omarchy-shell shell summon omarchy.x "$(id)"`), null, 'menu leaves shell-expanded payloads to bash')
assertEqual(menu.summonAction("omarchy-theme-set nord"), null, 'menu leaves ordinary actions to bash')
assert(
/var summon = MenuModel\.summonAction\(command\)\s*if \(summon && root\.shell && root\.shell\.summon\(summon\.id, summon\.payload\)\) return\s*Util\.execDetached\(command\)/.test(menuQml),
'menu falls back to bash when an in-process summon is refused'
)
const parsed = menu.parseMenuJsonc(`
{
// comment