From f663e6f6908a9dfb05ac22e867e726c1bf6f0960 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 11 Jun 2025 00:41:05 +1000 Subject: internal: refactor for packaging Package using python-build, python-installer and hatch --- src/caelestia/subcommands/clipboard.py | 11 +++++ src/caelestia/subcommands/emoji.py | 11 +++++ src/caelestia/subcommands/pip.py | 11 +++++ src/caelestia/subcommands/record.py | 11 +++++ src/caelestia/subcommands/scheme.py | 11 +++++ src/caelestia/subcommands/screenshot.py | 11 +++++ src/caelestia/subcommands/shell.py | 41 +++++++++++++++++ src/caelestia/subcommands/toggle.py | 82 +++++++++++++++++++++++++++++++++ src/caelestia/subcommands/variant.py | 11 +++++ src/caelestia/subcommands/wallpaper.py | 11 +++++ src/caelestia/subcommands/wsaction.py | 18 ++++++++ 11 files changed, 229 insertions(+) create mode 100644 src/caelestia/subcommands/clipboard.py create mode 100644 src/caelestia/subcommands/emoji.py create mode 100644 src/caelestia/subcommands/pip.py create mode 100644 src/caelestia/subcommands/record.py create mode 100644 src/caelestia/subcommands/scheme.py create mode 100644 src/caelestia/subcommands/screenshot.py create mode 100644 src/caelestia/subcommands/shell.py create mode 100644 src/caelestia/subcommands/toggle.py create mode 100644 src/caelestia/subcommands/variant.py create mode 100644 src/caelestia/subcommands/wallpaper.py create mode 100644 src/caelestia/subcommands/wsaction.py (limited to 'src/caelestia/subcommands') diff --git a/src/caelestia/subcommands/clipboard.py b/src/caelestia/subcommands/clipboard.py new file mode 100644 index 0000000..37f9a2b --- /dev/null +++ b/src/caelestia/subcommands/clipboard.py @@ -0,0 +1,11 @@ +from argparse import Namespace + + +class Command: + args: Namespace + + def __init__(self, args: Namespace) -> None: + self.args = args + + def run(self) -> None: + pass diff --git a/src/caelestia/subcommands/emoji.py b/src/caelestia/subcommands/emoji.py new file mode 100644 index 0000000..37f9a2b --- /dev/null +++ b/src/caelestia/subcommands/emoji.py @@ -0,0 +1,11 @@ +from argparse import Namespace + + +class Command: + args: Namespace + + def __init__(self, args: Namespace) -> None: + self.args = args + + def run(self) -> None: + pass diff --git a/src/caelestia/subcommands/pip.py b/src/caelestia/subcommands/pip.py new file mode 100644 index 0000000..37f9a2b --- /dev/null +++ b/src/caelestia/subcommands/pip.py @@ -0,0 +1,11 @@ +from argparse import Namespace + + +class Command: + args: Namespace + + def __init__(self, args: Namespace) -> None: + self.args = args + + def run(self) -> None: + pass diff --git a/src/caelestia/subcommands/record.py b/src/caelestia/subcommands/record.py new file mode 100644 index 0000000..37f9a2b --- /dev/null +++ b/src/caelestia/subcommands/record.py @@ -0,0 +1,11 @@ +from argparse import Namespace + + +class Command: + args: Namespace + + def __init__(self, args: Namespace) -> None: + self.args = args + + def run(self) -> None: + pass diff --git a/src/caelestia/subcommands/scheme.py b/src/caelestia/subcommands/scheme.py new file mode 100644 index 0000000..37f9a2b --- /dev/null +++ b/src/caelestia/subcommands/scheme.py @@ -0,0 +1,11 @@ +from argparse import Namespace + + +class Command: + args: Namespace + + def __init__(self, args: Namespace) -> None: + self.args = args + + def run(self) -> None: + pass diff --git a/src/caelestia/subcommands/screenshot.py b/src/caelestia/subcommands/screenshot.py new file mode 100644 index 0000000..37f9a2b --- /dev/null +++ b/src/caelestia/subcommands/screenshot.py @@ -0,0 +1,11 @@ +from argparse import Namespace + + +class Command: + args: Namespace + + def __init__(self, args: Namespace) -> None: + self.args = args + + def run(self) -> None: + pass diff --git a/src/caelestia/subcommands/shell.py b/src/caelestia/subcommands/shell.py new file mode 100644 index 0000000..2d8d14e --- /dev/null +++ b/src/caelestia/subcommands/shell.py @@ -0,0 +1,41 @@ +import subprocess +from argparse import Namespace + +from caelestia import data + + +class Command: + args: Namespace + + def __init__(self, args: Namespace) -> None: + self.args = args + + def run(self) -> None: + if self.args.show: + # Print the ipc + self.print_ipc() + elif self.args.log: + # Print the log + self.print_log() + elif self.args.message: + # Send a message + self.message(*self.args.message) + else: + # Start the shell + self.shell() + + def shell(self, *args: list[str]) -> str: + return subprocess.check_output(["qs", "-p", data.c_data_dir / "shell", *args], text=True) + + def print_ipc(self) -> None: + print(self.shell("ipc", "show"), end="") + + def print_log(self) -> None: + log = self.shell("log") + # FIXME: remove when logging rules are added/warning is removed + for line in log.splitlines(): + if "QProcess: Destroyed while process" not in line: + print(line) + + def message(self, *args: list[str]) -> None: + print(self.shell("ipc", "call", *args), end="") diff --git a/src/caelestia/subcommands/toggle.py b/src/caelestia/subcommands/toggle.py new file mode 100644 index 0000000..2122910 --- /dev/null +++ b/src/caelestia/subcommands/toggle.py @@ -0,0 +1,82 @@ +from argparse import Namespace + +from caelestia.utils import hypr + + +class Command: + args: Namespace + clients: list[dict[str, any]] = None + app2unit: str = None + + def __init__(self, args: Namespace) -> None: + self.args = args + + def run(self) -> None: + getattr(self, self.args.workspace)() + + def get_clients(self) -> list[dict[str, any]]: + if self.clients is None: + self.clients = hypr.message("clients") + + return self.clients + + def get_app2unit(self) -> str: + if self.app2unit is None: + import shutil + + self.app2unit = shutil.which("app2unit") + + return self.app2unit + + def move_client(self, selector: callable, workspace: str) -> None: + for client in self.get_clients(): + if selector(client): + hypr.dispatch("movetoworkspacesilent", f"special:{workspace},address:{client['address']}") + + def spawn_client(self, selector: callable, spawn: list[str]) -> bool: + exists = any(selector(client) for client in self.get_clients()) + + if not exists: + import subprocess + + subprocess.Popen([self.get_app2unit(), "--", *spawn], start_new_session=True) + + return not exists + + def spawn_or_move(self, selector: callable, spawn: list[str], workspace: str) -> None: + if not self.spawn_client(selector, spawn): + self.move_client(selector, workspace) + + def communication(self) -> None: + self.spawn_or_move(lambda c: c["class"] == "discord", ["discord"], "communication") + self.move_client(lambda c: c["class"] == "whatsapp", "communication") + + def music(self) -> None: + self.spawn_or_move( + lambda c: c["class"] == "Spotify" or c["initialTitle"] == "Spotify" or c["initialTitle"] == "Spotify Free", + ["spicetify", "watch", "-s"], + "music", + ) + self.move_client(lambda c: c["class"] == "feishin", "music") + + def sysmon(self) -> None: + self.spawn_client( + lambda c: c["class"] == "btop" and c["title"] == "btop" and c["workspace"]["name"] == "special:sysmon", + ["foot", "-a", "btop", "-T", "btop", "--", "btop"], + "sysmon", + ) + + def todo(self) -> None: + self.spawn_or_move(lambda c: c["class"] == "Todoist", ["todoist"], "todo") + + def specialws(self) -> None: + workspaces = hypr.message("workspaces") + on_special_ws = any(ws["name"] == "special:special" for ws in workspaces) + toggle_ws = "special" + + if not on_special_ws: + active_ws = hypr.message("activewindow")["workspace"]["name"] + if active_ws.startswith("special:"): + toggle_ws = active_ws[8:] + + hypr.dispatch("togglespecialworkspace", toggle_ws) diff --git a/src/caelestia/subcommands/variant.py b/src/caelestia/subcommands/variant.py new file mode 100644 index 0000000..37f9a2b --- /dev/null +++ b/src/caelestia/subcommands/variant.py @@ -0,0 +1,11 @@ +from argparse import Namespace + + +class Command: + args: Namespace + + def __init__(self, args: Namespace) -> None: + self.args = args + + def run(self) -> None: + pass diff --git a/src/caelestia/subcommands/wallpaper.py b/src/caelestia/subcommands/wallpaper.py new file mode 100644 index 0000000..37f9a2b --- /dev/null +++ b/src/caelestia/subcommands/wallpaper.py @@ -0,0 +1,11 @@ +from argparse import Namespace + + +class Command: + args: Namespace + + def __init__(self, args: Namespace) -> None: + self.args = args + + def run(self) -> None: + pass diff --git a/src/caelestia/subcommands/wsaction.py b/src/caelestia/subcommands/wsaction.py new file mode 100644 index 0000000..d496381 --- /dev/null +++ b/src/caelestia/subcommands/wsaction.py @@ -0,0 +1,18 @@ +from argparse import Namespace + +from caelestia.utils import hypr + + +class Command: + args: Namespace + + def __init__(self, args: Namespace) -> None: + self.args = args + + def run(self) -> None: + active_ws = hypr.message("activeworkspace")["id"] + + if self.args.group: + hypr.dispatch(self.args.dispatcher, (self.args.workspace - 1) * 10 + active_ws % 10) + else: + hypr.dispatch(self.args.dispatcher, int((active_ws - 1) / 10) * 10 + self.args.workspace) -- cgit v1.2.3-freya From dc855e1b01e9b4526927b8bd69daca733afd97c2 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 11 Jun 2025 17:37:04 +1000 Subject: internal: refactor scheme Also use a single file to store scheme data --- src/caelestia/data.py | 120 ----------------------- src/caelestia/parser.py | 2 +- src/caelestia/subcommands/shell.py | 4 +- src/caelestia/utils/hypr.py | 2 +- src/caelestia/utils/paths.py | 13 +++ src/caelestia/utils/scheme.py | 194 +++++++++++++++++++++++++++++++++++++ 6 files changed, 211 insertions(+), 124 deletions(-) delete mode 100644 src/caelestia/data.py create mode 100644 src/caelestia/utils/paths.py (limited to 'src/caelestia/subcommands') diff --git a/src/caelestia/data.py b/src/caelestia/data.py deleted file mode 100644 index fa97a03..0000000 --- a/src/caelestia/data.py +++ /dev/null @@ -1,120 +0,0 @@ -import os -from pathlib import Path - -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")) - -c_config_dir = config_dir / "caelestia" -c_data_dir = data_dir / "caelestia" -c_state_dir = state_dir / "caelestia" - -scheme_name_path = c_state_dir / "scheme/name.txt" -scheme_flavour_path = c_state_dir / "scheme/flavour.txt" -scheme_colours_path = c_state_dir / "scheme/colours.txt" -scheme_mode_path = c_state_dir / "scheme/mode.txt" -scheme_variant_path = c_state_dir / "scheme/variant.txt" - -scheme_data_path = Path(__file__).parent / "data/schemes" - -scheme_variants = [ - "tonalspot", - "vibrant", - "expressive", - "fidelity", - "fruitsalad", - "monochrome", - "neutral", - "rainbow", - "content", -] - -scheme_names: list[str] = None -scheme_flavours: list[str] = None -scheme_modes: list[str] = None - -scheme_name: str = None -scheme_flavour: str = None -scheme_colours: dict[str, str] = None -scheme_mode: str = None -scheme_variant: str = None - - -def get_scheme_path() -> Path: - return (scheme_data_path / get_scheme_name() / get_scheme_flavour() / get_scheme_mode()).with_suffix(".txt") - - -def get_scheme_names() -> list[str]: - global scheme_names - - if scheme_names is None: - scheme_names = [f.name for f in scheme_data_path.iterdir() if f.is_dir()] - - return scheme_names - - -def get_scheme_flavours() -> list[str]: - global scheme_flavours - - if scheme_flavours is None: - scheme_flavours = [f.name for f in (scheme_data_path / get_scheme_name()).iterdir() if f.is_dir()] - - return scheme_flavours - - -def get_scheme_modes() -> list[str]: - global scheme_modes - - if scheme_modes is None: - scheme_modes = [ - f.stem for f in (scheme_data_path / get_scheme_name() / get_scheme_flavour()).iterdir() if f.is_file() - ] - - return scheme_modes - - -def get_scheme_name() -> str: - global scheme_name - - if scheme_name is None: - scheme_name = scheme_name_path.read_text().strip() if scheme_name_path.exists() else "catppuccin" - - return scheme_name - - -def get_scheme_flavour() -> str: - global scheme_flavour - - if scheme_flavour is None: - scheme_flavour = scheme_flavour_path.read_text().strip() if scheme_flavour_path.exists() else "mocha" - - return scheme_flavour - - -def get_scheme_colours() -> dict[str, str]: - global scheme_colours - - if scheme_colours is None: - scheme_colours = { - k.strip(): v.strip() for k, v in (line.split(" ") for line in get_scheme_path().read_text().splitlines()) - } - - return scheme_colours - - -def get_scheme_mode() -> str: - global scheme_mode - - if scheme_mode is None: - scheme_mode = scheme_mode_path.read_text().strip() if scheme_mode_path.exists() else "dark" - - return scheme_mode - - -def get_scheme_variant() -> str: - global scheme_variant - - if scheme_variant is None: - scheme_variant = scheme_variant_path.read_text().strip() if scheme_variant_path.exists() else "tonalspot" - - return scheme_variant diff --git a/src/caelestia/parser.py b/src/caelestia/parser.py index 00556b0..eb8734e 100644 --- a/src/caelestia/parser.py +++ b/src/caelestia/parser.py @@ -1,6 +1,5 @@ import argparse -from caelestia.data import get_scheme_names, scheme_variants from caelestia.subcommands import ( clipboard, emoji, @@ -14,6 +13,7 @@ from caelestia.subcommands import ( wallpaper, wsaction, ) +from caelestia.utils.scheme import get_scheme_names, scheme_variants def parse_args() -> (argparse.ArgumentParser, argparse.Namespace): diff --git a/src/caelestia/subcommands/shell.py b/src/caelestia/subcommands/shell.py index 2d8d14e..25a39d8 100644 --- a/src/caelestia/subcommands/shell.py +++ b/src/caelestia/subcommands/shell.py @@ -1,7 +1,7 @@ import subprocess from argparse import Namespace -from caelestia import data +from caelestia.utils import paths class Command: @@ -25,7 +25,7 @@ class Command: self.shell() def shell(self, *args: list[str]) -> str: - return subprocess.check_output(["qs", "-p", data.c_data_dir / "shell", *args], text=True) + return subprocess.check_output(["qs", "-p", paths.c_data_dir / "shell", *args], text=True) def print_ipc(self) -> None: print(self.shell("ipc", "show"), end="") diff --git a/src/caelestia/utils/hypr.py b/src/caelestia/utils/hypr.py index d829f22..621e28e 100644 --- a/src/caelestia/utils/hypr.py +++ b/src/caelestia/utils/hypr.py @@ -5,7 +5,7 @@ import socket socket_path = f"{os.getenv('XDG_RUNTIME_DIR')}/hypr/{os.getenv('HYPRLAND_INSTANCE_SIGNATURE')}/.socket.sock" -def message(msg: str, json: bool = True) -> str or dict[str, any]: +def message(msg: str, json: bool = True) -> str | dict[str, any]: with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: sock.connect(socket_path) diff --git a/src/caelestia/utils/paths.py b/src/caelestia/utils/paths.py new file mode 100644 index 0000000..aff6ea0 --- /dev/null +++ b/src/caelestia/utils/paths.py @@ -0,0 +1,13 @@ +import os +from pathlib import Path + +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")) + +c_config_dir = config_dir / "caelestia" +c_data_dir = data_dir / "caelestia" +c_state_dir = state_dir / "caelestia" + +scheme_path = c_state_dir / "scheme.json" +scheme_data_path = Path(__file__).parent.parent / "data/schemes" diff --git a/src/caelestia/utils/scheme.py b/src/caelestia/utils/scheme.py index e69de29..79a0c21 100644 --- a/src/caelestia/utils/scheme.py +++ b/src/caelestia/utils/scheme.py @@ -0,0 +1,194 @@ +import json +from pathlib import Path + +from caelestia.utils.paths import scheme_data_path, scheme_path + + +class Scheme: + _name: str + _flavour: str + _mode: str + _variant: str + _colours: dict[str, str] + + def __init__(self, json: dict[str, any] | None) -> None: + if 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"] + + @property + def name(self) -> str: + return self._name + + @name.setter + def name(self, name: str) -> None: + if name == self._name: + return + + if name not in get_scheme_names(): + raise ValueError(f"Invalid scheme name: {name}") + + self._name = name + self._check_flavour() + self._check_mode() + self._update_colours() + self.save() + + @property + def flavour(self) -> str: + return self._flavour + + @flavour.setter + def flavour(self, flavour: str) -> None: + if flavour == self._flavour: + return + + if flavour not in get_scheme_flavours(): + raise ValueError(f"Invalid scheme flavour: {flavour}") + + self._flavour = flavour + self._check_mode() + self._update_colours() + self.save() + + @property + def mode(self) -> str: + return self._mode + + @mode.setter + def mode(self, mode: str) -> None: + if mode == self._mode: + return + + if mode not in get_scheme_modes(): + raise ValueError(f"Invalid scheme mode: {mode}") + + self._mode = mode + self._update_colours() + self.save() + + @property + def variant(self) -> str: + return self._variant + + @variant.setter + def variant(self, variant: str) -> None: + self._variant = variant + + @property + def colours(self) -> dict[str, str]: + return self._colours + + def get_colours_path(self) -> Path: + return (scheme_data_path / self.name / self.flavour / self.mode).with_suffix(".txt") + + def save(self) -> None: + scheme_path.parent.mkdir(parents=True, exist_ok=True) + with scheme_path.open("w") as f: + json.dump( + { + "name": self.name, + "flavour": self.flavour, + "mode": self.mode, + "variant": self.variant, + "colours": self.colours, + }, + f, + ) + + def _check_flavour(self) -> None: + global scheme_flavours + scheme_flavours = None + if self._flavour not in get_scheme_flavours(): + self._flavour = get_scheme_flavours()[0] + + def _check_mode(self) -> None: + global scheme_modes + scheme_modes = None + if self._mode not in get_scheme_modes(): + self._mode = get_scheme_modes()[0] + + def _update_colours(self) -> None: + self._colours = read_colours_from_file(self.get_colours_path()) + + +scheme_variants = [ + "tonalspot", + "vibrant", + "expressive", + "fidelity", + "fruitsalad", + "monochrome", + "neutral", + "rainbow", + "content", +] + +scheme_names: list[str] = None +scheme_flavours: list[str] = None +scheme_modes: list[str] = None + +scheme: Scheme = None + + +def read_colours_from_file(path: Path) -> dict[str, str]: + return {k.strip(): v.strip() for k, v in (line.split(" ") for line in path.read_text().splitlines())} + + +def get_scheme_path() -> Path: + return get_scheme().get_colours_path() + + +def get_scheme() -> Scheme: + global scheme + + if scheme is None: + try: + scheme_json = json.loads(scheme_path.read_text()) + scheme = Scheme(scheme_json) + except (IOError, json.JSONDecodeError): + scheme = Scheme(None) + + return scheme + + +def get_scheme_names() -> list[str]: + global scheme_names + + if scheme_names is None: + scheme_names = [f.name for f in scheme_data_path.iterdir() if f.is_dir()] + scheme_names.append("dynamic") + + return scheme_names + + +def get_scheme_flavours() -> list[str]: + global scheme_flavours + + if scheme_flavours is None: + name = get_scheme().name + if name == "dynamic": + scheme_flavours = ["default", "alt1", "alt2"] + else: + scheme_flavours = [f.name for f in (scheme_data_path / name).iterdir() if f.is_dir()] + + return scheme_flavours + + +def get_scheme_modes() -> list[str]: + global scheme_modes + + if scheme_modes is None: + scheme = get_scheme() + scheme_modes = [f.stem for f in (scheme_data_path / scheme.name / scheme.flavour).iterdir() if f.is_file()] + + return scheme_modes -- cgit v1.2.3-freya From f43987ef2f55ede746c5cc37567f5e74ba515fb3 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 11 Jun 2025 18:07:34 +1000 Subject: feat: impl scheme command (partial) --- src/caelestia/parser.py | 1 - src/caelestia/subcommands/scheme.py | 16 +++++++++++++++- src/caelestia/utils/scheme.py | 3 +++ 3 files changed, 18 insertions(+), 2 deletions(-) (limited to 'src/caelestia/subcommands') diff --git a/src/caelestia/parser.py b/src/caelestia/parser.py index eb8734e..9718938 100644 --- a/src/caelestia/parser.py +++ b/src/caelestia/parser.py @@ -61,7 +61,6 @@ def parse_args() -> (argparse.ArgumentParser, argparse.Namespace): # Create parser for scheme opts scheme_parser = command_parser.add_parser("scheme", help="manage the colour scheme") scheme_parser.set_defaults(cls=scheme.Command) - scheme_parser.add_argument("-g", "--get", action="store_true", help="print the current scheme") scheme_parser.add_argument("-r", "--random", action="store_true", help="switch to a random scheme") scheme_parser.add_argument("-n", "--name", choices=get_scheme_names(), help="the name of the scheme to switch to") scheme_parser.add_argument("-f", "--flavour", help="the flavour to switch to") diff --git a/src/caelestia/subcommands/scheme.py b/src/caelestia/subcommands/scheme.py index 37f9a2b..19e62db 100644 --- a/src/caelestia/subcommands/scheme.py +++ b/src/caelestia/subcommands/scheme.py @@ -1,5 +1,7 @@ from argparse import Namespace +from caelestia.utils.scheme import get_scheme + class Command: args: Namespace @@ -8,4 +10,16 @@ class Command: self.args = args def run(self) -> None: - pass + scheme = get_scheme() + + if self.args.random: + scheme.set_random() + elif self.args.name or self.args.flavour or self.args.mode: + if self.args.name: + scheme.name = self.args.name + if self.args.flavour: + scheme.flavour = self.args.flavour + if self.args.mode: + scheme.mode = self.args.mode + else: + print(scheme) diff --git a/src/caelestia/utils/scheme.py b/src/caelestia/utils/scheme.py index 79a0c21..4b05100 100644 --- a/src/caelestia/utils/scheme.py +++ b/src/caelestia/utils/scheme.py @@ -120,6 +120,9 @@ class Scheme: def _update_colours(self) -> None: self._colours = read_colours_from_file(self.get_colours_path()) + def __str__(self) -> str: + return f"Scheme(name={self.name}, flavour={self.flavour}, mode={self.mode}, variant={self.variant})" + scheme_variants = [ "tonalspot", -- cgit v1.2.3-freya From d44bde166782928db85242e86df6190e9bcfa92a Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 11 Jun 2025 20:37:37 +1000 Subject: feat: theme hypr and terminals --- src/caelestia/subcommands/scheme.py | 2 + src/caelestia/utils/hypr.py | 2 +- src/caelestia/utils/theme.py | 86 +++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 src/caelestia/utils/theme.py (limited to 'src/caelestia/subcommands') diff --git a/src/caelestia/subcommands/scheme.py b/src/caelestia/subcommands/scheme.py index 19e62db..fa1f49a 100644 --- a/src/caelestia/subcommands/scheme.py +++ b/src/caelestia/subcommands/scheme.py @@ -1,6 +1,7 @@ from argparse import Namespace from caelestia.utils.scheme import get_scheme +from caelestia.utils.theme import apply_colours class Command: @@ -21,5 +22,6 @@ class Command: scheme.flavour = self.args.flavour if self.args.mode: scheme.mode = self.args.mode + apply_colours(scheme.colours) else: print(scheme) diff --git a/src/caelestia/utils/hypr.py b/src/caelestia/utils/hypr.py index 621e28e..3ba89cf 100644 --- a/src/caelestia/utils/hypr.py +++ b/src/caelestia/utils/hypr.py @@ -24,4 +24,4 @@ def message(msg: str, json: bool = True) -> str | dict[str, any]: def dispatch(dispatcher: str, *args: list[any]) -> bool: - return message(f"dispatch {dispatcher} {' '.join(str(a) for a in args)}".rstrip(), json=False) == "ok" + return message(f"dispatch {dispatcher} {' '.join(map(str, args))}".rstrip(), json=False) == "ok" diff --git a/src/caelestia/utils/theme.py b/src/caelestia/utils/theme.py new file mode 100644 index 0000000..7774472 --- /dev/null +++ b/src/caelestia/utils/theme.py @@ -0,0 +1,86 @@ +import json +import subprocess +from pathlib import Path + +from caelestia.utils.paths import config_dir + + +def gen_conf(colours: dict[str, str]) -> str: + conf = "" + for name, colour in colours.items(): + conf += f"${name} = {colour}\n" + return conf + + +def gen_scss(colours: dict[str, str]) -> str: + scss = "" + for name, colour in colours.items(): + scss += f"${name}: {colour};\n" + return scss + + +def c2s(c: str, *i: list[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\\" + + +def gen_sequences(colours: dict[str, str]) -> str: + """ + 10: foreground + 11: background + 12: cursor + 17: selection + 4: + 0 - 7: normal colours + 8 - 15: bright colours + 16+: 256 colours + """ + return ( + c2s(colours["onSurface"], 10) + + c2s(colours["surface"], 11) + + c2s(colours["secondary"], 12) + + c2s(colours["secondary"], 17) + + c2s(colours["surfaceContainer"], 4, 0) + + c2s(colours["red"], 4, 1) + + c2s(colours["green"], 4, 2) + + c2s(colours["yellow"], 4, 3) + + c2s(colours["blue"], 4, 4) + + c2s(colours["pink"], 4, 5) + + c2s(colours["teal"], 4, 6) + + c2s(colours["onSurfaceVariant"], 4, 7) + + c2s(colours["surfaceContainer"], 4, 8) + + c2s(colours["red"], 4, 9) + + c2s(colours["green"], 4, 10) + + c2s(colours["yellow"], 4, 11) + + c2s(colours["blue"], 4, 12) + + c2s(colours["pink"], 4, 13) + + c2s(colours["teal"], 4, 14) + + c2s(colours["onSurfaceVariant"], 4, 15) + + c2s(colours["primary"], 4, 16) + + c2s(colours["secondary"], 4, 17) + + c2s(colours["tertiary"], 4, 18) + ) + + +def try_write(path: Path, content: str) -> None: + try: + path.write_text(content) + except FileNotFoundError: + pass + + +def apply_terms(sequences: str) -> None: + pts_path = Path("/dev/pts") + for pt in pts_path.iterdir(): + if pt.name.isdigit(): + with pt.open("a") as f: + f.write(sequences) + + +def apply_hypr(conf: str) -> None: + try_write(config_dir / "hypr/scheme/current.conf", conf) + + +def apply_colours(colours: dict[str, str]) -> None: + apply_terms(gen_sequences(colours)) + apply_hypr(gen_conf(colours)) -- cgit v1.2.3-freya From 3fa4a5f7b74a8e05a5cc0fdfaa2fc85f071a4dbe Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 11 Jun 2025 20:42:33 +1000 Subject: toggles: fix sysmon + not toggling ws --- src/caelestia/subcommands/toggle.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src/caelestia/subcommands') diff --git a/src/caelestia/subcommands/toggle.py b/src/caelestia/subcommands/toggle.py index 2122910..fd49c30 100644 --- a/src/caelestia/subcommands/toggle.py +++ b/src/caelestia/subcommands/toggle.py @@ -50,6 +50,7 @@ class Command: def communication(self) -> None: self.spawn_or_move(lambda c: c["class"] == "discord", ["discord"], "communication") self.move_client(lambda c: c["class"] == "whatsapp", "communication") + hypr.dispatch("togglespecialworkspace", "communication") def music(self) -> None: self.spawn_or_move( @@ -58,16 +59,18 @@ class Command: "music", ) self.move_client(lambda c: c["class"] == "feishin", "music") + hypr.dispatch("togglespecialworkspace", "music") def sysmon(self) -> None: self.spawn_client( lambda c: c["class"] == "btop" and c["title"] == "btop" and c["workspace"]["name"] == "special:sysmon", ["foot", "-a", "btop", "-T", "btop", "--", "btop"], - "sysmon", ) + hypr.dispatch("togglespecialworkspace", "sysmon") def todo(self) -> None: self.spawn_or_move(lambda c: c["class"] == "Todoist", ["todoist"], "todo") + hypr.dispatch("togglespecialworkspace", "todo") def specialws(self) -> None: workspaces = hypr.message("workspaces") -- cgit v1.2.3-freya From 2161e3ee6b61c0ebd56142ccd9502404d3ee1314 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 11 Jun 2025 22:48:09 +1000 Subject: theme: better spicetify colours --- src/caelestia/data/templates/spicetify-dark.ini | 19 +++++++++++++++++++ src/caelestia/data/templates/spicetify-light.ini | 19 +++++++++++++++++++ src/caelestia/data/templates/spicetify.ini | 17 ----------------- src/caelestia/subcommands/scheme.py | 2 +- src/caelestia/utils/theme.py | 8 ++++---- 5 files changed, 43 insertions(+), 22 deletions(-) create mode 100644 src/caelestia/data/templates/spicetify-dark.ini create mode 100644 src/caelestia/data/templates/spicetify-light.ini delete mode 100644 src/caelestia/data/templates/spicetify.ini (limited to 'src/caelestia/subcommands') diff --git a/src/caelestia/data/templates/spicetify-dark.ini b/src/caelestia/data/templates/spicetify-dark.ini new file mode 100644 index 0000000..4bf85eb --- /dev/null +++ b/src/caelestia/data/templates/spicetify-dark.ini @@ -0,0 +1,19 @@ +[caelestia] +text = {{ $onSurface }} ; Main text colour +subtext = {{ $onSurfaceVariant }} ; Subtext colour +main = {{ $surfaceContainer }} ; Panel backgrounds +highlight = {{ $primary }} ; Doesn't seem to do anything +misc = {{ $primary }} ; Doesn't seem to do anything +notification = {{ $outline }} ; Notifications probably +notification-error = {{ $error }} ; Error notifications probably +shadow = {{ $shadow }} ; Shadow for covers, context menu, also affects playlist/artist banners +card = {{ $surfaceContainerHigh }} ; Context menu and tooltips +player = {{ $secondaryContainer }} ; Background for top result in search +sidebar = {{ $surface }} ; Background +main-elevated = {{ $surfaceContainerHigh }} ; Higher layers than main, e.g. search bar +highlight-elevated = {{ $surfaceContainerHighest }} ; Home button and search bar accelerator +selected-row = {{ $onSurface }} ; Selections, hover, other coloured text and slider background +button = {{ $primary }} ; Slider and text buttons +button-active = {{ $primary }} ; Background buttons +button-disabled = {{ $outline }} ; Disabled buttons +tab-active = {{ $surfaceContainerHigh }} ; Profile fallbacks in search diff --git a/src/caelestia/data/templates/spicetify-light.ini b/src/caelestia/data/templates/spicetify-light.ini new file mode 100644 index 0000000..a8b361b --- /dev/null +++ b/src/caelestia/data/templates/spicetify-light.ini @@ -0,0 +1,19 @@ +[caelestia] +text = {{ $onSurface }} ; Main text colour +subtext = {{ $onSurfaceVariant }} ; Subtext colour +main = {{ $surface }} ; Panel backgrounds +highlight = {{ $primary }} ; Doesn't seem to do anything +misc = {{ $primary }} ; Doesn't seem to do anything +notification = {{ $outline }} ; Notifications probably +notification-error = {{ $error }} ; Error notifications probably +shadow = {{ $shadow }} ; Shadow for covers, context menu, also affects playlist/artist banners +card = {{ $surfaceContainer }} ; Context menu and tooltips +player = {{ $secondaryContainer }} ; Background for top result in search +sidebar = {{ $surfaceContainer }} ; Background +main-elevated = {{ $surfaceContainerHigh }} ; Higher layers than main, e.g. search bar +highlight-elevated = {{ $surfaceContainerHighest }} ; Home button and search bar accelerator +selected-row = {{ $onSurface }} ; Selections, hover, other coloured text and slider background +button = {{ $primary }} ; Slider and text buttons +button-active = {{ $primary }} ; Background buttons +button-disabled = {{ $outline }} ; Disabled buttons +tab-active = {{ $surfaceContainer }} ; Profile fallbacks in search diff --git a/src/caelestia/data/templates/spicetify.ini b/src/caelestia/data/templates/spicetify.ini deleted file mode 100644 index 976a0c9..0000000 --- a/src/caelestia/data/templates/spicetify.ini +++ /dev/null @@ -1,17 +0,0 @@ -[caelestia] -text = {{ $text }} ; Main text colour -subtext = {{ $subtext0 }} ; Subtext colour -main = {{ $base }} ; Panel backgrounds -highlight = {{ $primary }} ; Doesn't seem to do anything -misc = {{ $primary }} ; Doesn't seem to do anything -notification = {{ $overlay0 }} ; Notifications probably -notification-error = {{ $error }} ; Error notifications probably -shadow = {{ $mantle }} ; Shadow for covers, context menu, also affects playlist/artist banners -card = {{ $surface0 }} ; Context menu and tooltips -player = {{ $base }} ; Doesn't seem to do anything -sidebar = {{ $mantle }} ; Background -main-elevated = {{ $surface0 }} ; Higher layers than main, e.g. search bar -selected-row = {{ $text }} ; Selections, hover, other coloured text and slider background -button = {{ $primary }} ; Slider and text buttons -button-active = {{ $primary }} ; Background buttons -button-disabled = {{ $overlay0 }} ; Disabled buttons diff --git a/src/caelestia/subcommands/scheme.py b/src/caelestia/subcommands/scheme.py index fa1f49a..e149d13 100644 --- a/src/caelestia/subcommands/scheme.py +++ b/src/caelestia/subcommands/scheme.py @@ -22,6 +22,6 @@ class Command: scheme.flavour = self.args.flavour if self.args.mode: scheme.mode = self.args.mode - apply_colours(scheme.colours) + apply_colours(scheme.colours, scheme.mode) else: print(scheme) diff --git a/src/caelestia/utils/theme.py b/src/caelestia/utils/theme.py index 7f29637..3250ea3 100644 --- a/src/caelestia/utils/theme.py +++ b/src/caelestia/utils/theme.py @@ -97,8 +97,8 @@ def apply_discord(scss: str) -> None: try_write(config_dir / client / "themes/caelestia.theme.css", conf) -def apply_spicetify(colours: dict[str, str]) -> None: - template = gen_replace(colours, templates_dir / "spicetify.ini") +def apply_spicetify(colours: dict[str, str], mode: str) -> None: + template = gen_replace(colours, templates_dir / f"spicetify-{mode}.ini") try_write(config_dir / "spicetify/Themes/caelestia/color.ini", template) @@ -113,10 +113,10 @@ def apply_btop(colours: dict[str, str]) -> None: subprocess.run(["killall", "-USR2", "btop"]) -def apply_colours(colours: dict[str, str]) -> None: +def apply_colours(colours: dict[str, str], mode: str) -> None: apply_terms(gen_sequences(colours)) apply_hypr(gen_conf(colours)) apply_discord(gen_scss(colours)) - apply_spicetify(colours) + apply_spicetify(colours, mode) apply_fuzzel(colours) apply_btop(colours) -- cgit v1.2.3-freya From 672ef4a2d9291fb4333e6d6aa807826d6860259a Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 12 Jun 2025 16:00:43 +1000 Subject: scheme: impl random + fix single schemes --- src/caelestia/data/schemes/oldworld/dark.txt | 81 ---------------------- .../data/schemes/oldworld/default/dark.txt | 81 ++++++++++++++++++++++ src/caelestia/data/schemes/onedark/dark.txt | 81 ---------------------- .../data/schemes/onedark/default/dark.txt | 81 ++++++++++++++++++++++ src/caelestia/data/schemes/shadotheme/dark.txt | 81 ---------------------- .../data/schemes/shadotheme/default/dark.txt | 81 ++++++++++++++++++++++ src/caelestia/subcommands/scheme.py | 1 + src/caelestia/utils/material/generator.py | 2 +- src/caelestia/utils/scheme.py | 8 +++ 9 files changed, 253 insertions(+), 244 deletions(-) delete mode 100644 src/caelestia/data/schemes/oldworld/dark.txt create mode 100644 src/caelestia/data/schemes/oldworld/default/dark.txt delete mode 100644 src/caelestia/data/schemes/onedark/dark.txt create mode 100644 src/caelestia/data/schemes/onedark/default/dark.txt delete mode 100644 src/caelestia/data/schemes/shadotheme/dark.txt create mode 100644 src/caelestia/data/schemes/shadotheme/default/dark.txt (limited to 'src/caelestia/subcommands') diff --git a/src/caelestia/data/schemes/oldworld/dark.txt b/src/caelestia/data/schemes/oldworld/dark.txt deleted file mode 100644 index 846dc18..0000000 --- a/src/caelestia/data/schemes/oldworld/dark.txt +++ /dev/null @@ -1,81 +0,0 @@ -rosewater f4d3d3 -flamingo edbab5 -pink e29eca -mauve c99ee2 -red ea83a5 -maroon f49eba -peach f5a191 -yellow e6b99d -green 90b99f -teal 85b5ba -sky 69b6e0 -sapphire 5b9fba -blue 92a2d5 -lavender aca1cf -text c9c7cd -subtext1 9998a8 -subtext0 757581 -overlay2 57575f -overlay1 3e3e43 -overlay0 353539 -surface2 2a2a2d -surface1 27272a -surface0 1b1b1d -base 161617 -mantle 131314 -crust 101011 -success 90b99f -primary_paletteKeyColor 5A77AB -secondary_paletteKeyColor 6E778A -tertiary_paletteKeyColor 8A6E8E -neutral_paletteKeyColor 76777D -neutral_variant_paletteKeyColor 74777F -background 111318 -onBackground E2E2E9 -surface 111318 -surfaceDim 111318 -surfaceBright 37393E -surfaceContainerLowest 0C0E13 -surfaceContainerLow 191C20 -surfaceContainer 1E2025 -surfaceContainerHigh 282A2F -surfaceContainerHighest 33353A -onSurface E2E2E9 -surfaceVariant 44474E -onSurfaceVariant C4C6D0 -inverseSurface E2E2E9 -inverseOnSurface 2E3036 -outline 8E9099 -outlineVariant 44474E -shadow 000000 -scrim 000000 -surfaceTint ABC7FF -primary ABC7FF -onPrimary 0B305F -primaryContainer 284777 -onPrimaryContainer D7E3FF -inversePrimary 415E91 -secondary BEC6DC -onSecondary 283141 -secondaryContainer 3E4759 -onSecondaryContainer DAE2F9 -tertiary DDBCE0 -onTertiary 3F2844 -tertiaryContainer A587A9 -onTertiaryContainer 000000 -error FFB4AB -onError 690005 -errorContainer 93000A -onErrorContainer FFDAD6 -primaryFixed D7E3FF -primaryFixedDim ABC7FF -onPrimaryFixed 001B3F -onPrimaryFixedVariant 284777 -secondaryFixed DAE2F9 -secondaryFixedDim BEC6DC -onSecondaryFixed 131C2B -onSecondaryFixedVariant 3E4759 -tertiaryFixed FAD8FD -tertiaryFixedDim DDBCE0 -onTertiaryFixed 28132E -onTertiaryFixedVariant 573E5C \ No newline at end of file diff --git a/src/caelestia/data/schemes/oldworld/default/dark.txt b/src/caelestia/data/schemes/oldworld/default/dark.txt new file mode 100644 index 0000000..846dc18 --- /dev/null +++ b/src/caelestia/data/schemes/oldworld/default/dark.txt @@ -0,0 +1,81 @@ +rosewater f4d3d3 +flamingo edbab5 +pink e29eca +mauve c99ee2 +red ea83a5 +maroon f49eba +peach f5a191 +yellow e6b99d +green 90b99f +teal 85b5ba +sky 69b6e0 +sapphire 5b9fba +blue 92a2d5 +lavender aca1cf +text c9c7cd +subtext1 9998a8 +subtext0 757581 +overlay2 57575f +overlay1 3e3e43 +overlay0 353539 +surface2 2a2a2d +surface1 27272a +surface0 1b1b1d +base 161617 +mantle 131314 +crust 101011 +success 90b99f +primary_paletteKeyColor 5A77AB +secondary_paletteKeyColor 6E778A +tertiary_paletteKeyColor 8A6E8E +neutral_paletteKeyColor 76777D +neutral_variant_paletteKeyColor 74777F +background 111318 +onBackground E2E2E9 +surface 111318 +surfaceDim 111318 +surfaceBright 37393E +surfaceContainerLowest 0C0E13 +surfaceContainerLow 191C20 +surfaceContainer 1E2025 +surfaceContainerHigh 282A2F +surfaceContainerHighest 33353A +onSurface E2E2E9 +surfaceVariant 44474E +onSurfaceVariant C4C6D0 +inverseSurface E2E2E9 +inverseOnSurface 2E3036 +outline 8E9099 +outlineVariant 44474E +shadow 000000 +scrim 000000 +surfaceTint ABC7FF +primary ABC7FF +onPrimary 0B305F +primaryContainer 284777 +onPrimaryContainer D7E3FF +inversePrimary 415E91 +secondary BEC6DC +onSecondary 283141 +secondaryContainer 3E4759 +onSecondaryContainer DAE2F9 +tertiary DDBCE0 +onTertiary 3F2844 +tertiaryContainer A587A9 +onTertiaryContainer 000000 +error FFB4AB +onError 690005 +errorContainer 93000A +onErrorContainer FFDAD6 +primaryFixed D7E3FF +primaryFixedDim ABC7FF +onPrimaryFixed 001B3F +onPrimaryFixedVariant 284777 +secondaryFixed DAE2F9 +secondaryFixedDim BEC6DC +onSecondaryFixed 131C2B +onSecondaryFixedVariant 3E4759 +tertiaryFixed FAD8FD +tertiaryFixedDim DDBCE0 +onTertiaryFixed 28132E +onTertiaryFixedVariant 573E5C \ No newline at end of file diff --git a/src/caelestia/data/schemes/onedark/dark.txt b/src/caelestia/data/schemes/onedark/dark.txt deleted file mode 100644 index 269096e..0000000 --- a/src/caelestia/data/schemes/onedark/dark.txt +++ /dev/null @@ -1,81 +0,0 @@ -rosewater edcbc5 -flamingo d3a4a4 -pink d792c6 -mauve c678dd -red be5046 -maroon e06c75 -peach d19a66 -yellow e5c07b -green 98c379 -teal 56b6c2 -sky 90ccd7 -sapphire 389dcc -blue 61afef -lavender 8e98d9 -text abb2bf -subtext1 95a0b5 -subtext0 838b9c -overlay2 767f8f -overlay1 666e7c -overlay0 5c6370 -surface2 4b5263 -surface1 3c414f -surface0 30343e -base 282c34 -mantle 21242b -crust 1e2126 -success 98c379 -primary_paletteKeyColor 5878AB -secondary_paletteKeyColor 6E778A -tertiary_paletteKeyColor 896E8F -neutral_paletteKeyColor 75777D -neutral_variant_paletteKeyColor 74777F -background 111318 -onBackground E1E2E9 -surface 111318 -surfaceDim 111318 -surfaceBright 37393E -surfaceContainerLowest 0C0E13 -surfaceContainerLow 191C20 -surfaceContainer 1D2024 -surfaceContainerHigh 282A2F -surfaceContainerHighest 33353A -onSurface E1E2E9 -surfaceVariant 43474E -onSurfaceVariant C4C6CF -inverseSurface E1E2E9 -inverseOnSurface 2E3035 -outline 8E9099 -outlineVariant 43474E -shadow 000000 -scrim 000000 -surfaceTint A8C8FF -primary A8C8FF -onPrimary 06305F -primaryContainer 254777 -onPrimaryContainer D5E3FF -inversePrimary 3F5F90 -secondary BDC7DC -onSecondary 273141 -secondaryContainer 40495B -onSecondaryContainer D9E3F8 -tertiary DBBCE1 -onTertiary 3E2845 -tertiaryContainer A387AA -onTertiaryContainer 000000 -error FFB4AB -onError 690005 -errorContainer 93000A -onErrorContainer FFDAD6 -primaryFixed D5E3FF -primaryFixedDim A8C8FF -onPrimaryFixed 001B3C -onPrimaryFixedVariant 254777 -secondaryFixed D9E3F8 -secondaryFixedDim BDC7DC -onSecondaryFixed 121C2B -onSecondaryFixedVariant 3E4758 -tertiaryFixed F8D8FE -tertiaryFixedDim DBBCE1 -onTertiaryFixed 28132F -onTertiaryFixedVariant 563E5D \ No newline at end of file diff --git a/src/caelestia/data/schemes/onedark/default/dark.txt b/src/caelestia/data/schemes/onedark/default/dark.txt new file mode 100644 index 0000000..269096e --- /dev/null +++ b/src/caelestia/data/schemes/onedark/default/dark.txt @@ -0,0 +1,81 @@ +rosewater edcbc5 +flamingo d3a4a4 +pink d792c6 +mauve c678dd +red be5046 +maroon e06c75 +peach d19a66 +yellow e5c07b +green 98c379 +teal 56b6c2 +sky 90ccd7 +sapphire 389dcc +blue 61afef +lavender 8e98d9 +text abb2bf +subtext1 95a0b5 +subtext0 838b9c +overlay2 767f8f +overlay1 666e7c +overlay0 5c6370 +surface2 4b5263 +surface1 3c414f +surface0 30343e +base 282c34 +mantle 21242b +crust 1e2126 +success 98c379 +primary_paletteKeyColor 5878AB +secondary_paletteKeyColor 6E778A +tertiary_paletteKeyColor 896E8F +neutral_paletteKeyColor 75777D +neutral_variant_paletteKeyColor 74777F +background 111318 +onBackground E1E2E9 +surface 111318 +surfaceDim 111318 +surfaceBright 37393E +surfaceContainerLowest 0C0E13 +surfaceContainerLow 191C20 +surfaceContainer 1D2024 +surfaceContainerHigh 282A2F +surfaceContainerHighest 33353A +onSurface E1E2E9 +surfaceVariant 43474E +onSurfaceVariant C4C6CF +inverseSurface E1E2E9 +inverseOnSurface 2E3035 +outline 8E9099 +outlineVariant 43474E +shadow 000000 +scrim 000000 +surfaceTint A8C8FF +primary A8C8FF +onPrimary 06305F +primaryContainer 254777 +onPrimaryContainer D5E3FF +inversePrimary 3F5F90 +secondary BDC7DC +onSecondary 273141 +secondaryContainer 40495B +onSecondaryContainer D9E3F8 +tertiary DBBCE1 +onTertiary 3E2845 +tertiaryContainer A387AA +onTertiaryContainer 000000 +error FFB4AB +onError 690005 +errorContainer 93000A +onErrorContainer FFDAD6 +primaryFixed D5E3FF +primaryFixedDim A8C8FF +onPrimaryFixed 001B3C +onPrimaryFixedVariant 254777 +secondaryFixed D9E3F8 +secondaryFixedDim BDC7DC +onSecondaryFixed 121C2B +onSecondaryFixedVariant 3E4758 +tertiaryFixed F8D8FE +tertiaryFixedDim DBBCE1 +onTertiaryFixed 28132F +onTertiaryFixedVariant 563E5D \ No newline at end of file diff --git a/src/caelestia/data/schemes/shadotheme/dark.txt b/src/caelestia/data/schemes/shadotheme/dark.txt deleted file mode 100644 index e178804..0000000 --- a/src/caelestia/data/schemes/shadotheme/dark.txt +++ /dev/null @@ -1,81 +0,0 @@ -rosewater f1c4e0 -flamingo F18FB0 -pink a8899c -mauve E9729D -red B52A5B -maroon FF4971 -peach ff79c6 -yellow 8897F4 -green 6a5acd -teal F18FB0 -sky 4484d1 -sapphire 2f77a1 -blue bd93f9 -lavender 849BE0 -text e3c7fc -subtext1 CBB2E1 -subtext0 B39DC7 -overlay2 9A88AC -overlay1 827392 -overlay0 6A5D77 -surface2 52485D -surface1 393342 -surface0 211E28 -base 09090d -mantle 060608 -crust 030304 -success 37d4a7 -primary_paletteKeyColor 6F72AC -secondary_paletteKeyColor 75758B -tertiary_paletteKeyColor 936B83 -neutral_paletteKeyColor 78767D -neutral_variant_paletteKeyColor 777680 -background 131318 -onBackground E4E1E9 -surface 131318 -surfaceDim 131318 -surfaceBright 39383F -surfaceContainerLowest 0E0E13 -surfaceContainerLow 1B1B21 -surfaceContainer 1F1F25 -surfaceContainerHigh 2A292F -surfaceContainerHighest 35343A -onSurface E4E1E9 -surfaceVariant 46464F -onSurfaceVariant C7C5D0 -inverseSurface E4E1E9 -inverseOnSurface 303036 -outline 918F9A -outlineVariant 46464F -shadow 000000 -scrim 000000 -surfaceTint BFC1FF -primary BFC1FF -onPrimary 282B60 -primaryContainer 3F4178 -onPrimaryContainer E1E0FF -inversePrimary 565992 -secondary C5C4DD -onSecondary 2E2F42 -secondaryContainer 47475B -onSecondaryContainer E2E0F9 -tertiary E8B9D4 -onTertiary 46263B -tertiaryContainer AF849D -onTertiaryContainer 000000 -error FFB4AB -onError 690005 -errorContainer 93000A -onErrorContainer FFDAD6 -primaryFixed E1E0FF -primaryFixedDim BFC1FF -onPrimaryFixed 12144B -onPrimaryFixedVariant 3F4178 -secondaryFixed E2E0F9 -secondaryFixedDim C5C4DD -onSecondaryFixed 191A2C -onSecondaryFixedVariant 454559 -tertiaryFixed FFD8ED -tertiaryFixedDim E8B9D4 -onTertiaryFixed 2E1125 -onTertiaryFixedVariant 5F3C52 \ No newline at end of file diff --git a/src/caelestia/data/schemes/shadotheme/default/dark.txt b/src/caelestia/data/schemes/shadotheme/default/dark.txt new file mode 100644 index 0000000..e178804 --- /dev/null +++ b/src/caelestia/data/schemes/shadotheme/default/dark.txt @@ -0,0 +1,81 @@ +rosewater f1c4e0 +flamingo F18FB0 +pink a8899c +mauve E9729D +red B52A5B +maroon FF4971 +peach ff79c6 +yellow 8897F4 +green 6a5acd +teal F18FB0 +sky 4484d1 +sapphire 2f77a1 +blue bd93f9 +lavender 849BE0 +text e3c7fc +subtext1 CBB2E1 +subtext0 B39DC7 +overlay2 9A88AC +overlay1 827392 +overlay0 6A5D77 +surface2 52485D +surface1 393342 +surface0 211E28 +base 09090d +mantle 060608 +crust 030304 +success 37d4a7 +primary_paletteKeyColor 6F72AC +secondary_paletteKeyColor 75758B +tertiary_paletteKeyColor 936B83 +neutral_paletteKeyColor 78767D +neutral_variant_paletteKeyColor 777680 +background 131318 +onBackground E4E1E9 +surface 131318 +surfaceDim 131318 +surfaceBright 39383F +surfaceContainerLowest 0E0E13 +surfaceContainerLow 1B1B21 +surfaceContainer 1F1F25 +surfaceContainerHigh 2A292F +surfaceContainerHighest 35343A +onSurface E4E1E9 +surfaceVariant 46464F +onSurfaceVariant C7C5D0 +inverseSurface E4E1E9 +inverseOnSurface 303036 +outline 918F9A +outlineVariant 46464F +shadow 000000 +scrim 000000 +surfaceTint BFC1FF +primary BFC1FF +onPrimary 282B60 +primaryContainer 3F4178 +onPrimaryContainer E1E0FF +inversePrimary 565992 +secondary C5C4DD +onSecondary 2E2F42 +secondaryContainer 47475B +onSecondaryContainer E2E0F9 +tertiary E8B9D4 +onTertiary 46263B +tertiaryContainer AF849D +onTertiaryContainer 000000 +error FFB4AB +onError 690005 +errorContainer 93000A +onErrorContainer FFDAD6 +primaryFixed E1E0FF +primaryFixedDim BFC1FF +onPrimaryFixed 12144B +onPrimaryFixedVariant 3F4178 +secondaryFixed E2E0F9 +secondaryFixedDim C5C4DD +onSecondaryFixed 191A2C +onSecondaryFixedVariant 454559 +tertiaryFixed FFD8ED +tertiaryFixedDim E8B9D4 +onTertiaryFixed 2E1125 +onTertiaryFixedVariant 5F3C52 \ No newline at end of file diff --git a/src/caelestia/subcommands/scheme.py b/src/caelestia/subcommands/scheme.py index e149d13..973cfce 100644 --- a/src/caelestia/subcommands/scheme.py +++ b/src/caelestia/subcommands/scheme.py @@ -15,6 +15,7 @@ class Command: if self.args.random: scheme.set_random() + apply_colours(scheme.colours, scheme.mode) elif self.args.name or self.args.flavour or self.args.mode: if self.args.name: scheme.name = self.args.name diff --git a/src/caelestia/utils/material/generator.py b/src/caelestia/utils/material/generator.py index 33ff0e8..235b2ce 100755 --- a/src/caelestia/utils/material/generator.py +++ b/src/caelestia/utils/material/generator.py @@ -187,6 +187,6 @@ def gen_scheme(scheme, primary: Hct, colours: list[Hct]) -> dict[str, str]: colours["success"] = harmonize(base[8], primary) # For debugging - print("\n".join(["{}: \x1b[48;2;{};{};{}m \x1b[0m".format(n, *c.to_rgba()[:3]) for n, c in colours.items()])) + # print("\n".join(["{}: \x1b[48;2;{};{};{}m \x1b[0m".format(n, *c.to_rgba()[:3]) for n, c in colours.items()])) return {k: hex(v.to_int())[4:] for k, v in colours.items()} diff --git a/src/caelestia/utils/scheme.py b/src/caelestia/utils/scheme.py index 66cd697..ce8d6fe 100644 --- a/src/caelestia/utils/scheme.py +++ b/src/caelestia/utils/scheme.py @@ -1,4 +1,5 @@ import json +import random from pathlib import Path from caelestia.utils.material import get_colours_for_image @@ -106,6 +107,13 @@ class Scheme: f, ) + def set_random(self) -> None: + self._name = random.choice(get_scheme_names()) + self._flavour = random.choice(get_scheme_flavours()) + self._mode = random.choice(get_scheme_modes()) + self._update_colours() + self.save() + def _check_flavour(self) -> None: global scheme_flavours scheme_flavours = None -- cgit v1.2.3-freya From e75e727262573de25ee1e1e75bd93e3647dc3609 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 12 Jun 2025 16:23:14 +1000 Subject: scheme: add variant option Remove variant subcommand --- src/caelestia/parser.py | 22 ++-------------------- src/caelestia/subcommands/scheme.py | 4 +++- src/caelestia/subcommands/variant.py | 11 ----------- src/caelestia/utils/scheme.py | 5 +++++ 4 files changed, 10 insertions(+), 32 deletions(-) delete mode 100644 src/caelestia/subcommands/variant.py (limited to 'src/caelestia/subcommands') diff --git a/src/caelestia/parser.py b/src/caelestia/parser.py index 9718938..3f6f506 100644 --- a/src/caelestia/parser.py +++ b/src/caelestia/parser.py @@ -1,18 +1,6 @@ import argparse -from caelestia.subcommands import ( - clipboard, - emoji, - pip, - record, - scheme, - screenshot, - shell, - toggle, - variant, - wallpaper, - wsaction, -) +from caelestia.subcommands import clipboard, emoji, pip, record, scheme, screenshot, shell, toggle, wallpaper, wsaction from caelestia.utils.scheme import get_scheme_names, scheme_variants @@ -65,13 +53,7 @@ def parse_args() -> (argparse.ArgumentParser, argparse.Namespace): scheme_parser.add_argument("-n", "--name", choices=get_scheme_names(), help="the name of the scheme to switch to") scheme_parser.add_argument("-f", "--flavour", help="the flavour to switch to") scheme_parser.add_argument("-m", "--mode", choices=["dark", "light"], help="the mode to switch to") - - # Create parser for variant opts - variant_parser = command_parser.add_parser("variant", help="manage the dynamic scheme variant") - variant_parser.set_defaults(cls=variant.Command) - variant_parser.add_argument("-g", "--get", action="store_true", help="print the current dynamic scheme variant") - variant_parser.add_argument("-s", "--set", choices=scheme_variants, help="set the current dynamic scheme variant") - variant_parser.add_argument("-r", "--random", action="store_true", help="switch to a random variant") + scheme_parser.add_argument("-v", "--variant", choices=scheme_variants, help="the variant to switch to") # Create parser for screenshot opts screenshot_parser = command_parser.add_parser("screenshot", help="take a screenshot") diff --git a/src/caelestia/subcommands/scheme.py b/src/caelestia/subcommands/scheme.py index 973cfce..c95df96 100644 --- a/src/caelestia/subcommands/scheme.py +++ b/src/caelestia/subcommands/scheme.py @@ -16,13 +16,15 @@ class Command: if self.args.random: scheme.set_random() apply_colours(scheme.colours, scheme.mode) - elif self.args.name or self.args.flavour or self.args.mode: + elif self.args.name or self.args.flavour or self.args.mode or self.args.variant: if self.args.name: scheme.name = self.args.name if self.args.flavour: scheme.flavour = self.args.flavour if self.args.mode: scheme.mode = self.args.mode + if self.args.variant: + scheme.variant = self.args.variant apply_colours(scheme.colours, scheme.mode) else: print(scheme) diff --git a/src/caelestia/subcommands/variant.py b/src/caelestia/subcommands/variant.py deleted file mode 100644 index 37f9a2b..0000000 --- a/src/caelestia/subcommands/variant.py +++ /dev/null @@ -1,11 +0,0 @@ -from argparse import Namespace - - -class Command: - args: Namespace - - def __init__(self, args: Namespace) -> None: - self.args = args - - def run(self) -> None: - pass diff --git a/src/caelestia/utils/scheme.py b/src/caelestia/utils/scheme.py index f25cf62..c978231 100644 --- a/src/caelestia/utils/scheme.py +++ b/src/caelestia/utils/scheme.py @@ -84,7 +84,12 @@ class Scheme: @variant.setter def variant(self, variant: str) -> None: + if variant == self._variant: + return + self._variant = variant + self._update_colours() + self.save() @property def colours(self) -> dict[str, str]: -- cgit v1.2.3-freya From c043a14ca24f70e81b69133350a1174d2e6572fc Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 12 Jun 2025 21:35:05 +1000 Subject: feat: impl wallpaper --- src/caelestia/parser.py | 12 +++- src/caelestia/subcommands/wallpaper.py | 12 +++- src/caelestia/utils/paths.py | 7 +- src/caelestia/utils/scheme.py | 12 ++-- src/caelestia/utils/wallpaper.py | 123 +++++++++++++++++++++++++++++++++ 5 files changed, 154 insertions(+), 12 deletions(-) create mode 100644 src/caelestia/utils/wallpaper.py (limited to 'src/caelestia/subcommands') diff --git a/src/caelestia/parser.py b/src/caelestia/parser.py index 3f6f506..f8c7bac 100644 --- a/src/caelestia/parser.py +++ b/src/caelestia/parser.py @@ -1,7 +1,9 @@ import argparse from caelestia.subcommands import clipboard, emoji, pip, record, scheme, screenshot, shell, toggle, wallpaper, wsaction +from caelestia.utils.paths import wallpapers_dir from caelestia.utils.scheme import get_scheme_names, scheme_variants +from caelestia.utils.wallpaper import get_wallpaper def parse_args() -> (argparse.ArgumentParser, argparse.Namespace): @@ -81,14 +83,18 @@ def parse_args() -> (argparse.ArgumentParser, argparse.Namespace): # Create parser for wallpaper opts wallpaper_parser = command_parser.add_parser("wallpaper", help="manage the wallpaper") wallpaper_parser.set_defaults(cls=wallpaper.Command) - wallpaper_parser.add_argument("-g", "--get", action="store_true", help="print the current wallpaper") - wallpaper_parser.add_argument("-r", "--random", action="store_true", help="switch to a random wallpaper") + wallpaper_parser.add_argument( + "-p", "--print", nargs="?", const=get_wallpaper(), metavar="PATH", help="print the scheme for a wallpaper" + ) + wallpaper_parser.add_argument( + "-r", "--random", nargs="?", const=wallpapers_dir, metavar="DIR", help="switch to a random wallpaper" + ) wallpaper_parser.add_argument("-f", "--file", help="the path to the wallpaper to switch to") wallpaper_parser.add_argument("-n", "--no-filter", action="store_true", help="do not filter by size") wallpaper_parser.add_argument( "-t", "--threshold", - default=80, + default=0.8, help="the minimum percentage of the largest monitor size the image must be greater than to be selected", ) wallpaper_parser.add_argument( diff --git a/src/caelestia/subcommands/wallpaper.py b/src/caelestia/subcommands/wallpaper.py index 37f9a2b..1440484 100644 --- a/src/caelestia/subcommands/wallpaper.py +++ b/src/caelestia/subcommands/wallpaper.py @@ -1,5 +1,8 @@ +import json from argparse import Namespace +from caelestia.utils.wallpaper import get_colours_for_wall, get_wallpaper, set_random, set_wallpaper + class Command: args: Namespace @@ -8,4 +11,11 @@ class Command: self.args = args def run(self) -> None: - pass + if self.args.print: + print(json.dumps(get_colours_for_wall(self.args.print, self.args.no_smart))) + elif self.args.file: + set_wallpaper(self.args.file, self.args.no_smart) + elif self.args.random: + set_random(self.args) + else: + print(get_wallpaper()) diff --git a/src/caelestia/utils/paths.py b/src/caelestia/utils/paths.py index 3b5a7a6..f81b996 100644 --- a/src/caelestia/utils/paths.py +++ b/src/caelestia/utils/paths.py @@ -22,11 +22,14 @@ scheme_path = c_state_dir / "scheme.json" scheme_data_dir = cli_data_dir / "schemes" scheme_cache_dir = c_cache_dir / "schemes" -last_wallpaper_path = c_state_dir / "wallpaper/last.txt" +wallpapers_dir = Path.home() / "Pictures/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" +thumbnail_cache_dir = c_cache_dir / "thumbnails" -def compute_hash(path: str) -> str: +def compute_hash(path: Path | str) -> str: sha = hashlib.sha256() with open(path, "rb") as f: diff --git a/src/caelestia/utils/scheme.py b/src/caelestia/utils/scheme.py index 9027589..0d6cfb5 100644 --- a/src/caelestia/utils/scheme.py +++ b/src/caelestia/utils/scheme.py @@ -59,8 +59,7 @@ class Scheme: self._flavour = flavour self._check_mode() - self._update_colours() - self.save() + self.update_colours() @property def mode(self) -> str: @@ -75,8 +74,7 @@ class Scheme: raise ValueError(f'Invalid scheme mode: "{mode}". Valid modes: {get_scheme_modes()}') self._mode = mode - self._update_colours() - self.save() + self.update_colours() @property def variant(self) -> str: @@ -88,8 +86,7 @@ class Scheme: return self._variant = variant - self._update_colours() - self.save() + self.update_colours() @property def colours(self) -> dict[str, str]: @@ -115,6 +112,9 @@ class Scheme: self._name = random.choice(get_scheme_names()) self._flavour = random.choice(get_scheme_flavours()) self._mode = random.choice(get_scheme_modes()) + self.update_colours() + + def update_colours(self) -> None: self._update_colours() self.save() diff --git a/src/caelestia/utils/wallpaper.py b/src/caelestia/utils/wallpaper.py new file mode 100644 index 0000000..c8b3a72 --- /dev/null +++ b/src/caelestia/utils/wallpaper.py @@ -0,0 +1,123 @@ +import random +from argparse import Namespace +from pathlib import Path + +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.paths import ( + compute_hash, + thumbnail_cache_dir, + wallpaper_link_path, + wallpaper_path_path, + wallpaper_thumbnail_path, +) +from caelestia.utils.scheme import Scheme, get_scheme +from caelestia.utils.theme import apply_colours + + +def is_valid_image(path: Path | str) -> bool: + path = Path(path) + return path.is_file() and path.suffix in [".jpg", ".jpeg", ".png", ".webp", ".tif", ".tiff"] + + +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: + return wallpaper_path_path.read_text() + + +def get_wallpapers(args: Namespace) -> list[Path]: + dir = Path(args.random) + if not dir.is_dir(): + return [] + + walls = [f for f in dir.rglob("*") if is_valid_image(f)] + + if args.no_filter: + return walls + + monitors = message("monitors") + filter_size = monitors[0]["width"], monitors[0]["height"] + for monitor in monitors[1:]: + if filter_size[0] > monitor["width"]: + filter_size[0] = monitor["width"] + if filter_size[1] > monitor["height"]: + filter_size[1] = monitor["height"] + + return [f for f in walls if check_wall(f, filter_size, args.threshold)] + + +def get_thumb(wall: Path) -> Path: + thumb = (thumbnail_cache_dir / compute_hash(wall)).with_suffix(".jpg") + + if not thumb.exists(): + with Image.open(wall) as img: + img = img.convert("RGB") + img.thumbnail((128, 128), Image.NEAREST) + thumb.parent.mkdir(parents=True, exist_ok=True) + img.save(thumb, "JPEG") + + return thumb + + +def get_smart_mode(wall: Path) -> str: + with Image.open(get_thumb(wall)) as img: + img.thumbnail((1, 1), Image.LANCZOS) + tone = Hct.from_int(argb_from_rgb(*img.getpixel((0, 0)))).tone + return "light" if tone > 60 else "dark" + + +def get_colours_for_wall(wall: Path | str, no_smart: bool) -> None: + scheme = get_scheme() + + if not no_smart: + scheme = Scheme( + { + "name": scheme.name, + "flavour": scheme.flavour, + "mode": get_smart_mode(wall), + "variant": scheme.variant, + "colours": scheme.colours, + } + ) + + return get_colours_for_image(get_thumb(wall), scheme) + + +def set_wallpaper(wall: Path | str, no_smart: bool) -> None: + if not is_valid_image(wall): + raise ValueError(f'"{wall}" is not a valid image') + + # 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) + + # Generate thumbnail or get from cache + thumb = get_thumb(wall) + 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 based on wallpaper colour + scheme.mode = get_smart_mode(wall) + + # Update colours + scheme.update_colours() + apply_colours(scheme.colours, scheme.mode) + + +def set_random(args: Namespace) -> None: + set_wallpaper(random.choice(get_wallpapers(args)), args.no_smart) -- cgit v1.2.3-freya From 796d538b168855ebd2afd62278800151816adcab Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 13 Jun 2025 00:42:46 +1000 Subject: feat: impl screenshot command --- src/caelestia/parser.py | 2 +- src/caelestia/subcommands/screenshot.py | 69 ++++++++++++++++++++++++++++++++- src/caelestia/subcommands/toggle.py | 14 +------ src/caelestia/utils/paths.py | 3 ++ 4 files changed, 74 insertions(+), 14 deletions(-) (limited to 'src/caelestia/subcommands') diff --git a/src/caelestia/parser.py b/src/caelestia/parser.py index f8c7bac..02ac3b9 100644 --- a/src/caelestia/parser.py +++ b/src/caelestia/parser.py @@ -60,7 +60,7 @@ def parse_args() -> (argparse.ArgumentParser, argparse.Namespace): # Create parser for screenshot opts screenshot_parser = command_parser.add_parser("screenshot", help="take a screenshot") screenshot_parser.set_defaults(cls=screenshot.Command) - screenshot_parser.add_argument("-r", "--region", help="take a screenshot of a region") + screenshot_parser.add_argument("-r", "--region", nargs="?", const="slurp", help="take a screenshot of a region") screenshot_parser.add_argument( "-f", "--freeze", action="store_true", help="freeze the screen while selecting a region" ) diff --git a/src/caelestia/subcommands/screenshot.py b/src/caelestia/subcommands/screenshot.py index 37f9a2b..73d65f7 100644 --- a/src/caelestia/subcommands/screenshot.py +++ b/src/caelestia/subcommands/screenshot.py @@ -1,4 +1,9 @@ +import subprocess from argparse import Namespace +from datetime import datetime + +from caelestia.utils import hypr +from caelestia.utils.paths import screenshots_cache_dir, screenshots_dir class Command: @@ -8,4 +13,66 @@ class Command: self.args = args def run(self) -> None: - pass + if self.args.region: + self.region() + else: + self.fullscreen() + + def region(self) -> None: + freeze_proc = None + + if self.args.freeze: + freeze_proc = subprocess.Popen(["wayfreeze", "--hide-cursor"]) + + if self.args.region == "slurp": + ws = hypr.message("activeworkspace")["id"] + geoms = [ + f"{','.join(map(str, c['at']))} {'x'.join(map(str, c['size']))}" + for c in hypr.message("clients") + if c["workspace"]["id"] == ws + ] + region = subprocess.check_output(["slurp"], input="\n".join(geoms), text=True) + else: + region = self.args.region + + sc_data = subprocess.check_output(["grim", "-l", "0", "-g", region.strip(), "-"]) + swappy = subprocess.Popen(["swappy", "-f", "-"], stdin=subprocess.PIPE, start_new_session=True) + swappy.stdin.write(sc_data) + swappy.stdin.close() + + if freeze_proc: + freeze_proc.kill() + + def fullscreen(self) -> None: + sc_data = subprocess.check_output(["grim", "-"]) + + subprocess.run(["wl-copy"], input=sc_data) + + dest = screenshots_cache_dir / datetime.now().strftime("%Y%m%d%H%M%S") + screenshots_cache_dir.mkdir(exist_ok=True, parents=True) + dest.write_bytes(sc_data) + + action = subprocess.check_output( + [ + "notify-send", + "-i", + "image-x-generic-symbolic", + "-h", + f"STRING:image-path:{dest}", + "-a", + "caelestia-cli", + "--action=open=Open", + "--action=save=Save", + "Screenshot taken", + f"Screenshot stored in {dest} and copied to clipboard", + ], + text=True, + ).strip() + + if action == "open": + subprocess.Popen(["swappy", "-f", dest], start_new_session=True) + elif action == "save": + new_dest = (screenshots_dir / dest.name).with_suffix(".png") + new_dest.parent.mkdir(exist_ok=True, parents=True) + dest.rename(new_dest) + subprocess.run(["notify-send", "Screenshot saved", f"Saved to {new_dest}"]) diff --git a/src/caelestia/subcommands/toggle.py b/src/caelestia/subcommands/toggle.py index fd49c30..b8ad11b 100644 --- a/src/caelestia/subcommands/toggle.py +++ b/src/caelestia/subcommands/toggle.py @@ -1,3 +1,4 @@ +import subprocess from argparse import Namespace from caelestia.utils import hypr @@ -6,7 +7,6 @@ from caelestia.utils import hypr class Command: args: Namespace clients: list[dict[str, any]] = None - app2unit: str = None def __init__(self, args: Namespace) -> None: self.args = args @@ -20,14 +20,6 @@ class Command: return self.clients - def get_app2unit(self) -> str: - if self.app2unit is None: - import shutil - - self.app2unit = shutil.which("app2unit") - - return self.app2unit - def move_client(self, selector: callable, workspace: str) -> None: for client in self.get_clients(): if selector(client): @@ -37,9 +29,7 @@ class Command: exists = any(selector(client) for client in self.get_clients()) if not exists: - import subprocess - - subprocess.Popen([self.get_app2unit(), "--", *spawn], start_new_session=True) + subprocess.Popen(["app2unit", "--", *spawn], start_new_session=True) return not exists diff --git a/src/caelestia/utils/paths.py b/src/caelestia/utils/paths.py index 37aeeef..6a98dae 100644 --- a/src/caelestia/utils/paths.py +++ b/src/caelestia/utils/paths.py @@ -28,6 +28,9 @@ wallpaper_link_path = c_state_dir / "wallpaper/current" wallpaper_thumbnail_path = c_state_dir / "wallpaper/thumbnail.jpg" wallpapers_cache_dir = c_cache_dir / "wallpapers" +screenshots_dir = Path.home() / "Pictures/Screenshots" +screenshots_cache_dir = c_cache_dir / "screenshots" + def compute_hash(path: Path | str) -> str: sha = hashlib.sha256() -- cgit v1.2.3-freya From 9da9d7bb1b254d5d94265bda5e052ca4feee1b9a Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 13 Jun 2025 14:50:25 +1000 Subject: wallpaper: fix when no wall --- src/caelestia/subcommands/wallpaper.py | 2 +- src/caelestia/utils/wallpaper.py | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) (limited to 'src/caelestia/subcommands') diff --git a/src/caelestia/subcommands/wallpaper.py b/src/caelestia/subcommands/wallpaper.py index 1440484..940dcb5 100644 --- a/src/caelestia/subcommands/wallpaper.py +++ b/src/caelestia/subcommands/wallpaper.py @@ -18,4 +18,4 @@ class Command: elif self.args.random: set_random(self.args) else: - print(get_wallpaper()) + print(get_wallpaper() or "No wallpaper set") diff --git a/src/caelestia/utils/wallpaper.py b/src/caelestia/utils/wallpaper.py index 1146c73..0a666be 100644 --- a/src/caelestia/utils/wallpaper.py +++ b/src/caelestia/utils/wallpaper.py @@ -31,7 +31,10 @@ def check_wall(wall: Path, filter_size: tuple[int, int], threshold: float) -> bo def get_wallpaper() -> str: - return wallpaper_path_path.read_text() + try: + return wallpaper_path_path.read_text() + except IOError: + return None def get_wallpapers(args: Namespace) -> list[Path]: @@ -71,17 +74,17 @@ def get_thumb(wall: Path, cache: Path) -> Path: def get_smart_mode(wall: Path, cache: Path) -> str: mode_cache = cache / "mode.txt" - if mode_cache.exists(): + try: return mode_cache.read_text() + except IOError: + with Image.open(get_thumb(wall, cache)) as img: + img.thumbnail((1, 1), Image.LANCZOS) + mode = "light" if Hct.from_int(argb_from_rgb(*img.getpixel((0, 0)))).tone > 60 else "dark" - with Image.open(get_thumb(wall, cache)) as img: - img.thumbnail((1, 1), Image.LANCZOS) - mode = "light" if Hct.from_int(argb_from_rgb(*img.getpixel((0, 0)))).tone > 60 else "dark" + mode_cache.parent.mkdir(parents=True, exist_ok=True) + mode_cache.write_text(mode) - mode_cache.parent.mkdir(parents=True, exist_ok=True) - mode_cache.write_text(mode) - - return mode + return mode def get_colours_for_wall(wall: Path | str, no_smart: bool) -> None: -- cgit v1.2.3-freya From b805f8d67725352817d248562683cb90bf314401 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 14 Jun 2025 02:11:10 +1000 Subject: feat: impl recording subcommand --- src/caelestia/parser.py | 2 +- src/caelestia/subcommands/record.py | 113 +++++++++++++++++++++++++++++++++++- src/caelestia/utils/paths.py | 4 ++ 3 files changed, 117 insertions(+), 2 deletions(-) (limited to 'src/caelestia/subcommands') diff --git a/src/caelestia/parser.py b/src/caelestia/parser.py index 02ac3b9..6d0b552 100644 --- a/src/caelestia/parser.py +++ b/src/caelestia/parser.py @@ -68,7 +68,7 @@ def parse_args() -> (argparse.ArgumentParser, argparse.Namespace): # Create parser for record opts record_parser = command_parser.add_parser("record", help="start a screen recording") record_parser.set_defaults(cls=record.Command) - record_parser.add_argument("-r", "--region", action="store_true", help="record a region") + record_parser.add_argument("-r", "--region", nargs="?", const="slurp", help="record a region") record_parser.add_argument("-s", "--sound", action="store_true", help="record audio") # Create parser for clipboard opts diff --git a/src/caelestia/subcommands/record.py b/src/caelestia/subcommands/record.py index 37f9a2b..a4fa51d 100644 --- a/src/caelestia/subcommands/record.py +++ b/src/caelestia/subcommands/record.py @@ -1,4 +1,9 @@ +import subprocess +import time from argparse import Namespace +from datetime import datetime + +from caelestia.utils.paths import recording_notif_path, recording_path, recordings_dir class Command: @@ -8,4 +13,110 @@ class Command: self.args = args def run(self) -> None: - pass + proc = subprocess.run(["pidof", "wl-screenrec"]) + if proc.returncode == 0: + self.stop() + else: + self.start() + + def start(self) -> None: + args = [] + + if self.args.region: + if self.args.region == "slurp": + region = subprocess.check_output(["slurp"], text=True) + else: + region = self.args.region + args += ["-g", region.strip()] + + if self.args.sound: + sources = subprocess.check_output(["pactl", "list", "short", "sources"], text=True).splitlines() + for source in sources: + if "RUNNING" in source: + args += ["--audio", "--audio-device", source.split()[1]] + break + else: + raise ValueError("No audio source found") + + proc = subprocess.Popen( + ["wl-screenrec", *args, "--codec", "hevc", "-f", recording_path], + stderr=subprocess.PIPE, + text=True, + start_new_session=True, + ) + + # Send notif if proc hasn't ended after a small delay + time.sleep(0.1) + if proc.poll() is None: + notif = subprocess.check_output( + ["notify-send", "-p", "-a", "caelestia-cli", "Recording started", "Recording..."], text=True + ).strip() + recording_notif_path.write_text(notif) + else: + subprocess.run( + [ + "notify-send", + "-a", + "caelestia-cli", + "Recording failed", + f"Recording failed to start: {proc.communicate()[1]}", + ] + ) + + def stop(self) -> None: + subprocess.run(["pkill", "wl-screenrec"]) + + # Move to recordings folder + new_path = recordings_dir / f"recording_{datetime.now().strftime('%Y%m%d_%H-%M-%S')}.mp4" + recording_path.rename(new_path) + + # Close start notification + try: + notif = recording_notif_path.read_text() + subprocess.run( + [ + "gdbus", + "call", + "--session", + "--dest=org.freedesktop.Notifications", + "--object-path=/org/freedesktop/Notifications", + "--method=org.freedesktop.Notifications.CloseNotification", + notif, + ] + ) + except IOError: + pass + + action = subprocess.check_output( + [ + "notify-send", + "-a", + "caelestia-cli", + "--action=watch=Watch", + "--action=open=Open", + "--action=delete=Delete", + "Recording stopped", + f"Recording saved in {new_path}", + ], + text=True, + ).strip() + + if action == "watch": + subprocess.Popen(["app2unit", "-O", new_path], start_new_session=True) + elif action == "open": + p = subprocess.run( + [ + "dbus-send", + "--session", + "--dest=org.freedesktop.FileManager1", + "--type=method_call", + "/org/freedesktop/FileManager1", + "org.freedesktop.FileManager1.ShowItems", + f"array:string:file://{new_path}", + "string:", + ] + ) + if p.returncode != 0: + subprocess.Popen(["app2unit", "-O", new_path.parent], start_new_session=True) + elif action == "delete": + new_path.unlink() diff --git a/src/caelestia/utils/paths.py b/src/caelestia/utils/paths.py index 6a98dae..a4ef36f 100644 --- a/src/caelestia/utils/paths.py +++ b/src/caelestia/utils/paths.py @@ -31,6 +31,10 @@ wallpapers_cache_dir = c_cache_dir / "wallpapers" screenshots_dir = Path.home() / "Pictures/Screenshots" screenshots_cache_dir = c_cache_dir / "screenshots" +recordings_dir = Path.home() / "Videos/Recordings" +recording_path = c_state_dir / "record/recording.mp4" +recording_notif_path = c_state_dir / "record/notifid.txt" + def compute_hash(path: Path | str) -> str: sha = hashlib.sha256() -- cgit v1.2.3-freya From 15c47a622dfce8c860fffd92f2336a9e2917f697 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 14 Jun 2025 15:03:58 +1000 Subject: feat: impl clipboard subcommand --- src/caelestia/subcommands/clipboard.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) (limited to 'src/caelestia/subcommands') diff --git a/src/caelestia/subcommands/clipboard.py b/src/caelestia/subcommands/clipboard.py index 37f9a2b..c0eddb5 100644 --- a/src/caelestia/subcommands/clipboard.py +++ b/src/caelestia/subcommands/clipboard.py @@ -1,3 +1,4 @@ +import subprocess from argparse import Namespace @@ -8,4 +9,17 @@ class Command: self.args = args def run(self) -> None: - pass + clip = subprocess.check_output(["cliphist", "list"]) + + if self.args.delete: + args = ["--prompt=del > ", "--placeholder=Delete from clipboard"] + else: + args = ["--placeholder=Type to search clipboard"] + + chosen = subprocess.check_output(["fuzzel", "--dmenu", *args], input=clip) + + if self.args.delete: + subprocess.run(["cliphist", "delete"], input=chosen) + else: + decoded = subprocess.check_output(["cliphist", "decode"], input=chosen) + subprocess.run(["wl-copy"], input=decoded) -- cgit v1.2.3-freya From 427b9185a8eb9ee413899bb8891e91acc2557fb8 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 14 Jun 2025 15:08:03 +1000 Subject: feat: impl emoji picker subcommand --- src/caelestia/subcommands/emoji.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'src/caelestia/subcommands') diff --git a/src/caelestia/subcommands/emoji.py b/src/caelestia/subcommands/emoji.py index 37f9a2b..f04b502 100644 --- a/src/caelestia/subcommands/emoji.py +++ b/src/caelestia/subcommands/emoji.py @@ -1,5 +1,8 @@ +import subprocess from argparse import Namespace +from caelestia.utils.paths import cli_data_dir + class Command: args: Namespace @@ -8,4 +11,8 @@ class Command: self.args = args def run(self) -> None: - pass + emojis = (cli_data_dir / "emojis.txt").read_text() + chosen = subprocess.check_output( + ["fuzzel", "--dmenu", "--placeholder=Type to search emojis"], input=emojis, text=True + ) + subprocess.run(["wl-copy"], input=chosen.split()[0], text=True) -- cgit v1.2.3-freya From 4409620ac7a6965259fc6407127799c468f5421c Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 14 Jun 2025 15:34:47 +1000 Subject: feat: impl pip subcommand --- src/caelestia/subcommands/pip.py | 35 ++++++++++++++++++++++++++++++++++- src/caelestia/utils/hypr.py | 4 +++- 2 files changed, 37 insertions(+), 2 deletions(-) (limited to 'src/caelestia/subcommands') diff --git a/src/caelestia/subcommands/pip.py b/src/caelestia/subcommands/pip.py index 37f9a2b..5f1b5fa 100644 --- a/src/caelestia/subcommands/pip.py +++ b/src/caelestia/subcommands/pip.py @@ -1,5 +1,9 @@ +import re +import socket from argparse import Namespace +from caelestia.utils import hypr + class Command: args: Namespace @@ -8,4 +12,33 @@ class Command: self.args = args def run(self) -> None: - pass + if self.args.daemon: + self.daemon() + else: + win = hypr.message("activewindow") + if win["floating"]: + self.handle_window(win["address"], win["workspace"]["name"]) + + def handle_window(self, address: str, ws: str) -> None: + mon_id = next(w for w in hypr.message("workspaces") if w["name"] == ws)["monitorID"] + mon = next(m for m in hypr.message("monitors") if m["id"] == mon_id) + width, height = next(c for c in hypr.message("clients") if c["address"] == address)["size"] + + scale_factor = mon["height"] / 4 / height + scaled_win_size = f"{int(width * scale_factor)} {int(height * scale_factor)}" + off = min(mon["width"], mon["height"]) * 0.03 + move_to = f"{int(mon['width'] - off - width * scale_factor)} {int(mon['height'] - off - height * scale_factor)}" + + hypr.dispatch("resizewindowpixel", "exact", f"{scaled_win_size},address:{address}") + hypr.dispatch("movewindowpixel", "exact", f"{move_to},address:{address}") + + def daemon(self) -> None: + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: + sock.connect(hypr.socket2_path) + + while True: + data = sock.recv(4096).decode() + if data.startswith("openwindow>>"): + address, ws, cls, title = data[12:].split(",") + if re.match(r"^[Pp]icture(-| )in(-| )[Pp]icture$", title): + self.handle_window(f"0x{address}", ws) diff --git a/src/caelestia/utils/hypr.py b/src/caelestia/utils/hypr.py index 3ba89cf..f89cd98 100644 --- a/src/caelestia/utils/hypr.py +++ b/src/caelestia/utils/hypr.py @@ -2,7 +2,9 @@ import json as j import os import socket -socket_path = f"{os.getenv('XDG_RUNTIME_DIR')}/hypr/{os.getenv('HYPRLAND_INSTANCE_SIGNATURE')}/.socket.sock" +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]: -- cgit v1.2.3-freya