Extracted installation methods, created a new class for theme installers

This commit is contained in:
Vladyslav Hroshev
2025-03-31 23:53:14 +03:00
parent 1bd1b781db
commit 860cacaa2c
10 changed files with 296 additions and 236 deletions

View File

@@ -14,206 +14,34 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
import json # working with json files
import argparse # command-line options
import os.path
import shutil import shutil
import textwrap # example text in argparse
from scripts import config # folder and files definitions from scripts import config
from scripts.tweaks_manager import TweaksManager # load tweaks from files from scripts.install import ArgumentsDefiner
from scripts.install.colors_definer import ColorsDefiner
from scripts.utils import remove_files # delete already installed Marble theme from scripts.install.global_theme_installer import GlobalThemeInstaller
from scripts.utils.gnome import apply_gnome_theme # apply theme to GNOME shell from scripts.install.local_theme_installer import LocalThemeInstaller
from scripts.utils.gnome import apply_gnome_theme
from scripts.theme import Theme
from scripts.gdm import GlobalTheme
def parse_args(colors) -> argparse.Namespace:
"""
Parse command-line arguments
:return: parsed arguments
"""
# script description
parser = argparse.ArgumentParser(prog="python install.py",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=textwrap.dedent('''
Examples:
-a all accent colors, light & dark mode
--all --mode dark all accent colors, dark mode
--purple --mode=light purple accent color, light mode
--hue 150 --name coldgreen custom coldgreen accent color, light & dark mode
--red --green --sat=70 red, green accent colors, 70% of stock saturation
--hue=200 --name=grayblue --sat=50 --mode=dark
custom grayblue accent color, 50% of stock saturation, dark mode
'''))
# Default arguments
parser.add_argument('-r', '--remove', action='store_true', help='remove Marble themes')
parser.add_argument('-ri', '--reinstall', action='store_true', help='reinstall Marble themes')
default_args = parser.add_argument_group('Install default theme')
default_args.add_argument('-a', '--all', action='store_true', help='all available accent colors')
for color in colors["colors"]:
default_args.add_argument(f'--{color}', action='store_true', help=f'{color} theme only')
custom_args = parser.add_argument_group('Install custom color theme')
custom_args.add_argument('--hue', type=int, choices=range(0, 361), help='generate theme from Hue prompt',
metavar='(0 - 360)')
custom_args.add_argument('--name', nargs='?', help='theme name (optional)')
color_styles = parser.add_argument_group("Theme color tweaks")
color_styles.add_argument("--filled", action="store_true", help="make accent color more vibrant")
color_tweaks = parser.add_argument_group('Optional theme tweaks')
color_tweaks.add_argument('--mode', choices=['light', 'dark'], help='select a specific theme mode to install')
color_tweaks.add_argument('--sat', type=int, choices=range(0, 251),
help='custom color saturation (<100%% - reduce, >100%% - increase)', metavar='(0 - 250)')
gdm_theming = parser.add_argument_group('GDM theming')
gdm_theming.add_argument('--gdm', action='store_true', help='install GDM theme. \
Requires root privileges. You must specify a specific color.')
# Dynamically load arguments from each tweak script
tweaks_manager = TweaksManager()
tweaks_manager.define_arguments(parser)
return parser.parse_args()
def apply_tweaks(args, theme, colors):
"""
Apply theme tweaks
:param args: parsed arguments
:param theme: Theme object
:param colors: colors from colors.json
"""
tweaks_manager = TweaksManager()
tweaks_manager.apply_tweaks(args, theme, colors)
def install_theme(theme, hue, theme_name, sat, gdm=False):
"""
Check if GDM and install theme
:param theme: object to install
:param hue: color hue
:param theme_name: future theme name
:param sat: color saturation
:param gdm: if GDM theme
"""
if gdm:
theme.install(hue, sat)
else:
theme.install(hue, theme_name, sat)
def apply_colors(args, theme, colors, gdm=False):
"""
Apply accent colors to the theme
:param args: parsed arguments
:param theme: Theme object
:param colors: colors from colors.json
:param gdm: if GDM theme
"""
is_colors = False # check if any color arguments specified
# if custom color
if args.hue:
hue = args.hue
theme_name = args.name if args.name else f'hue{hue}'
install_theme(theme, hue, theme_name, args.sat, gdm)
return None
else:
for color in colors["colors"]:
if args.all or getattr(args, color):
is_colors = True
hue = colors["colors"][color]["h"]
# if saturation already defined in color (gray)
sat = colors["colors"][color].get("s", args.sat)
install_theme(theme, hue, color, sat, gdm)
if gdm:
return None
if not is_colors:
print('No color arguments specified. Use -h or --help to see the available options.')
return 1
return None
def global_theme(args, colors):
"""
Apply GDM theme
:param args: parsed arguments
:param colors: colors from colors.json
"""
gdm_temp = os.path.join(config.temp_folder, config.gdm_folder)
gdm_theme = GlobalTheme(colors, f"{config.raw_theme_folder}/{config.gnome_folder}",
config.global_gnome_shell_theme, config.gnome_shell_gresource,
gdm_temp, mode=args.mode, is_filled=args.filled)
if args.remove:
gdm_rm_status = gdm_theme.remove()
if gdm_rm_status == 0:
print("GDM theme removed successfully.")
return
for theme in gdm_theme.themes:
apply_tweaks(args, theme.theme, colors)
if apply_colors(args, gdm_theme, colors, gdm=True) is None:
print("\nGDM theme installed successfully.")
print("You need to restart gdm.service to apply changes.")
print("Run \"systemctl restart gdm.service\" to restart GDM.")
def local_theme(args, colors):
"""
Apply local theme
:param args: parsed arguments
:param colors: colors from colors.json
"""
if args.remove or args.reinstall:
remove_files(args, colors["colors"])
if not args.reinstall:
return
gnome_shell_theme = Theme("gnome-shell", colors, f"{config.raw_theme_folder}/{config.gnome_folder}",
config.themes_folder, config.temp_folder,
mode=args.mode, is_filled=args.filled)
apply_tweaks(args, gnome_shell_theme, colors)
apply_colors(args, gnome_shell_theme, colors)
def main(): def main():
colors = json.load(open(config.colors_json)) colors_definer = ColorsDefiner(config.colors_json)
args = ArgumentsDefiner(colors_definer.colors).parse()
args = parse_args(colors) installer_class = GlobalThemeInstaller if args.gdm else LocalThemeInstaller
installer = installer_class(args, colors_definer)
if args.gdm:
global_theme(args, colors)
if args.remove or args.reinstall:
installer.remove()
else: else:
local_theme(args, colors) installer.install()
if args.remove == args.reinstall: if not args.gdm and args.remove == args.reinstall:
apply_gnome_theme() apply_gnome_theme()
if __name__ == "__main__": if __name__ == "__main__":
try: try:
main() main()
finally: finally:
shutil.rmtree(config.temp_folder, ignore_errors=True) shutil.rmtree(config.temp_folder, ignore_errors=True)

View File

@@ -42,23 +42,8 @@ class GlobalTheme:
self.gst = os.path.join(self.destination_folder, self.destination_file) # use backup file if theme is installed self.gst = os.path.join(self.destination_folder, self.destination_file) # use backup file if theme is installed
self.themes: list[ThemePrepare] = [] self.themes: list[ThemePrepare] = []
self.is_filled = is_filled
try: self.mode = mode
gnome_version = gnome.gnome_version()
gnome_major = gnome_version.split(".")[0]
if int(gnome_major) >= 44:
self.themes += [
self.__create_theme("gnome-shell-light", mode='light', should_label=True, is_filled=is_filled),
self.__create_theme("gnome-shell-dark", mode='dark', is_filled=is_filled)
]
except Exception as e:
print(f"Error: {e}")
print("Using single theme.")
if not self.themes:
self.themes.append(
self.__create_theme(
"gnome-shell", mode=mode if mode else 'dark', is_filled=is_filled))
def __create_theme(self, theme_type, mode=None, should_label=False, is_filled=False): def __create_theme(self, theme_type, mode=None, should_label=False, is_filled=False):
@@ -66,6 +51,7 @@ class GlobalTheme:
theme = Theme(theme_type, self.colors_json, self.theme_folder, theme = Theme(theme_type, self.colors_json, self.theme_folder,
self.extracted_theme, self.temp_folder, self.extracted_theme, self.temp_folder,
mode=mode, is_filled=is_filled) mode=mode, is_filled=is_filled)
theme.prepare()
theme_file = os.path.join(self.extracted_theme, f"{theme_type}.css") theme_file = os.path.join(self.extracted_theme, f"{theme_type}.css")
return ThemePrepare(theme=theme, theme_file=theme_file, should_label=should_label) return ThemePrepare(theme=theme, theme_file=theme_file, should_label=should_label)
@@ -117,7 +103,7 @@ class GlobalTheme:
gnome_styles = gnome_theme.read() + self.backup_trigger gnome_styles = gnome_theme.read() + self.backup_trigger
theme.add_to_start(gnome_styles) theme.add_to_start(gnome_styles)
def __prepare(self, hue, color, sat=None): def __generate_themes(self, hue, color, sat=None):
""" """
Generate theme files for gnome-shell-theme.gresource.xml Generate theme files for gnome-shell-theme.gresource.xml
:param hue: color hue :param hue: color hue
@@ -165,6 +151,24 @@ class GlobalTheme:
</gresource> </gresource>
</gresources>""" </gresources>"""
def prepare(self):
try:
gnome_version = gnome.gnome_version()
gnome_major = gnome_version.split(".")[0]
if int(gnome_major) >= 44:
self.themes += [
self.__create_theme("gnome-shell-light", mode='light', should_label=True, is_filled=self.is_filled),
self.__create_theme("gnome-shell-dark", mode='dark', is_filled=self.is_filled)
]
except Exception as e:
print(f"Error: {e}")
print("Using single theme.")
if not self.themes:
self.themes.append(
self.__create_theme(
"gnome-shell", mode=self.mode if self.mode else 'dark', is_filled=self.is_filled))
def install(self, hue, sat=None): def install(self, hue, sat=None):
""" """
Install theme globally Install theme globally
@@ -182,7 +186,7 @@ class GlobalTheme:
self.__extract() self.__extract()
# generate theme files for global theme # generate theme files for global theme
self.__prepare(hue, 'Marble', sat) self.__generate_themes(hue, 'Marble', sat)
# generate gnome-shell-theme.gresource.xml # generate gnome-shell-theme.gresource.xml
with open(f"{self.extracted_theme}/{self.destination_file}.xml", 'w') as gresource_xml: with open(f"{self.extracted_theme}/{self.destination_file}.xml", 'w') as gresource_xml:

View File

@@ -0,0 +1 @@
from scripts.install.arguments_definer import ArgumentsDefiner

View File

@@ -0,0 +1,73 @@
import argparse
import textwrap
from typing import Any
from scripts.tweaks_manager import TweaksManager
class ArgumentsDefiner:
def __init__(self, colors: dict[str, Any]):
self._parser = argparse.ArgumentParser(prog="python install.py",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=self._get_epilog())
self._define_default_arguments()
self._define_color_arguments(colors)
self._define_custom_color_arguments()
self._define_theme_styles_arguments()
self._define_color_tweaks_arguments()
self._define_gdm_arguments()
self._define_tweaks_arguments()
def parse(self) -> argparse.Namespace:
return self._parser.parse_args()
@staticmethod
def _get_epilog():
return textwrap.dedent('''
Examples:
-a all accent colors, light & dark mode
--all --mode dark all accent colors, dark mode
--purple --mode=light purple accent color, light mode
--hue 150 --name coldgreen custom coldgreen accent color, light & dark mode
--red --green --sat=70 red, green accent colors, 70% of stock saturation
--hue=200 --name=grayblue --sat=50 --mode=dark
custom grayblue accent color, 50% of stock saturation, dark mode
''')
def _define_default_arguments(self):
self._parser.add_argument('-r', '--remove', action='store_true', help='remove Marble themes')
self._parser.add_argument('-ri', '--reinstall', action='store_true', help='reinstall Marble themes')
def _define_color_arguments(self, colors: dict[str, Any]):
default_args = self._parser.add_argument_group('Install default theme')
default_args.add_argument('-a', '--all', action='store_true', help='all available accent colors')
for color in colors:
default_args.add_argument(f'--{color}', action='store_true', help=f'{color} theme only')
def _define_custom_color_arguments(self):
custom_args = self._parser.add_argument_group('Install custom color theme')
custom_args.add_argument('--hue', type=int, choices=range(0, 361), help='generate theme from Hue prompt',
metavar='(0 - 360)')
custom_args.add_argument('--name', nargs='?', help='theme name (optional)')
def _define_theme_styles_arguments(self):
color_styles = self._parser.add_argument_group("Theme color styles")
color_styles.add_argument("--filled", action="store_true", help="make accent color more vibrant")
def _define_color_tweaks_arguments(self):
color_tweaks = self._parser.add_argument_group('Optional theme tweaks')
color_tweaks.add_argument('--mode', choices=['light', 'dark'], help='select a specific theme mode to install')
color_tweaks.add_argument('--sat', type=int, choices=range(0, 251),
help='custom color saturation (<100%% - reduce, >100%% - increase)',
metavar='(0 - 250)')
def _define_gdm_arguments(self):
gdm_theming = self._parser.add_argument_group('GDM theming')
gdm_theming.add_argument('--gdm', action='store_true', help='install GDM theme. \
Requires root privileges. You must specify a specific color.')
def _define_tweaks_arguments(self):
tweaks_manager = TweaksManager()
tweaks_manager.define_arguments(self._parser)

View File

@@ -0,0 +1,17 @@
import json
class ColorsDefiner:
# TODO: Create a class for each replacer
replacers: dict[ str, # ACCENT-COLOR
dict[ str, # default, light/dark
str | dict[str, int] # random string, s/l/a
]
]
# TODO: Create a class for each color
colors: dict[str, dict[str, int]]
def __init__(self, filename):
colors_dict = json.load(open(filename))
self.replacers = colors_dict["elements"]
self.colors = colors_dict["colors"]

View File

@@ -0,0 +1,28 @@
import os
from scripts import config
from scripts.gdm import GlobalTheme
from scripts.install.theme_installer import ThemeInstaller
class GlobalThemeInstaller(ThemeInstaller):
theme: GlobalTheme
def remove(self):
gdm_rm_status = self.theme.remove()
if gdm_rm_status == 0:
print("GDM theme removed successfully.")
def _define_theme(self):
gdm_temp = os.path.join(config.temp_folder, config.gdm_folder)
self.theme = GlobalTheme(self.colors, f"{config.raw_theme_folder}/{config.gnome_folder}",
config.global_gnome_shell_theme, config.gnome_shell_gresource,
gdm_temp, mode=self.args.mode, is_filled=self.args.filled)
def _install_theme(self, hue, theme_name, sat):
self.theme.prepare()
self.theme.install(hue, sat)
def _apply_tweaks_to_theme(self):
for theme in self.theme.themes:
self._apply_tweaks(theme.theme)

View File

@@ -0,0 +1,31 @@
import os.path
from scripts import config
from scripts.install.theme_installer import ThemeInstaller
from scripts.theme import Theme
from scripts.utils import remove_files
class LocalThemeInstaller(ThemeInstaller):
theme: Theme
def remove(self):
args = self.args
colors = self.colors.colors
if args.remove or args.reinstall:
remove_files(args, colors)
if not args.reinstall:
return
def _define_theme(self):
theme_folder = os.path.join(config.raw_theme_folder, config.gnome_folder)
self.theme = Theme("gnome-shell", self.colors, theme_folder,
config.themes_folder, config.temp_folder,
mode=self.args.mode, is_filled=self.args.filled)
def _install_theme(self, hue, theme_name, sat):
self.theme.prepare()
self.theme.install(hue, theme_name, sat)
def _apply_tweaks_to_theme(self):
self._apply_tweaks(self.theme)

View File

@@ -0,0 +1,74 @@
import argparse
from scripts.install.colors_definer import ColorsDefiner
from scripts.theme import Theme
from scripts.tweaks_manager import TweaksManager
class ThemeInstaller:
theme: Theme
def __init__(self, args: argparse.Namespace, colors: ColorsDefiner):
self.args = args
self.colors = colors
self.stop_after_first_installed_color = False
self._define_theme()
def remove(self):
pass
def install(self):
self.theme.prepare()
self._apply_tweaks_to_theme()
self._apply_colors()
def _define_theme(self):
pass
def _install_theme(self, hue, theme_name, sat):
pass
def _apply_tweaks_to_theme(self):
pass
def _apply_tweaks(self, theme):
tweaks_manager = TweaksManager()
tweaks_manager.apply_tweaks(self.args, theme, self.colors)
def _apply_colors(self):
installed_any = False
if self.args.hue:
installed_any = True
self._apply_custom_color()
else:
installed_any = self._apply_default_color()
if not installed_any:
raise Exception('No color arguments specified. Use -h or --help to see the available options.')
def _apply_custom_color(self):
name = self.args.name
hue = self.args.hue
sat = self.args.sat
theme_name = name if name else f'hue{hue}'
self._install_theme(hue, theme_name, sat)
def _apply_default_color(self) -> bool:
colors = self.colors.colors
args = self.args
installed_any = False
for color, values in colors.items():
if self.args.all or getattr(self.args, color, False):
hue = values.get('h')
sat = values.get('s', args.sat) # if saturation already defined in color (gray)
self._install_theme(hue, color, sat)
installed_any = True
if self.stop_after_first_installed_color:
break
return installed_any

View File

@@ -2,6 +2,7 @@ import os
import shutil import shutil
import colorsys # colorsys.hls_to_rgb(h, l, s) import colorsys # colorsys.hls_to_rgb(h, l, s)
from .install.colors_definer import ColorsDefiner
from .utils import ( from .utils import (
replace_keywords, # replace keywords in file replace_keywords, # replace keywords in file
copy_files, # copy files from source to destination copy_files, # copy files from source to destination
@@ -23,31 +24,14 @@ class Theme:
:param is_filled: if True, theme will be filled :param is_filled: if True, theme will be filled
""" """
self.colors = colors_json self.colors: ColorsDefiner = colors_json
self.temp_folder = f"{temp_folder}/{theme_type}" self.temp_folder = f"{temp_folder}/{theme_type}"
self.theme_folder = theme_folder self.theme_folder = theme_folder
self.theme_type = theme_type self.theme_type = theme_type
self.mode = [mode] if mode else ['light', 'dark'] self.mode = [mode] if mode else ['light', 'dark']
self.destination_folder = destination_folder self.destination_folder = destination_folder
self.main_styles = f"{self.temp_folder}/{theme_type}.css" self.main_styles = f"{self.temp_folder}/{theme_type}.css"
self.is_filled = is_filled
# move files to temp folder
copy_files(self.theme_folder, self.temp_folder)
generate_file(f"{self.theme_folder}", self.temp_folder, self.main_styles)
# after generating main styles, remove .css and .versions folders
shutil.rmtree(f"{self.temp_folder}/.css/", ignore_errors=True)
shutil.rmtree(f"{self.temp_folder}/.versions/", ignore_errors=True)
# if theme is filled
if is_filled:
for apply_file in os.listdir(f"{self.temp_folder}/"):
replace_keywords(f"{self.temp_folder}/{apply_file}",
("BUTTON-COLOR", "ACCENT-FILLED-COLOR"),
("BUTTON_HOVER", "ACCENT-FILLED_HOVER"),
("BUTTON_ACTIVE", "ACCENT-FILLED_ACTIVE"),
("BUTTON_INSENSITIVE", "ACCENT-FILLED_INSENSITIVE"),
("BUTTON-TEXT-COLOR", "TEXT-BLACK-COLOR"),
("BUTTON-TEXT_SECONDARY", "TEXT-BLACK_SECONDARY"))
def __add__(self, other): def __add__(self, other):
""" """
@@ -93,18 +77,18 @@ class Theme:
# colorsys works in range(0, 1) # colorsys works in range(0, 1)
h = hue / 360 h = hue / 360
for element in self.colors["elements"]: for element in self.colors.replacers:
# if color has default color and hasn't been replaced # if color has default color and hasn't been replaced
if theme_mode not in self.colors["elements"][element] and self.colors["elements"][element]["default"]: if theme_mode not in self.colors.replacers[element] and self.colors.replacers[element]["default"]:
default_element = self.colors["elements"][element]["default"] default_element = self.colors.replacers[element]["default"]
default_color = self.colors["elements"][default_element][theme_mode] default_color = self.colors.replacers[default_element][theme_mode]
self.colors["elements"][element][theme_mode] = default_color self.colors.replacers[element][theme_mode] = default_color
# convert sla to range(0, 1) # convert sla to range(0, 1)
lightness = int(self.colors["elements"][element][theme_mode]["l"]) / 100 lightness = int(self.colors.replacers[element][theme_mode]["l"]) / 100
saturation = int(self.colors["elements"][element][theme_mode]["s"]) / 100 if sat is None else \ saturation = int(self.colors.replacers[element][theme_mode]["s"]) / 100 if sat is None else \
int(self.colors["elements"][element][theme_mode]["s"]) * (sat / 100) / 100 int(self.colors.replacers[element][theme_mode]["s"]) * (sat / 100) / 100
alpha = self.colors["elements"][element][theme_mode]["a"] alpha = self.colors.replacers[element][theme_mode]["a"]
# convert hsl to rgb and multiply every item # convert hsl to rgb and multiply every item
red, green, blue = [int(item * 255) for item in colorsys.hls_to_rgb(h, lightness, saturation)] red, green, blue = [int(item * 255) for item in colorsys.hls_to_rgb(h, lightness, saturation)]
@@ -127,6 +111,25 @@ class Theme:
for apply_file in os.listdir(f"{source}/"): for apply_file in os.listdir(f"{source}/"):
self.__apply_colors(hue, destination, theme_mode, apply_file, sat=sat) self.__apply_colors(hue, destination, theme_mode, apply_file, sat=sat)
def prepare(self):
# move files to temp folder
copy_files(self.theme_folder, self.temp_folder)
generate_file(f"{self.theme_folder}", self.temp_folder, self.main_styles)
# after generating main styles, remove .css and .versions folders
shutil.rmtree(f"{self.temp_folder}/.css/", ignore_errors=True)
shutil.rmtree(f"{self.temp_folder}/.versions/", ignore_errors=True)
# if theme is filled
if self.is_filled:
for apply_file in os.listdir(f"{self.temp_folder}/"):
replace_keywords(f"{self.temp_folder}/{apply_file}",
("BUTTON-COLOR", "ACCENT-FILLED-COLOR"),
("BUTTON_HOVER", "ACCENT-FILLED_HOVER"),
("BUTTON_ACTIVE", "ACCENT-FILLED_ACTIVE"),
("BUTTON_INSENSITIVE", "ACCENT-FILLED_INSENSITIVE"),
("BUTTON-TEXT-COLOR", "TEXT-BLACK-COLOR"),
("BUTTON-TEXT_SECONDARY", "TEXT-BLACK_SECONDARY"))
def install(self, hue, name, sat=None, destination=None): def install(self, hue, name, sat=None, destination=None):
""" """
Copy files and generate theme with different accent color Copy files and generate theme with different accent color

View File

@@ -5,11 +5,12 @@
import argparse import argparse
import shutil import shutil
from collections import defaultdict from collections import defaultdict
from typing import Any
from .. import config from .. import config
import os import os
def remove_files(args: argparse.Namespace, colors: dict[str, str]): def remove_files(args: argparse.Namespace, colors: dict[str, Any]):
"""Delete already installed Marble theme""" """Delete already installed Marble theme"""
themes = detect_themes(config.themes_folder) themes = detect_themes(config.themes_folder)