Build the Panama Hyprland desktop
This commit is contained in:
+34
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
|
||||
fail() {
|
||||
printf 'activity-indicator contract: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
qs ipc call activity close >/dev/null 2>&1 || true
|
||||
qs ipc call activity clear >/dev/null 2>&1 || true
|
||||
qs ipc call status-events reset >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
[[ -f "$repo_dir/config/dot/quickshell/modules/bar/ActivityIndicator.qml" ]] || fail 'bar indicator is missing'
|
||||
[[ -f "$repo_dir/config/dot/quickshell/modules/bar/ActivityPanel.qml" ]] || fail 'activity detail panel is missing'
|
||||
rg -q 'ActivityIndicator \{' "$repo_dir/config/dot/quickshell/modules/bar/Bar.qml" || fail 'bar does not instantiate the activity indicator'
|
||||
! rg -q '^[[:space:]]*RecordingIndicator \{\}' "$repo_dir/config/dot/quickshell/shell.qml" || fail 'legacy recording popup is still instantiated'
|
||||
|
||||
qs ipc call activity fixture microphone-on >/dev/null
|
||||
qs ipc call activity open >/dev/null
|
||||
for _ in $(seq 1 20); do
|
||||
if hyprctl layers | rg -q 'namespace: qs-popover-activity'; then
|
||||
printf 'activity-indicator contract: PASS\n'
|
||||
exit 0
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
fail 'active privacy state did not map its detail panel'
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
|
||||
fail() {
|
||||
printf 'activity-state contract: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
activity() {
|
||||
qs ipc call activity status | jq -r ".${1}"
|
||||
}
|
||||
|
||||
event() {
|
||||
qs ipc call status-events status | jq -r ".${1}"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
qs ipc call activity clear >/dev/null 2>&1 || true
|
||||
qs ipc call status-events reset >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
qs ipc show | rg -q '^target activity$' || fail 'activity IPC target is missing'
|
||||
rg -q 'StatusEvents.publish' "$repo_dir/config/dot/quickshell/services/Capture.qml" || fail 'capture does not publish recording transitions'
|
||||
rg -q 'status-events screenshot' "$repo_dir/config/dot/quickshell/services/Capture.qml" || fail 'successful screenshot path does not publish an event'
|
||||
rg -q 'StatusEvents.publish' "$repo_dir/config/dot/quickshell/services/NightLight.qml" || fail 'Night Light does not publish transitions'
|
||||
|
||||
qs ipc call status-events reset >/dev/null
|
||||
qs ipc call activity fixture microphone-on >/dev/null
|
||||
[[ "$(activity microphoneActive)" == "true" ]] || fail 'microphone state did not become persistent'
|
||||
[[ "$(event activeKey)" == "privacy-microphone" ]] || fail 'microphone transition did not publish Signal Glass event'
|
||||
|
||||
qs ipc call status-events dismiss >/dev/null
|
||||
[[ "$(activity microphoneActive)" == "true" ]] || fail 'capsule dismissal cleared persistent privacy state'
|
||||
|
||||
qs ipc call activity fixture all-off >/dev/null
|
||||
[[ "$(activity microphoneActive)" == "false" ]] || fail 'microphone state did not clear'
|
||||
[[ "$(event activeKey)" == "privacy-microphone" ]] || fail 'microphone release did not publish a transition'
|
||||
[[ "$(event title)" == "Microphone released" ]] || fail 'microphone release copy is incorrect'
|
||||
|
||||
trap - EXIT
|
||||
cleanup
|
||||
printf 'activity-state contract: PASS\n'
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
bluetooth="$repo_dir/config/dot/quickshell/modules/quicksettings/BluetoothList.qml"
|
||||
|
||||
rg -q 'const shouldDiscover = root\.active && root\.adapter\.enabled' "$bluetooth" || {
|
||||
printf 'bluetooth discovery contract: desired state is not calculated idempotently\n' >&2
|
||||
exit 1
|
||||
}
|
||||
rg -q 'shouldDiscover && !root\.adapter\.discovering' "$bluetooth" || {
|
||||
printf 'bluetooth discovery contract: redundant BlueZ starts are not guarded\n' >&2
|
||||
exit 1
|
||||
}
|
||||
rg -q '!shouldDiscover && root\.discoveryOwned && root\.adapter\.discovering' "$bluetooth" || {
|
||||
printf 'bluetooth discovery contract: the picker may stop discovery it does not own\n' >&2
|
||||
exit 1
|
||||
}
|
||||
! rg -q 'Component\.onCompleted: root\.syncDiscovery' "$bluetooth" || {
|
||||
printf 'bluetooth discovery contract: inactive construction still writes BlueZ state\n' >&2
|
||||
exit 1
|
||||
}
|
||||
rg -Uq 'onAdapterChanged:[^{\n]*\{[^}]*if \(root\.active\)' "$bluetooth" || {
|
||||
printf 'bluetooth discovery contract: adapter changes are not gated by an open panel\n' >&2
|
||||
exit 1
|
||||
}
|
||||
rg -Uq 'Component\.onDestruction:[^{\n]*\{[^}]*root\.discoveryOwned' "$bluetooth" || {
|
||||
printf 'bluetooth discovery contract: owned discovery is not released on destruction\n' >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
printf 'bluetooth discovery contract: PASS\n'
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
events="$repo_dir/config/dot/quickshell/services/StatusEvents.qml"
|
||||
focus="$repo_dir/config/dot/quickshell/services/FocusSession.qml"
|
||||
devices="$repo_dir/config/dot/quickshell/services/DeviceEvents.qml"
|
||||
privacy="$repo_dir/config/dot/quickshell/services/PrivacyState.qml"
|
||||
|
||||
fail() {
|
||||
printf 'curated-events policy: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
rg -q 'action === "open-workspace"' "$events" || fail 'broker cannot route a retained workspace action'
|
||||
rg -q 'actionId: "open-workspace"' "$focus" || fail 'focus completion does not retain an actionable workspace destination'
|
||||
rg -q 'actionData: String\(finishedWorkspaceId\)' "$focus" || fail 'focus completion omits its workspace id'
|
||||
rg -q 'id: discoverySettle' "$devices" || fail 'device discovery has no silent initialization window'
|
||||
! rg -Uq 'Component\.onCompleted:[^{\n]*\{[^}]*outputInitialized = true' "$devices" || fail 'output monitoring announces initial discovery'
|
||||
rg -Uq 'function clearFixture\(\): void \{[^}]*initialized = false' "$privacy" || fail 'leaving a fixture can emit a false privacy transition'
|
||||
|
||||
printf 'curated-events policy: PASS\n'
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
fail() {
|
||||
printf 'focus-session contract: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
read_prop() {
|
||||
qs ipc call focus status | jq -r ".${1}"
|
||||
}
|
||||
|
||||
qs ipc show | rg -q '^target focus$' || fail 'focus IPC target is missing'
|
||||
|
||||
# Keep this contract reversible even if an assertion fails halfway through.
|
||||
cleanup() {
|
||||
qs ipc call focus end >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
qs ipc call focus start >/dev/null
|
||||
[[ "$(read_prop active)" == "true" ]] || fail 'start did not activate a session'
|
||||
[[ "$(read_prop paused)" == "false" ]] || fail 'new session started paused'
|
||||
[[ "$(read_prop workspaceId)" =~ ^[1-9][0-9]*$ ]] || fail 'session did not bind a positive workspace'
|
||||
|
||||
qs ipc call focus dismiss >/dev/null
|
||||
[[ "$(read_prop capsuleVisible)" == "false" ]] || fail 'dismiss did not hide the capsule'
|
||||
|
||||
qs ipc call focus pause >/dev/null
|
||||
[[ "$(read_prop paused)" == "true" ]] || fail 'pause did not freeze the session'
|
||||
|
||||
qs ipc call focus reveal >/dev/null
|
||||
[[ "$(read_prop capsuleVisible)" == "true" ]] || fail 'reveal did not show the capsule'
|
||||
|
||||
qs ipc call focus overview >/dev/null
|
||||
sleep 0.2
|
||||
hyprctl layers | rg -q 'namespace: qs-overview' || fail 'focus overview did not map Mission Control'
|
||||
qs ipc call overview close >/dev/null
|
||||
|
||||
qs ipc call focus end >/dev/null
|
||||
[[ "$(read_prop active)" == "false" ]] || fail 'end did not clear the session'
|
||||
|
||||
trap - EXIT
|
||||
printf 'focus-session contract: PASS\n'
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
fail() {
|
||||
printf 'focus-session expiry: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
qs ipc call focus end >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
qs ipc call focus end >/dev/null 2>&1 || true
|
||||
qs ipc call focus start >/dev/null
|
||||
|
||||
state_file=$(find "${XDG_STATE_HOME:-$HOME/.local/state}/quickshell/by-shell" -name focus-session.json -print -quit)
|
||||
[[ -n "$state_file" ]] || fail 'persistent state file was not created'
|
||||
|
||||
expired=$(mktemp "${state_file}.expired.XXXXXX")
|
||||
jq '.paused = false | .deadlineMs = 1' "$state_file" >"$expired"
|
||||
chmod --reference="$state_file" "$expired"
|
||||
mv "$expired" "$state_file"
|
||||
|
||||
qs kill >/dev/null
|
||||
qs --daemonize >/dev/null
|
||||
|
||||
status=""
|
||||
for _ in $(seq 1 60); do
|
||||
status=$(qs ipc call focus status 2>/dev/null || true)
|
||||
if jq -e '.active == false' <<<"$status" >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 0.2
|
||||
done
|
||||
|
||||
jq -e '.active == false and .caffeine == false and .doNotDisturb == false' <<<"$status" >/dev/null || fail 'expired session was not completed and released'
|
||||
|
||||
trap - EXIT
|
||||
printf 'focus-session expiry: PASS\n'
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
fail() {
|
||||
printf 'focus-session restart: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
qs ipc call focus end >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
qs ipc call focus end >/dev/null 2>&1 || true
|
||||
qs ipc call focus start >/dev/null
|
||||
qs ipc call focus pause >/dev/null
|
||||
before=$(qs ipc call focus status)
|
||||
|
||||
qs kill >/dev/null
|
||||
qs --daemonize >/dev/null
|
||||
|
||||
status=""
|
||||
for _ in $(seq 1 60); do
|
||||
status=$(qs ipc call focus status 2>/dev/null || true)
|
||||
if jq -e '.active == true and .paused == true' <<<"$status" >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 0.2
|
||||
done
|
||||
|
||||
jq -e '.active == true' <<<"$status" >/dev/null || fail 'active session was not restored'
|
||||
jq -e '.paused == true' <<<"$status" >/dev/null || fail 'paused state was not restored'
|
||||
[[ "$(jq -r .remaining <<<"$status")" == "$(jq -r .remaining <<<"$before")" ]] || fail 'paused remainder changed across restart'
|
||||
jq -e '.doNotDisturb == true' <<<"$status" >/dev/null || fail 'Do Not Disturb was not reclaimed'
|
||||
jq -e '.caffeine == true' <<<"$status" >/dev/null || fail 'Caffeine was not reclaimed'
|
||||
|
||||
cleanup
|
||||
trap - EXIT
|
||||
printf 'focus-session restart: PASS\n'
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
body="$repo_dir/config/dot/quickshell/modules/overview/OverviewBody.qml"
|
||||
thumbnail="$repo_dir/config/dot/quickshell/modules/overview/WindowThumbnail.qml"
|
||||
|
||||
fail() {
|
||||
printf 'overview-keyboard contract: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
rg -q 'Keys\.onLeftPressed' "$body" || fail 'Left does not navigate workspaces'
|
||||
rg -q 'Keys\.onRightPressed' "$body" || fail 'Right does not navigate workspaces'
|
||||
rg -q 'Keys\.onUpPressed' "$body" || fail 'Up does not navigate search results'
|
||||
rg -q 'Keys\.onDownPressed' "$body" || fail 'Down does not navigate search results'
|
||||
rg -q 'Keys\.onDeletePressed' "$body" || fail 'Delete cannot close the selected search result'
|
||||
rg -q 'selectedResultIndex' "$body" || fail 'search results have no keyboard selection model'
|
||||
rg -q 'property bool selected' "$thumbnail" || fail 'thumbnail cannot render keyboard selection'
|
||||
|
||||
printf 'overview-keyboard contract: PASS\n'
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
fail() {
|
||||
printf 'overview live actions: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
original_workspace="$(hyprctl -j activeworkspace | jq -r '.id')"
|
||||
address=""
|
||||
|
||||
cleanup() {
|
||||
qs ipc call overview close >/dev/null 2>&1 || true
|
||||
if [[ -n "$address" ]] && hyprctl -j clients | jq -e --arg address "$address" '.[] | select(.address == $address)' >/dev/null; then
|
||||
hyprctl dispatch "hl.dsp.window.close({ window = \"address:$address\" })" >/dev/null 2>&1 || true
|
||||
fi
|
||||
hyprctl dispatch "hl.dsp.focus({ workspace = $original_workspace })" >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
kitty --class panama-overview-contract --title "Panama Overview Contract" sh -c 'exec sleep 300' >/dev/null 2>&1 &
|
||||
|
||||
for _ in $(seq 1 40); do
|
||||
address="$(hyprctl -j clients | jq -r '.[] | select(.class == "panama-overview-contract") | .address' | head -1)"
|
||||
[[ -n "$address" ]] && break
|
||||
sleep 0.1
|
||||
done
|
||||
[[ -n "$address" ]] || fail 'disposable window did not appear'
|
||||
|
||||
target_workspace="$(hyprctl -j workspaces | jq -r --argjson current "$original_workspace" '[.[] | select(.id > 0 and .id != $current)][0].id // empty')"
|
||||
if [[ -z "$target_workspace" ]]; then
|
||||
target_workspace=$((original_workspace + 1))
|
||||
fi
|
||||
|
||||
qs ipc call overview-actions move "$address" "$target_workspace" >/dev/null
|
||||
sleep 0.25
|
||||
[[ "$(hyprctl -j clients | jq -r --arg address "$address" '.[] | select(.address == $address) | .workspace.name')" == "$target_workspace" ]] \
|
||||
|| fail 'exact-address workspace move failed'
|
||||
|
||||
qs ipc call overview-actions move "$address" special:scratch >/dev/null
|
||||
sleep 0.25
|
||||
[[ "$(hyprctl -j clients | jq -r --arg address "$address" '.[] | select(.address == $address) | .workspace.name')" == "special:scratch" ]] \
|
||||
|| fail 'exact-address scratchpad move failed'
|
||||
|
||||
qs ipc call overview open >/dev/null
|
||||
sleep 0.2
|
||||
qs ipc call overview-actions restore "$address" >/dev/null
|
||||
sleep 0.3
|
||||
[[ "$(hyprctl -j clients | jq -r --arg address "$address" '.[] | select(.address == $address) | .workspace.id')" == "$original_workspace" ]] \
|
||||
|| fail 'scratchpad restore did not return to the selected workspace'
|
||||
|
||||
qs ipc call overview-actions close "$address" >/dev/null
|
||||
for _ in $(seq 1 30); do
|
||||
if ! hyprctl -j clients | jq -e --arg address "$address" '.[] | select(.address == $address)' >/dev/null; then
|
||||
address=""
|
||||
trap - EXIT
|
||||
cleanup
|
||||
printf 'overview live actions: PASS\n'
|
||||
exit 0
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
fail 'exact-address close did not close the disposable window'
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cleanup() {
|
||||
qs ipc call overview close >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
qs ipc show | rg -A8 '^target overview$' | rg -q 'function search\(query: string\): void'
|
||||
qs ipc call overview search ChatGPT >/dev/null
|
||||
|
||||
for _ in $(seq 1 20); do
|
||||
if hyprctl layers | rg -q 'namespace: qs-overview'; then
|
||||
printf 'overview-search contract: PASS\n'
|
||||
exit 0
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
printf 'overview-search contract: overview did not map\n' >&2
|
||||
exit 1
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
body="$repo_dir/config/dot/quickshell/modules/overview/OverviewBody.qml"
|
||||
thumbnail="$repo_dir/config/dot/quickshell/modules/overview/WindowThumbnail.qml"
|
||||
workspace="$repo_dir/config/dot/quickshell/modules/overview/WorkspaceCard.qml"
|
||||
|
||||
fail() {
|
||||
printf 'overview-window-actions contract: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
rg -q 'function addressOf\(toplevel: var\): string' "$body" || fail 'address normalization boundary is missing'
|
||||
rg -q 'function closeToplevel\(toplevel: var\): void' "$body" || fail 'address-scoped close boundary is missing'
|
||||
rg -q 'function moveToplevel\(toplevel: var, workspaceName: string\): void' "$body" || fail 'address-scoped move boundary is missing'
|
||||
rg -q 'function restoreToplevel\(toplevel: var\): void' "$body" || fail 'scratchpad restore boundary is missing'
|
||||
rg -q 'hl\.dsp\.window\.close\(\{ window = ' "$body" || fail 'close does not use the modern exact-window dispatcher'
|
||||
rg -q 'hl\.dsp\.window\.move\(\{ workspace = ' "$body" || fail 'move does not use the modern exact-window dispatcher'
|
||||
rg -q 'signal closeRequested' "$thumbnail" || fail 'thumbnail has no close intent'
|
||||
rg -q 'Drag\.active:' "$thumbnail" || fail 'thumbnail has no drag source'
|
||||
rg -q 'DropArea \{' "$workspace" || fail 'workspace card has no drop target'
|
||||
qs ipc show | rg -q '^target overview-actions$' || fail 'live action contract surface is missing'
|
||||
|
||||
printf 'overview-window-actions contract: PASS\n'
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
shelf="$repo_dir/config/dot/quickshell/modules/overview/ScratchpadShelf.qml"
|
||||
body="$repo_dir/config/dot/quickshell/modules/overview/OverviewBody.qml"
|
||||
keys="$repo_dir/config/dot/hypr/keybinds.lua"
|
||||
|
||||
fail() {
|
||||
printf 'scratchpad-shelf contract: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ -f "$shelf" ]] || fail 'global scratchpad shelf is missing'
|
||||
rg -q 'special:scratch' "$body" || fail 'overview does not discover the global scratch workspace'
|
||||
rg -q 'ScratchpadShelf \{' "$body" || fail 'overview does not instantiate the shelf'
|
||||
rg -q 'DropArea \{' "$shelf" || fail 'shelf is not a drag destination'
|
||||
rg -q 'onRestoreRequested' "$body" || fail 'shelf cards cannot restore to the selected workspace'
|
||||
rg -q 'SUPER.*X|mod .*\+ X' "$keys" || fail 'Super+X scratchpad toggle disappeared'
|
||||
rg -q 'SHIFT \+ X' "$keys" || fail 'Super+Shift+X send-to-scratchpad binding disappeared'
|
||||
|
||||
qs ipc call overview open >/dev/null
|
||||
for _ in $(seq 1 20); do
|
||||
if hyprctl layers | rg -q 'namespace: qs-overview'; then
|
||||
qs ipc call overview close >/dev/null
|
||||
printf 'scratchpad-shelf contract: PASS\n'
|
||||
exit 0
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
fail 'overview did not map with the shelf present'
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
fail() {
|
||||
printf 'screen intelligence contract: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
fixture_dir="$(mktemp -d)"
|
||||
cleanup() {
|
||||
qs ipc call screen-intelligence close >/dev/null 2>&1 || true
|
||||
qs ipc call capture close >/dev/null 2>&1 || true
|
||||
rm -rf "$fixture_dir"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
qs ipc show | rg -q '^target screen-intelligence$' || fail 'screen-intelligence IPC target is missing'
|
||||
|
||||
status="$(qs ipc call screen-intelligence status)"
|
||||
jq -e '.phase == "idle" and (.ocrReady | type == "boolean") and (.codeReady | type == "boolean")' <<<"$status" >/dev/null \
|
||||
|| fail 'readiness status is malformed'
|
||||
|
||||
fixture="$fixture_dir/panama-live-ocr.png"
|
||||
magick -size 1600x260 xc:white \
|
||||
-gravity center -fill black -font DejaVu-Sans -pointsize 72 \
|
||||
-annotate 0 'PANAMA LIVE OCR' "$fixture"
|
||||
|
||||
qs ipc call screen-intelligence analyzeFile "$fixture" >/dev/null
|
||||
for _ in $(seq 1 100); do
|
||||
status="$(qs ipc call screen-intelligence status)"
|
||||
phase="$(jq -r .phase <<<"$status")"
|
||||
[[ "$phase" == "ready" || "$phase" == "error" ]] && break
|
||||
sleep 0.1
|
||||
done
|
||||
[[ "$(jq -r .phase <<<"$status")" == "ready" ]] \
|
||||
|| fail "fixture recognition did not finish successfully: $(jq -r .error <<<"$status")"
|
||||
[[ "$(jq -r .text <<<"$status" | tr '\n' ' ')" == *'PANAMA LIVE OCR'* ]] \
|
||||
|| fail 'live service did not expose recognized fixture text'
|
||||
|
||||
qs ipc call screen-intelligence close >/dev/null
|
||||
[[ "$(qs ipc call screen-intelligence status | jq -r .phase)" == "idle" ]] \
|
||||
|| fail 'close did not reset recognition state'
|
||||
|
||||
qs ipc call screen-intelligence open >/dev/null
|
||||
for _ in $(seq 1 40); do
|
||||
capture_status="$(qs ipc call capture status 2>/dev/null || true)"
|
||||
if jq -e '.open == true' <<<"$capture_status" >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
jq -e '.open == true and .mode == "selection" and .intelligenceMode == true' <<<"$capture_status" >/dev/null \
|
||||
|| fail 'direct open did not select Selection + Read mode'
|
||||
|
||||
printf 'screen intelligence contract: PASS\n'
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
fail() {
|
||||
printf 'screen intelligence helper contract: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
helper="$project_root/config/dot/quickshell/scripts/screen-intelligence"
|
||||
fixture_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "$fixture_dir"' EXIT
|
||||
|
||||
[[ -x "$helper" ]] || fail 'recognition helper is missing or not executable'
|
||||
command -v magick >/dev/null || fail 'ImageMagick is required to build the OCR fixture'
|
||||
|
||||
probe="$($helper probe)"
|
||||
jq -e '.tesseract == true and .zbar == true' <<<"$probe" >/dev/null \
|
||||
|| fail 'Tesseract and ZBar are not ready'
|
||||
|
||||
fixture="$fixture_dir/panama-ocr.png"
|
||||
magick -size 1600x260 xc:white \
|
||||
-gravity center -fill black -font DejaVu-Sans -pointsize 72 \
|
||||
-annotate 0 'PANAMA SCREEN INTELLIGENCE' "$fixture"
|
||||
|
||||
result="$($helper analyze-file "$fixture")"
|
||||
jq -e '.ok == true' <<<"$result" >/dev/null || fail "recognition failed: $(jq -r .error <<<"$result")"
|
||||
text="$(jq -r .text <<<"$result" | tr '\n' ' ' | tr -s ' ')"
|
||||
[[ "$text" == *'PANAMA SCREEN INTELLIGENCE'* ]] \
|
||||
|| fail "literal fixture text was not recognized: $text"
|
||||
|
||||
printf 'screen intelligence helper contract: PASS\n'
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
fail() {
|
||||
printf 'settings pages contract: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
qs ipc call settings close >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
pages=(home appearance displays connectivity desktop sound notifications screen-intelligence shortcuts services about)
|
||||
for page in "${pages[@]}"; do
|
||||
qs ipc call settings page "$page" >/dev/null
|
||||
for _ in $(seq 1 20); do
|
||||
[[ "$(qs ipc call settings status | jq -r .page)" == "$page" ]] && break
|
||||
sleep 0.1
|
||||
done
|
||||
[[ "$(qs ipc call settings status | jq -r .page)" == "$page" ]] || fail "$page did not route"
|
||||
hyprctl -j clients | jq -e '[.[] | select(.title == "Panama Settings")] | length == 1' >/dev/null \
|
||||
|| fail "$page created a missing or duplicate Settings window"
|
||||
done
|
||||
|
||||
qs ipc call settings page '__unsupported__' >/dev/null
|
||||
[[ "$(qs ipc call settings status | jq -r .page)" == "home" ]] || fail 'unsupported page did not fall back to Home'
|
||||
|
||||
hyprctl -j binds | jq -e '.[] | select(.description == "Panama Settings" and .key == "I" and .modmask == 64)' >/dev/null \
|
||||
|| fail 'Super+I is not registered as Panama Settings'
|
||||
hyprctl -j binds | jq -e '.[] | select(.description == "Screen Intelligence" and .key == "S" and .modmask == 65)' >/dev/null \
|
||||
|| fail 'Super+Shift+S is not registered as Screen Intelligence'
|
||||
|
||||
desktop_file="$HOME/.local/share/applications/panama-settings.desktop"
|
||||
[[ -L "$desktop_file" || -f "$desktop_file" ]] || fail 'searchable desktop entry is not installed'
|
||||
desktop-file-validate "$desktop_file" >/dev/null || fail 'desktop entry is invalid'
|
||||
gnome_desktop_file="$HOME/.local/share/applications/gnome-settings-hyprland.desktop"
|
||||
[[ -L "$gnome_desktop_file" || -f "$gnome_desktop_file" ]] || fail 'searchable GNOME Settings fallback is not installed'
|
||||
desktop-file-validate "$gnome_desktop_file" >/dev/null || fail 'GNOME Settings fallback entry is invalid'
|
||||
intelligence_desktop_file="$HOME/.local/share/applications/panama-screen-intelligence.desktop"
|
||||
[[ -L "$intelligence_desktop_file" || -f "$intelligence_desktop_file" ]] || fail 'Screen Intelligence desktop entry is not installed'
|
||||
desktop-file-validate "$intelligence_desktop_file" >/dev/null || fail 'Screen Intelligence desktop entry is invalid'
|
||||
|
||||
trap - EXIT
|
||||
cleanup
|
||||
printf 'settings pages contract: PASS\n'
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
harness="$repo_dir/config/dot/quickshell/settings-preferences-harness.qml"
|
||||
state_home="$(mktemp -d /tmp/panama-settings-state.XXXXXX)"
|
||||
|
||||
fail() {
|
||||
printf 'settings preferences contract: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
qs_for_harness() {
|
||||
XDG_STATE_HOME="$state_home" qs -p "$harness" "$@"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
qs_for_harness kill >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
start_harness() {
|
||||
XDG_STATE_HOME="$state_home" qs -p "$harness" --daemonize >/dev/null
|
||||
for _ in $(seq 1 40); do
|
||||
if qs_for_harness ipc show 2>/dev/null | rg -q '^target settings-pref-test$'; then
|
||||
return
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
fail 'test IPC target did not start'
|
||||
}
|
||||
|
||||
stop_harness() {
|
||||
qs_for_harness kill >/dev/null 2>&1 || true
|
||||
for _ in $(seq 1 40); do
|
||||
if ! qs_for_harness ipc show >/dev/null 2>&1; then
|
||||
return
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
fail 'test shell did not stop cleanly'
|
||||
}
|
||||
|
||||
start_harness
|
||||
qs_for_harness ipc call settings-pref-test applyFixture >/dev/null
|
||||
|
||||
expected='{"use24Hour":true,"showSeconds":false,"dockAutohide":false,"focusDurationMinutes":70,"autoHdr":false,"vrrPolicy":0,"directScanoutPolicy":0,"nightLightEnabled":true,"nightLightAutomatic":true,"nightLightTemperature":4100,"lastPage":"desktop"}'
|
||||
before="$(qs_for_harness ipc call settings-pref-test status | jq -c 'del(.stateDir)')"
|
||||
[[ "$before" == "$expected" ]] || fail "fixture did not apply: $before"
|
||||
|
||||
state_file=""
|
||||
for _ in $(seq 1 40); do
|
||||
state_file="$(find "$state_home" -name panama-settings.json -print -quit)"
|
||||
if [[ -n "$state_file" ]] && jq -e '.lastPage == "desktop" and .focusDurationMinutes == 70' "$state_file" >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
[[ -n "$state_file" ]] || fail 'preferences file was not created'
|
||||
jq -e '.use24Hour == true and .showSeconds == false and .dockAutohide == false and .autoHdr == false and .directScanoutPolicy == 0 and .nightLightEnabled == true and .nightLightAutomatic == true and .nightLightTemperature == 4100' "$state_file" >/dev/null \
|
||||
|| fail 'preferences file did not contain the complete atomic update'
|
||||
|
||||
# The contract is specifically restart persistence, not merely live QML state.
|
||||
stop_harness
|
||||
start_harness
|
||||
after=""
|
||||
for _ in $(seq 1 40); do
|
||||
after="$(qs_for_harness ipc call settings-pref-test status | jq -c 'del(.stateDir)')"
|
||||
[[ "$after" == "$expected" ]] && break
|
||||
sleep 0.1
|
||||
done
|
||||
[[ "$after" == "$expected" ]] || fail "values did not survive restart: $after"
|
||||
|
||||
trap - EXIT
|
||||
cleanup
|
||||
printf 'settings preferences contract: PASS\n'
|
||||
Executable
+63
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
harness="$repo_dir/config/dot/quickshell/settings-system-harness.qml"
|
||||
|
||||
fail() {
|
||||
printf 'settings system contract: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
qs_for_harness() {
|
||||
qs -p "$harness" "$@"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
qs_for_harness kill >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
original_auto_hdr="$(hyprctl -j getoption render:cm_auto_hdr | jq -r .int)"
|
||||
original_vrr="$(hyprctl -j getoption misc:vrr | jq -r .int)"
|
||||
original_direct="$(hyprctl -j getoption render:direct_scanout | jq -r .int)"
|
||||
|
||||
qs_for_harness --daemonize >/dev/null
|
||||
for _ in $(seq 1 40); do
|
||||
if qs_for_harness ipc show 2>/dev/null | rg -q '^target settings-system-test$'; then
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
qs_for_harness ipc show 2>/dev/null | rg -q '^target settings-system-test$' || fail 'test IPC target did not start'
|
||||
|
||||
qs_for_harness ipc call settings-system-test refresh >/dev/null
|
||||
status='{}'
|
||||
for _ in $(seq 1 40); do
|
||||
status="$(qs_for_harness ipc call settings-system-test status | jq -c .)"
|
||||
if jq -e '.monitorName != "" and .width > 0 and .height > 0 and .busy == false' <<<"$status" >/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
jq -e '.monitorName == "DP-2" and .width == 4500 and .height == 3000 and .scale == 1.5 and .format == "XRGB2101010"' <<<"$status" >/dev/null \
|
||||
|| fail "live monitor data was not normalized: $status"
|
||||
|
||||
[[ "$(qs_for_harness ipc call settings-system-test panelAllowed '__definitely_not_a_panel__' | jq -r .)" == "false" ]] \
|
||||
|| fail 'unsupported GNOME panel was accepted'
|
||||
[[ "$(qs_for_harness ipc call settings-system-test panelAllowed sound | jq -r .)" == "true" ]] \
|
||||
|| fail 'supported GNOME sound panel was rejected'
|
||||
|
||||
# Apply the exact current values: this exercises the real allow-listed command
|
||||
# without changing the daily-driver compositor state.
|
||||
qs_for_harness ipc call settings-system-test apply "$([[ "$original_auto_hdr" == 1 ]] && printf true || printf false)" "$original_vrr" "$original_direct" >/dev/null
|
||||
sleep 0.3
|
||||
|
||||
[[ "$(hyprctl -j getoption render:cm_auto_hdr | jq -r .int)" == "$original_auto_hdr" ]] || fail 'Auto HDR changed unexpectedly'
|
||||
[[ "$(hyprctl -j getoption misc:vrr | jq -r .int)" == "$original_vrr" ]] || fail 'VRR changed unexpectedly'
|
||||
[[ "$(hyprctl -j getoption render:direct_scanout | jq -r .int)" == "$original_direct" ]] || fail 'direct scanout changed unexpectedly'
|
||||
|
||||
trap - EXIT
|
||||
cleanup
|
||||
printf 'settings system contract: PASS\n'
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
fail() {
|
||||
printf 'settings window contract: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
qs ipc call settings close >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
qs ipc show | rg -q '^target settings$' || fail 'settings IPC target is missing'
|
||||
qs ipc call settings open >/dev/null
|
||||
|
||||
for _ in $(seq 1 40); do
|
||||
if hyprctl -j clients | jq -e '[.[] | select(.title == "Panama Settings")] | length == 1' >/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
hyprctl -j clients | jq -e '[.[] | select(.title == "Panama Settings")] | length == 1' >/dev/null \
|
||||
|| fail 'exactly one Settings window did not map'
|
||||
hyprctl -j clients | jq -e '.[] | select(.title == "Panama Settings" and .floating == false)' >/dev/null \
|
||||
|| fail 'Settings window is not tiled'
|
||||
|
||||
qs ipc call settings page displays >/dev/null
|
||||
[[ "$(qs ipc call settings status | jq -r .page)" == "displays" ]] || fail 'Displays page did not route'
|
||||
|
||||
qs ipc call settings page desktop >/dev/null
|
||||
[[ "$(qs ipc call settings status | jq -r .page)" == "desktop" ]] || fail 'Desktop page did not route'
|
||||
|
||||
address="$(hyprctl -j clients | jq -r '.[] | select(.title == "Panama Settings") | .address')"
|
||||
hyprctl dispatch "hl.dsp.window.close({ window = \"address:$address\" })" >/dev/null
|
||||
for _ in $(seq 1 40); do
|
||||
[[ "$(qs ipc call settings status | jq -r .open)" == "false" ]] && break
|
||||
sleep 0.1
|
||||
done
|
||||
[[ "$(qs ipc call settings status | jq -r .open)" == "false" ]] || fail 'compositor close did not synchronize shell state'
|
||||
|
||||
qs ipc call settings open >/dev/null
|
||||
for _ in $(seq 1 40); do
|
||||
hyprctl -j clients | jq -e '.[] | select(.title == "Panama Settings")' >/dev/null && break
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
qs ipc call settings close >/dev/null
|
||||
for _ in $(seq 1 40); do
|
||||
if ! hyprctl -j clients | jq -e '.[] | select(.title == "Panama Settings")' >/dev/null; then
|
||||
trap - EXIT
|
||||
printf 'settings window contract: PASS\n'
|
||||
exit 0
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
fail 'Settings window did not close'
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
shell_file="$repo_dir/config/dot/quickshell/shell.qml"
|
||||
rules_file="$repo_dir/config/dot/hypr/rules.lua"
|
||||
|
||||
fail() {
|
||||
printf 'signal-glass contract: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
qs ipc call status-events reset >/dev/null 2>&1 || true
|
||||
qs ipc call focus end >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
[[ -f "$repo_dir/config/dot/quickshell/modules/signals/SignalGlass.qml" ]] || fail 'shared host is missing'
|
||||
[[ -f "$repo_dir/config/dot/quickshell/modules/signals/EventCard.qml" ]] || fail 'event presentation is missing'
|
||||
[[ -f "$repo_dir/config/dot/quickshell/modules/signals/FocusCard.qml" ]] || fail 'focus presentation is missing'
|
||||
[[ "$(rg -c 'SignalGlass \{\}' "$shell_file")" == "1" ]] || fail 'shell does not instantiate exactly one shared host delegate'
|
||||
! rg -q 'FocusCapsule \{\}' "$shell_file" || fail 'legacy focus-only host is still instantiated'
|
||||
rg -q 'namespace = "\^qs-signal-glass\$"' "$rules_file" || fail 'Hyprland layer rules do not match the shared namespace'
|
||||
|
||||
qs ipc call status-events reset >/dev/null
|
||||
qs ipc call status-events fixture critical >/dev/null
|
||||
for _ in $(seq 1 20); do
|
||||
if hyprctl layers | rg -q 'namespace: qs-signal-glass'; then
|
||||
printf 'signal-glass contract: PASS\n'
|
||||
exit 0
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
fail 'event did not map the shared Signal Glass layer'
|
||||
Executable
+55
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
fail() {
|
||||
printf 'status-events contract: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
status() {
|
||||
qs ipc call status-events status | jq -r ".${1}"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
qs ipc call status-events reset >/dev/null 2>&1 || true
|
||||
if [[ "${restore_dnd:-false}" == "true" ]]; then
|
||||
qs ipc call notifications dnd >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
qs ipc show | rg -q '^target status-events$' || fail 'IPC target is missing'
|
||||
qs ipc call status-events reset >/dev/null
|
||||
|
||||
qs ipc call status-events fixture ambient >/dev/null
|
||||
[[ "$(status activeKey)" == "device-output" ]] || fail 'ambient fixture did not become active'
|
||||
[[ "$(status queueLength)" == "0" ]] || fail 'first event unexpectedly queued'
|
||||
|
||||
qs ipc call status-events fixture ambient-replacement >/dev/null
|
||||
[[ "$(status activeKey)" == "device-output" ]] || fail 'equivalent event changed its stable key'
|
||||
[[ "$(status title)" == "Studio Display" ]] || fail 'equivalent event did not replace active content'
|
||||
[[ "$(status queueLength)" == "0" ]] || fail 'equivalent event stacked instead of replacing'
|
||||
|
||||
qs ipc call status-events fixture critical >/dev/null
|
||||
[[ "$(status activeKey)" == "privacy-microphone" ]] || fail 'critical event did not preempt ambient event'
|
||||
[[ "$(status priority)" == "90" ]] || fail 'critical event priority was not preserved'
|
||||
[[ "$(status queueLength)" == "1" ]] || fail 'preempted event was not retained once'
|
||||
|
||||
qs ipc call status-events dismiss >/dev/null
|
||||
[[ "$(status activeKey)" == "device-output" ]] || fail 'dismiss did not reveal the queued ambient event'
|
||||
|
||||
qs ipc call status-events reset >/dev/null
|
||||
dnd_now="$(qs ipc call notifications dnd)"
|
||||
if [[ "$dnd_now" == "true" ]]; then
|
||||
restore_dnd=true
|
||||
fi
|
||||
qs ipc call status-events fixture ambient >/dev/null
|
||||
[[ "$(status active)" == "false" ]] || fail 'DND did not suppress an ambient event'
|
||||
|
||||
qs ipc call status-events fixture critical >/dev/null
|
||||
[[ "$(status activeKey)" == "privacy-microphone" ]] || fail 'DND suppressed a privacy event'
|
||||
|
||||
trap - EXIT
|
||||
cleanup
|
||||
printf 'status-events contract: PASS\n'
|
||||
Reference in New Issue
Block a user