diff --git a/bin/omarchy-theme-set b/bin/omarchy-theme-set index 4bc3130a..346f5a17 100755 --- a/bin/omarchy-theme-set +++ b/bin/omarchy-theme-set @@ -13,6 +13,7 @@ CURRENT_THEME_PATH="$HOME/.config/omarchy/current/theme" NEXT_THEME_PATH="$HOME/.config/omarchy/current/next-theme" CURRENT_BACKGROUND_LINK="$HOME/.config/omarchy/current/background" BACKGROUND_TRANSITION_CACHE="$HOME/.cache/omarchy/background-transitions" +THEME_SET_LOCK="${XDG_RUNTIME_DIR:-/tmp}/omarchy-theme-set.lock" USER_THEMES_PATH="$HOME/.config/omarchy/themes" OMARCHY_THEMES_PATH="$OMARCHY_PATH/themes" @@ -30,22 +31,34 @@ run_parallel() { done } -snapshot_current_background() { - local current_background snapshot extension +shell_ipc() { + timeout 2 omarchy-shell-ipc --if-running "$@" >/dev/null 2>&1 +} - current_background=$(readlink -f "$CURRENT_BACKGROUND_LINK" 2>/dev/null || true) - [[ -f $current_background ]] || return +snapshot_background_path() { + local background="$1" + local name="$2" + local snapshot extension + + [[ -f $background ]] || return mkdir -p "$BACKGROUND_TRANSITION_CACHE" - extension=${current_background##*.} - snapshot="$BACKGROUND_TRANSITION_CACHE/previous-$$.$extension" - ln "$current_background" "$snapshot" 2>/dev/null || cp "$current_background" "$snapshot" + extension=${background##*.} + snapshot="$BACKGROUND_TRANSITION_CACHE/$name-$$.$extension" + ln "$background" "$snapshot" 2>/dev/null || cp "$background" "$snapshot" echo "$snapshot" } +snapshot_current_background() { + local current_background + + current_background=$(readlink -f "$CURRENT_BACKGROUND_LINK" 2>/dev/null || true) + snapshot_background_path "$current_background" "previous" +} + set_theme_background() { local backgrounds=() - local current_background index new_background next_index + local current_background index new_background next_index new_background_snapshot mapfile -d '' -t backgrounds < <( find -L "$HOME/.config/omarchy/backgrounds/$THEME_NAME/" "$CURRENT_THEME_PATH/backgrounds/" -maxdepth 1 -type f \ @@ -55,7 +68,7 @@ set_theme_background() { if (( ${#backgrounds[@]} == 0 )); then omarchy-notification-send "No background was found for theme" -t 2000 - omarchy-shell-ipc-fast shell applyTheme "$colors_payload" "$shell_payload" >/dev/null 2>&1 || true + shell_ipc shell applyTheme "$colors_payload" "$shell_payload" || true return fi @@ -75,16 +88,25 @@ set_theme_background() { new_background="${backgrounds[$next_index]}" fi - ln -nsf "$new_background" "$CURRENT_BACKGROUND_LINK" + new_background_snapshot=$(snapshot_background_path "$new_background" "next") - if [[ -f $OLD_BACKGROUND_SNAPSHOT ]]; then - omarchy-shell-ipc-fast background themeTransition "$OLD_BACKGROUND_SNAPSHOT" "$new_background" "$colors_payload" "$shell_payload" >/dev/null 2>&1 || \ - omarchy-shell-ipc-fast shell applyTheme "$colors_payload" "$shell_payload" >/dev/null 2>&1 || true - (sleep 3; rm -f "$OLD_BACKGROUND_SNAPSHOT") & + if [[ -f $OLD_BACKGROUND_SNAPSHOT && -f $new_background_snapshot ]]; then + shell_ipc background themeTransition "$OLD_BACKGROUND_SNAPSHOT" "$new_background_snapshot" "$new_background" "$colors_payload" "$shell_payload" || \ + shell_ipc shell applyTheme "$colors_payload" "$shell_payload" || true + (sleep 3; rm -f "$OLD_BACKGROUND_SNAPSHOT" "$new_background_snapshot") & + elif [[ -f $new_background_snapshot ]]; then + shell_ipc background themeTransition "" "$new_background_snapshot" "$new_background" "$colors_payload" "$shell_payload" || \ + shell_ipc shell applyTheme "$colors_payload" "$shell_payload" || true + (sleep 3; rm -f "$new_background_snapshot") & else - omarchy-shell-ipc-fast background themeTransition "" "$new_background" "$colors_payload" "$shell_payload" >/dev/null 2>&1 || \ - omarchy-shell-ipc-fast shell applyTheme "$colors_payload" "$shell_payload" >/dev/null 2>&1 || true + shell_ipc background themeTransition "$OLD_BACKGROUND_SNAPSHOT" "$new_background" "$new_background" "$colors_payload" "$shell_payload" || \ + shell_ipc shell applyTheme "$colors_payload" "$shell_payload" || true + if [[ -f $OLD_BACKGROUND_SNAPSHOT ]]; then + (sleep 3; rm -f "$OLD_BACKGROUND_SNAPSHOT") & + fi fi + + ln -nsf "$new_background" "$CURRENT_BACKGROUND_LINK" } THEME_NAME=$(echo "$1" | sed -E 's/<[^>]+>//g' | tr '[:upper:]' '[:lower:]' | tr ' ' '-') @@ -94,9 +116,15 @@ if [[ ! -d $OMARCHY_THEMES_PATH/$THEME_NAME ]] && [[ ! -d $USER_THEMES_PATH/$THE exit 1 fi +# Serialize theme changes. Theme switching rebuilds a shared next-theme staging +# directory and updates current theme/background symlinks; concurrent calls can +# otherwise race and make one selection appear to be ignored. +exec 9>"$THEME_SET_LOCK" +flock 9 + # Prevent file-watch reloads from changing shell colors before the background # reveal is ready. The theme IPC below resumes reloads as it applies colors. -omarchy-shell-ipc --if-running shell suspendThemeReloads >/dev/null 2>&1 || true +shell_ipc shell suspendThemeReloads || true # Setup clean next theme directory (for atomic theme config swapping) rm -rf "$NEXT_THEME_PATH" @@ -131,7 +159,7 @@ echo "$THEME_NAME" >"$HOME/.config/omarchy/current/theme.name" colors_payload=$([[ -f $CURRENT_THEME_PATH/colors.toml ]] && base64 -w 0 "$CURRENT_THEME_PATH/colors.toml") shell_payload=$([[ -f $CURRENT_THEME_PATH/shell.toml ]] && base64 -w 0 "$CURRENT_THEME_PATH/shell.toml") if [[ $OMARCHY_THEME_SKIP_BACKGROUND == "1" ]]; then - omarchy-shell-ipc-fast shell applyTheme "$colors_payload" "$shell_payload" >/dev/null 2>&1 || true + shell_ipc shell applyTheme "$colors_payload" "$shell_payload" || true else set_theme_background fi diff --git a/default/quickshell/omarchy-shell/plugins/background/Background.qml b/default/quickshell/omarchy-shell/plugins/background/Background.qml index e4e4325d..cba235b1 100644 --- a/default/quickshell/omarchy-shell/plugins/background/Background.qml +++ b/default/quickshell/omarchy-shell/plugins/background/Background.qml @@ -2,6 +2,8 @@ import Quickshell import Quickshell.Io import Quickshell.Wayland import QtQuick +import QtQuick.Effects +import QtQuick.Shapes import qs.Commons as NoctaliaCommons Item { @@ -36,14 +38,15 @@ Item { } function setBackground(path, instant) { - transitionBackground("", path, instant) + transitionBackground("", path, path, instant, false) } - function transitionBackground(fromPath, path, instant) { + function transitionBackground(fromPath, path, finalPath, instant, force) { path = String(path || "").trim() + finalPath = String(finalPath || path).trim() fromPath = String(fromPath || "").trim() - if (!path || path === currentBackground) return - currentBackground = path + if (!path || (!force && finalPath === currentBackground)) return + currentBackground = finalPath backgroundVersion += 1 revealStartedVersion = -1 @@ -83,8 +86,8 @@ Item { pendingShellRaw = "" } - function transitionBackgroundWithTheme(fromPath, path, colorsB64, shellB64) { - transitionBackground(fromPath, path, false) + function transitionBackgroundWithTheme(fromPath, path, finalPath, colorsB64, shellB64) { + transitionBackground(fromPath, path, finalPath, false, true) setPendingTheme(colorsB64, shellB64) if (!incomingBackground || revealProgress >= 1) applyPendingTheme() } @@ -142,11 +145,11 @@ Item { } function transition(fromPath: string, path: string): void { - root.transitionBackground(fromPath, path, false) + root.transitionBackground(fromPath, path, path, false, false) } - function themeTransition(fromPath: string, path: string, colorsB64: string, shellB64: string): void { - root.transitionBackgroundWithTheme(fromPath, path, colorsB64, shellB64) + function themeTransition(fromPath: string, path: string, finalPath: string, colorsB64: string, shellB64: string): void { + root.transitionBackgroundWithTheme(fromPath, path, finalPath, colorsB64, shellB64) } } @@ -167,7 +170,7 @@ Item { easing.type: Easing.InOutCubic onFinished: { if (root.incomingBackground) { - root.displayedBackground = root.incomingBackground + root.displayedBackground = root.currentBackground || root.incomingBackground root.finishingTransition = true } root.revealProgress = 1 @@ -188,6 +191,19 @@ Item { anchors { top: true; bottom: true; left: true; right: true } color: "transparent" property bool maskReady: false + + function maybeStartReveal() { + if (!root.incomingBackground || root.revealProgress !== 0 || maskReady) return + if (incomingFrame.status !== Image.Ready) return + if (root.oldBackground && oldFrame.status !== Image.Ready) return + Qt.callLater(function() { + if (!root.incomingBackground || root.revealProgress !== 0 || maskReady) return + if (incomingFrame.status !== Image.Ready) return + if (root.oldBackground && oldFrame.status !== Image.Ready) return + root.startReveal(panel) + }) + } + WlrLayershell.namespace: "omarchy-background" WlrLayershell.layer: WlrLayer.Background WlrLayershell.keyboardFocus: WlrKeyboardFocus.None @@ -199,7 +215,7 @@ Item { source: root.imageUrl(root.displayedBackground) fillMode: Image.PreserveAspectCrop asynchronous: true - cache: false + cache: true onStatusChanged: { if (status === Image.Ready && root.finishingTransition) { root.incomingBackground = "" @@ -209,18 +225,6 @@ Item { } } - Image { - id: incomingFrame - anchors.fill: parent - source: root.imageUrl(root.incomingBackground) - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: false - visible: root.incomingBackground !== "" && status === Image.Ready - opacity: root.revealProgress >= 1 ? 1 : 0.001 - onStatusChanged: if (status === Image.Ready && root.incomingBackground) revealCanvas.prepareImage() - } - Image { id: oldFrame anchors.fill: parent @@ -228,94 +232,71 @@ Item { fillMode: Image.PreserveAspectCrop asynchronous: true cache: false + smooth: true + mipmap: true visible: root.oldBackground !== "" && root.revealProgress < 1 - onStatusChanged: if (status === Image.Ready && root.incomingBackground) revealCanvas.requestPaint() + onStatusChanged: panel.maybeStartReveal() } - Canvas { - id: revealCanvas + Item { + id: incomingLayer anchors.fill: parent - visible: root.incomingBackground !== "" && root.revealProgress < 1 && incomingFrame.status === Image.Ready && oldFrame.status === Image.Ready - opacity: panel.maskReady ? 1 : 0 - renderTarget: Canvas.FramebufferObject - renderStrategy: Canvas.Immediate + visible: root.incomingBackground !== "" && incomingFrame.status === Image.Ready && (root.revealProgress >= 1 || panel.maskReady) + layer.enabled: root.incomingBackground !== "" && root.revealProgress < 1 + layer.smooth: true + layer.effect: MultiEffect { + maskEnabled: true + maskSource: revealMask + maskThresholdMin: 0.5 + maskSpreadAtMin: 0.02 + } + + Image { + id: incomingFrame + anchors.fill: parent + source: root.imageUrl(root.incomingBackground) + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: false + smooth: true + mipmap: true + onStatusChanged: panel.maybeStartReveal() + } + } + + Item { + id: revealMask + anchors.fill: parent + visible: false + layer.enabled: true readonly property real slant: -0.18 + readonly property real centerTop: width / 2 - slant * height / 2 + readonly property real centerBottom: width / 2 + slant * height / 2 + readonly property real reach: width / 2 + Math.abs(slant) * height / 2 + 4 + readonly property real spread: reach * root.revealProgress - function prepareImage() { - var src = root.imageUrl(root.incomingBackground) - if (!src) return - if (isImageLoaded(src)) { - requestPaint() - } else if (!isImageLoading(src)) { - loadImage(src) + Shape { + anchors.fill: parent + antialiasing: true + preferredRendererType: Shape.CurveRenderer + ShapePath { + fillColor: "white" + strokeColor: "transparent" + startX: revealMask.centerTop - revealMask.spread; startY: 0 + PathLine { x: revealMask.centerTop + revealMask.spread; y: 0 } + PathLine { x: revealMask.centerBottom + revealMask.spread; y: revealMask.height } + PathLine { x: revealMask.centerBottom - revealMask.spread; y: revealMask.height } + PathLine { x: revealMask.centerTop - revealMask.spread; y: 0 } } } + } - onImageLoaded: function(url) { - if (url === root.imageUrl(root.incomingBackground)) requestPaint() - } - - onPaint: { - var ctx = getContext("2d") - ctx.reset() - ctx.clearRect(0, 0, width, height) - - var src = root.imageUrl(root.incomingBackground) - if (!src || incomingFrame.status !== Image.Ready || root.revealProgress >= 1) return - if (!isImageLoaded(src)) { - prepareImage() - return - } - - var iw = incomingFrame.sourceSize.width - var ih = incomingFrame.sourceSize.height - if (iw <= 0 || ih <= 0 || width <= 0 || height <= 0) return - - var sx = 0 - var sy = 0 - var sw = iw - var sh = ih - if (iw / ih > width / height) { - sw = ih * width / height - sx = (iw - sw) / 2 - } else { - sh = iw * height / width - sy = (ih - sh) / 2 - } - - var centerTop = width / 2 - slant * height / 2 - var centerBottom = width / 2 + slant * height / 2 - var reach = width / 2 + Math.abs(slant) * height / 2 + 4 - var spread = reach * root.revealProgress - var leftTop = centerTop - spread - var leftBottom = centerBottom - spread - var rightTop = centerTop + spread - var rightBottom = centerBottom + spread - - ctx.save() - ctx.beginPath() - ctx.moveTo(leftTop, 0) - ctx.lineTo(rightTop, 0) - ctx.lineTo(rightBottom, height) - ctx.lineTo(leftBottom, height) - ctx.closePath() - ctx.clip() - ctx.drawImage(src, sx, sy, sw, sh, 0, 0, width, height) - ctx.restore() - - if (!panel.maskReady && root.incomingBackground && root.revealProgress === 0) { - Qt.callLater(function() { root.startReveal(panel) }) - } - } - - Connections { - target: root - function onRevealProgressChanged() { revealCanvas.requestPaint() } - function onIncomingBackgroundChanged() { - panel.maskReady = false - revealCanvas.prepareImage() - } + Connections { + target: root + function onIncomingBackgroundChanged() { + panel.maskReady = false + panel.maybeStartReveal() } }