Nix-Vibe public snapshot (squashed history)
Current state of main at 0240060 feat(hp-laptop): install TeleportFling from its flake. History intentionally collapsed to a single commit; this repo mirrors only the latest state.
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env python3
|
||||
"""QuickShell wallpaper picker backend.
|
||||
|
||||
Wallpapers are the image files in ~/Pictures/wallpapers (hyprpaper reads that
|
||||
dir as the initial set; the picker surfaces whatever is there at runtime).
|
||||
|
||||
Commands
|
||||
--------
|
||||
list -> JSON array of {name, path, active} (active = matches the state file)
|
||||
set <p> -> persist <p> to ~/.cache/quickshell/wallpaper, then apply it now
|
||||
apply -> re-apply the persisted choice (login autostart; retries while
|
||||
hyprpaper's control socket comes up)
|
||||
path -> print the persisted path (empty if never picked)
|
||||
|
||||
Apply mechanism
|
||||
---------------
|
||||
hyprpaper >= 0.8 ships a new control protocol: the legacy `preload`/`reload`
|
||||
IPC commands are gone, and `hyprctl hyprpaper wallpaper <monitor>,<path>`
|
||||
takes a real output name (no `*`). We query `hyprctl monitors -j` for the
|
||||
current output names and apply to each. Persisting to a cache file (not
|
||||
hyprpaper.conf) keeps the Nix-generated config authoritative; the login
|
||||
autostart line in hyprland.nix re-applies the choice after hyprpaper starts,
|
||||
defaulting to the config wallpaper until the user picks something.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
HOME = os.path.expanduser("~")
|
||||
WALL_DIR = os.path.join(HOME, "Pictures", "wallpapers")
|
||||
CACHE_DIR = os.path.join(HOME, ".cache", "quickshell")
|
||||
STATE_FILE = os.path.join(CACHE_DIR, "wallpaper")
|
||||
|
||||
IMAGE_EXT = (".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif")
|
||||
HYPRCTL = "hyprctl"
|
||||
|
||||
|
||||
def ensure_cache_dir():
|
||||
os.makedirs(CACHE_DIR, exist_ok=True)
|
||||
|
||||
|
||||
def scan_names():
|
||||
if not os.path.isdir(WALL_DIR):
|
||||
return []
|
||||
try:
|
||||
names = os.listdir(WALL_DIR)
|
||||
except OSError:
|
||||
return []
|
||||
return sorted(n for n in names if n.lower().endswith(IMAGE_EXT))
|
||||
|
||||
|
||||
def read_state():
|
||||
try:
|
||||
with open(STATE_FILE) as fh:
|
||||
path = fh.read().strip()
|
||||
return path if os.path.isfile(path) else ""
|
||||
except OSError:
|
||||
return ""
|
||||
|
||||
|
||||
def list_walls():
|
||||
current = read_state()
|
||||
entries = []
|
||||
for name in scan_names():
|
||||
path = os.path.join(WALL_DIR, name)
|
||||
entries.append({"name": name, "path": path, "active": path == current})
|
||||
return entries
|
||||
|
||||
|
||||
def monitors():
|
||||
"""Current Hyprland output names (e.g. eDP-1), empty if unavailable."""
|
||||
try:
|
||||
out = subprocess.run(
|
||||
[HYPRCTL, "monitors", "-j"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
except OSError:
|
||||
return []
|
||||
if out.returncode != 0:
|
||||
return []
|
||||
try:
|
||||
data = json.loads(out.stdout)
|
||||
return [m["name"] for m in data if "name" in m]
|
||||
except (ValueError, KeyError, TypeError):
|
||||
return []
|
||||
|
||||
|
||||
def apply_path(path, retries=10, delay=0.5):
|
||||
"""Apply via hyprctl hyprpaper, absorbing the login socket race."""
|
||||
if not os.path.isfile(path):
|
||||
return False, "no such file: %s" % path
|
||||
last = ""
|
||||
for _ in range(retries):
|
||||
mons = monitors()
|
||||
if not mons:
|
||||
last = "no monitors available"
|
||||
time.sleep(delay)
|
||||
continue
|
||||
try:
|
||||
errs = []
|
||||
ok = True
|
||||
for mon in mons:
|
||||
setw = subprocess.run(
|
||||
[HYPRCTL, "hyprpaper", "wallpaper", mon + "," + path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if setw.returncode != 0:
|
||||
ok = False
|
||||
errs.append(setw.stderr.strip())
|
||||
except OSError as exc:
|
||||
return False, "hyprctl unavailable: %s" % exc
|
||||
if ok:
|
||||
return True, ""
|
||||
last = "; ".join(errs) or "hyprctl hyprpaper wallpaper failed"
|
||||
time.sleep(delay)
|
||||
return False, last
|
||||
|
||||
|
||||
def set_wall(path):
|
||||
if not os.path.isfile(path):
|
||||
return {"ok": False, "error": "no such file: %s" % path}
|
||||
ensure_cache_dir()
|
||||
with open(STATE_FILE, "w") as fh:
|
||||
fh.write(path + "\n")
|
||||
ok, err = apply_path(path)
|
||||
return {"ok": ok, "stderr": err}
|
||||
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
cmd = args[0] if args else "list"
|
||||
if cmd == "list":
|
||||
print(json.dumps(list_walls()))
|
||||
elif cmd == "set":
|
||||
print(json.dumps(set_wall(args[1] if len(args) > 1 else "")))
|
||||
elif cmd == "apply":
|
||||
current = read_state()
|
||||
print(json.dumps(set_wall(current) if current else {"ok": True, "stderr": ""}))
|
||||
elif cmd == "path":
|
||||
print(read_state())
|
||||
else:
|
||||
print(json.dumps({"ok": False, "error": "unknown command: " + cmd}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user