From 860cacaa2cbbab0777984f477cec0df51f0ca748 Mon Sep 17 00:00:00 2001 From: Vladyslav Hroshev Date: Mon, 31 Mar 2025 23:53:14 +0300 Subject: [PATCH 1/8] Extracted installation methods, created a new class for theme installers --- install.py | 204 ++-------------------- scripts/gdm.py | 42 +++-- scripts/install/__init__.py | 1 + scripts/install/arguments_definer.py | 73 ++++++++ scripts/install/colors_definer.py | 17 ++ scripts/install/global_theme_installer.py | 28 +++ scripts/install/local_theme_installer.py | 31 ++++ scripts/install/theme_installer.py | 74 ++++++++ scripts/theme.py | 59 ++++--- scripts/utils/remove_files.py | 3 +- 10 files changed, 296 insertions(+), 236 deletions(-) create mode 100644 scripts/install/__init__.py create mode 100644 scripts/install/arguments_definer.py create mode 100644 scripts/install/colors_definer.py create mode 100644 scripts/install/global_theme_installer.py create mode 100644 scripts/install/local_theme_installer.py create mode 100644 scripts/install/theme_installer.py 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) From e5f8662269cd97fcbb2417e102cff8e0b42d0fbc Mon Sep 17 00:00:00 2001 From: Vladyslav Hroshev Date: Tue, 1 Apr 2025 13:23:46 +0300 Subject: [PATCH 2/8] Fixed GDM installation, reworked label_files.py, fixed theme removing --- install.py | 5 ++- scripts/gdm.py | 25 ++++++----- scripts/install/global_theme_installer.py | 8 +++- scripts/install/local_theme_installer.py | 11 +++-- scripts/install/theme_installer.py | 25 +++++++++-- scripts/utils/__init__.py | 1 - scripts/utils/files_labeler.py | 51 +++++++++++++++++++++++ scripts/utils/label_files.py | 44 ------------------- 8 files changed, 100 insertions(+), 70 deletions(-) create mode 100644 scripts/utils/files_labeler.py delete mode 100644 scripts/utils/label_files.py diff --git a/install.py b/install.py index a7e0c6b..1c2f84c 100644 --- a/install.py +++ b/install.py @@ -33,10 +33,11 @@ def main(): if args.remove or args.reinstall: installer.remove() - else: + + if not args.remove: installer.install() - if not args.gdm and args.remove == args.reinstall: + if not args.gdm and not args.remove: apply_gnome_theme() diff --git a/scripts/gdm.py b/scripts/gdm.py index ad4601a..947245b 100644 --- a/scripts/gdm.py +++ b/scripts/gdm.py @@ -2,8 +2,9 @@ import os import subprocess from .theme import Theme -from .utils import label_files, remove_properties, remove_keywords, gnome +from .utils import remove_properties, remove_keywords, gnome from . import config +from .utils.files_labeler import FilesLabeler class ThemePrepare: @@ -11,7 +12,7 @@ class ThemePrepare: Theme object prepared for installation """ - def __init__(self, theme, theme_file, should_label=False): + def __init__(self, theme: Theme, theme_file, should_label=False): self.theme = theme self.theme_file = theme_file self.should_label = should_label @@ -38,7 +39,7 @@ class GlobalTheme: self.backup_file = f"{self.destination_file}.backup" self.backup_trigger = "\n/* Marble theme */\n" # trigger to check if theme is installed - self.extracted_theme = os.path.join(self.temp_folder, config.extracted_gdm_folder) + self.extracted_theme: str = os.path.join(self.temp_folder, config.extracted_gdm_folder) self.gst = os.path.join(self.destination_folder, self.destination_file) # use backup file if theme is installed self.themes: list[ThemePrepare] = [] @@ -46,7 +47,7 @@ class GlobalTheme: self.mode = mode - def __create_theme(self, theme_type, mode=None, should_label=False, is_filled=False): + def __create_theme(self, theme_type: str, mode=None, should_label=False, is_filled=False): """Helper to create theme objects""" theme = Theme(theme_type, self.colors_json, self.theme_folder, self.extracted_theme, self.temp_folder, @@ -111,16 +112,18 @@ class GlobalTheme: :param sat: color saturation """ - for theme in self.themes: - if theme.should_label: - label_files(theme.theme.temp_folder, "light", theme.theme.main_styles) + for theme_prepare in self.themes: + if theme_prepare.should_label: + temp_folder = theme_prepare.theme.temp_folder + main_styles = theme_prepare.theme.main_styles + FilesLabeler(temp_folder, main_styles).append_label("light") - remove_keywords(theme.theme_file, "!important") - remove_properties(theme.theme_file, "background-color", "color", "box-shadow", "border-radius") + remove_keywords(theme_prepare.theme_file, "!important") + remove_properties(theme_prepare.theme_file, "background-color", "color", "box-shadow", "border-radius") - self.__add_gnome_styles(theme.theme) + self.__add_gnome_styles(theme_prepare.theme) - theme.theme.install(hue, color, sat, destination=self.extracted_theme) + theme_prepare.theme.install(hue, color, sat, destination=self.extracted_theme) def __backup(self): diff --git a/scripts/install/global_theme_installer.py b/scripts/install/global_theme_installer.py index 2f75fc1..993b273 100644 --- a/scripts/install/global_theme_installer.py +++ b/scripts/install/global_theme_installer.py @@ -20,9 +20,13 @@ class GlobalThemeInstaller(ThemeInstaller): 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 + self._apply_tweaks(theme.theme) + + def _after_install(self): + print("\nGDM theme installed successfully.") + print("You need to restart gdm.service to apply changes.") + print("Run \"systemctl restart gdm.service\" to restart GDM.") \ No newline at end of file diff --git a/scripts/install/local_theme_installer.py b/scripts/install/local_theme_installer.py index 7409e5f..1d20451 100644 --- a/scripts/install/local_theme_installer.py +++ b/scripts/install/local_theme_installer.py @@ -10,12 +10,8 @@ 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 + remove_files(self.args, colors) def _define_theme(self): theme_folder = os.path.join(config.raw_theme_folder, config.gnome_folder) @@ -28,4 +24,7 @@ class LocalThemeInstaller(ThemeInstaller): self.theme.install(hue, theme_name, sat) def _apply_tweaks_to_theme(self): - self._apply_tweaks(self.theme) \ No newline at end of file + self._apply_tweaks(self.theme) + + def _after_install(self): + print("\nTheme installed successfully.") \ No newline at end of file diff --git a/scripts/install/theme_installer.py b/scripts/install/theme_installer.py index d525436..eef91d4 100644 --- a/scripts/install/theme_installer.py +++ b/scripts/install/theme_installer.py @@ -1,11 +1,13 @@ import argparse +from abc import ABC, abstractmethod from scripts.install.colors_definer import ColorsDefiner from scripts.theme import Theme from scripts.tweaks_manager import TweaksManager -class ThemeInstaller: +class ThemeInstaller(ABC): + """Base class for theme installers""" theme: Theme def __init__(self, args: argparse.Namespace, colors: ColorsDefiner): @@ -14,24 +16,39 @@ class ThemeInstaller: self.stop_after_first_installed_color = False self._define_theme() + @abstractmethod def remove(self): + """Method for removing already installed themes""" pass def install(self): self.theme.prepare() self._apply_tweaks_to_theme() self._apply_colors() + self._after_install() + @abstractmethod def _define_theme(self): + """Here is the place to define the theme object""" pass - def _install_theme(self, hue, theme_name, sat): - pass - + @abstractmethod def _apply_tweaks_to_theme(self): + """Should apply the tweaks for prepared theme""" + pass + + @abstractmethod + def _install_theme(self, hue, theme_name, sat): + """Should say how to install the defined theme""" + pass + + @abstractmethod + def _after_install(self): + """Method to be called after the theme is installed. Can be used for logging or other actions""" pass def _apply_tweaks(self, theme): + """This method should be called in the _apply_tweaks_to_theme method""" tweaks_manager = TweaksManager() tweaks_manager.apply_tweaks(self.args, theme, self.colors) diff --git a/scripts/utils/__init__.py b/scripts/utils/__init__.py index 2ae0305..786b174 100644 --- a/scripts/utils/__init__.py +++ b/scripts/utils/__init__.py @@ -3,7 +3,6 @@ from .copy_files import copy_files from .destinaiton_return import destination_return from .generate_file import generate_file from .hex_to_rgba import hex_to_rgba -from .label_files import label_files from .remove_files import remove_files from .remove_keywords import remove_keywords from .remove_properties import remove_properties diff --git a/scripts/utils/files_labeler.py b/scripts/utils/files_labeler.py new file mode 100644 index 0000000..1b23514 --- /dev/null +++ b/scripts/utils/files_labeler.py @@ -0,0 +1,51 @@ +import os + +type LabeledFileGroup = tuple[str, str] + +class FilesLabeler: + def __init__(self, directory: str, *args: str): + """ + Initialize the working directory and files to change + """ + self.directory = directory + self.files = args + + def append_label(self, label: str): + """ + Append a label to all files in the directory + and update references in the files + """ + labeled_files = self._label_files(label) + self._update_references(labeled_files) + + def _label_files(self, label: str) -> list[LabeledFileGroup]: + labeled_files = [] + for filename in os.listdir(self.directory): + if label in filename: continue + + name, extension = os.path.splitext(filename) + new_filename = f"{name}-{label}{extension}" + + old_filepath = os.path.join(self.directory, filename) + new_filepath = os.path.join(self.directory, new_filename) + os.rename(old_filepath, new_filepath) + + labeled_files.append((filename, new_filename)) + return labeled_files + + def _update_references(self, labeled_files: list[LabeledFileGroup]): + for file_path in self.files: + with open(file_path, 'r') as file: + file_content = file.read() + + file_content = self._update_references_in_file(file_content, labeled_files) + + with open(file_path, 'w') as file: + file.write(file_content) + + @staticmethod + def _update_references_in_file(file_content: str, labeled_files: list[LabeledFileGroup]) -> str: + replaced_content = file_content + for old_name, new_name in labeled_files: + replaced_content = replaced_content.replace(old_name, new_name) + return replaced_content \ No newline at end of file diff --git a/scripts/utils/label_files.py b/scripts/utils/label_files.py deleted file mode 100644 index 801b4d5..0000000 --- a/scripts/utils/label_files.py +++ /dev/null @@ -1,44 +0,0 @@ -import os - -def label_files(directory, label, *args): - """ - Add a label to all files in a directory - :param directory: folder where files are located - :param label: label to add - :param args: files to change links to labeled files - :return: - """ - - # Open all files - files = [open(file, 'r') for file in args] - read_files = [] - - filenames = [] - - for filename in os.listdir(directory): - # Skip if the file is already labeled - if label in filename: - continue - - # Split the filename into name and extension - name, extension = os.path.splitext(filename) - - # Form the new filename and rename the file - new_filename = f"{name}-{label}{extension}" - os.rename(os.path.join(directory, filename), os.path.join(directory, new_filename)) - - filenames.append((filename, new_filename)) - - # Replace the filename in all files - for i, file in enumerate(files): - read_file = file.read() - read_file.replace(filenames[i][0], filenames[i][1]) - read_files.append(read_file) - file.close() - - write_files = [open(file, 'w') for file in args] - - # Write the changes to the files and close them - for i, file in enumerate(write_files): - file.write(read_files[i]) - file.close() From 038c2fcda1f8fb991f094d56739122ecbd481363 Mon Sep 17 00:00:00 2001 From: Vladyslav Hroshev Date: Tue, 1 Apr 2025 19:11:06 +0300 Subject: [PATCH 3/8] Run installation concurrently (Unexpected behavior), class for managing console lines --- scripts/install/theme_installer.py | 22 +++-- scripts/theme.py | 154 +++++++++++++++-------------- scripts/utils/console.py | 92 +++++++++++++++++ scripts/utils/copy_files.py | 7 +- 4 files changed, 190 insertions(+), 85 deletions(-) create mode 100644 scripts/utils/console.py diff --git a/scripts/install/theme_installer.py b/scripts/install/theme_installer.py index eef91d4..0e6ae90 100644 --- a/scripts/install/theme_installer.py +++ b/scripts/install/theme_installer.py @@ -1,4 +1,5 @@ import argparse +import concurrent.futures from abc import ABC, abstractmethod from scripts.install.colors_definer import ColorsDefiner @@ -75,17 +76,24 @@ class ThemeInstaller(ABC): def _apply_default_color(self) -> bool: colors = self.colors.colors args = self.args - installed_any = False + colors_to_install = [] for color, values in colors.items(): - if self.args.all or getattr(self.args, color, False): + if args.all or getattr(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 + sat = values.get('s', args.sat) + colors_to_install.append((hue, color, sat)) if self.stop_after_first_installed_color: break - return installed_any \ No newline at end of file + if not colors_to_install: + return False + self._run_concurrent_installation(colors_to_install) + return True + + def _run_concurrent_installation(self, colors_to_install): + with concurrent.futures.ThreadPoolExecutor() as executor: + futures = [executor.submit(self._install_theme, hue, color, sat) + for hue, color, sat in colors_to_install] + concurrent.futures.wait(futures) \ No newline at end of file diff --git a/scripts/theme.py b/scripts/theme.py index 43008a0..05573de 100644 --- a/scripts/theme.py +++ b/scripts/theme.py @@ -8,6 +8,7 @@ from .utils import ( copy_files, # copy files from source to destination destination_return, # copied/modified theme location generate_file) # combine files from folder to one file +from .utils.console import Console, Color, Format class Theme: @@ -28,7 +29,7 @@ class Theme: 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.modes = [mode] if mode else ['light', 'dark'] self.destination_folder = destination_folder self.main_styles = f"{self.temp_folder}/{theme_type}.css" self.is_filled = is_filled @@ -58,9 +59,81 @@ class Theme: return self - def __del__(self): - # delete temp folder - shutil.rmtree(self.temp_folder, ignore_errors=True) + 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 add_to_start(self, content): + """ + Add content to the start of main styles + :param content: content to add + """ + + with open(self.main_styles, 'r') as main_styles: + main_content = main_styles.read() + + with open(self.main_styles, 'w') as main_styles: + main_styles.write(content + '\n' + main_content) + + def install(self, hue, name: str, sat=None, destination=None): + """ + Copy files and generate theme with specified accent color + :param hue + :param name: theme name + :param sat + :param destination: folder where theme will be installed + """ + + joint_modes = f"({', '.join(self.modes)})" + + line = Console.Line(name) + formatted_name = Console.format(name.capitalize(), color=Color.get(name), format_type=Format.BOLD) + formatted_mode = Console.format(joint_modes, color=Color.GRAY) + line.update(f"Creating {formatted_name} {formatted_mode} theme...") + + try: + self._install_and_apply_theme(hue, name, sat=sat, destination=destination) + line.success(f"{formatted_name} {formatted_mode} theme created successfully.") + + except Exception as err: + line.error(f"Error installing {formatted_name} theme: {str(err)}") + + def _install_and_apply_theme(self, hue, name, sat=None, destination=None): + is_dest = bool(destination) + for mode in self.modes: + if not is_dest: + 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) + + 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 __apply_colors(self, hue, destination, theme_mode, apply_file, sat=None): """ @@ -96,75 +169,4 @@ class Theme: 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 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 - :param hue - :param name: theme name - :param sat: color saturation (optional) - :param destination: folder where theme will be installed - """ - - is_dest = bool(destination) - - print(f"Creating {name} {', '.join(self.mode)} theme...", end=" ") - - try: - for mode in self.mode: - if not is_dest: - 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.") - - def add_to_start(self, content): - """ - Add content to the start of main styles - :param content: content to add - """ - - with open(self.main_styles, 'r') as main_styles: - main_content = main_styles.read() - - with open(self.main_styles, 'w') as main_styles: - main_styles.write(content + '\n' + main_content) + replace_keywords(os.path.expanduser(f"{destination}/{apply_file}"), *replaced_colors) \ No newline at end of file diff --git a/scripts/utils/console.py b/scripts/utils/console.py new file mode 100644 index 0000000..1257238 --- /dev/null +++ b/scripts/utils/console.py @@ -0,0 +1,92 @@ +import sys +import threading +from enum import Enum +from typing import Optional + + +class Console: + """Manages console output for concurrent processes with line tracking""" + _print_lock = threading.Lock() + _line_mapping = {} + _next_line = 0 + + class Line: + def __init__(self, name): + """Initialize a new managed line""" + self.name = name + self._reserve_line() + + def update(self, message, icon="⏳"): + """Update the status message for the line""" + with Console._print_lock: + # Calculate how many lines to move up + lines_up = Console._next_line - Console._line_mapping[self.name] + # Move the cursor to correct line + if lines_up > 0: + sys.stdout.write(f"\033[{lines_up}F") + # Clear line and write status + sys.stdout.write(f"\r\033[K{icon} {message}") + # Move the cursor back down + if lines_up > 0: + sys.stdout.write(f"\033[{lines_up}E") + sys.stdout.flush() + + def success(self, message): + self.update(message, "✅") + + def error(self, message): + self.update(message, "❌") + + def warn(self, message): + self.update(message, "⚠️") + + def _reserve_line(self): + """Reserve a line for future updates""" + with Console._print_lock: + line_number = Console._next_line + Console._next_line += 1 + Console._line_mapping[self.name] = line_number + sys.stdout.write(f"\n") # Ensure we have a new line + sys.stdout.flush() + return line_number + + @staticmethod + def format(text: str, color: Optional['Color']=None, format_type: Optional['Format']=None): + """Apply color and formatting to text""" + formatted_text = text + + if color: + formatted_text = color.value + formatted_text + Color.NORMAL.value + if format_type: + formatted_text = format_type.value + formatted_text + Format.NORMAL.value + + return formatted_text + + +class Color(Enum): + """ANSI color codes for terminal output""" + NORMAL = '\033[0m' # Reset color + BLACK = '\033[30m' + RED = '\033[31m' + GREEN = '\033[32m' + YELLOW = '\033[33m' + BLUE = '\033[34m' + MAGENTA = '\033[35m' + PURPLE = '\033[35m' + CYAN = '\033[36m' + WHITE = '\033[37m' + GRAY = '\033[90m' + + @classmethod + def get(cls, color: str) -> Optional['Color']: + return getattr(cls, color.upper(), None) + + +class Format(Enum): + """ANSI formatting codes for terminal output""" + NORMAL = '\033[0m' # Reset formatting + BOLD = '\033[1m' + ITALIC = '\033[3m' + UNDERLINE = '\033[4m' + BLINK = '\033[5m' + REVERSE = '\033[7m' \ No newline at end of file diff --git a/scripts/utils/copy_files.py b/scripts/utils/copy_files.py index 6eb239f..1839fb7 100644 --- a/scripts/utils/copy_files.py +++ b/scripts/utils/copy_files.py @@ -1,4 +1,6 @@ import os +import shutil + def copy_files(source, destination): """ @@ -7,6 +9,7 @@ def copy_files(source, destination): :param destination: where files will be pasted """ - destination = os.path.expanduser(destination) # expand ~ to /home/user + destination = os.path.expanduser(destination) os.makedirs(destination, exist_ok=True) - os.system(f"cp -aT {source} {destination}") + + shutil.copytree(source, destination, dirs_exist_ok=True) From e46181e19d8300640241fd716fe4a19b041815c5 Mon Sep 17 00:00:00 2001 From: Vladyslav Hroshev Date: Tue, 1 Apr 2025 19:34:00 +0300 Subject: [PATCH 4/8] Fixed concurrent installation, replaced misspelled class - Removed redundant theme.prepare() in LocalThemeInstaller._install_theme() (already calls in abstract ThemeInstaller); - Replaced misspelled class in messages.css --- scripts/install/local_theme_installer.py | 1 - theme/gnome-shell/.css/messages.css | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/install/local_theme_installer.py b/scripts/install/local_theme_installer.py index 1d20451..a5c9812 100644 --- a/scripts/install/local_theme_installer.py +++ b/scripts/install/local_theme_installer.py @@ -20,7 +20,6 @@ class LocalThemeInstaller(ThemeInstaller): 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): diff --git a/theme/gnome-shell/.css/messages.css b/theme/gnome-shell/.css/messages.css index 94031e9..6f89f53 100644 --- a/theme/gnome-shell/.css/messages.css +++ b/theme/gnome-shell/.css/messages.css @@ -101,7 +101,7 @@ /* that's much better than adding "margin: $base_padding * 0.5;" to .message-close-button */ .message-close-button { margin: 0; } .message-header:ltr > :last-child { margin-right: 2px; } -.messahe-header:rtl > :first-child { margin-left: 2px; } +.message-header:rtl > :first-child { margin-left: 2px; } /* close button, expand button (46+) */ .message-close-button, From 627e5b16ce264756365c409bd3a737429596050b Mon Sep 17 00:00:00 2001 From: Vladyslav Hroshev Date: Tue, 1 Apr 2025 21:16:28 +0300 Subject: [PATCH 5/8] Formatted logs --- scripts/config.py | 2 + scripts/gdm.py | 41 ++++++------ scripts/install/global_theme_installer.py | 13 +++- scripts/install/local_theme_installer.py | 5 +- scripts/utils/console.py | 13 ++-- scripts/utils/gnome.py | 52 ++++++++++----- scripts/utils/parse_folder.py | 11 ++++ scripts/utils/remove_files.py | 77 +++++++++++------------ 8 files changed, 133 insertions(+), 81 deletions(-) create mode 100644 scripts/utils/parse_folder.py diff --git a/scripts/config.py b/scripts/config.py index 952fe19..83c9732 100644 --- a/scripts/config.py +++ b/scripts/config.py @@ -19,3 +19,5 @@ extracted_gdm_folder = "theme" gnome_shell_css = f"{temp_gnome_folder}/gnome-shell.css" tweak_file = f"./{tweaks_folder}/*/tweak.py" colors_json = "colors.json" + +user_themes_extension = "/org/gnome/shell/extensions/user-theme/name" diff --git a/scripts/gdm.py b/scripts/gdm.py index 947245b..4a5104d 100644 --- a/scripts/gdm.py +++ b/scripts/gdm.py @@ -4,6 +4,7 @@ import subprocess from .theme import Theme from .utils import remove_properties, remove_keywords, gnome from . import config +from .utils.console import Console, Color, Format from .utils.files_labeler import FilesLabeler @@ -69,7 +70,8 @@ class GlobalTheme: """ Extract gresource files to temp folder """ - print("Extracting gresource files...") + extract_line = Console.Line() + extract_line.update("Extracting gresource files...") resources = subprocess.getoutput(f"gresource list {self.gst}").split("\n") prefix = "/org/gnome/shell/" @@ -84,6 +86,8 @@ class GlobalTheme: with open(output_path, 'wb') as f: subprocess.run(["gresource", "extract", self.gst, resource], stdout=f, check=True) + extract_line.success("Extracted gresource files.") + except FileNotFoundError as e: if "gresource" in str(e): print("Error: 'gresource' command not found.") @@ -127,22 +131,16 @@ class GlobalTheme: def __backup(self): - """ - Backup installed theme - """ - if self.__is_installed(): return - # backup installed theme - print("Backing up default theme...") + backup_line = Console.Line() + + backup_line.update("Backing up default theme...") subprocess.run(["cp", "-aT", self.gst, f"{self.gst}.backup"], cwd=self.destination_folder, check=True) + backup_line.success("Backed up default theme.") def __generate_gresource_xml(self): - """ - Generates.gresource.xml - """ - # list of files to add to gnome-shell-theme.gresource.xml files = [f"{file}" for file in os.listdir(self.extracted_theme)] nl = "\n" # fstring doesn't support newline character @@ -197,21 +195,23 @@ class GlobalTheme: gresource_xml.write(generated_xml) # compile gnome-shell-theme.gresource.xml - print("Compiling theme...") + compile_line = Console.Line() + compile_line.update("Compiling gnome-shell theme...") subprocess.run(["glib-compile-resources" , f"{self.destination_file}.xml"], cwd=self.extracted_theme, check=True) + compile_line.success("Theme compiled.") # backup installed theme self.__backup() # install theme - print("Installing theme...") + install_line = Console.Line() + install_line.update("Moving compiled theme to system folder...") subprocess.run(["sudo", "cp", "-f", f"{self.extracted_theme}/{self.destination_file}", f"{self.destination_folder}/{self.destination_file}"], check=True) - - print("Theme installed successfully.") + install_line.success("Theme moved to system folder.") def remove(self): @@ -221,16 +221,19 @@ class GlobalTheme: # use backup file if theme is installed if self.__is_installed(): - print("Theme is installed. Removing...") + removing_line = Console.Line() + removing_line.update("Theme is installed. Removing...") backup_path = os.path.join(self.destination_folder, self.backup_file) dest_path = os.path.join(self.destination_folder, self.destination_file) if os.path.isfile(backup_path): subprocess.run(["sudo", "mv", backup_path, dest_path], check=True) + removing_line.success("Global theme removed successfully. Restart GDM to apply changes.") else: - print("Backup file not found. Try reinstalling gnome-shell package.") + formatted_shell = Console.format("gnome-shell", color=Color.BLUE, format_type=Format.BOLD) + removing_line.error(f"Backup file not found. Try reinstalling {formatted_shell} package.") else: - print("Theme is not installed. Nothing to remove.") - print("If theme is still installed globally, try reinstalling gnome-shell package.") + Console.Line().error("Theme is not installed. Nothing to remove.") + Console.Line().update("If theme is still installed globally, try reinstalling gnome-shell package.", icon="⚠️") diff --git a/scripts/install/global_theme_installer.py b/scripts/install/global_theme_installer.py index 993b273..a45a341 100644 --- a/scripts/install/global_theme_installer.py +++ b/scripts/install/global_theme_installer.py @@ -3,6 +3,7 @@ import os from scripts import config from scripts.gdm import GlobalTheme from scripts.install.theme_installer import ThemeInstaller +from scripts.utils.console import Console, Color, Format class GlobalThemeInstaller(ThemeInstaller): @@ -27,6 +28,12 @@ class GlobalThemeInstaller(ThemeInstaller): self._apply_tweaks(theme.theme) def _after_install(self): - print("\nGDM theme installed successfully.") - print("You need to restart gdm.service to apply changes.") - print("Run \"systemctl restart gdm.service\" to restart GDM.") \ No newline at end of file + print() + Console.Line().update( + Console.format("GDM theme installed successfully.", color=Color.GREEN, format_type=Format.BOLD), + icon="🥳" + ) + Console.Line().update("You need to restart GDM to apply changes.", icon="ℹ️ ") + + formatted_command = Console.format("systemctl restart gdm.service", color=Color.YELLOW, format_type=Format.BOLD) + Console.Line().update(f"Run {formatted_command} to restart GDM.", icon="🔄") \ No newline at end of file diff --git a/scripts/install/local_theme_installer.py b/scripts/install/local_theme_installer.py index a5c9812..7648003 100644 --- a/scripts/install/local_theme_installer.py +++ b/scripts/install/local_theme_installer.py @@ -4,6 +4,7 @@ from scripts import config from scripts.install.theme_installer import ThemeInstaller from scripts.theme import Theme from scripts.utils import remove_files +from scripts.utils.console import Console, Color, Format class LocalThemeInstaller(ThemeInstaller): @@ -26,4 +27,6 @@ class LocalThemeInstaller(ThemeInstaller): self._apply_tweaks(self.theme) def _after_install(self): - print("\nTheme installed successfully.") \ No newline at end of file + print() + formatted_output = Console.format("Theme installed successfully.", color=Color.GREEN, format_type=Format.BOLD) + Console.Line().update(formatted_output, icon="🥳") \ No newline at end of file diff --git a/scripts/utils/console.py b/scripts/utils/console.py index 1257238..af44267 100644 --- a/scripts/utils/console.py +++ b/scripts/utils/console.py @@ -11,9 +11,9 @@ class Console: _next_line = 0 class Line: - def __init__(self, name): + def __init__(self, name: Optional[str]=None): """Initialize a new managed line""" - self.name = name + self.name = name or f"line_{Console._next_line}" self._reserve_line() def update(self, message, icon="⏳"): @@ -25,7 +25,10 @@ class Console: if lines_up > 0: sys.stdout.write(f"\033[{lines_up}F") # Clear line and write status - sys.stdout.write(f"\r\033[K{icon} {message}") + if icon.strip() == "": + sys.stdout.write(f"\r\033[K{message}") + else: + sys.stdout.write(f"\r\033[K{icon} {message}") # Move the cursor back down if lines_up > 0: sys.stdout.write(f"\033[{lines_up}E") @@ -78,8 +81,8 @@ class Color(Enum): GRAY = '\033[90m' @classmethod - def get(cls, color: str) -> Optional['Color']: - return getattr(cls, color.upper(), None) + def get(cls, color: str, default: Optional['Color']=None) -> Optional['Color']: + return getattr(cls, color.upper(), default) class Format(Enum): diff --git a/scripts/utils/gnome.py b/scripts/utils/gnome.py index cd687c3..1262503 100644 --- a/scripts/utils/gnome.py +++ b/scripts/utils/gnome.py @@ -1,10 +1,15 @@ import subprocess +import time + +from scripts import config +from scripts.utils.console import Console, Format, Color +from scripts.utils.parse_folder import parse_folder + def gnome_version() -> str | None: """ Get gnome-shell version """ - try: output = subprocess.check_output(['gnome-shell', '--version'], text=True).strip() return output.split(' ')[2] @@ -13,21 +18,40 @@ def gnome_version() -> str | None: def apply_gnome_theme(theme=None) -> bool: """ - Apply gnome-shell theme - :param theme: theme name + Applies the theme in user theme extension if it is Marble and extension installed. """ - try: if theme is None: - current_theme = subprocess.check_output(['dconf', 'read', '/org/gnome/shell/extensions/user-theme/name'], text=True).strip().strip("'") - if current_theme.startswith("Marble"): - theme = current_theme - else: - return False + theme = get_current_theme() - subprocess.run(['dconf', 'reset', '/org/gnome/shell/extensions/user-theme/name'], check=True) - subprocess.run(['dconf', 'write', '/org/gnome/shell/extensions/user-theme/name', f"'{theme}'"], check=True) - print(f"Theme '{theme}' applied.") - except subprocess.CalledProcessError: + line = Console.Line("apply_gnome_theme") + (color, _) = parse_folder(theme) + formatted_theme = Console.format(theme, color=Color.get(color, Color.GRAY), format_type=Format.BOLD) + + line.update(f"Applying {formatted_theme} theme...") + time.sleep(0.1) # applying the theme may freeze, so we need to wait a bit + apply_user_theme(theme) + line.success(f"Theme {formatted_theme} applied.") + except Exception: return False - return True \ No newline at end of file + return True + + +def get_current_theme() -> str: + """ + Throws an error if theme is not Marble. + """ + try: + output = subprocess.check_output(['dconf', 'read', config.user_themes_extension], text=True) + output = output.strip().strip("'") + + if not output.startswith("Marble"): + raise Exception(f"Theme {output} doesn't appear to be a Marble theme") + return output + except subprocess.CalledProcessError: + raise Exception("User theme extension not found.") + + +def apply_user_theme(theme_name: str): + subprocess.run(['dconf', 'reset', config.user_themes_extension], check=True) + subprocess.run(['dconf', 'write', config.user_themes_extension, f"'{theme_name}'"], check=True) \ No newline at end of file diff --git a/scripts/utils/parse_folder.py b/scripts/utils/parse_folder.py new file mode 100644 index 0000000..e150735 --- /dev/null +++ b/scripts/utils/parse_folder.py @@ -0,0 +1,11 @@ +def parse_folder(folder: str) -> tuple[str, str] | None: + """Parse a folder name into color and mode""" + folder_arr = folder.split("-") + + if len(folder_arr) < 2 or folder_arr[0] != "Marble": + return None + + color = "-".join(folder_arr[1:-1]) + mode = folder_arr[-1] + + return color, mode \ No newline at end of file diff --git a/scripts/utils/remove_files.py b/scripts/utils/remove_files.py index a78c602..ffb9173 100644 --- a/scripts/utils/remove_files.py +++ b/scripts/utils/remove_files.py @@ -7,10 +7,12 @@ import shutil from collections import defaultdict from typing import Any +from .console import Console, Color, Format +from .parse_folder import parse_folder from .. import config import os -def remove_files(args: argparse.Namespace, colors: dict[str, Any]): +def remove_files(args: argparse.Namespace, formatted_colors: dict[str, Any]): """Delete already installed Marble theme""" themes = detect_themes(config.themes_folder) @@ -20,23 +22,26 @@ def remove_files(args: argparse.Namespace, colors: dict[str, Any]): filtered_themes = themes if not args.all: args_dict = vars(args) - arguments = [color for color in colors.keys() if args_dict.get(color)] + arguments = [color for color in formatted_colors.keys() if args_dict.get(color)] filtered_themes = themes.filter(arguments) if not filtered_themes: - print("No matching themes found.") + Console.Line().error("No matching themes found.") return - colors = [color for (color, modes) in filtered_themes] - print(f"The following themes will be deleted: {', '.join(colors)}.") + formatted_colors = [ + Console.format(color, color=Color.get(color), format_type=Format.BOLD) + for (color, modes) in filtered_themes + ] + Console.Line().warn(f"The following themes will be deleted: {', '.join(formatted_colors)}.") if args.mode: - print(f"Theme modes to be deleted: {args.mode}.") + Console.Line().warn(f"Theme modes to be deleted: {args.mode}.") - if input(f"Proceed? (y/N) ").lower() == "y": + if proceed_input().lower() == "y": filtered_themes.remove(args.mode) - print("Themes deleted successfully.") + Console.Line().success("Themes deleted successfully.") else: - print("Operation cancelled.") + Console.Line().error("Operation cancelled.") def detect_themes(path: str) -> 'Themes': @@ -59,41 +64,12 @@ def detect_themes(path: str) -> 'Themes': return themes -def parse_folder(folder: str) -> tuple[str, str] | None: - """Parse a folder name into color and mode""" - folder_arr = folder.split("-") - - if len(folder_arr) < 2 or folder_arr[0] != "Marble": - return None - - color = "-".join(folder_arr[1:-1]) - mode = folder_arr[-1] - - return color, mode - - -class ThemeMode: - """Concrete theme with mode and path""" - mode: str - path: str - - def __init__(self, mode: str, path: str): - self.mode = mode - self.path = path - - def remove(self): - try: - shutil.rmtree(self.path) - except Exception as e: - print(f"Error deleting {self.path}: {e}") - - class Themes: """Collection of themes grouped by color""" def __init__(self): self.by_color: dict[str, list[ThemeMode]] = defaultdict(list) # color: list[ThemeMode] - def add_theme(self, color: str, theme_mode: ThemeMode): + def add_theme(self, color: str, theme_mode: 'ThemeMode'): self.by_color[color].append(theme_mode) def filter(self, colors: list[str]): @@ -121,3 +97,26 @@ class Themes: def __iter__(self): for color, modes in self.by_color.items(): yield color, modes + + +class ThemeMode: + """Concrete theme with mode and path""" + mode: str + path: str + + def __init__(self, mode: str, path: str): + self.mode = mode + self.path = path + + def remove(self): + try: + shutil.rmtree(self.path) + except Exception as e: + print(f"Error deleting {self.path}: {e}") + + +def proceed_input(): + formatted_agree = Console.format("y", color=Color.GREEN, format_type=Format.BOLD) + formatted_disagree = Console.format("N", color=Color.RED, format_type=Format.BOLD) + formatted_proceed = Console.format("Proceed?", format_type=Format.BOLD) + return input(f"{formatted_proceed} ({formatted_agree}/{formatted_disagree}) ") \ No newline at end of file From 75cbf0887944b7422c855844394fb2133844d510 Mon Sep 17 00:00:00 2001 From: Vladyslav Hroshev Date: Tue, 1 Apr 2025 21:20:28 +0300 Subject: [PATCH 6/8] Fixed opaque tweak, decreased sleep in apply_gnome_theme --- scripts/utils/gnome.py | 2 +- tweaks/opaque/tweak.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/utils/gnome.py b/scripts/utils/gnome.py index 1262503..9448766 100644 --- a/scripts/utils/gnome.py +++ b/scripts/utils/gnome.py @@ -29,7 +29,7 @@ def apply_gnome_theme(theme=None) -> bool: formatted_theme = Console.format(theme, color=Color.get(color, Color.GRAY), format_type=Format.BOLD) line.update(f"Applying {formatted_theme} theme...") - time.sleep(0.1) # applying the theme may freeze, so we need to wait a bit + time.sleep(0.025) # applying the theme may freeze, so we need to wait a bit apply_user_theme(theme) line.success(f"Theme {formatted_theme} applied.") except Exception: diff --git a/tweaks/opaque/tweak.py b/tweaks/opaque/tweak.py index d47af50..232c2ac 100755 --- a/tweaks/opaque/tweak.py +++ b/tweaks/opaque/tweak.py @@ -1,9 +1,12 @@ +from scripts.install.colors_definer import ColorsDefiner + + def define_arguments(parser): color_args = parser.add_argument_group('Color tweaks') color_args.add_argument('-O', '--opaque', action='store_true', help='make the background in menus/popovers opaque') -def apply_tweak(args, theme, colors): +def apply_tweak(args, theme, colors: ColorsDefiner): if args.opaque: - colors["elements"]["BACKGROUND-COLOR"]["light"]["a"] = 1 - colors["elements"]["BACKGROUND-COLOR"]["dark"]["a"] = 1 + colors.replacers["BACKGROUND-COLOR"]["light"]["a"] = 1 + colors.replacers["BACKGROUND-COLOR"]["dark"]["a"] = 1 From b44c12c18f561b0176ede699dc40a5db5ea72978 Mon Sep 17 00:00:00 2001 From: Vladyslav Hroshev Date: Tue, 1 Apr 2025 21:56:13 +0300 Subject: [PATCH 7/8] Fixed lint with flake8. Update a Python version in README.md --- README.md | 2 +- scripts/utils/files_labeler.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2da141a..f9e91e4 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Icon theme: https://github.com/vinceliuice/Colloid-icon-theme ## 🚧 Requirements - GNOME 42-48. Correct functionality on other versions is not guaranteed. - [User Themes](https://extensions.gnome.org/extension/19/user-themes/ "User Themes") extension. -- Python 3.2+. +- Python 3.9+. ## 💡 Installation diff --git a/scripts/utils/files_labeler.py b/scripts/utils/files_labeler.py index 1b23514..ddfcf26 100644 --- a/scripts/utils/files_labeler.py +++ b/scripts/utils/files_labeler.py @@ -1,6 +1,7 @@ import os +from typing import Tuple -type LabeledFileGroup = tuple[str, str] +type LabeledFileGroup = Tuple[str, str] class FilesLabeler: def __init__(self, directory: str, *args: str): From ef357ee6e27037fa455c7a018a08aa8fea4ce50e Mon Sep 17 00:00:00 2001 From: Vladyslav Hroshev Date: Tue, 1 Apr 2025 21:58:46 +0300 Subject: [PATCH 8/8] Updated Python version in workflow --- .github/workflows/python-app.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 915509e..52b784b 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -7,11 +7,9 @@ on: push: branches: - "main" - - "gdm" pull_request: branches: - "main" - - "gdm" permissions: contents: read @@ -23,10 +21,10 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up Python 3.10 + - name: Set up Python 3.13 uses: actions/setup-python@v3 with: - python-version: "3.10" + python-version: "3.13" - name: Install dependencies run: | python -m pip install --upgrade pip