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"
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
@@ -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"
@@ -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) })
}
}
@@ -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"