mirror of
https://github.com/caelestia-dots/cli.git
synced 2026-06-05 14:59:29 -05:00
b00c601d0a
LSP was screaming at me so I decided to just address it to get it off my screen. + Fixed the type hints := Modified and added type hints for certain functions and variables in most of the files in the utils/ folder (and some in the subcommands/ folder) for clarity and so pyright's type checker wouldn't cry. :+ To resolve certain type issues, I had to add a bit more tiny additional code such as, additional checks if a variable is None, a tiny class in utils/material/generator.py to resolve the constructor usage mismatch between what the DynamicScheme accepts and what the code actually passes, and etc. - Renamed certain functions and variables for clarity and also for some to not collide with pre-existing definitions from well-known library imports. + PIL has reorganized their code a bit, so the code is made to reflect their new definitions. = Reorganized the single import statement for "colourfulness" in utils/wallpaper.py to be close to the top. (I think that's it) Side Effects?: Everything should work the same as no logic change was done whatsover (unless we consider the added if statements for type checking as a logic change). I've tested it, everything seems to be in urdir.
218 lines
6.2 KiB
Python
218 lines
6.2 KiB
Python
import json
|
|
import os
|
|
import random
|
|
import subprocess
|
|
|
|
from argparse import Namespace
|
|
from pathlib import Path
|
|
from typing import cast
|
|
|
|
from materialyoucolor.hct import Hct
|
|
from materialyoucolor.utils.color_utils import argb_from_rgb
|
|
from PIL import Image
|
|
|
|
from caelestia.utils.hypr import message
|
|
from caelestia.utils.material import get_colours_for_image
|
|
from caelestia.utils.colourfulness import get_variant
|
|
from caelestia.utils.paths import (
|
|
compute_hash,
|
|
user_config_path,
|
|
wallpaper_link_path,
|
|
wallpaper_path_path,
|
|
wallpaper_thumbnail_path,
|
|
wallpapers_cache_dir,
|
|
)
|
|
from caelestia.utils.scheme import Scheme, get_scheme
|
|
from caelestia.utils.theme import apply_colours
|
|
|
|
|
|
def is_valid_image(path: Path) -> bool:
|
|
return path.is_file() and path.suffix in [".jpg", ".jpeg", ".png", ".webp", ".tif", ".tiff", ".gif"]
|
|
|
|
|
|
def check_wall(wall: Path, filter_size: tuple[int, int], threshold: float) -> bool:
|
|
with Image.open(wall) as img:
|
|
width, height = img.size
|
|
return width >= filter_size[0] * threshold and height >= filter_size[1] * threshold
|
|
|
|
|
|
def get_wallpaper() -> str | None:
|
|
try:
|
|
return wallpaper_path_path.read_text()
|
|
except IOError:
|
|
return None
|
|
|
|
|
|
def get_wallpapers(args: Namespace) -> list[Path]:
|
|
directory = Path(args.random)
|
|
if not directory.is_dir():
|
|
return []
|
|
|
|
walls = [f for f in directory.rglob("*") if is_valid_image(f)]
|
|
|
|
if args.no_filter:
|
|
return walls
|
|
|
|
monitors = cast(list[dict[str, int]], message("monitors"))
|
|
filter_size = min(m["width"] for m in monitors), min(m["height"] for m in monitors)
|
|
|
|
return [f for f in walls if check_wall(f, filter_size, args.threshold)]
|
|
|
|
|
|
def get_thumb(wall: Path, cache: Path) -> Path:
|
|
thumb = cache / "thumbnail.jpg"
|
|
|
|
if not thumb.exists():
|
|
with Image.open(wall) as img:
|
|
img = img.convert("RGB")
|
|
img.thumbnail((128, 128), Image.Resampling.NEAREST)
|
|
thumb.parent.mkdir(parents=True, exist_ok=True)
|
|
img.save(thumb, "JPEG")
|
|
|
|
return thumb
|
|
|
|
|
|
def get_smart_opts(wall: Path, cache: Path) -> dict:
|
|
opts_cache = cache / "smart.json"
|
|
|
|
try:
|
|
return json.loads(opts_cache.read_text())
|
|
except (IOError, json.JSONDecodeError):
|
|
pass
|
|
|
|
opts = {}
|
|
|
|
with Image.open(get_thumb(wall, cache)) as img:
|
|
opts["variant"] = get_variant(img)
|
|
img.thumbnail((1, 1), Image.Resampling.LANCZOS)
|
|
|
|
# Cast the pixel to a tuple of 3 integers to safely unpack it
|
|
pixel = cast(tuple[int, int, int], img.getpixel((0, 0)))
|
|
hct = Hct.from_int(argb_from_rgb(*pixel))
|
|
|
|
opts["mode"] = "light" if hct.tone > 60 else "dark"
|
|
|
|
opts_cache.parent.mkdir(parents=True, exist_ok=True)
|
|
with opts_cache.open("w") as f:
|
|
json.dump(opts, f)
|
|
|
|
return opts
|
|
|
|
|
|
def get_colours_for_wall(wall: Path | str, no_smart: bool) -> None:
|
|
wall = Path(wall)
|
|
scheme = get_scheme()
|
|
cache = wallpapers_cache_dir / compute_hash(wall)
|
|
|
|
if wall.suffix.lower() == ".gif":
|
|
wall = convert_gif(wall)
|
|
|
|
name = "dynamic"
|
|
|
|
if not no_smart:
|
|
smart_opts = get_smart_opts(wall, cache)
|
|
scheme = Scheme(
|
|
{
|
|
"name": name,
|
|
"flavour": scheme.flavour,
|
|
"mode": smart_opts["mode"],
|
|
"variant": smart_opts["variant"],
|
|
"colours": scheme.colours,
|
|
}
|
|
)
|
|
|
|
return {
|
|
"name": name,
|
|
"flavour": scheme.flavour,
|
|
"mode": scheme.mode,
|
|
"variant": scheme.variant,
|
|
"colours": get_colours_for_image(get_thumb(wall, cache), scheme),
|
|
}
|
|
|
|
|
|
def convert_gif(wall: Path) -> Path:
|
|
cache = wallpapers_cache_dir / compute_hash(wall)
|
|
output_path = cache / "first_frame.png"
|
|
|
|
if not output_path.exists():
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
with Image.open(wall) as img:
|
|
try:
|
|
img.seek(0)
|
|
except EOFError:
|
|
pass
|
|
|
|
img = img.convert("RGB")
|
|
img.save(output_path, "PNG")
|
|
|
|
return output_path
|
|
|
|
|
|
def set_wallpaper(wall: Path, no_smart: bool) -> None:
|
|
# Make path absolute
|
|
wall = Path(wall).resolve()
|
|
|
|
if not is_valid_image(wall):
|
|
raise ValueError(f'"{wall}" is not a valid image')
|
|
|
|
# Use gif's 1st frame for thumb only
|
|
wall_cache = convert_gif(wall) if wall.suffix.lower() == ".gif" else wall
|
|
|
|
# Update files
|
|
wallpaper_path_path.parent.mkdir(parents=True, exist_ok=True)
|
|
wallpaper_path_path.write_text(str(wall))
|
|
wallpaper_link_path.parent.mkdir(parents=True, exist_ok=True)
|
|
wallpaper_link_path.unlink(missing_ok=True)
|
|
wallpaper_link_path.symlink_to(wall)
|
|
|
|
cache = wallpapers_cache_dir / compute_hash(wall_cache)
|
|
|
|
# Generate thumbnail or get from cache
|
|
thumb = get_thumb(wall_cache, cache)
|
|
wallpaper_thumbnail_path.parent.mkdir(parents=True, exist_ok=True)
|
|
wallpaper_thumbnail_path.unlink(missing_ok=True)
|
|
wallpaper_thumbnail_path.symlink_to(thumb)
|
|
|
|
scheme = get_scheme()
|
|
|
|
# Change mode and variant based on wallpaper colour
|
|
if scheme.name == "dynamic" and not no_smart:
|
|
smart_opts = get_smart_opts(wall_cache, cache)
|
|
scheme.mode = smart_opts["mode"]
|
|
scheme.variant = smart_opts["variant"]
|
|
|
|
# Update colours
|
|
scheme.update_colours()
|
|
apply_colours(scheme.colours, scheme.mode)
|
|
|
|
# Run custom post-hook if configured
|
|
try:
|
|
cfg = json.loads(user_config_path.read_text()).get("wallpaper", {})
|
|
if post_hook := cfg.get("postHook"):
|
|
subprocess.run(
|
|
post_hook,
|
|
shell=True,
|
|
env={**os.environ, "WALLPAPER_PATH": str(wall)},
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
pass
|
|
|
|
|
|
def set_random(args: Namespace) -> None:
|
|
wallpapers = get_wallpapers(args)
|
|
|
|
if not wallpapers:
|
|
raise ValueError("No valid wallpapers found")
|
|
|
|
try:
|
|
last_wall = wallpaper_path_path.read_text()
|
|
wallpapers.remove(Path(last_wall))
|
|
|
|
if not wallpapers:
|
|
raise ValueError("Only valid wallpaper is current")
|
|
except (FileNotFoundError, ValueError):
|
|
pass
|
|
|
|
set_wallpaper(random.choice(wallpapers), args.no_smart)
|