diff --git a/hypr/bindings.conf b/hypr/bindings.conf index daadc2e..f14b9cc 100644 --- a/hypr/bindings.conf +++ b/hypr/bindings.conf @@ -7,5 +7,7 @@ source = ~/.config/hypr/bindings/tiling.conf source = ~/.config/hypr/bindings/hyprshot.conf source = ~/.config/hypr/bindings/randrwall.conf source = ~/.config/hypr/bindings/media.conf +source = ~/.config/hypr/bindings/lock.conf +source = ~/.config/hypr/bindings/programs.conf diff --git a/hypr/bindings/lock.conf b/hypr/bindings/lock.conf new file mode 100644 index 0000000..5c4d558 --- /dev/null +++ b/hypr/bindings/lock.conf @@ -0,0 +1 @@ +bind = CTRL ALT, Q, exec, hyprlock diff --git a/hypr/bindings/programs.conf b/hypr/bindings/programs.conf new file mode 100644 index 0000000..1ae84db --- /dev/null +++ b/hypr/bindings/programs.conf @@ -0,0 +1,6 @@ +bind = $mainMod, RETURN, exec, $terminal +bind = $mainMod, W, killactive, +bind = $mainMod, M, exit, +bind = $mainMod, E, exec, $fileManager +bind = $mainMod, B, exec, $browser +bind = ALT, space, exec, $menu diff --git a/hypr/bindings/randrwall.conf b/hypr/bindings/randrwall.conf index 7588bb5..3ecfd59 100644 --- a/hypr/bindings/randrwall.conf +++ b/hypr/bindings/randrwall.conf @@ -1,5 +1,4 @@ - -$randrwall = "/home/stereov/Developer/antistereov/randrwall/randrwall.py" +$randrwall = "~/.config/hypr/scripts/randrwall.py" bind = SUPER SHIFT, W, exec, $randrwall random bind = SUPER SHIFT, F, exec, $randrwall favorite diff --git a/hypr/bindings/tiling.conf b/hypr/bindings/tiling.conf index cdf41e0..c6b0029 100644 --- a/hypr/bindings/tiling.conf +++ b/hypr/bindings/tiling.conf @@ -1,14 +1,7 @@ $mainMod = SUPER # Sets "Windows" key as main modifier # Example binds, see https://wiki.hyprland.org/Configuring/Binds/ for more - -bind = $mainMod, RETURN, exec, $terminal -bind = $mainMod, W, killactive, -bind = $mainMod, M, exit, -bind = $mainMod, E, exec, $fileManager -bind = $mainMod, B, exec, $browser bind = $mainMod, F, togglefloating, -bind = ALT, space, exec, $menu bind = $mainMod, P, pseudo, # dwindle bind = $mainMod SHIFT, J, togglesplit, # dwindle @@ -66,7 +59,3 @@ bind = $mainMod, mouse_up, workspace, e-1 bindm = $mainMod, mouse:272, movewindow bindm = $mainMod, mouse:273, resizewindow -bind = CTRL ALT, Q, exec, hyprlock -bindl = , switch:Lid Switch, exec, hyprlock - - diff --git a/hypr/monitors.conf b/hypr/monitors.conf index 11bd958..314dc20 100644 --- a/hypr/monitors.conf +++ b/hypr/monitors.conf @@ -4,6 +4,9 @@ # See https://wiki.hyprland.org/Configuring/Monitors/ monitor=,preferred, auto, auto -monitor=eDP-1, 2256x1504, auto, 1.175 -monitor=DP-10, 3440x1440, -3440x0, 1 +monitor=eDP-1, 2256x1504, 0x0, 1.175 +monitor=DP-10, 3840x2160, -640x-1800, 1.2 + +bindl = , switch:on:Lid Switch, exec, ~/.config/hypr/scripts/lid.sh off +bindl = , switch:off:Lid Switch, exec, ~/.config/hypr/scripts/lid.sh on diff --git a/hypr/scripts/lid.sh b/hypr/scripts/lid.sh new file mode 100755 index 0000000..212780f --- /dev/null +++ b/hypr/scripts/lid.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail + +ACTION="${1:-}" # "on" oder "off" +LAPTOP="eDP-1" +LAPTOP_MODE="2256x1504" +LAPTOP_SCALE="1.175" + +has_external() { + hyprctl monitors | awk '/^Monitor /{print $2}' | grep -vq "^${LAPTOP}$" +} + +enable_laptop() { + hyprctl keyword monitor "${LAPTOP}, ${LAPTOP_MODE}, 0x0, ${LAPTOP_SCALE}" + hyprctl dispatch dpms on "${LAPTOP}" 2>/dev/null || true +} + +disable_laptop() { + hyprctl keyword monitor "${LAPTOP}, disable" +} + +case "$ACTION" in +off) + if has_external; then + disable_laptop + else + disable_laptop + hyprlock + fi + ;; + +on) + enable_laptop + ;; + +*) + echo "usage: $0 {on|off}" >&2 + exit 2 + ;; +esac diff --git a/hypr/scripts/randrwall.py b/hypr/scripts/randrwall.py new file mode 100755 index 0000000..4dcdbba --- /dev/null +++ b/hypr/scripts/randrwall.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python3 + +import argparse +import json +from os import environ +import random +import shutil +import subprocess +import sys +from pathlib import Path +from urllib.parse import quote + +import urllib.request + +HOME = Path.home() + +BASE_DIR = HOME / "Pictures" / "Wallpapers" +FAV_DIR = BASE_DIR / "favorites" +CACHE_DIR = BASE_DIR / "cache" +CURRENT = BASE_DIR / "current" +STATE_FILE = HOME / ".cache" / "randrwall_state.json" + +DEFAULT_TAGS = [ + "space", + "nebula", + "cyberpunk", + "minimal", + "architecture", + "landscape", + "fantasy", + "digital art", +] +ATLEAST = "3840x2160" +RATIOS = "16x9,21x9" +SORTING = "random" + +CATEGORIES = "110" +PURITY = "100" + + +def load_env_file(path: Path) -> None: + if not path.exists(): + return + for line in path.read_text().splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + if "=" not in line: + continue + k, v = line.split("=", 1) + environ.setdefault(k.strip(), v.strip()) + + +load_env_file(Path() / ".env") +API_KEY = environ.get("WALLHAVEN_API_KEY") + + +def run(cmd: list[str]) -> None: + subprocess.run(cmd, check=True) + + +def ensure_dirs() -> None: + FAV_DIR.mkdir(parents=True, exist_ok=True) + CACHE_DIR.mkdir(parents=True, exist_ok=True) + STATE_FILE.parent.mkdir(parents=True, exist_ok=True) + + +def read_state() -> dict: + if STATE_FILE.exists(): + try: + return json.loads(STATE_FILE.read_text(encoding="utf-8")) + except Exception: + return {} + return {} + + +def write_state(state: dict) -> None: + STATE_FILE.write_text(json.dumps(state, indent=2), encoding="utf-8") + + +def is_image(path: Path) -> bool: + return path.suffix.lower() in {".jpg", ".jpeg", ".png", ".webp"} + + +def swww_set(path: Path) -> None: + run( + [ + "swww", + "img", + str(path), + ] + ) + + +def pick_random_from(dir_path: Path) -> Path | None: + if not dir_path.exists(): + return None + + imgs = [p for p in dir_path.iterdir() if p.is_file() and is_image(p)] + if not imgs: + return None + return random.choice(imgs) + + +UA = "randrwall/0.1 (+https://gitea.stereov.io/antistereov/randrwall)" + +HEADERS = { + "User-Agent": UA, + "Referer": "https://wallhaven.cc/", + "Accept": "*/*", +} + + +def http_get_json(url: str, timeout: int = 15) -> dict: + req = urllib.request.Request(url, headers=HEADERS) + with urllib.request.urlopen(req, timeout=timeout) as resp: + return json.loads(resp.read().decode("utf-8")) + + +def wallhaven_search_random(tags: list[str]) -> str: + tag = random.choice(tags) + q = quote(tag) + + params = ( + f"q={q}" + f"&categories={CATEGORIES}" + f"$purity={PURITY}" + f"&atleast={ATLEAST}" + f"&ratios={quote(RATIOS)}" + f"&sorting={SORTING}" + "&order=desc" + "&page=1" + ) + + if API_KEY: + print("Using API key") + params += f"&apiKey={API_KEY}" + + url = f"https://wallhaven.cc/api/v1/search?{params}" + + data = http_get_json(url) + + arr = data.get("data") or [] + if not arr: + raise RuntimeError(f"Wallhaven: no results for tag='{tag}'") + + item = random.choice(arr) + path = item.get("path") + if not path: + raise RuntimeError("Wallhaven: response does not contain 'path'") + return path + + +def prune_cache(max_files: int = 10) -> None: + if max_files <= 0: + return + + files = [p for p in CACHE_DIR.iterdir() if p.is_file() and is_image(p)] + files.sort(key=lambda p: p.stat().st_mtime, reverse=True) + + for p in files[max_files:]: + try: + p.unlink() + except Exception: + pass + + +def download_to_cache(url: str) -> Path: + filename = url.split("/")[-1] + out = CACHE_DIR / filename + if out.exists() and out.stat().st_size > 0: + return out + + tmp_path = CACHE_DIR / (filename + ".part") + + req = urllib.request.Request(url, headers=HEADERS, method="GET") + + try: + with urllib.request.urlopen(req, timeout=30) as resp: + if resp.status != 200: + raise RuntimeError(f"Download failed: HTTP {resp.status}") + with open(tmp_path, "wb") as f: + shutil.copyfileobj(resp, f) + + tmp_path.replace(out) + prune_cache() + return out + finally: + try: + if tmp_path.exists(): + tmp_path.unlink() + + except Exception: + pass + + +def copy_to_current(img: Path) -> None: + shutil.copy2(img, CURRENT) + + +def cmd_random(args: argparse.Namespace) -> None: + ensure_dirs() + state = read_state() + + use_fav = random.random() < args.fav_weight + fav_pick = pick_random_from(FAV_DIR) if use_fav else None + + if fav_pick is not None: + swww_set(fav_pick) + state["current"] = str(fav_pick) + state["source"] = "favorites" + write_state(state) + print(f"Set (favorites): {fav_pick}") + copy_to_current(fav_pick) + return + + url = wallhaven_search_random(args.tags) + img = download_to_cache(url) + swww_set(img) + copy_to_current(img) + + state["current"] = str(img) + state["source"] = "wallhaven" + state["wallhaven_url"] = url + write_state(state) + print(f"Set (download): {img}") + + +def cmd_favorite(args: argparse.Namespace) -> None: + ensure_dirs() + state = read_state() + cur = state.get("current") + + if not cur: + print( + "No current wallpaper found in state. Please set one first via 'random'", + file=sys.stderr, + ) + sys.exit(1) + + src = Path(cur) + if not src.exists(): + print(f"Current wallpaper does not exist anymore: {src}", file=sys.stderr) + sys.exit(1) + + dest = FAV_DIR / src.name + if dest.exists(): + print(f"Already in favorites: {dest}") + return + + shutil.copy2(src, dest) + print(f"Saved to favorites: {dest}") + + +def reload(args: argparse.Namespace) -> None: + ensure_dirs() + state = read_state() + cur = state.get("current") + + if not cur: + print( + "No current wallpaper found in state. Please set one first via 'random'", + file=sys.stderr, + ) + sys.exit(1) + + src = Path(cur) + if not src.exists(): + print(f"Current wallpaper does not exist anymore: {src}", file=sys.stderr) + sys.exit(1) + + swww_set(cur) + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Hyprland wallpaper helper (swww + wallhaven + favorites" + ) + sub = parser.add_subparsers(dest="cmd", required=True) + + p_random = sub.add_parser( + "random", help="Set random wallpaper (favorites or download)" + ) + p_random.add_argument( + "--fav-weight", + type=float, + default=0.3, + help="Probability to pick from favorites (0..1). Default: 0.3", + ) + p_random.add_argument( + "--tags", + nargs="+", + default=DEFAULT_TAGS, + help=f"Wall haven tags list. Default: {DEFAULT_TAGS}", + ) + p_random.set_defaults(func=cmd_random) + + p_fav = sub.add_parser("favorite", help="Save current wallpaper to favorites") + p_fav.set_defaults(func=cmd_favorite) + + p_fav = sub.add_parser("reload", help="Reload current wallpaper") + p_fav.set_defaults(func=reload) + + args = parser.parse_args() + args.func(args) + + +if __name__ == "__main__": + main()