Merge upstream quattro for the Qt 6.12 palette fix (#14553)
Qt 6.12 shadows the bare Color singleton, so the bar and every Color role read undefined on fresh installs. Keep our I18n text in the four conflicting files, qualify #14433's notification button color, give the new dictation menu rows and notifications Chinese, and drop table entries for the removed Voxtype and FIDO2 texts.
This commit is contained in:
commit
fcd681c099
239 files changed
+3996
-616
No files matched your search
+50
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
require_compositor "same-path background fallback"
|
||||
require_command quickshell
|
||||
require_command grim
|
||||
require_command magick
|
||||
|
||||
stage=$(mktemp -d)
|
||||
qs_pid=""
|
||||
cleanup() {
|
||||
[[ -z $qs_pid ]] || { kill "$qs_pid" 2>/dev/null || true; wait "$qs_pid" 2>/dev/null || true; }
|
||||
rm -rf "$stage"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
mkdir -p "$stage/home/.local/state/omarchy/current"
|
||||
ln -s "$ROOT/shell/plugins/background" "$stage/background"
|
||||
ln -s "$ROOT/shell/Commons" "$stage/Commons"
|
||||
ln -s "$ROOT/shell/Ui" "$stage/Ui"
|
||||
cp "$SHELL_TEST_DIR/fixtures/background-instant-reload/shell.qml" "$stage/shell.qml"
|
||||
magick -size 128x128 xc:magenta "$stage/still.png"
|
||||
ln -s "$stage/still.png" "$stage/home/.local/state/omarchy/current/background"
|
||||
: >"$stage/command"
|
||||
HOME="$stage/home" RELOAD_TEST_COMMAND="$stage/command" RELOAD_TEST_RESULT="$stage/result" \
|
||||
quickshell -p "$stage" --no-color >"$stage/quickshell.log" 2>&1 &
|
||||
qs_pid=$!
|
||||
wait_phase() {
|
||||
for attempt in {1..100}; do
|
||||
[[ -f $stage/result && $(<"$stage/result") == "$1" ]] && return 0
|
||||
sleep 0.03
|
||||
done
|
||||
fail "same-path background reaches $1" "$(cat "$stage/quickshell.log")"
|
||||
}
|
||||
pixel() {
|
||||
local output
|
||||
output=$(hyprctl -j monitors | jq -r '.[0].name')
|
||||
timeout -k 1 3 grim -o "$output" "$stage/pixel.png"
|
||||
magick "$stage/pixel.png" -format '%[hex:p{32,256}]' info:
|
||||
}
|
||||
wait_phase initial
|
||||
[[ $(pixel) == FF00FF ]] || fail "the initial background is visible"
|
||||
magick -size 128x128 xc:cyan "$stage/still.png"
|
||||
printf 'reload\n' >"$stage/command"
|
||||
wait_phase reloaded
|
||||
[[ $(pixel) == 00FFFF ]] || fail "the instant fallback reloads new pixels behind an unchanged path"
|
||||
printf 'done\n' >"$stage/command"
|
||||
wait "$qs_pid" || fail "the same-path reload fixture exits cleanly" "$(cat "$stage/quickshell.log")"
|
||||
qs_pid=""
|
||||
pass "the instant fallback reloads a replaced image at the same path"
|
||||
@@ -0,0 +1,93 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
require_compositor "cloned background intro cover test"
|
||||
require_command quickshell
|
||||
require_command grim
|
||||
require_command magick
|
||||
|
||||
stage=$(mktemp -d)
|
||||
qs_pid=""
|
||||
cleanup() {
|
||||
[[ -z $qs_pid ]] || { kill "$qs_pid" 2>/dev/null || true; wait "$qs_pid" 2>/dev/null || true; }
|
||||
rm -rf "$stage"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
mkdir -p "$stage/bin" "$stage/home/.local/state/omarchy/current"
|
||||
cat >"$stage/bin/noop" <<'SH'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
SH
|
||||
chmod +x "$stage/bin/noop"
|
||||
for command in omarchy-shell omarchy-plugin-enable omarchy-notification-send; do
|
||||
ln -s noop "$stage/bin/$command"
|
||||
done
|
||||
cat >"$stage/bin/omarchy-plugin-list" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '[{"id":"intro-test.background"}]\n'
|
||||
SH
|
||||
cat >"$stage/bin/omarchy-theme-bg-boot-intro" <<'SH'
|
||||
#!/bin/bash
|
||||
for attempt in {1..200}; do
|
||||
[[ ! -s $INTRO_TEST_RELEASE ]] || exit 0
|
||||
sleep 0.05
|
||||
done
|
||||
exit 1
|
||||
SH
|
||||
chmod +x "$stage/bin/omarchy-plugin-list" "$stage/bin/omarchy-theme-bg-boot-intro"
|
||||
HOME="$stage/home" USER=intro-test OMARCHY_PATH="$ROOT" PATH="$stage/bin:$ROOT/bin:$PATH" \
|
||||
"$ROOT/bin/omarchy-plugin-clone" omarchy.background >/dev/null
|
||||
ln -s "$stage/home/.config/omarchy/plugins/intro-test.background" "$stage/clone"
|
||||
ln -s "$ROOT/shell/Commons" "$stage/Commons"
|
||||
ln -s "$ROOT/shell/Ui" "$stage/Ui"
|
||||
ln -s "$ROOT/shell/services" "$stage/services"
|
||||
cp "$SHELL_TEST_DIR/fixtures/background-intro-clone/shell.qml" "$stage/shell.qml"
|
||||
magick -size 128x128 xc:magenta "$stage/still.png"
|
||||
ln -s "$stage/still.png" "$stage/home/.local/state/omarchy/current/background"
|
||||
|
||||
wait_phase() {
|
||||
for attempt in {1..100}; do
|
||||
[[ -s $stage/result.json ]] && [[ $(jq -r .phase "$stage/result.json") == "$1" ]] && return 0
|
||||
sleep 0.03
|
||||
done
|
||||
fail "cloned background reaches the $1 phase" "$(cat "$stage/quickshell.log")"
|
||||
}
|
||||
capture_pixel() {
|
||||
hyprctl dispatch 'hl.dsp.dpms({ action = "enable" })' >/dev/null
|
||||
local output
|
||||
output=$(hyprctl -j monitors | jq -r '.[0].name')
|
||||
timeout -k 1 3 grim -o "$output" "$stage/$1.png"
|
||||
magick "$stage/$1.png" -format '%[hex:p{32,256}]' info:
|
||||
}
|
||||
for consumed in false true; do
|
||||
if [[ $consumed == true ]]; then
|
||||
printf '%s\n' "$HYPRLAND_INSTANCE_SIGNATURE" >"$stage/home/.local/state/omarchy/background-intro.session-id"
|
||||
fi
|
||||
rm -f "$stage/result.json"
|
||||
: >"$stage/release"
|
||||
HOME="$stage/home" OMARCHY_PATH="$ROOT" PATH="$stage/bin:$PATH" INTRO_TEST_RESULT="$stage/result.json" INTRO_TEST_RELEASE="$stage/release" \
|
||||
quickshell -p "$stage" --no-color >"$stage/quickshell.log" 2>&1 &
|
||||
qs_pid=$!
|
||||
wait_phase cover
|
||||
jq -e --argjson consumed "$consumed" '.scoped and .privateCoordinator and .selected and (.covered == ($consumed | not))' "$stage/result.json" >/dev/null || \
|
||||
fail "the clone is scoped and a consumed login never maps the cover" "$(cat "$stage/result.json")"
|
||||
if [[ $consumed == false ]]; then
|
||||
pixel=$(capture_pixel cover)
|
||||
[[ $pixel == "000000" ]] || fail "a fresh login keeps the cloned wallpaper hidden until startup is ready" "$pixel"
|
||||
[[ $(magick "$stage/cover.png" -format '%[hex:p{32,10}]' info:) == "000000" ]] || fail "the startup cover hides the bar as well as the wallpaper"
|
||||
else
|
||||
[[ $(capture_pixel cover) == "FF00FF" ]] || fail "a consumed login does not flash a cover over the cloned background"
|
||||
fi
|
||||
printf 'captured\n' >"$stage/release"
|
||||
wait_phase still
|
||||
[[ $(capture_pixel still) == "FF00FF" ]] || fail "releasing the shared cover reveals the cloned background"
|
||||
printf 'done\n' >"$stage/release"
|
||||
wait "$qs_pid" || fail "the cloned background fixture exits cleanly" "$(cat "$stage/quickshell.log")"
|
||||
qs_pid=""
|
||||
if rg -q 'ReferenceError|TypeError|Unable to assign|Binding loop' "$stage/quickshell.log"; then
|
||||
fail "the cloned background fixture has no QML errors" "$(cat "$stage/quickshell.log")"
|
||||
fi
|
||||
done
|
||||
pass "a real clone is covered only for startup, with its scoped facade and consumed-login behavior intact"
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
require_compositor "background intro lifecycle test"
|
||||
require_command quickshell
|
||||
|
||||
stage=$(mktemp -d)
|
||||
trap 'rm -rf "$stage"' EXIT
|
||||
mkdir -p "$stage/services" "$stage/bin" "$stage/home"
|
||||
printf 'P6\n1 1\n255\n\377\377\377' >"$stage/cover.ppm"
|
||||
cp "$ROOT/shell/services/BackgroundIntro.qml" "$stage/services/"
|
||||
ln -s "$ROOT/shell/Commons" "$stage/Commons"
|
||||
cp "$SHELL_TEST_DIR/fixtures/background-intro-lifecycle/shell.qml" "$stage/shell.qml"
|
||||
cat >"$stage/bin/omarchy-theme-bg-boot-intro" <<'SH'
|
||||
#!/bin/bash
|
||||
printf 'intro\n' >>"$INTRO_TEST_LOG"
|
||||
sleep 2
|
||||
SH
|
||||
chmod +x "$stage/bin/omarchy-theme-bg-boot-intro"
|
||||
cat >"$stage/bin/owe" <<'SH'
|
||||
#!/bin/bash
|
||||
if [[ -f $INTRO_TEST_FRAME_READY ]]; then
|
||||
sleep 0.25
|
||||
printf '{"kind":"video","ready":true,"has_transition":false,"time_pos":0.05}\n'
|
||||
else
|
||||
printf '{"kind":"video","ready":true,"has_transition":true,"time_pos":0}\n'
|
||||
fi
|
||||
SH
|
||||
chmod +x "$stage/bin/owe"
|
||||
output=$(HOME="$stage/home" PATH="$stage/bin:$PATH" INTRO_TEST_LOG="$stage/starts" INTRO_TEST_COVER="$stage/cover.ppm" INTRO_TEST_FRAME_READY="$stage/video-ready" timeout 10 quickshell -p "$stage" --no-color 2>&1) || fail "background intro fixture exits cleanly" "$output"
|
||||
[[ $output == *"RESULT pass"* ]] || fail "background intro lifecycle assertions pass" "$output"
|
||||
if rg -q 'RESULT fail|ReferenceError|TypeError|Error:|Unable to assign|Binding loop' <<<"$output"; then
|
||||
fail "background intro fixture has no QML errors" "$output"
|
||||
fi
|
||||
[[ $(wc -l <"$stage/starts") == 1 ]] || fail "recreating the background service does not run another launcher"
|
||||
pass "the cover stays while waiting and remains off when OWE recreates the background service"
|
||||
Executable
+303
@@ -0,0 +1,303 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
|
||||
intro_home="$test_tmp/home"
|
||||
intro_runtime="$test_tmp/runtime"
|
||||
intro_state="$intro_home/.local/state/omarchy/current"
|
||||
theme_intro_dir="$intro_state/theme/backgrounds/intros"
|
||||
background="$intro_state/theme/backgrounds/road.webp"
|
||||
mkdir -p "$(dirname "$background")" "$theme_intro_dir" "$intro_runtime"
|
||||
printf 'matching still\n' >"$background"
|
||||
printf 'theme video\n' >"$theme_intro_dir/road.mp4"
|
||||
printf 'tokyo-night\n' >"$intro_state/theme.name"
|
||||
ln -s "$background" "$intro_state/background"
|
||||
|
||||
resolved=$(HOME="$intro_home" "$ROOT/bin/omarchy-theme-bg-boot-intro" --resolve-only)
|
||||
[[ $resolved == "$theme_intro_dir/road.mp4" ]] || fail "a sibling intro matches the current image by filename" "$resolved"
|
||||
rm "$theme_intro_dir/road.mp4"
|
||||
resolved=$(HOME="$intro_home" "$ROOT/bin/omarchy-theme-bg-boot-intro" --resolve-only)
|
||||
[[ -z $resolved ]] || fail "a background without a matching video has no intro" "$resolved"
|
||||
printf 'theme video\n' >"$theme_intro_dir/road.mp4"
|
||||
|
||||
custom_background="$intro_home/.config/omarchy/backgrounds/tokyo-night/road.webp"
|
||||
mkdir -p "${custom_background%/*}"
|
||||
printf 'different still with the same name\n' >"$custom_background"
|
||||
ln -nsf "$custom_background" "$intro_state/background"
|
||||
resolved=$(HOME="$intro_home" "$ROOT/bin/omarchy-theme-bg-boot-intro" --resolve-only)
|
||||
[[ -z $resolved ]] || fail "a different image directory does not inherit the theme intro" "$resolved"
|
||||
ln -nsf "$background" "$intro_state/background"
|
||||
|
||||
toggle="$intro_home/.local/state/omarchy/toggles/background-intros-off"
|
||||
marker="$intro_home/.local/state/omarchy/background-intro.session-id"
|
||||
command_bin="$test_tmp/bin"
|
||||
command_log="$test_tmp/command-log"
|
||||
mkdir -p "$command_bin"
|
||||
cat >"$command_bin/owe" <<'SH'
|
||||
#!/bin/bash
|
||||
printf 'owe: %s\n' "$*" >>"$COMMAND_LOG"
|
||||
case ${1:-} in
|
||||
intro)
|
||||
[[ -z ${COVER_READY_FILE:-} || -f $COVER_READY_FILE ]] || exit 1
|
||||
[[ ${OWE_FAIL:-} != "interrupted" && ${OWE_FAIL:-} != "unavailable" ]] || exit 1
|
||||
[[ -z ${OWE_READY_FILE:-} || -f $OWE_READY_FILE ]] || exit 1
|
||||
;;
|
||||
status)
|
||||
[[ ${OWE_FAIL:-} != "unavailable" ]] || exit 1
|
||||
[[ -z ${OWE_READY_FILE:-} || -f $OWE_READY_FILE ]] || exit 1
|
||||
printf '{"status":"ok"}\n'
|
||||
;;
|
||||
esac
|
||||
SH
|
||||
chmod +x "$command_bin/owe"
|
||||
cat >"$command_bin/hyprctl" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '{"option":"animations:enabled","int":%s,"bool":%s}\n' \
|
||||
"$([[ ${ANIMATIONS:-on} == on ]] && echo 1 || echo 0)" "$([[ ${ANIMATIONS:-on} == on ]] && echo true || echo false)"
|
||||
SH
|
||||
chmod +x "$command_bin/hyprctl"
|
||||
cat >"$command_bin/omarchy-shell" <<'SH'
|
||||
#!/bin/bash
|
||||
if [[ $2 == "themeIntroCoverStatus" ]]; then
|
||||
if [[ -n ${COVER_READY_FILE:-} && ! -f $COVER_READY_FILE ]]; then
|
||||
echo "loading"
|
||||
else
|
||||
echo "${COVER_STATUS:-ready}"
|
||||
fi
|
||||
fi
|
||||
SH
|
||||
chmod +x "$command_bin/omarchy-shell"
|
||||
|
||||
mkdir -p "$(dirname "$toggle")"
|
||||
resolved=$(PATH="$command_bin:$PATH" HOME="$intro_home" "$ROOT/bin/omarchy-theme-bg-boot-intro" --resolve-startup)
|
||||
[[ $resolved == "$theme_intro_dir/road.mp4" ]] || fail "OWE can prepare the selected intro while the shell starts" "$resolved"
|
||||
[[ ! -e $marker && ! -s $command_log ]] || fail "startup preparation does not consume the login or start playback"
|
||||
resolved=$(PATH="$command_bin:$PATH" HOME="$intro_home" ANIMATIONS=off "$ROOT/bin/omarchy-theme-bg-boot-intro" --resolve-startup)
|
||||
[[ -z $resolved ]] || fail "disabled animations skip startup preparation"
|
||||
touch "$toggle"
|
||||
resolved=$(PATH="$command_bin:$PATH" HOME="$intro_home" "$ROOT/bin/omarchy-theme-bg-boot-intro" --resolve-startup)
|
||||
[[ -z $resolved ]] || fail "disabled intros skip startup preparation"
|
||||
pass "speculative login preparation respects settings and leaves playback to the shell"
|
||||
resolved=$(PATH="$command_bin:$PATH" HOME="$intro_home" XDG_RUNTIME_DIR="$intro_runtime" COMMAND_LOG="$command_log" OMARCHY_SESSION_ID=disabled-boot "$ROOT/bin/omarchy-theme-bg-boot-intro")
|
||||
[[ -z $resolved && $(<"$marker") == "disabled-boot" ]] || fail "the global toggle consumes the current login without playing"
|
||||
rm "$toggle"
|
||||
resolved=$(PATH="$command_bin:$PATH" HOME="$intro_home" XDG_RUNTIME_DIR="$intro_runtime" COMMAND_LOG="$command_log" OMARCHY_SESSION_ID=disabled-boot "$ROOT/bin/omarchy-theme-bg-boot-intro")
|
||||
[[ -z $resolved ]] || fail "enabling midway through a login does not start a delayed intro" "$resolved"
|
||||
|
||||
: >"$command_log"
|
||||
PATH="$command_bin:$PATH" HOME="$intro_home" XDG_RUNTIME_DIR="$intro_runtime" COMMAND_LOG="$command_log" ANIMATIONS=off OMARCHY_SESSION_ID=still-boot "$ROOT/bin/omarchy-theme-bg-boot-intro"
|
||||
[[ $(<"$marker") == "still-boot" ]] || fail "with animations off the session is consumed"
|
||||
! grep -q '^owe: intro ' "$command_log" || fail "with animations off no intro plays"
|
||||
|
||||
rm "$marker"
|
||||
: >"$command_log"
|
||||
started=$SECONDS
|
||||
if ! PATH="$command_bin:$PATH" HOME="$intro_home" XDG_RUNTIME_DIR="$intro_runtime" COMMAND_LOG="$command_log" OWE_FAIL=unavailable OMARCHY_SESSION_ID=unavailable-boot "$ROOT/bin/omarchy-theme-bg-boot-intro"; then
|
||||
fail "an unavailable OWE settles startup rather than requesting another launcher"
|
||||
fi
|
||||
(( SECONDS - started >= 4 && SECONDS - started <= 7 )) || fail "startup waits only through the five-second window" "$((SECONDS - started))"
|
||||
[[ $(<"$marker") == "unavailable-boot" ]] || fail "an unavailable OWE still settles the session"
|
||||
! grep -q '^owe: intro ' "$command_log" || fail "an unavailable OWE never starts an intro"
|
||||
PATH="$command_bin:$PATH" HOME="$intro_home" XDG_RUNTIME_DIR="$intro_runtime" COMMAND_LOG="$command_log" OMARCHY_SESSION_ID=unavailable-boot "$ROOT/bin/omarchy-theme-bg-boot-intro"
|
||||
! grep -q '^owe: intro ' "$command_log" || fail "starting OWE after the deadline does not play a late intro"
|
||||
|
||||
rm "$marker"
|
||||
: >"$command_log"
|
||||
ready="$test_tmp/owe-ready"
|
||||
(sleep 1; touch "$ready") &
|
||||
ready_pid=$!
|
||||
PATH="$command_bin:$PATH" HOME="$intro_home" XDG_RUNTIME_DIR="$intro_runtime" COMMAND_LOG="$command_log" OWE_READY_FILE="$ready" OMARCHY_SESSION_ID=delayed-boot "$ROOT/bin/omarchy-theme-bg-boot-intro"
|
||||
wait "$ready_pid"
|
||||
[[ $(<"$marker") == "delayed-boot" ]] || fail "waiting for OWE keeps the session consumed"
|
||||
grep -Fxq "owe: intro --start first-frame $theme_intro_dir/road.mp4" "$command_log" || fail "a ready OWE starts on the video's first frame without revealing the still"
|
||||
|
||||
rm "$marker" "$ready"
|
||||
: >"$command_log"
|
||||
PATH="$command_bin:$PATH" HOME="$intro_home" XDG_RUNTIME_DIR="$intro_runtime" COMMAND_LOG="$command_log" OWE_READY_FILE="$ready" OMARCHY_SESSION_ID=restart-while-waiting "$ROOT/bin/omarchy-theme-bg-boot-intro" &
|
||||
waiting_pid=$!
|
||||
for attempt in {1..100}; do
|
||||
[[ -f $marker && $(<"$marker") == "restart-while-waiting" ]] && break
|
||||
sleep 0.01
|
||||
done
|
||||
if ! PATH="$command_bin:$PATH" HOME="$intro_home" XDG_RUNTIME_DIR="$intro_runtime" COMMAND_LOG="$command_log" OWE_READY_FILE="$ready" OMARCHY_SESSION_ID=restart-while-waiting timeout 1 "$ROOT/bin/omarchy-theme-bg-boot-intro"; then
|
||||
kill "$waiting_pid" 2>/dev/null || true
|
||||
wait "$waiting_pid" || true
|
||||
fail "a restarted shell reads the consumed session without waiting for the old launcher"
|
||||
fi
|
||||
touch "$ready"
|
||||
wait "$waiting_pid"
|
||||
|
||||
rm "$marker" "$ready"
|
||||
: >"$command_log"
|
||||
(sleep 0.5; touch "$toggle" "$ready") &
|
||||
ready_pid=$!
|
||||
PATH="$command_bin:$PATH" HOME="$intro_home" XDG_RUNTIME_DIR="$intro_runtime" COMMAND_LOG="$command_log" OWE_READY_FILE="$ready" OMARCHY_SESSION_ID=disabled-while-waiting "$ROOT/bin/omarchy-theme-bg-boot-intro"
|
||||
wait "$ready_pid"
|
||||
! grep -q '^owe: intro ' "$command_log" || fail "disabling intros while waiting cancels startup playback"
|
||||
rm "$toggle" "$ready" "$marker"
|
||||
: >"$command_log"
|
||||
(sleep 0.5; ln -nsf "$custom_background" "$intro_state/background"; touch "$ready") &
|
||||
ready_pid=$!
|
||||
PATH="$command_bin:$PATH" HOME="$intro_home" XDG_RUNTIME_DIR="$intro_runtime" COMMAND_LOG="$command_log" OWE_READY_FILE="$ready" OMARCHY_SESSION_ID=changed-while-waiting "$ROOT/bin/omarchy-theme-bg-boot-intro"
|
||||
wait "$ready_pid"
|
||||
! grep -q '^owe: intro ' "$command_log" || fail "changing the background while waiting cancels its old intro"
|
||||
ln -nsf "$background" "$intro_state/background"
|
||||
|
||||
rm "$marker"
|
||||
if PATH="$command_bin:$PATH" HOME="$intro_home" XDG_RUNTIME_DIR="$intro_runtime" COMMAND_LOG="$command_log" OWE_FAIL=interrupted OMARCHY_SESSION_ID=interrupted-boot "$ROOT/bin/omarchy-theme-bg-boot-intro"; then
|
||||
fail "an interrupted intro reports a failure"
|
||||
else
|
||||
status=$?
|
||||
(( status == 1 )) || fail "an interrupted intro does not request a retry" "$status"
|
||||
fi
|
||||
[[ $(<"$marker") == "interrupted-boot" ]] || fail "an intro interrupted after OWE accepted it still consumes the session"
|
||||
|
||||
rm "$marker"
|
||||
: >"$command_log"
|
||||
for index in {1..32}; do
|
||||
PATH="$command_bin:$PATH" HOME="$intro_home" XDG_RUNTIME_DIR="$intro_runtime" COMMAND_LOG="$command_log" OMARCHY_SESSION_ID=concurrent-boot "$ROOT/bin/omarchy-theme-bg-boot-intro" >"$test_tmp/output.$index" &
|
||||
intro_pids[$index]=$!
|
||||
done
|
||||
for intro_pid in "${intro_pids[@]}"; do
|
||||
wait "$intro_pid"
|
||||
done
|
||||
intro_count=$(grep -c '^owe: intro ' "$command_log")
|
||||
(( intro_count == 1 )) || fail "concurrent launchers start exactly one intro" "$intro_count"
|
||||
|
||||
pass "login intros settle the session once, wait briefly for OWE, and start on the first frame"
|
||||
|
||||
: >"$command_log"
|
||||
for session in first-login first-login second-login; do
|
||||
PATH="$command_bin:$PATH" HOME="$intro_home" XDG_RUNTIME_DIR="$intro_runtime" COMMAND_LOG="$command_log" \
|
||||
OMARCHY_SESSION_ID="" HYPRLAND_INSTANCE_SIGNATURE="$session" "$ROOT/bin/omarchy-theme-bg-boot-intro"
|
||||
done
|
||||
intro_count=$(grep -c '^owe: intro ' "$command_log")
|
||||
(( intro_count == 2 )) || fail "a new login replays the intro while a shell restart in the same session does not" "$intro_count"
|
||||
[[ $(<"$marker") == "second-login" ]] || fail "the marker identifies the latest compositor session"
|
||||
pass "logging out and back in replays the intro without requiring a reboot"
|
||||
|
||||
ln -s "$ROOT/bin/omarchy-theme-bg-boot-intro" "$command_bin/omarchy-theme-bg-boot-intro"
|
||||
|
||||
cat >"$command_bin/omarchy-notification-send" <<'SH'
|
||||
#!/bin/bash
|
||||
printf 'notification: %s\n' "$*" >>"$COMMAND_LOG"
|
||||
SH
|
||||
cat >"$command_bin/omarchy-theme-bg-current" <<'SH'
|
||||
#!/bin/bash
|
||||
printf 'Road\n'
|
||||
SH
|
||||
chmod +x "$command_bin"/omarchy-*
|
||||
|
||||
PATH="$command_bin:$PATH" HOME="$intro_home" COMMAND_LOG="$command_log" "$ROOT/bin/omarchy-theme-bg-intro" disable
|
||||
if PATH="$command_bin:$PATH" HOME="$intro_home" "$ROOT/bin/omarchy-theme-bg-intro" status; then
|
||||
fail "disabled background intros report a disabled status"
|
||||
fi
|
||||
PATH="$command_bin:$PATH" HOME="$intro_home" COMMAND_LOG="$command_log" "$ROOT/bin/omarchy-theme-bg-intro" enable
|
||||
PATH="$command_bin:$PATH" HOME="$intro_home" "$ROOT/bin/omarchy-theme-bg-intro" status || fail "enabled background intros report an enabled status"
|
||||
pass "background intros have one global on/off control"
|
||||
|
||||
: >"$command_log"
|
||||
before_marker=$(<"$marker")
|
||||
PATH="$command_bin:$PATH" HOME="$intro_home" COMMAND_LOG="$command_log" "$ROOT/bin/omarchy-theme-bg-boot-intro" --theme-switch "$background" "$(stat -Lc '%d:%i' "$intro_state/theme")"
|
||||
grep -Fxq 'owe: refresh' "$command_log" || fail "unprepared theme switching synchronizes OWE with the selected image"
|
||||
grep -Fxq "owe: intro --start first-frame $theme_intro_dir/road.mp4" "$command_log" || fail "theme switching plays its matching intro even after boot was consumed"
|
||||
: >"$command_log"
|
||||
PATH="$command_bin:$PATH" HOME="$intro_home" COMMAND_LOG="$command_log" "$ROOT/bin/omarchy-theme-bg-boot-intro" --theme-switch "$background" "$(stat -Lc '%d:%i' "$intro_state/theme")" true
|
||||
grep -Fxq "owe: intro --start first-frame --refresh $theme_intro_dir/road.mp4" "$command_log" || fail "prepared theme switching synchronizes and starts in one call"
|
||||
! grep -Fxq 'owe: refresh' "$command_log" || fail "prepared playback avoids an extra refresh call"
|
||||
cover_ready="$test_tmp/cover-ready"
|
||||
(sleep 0.2; touch "$cover_ready") &
|
||||
cover_pid=$!
|
||||
PATH="$command_bin:$PATH" HOME="$intro_home" COMMAND_LOG="$command_log" COVER_READY_FILE="$cover_ready" "$ROOT/bin/omarchy-theme-bg-boot-intro" --theme-switch "$background" "$(stat -Lc '%d:%i' "$intro_state/theme")" true || fail "playback waits for the outgoing cover before starting"
|
||||
wait "$cover_pid"
|
||||
: >"$command_log"
|
||||
if PATH="$command_bin:$PATH" HOME="$intro_home" COMMAND_LOG="$command_log" COVER_STATUS=error "$ROOT/bin/omarchy-theme-bg-boot-intro" --theme-switch "$background" "$(stat -Lc '%d:%i' "$intro_state/theme")" true; then
|
||||
fail "a failed cover requests the still fallback"
|
||||
fi
|
||||
! grep -q '^owe: intro ' "$command_log" || fail "a failed outgoing cover cannot expose an intro"
|
||||
[[ $(<"$marker") == "$before_marker" ]] || fail "theme switching does not reopen the session marker"
|
||||
: >"$command_log"
|
||||
PATH="$command_bin:$PATH" HOME="$intro_home" COMMAND_LOG="$command_log" "$ROOT/bin/omarchy-theme-bg-boot-intro" --theme-switch "$custom_background" "$(stat -Lc '%d:%i' "$intro_state/theme")"
|
||||
! grep -q '^owe: intro ' "$command_log" || fail "a superseded theme switch does not play an old intro"
|
||||
theme_id=$(stat -Lc '%d:%i' "$intro_state/theme")
|
||||
: >"$command_log"
|
||||
if PATH="$command_bin:$PATH" HOME="$intro_home" COMMAND_LOG="$command_log" ANIMATIONS=off "$ROOT/bin/omarchy-theme-bg-boot-intro" --theme-switch "$background" "$theme_id"; then
|
||||
fail "a current theme launch requests its still fallback when animations were disabled"
|
||||
fi
|
||||
! grep -q '^owe: intro ' "$command_log" || fail "a canceled theme intro does not play"
|
||||
mv "$intro_state/theme" "$intro_state/previous-theme"
|
||||
cp -r "$intro_state/previous-theme" "$intro_state/theme"
|
||||
: >"$command_log"
|
||||
PATH="$command_bin:$PATH" HOME="$intro_home" COMMAND_LOG="$command_log" "$ROOT/bin/omarchy-theme-bg-boot-intro" --theme-switch "$background" "$theme_id"
|
||||
! grep -q '^owe: intro ' "$command_log" || fail "reused filenames do not let a superseded theme launch play"
|
||||
pass "theme switches can play their selected intro without resetting login startup"
|
||||
|
||||
migration_home="$test_tmp/migration-home"
|
||||
migration_bin="$test_tmp/migration-bin"
|
||||
migration_calls="$test_tmp/migration-calls"
|
||||
mkdir -p "$migration_home/.local/state/omarchy/current" "$migration_bin"
|
||||
printf 'catppuccin\n' >"$migration_home/.local/state/omarchy/current/theme.name"
|
||||
cat >"$migration_bin/omarchy-theme-refresh" <<'SH'
|
||||
#!/bin/bash
|
||||
printf 'refresh\n' >>"$MIGRATION_CALLS"
|
||||
SH
|
||||
chmod +x "$migration_bin/omarchy-theme-refresh"
|
||||
|
||||
HOME="$migration_home" PATH="$migration_bin:$PATH" MIGRATION_CALLS="$migration_calls" OMARCHY_PATH="$ROOT" OMARCHY_SESSION_ID=migration-boot bash -euo pipefail "$ROOT/migrations/1788281348.sh" >/dev/null
|
||||
[[ $(<"$migration_calls") == "refresh" ]] || fail "the migration refreshes an active theme with packaged intros"
|
||||
[[ $(<"$migration_home/.local/state/omarchy/background-intro.session-id") == "migration-boot" ]] || fail "the migration defers a newly installed intro until the next login"
|
||||
|
||||
mkdir -p "$intro_runtime/hypr/older-session" "$intro_runtime/hypr/current-session"
|
||||
touch -d '2 minutes ago' "$intro_runtime/hypr/older-session"
|
||||
HOME="$migration_home" PATH="$migration_bin:$PATH" MIGRATION_CALLS="$migration_calls" OMARCHY_PATH="$ROOT" \
|
||||
OMARCHY_SESSION_ID="" HYPRLAND_INSTANCE_SIGNATURE="" XDG_RUNTIME_DIR="$intro_runtime" \
|
||||
bash -euo pipefail "$ROOT/migrations/1788281348.sh" >/dev/null
|
||||
[[ $(<"$migration_home/.local/state/omarchy/background-intro.session-id") == "current-session" ]] || fail "an update outside Hyprland consumes the session that shell restart will use"
|
||||
HOME="$migration_home" PATH="$migration_bin:$PATH" MIGRATION_CALLS="$migration_calls" OMARCHY_PATH="$ROOT" \
|
||||
OMARCHY_SESSION_ID="" HYPRLAND_INSTANCE_SIGNATURE="" XDG_RUNTIME_DIR="$test_tmp/no-session" \
|
||||
bash -euo pipefail "$ROOT/migrations/1788281348.sh" >/dev/null
|
||||
[[ $(<"$migration_home/.local/state/omarchy/background-intro.session-id") == "current-session" ]] || fail "a headless update leaves the previous session marker alone"
|
||||
|
||||
pass "the migration does not start a login intro during an update"
|
||||
|
||||
packaged_pairs=0
|
||||
for intro in "$ROOT"/themes/*/backgrounds/intros/*.mp4; do
|
||||
[[ -f $intro ]] || continue
|
||||
backgrounds=${intro%/intros/*}
|
||||
stem=${intro##*/}
|
||||
stem=${stem%.mp4}
|
||||
matches=0
|
||||
for extension in jpg jpeg png bmp webp; do
|
||||
[[ ! -f $backgrounds/$stem.$extension ]] || ((++matches))
|
||||
done
|
||||
(( matches == 1 )) || fail "$intro has exactly one matching still image" "$matches"
|
||||
((++packaged_pairs))
|
||||
done
|
||||
(( packaged_pairs > 0 )) || fail "no packaged theme intros were found"
|
||||
pass "theme intro filenames match their sibling background images"
|
||||
|
||||
# Picker preparation is speculative and must not consume login startup or play.
|
||||
prepare_root="$test_tmp/prepare-repo"
|
||||
prepare_backgrounds="$prepare_root/themes/demo/backgrounds"
|
||||
mkdir -p "$prepare_backgrounds/intros" "$intro_home/.local/state/omarchy/theme-backgrounds"
|
||||
printf 'first still\n' >"$prepare_backgrounds/1-first.webp"
|
||||
printf 'remembered still\n' >"$prepare_backgrounds/2-remembered.webp"
|
||||
printf 'remembered video\n' >"$prepare_backgrounds/intros/2-remembered.mp4"
|
||||
printf '%s\n' "$intro_state/theme/backgrounds/2-remembered.webp" >"$intro_home/.local/state/omarchy/theme-backgrounds/demo"
|
||||
rm -f "$toggle"
|
||||
: >"$command_log"
|
||||
marker_before=$(<"$marker")
|
||||
PATH="$command_bin:$PATH" HOME="$intro_home" OMARCHY_PATH="$prepare_root" COMMAND_LOG="$command_log" "$ROOT/bin/omarchy-theme-bg-boot-intro" --prepare-theme demo
|
||||
grep -Fxq "owe: intro-prepare $prepare_backgrounds/intros/2-remembered.mp4" "$command_log" || fail "picker preparation follows the theme's remembered background"
|
||||
! grep -q '^owe: intro ' "$command_log" || fail "preparation does not play an intro"
|
||||
[[ $(<"$marker") == "$marker_before" ]] || fail "preparation leaves login startup alone"
|
||||
touch "$toggle"
|
||||
: >"$command_log"
|
||||
PATH="$command_bin:$PATH" HOME="$intro_home" OMARCHY_PATH="$prepare_root" COMMAND_LOG="$command_log" "$ROOT/bin/omarchy-theme-bg-boot-intro" --prepare-theme demo
|
||||
[[ ! -s $command_log ]] || fail "disabled intros do not warm the renderer"
|
||||
pass "picker preparation respects the remembered background, login startup, and intro toggle"
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
require_compositor "pending background theme cancellation"
|
||||
require_command quickshell
|
||||
require_command python3
|
||||
require_command magick
|
||||
|
||||
stage=$(mktemp -d)
|
||||
trap 'rm -rf "$stage"' EXIT
|
||||
mkdir -p "$stage/home/.local/state/omarchy/current"
|
||||
ln -s "$ROOT/shell/plugins/background" "$stage/background"
|
||||
ln -s "$ROOT/shell/Commons" "$stage/Commons"
|
||||
ln -s "$ROOT/shell/Ui" "$stage/Ui"
|
||||
magick -size 128x128 xc:magenta "$stage/still.png"
|
||||
ln -s "$stage/still.png" "$stage/home/.local/state/omarchy/current/background"
|
||||
python3 - "$ROOT/shell/shell.qml" "$SHELL_TEST_DIR/fixtures/background-pending-theme/shell.qml" "$stage/shell.qml" <<'PY'
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
source = Path(sys.argv[1]).read_text()
|
||||
method = re.search(r'^ function applyTheme\([^\n]+\n.*?^ }', source, re.M | re.S)
|
||||
assert method, 'real shell applyTheme method not found'
|
||||
fixture = Path(sys.argv[2]).read_text()
|
||||
Path(sys.argv[3]).write_text(fixture.replace(' // APPLY_THEME_FUNCTION', method.group()))
|
||||
PY
|
||||
output=$(HOME="$stage/home" timeout 10 quickshell -p "$stage" --no-color 2>&1) || fail "pending theme fixture exits cleanly" "$output"
|
||||
[[ $output == *"RESULT pass"* && $output != *"RESULT fail"* ]] || fail "the authoritative palette survives the previous background fallback timer" "$output"
|
||||
pass "a pending still transition cannot restore old colors after the current theme is applied"
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(dirname "$0")/base-test.sh"
|
||||
require_compositor "failed background startup"
|
||||
require_command quickshell
|
||||
|
||||
stage=$(mktemp -d)
|
||||
trap 'rm -rf "$stage"' EXIT
|
||||
mkdir -p "$stage/bin" "$stage/home/.local/state/omarchy/current"
|
||||
ln -s "$stage/deleted.png" "$stage/home/.local/state/omarchy/current/background"
|
||||
for component in Commons Ui services; do
|
||||
ln -s "$ROOT/shell/$component" "$stage/$component"
|
||||
done
|
||||
ln -s "$ROOT/shell/plugins/background" "$stage/background"
|
||||
cp "$SHELL_TEST_DIR/fixtures/background-startup-failure/shell.qml" "$stage/shell.qml"
|
||||
cat >"$stage/bin/omarchy-theme-bg-boot-intro" <<'SH'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
SH
|
||||
chmod +x "$stage/bin/omarchy-theme-bg-boot-intro"
|
||||
|
||||
output=$(HOME="$stage/home" PATH="$stage/bin:$PATH" timeout 13 quickshell -p "$stage" --no-color 2>&1) || fail "failed background startup exits cleanly" "$output"
|
||||
[[ $output == *"RESULT pass"* ]] || fail "a missing wallpaper reveals the desktop within the startup deadline" "$output"
|
||||
if rg -q 'RESULT fail|ReferenceError|TypeError|Unable to assign|Binding loop' <<<"$output"; then
|
||||
fail "failed background startup has no QML errors" "$output"
|
||||
fi
|
||||
pass "a missing wallpaper reveals the desktop within the startup deadline"
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
require_compositor "background startup test"
|
||||
require_command quickshell
|
||||
|
||||
stage=$(mktemp -d)
|
||||
trap 'rm -rf "$stage"' EXIT
|
||||
mkdir -p "$stage/services" "$stage/bin" "$stage/home/.local/state/omarchy/current"
|
||||
printf 'P6\n1 1\n255\n\377\000\377' >"$stage/still.ppm"
|
||||
ln -s "$stage/still.ppm" "$stage/home/.local/state/omarchy/current/background"
|
||||
ln -s "$ROOT/shell/Commons" "$stage/Commons"
|
||||
cp "$ROOT/shell/services/BackgroundIntro.qml" "$stage/services/"
|
||||
cp "$SHELL_TEST_DIR/fixtures/background-startup/shell.qml" "$stage/shell.qml"
|
||||
cat >"$stage/bin/omarchy-theme-bg-boot-intro" <<'SH'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
SH
|
||||
chmod +x "$stage/bin/omarchy-theme-bg-boot-intro"
|
||||
cat >"$stage/bin/hyprctl" <<'SH'
|
||||
#!/bin/bash
|
||||
if [[ $1 == "eval" ]]; then
|
||||
printf '%s\n' "$*" >>"$STARTUP_CURSOR_LOG"
|
||||
else
|
||||
/usr/bin/hyprctl "$@"
|
||||
fi
|
||||
SH
|
||||
chmod +x "$stage/bin/hyprctl"
|
||||
: >"$stage/cursor.log"
|
||||
output=$(HOME="$stage/home" PATH="$stage/bin:$PATH" STARTUP_CURSOR_LOG="$stage/cursor.log" timeout 6 quickshell -p "$stage" --no-color 2>&1) || fail "background startup fixture exits cleanly" "$output"
|
||||
[[ $output == *"RESULT pass"* ]] || fail "the wallpaper restores before background plugin loading" "$output"
|
||||
if rg -q 'RESULT fail|ReferenceError|TypeError|Error:|Unable to assign|Binding loop' <<<"$output"; then
|
||||
fail "background startup fixture has no QML errors" "$output"
|
||||
fi
|
||||
pass "fresh startup hides the desktop until both the background and bar are ready"
|
||||
pass "the startup cursor stays hidden until the desktop begins fading in"
|
||||
@@ -282,7 +282,7 @@ assert(
|
||||
'bar uses nearest insertion targeting for widget and free-space drops'
|
||||
)
|
||||
assert(
|
||||
/component DragGhostPanel:[\s\S]*?readonly property var targetRect: root\.barDragTargetGeometry[\s\S]*?color: Color\.accent/.test(barSource),
|
||||
/component DragGhostPanel:[\s\S]*?readonly property var targetRect: root\.barDragTargetGeometry[\s\S]*?color: Commons\.Color\.accent/.test(barSource),
|
||||
'bar draws the insertion marker above the bar in the drag overlay'
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
migration="$ROOT/migrations/1791376887.sh"
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
|
||||
home="$test_tmp/home"
|
||||
config_dir="$home/.config"
|
||||
mkdir -p "$config_dir" "$test_tmp/targets"
|
||||
|
||||
printf '%s\n' '--enable-features=TouchpadOverscrollHistoryNavigation' >"$config_dir/chromium-flags.conf"
|
||||
printf '%s\n' '--enable-features=' >"$config_dir/chrome-flags.conf"
|
||||
printf '%s' '--ozone-platform=wayland' >"$config_dir/microsoft-edge-stable-flags.conf"
|
||||
printf '%s\n' '--enable-features=BraveFeature' >"$test_tmp/targets/brave-flags.conf"
|
||||
ln -s "$test_tmp/targets/brave-flags.conf" "$config_dir/brave-flags.conf"
|
||||
|
||||
run_migration() {
|
||||
HOME="$home" bash -euo pipefail "$migration" >/dev/null
|
||||
}
|
||||
|
||||
run_migration
|
||||
run_migration
|
||||
|
||||
grep -Fxq -- '--enable-features=TouchpadOverscrollHistoryNavigation,OverlayScrollbar' "$config_dir/chromium-flags.conf" ||
|
||||
fail "the migration preserves existing Chromium features"
|
||||
overlay_count=$(grep -o 'OverlayScrollbar' "$config_dir/chromium-flags.conf" | wc -l)
|
||||
(( overlay_count == 1 )) || fail "the migration does not duplicate the Chromium feature"
|
||||
pass "the migration preserves existing features and can run twice"
|
||||
|
||||
[[ $(cat "$config_dir/chrome-flags.conf") == '--enable-features=OverlayScrollbar' ]] ||
|
||||
fail "the migration fills an empty feature list"
|
||||
[[ $(cat "$config_dir/microsoft-edge-stable-flags.conf") == $'--ozone-platform=wayland\n--enable-features=OverlayScrollbar' ]] ||
|
||||
fail "the migration adds a feature list with a missing trailing newline"
|
||||
pass "the migration handles empty and missing feature lists"
|
||||
|
||||
[[ -L $config_dir/brave-flags.conf ]] || fail "the migration preserves symlinked browser flags"
|
||||
grep -Fxq -- '--enable-features=BraveFeature,OverlayScrollbar' "$config_dir/brave-flags.conf" ||
|
||||
fail "the migration updates the target of symlinked browser flags"
|
||||
pass "the migration follows symlinked browser flags"
|
||||
|
||||
[[ ! -e $config_dir/brave-origin-flags.conf ]] || fail "the migration leaves missing browser flags absent"
|
||||
pass "the migration skips missing browser flags"
|
||||
@@ -0,0 +1,342 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
|
||||
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
export HOME="$test_tmp/home" XDG_CONFIG_HOME="$test_tmp/config" OMARCHY_PATH="$ROOT"
|
||||
export DICTATION_LOG="$test_tmp/calls" DICTATION_INSTALLED="voxtype superwhisper"
|
||||
mkdir -p "$test_tmp/bin" "$XDG_CONFIG_HOME/omarchy/defaults"
|
||||
export PATH="$test_tmp/bin:$ROOT/bin:$PATH"
|
||||
config="$XDG_CONFIG_HOME/omarchy/defaults/dictation"
|
||||
|
||||
cat > "$test_tmp/bin/omarchy-cmd-present" <<'SH'
|
||||
#!/bin/bash
|
||||
case "$1" in
|
||||
omarchy-dictation-*|omarchy-install-dictation-*) command -v "$1" >/dev/null ;;
|
||||
*) [[ " $DICTATION_INSTALLED " == *" $1 "* ]] ;;
|
||||
esac
|
||||
SH
|
||||
cat > "$test_tmp/bin/omarchy-cmd-missing" <<'SH'
|
||||
#!/bin/bash
|
||||
! omarchy-cmd-present "$1"
|
||||
SH
|
||||
cat > "$test_tmp/bin/omarchy-launch-floating-terminal-with-presentation" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$*" > "$DICTATION_INSTALL_LOG"
|
||||
SH
|
||||
export DICTATION_INSTALL_LOG="$test_tmp/install"
|
||||
for backend in voxtype superwhisper; do
|
||||
cat > "$test_tmp/bin/$backend" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s %s\n' "${0##*/}" "$*" >> "$DICTATION_LOG"
|
||||
if [[ $1 == "status" ]]; then echo "${VOXTYPE_STATUS:-idle}"; exit 0; fi
|
||||
if [[ -n ${FAIL_SETUP_STEP:-} && $* == "$FAIL_SETUP_STEP" ]]; then exit 7; fi
|
||||
if [[ ${0##*/} == "superwhisper" && $1 == "shortcuts" && ${DICTATION_EXIT:-0} == 0 ]]; then
|
||||
if [[ $2 == "show" ]]; then
|
||||
cat "$SHORTCUT_STATE"
|
||||
elif [[ $2 == "set" ]]; then
|
||||
chord=$4
|
||||
[[ $chord != "none" ]] || chord=""
|
||||
updated=$(jq --arg action "$3" --arg chord "$chord" '.shortcuts[$action] = $chord' "$SHORTCUT_STATE")
|
||||
jq -e '.shortcuts | .hold != "RightAlt" and (.hold != "" or .toggle != "")' <<< "$updated" >/dev/null || exit 1
|
||||
printf '%s\n' "$updated" > "$SHORTCUT_STATE"
|
||||
fi
|
||||
fi
|
||||
exit "${DICTATION_EXIT:-0}"
|
||||
SH
|
||||
done
|
||||
for command in omarchy-pkg-add omarchy-notification-send hyprctl omarchy-restart-shell; do
|
||||
cat > "$test_tmp/bin/$command" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s %s\n' "${0##*/}" "$*" >> "$DICTATION_LOG"
|
||||
exit "${SETUP_EXIT:-0}"
|
||||
SH
|
||||
done
|
||||
cat > "$test_tmp/bin/systemctl" <<'SH'
|
||||
#!/bin/bash
|
||||
if [[ $* == "--user is-active --quiet superwhisper.service" ]]; then
|
||||
[[ ${DICTATION_SERVICE_ACTIVE:-1} == 1 ]]
|
||||
else
|
||||
printf '%s %s\n' "${0##*/}" "$*" >> "$DICTATION_LOG"
|
||||
exit "${SERVICE_EXIT:-0}"
|
||||
fi
|
||||
SH
|
||||
cat > "$test_tmp/bin/omarchy-hw-vulkan" <<'SH'
|
||||
#!/bin/bash
|
||||
exit 1
|
||||
SH
|
||||
cat > "$test_tmp/bin/gum" <<'SH'
|
||||
#!/bin/bash
|
||||
exit "${CONFIRM_EXIT:-0}"
|
||||
SH
|
||||
cat > "$test_tmp/bin/bash" <<'SH'
|
||||
#!/bin/bash
|
||||
if [[ $1 == "/usr/share/superwhisper/setup-user" ]]; then
|
||||
printf '%s\n' setup-user >> "$DICTATION_LOG"
|
||||
exit "${SETUP_EXIT:-0}"
|
||||
else
|
||||
exec /bin/bash "$@"
|
||||
fi
|
||||
SH
|
||||
chmod +x "$test_tmp/bin/"*
|
||||
|
||||
if omarchy-dictation start 2> "$test_tmp/error"; then
|
||||
fail "unset backend must fail even when providers are installed"
|
||||
fi
|
||||
[[ ! -e $DICTATION_LOG ]] || fail "recording never configures or installs a backend"
|
||||
pass "dictation requires explicit configuration"
|
||||
omarchy-default-dictation superwhisper
|
||||
[[ $(cat "$DICTATION_INSTALL_LOG") == "omarchy-install-dictation-superwhisper" ]] || fail "Defaults launches backend installer"
|
||||
[[ ! -e $config ]] || fail "Defaults does not select before setup succeeds"
|
||||
if omarchy-default-dictation unknown > "$test_tmp/output" 2>&1; then fail "missing installer must fail"; fi
|
||||
if omarchy-default-dictation '../invalid' > "$test_tmp/output" 2>&1; then fail "invalid backend must fail"; fi
|
||||
pass "Defaults delegates setup without prematurely selecting a backend"
|
||||
|
||||
|
||||
for backend in voxtype superwhisper; do
|
||||
printf '%s\n' "$backend" > "$config"
|
||||
[[ $(omarchy-default-dictation) == "$backend" ]] || fail "configured backend is readable"
|
||||
: > "$DICTATION_LOG"
|
||||
for action in start stop toggle; do omarchy dictation "$action"; done
|
||||
if [[ $backend == "voxtype" ]]; then
|
||||
expected=$'voxtype record start\nvoxtype status\nvoxtype record stop\nvoxtype record toggle'
|
||||
else
|
||||
expected=$'superwhisper start\nsuperwhisper stop\nsuperwhisper record'
|
||||
fi
|
||||
[[ $(cat "$DICTATION_LOG") == "$expected" ]] || fail "adapter dispatches recording for $backend"
|
||||
done
|
||||
pass "built-in adapters implement start stop and toggle"
|
||||
|
||||
cat > "$test_tmp/bin/omarchy-dictation-future-backend" <<'SH'
|
||||
#!/bin/bash
|
||||
[[ ! -e /proc/$$/fd/9 ]] || exit 90
|
||||
printf '%s\n' "$1" >> "$DICTATION_LOG"
|
||||
exit "${DICTATION_EXIT:-0}"
|
||||
SH
|
||||
chmod +x "$test_tmp/bin/omarchy-dictation-future-backend"
|
||||
cat > "$test_tmp/bin/omarchy-install-dictation-future-backend" <<'SH'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
SH
|
||||
chmod +x "$test_tmp/bin/omarchy-install-dictation-future-backend"
|
||||
omarchy-default-dictation future-backend
|
||||
[[ $(cat "$DICTATION_INSTALL_LOG") == "omarchy-install-dictation-future-backend" ]] || fail "third backend installer requires no selector changes"
|
||||
printf '%s\n' future-backend > "$config"
|
||||
: > "$DICTATION_LOG"
|
||||
for action in start stop toggle; do omarchy-dictation "$action"; done
|
||||
[[ $(cat "$DICTATION_LOG") == $'start\nstop\ntoggle' ]] || fail "third backend requires no dispatcher changes"
|
||||
result=0
|
||||
DICTATION_EXIT=7 omarchy-dictation start || result=$?
|
||||
(( result == 7 )) || fail "backend failures propagate"
|
||||
rm "$test_tmp/bin/omarchy-dictation-future-backend"
|
||||
if omarchy-dictation start 2> "$test_tmp/error"; then fail "missing adapter must fail"; fi
|
||||
printf '%s\n' '../invalid' > "$config"
|
||||
if omarchy-dictation start 2> "$test_tmp/error"; then fail "invalid backend must fail"; fi
|
||||
pass "external adapters follow the same protocol and errors propagate"
|
||||
|
||||
printf '%s\n' voxtype > "$config"
|
||||
export SHORTCUT_STATE="$test_tmp/shortcuts.json"
|
||||
printf '%s\n' '{"shortcuts":{"hold":"RightAlt","toggle":"","cancel":"Escape"}}' > "$SHORTCUT_STATE"
|
||||
if SETUP_EXIT=1 omarchy-install-dictation-superwhisper > "$test_tmp/output" 2>&1; then fail "failed installation must fail"; fi
|
||||
[[ $(cat "$config") == "voxtype" ]] || fail "failed setup must preserve selection"
|
||||
if DICTATION_EXIT=7 omarchy-install-dictation-superwhisper > "$test_tmp/output" 2>&1; then fail "failed shortcut setup must fail"; fi
|
||||
[[ $(cat "$config") == "voxtype" ]] || fail "failed shortcuts must preserve selection"
|
||||
omarchy-install-dictation-superwhisper
|
||||
[[ $(omarchy-default-dictation) == "superwhisper" ]] || fail "installer selects Superwhisper"
|
||||
jq -e '.shortcuts | .hold == "" and .toggle == "Alt+Space"' "$SHORTCUT_STATE" >/dev/null || fail "installer repairs conflicting hold-only profiles"
|
||||
printf '%s\n' '{"shortcuts":{"hold":"RightAlt","toggle":"RightSuper","cancel":"Escape"}}' > "$SHORTCUT_STATE"
|
||||
omarchy-install-dictation-superwhisper
|
||||
jq -e '.shortcuts | .hold == "" and .toggle == "Alt+Space"' "$SHORTCUT_STATE" >/dev/null || fail "installer repairs conflicting profiles with an existing toggle"
|
||||
omarchy-install-dictation-superwhisper
|
||||
pass "Superwhisper installer owns setup and saves selection only after success"
|
||||
|
||||
DICTATION_INSTALLED=superwhisper
|
||||
CONFIRM_EXIT=1 omarchy-install-dictation-voxtype
|
||||
[[ $(cat "$config") == "superwhisper" ]] || fail "cancelled installation preserves selection"
|
||||
omarchy-install-dictation-voxtype
|
||||
[[ $(omarchy-default-dictation) == "voxtype" ]] || fail "installer selects Voxtype"
|
||||
DICTATION_INSTALLED="voxtype superwhisper"
|
||||
: > "$DICTATION_LOG"
|
||||
omarchy-install-dictation-voxtype
|
||||
grep -q 'voxtype setup --download --no-post-install' "$DICTATION_LOG" || fail "installed Voxtype retries model setup"
|
||||
printf '%s\n' 'custom model configuration' > "$XDG_CONFIG_HOME/voxtype/config.toml"
|
||||
printf '%s\n' superwhisper > "$config"
|
||||
for step in 'setup --download --no-post-install' 'setup systemd'; do
|
||||
if FAIL_SETUP_STEP="$step" omarchy-install-dictation-voxtype > "$test_tmp/output" 2>&1; then fail "incomplete setup must fail"; fi
|
||||
[[ $(cat "$config") == "superwhisper" ]] || fail "incomplete setup preserves selection"
|
||||
done
|
||||
if SERVICE_EXIT=1 omarchy-install-dictation-voxtype > "$test_tmp/output" 2>&1; then fail "service failure must fail"; fi
|
||||
[[ $(cat "$config") == "superwhisper" ]] || fail "service failure preserves selection"
|
||||
omarchy-install-dictation-voxtype
|
||||
[[ $(cat "$config") == "voxtype" ]] || fail "retry finishes installed Voxtype setup"
|
||||
[[ $(cat "$XDG_CONFIG_HOME/voxtype/config.toml") == "custom model configuration" ]] || fail "retry preserves custom configuration"
|
||||
pass "Voxtype retries incomplete model and service setup without replacing configuration"
|
||||
|
||||
: > "$DICTATION_LOG"
|
||||
if DICTATION_EXIT=7 omarchy-dictation-use superwhisper > "$test_tmp/output" 2>&1; then fail "failed stop must prevent switching"; fi
|
||||
[[ $(cat "$config") == "voxtype" ]] || fail "failed stop preserves selection"
|
||||
omarchy-dictation-use superwhisper
|
||||
[[ $(cat "$DICTATION_LOG") == $'voxtype status\nvoxtype record stop\nvoxtype status\nvoxtype record stop' ]] || fail "switch stops the previous backend"
|
||||
[[ $(cat "$config") == "superwhisper" ]] || fail "successful stop permits switching"
|
||||
: > "$DICTATION_LOG"
|
||||
omarchy-dictation-use superwhisper
|
||||
[[ ! -s $DICTATION_LOG ]] || fail "reselecting does not interrupt recording"
|
||||
printf '%s\n' voxtype > "$config"
|
||||
VOXTYPE_STATUS=stopped omarchy-dictation-use superwhisper
|
||||
[[ $(cat "$config") == "superwhisper" ]] || fail "a stopped Voxtype daemon permits switching"
|
||||
printf '%s\n' superwhisper > "$config"
|
||||
DICTATION_SERVICE_ACTIVE=0 omarchy-dictation-use voxtype
|
||||
[[ $(cat "$config") == "voxtype" ]] || fail "a stopped Superwhisper service permits switching"
|
||||
printf '%s\n' removed-backend > "$config"
|
||||
omarchy-dictation-use voxtype
|
||||
[[ $(cat "$config") == "voxtype" ]] || fail "a removed adapter permits recovery"
|
||||
pass "switching stops recordings, preserves failed stops, and recovers unavailable providers"
|
||||
|
||||
# Readers may run throughout a switch, but must never observe a truncated file.
|
||||
(
|
||||
for ((i = 0; i < 200; i++)); do
|
||||
selection=$(cat "$config")
|
||||
[[ $selection == "voxtype" || $selection == "superwhisper" ]] || exit 1
|
||||
done
|
||||
) &
|
||||
reader=$!
|
||||
for ((i = 0; i < 20; i++)); do
|
||||
omarchy-dictation-use voxtype
|
||||
omarchy-dictation-use superwhisper
|
||||
done
|
||||
wait "$reader" || fail "concurrent readers always see a complete selection"
|
||||
printf '%s\n' voxtype > "$config"
|
||||
pass "selection updates are atomic for concurrent readers"
|
||||
|
||||
lua <<'LUA'
|
||||
local root = os.getenv("ROOT")
|
||||
local configured, bindings = false, {}
|
||||
o = {
|
||||
shell_succeeds = function(command)
|
||||
assert(command == "omarchy-default-dictation")
|
||||
return configured
|
||||
end,
|
||||
bind = function(...) table.insert(bindings, {...}) end,
|
||||
}
|
||||
dofile(root .. "/default/hypr/bindings/dictation.lua")
|
||||
assert(#bindings == 0, "unconfigured dictation leaves application keys available")
|
||||
configured = true
|
||||
dofile(root .. "/default/hypr/bindings/dictation.lua")
|
||||
assert(#bindings == 5, "any configured backend receives the same bindings")
|
||||
LUA
|
||||
pass "dictation shortcuts require a selection without limiting backend names"
|
||||
|
||||
# Fresh users receive the backend choice through the settings package's skel.
|
||||
cp "$ROOT/config/omarchy/defaults/dictation" "$config"
|
||||
[[ $(omarchy-default-dictation) == "superwhisper" ]] || fail "fresh users default to Superwhisper"
|
||||
printf '%s\n' voxtype > "$config"
|
||||
pass "fresh users get Superwhisper as their default backend"
|
||||
|
||||
lua <<'LUA'
|
||||
local root = os.getenv("ROOT")
|
||||
package.path = root .. "/?.lua;" .. package.path
|
||||
package.loaded["default.hypr.paths"] = { config_home = "/config" }
|
||||
local selected, available, loaded = "superwhisper", true, {}
|
||||
local missing = false
|
||||
o = { cmd_missing = function() return missing end }
|
||||
local real_open, real_popen, real_dofile = io.open, io.popen, dofile
|
||||
io.popen = function(command)
|
||||
assert(command == "omarchy-default-dictation 2>/dev/null")
|
||||
return { read = function() return selected end, close = function() end }
|
||||
end
|
||||
io.open = function(path)
|
||||
assert(path == "/config/" .. selected .. "/shortcuts.lua")
|
||||
if available then return { close = function() end } end
|
||||
end
|
||||
dofile = function(path) table.insert(loaded, path) end
|
||||
local function load_backend()
|
||||
package.loaded["default.hypr.dictation-backend"] = nil
|
||||
require("default.hypr.dictation-backend")
|
||||
end
|
||||
load_backend()
|
||||
assert(loaded[1] == "/config/superwhisper/shortcuts.lua")
|
||||
selected, available = "voxtype", false
|
||||
load_backend()
|
||||
assert(#loaded == 1, "a backend with no integration should need no config")
|
||||
selected, available = "future-backend", true
|
||||
load_backend()
|
||||
assert(loaded[2] == "/config/future-backend/shortcuts.lua", "integration must not be limited to named providers")
|
||||
missing = true
|
||||
load_backend()
|
||||
assert(#loaded == 2, "an uninstalled backend must not load a stale bridge")
|
||||
missing = false
|
||||
dofile = function() error("broken generated bridge") end
|
||||
load_backend()
|
||||
assert(#loaded == 2, "a broken bridge must not abort the desktop configuration")
|
||||
dofile = function(path) table.insert(loaded, path) end
|
||||
selected = "../../outside"
|
||||
load_backend()
|
||||
assert(#loaded == 2, "a backend must not escape the config directory")
|
||||
selected = nil
|
||||
load_backend()
|
||||
assert(#loaded == 2, "no installed backend must leave configuration usable")
|
||||
io.open, io.popen, dofile = real_open, real_popen, real_dofile
|
||||
LUA
|
||||
pass "desktop integration follows the selected backend without personal Hyprland config"
|
||||
|
||||
cat > "$test_tmp/bin/systemctl" <<'SH'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
SH
|
||||
cat > "$test_tmp/bin/omarchy-pkg-drop" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$*" >> "$DICTATION_LOG"
|
||||
exit "${REMOVE_EXIT:-0}"
|
||||
SH
|
||||
chmod +x "$test_tmp/bin/systemctl" "$test_tmp/bin/omarchy-pkg-drop"
|
||||
omarchy-remove-dictation-voxtype
|
||||
[[ ! -e $config ]] || fail "removing Voxtype clears selection"
|
||||
if omarchy-default-dictation 2> "$test_tmp/error"; then fail "removal must not auto-select another backend"; fi
|
||||
pass "removal clears selection without automatic fallback"
|
||||
|
||||
cat > "$test_tmp/bin/omarchy-shell" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$*" >> "$DICTATION_LOG"
|
||||
SH
|
||||
chmod +x "$test_tmp/bin/omarchy-shell"
|
||||
export XDG_DATA_HOME="$test_tmp/data"
|
||||
mkdir -p "$HOME/.local/bin" "$XDG_DATA_HOME/superwhisper/app" "$XDG_DATA_HOME/superwhisper/models" "$XDG_CONFIG_HOME/omarchy/plugins" "$HOME/.agents/skills" "$HOME/.claude/skills"
|
||||
ln -s /usr/bin/superwhisper "$HOME/.local/bin/superwhisper"
|
||||
ln -s /opt/superwhisper "$XDG_DATA_HOME/superwhisper/app/current"
|
||||
ln -s /opt/superwhisper/assets/omarchy-plugin/superwhisper-panel "$XDG_CONFIG_HOME/omarchy/plugins/superwhisper-panel"
|
||||
ln -s /opt/superwhisper/assets/agent-skill/superwhisper "$HOME/.agents/skills/superwhisper"
|
||||
ln -s /custom/skill "$HOME/.claude/skills/superwhisper"
|
||||
printf '%s\n' retained > "$XDG_DATA_HOME/superwhisper/models/model"
|
||||
printf '%s\n' superwhisper > "$config"
|
||||
if REMOVE_EXIT=1 omarchy-remove-dictation-superwhisper > "$test_tmp/output" 2>&1; then
|
||||
fail "failed package removal must fail"
|
||||
fi
|
||||
[[ -L $HOME/.local/bin/superwhisper && $(cat "$config") == "superwhisper" ]] || fail "failed removal preserves selection and links"
|
||||
omarchy-remove-dictation-superwhisper
|
||||
[[ ! -e $config ]] || fail "Superwhisper removal clears its selection"
|
||||
for link in "$HOME/.local/bin/superwhisper" "$XDG_DATA_HOME/superwhisper/app/current" "$XDG_CONFIG_HOME/omarchy/plugins/superwhisper-panel" "$HOME/.agents/skills/superwhisper"; do
|
||||
[[ ! -L $link ]] || fail "Superwhisper removal unlinks packaged integration"
|
||||
done
|
||||
[[ $(readlink "$HOME/.claude/skills/superwhisper") == "/custom/skill" ]] || fail "custom skills are preserved"
|
||||
[[ -f $XDG_DATA_HOME/superwhisper/models/model ]] || fail "downloaded models are retained"
|
||||
printf '%s\n' voxtype > "$config"
|
||||
omarchy-remove-dictation-superwhisper
|
||||
[[ $(cat "$config") == "voxtype" ]] || fail "removing Superwhisper preserves another selected backend"
|
||||
pass "Superwhisper removal clears its selection and owned links while preserving user data and other backends"
|
||||
|
||||
cat > "$test_tmp/bin/omarchy-pkg-present" <<'SH'
|
||||
#!/bin/bash
|
||||
[[ ${VOXTYPE_PACKAGE:-0} == 1 && $1 == "voxtype-bin" ]]
|
||||
SH
|
||||
chmod +x "$test_tmp/bin/omarchy-pkg-present"
|
||||
rm "$config"
|
||||
VOXTYPE_PACKAGE=0 /bin/bash -euo pipefail "$ROOT/migrations/1791479273.sh"
|
||||
[[ ! -e $config ]] || fail "migration does not select an uninstalled backend"
|
||||
VOXTYPE_PACKAGE=1 /bin/bash -euo pipefail "$ROOT/migrations/1791479273.sh"
|
||||
[[ $(cat "$config") == "voxtype" ]] || fail "existing Voxtype users keep dictation after upgrade"
|
||||
printf '%s\n' future-backend > "$config"
|
||||
VOXTYPE_PACKAGE=1 /bin/bash -euo pipefail "$ROOT/migrations/1791479273.sh"
|
||||
[[ $(cat "$config") == "future-backend" ]] || fail "upgrade preserves explicit backend selections"
|
||||
pass "one-time upgrade preserves existing Voxtype without runtime autodetection"
|
||||
@@ -0,0 +1,28 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "background" as BackgroundPlugin
|
||||
|
||||
ShellRoot {
|
||||
property bool reloaded: false
|
||||
BackgroundPlugin.Background { id: background }
|
||||
FileView { id: command; path: Quickshell.env("RELOAD_TEST_COMMAND"); watchChanges: true; onFileChanged: reload() }
|
||||
FileView { id: result; path: Quickshell.env("RELOAD_TEST_RESULT"); atomicWrites: true }
|
||||
|
||||
Timer { interval: 600; running: true; onTriggered: result.setText("initial") }
|
||||
Timer { id: settled; interval: 600; onTriggered: result.setText("reloaded") }
|
||||
Timer {
|
||||
interval: 100
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (command.text().trim() === "done") {
|
||||
Qt.quit()
|
||||
} else if (command.text().trim() === "reload" && !reloaded) {
|
||||
reloaded = true
|
||||
background.setBackground(background.currentBackground, true)
|
||||
settled.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "services"
|
||||
import "clone" as Clone
|
||||
|
||||
ShellRoot {
|
||||
id: test
|
||||
property var services: ({ "intro-test.background": background })
|
||||
function firstPartyServiceFor(id) { return id === "omarchy.background" ? background : null }
|
||||
|
||||
PluginShellApi { id: facade; pluginId: "intro-test.background" }
|
||||
Clone.Background { id: background; shell: facade }
|
||||
BackgroundIntro { id: intro; host: test }
|
||||
property bool stillReported: false
|
||||
FileView { id: release; path: Quickshell.env("INTRO_TEST_RELEASE"); watchChanges: true; onFileChanged: reload() }
|
||||
FileView { id: result; path: Quickshell.env("INTRO_TEST_RESULT"); atomicWrites: true }
|
||||
|
||||
Timer {
|
||||
interval: 600
|
||||
running: true
|
||||
onTriggered: result.setText(JSON.stringify({
|
||||
phase: "cover",
|
||||
scoped: background.shell.pluginId === "intro-test.background",
|
||||
privateCoordinator: background.shell.bootIntro === undefined,
|
||||
selected: background.displayedBackground.endsWith("still.png"),
|
||||
covered: intro.cover
|
||||
}))
|
||||
}
|
||||
Timer {
|
||||
interval: 100
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (release.text().trim() === "done") {
|
||||
Qt.quit()
|
||||
} else if (release.text().trim() === "captured" && !intro.cover && !intro.startupPending && !test.stillReported) {
|
||||
test.stillReported = true
|
||||
result.setText(JSON.stringify({ phase: "still", covered: intro.cover }))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "services"
|
||||
import "Commons" as Commons
|
||||
|
||||
ShellRoot {
|
||||
id: test
|
||||
property bool failed: false
|
||||
property var services: ({})
|
||||
function firstPartyServiceFor(id) { return services[id] || null }
|
||||
function check(ok, message) {
|
||||
if (!ok) {
|
||||
failed = true
|
||||
console.log("RESULT fail " + message)
|
||||
}
|
||||
}
|
||||
|
||||
QtObject { id: retainedBackground; property bool suspended: false }
|
||||
BackgroundIntro { id: intro; host: test }
|
||||
|
||||
Process {
|
||||
id: revealVideo
|
||||
command: ["touch", Quickshell.env("INTRO_TEST_FRAME_READY")]
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 300
|
||||
running: true
|
||||
onTriggered: {
|
||||
test.check(intro.checked, "startup runs even when OWE left the background disabled for a video")
|
||||
test.services = ({ "omarchy.background": retainedBackground })
|
||||
intro.prepareTheme(Quickshell.env("INTRO_TEST_COVER"), "theme-one", Qt.btoa('background = "#123456"'), "")
|
||||
test.check(intro.themeCoverStatus("theme-one") === "loading", "the cover waits for decoding and presentation")
|
||||
test.check(intro.themeStatus("theme-one") === "pending", "app retints wait for the intro reveal")
|
||||
test.check(!Qt.colorEqual(Commons.Color.background, "#123456"), "the palette waits for OWE's first frame")
|
||||
intro.finishTheme("superseded-theme")
|
||||
test.check(intro.themeToken === "theme-one", "an older intro cannot release the pending handoff")
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
interval: 500
|
||||
running: true
|
||||
onTriggered: {
|
||||
test.check(intro.cover, "the still stays covered while the launcher waits")
|
||||
test.check(intro.themeCoverStatus("theme-one") === "ready", "the outgoing cover is presented before the shell is hidden")
|
||||
retainedBackground.suspended = true
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
interval: 600
|
||||
running: true
|
||||
onTriggered: {
|
||||
test.check(intro.themeToken === "theme-one", "a leftover renderer still stays covered until video is revealed")
|
||||
revealVideo.running = true
|
||||
handoffDeadline = Date.now() + 3000
|
||||
handoff.start()
|
||||
}
|
||||
}
|
||||
property double handoffDeadline: 0
|
||||
property bool openingFadeObserved: false
|
||||
Connections {
|
||||
target: intro
|
||||
function onThemeTokenChanged() {
|
||||
if (handoff.running && intro.themeToken === "") {
|
||||
Qt.callLater(function() {
|
||||
test.check(intro.themeStatus("theme-one") === "pending", "app retints also wait through the opening crossfade")
|
||||
test.openingFadeObserved = true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
id: handoff
|
||||
interval: 16
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (intro.themeToken || !test.openingFadeObserved) {
|
||||
if (Date.now() >= test.handoffDeadline) {
|
||||
test.check(false, "the ready video releases its cover before the deadline")
|
||||
Qt.quit()
|
||||
}
|
||||
return
|
||||
}
|
||||
stop()
|
||||
test.check(!intro.cover && intro.checked, "OWE taking the background releases the cover")
|
||||
test.check(intro.themeToken === "" && Qt.colorEqual(Commons.Color.background, "#123456"), "first-frame handoff starts the palette and wallpaper fade together")
|
||||
retainedBackground.suspended = false
|
||||
Qt.callLater(function() { test.check(!intro.cover, "resuming the retained background cannot cover or retry playback") })
|
||||
completionDeadline = Date.now() + 3000
|
||||
completion.start()
|
||||
}
|
||||
}
|
||||
property double completionDeadline: 0
|
||||
Timer {
|
||||
id: completion
|
||||
interval: 16
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (intro.themeStatus("theme-one") !== "ready" || !intro.startupSettled) {
|
||||
if (Date.now() >= test.completionDeadline) {
|
||||
test.check(false, "the fade and launcher complete before the deadline")
|
||||
Qt.quit()
|
||||
}
|
||||
return
|
||||
}
|
||||
stop()
|
||||
test.check(intro.themeStatus("theme-one") === "ready", "the completed crossfade releases app retints")
|
||||
intro.prepareTheme("", "failed-theme", Qt.btoa('background = "#654321"'), "")
|
||||
intro.finishTheme("failed-theme")
|
||||
test.check(Qt.colorEqual(Commons.Color.background, "#654321"), "failed playback still releases its pending palette")
|
||||
intro.prepareTheme("", "cancelled-theme", Qt.btoa('background = "#abcdef"'), "")
|
||||
intro.cancelTheme()
|
||||
intro.finishTheme("cancelled-theme")
|
||||
test.check(Qt.colorEqual(Commons.Color.background, "#654321") && !intro.themeToken, "a superseding theme cannot be overwritten by an older completion")
|
||||
test.check(!intro.cover, "launcher completion leaves the still uncovered")
|
||||
if (!test.failed) console.log("RESULT pass")
|
||||
Qt.quit()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Commons as Commons
|
||||
import "background" as BackgroundPlugin
|
||||
|
||||
ShellRoot {
|
||||
id: shell
|
||||
function firstPartyServiceFor(id) { return id === "omarchy.background" ? background : null }
|
||||
BackgroundPlugin.Background { id: background }
|
||||
|
||||
// APPLY_THEME_FUNCTION
|
||||
|
||||
Timer {
|
||||
interval: 600
|
||||
running: true
|
||||
onTriggered: {
|
||||
var oldColors = Qt.btoa('accent = "#ff0000"\nbackground = "#000000"\nforeground = "#ffffff"\n')
|
||||
var newColors = Qt.btoa('accent = "#123456"\nbackground = "#000000"\nforeground = "#ffffff"\n')
|
||||
background.transitionBackgroundWithTheme("", "/missing-still.png", "/missing-still.png", oldColors, "")
|
||||
shell.applyTheme(newColors, "")
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
interval: 1300
|
||||
running: true
|
||||
onTriggered: {
|
||||
if (Qt.colorEqual(Commons.Color.accent, "#123456")) {
|
||||
console.log("RESULT pass")
|
||||
} else {
|
||||
console.log("RESULT fail previous colors returned: " + Commons.Color.accent)
|
||||
}
|
||||
Qt.quit()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import "services"
|
||||
import "background" as BackgroundPlugin
|
||||
|
||||
ShellRoot {
|
||||
id: test
|
||||
property var services: ({ "omarchy.background": background })
|
||||
property var bar: ({})
|
||||
property bool failed: false
|
||||
function firstPartyServiceFor(id) { return services[id] || null }
|
||||
function check(ok, message) {
|
||||
if (!ok) {
|
||||
failed = true
|
||||
console.log("RESULT fail " + message)
|
||||
}
|
||||
}
|
||||
|
||||
BackgroundPlugin.Background { id: background }
|
||||
BackgroundIntro { id: intro; host: test }
|
||||
|
||||
Timer {
|
||||
interval: 600
|
||||
running: true
|
||||
onTriggered: {
|
||||
test.check(background.displayedBackground.endsWith("deleted.png"), "the missing wallpaper is selected")
|
||||
test.check(!background.ready && intro.startupPending && intro.cover, "the failed image cannot release startup through readiness")
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
interval: 11000
|
||||
running: true
|
||||
onTriggered: {
|
||||
test.check(!background.ready, "the missing wallpaper never becomes ready")
|
||||
test.check(!intro.startupPending && !intro.cover && intro.startupOpacity === 0, "the deadline fades the cover away despite the failed wallpaper")
|
||||
if (!test.failed) console.log("RESULT pass")
|
||||
Qt.quit()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "services"
|
||||
|
||||
ShellRoot {
|
||||
id: test
|
||||
property var services: ({})
|
||||
property var pluginRegistry: null
|
||||
property var bar: null
|
||||
property bool failed: false
|
||||
function firstPartyServiceFor(id) { return services[id] || null }
|
||||
function check(ok, message) {
|
||||
if (!ok) {
|
||||
failed = true
|
||||
console.log("RESULT fail " + message)
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: registry
|
||||
property var installedPlugins: ({ "omarchy.background": {} })
|
||||
signal pluginsChanged()
|
||||
function resolveEnabledId(id) { return id }
|
||||
function isEnabled(id) { return false }
|
||||
}
|
||||
QtObject {
|
||||
id: background
|
||||
property bool suspended: false
|
||||
property bool ready: false
|
||||
}
|
||||
BackgroundIntro { id: intro; host: test }
|
||||
FileView { id: cursorLog; path: Quickshell.env("STARTUP_CURSOR_LOG"); printErrors: false }
|
||||
|
||||
Timer {
|
||||
interval: 400
|
||||
running: true
|
||||
onTriggered: {
|
||||
test.check(!intro.backgroundActive, "the plugin has not loaded yet")
|
||||
test.check(intro.cover && !intro.themeBackground, "fresh startup does not prepare an outgoing still image")
|
||||
test.check(intro.startupSettled, "the launcher has finished without an intro")
|
||||
test.check(intro.startupPending && intro.startupOpacity === 1, "a fresh login hides the desktop until its media and bar are ready")
|
||||
cursorLog.reload()
|
||||
cursorLog.waitForJob()
|
||||
test.check(!cursorLog.text().trim(), "startup does not restore the cursor while media is loading")
|
||||
test.services = ({ "omarchy.background": background })
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
interval: 600
|
||||
running: true
|
||||
onTriggered: {
|
||||
test.check(intro.cover, "a loading plugin does not release the startup wallpaper")
|
||||
background.ready = true
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
interval: 800
|
||||
running: true
|
||||
onTriggered: {
|
||||
test.check(!intro.cover && !intro.themeBackground, "a ready plugin releases the startup cover and its image")
|
||||
test.check(intro.startupPending && intro.startupOpacity === 1, "a ready background alone cannot reveal an unready bar")
|
||||
cursorLog.reload()
|
||||
cursorLog.waitForJob()
|
||||
test.check(!cursorLog.text().trim(), "startup does not restore the cursor before the bar is ready")
|
||||
test.bar = ({})
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
interval: 1400
|
||||
running: true
|
||||
onTriggered: {
|
||||
test.check(!intro.startupPending && intro.startupOpacity === 0, "the desktop fades in after both the media and bar are ready")
|
||||
cursorLog.reload()
|
||||
cursorLog.waitForJob()
|
||||
test.check(cursorLog.text().includes("omarchy_startup_cursor_restore()"), "the opening fade restores the cursor")
|
||||
test.services = ({})
|
||||
intro.cover = true
|
||||
test.pluginRegistry = registry
|
||||
registry.pluginsChanged()
|
||||
test.check(!intro.cover, "a disabled background plugin does not leave a startup cover on screen")
|
||||
if (!test.failed) console.log("RESULT pass")
|
||||
Qt.quit()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,16 +198,6 @@ ShellRoot {
|
||||
root.assertTrue(root.commandCount("omarchy-capture-screenrecording --stop-recording") === 1, "Screen Recording left click stops active recording")
|
||||
}
|
||||
|
||||
var dictation = root.createIndicator("Dictation")
|
||||
if (dictation) {
|
||||
dictation.moduleName = "Dictation"
|
||||
root.injectBar(dictation)
|
||||
dictation.triggerPress(Qt.LeftButton)
|
||||
dictation.triggerPress(Qt.RightButton)
|
||||
root.assertTrue(root.commandCount("omarchy-voxtype-config") === 2, "Dictation clicks run config command")
|
||||
root.assertTrue(root.commandCount("omarchy-voxtype-model") === 0, "Dictation clicks do not run model command")
|
||||
}
|
||||
|
||||
var stayAwake = root.createIndicator("StayAwake")
|
||||
if (stayAwake) {
|
||||
stayAwake.moduleName = "StayAwake"
|
||||
|
||||
@@ -2,6 +2,7 @@ import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Networking
|
||||
import qs.Commons
|
||||
import qs.Commons as Commons
|
||||
import "mocks"
|
||||
import "network" as Network
|
||||
|
||||
@@ -21,9 +22,9 @@ ShellRoot {
|
||||
Network.Panel {
|
||||
id: panel
|
||||
bar: QtObject {
|
||||
property color foreground: Color.foreground
|
||||
property color barForeground: Color.foreground
|
||||
property color urgent: Color.urgent
|
||||
property color foreground: Commons.Color.foreground
|
||||
property color barForeground: Commons.Color.foreground
|
||||
property color urgent: Commons.Color.urgent
|
||||
property string fontFamily: Style.font.family
|
||||
property string position: "top"
|
||||
property int barSize: 24
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Commons as Commons
|
||||
import "mocks"
|
||||
import "network" as Network
|
||||
import "network/Model.js" as Model
|
||||
@@ -25,9 +26,9 @@ ShellRoot {
|
||||
Network.Panel {
|
||||
id: panel
|
||||
bar: QtObject {
|
||||
property color foreground: Color.foreground
|
||||
property color barForeground: Color.foreground
|
||||
property color urgent: Color.urgent
|
||||
property color foreground: Commons.Color.foreground
|
||||
property color barForeground: Commons.Color.foreground
|
||||
property color urgent: Commons.Color.urgent
|
||||
property string fontFamily: Style.font.family
|
||||
property string position: "top"
|
||||
property int barSize: 24
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package.path = os.getenv("OMARCHY_PATH") .. "/?.lua;" .. package.path
|
||||
o = {
|
||||
shell_quote = function(value) return "'" .. value:gsub("'", "'\\''") .. "'" end,
|
||||
launch = function(value) return value end,
|
||||
}
|
||||
local exec = hl.exec_cmd
|
||||
hl.exec_cmd = function(command)
|
||||
if command == "omarchy-launch-shell" then
|
||||
local file = assert(io.open(os.getenv("CURSOR_TEST_STAGE") .. "/session", "w"))
|
||||
file:write(os.getenv("WAYLAND_DISPLAY") .. "\n" .. os.getenv("HYPRLAND_INSTANCE_SIGNATURE"))
|
||||
file:close()
|
||||
exec("quickshell -p " .. o.shell_quote(os.getenv("CURSOR_TEST_STAGE")))
|
||||
elseif command:match("^hyprctl ") then
|
||||
exec(command)
|
||||
end
|
||||
end
|
||||
require("default.hypr.autostart")
|
||||
hl.config({
|
||||
misc = { disable_hyprland_logo = true, disable_splash_rendering = true, background_color = "rgb(000000)", disable_watchdog_warning = true },
|
||||
xwayland = { enabled = false },
|
||||
cursor = { enable_hyprcursor = false },
|
||||
})
|
||||
hl.monitor({ output = "", mode = "640x480@60", position = "auto", scale = 1 })
|
||||
hl.layer_rule({ match = { namespace = "omarchy-background" }, no_anim = true })
|
||||
hl.bind("SUPER + Q", hl.dsp.exec_cmd("true"))
|
||||
@@ -0,0 +1,38 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import "services"
|
||||
|
||||
ShellRoot {
|
||||
id: test
|
||||
property var bar: ({})
|
||||
property var services: ({ "omarchy.background": background })
|
||||
function firstPartyServiceFor(id) { return background }
|
||||
QtObject { id: background; property bool suspended: false; property bool ready: false }
|
||||
BackgroundIntro {
|
||||
id: intro
|
||||
host: test
|
||||
onStartupPendingChanged: if (!startupPending) phase.setText("revealed")
|
||||
}
|
||||
FileView {
|
||||
id: release
|
||||
path: Quickshell.env("CURSOR_TEST_STAGE") + "/release"
|
||||
watchChanges: true
|
||||
onLoaded: background.ready = text().trim() === "reveal"
|
||||
onFileChanged: reload()
|
||||
}
|
||||
FileView {
|
||||
id: phase
|
||||
path: Quickshell.env("CURSOR_TEST_STAGE") + "/phase"
|
||||
atomicWrites: true
|
||||
}
|
||||
PanelWindow {
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
WlrLayershell.layer: WlrLayer.Bottom
|
||||
mask: Region {}
|
||||
color: "magenta"
|
||||
}
|
||||
Component.onCompleted: phase.setText("holding")
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import os
|
||||
import pathlib
|
||||
import pwd
|
||||
import signal
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
root = pathlib.Path(os.environ['ROOT'])
|
||||
with tempfile.TemporaryDirectory(prefix='theme-ssh-') as directory:
|
||||
stage = pathlib.Path(directory)
|
||||
for name in ('host', 'identity'):
|
||||
subprocess.run(['ssh-keygen', '-q', '-t', 'ed25519', '-N', '', '-f', str(stage / name)], check=True)
|
||||
try:
|
||||
with socket.socket() as port_probe:
|
||||
port_probe.bind(('127.0.0.1', 0))
|
||||
port = port_probe.getsockname()[1]
|
||||
except PermissionError:
|
||||
print('ok - sandbox blocks loopback sockets; skipping real SSH connection reuse # SKIP')
|
||||
raise SystemExit(0)
|
||||
remote = stage / 'remote'
|
||||
remote.write_text('#!/bin/bash\ncat >>"' + str(stage / 'received') + '"\necho theme-received\n')
|
||||
remote.chmod(0o700)
|
||||
server_config = stage / 'sshd.conf'
|
||||
server_config.write_text(f'''Port {port}
|
||||
ListenAddress 127.0.0.1
|
||||
HostKey {stage / 'host'}
|
||||
PidFile {stage / 'server.pid'}
|
||||
AuthorizedKeysFile {stage / 'identity.pub'}
|
||||
PasswordAuthentication no
|
||||
KbdInteractiveAuthentication no
|
||||
UsePAM no
|
||||
StrictModes no
|
||||
AllowUsers {pwd.getpwuid(os.getuid()).pw_name}
|
||||
LogLevel VERBOSE
|
||||
ForceCommand /bin/bash {remote}
|
||||
''')
|
||||
host_key = (stage / 'host.pub').read_text().split()
|
||||
(stage / 'known_hosts').write_text(f'[127.0.0.1]:{port} {host_key[0]} {host_key[1]}\n')
|
||||
client_config = stage / 'ssh.conf'
|
||||
client_config.write_text(f'''Host fixture
|
||||
HostName 127.0.0.1
|
||||
Port {port}
|
||||
User {pwd.getpwuid(os.getuid()).pw_name}
|
||||
IdentityFile {stage / 'identity'}
|
||||
IdentityAgent none
|
||||
IdentitiesOnly yes
|
||||
StrictHostKeyChecking yes
|
||||
UserKnownHostsFile {stage / 'known_hosts'}
|
||||
''')
|
||||
home = stage / 'home'
|
||||
current = home / '.local/state/omarchy/current'
|
||||
current.mkdir(parents=True)
|
||||
toggles = home / '.local/state/omarchy/toggles'
|
||||
toggles.mkdir()
|
||||
(toggles / 'herdr-theme-sync').touch()
|
||||
runtime = stage / 'run'
|
||||
runtime.mkdir(mode=0o700)
|
||||
stub_bin = stage / 'bin'
|
||||
stub_bin.mkdir()
|
||||
scripts = {
|
||||
'herdr': "#!/bin/bash\nprintf 'id\\tfixture\\tfixture\\tdefault\\tenabled\\n'\n",
|
||||
'ssh': '#!/bin/bash\nexec /usr/bin/ssh -F "' + str(client_config) + '" "$@"\n',
|
||||
}
|
||||
for name, content in scripts.items():
|
||||
path = stub_bin / name
|
||||
path.write_text(content)
|
||||
path.chmod(0o700)
|
||||
environment = {**os.environ, 'HOME': str(home), 'XDG_RUNTIME_DIR': str(runtime), 'PATH': str(stub_bin) + ':' + str(root / 'bin') + ':' + os.environ['PATH']}
|
||||
log_path = stage / 'sshd.log'
|
||||
with log_path.open('w') as log:
|
||||
server = subprocess.Popen([shutil.which('sshd'), '-D', '-e', '-f', str(server_config)], stdout=log, stderr=log, start_new_session=True)
|
||||
try:
|
||||
deadline = time.monotonic() + 5
|
||||
while True:
|
||||
try:
|
||||
with socket.create_connection(('127.0.0.1', port), timeout=0.1):
|
||||
break
|
||||
except OSError:
|
||||
if server.poll() is not None and 'Missing privilege separation directory' in log_path.read_text():
|
||||
print('ok - SSH server runtime directory is unavailable; skipping real SSH connection reuse # SKIP')
|
||||
raise SystemExit(0)
|
||||
assert server.poll() is None and time.monotonic() < deadline, log_path.read_text()
|
||||
time.sleep(0.05)
|
||||
for theme in ('tokyo-night', 'nord'):
|
||||
(current / 'theme.name').write_text(theme + '\n')
|
||||
result = subprocess.run([str(root / 'bin/omarchy-theme-set-herdr-machines')], env=environment, capture_output=True, text=True, timeout=10)
|
||||
assert result.returncode == 0 and 'theme-received' in result.stdout, (result.stdout, result.stderr, log_path.read_text())
|
||||
if theme == 'tokyo-night':
|
||||
(stage / 'identity').rename(stage / 'identity-unavailable')
|
||||
assert log_path.read_text().count('Accepted publickey') == 1, log_path.read_text()
|
||||
received = (stage / 'received').read_text()
|
||||
assert 'theme=tokyo-night' in received and 'theme=nord' in received
|
||||
assert (runtime / 'omarchy-theme-sync').stat().st_mode & 0o777 == 0o700
|
||||
lock = runtime / 'omarchy-theme-set-herdr-machines.lock'
|
||||
assert subprocess.run(['flock', '-n', str(lock), 'true']).returncode == 0, 'SSH master retained the theme sync lock'
|
||||
print('ok - two theme syncs authenticate once, even with the key removed before the second sync')
|
||||
print('ok - persistent SSH master leaves the sync lock available')
|
||||
finally:
|
||||
subprocess.run(['/usr/bin/ssh', '-F', str(client_config), '-o', 'ControlPath=' + str(runtime / 'omarchy-theme-sync/%C'), '-O', 'exit', 'fixture'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=5)
|
||||
if server.poll() is None:
|
||||
os.killpg(server.pid, signal.SIGTERM)
|
||||
server.wait(timeout=5)
|
||||
@@ -147,8 +147,11 @@ trap 'rm -rf "$tmpdir"' EXIT
|
||||
home="$tmpdir/home"
|
||||
stub_bin="$tmpdir/bin"
|
||||
mkdir -p "$home" "$stub_bin"
|
||||
touch "$stub_bin/voxtype"
|
||||
chmod +x "$stub_bin/voxtype"
|
||||
cat > "$stub_bin/omarchy-default-dictation" <<'SH'
|
||||
#!/bin/bash
|
||||
echo voxtype
|
||||
SH
|
||||
chmod +x "$stub_bin/omarchy-default-dictation"
|
||||
|
||||
bindings=$(PATH="$stub_bin:$PATH" list_bindings "$home")
|
||||
[[ -n $bindings ]] || fail "default bindings load for the conflict check"
|
||||
|
||||
@@ -144,36 +144,29 @@ no_bindings_output=$(run_omarchy_bindings "$no_bindings_home" 'omarchy_default_b
|
||||
[[ -z $no_bindings_output ]] || fail "default binding variable disables all Omarchy bindings" "$no_bindings_output"
|
||||
pass "default binding variable disables all Omarchy bindings"
|
||||
|
||||
voxtype_home="$tmpdir/voxtype-home"
|
||||
voxtype_bin="$tmpdir/voxtype-bin"
|
||||
mkdir -p "$voxtype_home" "$voxtype_bin"
|
||||
touch "$voxtype_bin/voxtype"
|
||||
chmod +x "$voxtype_bin/voxtype"
|
||||
voxtype_output=$(PATH="$voxtype_bin:$PATH" run_omarchy_bindings "$voxtype_home")
|
||||
grep -Fq $'SUPER + CTRL + X Toggle dictation' <<<"$voxtype_output" ||
|
||||
fail "installed Voxtype enables its toggle binding"
|
||||
grep -Fq $'F9 Start dictation (push-to-talk)' <<<"$voxtype_output" ||
|
||||
fail "installed Voxtype enables its push-to-talk binding"
|
||||
grep -Fq $'F9 Stop dictation (push-to-talk)' <<<"$voxtype_output" ||
|
||||
fail "installed Voxtype enables its release binding"
|
||||
pass "installed Voxtype conditionally enables dictation bindings"
|
||||
|
||||
voxtype_without_execute_output=$(PATH="$voxtype_bin:$PATH" run_omarchy_bindings \
|
||||
"$voxtype_home" 'os.execute = function() return nil, "No child processes", 10 end')
|
||||
grep -Fq $'SUPER + CTRL + X Toggle dictation' <<<"$voxtype_without_execute_output" ||
|
||||
fail "Voxtype detection does not require spawning a subprocess"
|
||||
pass "installed Voxtype detection works without os.execute"
|
||||
|
||||
dictation_home="$tmpdir/dictation-home"
|
||||
missing_bin="$tmpdir/missing-bin"
|
||||
mkdir -p "$missing_bin"
|
||||
mkdir -p "$dictation_home" "$missing_bin"
|
||||
ln -s "$(command -v lua)" "$missing_bin/lua"
|
||||
ln -s "$(command -v lspci)" "$missing_bin/lspci"
|
||||
ln -s "$(command -v sort)" "$missing_bin/sort"
|
||||
missing_voxtype_output=$(PATH="$missing_bin" run_omarchy_bindings "$voxtype_home")
|
||||
if grep -Fq $'SUPER + CTRL + X Toggle dictation' <<<"$missing_voxtype_output"; then
|
||||
fail "missing Voxtype skips its bindings"
|
||||
fi
|
||||
pass "missing Voxtype skips dictation bindings"
|
||||
dictation_output=$(PATH="$missing_bin" run_omarchy_bindings "$dictation_home")
|
||||
if grep -Fq 'dictation' <<<"$dictation_output"; then fail "unconfigured dictation leaves application shortcuts available"; fi
|
||||
cat > "$missing_bin/omarchy-default-dictation" <<'SH'
|
||||
#!/bin/bash
|
||||
echo future-backend
|
||||
SH
|
||||
chmod +x "$missing_bin/omarchy-default-dictation"
|
||||
dictation_output=$(PATH="$missing_bin" run_omarchy_bindings "$dictation_home")
|
||||
for binding in \
|
||||
$'SUPER + CTRL + X\tToggle dictation' \
|
||||
$'F9\tStart dictation (push-to-talk)' \
|
||||
$'F9\tStop dictation (push-to-talk)' \
|
||||
$'ALT + Alt_R\tStart dictation (push-to-talk)' \
|
||||
$'ALT + Alt_R\tStop dictation (push-to-talk)'; do
|
||||
grep -Fq "$binding" <<<"$dictation_output" || fail "dictation bindings load for any selected backend" "$binding"
|
||||
done
|
||||
pass "dictation bindings require a selection without hardcoding backends"
|
||||
|
||||
# Lazydocker is optional on new installs, while existing installs keep the hotkey.
|
||||
lazydocker_bin="$tmpdir/lazydocker-bin"
|
||||
|
||||
Executable
+70
@@ -0,0 +1,70 @@
|
||||
#!/bin/bash
|
||||
|
||||
source "$(dirname "$0")/base-test.sh"
|
||||
|
||||
if ! systemctl --user show-environment >/dev/null 2>&1; then
|
||||
skip "no user manager; skipping compositor shutdown"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
require_command python3
|
||||
require_command systemd-run
|
||||
|
||||
test_tmp=$(mktemp -d)
|
||||
unit="omarchy-test-hyprland-shutdown-$$.service"
|
||||
trap 'systemctl --user stop "$unit" >/dev/null 2>&1; rm -rf "$test_tmp"' EXIT
|
||||
|
||||
cat >"$test_tmp/compositor.py" <<'PY'
|
||||
import os
|
||||
import pathlib
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
|
||||
state = pathlib.Path(sys.argv[1])
|
||||
child = os.fork()
|
||||
if child == 0:
|
||||
def stop_shell(signum, frame):
|
||||
(state / "shell-stopped").touch()
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGTERM, stop_shell)
|
||||
(state / "shell-ready").touch()
|
||||
while True:
|
||||
signal.pause()
|
||||
|
||||
def stop_compositor(signum, frame):
|
||||
# Give an early shell SIGTERM time to remove the wallpaper.
|
||||
time.sleep(0.2)
|
||||
if not (state / "shell-stopped").exists():
|
||||
(state / "wallpaper-retained").touch()
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGTERM, stop_compositor)
|
||||
while not (state / "shell-ready").exists():
|
||||
time.sleep(0.01)
|
||||
(state / "ready").touch()
|
||||
while True:
|
||||
signal.pause()
|
||||
PY
|
||||
|
||||
dropin="$ROOT/etc/systemd/user/wayland-wm@hyprland.desktop.service.d/10-keep-shell.conf"
|
||||
kill_mode=$(sed -n 's/^KillMode=//p' "$dropin")
|
||||
systemd-run --user --collect --quiet --unit="$unit" \
|
||||
--property="KillMode=$kill_mode" --property=TimeoutStopSec=2s \
|
||||
python3 "$test_tmp/compositor.py" "$test_tmp" || fail "isolated compositor starts"
|
||||
|
||||
for (( attempt = 0; attempt < 100; attempt++ )); do
|
||||
[[ -f $test_tmp/ready ]] && break
|
||||
sleep 0.02
|
||||
done
|
||||
[[ -f $test_tmp/ready ]] || fail "isolated compositor and shell are ready"
|
||||
|
||||
systemctl --user stop "$unit" || fail "isolated compositor stops"
|
||||
[[ -f $test_tmp/wallpaper-retained ]] || fail "wallpaper survives compositor teardown"
|
||||
pass "wallpaper survives compositor teardown"
|
||||
|
||||
if systemctl --user is-active --quiet "$unit"; then
|
||||
fail "compositor shutdown leaves no running service"
|
||||
fi
|
||||
pass "compositor shutdown leaves no running service"
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
source "$(dirname "$0")/base-test.sh"
|
||||
require_compositor "cold compositor cursor startup"
|
||||
require_command Hyprland
|
||||
require_command quickshell
|
||||
require_command grim
|
||||
require_command magick
|
||||
require_command python3
|
||||
|
||||
python3 <<'PY'
|
||||
import os
|
||||
import pathlib
|
||||
import signal
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
root = pathlib.Path(os.environ["ROOT"])
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
stage = pathlib.Path(directory)
|
||||
home = stage / "home"
|
||||
home.mkdir()
|
||||
(stage / "release").write_text("")
|
||||
(stage / "bin").mkdir()
|
||||
launcher = stage / "bin/omarchy-theme-bg-boot-intro"
|
||||
launcher.write_text("#!/bin/bash\nexit 0\n")
|
||||
launcher.chmod(0o755)
|
||||
for name in ("Commons", "services"):
|
||||
(stage / name).symlink_to(root / "shell" / name)
|
||||
fixture = root / "test/shell.d/fixtures/startup-cursor"
|
||||
(stage / "shell.qml").write_text((fixture / "shell.qml").read_text())
|
||||
env = os.environ.copy()
|
||||
env.update(HOME=str(home), OMARCHY_PATH=str(root), CURSOR_TEST_STAGE=str(stage),
|
||||
PATH=str(stage / "bin") + ":" + env["PATH"], AQ_BACKEND="wayland",
|
||||
HYPRLAND_NO_SD_VARS="1", HYPRLAND_NO_SD_NOTIFY="1", GSETTINGS_BACKEND="memory",
|
||||
XCURSOR_THEME="Adwaita", XCURSOR_PATH="/usr/share/icons:/usr/share/pixmaps", XCURSOR_SIZE="24", HYPRCURSOR_SIZE="24")
|
||||
env.pop("HYPRLAND_INSTANCE_SIGNATURE", None)
|
||||
env.pop("HYPRCURSOR_THEME", None)
|
||||
log = open(stage / "compositor.log", "w")
|
||||
compositor = subprocess.Popen(["Hyprland", "--config", str(fixture / "hyprland.lua")],
|
||||
env=env, stdout=log, stderr=log, start_new_session=True)
|
||||
try:
|
||||
deadline = time.monotonic() + 5
|
||||
while not (stage / "session").exists():
|
||||
assert compositor.poll() is None, "cold compositor exited before startup"
|
||||
assert time.monotonic() < deadline, "cold compositor never started"
|
||||
time.sleep(0.005)
|
||||
display, signature = (stage / "session").read_text().splitlines()
|
||||
env.update(WAYLAND_DISPLAY=display, HYPRLAND_INSTANCE_SIGNATURE=signature)
|
||||
# Capture immediately, including before Quickshell loads its cover. The
|
||||
# fixture cannot reveal while grim or magick is busy taking these frames.
|
||||
deadline = time.monotonic() + 5
|
||||
for sample in range(3):
|
||||
assert time.monotonic() < deadline, "initial compositor captures timed out"
|
||||
screenshot = stage / "frame.png"
|
||||
subprocess.run(["grim", "-c", str(screenshot)], env=env, check=True,
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=5)
|
||||
maximum = subprocess.check_output(["magick", str(screenshot), "-format", "%[fx:maxima]", "info:"], text=True, timeout=5)
|
||||
assert maximum == "0", "the cursor appeared in the first compositor frames"
|
||||
|
||||
def wait_phase(expected):
|
||||
deadline = time.monotonic() + 5
|
||||
while not (stage / "phase").exists() or (stage / "phase").read_text() != expected:
|
||||
assert compositor.poll() is None and time.monotonic() < deadline, "startup never reached " + expected
|
||||
time.sleep(0.01)
|
||||
|
||||
wait_phase("holding")
|
||||
(stage / "release").write_text("reveal")
|
||||
wait_phase("revealed")
|
||||
# Cursor visibility is polled by the compositor independently of the fade.
|
||||
deadline = time.monotonic() + 5
|
||||
while True:
|
||||
subprocess.run(["grim", "-c", str(screenshot)], env=env, check=True,
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=5)
|
||||
green = subprocess.check_output(["magick", str(screenshot), "-channel", "G", "-separate", "-threshold", "0", "-format", "%[fx:mean*w*h]", "info:"], text=True, timeout=5)
|
||||
if float(green) > 20:
|
||||
break
|
||||
assert time.monotonic() < deadline, "the normal cursor did not return with the desktop"
|
||||
finally:
|
||||
os.killpg(compositor.pid, signal.SIGTERM)
|
||||
compositor.wait(timeout=10)
|
||||
log.close()
|
||||
PY
|
||||
pass "a fresh compositor never exposes its cursor before the desktop reveal"
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
source "$(dirname "$0")/base-test.sh"
|
||||
require_command lua
|
||||
|
||||
lua <<'LUA'
|
||||
package.path = os.getenv("ROOT") .. "/?.lua;" .. package.path
|
||||
local events, recovery, command = {}, nil, nil
|
||||
local config = { invisible = false, enable_hyprcursor = true, sync_gsettings_theme = true }
|
||||
local env = { XCURSOR_THEME = "my-xcursor", HYPRCURSOR_THEME = "my-hyprcursor", XCURSOR_PATH = "/my/icons" }
|
||||
local getenv = os.getenv
|
||||
os.getenv = function(name) return env[name] or getenv(name) end
|
||||
local monitors = {}
|
||||
hl = {
|
||||
on = function(event, callback) events[event] = callback end,
|
||||
get_monitors = function() return monitors end,
|
||||
get_config = function(key) return config[key:match("%.(.+)")] end,
|
||||
env = function(name, value) env[name] = value end,
|
||||
config = function(values)
|
||||
for name, value in pairs(values.cursor) do config[name] = value end
|
||||
end,
|
||||
timer = function(callback, options)
|
||||
assert(options.timeout == 15000 and options.type == "oneshot")
|
||||
recovery = callback
|
||||
end,
|
||||
exec_cmd = function(value)
|
||||
if value == "omarchy-launch-shell" then
|
||||
assert(config.invisible and env.XCURSOR_THEME == "my-xcursor", "hide the compositor cursor while restoring application settings before launch")
|
||||
end
|
||||
command = value
|
||||
end,
|
||||
}
|
||||
o = { shell_quote = function(value) return "'" .. value .. "'" end, launch = function(value) return value end }
|
||||
|
||||
require("default.hypr.autostart")
|
||||
assert(not config.invisible, "loading the module must wait for user configuration")
|
||||
events["config.reloaded"]()
|
||||
assert(config.invisible and not config.enable_hyprcursor and not config.sync_gsettings_theme)
|
||||
assert(env.XCURSOR_THEME == "omarchy-startup" and env.XCURSOR_PATH:match("/default/hypr/cursors:/my/icons$"))
|
||||
assert(omarchy_startup_cursor_pending, "the first compositor frame must use the blank cursor")
|
||||
events["hyprland.start"]()
|
||||
assert(env.XCURSOR_THEME == "my-xcursor" and env.XCURSOR_PATH == "/my/icons", "applications must inherit the user's cursor")
|
||||
assert(config.invisible, "starting applications must not reveal the cursor")
|
||||
|
||||
local previous_recovery = recovery
|
||||
events["config.reloaded"]()
|
||||
assert(recovery ~= previous_recovery and config.invisible, "a config reload must retain startup hiding and rearm recovery")
|
||||
recovery()
|
||||
assert(command:match("setcursor 'my%-xcursor'"), "restore the Xcursor fallback before revealing the pointer")
|
||||
assert(config.invisible, "wait for the normal theme before restoring visibility")
|
||||
omarchy_startup_cursor_restore(true)
|
||||
assert(not config.invisible and config.enable_hyprcursor and config.sync_gsettings_theme)
|
||||
assert(command:match("setcursor 'my%-hyprcursor'"), "restore the user's Hyprcursor theme")
|
||||
local previous_command = command
|
||||
previous_recovery()
|
||||
assert(command == previous_command, "recovery must not change a revealed cursor")
|
||||
events["config.reloaded"]()
|
||||
assert(not config.invisible and env.XCURSOR_THEME == "my-xcursor", "ordinary config reloads must not hide the cursor")
|
||||
|
||||
omarchy_startup_cursor_pending = nil
|
||||
monitors = { {} }
|
||||
events["config.reloaded"]()
|
||||
assert(not omarchy_startup_cursor_pending and not config.invisible, "installing the fix in a running compositor must leave its cursor alone")
|
||||
LUA
|
||||
pass "the first compositor frame starts blank and the reveal restores user cursor settings"
|
||||
@@ -147,7 +147,7 @@ assert(
|
||||
)
|
||||
assert(
|
||||
/OverlayWindow \{\s*id: panel\s*shown: root\.opened\s*shownKeyboardFocus: root\.imagesLoaded \? WlrKeyboardFocus\.Exclusive : WlrKeyboardFocus\.None/.test(imagePickerQml),
|
||||
'image picker parks on OverlayWindow and takes the keyboard once images load'
|
||||
'image picker uses OverlayWindow and takes the keyboard once images load'
|
||||
)
|
||||
assert(
|
||||
/model: visibleImages/.test(imagePickerQml) &&
|
||||
|
||||
@@ -20,7 +20,11 @@ trap cleanup EXIT
|
||||
|
||||
fake_bin="$test_tmp/bin"
|
||||
shell_root="$test_tmp/root"
|
||||
mkdir -p "$fake_bin" "$shell_root/shell"
|
||||
launch_home="$test_tmp/home"
|
||||
startup_wallpaper="$test_tmp/Selected wallpaper.webp"
|
||||
mkdir -p "$fake_bin" "$shell_root/shell" "$launch_home/.local/state/omarchy/current"
|
||||
touch "$startup_wallpaper"
|
||||
ln -s "$startup_wallpaper" "$launch_home/.local/state/omarchy/current/background"
|
||||
|
||||
# Each launch consumes the next status from OMARCHY_TEST_QS_STATUSES; "run"
|
||||
# stands in for a healthy shell that keeps going until stopped.
|
||||
@@ -28,8 +32,8 @@ cat >"$fake_bin/quickshell" <<'SH'
|
||||
#!/bin/bash
|
||||
|
||||
printf '%s\n' "$*" >>"$OMARCHY_TEST_QS_LOG"
|
||||
printf 'watcher=%s popup=%s\n' \
|
||||
"${QS_DISABLE_FILE_WATCHER:-unset}" "${QS_NO_RELOAD_POPUP:-unset}" >>"$OMARCHY_TEST_QS_ENV_LOG"
|
||||
printf 'watcher=%s popup=%s background=%s\n' \
|
||||
"${QS_DISABLE_FILE_WATCHER:-unset}" "${QS_NO_RELOAD_POPUP:-unset}" "${OMARCHY_STARTUP_BACKGROUND:-unset}" >>"$OMARCHY_TEST_QS_ENV_LOG"
|
||||
|
||||
launches=$(wc -l <"$OMARCHY_TEST_QS_LOG")
|
||||
status=$(awk -v n="$launches" 'NR == n { print; found = 1 } END { if (!found) print "0" }' <<<"$OMARCHY_TEST_QS_STATUSES")
|
||||
@@ -90,6 +94,7 @@ launch_shell() {
|
||||
: >"$logger_log"
|
||||
|
||||
PATH="$fake_bin:$PATH" \
|
||||
HOME="$launch_home" \
|
||||
OMARCHY_PATH="$shell_root" \
|
||||
OMARCHY_TEST_QS_LOG="$qs_log" \
|
||||
OMARCHY_TEST_QS_ENV_LOG="$qs_env_log" \
|
||||
@@ -113,9 +118,11 @@ pass "a shell that exits cleanly is left alone"
|
||||
|
||||
# A misspelled variable would leave Quickshell hot-reloading the tree pacman
|
||||
# rewrites underneath it, which is what crashes the restart that follows.
|
||||
[[ $(<"$qs_env_log") == "watcher=1 popup=1" ]] ||
|
||||
[[ $(<"$qs_env_log") == "watcher=1 popup=1 "* ]] ||
|
||||
fail "the shell launches with Quickshell's own reloading off" "$(<"$qs_env_log")"
|
||||
pass "the shell launches with Quickshell's config watcher and reload popup off"
|
||||
[[ $(<"$qs_env_log") == *"background=$startup_wallpaper" ]] || fail "the shell receives the resolved wallpaper path before startup" "$(<"$qs_env_log")"
|
||||
pass "the selected wallpaper can decode as soon as Quickshell starts"
|
||||
|
||||
# Qt leaves through _exit(), so Quickshell's crash handler never relaunches it.
|
||||
launch_shell $'255\n0' || fail "a shell that died on a Wayland error is relaunched"
|
||||
|
||||
@@ -71,7 +71,7 @@ const fs = require('fs')
|
||||
const defaultItems = menu.parseMenuJsonc(fs.readFileSync(path.join(root, 'default/omarchy/omarchy-menu.jsonc'), 'utf8'))
|
||||
const guardText = defaultItems.map(item => `${item.when}\n${item.checked}\n${item.disabled}`).join('\n')
|
||||
const repeated = [...new Set(
|
||||
(guardText.match(/\$\((omarchy-[a-z0-9-]+)\)/g) || []).map(match => match.slice(2, -1))
|
||||
(guardText.match(/\$\((omarchy-[a-z0-9-]+(?: [a-z0-9-]+)*)\)/g) || []).map(match => match.slice(2, -1))
|
||||
)].filter(command => guardText.split(`$(${command})`).length > 2)
|
||||
assertDeepEqual(
|
||||
repeated.filter(command => !menu.guardReaders.includes(command)),
|
||||
|
||||
@@ -356,6 +356,7 @@ assertDeepEqual(
|
||||
[
|
||||
'remove.package',
|
||||
'remove.ai',
|
||||
'remove.dictation',
|
||||
'remove.service',
|
||||
'remove.development',
|
||||
'remove.theme',
|
||||
@@ -367,7 +368,7 @@ assertDeepEqual(
|
||||
'remove.preinstalls',
|
||||
'remove.security'
|
||||
],
|
||||
'menu orders Remove categories like their Install counterparts, followed by Remove-only categories'
|
||||
'menu keeps the Remove category order'
|
||||
)
|
||||
assert(
|
||||
defaultById['setup.security.passwordless-sudo'].action.includes('omarchy-sudo-passwordless'),
|
||||
@@ -390,6 +391,11 @@ assertEqual(
|
||||
'omarchy-bar transparent toggle',
|
||||
'menu exposes Menu Bar transparency as a toggle'
|
||||
)
|
||||
assert(
|
||||
!defaultItems.some(item => item.id.startsWith('style.background-intro'))
|
||||
&& defaultById['trigger.toggle.animations'].action === 'omarchy-toggle-animations',
|
||||
'menu uses the existing animations toggle without separate background intro controls'
|
||||
)
|
||||
assertDeepEqual(
|
||||
defaultItems.filter(item => item.parent === 'setup.plugin').map(item => item.label),
|
||||
['Enable Plugin', 'Disable Plugin', 'Add Plugin', 'Clone Plugin', 'Remove Plugin'],
|
||||
|
||||
@@ -100,6 +100,54 @@ env \
|
||||
[[ $(<"$install_log") == "use -g node@latest" ]] || fail "installer only invokes Mise for the global Node setup"
|
||||
pass "new installs do not add project bin directories to PATH"
|
||||
|
||||
# An offline install unpacks the Node tarball the ISO bundles, and the ISO
|
||||
# bundles the one for its own architecture. Both are on offer here; each
|
||||
# machine must take its own. uname and the bundle directory are stand-ins.
|
||||
bundle="$test_dir/bundle"
|
||||
mkdir -p "$bundle" "$test_dir/node-src"
|
||||
for platform in linux-x64 linux-arm64; do
|
||||
mkdir -p "$test_dir/node-src/node-v24.1.0-$platform/bin"
|
||||
echo "$platform" >"$test_dir/node-src/node-v24.1.0-$platform/bin/node"
|
||||
tar -czf "$bundle/node-v24.1.0-$platform.tar.gz" -C "$test_dir/node-src" "node-v24.1.0-$platform"
|
||||
done
|
||||
cat >"$test_dir/bin/uname" <<'SH'
|
||||
#!/bin/bash
|
||||
[[ $* == -m ]] && { echo "$UNAME_TEST_MACHINE"; exit; }
|
||||
exec /usr/bin/uname "$@"
|
||||
SH
|
||||
cat >"$test_dir/bin/find" <<'SH'
|
||||
#!/bin/bash
|
||||
[[ $1 == /opt/packages ]] && { shift; exec /usr/bin/find "$NODE_TEST_BUNDLE" "$@"; }
|
||||
exec /usr/bin/find "$@"
|
||||
SH
|
||||
chmod +x "$test_dir/bin/uname" "$test_dir/bin/find"
|
||||
|
||||
for case in "x86_64 linux-x64" "aarch64 linux-arm64"; do
|
||||
read -r machine platform <<<"$case"
|
||||
arch_home="$test_dir/$machine-home"
|
||||
arch_log="$test_dir/$machine-mise.log"
|
||||
mkdir -p "$arch_home"
|
||||
env \
|
||||
HOME="$arch_home" \
|
||||
MISE_TEST_LOG="$arch_log" \
|
||||
NODE_TEST_BUNDLE="$bundle" \
|
||||
UNAME_TEST_MACHINE="$machine" \
|
||||
OMARCHY_SETUP_CONTEXT=iso-chroot \
|
||||
PATH="$test_dir/bin:/usr/bin" \
|
||||
bash -euo pipefail -c 'source "$1"' bash "$ROOT/install/user/mise-work.sh"
|
||||
|
||||
installed="$arch_home/.local/share/mise/installs/node/24.1.0/bin/node"
|
||||
[[ -f $installed && $(<"$installed") == "$platform" ]] || fail "$machine unpacks the $platform Node tarball"
|
||||
[[ $(head -n1 "$arch_log") == "use -g node@24.1.0" ]] || fail "$machine hands Mise the bundled Node version"
|
||||
done
|
||||
[[ ! -e $bundle/node-v24.1.0-linux-x64.tar.gz ]] || rm "$bundle/node-v24.1.0-linux-x64.tar.gz"
|
||||
if env HOME="$test_dir/missing-home" MISE_TEST_LOG="$test_dir/missing.log" NODE_TEST_BUNDLE="$bundle" \
|
||||
UNAME_TEST_MACHINE=x86_64 OMARCHY_SETUP_CONTEXT=iso-chroot PATH="$test_dir/bin:/usr/bin" \
|
||||
bash -euo pipefail -c 'source "$1"' bash "$ROOT/install/user/mise-work.sh" 2>/dev/null; then
|
||||
fail "an x86_64 install accepts another architecture's Node tarball"
|
||||
fi
|
||||
pass "offline installs unpack the Node tarball for the machine's architecture"
|
||||
|
||||
stock_home="$test_dir/stock-home"
|
||||
stock_config="$stock_home/Work/.mise.toml"
|
||||
stock_project="$stock_home/Work/tries/untrusted-repository"
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
test_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$test_dir"' EXIT
|
||||
mkdir -p "$test_dir/bin"
|
||||
export PACMAN_CONFIG="$test_dir/pacman.conf" CALL_LOG="$test_dir/calls"
|
||||
sed "s|/etc/pacman.conf|$PACMAN_CONFIG|g" "$ROOT/migrations/1791403252.sh" >"$test_dir/migration.sh"
|
||||
|
||||
cat >"$test_dir/bin/sudo" <<'SH'
|
||||
#!/bin/bash
|
||||
[[ ${CONFIG_STATUS:-0} == 0 ]] || exit "$CONFIG_STATUS"
|
||||
if [[ $1 == "install" ]]; then
|
||||
install -m 644 "${@: -2}"
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
SH
|
||||
cat >"$test_dir/bin/omarchy-update-system-pkgs" <<'SH'
|
||||
#!/bin/bash
|
||||
# Snapshot exactly the configuration visible to the package updater.
|
||||
cat "$PACMAN_CONFIG" >"$CALL_LOG"
|
||||
exit "${UPDATE_STATUS:-0}"
|
||||
SH
|
||||
chmod +x "$test_dir/bin/"*
|
||||
|
||||
run() {
|
||||
rm -f "$CALL_LOG"
|
||||
env PATH="$test_dir/bin:$PATH" "$@" bash -euo pipefail "$test_dir/migration.sh" >"$test_dir/output" 2>&1
|
||||
}
|
||||
|
||||
for channel in stable rc edge; do
|
||||
cat >"$PACMAN_CONFIG" <<CONF
|
||||
[options]
|
||||
Architecture = auto
|
||||
IgnorePkg = custom-package
|
||||
[custom]
|
||||
Server = https://custom.example/\$arch
|
||||
[core]
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
[omarchy]
|
||||
Server = https://pkgs.omarchy.org/$channel/\$arch
|
||||
SigLevel = Required
|
||||
[extra]
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
CONF
|
||||
cp "$PACMAN_CONFIG" "$test_dir/original"
|
||||
run || fail "$channel migration succeeds"
|
||||
[[ $(awk '/^\[/ && $0 != "[options]" { print; exit }' "$CALL_LOG") == "[omarchy]" ]] || fail "packages update after OPR is prioritized"
|
||||
cmp -s "$PACMAN_CONFIG.bak" "$test_dir/original" || fail "original configuration is backed up"
|
||||
for setting in 'IgnorePkg = custom-package' '[custom]' 'Server = https://custom.example/$arch' 'SigLevel = Required' "Server = https://pkgs.omarchy.org/$channel/\$arch"; do
|
||||
grep -qxF "$setting" "$CALL_LOG" || fail "repository settings survive"
|
||||
done
|
||||
cp "$PACMAN_CONFIG" "$test_dir/ordered"
|
||||
run || fail "already ordered configuration still upgrades"
|
||||
cmp -s "$PACMAN_CONFIG" "$test_dir/ordered" || fail "repeated ordering is idempotent"
|
||||
cmp -s "$PACMAN_CONFIG.bak" "$test_dir/original" || fail "repeat preserves original backup"
|
||||
pass "$channel updates packages after ordering and preserves configuration on repeat"
|
||||
done
|
||||
|
||||
if run UPDATE_STATUS=42; then
|
||||
fail "failed package update leaves the migration pending"
|
||||
fi
|
||||
[[ -f $CALL_LOG ]] || fail "failed update was attempted"
|
||||
run || fail "failed package update can be retried after ordering"
|
||||
pass "package update failure propagates and retry updates already ordered repositories"
|
||||
|
||||
# A failed configuration write must never proceed to the package update.
|
||||
cp "$test_dir/original" "$PACMAN_CONFIG"
|
||||
if run CONFIG_STATUS=1; then
|
||||
fail "failed configuration write stops the migration"
|
||||
fi
|
||||
[[ ! -e $CALL_LOG ]] || fail "packages are not updated after a failed configuration write"
|
||||
pass "configuration failure prevents the package update"
|
||||
@@ -9,31 +9,122 @@ const fs = require('fs')
|
||||
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8')
|
||||
const overlay = read('shell/Ui/OverlayWindow.qml')
|
||||
|
||||
// Closed, the surface parks 1x1, input-less and without keyboard below
|
||||
// windows: off the overlay layer, so it never blocks direct scanout.
|
||||
// Closing must unmap, not leave a surface on an output that may disappear.
|
||||
assert(
|
||||
overlay.includes('visible: true') &&
|
||||
overlay.includes('anchors { top: true; left: true; bottom: shown; right: shown }') &&
|
||||
overlay.includes('implicitWidth: 1') &&
|
||||
overlay.includes('implicitHeight: 1') &&
|
||||
overlay.includes('mask: shown ? null : emptyRegion') &&
|
||||
overlay.includes('WlrLayershell.layer: shown ? shownLayer : WlrLayer.Bottom') &&
|
||||
overlay.includes('WlrLayershell.keyboardFocus: shown ? shownKeyboardFocus : WlrKeyboardFocus.None'),
|
||||
'overlay window parks a 1x1 input-less surface below windows while hidden'
|
||||
overlay.includes('visible: shown && !!targetScreen && !!targetMonitor') &&
|
||||
overlay.includes('anchors { top: true; left: true; bottom: true; right: true }') &&
|
||||
!overlay.includes('implicitWidth: 1') && !overlay.includes('WlrLayer.Bottom'),
|
||||
'closed overlays unmap instead of parking a surface'
|
||||
)
|
||||
|
||||
// Content stays hidden until the surface has grown, so no frame drawn at 1x1
|
||||
// is stretched across the screen.
|
||||
const readyExpression = /readonly property bool contentReady: ([^\n]*(?:\n &&[^\n]*)*)/.exec(overlay)[1]
|
||||
const ready = new Function('shown', 'backingWindowVisible', 'width', 'height', 'targetScreen', 'targetMonitor', 'devicePixelRatio', `return ${readyExpression}`)
|
||||
const screen = { name: 'eDP-1', width: 1200, height: 750 }
|
||||
const monitor = { name: 'eDP-1', scale: 1.6 }
|
||||
for (const scale of [2, 1, 1.6]) {
|
||||
assertEqual(ready(true, true, 1200, 750, screen, monitor, scale), scale === 1.6,
|
||||
`opening frames at ${scale}x wait for the monitor's 1.6x scale`)
|
||||
}
|
||||
for (const scale of [1, 1.25, 1.5, 2]) {
|
||||
assert(ready(true, true, 1200, 750, screen, { scale }, scale),
|
||||
`overlay reveals at the correct ${scale}x scale`)
|
||||
}
|
||||
assert(ready(true, true, 1200, 750, screen, { scale: 1.333333 }, 160 / 120),
|
||||
'scale comparison tolerates compositor floating point rounding')
|
||||
assert(!ready(true, true, 500, 500, screen, monitor, 1.6),
|
||||
'initial window geometry never stretches content across the screen')
|
||||
assert(!ready(true, false, 1200, 750, screen, monitor, 1.6),
|
||||
'content waits for a mapped backing window')
|
||||
assert(!ready(false, true, 1200, 750, screen, monitor, 1.6),
|
||||
'closed content stays transparent even at the correct scale')
|
||||
assert(!ready(true, true, 1200, 750, null, monitor, 1.6) &&
|
||||
!ready(true, true, 1200, 750, screen, null, 1.6),
|
||||
'content waits for a connected screen and its monitor information')
|
||||
assert(
|
||||
/target: window\.contentItem\s*property: "visible"\s*value: window\.shown && window\.width > 1 && window\.height > 1/.test(overlay),
|
||||
'overlay window draws nothing until the surface has grown'
|
||||
/target: window\.contentItem\s*property: "opacity"\s*value: window\.contentRevealed \? 1 : 0/.test(overlay),
|
||||
'transparent opening frames preserve immediate keyboard handling'
|
||||
)
|
||||
|
||||
assert(
|
||||
/onShownChanged: if \(shown\) targetScreen = focusedScreen\(\) \|\| targetScreen/.test(overlay) &&
|
||||
overlay.includes('screen: targetScreen'),
|
||||
'overlay window follows the focused monitor each time it is shown'
|
||||
)
|
||||
const revealBody = /onContentReadyChanged: \{([\s\S]*?)\n \}/.exec(overlay)[1]
|
||||
const reveal = new Function('window', 'Qt', `with (window) { ${revealBody} }`)
|
||||
const reveals = []
|
||||
const revealQt = { callLater(callback) { reveals.push(callback) } }
|
||||
const content = { contentReady: true, contentRevealed: false }
|
||||
reveal(content, revealQt)
|
||||
assert(!content.contentRevealed, 'scale and resize notifications finish before content reveals')
|
||||
reveals.shift()()
|
||||
assert(content.contentRevealed, 'settled content reveals on the next event-loop turn')
|
||||
content.contentReady = false
|
||||
reveal(content, revealQt)
|
||||
assert(!content.contentRevealed, 'losing readiness immediately hides content')
|
||||
content.contentReady = true
|
||||
reveal(content, revealQt)
|
||||
content.contentReady = false
|
||||
reveal(content, revealQt)
|
||||
reveals.shift()()
|
||||
assert(!content.contentRevealed, 'a close or screen loss cancels a pending reveal')
|
||||
|
||||
const focusedScreenBody = /function focusedScreen\(\) \{([\s\S]*?)\n \}/.exec(overlay)[1]
|
||||
const focusedScreen = new Function('Hyprland', 'Quickshell', focusedScreenBody)
|
||||
const external = { name: 'DP-1', width: 1920, height: 1080 }
|
||||
assertEqual(focusedScreen({ focusedMonitor: { name: 'DP-1' } }, { screens: [screen, external] }), external,
|
||||
'overlay selects the focused output')
|
||||
assertEqual(focusedScreen({ focusedMonitor: { name: 'DP-1' } }, { screens: [screen] }), screen,
|
||||
'disconnecting the focused output falls back to a connected screen')
|
||||
assertEqual(focusedScreen({ focusedMonitor: null }, { screens: [] }), null,
|
||||
'no outputs leaves the overlay unmapped')
|
||||
|
||||
const shownHandler = /onShownChanged: \{([\s\S]*?)\n \}/.exec(overlay)[1]
|
||||
let refreshed = 0
|
||||
const Hyprland = { refreshMonitors() { refreshed++ } }
|
||||
const changeShown = new Function('shown', 'focusedScreen', 'Hyprland', `let targetScreen = null; ${shownHandler}; return targetScreen`)
|
||||
assertEqual(changeShown(false, () => external, Hyprland), null, 'closing drops the previous screen reference')
|
||||
assertEqual(refreshed, 0, 'closing does not refresh monitor data')
|
||||
assertEqual(changeShown(true, () => screen, Hyprland), screen, 'reopening selects a current screen')
|
||||
assertEqual(refreshed, 1, 'opening refreshes monitor scales after a live change while closed')
|
||||
const scaleHandler = /onDevicePixelRatioChanged: ([^\n]+)/.exec(overlay)[1]
|
||||
const changeScale = new Function('shown', 'Hyprland', scaleHandler)
|
||||
changeScale(false, Hyprland)
|
||||
assertEqual(refreshed, 1, 'scale changes on hidden windows do not refresh monitor data')
|
||||
const changedScreen = { name: 'eDP-1', width: 960, height: 600 }
|
||||
assert(!ready(true, true, 960, 600, changedScreen, monitor, 2),
|
||||
'a stale monitor scale keeps content transparent after a live scale change')
|
||||
Hyprland.refreshMonitors = () => { refreshed++; monitor.scale = 2 }
|
||||
changeScale(true, Hyprland)
|
||||
assertEqual(refreshed, 2, 'a showing window refreshes monitor scales when Qt receives a live change')
|
||||
assert(ready(true, true, 960, 600, changedScreen, monitor, 2),
|
||||
'fresh monitor data lets content reveal at the new scale')
|
||||
|
||||
const screensChangedBody = /function onScreensChanged\(\) \{([\s\S]*?)\n \}/.exec(overlay)[1]
|
||||
const screensChanged = new Function('window', 'Quickshell', 'Qt', screensChangedBody)
|
||||
const pending = []
|
||||
const Qt = { callLater(callback) { pending.push(callback) } }
|
||||
const window = { shown: true, targetScreen: external, focusedScreen: () => screen }
|
||||
screensChanged(window, { screens: [screen] }, Qt)
|
||||
assertEqual(window.targetScreen, null, 'screen removal unmaps before Qt finishes destroying the old window')
|
||||
pending.shift()()
|
||||
assertEqual(window.targetScreen, screen, 'a showing overlay moves off a disconnected screen')
|
||||
window.targetScreen = screen
|
||||
window.focusedScreen = () => external
|
||||
screensChanged(window, { screens: [screen, external] }, Qt)
|
||||
assertEqual(window.targetScreen, screen, 'connecting another screen does not move a showing overlay')
|
||||
window.shown = false
|
||||
window.targetScreen = null
|
||||
screensChanged(window, { screens: [screen, external] }, Qt)
|
||||
assertEqual(window.targetScreen, null, 'screen changes leave a closed overlay unmapped')
|
||||
window.shown = true
|
||||
window.targetScreen = external
|
||||
screensChanged(window, { screens: [screen] }, Qt)
|
||||
window.shown = false
|
||||
pending.shift()()
|
||||
assertEqual(window.targetScreen, null, 'closing before a deferred remap prevents a stale reopen')
|
||||
window.shown = true
|
||||
window.targetScreen = external
|
||||
screensChanged(window, { screens: [screen] }, Qt)
|
||||
window.targetScreen = screen
|
||||
window.focusedScreen = () => external
|
||||
pending.shift()()
|
||||
assertEqual(window.targetScreen, screen, 'a deferred remap preserves the screen chosen by a newer open')
|
||||
|
||||
const overlays = {
|
||||
'shell/plugins/menu/Menu.qml': 'shown: root.opened && root.rowsLoaded',
|
||||
@@ -48,11 +139,11 @@ for (const [file, shown] of Object.entries(overlays)) {
|
||||
const qml = read(file)
|
||||
assert(
|
||||
qml.includes('OverlayWindow {') && qml.includes(shown) && !/PanelWindow \{\s*(id: panel\s*)?visible: root\.opened/.test(qml),
|
||||
`${file} keeps its surface through OverlayWindow`
|
||||
`${file} uses the shared overlay scale and lifecycle handling`
|
||||
)
|
||||
}
|
||||
|
||||
// Window visibility never changes now, so close-time resets key off shown.
|
||||
// Layout resets on a requested close, independent of mapping readiness.
|
||||
const menuQml = read('shell/plugins/menu/Menu.qml')
|
||||
assert(
|
||||
menuQml.includes('onShownChanged: if (!shown) { cardTop = -1; maxRowsHeight = -1 }') &&
|
||||
@@ -60,9 +151,15 @@ assert(
|
||||
!/onVisibleChanged: if \(!visible\) \{ cardTop/.test(menuQml),
|
||||
'the menu unfreezes its layout when the overlay hides'
|
||||
)
|
||||
const screenHandler = /onTargetScreenChanged: \{([^\n]*)\}/.exec(menuQml)[1]
|
||||
const changeScreen = new Function('panel', `with (panel) { ${screenHandler} }`)
|
||||
const menuPanel = { shown: true, cardTop: 945, maxRowsHeight: 510 }
|
||||
changeScreen(menuPanel)
|
||||
assertEqual(menuPanel.cardTop, -1, 'moving an open menu off a disconnected output unfreezes its old top edge')
|
||||
assertEqual(menuPanel.maxRowsHeight, -1, 'moving an open menu drops the old output row-height limit')
|
||||
assert(menuPanel.shown, 'resetting the layout keeps the menu open')
|
||||
|
||||
// A kept surface grows from its parked 1x1 when shown, which Hyprland animates
|
||||
// as a slide in from the corner unless its layer rule turns animation off.
|
||||
// Fullscreen overlays open without an additional compositor animation.
|
||||
const shellRules = read('default/hypr/apps/omarchy-shell.lua')
|
||||
const noAnim = /namespace = "\^\(([^)]*)\)\$" \}, no_anim = true/.exec(shellRules)
|
||||
assert(noAnim, 'the shell overlays share one no-animation layer rule')
|
||||
|
||||
@@ -9,9 +9,9 @@ const read = name => fs.readFileSync(path.join(root, name), 'utf8')
|
||||
const indicator = read('shell/plugins/bar/indicators/PasswordlessSudo.qml')
|
||||
const widget = read('shell/plugins/bar/widgets/Indicators.qml')
|
||||
const manifest = JSON.parse(read('shell/plugins/bar/widgets/Indicators.manifest.json'))
|
||||
assert(widget.match(/defaultIndicatorEntries: \[ "PasswordlessSudo", "Dictation"/), 'passwordless sudo is included in the default indicator tray')
|
||||
assert(widget.match(/defaultIndicatorEntries: \[ "PasswordlessSudo", "ScreenRecording"/), 'passwordless sudo is included in the default indicator tray')
|
||||
assert(manifest.barWidget.schema.find(field => field.key === 'items').options.some(option => option.value === 'PasswordlessSudo'), 'passwordless sudo is configurable alongside Night Light')
|
||||
assert(indicator.includes('useActiveColor: true') && indicator.includes('activeColor: Color.urgent'), 'active passwordless sudo uses the theme danger color')
|
||||
assert(indicator.includes('useActiveColor: true') && indicator.includes('activeColor: Commons.Color.urgent'), 'active passwordless sudo uses the theme danger color')
|
||||
assertEqual(indicator.match(/activeText: "([^"]+)"/)[1], indicator.match(/inactiveText: "([^"]+)"/)[1], 'sudo keeps the same icon in both states')
|
||||
assert(!widget.includes('sudoHorizontal') && !widget.includes('sudoVertical'), 'sudo participates in the normal indicator blocks')
|
||||
assert(!indicator.includes('visible:'), 'sudo uses the shared indicator visibility and hover behavior')
|
||||
|
||||
Executable
+203
@@ -0,0 +1,203 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
home="$test_tmp/home"
|
||||
state="$home/.local/state/omarchy/current"
|
||||
themes="$home/.config/omarchy/themes"
|
||||
commands="$test_tmp/bin"
|
||||
log="$test_tmp/commands"
|
||||
mkdir -p "$state" "$commands" "$home/.local/state/omarchy/theme-backgrounds"
|
||||
for theme in alpha beta; do
|
||||
mkdir -p "$themes/$theme/backgrounds/intros"
|
||||
cp "$ROOT/themes/tokyo-night/colors.toml" "$themes/$theme/colors.toml"
|
||||
printf 'still\n' >"$themes/$theme/backgrounds/1-sky.webp"
|
||||
printf 'still\n' >"$themes/$theme/backgrounds/2-road.webp"
|
||||
printf 'intro\n' >"$themes/$theme/backgrounds/intros/2-road.mp4"
|
||||
done
|
||||
cp -r "$themes/alpha" "$state/theme"
|
||||
printf 'alpha\n' >"$state/theme.name"
|
||||
ln -s "$state/theme/backgrounds/1-sky.webp" "$state/background"
|
||||
printf '%s\n' "$state/theme/backgrounds/2-road.webp" >"$home/.local/state/omarchy/theme-backgrounds/beta"
|
||||
printf 'already-consumed\n' >"$home/.local/state/omarchy/background-intro.session-id"
|
||||
|
||||
cat >"$commands/noop" <<'STUB'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
STUB
|
||||
chmod +x "$commands/noop"
|
||||
# Post-theme retints run through bash -lc; this fixture only exercises selection and presentation.
|
||||
for command in omarchy-theme-set-templates omarchy-hook omarchy-theme-set-herdr-machines omarchy-theme-bg-cache; do
|
||||
ln -s noop "$commands/$command"
|
||||
done
|
||||
cat >"$commands/bash" <<'STUB'
|
||||
#!/bin/bash
|
||||
if [[ $1 == "-lc" && $2 == "omarchy-restart-hyprctl" ]]; then
|
||||
omarchy-restart-hyprctl
|
||||
fi
|
||||
STUB
|
||||
cat >"$commands/omarchy-restart-hyprctl" <<'STUB'
|
||||
#!/bin/bash
|
||||
echo "hypr-reload" >>"$TEST_LOG"
|
||||
STUB
|
||||
cat >"$commands/hyprctl" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf '{"bool":%s}\n' "${TEST_ANIMATIONS:-true}"
|
||||
STUB
|
||||
cat >"$commands/omarchy-shell" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf 'shell: %s\n' "$*" >>"$TEST_LOG"
|
||||
if [[ -n ${TEST_GATE_DIR:-} ]]; then
|
||||
if [[ $2 == "themeIntroStatus" && ! -f $TEST_GATE_DIR/fade-ready ]]; then
|
||||
if [[ ! -f $TEST_GATE_DIR/fade-waited ]]; then
|
||||
touch "$TEST_GATE_DIR/fade-waited"
|
||||
echo "pending"
|
||||
else
|
||||
touch "$TEST_GATE_DIR/fade-ready"
|
||||
echo "ready"
|
||||
fi
|
||||
elif [[ $2 == "themeIntroCoverStatus" && ! -f $TEST_GATE_DIR/cover-ready ]]; then
|
||||
if [[ ! -f $TEST_GATE_DIR/fade-ready || $(<"$TEST_STATE/theme.name") != "$TEST_OLD_THEME" ]]; then
|
||||
touch "$TEST_GATE_DIR/premature-swap"
|
||||
fi
|
||||
if [[ ! -f $TEST_GATE_DIR/cover-waited ]]; then
|
||||
touch "$TEST_GATE_DIR/cover-waited"
|
||||
echo "loading"
|
||||
else
|
||||
touch "$TEST_GATE_DIR/cover-ready"
|
||||
echo "ready"
|
||||
fi
|
||||
else
|
||||
echo "ready"
|
||||
fi
|
||||
fi
|
||||
STUB
|
||||
cat >"$commands/owe" <<'STUB'
|
||||
#!/bin/bash
|
||||
if [[ $1 == intro && -n ${TEST_RELEASE:-} ]]; then
|
||||
printf '%s\n' "$$" >"$TEST_INTRO_PID"
|
||||
fi
|
||||
printf 'owe: %s\n' "$*" >>"$TEST_LOG"
|
||||
[[ ${TEST_OWE_FAIL:-false} != true ]] || exit 1
|
||||
if [[ $1 == "render" ]]; then
|
||||
[[ ${TEST_LIVE_FRAME:-false} == "true" ]] || exit 1
|
||||
snapshot=$(jq -r '.path' <<<"$2")
|
||||
printf 'live video frame\n' >"$snapshot"
|
||||
elif [[ $1 == "intro-prepare" && ${TEST_OWE_NO_PREPARE:-false} == "true" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
if [[ $1 == intro && -n ${TEST_RELEASE:-} ]]; then
|
||||
for attempt in {1..100}; do
|
||||
[[ ! -f $TEST_RELEASE ]] || exit 0
|
||||
sleep 0.05
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
STUB
|
||||
chmod +x "$commands/bash" "$commands/omarchy-restart-hyprctl" "$commands/hyprctl" "$commands/omarchy-shell" "$commands/owe"
|
||||
|
||||
set_theme() {
|
||||
: >"$log"
|
||||
HOME="$home" OMARCHY_PATH="$ROOT" PATH="$commands:$ROOT/bin:$PATH" XDG_RUNTIME_DIR="$test_tmp" TEST_LOG="$log" \
|
||||
/bin/bash "$ROOT/bin/omarchy-theme-set" "$1" >"$test_tmp/stdout" 2>"$test_tmp/stderr" || fail "theme selection completes" "$(cat "$test_tmp/stderr")"
|
||||
}
|
||||
wait_command() {
|
||||
for attempt in {1..100}; do
|
||||
grep -q "$1" "$log" && return 0
|
||||
sleep 0.02
|
||||
done
|
||||
fail "theme selection reaches $1" "$(cat "$log")"
|
||||
}
|
||||
|
||||
set_theme beta
|
||||
wait_command '^owe: intro '
|
||||
grep -Fxq "owe: intro --start first-frame --refresh $state/theme/backgrounds/intros/2-road.mp4" "$log" || fail "a theme switch plays the remembered background's matching prepared intro"
|
||||
[[ $(readlink "$state/background") == "$state/theme/backgrounds/2-road.webp" ]] || fail "the remembered background is selected"
|
||||
! grep -qE '^shell: background (prepare|themeTransition)' "$log" || fail "an intro replaces the normal still transition"
|
||||
[[ $(<"$home/.local/state/omarchy/background-intro.session-id") == already-consumed ]] || fail "a theme switch leaves login consumption unchanged"
|
||||
pass "switching themes plays the matching intro for the remembered background"
|
||||
|
||||
set_theme beta
|
||||
[[ $(readlink "$state/background") == "$state/theme/backgrounds/1-sky.webp" ]] || fail "reselecting the current theme cycles its background"
|
||||
grep -q '^shell: background themeTransition ' "$log" || fail "same-theme cycling uses the normal still transition"
|
||||
! grep -q '^owe: intro ' "$log" || fail "same-theme cycling does not play an intro"
|
||||
OMARCHY_THEME_SKIP_BACKGROUND=1 set_theme alpha
|
||||
! grep -q '^owe: intro ' "$log" || fail "theme refresh does not play an intro"
|
||||
pass "same-theme cycling and refresh keep the normal background behavior"
|
||||
|
||||
printf '%s\n' "$state/theme/backgrounds/2-road.webp" >"$home/.local/state/omarchy/theme-backgrounds/beta"
|
||||
mkdir -p "$home/.local/state/omarchy/toggles"
|
||||
touch "$home/.local/state/omarchy/toggles/background-intros-off"
|
||||
set_theme beta
|
||||
grep -q '^shell: background themeTransition ' "$log" || fail "disabled intros use the normal still transition"
|
||||
! grep -q '^owe: intro ' "$log" || fail "disabled intros do not play on theme selection"
|
||||
rm "$home/.local/state/omarchy/toggles/background-intros-off"
|
||||
printf '%s\n' "$state/theme/backgrounds/2-road.webp" >"$home/.local/state/omarchy/theme-backgrounds/alpha"
|
||||
TEST_ANIMATIONS=false set_theme alpha
|
||||
grep -q '^shell: background themeTransition ' "$log" || fail "disabled animations use the normal still transition"
|
||||
! grep -q '^owe: intro ' "$log" || fail "disabled animations do not play an intro"
|
||||
OMARCHY_THEME_HEADLESS=1 set_theme beta
|
||||
! grep -q '^owe: intro ' "$log" || fail "headless theme setup does not play an intro"
|
||||
pass "theme intros respect the global toggle, disabled animations, and headless setup"
|
||||
|
||||
# Alpha's remembered image must have a clip for these presentation checks.
|
||||
printf '%s\n' "$state/theme/backgrounds/2-road.webp" >"$home/.local/state/omarchy/theme-backgrounds/alpha"
|
||||
TEST_OWE_FAIL=true set_theme alpha
|
||||
wait_command '^shell: background setInstant '
|
||||
wait_command '^hypr-reload$'
|
||||
! grep -q '^owe: intro ' "$log" || fail "an unavailable renderer falls back to the still"
|
||||
pass "unavailable OWE falls back to the selected still image"
|
||||
|
||||
printf '%s\n' "$state/theme/backgrounds/2-road.webp" >"$home/.local/state/omarchy/theme-backgrounds/beta"
|
||||
release="$test_tmp/release"
|
||||
TEST_LIVE_FRAME=true TEST_INTRO_PID="$test_tmp/intro.pid" TEST_RELEASE="$release" set_theme beta
|
||||
wait_command '^owe: intro '
|
||||
cover=$(awk '/^shell: shell prepareThemeIntro / { print $4; exit }' "$log")
|
||||
[[ -f $cover && $(<"$cover") == "live video frame" ]] || fail "an interrupted video supplies the outgoing cover instead of its final still"
|
||||
kill -0 "$(<"$test_tmp/intro.pid")" || fail "the fake renderer is still playing during the lock check"
|
||||
timeout 1 flock "$test_tmp/omarchy-theme-set.lock" true || fail "theme playback does not retain the theme selection lock"
|
||||
touch "$release"
|
||||
pass "interrupted playback uses its current frame and releases the theme selection lock"
|
||||
|
||||
TEST_OWE_NO_PREPARE=true set_theme alpha
|
||||
wait_command '^owe: intro '
|
||||
grep -Fxq "owe: intro --start first-frame $state/theme/backgrounds/intros/2-road.mp4" "$log" || fail "older OWE uses the ordinary intro path"
|
||||
pass "older OWE keeps the ordinary intro startup path"
|
||||
|
||||
gates="$test_tmp/gates"
|
||||
mkdir "$gates"
|
||||
TEST_GATE_DIR="$gates" TEST_STATE="$state" TEST_OLD_THEME=alpha set_theme beta
|
||||
wait_command '^owe: intro '
|
||||
[[ -f $gates/fade-waited && -f $gates/cover-waited && -f $gates/cover-ready ]] || fail "a rapid selection waits for the opening fade and replacement cover"
|
||||
[[ ! -f $gates/premature-swap ]] || fail "the outgoing fade and cover settle before replacing the active theme"
|
||||
pass "rapid switches present the outgoing video cover before replacing its still"
|
||||
|
||||
wait_command '^hypr-reload$'
|
||||
release="$test_tmp/reload-release"
|
||||
TEST_INTRO_PID="$test_tmp/intro.pid" TEST_RELEASE="$release" set_theme alpha
|
||||
wait_command '^owe: intro '
|
||||
! grep -q '^hypr-reload$' "$log" || fail "the compositor is not reloaded during intro playback"
|
||||
touch "$release"
|
||||
wait_command '^hypr-reload$'
|
||||
(( $(grep -c '^hypr-reload$' "$log") == 1 )) || fail "the compositor reloads once after playback"
|
||||
pass "theme intros defer the compositor reload until playback finishes"
|
||||
|
||||
release="$test_tmp/superseded-release"
|
||||
TEST_INTRO_PID="$test_tmp/intro.pid" TEST_RELEASE="$release" set_theme beta
|
||||
wait_command '^owe: intro '
|
||||
cover=$(awk '/^shell: shell prepareThemeIntro / { print $4; exit }' "$log")
|
||||
! grep -q '^hypr-reload$' "$log" || fail "the held intro has not reloaded the compositor"
|
||||
OMARCHY_THEME_SKIP_BACKGROUND=1 set_theme alpha
|
||||
grep -q '^hypr-reload$' "$log" || fail "a theme refresh reloads the compositor immediately"
|
||||
touch "$release"
|
||||
for attempt in {1..100}; do
|
||||
[[ -f $cover ]] || break
|
||||
sleep 0.02
|
||||
done
|
||||
[[ ! -f $cover ]] || fail "the superseded intro finishes cleanup"
|
||||
(( $(grep -c '^hypr-reload$' "$log") == 1 )) || fail "the superseded intro cannot reload during a newer theme"
|
||||
pass "superseded intros do not reload the compositor"
|
||||
@@ -35,6 +35,7 @@ EOF
|
||||
# ssh runs the script it receives against a fake home for that machine, as the remote would.
|
||||
cat >"$stub_bin/ssh" <<'EOF'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$@" >>"$SYNC_TEST/ssh-options"
|
||||
while [[ $1 == "-o" ]]; do
|
||||
shift 2
|
||||
done
|
||||
@@ -61,7 +62,7 @@ EOF
|
||||
chmod +x "$stub_bin"/* "$remote_bin"/*
|
||||
|
||||
reset_remotes() {
|
||||
rm -rf "$SYNC_TEST/remotes" "$SYNC_TEST/ssh-calls"
|
||||
rm -rf "$SYNC_TEST/remotes" "$SYNC_TEST/ssh-calls" "$SYNC_TEST/ssh-options"
|
||||
local machine
|
||||
for machine in alpha beta gamma local-box; do
|
||||
mkdir -p "$SYNC_TEST/remotes/$machine/.local/state/omarchy/current"
|
||||
@@ -111,7 +112,14 @@ pass "leaves this machine alone when it is listed"
|
||||
[[ $output == *"down: ssh: connect to host down"* ]] || fail "logs an unreachable machine"
|
||||
pass "logs an unreachable machine"
|
||||
|
||||
# Connects one machine at a time, so an SSH agent asks for approval at most once.
|
||||
# Keep reusable connections in a directory only this user can access.
|
||||
grep -qx 'ControlMaster=auto' "$SYNC_TEST/ssh-options" || fail "reuses authenticated SSH connections"
|
||||
grep -qx 'ControlPersist=600' "$SYNC_TEST/ssh-options" || fail "keeps connections for ten idle minutes"
|
||||
grep -qxF "ControlPath=$SYNC_TEST/run/omarchy-theme-sync/%C" "$SYNC_TEST/ssh-options" || fail "keeps separate control sockets for each SSH destination"
|
||||
[[ $(stat -c %a "$SYNC_TEST/run/omarchy-theme-sync") == "700" ]] || fail "control socket directory is private"
|
||||
pass "reuses SSH connections for ten idle minutes in a private runtime directory"
|
||||
|
||||
# Connects one machine at a time.
|
||||
[[ $(paste -sd ' ' "$SYNC_TEST/ssh-calls") == "down alpha beta gamma local-box" ]] || fail "connects to machines one at a time in order"
|
||||
pass "connects to machines one at a time in order"
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
if ! command -v sshd >/dev/null; then
|
||||
skip "no SSH server; skipping real theme sync connection reuse"
|
||||
exit 0
|
||||
fi
|
||||
require_command python3
|
||||
require_command ssh
|
||||
require_command ssh-keygen
|
||||
|
||||
python3 "$SHELL_TEST_DIR/fixtures/theme-set-herdr-ssh-reuse.py"
|
||||
@@ -330,6 +330,7 @@ video_snapshot=$(snapshot_background_path "$CURRENT_THEME_PATH/backgrounds/old.m
|
||||
|
||||
CHOSEN_THEME_BACKGROUND="$transition_home/disappeared.mp4"
|
||||
BACKGROUND_TRANSITION_SNAPSHOTS=false
|
||||
THEME_BACKGROUND_INTRO=""
|
||||
OLD_BACKGROUND_SNAPSHOT=""
|
||||
PREPARED_BACKGROUND=""
|
||||
PREPARED_BACKGROUND_SNAPSHOT=""
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
source "$(dirname "$0")/base-test.sh"
|
||||
|
||||
test_home=$(mktemp -d)
|
||||
test_bin=$(mktemp -d)
|
||||
log_file=$(mktemp)
|
||||
hook_path="$test_home/.config/omarchy/hooks/post-update.d/install-voxtype.hook"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$test_home" "$test_bin"
|
||||
rm -f "$log_file"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$(dirname "$hook_path")"
|
||||
|
||||
cat >"$test_bin/omarchy-notification-send" <<'EOF'
|
||||
#!/bin/bash
|
||||
echo notification >>"$TEST_LOG"
|
||||
exec_args=()
|
||||
while (($# > 0)); do
|
||||
if [[ $1 == "--exec" ]]; then shift; exec_args=("$@"); break; fi
|
||||
shift
|
||||
done
|
||||
((${#exec_args[@]})) && echo "exec:${exec_args[*]}" >>"$TEST_LOG"
|
||||
EOF
|
||||
chmod +x "$test_bin/omarchy-notification-send"
|
||||
|
||||
# The shell runs the click command, so the invitation must not need a unit of its
|
||||
# own to keep a blocked sender alive until the toast is answered.
|
||||
cat >"$test_bin/systemd-run" <<'EOF'
|
||||
#!/bin/bash
|
||||
echo "systemd-run:$*" >>"$TEST_LOG"
|
||||
EOF
|
||||
chmod +x "$test_bin/systemd-run"
|
||||
|
||||
run_invitation_hook() {
|
||||
cp "$ROOT/install/user/first-run/install-voxtype.hook" "$hook_path"
|
||||
HOME="$test_home" PATH="$test_bin:$ROOT/bin:$PATH" TEST_LOG="$log_file" bash "$hook_path"
|
||||
}
|
||||
|
||||
run_invitation_hook
|
||||
|
||||
[[ -f $test_home/.local/state/omarchy/done/voxtype-install-invitation ]] || fail "Voxtype invitation records completion"
|
||||
[[ -f $hook_path ]] || fail "Voxtype invitation keeps its hook installed"
|
||||
[[ $(grep -c '^notification$' "$log_file") -eq 1 ]] || fail "Voxtype invitation sends one notification"
|
||||
grep -qx 'exec:omarchy-launch-floating-terminal-with-presentation omarchy-voxtype-install' "$log_file" ||
|
||||
fail "Voxtype invitation attaches the installer to the notification"
|
||||
grep -q '^systemd-run:' "$log_file" && fail "Voxtype invitation needs no unit to hold an unanswered toast"
|
||||
|
||||
HOME="$test_home" PATH="$test_bin:$ROOT/bin:$PATH" TEST_LOG="$log_file" bash "$hook_path"
|
||||
|
||||
[[ -f $hook_path ]] || fail "completed Voxtype invitation keeps its hook installed"
|
||||
[[ $(grep -c '^notification$' "$log_file") -eq 1 ]] || fail "completed Voxtype invitation hook does not notify again"
|
||||
|
||||
pass "Voxtype invitation only runs once"
|
||||
Reference in new issue
Block a user