mirror of
https://github.com/imarkoff/Marble-shell-theme.git
synced 2025-09-17 08:47:55 -07:00
Covered Theme module with tests
- Extracted `ColorReplacementGenerator`; - Extracted `ColorConverterImpl`; - Added documentation to some classes; - `hex_to_rgba` now supports shorthand hex colors (#fff).
This commit is contained in:
@@ -1,42 +0,0 @@
|
||||
import colorsys
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class ColorConverter(ABC):
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def hex_to_rgba(hex_color):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def hsl_to_rgb(hue, saturation, lightness):
|
||||
pass
|
||||
|
||||
|
||||
class ColorConverterImpl(ColorConverter):
|
||||
@staticmethod
|
||||
def hex_to_rgba(hex_color):
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
def hsl_to_rgb(hue, saturation, lightness):
|
||||
# colorsys works in range(0, 1)
|
||||
h = hue / 360
|
||||
red, green, blue = [int(item * 255) for item in colorsys.hls_to_rgb(h, lightness, saturation)]
|
||||
return red, green, blue
|
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
|
@@ -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 = []
|
||||
|
@@ -1,19 +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(content + '\n')
|
||||
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)
|
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]
|
@@ -2,11 +2,12 @@ import os.path
|
||||
|
||||
from scripts import config
|
||||
from scripts.install.colors_definer import ColorsDefiner
|
||||
from scripts.utils.color_converter import ColorConverterImpl
|
||||
from scripts.utils.color_converter.color_converter_impl import ColorConverterImpl
|
||||
from scripts.utils.logger.console import Console
|
||||
from scripts.utils.style_manager import StyleManager
|
||||
from scripts.utils.theme.theme import Theme
|
||||
from scripts.utils.theme.theme_color_applier import ColorReplacementGenerator, ThemeColorApplier
|
||||
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
|
||||
|
@@ -1,16 +1,14 @@
|
||||
import copy
|
||||
import os
|
||||
|
||||
from scripts.install.colors_definer import ColorsDefiner
|
||||
from scripts.types.installation_color import InstallationColor, InstallationMode
|
||||
from scripts.utils import replace_keywords
|
||||
from scripts.utils.color_converter import ColorConverter
|
||||
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"):
|
||||
def __init__(self, color_replacement_generator: ColorReplacementGenerator):
|
||||
self.color_replacement_generator = color_replacement_generator
|
||||
|
||||
def apply(self, theme_color: InstallationColor, destination: str, mode: InstallationMode):
|
||||
@@ -20,42 +18,3 @@ class ThemeColorApplier:
|
||||
for filename in os.listdir(destination):
|
||||
file_path = os.path.join(destination, filename)
|
||||
replace_keywords(file_path, *replacements)
|
||||
|
||||
|
||||
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]
|
@@ -1,4 +1,20 @@
|
||||
import os
|
||||
|
||||
|
||||
class ThemePathProvider:
|
||||
@staticmethod
|
||||
def get_theme_path(themes_folder: str, path_name: str, theme_mode: str, theme_type: str) -> str:
|
||||
return f"{themes_folder}/Marble-{path_name}-{theme_mode}/{theme_type}/"
|
||||
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
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import os
|
||||
import warnings
|
||||
|
||||
from scripts.utils import replace_keywords
|
||||
from scripts.utils.theme.theme_temp_manager import ThemeTempManager
|
||||
@@ -45,11 +44,10 @@ class ThemePreparation:
|
||||
"""
|
||||
Extract theme from source folder and prepare it for installation.
|
||||
"""
|
||||
self.file_manager.prepare_files(self.sources_location)
|
||||
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()
|
||||
|
||||
@warnings.deprecated
|
||||
def replace_filled_keywords(self):
|
||||
"""
|
||||
Replace keywords in the theme files for filled mode.
|
||||
@@ -62,4 +60,4 @@ class ThemePreparation:
|
||||
("BUTTON_ACTIVE", "ACCENT-FILLED_ACTIVE"),
|
||||
("BUTTON_INSENSITIVE", "ACCENT-FILLED_INSENSITIVE"),
|
||||
("BUTTON-TEXT-COLOR", "TEXT-BLACK-COLOR"),
|
||||
("BUTTON-TEXT_SECONDARY", "TEXT-BLACK_SECONDARY"))
|
||||
("BUTTON-TEXT_SECONDARY", "TEXT-BLACK_SECONDARY"))
|
||||
|
@@ -1,8 +1,6 @@
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from scripts.utils import copy_files
|
||||
|
||||
|
||||
class ThemeTempManager:
|
||||
"""
|
||||
@@ -10,18 +8,21 @@ class ThemeTempManager:
|
||||
"""
|
||||
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):
|
||||
shutil.copy(content, self.temp_folder)
|
||||
final_path = os.path.join(self.temp_folder, os.path.basename(content))
|
||||
shutil.copy(content, final_path)
|
||||
else:
|
||||
shutil.copytree(content, self.temp_folder)
|
||||
shutil.copytree(content, self.temp_folder, dirs_exist_ok=True)
|
||||
return self
|
||||
|
||||
def prepare_files(self, sources_location: str):
|
||||
"""Prepare files in temp folder"""
|
||||
copy_files(sources_location, self.temp_folder)
|
||||
|
||||
def cleanup(self):
|
||||
"""Remove temporary folders"""
|
||||
shutil.rmtree(f"{self.temp_folder}/.css/", ignore_errors=True)
|
||||
|
Reference in New Issue
Block a user