diff --git a/install.py b/install.py index 5203b4b..10eff20 100644 --- a/install.py +++ b/install.py @@ -15,241 +15,18 @@ # along with this program. If not, see . -import colorsys # convert hsl to rgv +import json # working with json files import os # system commands, working with files -import json # colors.json import argparse # command-line options import textwrap # example text in argparse +from scripts import config # folder and files definitions -# folder definitions -temp_folder = "./.temp" -gnome_folder = "gnome-shell" -temp_gnome_folder = f"{temp_folder}/{gnome_folder}" -tweaks_folder = "./tweaks" -themes_folder = "~/.themes" +from scripts.utils import ( + remove_files, # delete already installed Marble theme + hex_to_rgba) # convert HEX to RGBA -# files definitions -gnome_shell_css = f"{temp_gnome_folder}/gnome-shell.css" - - -def generate_file(folder, final_file): - """ - Combines all files in a folder into a single file - :param folder: source folder - :param final_file: location where file will be created - """ - - opened_file = open(final_file, "w") - - for file in os.listdir(folder): - opened_file.write(open(folder + file).read() + '\n') - - opened_file.close() - - -def concatenate_files(file, edit_file): - """ - Merge two files - :param file: file you want to append - :param edit_file: where it will be appended - """ - - open(edit_file, 'a').write('\n' + open(file).read()) - - -def remove_files(): - """ - Delete already installed Marble theme - """ - - paths = (themes_folder, "~/.local/share/themes") - - print("💡 You do not need to delete files if you want to update theme.\n") - - confirmation = input(f"Do you want to delete all \"Marble\" folders in {' and in '.join(paths)}? (y/N) ").lower() - - if confirmation == "y": - for path in paths: - - # Check if the path exists - if os.path.exists(os.path.expanduser(path)): - - # Get the list of folders in the path - folders = os.listdir(os.path.expanduser(path)) - - # toggle if folder has no marble theme - found_folder = False - - for folder in folders: - if folder.startswith("Marble"): - folder_path = os.path.join(os.path.expanduser(path), folder) - print(f"Deleting folder {folder_path}...", end='') - - try: - os.system(f"rm -r {folder_path}") - - except Exception as e: - print(f"Error deleting folder {folder_path}: {e}") - - else: - found_folder = True - print("Done.") - - if not found_folder: - print(f"No folders starting with \"Marble\" found in {path}.") - - else: - print(f"The path {path} does not exist.") - - -def destination_return(path_name, theme_mode): - """ - Copied/modified theme location - :param path_name: color name - :param theme_mode: theme name (light or dark) - :return: copied files' folder location - """ - - return f"{themes_folder}/Marble-{path_name}-{theme_mode}/" - - -def copy_files(source, destination): - """ - Copy files from the source to another directory - :param source: where files will be copied - :param destination: where files will be pasted - """ - - destination_dirs = destination.split("/") # list of folders - loop_create_dirs = f"{destination_dirs[0]}/" - - # traverse through folders and create them - for i in range(1, len(destination_dirs)): - loop_create_dirs += f"{destination_dirs[i]}/" - os.system(f"mkdir -p {loop_create_dirs}") - - os.system(f"cp -aT {source} {destination}") - - -def replace_keywords(file, *args): - """ - Replace file with several keywords - :param file: file name where keywords must be replaced - :param args: (keyword, replacement), (...), ... - """ - - # skip binary files in project - if not file.lower().endswith(('.css', '.scss', '.svg')): - return - - with open(file, "r") as read_file: - content = read_file.read() - - for keyword, replacement in args: - content = content.replace(keyword, replacement) - - with open(file, "w") as write_file: - write_file.write(content) - - -def apply_colors(hue, destination, theme_mode, apply_file, sat=None): - """ - Install accent colors from colors.json to different file - :param hue - :param destination: file directory - :param theme_mode: theme name (light or dark) - :param apply_file: file name - :param sat: color saturation (optional) - """ - - # list of (keyword, replaced value) - replaced_colors = list() - - # colorsys works in range(0, 1) - h = hue / 360 - for element in colors["elements"]: - # if color is has default color and hasn't been replaced - if theme_mode not in colors["elements"][element] and colors["elements"][element]["default"]: - default_element = colors["elements"][element]["default"] - default_color = colors["elements"][default_element][theme_mode] - colors["elements"][element][theme_mode] = default_color - - # convert sla to range(0, 1) - lightness = int(colors["elements"][element][theme_mode]["l"]) / 100 - saturation = int(colors["elements"][element][theme_mode]["s"]) / 100 if sat is None else \ - int(colors["elements"][element][theme_mode]["s"]) * (sat / 100) / 100 - alpha = colors["elements"][element][theme_mode]["a"] - - # convert hsl to rgb and multiply every item - red, green, blue = [int(item * 256) for item in colorsys.hls_to_rgb(h, lightness, saturation)] - - replaced_colors.append((element, f"rgba({red}, {green}, {blue}, {alpha})")) - - # replace colors - replace_keywords(os.path.expanduser(f"{destination}/{apply_file}"), *replaced_colors) - - -def apply_theme(hue, destination, theme_mode, sat=None): - """ - Apply theme to all files listed in "apply-theme-files" (colors.json) - :param hue - :param destination: file directory - :param theme_mode: theme name (light or dark) - :param sat: color saturation (optional) - """ - - for apply_file in os.listdir(f"{temp_gnome_folder}/"): - apply_colors(hue, destination, theme_mode, apply_file, sat=sat) - - -def install_color(hue, name, theme_mode, sat=None): - """ - Copy files and generate theme with different accent color - :param hue - :param name: theme name - :param theme_mode: light or dark mode - :param sat: color saturation (optional) - """ - - print(f"Creating {name} {', '.join(theme_mode)} theme...", end=" ") - - try: - for mode in theme_mode: - destination = destination_return(name, mode) - - copy_files(temp_folder, destination) - apply_theme(hue, f"{destination}/{gnome_folder}", mode, sat=sat) - - except Exception as err: - print("\nError: " + str(err)) - - else: - print("Done.") - - -def hex_to_rgba(hex_color): - """ - Convert hex(a) to rgba - :param hex_color: input value - """ - - try: - if len(hex_color) in range(6, 10): - hex_color = hex_color.lstrip('#') + "ff" - # if is convertable - int(hex_color[:], 16) - else: - raise ValueError - - except ValueError: - raise ValueError(f'Error: Invalid HEX color code: {hex_color}') - - else: - return int(hex_color[0:2], 16), \ - int(hex_color[2:4], 16), \ - int(hex_color[4:6], 16), \ - int(hex_color[6:8], 16) / 255 +from scripts.theme import Theme def main(): @@ -303,12 +80,11 @@ def main(): args = parser.parse_args() - # is used as list because of install_color - mode = [args.mode] if args.mode else ['light', 'dark'] + colors = json.load(open(config.colors_json)) - # move files to temp folder - copy_files(f"./theme/{gnome_folder}/", f"{temp_gnome_folder}") - generate_file(f"./theme/{gnome_folder}_css/", gnome_shell_css) + gnome_shell_theme = Theme("gnome-shell", colors, f"{config.raw_theme_folder}/{config.gnome_folder}", + config.themes_folder, config.temp_folder, + mode=args.mode, is_filled=args.filled) # remove marble theme if args.remove: @@ -316,23 +92,26 @@ def main(): # panel tweaks if args.panel_default_size: - concatenate_files(f"{tweaks_folder}/panel/def-size.css", gnome_shell_css) + with open(f"{config.tweaks_folder}/panel/def-size.css", "r") as f: + gnome_shell_theme += f.read() if args.panel_no_pill: - concatenate_files(f"{tweaks_folder}/panel/no-pill.css", gnome_shell_css) + with open(f"{config.tweaks_folder}/panel/no-pill.css", "r") as f: + gnome_shell_theme += f.read() if args.panel_text_color: - open(f"{temp_gnome_folder}/{gnome_folder}.css", "a") \ - .write(".panel-button,\ + gnome_shell_theme += ".panel-button,\ .clock,\ .clock-display StIcon {\ color: rgba(" + ', '.join(map(str, hex_to_rgba(args.panel_text_color))) + ");\ - }") + }" # dock tweaks if args.launchpad: - concatenate_files(f"{tweaks_folder}/launchpad/launchpad.css", gnome_shell_css) - os.system(f"cp {tweaks_folder}/launchpad/launchpad.png {temp_gnome_folder}/") + with open(f"{config.tweaks_folder}/launchpad/launchpad.css", "r") as f: + gnome_shell_theme += f.read() + + gnome_shell_theme *= f"{config.tweaks_folder}/launchpad/launchpad.png" # color tweaks if args.filled: @@ -352,31 +131,30 @@ def main(): # if saturation already defined in color (gray) sat = colors["colors"][color]["s"] if colors["colors"][color]["s"] is not None else args.sat - install_color(hue, color, mode, sat) + gnome_shell_theme.install(hue, color, sat) elif args.red or args.pink or args.purple or args.blue or args.green or args.yellow or args.gray: + # install selected colors for color in colors["colors"]: if getattr(args, color): # if argument name is in defined colors hue = colors["colors"][color]["h"] # if saturation already defined in color (gray) sat = colors["colors"][color]["s"] if colors["colors"][color]["s"] is not None else args.sat - install_color(hue, color, mode, sat) + gnome_shell_theme.install(hue, color, sat) # custom color elif args.hue: hue = args.hue theme_name = args.name if args.name else f'hue{hue}' # if defined name - install_color(hue, theme_name, mode, args.sat) + gnome_shell_theme.install(hue, theme_name, args.sat) else: print('No arguments or no color arguments specified. Use -h or --help to see the available options.') if __name__ == "__main__": - colors = json.load(open("colors.json")) # used as database for replacing colors - main() - os.system(f"rm -r {temp_folder}") + os.system(f"rm -r {config.temp_folder}") diff --git a/scripts/config.py b/scripts/config.py new file mode 100644 index 0000000..e4603ca --- /dev/null +++ b/scripts/config.py @@ -0,0 +1,12 @@ +# folder definitions +temp_folder = ".temp" +gnome_folder = "gnome-shell" +temp_gnome_folder = f"{temp_folder}/{gnome_folder}" +tweaks_folder = "tweaks" +themes_folder = "~/.themes" +raw_theme_folder = "theme" +scripts_folder = "scripts" + +# files definitions +gnome_shell_css = f"{temp_gnome_folder}/gnome-shell.css" +colors_json = "colors.json" diff --git a/scripts/theme.py b/scripts/theme.py new file mode 100644 index 0000000..f165648 --- /dev/null +++ b/scripts/theme.py @@ -0,0 +1,148 @@ +import os +import shutil +import colorsys # colorsys.hls_to_rgb(h, l, s) + +from . import config # name of folders and files +from .utils import ( + replace_keywords, # replace keywords in file + copy_files, # copy files from source to destination + destination_return, # copied/modified theme location + generate_file) # combine files from folder to one file + + +class Theme: + def __init__(self, theme_type, colors_json, theme_folder, destination_folder, temp_folder, + mode=None, is_filled=False): + """ + Initialize Theme class + :param colors_json: location of a json file with colors + :param theme_type: theme type (gnome-shell, gtk, etc.) + :param theme_folder: raw theme location + :param destination_folder: folder where themes will be installed + :param temp_folder: folder where files will be collected + :param mode: theme mode (light or dark) + :param is_filled: if True, theme will be filled + """ + + self.colors = colors_json + self.temp_folder = f"{temp_folder}/{theme_type}" + self.theme_folder = theme_folder + self.theme_type = theme_type + self.mode = [mode] if mode else ['light', 'dark'] + self.destination_folder = destination_folder + self.main_styles = f"{self.temp_folder}/{theme_type}.css" + + # move files to temp folder + copy_files(self.theme_folder, self.temp_folder) + generate_file(f"{self.theme_folder}_css/", self.main_styles) + + # if theme is filled + if is_filled: + for apply_file in os.listdir(f"{self.temp_folder}/"): + replace_keywords(f"{self.temp_folder}/{apply_file}", + ("BUTTON-COLOR", "ACCENT-FILLED-COLOR"), + ("BUTTON_HOVER", "ACCENT-FILLED_HOVER"), + ("BUTTON_INSENSITIVE", "ACCENT-FILLED_INSENSITIVE"), + ("BUTTON-TEXT-COLOR", "TEXT-BLACK-COLOR"), + ("BUTTON-TEXT_SECONDARY", "TEXT-BLACK_SECONDARY")) + + def __add__(self, other): + """ + Add to main styles another styles + :param other: styles to add + :return: new Theme object + """ + + with open(self.main_styles, 'a') as main_styles: + main_styles.write('\n' + other) + return self + + def __mul__(self, other): + """ + Copy files to temp folder + :param other: file or folder + :return: new Theme object + """ + + if os.path.isfile(other): + shutil.copy(other, self.temp_folder) + else: + shutil.copytree(other, self.temp_folder) + + return self + + def __del__(self): + # delete temp folder + os.system(f"rm -r {self.temp_folder}") + + def __apply_colors(self, hue, destination, theme_mode, apply_file, sat=None): + """ + Install accent colors from colors.json to different file + :param hue + :param destination: file directory + :param theme_mode: theme name (light or dark) + :param apply_file: file name + :param sat: color saturation (optional) + """ + + # list of (keyword, replaced value) + replaced_colors = list() + + # colorsys works in range(0, 1) + h = hue / 360 + for element in self.colors["elements"]: + # if color has default color and hasn't been replaced + if theme_mode not in self.colors["elements"][element] and self.colors["elements"][element]["default"]: + default_element = self.colors["elements"][element]["default"] + default_color = self.colors["elements"][default_element][theme_mode] + self.colors["elements"][element][theme_mode] = default_color + + # convert sla to range(0, 1) + lightness = int(self.colors["elements"][element][theme_mode]["l"]) / 100 + saturation = int(self.colors["elements"][element][theme_mode]["s"]) / 100 if sat is None else \ + int(self.colors["elements"][element][theme_mode]["s"]) * (sat / 100) / 100 + alpha = self.colors["elements"][element][theme_mode]["a"] + + # convert hsl to rgb and multiply every item + red, green, blue = [int(item * 256) for item in colorsys.hls_to_rgb(h, lightness, saturation)] + + replaced_colors.append((element, f"rgba({red}, {green}, {blue}, {alpha})")) + + # replace colors + replace_keywords(os.path.expanduser(f"{destination}/{apply_file}"), *replaced_colors) + + def __apply_theme(self, hue, source, destination, theme_mode, sat=None): + """ + Apply theme to all files in directory + :param hue + :param source + :param destination: file directory + :param theme_mode: theme name (light or dark) + :param sat: color saturation (optional) + """ + + for apply_file in os.listdir(f"{source}/"): + self.__apply_colors(hue, destination, theme_mode, apply_file, sat=sat) + + def install(self, hue, name, sat=None): + """ + Copy files and generate theme with different accent color + :param hue + :param name: theme name + :param sat: color saturation (optional) + """ + + print(f"Creating {name} {', '.join(self.mode)} theme...", end=" ") + + try: + for mode in self.mode: + destination = destination_return(self.destination_folder, name, mode, self.theme_type) + + copy_files(self.temp_folder + '/', destination) + self.__apply_theme(hue, self.temp_folder, destination, mode, sat=sat) + + except Exception as err: + print("\nError: " + str(err)) + + else: + print("Done.") diff --git a/scripts/utils.py b/scripts/utils.py new file mode 100644 index 0000000..87f604d --- /dev/null +++ b/scripts/utils.py @@ -0,0 +1,141 @@ +import os +from . import config # name of folders and files + + +def generate_file(folder, final_file): + """ + Combines all files in a folder into a single file + :param folder: source folder + :param final_file: location where file will be created + """ + + opened_file = open(final_file, "w") + + for file in os.listdir(folder): + opened_file.write(open(folder + file).read() + '\n') + + opened_file.close() + + +def concatenate_files(edit_file, file): + """ + Merge two files + :param edit_file: where it will be appended + :param file: file you want to append + """ + + open(edit_file, 'a').write('\n' + open(file).read()) + + +def remove_files(): + """ + Delete already installed Marble theme + """ + + paths = (config.themes_folder, "~/.local/share/themes") + + print("💡 You do not need to delete files if you want to update theme.\n") + + confirmation = input(f"Do you want to delete all \"Marble\" folders in {' and in '.join(paths)}? (y/N) ").lower() + + if confirmation == "y": + for path in paths: + + # Check if the path exists + if os.path.exists(os.path.expanduser(path)): + + # Get the list of folders in the path + folders = os.listdir(os.path.expanduser(path)) + + # toggle if folder has no marble theme + found_folder = False + + for folder in folders: + if folder.startswith("Marble"): + folder_path = os.path.join(os.path.expanduser(path), folder) + print(f"Deleting folder {folder_path}...", end='') + + try: + os.system(f"rm -r {folder_path}") + + except Exception as e: + print(f"Error deleting folder {folder_path}: {e}") + + else: + found_folder = True + print("Done.") + + if not found_folder: + print(f"No folders starting with \"Marble\" found in {path}.") + + else: + print(f"The path {path} does not exist.") + + +def destination_return(themes_folder, path_name, theme_mode, theme_type): + """ + Copied/modified theme location + :param themes_folder: themes folder location + :param path_name: color name + :param theme_mode: theme name (light or dark) + :return: copied files' folder location + """ + + return f"{themes_folder}/Marble-{path_name}-{theme_mode}/{theme_type}/" + + +def copy_files(source, destination): + """ + Copy files from the source to another directory + :param source: where files will be copied + :param destination: where files will be pasted + """ + + destination = os.path.expanduser(destination) # expand ~ to /home/user + os.makedirs(destination, exist_ok=True) + os.system(f"cp -aT {source} {destination}") + + +def replace_keywords(file, *args): + """ + Replace file with several keywords + :param file: file name where keywords must be replaced + :param args: (keyword, replacement), (...), ... + """ + + # skip binary files in project + if not file.lower().endswith(('.css', '.scss', '.svg')): + return + + with open(file, "r") as read_file: + content = read_file.read() + + for keyword, replacement in args: + content = content.replace(keyword, replacement) + + with open(file, "w") as write_file: + write_file.write(content) + + +def hex_to_rgba(hex_color): + """ + Convert hex(a) to rgba + :param hex_color: input value + """ + + try: + if len(hex_color) in range(6, 10): + hex_color = hex_color.lstrip('#') + "ff" + # if is convertable + int(hex_color[:], 16) + else: + raise ValueError + + except ValueError: + raise ValueError(f'Error: Invalid HEX color code: {hex_color}') + + else: + return int(hex_color[0:2], 16), \ + int(hex_color[2:4], 16), \ + int(hex_color[4:6], 16), \ + int(hex_color[6:8], 16) / 255