2024-06-03 19:38:03 +00:00
from sys import exit
2025-02-03 22:17:53 +00:00
from os import chdir , getenv , system , path
2024-06-03 19:38:03 +00:00
import logging
import tomllib
from datetime import datetime , time
2024-12-31 01:26:53 +00:00
from apscheduler . schedulers . background import BackgroundScheduler
2024-06-03 19:38:03 +00:00
from apscheduler . triggers . cron import CronTrigger
2025-02-03 21:35:51 +00:00
from typing import Dict , List
2024-06-03 19:38:03 +00:00
2025-02-03 23:36:12 +00:00
from wallman . wallman_classes import ConfigError , ConfigGeneral , ConfigFile
2024-12-31 01:26:53 +00:00
2024-10-16 17:39:13 +00:00
# Setup Logging. NOTE: Declaration as a global variable is necessary to ensure correct functionality across multiple modules.
2024-12-27 23:21:27 +00:00
global logger
2024-09-01 17:29:04 +00:00
logger = logging . getLogger ( " wallman " )
2024-06-03 19:38:03 +00:00
class _ConfigLib :
2025-01-01 19:48:25 +00:00
# Initializes the most important config values. TODO: Add handling for the empty config edge case and the FileNotFound case
2024-12-30 02:40:50 +00:00
def __init__ ( self ) - > None :
2024-12-31 01:26:53 +00:00
self . config_file : ConfigFile = self . _initialize_config ( ) # Full config
2024-06-03 19:38:03 +00:00
# Dictionaries
2024-12-31 01:26:53 +00:00
self . config_general : ConfigGeneral = self . config_file [ " general " ]
2024-12-30 02:40:50 +00:00
self . config_changing_times : Dict [ str , str ] = self . config_file [ " changing_times " ]
2024-06-03 19:38:03 +00:00
# Values in Dicts
self . config_wallpaper_sets_enabled : bool = self . config_general [ " enable_wallpaper_sets " ]
2024-12-30 02:40:50 +00:00
self . config_used_sets : List [ str ] = self . config_general [ " used_sets " ]
2024-06-03 19:38:03 +00:00
self . config_wallpapers_per_set : int = self . config_general [ " wallpapers_per_set " ]
self . config_total_changing_times : int = len ( self . config_changing_times )
2024-06-20 15:57:20 +00:00
self . config_log_level : str = self . config_general . get ( " loglevel " , " INFO " ) . upper ( )
2024-12-27 23:16:44 +00:00
self . config_behavior : str = self . _set_behavior ( )
2025-02-03 19:09:26 +00:00
# Setup logging
self . _set_log_level ( )
2024-10-16 17:39:13 +00:00
# HACK: Add a function to handle these try/except blocks cleanlier.
2024-06-03 19:38:03 +00:00
try :
2024-06-09 19:25:26 +00:00
self . config_notify : bool = self . config_general [ " notify " ]
2024-06-03 19:38:03 +00:00
except KeyError :
2024-10-16 17:39:13 +00:00
self . config_notify : bool = False
2024-06-03 19:38:03 +00:00
logger . warning ( " ' notify ' is not set in dictionary general in the config file, defaulting to ' false ' . " )
2024-09-01 17:29:04 +00:00
try :
2024-12-30 02:40:50 +00:00
self . config_systray : bool = self . config_general [ " systray " ]
2024-09-01 17:29:04 +00:00
except KeyError :
2024-12-30 02:40:50 +00:00
self . config_systray : bool = True
2024-09-01 17:29:04 +00:00
logger . warning ( " ' systray ' is not set in the dictionary general in the config file, defaulting to ' true ' . " )
2024-10-16 17:39:13 +00:00
# Setup systray.
2024-09-01 17:29:04 +00:00
if self . config_systray :
2025-02-03 19:09:26 +00:00
self . _verify_systray_deps ( )
2024-09-01 17:29:04 +00:00
2024-10-16 17:39:13 +00:00
# Read config. TODO: Add error handling for the config not found case.
2024-12-31 01:26:53 +00:00
def _initialize_config ( self ) - > ConfigFile :
2024-10-16 17:39:13 +00:00
chdir ( str ( getenv ( " HOME " ) ) + " /.config/wallman/ " )
2024-09-01 17:29:04 +00:00
try :
2025-02-03 19:09:26 +00:00
with open ( " wallman.toml " , " rb " ) as config_file :
data : ConfigFile = tomllib . load ( config_file ) #pyright:ignore
return data
except FileNotFoundError :
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 ( " pillow " ) is None :
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? " )
2024-09-01 17:29:04 +00:00
self . config_systray = False
2024-06-09 19:25:26 +00:00
def _set_log_level ( self ) :
global logging
global logger
2024-09-02 11:16:20 +00:00
chdir ( " /var/log/wallman/ " )
2024-12-30 02:40:50 +00:00
numeric_level : int = getattr ( logging , self . config_log_level , logging . INFO )
2024-06-09 19:25:26 +00:00
logger . setLevel ( numeric_level )
2025-02-03 22:17:53 +00:00
if not path . exists ( " wallman.log " ) :
system ( " touch wallman.log " )
2024-06-09 19:25:26 +00:00
logging . basicConfig ( filename = " wallman.log " , encoding = " utf-8 " , level = numeric_level )
2024-12-27 23:16:44 +00:00
# TODO: Make this all just work inside the try/except block, there is no need for get()
# TODO: Adjust these variable names
def _set_behavior ( self ) - > str :
try :
self . config_general . get ( " behavior " )
except KeyError :
2024-12-30 02:40:50 +00:00
logger . error ( " There is no wallpaper behavior specified in general, defaulting to fill... " )
print ( " ERROR: There is no wallpaper behavior specified in general, defaulting to fill... " )
2024-12-27 23:16:44 +00:00
2025-01-01 19:58:10 +00:00
human_behaviors : List [ str ] = [ " plain " , " tile " , " center " , " fill " , " max " , " scale " ]
2024-12-27 23:16:44 +00:00
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 ( )
2024-12-30 02:40:50 +00:00
if behavior not in human_behaviors and behavior not in machine_behaviors :
2024-12-27 23:16:44 +00:00
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 :
2025-01-01 19:58:10 +00:00
case " plain " :
2024-12-27 23:16:44 +00:00
behavior = " --bg "
case " tile " :
behavior = " --bg-tile "
case " center " :
behavior = " --bg-center "
case " max " :
behavior = " --bg-max "
case " scale " :
behavior = " --bg-scale "
case _ :
behavior = " --bg-fill "
logger . info ( f " The wallpaper behavior ' { behavior } ' has been set. " )
return behavior
2024-12-30 02:40:50 +00:00
def _set_fallback_wallpaper ( self ) - > None :
2024-06-03 19:38:03 +00:00
if self . config_general [ " fallback_wallpaper " ] :
2024-12-27 23:16:44 +00:00
system ( f " feh { self . config_behavior } --no-fehbg { self . config_general [ ' fallback_wallpaper ' ] } " )
2024-06-03 19:38:03 +00:00
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... " )
class ConfigValidity ( _ConfigLib ) :
2024-10-16 17:39:13 +00:00
# TODO: Add handling for the empty config case.
2024-06-03 19:38:03 +00:00
def __init__ ( self ) :
super ( ) . __init__ ( )
2024-12-30 02:40:50 +00:00
def _check_fallback_wallpaper ( self ) - > bool :
2024-12-31 01:26:53 +00:00
# TODO: Make self.config_general["fallback_wallpaper"] a class-wide variable in ConfigLib
2024-06-03 19:38:03 +00:00
if self . config_general [ " fallback_wallpaper " ] :
logger . debug ( " A fallback wallpaper has been defined. " )
2024-06-06 15:47:44 +00:00
return True
2024-06-03 19:38:03 +00:00
else :
logger . warning ( " No fallback wallpaper has been provided. If the config is written incorrectly, the program will not be able to be executed. " )
2024-06-06 15:47:44 +00:00
return False
2024-06-03 19:38:03 +00:00
2024-06-05 13:07:21 +00:00
def _check_wallpapers_per_set_and_changing_times ( self ) - > bool :
2024-06-03 19:38:03 +00:00
# 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 " )
2024-06-05 13:07:21 +00:00
return True
2024-06-03 19:38:03 +00:00
else :
try :
self . _set_fallback_wallpaper ( )
2024-09-02 11:16:20 +00:00
logger . error ( " The amount of changing_times and the amount of wallpapers_per_set does not match, the fallback wallpaper has been set. " )
print ( " ERROR: The amount of changing_times and the amount of wallpapers_per_set does not match, the fallback wallpaper has been set. " )
2024-06-05 13:07:21 +00:00
return False
2024-06-03 19:38:03 +00:00
except ConfigError :
logger . critical ( " The amount of changing times and the amount of wallpapers per set does not match, exiting... " )
raise ConfigError ( " Please provide an amount of changing_times equal to wallpapers_per_set, exiting... " )
2024-06-05 13:07:21 +00:00
def _check_general_validity ( self ) - > bool :
2024-10-16 17:39:13 +00:00
# FIXME!
2025-01-01 19:48:25 +00:00
# TODO: Adjust it to check for the actually required variables existing rather than check if a number of options is set, which is highly error prone.
2024-06-03 19:38:03 +00:00
if len ( self . config_general ) < 3 :
try :
self . _set_fallback_wallpaper ( )
logger . error ( " An insufficient amount of elements has been provided for general, the fallback wallpaper has been set. " )
print ( " ERROR: An insufficient amount of wallpapers has been provided for general, the fallback wallpaper has been set. " )
2024-06-05 13:07:21 +00:00
return False
2024-06-03 19:38:03 +00:00
except ConfigError :
logger . critical ( " An insufficient amount of elements for general has been provided, exiting... " )
raise ConfigError ( " general should have at least 3 elements, exiting... " )
2024-06-05 13:07:21 +00:00
else :
logger . debug ( " A valid amount of options has been provided in general " )
return True
2024-12-30 02:40:50 +00:00
def _check_wallpaper_dicts ( self ) - > bool :
2024-06-03 19:38:03 +00:00
# This block checks if a dictionary for each wallpaper set exists
for wallpaper_set in self . config_used_sets :
if wallpaper_set in self . config_file :
logger . debug ( f " The dictionary { wallpaper_set } has been found in config. " )
2024-06-05 13:07:21 +00:00
return True
2024-10-16 17:39:13 +00:00
# TODO split this into smaller pieces. This goes too deep.
2024-06-03 19:38:03 +00:00
else :
try :
self . _set_fallback_wallpaper ( )
logger . error ( f " The dictionary { wallpaper_set } has not been found in the config, the fallback wallpaper has been set. " )
print ( f " ERROR: The dictionary { wallpaper_set } has not been found in the config, the fallback wallpaper has been set. " )
2024-06-05 13:07:21 +00:00
return False
2024-06-03 19:38:03 +00:00
except ConfigError :
logger . critical ( f " No dictionary { wallpaper_set } has been found in the config exiting... " )
raise ConfigError ( f " The dictionary { wallpaper_set } has not been found in the config, exiting... " )
2024-12-30 02:40:50 +00:00
return False
2024-06-03 19:38:03 +00:00
2024-12-30 02:40:50 +00:00
def _check_wallpaper_amount ( self ) - > bool :
2024-06-03 19:38:03 +00:00
# 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 :
logger . debug ( f " Dictionary { wallpaper_set } has sufficient values. " )
2024-06-05 13:07:21 +00:00
return True
2024-06-03 19:38:03 +00:00
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. " )
2024-06-05 13:07:21 +00:00
return False
2024-06-03 19:38:03 +00:00
except ConfigError :
logger . critical ( f " Dictionary { wallpaper_set } does not have sufficient entries, exciting... " )
raise ConfigError ( f " Dictionary { wallpaper_set } does not have the correct amount of entries, exciting... " )
2024-12-30 02:40:50 +00:00
return False
2024-06-03 19:38:03 +00:00
2024-06-07 20:41:41 +00:00
def validate_config ( self ) - > bool :
2024-10-16 17:39:13 +00:00
# NOTE: Consider changing this to exit(-1)
# HACK: Consider using different exit codes for different errors to help users debug.
2024-06-05 13:07:21 +00:00
if not self . _check_fallback_wallpaper ( ) :
2024-06-06 15:47:44 +00:00
pass
2024-06-05 13:07:21 +00:00
if not self . _check_wallpapers_per_set_and_changing_times ( ) :
exit ( 1 )
if not self . _check_general_validity ( ) :
exit ( 1 )
if not self . _check_wallpaper_dicts ( ) :
exit ( 1 )
if not self . _check_wallpaper_amount ( ) :
exit ( 1 )
2024-06-03 19:38:03 +00:00
logger . debug ( " The config file has been validated successfully (No Errors) " )
2024-06-07 20:41:41 +00:00
return True
2024-06-03 19:38:03 +00:00
2024-10-16 17:39:13 +00:00
# TODO: Improve modularity. See notes inside the class for more details.
# TODO: Ensure functionality and if needed add handling for the 1 wallpaper per set case.
2024-06-03 19:38:03 +00:00
class WallpaperLogic ( _ConfigLib ) :
2024-12-30 02:40:50 +00:00
def __init__ ( self ) - > None :
2024-06-03 19:38:03 +00:00
super ( ) . __init__ ( )
2025-01-26 22:34:46 +00:00
self . wallpaper_list : List [ str ] = None # pyright: ignore
self . chosen_wallpaper_set : str = None # pyright: ignore
2024-06-03 19:38:03 +00:00
2024-10-16 17:39:13 +00:00
# NOTE: This function could be in a different file because it's not needed in the case only 1 wallpaper per set is needed.
2024-06-03 19:38:03 +00:00
# Returns a list of a split string that contains a changing time from the config file
2024-12-30 02:40:50 +00:00
def _clean_times ( self , desired_time : int ) - > List [ str ] :
unclean_times : str = list ( self . config_changing_times . values ( ) ) [ desired_time ]
2024-06-03 19:38:03 +00:00
return unclean_times . split ( " : " )
2024-10-16 17:39:13 +00:00
# NOTE: This could be in a different file because it's not needed in the "Only one wallpaper set" case.
2024-06-03 19:38:03 +00:00
def _choose_wallpaper_set ( self ) - > None :
from random import choice as choose_from
self . chosen_wallpaper_set = choose_from ( self . config_used_sets )
2024-12-30 02:40:50 +00:00
self . wallpaper_list : List [ str ] = list ( self . config_file [ self . chosen_wallpaper_set ] . values ( ) )
2024-06-03 19:38:03 +00:00
logger . debug ( f " Chose wallpaper set { self . chosen_wallpaper_set } " )
2024-10-16 17:39:13 +00:00
# NOTE: Same as _clean_times()
2024-06-03 19:38:03 +00:00
# Verify if a given time is in a given range
2024-12-30 02:40:50 +00:00
def _time_in_range ( self , start : time , end : time , x : time ) - > bool :
2024-06-03 19:38:03 +00:00
if start < = end :
return start < = x < = end
else :
return start < = x or x < end
2024-10-16 17:39:13 +00:00
# NOTE: Potentially add handling for this to be also usable for notify_user and add logging if notify_user fails. Consider adding an argument that is where it's called from and handle accordingly.
2024-12-30 02:40:50 +00:00
def _check_system_exitcode ( self , code : int ) - > bool :
2024-06-05 13:33:02 +00:00
if code != 0 :
try :
2024-06-08 04:36:12 +00:00
self . _set_fallback_wallpaper ( )
2024-06-09 18:19:15 +00:00
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. " )
2024-06-05 13:33:02 +00:00
return False
except ConfigError :
2024-06-09 18:19:15 +00:00
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. " )
2024-06-05 13:33:02 +00:00
return False
else :
2024-06-09 19:25:26 +00:00
logger . info ( f " The wallpaper { self . wallpaper_list [ self . current_time_range ] } has been set. " )
2024-06-05 13:33:02 +00:00
return True
2024-10-16 17:39:13 +00:00
# NOTE: Add error handling in case libnotify is not installed or notify-send fails for any other reason.
# TODO: Add a check whether config[notify] is true or not.
2024-06-03 19:38:03 +00:00
def _notify_user ( self ) :
system ( " notify-send ' Wallman ' ' A new Wallpaper has been set. ' " )
logger . debug ( " Sent desktop notification. " )
2024-10-16 17:39:13 +00:00
# TODO: Clean this up. It's way too large and way too intimidating.
# NOTE: This could be in a different for the case that the user only wants 1 wallpaper per set.
# TODO: Add an way for the user to choose if the wallpaper should scale, fill or otherwise. This needs to be editable in the config file.
2024-06-05 13:33:02 +00:00
def set_wallpaper_by_time ( self ) - > bool :
2024-06-03 19:38:03 +00:00
# Ensure use of a consistent wallpaper set
2025-01-26 22:34:46 +00:00
if not self . chosen_wallpaper_set :
2024-06-03 19:38:03 +00:00
self . _choose_wallpaper_set ( )
for time_range in range ( self . config_total_changing_times - 1 ) :
2024-06-09 18:19:15 +00:00
self . current_time_range = time_range # Store current time for better debugging output
2024-12-30 02:40:50 +00:00
clean_time : List [ str ] = self . _clean_times ( time_range )
clean_time_two : List [ str ] = self . _clean_times ( time_range + 1 )
2024-10-16 17:39:13 +00:00
# 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.
2024-06-03 19:38:03 +00:00
# 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 ( ) ) :
2024-12-30 02:40:50 +00:00
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 )
2024-10-16 17:39:13 +00:00
# TODO: Add this check to _notify_user.
2024-06-03 19:38:03 +00:00
if self . config_notify :
self . _notify_user ( )
2024-06-05 13:33:02 +00:00
return has_wallpaper_been_set
2024-06-03 19:38:03 +00:00
else :
continue
2024-12-30 02:40:50 +00:00
exitcode : int = system ( f " feh { self . config_behavior } --no-fehbg { self . wallpaper_list [ - 1 ] } " )
has_wallpaper_been_set : bool = self . _check_system_exitcode ( exitcode )
2024-06-03 19:38:03 +00:00
if self . config_notify :
self . _notify_user ( )
2024-06-05 13:33:02 +00:00
return has_wallpaper_been_set
2024-06-03 19:38:03 +00:00
2024-10-16 17:39:13 +00:00
# NOTE: Consider avoiding nested functions.
2024-12-30 02:40:50 +00:00
def schedule_wallpapers ( self ) - > None :
def _schedule_background_wallpapers ( ) - > BackgroundScheduler :
2024-09-01 17:29:04 +00:00
from apscheduler . schedulers . background import BackgroundScheduler
scheduler = BackgroundScheduler ( )
# Create a scheduled job for every changing time
2024-10-16 17:39:13 +00:00
# NOTE: This should be a function.
2024-09-01 17:29:04 +00:00
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 . start ( )
logger . info ( " The background scheduler has been started. " )
return scheduler
2024-12-30 02:40:50 +00:00
def _schedule_blocking_wallpapers ( ) - > None :
2024-09-01 17:29:04 +00:00
from apscheduler . schedulers . blocking import BlockingScheduler
scheduler = BlockingScheduler ( )
# Create a scheduled job for every changing time
2024-10-16 17:39:13 +00:00
# NOTE: Thisshould be a function.
2024-09-01 17:29:04 +00:00
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 ] ) )
logger . info ( " The blocking scheduler has been started. " )
2024-09-02 14:56:13 +00:00
scheduler . start ( )
2024-09-01 17:29:04 +00:00
if self . config_systray :
import wallman_systray as systray
from functools import partial
2025-02-03 19:09:26 +00:00
2024-12-30 02:40:50 +00:00
scheduler : BackgroundScheduler = _schedule_background_wallpapers ( )
menu : systray . Menu = systray . Menu (
2024-10-16 16:41:54 +00:00
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 ) )
2024-09-01 17:29:04 +00:00
)
2024-09-02 11:16:20 +00:00
icon = systray . Icon ( " wallman_icon " , systray . icon_image , " My Tray Icon " , menu )
2024-09-01 17:29:04 +00:00
icon . run ( )
else :
_schedule_blocking_wallpapers ( )