swww: switch to awww-manager + timer
This commit is contained in:
30
dot_config/awww-manager/config.toml.tmpl
Normal file
30
dot_config/awww-manager/config.toml.tmpl
Normal 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"
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
8
dot_config/systemd/user/awww-manager-random.service
Normal file
8
dot_config/systemd/user/awww-manager-random.service
Normal 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
|
||||
12
dot_config/systemd/user/awww-manager-random.timer
Normal file
12
dot_config/systemd/user/awww-manager-random.timer
Normal 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
|
||||
@@ -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
|
||||
|
||||
|
||||
BIN
dot_config/wallpaper/dp-1/Generated image 1.png
Normal file
BIN
dot_config/wallpaper/dp-1/Generated image 1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 MiB |
BIN
dot_config/wallpaper/dp-1/girl_apex-neon.jpeg
Normal file
BIN
dot_config/wallpaper/dp-1/girl_apex-neon.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.1 MiB |
1
dot_config/wallpaper/dp-1/symlink_current
Normal file
1
dot_config/wallpaper/dp-1/symlink_current
Normal file
@@ -0,0 +1 @@
|
||||
girl_apex-neon.jpeg
|
||||
BIN
dot_config/wallpaper/dp-2/dark_sorc_upscaled.jpg
Normal file
BIN
dot_config/wallpaper/dp-2/dark_sorc_upscaled.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.0 MiB |
BIN
dot_config/wallpaper/dp-2/neon-rooftop.png
Normal file
BIN
dot_config/wallpaper/dp-2/neon-rooftop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.8 MiB |
BIN
dot_config/wallpaper/dp-2/neon-window-skyline.png
Normal file
BIN
dot_config/wallpaper/dp-2/neon-window-skyline.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.0 MiB |
1
dot_config/wallpaper/dp-2/symlink_current
Normal file
1
dot_config/wallpaper/dp-2/symlink_current
Normal file
@@ -0,0 +1 @@
|
||||
neon-rooftop.png
|
||||
BIN
dot_config/wallpaper/lockscreen/dark_cyber_sorc.jpeg
Normal file
BIN
dot_config/wallpaper/lockscreen/dark_cyber_sorc.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.4 MiB |
1
dot_config/wallpaper/lockscreen/symlink_current
Normal file
1
dot_config/wallpaper/lockscreen/symlink_current
Normal file
@@ -0,0 +1 @@
|
||||
dark_cyber_sorc.jpeg
|
||||
341
dot_local/bin/executable_awww-manager.py
Normal file
341
dot_local/bin/executable_awww-manager.py
Normal 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()
|
||||
@@ -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 }}
|
||||
Reference in New Issue
Block a user