Faster and smoother theme changes

This commit is contained in:
David Heinemeier Hansson
2026-05-17 17:03:28 +02:00
parent 4e50960c54
commit 8ad2f75758
4 changed files with 120 additions and 30 deletions
+36 -4
View File
@@ -12,6 +12,7 @@ fi
CURRENT_THEME_PATH="$HOME/.config/omarchy/current/theme" CURRENT_THEME_PATH="$HOME/.config/omarchy/current/theme"
NEXT_THEME_PATH="$HOME/.config/omarchy/current/next-theme" NEXT_THEME_PATH="$HOME/.config/omarchy/current/next-theme"
CURRENT_BACKGROUND_LINK="$HOME/.config/omarchy/current/background" CURRENT_BACKGROUND_LINK="$HOME/.config/omarchy/current/background"
BACKGROUND_TRANSITION_CACHE="$HOME/.cache/omarchy/background-transitions"
USER_THEMES_PATH="$HOME/.config/omarchy/themes" USER_THEMES_PATH="$HOME/.config/omarchy/themes"
OMARCHY_THEMES_PATH="$OMARCHY_PATH/themes" OMARCHY_THEMES_PATH="$OMARCHY_PATH/themes"
@@ -29,6 +30,19 @@ run_parallel() {
done 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() { set_theme_background() {
local backgrounds=() local backgrounds=()
local current_background index new_background next_index local current_background index new_background next_index
@@ -41,6 +55,7 @@ set_theme_background() {
if (( ${#backgrounds[@]} == 0 )); then if (( ${#backgrounds[@]} == 0 )); then
omarchy-notification-send "No background was found for theme" -t 2000 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 return
fi fi
@@ -61,7 +76,15 @@ set_theme_background() {
fi fi
ln -nsf "$new_background" "$CURRENT_BACKGROUND_LINK" 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 ' ' '-') 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 exit 1
fi 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) # Setup clean next theme directory (for atomic theme config swapping)
rm -rf "$NEXT_THEME_PATH" rm -rf "$NEXT_THEME_PATH"
mkdir -p "$NEXT_THEME_PATH" mkdir -p "$NEXT_THEME_PATH"
@@ -87,6 +114,11 @@ fi
# Generate dynamic configs # Generate dynamic configs
omarchy-theme-set-templates 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 # Swap next theme in as current
rm -rf "$CURRENT_THEME_PATH" rm -rf "$CURRENT_THEME_PATH"
mv "$NEXT_THEME_PATH" "$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. # 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") 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") 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
omarchy-shell-ipc-fast shell applyTheme "$colors_payload" "$shell_payload" >/dev/null 2>&1 || true
if [[ $OMARCHY_THEME_SKIP_BACKGROUND != "1" ]]; then else
set_theme_background set_theme_background
fi fi
@@ -170,7 +170,18 @@ QtObject {
shellValues = parsed shellValues = parsed
} }
property bool themeReloadSuspended: false
function suspendThemeReloads() {
themeReloadSuspended = true
}
function resumeThemeReloads() {
themeReloadSuspended = false
}
function reloadTheme() { function reloadTheme() {
if (themeReloadSuspended) return
colorsFile.reload() colorsFile.reload()
shellFile.reload() shellFile.reload()
} }
@@ -184,7 +195,7 @@ QtObject {
watchChanges: true watchChanges: true
printErrors: false printErrors: false
onLoaded: root.loadColors(text()) onLoaded: root.loadColors(text())
onFileChanged: reload() onFileChanged: root.reloadTheme()
} }
property FileView shellFile: FileView { property FileView shellFile: FileView {
id: shellFile id: shellFile
@@ -193,7 +204,7 @@ QtObject {
printErrors: false printErrors: false
onLoaded: root.loadShell(text()) onLoaded: root.loadShell(text())
onLoadFailed: root.loadShell("") onLoadFailed: root.loadShell("")
onFileChanged: reload() onFileChanged: root.reloadTheme()
} }
property FileView themeNameFile: FileView { property FileView themeNameFile: FileView {
path: Quickshell.env("HOME") + "/.config/omarchy/current/theme.name" path: Quickshell.env("HOME") + "/.config/omarchy/current/theme.name"
@@ -2,6 +2,7 @@ import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Wayland import Quickshell.Wayland
import QtQuick import QtQuick
import qs.Commons as NoctaliaCommons
Item { Item {
id: root id: root
@@ -19,6 +20,10 @@ Item {
property string oldBackground: "" property string oldBackground: ""
property bool finishingTransition: false property bool finishingTransition: false
property int backgroundVersion: 0 property int backgroundVersion: 0
property int revealStartedVersion: -1
property int pendingThemeVersion: -1
property string pendingColorsRaw: ""
property string pendingShellRaw: ""
property real revealProgress: 1 property real revealProgress: 1
function imageUrl(path) { function imageUrl(path) {
@@ -31,10 +36,16 @@ Item {
} }
function setBackground(path, instant) { function setBackground(path, instant) {
transitionBackground("", path, instant)
}
function transitionBackground(fromPath, path, instant) {
path = String(path || "").trim() path = String(path || "").trim()
fromPath = String(fromPath || "").trim()
if (!path || path === currentBackground) return if (!path || path === currentBackground) return
currentBackground = path currentBackground = path
backgroundVersion += 1 backgroundVersion += 1
revealStartedVersion = -1
revealAnimation.stop() revealAnimation.stop()
finishingTransition = false finishingTransition = false
@@ -47,11 +58,46 @@ Item {
return return
} }
oldBackground = displayedBackground oldBackground = fromPath || displayedBackground
incomingBackground = path incomingBackground = path
revealProgress = 0 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() { function openSelector() {
if (!bgSwitchProc.running) bgSwitchProc.running = true if (!bgSwitchProc.running) bgSwitchProc.running = true
} }
@@ -94,6 +140,14 @@ Item {
function setInstant(path: string): void { function setInstant(path: string): void {
root.setBackground(path, true) 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 { Timer {
@@ -230,41 +284,28 @@ Item {
sy = (ih - sh) / 2 sy = (ih - sh) / 2
} }
var center = width / 2 var centerTop = width / 2 - slant * height / 2
var reach = width / 2 + Math.abs(slant) * height + 4 var centerBottom = width / 2 + slant * height / 2
var reach = width / 2 + Math.abs(slant) * height / 2 + 4
var spread = reach * root.revealProgress var spread = reach * root.revealProgress
var leftTop = center - spread var leftTop = centerTop - spread
var leftBottom = center - spread + slant * height var leftBottom = centerBottom - spread
var rightTop = center + spread var rightTop = centerTop + spread
var rightBottom = center + spread + slant * height var rightBottom = centerBottom + spread
ctx.save() ctx.save()
ctx.beginPath() ctx.beginPath()
ctx.moveTo(center, 0) ctx.moveTo(leftTop, 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.lineTo(rightTop, 0) ctx.lineTo(rightTop, 0)
ctx.lineTo(rightBottom, height) ctx.lineTo(rightBottom, height)
ctx.lineTo(center, height) ctx.lineTo(leftBottom, height)
ctx.closePath() ctx.closePath()
ctx.clip() ctx.clip()
ctx.drawImage(src, sx, sy, sw, sh, 0, 0, width, height) ctx.drawImage(src, sx, sy, sw, sh, 0, 0, width, height)
ctx.restore() ctx.restore()
if (!panel.maskReady && root.incomingBackground && root.revealProgress === 0) { if (!panel.maskReady && root.incomingBackground && root.revealProgress === 0) {
Qt.callLater(function() { Qt.callLater(function() { root.startReveal(panel) })
panel.maskReady = true
revealAnimation.restart()
})
} }
} }
@@ -792,11 +792,17 @@ ShellRoot {
var shellRaw = "" var shellRaw = ""
try { colorsRaw = Qt.atob(String(colorsB64 || "")) } catch (e) { colorsRaw = "" } try { colorsRaw = Qt.atob(String(colorsB64 || "")) } catch (e) { colorsRaw = "" }
try { shellRaw = Qt.atob(String(shellB64 || "")) } catch (e2) { shellRaw = "" } try { shellRaw = Qt.atob(String(shellB64 || "")) } catch (e2) { shellRaw = "" }
NoctaliaCommons.Color.resumeThemeReloads()
NoctaliaCommons.Color.loadColors(colorsRaw) NoctaliaCommons.Color.loadColors(colorsRaw)
NoctaliaCommons.Color.loadShell(shellRaw) NoctaliaCommons.Color.loadShell(shellRaw)
return "ok" return "ok"
} }
function suspendThemeReloads(): string {
NoctaliaCommons.Color.suspendThemeReloads()
return "ok"
}
function reloadTheme(): string { function reloadTheme(): string {
NoctaliaCommons.Color.reloadTheme() NoctaliaCommons.Color.reloadTheme()
return "ok" return "ok"