diff --git a/bin/omarchy-hyprland-reload-guard b/bin/omarchy-hyprland-reload-guard new file mode 100755 index 00000000..6110ceda --- /dev/null +++ b/bin/omarchy-hyprland-reload-guard @@ -0,0 +1,138 @@ +#!/bin/bash + +# omarchy:summary=Pause or resume Hyprland config auto-reload around package transactions. +# omarchy:hidden=true + +set -euo pipefail + +command="${1:-}" +case "$command" in + pause | resume) ;; + *) + echo "Usage: omarchy-hyprland-reload-guard pause|resume" >&2 + exit 1 + ;; +esac + +run_root="${OMARCHY_HYPRLAND_RELOAD_GUARD_RUN_ROOT:-/run/user}" +state_dir="${OMARCHY_HYPRLAND_RELOAD_GUARD_STATE_DIR:-/run/omarchy/hyprland-reload-guard}" +hyprctl_bin="${HYPRCTL:-}" + +if [[ -z $hyprctl_bin ]]; then + hyprctl_bin=/usr/bin/hyprctl + [[ -x $hyprctl_bin ]] || hyprctl_bin=$(command -v hyprctl || true) +fi +[[ -n $hyprctl_bin && -x $hyprctl_bin ]] || exit 0 + +hyprctl_instance() { + local runtime_dir="$1" + local signature="$2" + shift 2 + + XDG_RUNTIME_DIR="$runtime_dir" \ + HYPRLAND_INSTANCE_SIGNATURE="$signature" \ + "$hyprctl_bin" --instance "$signature" "$@" +} + +option_bool() { + local runtime_dir="$1" + local signature="$2" + local option="$3" + local output="" + + output=$(hyprctl_instance "$runtime_dir" "$signature" -j getoption "$option" 2>/dev/null) || return 1 + + if command -v jq >/dev/null 2>&1; then + jq -r 'if .bool == true then "true" else "false" end' <<<"$output" + elif [[ $output == *'"bool": true'* ]]; then + printf 'true\n' + else + printf 'false\n' + fi +} + +instances() { + [[ -d $run_root ]] || return 0 + + find "$run_root" -mindepth 3 -maxdepth 3 -type d -path "$run_root/*/hypr/*" -print 2>/dev/null | + while IFS= read -r instance_dir; do + local signature hypr_dir runtime_dir uid + + signature="${instance_dir##*/}" + hypr_dir="${instance_dir%/*}" + runtime_dir="${hypr_dir%/*}" + uid="${runtime_dir##*/}" + + [[ $uid =~ ^[0-9]+$ ]] || continue + [[ -n $signature ]] || continue + + printf '%s\t%s\t%s\n' "$runtime_dir" "$uid" "$signature" + done +} + +state_file_for() { + local signature="$1" + + printf '%s/%s\n' "$state_dir" "$signature" +} + +pause_instance() { + local runtime_dir="$1" + local uid="$2" + local signature="$3" + local disable_autoreload suppress_errors state_file + + mkdir -p "$state_dir" + state_file=$(state_file_for "$signature") + + if [[ ! -f $state_file ]]; then + disable_autoreload=$(option_bool "$runtime_dir" "$signature" misc.disable_autoreload) || return 0 + suppress_errors=$(option_bool "$runtime_dir" "$signature" debug.suppress_errors) || return 0 + + printf '%s\t%s\t%s\n' "$uid" "$disable_autoreload" "$suppress_errors" >"$state_file" + chmod 0600 "$state_file" 2>/dev/null || true + fi + + hyprctl_instance "$runtime_dir" "$signature" eval \ + 'hl.config({ misc = { disable_autoreload = true }, debug = { suppress_errors = true } })' \ + >/dev/null 2>&1 || true +} + +resume_instance() { + local state_file="$1" + local signature uid disable_autoreload suppress_errors runtime_dir + + signature="${state_file##*/}" + IFS=$'\t' read -r uid disable_autoreload suppress_errors <"$state_file" || return 0 + runtime_dir="$run_root/$uid" + + if [[ -d $runtime_dir ]]; then + hyprctl_instance "$runtime_dir" "$signature" eval \ + "hl.config({ debug = { suppress_errors = $suppress_errors } })" \ + >/dev/null 2>&1 || true + + hyprctl_instance "$runtime_dir" "$signature" reload >/dev/null 2>&1 || true + + hyprctl_instance "$runtime_dir" "$signature" eval \ + "hl.config({ misc = { disable_autoreload = $disable_autoreload }, debug = { suppress_errors = $suppress_errors } })" \ + >/dev/null 2>&1 || true + fi + + rm -f "$state_file" +} + +case "$command" in + pause) + while IFS=$'\t' read -r runtime_dir uid signature; do + pause_instance "$runtime_dir" "$uid" "$signature" + done < <(instances) + ;; + resume) + [[ -d $state_dir ]] || exit 0 + find "$state_dir" -mindepth 1 -maxdepth 1 -type f -print 2>/dev/null | + while IFS= read -r state_file; do + resume_instance "$state_file" + done + rmdir "$state_dir" 2>/dev/null || true + ;; +esac diff --git a/default/libalpm/hooks/10-omarchy-hyprland-reload-pause.hook b/default/libalpm/hooks/10-omarchy-hyprland-reload-pause.hook new file mode 100644 index 00000000..27340491 --- /dev/null +++ b/default/libalpm/hooks/10-omarchy-hyprland-reload-pause.hook @@ -0,0 +1,12 @@ +[Trigger] +Operation = Install +Operation = Upgrade +Type = Package +Target = omarchy-settings +Target = omarchy-settings-dev + +[Action] +Description = Pausing Hyprland config auto-reload for Omarchy settings update... +When = PreTransaction +Depends = omarchy +Exec = /usr/bin/omarchy-hyprland-reload-guard pause diff --git a/default/libalpm/hooks/90-omarchy-hyprland-reload-resume.hook b/default/libalpm/hooks/90-omarchy-hyprland-reload-resume.hook new file mode 100644 index 00000000..4f1b1375 --- /dev/null +++ b/default/libalpm/hooks/90-omarchy-hyprland-reload-resume.hook @@ -0,0 +1,12 @@ +[Trigger] +Operation = Install +Operation = Upgrade +Type = Package +Target = omarchy-settings +Target = omarchy-settings-dev + +[Action] +Description = Reloading Hyprland after Omarchy settings update... +When = PostTransaction +Depends = omarchy +Exec = /usr/bin/omarchy-hyprland-reload-guard resume diff --git a/docs/file-layout.md b/docs/file-layout.md index 19814dec..fa6d2d7f 100644 --- a/docs/file-layout.md +++ b/docs/file-layout.md @@ -54,8 +54,8 @@ bin/omarchy-debug, bin/omarchy-debug-idle, bin/omarchy-upload-log ──► omarchy-settings /usr/bin/ (needed before omarchy is installed) -default/libalpm/hooks/00-omarchy-update-guard.hook - ──► omarchy /usr/share/libalpm/hooks/00-omarchy-update-guard.hook +default/libalpm/hooks/*.hook + ──► omarchy /usr/share/libalpm/hooks/*.hook install/** ──► omarchy /usr/share/omarchy/install/ migrations/system/** ──► omarchy /usr/share/omarchy/migrations/system/ diff --git a/docs/update-process.md b/docs/update-process.md index ca6d202a..6ef5f547 100644 --- a/docs/update-process.md +++ b/docs/update-process.md @@ -151,6 +151,14 @@ sudo env OMARCHY_ALLOW_DIRECT_PACMAN=1 pacman -Syu The guard does not start `omarchy update` itself because pacman is already in a transaction setup path; it only aborts with instructions. +The `omarchy` package also installs ALPM hooks for `omarchy-settings` / +`omarchy-settings-dev` installs and upgrades. The pre-transaction hook runs +`omarchy-hyprland-reload-guard pause` to disable live Hyprland config reloads +while `/usr/share/omarchy/default/hypr/**` is replaced. The post-transaction +hook runs `omarchy-hyprland-reload-guard resume`, forces one `hyprctl reload`, +and restores the session's previous `misc.disable_autoreload` and +`debug.suppress_errors` values. + ## Path 1: `omarchy update` High-level flow: diff --git a/test/shell.d/config-test.sh b/test/shell.d/config-test.sh index e2c6a85e..f28aedb7 100755 --- a/test/shell.d/config-test.sh +++ b/test/shell.d/config-test.sh @@ -127,12 +127,18 @@ for source, destination, legacy in package_defaults: if destination and (source not in pkgbuild or destination not in pkgbuild): errors.append(f"PKGBUILD does not explicitly install {source} -> {destination}") -guard_hook = "default/libalpm/hooks/00-omarchy-update-guard.hook" -guard_destination = "/usr/share/libalpm/hooks/00-omarchy-update-guard.hook" -if not (root / guard_hook).exists(): - errors.append(f"missing package default source: {guard_hook}") -if guard_hook not in omarchy_pkgbuild or guard_destination not in omarchy_pkgbuild: - errors.append(f"omarchy PKGBUILD does not install {guard_hook} -> {guard_destination}") +alpm_hooks = [ + "00-omarchy-update-guard.hook", + "10-omarchy-hyprland-reload-pause.hook", + "90-omarchy-hyprland-reload-resume.hook", +] +for hook in alpm_hooks: + source = f"default/libalpm/hooks/{hook}" + destination = f"/usr/share/libalpm/hooks/{hook}" + if not (root / source).exists(): + errors.append(f"missing package default source: {source}") + if source not in omarchy_pkgbuild or destination not in omarchy_pkgbuild: + errors.append(f"omarchy PKGBUILD does not install {source} -> {destination}") if errors: print("\n".join(errors), file=sys.stderr) diff --git a/test/shell.d/hyprland-reload-guard-test.sh b/test/shell.d/hyprland-reload-guard-test.sh new file mode 100755 index 00000000..6effb703 --- /dev/null +++ b/test/shell.d/hyprland-reload-guard-test.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "$0")/base-test.sh" + +test_tmp=$(mktemp -d) +trap 'rm -rf "$test_tmp"' EXIT + +run_root="$test_tmp/run-user" +state_dir="$test_tmp/state" +hyprctl_log="$test_tmp/hyprctl.log" +fake_hyprctl="$test_tmp/hyprctl" +signature="test-signature" +runtime_dir="$run_root/1000" + +mkdir -p "$runtime_dir/hypr/$signature" + +cat >"$fake_hyprctl" <<'BASH' +#!/bin/bash + +printf '%s\t%s\t%s\n' "$XDG_RUNTIME_DIR" "$HYPRLAND_INSTANCE_SIGNATURE" "$*" >>"$FAKE_HYPRCTL_LOG" + +case "$*" in + *'getoption misc.disable_autoreload'*) + printf '{"option":"misc.disable_autoreload","bool":%s,"set":true}\n' "${FAKE_DISABLE_AUTORELOAD:-false}" + ;; + *'getoption debug.suppress_errors'*) + printf '{"option":"debug.suppress_errors","bool":%s,"set":true}\n' "${FAKE_SUPPRESS_ERRORS:-false}" + ;; + *) + printf 'ok\n' + ;; +esac +BASH +chmod 0755 "$fake_hyprctl" + +FAKE_HYPRCTL_LOG="$hyprctl_log" \ + HYPRCTL="$fake_hyprctl" \ + OMARCHY_HYPRLAND_RELOAD_GUARD_RUN_ROOT="$run_root" \ + OMARCHY_HYPRLAND_RELOAD_GUARD_STATE_DIR="$state_dir" \ + "$ROOT/bin/omarchy-hyprland-reload-guard" pause + +state_file="$state_dir/$signature" +[[ -f $state_file ]] || fail "reload guard stores Hyprland state on pause" +grep -Fx $'1000 false false' "$state_file" >/dev/null || fail "reload guard records previous Hyprland reload settings" +grep -F 'hl.config({ misc = { disable_autoreload = true }, debug = { suppress_errors = true } })' "$hyprctl_log" >/dev/null || fail "reload guard pauses autoreload with hyprctl eval" +pass "reload guard pauses live Hyprland reloads" + +: >"$hyprctl_log" +FAKE_HYPRCTL_LOG="$hyprctl_log" \ + HYPRCTL="$fake_hyprctl" \ + OMARCHY_HYPRLAND_RELOAD_GUARD_RUN_ROOT="$run_root" \ + OMARCHY_HYPRLAND_RELOAD_GUARD_STATE_DIR="$state_dir" \ + "$ROOT/bin/omarchy-hyprland-reload-guard" resume + +[[ ! -e $state_file ]] || fail "reload guard clears Hyprland state after resume" +grep -F $'test-signature --instance test-signature reload' "$hyprctl_log" >/dev/null || fail "reload guard forces one Hyprland reload after package transaction" +grep -F 'hl.config({ misc = { disable_autoreload = false }, debug = { suppress_errors = false } })' "$hyprctl_log" >/dev/null || fail "reload guard restores previous Hyprland reload settings" +pass "reload guard resumes live Hyprland reloads"