Overlay layer following DialHost, keyboardFocus None throughout, and an input mask limited to the card so taps elsewhere reach the app underneath. Anchors to the pointer hint above the touch point, clamped on screen. Chip shows a character count, never a preview: the surface floats over the app that owns the selection and the content may be a password, so it stays ambient and discloses nothing. Read Aloud is live via Speech; agent and reference actions render with the reason they cannot act yet. Adds DeviceEvidence, reporting input to the state machine's existing Request::Input with an intent label, so a new input surface is not another isolated actor per DEVICE-STATE-MACHINE 1. Opt-in: nothing loads or watches until Config.options.selection.enable.
436 lines
23 KiB
Shell
Executable file
436 lines
23 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 rsync this surface to the phone and deploy there
|
|
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
|
|
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/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/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/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/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"
|
|
rsync -a --delete -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
|
|
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')"
|