diff --git a/install.py b/install.py index 50b826f..a7e0c6b 100644 --- a/install.py +++ b/install.py @@ -14,206 +14,34 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import json # working with json files -import argparse # command-line options -import os.path import shutil -import textwrap # example text in argparse -from scripts import config # folder and files definitions -from scripts.tweaks_manager import TweaksManager # load tweaks from files - -from scripts.utils import remove_files # delete already installed Marble theme -from scripts.utils.gnome import apply_gnome_theme # apply theme to GNOME shell - -from scripts.theme import Theme -from scripts.gdm import GlobalTheme - - -def parse_args(colors) -> argparse.Namespace: - """ - Parse command-line arguments - :return: parsed arguments - """ - - # script description - parser = argparse.ArgumentParser(prog="python install.py", - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=textwrap.dedent(''' - Examples: - -a all accent colors, light & dark mode - --all --mode dark all accent colors, dark mode - --purple --mode=light purple accent color, light mode - --hue 150 --name coldgreen custom coldgreen accent color, light & dark mode - --red --green --sat=70 red, green accent colors, 70% of stock saturation - --hue=200 --name=grayblue --sat=50 --mode=dark - custom grayblue accent color, 50% of stock saturation, dark mode - ''')) - - # Default arguments - parser.add_argument('-r', '--remove', action='store_true', help='remove Marble themes') - parser.add_argument('-ri', '--reinstall', action='store_true', help='reinstall Marble themes') - - default_args = parser.add_argument_group('Install default theme') - default_args.add_argument('-a', '--all', action='store_true', help='all available accent colors') - - for color in colors["colors"]: - default_args.add_argument(f'--{color}', action='store_true', help=f'{color} theme only') - - custom_args = parser.add_argument_group('Install custom color theme') - custom_args.add_argument('--hue', type=int, choices=range(0, 361), help='generate theme from Hue prompt', - metavar='(0 - 360)') - custom_args.add_argument('--name', nargs='?', help='theme name (optional)') - - color_styles = parser.add_argument_group("Theme color tweaks") - color_styles.add_argument("--filled", action="store_true", help="make accent color more vibrant") - - color_tweaks = parser.add_argument_group('Optional theme tweaks') - color_tweaks.add_argument('--mode', choices=['light', 'dark'], help='select a specific theme mode to install') - color_tweaks.add_argument('--sat', type=int, choices=range(0, 251), - help='custom color saturation (<100%% - reduce, >100%% - increase)', metavar='(0 - 250)') - - gdm_theming = parser.add_argument_group('GDM theming') - gdm_theming.add_argument('--gdm', action='store_true', help='install GDM theme. \ - Requires root privileges. You must specify a specific color.') - - # Dynamically load arguments from each tweak script - tweaks_manager = TweaksManager() - tweaks_manager.define_arguments(parser) - - return parser.parse_args() - - -def apply_tweaks(args, theme, colors): - """ - Apply theme tweaks - :param args: parsed arguments - :param theme: Theme object - :param colors: colors from colors.json - """ - - tweaks_manager = TweaksManager() - tweaks_manager.apply_tweaks(args, theme, colors) - - -def install_theme(theme, hue, theme_name, sat, gdm=False): - """ - Check if GDM and install theme - :param theme: object to install - :param hue: color hue - :param theme_name: future theme name - :param sat: color saturation - :param gdm: if GDM theme - """ - - if gdm: - theme.install(hue, sat) - else: - theme.install(hue, theme_name, sat) - - -def apply_colors(args, theme, colors, gdm=False): - """ - Apply accent colors to the theme - :param args: parsed arguments - :param theme: Theme object - :param colors: colors from colors.json - :param gdm: if GDM theme - """ - - is_colors = False # check if any color arguments specified - - # if custom color - if args.hue: - hue = args.hue - theme_name = args.name if args.name else f'hue{hue}' - - install_theme(theme, hue, theme_name, args.sat, gdm) - return None - - else: - for color in colors["colors"]: - if args.all or getattr(args, color): - is_colors = True - - hue = colors["colors"][color]["h"] - # if saturation already defined in color (gray) - sat = colors["colors"][color].get("s", args.sat) - - install_theme(theme, hue, color, sat, gdm) - if gdm: - return None - - if not is_colors: - print('No color arguments specified. Use -h or --help to see the available options.') - return 1 - - return None - - -def global_theme(args, colors): - """ - Apply GDM theme - :param args: parsed arguments - :param colors: colors from colors.json - """ - gdm_temp = os.path.join(config.temp_folder, config.gdm_folder) - gdm_theme = GlobalTheme(colors, f"{config.raw_theme_folder}/{config.gnome_folder}", - config.global_gnome_shell_theme, config.gnome_shell_gresource, - gdm_temp, mode=args.mode, is_filled=args.filled) - - if args.remove: - gdm_rm_status = gdm_theme.remove() - if gdm_rm_status == 0: - print("GDM theme removed successfully.") - return - - for theme in gdm_theme.themes: - apply_tweaks(args, theme.theme, colors) - - if apply_colors(args, gdm_theme, colors, gdm=True) is None: - print("\nGDM theme installed successfully.") - print("You need to restart gdm.service to apply changes.") - print("Run \"systemctl restart gdm.service\" to restart GDM.") - - -def local_theme(args, colors): - """ - Apply local theme - :param args: parsed arguments - :param colors: colors from colors.json - """ - - if args.remove or args.reinstall: - remove_files(args, colors["colors"]) - if not args.reinstall: - return - - 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) - - apply_tweaks(args, gnome_shell_theme, colors) - apply_colors(args, gnome_shell_theme, colors) +from scripts import config +from scripts.install import ArgumentsDefiner +from scripts.install.colors_definer import ColorsDefiner +from scripts.install.global_theme_installer import GlobalThemeInstaller +from scripts.install.local_theme_installer import LocalThemeInstaller +from scripts.utils.gnome import apply_gnome_theme def main(): - colors = json.load(open(config.colors_json)) + colors_definer = ColorsDefiner(config.colors_json) + args = ArgumentsDefiner(colors_definer.colors).parse() - args = parse_args(colors) - - if args.gdm: - global_theme(args, colors) + installer_class = GlobalThemeInstaller if args.gdm else LocalThemeInstaller + installer = installer_class(args, colors_definer) + if args.remove or args.reinstall: + installer.remove() else: - local_theme(args, colors) + installer.install() - if args.remove == args.reinstall: - apply_gnome_theme() + if not args.gdm and args.remove == args.reinstall: + apply_gnome_theme() if __name__ == "__main__": try: main() finally: - shutil.rmtree(config.temp_folder, ignore_errors=True) + shutil.rmtree(config.temp_folder, ignore_errors=True) \ No newline at end of file diff --git a/scripts/gdm.py b/scripts/gdm.py index fb8b838..ad4601a 100644 --- a/scripts/gdm.py +++ b/scripts/gdm.py @@ -42,23 +42,8 @@ class GlobalTheme: self.gst = os.path.join(self.destination_folder, self.destination_file) # use backup file if theme is installed self.themes: list[ThemePrepare] = [] - - try: - gnome_version = gnome.gnome_version() - gnome_major = gnome_version.split(".")[0] - if int(gnome_major) >= 44: - self.themes += [ - self.__create_theme("gnome-shell-light", mode='light', should_label=True, is_filled=is_filled), - self.__create_theme("gnome-shell-dark", mode='dark', is_filled=is_filled) - ] - except Exception as e: - print(f"Error: {e}") - print("Using single theme.") - - if not self.themes: - self.themes.append( - self.__create_theme( - "gnome-shell", mode=mode if mode else 'dark', is_filled=is_filled)) + self.is_filled = is_filled + self.mode = mode def __create_theme(self, theme_type, mode=None, should_label=False, is_filled=False): @@ -66,6 +51,7 @@ class GlobalTheme: theme = Theme(theme_type, self.colors_json, self.theme_folder, self.extracted_theme, self.temp_folder, mode=mode, is_filled=is_filled) + theme.prepare() theme_file = os.path.join(self.extracted_theme, f"{theme_type}.css") return ThemePrepare(theme=theme, theme_file=theme_file, should_label=should_label) @@ -117,7 +103,7 @@ class GlobalTheme: gnome_styles = gnome_theme.read() + self.backup_trigger theme.add_to_start(gnome_styles) - def __prepare(self, hue, color, sat=None): + def __generate_themes(self, hue, color, sat=None): """ Generate theme files for gnome-shell-theme.gresource.xml :param hue: color hue @@ -165,6 +151,24 @@ class GlobalTheme: """ + def prepare(self): + try: + gnome_version = gnome.gnome_version() + gnome_major = gnome_version.split(".")[0] + if int(gnome_major) >= 44: + self.themes += [ + self.__create_theme("gnome-shell-light", mode='light', should_label=True, is_filled=self.is_filled), + self.__create_theme("gnome-shell-dark", mode='dark', is_filled=self.is_filled) + ] + except Exception as e: + print(f"Error: {e}") + print("Using single theme.") + + if not self.themes: + self.themes.append( + self.__create_theme( + "gnome-shell", mode=self.mode if self.mode else 'dark', is_filled=self.is_filled)) + def install(self, hue, sat=None): """ Install theme globally @@ -182,7 +186,7 @@ class GlobalTheme: self.__extract() # generate theme files for global theme - self.__prepare(hue, 'Marble', sat) + self.__generate_themes(hue, 'Marble', sat) # generate gnome-shell-theme.gresource.xml with open(f"{self.extracted_theme}/{self.destination_file}.xml", 'w') as gresource_xml: diff --git a/scripts/install/__init__.py b/scripts/install/__init__.py new file mode 100644 index 0000000..71152d8 --- /dev/null +++ b/scripts/install/__init__.py @@ -0,0 +1 @@ +from scripts.install.arguments_definer import ArgumentsDefiner \ No newline at end of file diff --git a/scripts/install/arguments_definer.py b/scripts/install/arguments_definer.py new file mode 100644 index 0000000..c30186b --- /dev/null +++ b/scripts/install/arguments_definer.py @@ -0,0 +1,73 @@ +import argparse +import textwrap +from typing import Any + +from scripts.tweaks_manager import TweaksManager + + +class ArgumentsDefiner: + def __init__(self, colors: dict[str, Any]): + self._parser = argparse.ArgumentParser(prog="python install.py", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=self._get_epilog()) + + self._define_default_arguments() + self._define_color_arguments(colors) + self._define_custom_color_arguments() + self._define_theme_styles_arguments() + self._define_color_tweaks_arguments() + self._define_gdm_arguments() + self._define_tweaks_arguments() + + def parse(self) -> argparse.Namespace: + return self._parser.parse_args() + + @staticmethod + def _get_epilog(): + return textwrap.dedent(''' + Examples: + -a all accent colors, light & dark mode + --all --mode dark all accent colors, dark mode + --purple --mode=light purple accent color, light mode + --hue 150 --name coldgreen custom coldgreen accent color, light & dark mode + --red --green --sat=70 red, green accent colors, 70% of stock saturation + --hue=200 --name=grayblue --sat=50 --mode=dark + custom grayblue accent color, 50% of stock saturation, dark mode + ''') + + def _define_default_arguments(self): + self._parser.add_argument('-r', '--remove', action='store_true', help='remove Marble themes') + self._parser.add_argument('-ri', '--reinstall', action='store_true', help='reinstall Marble themes') + + def _define_color_arguments(self, colors: dict[str, Any]): + default_args = self._parser.add_argument_group('Install default theme') + default_args.add_argument('-a', '--all', action='store_true', help='all available accent colors') + + for color in colors: + default_args.add_argument(f'--{color}', action='store_true', help=f'{color} theme only') + + def _define_custom_color_arguments(self): + custom_args = self._parser.add_argument_group('Install custom color theme') + custom_args.add_argument('--hue', type=int, choices=range(0, 361), help='generate theme from Hue prompt', + metavar='(0 - 360)') + custom_args.add_argument('--name', nargs='?', help='theme name (optional)') + + def _define_theme_styles_arguments(self): + color_styles = self._parser.add_argument_group("Theme color styles") + color_styles.add_argument("--filled", action="store_true", help="make accent color more vibrant") + + def _define_color_tweaks_arguments(self): + color_tweaks = self._parser.add_argument_group('Optional theme tweaks') + color_tweaks.add_argument('--mode', choices=['light', 'dark'], help='select a specific theme mode to install') + color_tweaks.add_argument('--sat', type=int, choices=range(0, 251), + help='custom color saturation (<100%% - reduce, >100%% - increase)', + metavar='(0 - 250)') + + def _define_gdm_arguments(self): + gdm_theming = self._parser.add_argument_group('GDM theming') + gdm_theming.add_argument('--gdm', action='store_true', help='install GDM theme. \ + Requires root privileges. You must specify a specific color.') + + def _define_tweaks_arguments(self): + tweaks_manager = TweaksManager() + tweaks_manager.define_arguments(self._parser) \ No newline at end of file diff --git a/scripts/install/colors_definer.py b/scripts/install/colors_definer.py new file mode 100644 index 0000000..1e46755 --- /dev/null +++ b/scripts/install/colors_definer.py @@ -0,0 +1,17 @@ +import json + + +class ColorsDefiner: + # TODO: Create a class for each replacer + replacers: dict[ str, # ACCENT-COLOR + dict[ str, # default, light/dark + str | dict[str, int] # random string, s/l/a + ] + ] + # TODO: Create a class for each color + colors: dict[str, dict[str, int]] + + def __init__(self, filename): + colors_dict = json.load(open(filename)) + self.replacers = colors_dict["elements"] + self.colors = colors_dict["colors"] diff --git a/scripts/install/global_theme_installer.py b/scripts/install/global_theme_installer.py new file mode 100644 index 0000000..2f75fc1 --- /dev/null +++ b/scripts/install/global_theme_installer.py @@ -0,0 +1,28 @@ +import os + +from scripts import config +from scripts.gdm import GlobalTheme +from scripts.install.theme_installer import ThemeInstaller + + +class GlobalThemeInstaller(ThemeInstaller): + theme: GlobalTheme + + def remove(self): + gdm_rm_status = self.theme.remove() + if gdm_rm_status == 0: + print("GDM theme removed successfully.") + + def _define_theme(self): + gdm_temp = os.path.join(config.temp_folder, config.gdm_folder) + self.theme = GlobalTheme(self.colors, f"{config.raw_theme_folder}/{config.gnome_folder}", + config.global_gnome_shell_theme, config.gnome_shell_gresource, + gdm_temp, mode=self.args.mode, is_filled=self.args.filled) + + def _install_theme(self, hue, theme_name, sat): + self.theme.prepare() + self.theme.install(hue, sat) + + def _apply_tweaks_to_theme(self): + for theme in self.theme.themes: + self._apply_tweaks(theme.theme) \ No newline at end of file diff --git a/scripts/install/local_theme_installer.py b/scripts/install/local_theme_installer.py new file mode 100644 index 0000000..7409e5f --- /dev/null +++ b/scripts/install/local_theme_installer.py @@ -0,0 +1,31 @@ +import os.path + +from scripts import config +from scripts.install.theme_installer import ThemeInstaller +from scripts.theme import Theme +from scripts.utils import remove_files + + +class LocalThemeInstaller(ThemeInstaller): + theme: Theme + + def remove(self): + args = self.args + colors = self.colors.colors + if args.remove or args.reinstall: + remove_files(args, colors) + if not args.reinstall: + return + + def _define_theme(self): + theme_folder = os.path.join(config.raw_theme_folder, config.gnome_folder) + self.theme = Theme("gnome-shell", self.colors, theme_folder, + config.themes_folder, config.temp_folder, + mode=self.args.mode, is_filled=self.args.filled) + + def _install_theme(self, hue, theme_name, sat): + self.theme.prepare() + self.theme.install(hue, theme_name, sat) + + def _apply_tweaks_to_theme(self): + self._apply_tweaks(self.theme) \ No newline at end of file diff --git a/scripts/install/theme_installer.py b/scripts/install/theme_installer.py new file mode 100644 index 0000000..d525436 --- /dev/null +++ b/scripts/install/theme_installer.py @@ -0,0 +1,74 @@ +import argparse + +from scripts.install.colors_definer import ColorsDefiner +from scripts.theme import Theme +from scripts.tweaks_manager import TweaksManager + + +class ThemeInstaller: + theme: Theme + + def __init__(self, args: argparse.Namespace, colors: ColorsDefiner): + self.args = args + self.colors = colors + self.stop_after_first_installed_color = False + self._define_theme() + + def remove(self): + pass + + def install(self): + self.theme.prepare() + self._apply_tweaks_to_theme() + self._apply_colors() + + def _define_theme(self): + pass + + def _install_theme(self, hue, theme_name, sat): + pass + + def _apply_tweaks_to_theme(self): + pass + + def _apply_tweaks(self, theme): + tweaks_manager = TweaksManager() + tweaks_manager.apply_tweaks(self.args, theme, self.colors) + + def _apply_colors(self): + installed_any = False + + if self.args.hue: + installed_any = True + self._apply_custom_color() + else: + installed_any = self._apply_default_color() + + if not installed_any: + raise Exception('No color arguments specified. Use -h or --help to see the available options.') + + def _apply_custom_color(self): + name = self.args.name + hue = self.args.hue + sat = self.args.sat + + theme_name = name if name else f'hue{hue}' + self._install_theme(hue, theme_name, sat) + + def _apply_default_color(self) -> bool: + colors = self.colors.colors + args = self.args + installed_any = False + + for color, values in colors.items(): + if self.args.all or getattr(self.args, color, False): + hue = values.get('h') + sat = values.get('s', args.sat) # if saturation already defined in color (gray) + + self._install_theme(hue, color, sat) + installed_any = True + + if self.stop_after_first_installed_color: + break + + return installed_any \ No newline at end of file diff --git a/scripts/theme.py b/scripts/theme.py index 709a333..43008a0 100644 --- a/scripts/theme.py +++ b/scripts/theme.py @@ -2,6 +2,7 @@ import os import shutil import colorsys # colorsys.hls_to_rgb(h, l, s) +from .install.colors_definer import ColorsDefiner from .utils import ( replace_keywords, # replace keywords in file copy_files, # copy files from source to destination @@ -23,31 +24,14 @@ class Theme: :param is_filled: if True, theme will be filled """ - self.colors = colors_json + self.colors: ColorsDefiner = 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}", self.temp_folder, self.main_styles) - # after generating main styles, remove .css and .versions folders - shutil.rmtree(f"{self.temp_folder}/.css/", ignore_errors=True) - shutil.rmtree(f"{self.temp_folder}/.versions/", ignore_errors=True) - - # 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_ACTIVE", "ACCENT-FILLED_ACTIVE"), - ("BUTTON_INSENSITIVE", "ACCENT-FILLED_INSENSITIVE"), - ("BUTTON-TEXT-COLOR", "TEXT-BLACK-COLOR"), - ("BUTTON-TEXT_SECONDARY", "TEXT-BLACK_SECONDARY")) + self.is_filled = is_filled def __add__(self, other): """ @@ -93,18 +77,18 @@ class Theme: # colorsys works in range(0, 1) h = hue / 360 - for element in self.colors["elements"]: + for element in self.colors.replacers: # 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 + if theme_mode not in self.colors.replacers[element] and self.colors.replacers[element]["default"]: + default_element = self.colors.replacers[element]["default"] + default_color = self.colors.replacers[default_element][theme_mode] + self.colors.replacers[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"] + lightness = int(self.colors.replacers[element][theme_mode]["l"]) / 100 + saturation = int(self.colors.replacers[element][theme_mode]["s"]) / 100 if sat is None else \ + int(self.colors.replacers[element][theme_mode]["s"]) * (sat / 100) / 100 + alpha = self.colors.replacers[element][theme_mode]["a"] # convert hsl to rgb and multiply every item red, green, blue = [int(item * 255) for item in colorsys.hls_to_rgb(h, lightness, saturation)] @@ -127,6 +111,25 @@ class Theme: for apply_file in os.listdir(f"{source}/"): self.__apply_colors(hue, destination, theme_mode, apply_file, sat=sat) + def prepare(self): + # move files to temp folder + copy_files(self.theme_folder, self.temp_folder) + generate_file(f"{self.theme_folder}", self.temp_folder, self.main_styles) + # after generating main styles, remove .css and .versions folders + shutil.rmtree(f"{self.temp_folder}/.css/", ignore_errors=True) + shutil.rmtree(f"{self.temp_folder}/.versions/", ignore_errors=True) + + # if theme is filled + if self.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_ACTIVE", "ACCENT-FILLED_ACTIVE"), + ("BUTTON_INSENSITIVE", "ACCENT-FILLED_INSENSITIVE"), + ("BUTTON-TEXT-COLOR", "TEXT-BLACK-COLOR"), + ("BUTTON-TEXT_SECONDARY", "TEXT-BLACK_SECONDARY")) + def install(self, hue, name, sat=None, destination=None): """ Copy files and generate theme with different accent color diff --git a/scripts/utils/remove_files.py b/scripts/utils/remove_files.py index b3fd73b..a78c602 100644 --- a/scripts/utils/remove_files.py +++ b/scripts/utils/remove_files.py @@ -5,11 +5,12 @@ import argparse import shutil from collections import defaultdict +from typing import Any from .. import config import os -def remove_files(args: argparse.Namespace, colors: dict[str, str]): +def remove_files(args: argparse.Namespace, colors: dict[str, Any]): """Delete already installed Marble theme""" themes = detect_themes(config.themes_folder)