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