rainmeter.py
"""Module for getting Rainmeter-specific paths."""
import os
import re
import filecmp
import ctypes
# own dependencies
from . import logger
from .path.program_path_provider import get_cached_program_path
from .path.setting_path_provider import get_cached_setting_path
from .path.program_drive_provider import get_cached_program_drive
from .path.plugin_path_provider import get_cached_plugin_path
from .path.addon_path_provider import get_cached_addon_path
from .path.skin_path_provider import get_cached_skin_path
def get_current_path(filepath):
"""Get the value of the #CURRENTPATH# variable for the specified path.
Returns None if the file path is not in the skins folder
"""
filepath = os.path.normpath(filepath)
skinspath = get_cached_skin_path()
if not skinspath or not filepath.startswith(skinspath):
logger.info("current path could not be found because" +
" either the skins path could not be found or the current file" +
" is not located in the skins path.")
return
if os.path.isfile(filepath):
return os.path.dirname(filepath) + "\\"
else:
return filepath + "\\"
def get_root_config_path(filepath):
"""Get the value of the #ROOTCONFIGPATH# variable for the specified path.
Returns None if the path is not in the skins folder
"""
filepath = os.path.normpath(filepath)
skinspath = get_cached_skin_path()
if not skinspath or not filepath.startswith(skinspath):
logger.info("root config path could not be found" +
" because either the skins path could not be found or the" +
" current file is not located in the skins path.")
return
relpath = os.path.relpath(filepath, skinspath)
logger.info(os.path.join(skinspath, relpath.split("\\")[0]) + "\\")
return os.path.join(skinspath, relpath.split("\\")[0]) + "\\"
def get_current_file(filepath):
"""Get the value of the #CURRENTFILE# variable for the specified path.
Returns None if the path is not in the skins folder
"""
filepath = os.path.normpath(filepath)
skinspath = get_cached_skin_path()
if not skinspath or not filepath.startswith(skinspath):
logger.info("current file could not be found because" +
" either the skins path could not be found or the current" +
" file is not located in the skins path.")
return
if os.path.isfile(filepath):
return os.path.basename(filepath)
else:
logger.info("specified path is not a file.")
return
def get_current_config(filepath):
"""Get the value of the #CURRENTCONFIG# variable for the specified path.
Returns None if the path is not in the skins folder
"""
filepath = os.path.normpath(filepath)
skinspath = get_cached_skin_path()
if not skinspath:
logger.info("current config could not be found" +
" because the skins path could not be found.")
return
if not filepath.startswith(skinspath):
logger.info("current config could not be found" +
" because the current file is not located in the skins path.")
"""workaround for symlinks:
It could exists a symlink in the rainmeter skins folder.
Rainmeter detects it correctly but uses the name of the symlink
for the config name."""
logger.info("trying symlink detection to find the skin config.")
skins = os.listdir(skinspath)
for skin in skins:
check = os.path.join(skinspath, skin)
# 0x0400 is the attribute for reparse point https://msdn.microsoft.com/en-us/library/ee332330(VS.85).aspx
symbolic = os.path.isdir(check) and (ctypes.windll.kernel32.GetFileAttributesW(str(check)) & 0x0400) > 0
if symbolic:
logger.info("detected a symbolic link '" + check + "' through reparse pointer in file attributes.")
walked = os.walk(check)
# we walk deeper here because there can also be sub configs hidden in folders like <config/subconfig/config.ini>
# which results the config being <config/subconfig>
# at this point <skin> is probably the <config/
for root, dirs, files in walked:
for file in files:
probe = os.path.join(root, file)
if filecmp.cmp(probe, filepath):
config = os.path.relpath(root, skinspath)
logger.info("found same file '" + file + "'. Recreate interpreted config '" + config + "'.")
return config
# when we found the file, we need to reverse the path to detect the sub configs
# _, probe_path_and_file = os.path.splitdrive(probe)
# _, actual_path_and_file = os.path.splitdrive(filepath)
# prime the algorithm to remove the files
# probe_path, _ = os.path.split(probe_path_and_file)
# actual_path, _ = os.path.split(actual_path_and_file)
# while 1:
# probe_path, probe_folder = os.path.split(probe_path)
# actual_path, actual_folder = os.path.split(actual_path)
# if probe_folder != actual_folder:
# logger.info("Found last not matching folder '" + probe_folder + "' with '" + actual_folder + "'")
# directory = os.path.dirname(probe)
return
# before you get the whole path called from the skin D:\Documents\Rainmeter\Skins\test\test.ini
if os.path.isfile(filepath):
filepath = os.path.dirname(filepath)
# after the file name is trimmed D:\Documents\Rainmeter\Skins\test
# the result is the directory path minus the skin path
# D:\Documents\Rainmeter\Skins\test minus D:\Documents\Rainmeter\Skins\test equals test
# thus the config is called <test>
return os.path.relpath(filepath, skinspath)
def get_resources_path(filepath):
"""Get the value of the #@# variable for the specified path.
Returns None if the path is not in the skins folder
"""
rfp = get_root_config_path(filepath)
if not rfp:
return
logger.info(os.path.join(rfp, "@Resources") + "\\")
return os.path.join(rfp, "@Resources") + "\\"
def replace_variables(string, filepath):
"""Replace Rainmeter built-in variables and Windows environment variables in string.
Replaces occurrences of the following variables in the string:
#CURRENTFILE#
#CURRENTPATH#
#ROOTCONFIGPATH#
#CURRENTCONFIG#
#@#
#SKINSPATH#
#SETTINGSPATH#
#PROGRAMPATH#
#PROGRAMDRIVE#
#ADDONSPATH#
#PLUGINSPATH#
Any Windows environment variables (like %APPDATA%)
filepath must be a skin file located in a subdirectory of the skins folder
"""
variables = {
# lambdas for lazy evaluation
"#CURRENTFILE#": lambda: get_current_file(filepath),
"#CURRENTPATH#": lambda: get_current_path(filepath),
"#ROOTCONFIGPATH#": lambda: get_root_config_path(filepath),
"#CURRENTCONFIG#": lambda: get_current_config(filepath),
"#@#": lambda: get_resources_path(filepath),
"#SKINSPATH#": get_cached_skin_path,
"#SETTINGSPATH#": get_cached_setting_path,
"#PROGRAMPATH#": get_cached_program_path,
"#PROGRAMDRIVE#": get_cached_program_drive,
"#ADDONSPATH#": get_cached_addon_path,
"#PLUGINSPATH#": get_cached_plugin_path
}
pattern = re.compile("(?i)" + "|".join(list(variables.keys())))
# replace Rainmeter variables
repl = pattern.sub(lambda x: variables[x.group().upper()](),
string)
# expand windows environment variables
repl = os.path.expandvars(repl)
return repl
def make_path(string, filepath):
"""Make the string into an absolute path of an existing file or folder.
Replacing Rainmeter built-in variables relative to the file specified in
filepath (see replace_variables()) will return None if the file or folder
doesn't exist, or if string is None or empty.
"""
if not string:
return None
repl = replace_variables(string, filepath)
norm = os.path.normpath(repl)
# For relative paths, try folder of current file first
if not os.path.isabs(norm):
curpath = get_current_path(filepath)
if curpath:
abso = os.path.join(curpath, norm)
else:
abso = os.path.join(os.path.dirname(filepath), norm)
if os.path.exists(abso):
return abso
# if that doesn't work, try relative to skins path
# (for #CURRENTCONFIG#)
abso = os.path.join(get_cached_skin_path(), norm)
if os.path.exists(abso):
return abso
# for absolute paths, try opening containing folder if file does not exist
else:
if os.path.exists(norm):
return norm
if os.path.exists(os.path.dirname(norm)):
return os.path.dirname(norm)
return