diff --git a/bin/omarchy-theme-set b/bin/omarchy-theme-set index 6648cc48..4bc3130a 100755 --- a/bin/omarchy-theme-set +++ b/bin/omarchy-theme-set @@ -12,6 +12,7 @@ fi 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" USER_THEMES_PATH="$HOME/.config/omarchy/themes" OMARCHY_THEMES_PATH="$OMARCHY_PATH/themes" @@ -29,6 +30,19 @@ run_parallel() { done } +snapshot_current_background() { + local current_background snapshot extension + + current_background=$(readlink -f "$CURRENT_BACKGROUND_LINK" 2>/dev/null || true) + [[ -f $current_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" + echo "$snapshot" +} + set_theme_background() { local backgrounds=() local current_background index new_background next_index @@ -41,6 +55,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 return fi @@ -61,7 +76,15 @@ set_theme_background() { fi ln -nsf "$new_background" "$CURRENT_BACKGROUND_LINK" - omarchy-shell-ipc-fast background setInstant "$new_background" >/dev/null 2>&1 || true + + 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") & + 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 + fi } THEME_NAME=$(echo "$1" | sed -E 's/<[^>]+>//g' | tr '[:upper:]' '[:lower:]' | tr ' ' '-') @@ -71,6 +94,10 @@ if [[ ! -d $OMARCHY_THEMES_PATH/$THEME_NAME ]] && [[ ! -d $USER_THEMES_PATH/$THE exit 1 fi +# 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 + # Setup clean next theme directory (for atomic theme config swapping) rm -rf "$NEXT_THEME_PATH" mkdir -p "$NEXT_THEME_PATH" @@ -87,6 +114,11 @@ fi # Generate dynamic configs omarchy-theme-set-templates +OLD_BACKGROUND_SNAPSHOT="" +if [[ $OMARCHY_THEME_SKIP_BACKGROUND != "1" ]]; then + OLD_BACKGROUND_SNAPSHOT=$(snapshot_current_background) +fi + # Swap next theme in as current rm -rf "$CURRENT_THEME_PATH" mv "$NEXT_THEME_PATH" "$CURRENT_THEME_PATH" @@ -98,9 +130,9 @@ echo "$THEME_NAME" >"$HOME/.config/omarchy/current/theme.name" # for file-watch debounce while the rest of the theme hooks run. 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") -omarchy-shell-ipc-fast shell applyTheme "$colors_payload" "$shell_payload" >/dev/null 2>&1 || true - -if [[ $OMARCHY_THEME_SKIP_BACKGROUND != "1" ]]; then +if [[ $OMARCHY_THEME_SKIP_BACKGROUND == "1" ]]; then + omarchy-shell-ipc-fast shell applyTheme "$colors_payload" "$shell_payload" >/dev/null 2>&1 || true +else set_theme_background fi diff --git a/default/quickshell/omarchy-shell/Commons/Color.qml b/default/quickshell/omarchy-shell/Commons/Color.qml index 6091603c..47cb91b2 100644 --- a/default/quickshell/omarchy-shell/Commons/Color.qml +++ b/default/quickshell/omarchy-shell/Commons/Color.qml @@ -170,7 +170,18 @@ QtObject { shellValues = parsed } + property bool themeReloadSuspended: false + + function suspendThemeReloads() { + themeReloadSuspended = true + } + + function resumeThemeReloads() { + themeReloadSuspended = false + } + function reloadTheme() { + if (themeReloadSuspended) return colorsFile.reload() shellFile.reload() } @@ -184,7 +195,7 @@ QtObject { watchChanges: true printErrors: false onLoaded: root.loadColors(text()) - onFileChanged: reload() + onFileChanged: root.reloadTheme() } property FileView shellFile: FileView { id: shellFile @@ -193,7 +204,7 @@ QtObject { printErrors: false onLoaded: root.loadShell(text()) onLoadFailed: root.loadShell("") - onFileChanged: reload() + onFileChanged: root.reloadTheme() } property FileView themeNameFile: FileView { path: Quickshell.env("HOME") + "/.config/omarchy/current/theme.name" diff --git a/default/quickshell/omarchy-shell/plugins/background/Background.qml b/default/quickshell/omarchy-shell/plugins/background/Background.qml index 6d54dbf2..e4e4325d 100644 --- a/default/quickshell/omarchy-shell/plugins/background/Background.qml +++ b/default/quickshell/omarchy-shell/plugins/background/Background.qml @@ -2,6 +2,7 @@ import Quickshell import Quickshell.Io import Quickshell.Wayland import QtQuick +import qs.Commons as NoctaliaCommons Item { id: root @@ -19,6 +20,10 @@ Item { property string oldBackground: "" property bool finishingTransition: false property int backgroundVersion: 0 + property int revealStartedVersion: -1 + property int pendingThemeVersion: -1 + property string pendingColorsRaw: "" + property string pendingShellRaw: "" property real revealProgress: 1 function imageUrl(path) { @@ -31,10 +36,16 @@ Item { } function setBackground(path, instant) { + transitionBackground("", path, instant) + } + + function transitionBackground(fromPath, path, instant) { path = String(path || "").trim() + fromPath = String(fromPath || "").trim() if (!path || path === currentBackground) return currentBackground = path backgroundVersion += 1 + revealStartedVersion = -1 revealAnimation.stop() finishingTransition = false @@ -47,11 +58,46 @@ Item { return } - oldBackground = displayedBackground + oldBackground = fromPath || displayedBackground incomingBackground = path revealProgress = 0 } + function decodePayload(payload) { + try { return Qt.atob(String(payload || "")) } catch (e) { return "" } + } + + function setPendingTheme(colorsB64, shellB64) { + pendingColorsRaw = decodePayload(colorsB64) + pendingShellRaw = decodePayload(shellB64) + pendingThemeVersion = backgroundVersion + } + + function applyPendingTheme() { + if (pendingThemeVersion !== backgroundVersion) return + NoctaliaCommons.Color.resumeThemeReloads() + NoctaliaCommons.Color.loadColors(pendingColorsRaw) + NoctaliaCommons.Color.loadShell(pendingShellRaw) + pendingThemeVersion = -1 + pendingColorsRaw = "" + pendingShellRaw = "" + } + + function transitionBackgroundWithTheme(fromPath, path, colorsB64, shellB64) { + transitionBackground(fromPath, path, false) + setPendingTheme(colorsB64, shellB64) + if (!incomingBackground || revealProgress >= 1) applyPendingTheme() + } + + function startReveal(panel) { + if (!incomingBackground) return + panel.maskReady = true + if (revealStartedVersion === backgroundVersion) return + revealStartedVersion = backgroundVersion + applyPendingTheme() + revealAnimation.restart() + } + function openSelector() { if (!bgSwitchProc.running) bgSwitchProc.running = true } @@ -94,6 +140,14 @@ Item { function setInstant(path: string): void { root.setBackground(path, true) } + + function transition(fromPath: string, path: string): void { + root.transitionBackground(fromPath, path, false) + } + + function themeTransition(fromPath: string, path: string, colorsB64: string, shellB64: string): void { + root.transitionBackgroundWithTheme(fromPath, path, colorsB64, shellB64) + } } Timer { @@ -230,41 +284,28 @@ Item { sy = (ih - sh) / 2 } - var center = width / 2 - var reach = width / 2 + Math.abs(slant) * height + 4 + 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 = center - spread - var leftBottom = center - spread + slant * height - var rightTop = center + spread - var rightBottom = center + spread + slant * height + var leftTop = centerTop - spread + var leftBottom = centerBottom - spread + var rightTop = centerTop + spread + var rightBottom = centerBottom + spread ctx.save() ctx.beginPath() - ctx.moveTo(center, 0) - ctx.lineTo(leftTop, 0) - ctx.lineTo(leftBottom, height) - ctx.lineTo(center, height) - ctx.closePath() - ctx.clip() - ctx.drawImage(src, sx, sy, sw, sh, 0, 0, width, height) - ctx.restore() - - ctx.save() - ctx.beginPath() - ctx.moveTo(center, 0) + ctx.moveTo(leftTop, 0) ctx.lineTo(rightTop, 0) ctx.lineTo(rightBottom, height) - ctx.lineTo(center, 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() { - panel.maskReady = true - revealAnimation.restart() - }) + Qt.callLater(function() { root.startReveal(panel) }) } } diff --git a/default/quickshell/omarchy-shell/shell.qml b/default/quickshell/omarchy-shell/shell.qml index 922912b6..8ce4f634 100644 --- a/default/quickshell/omarchy-shell/shell.qml +++ b/default/quickshell/omarchy-shell/shell.qml @@ -792,11 +792,17 @@ ShellRoot { var shellRaw = "" try { colorsRaw = Qt.atob(String(colorsB64 || "")) } catch (e) { colorsRaw = "" } try { shellRaw = Qt.atob(String(shellB64 || "")) } catch (e2) { shellRaw = "" } + NoctaliaCommons.Color.resumeThemeReloads() NoctaliaCommons.Color.loadColors(colorsRaw) NoctaliaCommons.Color.loadShell(shellRaw) return "ok" } + function suspendThemeReloads(): string { + NoctaliaCommons.Color.suspendThemeReloads() + return "ok" + } + function reloadTheme(): string { NoctaliaCommons.Color.reloadTheme() return "ok"