wallpaper engine

This commit is contained in:
2026-08-04 16:58:40 +09:00
parent 5fb55f2e6a
commit 1b4a5fa5a2
7 changed files with 429 additions and 1 deletions
@@ -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
@@ -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[@]}"
@@ -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" ];
};
};
}