diff --git a/pyproject.toml b/pyproject.toml index f3ca632..d9427b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,3 +40,6 @@ Issues = "https://git.entheuer.de/emma/Wallman/issues" [project.scripts] wallman = "wallman.main:main" + +[tool.mypy] +disable_error_code = ["import-untyped", "no-redef"] diff --git a/wallman/main.py b/wallman/main.py index 3aeb0b2..c5ca1fe 100644 --- a/wallman/main.py +++ b/wallman/main.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 from wallman.wallman_lib import WallpaperLogic -def main(): + +def main() -> None: logic: WallpaperLogic = WallpaperLogic() logic.set_wallpaper_by_time() logic.schedule_wallpapers() + if __name__ == "__main__": main() diff --git a/wallman/wallman_classes.py b/wallman/wallman_classes.py index e788616..19b66b6 100644 --- a/wallman/wallman_classes.py +++ b/wallman/wallman_classes.py @@ -7,9 +7,11 @@ linting and type checking go. I should also consider to move some other import here """ + class ConfigError(Exception): pass + class ConfigGeneral(TypedDict): enable_wallpaper_sets: bool used_sets: List[str] @@ -20,6 +22,7 @@ class ConfigGeneral(TypedDict): systray: bool behavior: str + class ConfigFile(TypedDict): general: ConfigGeneral changing_times: Dict[str, str] diff --git a/wallman/wallman_lib.py b/wallman/wallman_lib.py index 9058596..4479bf1 100644 --- a/wallman/wallman_lib.py +++ b/wallman/wallman_lib.py @@ -12,11 +12,12 @@ from wallman.wallman_classes import ConfigError, ConfigGeneral, ConfigFile global logger logger = logging.getLogger("wallman") + class _Config: # Initializes the most important config values. def __init__(self) -> None: # Config file - self.config_file: ConfigFile = self._initialize_config() # Full config + self.config_file: ConfigFile = self._initialize_config() # Full config # Config general valid_general: bool = self._initialize_general() if not valid_general: @@ -26,36 +27,49 @@ class _Config: # Changing times valid_changing_times: bool = self._initialize_changing_times() if not valid_changing_times: - logger.critical("The amount of provided changing times does not match the amount of wallpapers per set, or the dictionary has not been found in the config file.") - print("CRITICAL: The amount of provided changing times does not match the amount of wallpapers per set, or the dictionary has not been found in the config file.") + logger.critical( + "The amount of provided changing times does not match the amount of wallpapers per set, or the dictionary has not been found in the config file." + ) + print( + "CRITICAL: The amount of provided changing times does not match the amount of wallpapers per set, or the dictionary has not been found in the config file." + ) raise ConfigError # Wallpaper sets valid_wallpaper_amount: bool = self._check_wallpaper_amount() if not valid_wallpaper_amount: - raise ConfigError("The amount of wallpapers in a set does not match the amount of wallpapers_per_set provided in general.") + raise ConfigError( + "The amount of wallpapers in a set does not match the amount of wallpapers_per_set provided in general." + ) # Read config def _initialize_config(self) -> ConfigFile: chdir(str(getenv("HOME")) + "/.config/wallman/") try: with open("wallman.toml", "rb") as config_file: - data: ConfigFile = tomllib.load(config_file) #pyright:ignore + data: ConfigFile = tomllib.load(config_file) # type: ignore #pyright:ignore return data except FileNotFoundError: - raise FileNotFoundError("No config file could be found in ~/.config/wallman/wallman.toml") + raise FileNotFoundError( + "No config file could be found in ~/.config/wallman/wallman.toml" + ) except tomllib.TOMLDecodeError as e: print("ERROR: Config could not be parsed: Invalid TOML Syntax") raise e def _verify_systray_deps(self): from importlib import util + if util.find_spec("pystray") is None or util.find_spec("PIL") is None: - logger.error("systray is enabled, but dependencies for the systray couldn't be found. Are pystray and pillow installed?") + logger.error( + "systray is enabled, but dependencies for the systray couldn't be found. Are pystray and pillow installed?" + ) logger.info("Setting self.config_systray to false.") - print("ERROR: systray is enabled, but dependencies for the systray couldn't be found. Are pystray and pillow installed?") + print( + "ERROR: systray is enabled, but dependencies for the systray couldn't be found. Are pystray and pillow installed?" + ) self.config_systray = False - def _set_log_level(self): + def _set_log_level(self) -> None: global logging global logger chdir("/var/log/wallman/") @@ -63,21 +77,38 @@ class _Config: logger.setLevel(numeric_level) if not path.exists("wallman.log"): system("touch wallman.log") - logging.basicConfig(filename="wallman.log", encoding="utf-8", level=numeric_level) + logging.basicConfig( + filename="wallman.log", encoding="utf-8", level=numeric_level + ) def _set_behavior(self) -> str: try: - behavior = self.config_general["behavior"] + behavior: str = self.config_general["behavior"] except KeyError: - logger.warning("There is no wallpaper behavior specified in general, defaulting to fill...") - print("WARNING: There is no wallpaper behavior specified in general, defaulting to fill...") + logger.warning( + "There is no wallpaper behavior specified in general, defaulting to fill..." + ) + print( + "WARNING: There is no wallpaper behavior specified in general, defaulting to fill..." + ) human_behaviors: List[str] = ["plain", "tile", "center", "fill", "max", "scale"] - machine_behaviors: List[str] = ["--bg", "--bg-tile", "--bg-center", "--bg-fill", "--bg-max", "--bg-scale"] + machine_behaviors: List[str] = [ + "--bg", + "--bg-tile", + "--bg-center", + "--bg-fill", + "--bg-max", + "--bg-scale", + ] behavior: str = self.config_general.get("behavior", "--bg-fill").lower() if behavior not in human_behaviors and behavior not in machine_behaviors: - logging.error(f"The value provided for behaviors, {behavior}, is not valid. Defaulting to fill...") - print(f"ERROR: The value provided for behaviors, {behavior}, is not valid. Defaulting to --bg-fill...") + logging.error( + f"The value provided for behaviors, {behavior}, is not valid. Defaulting to fill..." + ) + print( + f"ERROR: The value provided for behaviors, {behavior}, is not valid. Defaulting to --bg-fill..." + ) if behavior not in machine_behaviors: match behavior: @@ -99,12 +130,18 @@ class _Config: def _set_fallback_wallpaper(self) -> None: if self.config_fallback_wallpaper: - successfully_set: int = system(f"feh {self.config_behavior} --no-fehbg {self.config_fallback_wallpaper}") + successfully_set: int = system( + f"feh {self.config_behavior} --no-fehbg {self.config_fallback_wallpaper}" + ) if successfully_set == 0: logger.info("The fallback Wallpaper has been set.") else: - logger.critical("An Error occured and no fallback wallpaper was provided, exiting...") - raise ConfigError("An error occured and no fallback wallpaper has been set, exiting...") + logger.critical( + "An Error occured and no fallback wallpaper was provided, exiting..." + ) + raise ConfigError( + "An error occured and no fallback wallpaper has been set, exiting..." + ) def _initialize_general(self) -> bool: # Create Config General Dict @@ -112,41 +149,67 @@ class _Config: self.config_general: ConfigGeneral = self.config_file["general"] except KeyError: print("CRITICAL: No general dictionary found in Config file.") - raise ConfigError("The general dictionary could not be found in the config, exiting!") + raise ConfigError( + "The general dictionary could not be found in the config, exiting!" + ) # Set up logger. self.config_log_level = self.config_general.get("log_level", "INFO").upper() self._set_log_level() logger.debug(f"Log level has been set to {self.config_log_level}") logger.debug("Logger initialized successfully") # Set up fallback wallpaper - self.config_fallback_wallpaper: str = self.config_general.get("fallback_wallpaper", "/etc/wallman/DefaultFallbackWallpaper.jpg") + self.config_fallback_wallpaper: str = self.config_general.get( + "fallback_wallpaper", "/etc/wallman/DefaultFallbackWallpaper.jpg" + ) logger.debug(f"Set fallback wallpaper: {self.config_fallback_wallpaper}") # Wallpapers per set try: - self.config_wallpapers_per_set: int = self.config_general["wallpapers_per_set"] - logger.debug(f"Set config_wallpapers_per_set to {self.config_wallpapers_per_set}") + self.config_wallpapers_per_set: int = self.config_general[ + "wallpapers_per_set" + ] + logger.debug( + f"Set config_wallpapers_per_set to {self.config_wallpapers_per_set}" + ) except KeyError: - print("CRITICAL: No option wallpapers_per_set provided in the general dictionary. Attempting to set the fallback wallpaper") - logger.critical("No option wallpapers_per_set provided in the general dictionary. Attempting to set the fallback wallpaper") + print( + "CRITICAL: No option wallpapers_per_set provided in the general dictionary. Attempting to set the fallback wallpaper" + ) + logger.critical( + "No option wallpapers_per_set provided in the general dictionary. Attempting to set the fallback wallpaper" + ) self._set_fallback_wallpaper() return False # Are wallpaper sets enabled to begin with? try: - self.config_wallpaper_sets_enabled: bool = self.config_general["enable_wallpaper_sets"] - logger.debug(f"Set config_wallpaper_sets_enabled to {self.config_wallpaper_sets_enabled}") + self.config_wallpaper_sets_enabled: bool = self.config_general[ + "enable_wallpaper_sets" + ] + logger.debug( + f"Set config_wallpaper_sets_enabled to {self.config_wallpaper_sets_enabled}" + ) except KeyError: - logger.critical("No option enable_wallpaper_sets provided in the general dictionary. Attempting to set the fallback wallpaper") - print("CRITICAL: No option enable_wallpaper_sets provided in the general dictionary. Attempting to set the fallback wallpaper") + logger.critical( + "No option enable_wallpaper_sets provided in the general dictionary. Attempting to set the fallback wallpaper" + ) + print( + "CRITICAL: No option enable_wallpaper_sets provided in the general dictionary. Attempting to set the fallback wallpaper" + ) self._set_fallback_wallpaper() return False # Configure used sets if self.config_wallpaper_sets_enabled: try: self.config_used_sets: List[str] = self.config_general["used_sets"] - logger.debug(f"These wallpaper sets are in use: {self.config_used_sets}") + logger.debug( + f"These wallpaper sets are in use: {self.config_used_sets}" + ) except KeyError: - print("CRITICAL: No array used_sets provided in the general dictionary. Attempting to set the fallback wallpaper.") - logger.critical("No array used_sets provided in the general dictionary. Attempting to set the fallback wallpaper.") + print( + "CRITICAL: No array used_sets provided in the general dictionary. Attempting to set the fallback wallpaper." + ) + logger.critical( + "No array used_sets provided in the general dictionary. Attempting to set the fallback wallpaper." + ) self._set_fallback_wallpaper() return False # Systray @@ -166,52 +229,80 @@ class _Config: logger.debug(f"Set config_notify to {self.config_notify}.") except KeyError: self.config_notify: bool = False - logger.warning("notify is not set in dictionary general in the config file, defaulting to 'false'.") + logger.warning( + "notify is not set in dictionary general in the config file, defaulting to 'false'." + ) return True def _initialize_changing_times(self) -> bool: try: - self.config_changing_times: Dict[str, str] = self.config_file["changing_times"] + self.config_changing_times: Dict[str, str] = self.config_file[ + "changing_times" + ] self.config_total_changing_times: int = len(self.config_changing_times) logger.debug(f"Changing times are {self.config_changing_times}") except KeyError: - logger.critical("No dictionary called changing_times has been found in the config file.") - print("CRITICAL: No dictionary called changing_times has been found in the config file.") + logger.critical( + "No dictionary called changing_times has been found in the config file." + ) + print( + "CRITICAL: No dictionary called changing_times has been found in the config file." + ) return False return self._wallpapers_per_set_and_changing_times_match() def _wallpapers_per_set_and_changing_times_match(self) -> bool: # Check if the amount of wallpapers_per_set and given changing times match if self.config_total_changing_times == self.config_wallpapers_per_set: - logger.debug("The amount of changing times and wallpapers per set is set correctly") + logger.debug( + "The amount of changing times and wallpapers per set is set correctly" + ) return True else: try: self._set_fallback_wallpaper() - logger.critical("The amount of changing_times and the amount of wallpapers_per_set does not match, the fallback wallpaper has been set.") - print("CRITICAL: The amount of changing_times and the amount of wallpapers_per_set does not match, the fallback wallpaper has been set.") + logger.critical( + "The amount of changing_times and the amount of wallpapers_per_set does not match, the fallback wallpaper has been set." + ) + print( + "CRITICAL: The amount of changing_times and the amount of wallpapers_per_set does not match, the fallback wallpaper has been set." + ) return False except ConfigError: - logger.critical("The amount of changing times and the amount of wallpapers per set does not match, exiting...") - print("CRITICAL: The amount of changing times and the amount of wallpapers per set does not match, exiting...") - raise ConfigError("The amount of changing times and the amount of wallpapers per set does not match.") + logger.critical( + "The amount of changing times and the amount of wallpapers per set does not match, exiting..." + ) + print( + "CRITICAL: The amount of changing times and the amount of wallpapers per set does not match, exiting..." + ) + raise ConfigError( + "The amount of changing times and the amount of wallpapers per set does not match." + ) def _check_wallpaper_amount(self) -> bool: # This block checks if if each wallpaper set dictionary provides enough wallpapers to satisfy wallpapers_per_set for wallpaper_set in self.config_used_sets: - if len(self.config_file[wallpaper_set]) == self.config_wallpapers_per_set: + if len(self.config_file[wallpaper_set]) == self.config_wallpapers_per_set: # type: ignore logger.debug(f"Dictionary {wallpaper_set} has sufficient values.") return True else: try: self._set_fallback_wallpaper() - logger.error(f"The Dictionary {wallpaper_set} does not have sufficient entries, the fallback wallpaper has been set.") - print(f"ERROR: The Dictionaty {wallpaper_set} does not have sufficient entries, the fallback wallpaper has been set.") + logger.error( + f"The Dictionary {wallpaper_set} does not have sufficient entries, the fallback wallpaper has been set." + ) + print( + f"ERROR: The Dictionaty {wallpaper_set} does not have sufficient entries, the fallback wallpaper has been set." + ) return False except ConfigError: - logger.critical(f"Dictionary {wallpaper_set} does not have sufficient entries, exciting...") - print(f"Dictionary {wallpaper_set} does not have sufficient entries, exciting...") + logger.critical( + f"Dictionary {wallpaper_set} does not have sufficient entries, exciting..." + ) + print( + f"Dictionary {wallpaper_set} does not have sufficient entries, exciting..." + ) return False @@ -220,8 +311,8 @@ class _Config: class WallpaperLogic(_Config): def __init__(self) -> None: super().__init__() - self.wallpaper_list: List[str] = None # pyright: ignore - self.chosen_wallpaper_set: str = None # pyright: ignore + self.wallpaper_list: List[str] = None # type: ignore # pyright: ignore + self.chosen_wallpaper_set: str = None # type: ignore # pyright: ignore # NOTE: This function could be in a different file because it's not needed in the case only 1 wallpaper per set is needed. # Returns a list of a split string that contains a changing time from the config file @@ -232,8 +323,11 @@ class WallpaperLogic(_Config): # NOTE: This could be in a different file because it's not needed in the "Only one wallpaper set" case. def _choose_wallpaper_set(self) -> None: from random import choice as choose_from + self.chosen_wallpaper_set = choose_from(self.config_used_sets) - self.wallpaper_list: List[str] = list(self.config_file[self.chosen_wallpaper_set].values()) + self.wallpaper_list: List[str] = list( + self.config_file[self.chosen_wallpaper_set].values() + ) # type: ignore logger.debug(f"Chose wallpaper set {self.chosen_wallpaper_set}") # NOTE: Same as _clean_times() @@ -249,15 +343,25 @@ class WallpaperLogic(_Config): if code != 0: try: self._set_fallback_wallpaper() - logger.error(f"The wallpaper {self.wallpaper_list[self.current_time_range]} has not been found, the fallback wallpaper has been set. Future wallpapers will still attempted to be set.") - print(f"ERROR: The wallpaper {self.wallpaper_list[self.current_time_range]} has not been found, the fallback wallpaper has been set. Future wallpapers will still attempted to be set.") + logger.error( + f"The wallpaper {self.wallpaper_list[self.current_time_range]} has not been found, the fallback wallpaper has been set. Future wallpapers will still attempted to be set." + ) + print( + f"ERROR: The wallpaper {self.wallpaper_list[self.current_time_range]} has not been found, the fallback wallpaper has been set. Future wallpapers will still attempted to be set." + ) return False except ConfigError: - logger.error(f"The wallpaper {self.wallpaper_list[self.current_time_range]} has not been found and no fallback wallpaper has been set. Future wallpapers will still attempted to be set.") - print(f"ERROR: The wallpaper {self.wallpaper_list[self.current_time_range]} has not been found and no fallback wallpaper has been set. Future wallpapers will still attempted to be set.") + logger.error( + f"The wallpaper {self.wallpaper_list[self.current_time_range]} has not been found and no fallback wallpaper has been set. Future wallpapers will still attempted to be set." + ) + print( + f"ERROR: The wallpaper {self.wallpaper_list[self.current_time_range]} has not been found and no fallback wallpaper has been set. Future wallpapers will still attempted to be set." + ) return False else: - logger.info(f"The wallpaper {self.wallpaper_list[self.current_time_range]} has been set.") + logger.info( + f"The wallpaper {self.wallpaper_list[self.current_time_range]} has been set." + ) return True # NOTE: Add error handling in case libnotify is not installed or notify-send fails for any other reason. @@ -274,14 +378,26 @@ class WallpaperLogic(_Config): if not self.chosen_wallpaper_set: self._choose_wallpaper_set() for time_range in range(self.config_total_changing_times - 1): - self.current_time_range = time_range # Store current time for better debugging output + self.current_time_range = ( + time_range # Store current time for better debugging output + ) clean_time: List[str] = self._clean_times(time_range) clean_time_two: List[str] = self._clean_times(time_range + 1) # HACK on this to make it more readable. This function call is way too long. Consider storing these in a bunch of temporary variables, though keep function length in mind. # HACK on this to see if this logic can be simplified. It's very ugly to check it that way. # Check if the current time is between a given and the following changing time and if so, set that wallpaper. If not, keep trying. - if self._time_in_range(time(int(clean_time[0]), int(clean_time[1]), int(clean_time[2])), time(int(clean_time_two[0]), int(clean_time_two[1]), int(clean_time_two[2])), datetime.now().time()): - exitcode: int = system(f"feh {self.config_behavior} --no-fehbg --quiet {self.wallpaper_list[time_range]}") + if self._time_in_range( + time(int(clean_time[0]), int(clean_time[1]), int(clean_time[2])), + time( + int(clean_time_two[0]), + int(clean_time_two[1]), + int(clean_time_two[2]), + ), + datetime.now().time(), + ): + exitcode: int = system( + f"feh {self.config_behavior} --no-fehbg --quiet {self.wallpaper_list[time_range]}" + ) has_wallpaper_been_set: bool = self._check_system_exitcode(exitcode) # TODO: Add this check to _notify_user. if self.config_notify: @@ -290,7 +406,9 @@ class WallpaperLogic(_Config): else: continue - exitcode: int = system(f"feh {self.config_behavior} --no-fehbg {self.wallpaper_list[-1]}") + exitcode: int = system( + f"feh {self.config_behavior} --no-fehbg {self.wallpaper_list[-1]}" + ) has_wallpaper_been_set: bool = self._check_system_exitcode(exitcode) if self.config_notify: self._notify_user() @@ -300,24 +418,36 @@ class WallpaperLogic(_Config): def schedule_wallpapers(self) -> None: def _schedule_background_wallpapers() -> BackgroundScheduler: from apscheduler.schedulers.background import BackgroundScheduler + scheduler = BackgroundScheduler() # Create a scheduled job for every changing time # NOTE: This should be a function. for changing_time in range(len(self.config_changing_times)): clean_time = self._clean_times(changing_time) - scheduler.add_job(self.set_wallpaper_by_time, trigger=CronTrigger(hour=clean_time[0], minute=clean_time[1], second=clean_time[2])) + scheduler.add_job( + self.set_wallpaper_by_time, + trigger=CronTrigger( + hour=clean_time[0], minute=clean_time[1], second=clean_time[2] + ), + ) scheduler.start() logger.info("The background scheduler has been started.") return scheduler def _schedule_blocking_wallpapers() -> None: from apscheduler.schedulers.blocking import BlockingScheduler + scheduler = BlockingScheduler() # Create a scheduled job for every changing time # NOTE: Thisshould be a function. for changing_time in range(len(self.config_changing_times)): clean_time = self._clean_times(changing_time) - scheduler.add_job(self.set_wallpaper_by_time, trigger=CronTrigger(hour=clean_time[0], minute=clean_time[1], second=clean_time[2])) + scheduler.add_job( + self.set_wallpaper_by_time, + trigger=CronTrigger( + hour=clean_time[0], minute=clean_time[1], second=clean_time[2] + ), + ) logger.info("The blocking scheduler has been started.") scheduler.start() @@ -326,12 +456,30 @@ class WallpaperLogic(_Config): from functools import partial scheduler: BackgroundScheduler = _schedule_background_wallpapers() - menu: systray.Menu = systray.Menu ( - systray.item("Re-Set Wallpaper", partial(systray.set_wallpaper_again, wallpaper_setter=self.set_wallpaper_by_time)), - systray.item("Reroll Wallpapers", partial(systray.reroll_wallpapers, wallpaper_chooser=self._choose_wallpaper_set, wallpaper_setter=self.set_wallpaper_by_time)), - systray.item("Quit", partial(systray.on_quit, shutdown_scheduler=scheduler.shutdown)) + menu: systray.Menu = systray.Menu( + systray.item( + "Re-Set Wallpaper", + partial( + systray.set_wallpaper_again, + wallpaper_setter=self.set_wallpaper_by_time, + ), + ), + systray.item( + "Reroll Wallpapers", + partial( + systray.reroll_wallpapers, + wallpaper_chooser=self._choose_wallpaper_set, + wallpaper_setter=self.set_wallpaper_by_time, + ), + ), + systray.item( + "Quit", + partial(systray.on_quit, shutdown_scheduler=scheduler.shutdown), + ), + ) + icon = systray.Icon( + "wallman_icon", systray.icon_image, "My Tray Icon", menu ) - icon = systray.Icon("wallman_icon", systray.icon_image, "My Tray Icon", menu) icon.run() else: _schedule_blocking_wallpapers() diff --git a/wallman/wallman_systray.py b/wallman/wallman_systray.py index bd29939..d123912 100644 --- a/wallman/wallman_systray.py +++ b/wallman/wallman_systray.py @@ -1,22 +1,28 @@ from os import chdir import logging from PIL import Image -from pystray import Icon, MenuItem as item, Menu # noqa: F401 +from pystray import Icon, MenuItem as item, Menu # noqa: F401 + # Use logger that is also in wallman_lib logger = logging.getLogger("wallman") + # This should always be ran with "set_wallpaper_by_time" as input! -def set_wallpaper_again(icon, item, wallpaper_setter): # noqa: F811 +def set_wallpaper_again(icon, item, wallpaper_setter): # noqa: F811 logging.info("Re-Setting wallpaper due to systray input.") wallpaper_setter() -def reroll_wallpapers(icon, item, wallpaper_chooser, wallpaper_setter): # noqa: F811 - logging.info("Rerolling Wallpaper sets and resetting wallpaper due to systray input") + +def reroll_wallpapers(icon, item, wallpaper_chooser, wallpaper_setter): # noqa: F811 + logging.info( + "Rerolling Wallpaper sets and resetting wallpaper due to systray input" + ) wallpaper_chooser() wallpaper_setter() + # This should always be ran with "scheduler.shutdown" as input! -def on_quit(icon, item, shutdown_scheduler): # noqa: F811 +def on_quit(icon, item, shutdown_scheduler): # noqa: F811 logging.info("Shutting down wallman due to systray input.") shutdown_scheduler() icon.stop() @@ -26,5 +32,9 @@ chdir("/etc/wallman/icons/") try: icon_image: Image.Image = Image.open("systrayIcon.jpg") except FileNotFoundError: - logger.error("/etc/wallman/icons/systrayIcon.jpg has not been found, wallman will launch without a systray.") - print("ERROR: /etc/wallman/icons/systrayIcon.jpg has not been found, wallman will launch without a systray.") + logger.error( + "/etc/wallman/icons/systrayIcon.jpg has not been found, wallman will launch without a systray." + ) + print( + "ERROR: /etc/wallman/icons/systrayIcon.jpg has not been found, wallman will launch without a systray." + )