Watch
1
0
Fork
You've already forked souveraine
0
souveraine/surfaces/quickshell/deploy.sh
Fimeg 16c6ee1033 multitasking: one owner for the transition, and the release travels
ZoneTransition owns the card rect, the four overview verbs, the end-target
table and the gesture clock. ZoneOverview and the rail read it; neither
derives a geometry.

Deletes poseActiveZone/clearPose/_posed, zoneCard.scale and
zonePullProgress. The card's ColumnLayout went too — its margin was a second
opinion about where a window sits in its zone; panes are the compositor's
tiling scaled.

The swipe felt wrong because the release committed where the thumb left it.
settleTo() travels to the destination first and commits on arrival, with
quickstep's numbers (350ms cap, min(1/0.7, 1/0.3)). Abandoned half-swipes
settle to last_zone instead of snapping. Dwell gate restored — quickstep
gates the same way and "ever paused" is a latch.

Card box is measured from the surface, not assumed from the screen: the
overview panel respects exclusive zones and its height moves.

Strand repro passes on hardware.
2026-08-07 12:26:12 -04:00

518 lines
28 KiB
Shell
Executable file

#!/usr/bin/env bash
# Compose the Souveraine quickshell config (`qs -c souveraine`).
#
# Model: ~/.config/quickshell/souveraine is BUILT by this script —
# - our files (this repo) are symlinked in, repo stays source of truth
# - untouched upstream directories are borrowed as whole-dir symlinks
# into the ii tree (so upstream updates flow through)
# - directories where we override any file are composed file-by-file
# The ii tree itself is DEPLOYED from this repo (ii-base/, the pinned base —
# vendored 2026-07-21 after laptop/phone drifted ~900 files): every run
# rsyncs ii-base -> ~/.config/quickshell/ii, so "borrowed from ii" means
# borrowed from the same pin on every device. On the phone (aarch64) the
# ii-phone/ overlay is applied on top — the declared home for phone-only
# files (Cellular, mobile bar/OSK/wallpaper behavior). Never hand-edit
# ~/.config/quickshell/ii; change ii-base/ (or ii-phone/) and redeploy.
#
# deploy.sh compose ~/.config/quickshell/souveraine
# deploy.sh -u remove the souveraine config dir (ii untouched)
# deploy.sh --legacy-clean remove the OLD overlay symlinks from ii and
# restore its .upstream backups (one-time migration)
# deploy.sh --phone copy this surface to the phone and deploy there.
# NEVER deletes on the device: it snapshots the phone's
# tree first, copies by checksum, and only *reports*
# files that exist solely on the phone.
set -euo pipefail
SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
QS="${HOME}/.config/quickshell"
II="${QS}/ii"
SV="${QS}/souveraine"
TARGET_ARCH="${SOUVERAINE_TARGET_ARCH:-$(uname -m)}"
# Manifest: "<repo-relative> <target-relative-to-~/.config/quickshell>"
MANIFEST="
shell.qml souveraine/shell.qml
GlobalStates.qml souveraine/GlobalStates.qml
SettingsWindow.qml souveraine/SettingsWindow.qml
panelFamilies/SouveraineFamily.qml souveraine/panelFamilies/SouveraineFamily.qml
services/Souveraine.qml souveraine/services/Souveraine.qml
services/Ai.qml souveraine/services/Ai.qml
services/Cellular.qml souveraine/services/Cellular.qml
services/ChargeRate.qml souveraine/services/ChargeRate.qml
services/Haptics.qml souveraine/services/Haptics.qml
services/Selection.qml souveraine/services/Selection.qml
services/DeviceEvidence.qml souveraine/services/DeviceEvidence.qml
services/Gestures.qml souveraine/services/Gestures.qml
modules/souveraine/dial/RadialDial.qml souveraine/modules/souveraine/dial/RadialDial.qml
modules/souveraine/dial/DialHost.qml souveraine/modules/souveraine/dial/DialHost.qml
modules/souveraine/dial/qmldir souveraine/modules/souveraine/dial/qmldir
modules/souveraine/selection/SelectionHost.qml souveraine/modules/souveraine/selection/SelectionHost.qml
modules/souveraine/selection/SelectionChip.qml souveraine/modules/souveraine/selection/SelectionChip.qml
modules/souveraine/selection/SelectionActions.qml souveraine/modules/souveraine/selection/SelectionActions.qml
modules/souveraine/selection/SelectionAction.qml souveraine/modules/souveraine/selection/SelectionAction.qml
modules/souveraine/selection/SelectionSeparator.qml souveraine/modules/souveraine/selection/SelectionSeparator.qml
modules/souveraine/selection/qmldir souveraine/modules/souveraine/selection/qmldir
modules/souveraine/windowSheet/WindowSheet.qml souveraine/modules/souveraine/windowSheet/WindowSheet.qml
modules/souveraine/windowSheet/SheetButton.qml souveraine/modules/souveraine/windowSheet/SheetButton.qml
modules/souveraine/windowSheet/PowerMenu.qml souveraine/modules/souveraine/windowSheet/PowerMenu.qml
modules/souveraine/windowSheet/qmldir souveraine/modules/souveraine/windowSheet/qmldir
services/Face.qml souveraine/services/Face.qml
services/ViewtopControl.qml souveraine/services/ViewtopControl.qml
services/ZoneTransition.qml souveraine/services/ZoneTransition.qml
services/Hyprsunset.qml souveraine/services/Hyprsunset.qml
modules/souveraine/subconscious/SubconsciousTicker.qml souveraine/modules/souveraine/subconscious/SubconsciousTicker.qml
modules/souveraine/subconscious/SubconsciousEventPanel.qml souveraine/modules/souveraine/subconscious/SubconsciousEventPanel.qml
modules/souveraine/subconscious/qmldir souveraine/modules/souveraine/subconscious/qmldir
services/Network.qml souveraine/services/Network.qml
services/TaskbarApps.qml souveraine/services/TaskbarApps.qml
services/GlobalFocusGrab.qml souveraine/services/GlobalFocusGrab.qml
services/Idle.qml souveraine/services/Idle.qml
services/IdleCoordinator.qml souveraine/services/IdleCoordinator.qml
services/LockContentPolicy.qml souveraine/services/LockContentPolicy.qml
services/WallpaperAssets.qml souveraine/services/WallpaperAssets.qml
services/WallpaperDownload.qml souveraine/services/WallpaperDownload.qml
scripts/wallpaper/download_wallhaven.sh souveraine/scripts/wallpaper/download_wallhaven.sh
services/ConflictKiller.qml souveraine/services/ConflictKiller.qml
services/SessionEvents.qml souveraine/services/SessionEvents.qml
services/SessiondBridge.qml souveraine/services/SessiondBridge.qml
services/SessiondPolicy.qml souveraine/services/SessiondPolicy.qml
services/StepUpAuth.qml souveraine/services/StepUpAuth.qml
services/SessionAudit.qml souveraine/services/SessionAudit.qml
services/Speech.qml souveraine/services/Speech.qml
services/NotifyEvents.qml souveraine/services/NotifyEvents.qml
services/CrashReporter.qml souveraine/services/CrashReporter.qml
modules/ii/sidebarLeft/SidebarLeft.qml souveraine/modules/ii/sidebarLeft/SidebarLeft.qml
modules/ii/sidebarLeft/AiChat.qml souveraine/modules/ii/sidebarLeft/AiChat.qml
modules/ii/sidebarRight/SidebarRight.qml souveraine/modules/ii/sidebarRight/SidebarRight.qml
modules/ii/sidebarRight/SidebarRightContent.qml souveraine/modules/ii/sidebarRight/SidebarRightContent.qml
modules/ii/sidebarRight/QuickSliders.qml souveraine/modules/ii/sidebarRight/QuickSliders.qml
modules/ii/sidebarRight/volumeMixer/VolumeDialogContent.qml souveraine/modules/ii/sidebarRight/volumeMixer/VolumeDialogContent.qml
modules/ii/bar/UtilButtons.qml souveraine/modules/ii/bar/UtilButtons.qml
modules/common/Config.qml souveraine/modules/common/Config.qml
modules/common/ShellModel.qml souveraine/modules/common/ShellModel.qml
modules/common/functions/Session.qml souveraine/modules/common/functions/Session.qml
modules/common/widgets/ContentPage.qml souveraine/modules/common/widgets/ContentPage.qml
modules/common/widgets/FullscreenPolkitWindow.qml souveraine/modules/common/widgets/FullscreenPolkitWindow.qml
modules/common/widgets/FloatingActionButton.qml souveraine/modules/common/widgets/FloatingActionButton.qml
modules/common/widgets/StyledToolTip.qml souveraine/modules/common/widgets/StyledToolTip.qml
modules/settings/DeviceConfig.qml souveraine/modules/settings/DeviceConfig.qml
modules/settings/NetworkConfig.qml souveraine/modules/settings/NetworkConfig.qml
modules/settings/DisplayConfig.qml souveraine/modules/settings/DisplayConfig.qml
modules/settings/SoundConfig.qml souveraine/modules/settings/SoundConfig.qml
modules/settings/LockConfig.qml souveraine/modules/settings/LockConfig.qml
modules/settings/WallpaperConfig.qml souveraine/modules/settings/WallpaperConfig.qml
modules/settings/OverviewConfig.qml souveraine/modules/settings/OverviewConfig.qml
modules/settings/DockConfig.qml souveraine/modules/settings/DockConfig.qml
modules/settings/NavigationConfig.qml souveraine/modules/settings/NavigationConfig.qml
modules/settings/KeyboardConfig.qml souveraine/modules/settings/KeyboardConfig.qml
modules/settings/IdleConfig.qml souveraine/modules/settings/IdleConfig.qml
modules/settings/SpeechConfig.qml souveraine/modules/settings/SpeechConfig.qml
modules/ii/polkit/Polkit.qml souveraine/modules/ii/polkit/Polkit.qml
modules/ii/dock/Dock.qml souveraine/modules/ii/dock/Dock.qml
modules/ii/dock/DockManifest.qml souveraine/modules/ii/dock/DockManifest.qml
modules/ii/dock/DockApps.qml souveraine/modules/ii/dock/DockApps.qml
modules/ii/dock/DockAppButton.qml souveraine/modules/ii/dock/DockAppButton.qml
modules/ii/dock/DockButton.qml souveraine/modules/ii/dock/DockButton.qml
modules/ii/dock/DockSeparator.qml souveraine/modules/ii/dock/DockSeparator.qml
modules/ii/dock/DockStack.qml souveraine/modules/ii/dock/DockStack.qml
modules/ii/appInventory/AppInventory.qml souveraine/modules/ii/appInventory/AppInventory.qml
modules/ii/appInventory/AppInventoryScope.qml souveraine/modules/ii/appInventory/AppInventoryScope.qml
modules/ii/overview/Overview.qml souveraine/modules/ii/overview/Overview.qml
modules/ii/overview/AppGrid.qml souveraine/modules/ii/overview/AppGrid.qml
modules/ii/background/widgets/clock/CookieClock.qml souveraine/modules/ii/background/widgets/clock/CookieClock.qml
modules/ii/background/widgets/clock/CookieQuote.qml souveraine/modules/ii/background/widgets/clock/CookieQuote.qml
modules/ii/screenCorners/ScreenCorners.qml souveraine/modules/ii/screenCorners/ScreenCorners.qml
modules/ii/onScreenKeyboard/OnScreenKeyboard.qml souveraine/modules/ii/onScreenKeyboard/OnScreenKeyboard.qml
modules/common/Persistent.qml souveraine/modules/common/Persistent.qml
modules/common/panels/lock/LockScreen.qml souveraine/modules/common/panels/lock/LockScreen.qml
modules/ii/lock/Lock.qml souveraine/modules/ii/lock/Lock.qml
modules/ii/lock/TouchLockSurface.qml souveraine/modules/ii/lock/TouchLockSurface.qml
modules/souveraine/lock/LockMediaCard.qml souveraine/modules/souveraine/lock/LockMediaCard.qml
modules/souveraine/lock/LockAgentCard.qml souveraine/modules/souveraine/lock/LockAgentCard.qml
modules/souveraine/lock/LockNotifyCard.qml souveraine/modules/souveraine/lock/LockNotifyCard.qml
modules/souveraine/lock/LockSurfaceHost.qml souveraine/modules/souveraine/lock/LockSurfaceHost.qml
modules/souveraine/lock/qmldir souveraine/modules/souveraine/lock/qmldir
modules/souveraine/navigation/SystemGestureRail.qml souveraine/modules/souveraine/navigation/SystemGestureRail.qml
modules/souveraine/navigation/WindowOverview.qml souveraine/modules/souveraine/navigation/WindowOverview.qml
modules/souveraine/navigation/ZoneOverview.qml souveraine/modules/souveraine/navigation/ZoneOverview.qml
modules/souveraine/navigation/qmldir souveraine/modules/souveraine/navigation/qmldir
modules/souveraine/boot/BootBloom.qml souveraine/modules/souveraine/boot/BootBloom.qml
modules/souveraine/boot/BootBloom.frag.qsb souveraine/modules/souveraine/boot/BootBloom.frag.qsb
modules/souveraine/boot/souvie.png souveraine/modules/souveraine/boot/souvie.png
modules/souveraine/boot/qmldir souveraine/modules/souveraine/boot/qmldir
modules/ii/sessionScreen/SessionScreen.qml souveraine/modules/ii/sessionScreen/SessionScreen.qml
qmldir souveraine/qmldir
panelFamilies/qmldir souveraine/panelFamilies/qmldir
services/qmldir souveraine/services/qmldir
modules/common/qmldir souveraine/modules/common/qmldir
modules/common/functions/qmldir souveraine/modules/common/functions/qmldir
modules/common/panels/lock/qmldir souveraine/modules/common/panels/lock/qmldir
modules/common/widgets/qmldir souveraine/modules/common/widgets/qmldir
modules/ii/appInventory/qmldir souveraine/modules/ii/appInventory/qmldir
modules/ii/bar/qmldir souveraine/modules/ii/bar/qmldir
modules/ii/dock/qmldir souveraine/modules/ii/dock/qmldir
modules/ii/lock/qmldir souveraine/modules/ii/lock/qmldir
modules/ii/onScreenKeyboard/qmldir souveraine/modules/ii/onScreenKeyboard/qmldir
modules/ii/overview/qmldir souveraine/modules/ii/overview/qmldir
modules/ii/polkit/qmldir souveraine/modules/ii/polkit/qmldir
modules/ii/screenCorners/qmldir souveraine/modules/ii/screenCorners/qmldir
modules/ii/sessionScreen/qmldir souveraine/modules/ii/sessionScreen/qmldir
modules/ii/sidebarLeft/qmldir souveraine/modules/ii/sidebarLeft/qmldir
modules/ii/sidebarRight/qmldir souveraine/modules/ii/sidebarRight/qmldir
modules/ii/sidebarRight/volumeMixer/qmldir souveraine/modules/ii/sidebarRight/volumeMixer/qmldir
modules/settings/qmldir souveraine/modules/settings/qmldir
"
# wifi fallback: PHONE_HOST=casey@10.10.20.234 ./deploy.sh --phone
PHONE_USB="${PHONE_HOST:-casey@172.16.42.1}"
PHONE_DEST="souveraine-surfaces/quickshell"
manifest_lines() { printf '%s\n' "$MANIFEST" | sed '/^[[:space:]]*$/d'; }
# Compile the boot bloom shader to .qsb (Qt RHI) if the source is newer. The
# .qsb is architecture-independent (SPIR-V + reflection), so we build it once
# here and the manifest symlink / phone rsync carries it — no qsb on the phone.
compile_shaders() {
local frag="$SRC/modules/souveraine/boot/BootBloom.frag"
local qsb_out="$SRC/modules/souveraine/boot/BootBloom.frag.qsb"
[[ -f "$frag" ]] || return 0
if [[ ! -f "$qsb_out" || "$frag" -nt "$qsb_out" ]]; then
local qsb
qsb="$(command -v qsb || echo /usr/lib/qt6/bin/qsb)"
if [[ ! -x "$qsb" ]]; then
echo "ERROR: qsb not found (need qt6-shadertools) to build BootBloom.frag.qsb" >&2
exit 1
fi
# --glsl 100es,120,150 + --hlsl/--msl targets so it runs on the Adreno
# GLES backend the phone uses; -O optimizes.
"$qsb" --glsl "100es,120,150" --hlsl 50 --msl 12 -O -o "$qsb_out" "$frag"
echo "compiled BootBloom.frag -> BootBloom.frag.qsb"
fi
}
compile_shaders
if [[ "${1:-}" == "--manifest" ]]; then
manifest_lines
exit 0
fi
if [[ "${1:-}" == "--phone" ]]; then
ssh_i=(ssh -F /dev/null -i "$HOME/.ssh/ani" -o ConnectTimeout=5)
maintenance_active=0
hypridle_was_active=0
phone_lock_state() {
local state last_secure
state="$("${ssh_i[@]}" "$PHONE_USB" \
"qs -c souveraine ipc --any-display call session state 2>/dev/null" || true)"
if grep -Eq '"locked"[[:space:]]*:[[:space:]]*false' <<< "$state" \
&& grep -Eq '"lockRequested"[[:space:]]*:[[:space:]]*false' <<< "$state"; then
printf '%s\n' unlocked
return
fi
if grep -Eq '"locked"[[:space:]]*:[[:space:]]*true|"lockRequested"[[:space:]]*:[[:space:]]*true' <<< "$state"; then
printf '%s\n' locked
return
fi
# Bootstrap for a phone that predates the session IPC handler. Every
# compositor-acknowledged lock edge is journaled by LockScreen.qml.
last_secure="$("${ssh_i[@]}" "$PHONE_USB" \
"journalctl --user -u souveraine-shell.service -b --no-pager | sed -n 's/.*\\[lock\\] session lock secure=\\(true\\|false\\).*/\\1/p' | tail -n 1" || true)"
[[ "$last_secure" == "false" ]] && printf '%s\n' unlocked || printf '%s\n' locked
}
finish_phone_maintenance() {
(( maintenance_active )) || return 0
if (( hypridle_was_active )); then
"${ssh_i[@]}" "$PHONE_USB" \
"systemctl --user restart hypridle.service; systemctl --user stop souveraine-deploy-lease.timer 2>/dev/null || true; systemctl --user reset-failed souveraine-deploy-lease.service 2>/dev/null || true" \
>/dev/null || true
fi
maintenance_active=0
}
if [[ "$(phone_lock_state)" != "unlocked" ]]; then
echo "ERROR: phone session is locked; refusing a live shell deploy" >&2
exit 1
fi
if "${ssh_i[@]}" "$PHONE_USB" "systemctl --user is-active --quiet hypridle.service"; then
hypridle_was_active=1
# Schedule recovery before stopping idle handling. If this deploy or
# its SSH connection dies, systemd restores hypridle within five
# minutes. Manual lock remains available throughout the lease.
"${ssh_i[@]}" "$PHONE_USB" \
"systemctl --user stop souveraine-deploy-lease.timer 2>/dev/null || true; systemd-run --user --unit=souveraine-deploy-lease --on-active=5min --timer-property=AccuracySec=1s --collect /usr/bin/systemctl --user restart hypridle.service >/dev/null; systemctl --user stop hypridle.service"
fi
maintenance_active=1
trap finish_phone_maintenance EXIT
# Close the small check→inhibit race. A manual lock always wins.
if [[ "$(phone_lock_state)" != "unlocked" ]]; then
echo "ERROR: phone locked while acquiring the maintenance lease" >&2
exit 1
fi
deployed_revision="unknown"
if repo_root="$(git -C "$SRC" rev-parse --show-toplevel 2>/dev/null)"; then
deployed_revision="$(git -C "$repo_root" rev-parse HEAD)"
if ! git -C "$repo_root" diff --quiet -- "$SRC" \
|| [[ -n "$(git -C "$repo_root" ls-files --others --exclude-standard -- "$SRC")" ]]; then
deployed_revision="${deployed_revision}-dirty"
fi
fi
"${ssh_i[@]}" "$PHONE_USB" "mkdir -p ~/$PHONE_DEST"
# NEVER --delete into the phone's tree.
#
# `~/souveraine-surfaces` on the device is not a git repo — nothing there has
# history — and files get edited directly on the phone. This path used to be
# `rsync -a --delete` with no snapshot, so an on-device edit was destroyed
# with no record that it had existed. It cost real work more than once, and
# it is the root of both the ~900-file ii drift and the stevia pkgrels that
# only ever existed on the phone.
#
# Note the asymmetry that made it survive: the LOCAL compose path below
# snapshots to .ii-previous/.souveraine-previous before it switches
# anything. Only the phone — the one machine that is a daily driver and has
# no history — was handled without one.
#
# So: snapshot first, copy without deleting, and REPORT what a --delete
# would have taken. Divergence becomes visible instead of being silently
# resolved in the laptop's favour.
PHONE_SNAPSHOT="souveraine-surfaces/.quickshell-previous"
if ! "${ssh_i[@]}" "$PHONE_USB" \
"mkdir -p ~/$PHONE_SNAPSHOT && rsync -a --delete ~/$PHONE_DEST/ ~/$PHONE_SNAPSHOT/"; then
echo "ERROR: could not snapshot the phone's surface tree — refusing to write to it" >&2
exit 1
fi
echo "phone tree snapshotted to ~/$PHONE_SNAPSHOT"
# What a --delete would have destroyed. Listed, never removed.
phone_only="$(rsync -ain --delete -e "ssh -F /dev/null -i $HOME/.ssh/ani" \
"$SRC/" "$PHONE_USB:$PHONE_DEST/" 2>/dev/null | sed -n 's/^deleting //p')"
if [[ -n "$phone_only" ]]; then
echo "phone-only files LEFT IN PLACE (a --delete would have destroyed these):" >&2
printf '%s\n' "$phone_only" | sed 's/^/ /' >&2
echo " → if any of that is work you want, copy it back into the repo now." >&2
fi
# Checksum, not timestamp. Plain -a reports files that differ only in mtime
# from earlier hand copies, which lies about what is actually changing.
rsync -ac -e "ssh -F /dev/null -i $HOME/.ssh/ani" "$SRC/" "$PHONE_USB:$PHONE_DEST/"
printf '%s\n' "$deployed_revision" \
| "${ssh_i[@]}" "$PHONE_USB" "cat > ~/$PHONE_DEST/DEPLOYED-REVISION"
"${ssh_i[@]}" "$PHONE_USB" "bash ~/$PHONE_DEST/deploy.sh"
# Phone-only: install mobile launchers into the app grid.
"${ssh_i[@]}" "$PHONE_USB" "mkdir -p ~/.local/share/applications && ln -sf ~/$PHONE_DEST/souveraine-settings.desktop ~/.local/share/applications/souveraine-settings.desktop && ln -sf ~/$PHONE_DEST/ii-base/modules/common/workspace-selector.desktop ~/.local/share/applications/workspace-selector.desktop"
finish_phone_maintenance
trap - EXIT
echo "Deployed to phone. Hyprland starts qs -c souveraine; no secondary shell is required."
exit 0
fi
if [[ "${1:-}" == "-u" || "${1:-}" == "--uninstall" ]]; then
rm -rf "$SV"
echo "removed $SV (ii tree untouched)"
exit 0
fi
if [[ "${1:-}" == "--legacy-clean" ]]; then
# One-time migration: strip the old overlay out of ii.
# 1. remove any symlink in ii that points into a souveraine checkout
find "$II" -type l | while read -r l; do
case "$(readlink "$l")" in
*souveraine*) rm -f "$l"; echo "unlinked ${l#$QS/}" ;;
esac
done
# 2. restore upstream backups
find "$II" -name '*.upstream' | while read -r u; do
mv -f "$u" "${u%.upstream}"
echo "restored ${u%.upstream}"
done
exit 0
fi
# --- Sync the pinned ii base --------------------------------------------
# Build both trees away from their live paths, then converge the stable live
# directories with delayed renames. Quickshell's watcher follows the config
# directory inode and does not tolerate replacing that root, even atomically.
# The three-phase order keeps every symlink valid throughout the update:
# add/update ii files, switch the composed surface, then delete stale ii files.
mkdir -p "$QS"
clear_generated_tree() {
local path="$1"
case "$path" in
"$QS/.ii-previous"|"$QS/.souveraine-previous"|"$QS"/.ii-next.*|"$QS"/.souveraine-next.*)
[[ ! -e "$path" && ! -L "$path" ]] || find "$path" -depth -delete
;;
*)
echo "refusing to clear unexpected rollback path: $path" >&2
return 1
;;
esac
}
snapshot_tree() { # live rollback
local live="$1" rollback="$2"
clear_generated_tree "$rollback"
if [[ -e "$live" || -L "$live" ]]; then
mkdir -p "$rollback"
rsync -a --delete --exclude='.git' "$live/" "$rollback/"
fi
}
II_NEXT="$QS/.ii-next.$$"
SV_NEXT="$QS/.souveraine-next.$$"
[[ ! -e "$II_NEXT" && ! -e "$SV_NEXT" ]] || {
echo "staging path already exists; refusing: $II_NEXT or $SV_NEXT" >&2
exit 1
}
cleanup_staging() {
clear_generated_tree "$II_NEXT"
clear_generated_tree "$SV_NEXT"
}
trap cleanup_staging EXIT
mkdir -p "$II_NEXT"
rsync -a --delete --exclude='.git' "$SRC/ii-base/" "$II_NEXT/"
if [[ "$TARGET_ARCH" == "aarch64" ]]; then
rsync -a "$SRC/ii-phone/" "$II_NEXT/"
ii_description="pin + phone overlay"
else
ii_description="pin"
fi
install -m 0644 "$SRC/ii-base.pin" "$II_NEXT/.souveraine-upstream-pin"
snapshot_tree "$II" "$QS/.ii-previous"
mkdir -p "$II"
# Phase 1: make every path the new composition can reference available, but
# retain old extras until their Souveraine override/symlink has switched.
rsync -a --delay-updates "$II_NEXT/" "$II/"
# --- Compose -------------------------------------------------------------
# Targets under souveraine/, relative to $SV
SV_TARGETS="$(manifest_lines | awk '$2 ~ /^souveraine\// {sub(/^souveraine\//, "", $2); print $2}')"
is_replaced() { # exact file override
grep -qxF "$1" <<< "$SV_TARGETS"
}
is_touched() { # dir contains an override somewhere below
grep -q "^$1/" <<< "$SV_TARGETS"
}
compose_dir() { # $1 = path relative to ii root ("" for root)
local rel="$1" entry name erel
mkdir -p "$SV_NEXT${rel:+/$rel}"
for entry in "$II_NEXT${rel:+/$rel}"/*; do
[[ -e "$entry" ]] || continue
name="$(basename "$entry")"
[[ "$name" == *.upstream ]] && continue
erel="${rel:+$rel/}$name"
if [[ -d "$entry" ]]; then
if is_touched "$erel"; then
compose_dir "$erel"
else
ln -sfn "$II/$erel" "$SV_NEXT/$erel"
fi
else
# root files and files inside touched dirs: link individually,
# skipping ones our manifest replaces
is_replaced "$erel" && continue
ln -sf "$II/$erel" "$SV_NEXT/$erel"
fi
done
}
# Build from scratch at the staging path: cheap, with no stale links.
compose_dir ""
# Our files on top (this also creates dirs that exist only in our tree,
# e.g. brand-new settings pages)
mkdir -p "$QS/pill"
while read -r rel target; do
[[ -z "$rel" ]] && continue
src="$SRC/$rel"; t="$SV_NEXT/${target#souveraine/}"
[[ -f "$src" ]] || { echo "missing $src" >&2; exit 1; }
mkdir -p "$(dirname "$t")"
ln -sf "$src" "$t"
done < <(manifest_lines)
snapshot_tree "$SV" "$QS/.souveraine-previous"
mkdir -p "$SV"
# Phase 2: switch the composed tree using delayed per-entry renames while its
# root directory remains stable for Quickshell's recursive watcher.
rsync -a --delete-delay --delay-updates "$SV_NEXT/" "$SV/"
# Phase 3: the composition no longer points at obsolete ii-only paths, so it
# is now safe to remove them and finish exact convergence with the pin.
rsync -a --delete-delay --delay-updates "$II_NEXT/" "$II/"
clear_generated_tree "$II_NEXT"
clear_generated_tree "$SV_NEXT"
trap - EXIT
echo "ii synced from $ii_description (rollback: $QS/.ii-previous)"
n_ours=$(manifest_lines | grep -c .)
n_borrowed_dirs=$(find "$SV" -maxdepth 3 -type l -xtype d | wc -l)
echo "souveraine config composed at $SV ($n_ours files ours, $n_borrowed_dirs dirs borrowed from ii)"
# Every qmldir entry must resolve to a file that exists in the composed tree.
#
# This is the failure that has bitten three times (ReloadPopup 2026-07-22,
# SessiondPolicy 07-25, ChargeRate 07-26). A qmldir naming a file that is not
# there does not fail that one type: Quickshell fails the whole MODULE, and
# `qs.services` failing takes GlobalStates with it, which takes shell.qml.
# The shell then refuses every reload and keeps serving the last scene that
# loaded — silently, for days. On 2026-07-27 the laptop was found running a
# 49-hour-old scene for exactly this reason.
#
# Loud, not fatal: the tree is already switched by this point, and the old
# tree is no better. Say it clearly and exit non-zero so a caller notices.
qmldir_broken=0
while IFS= read -r qd; do
dir=$(dirname "$qd")
# Field 2 of `singleton Name 1.0 File.qml` / `Name 1.0 File.qml` is the
# version for singletons and the file for plain types; take the last
# field, which is the filename in both shapes.
while read -r file; do
[[ -n "$file" ]] || continue
[[ -e "$dir/$file" ]] && continue
echo "BROKEN qmldir: $qd names '$file' which is not in the composed tree" >&2
qmldir_broken=1
done < <(awk '/\.qml[[:space:]]*$/ { print $NF }' "$qd")
done < <(find "$SV" -name qmldir -type f -o -name qmldir -type l)
if (( qmldir_broken )); then
echo "REFUSING TO CALL THIS DEPLOYED: the shell will fail every reload and" >&2
echo "keep serving its last good scene. Add the missing file to the deploy" >&2
echo "manifest above, or remove its qmldir line." >&2
exit 1
fi
# The phone's quick-toggle chooser is a WHOLE-FILE override of the base one, so
# a choice added to the base never reaches the phone unless it is added there
# too. That drift is silent by construction: the panel derives what it can build
# from the chooser, so a missing choice is not an error, it is a toggle that
# quietly stops existing on one device. Base must be a subset of phone.
#
# This is the same class as the qmldir check above and it has already cost a
# session: five configured toggles rendered nothing for a day because a second
# enumeration had drifted from this one.
chooser_rel="modules/ii/sidebarRight/quickToggles/androidStyle/AndroidToggleDelegateChooser.qml"
base_chooser="$SRC/ii-base/$chooser_rel"
phone_chooser="$SRC/ii-phone/$chooser_rel"
if [[ -f "$base_chooser" && -f "$phone_chooser" ]]; then
role_values() { grep -o 'roleValue: "[A-Za-z]*"' "$1" | sed 's/.*"\(.*\)"/\1/' | sort -u; }
missing_on_phone="$(comm -23 <(role_values "$base_chooser") <(role_values "$phone_chooser") | tr '\n' ' ')"
if [[ -n "${missing_on_phone// /}" ]]; then
echo "BROKEN toggle chooser overlay: ii-phone is missing base choices: $missing_on_phone" >&2
echo "The phone chooser overrides the base file wholesale — add the same" >&2
echo "DelegateChoice blocks to ii-phone/$chooser_rel or those toggles will" >&2
echo "silently not exist on the phone." >&2
exit 1
fi
fi
if grep -q 'qsConfig", "ii"' "$HOME/.config/hypr/hyprland/variables.lua" 2>/dev/null; then
echo "NOTE: hyprland qsConfig is still 'ii' — flip variables.lua to 'souveraine' to switch."
fi
echo "Run with: qs -c souveraine (restart: pkill -f 'qs -c souveraine')"