theming: re-theme every app on wallpaper switch, crossfade wallpaper
settings/theming.nix renders a full config set per wallpaper (GTK, ghostty, btop, hyprland, vesktop, zen, folder icons) from its own base16 palette; theme-apply swaps them in and reloads what can reload. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
42adab61df
commit
1091495674
6 changed files with 560 additions and 227 deletions
|
|
@ -6,51 +6,18 @@ let
|
|||
isMacbook = config.networking.hostName == "FredOS-Macbook";
|
||||
ui = import ./ui.nix; # shared corner-rounding knob
|
||||
|
||||
# ── Wallpaper set ──────────────────────────────────────────────────────
|
||||
# Every image in walls/ gets its own dark base16 palette, generated at
|
||||
# build time by stylix's own palette-generator. The shell picks one at
|
||||
# RUNTIME (state file + FileView in Theme.qml), so switching wallpaper
|
||||
# re-tints the whole shell without a rebuild.
|
||||
# ── Wallpapers ─────────────────────────────────────────────────────────
|
||||
# Built in settings/theming.nix: one entry per image in walls/, each with
|
||||
# a theme directory holding that wallpaper's base16 palette and the config
|
||||
# files every other app is re-themed from. The shell picks one at RUNTIME
|
||||
# (state file + FileView in Theme.qml), so switching re-tints the desktop
|
||||
# without a rebuild.
|
||||
#
|
||||
# One derivation per image, not one for the set: palette generation is a
|
||||
# genetic search that takes seconds, and per-image derivations mean adding
|
||||
# a wallpaper only regenerates the new one. builtins.path hashes the image
|
||||
# content alone, so unrelated repo edits don't invalidate any of them.
|
||||
paletteGenerator =
|
||||
inputs.stylix.packages.${pkgs.stdenv.hostPlatform.system}.palette-generator;
|
||||
|
||||
wallFiles = builtins.attrNames (lib.filterAttrs
|
||||
(n: t: t == "regular"
|
||||
&& lib.any (e: lib.hasSuffix e (lib.toLower n)) [ ".png" ".jpg" ".jpeg" ])
|
||||
(builtins.readDir (inputs.self + "/walls")));
|
||||
|
||||
# Store names allow a narrow charset, filenames don't. Keep wallpaper
|
||||
# names plain — a space survives, anything more exotic may not.
|
||||
wallStem = f: lib.head (lib.splitString "." f);
|
||||
wallSlug = f: lib.replaceStrings [ " " ] [ "-" ] (wallStem f);
|
||||
wallImage = f: builtins.path {
|
||||
name = "wall-" + lib.replaceStrings [ " " ] [ "-" ] f;
|
||||
path = inputs.self + "/walls/${f}";
|
||||
};
|
||||
wallPalette = f: pkgs.runCommand "palette-${wallSlug f}" { } ''
|
||||
mkdir -p $out
|
||||
${paletteGenerator}/bin/palette-generator dark ${wallImage f} $out/palette.json
|
||||
'';
|
||||
|
||||
# The list Theme.qml carries verbatim. Naming the store paths inside a
|
||||
# home-manager file keeps both image and palette alive as generation
|
||||
# references — no extra gcroot needed.
|
||||
wallsJson = builtins.toJSON (map (f: {
|
||||
name = wallStem f;
|
||||
image = "${wallImage f}";
|
||||
palette = "${wallPalette f}/palette.json";
|
||||
}) wallFiles);
|
||||
|
||||
# Falls back to walls/wallpaper.png — the one stylix themes everything
|
||||
# else from — so a missing state file lands on the build-time scheme.
|
||||
defaultWall = if lib.elem "wallpaper" (map wallStem wallFiles)
|
||||
then "wallpaper"
|
||||
else wallStem (lib.head wallFiles);
|
||||
# Naming the store paths inside a home-manager file is what keeps the
|
||||
# images and themes alive as generation references — no extra gcroot.
|
||||
wallsJson = builtins.toJSON config.fred.theming.walls;
|
||||
defaultWall = config.fred.theming.defaultWall;
|
||||
themeApply = config.fred.theming.apply;
|
||||
in
|
||||
{
|
||||
config = lib.mkIf (lib.elem config.networking.hostName [ "FredOS-Gaming" "FredOS-Macbook" ]) {
|
||||
|
|
@ -463,6 +430,7 @@ in
|
|||
readonly property var currentWall:
|
||||
walls.find(w => w.name === wallState.name) || walls[0] || null
|
||||
readonly property string wallImage: currentWall ? currentWall.image : ""
|
||||
readonly property string themeDir: currentWall ? currentWall.theme : ""
|
||||
|
||||
function setWall(name) {
|
||||
wallState.name = name;
|
||||
|
|
@ -476,7 +444,7 @@ in
|
|||
// scheme (walls/wallpaper.png), which is also what shows if
|
||||
// a palette file is ever missing or malformed.
|
||||
property FileView palFile: FileView {
|
||||
path: theme.currentWall ? theme.currentWall.palette : ""
|
||||
path: theme.themeDir === "" ? "" : theme.themeDir + "/palette.json"
|
||||
printErrors: false
|
||||
// Synchronous: an async first read paints a frame in the
|
||||
// previous scheme on every switch.
|
||||
|
|
@ -1360,6 +1328,7 @@ in
|
|||
readonly property string askFetch: "${askFetchScript}"
|
||||
readonly property string wxFetch: "${wxFetchScript}"
|
||||
readonly property string xdgOpen: "${pkgs.xdg-utils}/bin/xdg-open"
|
||||
readonly property string themeApply: "${themeApply}"
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
|
@ -1380,6 +1349,21 @@ in
|
|||
property var mainBar: null
|
||||
signal notificationReceived()
|
||||
|
||||
// Push the active wallpaper's theme out to everything that
|
||||
// isn't quickshell — GTK, ghostty, btop, vesktop, zen,
|
||||
// hyprland, folder icons. Also on startup, so a login (or a
|
||||
// soft reload after a rebuild) re-asserts the current choice
|
||||
// rather than leaving apps on whatever was last written.
|
||||
function applyTheme() {
|
||||
if (Theme.themeDir !== "")
|
||||
Quickshell.execDetached([Commands.themeApply, Theme.themeDir]);
|
||||
}
|
||||
Component.onCompleted: applyTheme()
|
||||
Connections {
|
||||
target: Theme
|
||||
function onThemeDirChanged() { root.applyTheme(); }
|
||||
}
|
||||
|
||||
// Bound in hyprland.nix: Super+R → launcher (bottom of the
|
||||
// bar window), Super+L → session menu (right edge).
|
||||
IpcHandler {
|
||||
|
|
@ -1588,11 +1572,57 @@ in
|
|||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
color: "black"
|
||||
|
||||
Image {
|
||||
// Two layers so a switch crossfades instead of
|
||||
// cutting: the new image loads into whichever layer
|
||||
// is hidden, then the pair swap opacity. The loser's
|
||||
// source is dropped once the fade ends — a spare
|
||||
// full-screen texture is real VRAM to leave resident
|
||||
// on a machine that also runs games.
|
||||
Item {
|
||||
id: wallLayer
|
||||
anchors.fill: parent
|
||||
source: "file://" + Theme.wallImage
|
||||
cache: false // same-URL pixmap cache survives soft reload
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
|
||||
readonly property string wanted: "file://" + Theme.wallImage
|
||||
property bool bFront: false
|
||||
|
||||
Image {
|
||||
id: wallA
|
||||
anchors.fill: parent
|
||||
cache: false // same-URL pixmap cache survives soft reload
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
opacity: wallLayer.bFront ? 0 : 1
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: 450; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: wallB
|
||||
anchors.fill: parent
|
||||
cache: false
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
opacity: wallLayer.bFront ? 1 : 0
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: 450; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
}
|
||||
|
||||
// Local files load synchronously, so by the time
|
||||
// the opacities cross the new frame is decoded —
|
||||
// no black flash between the two.
|
||||
Component.onCompleted: wallA.source = wanted;
|
||||
|
||||
onWantedChanged: {
|
||||
(bFront ? wallA : wallB).source = wanted;
|
||||
bFront = !bFront;
|
||||
_dropBack.restart();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: _dropBack
|
||||
interval: 500 // just past the fade
|
||||
onTriggered: (wallLayer.bFront ? wallA : wallB).source = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue