From 6a25b62eca505bd18039bee41aa6725db03676cf Mon Sep 17 00:00:00 2001 From: Seth Mackert Date: Wed, 15 Jan 2025 00:15:36 -0500 Subject: [PATCH] tmp --- src/kshift/conf.py | 136 ++---------------------- src/kshift/main.py | 6 +- src/kshift/theme.py | 253 ++++++++++++++++++++++++++++++++++++++++++++ src/kshift/utils.py | 87 --------------- 4 files changed, 262 insertions(+), 220 deletions(-) create mode 100644 src/kshift/theme.py diff --git a/src/kshift/conf.py b/src/kshift/conf.py index 1fe0092..657b99b 100644 --- a/src/kshift/conf.py +++ b/src/kshift/conf.py @@ -7,136 +7,12 @@ import requests import json -from kshift import utils +from kshift.theme import Theme from pathlib import Path from pydantic import BaseModel, Field, field_validator, model_validator -from typing import Dict, Optional, Union, List - - -class ThemeConfig(BaseModel): - # name: str = Field(description="Name for kshift theme") - colorscheme: Optional[str] = Field( - None, description="The name of the color scheme.") - icontheme: Optional[str] = Field(None, - description="The name of the icon theme.") - wallpaper: Optional[str] = Field( - None, description="Path to the wallpaper image.") - desktoptheme: Optional[str] = Field(None, - description="The desktop theme name.") - command: Optional[str] = Field( - None, description="Command to execute when the theme is applied.") - time: List[Union[str, datetime]] = Field( - [], description="The time when this theme is applied.") - enabled: bool = Field(True, description="Whether the theme is enabled.") - - def kshift(self) -> None: - - if self.wallpaper: - os.system(f"plasma-apply-wallpaperimage {self.wallpaper}") - - if self.colorscheme and self.colorscheme != utils.curr_colorscheme(): - os.system(f"plasma-apply-colorscheme {self.colorscheme}") - - # if self.icontheme and self.icontheme != utils.curr_icontheme(): - if self.icontheme: - plasma_changeicons = utils.find_plasma_changeicons() - if (plasma_changeicons is not None): - os.system(f"{plasma_changeicons} {self.icontheme}") - - if self.desktoptheme and self.desktoptheme != utils.curr_desktoptheme( - ): - os.system(f"plasma-apply-desktoptheme {self.desktoptheme}") - - if self.command: - os.system(self.command) - - @field_validator("colorscheme") - def validate_colorscheme(cls, value): - colorschemes = utils.get_colorschemes() - if value and value not in colorschemes: - raise ValueError( - f"Unknown colorscheme: {value}.\nValid options are {colorschemes}" - ) - else: - return value - - @field_validator("icontheme") - def validate_icontheme(cls, value): - iconthemes = utils.get_iconthemes() - if value and value not in iconthemes: - raise ValueError( - f"Unknown icontheme: {value}.\nValid options are {iconthemes}") - else: - return value - - @field_validator("wallpaper") - def validate_wallpaper(cls, value): - if value: - value = Path(value).expanduser() - if value.exists(): - return value - else: - raise ValueError(f"Wallpaper does not exist: {value}") - - @field_validator("desktoptheme") - def validate_desktoptheme(cls, value): - desktopthemes = utils.get_desktopthemes() - if value and value not in desktopthemes: - raise ValueError( - f"Unknown desktoptheme: {value}.\nValid options are {desktopthemes}" - ) - else: - return value - - @field_validator("time", mode="before") - def parse_time(cls, value): - if isinstance(value, str): - return [value] # Convert single string to a list - elif isinstance(value, list): - if not all(isinstance(item, str) for item in value): - raise ValueError( - "All elements in the 'time' list must be strings.") - return value - else: - raise TypeError("'time' must be a string or a list of strings.") - - @model_validator(mode="after") - def if_disabled(self): - if not self.enabled: - self.time = [] - - return self - - @field_validator("time", mode="after") - def convert_time_strings(cls, times): - new_times = [] - for item in times: - if isinstance(item, str): - if re.match(r'^\d{2}:\d{2}$', item): - # Parse "HH:MM" into a datetime object with today's date - now = datetime.now() - hour, minute = map(int, item.split(':')) - dt = now.replace(hour=hour, - minute=minute, - second=0, - microsecond=0) - # If the time has already passed today, schedule for tomorrow - if dt < now: - dt += timedelta(days=1) - item = dt - - new_times.append(item) - elif isinstance(item, datetime): - new_times.append(item) - else: - raise TypeError( - "Each time entry must be a string in systemd OnCalendar format." - ) - - return new_times - +from typing import Dict defaults = { "latitude": 39, @@ -147,8 +23,8 @@ def convert_time_strings(cls, times): "set_delay": 0, "net_timeout": 10, "themes": { - 'day': ThemeConfig(colorscheme="BreezeLight", time=["sunrise"]), - 'night': ThemeConfig(colorscheme="BreezeDark", time=["sunset"]), + 'day': Theme(colorscheme="BreezeLight", time=["sunrise"]), + 'night': Theme(colorscheme="BreezeDark", time=["sunset"]), } } @@ -190,7 +66,7 @@ class Config(BaseModel): ge=0, le=60, description="Network timeout in seconds, between 0 and 60.") - themes: Dict[str, ThemeConfig] = Field(defaults["themes"], + themes: Dict[str, Theme] = Field(defaults["themes"], description="Dictionary of themes.") # Environment-based paths @@ -229,7 +105,7 @@ class Config(BaseModel): description="Path to the cache file for sunrise and sunset data.") @model_validator(mode="after") - def set_dependant(self): + def set_dependant_paths(self): # Compute dependent paths self.systemd_loc = self.xdg_data / "systemd/user" self.config_loc_base = self.xdg_config / "kshift" diff --git a/src/kshift/main.py b/src/kshift/main.py index 8354fda..526bb1e 100755 --- a/src/kshift/main.py +++ b/src/kshift/main.py @@ -14,7 +14,7 @@ from importlib.resources import files -from kshift.conf import ThemeConfig, load_config +from kshift.conf import Theme, load_config from string import Template @@ -54,7 +54,7 @@ def log_theme_change(theme_name): logging.info(json.dumps(log_data)) # Use JSON for structured logs -def log_element_change(theme: ThemeConfig): +def log_element_change(theme: Theme): log_data = {"event": "specific_change", "theme": str(theme)} logging.info(json.dumps(log_data)) @@ -380,7 +380,7 @@ def theme(theme, colorscheme, icontheme, wallpaper, desktop_theme): if any([colorscheme, icontheme, wallpaper, desktop_theme]): # Apply individual theme elements dynamically - custom_theme = ThemeConfig( + custom_theme = Theme( colorscheme=colorscheme, icontheme=icontheme, wallpaper=wallpaper, diff --git a/src/kshift/theme.py b/src/kshift/theme.py new file mode 100644 index 0000000..187c3ba --- /dev/null +++ b/src/kshift/theme.py @@ -0,0 +1,253 @@ +from datetime import datetime, timedelta +from kshift import utils +import os +import re +import subprocess +import configparser + +from pathlib import Path + +from pydantic import BaseModel, Field, field_validator, model_validator +from typing import Optional, Union, List, Tuple + + +class BaseAttribute(BaseModel): + """Abstract base class for attribute configurations.""" + val: Optional[str] + + command: Optional[str] = None + + available: List[str] = [] + current: Optional[str] = None + + def apply(self): + if self.val and self.val != self.current: + os.system(f"{self.command} {self.val}") + + @classmethod + def fetch_themes(cls, cmd: str, + regex: str) -> Tuple[List[str], Optional[str]]: + """Fetch available and the current theme.""" + + try: + output = subprocess.run(cmd.split(), + capture_output=True, + text=True, + check=True).stdout.strip() + + available = [] + current = None + + for line in output.splitlines(): + match = re.search(regex, line) + if match: + available.append(match.group(1)) + if "current" in line: + current = match.group(1) + + return available, current + except subprocess.CalledProcessError as e: + raise RuntimeError(f"Failed to fetch themes: {e}") + + def init_themes(self, fetch_function): + """Generic initialization for themes.""" + + if not self.available or not self.current: + self.available, self.current = fetch_function() + return self + + @model_validator(mode="after") + def validate_theme(self): + if self.val not in self.available: + raise ValueError( + f"Invalid attribute: {self.val}. Available options are {self.available}." + ) + + return self + + +class Colorscheme(BaseAttribute): + + command = "plasma-apply-colorscheme" + + @classmethod + def fetch_colorschemes(cls) -> Tuple[List[str], Optional[str]]: + """Fetch available colorschemes and the current colorscheme.""" + return BaseAttribute.fetch_themes("{self.command} -l", + r" \* ([A-Za-z]*)") + + @model_validator(mode="after") + def init_colorschemes(self): + """Initialization of colorscheme variables.""" + + BaseAttribute.init_themes(self, Colorscheme.fetch_colorschemes) + return self + + +class IconTheme(BaseAttribute): + + @classmethod + def fetch_iconthemes(cls) -> Tuple[List[str], Optional[str]]: + available = [] + + home_dir = Path.home() + old_icon_dir = home_dir / ".icons" + icon_dir = home_dir / ".local/share/icons" + system_icon_dir = Path("/usr/share/icons") + + for path in [old_icon_dir, icon_dir, system_icon_dir]: + if path.exists(): + available += [ + item.name for item in path.iterdir() if item.is_dir() + ] + + current = "" + + kdeconfig_path = Path.home() / ".config/kdeglobals" + + if kdeconfig_path.exists(): + config = configparser.ConfigParser() + config.read(kdeconfig_path) + current = config.get("Icons", "Theme") + + return available, current + + @model_validator(mode="after") + def init_iconthemes(self): + if not self.command: + + search_paths = [ + "/usr/local/libexec", "/usr/local/lib", "/usr/libexec", + "/usr/lib", "/usr/lib/x86_64-linux-gnu/libexec", + "/usr/lib/aarch64-linux-gnu/libexec" + ] + + for path in search_paths: + executable_path = Path(path) / "plasma-changeicons" + if executable_path.is_file(): + self.command = str(executable_path) + + BaseAttribute.init_themes(self, IconTheme.fetch_iconthemes) + return self + + +class DesktopTheme(BaseAttribute): + + command = "plasma-apply-desktoptheme" + + @classmethod + def fetch_desktopthemes(cls) -> Tuple[List[str], Optional[str]]: + """Fetch available desktopthemes and the current desktoptheme.""" + return BaseAttribute.fetch_themes("{self.cmd} -l", r" \* ([A-Za-z]*)") + + @model_validator(mode="after") + def init_desktopthemes(self): + BaseAttribute.init_themes(self, DesktopTheme.fetch_desktopthemes) + return self + + +class Theme(BaseModel): + colorscheme: Optional[Colorscheme] = None + icontheme: Optional[IconTheme] = None + + wallpaper: Optional[str] = Field( + None, description="Path to the wallpaper image.") + desktoptheme: Optional[str] = Field(None, + description="The desktop theme name.") + command: Optional[str] = Field( + None, description="Command to execute when the theme is applied.") + time: List[Union[str, datetime]] = Field( + [], description="The time when this theme is applied.") + enabled: bool = Field(True, description="Whether the theme is enabled.") + + def kshift(self) -> None: + + if self.wallpaper: + os.system(f"plasma-apply-wallpaperimage {self.wallpaper}") + + for attr in [self.colorscheme, self.desktoptheme, self.icontheme]: + attr.apply() + + if self.command: + os.system(self.command) + + @model_validator(mode="before") + def parse_attributes(cls, values): + mtch = { + "colorscheme": Colorscheme, + "icontheme": IconTheme, + "desktoptheme": DesktopTheme + } + + for attr in mtch.keys(): + if attr in values and isinstance(values[attr], str): + cls = mtch[attr] + values[attr] = cls(val=values[attr]) + + return values + + @field_validator("wallpaper") + def validate_wallpaper(cls, value): + if value: + value = Path(value).expanduser() + if value.exists(): + return value + else: + raise ValueError(f"Wallpaper does not exist: {value}") + + @field_validator("desktoptheme") + def validate_desktoptheme(cls, value): + desktopthemes = utils.get_desktopthemes() + if value and value not in desktopthemes: + raise ValueError( + f"Unknown desktoptheme: {value}.\nValid options are {desktopthemes}" + ) + else: + return value + + @field_validator("time", mode="before") + def parse_time(cls, value): + if isinstance(value, str): + return [value] # Convert single string to a list + elif isinstance(value, list): + if not all(isinstance(item, str) for item in value): + raise ValueError( + "All elements in the 'time' list must be strings.") + return value + else: + raise TypeError("'time' must be a string or a list of strings.") + + @model_validator(mode="after") + def if_disabled(self): + if not self.enabled: + self.time = [] + + return self + + @field_validator("time", mode="after") + def convert_time_strings(cls, times): + new_times = [] + for item in times: + if isinstance(item, str): + if re.match(r'^\d{2}:\d{2}$', item): + # Parse "HH:MM" into a datetime object with today's date + now = datetime.now() + hour, minute = map(int, item.split(':')) + dt = now.replace(hour=hour, + minute=minute, + second=0, + microsecond=0) + # If the time has already passed today, schedule for tomorrow + if dt < now: + dt += timedelta(days=1) + item = dt + + new_times.append(item) + elif isinstance(item, datetime): + new_times.append(item) + else: + raise TypeError( + "Each time entry must be a string in systemd OnCalendar format." + ) + + return new_times diff --git a/src/kshift/utils.py b/src/kshift/utils.py index 5e1d8c0..5d6fbd4 100644 --- a/src/kshift/utils.py +++ b/src/kshift/utils.py @@ -1,92 +1,5 @@ -from datetime import datetime -from pathlib import Path import subprocess import re -import os -import configparser - - -# Gets the names of all available colorschemes -def get_colorschemes(): - arr = [] - - colorscheme_cmd = "plasma-apply-colorscheme -l" - output = subprocess.run(colorscheme_cmd.split(), - capture_output=True, - text=True).stdout.strip() - - for line in output.splitlines(): - r = re.search(" \\* ([A-Za-z]*)", line) - if r: - arr.append(r.group(1)) - - return arr - - -# Gets the current colorscheme -def curr_colorscheme(): - - curr = "" - - colorscheme_cmd = "plasma-apply-colorscheme -l" - output = subprocess.run( - colorscheme_cmd.split(), - stdout=subprocess.PIPE).stdout.decode('utf-8').strip() - - for line in output.splitlines(): - r = re.search(" \\* ([A-Za-z]*) \\(current color scheme\\)", line) - if r: - curr = r.group(1) - break - - return curr - - -def get_iconthemes(): - - arr = [] - - home_dir = Path.home() - old_icon_dir = home_dir / ".icons" - icon_dir = home_dir / ".local/share/icons" - system_icon_dir = Path("/usr/share/icons") - - for path in [old_icon_dir, icon_dir, system_icon_dir]: - if path.exists(): - arr += [item.name for item in path.iterdir() if item.is_dir()] - - return arr - - -# Gets the current icon theme -# TODO FIX, currently broken -def curr_icontheme(): - - curr = "" - kdeconfig_path = f"{os.path.expanduser('~')}/.config/kdeglobals" - - if os.path.exists(kdeconfig_path): - config = configparser.ConfigParser() - config.read(kdeconfig_path) - curr = config["Icons"]["Theme"] - - return curr - - -# Gets the path to plasma-changeicon -def find_plasma_changeicons(): - search_paths = [ - "/usr/local/libexec", "/usr/local/lib", "/usr/libexec", "/usr/lib", - "/usr/lib/x86_64-linux-gnu/libexec", - "/usr/lib/aarch64-linux-gnu/libexec" - ] - - for path in search_paths: - executable_path = os.path.join(path, "plasma-changeicons") - if os.path.isfile(executable_path): - return executable_path - - return None # Gets the names of all available desktop themes