mirror of
https://github.com/imarkoff/Marble-shell-theme.git
synced 2025-11-30 01:13:22 -08:00
Merge pull request #54 from imarkoff/unstable
SOLID refactoring, covered the program with tests
This commit is contained in:
2
.github/workflows/python-app.yml
vendored
2
.github/workflows/python-app.yml
vendored
@@ -38,4 +38,4 @@ jobs:
|
||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
pytest scripts/tests.py
|
||||
pytest tests/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# This file installs Marble shell theme for GNOME DE
|
||||
# Copyright (C) 2023-2025 Vladyslav Hroshev
|
||||
|
||||
import os
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
@@ -22,6 +22,7 @@ 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
|
||||
from scripts.utils.logger.console import Console
|
||||
|
||||
|
||||
def main():
|
||||
@@ -31,6 +32,12 @@ def main():
|
||||
installer_class = GlobalThemeInstaller if args.gdm else LocalThemeInstaller
|
||||
installer = installer_class(args, colors_definer)
|
||||
|
||||
if args.gdm:
|
||||
if os.getuid() != 0:
|
||||
Console().Line().error(
|
||||
"Global installation requires root privileges. Please run the script as root.")
|
||||
return
|
||||
|
||||
if args.remove or args.reinstall:
|
||||
installer.remove()
|
||||
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import os.path
|
||||
import os
|
||||
from tempfile import gettempdir
|
||||
|
||||
# folder definitions
|
||||
temp_folder = f"{gettempdir()}/marble"
|
||||
marble_folder = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
temp_folder = os.path.join(gettempdir(), 'marble')
|
||||
temp_tests_folder = os.path.join(temp_folder, 'tests')
|
||||
gdm_folder = "gdm"
|
||||
gnome_folder = "gnome-shell"
|
||||
temp_gnome_folder = f"{temp_folder}/{gnome_folder}"
|
||||
tweaks_folder = "tweaks"
|
||||
themes_folder = "~/.themes"
|
||||
themes_folder = os.path.expanduser("~/.themes")
|
||||
raw_theme_folder = "theme"
|
||||
scripts_folder = "scripts"
|
||||
|
||||
# GDM definitions
|
||||
global_gnome_shell_theme = "/usr/share/gnome-shell"
|
||||
@@ -18,8 +17,7 @@ ubuntu_gresource_link = "gtk-theme.gresource"
|
||||
extracted_gdm_folder = "theme"
|
||||
|
||||
# files definitions
|
||||
gnome_shell_css = f"{temp_gnome_folder}/gnome-shell.css"
|
||||
tweak_file = f"./{tweaks_folder}/*/tweak.py"
|
||||
colors_json = "colors.json"
|
||||
colors_json = os.path.join(marble_folder, "colors.json")
|
||||
|
||||
user_themes_extension = "/org/gnome/shell/extensions/user-theme/name"
|
||||
|
||||
149
scripts/gdm.py
149
scripts/gdm.py
@@ -1,149 +0,0 @@
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from .install.colors_definer import ColorsDefiner
|
||||
from .theme import Theme
|
||||
from .utils import remove_properties, remove_keywords
|
||||
from . import config
|
||||
from .utils.alternatives_updater import AlternativesUpdater
|
||||
from .utils.console import Console, Color, Format
|
||||
from .utils.files_labeler import FilesLabeler
|
||||
from .utils.gresource import Gresource, GresourceBackupNotFoundError
|
||||
|
||||
|
||||
class GlobalTheme:
|
||||
"""Class to install global theme for GDM"""
|
||||
def __init__(self,
|
||||
colors_json: ColorsDefiner, theme_folder: str,
|
||||
destination_folder: str, destination_file: str, temp_folder: str,
|
||||
mode: Optional[str] = None, is_filled = False
|
||||
):
|
||||
"""
|
||||
:param colors_json: location of a JSON file with color values
|
||||
:param theme_folder: raw theme location
|
||||
:param destination_folder: folder where themes will be installed
|
||||
:param temp_folder: folder where files will be collected
|
||||
:param mode: theme mode (light or dark)
|
||||
:param is_filled: if True, the theme will be filled
|
||||
"""
|
||||
self.colors_json = colors_json
|
||||
self.theme_folder = theme_folder
|
||||
self.destination_folder = destination_folder
|
||||
self.destination_file = destination_file
|
||||
self.temp_folder = temp_folder
|
||||
self.is_filled = is_filled
|
||||
self.mode = mode
|
||||
|
||||
self.themes: list[ThemePrepare] = []
|
||||
|
||||
self.__is_installed_trigger = "\n/* Marble theme */\n"
|
||||
self.__gresource_file = os.path.join(self.destination_folder, self.destination_file)
|
||||
|
||||
self.__gresource_temp_folder = os.path.join(self.temp_folder, config.extracted_gdm_folder)
|
||||
self.__gresource = Gresource(self.destination_file, self.__gresource_temp_folder, self.destination_folder)
|
||||
|
||||
def prepare(self):
|
||||
if self.__is_installed():
|
||||
Console.Line().info("Theme is installed. Reinstalling...")
|
||||
self.__gresource.use_backup_gresource()
|
||||
|
||||
self.__gresource.extract()
|
||||
self.__find_themes()
|
||||
|
||||
def __is_installed(self) -> bool:
|
||||
if not hasattr(self, '__is_installed_cached'):
|
||||
with open(self.__gresource_file, "rb") as f:
|
||||
self.__is_installed_cached = self.__is_installed_trigger.encode() in f.read()
|
||||
|
||||
return self.__is_installed_cached
|
||||
|
||||
def __find_themes(self):
|
||||
extracted_theme_files = os.listdir(self.__gresource_temp_folder)
|
||||
|
||||
allowed_modes = ("dark", "light")
|
||||
allowed_css = ("gnome-shell-dark", "gnome-shell-light", "gnome-shell")
|
||||
|
||||
for style_name in allowed_css:
|
||||
style_file = style_name + ".css"
|
||||
if style_file in extracted_theme_files:
|
||||
last_mode = style_name.split("-")[-1]
|
||||
mode = last_mode if last_mode in allowed_modes else None
|
||||
self.__append_theme(style_name, mode=mode or self.mode, label=mode)
|
||||
|
||||
def __append_theme(self, theme_type: str, mode = None, label: Optional[str] = None):
|
||||
"""Helper to create theme objects"""
|
||||
theme = Theme(theme_type, self.colors_json, self.theme_folder,
|
||||
self.__gresource_temp_folder, self.temp_folder,
|
||||
mode=mode, is_filled=self.is_filled)
|
||||
theme.prepare()
|
||||
theme_file = os.path.join(self.__gresource_temp_folder, f"{theme_type}.css")
|
||||
self.themes.append(ThemePrepare(theme=theme, theme_file=theme_file, label=label))
|
||||
|
||||
def install(self, hue, sat=None):
|
||||
"""Install theme globally"""
|
||||
if os.geteuid() != 0:
|
||||
raise Exception("Root privileges required to install GDM theme")
|
||||
|
||||
self.__generate_themes(hue, 'Marble', sat)
|
||||
|
||||
self.__gresource.compile()
|
||||
if not self.__is_installed():
|
||||
self.__gresource.backup()
|
||||
self.__gresource.move()
|
||||
|
||||
self.__update_alternatives()
|
||||
|
||||
def __generate_themes(self, hue: int, color: str, sat: Optional[int] = None):
|
||||
"""Generate theme files for gnome-shell-theme.gresource.xml"""
|
||||
for theme_prepare in self.themes:
|
||||
if theme_prepare.label is not None:
|
||||
temp_folder = theme_prepare.theme.temp_folder
|
||||
main_styles = theme_prepare.theme.main_styles
|
||||
FilesLabeler(temp_folder, main_styles).append_label(theme_prepare.label)
|
||||
|
||||
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_prepare.theme)
|
||||
|
||||
theme_prepare.theme.install(hue, color, sat, destination=self.__gresource_temp_folder)
|
||||
|
||||
def __add_gnome_styles(self, theme: Theme):
|
||||
"""Add gnome styles to the start of the file"""
|
||||
with open(f"{theme.destination_folder}/{theme.theme_type}.css", 'r') as gnome_theme:
|
||||
gnome_styles = gnome_theme.read() + self.__is_installed_trigger
|
||||
theme.add_to_start(gnome_styles)
|
||||
|
||||
def __update_alternatives(self):
|
||||
link = os.path.join(self.destination_folder, config.ubuntu_gresource_link)
|
||||
name = config.ubuntu_gresource_link
|
||||
path = os.path.join(self.destination_folder, self.destination_file)
|
||||
AlternativesUpdater.install_and_set(link, name, path)
|
||||
|
||||
def remove(self):
|
||||
if self.__is_installed():
|
||||
removing_line = Console.Line()
|
||||
removing_line.update("Theme is installed. Removing...")
|
||||
|
||||
try:
|
||||
self.__gresource.restore()
|
||||
removing_line.success("Global theme removed successfully. Restart GDM to apply changes.")
|
||||
except GresourceBackupNotFoundError:
|
||||
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:
|
||||
Console.Line().error("Theme is not installed. Nothing to remove.")
|
||||
Console.Line().warn("If theme is still installed globally, try reinstalling gnome-shell package.")
|
||||
|
||||
def __remove_alternatives(self):
|
||||
name = config.ubuntu_gresource_link
|
||||
path = os.path.join(self.destination_folder, self.destination_file)
|
||||
AlternativesUpdater.remove(name, path)
|
||||
|
||||
|
||||
class ThemePrepare:
|
||||
"""Theme data class prepared for installation"""
|
||||
def __init__(self, theme: Theme, theme_file, label: Optional[str] = None):
|
||||
self.theme = theme
|
||||
self.theme_file = theme_file
|
||||
self.label = label
|
||||
@@ -1,13 +1,11 @@
|
||||
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
|
||||
from scripts.utils.global_theme.gdm import GDMTheme
|
||||
from scripts.utils.global_theme.gdm_builder import GDMThemeBuilder
|
||||
from scripts.utils.logger.console import Console, Color, Format
|
||||
|
||||
|
||||
class GlobalThemeInstaller(ThemeInstaller):
|
||||
theme: GlobalTheme
|
||||
theme: GDMTheme
|
||||
|
||||
def remove(self):
|
||||
gdm_rm_status = self.theme.remove()
|
||||
@@ -15,13 +13,10 @@ class GlobalThemeInstaller(ThemeInstaller):
|
||||
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.install(hue, sat)
|
||||
gdm_builder = GDMThemeBuilder(self.colors)
|
||||
gdm_builder.with_mode(self.args.mode)
|
||||
gdm_builder.with_filled(self.args.filled)
|
||||
self.theme = gdm_builder.build()
|
||||
|
||||
def _apply_tweaks_to_theme(self):
|
||||
for theme in self.theme.themes:
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import os.path
|
||||
|
||||
from scripts import config
|
||||
from scripts.install.theme_installer import ThemeInstaller
|
||||
from scripts.theme import Theme
|
||||
from scripts.utils.theme.gnome_shell_theme_builder import GnomeShellThemeBuilder
|
||||
from scripts.utils.theme.theme import Theme
|
||||
from scripts.utils import remove_files
|
||||
from scripts.utils.console import Console, Color, Format
|
||||
from scripts.utils.logger.console import Console, Color, Format
|
||||
|
||||
|
||||
class LocalThemeInstaller(ThemeInstaller):
|
||||
@@ -15,13 +13,10 @@ class LocalThemeInstaller(ThemeInstaller):
|
||||
remove_files(self.args, colors)
|
||||
|
||||
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.install(hue, theme_name, sat)
|
||||
theme_builder = GnomeShellThemeBuilder(self.colors)
|
||||
theme_builder.with_mode(self.args.mode)
|
||||
theme_builder.filled(self.args.filled)
|
||||
self.theme = theme_builder.build()
|
||||
|
||||
def _apply_tweaks_to_theme(self):
|
||||
self._apply_tweaks(self.theme)
|
||||
|
||||
@@ -3,13 +3,13 @@ import concurrent.futures
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from scripts.install.colors_definer import ColorsDefiner
|
||||
from scripts.theme import Theme
|
||||
from scripts.types.theme_base import ThemeBase
|
||||
from scripts.tweaks_manager import TweaksManager
|
||||
|
||||
|
||||
class ThemeInstaller(ABC):
|
||||
"""Base class for theme installers"""
|
||||
theme: Theme
|
||||
theme: ThemeBase
|
||||
|
||||
def __init__(self, args: argparse.Namespace, colors: ColorsDefiner):
|
||||
self.args = args
|
||||
@@ -37,11 +37,6 @@ class ThemeInstaller(ABC):
|
||||
"""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"""
|
||||
@@ -53,15 +48,13 @@ class ThemeInstaller(ABC):
|
||||
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()
|
||||
return
|
||||
|
||||
if self._apply_default_color():
|
||||
return
|
||||
|
||||
if not installed_any:
|
||||
raise Exception('No color arguments specified. Use -h or --help to see the available options.')
|
||||
|
||||
def _apply_custom_color(self):
|
||||
@@ -70,7 +63,7 @@ class ThemeInstaller(ABC):
|
||||
sat = self.args.sat
|
||||
|
||||
theme_name = name if name else f'hue{hue}'
|
||||
self._install_theme(hue, theme_name, sat)
|
||||
self.theme.install(hue, theme_name, sat)
|
||||
|
||||
def _apply_default_color(self) -> bool:
|
||||
colors = self.colors.colors
|
||||
@@ -90,7 +83,7 @@ class ThemeInstaller(ABC):
|
||||
|
||||
def _run_concurrent_installation(self, colors_to_install):
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
futures = [executor.submit(self._install_theme, hue, color, sat)
|
||||
futures = [executor.submit(self.theme.install, hue, color, sat)
|
||||
for hue, color, sat in colors_to_install]
|
||||
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
# TODO: Add more tests
|
||||
|
||||
import unittest
|
||||
import os
|
||||
import json
|
||||
import shutil
|
||||
from unittest.mock import patch
|
||||
|
||||
from . import config
|
||||
from .theme import Theme
|
||||
|
||||
# folders
|
||||
tests_folder = '.tests'
|
||||
project_folder = '.'
|
||||
|
||||
|
||||
class TestInstall(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Create necessary directories
|
||||
os.makedirs(f"{project_folder}/{config.raw_theme_folder}/{config.gnome_folder}", exist_ok=True)
|
||||
os.makedirs(f"{tests_folder}/.themes", exist_ok=True)
|
||||
os.makedirs(f"{tests_folder}/.temp", exist_ok=True)
|
||||
|
||||
def tearDown(self):
|
||||
# Clean up after tests
|
||||
shutil.rmtree(tests_folder, ignore_errors=True)
|
||||
|
||||
@patch('scripts.utils.gnome.subprocess.check_output')
|
||||
def test_install_theme(self, mock_check_output):
|
||||
"""
|
||||
Test if theme is installed correctly (colors are replaced)
|
||||
"""
|
||||
mock_check_output.return_value = 'GNOME Shell 47.0\n'
|
||||
|
||||
# folders
|
||||
themes_folder = f"{tests_folder}/.themes"
|
||||
temp_folder = f"{tests_folder}/.temp"
|
||||
|
||||
# colors from colors.json
|
||||
colors_json = open(f"{project_folder}/{config.colors_json}")
|
||||
colors = json.load(colors_json)
|
||||
colors_json.close()
|
||||
|
||||
# create test theme
|
||||
test_theme = Theme("gnome-shell", colors,
|
||||
f"{project_folder}/{config.raw_theme_folder}/{config.gnome_folder}",
|
||||
themes_folder, temp_folder,
|
||||
mode='light', is_filled=True)
|
||||
|
||||
# install test theme
|
||||
test_theme.install(120, 'test', 70)
|
||||
|
||||
# folder with installed theme (.tests/.themes/Marble-test-light/gnome-shell)
|
||||
installed_theme = f"{themes_folder}/{os.listdir(themes_folder)[0]}/{config.gnome_folder}"
|
||||
|
||||
# check if files are installed
|
||||
for file in os.listdir(installed_theme):
|
||||
with open(f"{installed_theme}/{file}") as f:
|
||||
read_file = f.read()
|
||||
|
||||
for color in colors["elements"]:
|
||||
self.assertNotIn(color, read_file, msg=f"Color {color} is not replaced in {file}")
|
||||
|
||||
# delete test theme
|
||||
del test_theme
|
||||
shutil.rmtree(tests_folder)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
172
scripts/theme.py
172
scripts/theme.py
@@ -1,172 +0,0 @@
|
||||
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
|
||||
destination_return, # copied/modified theme location
|
||||
generate_file) # combine files from folder to one file
|
||||
from .utils.console import Console, Color, Format
|
||||
|
||||
|
||||
class Theme:
|
||||
def __init__(self, theme_type, colors_json, theme_folder, destination_folder, temp_folder,
|
||||
mode=None, is_filled=False):
|
||||
"""
|
||||
Initialize Theme class
|
||||
:param colors_json: location of a json file with colors
|
||||
:param theme_type: theme type (gnome-shell, gtk, etc.)
|
||||
:param theme_folder: raw theme location
|
||||
:param destination_folder: folder where themes will be installed
|
||||
:param temp_folder: folder where files will be collected
|
||||
:param mode: theme mode (light or dark)
|
||||
:param is_filled: if True, theme will be filled
|
||||
"""
|
||||
|
||||
self.colors: ColorsDefiner = colors_json
|
||||
self.temp_folder = f"{temp_folder}/{theme_type}"
|
||||
self.theme_folder = theme_folder
|
||||
self.theme_type = theme_type
|
||||
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
|
||||
|
||||
def __add__(self, other):
|
||||
"""
|
||||
Add to main styles another styles
|
||||
:param other: styles to add
|
||||
:return: new Theme object
|
||||
"""
|
||||
|
||||
with open(self.main_styles, 'a') as main_styles:
|
||||
main_styles.write('\n' + other)
|
||||
return self
|
||||
|
||||
def __mul__(self, other):
|
||||
"""
|
||||
Copy files to temp folder
|
||||
:param other: file or folder
|
||||
:return: new Theme object
|
||||
"""
|
||||
|
||||
if os.path.isfile(other):
|
||||
shutil.copy(other, self.temp_folder)
|
||||
else:
|
||||
shutil.copytree(other, self.temp_folder)
|
||||
|
||||
return self
|
||||
|
||||
def 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):
|
||||
"""
|
||||
Install accent colors from colors.json to different file
|
||||
:param hue
|
||||
:param destination: file directory
|
||||
:param theme_mode: theme name (light or dark)
|
||||
:param apply_file: file name
|
||||
:param sat: color saturation (optional)
|
||||
"""
|
||||
|
||||
# list of (keyword, replaced value)
|
||||
replaced_colors = list()
|
||||
|
||||
# colorsys works in range(0, 1)
|
||||
h = hue / 360
|
||||
for element in self.colors.replacers:
|
||||
# if color has default color and hasn't been replaced
|
||||
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.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)]
|
||||
|
||||
replaced_colors.append((element, f"rgba({red}, {green}, {blue}, {alpha})"))
|
||||
|
||||
# replace colors
|
||||
replace_keywords(os.path.expanduser(f"{destination}/{apply_file}"), *replaced_colors)
|
||||
0
scripts/types/__init__.py
Normal file
0
scripts/types/__init__.py
Normal file
11
scripts/types/installation_color.py
Normal file
11
scripts/types/installation_color.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal
|
||||
|
||||
InstallationMode = Literal["light", "dark"]
|
||||
|
||||
|
||||
@dataclass
|
||||
class InstallationColor:
|
||||
hue: int
|
||||
saturation: int | None
|
||||
modes: list[InstallationMode]
|
||||
12
scripts/types/theme_base.py
Normal file
12
scripts/types/theme_base.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class ThemeBase(ABC):
|
||||
"""Base class for theme installation and preparation."""
|
||||
@abstractmethod
|
||||
def prepare(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def install(self, hue: int, name: str, sat: float | None = None):
|
||||
pass
|
||||
@@ -1,8 +1,5 @@
|
||||
from .concatenate_files import concatenate_files
|
||||
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 .remove_files import remove_files
|
||||
from .remove_keywords import remove_keywords
|
||||
from .remove_properties import remove_properties
|
||||
|
||||
@@ -2,7 +2,7 @@ import functools
|
||||
import subprocess
|
||||
from typing import TypeAlias
|
||||
|
||||
from scripts.utils.console import Console
|
||||
from scripts.utils.logger.console import Console
|
||||
|
||||
PathString: TypeAlias = str | bytes
|
||||
|
||||
@@ -28,7 +28,7 @@ class AlternativesUpdater:
|
||||
|
||||
@staticmethod
|
||||
@ubuntu_specific
|
||||
def install_and_set(link: str, name: str, path: PathString, priority: int = 0):
|
||||
def install_and_set(link: PathString, name: str, path: PathString, priority: int = 0):
|
||||
AlternativesUpdater.install(link, name, path, priority)
|
||||
AlternativesUpdater.set(name, path)
|
||||
|
||||
|
||||
0
scripts/utils/color_converter/__init__.py
Normal file
0
scripts/utils/color_converter/__init__.py
Normal file
26
scripts/utils/color_converter/color_converter.py
Normal file
26
scripts/utils/color_converter/color_converter.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class ColorConverter(ABC):
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def hex_to_rgba(hex_color):
|
||||
"""
|
||||
Converts a HEX color code to RGBA format.
|
||||
:param hex_color: HEX color code (e.g., '#ff5733' or 'ff5733').
|
||||
:return: Tuple of RGBA values (red, green, blue, alpha).
|
||||
:raises ValueError: If the HEX color code is invalid.
|
||||
"""
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def hsl_to_rgb(hue, saturation, lightness):
|
||||
"""
|
||||
Converts HSL color values to RGB format.
|
||||
:param hue: Hue value (0-360).
|
||||
:param saturation: Saturation value (0-1).
|
||||
:param lightness: Lightness value (0-1).
|
||||
:return: Tuple of RGB values (red, green, blue) in range(0-255).
|
||||
"""
|
||||
pass
|
||||
43
scripts/utils/color_converter/color_converter_impl.py
Normal file
43
scripts/utils/color_converter/color_converter_impl.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import colorsys
|
||||
|
||||
from scripts.utils.color_converter.color_converter import ColorConverter
|
||||
|
||||
|
||||
class ColorConverterImpl(ColorConverter):
|
||||
@staticmethod
|
||||
def hex_to_rgba(hex_color):
|
||||
try:
|
||||
hex_color = hex_color.lstrip('#')
|
||||
|
||||
# Handle shorthand hex colors (e.g., #fff)
|
||||
if len(hex_color) == 3:
|
||||
hex_color = ''.join([char * 2 for char in hex_color])
|
||||
|
||||
# Add alpha channel if missing
|
||||
if len(hex_color) == 6:
|
||||
hex_color += "ff"
|
||||
|
||||
# Validate the hex color
|
||||
int(hex_color, 16)
|
||||
|
||||
except ValueError:
|
||||
raise ValueError(f'Error: Invalid HEX color code: #{hex_color}')
|
||||
|
||||
else:
|
||||
return int(hex_color[0:2], 16), \
|
||||
int(hex_color[2:4], 16), \
|
||||
int(hex_color[4:6], 16), \
|
||||
int(hex_color[6:8], 16) / 255
|
||||
|
||||
@staticmethod
|
||||
def hsl_to_rgb(hue, saturation, lightness):
|
||||
if hue > 360 or hue < 0:
|
||||
raise ValueError(f'Hue must be between 0 and 360, not {hue}')
|
||||
if saturation > 1 or saturation < 0:
|
||||
raise ValueError(f'Saturation must be between 0 and 1, not {saturation}')
|
||||
if lightness > 1 or lightness < 0:
|
||||
raise ValueError(f'Lightness must be between 0 and 1, not {lightness}')
|
||||
|
||||
h = hue / 360
|
||||
red, green, blue = [round(item * 255) for item in colorsys.hls_to_rgb(h, lightness, saturation)]
|
||||
return red, green, blue
|
||||
0
scripts/utils/command_runner/__init__.py
Normal file
0
scripts/utils/command_runner/__init__.py
Normal file
14
scripts/utils/command_runner/command_runner.py
Normal file
14
scripts/utils/command_runner/command_runner.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import subprocess
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class CommandRunner(ABC):
|
||||
@abstractmethod
|
||||
def run(self, command: list[str], **kwargs) -> subprocess.CompletedProcess:
|
||||
"""
|
||||
Run a command in the shell and return the output.
|
||||
:param command: Command to run.
|
||||
:param kwargs: Additional arguments for the command.
|
||||
:return: Output of the command.
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,8 @@
|
||||
import subprocess
|
||||
|
||||
from scripts.utils.command_runner.command_runner import CommandRunner
|
||||
|
||||
|
||||
class SubprocessCommandRunner(CommandRunner):
|
||||
def run(self, command: list[str], **kwargs) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(command, **kwargs)
|
||||
@@ -1,12 +0,0 @@
|
||||
def concatenate_files(edit_file, file):
|
||||
"""
|
||||
Merge two files
|
||||
:param edit_file: where it will be appended
|
||||
:param file: file you want to append
|
||||
"""
|
||||
|
||||
with open(file, 'r') as read_file:
|
||||
file_content = read_file.read()
|
||||
|
||||
with open(edit_file, 'a') as write_file:
|
||||
write_file.write('\n' + file_content)
|
||||
@@ -1,11 +0,0 @@
|
||||
def destination_return(themes_folder, path_name, theme_mode, theme_type):
|
||||
"""
|
||||
Copied/modified theme location
|
||||
:param themes_folder: themes folder location
|
||||
:param path_name: color name
|
||||
:param theme_mode: theme name (light or dark)
|
||||
:param theme_type: theme type (gnome-shell, gtk-4.0, ...)
|
||||
:return: copied files' folder location
|
||||
"""
|
||||
|
||||
return f"{themes_folder}/Marble-{path_name}-{theme_mode}/{theme_type}/"
|
||||
@@ -1,15 +1,28 @@
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Tuple, TypeAlias
|
||||
|
||||
LabeledFileGroup: TypeAlias = Tuple[str, str]
|
||||
|
||||
|
||||
class FilesLabelerFactory(ABC):
|
||||
@abstractmethod
|
||||
def create(self, temp_folder: str, *files_to_update_references: str) -> 'FilesLabeler':
|
||||
pass
|
||||
|
||||
|
||||
class FilesLabelerFactoryImpl(FilesLabelerFactory):
|
||||
def create(self, temp_folder: str, *files_to_update_references: str) -> 'FilesLabeler':
|
||||
return FilesLabeler(temp_folder, *files_to_update_references)
|
||||
|
||||
|
||||
class FilesLabeler:
|
||||
def __init__(self, directory: str, *args: str):
|
||||
def __init__(self, directory: str, *files_to_update_references: str):
|
||||
"""
|
||||
Initialize the working directory and files to change
|
||||
"""
|
||||
self.directory = directory
|
||||
self.files = args
|
||||
self.files = files_to_update_references
|
||||
|
||||
def append_label(self, label: str):
|
||||
"""
|
||||
|
||||
@@ -7,6 +7,9 @@ def get_version_folders(version, base_path):
|
||||
:param base_path: base path to version folders
|
||||
:return: list of matching version folders
|
||||
"""
|
||||
if not os.path.exists(base_path):
|
||||
return []
|
||||
|
||||
version_folders = os.listdir(base_path)
|
||||
version = int(version.split('.')[0]) # Use only the major version for comparison
|
||||
matching_folders = []
|
||||
|
||||
0
scripts/utils/global_theme/__init__.py
Normal file
0
scripts/utils/global_theme/__init__.py
Normal file
88
scripts/utils/global_theme/gdm.py
Normal file
88
scripts/utils/global_theme/gdm.py
Normal file
@@ -0,0 +1,88 @@
|
||||
from scripts.types.theme_base import ThemeBase
|
||||
from scripts.utils.global_theme.gdm_installer import GDMThemeInstaller
|
||||
from scripts.utils.global_theme.gdm_preparer import GDMThemePreparer
|
||||
from scripts.utils.global_theme.gdm_remover import GDMThemeRemover
|
||||
from scripts.utils.global_theme.gdm_theme_prepare import GDMThemePrepare
|
||||
|
||||
|
||||
class GDMTheme(ThemeBase):
|
||||
"""
|
||||
GDM theming module.
|
||||
|
||||
This module provides functionality to prepare, install, and remove GNOME Display Manager themes.
|
||||
It follows a workflow of:
|
||||
1. Preparing themes from existing GDM resources
|
||||
2. Installing themes with custom colors/styles
|
||||
3. Providing ability to restore original GDM themes
|
||||
|
||||
The main entry point is the GDMTheme class, which orchestrates the entire theme management process.
|
||||
"""
|
||||
def __init__(self, preparer: GDMThemePreparer, installer: GDMThemeInstaller, remover: GDMThemeRemover):
|
||||
"""
|
||||
:param preparer: GDMThemePreparer instance for preparing themes
|
||||
:param installer: GDMThemeInstaller instance for installing themes
|
||||
:param remover: GDMThemeRemover instance for removing themes
|
||||
"""
|
||||
self.preparer = preparer
|
||||
self.installer = installer
|
||||
self.remover = remover
|
||||
|
||||
self.themes: list[GDMThemePrepare] = []
|
||||
|
||||
def prepare(self):
|
||||
"""
|
||||
Prepare the theme for installation.
|
||||
|
||||
This method:
|
||||
1. Checks if a theme is already installed and uses backup if needed
|
||||
2. Extracts relevant theme files
|
||||
3. Processes them into ready-to-compile GDMThemePrepare objects
|
||||
|
||||
The processed themes are stored in the themes attribute for later use.
|
||||
"""
|
||||
if self._is_installed():
|
||||
self.preparer.use_backup_as_source()
|
||||
self.themes = self.preparer.prepare()
|
||||
|
||||
def _is_installed(self) -> bool:
|
||||
"""
|
||||
Check if a GDM theme is currently installed.
|
||||
|
||||
This looks for specific markers in the system gresource files
|
||||
that indicate our theme has been installed.
|
||||
|
||||
:return: True if a custom theme is installed, False otherwise
|
||||
"""
|
||||
return self.installer.is_installed()
|
||||
|
||||
def install(self, hue: int, name: str, sat: float | None = None):
|
||||
"""
|
||||
Install the prepared theme with specified color adjustments.
|
||||
|
||||
This method:
|
||||
1. Compiles theme files with the specified hue and saturation
|
||||
2. Creates a backup of the original GDM theme if one doesn't exist
|
||||
3. Installs the compiled theme to the system
|
||||
|
||||
:param hue: The hue adjustment (0-360) to apply to the theme
|
||||
:param name: The name of the theme to be installed. In GDM will only be shown in logger
|
||||
:param sat: Optional saturation adjustment (0-100) to apply
|
||||
"""
|
||||
self.installer.compile(self.themes, hue, name, sat)
|
||||
|
||||
if not self._is_installed():
|
||||
self.installer.backup()
|
||||
|
||||
self.installer.install()
|
||||
|
||||
def remove(self):
|
||||
"""
|
||||
Remove the installed theme and restore the original GDM theme.
|
||||
|
||||
If no theme is installed, displays a warning message to the user.
|
||||
This will restore from backup and update GDM alternatives if needed.
|
||||
"""
|
||||
if self._is_installed():
|
||||
self.remover.remove()
|
||||
else:
|
||||
self.remover.warn_not_installed()
|
||||
171
scripts/utils/global_theme/gdm_builder.py
Normal file
171
scripts/utils/global_theme/gdm_builder.py
Normal file
@@ -0,0 +1,171 @@
|
||||
import os.path
|
||||
from typing import Optional
|
||||
|
||||
from scripts import config
|
||||
from scripts.install.colors_definer import ColorsDefiner
|
||||
from scripts.types.installation_color import InstallationMode
|
||||
from scripts.utils.alternatives_updater import AlternativesUpdater, PathString
|
||||
from scripts.utils.command_runner.subprocess_command_runner import SubprocessCommandRunner
|
||||
from scripts.utils.files_labeler import FilesLabelerFactoryImpl
|
||||
from scripts.utils.global_theme.gdm import GDMTheme
|
||||
from scripts.utils.global_theme.gdm_installer import GDMThemeInstaller
|
||||
from scripts.utils.global_theme.gdm_preparer import GDMThemePreparer
|
||||
from scripts.utils.global_theme.gdm_remover import GDMThemeRemover
|
||||
from scripts.utils.global_theme.ubuntu_alternatives_updater import UbuntuGDMAlternativesUpdater
|
||||
from scripts.utils.gresource.gresource import Gresource
|
||||
from scripts.utils.logger.console import Console
|
||||
from scripts.utils.logger.logger import LoggerFactory
|
||||
from scripts.utils.theme.gnome_shell_theme_builder import GnomeShellThemeBuilder
|
||||
|
||||
|
||||
class GDMThemeBuilder:
|
||||
"""
|
||||
Builder class for creating GDMTheme instances with configurable components.
|
||||
|
||||
This class follows the Builder pattern to create a GDMTheme with all necessary
|
||||
dependencies. Dependencies can be injected via the with_* methods or will be
|
||||
automatically resolved during build() if not provided.
|
||||
|
||||
Example usage:
|
||||
builder = GMDThemeBuilder(colors_provider)
|
||||
theme = builder.with_mode("dark").with_filled(True).build()
|
||||
"""
|
||||
def __init__(self, colors_provider: ColorsDefiner):
|
||||
"""
|
||||
:param colors_provider: A provider for color definitions.
|
||||
"""
|
||||
self.colors_provider: ColorsDefiner = colors_provider
|
||||
self._temp_folder: PathString = os.path.join(config.temp_folder, config.gdm_folder)
|
||||
self._mode: Optional[InstallationMode] = None
|
||||
self._is_filled: bool = False
|
||||
|
||||
self._logger_factory: Optional[LoggerFactory] = None
|
||||
self._gresource: Optional[Gresource] = None
|
||||
self._ubuntu_gdm_alternatives_updater: Optional[UbuntuGDMAlternativesUpdater] = None
|
||||
|
||||
self._preparer: Optional[GDMThemePreparer] = None
|
||||
self._installer: Optional[GDMThemeInstaller] = None
|
||||
self._remover: Optional[GDMThemeRemover] = None
|
||||
|
||||
def with_mode(self, mode: InstallationMode | None) -> 'GDMThemeBuilder':
|
||||
"""Set the mode for the theme."""
|
||||
self._mode = mode
|
||||
return self
|
||||
|
||||
def with_filled(self, is_filled=True) -> 'GDMThemeBuilder':
|
||||
"""Set the filled state for the theme."""
|
||||
self._is_filled = is_filled
|
||||
return self
|
||||
|
||||
def with_logger_factory(self, logger_factory: LoggerFactory) -> 'GDMThemeBuilder':
|
||||
"""Inject a logger factory for logging purposes."""
|
||||
self._logger_factory = logger_factory
|
||||
return self
|
||||
|
||||
def with_gresource(self, gresource: Gresource) -> 'GDMThemeBuilder':
|
||||
"""Inject a gresource instance for managing gresource files."""
|
||||
self._gresource = gresource
|
||||
return self
|
||||
|
||||
def with_ubuntu_gdm_alternatives_updater(self, alternatives_updater: UbuntuGDMAlternativesUpdater) -> 'GDMThemeBuilder':
|
||||
"""Inject an alternatives updater for managing GDM alternatives."""
|
||||
self._ubuntu_gdm_alternatives_updater = alternatives_updater
|
||||
return self
|
||||
|
||||
def with_preparer(self, preparer: GDMThemePreparer) -> 'GDMThemeBuilder':
|
||||
"""Inject a preparer for preparing the theme."""
|
||||
self._preparer = preparer
|
||||
return self
|
||||
|
||||
def with_installer(self, installer: GDMThemeInstaller) -> 'GDMThemeBuilder':
|
||||
"""Inject an installer for installing the theme."""
|
||||
self._installer = installer
|
||||
return self
|
||||
|
||||
def with_remover(self, remover: GDMThemeRemover) -> 'GDMThemeBuilder':
|
||||
"""Inject a remover for removing the theme."""
|
||||
self._remover = remover
|
||||
return self
|
||||
|
||||
def build(self) -> GDMTheme:
|
||||
"""
|
||||
Build the GDMTheme object with the configured components.
|
||||
|
||||
Automatically resolves any dependencies that haven't been explicitly
|
||||
provided through with_* methods. The order of resolution ensures
|
||||
that dependencies are created before they're needed.
|
||||
|
||||
:return: A fully configured GDMTheme instance ready for use
|
||||
"""
|
||||
self._resolve_logger_factory()
|
||||
self._resolve_gresource()
|
||||
self._resolve_ubuntu_gdm_alternatives_updater()
|
||||
|
||||
self._resolve_preparer()
|
||||
self._resolve_installer()
|
||||
self._resolve_remover()
|
||||
|
||||
return GDMTheme(self._preparer, self._installer, self._remover)
|
||||
|
||||
def _resolve_logger_factory(self):
|
||||
"""Instantiate a default Console logger if not explicitly provided."""
|
||||
if self._logger_factory: return
|
||||
self._logger_factory = Console()
|
||||
|
||||
def _resolve_gresource(self):
|
||||
"""
|
||||
Create a Gresource handler if not explicitly provided.
|
||||
Uses configuration values for file paths and destinations.
|
||||
"""
|
||||
if self._gresource: return
|
||||
|
||||
gresource_file = config.gnome_shell_gresource
|
||||
temp_folder = os.path.join(self._temp_folder, config.extracted_gdm_folder)
|
||||
destination = config.global_gnome_shell_theme
|
||||
runner = SubprocessCommandRunner()
|
||||
|
||||
self._gresource = Gresource(
|
||||
gresource_file=gresource_file,
|
||||
temp_folder=temp_folder,
|
||||
destination=destination,
|
||||
logger_factory=self._logger_factory,
|
||||
runner=runner
|
||||
)
|
||||
|
||||
def _resolve_ubuntu_gdm_alternatives_updater(self):
|
||||
"""Create an UbuntuGDMAlternativesUpdater if not explicitly provided."""
|
||||
if self._ubuntu_gdm_alternatives_updater: return
|
||||
alternatives_updater = AlternativesUpdater()
|
||||
self._ubuntu_gdm_alternatives_updater = UbuntuGDMAlternativesUpdater(alternatives_updater)
|
||||
|
||||
def _resolve_preparer(self):
|
||||
"""Create a GDMThemePreparer if not explicitly provided."""
|
||||
if self._preparer: return
|
||||
theme_builder = GnomeShellThemeBuilder(self.colors_provider)
|
||||
files_labeler_factory = FilesLabelerFactoryImpl()
|
||||
self._preparer = GDMThemePreparer(
|
||||
temp_folder=self._temp_folder,
|
||||
default_mode=self._mode,
|
||||
is_filled=self._is_filled,
|
||||
gresource=self._gresource,
|
||||
theme_builder=theme_builder,
|
||||
logger_factory=self._logger_factory,
|
||||
files_labeler_factory=files_labeler_factory,
|
||||
)
|
||||
|
||||
def _resolve_installer(self):
|
||||
"""Create a GDMThemeInstaller if not explicitly provided."""
|
||||
if self._installer: return
|
||||
self._installer = GDMThemeInstaller(
|
||||
gresource=self._gresource,
|
||||
alternatives_updater=self._ubuntu_gdm_alternatives_updater,
|
||||
)
|
||||
|
||||
def _resolve_remover(self):
|
||||
"""Create a GDMThemeRemover if not explicitly provided."""
|
||||
if self._remover: return
|
||||
self._remover = GDMThemeRemover(
|
||||
gresource=self._gresource,
|
||||
alternatives_updater=self._ubuntu_gdm_alternatives_updater,
|
||||
logger_factory=self._logger_factory
|
||||
)
|
||||
66
scripts/utils/global_theme/gdm_installer.py
Normal file
66
scripts/utils/global_theme/gdm_installer.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from scripts.utils.global_theme.gdm_theme_prepare import GDMThemePrepare
|
||||
from scripts.utils.global_theme.ubuntu_alternatives_updater import UbuntuGDMAlternativesUpdater
|
||||
from scripts.utils.gresource.gresource import Gresource
|
||||
|
||||
|
||||
class GDMThemeInstaller:
|
||||
"""
|
||||
Handles the installation of GDM themes system-wide.
|
||||
|
||||
This class manages:
|
||||
- Compiling prepared theme resources into a gresource file
|
||||
- Creating backups of original system files
|
||||
- Installing compiled themes via the alternatives system
|
||||
- Detecting if a theme is already installed
|
||||
"""
|
||||
def __init__(self, gresource: Gresource, alternatives_updater: UbuntuGDMAlternativesUpdater):
|
||||
"""
|
||||
:param gresource: Handler for gresource operations
|
||||
:param alternatives_updater: Handler for update-alternatives operations
|
||||
"""
|
||||
self.gresource = gresource
|
||||
self.alternatives_updater = alternatives_updater
|
||||
|
||||
self._is_installed_trigger = "\n/* Marble theme */\n"
|
||||
|
||||
def is_installed(self) -> bool:
|
||||
"""
|
||||
Check if the theme is installed
|
||||
by looking for the trigger in the gresource file.
|
||||
"""
|
||||
return self.gresource.has_trigger(self._is_installed_trigger)
|
||||
|
||||
def compile(self, themes: list[GDMThemePrepare], hue: int, color: str, sat: int = None):
|
||||
"""
|
||||
Prepares themes for gresource and compiles them.
|
||||
:param themes: themes to be compiled
|
||||
:param hue: hue value for the theme
|
||||
:param color: the color name. in GDM will only be shown in logger
|
||||
:param sat: saturation value for the theme
|
||||
"""
|
||||
self._generate_themes(themes, hue, color, sat)
|
||||
self.gresource.compile()
|
||||
|
||||
def _generate_themes(self, themes: list[GDMThemePrepare], hue: int, color: str, sat: int = None):
|
||||
"""Generate theme files for further compiling by gresource"""
|
||||
for theme_prepare in themes:
|
||||
if theme_prepare.label is not None:
|
||||
theme_prepare.label_theme()
|
||||
|
||||
theme_prepare.remove_keywords("!important")
|
||||
theme_prepare.remove_properties("background-color", "color", "box-shadow", "border-radius")
|
||||
theme_prepare.prepend_source_styles(self._is_installed_trigger)
|
||||
|
||||
theme_prepare.install(hue, color, sat, destination=self.gresource.temp_folder)
|
||||
|
||||
def backup(self):
|
||||
"""Backup the current gresource file."""
|
||||
self.gresource.backup()
|
||||
|
||||
def install(self):
|
||||
"""
|
||||
Install the theme globally by moving the compiled gresource file to the destination.
|
||||
Also updates the alternatives for the gdm theme.
|
||||
"""
|
||||
self.gresource.move()
|
||||
self.alternatives_updater.install_and_set()
|
||||
94
scripts/utils/global_theme/gdm_preparer.py
Normal file
94
scripts/utils/global_theme/gdm_preparer.py
Normal file
@@ -0,0 +1,94 @@
|
||||
import os
|
||||
|
||||
from scripts.utils.files_labeler import FilesLabelerFactory
|
||||
from scripts.utils.global_theme.gdm_theme_prepare import GDMThemePrepare
|
||||
from scripts.utils.gresource.gresource import Gresource
|
||||
from scripts.utils.logger.logger import LoggerFactory
|
||||
from scripts.utils.theme.gnome_shell_theme_builder import GnomeShellThemeBuilder
|
||||
|
||||
|
||||
class GDMThemePreparer:
|
||||
"""
|
||||
GDM theme preparation module.
|
||||
|
||||
This module contains classes responsible for extracting and processing
|
||||
GDM theme resources for later compilation and installation.
|
||||
|
||||
The main class, GDMThemePreparer, orchestrates the extraction process
|
||||
and creates GDMThemePrepare objects representing processable themes.
|
||||
"""
|
||||
def __init__(self, temp_folder: str, default_mode: str | None, is_filled: bool,
|
||||
gresource: Gresource,
|
||||
theme_builder: GnomeShellThemeBuilder,
|
||||
logger_factory: LoggerFactory,
|
||||
files_labeler_factory: FilesLabelerFactory):
|
||||
"""
|
||||
:param temp_folder: Temporary folder for extracted theme files
|
||||
:param default_mode: Default theme mode to use if not specified in CSS filename
|
||||
:param is_filled: Whether to generate filled (True) or dimmed (False) styles
|
||||
:param gresource: Gresource instance for managing gresource files
|
||||
:param theme_builder: Theme builder instance for creating themes
|
||||
:param logger_factory: Logger factory for logging messages
|
||||
:param files_labeler_factory: Factory for creating FilesLabeler instances
|
||||
"""
|
||||
self.temp_folder = temp_folder
|
||||
self.gresource_temp_folder = gresource.temp_folder
|
||||
|
||||
self.default_mode = default_mode
|
||||
self.is_filled = is_filled
|
||||
|
||||
self.gresource = gresource
|
||||
self.theme_builder = theme_builder
|
||||
self.logger_factory = logger_factory
|
||||
self.files_labeler_factory = files_labeler_factory
|
||||
|
||||
def use_backup_as_source(self):
|
||||
"""Use backup gresource file for extraction"""
|
||||
self.gresource.use_backup_gresource()
|
||||
self.logger_factory.create_logger().info("Using backup gresource file for extraction...")
|
||||
|
||||
def prepare(self) -> list[GDMThemePrepare]:
|
||||
"""
|
||||
Extract and prepare GDM themes for processing.
|
||||
:return: List of prepared theme objects ready for compilation
|
||||
"""
|
||||
self.gresource.extract()
|
||||
return self._find_themes()
|
||||
|
||||
def _find_themes(self) -> list[GDMThemePrepare]:
|
||||
extracted_files = os.listdir(self.gresource_temp_folder)
|
||||
allowed_css = {"gnome-shell-dark.css", "gnome-shell-light.css", "gnome-shell.css"}
|
||||
|
||||
themes = [
|
||||
self._create_theme(file_name)
|
||||
for file_name in extracted_files
|
||||
if file_name in allowed_css
|
||||
]
|
||||
return themes
|
||||
|
||||
def _create_theme(self, file_name: str) -> GDMThemePrepare:
|
||||
"""Helper to create and prepare a theme"""
|
||||
mode = file_name.split("-")[-1].replace(".css", "")
|
||||
mode = mode if mode in {"dark", "light"} else self.default_mode
|
||||
|
||||
self._setup_theme_builder(file_name, mode)
|
||||
|
||||
theme = self.theme_builder.build()
|
||||
theme.prepare()
|
||||
|
||||
theme_file = os.path.join(self.gresource_temp_folder, file_name)
|
||||
files_labeler = self.files_labeler_factory.create(
|
||||
theme.temp_folder, theme.main_styles)
|
||||
return GDMThemePrepare(
|
||||
theme=theme, theme_file=theme_file, label=mode, files_labeler=files_labeler)
|
||||
|
||||
def _setup_theme_builder(self, file_name: str, mode: str):
|
||||
theme_name = file_name.replace(".css", "")
|
||||
|
||||
(self.theme_builder
|
||||
.with_temp_folder(self.temp_folder)
|
||||
.with_theme_name(theme_name)
|
||||
.with_mode(mode)
|
||||
.filled(self.is_filled)
|
||||
.with_logger_factory(self.logger_factory)
|
||||
.with_reset_dependencies())
|
||||
65
scripts/utils/global_theme/gdm_remover.py
Normal file
65
scripts/utils/global_theme/gdm_remover.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from scripts.utils.global_theme.ubuntu_alternatives_updater import UbuntuGDMAlternativesUpdater
|
||||
from scripts.utils.gresource import GresourceBackupNotFoundError
|
||||
from scripts.utils.gresource.gresource import Gresource
|
||||
from scripts.utils.logger.console import Console, Color, Format
|
||||
from scripts.utils.logger.logger import LoggerFactory
|
||||
|
||||
|
||||
class GDMThemeRemover:
|
||||
"""
|
||||
Responsible for safely removing installed GDM themes.
|
||||
|
||||
This class handles:
|
||||
- Restoring original gresource files from backups
|
||||
- Removing theme alternatives from the system
|
||||
- Providing feedback about removal status
|
||||
"""
|
||||
def __init__(self,
|
||||
gresource: Gresource,
|
||||
alternatives_updater: UbuntuGDMAlternativesUpdater,
|
||||
logger_factory: LoggerFactory):
|
||||
"""
|
||||
:param gresource: Handler for gresource operations
|
||||
:param alternatives_updater: Handler for update-alternatives operations
|
||||
:param logger_factory: Factory for creating loggers
|
||||
"""
|
||||
self.gresource = gresource
|
||||
self.alternatives_updater = alternatives_updater
|
||||
self.remover_logger = GDMRemoverLogger(logger_factory)
|
||||
|
||||
def remove(self):
|
||||
"""Restores the gresource backup and removes the alternatives."""
|
||||
self.remover_logger.start_removing()
|
||||
|
||||
try:
|
||||
self.gresource.restore()
|
||||
self.alternatives_updater.remove()
|
||||
self.remover_logger.success_removing()
|
||||
except GresourceBackupNotFoundError:
|
||||
self.remover_logger.error_removing()
|
||||
|
||||
def warn_not_installed(self):
|
||||
self.remover_logger.not_installed_warning()
|
||||
|
||||
|
||||
class GDMRemoverLogger:
|
||||
def __init__(self, logger_factory: LoggerFactory):
|
||||
self.logger_factory = logger_factory
|
||||
self.removing_line = None
|
||||
|
||||
def start_removing(self):
|
||||
self.removing_line = self.logger_factory.create_logger()
|
||||
self.removing_line.update("Theme is installed. Removing...")
|
||||
|
||||
def success_removing(self):
|
||||
self.removing_line.success("Global theme removed successfully. Restart GDM to apply changes.")
|
||||
|
||||
def error_removing(self):
|
||||
formatted_shell = Console.format("gnome-shell", color=Color.BLUE, format_type=Format.BOLD)
|
||||
self.removing_line.error(f"Backup file not found. Try reinstalling {formatted_shell} package.")
|
||||
|
||||
def not_installed_warning(self):
|
||||
self.logger_factory.create_logger().error(
|
||||
"Theme is not installed. Nothing to remove.")
|
||||
self.logger_factory.create_logger().warn(
|
||||
"If theme is still installed globally, try reinstalling gnome-shell package.")
|
||||
68
scripts/utils/global_theme/gdm_theme_prepare.py
Normal file
68
scripts/utils/global_theme/gdm_theme_prepare.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from scripts.utils import remove_keywords, remove_properties
|
||||
from scripts.utils.files_labeler import FilesLabeler
|
||||
from scripts.utils.theme.theme import Theme
|
||||
|
||||
|
||||
class GDMThemePrepare:
|
||||
"""
|
||||
Prepares theme files for installation into the GDM system.
|
||||
|
||||
This class handles:
|
||||
- Theme file labeling for dark/light variants
|
||||
- CSS property and keyword removal for customization
|
||||
- Theme installation with color adjustments
|
||||
"""
|
||||
def __init__(self, theme: Theme, theme_file: str, label: str | None,
|
||||
files_labeler: FilesLabeler):
|
||||
"""
|
||||
:param theme: The theme object to prepare
|
||||
:param theme_file: Path to the original decompiled CSS file
|
||||
:param label: Optional label for the theme (e.g. "dark", "light")
|
||||
:param files_labeler: FilesLabeler instance for labeling files
|
||||
"""
|
||||
self.theme = theme
|
||||
self.theme_file = theme_file
|
||||
self.label = label
|
||||
self.files_labeler = files_labeler
|
||||
|
||||
def label_theme(self):
|
||||
"""
|
||||
Label the theme files if the label is set.
|
||||
Also updates references in the theme files.
|
||||
:raises ValueError: if the label is not set
|
||||
"""
|
||||
if self.label is None:
|
||||
raise ValueError("Label is not set for the theme.")
|
||||
|
||||
self.files_labeler.append_label(self.label)
|
||||
|
||||
def remove_keywords(self, *args: str):
|
||||
"""Remove specific keywords from the theme file"""
|
||||
remove_keywords(self.theme_file, *args)
|
||||
|
||||
def remove_properties(self, *args: str):
|
||||
"""Remove specific properties from the theme file"""
|
||||
remove_properties(self.theme_file, *args)
|
||||
|
||||
def prepend_source_styles(self, trigger: str):
|
||||
"""
|
||||
Add source styles and installation trigger to the theme file.
|
||||
|
||||
This adds original theme styles and a marker that identifies
|
||||
the theme as installed by this application.
|
||||
|
||||
:param trigger: String marker used to identify installed themes
|
||||
"""
|
||||
with open(self.theme_file, 'r') as gnome_theme:
|
||||
gnome_styles = gnome_theme.read() + '\n' + trigger + '\n'
|
||||
self.theme.add_to_start(gnome_styles)
|
||||
|
||||
def install(self, hue: int, color: str, sat: int | None, destination: str):
|
||||
"""
|
||||
Install the theme to the specified destination
|
||||
:param hue: Hue value for the theme
|
||||
:param color: Color name for the theme
|
||||
:param sat: Saturation value for the theme
|
||||
:param destination: Destination folder for the theme
|
||||
"""
|
||||
self.theme.install(hue, color, sat, destination=destination)
|
||||
65
scripts/utils/global_theme/ubuntu_alternatives_updater.py
Normal file
65
scripts/utils/global_theme/ubuntu_alternatives_updater.py
Normal file
@@ -0,0 +1,65 @@
|
||||
import os.path
|
||||
|
||||
from scripts import config
|
||||
from scripts.utils.alternatives_updater import AlternativesUpdater
|
||||
|
||||
|
||||
class UbuntuGDMAlternativesUpdater:
|
||||
"""
|
||||
Manages update-alternatives for Ubuntu GDM themes.
|
||||
|
||||
This class handles:
|
||||
- Creating alternatives for GDM theme files
|
||||
- Setting installed theme as the active alternative
|
||||
- Removing theme alternatives during uninstallation
|
||||
"""
|
||||
def __init__(self, alternatives_updater: AlternativesUpdater):
|
||||
"""
|
||||
:param alternatives_updater: Handler for update-alternatives operations
|
||||
"""
|
||||
self.ubuntu_gresource_link_name = config.ubuntu_gresource_link
|
||||
self.destination_dir = config.global_gnome_shell_theme
|
||||
self.destination_file = config.gnome_shell_gresource
|
||||
|
||||
self.alternatives_updater = alternatives_updater
|
||||
|
||||
self._update_gresource_paths()
|
||||
|
||||
def _update_gresource_paths(self):
|
||||
self.ubuntu_gresource_path = os.path.join(self.destination_dir, self.ubuntu_gresource_link_name)
|
||||
self.gnome_gresource_path = os.path.join(self.destination_dir, self.destination_file)
|
||||
|
||||
def with_custom_destination(self, destination_dir: str, destination_file: str):
|
||||
"""Set custom destination directory and file for the theme."""
|
||||
self.destination_dir = destination_dir
|
||||
self.destination_file = destination_file
|
||||
self._update_gresource_paths()
|
||||
return self
|
||||
|
||||
def install_and_set(self, priority: int = 0):
|
||||
"""
|
||||
Add theme as an alternative and set it as active.
|
||||
|
||||
This creates a system alternative for the GDM theme and
|
||||
makes it the active selection with the specified priority.
|
||||
|
||||
:param priority: Priority level for the alternative (higher wins in conflicts)
|
||||
"""
|
||||
self.alternatives_updater.install_and_set(
|
||||
link=self.ubuntu_gresource_path,
|
||||
name=self.ubuntu_gresource_link_name,
|
||||
path=self.gnome_gresource_path,
|
||||
priority=priority
|
||||
)
|
||||
|
||||
def remove(self):
|
||||
"""
|
||||
Remove the theme alternative from the system.
|
||||
|
||||
This removes the previously installed alternative, allowing
|
||||
the system to fall back to the default GDM theme.
|
||||
"""
|
||||
self.alternatives_updater.remove(
|
||||
name=self.ubuntu_gresource_link_name,
|
||||
path=self.gnome_gresource_path
|
||||
)
|
||||
@@ -2,7 +2,7 @@ import subprocess
|
||||
import time
|
||||
|
||||
from scripts import config
|
||||
from scripts.utils.console import Console, Format, Color
|
||||
from scripts.utils.logger.console import Console, Format, Color
|
||||
from scripts.utils.parse_folder import parse_folder
|
||||
|
||||
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
import os
|
||||
import subprocess
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
|
||||
from scripts.utils.console import Console
|
||||
|
||||
|
||||
class GresourceBackupNotFoundError(FileNotFoundError):
|
||||
def __init__(self, location: str = None):
|
||||
if location:
|
||||
super().__init__(f"Gresource backup file not found: {location}")
|
||||
else:
|
||||
super().__init__("Gresource backup file not found.")
|
||||
|
||||
|
||||
class Gresource:
|
||||
"""Handles the extraction and compilation of gresource files for GNOME Shell themes."""
|
||||
|
||||
def __init__(self, gresource_file: str, temp_folder: str, destination: str):
|
||||
"""
|
||||
:param gresource_file: The name of the gresource file to be processed.
|
||||
:param temp_folder: The temporary folder where resources will be extracted.
|
||||
:param destination: The destination folder where the compiled gresource file will be saved.
|
||||
"""
|
||||
self.gresource_file = gresource_file
|
||||
self.temp_folder = temp_folder
|
||||
self.destination = destination
|
||||
|
||||
self.__temp_gresource = os.path.join(temp_folder, gresource_file)
|
||||
self.__destination_gresource = os.path.join(destination, gresource_file)
|
||||
self.__active_source_gresource = self.__destination_gresource
|
||||
self.__backup_gresource = os.path.join(destination, f"{gresource_file}.backup")
|
||||
self.__gresource_xml = os.path.join(temp_folder, f"{gresource_file}.xml")
|
||||
|
||||
def use_backup_gresource(self):
|
||||
if not os.path.exists(self.__backup_gresource):
|
||||
raise GresourceBackupNotFoundError(self.__backup_gresource)
|
||||
self.__active_source_gresource = self.__backup_gresource
|
||||
|
||||
def extract(self):
|
||||
extract_line = Console.Line()
|
||||
extract_line.update("Extracting gresource files...")
|
||||
|
||||
resources = self.__get_resources_list()
|
||||
self.__extract_resources(resources)
|
||||
|
||||
extract_line.success("Extracted gresource files.")
|
||||
|
||||
def __get_resources_list(self):
|
||||
resources_list_response = subprocess.run(
|
||||
["gresource", "list", self.__active_source_gresource],
|
||||
capture_output=True, text=True, check=False
|
||||
)
|
||||
|
||||
if resources_list_response.stderr:
|
||||
raise Exception(f"gresource could not process the theme file: {self.__active_source_gresource}")
|
||||
|
||||
return resources_list_response.stdout.strip().split("\n")
|
||||
|
||||
def __extract_resources(self, resources: list[str]):
|
||||
prefix = "/org/gnome/shell/theme/"
|
||||
try:
|
||||
for resource in resources:
|
||||
resource_path = resource.replace(prefix, "")
|
||||
output_path = os.path.join(self.temp_folder, resource_path)
|
||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||
|
||||
with open(output_path, 'wb') as f:
|
||||
subprocess.run(
|
||||
["gresource", "extract", self.__active_source_gresource, resource],
|
||||
stdout=f, check=True
|
||||
)
|
||||
except FileNotFoundError as e:
|
||||
if "gresource" in str(e):
|
||||
self.__raise_gresource_error(e)
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def __raise_gresource_error(e: Exception):
|
||||
print("Error: 'gresource' command not found.")
|
||||
print("Please install the glib2-devel package:")
|
||||
print(" - For Fedora/RHEL: sudo dnf install glib2-devel")
|
||||
print(" - For Ubuntu/Debian: sudo apt install libglib2.0-dev")
|
||||
print(" - For Arch: sudo pacman -S glib2-devel")
|
||||
raise Exception("Missing required dependency: glib2-devel") from e
|
||||
|
||||
def compile(self):
|
||||
compile_line = Console.Line()
|
||||
compile_line.update("Compiling gnome-shell theme...")
|
||||
|
||||
self.__create_gresource_xml()
|
||||
self.__compile_resources()
|
||||
|
||||
compile_line.success("Theme compiled.")
|
||||
|
||||
def __create_gresource_xml(self):
|
||||
with open(self.__gresource_xml, 'w') as gresource_xml:
|
||||
gresource_xml.write(self.__generate_gresource_xml())
|
||||
|
||||
def __generate_gresource_xml(self):
|
||||
files_to_include = self.__get_files_to_include()
|
||||
nl = "\n" # fstring doesn't support newline character
|
||||
return textwrap.dedent(f"""
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gresources>
|
||||
<gresource prefix="/org/gnome/shell/theme">
|
||||
{nl.join(files_to_include)}
|
||||
</gresource>
|
||||
</gresources>
|
||||
""")
|
||||
|
||||
def __get_files_to_include(self):
|
||||
temp_path = Path(self.temp_folder)
|
||||
return [
|
||||
f"<file>{file.relative_to(temp_path)}</file>"
|
||||
for file in temp_path.glob('**/*')
|
||||
if file.is_file()
|
||||
]
|
||||
|
||||
def __compile_resources(self):
|
||||
try:
|
||||
subprocess.run(["glib-compile-resources",
|
||||
"--sourcedir", self.temp_folder,
|
||||
"--target", self.__temp_gresource,
|
||||
self.__gresource_xml
|
||||
],
|
||||
cwd=self.temp_folder, check=True)
|
||||
except FileNotFoundError as e:
|
||||
if "glib-compile-resources" in str(e):
|
||||
self.__raise_gresource_error(e)
|
||||
raise
|
||||
|
||||
def backup(self):
|
||||
backup_line = Console.Line()
|
||||
backup_line.update("Backing up gresource files...")
|
||||
|
||||
subprocess.run(["cp", "-aT",
|
||||
self.__destination_gresource,
|
||||
self.__backup_gresource],
|
||||
check=True)
|
||||
|
||||
backup_line.success("Backed up gresource files.")
|
||||
|
||||
def restore(self):
|
||||
if not os.path.exists(self.__backup_gresource):
|
||||
raise GresourceBackupNotFoundError(self.__backup_gresource)
|
||||
|
||||
subprocess.run(["sudo", "mv", "-f",
|
||||
self.__backup_gresource,
|
||||
self.__destination_gresource],
|
||||
check=True)
|
||||
|
||||
|
||||
def move(self):
|
||||
move_line = Console.Line()
|
||||
move_line.update("Moving gresource files...")
|
||||
|
||||
subprocess.run(["sudo", "cp", "-f",
|
||||
self.__temp_gresource,
|
||||
self.__destination_gresource],
|
||||
check=True)
|
||||
|
||||
move_line.success("Moved gresource files.")
|
||||
21
scripts/utils/gresource/__init__.py
Normal file
21
scripts/utils/gresource/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
class GresourceBackupNotFoundError(FileNotFoundError):
|
||||
def __init__(self, location: str = None):
|
||||
if location:
|
||||
super().__init__(f"Gresource backup file not found: {location}")
|
||||
else:
|
||||
super().__init__("Gresource backup file not found.")
|
||||
|
||||
|
||||
class MissingDependencyError(Exception):
|
||||
def __init__(self, dependency: str):
|
||||
super().__init__(f"Missing required dependency: {dependency}")
|
||||
self.dependency = dependency
|
||||
|
||||
|
||||
def raise_gresource_error(tool: str, e: Exception):
|
||||
print(f"Error: '{tool}' command not found.")
|
||||
print("Please install the glib2-devel package:")
|
||||
print(" - For Fedora/RHEL: sudo dnf install glib2-devel")
|
||||
print(" - For Ubuntu/Debian: sudo apt install libglib2.0-dev")
|
||||
print(" - For Arch: sudo pacman -S glib2-devel")
|
||||
raise MissingDependencyError("glib2-devel") from e
|
||||
71
scripts/utils/gresource/gresource.py
Normal file
71
scripts/utils/gresource/gresource.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import os
|
||||
|
||||
from scripts.utils.alternatives_updater import PathString
|
||||
from scripts.utils.command_runner.command_runner import CommandRunner
|
||||
from scripts.utils.gresource.gresource_backuper import GresourceBackuperManager
|
||||
from scripts.utils.gresource.gresource_compiler import GresourceCompiler
|
||||
from scripts.utils.gresource.gresource_extractor import GresourceExtractor
|
||||
from scripts.utils.gresource.gresource_mover import GresourceMover
|
||||
from scripts.utils.logger.logger import LoggerFactory
|
||||
|
||||
|
||||
class Gresource:
|
||||
"""Orchestrator for gresource files."""
|
||||
|
||||
def __init__(
|
||||
self, gresource_file: str, temp_folder: PathString, destination: PathString,
|
||||
logger_factory: LoggerFactory, runner: CommandRunner
|
||||
):
|
||||
"""
|
||||
:param gresource_file: The name of the gresource file to be processed.
|
||||
:param temp_folder: The temporary folder where resources will be extracted.
|
||||
:param destination: The destination folder where the compiled gresource file will be saved.
|
||||
"""
|
||||
self.gresource_file = gresource_file
|
||||
self.temp_folder = temp_folder
|
||||
self.destination = destination
|
||||
|
||||
self.logger_factory = logger_factory
|
||||
self.runner = runner
|
||||
|
||||
self._temp_gresource = os.path.join(temp_folder, gresource_file)
|
||||
self._destination_gresource = os.path.join(destination, gresource_file)
|
||||
self._active_source_gresource = self._destination_gresource
|
||||
|
||||
self._backuper = GresourceBackuperManager(self._destination_gresource,
|
||||
logger_factory=self.logger_factory)
|
||||
|
||||
def has_trigger(self, trigger: str) -> bool:
|
||||
"""
|
||||
Check if the trigger is present in the gresource file.
|
||||
Used to detect if the theme is already installed.
|
||||
:param trigger: The trigger to check for.
|
||||
:return: True if the trigger is found, False otherwise.
|
||||
"""
|
||||
return self._backuper.has_trigger(trigger)
|
||||
|
||||
def use_backup_gresource(self):
|
||||
self._active_source_gresource = self._backuper.get_backup()
|
||||
return self._active_source_gresource
|
||||
|
||||
def extract(self):
|
||||
extractor = GresourceExtractor(self._active_source_gresource, self.temp_folder,
|
||||
logger_factory=self.logger_factory, runner=self.runner)
|
||||
extractor.extract()
|
||||
|
||||
def compile(self):
|
||||
compiler = GresourceCompiler(self.temp_folder, self._temp_gresource,
|
||||
logger_factory=self.logger_factory, runner=self.runner)
|
||||
compiler.compile()
|
||||
|
||||
def backup(self):
|
||||
self._backuper.backup()
|
||||
|
||||
def restore(self):
|
||||
self._backuper.restore()
|
||||
self._active_source_gresource = self._destination_gresource
|
||||
|
||||
def move(self):
|
||||
mover = GresourceMover(self._temp_gresource, self._destination_gresource,
|
||||
logger_factory=self.logger_factory)
|
||||
mover.move()
|
||||
59
scripts/utils/gresource/gresource_backuper.py
Normal file
59
scripts/utils/gresource/gresource_backuper.py
Normal file
@@ -0,0 +1,59 @@
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from scripts.utils.gresource import GresourceBackupNotFoundError
|
||||
from scripts.utils.logger.logger import LoggerFactory
|
||||
|
||||
|
||||
class GresourceBackuperManager:
|
||||
def __init__(self, destination_file: str, logger_factory: LoggerFactory):
|
||||
self.destination_file = destination_file
|
||||
self._backup_file = f"{destination_file}.backup"
|
||||
self._backuper = GresourceBackuper(destination_file, self._backup_file, logger_factory)
|
||||
|
||||
def has_trigger(self, trigger: str) -> bool:
|
||||
return self._backuper.has_trigger(trigger)
|
||||
|
||||
def backup(self):
|
||||
self._backuper.backup()
|
||||
|
||||
def restore(self):
|
||||
self._backuper.restore()
|
||||
|
||||
def get_backup(self) -> str:
|
||||
return self._backuper.get_backup()
|
||||
|
||||
|
||||
class GresourceBackuper:
|
||||
def __init__(self, destination_file: str, backup_file: str, logger_factory: LoggerFactory):
|
||||
self.destination_file = destination_file
|
||||
self.backup_file = backup_file
|
||||
self.logger_factory = logger_factory
|
||||
|
||||
def has_trigger(self, trigger: str) -> bool:
|
||||
with open(self.destination_file, "rb") as f:
|
||||
return trigger.encode() in f.read()
|
||||
|
||||
def get_backup(self) -> str:
|
||||
if not os.path.exists(self.backup_file):
|
||||
raise GresourceBackupNotFoundError(self.backup_file)
|
||||
return self.backup_file
|
||||
|
||||
def backup(self):
|
||||
backup_line = self.logger_factory.create_logger()
|
||||
backup_line.update("Backing up gresource files...")
|
||||
|
||||
if os.path.exists(self.backup_file):
|
||||
os.remove(self.backup_file)
|
||||
|
||||
shutil.copy2(self.destination_file, self.backup_file)
|
||||
|
||||
backup_line.success("Backed up gresource files.")
|
||||
|
||||
def restore(self):
|
||||
if not os.path.exists(self.backup_file):
|
||||
raise GresourceBackupNotFoundError(self.backup_file)
|
||||
|
||||
shutil.move(self.backup_file, self.destination_file)
|
||||
|
||||
self.logger_factory.create_logger().success("Restored gresource files.")
|
||||
68
scripts/utils/gresource/gresource_compiler.py
Normal file
68
scripts/utils/gresource/gresource_compiler.py
Normal file
@@ -0,0 +1,68 @@
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
|
||||
from scripts.utils.command_runner.command_runner import CommandRunner
|
||||
from scripts.utils.gresource import raise_gresource_error
|
||||
from scripts.utils.logger.logger import LoggerFactory
|
||||
|
||||
|
||||
class GresourceCompiler:
|
||||
def __init__(
|
||||
self, source_folder: str, target_file: str,
|
||||
logger_factory: LoggerFactory, runner: CommandRunner
|
||||
):
|
||||
self.source_folder = source_folder
|
||||
self.target_file = target_file
|
||||
self.gresource_xml = target_file + ".xml"
|
||||
|
||||
self.logger_factory = logger_factory
|
||||
self.runner = runner
|
||||
|
||||
def compile(self):
|
||||
compile_line = self.logger_factory.create_logger()
|
||||
compile_line.update("Compiling gnome-shell theme...")
|
||||
|
||||
self._create_gresource_xml()
|
||||
self._compile_resources()
|
||||
|
||||
compile_line.success("Compiled gnome-shell theme.")
|
||||
|
||||
def _create_gresource_xml(self):
|
||||
with open(self.gresource_xml, 'w') as gresource_xml:
|
||||
gresource_xml.write(self._generate_gresource_xml())
|
||||
|
||||
def _generate_gresource_xml(self):
|
||||
files_to_include = self._get_files_to_include()
|
||||
nl = "\n" # fstring doesn't support newline character
|
||||
return textwrap.dedent(f"""
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gresources>
|
||||
<gresource prefix="/org/gnome/shell/theme">
|
||||
{nl.join(files_to_include)}
|
||||
</gresource>
|
||||
</gresources>
|
||||
""")
|
||||
|
||||
def _get_files_to_include(self):
|
||||
source_path = Path(self.source_folder)
|
||||
return [
|
||||
f"<file>{file.relative_to(source_path)}</file>"
|
||||
for file in source_path.glob('**/*')
|
||||
if file.is_file()
|
||||
]
|
||||
|
||||
def _compile_resources(self):
|
||||
try:
|
||||
self._try_compile_resources()
|
||||
except FileNotFoundError as e:
|
||||
if "glib-compile-resources" in str(e):
|
||||
raise_gresource_error("glib-compile-resources", e)
|
||||
raise
|
||||
|
||||
def _try_compile_resources(self):
|
||||
self.runner.run(["glib-compile-resources",
|
||||
"--sourcedir", self.source_folder,
|
||||
"--target", self.target_file,
|
||||
self.gresource_xml
|
||||
],
|
||||
cwd=self.source_folder, check=True)
|
||||
56
scripts/utils/gresource/gresource_extractor.py
Normal file
56
scripts/utils/gresource/gresource_extractor.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import os
|
||||
|
||||
from scripts.utils.command_runner.command_runner import CommandRunner
|
||||
from scripts.utils.gresource import raise_gresource_error
|
||||
from scripts.utils.logger.logger import LoggerFactory
|
||||
|
||||
|
||||
class GresourceExtractor:
|
||||
def __init__(
|
||||
self, gresource_path: str, extract_folder: str,
|
||||
logger_factory: LoggerFactory, runner: CommandRunner
|
||||
):
|
||||
self.gresource_path = gresource_path
|
||||
self.extract_folder = extract_folder
|
||||
self.logger_factory = logger_factory
|
||||
self.runner = runner
|
||||
|
||||
def extract(self):
|
||||
extract_line = self.logger_factory.create_logger()
|
||||
extract_line.update("Extracting gresource files...")
|
||||
|
||||
self._try_extract_resources()
|
||||
|
||||
extract_line.success("Extracted gresource files.")
|
||||
|
||||
def _try_extract_resources(self):
|
||||
try:
|
||||
resources = self._get_resources_list()
|
||||
self._extract_resources(resources)
|
||||
except FileNotFoundError as e:
|
||||
print(e)
|
||||
if "gresource" in str(e):
|
||||
raise_gresource_error("gresource", e)
|
||||
raise
|
||||
except Exception as e:
|
||||
raise Exception(f"gresource could not process the theme file: {self.gresource_path}") from e
|
||||
|
||||
def _get_resources_list(self):
|
||||
resources_list_response = self.runner.run(
|
||||
["gresource", "list", self.gresource_path],
|
||||
capture_output=True, text=True, check=True
|
||||
)
|
||||
return resources_list_response.stdout.strip().split("\n")
|
||||
|
||||
def _extract_resources(self, resources: list[str]):
|
||||
prefix = "/org/gnome/shell/theme/"
|
||||
for resource in resources:
|
||||
resource_path = resource.replace(prefix, "")
|
||||
output_path = os.path.join(self.extract_folder, resource_path)
|
||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||
|
||||
with open(output_path, 'wb') as f:
|
||||
self.runner.run(
|
||||
["gresource", "extract", self.gresource_path, resource],
|
||||
stdout=f, check=True
|
||||
)
|
||||
21
scripts/utils/gresource/gresource_mover.py
Normal file
21
scripts/utils/gresource/gresource_mover.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from scripts.utils.logger.logger import LoggerFactory
|
||||
|
||||
|
||||
class GresourceMover:
|
||||
def __init__(self, source_file: str, destination_file: str, logger_factory: LoggerFactory):
|
||||
self.source_file = source_file
|
||||
self.destination_file = destination_file
|
||||
self.logger_factory = logger_factory
|
||||
|
||||
def move(self):
|
||||
move_line = self.logger_factory.create_logger()
|
||||
move_line.update("Moving gresource files...")
|
||||
|
||||
os.makedirs(os.path.dirname(self.destination_file), exist_ok=True)
|
||||
shutil.copyfile(self.source_file, self.destination_file)
|
||||
os.chmod(self.destination_file, 0o644)
|
||||
|
||||
move_line.success("Moved gresource files.")
|
||||
@@ -1,22 +0,0 @@
|
||||
def hex_to_rgba(hex_color):
|
||||
"""
|
||||
Convert hex(a) to rgba
|
||||
:param hex_color: input value
|
||||
"""
|
||||
|
||||
try:
|
||||
if len(hex_color) in range(6, 10):
|
||||
hex_color = hex_color.lstrip('#') + "ff"
|
||||
# if is convertable
|
||||
int(hex_color[:], 16)
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
except ValueError:
|
||||
raise ValueError(f'Error: Invalid HEX color code: {hex_color}')
|
||||
|
||||
else:
|
||||
return int(hex_color[0:2], 16), \
|
||||
int(hex_color[2:4], 16), \
|
||||
int(hex_color[4:6], 16), \
|
||||
int(hex_color[6:8], 16) / 255
|
||||
0
scripts/utils/logger/__init__.py
Normal file
0
scripts/utils/logger/__init__.py
Normal file
@@ -3,14 +3,24 @@ import threading
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from scripts.utils.logger.logger import LoggerFactory, Logger
|
||||
|
||||
class Console:
|
||||
|
||||
class Console(LoggerFactory):
|
||||
"""Manages console output for concurrent processes with line tracking"""
|
||||
_print_lock = threading.Lock()
|
||||
_line_mapping = {}
|
||||
_next_line = 0
|
||||
|
||||
class Line:
|
||||
def create_logger(self, name: Optional[str]=None) -> 'Console.Line':
|
||||
"""
|
||||
Create a logger instance with the given name.
|
||||
:param name: Name of the logger.
|
||||
:return: Logger instance.
|
||||
"""
|
||||
return Console.Line(name)
|
||||
|
||||
class Line(Logger):
|
||||
def __init__(self, name: Optional[str]=None):
|
||||
"""Initialize a new managed line"""
|
||||
self.name = name or f"line_{Console._next_line}"
|
||||
36
scripts/utils/logger/logger.py
Normal file
36
scripts/utils/logger/logger.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class LoggerFactory(ABC):
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def create_logger(name: Optional[str] = None) -> 'Logger':
|
||||
"""
|
||||
Create a logger instance with the given name.
|
||||
:param name: Name of the logger.
|
||||
:return: Logger instance.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class Logger(ABC):
|
||||
@abstractmethod
|
||||
def update(self, message: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def success(self, message):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def error(self, message):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def warn(self, message):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def info(self, message):
|
||||
pass
|
||||
@@ -7,7 +7,7 @@ import shutil
|
||||
from collections import defaultdict
|
||||
from typing import Any
|
||||
|
||||
from .console import Console, Color, Format
|
||||
from scripts.utils.logger.console import Console, Color, Format
|
||||
from .parse_folder import parse_folder
|
||||
from .. import config
|
||||
import os
|
||||
|
||||
@@ -10,10 +10,10 @@ def remove_properties(file, *args):
|
||||
with open(file, "r") as read_file:
|
||||
content = read_file.read()
|
||||
|
||||
for line in content.splitlines():
|
||||
for i, line in enumerate(content.splitlines()):
|
||||
if not any(prop in line for prop in args):
|
||||
new_content += line + "\n"
|
||||
elif "}" in line:
|
||||
elif "}" in line and not "{" in line:
|
||||
new_content += "}\n"
|
||||
|
||||
with open(file, "w") as write_file:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
def replace_keywords(file, *args):
|
||||
def replace_keywords(file, *args: tuple[str, str]):
|
||||
"""
|
||||
Replace file with several keywords
|
||||
:param file: file name where keywords must be replaced
|
||||
|
||||
40
scripts/utils/style_manager.py
Normal file
40
scripts/utils/style_manager.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import os
|
||||
|
||||
from scripts.utils import generate_file
|
||||
|
||||
|
||||
class StyleManager:
|
||||
"""Manages the style files for the theme."""
|
||||
|
||||
def __init__(self, output_file: str):
|
||||
"""
|
||||
:param output_file: The path to the output file where styles will be combined.
|
||||
"""
|
||||
self.output_file = output_file
|
||||
|
||||
def append_content(self, content: str):
|
||||
"""
|
||||
Append content to the output file.
|
||||
:raises FileNotFoundError: if the file does not exist.
|
||||
"""
|
||||
if not os.path.exists(self.output_file):
|
||||
raise FileNotFoundError(f"The file {self.output_file} does not exist.")
|
||||
with open(self.output_file, 'a') as output:
|
||||
output.write('\n' + content)
|
||||
|
||||
def prepend_content(self, content: str):
|
||||
"""
|
||||
Prepend content to the output file.
|
||||
:raises FileNotFoundError: if the file does not exist.
|
||||
"""
|
||||
with open(self.output_file, 'r') as output:
|
||||
main_content = output.read()
|
||||
with open(self.output_file, 'w') as output:
|
||||
output.write(content + '\n' + main_content)
|
||||
|
||||
def generate_combined_styles(self, sources_location: str, temp_folder: str):
|
||||
"""
|
||||
Generate the combined styles file
|
||||
by merging all styles from the source location.
|
||||
"""
|
||||
generate_file(sources_location, temp_folder, self.output_file)
|
||||
0
scripts/utils/theme/__init__.py
Normal file
0
scripts/utils/theme/__init__.py
Normal file
44
scripts/utils/theme/color_replacement_generator.py
Normal file
44
scripts/utils/theme/color_replacement_generator.py
Normal file
@@ -0,0 +1,44 @@
|
||||
import copy
|
||||
|
||||
from scripts.install.colors_definer import ColorsDefiner
|
||||
from scripts.types.installation_color import InstallationMode, InstallationColor
|
||||
from scripts.utils.color_converter.color_converter import ColorConverter
|
||||
|
||||
|
||||
class ColorReplacementGenerator:
|
||||
def __init__(self, colors_provider: ColorsDefiner, color_converter: ColorConverter):
|
||||
self.colors = copy.deepcopy(colors_provider)
|
||||
self.color_converter = color_converter
|
||||
|
||||
def convert(self, mode: InstallationMode, theme_color: InstallationColor) -> list[tuple[str, str]]:
|
||||
"""Generate a list of color replacements for the given theme color and mode"""
|
||||
return [
|
||||
(element, self._create_rgba_value(element, mode, theme_color))
|
||||
for element in self.colors.replacers
|
||||
]
|
||||
|
||||
def _create_rgba_value(self, element: str, mode: str, theme_color: InstallationColor) -> str:
|
||||
"""Create RGBA value for the specified element"""
|
||||
color_def = self._get_color_definition(element, mode)
|
||||
|
||||
lightness = int(color_def["l"]) / 100
|
||||
saturation = int(color_def["s"]) / 100
|
||||
if theme_color.saturation is not None:
|
||||
saturation *= theme_color.saturation / 100
|
||||
alpha = color_def["a"]
|
||||
|
||||
red, green, blue = self.color_converter.hsl_to_rgb(
|
||||
theme_color.hue, saturation, lightness
|
||||
)
|
||||
|
||||
return f"rgba({red}, {green}, {blue}, {alpha})"
|
||||
|
||||
def _get_color_definition(self, element: str, mode: str) -> dict:
|
||||
"""Get color definition for element, handling defaults if needed"""
|
||||
replacer = self.colors.replacers[element]
|
||||
|
||||
if mode not in replacer and replacer["default"]:
|
||||
default_element = replacer["default"]
|
||||
return self.colors.replacers[default_element][mode]
|
||||
|
||||
return replacer[mode]
|
||||
128
scripts/utils/theme/gnome_shell_theme_builder.py
Normal file
128
scripts/utils/theme/gnome_shell_theme_builder.py
Normal file
@@ -0,0 +1,128 @@
|
||||
import os.path
|
||||
|
||||
from scripts import config
|
||||
from scripts.install.colors_definer import ColorsDefiner
|
||||
from scripts.utils.color_converter.color_converter_impl import ColorConverterImpl
|
||||
from scripts.utils.logger.console import Console
|
||||
from scripts.utils.logger.logger import LoggerFactory
|
||||
from scripts.utils.style_manager import StyleManager
|
||||
from scripts.utils.theme.theme import Theme
|
||||
from scripts.utils.theme.theme_color_applier import ThemeColorApplier
|
||||
from scripts.utils.theme.color_replacement_generator import ColorReplacementGenerator
|
||||
from scripts.utils.theme.theme_installer import ThemeInstaller
|
||||
from scripts.utils.theme.theme_path_provider import ThemePathProvider
|
||||
from scripts.utils.theme.theme_preparation import ThemePreparation
|
||||
from scripts.utils.theme.theme_temp_manager import ThemeTempManager
|
||||
from theme import SourceFolder
|
||||
|
||||
|
||||
class GnomeShellThemeBuilder:
|
||||
"""
|
||||
Builder for creating a Gnome Shell theme.
|
||||
|
||||
Example:
|
||||
builder = GnomeShellThemeBuilder(colors_provider)
|
||||
theme = builder.with_mode("dark").filled().build()
|
||||
theme.prepare()
|
||||
theme.install(hue=200, name="MyTheme")
|
||||
"""
|
||||
|
||||
def __init__(self, colors_provider: ColorsDefiner):
|
||||
self.theme_name = "gnome-shell"
|
||||
self.colors_provider = colors_provider
|
||||
self.source_folder = SourceFolder().gnome_shell
|
||||
self._base_temp_folder = config.temp_folder
|
||||
self.destination_folder = config.themes_folder
|
||||
self.mode = None
|
||||
self.is_filled = False
|
||||
|
||||
self.temp_folder = os.path.join(self._base_temp_folder, self.theme_name)
|
||||
self.main_styles = os.path.join(self.temp_folder, f"{self.theme_name}.css")
|
||||
|
||||
self.logger_factory: LoggerFactory | None = None
|
||||
self.preparation: ThemePreparation | None = None
|
||||
self.installer: ThemeInstaller | None = None
|
||||
|
||||
def __update_paths(self):
|
||||
"""Update derived paths when base folder or theme name changes"""
|
||||
self.temp_folder = os.path.join(self._base_temp_folder, self.theme_name)
|
||||
self.main_styles = os.path.join(self.temp_folder, f"{self.theme_name}.css")
|
||||
|
||||
def with_temp_folder(self, temp_folder: str):
|
||||
"""Set the base temporary folder"""
|
||||
self._base_temp_folder = temp_folder
|
||||
self.__update_paths()
|
||||
return self
|
||||
|
||||
def with_theme_name(self, theme_name: str):
|
||||
"""Set the theme name"""
|
||||
self.theme_name = theme_name
|
||||
self.__update_paths()
|
||||
return self
|
||||
|
||||
def with_mode(self, mode):
|
||||
self.mode = mode
|
||||
return self
|
||||
|
||||
def filled(self, filled = True):
|
||||
self.is_filled = filled
|
||||
return self
|
||||
|
||||
def with_logger_factory(self, logger_factory: LoggerFactory | None):
|
||||
"""Inject a logger factory for logging purposes."""
|
||||
self.logger_factory = logger_factory
|
||||
return self
|
||||
|
||||
def with_preparation(self, preparation: ThemePreparation | None):
|
||||
"""Inject a preparation instance for preparing the theme."""
|
||||
self.preparation = preparation
|
||||
return self
|
||||
|
||||
def with_installer(self, installer: ThemeInstaller | None):
|
||||
"""Inject an installer for installing the theme."""
|
||||
self.installer = installer
|
||||
return self
|
||||
|
||||
|
||||
def with_reset_dependencies(self):
|
||||
"""Reset the dependencies for the theme preparation and installation."""
|
||||
self.preparation = None
|
||||
self.installer = None
|
||||
return self
|
||||
|
||||
def build(self) -> "Theme":
|
||||
"""
|
||||
Constructs and returns a Theme instance using the configured properties.
|
||||
|
||||
This method resolves all necessary dependencies for the theme's preparation
|
||||
and installation. The returned Theme will have the mode and filled options set
|
||||
according to the builder's configuration.
|
||||
|
||||
:return: Theme instance ready for preparation and installation
|
||||
"""
|
||||
self._resolve_preparation()
|
||||
self._resolve_installer()
|
||||
return Theme(self.preparation, self.installer, self.mode, self.is_filled)
|
||||
|
||||
def _resolve_preparation(self):
|
||||
if self.preparation: return
|
||||
|
||||
file_manager = ThemeTempManager(self.temp_folder)
|
||||
style_manager = StyleManager(self.main_styles)
|
||||
self.preparation = ThemePreparation(self.source_folder,
|
||||
file_manager=file_manager, style_manager=style_manager)
|
||||
|
||||
def _resolve_installer(self):
|
||||
if self.installer: return
|
||||
|
||||
color_converter = ColorConverterImpl()
|
||||
color_replacement_generator = ColorReplacementGenerator(
|
||||
colors_provider=self.colors_provider,
|
||||
color_converter=color_converter)
|
||||
color_applier = ThemeColorApplier(color_replacement_generator=color_replacement_generator)
|
||||
logger_factory = self.logger_factory or Console()
|
||||
path_provider = ThemePathProvider()
|
||||
self.installer = ThemeInstaller(self.theme_name, self.temp_folder, self.destination_folder,
|
||||
logger_factory=logger_factory,
|
||||
color_applier=color_applier,
|
||||
path_provider=path_provider)
|
||||
93
scripts/utils/theme/theme.py
Normal file
93
scripts/utils/theme/theme.py
Normal file
@@ -0,0 +1,93 @@
|
||||
from scripts.types.installation_color import InstallationColor
|
||||
from scripts.types.theme_base import ThemeBase
|
||||
from scripts.utils.theme.theme_installer import ThemeInstaller
|
||||
from scripts.utils.theme.theme_preparation import ThemePreparation
|
||||
|
||||
|
||||
class Theme(ThemeBase):
|
||||
"""
|
||||
Manages theme preparation and installation.
|
||||
|
||||
The Theme class orchestrates the process of preparing a theme by combining files,
|
||||
applying color schemes, and installing the final theme into a destination folder.
|
||||
"""
|
||||
|
||||
def __init__(self, preparation: ThemePreparation, installer: ThemeInstaller, mode=None, is_filled=False):
|
||||
"""
|
||||
:param preparation: Object responsible for theme extraction and preparation.
|
||||
:param installer: Object responsible for installing the theme.
|
||||
:param mode: Theme mode (e.g., 'light' or 'dark'). If not provided, both modes are used.
|
||||
:param is_filled: if True, theme will be filled
|
||||
"""
|
||||
self.modes = [mode] if mode else ['light', 'dark']
|
||||
self.is_filled = is_filled
|
||||
|
||||
self._preparation = preparation
|
||||
self._installer = installer
|
||||
|
||||
@property
|
||||
def temp_folder(self):
|
||||
"""The temporary folder path where the theme is prepared."""
|
||||
return self._preparation.temp_folder
|
||||
|
||||
@property
|
||||
def destination_folder(self):
|
||||
"""The destination folder path where the theme will be installed."""
|
||||
return self._installer.destination_folder
|
||||
|
||||
@property
|
||||
def main_styles(self):
|
||||
"""The path to the combined styles file generated during preparation."""
|
||||
return self._preparation.combined_styles_location
|
||||
|
||||
@property
|
||||
def theme_name(self):
|
||||
return self._installer.theme_type
|
||||
|
||||
def __add__(self, other: str) -> "Theme":
|
||||
"""
|
||||
Appends additional styles to the main styles file.
|
||||
:param other: The additional styles to append.
|
||||
"""
|
||||
self._preparation += other
|
||||
return self
|
||||
|
||||
def __mul__(self, other: str) -> "Theme":
|
||||
"""
|
||||
Adds a file to the theme, copying it to the temporary folder.
|
||||
:param other: The path of the file or folder to add.
|
||||
"""
|
||||
self._preparation *= other
|
||||
return self
|
||||
|
||||
def add_to_start(self, content) -> "Theme":
|
||||
"""
|
||||
Inserts content at the beginning of the main styles file.
|
||||
:param content: The content to insert.
|
||||
"""
|
||||
self._preparation.add_to_start(content)
|
||||
return self
|
||||
|
||||
def prepare(self):
|
||||
"""Extract theme from source folder and prepare it for installation."""
|
||||
self._preparation.prepare()
|
||||
if self.is_filled:
|
||||
self._preparation.replace_filled_keywords()
|
||||
|
||||
def install(self, hue, name: str, sat: float | None = None, destination: str | None = None):
|
||||
"""
|
||||
Installs the theme by applying the specified accent color and copying the finalized files
|
||||
to the designated destination.
|
||||
|
||||
Args:
|
||||
hue: The hue value for the accent color (0-360 degrees).
|
||||
name: The name of the theme.
|
||||
sat: The saturation value for the accent color.
|
||||
destination: The custom folder where the theme will be installed.
|
||||
"""
|
||||
theme_color = InstallationColor(
|
||||
hue=hue,
|
||||
saturation=sat,
|
||||
modes=self.modes
|
||||
)
|
||||
self._installer.install(theme_color, name, destination)
|
||||
20
scripts/utils/theme/theme_color_applier.py
Normal file
20
scripts/utils/theme/theme_color_applier.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import os
|
||||
|
||||
from scripts.types.installation_color import InstallationColor, InstallationMode
|
||||
from scripts.utils import replace_keywords
|
||||
from scripts.utils.theme.color_replacement_generator import ColorReplacementGenerator
|
||||
|
||||
|
||||
class ThemeColorApplier:
|
||||
"""Class to apply theme colors to files in a directory."""
|
||||
|
||||
def __init__(self, color_replacement_generator: ColorReplacementGenerator):
|
||||
self.color_replacement_generator = color_replacement_generator
|
||||
|
||||
def apply(self, theme_color: InstallationColor, destination: str, mode: InstallationMode):
|
||||
"""Apply theme colors to all files in the directory"""
|
||||
replacements = self.color_replacement_generator.convert(mode, theme_color)
|
||||
|
||||
for filename in os.listdir(destination):
|
||||
file_path = os.path.join(destination, filename)
|
||||
replace_keywords(file_path, *replacements)
|
||||
75
scripts/utils/theme/theme_installer.py
Normal file
75
scripts/utils/theme/theme_installer.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from scripts.types.installation_color import InstallationColor, InstallationMode
|
||||
from scripts.utils import copy_files
|
||||
from scripts.utils.logger.console import Console, Color, Format
|
||||
from scripts.utils.logger.logger import LoggerFactory
|
||||
from scripts.utils.theme.theme_color_applier import ThemeColorApplier
|
||||
from scripts.utils.theme.theme_path_provider import ThemePathProvider
|
||||
|
||||
|
||||
class ThemeInstaller:
|
||||
"""
|
||||
Handles the installation of themes by copying files and applying color schemes.
|
||||
"""
|
||||
|
||||
def __init__(self, theme_type: str, source_folder: str, destination_folder: str,
|
||||
logger_factory: LoggerFactory, color_applier: ThemeColorApplier, path_provider: ThemePathProvider):
|
||||
"""
|
||||
:param theme_type: type of the theme (e.g., gnome-shell, gtk)
|
||||
:param source_folder: folder containing the theme files (e.g. temp folder)
|
||||
:param destination_folder: folder where the theme will be installed
|
||||
"""
|
||||
self.theme_type = theme_type
|
||||
self.source_folder = source_folder
|
||||
self.destination_folder = destination_folder
|
||||
|
||||
self.logger_factory = logger_factory
|
||||
self.color_applier = color_applier
|
||||
self.path_provider = path_provider
|
||||
|
||||
def install(self, theme_color: InstallationColor, name: str, custom_destination: str = None):
|
||||
"""
|
||||
Install theme and generate theme with specified accent color
|
||||
:param theme_color: object containing color and modes
|
||||
:param name: theme name
|
||||
:param custom_destination: optional custom destination folder
|
||||
"""
|
||||
logger = InstallationLogger(name, theme_color.modes, self.logger_factory)
|
||||
|
||||
try:
|
||||
self._perform_installation(theme_color, name, custom_destination=custom_destination)
|
||||
logger.success()
|
||||
except Exception as err:
|
||||
logger.error(str(err))
|
||||
raise
|
||||
|
||||
def _perform_installation(self, theme_color, name, custom_destination=None):
|
||||
for mode in theme_color.modes:
|
||||
destination = (custom_destination or
|
||||
self.path_provider.get_theme_path(
|
||||
self.destination_folder, name, mode, self.theme_type))
|
||||
|
||||
copy_files(self.source_folder, destination)
|
||||
self.color_applier.apply(theme_color, destination, mode)
|
||||
|
||||
|
||||
class InstallationLogger:
|
||||
def __init__(self, name: str, modes: list[InstallationMode], logger_factory: LoggerFactory):
|
||||
self.name = name
|
||||
self.modes = modes
|
||||
|
||||
self.logger = logger_factory.create_logger(self.name)
|
||||
self._setup_logger()
|
||||
|
||||
def _setup_logger(self):
|
||||
self.formatted_name = Console.format(self.name.capitalize(),
|
||||
color=Color.get(self.name),
|
||||
format_type=Format.BOLD)
|
||||
joint_modes = f"({', '.join(self.modes)})"
|
||||
self.formatted_modes = Console.format(joint_modes, color=Color.GRAY)
|
||||
self.logger.update(f"Creating {self.formatted_name} {self.formatted_modes} theme...")
|
||||
|
||||
def success(self):
|
||||
self.logger.success(f"{self.formatted_name} {self.formatted_modes} theme created successfully.")
|
||||
|
||||
def error(self, error_message: str):
|
||||
self.logger.error(f"Error installing {self.formatted_name} theme: {error_message}")
|
||||
20
scripts/utils/theme/theme_path_provider.py
Normal file
20
scripts/utils/theme/theme_path_provider.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import os
|
||||
|
||||
|
||||
class ThemePathProvider:
|
||||
@staticmethod
|
||||
def get_theme_path(themes_folder: str, color_name: str, theme_mode: str, theme_type: str) -> str:
|
||||
"""
|
||||
Generates the path for the theme based on the provided parameters.
|
||||
:param themes_folder: The base folder where themes are stored.
|
||||
:param color_name: The name of the color scheme.
|
||||
:param theme_mode: The mode of the theme (e.g., 'light' or 'dark').
|
||||
:param theme_type: The type of the theme (e.g., 'gnome-shell', 'gtk').
|
||||
"""
|
||||
if not themes_folder or not color_name or not theme_mode or not theme_type:
|
||||
raise ValueError("All parameters must be non-empty strings.")
|
||||
|
||||
marble_name = '-'.join(["Marble", color_name, theme_mode])
|
||||
final_path = os.path.join(themes_folder, marble_name, theme_type, "")
|
||||
|
||||
return final_path
|
||||
63
scripts/utils/theme/theme_preparation.py
Normal file
63
scripts/utils/theme/theme_preparation.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import os
|
||||
|
||||
from scripts.utils import replace_keywords
|
||||
from scripts.utils.theme.theme_temp_manager import ThemeTempManager
|
||||
from scripts.utils.style_manager import StyleManager
|
||||
|
||||
|
||||
class ThemePreparation:
|
||||
"""
|
||||
Class for extracting themes from the source folder
|
||||
and preparing them for installation.
|
||||
"""
|
||||
|
||||
def __init__(self, sources_location: str, file_manager: ThemeTempManager, style_manager: StyleManager):
|
||||
self.sources_location = sources_location
|
||||
|
||||
self.file_manager = file_manager
|
||||
self.style_manager = style_manager
|
||||
|
||||
@property
|
||||
def temp_folder(self):
|
||||
return self.file_manager.temp_folder
|
||||
|
||||
@property
|
||||
def combined_styles_location(self):
|
||||
return self.style_manager.output_file
|
||||
|
||||
def __add__(self, content: str) -> "ThemePreparation":
|
||||
"""Append additional styles to the main styles file."""
|
||||
self.style_manager.append_content(content)
|
||||
return self
|
||||
|
||||
def __mul__(self, content: str) -> "ThemePreparation":
|
||||
"""Adds a file to the theme, copying it to the temporary folder."""
|
||||
self.file_manager.copy_to_temp(content)
|
||||
return self
|
||||
|
||||
def add_to_start(self, content) -> "ThemePreparation":
|
||||
"""Inserts content at the beginning of the main styles file."""
|
||||
self.style_manager.prepend_content(content)
|
||||
return self
|
||||
|
||||
def prepare(self):
|
||||
"""
|
||||
Extract theme from source folder and prepare it for installation.
|
||||
"""
|
||||
self.file_manager.copy_to_temp(self.sources_location)
|
||||
self.style_manager.generate_combined_styles(self.sources_location, self.temp_folder)
|
||||
self.file_manager.cleanup()
|
||||
|
||||
def replace_filled_keywords(self):
|
||||
"""
|
||||
Replace keywords in the theme files for filled mode.
|
||||
This method is deprecated and will be removed in future versions.
|
||||
"""
|
||||
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"))
|
||||
29
scripts/utils/theme/theme_temp_manager.py
Normal file
29
scripts/utils/theme/theme_temp_manager.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import os
|
||||
import shutil
|
||||
|
||||
|
||||
class ThemeTempManager:
|
||||
"""
|
||||
Manages operations with temp folder for Theme class
|
||||
"""
|
||||
def __init__(self, temp_folder: str):
|
||||
self.temp_folder = temp_folder
|
||||
os.makedirs(self.temp_folder, exist_ok=True)
|
||||
|
||||
def copy_to_temp(self, content: str):
|
||||
"""
|
||||
Copy a file or directory to the temporary folder.
|
||||
If the content is a file, it will be copied directly.
|
||||
If the content is a directory, all its contents will be copied to the temp folder.
|
||||
"""
|
||||
if os.path.isfile(content):
|
||||
final_path = os.path.join(self.temp_folder, os.path.basename(content))
|
||||
shutil.copy(content, final_path)
|
||||
else:
|
||||
shutil.copytree(content, self.temp_folder, dirs_exist_ok=True)
|
||||
return self
|
||||
|
||||
def cleanup(self):
|
||||
"""Remove temporary folders"""
|
||||
shutil.rmtree(f"{self.temp_folder}/.css/", ignore_errors=True)
|
||||
shutil.rmtree(f"{self.temp_folder}/.versions/", ignore_errors=True)
|
||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
14
tests/_helpers/__init__.py
Normal file
14
tests/_helpers/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import os
|
||||
|
||||
|
||||
def create_dummy_file(file_path: str, content: str = "dummy content"):
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
with open(file_path, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
|
||||
def try_remove_file(file_path: str):
|
||||
try:
|
||||
os.remove(file_path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
25
tests/_helpers/dummy_logger_factory.py
Normal file
25
tests/_helpers/dummy_logger_factory.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from typing import Optional
|
||||
|
||||
from scripts.utils.logger.logger import LoggerFactory, Logger
|
||||
|
||||
|
||||
class DummyLoggerFactory(LoggerFactory):
|
||||
def create_logger(self, name: Optional[str] = None) -> 'DummyLogger':
|
||||
return DummyLogger()
|
||||
|
||||
|
||||
class DummyLogger(Logger):
|
||||
def update(self, msg):
|
||||
pass
|
||||
|
||||
def success(self, msg):
|
||||
pass
|
||||
|
||||
def error(self, msg):
|
||||
pass
|
||||
|
||||
def warn(self, msg):
|
||||
pass
|
||||
|
||||
def info(self, msg):
|
||||
pass
|
||||
6
tests/_helpers/dummy_runner.py
Normal file
6
tests/_helpers/dummy_runner.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from scripts.utils.command_runner.command_runner import CommandRunner
|
||||
|
||||
|
||||
class DummyRunner(CommandRunner):
|
||||
def run(self, command: list[str], **kwargs) -> str:
|
||||
return "Dummy output"
|
||||
0
tests/utils/__init__.py
Normal file
0
tests/utils/__init__.py
Normal file
0
tests/utils/color_converter/__init__.py
Normal file
0
tests/utils/color_converter/__init__.py
Normal file
64
tests/utils/color_converter/test_color_converter_impl.py
Normal file
64
tests/utils/color_converter/test_color_converter_impl.py
Normal file
@@ -0,0 +1,64 @@
|
||||
import unittest
|
||||
|
||||
from scripts.utils.color_converter.color_converter_impl import ColorConverterImpl
|
||||
|
||||
|
||||
class ColorConverterImplTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.converter = ColorConverterImpl()
|
||||
|
||||
def test_hex_to_rgba_is_valid(self):
|
||||
hex_color = "#ff5733"
|
||||
expected_rgba = (255, 87, 51, 1.0)
|
||||
|
||||
result = self.converter.hex_to_rgba(hex_color)
|
||||
|
||||
self.assertEqual(result, expected_rgba)
|
||||
|
||||
def test_hex_to_rgba_is_invalid(self):
|
||||
hex_color = "#invalid"
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
self.converter.hex_to_rgba(hex_color)
|
||||
|
||||
def test_hex_to_rgba_with_alpha_is_valid(self):
|
||||
hex_color = "#ff5733ff"
|
||||
expected_rgba = (255, 87, 51, 1.0)
|
||||
|
||||
result = self.converter.hex_to_rgba(hex_color)
|
||||
|
||||
self.assertEqual(result, expected_rgba)
|
||||
|
||||
def test_hex_to_rgba_with_shorthand_is_valid(self):
|
||||
hex_color = "#fff"
|
||||
expected_rgba = (255, 255, 255, 1.0)
|
||||
|
||||
result = self.converter.hex_to_rgba(hex_color)
|
||||
|
||||
self.assertEqual(result, expected_rgba)
|
||||
|
||||
def test_hsl_to_rgb_is_valid(self):
|
||||
hue = 360
|
||||
saturation = 1
|
||||
lightness = 0.5
|
||||
expected_rgb = (255, 0, 0)
|
||||
|
||||
result = self.converter.hsl_to_rgb(hue, saturation, lightness)
|
||||
|
||||
self.assertEqual(result, expected_rgb)
|
||||
|
||||
def test_hsl_to_rgb_with_overflow_hue_is_invalid(self):
|
||||
hue = 400
|
||||
saturation = 1
|
||||
lightness = 0.5
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
self.converter.hsl_to_rgb(hue, saturation, lightness)
|
||||
|
||||
def test_hsl_to_rgb_with_invalid_saturation_and_lightness_is_invalid(self):
|
||||
hue = 360
|
||||
saturation = 1.5
|
||||
lightness = -2
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
self.converter.hsl_to_rgb(hue, saturation, lightness)
|
||||
0
tests/utils/global_theme/__init__.py
Normal file
0
tests/utils/global_theme/__init__.py
Normal file
81
tests/utils/global_theme/test_gdm.py
Normal file
81
tests/utils/global_theme/test_gdm.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from unittest import TestCase
|
||||
from unittest.mock import Mock
|
||||
|
||||
from scripts.utils.global_theme.gdm import GDMTheme
|
||||
from scripts.utils.global_theme.gdm_installer import GDMThemeInstaller
|
||||
from scripts.utils.global_theme.gdm_preparer import GDMThemePreparer
|
||||
from scripts.utils.global_theme.gdm_remover import GDMThemeRemover
|
||||
|
||||
|
||||
class GDMTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.preparer = Mock(spec=GDMThemePreparer)
|
||||
self.installer = Mock(spec=GDMThemeInstaller)
|
||||
self.remover = Mock(spec=GDMThemeRemover)
|
||||
|
||||
self.gdm = GDMTheme(self.preparer, self.installer, self.remover)
|
||||
|
||||
def test_prepare_uses_backup_if_installed(self):
|
||||
self.installer.is_installed.return_value = True
|
||||
|
||||
self.gdm.prepare()
|
||||
|
||||
self.preparer.use_backup_as_source.assert_called_once()
|
||||
|
||||
def test_prepare_does_not_use_backup_if_not_installed(self):
|
||||
self.installer.is_installed.return_value = False
|
||||
|
||||
self.gdm.prepare()
|
||||
|
||||
self.preparer.use_backup_as_source.assert_not_called()
|
||||
|
||||
def test_prepare_calls_preparer_prepare_and_sets_themes(self):
|
||||
mock_theme = Mock()
|
||||
self.preparer.prepare.return_value = [mock_theme]
|
||||
|
||||
self.gdm.prepare()
|
||||
|
||||
self.preparer.prepare.assert_called_once()
|
||||
self.assertEqual(self.gdm.themes, [mock_theme])
|
||||
|
||||
def test_install_correctly_passes_arguments_to_installer_compile(self):
|
||||
hue = 100
|
||||
name = "test_theme"
|
||||
sat = 0.5
|
||||
|
||||
self.gdm.install(hue, name, sat)
|
||||
|
||||
self.installer.compile.assert_called_once_with(self.gdm.themes, hue, name, sat)
|
||||
|
||||
def test_install_calls_installer_backup_if_not_installed(self):
|
||||
self.installer.is_installed.return_value = False
|
||||
|
||||
self.gdm.install(100, "test_theme")
|
||||
|
||||
self.installer.backup.assert_called_once()
|
||||
|
||||
def test_install_does_not_call_installer_backup_if_installed(self):
|
||||
self.installer.is_installed.return_value = True
|
||||
|
||||
self.gdm.install(100, "test_theme")
|
||||
|
||||
self.installer.backup.assert_not_called()
|
||||
|
||||
def test_install_calls_installer_install(self):
|
||||
self.gdm.install(100, "test_theme")
|
||||
|
||||
self.installer.install.assert_called_once()
|
||||
|
||||
def test_remove_calls_installer_remove_if_installed(self):
|
||||
self.installer.is_installed.return_value = True
|
||||
|
||||
self.gdm.remove()
|
||||
|
||||
self.remover.remove.assert_called_once()
|
||||
|
||||
def test_remove_calls_installer_warn_if_not_installed(self):
|
||||
self.installer.is_installed.return_value = False
|
||||
|
||||
self.gdm.remove()
|
||||
|
||||
self.remover.remove.assert_not_called()
|
||||
155
tests/utils/global_theme/test_gdm_builder.py
Normal file
155
tests/utils/global_theme/test_gdm_builder.py
Normal file
@@ -0,0 +1,155 @@
|
||||
from unittest import TestCase
|
||||
from unittest.mock import Mock
|
||||
|
||||
from scripts.types.installation_color import InstallationMode
|
||||
from scripts.utils.global_theme.gdm_builder import GDMThemeBuilder
|
||||
|
||||
|
||||
class GDMBuilderTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.colors_provider = Mock()
|
||||
self.builder = GDMThemeBuilder(colors_provider=self.colors_provider)
|
||||
|
||||
def test_with_mode_sets_correct_mode(self):
|
||||
self.builder._mode = None
|
||||
mode: InstallationMode = "dark"
|
||||
|
||||
builder = self.builder.with_mode(mode)
|
||||
|
||||
self.assertEqual(builder._mode, mode)
|
||||
|
||||
def test_with_filled_sets_correct_filled_state(self):
|
||||
self.builder._is_filled = False
|
||||
is_filled = True
|
||||
|
||||
builder = self.builder.with_filled(is_filled)
|
||||
|
||||
self.assertEqual(builder._is_filled, is_filled)
|
||||
|
||||
def test_with_logger_factory_sets_specified_logger_factory(self):
|
||||
logger_factory = Mock()
|
||||
builder = self.builder.with_logger_factory(logger_factory)
|
||||
self.assertEqual(builder._logger_factory, logger_factory)
|
||||
|
||||
def test_with_gresource_sets_specified_gresource(self):
|
||||
gresource = Mock()
|
||||
builder = self.builder.with_gresource(gresource)
|
||||
self.assertEqual(builder._gresource, gresource)
|
||||
|
||||
def test_with_ubuntu_gdm_alternatives_updater_sets_specified_updater(self):
|
||||
alternatives_updater = Mock()
|
||||
builder = self.builder.with_ubuntu_gdm_alternatives_updater(alternatives_updater)
|
||||
self.assertEqual(builder._ubuntu_gdm_alternatives_updater, alternatives_updater)
|
||||
|
||||
def test_with_preparer_sets_specified_preparer(self):
|
||||
preparer = Mock()
|
||||
builder = self.builder.with_preparer(preparer)
|
||||
self.assertEqual(builder._preparer, preparer)
|
||||
|
||||
def test_with_installer_sets_specified_installer(self):
|
||||
installer = Mock()
|
||||
builder = self.builder.with_installer(installer)
|
||||
self.assertEqual(builder._installer, installer)
|
||||
|
||||
def test_with_remover_sets_specified_remover(self):
|
||||
remover = Mock()
|
||||
builder = self.builder.with_remover(remover)
|
||||
self.assertEqual(builder._remover, remover)
|
||||
|
||||
def test_resolve_logger_factory_initializes_logger_factory(self):
|
||||
self.builder._logger_factory = None
|
||||
|
||||
self.builder._resolve_logger_factory()
|
||||
|
||||
self.assertIsNotNone(self.builder._logger_factory)
|
||||
|
||||
def test_resolve_gresource_initializes_gresource(self):
|
||||
self.builder._logger_factory = Mock()
|
||||
self.builder._gresource = None
|
||||
|
||||
self.builder._resolve_gresource()
|
||||
|
||||
self.assertIsNotNone(self.builder._gresource)
|
||||
|
||||
def test_builder_supports_chaining(self):
|
||||
theme = self.builder.with_mode("dark").with_filled(True).build()
|
||||
|
||||
self.assertIsNotNone(theme)
|
||||
|
||||
def test_resolve_ubuntu_gdm_alternatives_updater_initializes_gresource(self):
|
||||
self.builder._logger_factory = Mock()
|
||||
self.builder._gresource = Mock()
|
||||
self.builder._ubuntu_gdm_alternatives_updater = None
|
||||
self.builder._resolve_ubuntu_gdm_alternatives_updater()
|
||||
self.assertIsNotNone(self.builder._ubuntu_gdm_alternatives_updater)
|
||||
|
||||
def test_resolve_preparer_initializes_preparer(self):
|
||||
self.builder._logger_factory = Mock()
|
||||
self.builder._gresource = Mock()
|
||||
self.builder._ubuntu_gdm_alternatives_updater = Mock()
|
||||
self.builder._preparer = None
|
||||
|
||||
self.builder._resolve_preparer()
|
||||
|
||||
self.assertIsNotNone(self.builder._preparer)
|
||||
|
||||
def test_resolve_installer_initializes_installer(self):
|
||||
self.builder._gresource = Mock()
|
||||
self.builder._ubuntu_gdm_alternatives_updater = Mock()
|
||||
self.builder._installer = None
|
||||
|
||||
self.builder._resolve_installer()
|
||||
|
||||
self.assertIsNotNone(self.builder._installer)
|
||||
|
||||
def test_resolve_remover_initializes_remover(self):
|
||||
self.builder._gresource = Mock()
|
||||
self.builder._ubuntu_gdm_alternatives_updater = Mock()
|
||||
self.builder._remover = None
|
||||
|
||||
self.builder._resolve_remover()
|
||||
|
||||
self.assertIsNotNone(self.builder._remover)
|
||||
|
||||
def test_build_resolves_dependencies(self):
|
||||
self.builder._resolve_logger_factory = Mock()
|
||||
self.builder._resolve_gresource = Mock()
|
||||
self.builder._resolve_ubuntu_gdm_alternatives_updater = Mock()
|
||||
self.builder._resolve_preparer = Mock()
|
||||
self.builder._resolve_installer = Mock()
|
||||
self.builder._resolve_remover = Mock()
|
||||
|
||||
self.builder.build()
|
||||
|
||||
self.builder._resolve_logger_factory.assert_called_once()
|
||||
self.builder._resolve_gresource.assert_called_once()
|
||||
self.builder._resolve_ubuntu_gdm_alternatives_updater.assert_called_once()
|
||||
self.builder._resolve_preparer.assert_called_once()
|
||||
self.builder._resolve_installer.assert_called_once()
|
||||
self.builder._resolve_remover.assert_called_once()
|
||||
|
||||
def test_build_correctly_builds_gdm_theme(self):
|
||||
self.builder._preparer = Mock()
|
||||
self.builder._installer = Mock()
|
||||
self.builder._remover = Mock()
|
||||
|
||||
result = self.builder.build()
|
||||
|
||||
self.assertEqual(result.preparer, self.builder._preparer)
|
||||
self.assertEqual(result.installer, self.builder._installer)
|
||||
self.assertEqual(result.remover, self.builder._remover)
|
||||
|
||||
def test_build_with_explicit_dependencies_works_correctly(self):
|
||||
preparer = Mock()
|
||||
installer = Mock()
|
||||
remover = Mock()
|
||||
builder = (self.builder
|
||||
.with_preparer(preparer)
|
||||
.with_installer(installer)
|
||||
.with_remover(remover))
|
||||
|
||||
result = builder.build()
|
||||
|
||||
self.assertEqual(result.preparer, preparer)
|
||||
self.assertEqual(result.installer, installer)
|
||||
self.assertEqual(result.remover, remover)
|
||||
87
tests/utils/global_theme/test_gdm_installer.py
Normal file
87
tests/utils/global_theme/test_gdm_installer.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import os.path
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from scripts import config
|
||||
from scripts.utils.global_theme.gdm_installer import GDMThemeInstaller
|
||||
|
||||
|
||||
class GDMInstallerTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.temp_folder = os.path.join(config.temp_tests_folder, "gdm_installer")
|
||||
self.gresource = MagicMock()
|
||||
self.gresource.temp_folder = self.temp_folder
|
||||
|
||||
self.alternatives_updater = MagicMock()
|
||||
|
||||
self.gdm_installer = GDMThemeInstaller(
|
||||
gresource=self.gresource,
|
||||
alternatives_updater=self.alternatives_updater
|
||||
)
|
||||
|
||||
def test_is_installed_return_the_same_value_as_gresource(self):
|
||||
self.gresource.has_trigger.return_value = True
|
||||
|
||||
result = self.gdm_installer.is_installed()
|
||||
|
||||
self.assertTrue(result)
|
||||
self.gresource.has_trigger.assert_called_once()
|
||||
|
||||
def test_compile_does_not_call_label_theme_if_label_is_none(self):
|
||||
theme_prepare = MagicMock()
|
||||
theme_prepare.label = None
|
||||
theme_prepare.label_theme = MagicMock()
|
||||
|
||||
self.gdm_installer.compile(themes=[theme_prepare], hue=0, color="red", sat=None)
|
||||
|
||||
theme_prepare.label_theme.assert_not_called()
|
||||
|
||||
def test_compile_calls_label_theme_if_label_is_set(self):
|
||||
theme_prepare = MagicMock()
|
||||
theme_prepare.label = "dark"
|
||||
theme_prepare.label_theme = MagicMock()
|
||||
|
||||
self.gdm_installer.compile(themes=[theme_prepare], hue=0, color="red", sat=None)
|
||||
|
||||
theme_prepare.label_theme.assert_called_once()
|
||||
|
||||
def test_compile_calls_removes_keywords_and_properties_and_prepends_source_styles(self):
|
||||
theme_prepare = MagicMock()
|
||||
theme_prepare.remove_keywords = MagicMock()
|
||||
theme_prepare.remove_properties = MagicMock()
|
||||
theme_prepare.prepend_source_styles = MagicMock()
|
||||
|
||||
self.gdm_installer.compile(themes=[theme_prepare], hue=0, color="red", sat=None)
|
||||
|
||||
theme_prepare.remove_keywords.assert_called_once()
|
||||
theme_prepare.remove_properties.assert_called_once()
|
||||
theme_prepare.prepend_source_styles.assert_called_once()
|
||||
|
||||
def test_compile_installs_themes_with_correct_parameters(self):
|
||||
theme_prepare = MagicMock()
|
||||
theme_prepare.install = MagicMock()
|
||||
themes = [theme_prepare]
|
||||
hue = 0
|
||||
color = "red"
|
||||
sat = None
|
||||
|
||||
self.gdm_installer.compile(themes, hue, color, sat)
|
||||
|
||||
theme_prepare.install.assert_called_once()
|
||||
theme_prepare.install.assert_called_with(hue, color, sat, destination=self.temp_folder)
|
||||
|
||||
def test_compile_calls_gresource_compile(self):
|
||||
self.gdm_installer.compile([], 0, "red", None)
|
||||
|
||||
self.gresource.compile.assert_called_once()
|
||||
|
||||
def test_backup_calls_gresource_backup(self):
|
||||
self.gdm_installer.backup()
|
||||
|
||||
self.gresource.backup.assert_called_once()
|
||||
|
||||
def test_install_calls_gresource_move_and_alternatives_updater_install_and_set(self):
|
||||
self.gdm_installer.install()
|
||||
|
||||
self.gresource.move.assert_called_once()
|
||||
self.alternatives_updater.install_and_set.assert_called_once()
|
||||
124
tests/utils/global_theme/test_gdm_preparer.py
Normal file
124
tests/utils/global_theme/test_gdm_preparer.py
Normal file
@@ -0,0 +1,124 @@
|
||||
import os
|
||||
import shutil
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from scripts import config
|
||||
from scripts.types.theme_base import ThemeBase
|
||||
from scripts.utils.global_theme.gdm_preparer import GDMThemePreparer
|
||||
|
||||
|
||||
class DummyTheme(ThemeBase):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.temp_folder = None
|
||||
self.main_styles = None
|
||||
|
||||
def prepare(self):
|
||||
pass
|
||||
|
||||
def install(self, hue: int, name: str, sat: float | None = None):
|
||||
pass
|
||||
|
||||
|
||||
class TestGDMThemePreparer(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp_folder = os.path.join(config.temp_tests_folder, "gdm_preparer")
|
||||
|
||||
self.gresource = self._mock_gresource(self.temp_folder)
|
||||
self.theme_builder = self._mock_builder()
|
||||
|
||||
self.mock_logger = MagicMock()
|
||||
self.logger_factory = MagicMock()
|
||||
self.logger_factory.create_logger.return_value = self.mock_logger
|
||||
|
||||
self.preparer = GDMThemePreparer(
|
||||
temp_folder=self.temp_folder,
|
||||
default_mode="light",
|
||||
is_filled=True,
|
||||
gresource=self.gresource,
|
||||
theme_builder=self.theme_builder,
|
||||
logger_factory=self.logger_factory,
|
||||
files_labeler_factory=MagicMock(),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _mock_gresource(temp_folder):
|
||||
gresource = MagicMock()
|
||||
gresource.temp_folder = temp_folder
|
||||
gresource.extract = MagicMock()
|
||||
gresource.use_backup_gresource = MagicMock()
|
||||
return gresource
|
||||
|
||||
@staticmethod
|
||||
def _mock_builder():
|
||||
theme_builder = MagicMock()
|
||||
theme_builder.with_temp_folder.return_value = theme_builder
|
||||
theme_builder.with_theme_name.return_value = theme_builder
|
||||
theme_builder.with_mode.return_value = theme_builder
|
||||
theme_builder.filled.return_value = theme_builder
|
||||
theme_builder.with_logger_factory.return_value = theme_builder
|
||||
theme_builder.with_reset_dependencies.return_value = theme_builder
|
||||
theme_builder.build.return_value = DummyTheme()
|
||||
return theme_builder
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.temp_folder, ignore_errors=True)
|
||||
|
||||
def test_use_backup_as_source(self):
|
||||
self.preparer.use_backup_as_source()
|
||||
|
||||
self.gresource.use_backup_gresource.assert_called_once()
|
||||
|
||||
@patch("os.listdir")
|
||||
def test_preparer_extracts_gresource(self, mock_listdir):
|
||||
mock_listdir.return_value = ["gnome-shell.css"]
|
||||
|
||||
self.preparer.prepare()
|
||||
|
||||
self.gresource.extract.assert_called_once()
|
||||
|
||||
@patch("os.listdir")
|
||||
def test_preparer_scans_correct_directory(self, mock_listdir):
|
||||
mock_listdir.return_value = ["gnome-shell.css"]
|
||||
|
||||
self.preparer.prepare()
|
||||
|
||||
mock_listdir.assert_called_once_with(self.gresource.temp_folder)
|
||||
|
||||
@patch("os.listdir")
|
||||
def test_preparer_filters_valid_css_files(self, mock_listdir):
|
||||
valid_files = ["gnome-shell-dark.css", "gnome-shell-light.css", "gnome-shell.css"]
|
||||
invalid_files = ["other.css", "readme.txt"]
|
||||
mock_listdir.return_value = valid_files + invalid_files
|
||||
|
||||
themes = self.preparer.prepare()
|
||||
|
||||
self.assertEqual(len(themes), len(valid_files))
|
||||
|
||||
@patch("os.listdir")
|
||||
def test_preparer_assigns_correct_labels(self, mock_listdir):
|
||||
test_files = ["gnome-shell-dark.css", "gnome-shell-light.css", "gnome-shell.css"]
|
||||
mock_listdir.return_value = test_files
|
||||
|
||||
themes = self.preparer.prepare()
|
||||
|
||||
expected_labels = {
|
||||
"gnome-shell-dark.css": "dark",
|
||||
"gnome-shell-light.css": "light",
|
||||
"gnome-shell.css": "light" # Uses default_mode
|
||||
}
|
||||
|
||||
for theme_obj in themes:
|
||||
file_name = os.path.basename(theme_obj.theme_file)
|
||||
self.assertEqual(theme_obj.label, expected_labels[file_name])
|
||||
|
||||
@patch("os.listdir")
|
||||
def test_preparer_configures_theme_builder_correctly(self, mock_listdir):
|
||||
mock_listdir.return_value = ["gnome-shell-dark.css", "gnome-shell.css"]
|
||||
|
||||
self.preparer.prepare()
|
||||
|
||||
self.theme_builder.with_theme_name.assert_any_call("gnome-shell-dark")
|
||||
self.theme_builder.with_theme_name.assert_any_call("gnome-shell")
|
||||
|
||||
44
tests/utils/global_theme/test_gdm_remover.py
Normal file
44
tests/utils/global_theme/test_gdm_remover.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from scripts.utils.global_theme.gdm_remover import GDMThemeRemover
|
||||
from scripts.utils.gresource import GresourceBackupNotFoundError
|
||||
|
||||
|
||||
class GDMRemoverTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.gresource = MagicMock()
|
||||
self.alternatives_updater = MagicMock()
|
||||
self.logger = MagicMock()
|
||||
self.logger_factory = MagicMock(return_value=self.logger)
|
||||
|
||||
self.remover = GDMThemeRemover(
|
||||
gresource=self.gresource,
|
||||
alternatives_updater=self.alternatives_updater,
|
||||
logger_factory=self.logger_factory
|
||||
)
|
||||
|
||||
self.remover.remover_logger = MagicMock()
|
||||
|
||||
def test_remove_logs_start_message(self):
|
||||
self.remover.remove()
|
||||
|
||||
self.remover.remover_logger.start_removing.assert_called_once()
|
||||
|
||||
def test_remove_calls_gresource_restore_and_alternatives_remove(self):
|
||||
self.remover.remove()
|
||||
|
||||
self.gresource.restore.assert_called_once()
|
||||
self.alternatives_updater.remove.assert_called_once()
|
||||
|
||||
def test_remove_logs_success_message(self):
|
||||
self.remover.remove()
|
||||
|
||||
self.remover.remover_logger.success_removing.assert_called_once()
|
||||
|
||||
def test_remove_logs_error_message_when_backup_not_found(self):
|
||||
self.gresource.restore.side_effect = GresourceBackupNotFoundError()
|
||||
|
||||
self.remover.remove()
|
||||
|
||||
self.remover.remover_logger.error_removing.assert_called_once()
|
||||
118
tests/utils/global_theme/test_gdm_theme_prepare.py
Normal file
118
tests/utils/global_theme/test_gdm_theme_prepare.py
Normal file
@@ -0,0 +1,118 @@
|
||||
import os.path
|
||||
import shutil
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from scripts import config
|
||||
from scripts.utils.global_theme.gdm_theme_prepare import GDMThemePrepare
|
||||
from ..._helpers import create_dummy_file, try_remove_file
|
||||
|
||||
|
||||
class GDMThemePrepareTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.temp_folder = os.path.join(config.temp_tests_folder, "gdm_theme_prepare")
|
||||
self.main_styles = os.path.join(self.temp_folder, "gnome-shell.css")
|
||||
self.theme = MagicMock()
|
||||
self.theme.add_to_start.return_value = None
|
||||
self.theme.temp_folder = self.temp_folder
|
||||
self.theme.main_styles = self.main_styles
|
||||
|
||||
self.main_styles_destination = os.path.join(self.temp_folder, "gnome-shell-result.css")
|
||||
create_dummy_file(self.main_styles_destination, "body { background-color: #000; }")
|
||||
|
||||
self.files_labeler = MagicMock()
|
||||
|
||||
self.theme_prepare = GDMThemePrepare(
|
||||
theme=self.theme,
|
||||
theme_file=self.main_styles_destination,
|
||||
label=None,
|
||||
files_labeler=self.files_labeler,
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.temp_folder, ignore_errors=True)
|
||||
|
||||
def test_label_files_calls_labeler(self):
|
||||
self.theme_prepare.label = "dark"
|
||||
|
||||
self.theme_prepare.label_theme()
|
||||
|
||||
self.files_labeler.append_label.assert_called_once_with("dark")
|
||||
|
||||
def test_label_files_raises_value_error_if_label_none(self):
|
||||
self.theme_prepare.label = None
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
self.theme_prepare.label_theme()
|
||||
|
||||
def test_remove_keywords_removes_destination_keywords(self):
|
||||
try_remove_file(self.main_styles_destination)
|
||||
expected_content = "body { background-color: #000; }"
|
||||
create_dummy_file(self.main_styles_destination, "body {keyword1 background-color: #000 !important; }")
|
||||
keywords = ["keyword1", " !important"]
|
||||
|
||||
self.theme_prepare.remove_keywords(*keywords)
|
||||
|
||||
with open(self.main_styles_destination, 'r') as file:
|
||||
content = file.read()
|
||||
self.assertEqual(content, expected_content)
|
||||
try_remove_file(self.main_styles_destination)
|
||||
|
||||
def test_remove_properties_removes_destination_properties(self):
|
||||
try_remove_file(self.main_styles_destination)
|
||||
expected_content = "body {\n}\n"
|
||||
create_dummy_file(self.main_styles_destination, "body {\nbackground-color: #000;\n}")
|
||||
properties = ["background-color"]
|
||||
|
||||
self.theme_prepare.remove_properties(*properties)
|
||||
|
||||
with open(self.main_styles_destination, 'r') as file:
|
||||
actual_content = file.read()
|
||||
self.assertEqual(expected_content, actual_content)
|
||||
try_remove_file(self.main_styles_destination)
|
||||
|
||||
def test_remove_properties_removes_one_line_properties(self):
|
||||
try_remove_file(self.main_styles_destination)
|
||||
expected_content = ""
|
||||
create_dummy_file(self.main_styles_destination, "body { background-color: #000; }")
|
||||
properties = ["background-color"]
|
||||
|
||||
self.theme_prepare.remove_properties(*properties)
|
||||
|
||||
with open(self.main_styles_destination, 'r') as file:
|
||||
actual_content = file.read()
|
||||
self.assertEqual(expected_content, actual_content)
|
||||
try_remove_file(self.main_styles_destination)
|
||||
|
||||
def test_prepend_source_styles_prepends_destination_styles(self):
|
||||
try_remove_file(self.main_styles_destination)
|
||||
expected_content = "body { background-color: #000; }\n"
|
||||
create_dummy_file(self.main_styles_destination, "body { background-color: #000; }")
|
||||
|
||||
self.theme_prepare.prepend_source_styles("")
|
||||
|
||||
called_content: str = self.theme.add_to_start.call_args[0][0]
|
||||
self.assertTrue(called_content.startswith(expected_content))
|
||||
try_remove_file(self.main_styles_destination)
|
||||
|
||||
def test_prepend_source_styles_adds_trigger(self):
|
||||
try_remove_file(self.main_styles_destination)
|
||||
expected_content = "\ntrigger\n"
|
||||
create_dummy_file(self.main_styles_destination)
|
||||
trigger = "trigger"
|
||||
|
||||
self.theme_prepare.prepend_source_styles(trigger)
|
||||
|
||||
called_content: str = self.theme.add_to_start.call_args[0][0]
|
||||
self.assertTrue(expected_content in called_content)
|
||||
try_remove_file(self.main_styles_destination)
|
||||
|
||||
def test_install_passes_arguments_to_theme(self):
|
||||
hue = 0
|
||||
color = "#000000"
|
||||
sat = 100
|
||||
destination = os.path.join(self.temp_folder, "destination")
|
||||
|
||||
self.theme_prepare.install(hue, color, sat, destination)
|
||||
|
||||
self.theme.install.assert_called_once_with(hue, color, sat, destination=destination)
|
||||
@@ -0,0 +1,47 @@
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from scripts.utils.alternatives_updater import AlternativesUpdater
|
||||
from scripts.utils.global_theme.ubuntu_alternatives_updater import UbuntuGDMAlternativesUpdater
|
||||
|
||||
|
||||
class UbuntuGDMUpdateAlternativesTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.updater = MagicMock(spec=AlternativesUpdater)
|
||||
self.ubuntu_updater = UbuntuGDMAlternativesUpdater(
|
||||
alternatives_updater=self.updater
|
||||
)
|
||||
|
||||
def test_custom_destination_updates_correctly(self):
|
||||
custom_destination_dir = "/custom/path"
|
||||
custom_destination_file = "custom_file.gresource"
|
||||
|
||||
self.ubuntu_updater.with_custom_destination(
|
||||
custom_destination_dir, custom_destination_file
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.ubuntu_updater.destination_dir, custom_destination_dir
|
||||
)
|
||||
self.assertEqual(
|
||||
self.ubuntu_updater.destination_file, custom_destination_file
|
||||
)
|
||||
|
||||
def test_install_and_set_calls_updater_correctly(self):
|
||||
priority = 100
|
||||
self.ubuntu_updater.install_and_set(priority)
|
||||
|
||||
self.updater.install_and_set.assert_called_once_with(
|
||||
link=self.ubuntu_updater.ubuntu_gresource_path,
|
||||
name=self.ubuntu_updater.ubuntu_gresource_link_name,
|
||||
path=self.ubuntu_updater.gnome_gresource_path,
|
||||
priority=priority
|
||||
)
|
||||
|
||||
def test_remove_calls_updater_correctly(self):
|
||||
self.ubuntu_updater.remove()
|
||||
|
||||
self.updater.remove.assert_called_once_with(
|
||||
name=self.ubuntu_updater.ubuntu_gresource_link_name,
|
||||
path=self.ubuntu_updater.gnome_gresource_path
|
||||
)
|
||||
0
tests/utils/gresource/__init__.py
Normal file
0
tests/utils/gresource/__init__.py
Normal file
130
tests/utils/gresource/test_gresource.py
Normal file
130
tests/utils/gresource/test_gresource.py
Normal file
@@ -0,0 +1,130 @@
|
||||
import os.path
|
||||
import shutil
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from scripts import config
|
||||
from scripts.utils.gresource.gresource import Gresource
|
||||
from ..._helpers import create_dummy_file, try_remove_file
|
||||
from ..._helpers.dummy_logger_factory import DummyLoggerFactory
|
||||
from ..._helpers.dummy_runner import DummyRunner
|
||||
|
||||
|
||||
class GresourceTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.gresource_file = "test.gresource"
|
||||
self.temp_folder = os.path.join(config.temp_tests_folder, "gresource_temp")
|
||||
self.destination = os.path.join(config.temp_tests_folder, "gresource_dest")
|
||||
|
||||
self.temp_file = os.path.join(self.temp_folder, self.gresource_file)
|
||||
self.destination_file = os.path.join(self.destination, self.gresource_file)
|
||||
|
||||
self.logger = DummyLoggerFactory()
|
||||
self.runner = DummyRunner()
|
||||
|
||||
self.gresource = Gresource(
|
||||
self.gresource_file, self.temp_folder, self.destination,
|
||||
logger_factory=self.logger, runner=self.runner
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.temp_folder, ignore_errors=True)
|
||||
shutil.rmtree(self.destination, ignore_errors=True)
|
||||
|
||||
def test_use_backup_gresource(self):
|
||||
destination_file = os.path.join(self.destination, self.gresource_file)
|
||||
create_dummy_file(destination_file)
|
||||
self.gresource.backup()
|
||||
|
||||
self.gresource.use_backup_gresource()
|
||||
|
||||
assert self.gresource._active_source_gresource != self.gresource._destination_gresource
|
||||
assert os.path.exists(self.gresource._active_source_gresource)
|
||||
|
||||
try_remove_file(self.gresource._active_source_gresource)
|
||||
try_remove_file(destination_file)
|
||||
|
||||
def test_use_backup_gresource_not_found(self):
|
||||
destination_file = os.path.join(self.destination, self.gresource_file)
|
||||
try_remove_file(destination_file)
|
||||
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
self.gresource.use_backup_gresource()
|
||||
|
||||
def test_extract(self):
|
||||
"""Test that extract creates and calls GresourceExtractor correctly."""
|
||||
with patch('scripts.utils.gresource.gresource.GresourceExtractor') as mock_extractor_class:
|
||||
mock_extractor_instance = mock_extractor_class.return_value
|
||||
|
||||
self.gresource.extract()
|
||||
|
||||
mock_extractor_class.assert_called_once_with(
|
||||
self.gresource._active_source_gresource,
|
||||
self.temp_folder,
|
||||
logger_factory=self.logger,
|
||||
runner=self.runner
|
||||
)
|
||||
mock_extractor_instance.extract.assert_called_once()
|
||||
|
||||
def test_compile(self):
|
||||
"""Test that compile creates and calls GresourceCompiler correctly."""
|
||||
with (patch('scripts.utils.gresource.gresource.GresourceCompiler') as mock_compiler_class):
|
||||
mock_compiler_instance = mock_compiler_class.return_value
|
||||
|
||||
self.gresource.compile()
|
||||
|
||||
mock_compiler_class.assert_called_once_with(
|
||||
self.temp_folder,
|
||||
self.gresource._temp_gresource,
|
||||
logger_factory=self.logger,
|
||||
runner=self.runner
|
||||
)
|
||||
mock_compiler_instance.compile.assert_called_once()
|
||||
|
||||
def test_backup(self):
|
||||
create_dummy_file(self.destination_file)
|
||||
|
||||
self.gresource.backup()
|
||||
backup = self.gresource.use_backup_gresource()
|
||||
|
||||
assert os.path.exists(backup)
|
||||
self.gresource.restore()
|
||||
|
||||
def test_backup_not_found(self):
|
||||
try_remove_file(self.destination_file)
|
||||
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
self.gresource.backup()
|
||||
|
||||
def test_restore(self):
|
||||
destination_file = os.path.join(self.destination, self.gresource_file)
|
||||
create_dummy_file(destination_file, content="dummy content")
|
||||
self.gresource.backup()
|
||||
create_dummy_file(destination_file, content="new content")
|
||||
|
||||
self.gresource.restore()
|
||||
|
||||
assert os.path.exists(destination_file)
|
||||
with open(destination_file) as f:
|
||||
content = f.read()
|
||||
assert content == "dummy content"
|
||||
|
||||
def test_restore_not_found(self):
|
||||
destination_file = os.path.join(self.destination, self.gresource_file)
|
||||
try_remove_file(destination_file)
|
||||
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
self.gresource.restore()
|
||||
|
||||
def test_move(self):
|
||||
create_dummy_file(self.temp_file)
|
||||
|
||||
self.gresource.move()
|
||||
|
||||
assert os.path.exists(self.destination_file)
|
||||
|
||||
def test_move_not_found(self):
|
||||
try_remove_file(self.temp_file)
|
||||
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
self.gresource.move()
|
||||
100
tests/utils/gresource/test_gresource_backuper.py
Normal file
100
tests/utils/gresource/test_gresource_backuper.py
Normal file
@@ -0,0 +1,100 @@
|
||||
import os
|
||||
import shutil
|
||||
import unittest
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts import config
|
||||
from scripts.utils.gresource import GresourceBackupNotFoundError
|
||||
from scripts.utils.gresource.gresource_backuper import GresourceBackuperManager, GresourceBackuper
|
||||
from ..._helpers import create_dummy_file, try_remove_file
|
||||
from ..._helpers.dummy_logger_factory import DummyLoggerFactory
|
||||
|
||||
|
||||
class GresourceBackuperManagerTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.gresource_file = "test.gresource"
|
||||
self.temp_folder = os.path.join(config.temp_tests_folder, "backup_temp")
|
||||
self.destination = os.path.join(config.temp_tests_folder, "backup_dest")
|
||||
self.destination_file = os.path.join(self.temp_folder, self.gresource_file)
|
||||
|
||||
self.logger = DummyLoggerFactory()
|
||||
|
||||
self.backuper_manager = GresourceBackuperManager(self.destination_file,
|
||||
logger_factory=self.logger)
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.temp_folder, ignore_errors=True)
|
||||
shutil.rmtree(self.destination, ignore_errors=True)
|
||||
|
||||
def test_get_backup(self):
|
||||
create_dummy_file(self.destination_file)
|
||||
|
||||
self.backuper_manager.backup()
|
||||
backup = self.backuper_manager.get_backup()
|
||||
|
||||
assert os.path.exists(backup)
|
||||
|
||||
def test_backup_overwrites_existing_backup(self):
|
||||
"""Test that backup properly overwrites an existing backup file."""
|
||||
create_dummy_file(self.destination_file, content="original")
|
||||
create_dummy_file(self.backuper_manager._backup_file, content="old backup")
|
||||
|
||||
self.backuper_manager.backup()
|
||||
|
||||
with open(self.backuper_manager._backup_file, 'r') as f:
|
||||
content = f.read()
|
||||
assert content == "original"
|
||||
|
||||
|
||||
class GresourceBackuperTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp_folder = os.path.join(config.temp_tests_folder, "backup_temp")
|
||||
self.destination_file = os.path.join(self.temp_folder, "test.gresource")
|
||||
self.backup_file = f"{self.destination_file}.backup"
|
||||
|
||||
self.logger = DummyLoggerFactory()
|
||||
|
||||
self.backuper = GresourceBackuper(self.destination_file, self.backup_file,
|
||||
logger_factory=self.logger)
|
||||
|
||||
os.makedirs(self.temp_folder, exist_ok=True)
|
||||
|
||||
def test_get_backup(self):
|
||||
create_dummy_file(self.backup_file)
|
||||
|
||||
backup = self.backuper.get_backup()
|
||||
|
||||
assert os.path.exists(backup)
|
||||
assert backup == self.backup_file
|
||||
|
||||
def test_use_backup_gresource_not_found(self):
|
||||
try_remove_file(self.backup_file)
|
||||
|
||||
with pytest.raises(GresourceBackupNotFoundError):
|
||||
self.backuper.get_backup()
|
||||
|
||||
def test_backup_creates_backup_file(self):
|
||||
"""Test direct backup functionality."""
|
||||
create_dummy_file(self.destination_file)
|
||||
|
||||
self.backuper.backup()
|
||||
|
||||
assert os.path.exists(self.backup_file)
|
||||
|
||||
def test_backup_handles_missing_destination(self):
|
||||
"""Test backup behavior when destination file doesn't exist."""
|
||||
try_remove_file(self.destination_file)
|
||||
|
||||
with pytest.raises(FileNotFoundError):
|
||||
self.backuper.backup()
|
||||
|
||||
def test_restore_implementation(self):
|
||||
"""Test direct restore implementation."""
|
||||
create_dummy_file(self.backup_file)
|
||||
try_remove_file(self.destination_file)
|
||||
|
||||
self.backuper.restore()
|
||||
|
||||
assert os.path.exists(self.destination_file)
|
||||
assert not os.path.exists(self.backup_file)
|
||||
125
tests/utils/gresource/test_gresource_compiler.py
Normal file
125
tests/utils/gresource/test_gresource_compiler.py
Normal file
@@ -0,0 +1,125 @@
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts import config
|
||||
from scripts.utils.gresource import MissingDependencyError
|
||||
from scripts.utils.gresource.gresource_compiler import GresourceCompiler
|
||||
from ..._helpers.dummy_logger_factory import DummyLoggerFactory
|
||||
from ..._helpers.dummy_runner import DummyRunner
|
||||
|
||||
|
||||
class GresourceCompilerTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp_folder = os.path.join(config.temp_tests_folder, "gresource_compiler_temp")
|
||||
self.target_file = os.path.join(self.temp_folder, "test.gresource")
|
||||
|
||||
self.logger = DummyLoggerFactory()
|
||||
self.runner = DummyRunner()
|
||||
|
||||
self.compiler = GresourceCompiler(self.temp_folder, self.target_file,
|
||||
logger_factory=self.logger, runner=self.runner)
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.temp_folder, ignore_errors=True)
|
||||
|
||||
def test_compile_calls_correct_methods(self):
|
||||
"""Test that compile calls the right methods in sequence."""
|
||||
with (
|
||||
patch.object(self.compiler, '_create_gresource_xml') as mock_create_xml,
|
||||
patch.object(self.compiler, '_compile_resources') as mock_compile
|
||||
):
|
||||
self.compiler.compile()
|
||||
|
||||
# Verify methods were called correctly in order
|
||||
mock_create_xml.assert_called_once()
|
||||
mock_compile.assert_called_once()
|
||||
|
||||
def test_create_gresource_xml(self):
|
||||
"""Test that _create_gresource_xml creates the XML file with correct content."""
|
||||
with (
|
||||
patch("builtins.open", create=True) as mock_open,
|
||||
patch.object(self.compiler, '_generate_gresource_xml') as mock_generate
|
||||
):
|
||||
mock_generate.return_value = "<xml>test content</xml>"
|
||||
mock_file = MagicMock()
|
||||
mock_open.return_value.__enter__.return_value = mock_file
|
||||
|
||||
self.compiler._create_gresource_xml()
|
||||
|
||||
mock_open.assert_called_once_with(self.compiler.gresource_xml, 'w')
|
||||
mock_file.write.assert_called_once_with("<xml>test content</xml>")
|
||||
|
||||
def test_generate_gresource_xml(self):
|
||||
"""Test that _generate_gresource_xml creates correct XML structure."""
|
||||
with patch.object(self.compiler, '_get_files_to_include') as mock_get_files:
|
||||
mock_get_files.return_value = ["<file>file1.css</file>", "<file>subdir/file2.css</file>"]
|
||||
|
||||
result = self.compiler._generate_gresource_xml()
|
||||
|
||||
assert "<?xml version=" in result
|
||||
assert "<gresources>" in result
|
||||
assert "<gresource prefix=\"/org/gnome/shell/theme\">" in result
|
||||
assert "<file>file1.css</file>" in result
|
||||
assert "<file>subdir/file2.css</file>" in result
|
||||
|
||||
def test_get_files_to_include(self):
|
||||
"""Test that _get_files_to_include finds and formats files correctly."""
|
||||
self.__create_dummy_files_in_temp()
|
||||
|
||||
result = self.compiler._get_files_to_include()
|
||||
|
||||
assert len(result) == 2
|
||||
assert "<file>file1.css</file>" in result
|
||||
assert "<file>subdir/file2.css</file>" in result
|
||||
|
||||
def __create_dummy_files_in_temp(self):
|
||||
os.makedirs(self.temp_folder, exist_ok=True)
|
||||
test_file1 = os.path.join(self.temp_folder, "file1.css")
|
||||
test_subdir = os.path.join(self.temp_folder, "subdir")
|
||||
os.makedirs(test_subdir, exist_ok=True)
|
||||
test_file2 = os.path.join(test_subdir, "file2.css")
|
||||
|
||||
with open(test_file1, 'w') as f:
|
||||
f.write("test content")
|
||||
with open(test_file2, 'w') as f:
|
||||
f.write("test content")
|
||||
|
||||
def test_compile_resources(self):
|
||||
"""Test that _compile_resources runs the correct subprocess command."""
|
||||
with patch.object(self.runner, "run") as mock_run:
|
||||
self.compiler._compile_resources()
|
||||
|
||||
mock_run.assert_called_once()
|
||||
args = mock_run.call_args[0][0]
|
||||
assert args[0] == "glib-compile-resources"
|
||||
assert args[2] == self.temp_folder
|
||||
assert args[4] == self.compiler.target_file
|
||||
assert args[5] == self.compiler.gresource_xml
|
||||
|
||||
def test_compile_resources_file_not_found(self):
|
||||
"""Test that _compile_resources raises appropriate error when command not found."""
|
||||
with (
|
||||
patch.object(self.runner, "run", side_effect=FileNotFoundError("glib-compile-resources not found")),
|
||||
patch("builtins.print")
|
||||
):
|
||||
with pytest.raises(MissingDependencyError):
|
||||
self.compiler._compile_resources()
|
||||
|
||||
def test_try_compile_resources_called_process_error(self):
|
||||
"""Test handling of subprocess execution failures."""
|
||||
process_error = subprocess.CalledProcessError(1, "glib-compile-resources", output="Failed to compile")
|
||||
with patch.object(self.runner, "run", side_effect=process_error):
|
||||
with pytest.raises(subprocess.CalledProcessError):
|
||||
self.compiler._try_compile_resources()
|
||||
|
||||
def test_compile_resources_other_file_not_found_error(self):
|
||||
"""Test that other FileNotFoundError exceptions are propagated."""
|
||||
with patch.object(self.runner, "run", side_effect=FileNotFoundError("Some other file not found")):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
self.compiler._compile_resources()
|
||||
123
tests/utils/gresource/test_gresource_extractor.py
Normal file
123
tests/utils/gresource/test_gresource_extractor.py
Normal file
@@ -0,0 +1,123 @@
|
||||
import os
|
||||
import shutil
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts import config
|
||||
from scripts.utils.gresource import MissingDependencyError
|
||||
from scripts.utils.gresource.gresource_extractor import GresourceExtractor
|
||||
from ..._helpers.dummy_logger_factory import DummyLoggerFactory
|
||||
from ..._helpers.dummy_runner import DummyRunner
|
||||
|
||||
|
||||
class GresourceExtractorTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.gresource_file = "test.gresource"
|
||||
self.temp_folder = os.path.join(config.temp_tests_folder, "gresource_extractor_temp")
|
||||
self.destination = os.path.join(config.temp_tests_folder, "gresource_extractor_dest")
|
||||
|
||||
self.logger = DummyLoggerFactory()
|
||||
self.runner = DummyRunner()
|
||||
|
||||
self.extractor = GresourceExtractor(
|
||||
self.gresource_file, self.temp_folder,
|
||||
logger_factory=self.logger, runner=self.runner
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.temp_folder, ignore_errors=True)
|
||||
shutil.rmtree(self.destination, ignore_errors=True)
|
||||
|
||||
def test_extract_calls_correct_methods(self):
|
||||
with (
|
||||
patch.object(self.extractor, '_get_resources_list') as mock_get_list,
|
||||
patch.object(self.extractor, '_extract_resources') as mock_extract
|
||||
):
|
||||
resources = ["resource1", "resource2"]
|
||||
mock_get_list.return_value = resources
|
||||
|
||||
self.extractor.extract()
|
||||
|
||||
mock_get_list.assert_called_once()
|
||||
mock_extract.assert_called_once_with(resources)
|
||||
|
||||
def test_get_resources_list(self):
|
||||
"""Test that resources are correctly listed from the gresource file."""
|
||||
test_resources = ["/org/gnome/shell/theme/file1.css", "/org/gnome/shell/theme/file2.css"]
|
||||
|
||||
with patch.object(self.runner, "run") as mock_run:
|
||||
mock_run.return_value = self.__mock_gresources_list(
|
||||
stdout="\n".join(test_resources),
|
||||
stderr=""
|
||||
)
|
||||
|
||||
result = self.extractor._get_resources_list()
|
||||
|
||||
assert result == test_resources
|
||||
mock_run.assert_called_once()
|
||||
assert mock_run.call_args[0][0][1] == "list"
|
||||
|
||||
@staticmethod
|
||||
def __mock_gresources_list(stdout: str, stderr: str):
|
||||
mock_result = MagicMock()
|
||||
mock_result.stdout = stdout
|
||||
mock_result.stderr = stderr
|
||||
return mock_result
|
||||
|
||||
def test_get_resources_list_error(self):
|
||||
"""Test that an exception is raised when gresource fails to list resources."""
|
||||
with patch.object(self.runner, "run",
|
||||
side_effect=Exception("Error: gresource failed")):
|
||||
|
||||
with pytest.raises(Exception):
|
||||
self.extractor._get_resources_list()
|
||||
|
||||
def test_extract_resources(self):
|
||||
"""Test that resources are correctly extracted."""
|
||||
test_resources = [
|
||||
"/org/gnome/shell/theme/file1.css",
|
||||
"/org/gnome/shell/theme/subdir/file2.css"
|
||||
]
|
||||
|
||||
with (
|
||||
patch.object(self.runner, "run") as mock_run,
|
||||
patch("os.makedirs") as mock_makedirs,
|
||||
patch("builtins.open", create=True)
|
||||
):
|
||||
self.extractor._extract_resources(test_resources)
|
||||
|
||||
assert mock_makedirs.call_count == 2
|
||||
assert mock_run.call_count == 2
|
||||
for i, resource in enumerate(test_resources):
|
||||
args_list = mock_run.call_args_list[i][0][0]
|
||||
assert args_list[1] == "extract"
|
||||
assert args_list[3] == resource
|
||||
|
||||
def test_extract_resources_file_not_found(self):
|
||||
with (
|
||||
patch.object(self.runner, "run",
|
||||
side_effect=FileNotFoundError("gresource not found")),
|
||||
patch("builtins.print")
|
||||
):
|
||||
with pytest.raises(MissingDependencyError):
|
||||
self.extractor.extract()
|
||||
|
||||
def test_try_extract_resources(self):
|
||||
resources = ["/org/gnome/shell/theme/file.css"]
|
||||
|
||||
with (
|
||||
patch("os.makedirs"),
|
||||
patch("builtins.open", create=True) as mock_open
|
||||
):
|
||||
mock_file = MagicMock()
|
||||
mock_open.return_value.__enter__.return_value = mock_file
|
||||
|
||||
self.extractor._extract_resources(resources)
|
||||
|
||||
expected_path = os.path.join(self.temp_folder, "file.css")
|
||||
mock_open.assert_called_once_with(expected_path, 'wb')
|
||||
|
||||
def test_empty_resource_list(self):
|
||||
self.extractor._extract_resources([])
|
||||
51
tests/utils/gresource/test_gresource_mover.py
Normal file
51
tests/utils/gresource/test_gresource_mover.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import os.path
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from scripts import config
|
||||
from scripts.utils.gresource.gresource_mover import GresourceMover
|
||||
from ..._helpers import create_dummy_file, try_remove_file
|
||||
from ..._helpers.dummy_logger_factory import DummyLoggerFactory
|
||||
from ..._helpers.dummy_runner import DummyRunner
|
||||
|
||||
|
||||
class GresourceMoverTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.gresource_file = "test.gresource"
|
||||
self.source_file = os.path.join(config.temp_tests_folder, "gresource_mover_source", self.gresource_file)
|
||||
self.destination_file = os.path.join(config.temp_tests_folder, "gresource_mover_destination", self.gresource_file)
|
||||
|
||||
self.logger = DummyLoggerFactory()
|
||||
self.runner = DummyRunner()
|
||||
|
||||
self.mover = GresourceMover(self.source_file, self.destination_file,
|
||||
logger_factory=self.logger)
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
try_remove_file(self.source_file)
|
||||
try_remove_file(self.destination_file)
|
||||
|
||||
def test_move_with_correct_permissions(self):
|
||||
"""Test that move changes permissions correctly."""
|
||||
create_dummy_file(self.source_file)
|
||||
|
||||
self.mover.move()
|
||||
|
||||
assert os.path.exists(self.mover.destination_file)
|
||||
permissions = oct(os.stat(self.mover.destination_file).st_mode)[-3:]
|
||||
assert permissions == "644"
|
||||
|
||||
def test_move_handles_cp_error(self):
|
||||
"""Test that errors during copy are properly handled."""
|
||||
with patch('shutil.copyfile', side_effect=OSError):
|
||||
with self.assertRaises(OSError):
|
||||
self.mover.move()
|
||||
|
||||
def test_move_handles_chmod_error(self):
|
||||
"""Test that errors during chmod are properly handled."""
|
||||
create_dummy_file(self.source_file)
|
||||
|
||||
with patch('os.chmod', side_effect=PermissionError):
|
||||
with self.assertRaises(PermissionError):
|
||||
self.mover.move()
|
||||
57
tests/utils/test_files_labeler.py
Normal file
57
tests/utils/test_files_labeler.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import os.path
|
||||
import shutil
|
||||
from unittest import TestCase
|
||||
|
||||
from scripts import config
|
||||
from scripts.utils.files_labeler import FilesLabelerFactoryImpl
|
||||
from .._helpers import create_dummy_file
|
||||
|
||||
|
||||
class FilesLabelerTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.temp_folder = os.path.join(config.temp_tests_folder, "labeler")
|
||||
|
||||
self.files = ["file1.svg", "file2.png", "file3.svg"]
|
||||
self.styles_file = os.path.join(self.temp_folder, "styles-test.css") # styles files are already labeled
|
||||
self.original_styles_content = f"body {{ background: url('./{self.files[0]}'); }}"
|
||||
|
||||
self.factory = FilesLabelerFactoryImpl()
|
||||
|
||||
def _generate_test_files(self):
|
||||
self.tearDown()
|
||||
|
||||
for filename in self.files:
|
||||
create_dummy_file(os.path.join(self.temp_folder, filename))
|
||||
|
||||
create_dummy_file(self.styles_file, self.original_styles_content)
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.temp_folder, ignore_errors=True)
|
||||
|
||||
def test_append_label_correctly_labels_files(self):
|
||||
self._generate_test_files()
|
||||
label = "test"
|
||||
labeled_files = [(f, f.replace(".", f"-{label}.")) for f in self.files]
|
||||
labeler = self.factory.create(self.temp_folder)
|
||||
|
||||
labeler.append_label(label)
|
||||
|
||||
for original, labeled in labeled_files:
|
||||
labeled_path = os.path.join(self.temp_folder, labeled)
|
||||
original_path = os.path.join(self.temp_folder, original)
|
||||
self.assertTrue(os.path.exists(labeled_path))
|
||||
self.assertFalse(os.path.exists(original_path))
|
||||
|
||||
def test_append_label_correctly_updates_references(self):
|
||||
self._generate_test_files()
|
||||
label = "test"
|
||||
replaced_file = self.files[0].replace('.', f'-{label}.')
|
||||
expected_content = f"body {{ background: url('./{replaced_file}'); }}"
|
||||
labeler = self.factory.create(self.temp_folder, self.styles_file)
|
||||
|
||||
labeler.append_label(label)
|
||||
|
||||
with open(self.styles_file, 'r') as file:
|
||||
actual_content = file.read()
|
||||
self.assertNotEqual(actual_content, self.original_styles_content)
|
||||
self.assertEqual(actual_content, expected_content)
|
||||
80
tests/utils/test_style_manager.py
Normal file
80
tests/utils/test_style_manager.py
Normal file
@@ -0,0 +1,80 @@
|
||||
import os
|
||||
import shutil
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from scripts import config
|
||||
from scripts.utils.style_manager import StyleManager
|
||||
from .._helpers import create_dummy_file
|
||||
|
||||
|
||||
class StyleManagerTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp_folder = os.path.join(config.temp_tests_folder, "style_manager")
|
||||
os.makedirs(self.temp_folder, exist_ok=True)
|
||||
self.output_file = os.path.join(self.temp_folder, "output.css")
|
||||
self.manager = StyleManager(output_file=self.output_file)
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.temp_folder, ignore_errors=True)
|
||||
|
||||
def test_append_content(self):
|
||||
start_css = "body { background-color: blue; }"
|
||||
create_dummy_file(self.output_file, start_css)
|
||||
end_css = "h1 { color: red; }"
|
||||
|
||||
self.manager.append_content(end_css)
|
||||
|
||||
with open(self.output_file, 'r') as f:
|
||||
content = f.read()
|
||||
split_content = content.splitlines()
|
||||
assert split_content[0] == start_css
|
||||
assert split_content[1] == end_css
|
||||
os.remove(self.output_file)
|
||||
|
||||
def test_append_does_not_create_file(self):
|
||||
end_css = "h1 { color: red; }"
|
||||
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
self.manager.append_content(end_css)
|
||||
|
||||
def test_prepend_content(self):
|
||||
start_css = "body { background-color: blue; }"
|
||||
create_dummy_file(self.output_file, start_css)
|
||||
prepend_css = "h1 { color: red; }"
|
||||
|
||||
self.manager.prepend_content(prepend_css)
|
||||
|
||||
with open(self.output_file, 'r') as f:
|
||||
content = f.read()
|
||||
split_content = content.splitlines()
|
||||
assert split_content[0] == prepend_css
|
||||
assert split_content[1] == start_css
|
||||
os.remove(self.output_file)
|
||||
|
||||
def test_prepend_does_not_create_file(self):
|
||||
prepend_css = "h1 { color: red; }"
|
||||
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
self.manager.prepend_content(prepend_css)
|
||||
|
||||
def test_generate_combined_styles(self):
|
||||
source_folder = os.path.join(config.temp_tests_folder, "style_manager_source")
|
||||
source_css_folder = os.path.join(source_folder, ".css")
|
||||
first_file = os.path.join(source_css_folder, "file1.css")
|
||||
second_file = os.path.join(source_css_folder, "file2.css")
|
||||
first_css = "body { background-color: blue; }"
|
||||
second_css = "h1 { color: red; }"
|
||||
create_dummy_file(first_file, first_css)
|
||||
create_dummy_file(second_file, second_css)
|
||||
|
||||
with patch("subprocess.check_output", return_value="GNOME Shell 47.0"):
|
||||
self.manager.generate_combined_styles(source_folder, self.temp_folder)
|
||||
|
||||
with open(self.output_file, 'r') as f:
|
||||
content = f.read()
|
||||
split_content = content.splitlines()
|
||||
assert first_css in split_content
|
||||
assert second_css in split_content
|
||||
os.remove(self.output_file)
|
||||
shutil.rmtree(source_folder, ignore_errors=True)
|
||||
0
tests/utils/theme/__init__.py
Normal file
0
tests/utils/theme/__init__.py
Normal file
72
tests/utils/theme/assets/colors.json
Normal file
72
tests/utils/theme/assets/colors.json
Normal file
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"elements": {
|
||||
"BUTTON-COLOR": {
|
||||
"default": "ACCENT-COLOR"
|
||||
},
|
||||
"ACCENT-COLOR": {
|
||||
"light": {
|
||||
"s": 52,
|
||||
"l": 67,
|
||||
"a": 1
|
||||
},
|
||||
"dark": {
|
||||
"s": 42,
|
||||
"l": 26,
|
||||
"a": 1
|
||||
}
|
||||
},
|
||||
"ACCENT_HOVER": {
|
||||
"light": {
|
||||
"s": 50,
|
||||
"l": 60,
|
||||
"a": 0.8
|
||||
},
|
||||
"dark": {
|
||||
"s": 66,
|
||||
"l": 22,
|
||||
"a": 0.4
|
||||
}
|
||||
}
|
||||
},
|
||||
"colors": {
|
||||
"red": {
|
||||
"h": 0
|
||||
},
|
||||
"gray": {
|
||||
"h": 0,
|
||||
"s": 0
|
||||
},
|
||||
"expected": {
|
||||
"BUTTON-COLOR": {
|
||||
"0,100": {
|
||||
"light": "rgba(215, 127, 127, 1)",
|
||||
"dark": "rgba(94, 38, 38, 1)"
|
||||
},
|
||||
"0,0": {
|
||||
"light": "rgba(171, 171, 171, 1)",
|
||||
"dark": "rgba(66, 66, 66, 1)"
|
||||
}
|
||||
},
|
||||
"ACCENT-COLOR": {
|
||||
"0,100": {
|
||||
"light": "rgba(215, 127, 127, 1)",
|
||||
"dark": "rgba(94, 38, 38, 1)"
|
||||
},
|
||||
"0,0": {
|
||||
"light": "rgba(171, 171, 171, 1)",
|
||||
"dark": "rgba(66, 66, 66, 1)"
|
||||
}
|
||||
},
|
||||
"ACCENT_HOVER": {
|
||||
"0,100": {
|
||||
"light": "rgba(204, 102, 102, 0.8)",
|
||||
"dark": "rgba(93, 19, 19, 0.4)"
|
||||
},
|
||||
"0,0": {
|
||||
"light": "rgba(153, 153, 153, 0.8)",
|
||||
"dark": "rgba(56, 56, 56, 0.4)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
64
tests/utils/theme/test_color_replacement_generator.py
Normal file
64
tests/utils/theme/test_color_replacement_generator.py
Normal file
@@ -0,0 +1,64 @@
|
||||
import os.path
|
||||
import unittest
|
||||
|
||||
from scripts.install.colors_definer import ColorsDefiner
|
||||
from scripts.types.installation_color import InstallationColor, InstallationMode
|
||||
from scripts.utils.color_converter.color_converter_impl import ColorConverterImpl
|
||||
from scripts.utils.theme.color_replacement_generator import ColorReplacementGenerator
|
||||
|
||||
class ColorReplacementGeneratorTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
colors_location = os.path.join(os.path.dirname(__file__), "assets", "colors.json")
|
||||
self.colors_provider = ColorsDefiner(colors_location)
|
||||
self.color_converter = ColorConverterImpl()
|
||||
self.generator = ColorReplacementGenerator(self.colors_provider, self.color_converter)
|
||||
|
||||
def test_convert_red_color_in_dark_mode_generates_correct_rgba(self):
|
||||
theme_color = InstallationColor(hue=0, saturation=None, modes=[])
|
||||
mode: InstallationMode = "dark"
|
||||
expected_output = self._get_expected_output(theme_color, mode)
|
||||
|
||||
actual_output = self.generator.convert(mode, theme_color)
|
||||
|
||||
self._assert_expected_and_actual_replacers_match(expected_output, actual_output)
|
||||
|
||||
def test_convert_gray_color_in_light_mode_generates_correct_rgba(self):
|
||||
theme_color = InstallationColor(hue=0, saturation=0, modes=[])
|
||||
mode: InstallationMode = "light"
|
||||
expected_output = self._get_expected_output(theme_color, mode)
|
||||
|
||||
actual_output = self.generator.convert(mode, theme_color)
|
||||
|
||||
self._assert_expected_and_actual_replacers_match(expected_output, actual_output)
|
||||
|
||||
def test_convert_not_existent_mode_raises_key_error(self):
|
||||
theme_color = InstallationColor(hue=0, saturation=0, modes=[])
|
||||
mode = "not_existent_mode"
|
||||
|
||||
with self.assertRaises(KeyError):
|
||||
# noinspection PyTypeChecker
|
||||
self.generator.convert(mode, theme_color)
|
||||
|
||||
def _get_expected_output(self, theme_color: InstallationColor, mode: str):
|
||||
return [
|
||||
("ACCENT-COLOR", self._get_rgba("ACCENT-COLOR", theme_color, mode)),
|
||||
("ACCENT_HOVER", self._get_rgba("ACCENT_HOVER", theme_color, mode)),
|
||||
("BUTTON-COLOR", self._get_rgba("BUTTON-COLOR", theme_color, mode)),
|
||||
]
|
||||
|
||||
def _get_rgba(self, replacer_name: str, theme_color: InstallationColor, mode: str):
|
||||
expected_colors: dict = self.colors_provider.colors.get("expected")
|
||||
replacer_colors = expected_colors.get(replacer_name)
|
||||
saturation = theme_color.saturation if theme_color.saturation is not None else 100
|
||||
variant_colors = replacer_colors.get(f"{theme_color.hue},{saturation}")
|
||||
expected_rgba = variant_colors.get(mode)
|
||||
return expected_rgba
|
||||
|
||||
@staticmethod
|
||||
def _assert_expected_and_actual_replacers_match(expected: list, actual: list):
|
||||
for expected_element, expected_rgba in expected:
|
||||
actual_rgba = next(
|
||||
(rgba for element, rgba in actual if element == expected_element), None
|
||||
)
|
||||
assert actual_rgba is not None
|
||||
assert expected_rgba == actual_rgba
|
||||
76
tests/utils/theme/test_gnome_shell_theme_builder.py
Normal file
76
tests/utils/theme/test_gnome_shell_theme_builder.py
Normal file
@@ -0,0 +1,76 @@
|
||||
import unittest
|
||||
from unittest.mock import Mock
|
||||
import os
|
||||
|
||||
from scripts.utils.theme.gnome_shell_theme_builder import GnomeShellThemeBuilder
|
||||
|
||||
|
||||
class GnomeShellThemeBuilderTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.colors_provider = Mock()
|
||||
self.builder = GnomeShellThemeBuilder(self.colors_provider)
|
||||
|
||||
def test_builder_method_chaining_works_correctly(self):
|
||||
result = (self.builder.with_theme_name("test-theme")
|
||||
.with_mode("dark")
|
||||
.filled()
|
||||
.with_temp_folder("/tmp/test"))
|
||||
|
||||
self.assertIs(result, self.builder)
|
||||
self.assertEqual("test-theme", self.builder.theme_name)
|
||||
self.assertEqual("dark", self.builder.mode)
|
||||
self.assertTrue(self.builder.is_filled)
|
||||
self.assertEqual("/tmp/test", self.builder._base_temp_folder)
|
||||
|
||||
def test_paths_update_when_base_folder_changes(self):
|
||||
self.builder.with_temp_folder("/custom/temp")
|
||||
|
||||
expected_temp_folder = os.path.join("/custom/temp", self.builder.theme_name)
|
||||
expected_main_styles = os.path.join(expected_temp_folder, f"{self.builder.theme_name}.css")
|
||||
|
||||
self.assertEqual(expected_temp_folder, self.builder.temp_folder)
|
||||
self.assertEqual(expected_main_styles, self.builder.main_styles)
|
||||
|
||||
def test_paths_update_when_theme_name_changes(self):
|
||||
original_temp_folder = self.builder.temp_folder
|
||||
original_main_styles = self.builder.main_styles
|
||||
|
||||
self.builder.with_theme_name("custom-theme")
|
||||
|
||||
self.assertNotEqual(original_temp_folder, self.builder.temp_folder)
|
||||
self.assertNotEqual(original_main_styles, self.builder.main_styles)
|
||||
self.assertEqual(os.path.join(self.builder._base_temp_folder, "custom-theme"), self.builder.temp_folder)
|
||||
self.assertEqual(os.path.join(self.builder.temp_folder, "custom-theme.css"), self.builder.main_styles)
|
||||
|
||||
def test_default_values_are_set_properly(self):
|
||||
builder = GnomeShellThemeBuilder(self.colors_provider)
|
||||
|
||||
self.assertEqual("gnome-shell", builder.theme_name)
|
||||
self.assertIsNone(builder.mode)
|
||||
self.assertFalse(builder.is_filled)
|
||||
self.assertIsNone(builder.preparation)
|
||||
self.assertIsNone(builder.installer)
|
||||
|
||||
def test_build_correctly_resolves_dependencies(self):
|
||||
self.builder.build()
|
||||
|
||||
self.assertIsNotNone(self.builder.preparation)
|
||||
self.assertIsNotNone(self.builder.installer)
|
||||
|
||||
def test_build_correctly_creates_theme(self):
|
||||
self.builder.with_mode("light").filled()
|
||||
|
||||
theme = self.builder.build()
|
||||
|
||||
self.assertEqual(theme._preparation, self.builder.preparation)
|
||||
self.assertEqual(theme._installer, self.builder.installer)
|
||||
self.assertTrue(theme.is_filled)
|
||||
self.assertTrue(len(theme.modes) == 1)
|
||||
self.assertEqual(theme.modes[0], "light")
|
||||
|
||||
def test_filled_method_with_parameter(self):
|
||||
self.builder.filled(False)
|
||||
self.assertFalse(self.builder.is_filled)
|
||||
|
||||
self.builder.filled(True)
|
||||
self.assertTrue(self.builder.is_filled)
|
||||
90
tests/utils/theme/test_theme.py
Normal file
90
tests/utils/theme/test_theme.py
Normal file
@@ -0,0 +1,90 @@
|
||||
import os.path
|
||||
import unittest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from scripts import config
|
||||
from scripts.types.installation_color import InstallationColor
|
||||
from scripts.utils.theme.theme import Theme
|
||||
|
||||
|
||||
class ThemeTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.mock_preparation = MagicMock()
|
||||
self.mock_installer = MagicMock()
|
||||
|
||||
temp_folder = os.path.join(config.temp_tests_folder, "theme_temp")
|
||||
destination_folder = os.path.join(temp_folder, "theme_destination")
|
||||
|
||||
self.mock_preparation.temp_folder = temp_folder
|
||||
self.mock_preparation.combined_styles_location = os.path.join(temp_folder, "styles.css")
|
||||
self.mock_installer.destination_folder = destination_folder
|
||||
self.mock_installer.theme_type = "gtk"
|
||||
|
||||
self.theme = Theme(self.mock_preparation, self.mock_installer)
|
||||
|
||||
def test_default_initialization_works_correctly(self):
|
||||
self.assertEqual(self.theme.modes, ['light', 'dark'])
|
||||
self.assertFalse(self.theme.is_filled)
|
||||
|
||||
def test_initialization_with_specific_mode_works_correctly(self):
|
||||
theme_light = Theme(self.mock_preparation, self.mock_installer, mode='light')
|
||||
self.assertEqual(theme_light.modes, ['light'])
|
||||
|
||||
def test_initialization_with_is_filled_works_correctly(self):
|
||||
theme_filled = Theme(self.mock_preparation, self.mock_installer, is_filled=True)
|
||||
self.assertTrue(theme_filled.is_filled)
|
||||
|
||||
def test_properties_fetch_values_correctly(self):
|
||||
temp_folder = os.path.join(config.temp_tests_folder, "theme_temp")
|
||||
destination_folder = os.path.join(temp_folder, "theme_destination")
|
||||
|
||||
self.assertEqual(self.theme.temp_folder, temp_folder)
|
||||
self.assertEqual(self.theme.destination_folder, destination_folder)
|
||||
self.assertEqual(self.theme.main_styles, os.path.join(temp_folder, "styles.css"))
|
||||
self.assertEqual(self.theme.theme_name, "gtk")
|
||||
|
||||
def test_add_operator_called_once_and_return_value(self):
|
||||
result = self.theme + "additional styles"
|
||||
|
||||
self.mock_preparation.__iadd__.assert_called_once_with("additional styles")
|
||||
self.assertEqual(result, self.theme)
|
||||
|
||||
def test_mul_operator_called_once_and_return_value(self):
|
||||
result = self.theme * "/path/to/file"
|
||||
|
||||
self.mock_preparation.__imul__.assert_called_once_with("/path/to/file")
|
||||
self.assertEqual(result, self.theme)
|
||||
|
||||
def test_add_to_start_called_once_and_return_value(self):
|
||||
result = self.theme.add_to_start("starting content")
|
||||
|
||||
self.mock_preparation.add_to_start.assert_called_once_with("starting content")
|
||||
self.assertEqual(result, self.theme)
|
||||
|
||||
def test_prepare_called_once(self):
|
||||
self.theme.prepare()
|
||||
|
||||
self.mock_preparation.prepare.assert_called_once()
|
||||
|
||||
def test_install_without_optional_params_called_correctly(self):
|
||||
self.theme.install(200, "Green")
|
||||
|
||||
args = self.mock_installer.install.call_args[0]
|
||||
self.assertEqual(args[0].hue, 200)
|
||||
self.assertIsNone(args[0].saturation)
|
||||
self.assertEqual(args[1], "Green")
|
||||
self.assertIsNone(args[2])
|
||||
|
||||
def test_install_with_optional_params_called_correctly(self):
|
||||
self.theme.install(hue=180, name="Blue", sat=0.5, destination="/custom/dest")
|
||||
|
||||
self.mock_installer.install.assert_called_once()
|
||||
args = self.mock_installer.install.call_args[0]
|
||||
|
||||
theme_color = args[0]
|
||||
self.assertIsInstance(theme_color, InstallationColor)
|
||||
self.assertEqual(theme_color.hue, 180)
|
||||
self.assertEqual(theme_color.saturation, 0.5)
|
||||
self.assertEqual(theme_color.modes, ['light', 'dark'])
|
||||
self.assertEqual(args[1], "Blue")
|
||||
self.assertEqual(args[2], "/custom/dest")
|
||||
52
tests/utils/theme/test_theme_color_applier.py
Normal file
52
tests/utils/theme/test_theme_color_applier.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import os.path
|
||||
import shutil
|
||||
import unittest
|
||||
from unittest.mock import Mock
|
||||
|
||||
from scripts import config
|
||||
from scripts.utils.theme.theme_color_applier import ThemeColorApplier
|
||||
from ..._helpers import create_dummy_file
|
||||
|
||||
|
||||
class ThemeColorApplierTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
color_replacement_generator = Mock()
|
||||
color_replacement_generator.convert.return_value = [
|
||||
("ACCENT-COLOR", "rgba(255, 0, 0, 1)"),
|
||||
("ACCENT_HOVER", "rgba(255, 0, 0, 0.8)"),
|
||||
("BACKGROUND-COLOR", "rgba(0, 0, 0, 1)")
|
||||
]
|
||||
|
||||
self.temp_folder = os.path.join(config.temp_tests_folder, "color_applier")
|
||||
self._setup_temp_folder()
|
||||
|
||||
self.color_applier = ThemeColorApplier(color_replacement_generator)
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.temp_folder, ignore_errors=True)
|
||||
|
||||
def _setup_temp_folder(self):
|
||||
self.first_file = os.path.join(self.temp_folder, "file1.css")
|
||||
self.second_file = os.path.join(self.temp_folder, "file2.css")
|
||||
self.first_css = "body { background-color: ACCENT-COLOR; color: ACCENT_HOVER; }"
|
||||
self.second_css = "body { background-color: BACKGROUND-COLOR; }"
|
||||
create_dummy_file(self.first_file, self.first_css)
|
||||
create_dummy_file(self.second_file, self.second_css)
|
||||
|
||||
def test_colors_in_files_are_replaced_correctly(self):
|
||||
theme_color = Mock()
|
||||
|
||||
self.color_applier.apply(theme_color, self.temp_folder, "dark")
|
||||
|
||||
with open(self.first_file, "r") as file:
|
||||
content = file.read()
|
||||
replaced = self.first_css
|
||||
replaced = replaced.replace("ACCENT-COLOR", "rgba(255, 0, 0, 1)")
|
||||
replaced = replaced.replace("ACCENT_HOVER", "rgba(255, 0, 0, 0.8)")
|
||||
assert content == replaced
|
||||
|
||||
with open(self.second_file, "r") as file:
|
||||
content = file.read()
|
||||
replaced = self.second_css
|
||||
replaced = replaced.replace("BACKGROUND-COLOR", "rgba(0, 0, 0, 1)")
|
||||
assert content == replaced
|
||||
109
tests/utils/theme/test_theme_installer.py
Normal file
109
tests/utils/theme/test_theme_installer.py
Normal file
@@ -0,0 +1,109 @@
|
||||
import os
|
||||
import shutil
|
||||
import unittest
|
||||
from unittest.mock import Mock
|
||||
|
||||
from scripts import config
|
||||
from scripts.utils.theme.theme_installer import ThemeInstaller
|
||||
from scripts.utils.theme.theme_path_provider import ThemePathProvider
|
||||
from ..._helpers import create_dummy_file
|
||||
|
||||
|
||||
class ThemeInstallerTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.theme_type = "gnome-shell"
|
||||
self.source_folder = os.path.join(config.temp_tests_folder, "theme_installer_source")
|
||||
self.destination_folder = os.path.join(config.temp_tests_folder, "theme_installer_destination")
|
||||
self.custom_destination_folder = os.path.join(config.temp_tests_folder, "theme_installer_custom_destination")
|
||||
|
||||
self.logger_factory = Mock()
|
||||
self.color_applier = Mock()
|
||||
self.path_provider = ThemePathProvider()
|
||||
self.path_provider.get_theme_path = Mock(return_value=self.destination_folder)
|
||||
|
||||
self.theme_installer = ThemeInstaller(
|
||||
theme_type=self.theme_type,
|
||||
source_folder=self.source_folder,
|
||||
destination_folder=self.destination_folder,
|
||||
logger_factory=self.logger_factory,
|
||||
color_applier=self.color_applier,
|
||||
path_provider=self.path_provider,
|
||||
)
|
||||
|
||||
self._setup_source_folder()
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.source_folder, ignore_errors=True)
|
||||
shutil.rmtree(self.destination_folder, ignore_errors=True)
|
||||
shutil.rmtree(self.custom_destination_folder, ignore_errors=True)
|
||||
|
||||
def _setup_source_folder(self):
|
||||
os.makedirs(self.source_folder, exist_ok=True)
|
||||
first_file = os.path.join(self.source_folder, "file1.css")
|
||||
second_file = os.path.join(self.source_folder, "file2.css")
|
||||
first_css = "body { background-color: ACCENT-COLOR; color: ACCENT_HOVER; }"
|
||||
second_css = "body { background-color: BACKGROUND-COLOR; }"
|
||||
create_dummy_file(first_file, first_css)
|
||||
create_dummy_file(second_file, second_css)
|
||||
|
||||
def test_install_calls_get_theme_path_and_apply_methods_with_correct_parameters(self):
|
||||
theme_color = Mock()
|
||||
theme_color.modes = ["light"]
|
||||
name = "test-theme"
|
||||
|
||||
self.theme_installer.install(theme_color, name)
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.path_provider.get_theme_path.assert_called_once_with(
|
||||
self.destination_folder, name, "light", self.theme_type
|
||||
)
|
||||
self.color_applier.apply.assert_called_once_with(theme_color, self.destination_folder, "light")
|
||||
|
||||
def test_install_with_custom_destination_calls_get_theme_path_and_apply_methods_with_correct_parameters(self):
|
||||
theme_color = Mock()
|
||||
theme_color.modes = ["light"]
|
||||
name = "test-theme"
|
||||
os.makedirs(self.custom_destination_folder, exist_ok=True)
|
||||
|
||||
self.theme_installer.install(theme_color, name, self.custom_destination_folder)
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.path_provider.get_theme_path.assert_not_called()
|
||||
self.color_applier.apply.assert_called_once_with(theme_color, self.custom_destination_folder, "light")
|
||||
|
||||
def test_install_with_multiple_modes_calls_get_theme_path_and_apply_methods_for_each_mode(self):
|
||||
theme_color = Mock()
|
||||
theme_color.modes = ["light", "dark"]
|
||||
name = "test-theme"
|
||||
|
||||
self.theme_installer.install(theme_color, name)
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.assertEqual(self.path_provider.get_theme_path.call_count, 2)
|
||||
self.assertEqual(self.color_applier.apply.call_count, 2)
|
||||
|
||||
def test_install_raises_exception_and_logs_error(self):
|
||||
theme_color = Mock()
|
||||
theme_color.modes = ["light"]
|
||||
name = "test-theme"
|
||||
self.color_applier.apply.side_effect = Exception("Test error")
|
||||
|
||||
with self.assertRaises(Exception):
|
||||
self.theme_installer.install(theme_color, name)
|
||||
|
||||
logger_mock = self.logger_factory.create_logger.return_value
|
||||
self.assertTrue(logger_mock.error.called)
|
||||
|
||||
def test_install_copies_files_to_destination(self):
|
||||
theme_color = Mock()
|
||||
theme_color.modes = ["light"]
|
||||
name = "test-theme"
|
||||
destination = os.path.join(self.destination_folder, "actual_destination")
|
||||
self.path_provider.get_theme_path.return_value = destination
|
||||
|
||||
self.theme_installer.install(theme_color, name)
|
||||
|
||||
first_file_exists = os.path.exists(os.path.join(destination, "file1.css"))
|
||||
second_file_exists = os.path.exists(os.path.join(destination, "file2.css"))
|
||||
self.assertTrue(first_file_exists)
|
||||
self.assertTrue(second_file_exists)
|
||||
28
tests/utils/theme/test_theme_path_provider.py
Normal file
28
tests/utils/theme/test_theme_path_provider.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import unittest
|
||||
|
||||
from scripts.utils.theme.theme_path_provider import ThemePathProvider
|
||||
|
||||
|
||||
class ThemePathProviderTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.theme_path_provider = ThemePathProvider()
|
||||
|
||||
def test_get_theme_path_with_valid_values_returns_correct_path(self):
|
||||
themes_folder = "/usr/share/themes"
|
||||
color_name = "Marble"
|
||||
theme_mode = "dark"
|
||||
theme_type = "gnome-shell"
|
||||
|
||||
expected_path = f"{themes_folder}/Marble-{color_name}-{theme_mode}/{theme_type}/"
|
||||
actual_path = self.theme_path_provider.get_theme_path(themes_folder, color_name, theme_mode, theme_type)
|
||||
|
||||
assert expected_path == actual_path
|
||||
|
||||
def test_get_theme_path_with_empty_values_raises_exception(self):
|
||||
themes_folder = ""
|
||||
color_name = ""
|
||||
theme_mode = ""
|
||||
theme_type = ""
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
self.theme_path_provider.get_theme_path(themes_folder, color_name, theme_mode, theme_type)
|
||||
71
tests/utils/theme/test_theme_temp_manager.py
Normal file
71
tests/utils/theme/test_theme_temp_manager.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import os.path
|
||||
import shutil
|
||||
import unittest
|
||||
|
||||
from scripts import config
|
||||
from scripts.utils.theme.theme_temp_manager import ThemeTempManager
|
||||
from ..._helpers import create_dummy_file
|
||||
|
||||
|
||||
class ThemeTempManagerTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.source_folder = os.path.join(config.temp_tests_folder, "theme_temp_manager_source")
|
||||
self.temp_folder = os.path.join(config.temp_tests_folder, "theme_temp_manager")
|
||||
self.manager = ThemeTempManager(temp_folder=self.temp_folder)
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.temp_folder, ignore_errors=True)
|
||||
shutil.rmtree(self.source_folder, ignore_errors=True)
|
||||
|
||||
@staticmethod
|
||||
def _verify_file_copied(source, destination):
|
||||
assert os.path.exists(destination)
|
||||
assert os.path.getsize(destination) == os.path.getsize(source)
|
||||
assert open(destination).read() == open(source).read()
|
||||
|
||||
def test_file_copies_correctly_to_temp(self):
|
||||
test_file_name = "test_file.txt"
|
||||
test_file_location = os.path.join(self.source_folder, test_file_name)
|
||||
create_dummy_file(test_file_location)
|
||||
|
||||
self.manager.copy_to_temp(test_file_location)
|
||||
|
||||
final_file_location = os.path.join(self.temp_folder, test_file_name)
|
||||
self._verify_file_copied(test_file_location, final_file_location)
|
||||
os.remove(final_file_location)
|
||||
|
||||
def test_directory_content_copies_correctly_to_temp(self):
|
||||
test_dir_name = "test_dir"
|
||||
test_dir_location = os.path.join(self.source_folder, test_dir_name)
|
||||
os.makedirs(test_dir_location, exist_ok=True)
|
||||
|
||||
test_file_name = "test_file.txt"
|
||||
test_file_location = os.path.join(test_dir_location, test_file_name)
|
||||
create_dummy_file(test_file_location)
|
||||
|
||||
self.manager.copy_to_temp(test_dir_location)
|
||||
|
||||
final_file_location = os.path.join(self.temp_folder, test_file_name)
|
||||
self._verify_file_copied(test_file_location, final_file_location)
|
||||
os.remove(final_file_location)
|
||||
|
||||
def test_cleanup_removes_temp_folders(self):
|
||||
css_folder = os.path.join(self.temp_folder, ".css")
|
||||
versions_folder = os.path.join(self.temp_folder, ".versions")
|
||||
os.makedirs(css_folder, exist_ok=True)
|
||||
os.makedirs(versions_folder, exist_ok=True)
|
||||
|
||||
self.manager.cleanup()
|
||||
|
||||
assert not os.path.exists(css_folder)
|
||||
assert not os.path.exists(versions_folder)
|
||||
|
||||
def test_cleanup_non_existent_folders_do_not_raise_error(self):
|
||||
css_folder = os.path.join(self.temp_folder, ".css")
|
||||
versions_folder = os.path.join(self.temp_folder, ".versions")
|
||||
|
||||
self.manager.cleanup()
|
||||
|
||||
# Check that no error is raised and the method completes successfully
|
||||
assert not os.path.exists(css_folder)
|
||||
assert not os.path.exists(versions_folder)
|
||||
9
theme/__init__.py
Normal file
9
theme/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
import os
|
||||
|
||||
|
||||
class SourceFolder:
|
||||
themes_folder = os.path.dirname(__file__)
|
||||
|
||||
@property
|
||||
def gnome_shell(self):
|
||||
return os.path.join(self.themes_folder, "gnome-shell")
|
||||
@@ -1,5 +1,6 @@
|
||||
from scripts import config
|
||||
from scripts.utils import hex_to_rgba
|
||||
from scripts.utils.color_converter.color_converter_impl import ColorConverterImpl
|
||||
|
||||
panel_folder = f"{config.tweaks_folder}/panel"
|
||||
|
||||
|
||||
@@ -28,5 +29,5 @@ def apply_tweak(args, theme, colors):
|
||||
theme += ".panel-button,\
|
||||
.clock,\
|
||||
.clock-display StIcon {\
|
||||
color: rgba(" + ', '.join(map(str, hex_to_rgba(args.panel_text_color))) + ");\
|
||||
color: rgba(" + ', '.join(map(str, ColorConverterImpl.hex_to_rgba(args.panel_text_color))) + ");\
|
||||
}"
|
||||
|
||||
Reference in New Issue
Block a user