diff --git a/bin/omarchy-theme-bg-set b/bin/omarchy-theme-bg-set index 835321d7..34100c2f 100755 --- a/bin/omarchy-theme-bg-set +++ b/bin/omarchy-theme-bg-set @@ -20,6 +20,6 @@ fi # Create symlink to the new background media ln -nsf "$BACKGROUND" "$CURRENT_BACKGROUND_LINK" -# Update the live shell background immediately when it is running. The -# background plugin also polls this symlink, but IPC avoids the visible delay. +# Update the live shell background when it is running. Nothing watches the +# symlink, so this is how the shell learns of the new background. omarchy-shell -q background set "$BACKGROUND" diff --git a/bin/omarchy-theme-set b/bin/omarchy-theme-set index 87341efd..d6922c5d 100755 --- a/bin/omarchy-theme-set +++ b/bin/omarchy-theme-set @@ -207,11 +207,14 @@ set_theme_background() { shell_ipc shell applyTheme "$colors_payload" "$shell_payload" || true ln -nsf "$new_background" "$CURRENT_BACKGROUND_LINK" ( - if ! omarchy-theme-bg-boot-intro --theme-switch "$new_background" "$theme_id" "$intro_prepared"; then - if [[ $(readlink -f "$CURRENT_BACKGROUND_LINK") == "$(readlink -f "$new_background")" && - $(stat -Lc '%d:%i' "$CURRENT_THEME_PATH" 2>/dev/null) == "$theme_id" ]]; then - shell_ipc background setInstant "$new_background" || true - fi + # Played or not, the shell gets the new still: it still holds the old + # theme's path, which the theme swap above deleted, and loads it again + # when OWE hands the desktop back after the intro. While the intro plays + # the shell's background is suspended under it, so the swap is unseen. + omarchy-theme-bg-boot-intro --theme-switch "$new_background" "$theme_id" "$intro_prepared" || true + if [[ $(readlink -f "$CURRENT_BACKGROUND_LINK") == "$(readlink -f "$new_background")" && + $(stat -Lc '%d:%i' "$CURRENT_THEME_PATH" 2>/dev/null) == "$theme_id" ]]; then + shell_ipc background setInstant "$new_background" || true fi shell_ipc shell finishThemeIntro "$theme_id" || true wait_for_theme_intro themeIntroStatus "$theme_id" "pending" diff --git a/shell/Ui/BackgroundMedia.qml b/shell/Ui/BackgroundMedia.qml index 8640769f..fd818598 100644 --- a/shell/Ui/BackgroundMedia.qml +++ b/shell/Ui/BackgroundMedia.qml @@ -18,6 +18,8 @@ Item { property size decodeSize: Qt.size(0, 0) readonly property var current: imageLoader.item readonly property bool ready: current ? current.ready : false + // The file behind path could not be loaded, as when it was deleted. + readonly property bool failed: current ? current.failed : false readonly property bool video: Util.isVideoPath(path) // Cache-bust images selected in a running lock session, so a theme switch // that replaces the file behind an unchanged path shows the new pixels. @@ -35,6 +37,7 @@ Item { Image { readonly property bool ready: status === Image.Ready + readonly property bool failed: status === Image.Error source: !root.constrainDecode || root.decodeSize.width > 0 ? root.imageUrl : "" fillMode: Image.PreserveAspectCrop asynchronous: true diff --git a/shell/plugins/background/Background.qml b/shell/plugins/background/Background.qml index d5e0741d..a05f110c 100644 --- a/shell/plugins/background/Background.qml +++ b/shell/plugins/background/Background.qml @@ -16,6 +16,7 @@ Item { readonly property string home: Quickshell.env("HOME") readonly property string stateHome: home + "/.local/state" readonly property string currentBackgroundLink: stateHome + "/omarchy/current/background" + readonly property string currentThemeBackgrounds: stateHome + "/omarchy/current/theme/backgrounds" property string currentBackground: "" property string displayedBackground: "" @@ -66,6 +67,36 @@ Item { if (!readlinkProc.running) readlinkProc.running = true } + // The desktop must never be left without a wallpaper. A path that stops + // loading, say one that ran through a theme since replaced, is resolved + // again from the background link, which a theme switch always repoints. + // Only a different path is taken, so a file that is there but unreadable + // cannot loop. Resolving runs out of process, so a background chosen in the + // meantime wins over what it finds; if that one fails too, it is resolved + // in turn once the running lookup is done. + property string failedBackground: "" + property int recoverVersion: -1 + + function recoverBackground(failedPath) { + if (!failedPath || failedPath !== displayedBackground) return + failedBackground = failedPath + if (recoverProc.running) return + recoverVersion = backgroundVersion + recoverProc.running = true + } + + function finishRecovery(path) { + path = String(path || "").trim() + var failedPath = failedBackground + failedBackground = "" + if (recoverVersion !== backgroundVersion) { + recoverBackground(failedPath) + return + } + if (!path || path === failedPath) return + setBackground(path, true) + } + function setBackground(path, instant) { if (instant) reloadVersion += 1 transitionBackground("", path, path, instant, instant) @@ -85,6 +116,7 @@ Item { requestNativeSize(finalPath) currentBackground = finalPath backgroundVersion += 1 + failedBackground = "" revealStartedVersion = -1 revealAnimation.stop() @@ -220,6 +252,21 @@ Item { } } + // The link's target when it exists, or else the theme's first background, + // so a link left dangling still finds a wallpaper. + Process { + id: recoverProc + command: [ + "bash", "-c", + 'target=$(readlink -f "$1"); if [[ -e $target ]]; then echo "$target"; else ' + + 'find "$2" -maxdepth 1 -type f \\( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.webp" \\) | sort | head -n 1; fi', + "resolve", root.currentBackgroundLink, root.currentThemeBackgrounds + ] + stdout: StdioCollector { + onStreamFinished: root.finishRecovery(text) + } + } + ShellIpc { target: "background" @@ -365,6 +412,7 @@ Item { cached: true constrainDecode: true decodeSize: panel.decodeSize(root.displayedBackground) + onFailedChanged: if (failed) root.recoverBackground(path) onReadyChanged: { panel.readyFrames = 0 if (ready && root.finishingTransition) { diff --git a/test/shell.d/theme-intro-switch-test.sh b/test/shell.d/theme-intro-switch-test.sh index 8c172b03..71ad1abe 100755 --- a/test/shell.d/theme-intro-switch-test.sh +++ b/test/shell.d/theme-intro-switch-test.sh @@ -119,6 +119,9 @@ grep -Fxq "owe: intro --start first-frame --refresh $state/theme/backgrounds/int [[ $(readlink "$state/background") == "$state/theme/backgrounds/2-road.webp" ]] || fail "the remembered background is selected" ! grep -qE '^shell: background (prepare|themeTransition)' "$log" || fail "an intro replaces the normal still transition" [[ $(<"$home/.local/state/omarchy/background-intro.session-id") == already-consumed ]] || fail "a theme switch leaves login consumption unchanged" +# The shell holds the old theme's still, whose path the swap deleted, and loads +# it again when OWE hands the desktop back, so it gets the new one either way. +wait_command "^shell: background setInstant $state/theme/backgrounds/2-road.webp\$" pass "switching themes plays the matching intro for the remembered background" set_theme beta @@ -176,7 +179,12 @@ wait_command '^owe: intro ' [[ ! -f $gates/premature-swap ]] || fail "the outgoing fade and cover settle before replacing the active theme" pass "rapid switches present the outgoing video cover before replacing its still" -wait_command '^hypr-reload$' +# Earlier switches finish in the background, so the next one waits for them, +# or a late reload of theirs reads as one during its intro. +for attempt in {1..250}; do + pgrep -f "^/bin/bash $ROOT/bin/omarchy-theme-set (alpha|beta)\$" >/dev/null || break + sleep 0.02 +done release="$test_tmp/reload-release" TEST_INTRO_PID="$test_tmp/intro.pid" TEST_RELEASE="$release" set_theme alpha wait_command '^owe: intro '