From c4cb76c43d4b872584f1057afec380de0f157232 Mon Sep 17 00:00:00 2001 From: superduck00 <294879536+superduck00@users.noreply.github.com> Date: Wed, 4 Nov 2026 20:05:21 -0500 Subject: [PATCH] Open the launcher in one frame: keep its surface mapped Each open mapped a new layer surface, and the output's size and scale arrived a few frames later, so the card appeared ~130 ms after the hotkey. The surface now stays mapped once opened; hidden, it is transparent, takes no input and no keyboard, so opening is one commit. It is remapped only to move to another monitor. Hiding already rebuilds the root list, so opening reuses it when the candidates and stores are unchanged instead of searching again. Open to visible: ~130 ms -> 5-7 ms plus a frame. The smoke test now times the keyboard handoff, since a mapped layer sends no open/close events. Co-Authored-By: Claude Opus 5.5 --- tests/smoke.sh | 26 +++++++++-------------- ui/LauncherWindow.qml | 49 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 56 insertions(+), 19 deletions(-) diff --git a/tests/smoke.sh b/tests/smoke.sh index 310c0fb..2a6f69f 100755 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -24,28 +24,22 @@ search zzzz-no-match | grep -q '"id":"fallback-commands"' && pass "fallback sect count=$($cli call system.info | python3 -I -c 'import json,sys; print(json.load(sys.stdin)["data"]["candidates"])') (( count > 300 )) && pass "$count candidates (apps + Omarchy menu)" || bad "only $count candidates" -echo "toggle latency (show -> layer mapped, hide -> unmapped; budget 100 ms)" -sock="$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock" -events=$(mktemp) -socat -u "UNIX-CONNECT:$sock" - 2>/dev/null | while IFS= read -r line; do - case $line in *layer\>\>omacast*) echo "$(date +%s%N) ${line%%>>*}" >>"$events" ;; esac -done & -watcher=$! -sleep 0.3 -$cli hide +# The surface stays mapped between opens, so time the commit that takes or +# gives back the keyboard (Hyprland sends no event for it), polled over IPC. +echo "toggle latency (show -> keyboard taken, hide -> given back; budget 100 ms)" +ki() { hyprctl repl "for _, l in ipairs(hl.get_layers()) do if l.namespace == 'omacast' then return l.keyboard_interactivity end end return 0" 2>/dev/null; } +until_ki() { local deadline=$(( $(date +%s%N) + 1000000000 )); until [[ $(ki) == "$1" ]]; do (( $(date +%s%N) < deadline )) || return 1; done; } +$cli hide; sleep 0.3 worst=0 for i in 1 2 3 4 5; do - t0=$(date +%s%N); $cli show; sleep 0.3 - t1=$(date +%s%N); $cli hide; sleep 0.3 - open=$(grep openlayer "$events" | sed -n "${i}p" | cut -d' ' -f1) - close=$(grep closelayer "$events" | sed -n "${i}p" | cut -d' ' -f1) - if [[ -z $open || -z $close ]]; then bad "run $i: no layer event seen"; continue; fi - show_ms=$(( (open - t0) / 1000000 )); hide_ms=$(( (close - t1) / 1000000 )) + t0=$(date +%s%N); $cli show; until_ki 1 || { bad "run $i: launcher never took the keyboard"; continue; } + show_ms=$(( ($(date +%s%N) - t0) / 1000000 )); sleep 0.3 + t1=$(date +%s%N); $cli hide; until_ki 0 || { bad "run $i: launcher kept the keyboard"; continue; } + hide_ms=$(( ($(date +%s%N) - t1) / 1000000 )); sleep 0.3 printf ' run %d: show %3d ms, hide %3d ms\n' "$i" "$show_ms" "$hide_ms" (( show_ms > worst )) && worst=$show_ms (( hide_ms > worst )) && worst=$hide_ms done -kill "$watcher" 2>/dev/null; pkill -f "socat -u UNIX-CONNECT:$sock" 2>/dev/null; rm -f "$events" (( worst > 0 && worst < 100 )) && pass "worst toggle ${worst} ms" || bad "worst toggle ${worst} ms" exit $fail diff --git a/ui/LauncherWindow.qml b/ui/LauncherWindow.qml index 618a8ad..8a03702 100644 --- a/ui/LauncherWindow.qml +++ b/ui/LauncherWindow.qml @@ -1,5 +1,6 @@ import QtQuick import Quickshell +import Quickshell.Hyprland import Quickshell.Wayland import qs.Commons import qs.Commons as Commons @@ -33,13 +34,30 @@ PanelWindow { signal dismissRequested() - visible: window.shown + // The surface stays mapped once opened: hidden, it is fully transparent, + // takes no input and no keyboard. Mapping a new surface costs ~130 ms (the + // output's size and scale arrive a few frames late); flipping these is one + // commit. It is remapped only to move to another monitor. + property bool mapped: false + visible: window.mapped + screen: window.pinnedScreen + property var pinnedScreen: null + function focusedScreen() { + var monitor = Hyprland.focusedMonitor + var screens = Quickshell.screens + for (var i = 0; i < screens.length; i++) { + if (monitor && screens[i].name === monitor.name) return screens[i] + } + return window.pinnedScreen || (screens.length ? screens[0] : null) + } + mask: window.shown ? null : noInput + Region { id: noInput } anchors { top: true; bottom: true; left: true; right: true } color: "transparent" exclusionMode: ExclusionMode.Ignore WlrLayershell.namespace: "omacast" WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + WlrLayershell.keyboardFocus: window.shown ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None readonly property var general: { window.backend.storeRevision @@ -126,11 +144,26 @@ PanelWindow { if (typeof payload.query === "string") window.query = payload.query window.closePopups() window.historyIndex = -1 - window.rebuild(true) + // Hiding pops to root and rebuilds, so an unchanged root list is reused + // and the card shows on the next frame instead of after a search. + if (typeof payload.query === "string" || window.rootKey() !== window.builtRootKey) window.rebuild(true) + else window.select(window.nextSelectable(-1, 1), 0) Qt.callLater(function() { keyCatcher.forceActiveFocus() }) } + // Hidden, the mapped surface draws nothing. + Binding { + target: window.contentItem + property: "opacity" + value: window.shown ? 1 : 0 + } + onShownChanged: { + if (window.shown) { + var target = window.focusedScreen() + if (target !== window.pinnedScreen) window.pinnedScreen = target + window.mapped = true + } if (window.shown) return window.searchSerial++ window.backend.cancelSearch() @@ -172,8 +205,17 @@ PanelWindow { } + // What the root list depends on; "" when not at an empty root. + property string builtRootKey: "" + function rootKey() { + return window.atRoot && window.query === "" && window.rootFilter === "all" + ? window.backend.candidatesRevision + ":" + window.backend.storeRevision : "" + } + function rebuild(resetSelection, restoreIndex) { var serial = ++window.searchSerial + window.builtRootKey = "" + var key = window.rootKey() window.backend.cancelSearch() pointerGate.reset() var keepId = !resetSelection && window.selectedRow ? window.selectedRow.itemId : "" @@ -185,6 +227,7 @@ PanelWindow { if (serial !== window.searchSerial || revision !== window.backend.candidatesRevision) return window.applySearch(result, keepId, restoreIndex) window.searchPending = false + if (result.ok) window.builtRootKey = key }) }