diff --git a/src/caelestia/parser.py b/src/caelestia/parser.py index 110e065..aafa852 100644 --- a/src/caelestia/parser.py +++ b/src/caelestia/parser.py @@ -6,7 +6,7 @@ from caelestia.utils.scheme import get_scheme_names, scheme_variants from caelestia.utils.wallpaper import get_wallpaper -def parse_args() -> (argparse.ArgumentParser, argparse.Namespace): +def parse_args() -> tuple[argparse.ArgumentParser, argparse.Namespace]: parser = argparse.ArgumentParser(prog="caelestia", description="Main control script for the Caelestia dotfiles") parser.add_argument("-v", "--version", action="store_true", help="print the current version") diff --git a/src/caelestia/subcommands/screenshot.py b/src/caelestia/subcommands/screenshot.py index 6b4c00a..9a8cd60 100644 --- a/src/caelestia/subcommands/screenshot.py +++ b/src/caelestia/subcommands/screenshot.py @@ -26,8 +26,11 @@ class Command: else: sc_data = subprocess.check_output(["grim", "-l", "0", "-g", self.args.region.strip(), "-"]) swappy = subprocess.Popen(["swappy", "-f", "-"], stdin=subprocess.PIPE, start_new_session=True) - swappy.stdin.write(sc_data) - swappy.stdin.close() + + # Ensure stdin is not None for the type checker + if swappy.stdin: + swappy.stdin.write(sc_data) + swappy.stdin.close() def fullscreen(self) -> None: sc_data = subprocess.check_output(["grim", "-"]) diff --git a/src/caelestia/subcommands/shell.py b/src/caelestia/subcommands/shell.py index b9e81ed..48a71c2 100644 --- a/src/caelestia/subcommands/shell.py +++ b/src/caelestia/subcommands/shell.py @@ -33,11 +33,14 @@ class Command: subprocess.run(args) else: shell = subprocess.Popen(args, stdout=subprocess.PIPE, universal_newlines=True) - for line in shell.stdout: - if self.filter_log(line): - print(line, end="") - def shell(self, *args: list[str]) -> str: + # Ensure stdout is not None for the type checker + if shell.stdout: + for line in shell.stdout: + if self.filter_log(line): + print(line, end="") + + def shell(self, *args: str) -> str: return subprocess.check_output(["qs", "-c", "caelestia", *args], text=True) def filter_log(self, line: str) -> bool: diff --git a/src/caelestia/subcommands/toggle.py b/src/caelestia/subcommands/toggle.py index ba5a351..56565f3 100644 --- a/src/caelestia/subcommands/toggle.py +++ b/src/caelestia/subcommands/toggle.py @@ -3,6 +3,7 @@ import shlex import shutil from argparse import Namespace from collections import ChainMap +from typing import Any, Callable, cast from caelestia.utils import hypr from caelestia.utils.paths import user_config_path @@ -52,8 +53,8 @@ class DeepChainMap(ChainMap): class Command: args: Namespace - cfg: dict[str, dict[str, dict[str, any]]] | DeepChainMap - clients: list[dict[str, any]] = None + cfg: dict[str, dict[str, dict[str, Any]]] | DeepChainMap + clients: list[dict[str, Any]] | None = None def __init__(self, args: Namespace) -> None: self.args = args @@ -120,27 +121,27 @@ class Command: if not spawned: hypr.dispatch("togglespecialworkspace", self.args.workspace) - def get_clients(self) -> list[dict[str, any]]: + def get_clients(self) -> list[dict[str, Any]]: if self.clients is None: - self.clients = hypr.message("clients") - + self.clients = cast(list[dict[str, Any]], hypr.message("clients")) return self.clients - def move_client(self, selector: callable, workspace: str) -> None: + def move_client(self, selector: Callable, workspace: str) -> None: for client in self.get_clients(): if selector(client) and client["workspace"]["name"] != f"special:{workspace}": hypr.dispatch("movetoworkspacesilent", f"special:{workspace},address:{client['address']}") - def spawn_client(self, selector: callable, spawn: list[str]) -> bool: + def spawn_client(self, selector: Callable, spawn: list[str]) -> bool: if (spawn[0].endswith(".desktop") or shutil.which(spawn[0])) and not any( selector(client) for client in self.get_clients() ): hypr.dispatch("exec", f"[workspace special:{self.args.workspace}] app2unit -- {shlex.join(spawn)}") return True - return False + else: + return False - def handle_client_config(self, client: dict[str, any]) -> bool: - def selector(c: dict[str, any]) -> bool: + def handle_client_config(self, client: dict[str, Any]) -> bool: + def selector(c: dict[str, Any]) -> bool: # Each match is or, inside matches is and for match in client["match"]: if is_subset(c, match): @@ -156,5 +157,8 @@ class Command: return spawned def specialws(self) -> None: - special = next(m for m in hypr.message("monitors") if m["focused"])["specialWorkspace"]["name"] - hypr.dispatch("togglespecialworkspace", special[8:] or "special") + monitors = cast(list[dict[str, Any]], hypr.message("monitors")) + target = next((m for m in monitors if m.get("focused")), None) + if target: + special = target.get("specialWorkspace", {}).get("name", "")[8:] or "special" + hypr.dispatch("togglespecialworkspace", special) diff --git a/src/caelestia/utils/colourfulness.py b/src/caelestia/utils/colourfulness.py index f76c1b6..5e06eb3 100644 --- a/src/caelestia/utils/colourfulness.py +++ b/src/caelestia/utils/colourfulness.py @@ -11,8 +11,7 @@ def stddev(values: list[float], mean_val: float) -> float: return math.sqrt(sum((x - mean_val) ** 2 for x in values) / len(values)) if values else 0 -def calc_colourfulness(image: Image) -> float: - width, height = image.size +def calc_colourfulness(image: Image.Image) -> float: pixels = list(image.getdata()) # List of (R, G, B) tuples rg_diffs = [] @@ -32,7 +31,7 @@ def calc_colourfulness(image: Image) -> float: return math.sqrt(std_rg**2 + std_yb**2) + 0.3 * math.sqrt(mean_rg**2 + mean_yb**2) -def get_variant(image: Image) -> str: +def get_variant(image: Image.Image) -> str: colourfulness = calc_colourfulness(image) if colourfulness < 10: diff --git a/src/caelestia/utils/hypr.py b/src/caelestia/utils/hypr.py index 81bc123..d251b98 100644 --- a/src/caelestia/utils/hypr.py +++ b/src/caelestia/utils/hypr.py @@ -1,17 +1,18 @@ -import json as j +import json import os import socket +from typing import Any socket_base = f"{os.getenv('XDG_RUNTIME_DIR')}/hypr/{os.getenv('HYPRLAND_INSTANCE_SIGNATURE')}" socket_path = f"{socket_base}/.socket.sock" socket2_path = f"{socket_base}/.socket2.sock" -def message(msg: str, json: bool = True) -> str | dict[str, any]: +def message(msg: str, is_json: bool = True) -> str | dict[str, Any]: with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: sock.connect(socket_path) - if json: + if is_json: msg = f"j/{msg}" sock.send(msg.encode()) @@ -22,14 +23,17 @@ def message(msg: str, json: bool = True) -> str | dict[str, any]: break resp += new_resp.decode() - return j.loads(resp) if json else resp + return json.loads(resp) if is_json else resp -def dispatch(dispatcher: str, *args: list[any]) -> bool: - return message(f"dispatch {dispatcher} {' '.join(map(str, args))}".rstrip(), json=False) == "ok" +def dispatch(dispatcher: str, *args: str) -> bool: + return message(f"dispatch {dispatcher} {' '.join(map(str, args))}".rstrip(), is_json=False) == "ok" -def batch(*msgs: list[str], json: bool = False) -> str | dict[str, any]: - if json: - msgs = (f"j/{m.strip()}" for m in msgs) - return message(f"[[BATCH]]{';'.join(msgs)}", json=False) +def batch(*msgs: str, is_json: bool = False) -> str | dict[str, Any]: + formatted_msgs = msgs + + if is_json: + formatted_msgs = [f"j/{m.strip()}" for m in msgs] + + return message(f"[[BATCH]]{';'.join(formatted_msgs)}", is_json=False) diff --git a/src/caelestia/utils/material/generator.py b/src/caelestia/utils/material/generator.py index 384fdd1..cf241e9 100644 --- a/src/caelestia/utils/material/generator.py +++ b/src/caelestia/utils/material/generator.py @@ -11,6 +11,14 @@ from materialyoucolor.scheme.scheme_rainbow import SchemeRainbow from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot from materialyoucolor.scheme.scheme_vibrant import SchemeVibrant from materialyoucolor.utils.math_utils import difference_degrees, rotation_direction, sanitize_degrees_double +from typing import Protocol, Any + + +# The base DynamicScheme class requires a 'variant' argument, but the specific +# subclasses in get_scheme() handle that internally. This Protocol tells the type +# checker to expect our specific 3-argument setup instead of the base class signature. +class SchemeConstructor(Protocol): + def __call__(self, source_color_hct: Any, is_dark: bool, contrast_level: float) -> DynamicScheme: ... try: from materialyoucolor.dynamiccolor.dynamic_scheme import DynamicScheme @@ -147,7 +155,7 @@ def darken(colour: Hct, amount: float) -> Hct: return Hct.from_hct(colour.hue, colour.chroma - diff / 5, colour.tone - diff) -def get_scheme(scheme: str) -> DynamicScheme: +def get_scheme(scheme: str) -> SchemeConstructor: if scheme == "content": return SchemeContent if scheme == "expressive": @@ -168,12 +176,12 @@ def get_scheme(scheme: str) -> DynamicScheme: def gen_scheme(scheme, primary: Hct) -> dict[str, str]: - light = scheme.mode == "light" + is_light = scheme.mode == "light" colours = {} # Material colours - primary_scheme = get_scheme(scheme.variant)(primary, not light, 0) + primary_scheme = get_scheme(scheme.variant)(source_color_hct=primary, is_dark=not is_light, contrast_level=0.0) if hasattr(MaterialDynamicColors, "all_colors"): # materialyoucolor-python >= 3.0.0 dyn_colours = MaterialDynamicColors() for colour in dyn_colours.all_colors: @@ -191,28 +199,28 @@ def gen_scheme(scheme, primary: Hct) -> dict[str, str]: colours["neutral_variant_paletteKeyColor"] = colours["neutralVariantPaletteKeyColor"] # Harmonize terminal colours - for i, hct in enumerate(light_gruvbox if light else dark_gruvbox): + for i, hct in enumerate(light_gruvbox if is_light else dark_gruvbox): if scheme.variant == "monochrome": - colours[f"term{i}"] = grayscale(hct, light) + colours[f"term{i}"] = grayscale(hct, is_light) else: colours[f"term{i}"] = harmonize( - hct, colours["primary_paletteKeyColor"], (0.35 if i < 8 else 0.2) * (-1 if light else 1) + hct, colours["primary_paletteKeyColor"], (0.35 if i < 8 else 0.2) * (-1 if is_light else 1) ) # Harmonize named colours - for i, hct in enumerate(light_catppuccin if light else dark_catppuccin): + for i, hct in enumerate(light_catppuccin if is_light else dark_catppuccin): if scheme.variant == "monochrome": - colours[colour_names[i]] = grayscale(hct, light) + colours[colour_names[i]] = grayscale(hct, is_light) else: - colours[colour_names[i]] = harmonize(hct, colours["primary_paletteKeyColor"], (-0.2 if light else 0.05)) + colours[colour_names[i]] = harmonize(hct, colours["primary_paletteKeyColor"], (-0.2 if is_light else 0.05)) # KColours for colour in kcolours: colours[colour["name"]] = harmonize(colour["hct"], colours["primary"], 0.1) colours[f"{colour['name']}Selection"] = harmonize(colour["hct"], colours["onPrimaryFixedVariant"], 0.1) if scheme.variant == "monochrome": - colours[colour["name"]] = grayscale(colours[colour["name"]], light) - colours[f"{colour['name']}Selection"] = grayscale(colours[f"{colour['name']}Selection"], light) + colours[colour["name"]] = grayscale(colours[colour["name"]], is_light) + colours[f"{colour['name']}Selection"] = grayscale(colours[f"{colour['name']}Selection"], is_light) if scheme.variant == "neutral": for name, hct in colours.items(): @@ -221,8 +229,8 @@ def gen_scheme(scheme, primary: Hct) -> dict[str, str]: # Darken surfaces for hard flavour if scheme.flavour == "hard": for colour in "background", *(k for k in colours.keys() if k.startswith("surface")): - colours[colour] = lighten(colours[colour], 0.4) if light else darken(colours[colour], 0.8) - colours["term0"] = lighten(colours["term0"], 0.4) if light else darken(colours["term0"], 0.9) + colours[colour] = lighten(colours[colour], 0.4) if is_light else darken(colours[colour], 0.8) + colours["term0"] = lighten(colours["term0"], 0.4) if is_light else darken(colours["term0"], 0.9) # FIXME: deprecated stuff colours["text"] = colours["onBackground"] @@ -241,13 +249,13 @@ def gen_scheme(scheme, primary: Hct) -> dict[str, str]: # More darkening if hard flavour if scheme.flavour == "hard": for colour in "base", "mantle", "crust": - colours[colour] = lighten(colours[colour], 0.4) if light else darken(colours[colour], 0.9) + colours[colour] = lighten(colours[colour], 0.4) if is_light else darken(colours[colour], 0.9) for i in range(3): colours[f"overlay{i}"] = ( - lighten(colours[f"overlay{i}"], 0.4) if light else darken(colours[f"overlay{i}"], 0.8) + lighten(colours[f"overlay{i}"], 0.4) if is_light else darken(colours[f"overlay{i}"], 0.8) ) colours[f"surface{i}"] = ( - lighten(colours[f"surface{i}"], 0.4) if light else darken(colours[f"surface{i}"], 0.8) + lighten(colours[f"surface{i}"], 0.4) if is_light else darken(colours[f"surface{i}"], 0.8) ) # For debugging @@ -256,7 +264,7 @@ def gen_scheme(scheme, primary: Hct) -> dict[str, str]: colours = {k: hex(v.to_int())[4:] for k, v in colours.items()} # Extended material - if light: + if is_light: colours["success"] = "4F6354" colours["onSuccess"] = "FFFFFF" colours["successContainer"] = "D1E8D5" diff --git a/src/caelestia/utils/notify.py b/src/caelestia/utils/notify.py index ee86236..c88dece 100644 --- a/src/caelestia/utils/notify.py +++ b/src/caelestia/utils/notify.py @@ -1,7 +1,7 @@ import subprocess -def notify(*args: list[str]) -> str: +def notify(*args: str) -> str: return subprocess.check_output(["notify-send", "-a", "caelestia-cli", *args], text=True).strip() diff --git a/src/caelestia/utils/paths.py b/src/caelestia/utils/paths.py index 14eb550..3223b90 100644 --- a/src/caelestia/utils/paths.py +++ b/src/caelestia/utils/paths.py @@ -4,41 +4,42 @@ import os import shutil import tempfile from pathlib import Path +from typing import Any -config_dir = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config")) -data_dir = Path(os.getenv("XDG_DATA_HOME", Path.home() / ".local/share")) -state_dir = Path(os.getenv("XDG_STATE_HOME", Path.home() / ".local/state")) -cache_dir = Path(os.getenv("XDG_CACHE_HOME", Path.home() / ".cache")) -pictures_dir = Path(os.getenv("XDG_PICTURES_DIR", Path.home() / "Pictures")) -videos_dir = Path(os.getenv("XDG_VIDEOS_DIR", Path.home() / "Videos")) +config_dir: Path = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config")) +data_dir: Path = Path(os.getenv("XDG_DATA_HOME", Path.home() / ".local/share")) +state_dir: Path = Path(os.getenv("XDG_STATE_HOME", Path.home() / ".local/state")) +cache_dir: Path = Path(os.getenv("XDG_CACHE_HOME", Path.home() / ".cache")) +pictures_dir: Path = Path(os.getenv("XDG_PICTURES_DIR", Path.home() / "Pictures")) +videos_dir: Path = Path(os.getenv("XDG_VIDEOS_DIR", Path.home() / "Videos")) -c_config_dir = config_dir / "caelestia" -c_data_dir = data_dir / "caelestia" -c_state_dir = state_dir / "caelestia" -c_cache_dir = cache_dir / "caelestia" +c_config_dir: Path = config_dir / "caelestia" +c_data_dir: Path = data_dir / "caelestia" +c_state_dir: Path = state_dir / "caelestia" +c_cache_dir: Path = cache_dir / "caelestia" -user_config_path = c_config_dir / "cli.json" -cli_data_dir = Path(__file__).parent.parent / "data" -templates_dir = cli_data_dir / "templates" -user_templates_dir = c_config_dir / "templates" -theme_dir = c_state_dir / "theme" +user_config_path: Path = c_config_dir / "cli.json" +cli_data_dir: Path = Path(__file__).parent.parent / "data" +templates_dir: Path = cli_data_dir / "templates" +user_templates_dir: Path = c_config_dir / "templates" +theme_dir: Path = c_state_dir / "theme" -scheme_path = c_state_dir / "scheme.json" -scheme_data_dir = cli_data_dir / "schemes" -scheme_cache_dir = c_cache_dir / "schemes" +scheme_path: Path = c_state_dir / "scheme.json" +scheme_data_dir: Path = cli_data_dir / "schemes" +scheme_cache_dir: Path = c_cache_dir / "schemes" -wallpapers_dir = os.getenv("CAELESTIA_WALLPAPERS_DIR", pictures_dir / "Wallpapers") -wallpaper_path_path = c_state_dir / "wallpaper/path.txt" -wallpaper_link_path = c_state_dir / "wallpaper/current" -wallpaper_thumbnail_path = c_state_dir / "wallpaper/thumbnail.jpg" -wallpapers_cache_dir = c_cache_dir / "wallpapers" +wallpapers_dir: Path = Path(os.getenv("CAELESTIA_WALLPAPERS_DIR", pictures_dir / "Wallpapers")) +wallpaper_path_path: Path = c_state_dir / "wallpaper/path.txt" +wallpaper_link_path: Path = c_state_dir / "wallpaper/current" +wallpaper_thumbnail_path: Path = c_state_dir / "wallpaper/thumbnail.jpg" +wallpapers_cache_dir: Path = c_cache_dir / "wallpapers" -screenshots_dir = os.getenv("CAELESTIA_SCREENSHOTS_DIR", pictures_dir / "Screenshots") -screenshots_cache_dir = c_cache_dir / "screenshots" +screenshots_dir: Path = Path(os.getenv("CAELESTIA_SCREENSHOTS_DIR", pictures_dir / "Screenshots")) +screenshots_cache_dir: Path = c_cache_dir / "screenshots" -recordings_dir = os.getenv("CAELESTIA_RECORDINGS_DIR", videos_dir / "Recordings") -recording_path = c_state_dir / "record/recording.mp4" -recording_notif_path = c_state_dir / "record/notifid.txt" +recordings_dir: Path = Path(os.getenv("CAELESTIA_RECORDINGS_DIR", videos_dir / "Recordings")) +recording_path: Path = c_state_dir / "record/recording.mp4" +recording_notif_path: Path = c_state_dir / "record/notifid.txt" def compute_hash(path: Path | str) -> str: @@ -51,7 +52,7 @@ def compute_hash(path: Path | str) -> str: return sha.hexdigest() -def atomic_dump(path: Path, content: dict[str, any]) -> None: +def atomic_dump(path: Path, content: dict[str, Any]) -> None: with tempfile.NamedTemporaryFile("w") as f: json.dump(content, f) f.flush() diff --git a/src/caelestia/utils/scheme.py b/src/caelestia/utils/scheme.py index 57a5e0b..e08d777 100644 --- a/src/caelestia/utils/scheme.py +++ b/src/caelestia/utils/scheme.py @@ -1,6 +1,7 @@ import json import random from pathlib import Path +from typing import Any from caelestia.utils.notify import notify from caelestia.utils.paths import atomic_dump, scheme_data_dir, scheme_path @@ -14,19 +15,19 @@ class Scheme: _colours: dict[str, str] notify: bool - def __init__(self, json: dict[str, any] | None) -> None: - if json is None: + def __init__(self, scheme_json: dict[str, Any] | None) -> None: + if scheme_json is None: self._name = "catppuccin" self._flavour = "mocha" self._mode = "dark" self._variant = "tonalspot" self._colours = read_colours_from_file(self.get_colours_path()) else: - self._name = json["name"] - self._flavour = json["flavour"] - self._mode = json["mode"] - self._variant = json["variant"] - self._colours = json["colours"] + self._name = scheme_json["name"] + self._flavour = scheme_json["flavour"] + self._mode = scheme_json["mode"] + self._variant = scheme_json["variant"] + self._colours = scheme_json["colours"] self.notify = False @property @@ -196,7 +197,7 @@ scheme_variants = [ "content", ] -scheme: Scheme = None +scheme: Scheme | None = None def read_colours_from_file(path: Path) -> dict[str, str]: @@ -225,7 +226,7 @@ def get_scheme_names() -> list[str]: return [*(f.name for f in scheme_data_dir.iterdir() if f.is_dir()), "dynamic"] -def get_scheme_flavours(name: str = None) -> list[str]: +def get_scheme_flavours(name: str | None = None) -> list[str]: if name is None: name = get_scheme().name @@ -234,11 +235,11 @@ def get_scheme_flavours(name: str = None) -> list[str]: ) -def get_scheme_modes(name: str = None, flavour: str = None) -> list[str]: - if name is None: +def get_scheme_modes(name: str | None = None, flavour: str | None = None) -> list[str]: + if name is None or flavour is None: scheme = get_scheme() - name = scheme.name - flavour = scheme.flavour + name = name or scheme.name + flavour = flavour or scheme.flavour if name == "dynamic": return ["light", "dark"] diff --git a/src/caelestia/utils/theme.py b/src/caelestia/utils/theme.py index c5ced3a..30e73f8 100644 --- a/src/caelestia/utils/theme.py +++ b/src/caelestia/utils/theme.py @@ -4,6 +4,8 @@ import re import shutil import subprocess import tempfile +import shutil +import fcntl from pathlib import Path from caelestia.utils.colour import get_dynamic_colours @@ -34,10 +36,10 @@ def gen_scss(colours: dict[str, str]) -> str: def gen_replace(colours: dict[str, str], template: Path, hash: bool = False) -> str: - template = template.read_text() + new_template = template.read_text() for name, colour in colours.items(): - template = template.replace(f"{{{{ ${name} }}}}", f"#{colour}" if hash else colour) - return template + new_template = new_template.replace(f"{{{{ ${name} }}}}", f"#{colour}" if hash else colour) + return new_template def gen_replace_dynamic(colours: dict[str, str], template: Path, mode: str) -> str: @@ -65,7 +67,7 @@ def gen_replace_dynamic(colours: dict[str, str], template: Path, mode: str) -> s return template_filled -def c2s(c: str, *i: list[int]) -> str: +def hex_to_ansi(c: str, *i: int) -> str: """Hex to ANSI sequence (e.g. ffffff, 11 -> \x1b]11;rgb:ff/ff/ff\x1b\\)""" return f"\x1b]{';'.join(map(str, i))};rgb:{c[0:2]}/{c[2:4]}/{c[4:6]}\x1b\\" @@ -82,29 +84,29 @@ def gen_sequences(colours: dict[str, str]) -> str: 16+: 256 colours """ return ( - c2s(colours["onSurface"], 10) - + c2s(colours["surface"], 11) - + c2s(colours["secondary"], 12) - + c2s(colours["secondary"], 17) - + c2s(colours["term0"], 4, 0) - + c2s(colours["term1"], 4, 1) - + c2s(colours["term2"], 4, 2) - + c2s(colours["term3"], 4, 3) - + c2s(colours["term4"], 4, 4) - + c2s(colours["term5"], 4, 5) - + c2s(colours["term6"], 4, 6) - + c2s(colours["term7"], 4, 7) - + c2s(colours["term8"], 4, 8) - + c2s(colours["term9"], 4, 9) - + c2s(colours["term10"], 4, 10) - + c2s(colours["term11"], 4, 11) - + c2s(colours["term12"], 4, 12) - + c2s(colours["term13"], 4, 13) - + c2s(colours["term14"], 4, 14) - + c2s(colours["term15"], 4, 15) - + c2s(colours["primary"], 4, 16) - + c2s(colours["secondary"], 4, 17) - + c2s(colours["tertiary"], 4, 18) + hex_to_ansi(colours["onSurface"], 10) + + hex_to_ansi(colours["surface"], 11) + + hex_to_ansi(colours["secondary"], 12) + + hex_to_ansi(colours["secondary"], 17) + + hex_to_ansi(colours["term0"], 4, 0) + + hex_to_ansi(colours["term1"], 4, 1) + + hex_to_ansi(colours["term2"], 4, 2) + + hex_to_ansi(colours["term3"], 4, 3) + + hex_to_ansi(colours["term4"], 4, 4) + + hex_to_ansi(colours["term5"], 4, 5) + + hex_to_ansi(colours["term6"], 4, 6) + + hex_to_ansi(colours["term7"], 4, 7) + + hex_to_ansi(colours["term8"], 4, 8) + + hex_to_ansi(colours["term9"], 4, 9) + + hex_to_ansi(colours["term10"], 4, 10) + + hex_to_ansi(colours["term11"], 4, 11) + + hex_to_ansi(colours["term12"], 4, 12) + + hex_to_ansi(colours["term13"], 4, 13) + + hex_to_ansi(colours["term14"], 4, 14) + + hex_to_ansi(colours["term15"], 4, 15) + + hex_to_ansi(colours["primary"], 4, 16) + + hex_to_ansi(colours["secondary"], 4, 17) + + hex_to_ansi(colours["tertiary"], 4, 18) ) diff --git a/src/caelestia/utils/wallpaper.py b/src/caelestia/utils/wallpaper.py index 77dcb38..02d3f4e 100644 --- a/src/caelestia/utils/wallpaper.py +++ b/src/caelestia/utils/wallpaper.py @@ -2,8 +2,10 @@ 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 @@ -11,6 +13,7 @@ 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, @@ -33,7 +36,7 @@ def check_wall(wall: Path, filter_size: tuple[int, int], threshold: float) -> bo return width >= filter_size[0] * threshold and height >= filter_size[1] * threshold -def get_wallpaper() -> str: +def get_wallpaper() -> str | None: try: return wallpaper_path_path.read_text() except IOError: @@ -41,16 +44,16 @@ def get_wallpaper() -> str: def get_wallpapers(args: Namespace) -> list[Path]: - dir = Path(args.random) - if not dir.is_dir(): + directory = Path(args.random) + if not directory.is_dir(): return [] - walls = [f for f in dir.rglob("*") if is_valid_image(f)] + walls = [f for f in directory.rglob("*") if is_valid_image(f)] if args.no_filter: return walls - monitors = message("monitors") + 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)] @@ -62,14 +65,14 @@ def get_thumb(wall: Path, cache: Path) -> Path: if not thumb.exists(): with Image.open(wall) as img: img = img.convert("RGB") - img.thumbnail((128, 128), Image.NEAREST) + 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) -> str: +def get_smart_opts(wall: Path, cache: Path) -> dict: opts_cache = cache / "smart.json" try: @@ -77,15 +80,16 @@ def get_smart_opts(wall: Path, cache: Path) -> str: except (IOError, json.JSONDecodeError): pass - from caelestia.utils.colourfulness import get_variant - 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)) - img.thumbnail((1, 1), Image.LANCZOS) - hct = Hct.from_int(argb_from_rgb(*img.getpixel((0, 0)))) opts["mode"] = "light" if hct.tone > 60 else "dark" opts_cache.parent.mkdir(parents=True, exist_ok=True) @@ -144,7 +148,7 @@ def convert_gif(wall: Path) -> Path: return output_path -def set_wallpaper(wall: Path | str, no_smart: bool) -> None: +def set_wallpaper(wall: Path, no_smart: bool) -> None: # Make path absolute wall = Path(wall).resolve()