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 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 = 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") parser.add_argument("-v", "--version", action="store_true", help="print the current version")
+5 -2
View File
@@ -26,8 +26,11 @@ class Command:
else: else:
sc_data = subprocess.check_output(["grim", "-l", "0", "-g", self.args.region.strip(), "-"]) 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 = 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: def fullscreen(self) -> None:
sc_data = subprocess.check_output(["grim", "-"]) sc_data = subprocess.check_output(["grim", "-"])
+7 -4
View File
@@ -33,11 +33,14 @@ class Command:
subprocess.run(args) subprocess.run(args)
else: else:
shell = subprocess.Popen(args, stdout=subprocess.PIPE, universal_newlines=True) 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) return subprocess.check_output(["qs", "-c", "caelestia", *args], text=True)
def filter_log(self, line: str) -> bool: def filter_log(self, line: str) -> bool:
+16 -12
View File
@@ -3,6 +3,7 @@ import shlex
import shutil import shutil
from argparse import Namespace from argparse import Namespace
from collections import ChainMap from collections import ChainMap
from typing import Any, Callable, cast
from caelestia.utils import hypr from caelestia.utils import hypr
from caelestia.utils.paths import user_config_path from caelestia.utils.paths import user_config_path
@@ -52,8 +53,8 @@ class DeepChainMap(ChainMap):
class Command: class Command:
args: Namespace args: Namespace
cfg: dict[str, dict[str, dict[str, any]]] | DeepChainMap cfg: dict[str, dict[str, dict[str, Any]]] | DeepChainMap
clients: list[dict[str, any]] = None clients: list[dict[str, Any]] | None = None
def __init__(self, args: Namespace) -> None: def __init__(self, args: Namespace) -> None:
self.args = args self.args = args
@@ -120,27 +121,27 @@ class Command:
if not spawned: if not spawned:
hypr.dispatch("togglespecialworkspace", self.args.workspace) 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: if self.clients is None:
self.clients = hypr.message("clients") self.clients = cast(list[dict[str, Any]], hypr.message("clients"))
return self.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(): for client in self.get_clients():
if selector(client) and client["workspace"]["name"] != f"special:{workspace}": if selector(client) and client["workspace"]["name"] != f"special:{workspace}":
hypr.dispatch("movetoworkspacesilent", f"special:{workspace},address:{client['address']}") 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( if (spawn[0].endswith(".desktop") or shutil.which(spawn[0])) and not any(
selector(client) for client in self.get_clients() selector(client) for client in self.get_clients()
): ):
hypr.dispatch("exec", f"[workspace special:{self.args.workspace}] app2unit -- {shlex.join(spawn)}") hypr.dispatch("exec", f"[workspace special:{self.args.workspace}] app2unit -- {shlex.join(spawn)}")
return True return True
return False else:
return False
def handle_client_config(self, client: dict[str, any]) -> bool: def handle_client_config(self, client: dict[str, Any]) -> bool:
def selector(c: dict[str, any]) -> bool: def selector(c: dict[str, Any]) -> bool:
# Each match is or, inside matches is and # Each match is or, inside matches is and
for match in client["match"]: for match in client["match"]:
if is_subset(c, match): if is_subset(c, match):
@@ -156,5 +157,8 @@ class Command:
return spawned return spawned
def specialws(self) -> None: def specialws(self) -> None:
special = next(m for m in hypr.message("monitors") if m["focused"])["specialWorkspace"]["name"] monitors = cast(list[dict[str, Any]], hypr.message("monitors"))
hypr.dispatch("togglespecialworkspace", special[8:] or "special") 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 return math.sqrt(sum((x - mean_val) ** 2 for x in values) / len(values)) if values else 0
def calc_colourfulness(image: Image) -> float: def calc_colourfulness(image: Image.Image) -> float:
width, height = image.size
pixels = list(image.getdata()) # List of (R, G, B) tuples pixels = list(image.getdata()) # List of (R, G, B) tuples
rg_diffs = [] 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) 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) colourfulness = calc_colourfulness(image)
if colourfulness < 10: if colourfulness < 10:
+14 -10
View File
@@ -1,17 +1,18 @@
import json as j import json
import os import os
import socket import socket
from typing import Any
socket_base = f"{os.getenv('XDG_RUNTIME_DIR')}/hypr/{os.getenv('HYPRLAND_INSTANCE_SIGNATURE')}" socket_base = f"{os.getenv('XDG_RUNTIME_DIR')}/hypr/{os.getenv('HYPRLAND_INSTANCE_SIGNATURE')}"
socket_path = f"{socket_base}/.socket.sock" socket_path = f"{socket_base}/.socket.sock"
socket2_path = f"{socket_base}/.socket2.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: with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
sock.connect(socket_path) sock.connect(socket_path)
if json: if is_json:
msg = f"j/{msg}" msg = f"j/{msg}"
sock.send(msg.encode()) sock.send(msg.encode())
@@ -22,14 +23,17 @@ def message(msg: str, json: bool = True) -> str | dict[str, any]:
break break
resp += new_resp.decode() 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: def dispatch(dispatcher: str, *args: str) -> bool:
return message(f"dispatch {dispatcher} {' '.join(map(str, args))}".rstrip(), json=False) == "ok" 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]: def batch(*msgs: str, is_json: bool = False) -> str | dict[str, Any]:
if json: formatted_msgs = msgs
msgs = (f"j/{m.strip()}" for m in msgs)
return message(f"[[BATCH]]{';'.join(msgs)}", json=False) 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_tonal_spot import SchemeTonalSpot
from materialyoucolor.scheme.scheme_vibrant import SchemeVibrant from materialyoucolor.scheme.scheme_vibrant import SchemeVibrant
from materialyoucolor.utils.math_utils import difference_degrees, rotation_direction, sanitize_degrees_double 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: try:
from materialyoucolor.dynamiccolor.dynamic_scheme import DynamicScheme 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) 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": if scheme == "content":
return SchemeContent return SchemeContent
if scheme == "expressive": if scheme == "expressive":
@@ -168,12 +176,12 @@ def get_scheme(scheme: str) -> DynamicScheme:
def gen_scheme(scheme, primary: Hct) -> dict[str, str]: def gen_scheme(scheme, primary: Hct) -> dict[str, str]:
light = scheme.mode == "light" is_light = scheme.mode == "light"
colours = {} colours = {}
# Material 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 if hasattr(MaterialDynamicColors, "all_colors"): # materialyoucolor-python >= 3.0.0
dyn_colours = MaterialDynamicColors() dyn_colours = MaterialDynamicColors()
for colour in dyn_colours.all_colors: 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"] colours["neutral_variant_paletteKeyColor"] = colours["neutralVariantPaletteKeyColor"]
# Harmonize terminal colours # 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": if scheme.variant == "monochrome":
colours[f"term{i}"] = grayscale(hct, light) colours[f"term{i}"] = grayscale(hct, is_light)
else: else:
colours[f"term{i}"] = harmonize( 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 # 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": if scheme.variant == "monochrome":
colours[colour_names[i]] = grayscale(hct, light) colours[colour_names[i]] = grayscale(hct, is_light)
else: 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 # KColours
for colour in kcolours: for colour in kcolours:
colours[colour["name"]] = harmonize(colour["hct"], colours["primary"], 0.1) colours[colour["name"]] = harmonize(colour["hct"], colours["primary"], 0.1)
colours[f"{colour['name']}Selection"] = harmonize(colour["hct"], colours["onPrimaryFixedVariant"], 0.1) colours[f"{colour['name']}Selection"] = harmonize(colour["hct"], colours["onPrimaryFixedVariant"], 0.1)
if scheme.variant == "monochrome": if scheme.variant == "monochrome":
colours[colour["name"]] = grayscale(colours[colour["name"]], light) colours[colour["name"]] = grayscale(colours[colour["name"]], is_light)
colours[f"{colour['name']}Selection"] = grayscale(colours[f"{colour['name']}Selection"], light) colours[f"{colour['name']}Selection"] = grayscale(colours[f"{colour['name']}Selection"], is_light)
if scheme.variant == "neutral": if scheme.variant == "neutral":
for name, hct in colours.items(): for name, hct in colours.items():
@@ -221,8 +229,8 @@ def gen_scheme(scheme, primary: Hct) -> dict[str, str]:
# Darken surfaces for hard flavour # Darken surfaces for hard flavour
if scheme.flavour == "hard": if scheme.flavour == "hard":
for colour in "background", *(k for k in colours.keys() if k.startswith("surface")): 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[colour] = lighten(colours[colour], 0.4) if is_light else darken(colours[colour], 0.8)
colours["term0"] = lighten(colours["term0"], 0.4) if light else darken(colours["term0"], 0.9) colours["term0"] = lighten(colours["term0"], 0.4) if is_light else darken(colours["term0"], 0.9)
# FIXME: deprecated stuff # FIXME: deprecated stuff
colours["text"] = colours["onBackground"] colours["text"] = colours["onBackground"]
@@ -241,13 +249,13 @@ def gen_scheme(scheme, primary: Hct) -> dict[str, str]:
# More darkening if hard flavour # More darkening if hard flavour
if scheme.flavour == "hard": if scheme.flavour == "hard":
for colour in "base", "mantle", "crust": 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): for i in range(3):
colours[f"overlay{i}"] = ( 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}"] = ( 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 # 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()} colours = {k: hex(v.to_int())[4:] for k, v in colours.items()}
# Extended material # Extended material
if light: if is_light:
colours["success"] = "4F6354" colours["success"] = "4F6354"
colours["onSuccess"] = "FFFFFF" colours["onSuccess"] = "FFFFFF"
colours["successContainer"] = "D1E8D5" colours["successContainer"] = "D1E8D5"
+1 -1
View File
@@ -1,7 +1,7 @@
import subprocess 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() 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 shutil
import tempfile import tempfile
from pathlib import Path from pathlib import Path
from typing import Any
config_dir = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config")) config_dir: Path = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))
data_dir = Path(os.getenv("XDG_DATA_HOME", Path.home() / ".local/share")) data_dir: Path = Path(os.getenv("XDG_DATA_HOME", Path.home() / ".local/share"))
state_dir = Path(os.getenv("XDG_STATE_HOME", Path.home() / ".local/state")) state_dir: Path = Path(os.getenv("XDG_STATE_HOME", Path.home() / ".local/state"))
cache_dir = Path(os.getenv("XDG_CACHE_HOME", Path.home() / ".cache")) cache_dir: Path = Path(os.getenv("XDG_CACHE_HOME", Path.home() / ".cache"))
pictures_dir = Path(os.getenv("XDG_PICTURES_DIR", Path.home() / "Pictures")) pictures_dir: Path = Path(os.getenv("XDG_PICTURES_DIR", Path.home() / "Pictures"))
videos_dir = Path(os.getenv("XDG_VIDEOS_DIR", Path.home() / "Videos")) videos_dir: Path = Path(os.getenv("XDG_VIDEOS_DIR", Path.home() / "Videos"))
c_config_dir = config_dir / "caelestia" c_config_dir: Path = config_dir / "caelestia"
c_data_dir = data_dir / "caelestia" c_data_dir: Path = data_dir / "caelestia"
c_state_dir = state_dir / "caelestia" c_state_dir: Path = state_dir / "caelestia"
c_cache_dir = cache_dir / "caelestia" c_cache_dir: Path = cache_dir / "caelestia"
user_config_path = c_config_dir / "cli.json" user_config_path: Path = c_config_dir / "cli.json"
cli_data_dir = Path(__file__).parent.parent / "data" cli_data_dir: Path = Path(__file__).parent.parent / "data"
templates_dir = cli_data_dir / "templates" templates_dir: Path = cli_data_dir / "templates"
user_templates_dir = c_config_dir / "templates" user_templates_dir: Path = c_config_dir / "templates"
theme_dir = c_state_dir / "theme" theme_dir: Path = c_state_dir / "theme"
scheme_path = c_state_dir / "scheme.json" scheme_path: Path = c_state_dir / "scheme.json"
scheme_data_dir = cli_data_dir / "schemes" scheme_data_dir: Path = cli_data_dir / "schemes"
scheme_cache_dir = c_cache_dir / "schemes" scheme_cache_dir: Path = c_cache_dir / "schemes"
wallpapers_dir = os.getenv("CAELESTIA_WALLPAPERS_DIR", pictures_dir / "Wallpapers") wallpapers_dir: Path = Path(os.getenv("CAELESTIA_WALLPAPERS_DIR", pictures_dir / "Wallpapers"))
wallpaper_path_path = c_state_dir / "wallpaper/path.txt" wallpaper_path_path: Path = c_state_dir / "wallpaper/path.txt"
wallpaper_link_path = c_state_dir / "wallpaper/current" wallpaper_link_path: Path = c_state_dir / "wallpaper/current"
wallpaper_thumbnail_path = c_state_dir / "wallpaper/thumbnail.jpg" wallpaper_thumbnail_path: Path = c_state_dir / "wallpaper/thumbnail.jpg"
wallpapers_cache_dir = c_cache_dir / "wallpapers" wallpapers_cache_dir: Path = c_cache_dir / "wallpapers"
screenshots_dir = os.getenv("CAELESTIA_SCREENSHOTS_DIR", pictures_dir / "Screenshots") screenshots_dir: Path = Path(os.getenv("CAELESTIA_SCREENSHOTS_DIR", pictures_dir / "Screenshots"))
screenshots_cache_dir = c_cache_dir / "screenshots" screenshots_cache_dir: Path = c_cache_dir / "screenshots"
recordings_dir = os.getenv("CAELESTIA_RECORDINGS_DIR", videos_dir / "Recordings") recordings_dir: Path = Path(os.getenv("CAELESTIA_RECORDINGS_DIR", videos_dir / "Recordings"))
recording_path = c_state_dir / "record/recording.mp4" recording_path: Path = c_state_dir / "record/recording.mp4"
recording_notif_path = c_state_dir / "record/notifid.txt" recording_notif_path: Path = c_state_dir / "record/notifid.txt"
def compute_hash(path: Path | str) -> str: def compute_hash(path: Path | str) -> str:
@@ -51,7 +52,7 @@ def compute_hash(path: Path | str) -> str:
return sha.hexdigest() 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: with tempfile.NamedTemporaryFile("w") as f:
json.dump(content, f) json.dump(content, f)
f.flush() f.flush()
+14 -13
View File
@@ -1,6 +1,7 @@
import json import json
import random import random
from pathlib import Path from pathlib import Path
from typing import Any
from caelestia.utils.notify import notify from caelestia.utils.notify import notify
from caelestia.utils.paths import atomic_dump, scheme_data_dir, scheme_path from caelestia.utils.paths import atomic_dump, scheme_data_dir, scheme_path
@@ -14,19 +15,19 @@ class Scheme:
_colours: dict[str, str] _colours: dict[str, str]
notify: bool notify: bool
def __init__(self, json: dict[str, any] | None) -> None: def __init__(self, scheme_json: dict[str, Any] | None) -> None:
if json is None: if scheme_json is None:
self._name = "catppuccin" self._name = "catppuccin"
self._flavour = "mocha" self._flavour = "mocha"
self._mode = "dark" self._mode = "dark"
self._variant = "tonalspot" self._variant = "tonalspot"
self._colours = read_colours_from_file(self.get_colours_path()) self._colours = read_colours_from_file(self.get_colours_path())
else: else:
self._name = json["name"] self._name = scheme_json["name"]
self._flavour = json["flavour"] self._flavour = scheme_json["flavour"]
self._mode = json["mode"] self._mode = scheme_json["mode"]
self._variant = json["variant"] self._variant = scheme_json["variant"]
self._colours = json["colours"] self._colours = scheme_json["colours"]
self.notify = False self.notify = False
@property @property
@@ -196,7 +197,7 @@ scheme_variants = [
"content", "content",
] ]
scheme: Scheme = None scheme: Scheme | None = None
def read_colours_from_file(path: Path) -> dict[str, str]: 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"] 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: if name is None:
name = get_scheme().name 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]: def get_scheme_modes(name: str | None = None, flavour: str | None = None) -> list[str]:
if name is None: if name is None or flavour is None:
scheme = get_scheme() scheme = get_scheme()
name = scheme.name name = name or scheme.name
flavour = scheme.flavour flavour = flavour or scheme.flavour
if name == "dynamic": if name == "dynamic":
return ["light", "dark"] return ["light", "dark"]
+29 -27
View File
@@ -4,6 +4,8 @@ import re
import shutil import shutil
import subprocess import subprocess
import tempfile import tempfile
import shutil
import fcntl
from pathlib import Path from pathlib import Path
from caelestia.utils.colour import get_dynamic_colours 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: 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(): for name, colour in colours.items():
template = template.replace(f"{{{{ ${name} }}}}", f"#{colour}" if hash else colour) new_template = new_template.replace(f"{{{{ ${name} }}}}", f"#{colour}" if hash else colour)
return template return new_template
def gen_replace_dynamic(colours: dict[str, str], template: Path, mode: str) -> str: 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 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\\)""" """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\\" 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 16+: 256 colours
""" """
return ( return (
c2s(colours["onSurface"], 10) hex_to_ansi(colours["onSurface"], 10)
+ c2s(colours["surface"], 11) + hex_to_ansi(colours["surface"], 11)
+ c2s(colours["secondary"], 12) + hex_to_ansi(colours["secondary"], 12)
+ c2s(colours["secondary"], 17) + hex_to_ansi(colours["secondary"], 17)
+ c2s(colours["term0"], 4, 0) + hex_to_ansi(colours["term0"], 4, 0)
+ c2s(colours["term1"], 4, 1) + hex_to_ansi(colours["term1"], 4, 1)
+ c2s(colours["term2"], 4, 2) + hex_to_ansi(colours["term2"], 4, 2)
+ c2s(colours["term3"], 4, 3) + hex_to_ansi(colours["term3"], 4, 3)
+ c2s(colours["term4"], 4, 4) + hex_to_ansi(colours["term4"], 4, 4)
+ c2s(colours["term5"], 4, 5) + hex_to_ansi(colours["term5"], 4, 5)
+ c2s(colours["term6"], 4, 6) + hex_to_ansi(colours["term6"], 4, 6)
+ c2s(colours["term7"], 4, 7) + hex_to_ansi(colours["term7"], 4, 7)
+ c2s(colours["term8"], 4, 8) + hex_to_ansi(colours["term8"], 4, 8)
+ c2s(colours["term9"], 4, 9) + hex_to_ansi(colours["term9"], 4, 9)
+ c2s(colours["term10"], 4, 10) + hex_to_ansi(colours["term10"], 4, 10)
+ c2s(colours["term11"], 4, 11) + hex_to_ansi(colours["term11"], 4, 11)
+ c2s(colours["term12"], 4, 12) + hex_to_ansi(colours["term12"], 4, 12)
+ c2s(colours["term13"], 4, 13) + hex_to_ansi(colours["term13"], 4, 13)
+ c2s(colours["term14"], 4, 14) + hex_to_ansi(colours["term14"], 4, 14)
+ c2s(colours["term15"], 4, 15) + hex_to_ansi(colours["term15"], 4, 15)
+ c2s(colours["primary"], 4, 16) + hex_to_ansi(colours["primary"], 4, 16)
+ c2s(colours["secondary"], 4, 17) + hex_to_ansi(colours["secondary"], 4, 17)
+ c2s(colours["tertiary"], 4, 18) + hex_to_ansi(colours["tertiary"], 4, 18)
) )
+16 -12
View File
@@ -2,8 +2,10 @@ import json
import os import os
import random import random
import subprocess import subprocess
from argparse import Namespace from argparse import Namespace
from pathlib import Path from pathlib import Path
from typing import cast
from materialyoucolor.hct import Hct from materialyoucolor.hct import Hct
from materialyoucolor.utils.color_utils import argb_from_rgb 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.hypr import message
from caelestia.utils.material import get_colours_for_image from caelestia.utils.material import get_colours_for_image
from caelestia.utils.colourfulness import get_variant
from caelestia.utils.paths import ( from caelestia.utils.paths import (
compute_hash, compute_hash,
user_config_path, 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 return width >= filter_size[0] * threshold and height >= filter_size[1] * threshold
def get_wallpaper() -> str: def get_wallpaper() -> str | None:
try: try:
return wallpaper_path_path.read_text() return wallpaper_path_path.read_text()
except IOError: except IOError:
@@ -41,16 +44,16 @@ def get_wallpaper() -> str:
def get_wallpapers(args: Namespace) -> list[Path]: def get_wallpapers(args: Namespace) -> list[Path]:
dir = Path(args.random) directory = Path(args.random)
if not dir.is_dir(): if not directory.is_dir():
return [] 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: if args.no_filter:
return walls 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) 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)] 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(): if not thumb.exists():
with Image.open(wall) as img: with Image.open(wall) as img:
img = img.convert("RGB") 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) thumb.parent.mkdir(parents=True, exist_ok=True)
img.save(thumb, "JPEG") img.save(thumb, "JPEG")
return thumb 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" opts_cache = cache / "smart.json"
try: try:
@@ -77,15 +80,16 @@ def get_smart_opts(wall: Path, cache: Path) -> str:
except (IOError, json.JSONDecodeError): except (IOError, json.JSONDecodeError):
pass pass
from caelestia.utils.colourfulness import get_variant
opts = {} opts = {}
with Image.open(get_thumb(wall, cache)) as img: with Image.open(get_thumb(wall, cache)) as img:
opts["variant"] = get_variant(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["mode"] = "light" if hct.tone > 60 else "dark"
opts_cache.parent.mkdir(parents=True, exist_ok=True) opts_cache.parent.mkdir(parents=True, exist_ok=True)
@@ -144,7 +148,7 @@ def convert_gif(wall: Path) -> Path:
return output_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 # Make path absolute
wall = Path(wall).resolve() wall = Path(wall).resolve()