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-04 00:15:59 +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
2025-02-26 14:26:24 +00:00
class _Config :
2025-02-27 11:50:45 +00:00
# Initializes the most important config values.
2024-12-30 02:40:50 +00:00
def __init__ ( self ) - > None :
2025-02-27 11:50:45 +00:00
# Config file
2024-12-31 01:26:53 +00:00
self . config_file : ConfigFile = self . _initialize_config ( ) # Full config
2025-02-27 11:50:45 +00:00
# Config general
valid_general : bool = self . _initialize_general ( )
if not valid_general :
logger . critical ( " The general dictionary was not found or contains errors " )
print ( " CRITICAL: The general dictionary was not found or contains errors " )
raise ConfigError ( " The general dictionary was not found or contains errors " )
# 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. " )
2025-02-27 12:50:58 +00:00
raise ConfigError
2025-02-27 11:50:45 +00:00
# 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. " )
2024-09-01 17:29:04 +00:00
2025-02-26 14:26:24 +00:00
# Read config
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
2025-02-04 00:05:53 +00:00
if util . find_spec ( " pystray " ) is None or util . find_spec ( " PIL " ) is None :
2025-02-03 19:09:26 +00:00
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
def _set_behavior ( self ) - > str :
try :
2025-02-26 14:26:24 +00:00
behavior = self . config_general [ " behavior " ]
2024-12-27 23:16:44 +00:00
except KeyError :
2025-02-26 14:26:24 +00:00
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... " )
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 :
2025-02-27 11:50:45 +00:00
if 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... " )
2024-06-03 19:38:03 +00:00
2025-02-26 14:26:24 +00:00
def _initialize_general ( self ) - > bool :
# Create Config General Dict
try :
self . config_general : ConfigGeneral = self . config_file [ " general " ]
except KeyError :
print ( " CRITICAL: No general dictionary found in Config file. " )
2025-02-27 11:50:45 +00:00
raise ConfigError ( " The general dictionary could not be found in the config, exiting! " )
2025-02-26 14:26:24 +00:00
# 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
2025-02-27 11:50:45 +00:00
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 } " )
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 " )
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 } " )
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 " )
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 } " )
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. " )
self . _set_fallback_wallpaper ( )
return False
# Systray
try :
self . config_systray : bool = self . config_general [ " systray " ]
logger . debug ( f " config_systray has been set to: { self . config_systray } " )
except KeyError :
self . config_systray : bool = True
logger . warning ( " No option systray found in general. Defaulting to true... " )
if self . config_systray :
self . _verify_systray_deps ( )
# Wallpaper behavior
self . config_behavior = self . _set_behavior ( )
# Notifications
try :
self . config_notify : bool = self . config_general [ " notify " ]
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 ' . " )
2025-02-26 14:26:24 +00:00
return True
2025-02-27 11:50:45 +00:00
def _initialize_changing_times ( self ) - > bool :
try :
self . config_changing_times : Dict [ str , str ] = self . config_file [ " changing_times " ]
2025-02-27 12:50:58 +00:00
self . config_total_changing_times : int = len ( self . config_changing_times )
2025-02-27 11:50:45 +00:00
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. " )
2024-06-06 15:47:44 +00:00
return False
2025-02-27 11:50:45 +00:00
return self . _wallpapers_per_set_and_changing_times_match ( )
2024-06-03 19:38:03 +00:00
2025-02-27 11:50:45 +00:00
def _wallpapers_per_set_and_changing_times_match ( 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 ( )
2025-02-27 11:50:45 +00:00
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. " )
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... " )
2025-02-27 11:50:45 +00:00
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. " )
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... " )
2025-02-27 11:50:45 +00:00
print ( f " Dictionary { wallpaper_set } does not have sufficient entries, exciting... " )
2024-12-30 02:40:50 +00:00
return False
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.
2025-02-26 14:26:24 +00:00
class WallpaperLogic ( _Config ) :
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 :
2025-02-04 00:31:35 +00:00
import wallman . wallman_systray as systray
2024-09-01 17:29:04 +00:00
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 ( )