Watch
1
0
Fork
You've already forked souveraine
0
souveraine/surfaces/quickshell/deploy.sh

387 lines
20 KiB
Shell
Raw Normal View History

#!/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/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
quickshell: first-party lock/nav/session layer, retire the pill Grows Souveraine's own surfaces on top of the borrowed ii shell and drops the separate pill shell in favor of one integrated navigation rail. Session arbiter (functions/Session.qml): probe logind's Can* methods over busctl instead of guessing from installed binaries -- the answer carries the polkit tier (yes/challenge/na), so a swapless phone reports hibernate as na and refuses honestly rather than firing a verb that no-ops. Verbs run through a Process that logs exit codes and tracks lastAction; refusals log too. The busctl output is parsed with awk, not a sed regex buried under four escaping layers -- the sed version returned nothing on the phone and left every capability stuck at "unknown" (invisible on the laptop, where timing masked it). Every structured result is JSON-over-string; quickshell maps a var return to void. Lock trust: screenLocked (the shell's lock request) is now distinct from screenLockSecure (WlSessionLock.secure, the compositor's acknowledgement, mirrored from LockScreen). Cards that disclose personal data gate on secure, not on a button press. LockContentPolicy centralizes the ambient/personal/ step-up tiers so no card grows its own private rule. New first-party namespace modules/souveraine/: LockMediaCard, LockSurfaceHost, SystemGestureRail -- owned surfaces, not ii patches. IdleCoordinator gives one staged idle vocabulary (dim/lock) gated behind nativeCoordinatorEnabled, off until the native Wayland idle-notify is verified on the Pixel compositor; hypridle stays the adapter. WallpaperAssets selects aspect-aware variants for phone-vs-laptop display shapes. Pill retired: pill/shell.qml and PillConfig gone, replaced by NavigationConfig and the gesture rail. Hyprland starts qs -c souveraine directly; no secondary shell, no qsConfig flip. Verified on the phone: session.* reports challenge/na correctly, hibernate and unlock refuse, inhibit round-trips with its reason.
2026-07-14 20:00:57 -04:00
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/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
quickshell: session arbiter + fix IPC returns silently dropping payloads Fork ii's Session singleton and add a sessionctl.* surface. The IPC bug is the important half. Quickshell marshals exactly five types (string/int/bool/double/color) and maps a `var` return to VOID, discarding the payload with no error -- src/io/ipc.cpp, "void and var get mixed by qml engine". dock.*, shell.* and apps.* were all declared `: var`, so they registered as `(): void` and returned nothing at all. The {ok, reason} contract has never once reached a caller. All of them now return JSON as a string, which is what actually crosses the socket. Session: upstream fires `systemctl X || loginctl X` detached and throws the exit code away. Fine on a desktop with someone at the keyboard, not fine on a phone where the shell is the session manager and a verb that silently does nothing leaves you believing the machine is suspending when it isn't. So: probe loginctl/systemctl/hibernate once instead of assuming, run verbs through a Process that logs the exit code, and refuse honestly when the machine can't do the thing (the phone has no swap -- hibernate now says so instead of no-opping). Every upstream verb keeps its name and call sites. Inhibits carry a mandatory reason and get a cookie; state() lists who is holding the machine awake and why. "Why didn't it sleep" is now answerable. unlock() is refused by design -- the lock is the credential gate, so no IPC caller routes around the PIN pad. Named sessionctl, not session: ii's SessionScreen already owns "session", and quickshell drops duplicate targets silently rather than erroring. Idle: drop the 2>/dev/null and run hypridle through a Process, so a unit that fails to come back is a log line instead of a flat battery. Verified on the laptop: inhibit stops hypridle, uninhibit brings it back.
2026-07-14 18:31:59 -04:00
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/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
quickshell: first-party lock/nav/session layer, retire the pill Grows Souveraine's own surfaces on top of the borrowed ii shell and drops the separate pill shell in favor of one integrated navigation rail. Session arbiter (functions/Session.qml): probe logind's Can* methods over busctl instead of guessing from installed binaries -- the answer carries the polkit tier (yes/challenge/na), so a swapless phone reports hibernate as na and refuses honestly rather than firing a verb that no-ops. Verbs run through a Process that logs exit codes and tracks lastAction; refusals log too. The busctl output is parsed with awk, not a sed regex buried under four escaping layers -- the sed version returned nothing on the phone and left every capability stuck at "unknown" (invisible on the laptop, where timing masked it). Every structured result is JSON-over-string; quickshell maps a var return to void. Lock trust: screenLocked (the shell's lock request) is now distinct from screenLockSecure (WlSessionLock.secure, the compositor's acknowledgement, mirrored from LockScreen). Cards that disclose personal data gate on secure, not on a button press. LockContentPolicy centralizes the ambient/personal/ step-up tiers so no card grows its own private rule. New first-party namespace modules/souveraine/: LockMediaCard, LockSurfaceHost, SystemGestureRail -- owned surfaces, not ii patches. IdleCoordinator gives one staged idle vocabulary (dim/lock) gated behind nativeCoordinatorEnabled, off until the native Wayland idle-notify is verified on the Pixel compositor; hypridle stays the adapter. WallpaperAssets selects aspect-aware variants for phone-vs-laptop display shapes. Pill retired: pill/shell.qml and PillConfig gone, replaced by NavigationConfig and the gesture rail. Hyprland starts qs -c souveraine directly; no secondary shell, no qsConfig flip. Verified on the phone: session.* reports challenge/na correctly, hibernate and unlock refuse, inhibit round-trips with its reason.
2026-07-14 20:00:57 -04:00
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
quickshell: first-party lock/nav/session layer, retire the pill Grows Souveraine's own surfaces on top of the borrowed ii shell and drops the separate pill shell in favor of one integrated navigation rail. Session arbiter (functions/Session.qml): probe logind's Can* methods over busctl instead of guessing from installed binaries -- the answer carries the polkit tier (yes/challenge/na), so a swapless phone reports hibernate as na and refuses honestly rather than firing a verb that no-ops. Verbs run through a Process that logs exit codes and tracks lastAction; refusals log too. The busctl output is parsed with awk, not a sed regex buried under four escaping layers -- the sed version returned nothing on the phone and left every capability stuck at "unknown" (invisible on the laptop, where timing masked it). Every structured result is JSON-over-string; quickshell maps a var return to void. Lock trust: screenLocked (the shell's lock request) is now distinct from screenLockSecure (WlSessionLock.secure, the compositor's acknowledgement, mirrored from LockScreen). Cards that disclose personal data gate on secure, not on a button press. LockContentPolicy centralizes the ambient/personal/ step-up tiers so no card grows its own private rule. New first-party namespace modules/souveraine/: LockMediaCard, LockSurfaceHost, SystemGestureRail -- owned surfaces, not ii patches. IdleCoordinator gives one staged idle vocabulary (dim/lock) gated behind nativeCoordinatorEnabled, off until the native Wayland idle-notify is verified on the Pixel compositor; hypridle stays the adapter. WallpaperAssets selects aspect-aware variants for phone-vs-laptop display shapes. Pill retired: pill/shell.qml and PillConfig gone, replaced by NavigationConfig and the gesture rail. Hyprland starts qs -c souveraine directly; no secondary shell, no qsConfig flip. Verified on the phone: session.* reports challenge/na correctly, hibernate and unlock refuse, inhibit round-trips with its reason.
2026-07-14 20:00:57 -04:00
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
quickshell: first-party lock/nav/session layer, retire the pill Grows Souveraine's own surfaces on top of the borrowed ii shell and drops the separate pill shell in favor of one integrated navigation rail. Session arbiter (functions/Session.qml): probe logind's Can* methods over busctl instead of guessing from installed binaries -- the answer carries the polkit tier (yes/challenge/na), so a swapless phone reports hibernate as na and refuses honestly rather than firing a verb that no-ops. Verbs run through a Process that logs exit codes and tracks lastAction; refusals log too. The busctl output is parsed with awk, not a sed regex buried under four escaping layers -- the sed version returned nothing on the phone and left every capability stuck at "unknown" (invisible on the laptop, where timing masked it). Every structured result is JSON-over-string; quickshell maps a var return to void. Lock trust: screenLocked (the shell's lock request) is now distinct from screenLockSecure (WlSessionLock.secure, the compositor's acknowledgement, mirrored from LockScreen). Cards that disclose personal data gate on secure, not on a button press. LockContentPolicy centralizes the ambient/personal/ step-up tiers so no card grows its own private rule. New first-party namespace modules/souveraine/: LockMediaCard, LockSurfaceHost, SystemGestureRail -- owned surfaces, not ii patches. IdleCoordinator gives one staged idle vocabulary (dim/lock) gated behind nativeCoordinatorEnabled, off until the native Wayland idle-notify is verified on the Pixel compositor; hypridle stays the adapter. WallpaperAssets selects aspect-aware variants for phone-vs-laptop display shapes. Pill retired: pill/shell.qml and PillConfig gone, replaced by NavigationConfig and the gesture rail. Hyprland starts qs -c souveraine directly; no secondary shell, no qsConfig flip. Verified on the phone: session.* reports challenge/na correctly, hibernate and unlock refuse, inhibit round-trips with its reason.
2026-07-14 20:00:57 -04:00
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
quickshell: first-party lock/nav/session layer, retire the pill Grows Souveraine's own surfaces on top of the borrowed ii shell and drops the separate pill shell in favor of one integrated navigation rail. Session arbiter (functions/Session.qml): probe logind's Can* methods over busctl instead of guessing from installed binaries -- the answer carries the polkit tier (yes/challenge/na), so a swapless phone reports hibernate as na and refuses honestly rather than firing a verb that no-ops. Verbs run through a Process that logs exit codes and tracks lastAction; refusals log too. The busctl output is parsed with awk, not a sed regex buried under four escaping layers -- the sed version returned nothing on the phone and left every capability stuck at "unknown" (invisible on the laptop, where timing masked it). Every structured result is JSON-over-string; quickshell maps a var return to void. Lock trust: screenLocked (the shell's lock request) is now distinct from screenLockSecure (WlSessionLock.secure, the compositor's acknowledgement, mirrored from LockScreen). Cards that disclose personal data gate on secure, not on a button press. LockContentPolicy centralizes the ambient/personal/ step-up tiers so no card grows its own private rule. New first-party namespace modules/souveraine/: LockMediaCard, LockSurfaceHost, SystemGestureRail -- owned surfaces, not ii patches. IdleCoordinator gives one staged idle vocabulary (dim/lock) gated behind nativeCoordinatorEnabled, off until the native Wayland idle-notify is verified on the Pixel compositor; hypridle stays the adapter. WallpaperAssets selects aspect-aware variants for phone-vs-laptop display shapes. Pill retired: pill/shell.qml and PillConfig gone, replaced by NavigationConfig and the gesture rail. Hyprland starts qs -c souveraine directly; no secondary shell, no qsConfig flip. Verified on the phone: session.* reports challenge/na correctly, hibernate and unlock refuse, inhibit round-trips with its reason.
2026-07-14 20:00:57 -04:00
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 the mobile settings launcher 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"
finish_phone_maintenance
trap - EXIT
quickshell: first-party lock/nav/session layer, retire the pill Grows Souveraine's own surfaces on top of the borrowed ii shell and drops the separate pill shell in favor of one integrated navigation rail. Session arbiter (functions/Session.qml): probe logind's Can* methods over busctl instead of guessing from installed binaries -- the answer carries the polkit tier (yes/challenge/na), so a swapless phone reports hibernate as na and refuses honestly rather than firing a verb that no-ops. Verbs run through a Process that logs exit codes and tracks lastAction; refusals log too. The busctl output is parsed with awk, not a sed regex buried under four escaping layers -- the sed version returned nothing on the phone and left every capability stuck at "unknown" (invisible on the laptop, where timing masked it). Every structured result is JSON-over-string; quickshell maps a var return to void. Lock trust: screenLocked (the shell's lock request) is now distinct from screenLockSecure (WlSessionLock.secure, the compositor's acknowledgement, mirrored from LockScreen). Cards that disclose personal data gate on secure, not on a button press. LockContentPolicy centralizes the ambient/personal/ step-up tiers so no card grows its own private rule. New first-party namespace modules/souveraine/: LockMediaCard, LockSurfaceHost, SystemGestureRail -- owned surfaces, not ii patches. IdleCoordinator gives one staged idle vocabulary (dim/lock) gated behind nativeCoordinatorEnabled, off until the native Wayland idle-notify is verified on the Pixel compositor; hypridle stays the adapter. WallpaperAssets selects aspect-aware variants for phone-vs-laptop display shapes. Pill retired: pill/shell.qml and PillConfig gone, replaced by NavigationConfig and the gesture rail. Hyprland starts qs -c souveraine directly; no secondary shell, no qsConfig flip. Verified on the phone: session.* reports challenge/na correctly, hibernate and unlock refuse, inhibit round-trips with its reason.
2026-07-14 20:00:57 -04:00
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)"
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')"