Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 10 additions & 16 deletions tests/smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
49 changes: 46 additions & 3 deletions ui/LauncherWindow.qml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import qs.Commons
import qs.Commons as Commons
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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 : ""
Expand All @@ -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
})
}

Expand Down