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

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

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

View File

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