From 1b4a5fa5a292af3192679471cd6358265adf9731 Mon Sep 17 00:00:00 2001 From: moons Date: Tue, 4 Aug 2026 16:58:40 +0900 Subject: [PATCH] wallpaper engine --- .../linux-wallpaperengine/home/controller.sh | 167 ++++++++++++++++++ .../linux-wallpaperengine/home/launcher.sh | 57 ++++++ .../linux-wallpaperengine/home/nixos.nix | 158 +++++++++++++++++ .../linux-wallpaperengine/meta.nix | 3 + .../linux-wallpaperengine/settings.nix | 38 ++++ .../profiles/interface/linux-desktop/home.nix | 6 +- .../profiles/interface/linux-desktop/meta.nix | 1 + 7 files changed, 429 insertions(+), 1 deletion(-) create mode 100644 modules/applications/linux-wallpaperengine/home/controller.sh create mode 100644 modules/applications/linux-wallpaperengine/home/launcher.sh create mode 100644 modules/applications/linux-wallpaperengine/home/nixos.nix create mode 100644 modules/applications/linux-wallpaperengine/meta.nix create mode 100644 modules/applications/linux-wallpaperengine/settings.nix diff --git a/modules/applications/linux-wallpaperengine/home/controller.sh b/modules/applications/linux-wallpaperengine/home/controller.sh new file mode 100644 index 0000000..b8e6cc6 --- /dev/null +++ b/modules/applications/linux-wallpaperengine/home/controller.sh @@ -0,0 +1,167 @@ +# shellcheck shell=bash + +set -o errexit -o nounset -o pipefail + +playlist_file="$1" +default_id="$2" +configuration_id="$3" +shift 3 + +state_dir="${XDG_RUNTIME_DIR:?}/linux-wallpaperengine" +state_file="$state_dir/current" + +usage() { + printf '%s\n' \ + 'Usage: wallpaperengine-wallpaper COMMAND [ID]' \ + '' \ + 'Commands:' \ + ' select ID Select a declaratively configured ID for this session' \ + ' next Select the next configured ID' \ + ' previous Select the previous configured ID' \ + ' list List configured IDs and mark the selected one' \ + ' current Print the selected ID' \ + ' restart Restart the selected wallpaper' \ + ' stop Stop Wallpaper Engine for this session' +} + +load_playlist() { + mapfile -t wallpaper_ids < <(awk '/^[0-9]+$/ && !seen[$0]++' "$playlist_file") + if [ "${#wallpaper_ids[@]}" -eq 0 ]; then + echo "wallpaperengine-wallpaper: no wallpaper IDs are configured in settings.nix" >&2 + exit 1 + fi +} + +contains_id() { + local candidate="$1" + local id + + for id in "${wallpaper_ids[@]}"; do + [ "$id" = "$candidate" ] && return 0 + done + return 1 +} + +current_id() { + local saved_configuration="" + local saved_id="" + local -a saved_state=() + + if [ -s "$state_file" ]; then + mapfile -t saved_state <"$state_file" + saved_configuration="${saved_state[0]:-}" + saved_id="${saved_state[1]:-}" + fi + + if [ "$saved_configuration" = "$configuration_id" ] && contains_id "$saved_id"; then + printf '%s\n' "$saved_id" + else + printf '%s\n' "$default_id" + fi +} + +select_id() { + local id="$1" + + if ! contains_id "$id"; then + echo "wallpaperengine-wallpaper: ID is not declared in settings.nix: $id" >&2 + exit 2 + fi + + mkdir -p "$state_dir" + printf '%s\n%s\n' "$configuration_id" "$id" >"$state_file" + systemctl --user restart linux-wallpaperengine.service +} + +cycle() { + local step="$1" + local current + local index=0 + local next_index + + current=$(current_id) + for i in "${!wallpaper_ids[@]}"; do + if [ "${wallpaper_ids[$i]}" = "$current" ]; then + index="$i" + break + fi + done + + next_index=$(((index + step + ${#wallpaper_ids[@]}) % ${#wallpaper_ids[@]})) + select_id "${wallpaper_ids[$next_index]}" +} + +load_playlist + +command="${1:-}" +if [ -z "$command" ]; then + usage + exit 2 +fi +shift + +case "$command" in +select) + [ "$#" -eq 1 ] || { + usage >&2 + exit 2 + } + select_id "$1" + ;; +next) + [ "$#" -eq 0 ] || { + usage >&2 + exit 2 + } + cycle 1 + ;; +previous) + [ "$#" -eq 0 ] || { + usage >&2 + exit 2 + } + cycle -1 + ;; +list) + [ "$#" -eq 0 ] || { + usage >&2 + exit 2 + } + current=$(current_id) + for id in "${wallpaper_ids[@]}"; do + if [ "$id" = "$current" ]; then + printf '* %s\n' "$id" + else + printf ' %s\n' "$id" + fi + done + ;; +current) + [ "$#" -eq 0 ] || { + usage >&2 + exit 2 + } + current_id + ;; +restart) + [ "$#" -eq 0 ] || { + usage >&2 + exit 2 + } + systemctl --user restart linux-wallpaperengine.service + ;; +stop) + [ "$#" -eq 0 ] || { + usage >&2 + exit 2 + } + systemctl --user stop linux-wallpaperengine.service + ;; +help | -h | --help) + usage + ;; +*) + usage >&2 + exit 2 + ;; +esac diff --git a/modules/applications/linux-wallpaperengine/home/launcher.sh b/modules/applications/linux-wallpaperengine/home/launcher.sh new file mode 100644 index 0000000..4ff7849 --- /dev/null +++ b/modules/applications/linux-wallpaperengine/home/launcher.sh @@ -0,0 +1,57 @@ +# shellcheck shell=bash + +set -o errexit -o nounset -o pipefail + +playlist_file="$1" +default_id="$2" +configuration_id="$3" +assets_dir="$4" +workshop_dir="$5" +scaling="$6" +shift 6 + +state_file="${XDG_RUNTIME_DIR:?}/linux-wallpaperengine/current" +wallpaper_id="$default_id" +saved_configuration="" +saved_id="" +saved_state=() +outputs=() + +if [ -s "$state_file" ]; then + mapfile -t saved_state <"$state_file" + saved_configuration="${saved_state[0]:-}" + saved_id="${saved_state[1]:-}" + + if [ "$saved_configuration" = "$configuration_id" ] && + awk -v id="$saved_id" '$0 == id { found = 1 } END { exit !found }' "$playlist_file"; then + wallpaper_id="$saved_id" + fi +fi + +wallpaper_path="$workshop_dir/$wallpaper_id" +if [ ! -d "$wallpaper_path" ]; then + echo "linux-wallpaperengine: missing declared wallpaper: $wallpaper_path" >&2 + exit 1 +fi + +if [ ! -d "$assets_dir" ]; then + echo "linux-wallpaperengine: missing Wallpaper Engine assets: $assets_dir" >&2 + exit 1 +fi + +mapfile -t outputs < <(wlr-randr --json | jq -r '.[] | select(.enabled == true) | .name') +if [ "${#outputs[@]}" -eq 0 ]; then + echo "linux-wallpaperengine: no enabled Wayland outputs were detected" >&2 + exit 1 +fi + +engine_args=("$@" --assets-dir "$assets_dir") +for output in "${outputs[@]}"; do + engine_args+=( + --screen-root "$output" + --bg "$wallpaper_path" + --scaling "$scaling" + ) +done + +exec linux-wallpaperengine "${engine_args[@]}" diff --git a/modules/applications/linux-wallpaperengine/home/nixos.nix b/modules/applications/linux-wallpaperengine/home/nixos.nix new file mode 100644 index 0000000..a4519ad --- /dev/null +++ b/modules/applications/linux-wallpaperengine/home/nixos.nix @@ -0,0 +1,158 @@ +{ + config, + lib, + pkgs, + ... +}: +let + settings = import ../settings.nix; + inherit (settings) + assetsDirectory + fps + mute + scaling + selectedWallpaperId + switchIntervalSeconds + wallpaperIds + workshopDirectory + ; + + resolveHomePath = + path: + if lib.hasPrefix "~/" path then + "${config.home.homeDirectory}/${lib.removePrefix "~/" path}" + else if lib.hasPrefix "/" path then + path + else + "${config.home.homeDirectory}/${path}"; + + hasWallpapers = wallpaperIds != [ ]; + defaultWallpaperId = + if selectedWallpaperId != null then + selectedWallpaperId + else if hasWallpapers then + lib.head wallpaperIds + else + ""; + configurationId = builtins.hashString "sha256" (builtins.toJSON settings); + resolvedAssetsDirectory = resolveHomePath assetsDirectory; + resolvedWorkshopDirectory = resolveHomePath workshopDirectory; + playlist = pkgs.writeText "linux-wallpaperengine-playlist" ( + lib.concatMapStringsSep "\n" (id: id) wallpaperIds + "\n" + ); + engineArguments = lib.optional mute "--silent" ++ [ + "--fps" + (toString fps) + ]; + + controller = pkgs.writeShellApplication { + name = "wallpaperengine-wallpaper"; + runtimeInputs = [ + pkgs.coreutils + pkgs.gawk + pkgs.systemd + ]; + text = '' + exec ${lib.getExe pkgs.bash} ${./controller.sh} \ + ${lib.escapeShellArg playlist} \ + ${lib.escapeShellArg defaultWallpaperId} \ + ${lib.escapeShellArg configurationId} \ + "$@" + ''; + }; + + launcher = pkgs.writeShellApplication { + name = "linux-wallpaperengine-launcher"; + runtimeInputs = [ + pkgs.coreutils + pkgs.gawk + pkgs.jq + pkgs.linux-wallpaperengine + pkgs.wlr-randr + ]; + text = '' + exec ${lib.getExe pkgs.bash} ${./launcher.sh} \ + ${lib.escapeShellArg playlist} \ + ${lib.escapeShellArg defaultWallpaperId} \ + ${lib.escapeShellArg configurationId} \ + ${lib.escapeShellArg resolvedAssetsDirectory} \ + ${lib.escapeShellArg resolvedWorkshopDirectory} \ + ${lib.escapeShellArg scaling} \ + ${lib.escapeShellArgs engineArguments} + ''; + }; +in +assert lib.assertMsg (lib.all ( + id: builtins.isString id && builtins.match "[0-9]+" id != null +) wallpaperIds) "linux-wallpaperengine: wallpaperIds must contain only numeric strings"; +assert lib.assertMsg ( + lib.unique wallpaperIds == wallpaperIds +) "linux-wallpaperengine: wallpaperIds must not contain duplicates"; +assert lib.assertMsg ( + selectedWallpaperId == null || builtins.elem selectedWallpaperId wallpaperIds +) "linux-wallpaperengine: selectedWallpaperId must be null or a member of wallpaperIds"; +assert lib.assertMsg ( + switchIntervalSeconds == null || (builtins.isInt switchIntervalSeconds && switchIntervalSeconds > 0) +) "linux-wallpaperengine: switchIntervalSeconds must be null or a positive integer"; +assert lib.assertMsg ( + builtins.isInt fps && fps > 0 +) "linux-wallpaperengine: fps must be a positive integer"; +assert lib.assertMsg ( + builtins.isString assetsDirectory && assetsDirectory != "" +) "linux-wallpaperengine: assetsDirectory must be a non-empty string"; +assert lib.assertMsg ( + builtins.isString workshopDirectory && workshopDirectory != "" +) "linux-wallpaperengine: workshopDirectory must be a non-empty string"; +assert lib.assertMsg (builtins.elem scaling [ + "default" + "fill" + "fit" + "stretch" +]) "linux-wallpaperengine: scaling must be default, fill, fit, or stretch"; +{ + home.packages = [ + controller + pkgs.linux-wallpaperengine + ]; + + systemd.user.services = lib.optionalAttrs hasWallpapers { + linux-wallpaperengine = { + Unit = { + Description = "Linux Wallpaper Engine"; + After = [ "graphical-session.target" ]; + PartOf = [ "graphical-session.target" ]; + }; + Service = { + ExecStart = lib.getExe launcher; + Restart = "on-abnormal"; + }; + Install.WantedBy = [ "graphical-session.target" ]; + }; + + linux-wallpaperengine-switch = { + Unit.Description = "Switch Linux Wallpaper Engine wallpaper"; + Service = { + Type = "oneshot"; + ExecStart = "${lib.getExe controller} next"; + }; + }; + }; + + systemd.user.timers = + lib.optionalAttrs + (hasWallpapers && builtins.length wallpaperIds > 1 && switchIntervalSeconds != null) + { + linux-wallpaperengine-switch = { + Unit = { + Description = "Periodically switch Linux Wallpaper Engine wallpaper"; + PartOf = [ "graphical-session.target" ]; + }; + Timer = { + OnActiveSec = "${toString switchIntervalSeconds}s"; + OnUnitActiveSec = "${toString switchIntervalSeconds}s"; + Unit = "linux-wallpaperengine-switch.service"; + }; + Install.WantedBy = [ "graphical-session.target" ]; + }; + }; +} diff --git a/modules/applications/linux-wallpaperengine/meta.nix b/modules/applications/linux-wallpaperengine/meta.nix new file mode 100644 index 0000000..7a859d0 --- /dev/null +++ b/modules/applications/linux-wallpaperengine/meta.nix @@ -0,0 +1,3 @@ +{ + description = "Wallpaper Engine backgrounds for Linux"; +} diff --git a/modules/applications/linux-wallpaperengine/settings.nix b/modules/applications/linux-wallpaperengine/settings.nix new file mode 100644 index 0000000..a51a667 --- /dev/null +++ b/modules/applications/linux-wallpaperengine/settings.nix @@ -0,0 +1,38 @@ +{ + assetsDirectory = "~/.local/share/Steam/steamapps/common/wallpaper_engine/assets"; + workshopDirectory = "~/.local/share/Steam/steamapps/workshop/content/431960"; + + wallpaperIds = [ + "2649146222" + "2834355367" + "2836628501" + "2840196275" + "2846639513" + "2855820325" + "2857233724" + "2857994490" + "2861661119" + "2868304559" + "2872296948" + "2833810156" + "2835102142" + "2839511236" + "2845095254" + "2851762554" + "2857085655" + "2857263038" + "2859848175" + "2867283793" + "2869066585" + "2982896307" + ]; + + # null selects the first ID above. + selectedWallpaperId = null; + # Set a number such as 300 to rotate through multiple IDs. + switchIntervalSeconds = null; + + fps = 30; + mute = true; + scaling = "fill"; +} diff --git a/modules/profiles/interface/linux-desktop/home.nix b/modules/profiles/interface/linux-desktop/home.nix index 0a1955a..dda1922 100644 --- a/modules/profiles/interface/linux-desktop/home.nix +++ b/modules/profiles/interface/linux-desktop/home.nix @@ -1,7 +1,11 @@ -{ pkgs, ... }: { +{ pkgs, ... }: +{ home.packages = [ pkgs.playerctl ]; + + programs.noctalia.settings.wallpaper.enabled = false; + xdg.userDirs = { enable = true; createDirectories = true; diff --git a/modules/profiles/interface/linux-desktop/meta.nix b/modules/profiles/interface/linux-desktop/meta.nix index 1f88da4..ccff06d 100644 --- a/modules/profiles/interface/linux-desktop/meta.nix +++ b/modules/profiles/interface/linux-desktop/meta.nix @@ -11,6 +11,7 @@ "applications.ghostty" "applications.gtk" "applications.loupe" + "applications.linux-wallpaperengine" "applications.nautilus" "applications.papers" "applications.qalculate-gtk"