refactor: enforce stricter type hints (#91)

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.
This commit is contained in:
Kalagmitan
2026-03-15 19:56:05 +08:00
committed by GitHub
parent c930bd2604
commit b00c601d0a
12 changed files with 160 additions and 131 deletions
+1 -1
View File
@@ -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")
+5 -2
View File
@@ -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", "-"])
+7 -4
View File
@@ -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:
+16 -12
View File
@@ -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)
+2 -3
View File
@@ -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:
+14 -10
View File
@@ -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)
+25 -17
View File
@@ -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"
+1 -1
View File
@@ -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()
+30 -29
View File
@@ -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()
+14 -13
View File
@@ -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"]
+29 -27
View File
@@ -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)
)
+16 -12
View File
@@ -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()