swww: switch to awww-manager + timer

This commit is contained in:
s0wlz (Matthias Puchstein)
2026-01-01 19:38:48 +01:00
parent 9c18d728eb
commit b65a942347
17 changed files with 399 additions and 39 deletions

View File

@@ -0,0 +1,30 @@
[general]
backend = "swww"
# backend_cmd = "/usr/bin/swww"
# resize = "crop"
# filter = "Lanczos3"
# transition_type = "simple"
# transition_step = 90
# transition_fps = 30
# transition_duration = 3
# fill_color = "000000ff"
[[output]]
name = "DP-1"
mode = "cycle"
dir = "~/.config/wallpaper/dp-1"
extensions = ["png", "jpg", "jpeg", "gif", "webp"]
# Optional per-output overrides:
# resize = "crop"
# filter = "Lanczos3"
# transition_type = "simple"
[[output]]
name = "DP-2"
mode = "cycle"
dir = "~/.config/wallpaper/dp-2"
extensions = ["png", "jpg", "jpeg", "gif", "webp"]
# Optional per-output overrides:
# resize = "crop"
# filter = "Lanczos3"
# transition_type = "simple"

View File

@@ -1,13 +1,6 @@
{{- $lock_bg := "" -}}
{{- with (index .chezmoi.config.data "lockscreen-wallpaper") -}}
{{- $lock_bg = . -}}
{{- end -}}
{{- if eq $lock_bg "" -}}
{{- range .monitors -}}
{{- if index . "primary" -}}
{{- $lock_bg = .wallpaper -}}
{{- end -}}
{{- end -}}
{{- $lock_bg := (index .chezmoi.config.data "lockscreen-wallpaper") -}}
{{- if not $lock_bg -}}
{{- $lock_bg = "~/.config/wallpaper/lockscreen/current" -}}
{{- end -}}
general {
hide_cursor = true

View File

@@ -3,10 +3,8 @@ splash_offset = 2.0
ipc = true
{{- range .monitors }}
{{- if .wallpaper }}
wallpaper {
monitor = {{ .name }}
path = {{ .wallpaper }}
path = ~/.config/wallpaper/{{ .name | lower }}/current
}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,8 @@
[Unit]
Description=Randomize wallpapers via awww-manager
After=graphical-session.target swww.service
Wants=swww.service
[Service]
Type=oneshot
ExecStart=%h/.local/bin/awww-manager.py random

View File

@@ -0,0 +1,12 @@
[Unit]
Description=Randomize wallpapers every 60 minutes
[Timer]
OnBootSec=5min
OnUnitActiveSec=60min
AccuracySec=1min
Persistent=true
Unit=awww-manager-random.service
[Install]
WantedBy=timers.target

View File

@@ -6,7 +6,7 @@ After=graphical-session.target
[Service]
Type=simple
ExecStart=/usr/bin/swww-daemon
ExecStartPost=%h/.local/bin/set-wallpapers
ExecStartPost=%h/.local/bin/awww-manager.py apply
Restart=on-failure
RestartSec=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 MiB

View File

@@ -0,0 +1 @@
girl_apex-neon.jpeg

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 MiB

View File

@@ -0,0 +1 @@
neon-rooftop.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 MiB

View File

@@ -0,0 +1 @@
dark_cyber_sorc.jpeg

View File

@@ -0,0 +1,341 @@
#!/usr/bin/env python3
import argparse
import json
import os
import random
import shlex
import shutil
import subprocess
import sys
import time
__version__ = "0.1.0"
def _load_toml(path):
try:
import toml
except ImportError:
print("toml not available; install python-toml", file=sys.stderr)
raise SystemExit(2)
try:
with open(path, "r", encoding="utf-8") as handle:
return toml.load(handle)
except FileNotFoundError:
print(f"config not found: {path}", file=sys.stderr)
raise SystemExit(2)
def _config_path(explicit):
if explicit:
return explicit
if "AWWW_MANAGER_CONFIG" in os.environ:
return os.environ["AWWW_MANAGER_CONFIG"]
config_home = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
return os.path.join(config_home, "awww-manager", "config.toml")
def _state_path():
state_home = os.environ.get("XDG_STATE_HOME", os.path.expanduser("~/.local/state"))
return os.path.join(state_home, "awww-manager", "state.json")
def _load_state():
path = _state_path()
try:
with open(path, "r", encoding="utf-8") as handle:
return json.load(handle)
except FileNotFoundError:
return {}
except json.JSONDecodeError:
print(f"state file is invalid JSON: {path}", file=sys.stderr)
return {}
def _save_state(state):
path = _state_path()
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as handle:
json.dump(state, handle)
def _expand_path(value):
if not isinstance(value, str):
return value
return os.path.expandvars(os.path.expanduser(value))
def _merged_settings(general, output):
merged = dict(general)
merged.update(output)
return merged
def _quote_cmd(cmd):
return " ".join(shlex.quote(part) for part in cmd)
def _run(cmd, dry_run):
if dry_run:
print(_quote_cmd(cmd))
return
subprocess.run(cmd, check=True)
def _wait_for_backend(cmd, timeout, interval):
deadline = time.time() + timeout
while time.time() < deadline:
result = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
if result.returncode == 0:
return True
time.sleep(interval)
return False
def _build_img_command(base_cmd, settings, wallpaper):
cmd = [base_cmd, "img"]
output_name = settings.get("name")
if output_name:
cmd.extend(["-o", str(output_name)])
resize = settings.get("resize")
if resize:
cmd.extend(["--resize", str(resize)])
image_filter = settings.get("filter")
if image_filter:
cmd.extend(["--filter", str(image_filter)])
transition_type = settings.get("transition_type")
if transition_type:
cmd.extend(["--transition-type", str(transition_type)])
transition_step = settings.get("transition_step")
if transition_step is not None:
cmd.extend(["--transition-step", str(transition_step)])
transition_fps = settings.get("transition_fps")
if transition_fps is not None:
cmd.extend(["--transition-fps", str(transition_fps)])
transition_duration = settings.get("transition_duration")
if transition_duration is not None:
cmd.extend(["--transition-duration", str(transition_duration)])
transition_angle = settings.get("transition_angle")
if transition_angle is not None:
cmd.extend(["--transition-angle", str(transition_angle)])
transition_pos = settings.get("transition_pos")
if transition_pos:
cmd.extend(["--transition-pos", str(transition_pos)])
fill_color = settings.get("fill_color")
if fill_color:
cmd.extend(["--fill-color", str(fill_color)])
cmd.append(_expand_path(wallpaper))
return cmd
def _ensure_backend_available(base_cmd):
if not shutil.which(base_cmd):
print(f"backend not found in PATH: {base_cmd}", file=sys.stderr)
raise SystemExit(2)
def _output_mode(output):
mode = output.get("mode")
if mode:
return str(mode).lower()
if output.get("dir"):
return "cycle"
return "static"
def _output_key(output, index):
return output.get("name") or f"output-{index}"
def _collect_images(output):
dir_path = _expand_path(output.get("dir", ""))
if not dir_path:
return []
if not os.path.isdir(dir_path):
print(f"wallpaper dir not found: {dir_path}", file=sys.stderr)
return []
extensions = output.get("extensions") or ["png", "jpg", "jpeg", "gif", "webp"]
ext_set = {str(ext).lower().lstrip(".") for ext in extensions}
recursive = bool(output.get("recursive", False))
images = []
if recursive:
for root, _, files in os.walk(dir_path):
for filename in files:
_, ext = os.path.splitext(filename)
if ext.lower().lstrip(".") in ext_set:
images.append(os.path.join(root, filename))
else:
for filename in os.listdir(dir_path):
_, ext = os.path.splitext(filename)
if ext.lower().lstrip(".") in ext_set:
path = os.path.join(dir_path, filename)
if os.path.isfile(path):
images.append(path)
images.sort()
return images
def _choose_random(images, last_path):
if not images:
return None
if len(images) == 1:
return images[0]
choice = random.choice(images)
if last_path and choice == last_path:
choice = random.choice([path for path in images if path != last_path])
return choice
def _resolve_wallpaper(output, key, command, state):
mode = _output_mode(output)
if mode == "static":
return output.get("wallpaper"), False
images = _collect_images(output)
if not images:
return None, False
entry = state.get(key, {})
last_path = entry.get("path")
index = entry.get("index", 0)
if command == "random" or mode == "random":
selection = _choose_random(images, last_path)
state[key] = {"index": images.index(selection), "path": selection}
return selection, True
if command == "next":
index = (index + 1) % len(images)
elif command == "prev":
index = (index - 1) % len(images)
index = min(max(index, 0), len(images) - 1)
selection = images[index]
state[key] = {"index": index, "path": selection}
return selection, True
def cmd_apply_like(config, dry_run, wait_seconds, command):
general = config.get("general", {})
base_cmd = general.get("backend_cmd", general.get("backend", "swww"))
outputs = config.get("output", [])
if not outputs:
print("config has no [[output]] entries", file=sys.stderr)
raise SystemExit(2)
_ensure_backend_available(base_cmd)
if wait_seconds > 0:
ok = _wait_for_backend([base_cmd, "query"], wait_seconds, 0.1)
if not ok:
print(f"{base_cmd} backend not responding to query", file=sys.stderr)
raise SystemExit(2)
state = _load_state()
updated = False
for index, output in enumerate(outputs):
key = _output_key(output, index)
wallpaper, changed = _resolve_wallpaper(output, key, command, state)
if not wallpaper:
print(f"skipping output without wallpaper: {output.get('name', '<unknown>')}", file=sys.stderr)
continue
settings = _merged_settings(general, output)
cmd = _build_img_command(base_cmd, settings, wallpaper)
_run(cmd, dry_run)
updated = updated or changed
if updated and not dry_run:
_save_state(state)
def cmd_list(config):
outputs = config.get("output", [])
if not outputs:
print("config has no [[output]] entries", file=sys.stderr)
raise SystemExit(2)
state = _load_state()
for index, output in enumerate(outputs):
name = output.get("name", "<unknown>")
mode = _output_mode(output)
if mode == "static":
wallpaper = output.get("wallpaper", "")
current = _expand_path(wallpaper)
else:
images = _collect_images(output)
key = _output_key(output, index)
current = state.get(key, {}).get("path")
if not current and images:
current = images[0]
current = _expand_path(current or "")
print(f"{name}\t{mode}\t{current}")
def main():
parser = argparse.ArgumentParser(description="Apply wallpapers via swww/awww using a TOML config.")
parser.add_argument("--config", help="Path to config.toml (default: XDG config)")
parser.add_argument("--dry-run", action="store_true", help="Print commands without executing them")
parser.add_argument("--print-config", action="store_true", help="Print the config file and exit")
parser.add_argument("--version", action="version", version=f"awww-manager {__version__}")
parser.add_argument(
"--wait",
type=float,
default=2.0,
help="Seconds to wait for backend query to succeed (default: 2.0)",
)
subparsers = parser.add_subparsers(dest="command")
subparsers.add_parser("apply", help="Apply wallpapers from config")
subparsers.add_parser("list", help="List outputs and wallpapers from config")
subparsers.add_parser("next", help="Advance to the next wallpaper for cycle outputs")
subparsers.add_parser("prev", help="Go to the previous wallpaper for cycle outputs")
subparsers.add_parser("random", help="Randomize wallpapers for non-static outputs")
args = parser.parse_args()
config_path = _config_path(args.config)
if args.print_config:
try:
with open(config_path, "r", encoding="utf-8") as handle:
print(handle.read(), end="")
except FileNotFoundError:
print(f"config not found: {config_path}", file=sys.stderr)
raise SystemExit(2)
return
if not args.command:
parser.error("the following arguments are required: command")
config = _load_toml(config_path)
if args.command == "apply":
cmd_apply_like(config, args.dry_run, args.wait, "apply")
elif args.command == "next":
cmd_apply_like(config, args.dry_run, args.wait, "next")
elif args.command == "prev":
cmd_apply_like(config, args.dry_run, args.wait, "prev")
elif args.command == "random":
cmd_apply_like(config, args.dry_run, args.wait, "random")
elif args.command == "list":
cmd_list(config)
if __name__ == "__main__":
main()

View File

@@ -1,25 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
if ! command -v swww >/dev/null 2>&1; then
echo "swww not found in PATH" >&2
exit 1
fi
for _ in {1..50}; do
if swww query >/dev/null 2>&1; then
break
fi
sleep 0.1
done
if ! swww query >/dev/null 2>&1; then
echo "swww-daemon not responding" >&2
exit 1
fi
{{- range .monitors }}
{{- if .wallpaper }}
swww img -o {{ .name }} "{{ .wallpaper }}"
{{- end }}
{{- end }}