mirror of
https://github.com/imarkoff/Marble-shell-theme.git
synced 2025-11-21 13:06:02 -08:00
Merge pull request #52 from imarkoff/unstable
Colored logs, object oriented refactoring.
This commit is contained in:
6
.github/workflows/python-app.yml
vendored
6
.github/workflows/python-app.yml
vendored
@@ -7,11 +7,9 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "gdm"
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
- "gdm"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -23,10 +21,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python 3.10
|
||||
- name: Set up Python 3.13
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: "3.10"
|
||||
python-version: "3.13"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
|
||||
@@ -43,7 +43,7 @@ Icon theme: https://github.com/vinceliuice/Colloid-icon-theme
|
||||
## 🚧 Requirements
|
||||
- GNOME 42-48. Correct functionality on other versions is not guaranteed.
|
||||
- [User Themes](https://extensions.gnome.org/extension/19/user-themes/ "User Themes") extension.
|
||||
- Python 3.2+.
|
||||
- Python 3.9+.
|
||||
|
||||
## 💡 Installation
|
||||
|
||||
|
||||
205
install.py
205
install.py
@@ -14,206 +14,35 @@
|
||||
# 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)
|
||||
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:
|
||||
local_theme(args, colors)
|
||||
if not args.remove:
|
||||
installer.install()
|
||||
|
||||
if args.remove == args.reinstall:
|
||||
apply_gnome_theme()
|
||||
if not args.gdm and not args.remove:
|
||||
apply_gnome_theme()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
finally:
|
||||
shutil.rmtree(config.temp_folder, ignore_errors=True)
|
||||
shutil.rmtree(config.temp_folder, ignore_errors=True)
|
||||
@@ -19,3 +19,5 @@ extracted_gdm_folder = "theme"
|
||||
gnome_shell_css = f"{temp_gnome_folder}/gnome-shell.css"
|
||||
tweak_file = f"./{tweaks_folder}/*/tweak.py"
|
||||
colors_json = "colors.json"
|
||||
|
||||
user_themes_extension = "/org/gnome/shell/extensions/user-theme/name"
|
||||
|
||||
108
scripts/gdm.py
108
scripts/gdm.py
@@ -2,8 +2,10 @@ import os
|
||||
import subprocess
|
||||
|
||||
from .theme import Theme
|
||||
from .utils import label_files, remove_properties, remove_keywords, gnome
|
||||
from .utils import remove_properties, remove_keywords, gnome
|
||||
from . import config
|
||||
from .utils.console import Console, Color, Format
|
||||
from .utils.files_labeler import FilesLabeler
|
||||
|
||||
|
||||
class ThemePrepare:
|
||||
@@ -11,7 +13,7 @@ class ThemePrepare:
|
||||
Theme object prepared for installation
|
||||
"""
|
||||
|
||||
def __init__(self, theme, theme_file, should_label=False):
|
||||
def __init__(self, theme: Theme, theme_file, should_label=False):
|
||||
self.theme = theme
|
||||
self.theme_file = theme_file
|
||||
self.should_label = should_label
|
||||
@@ -38,34 +40,20 @@ class GlobalTheme:
|
||||
|
||||
self.backup_file = f"{self.destination_file}.backup"
|
||||
self.backup_trigger = "\n/* Marble theme */\n" # trigger to check if theme is installed
|
||||
self.extracted_theme = os.path.join(self.temp_folder, config.extracted_gdm_folder)
|
||||
self.extracted_theme: str = os.path.join(self.temp_folder, config.extracted_gdm_folder)
|
||||
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):
|
||||
def __create_theme(self, theme_type: str, mode=None, should_label=False, is_filled=False):
|
||||
"""Helper to create theme objects"""
|
||||
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)
|
||||
|
||||
@@ -82,7 +70,8 @@ class GlobalTheme:
|
||||
"""
|
||||
Extract gresource files to temp folder
|
||||
"""
|
||||
print("Extracting gresource files...")
|
||||
extract_line = Console.Line()
|
||||
extract_line.update("Extracting gresource files...")
|
||||
|
||||
resources = subprocess.getoutput(f"gresource list {self.gst}").split("\n")
|
||||
prefix = "/org/gnome/shell/"
|
||||
@@ -97,6 +86,8 @@ class GlobalTheme:
|
||||
with open(output_path, 'wb') as f:
|
||||
subprocess.run(["gresource", "extract", self.gst, resource], stdout=f, check=True)
|
||||
|
||||
extract_line.success("Extracted gresource files.")
|
||||
|
||||
except FileNotFoundError as e:
|
||||
if "gresource" in str(e):
|
||||
print("Error: 'gresource' command not found.")
|
||||
@@ -117,7 +108,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
|
||||
@@ -125,35 +116,31 @@ class GlobalTheme:
|
||||
:param sat: color saturation
|
||||
"""
|
||||
|
||||
for theme in self.themes:
|
||||
if theme.should_label:
|
||||
label_files(theme.theme.temp_folder, "light", theme.theme.main_styles)
|
||||
for theme_prepare in self.themes:
|
||||
if theme_prepare.should_label:
|
||||
temp_folder = theme_prepare.theme.temp_folder
|
||||
main_styles = theme_prepare.theme.main_styles
|
||||
FilesLabeler(temp_folder, main_styles).append_label("light")
|
||||
|
||||
remove_keywords(theme.theme_file, "!important")
|
||||
remove_properties(theme.theme_file, "background-color", "color", "box-shadow", "border-radius")
|
||||
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.theme)
|
||||
self.__add_gnome_styles(theme_prepare.theme)
|
||||
|
||||
theme.theme.install(hue, color, sat, destination=self.extracted_theme)
|
||||
theme_prepare.theme.install(hue, color, sat, destination=self.extracted_theme)
|
||||
|
||||
|
||||
def __backup(self):
|
||||
"""
|
||||
Backup installed theme
|
||||
"""
|
||||
|
||||
if self.__is_installed():
|
||||
return
|
||||
|
||||
# backup installed theme
|
||||
print("Backing up default theme...")
|
||||
backup_line = Console.Line()
|
||||
|
||||
backup_line.update("Backing up default theme...")
|
||||
subprocess.run(["cp", "-aT", self.gst, f"{self.gst}.backup"], cwd=self.destination_folder, check=True)
|
||||
backup_line.success("Backed up default theme.")
|
||||
|
||||
def __generate_gresource_xml(self):
|
||||
"""
|
||||
Generates.gresource.xml
|
||||
"""
|
||||
|
||||
# list of files to add to gnome-shell-theme.gresource.xml
|
||||
files = [f"<file>{file}</file>" for file in os.listdir(self.extracted_theme)]
|
||||
nl = "\n" # fstring doesn't support newline character
|
||||
@@ -165,6 +152,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 +187,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:
|
||||
@@ -190,21 +195,23 @@ class GlobalTheme:
|
||||
gresource_xml.write(generated_xml)
|
||||
|
||||
# compile gnome-shell-theme.gresource.xml
|
||||
print("Compiling theme...")
|
||||
compile_line = Console.Line()
|
||||
compile_line.update("Compiling gnome-shell theme...")
|
||||
subprocess.run(["glib-compile-resources" , f"{self.destination_file}.xml"],
|
||||
cwd=self.extracted_theme, check=True)
|
||||
compile_line.success("Theme compiled.")
|
||||
|
||||
# backup installed theme
|
||||
self.__backup()
|
||||
|
||||
# install theme
|
||||
print("Installing theme...")
|
||||
install_line = Console.Line()
|
||||
install_line.update("Moving compiled theme to system folder...")
|
||||
subprocess.run(["sudo", "cp", "-f",
|
||||
f"{self.extracted_theme}/{self.destination_file}",
|
||||
f"{self.destination_folder}/{self.destination_file}"],
|
||||
check=True)
|
||||
|
||||
print("Theme installed successfully.")
|
||||
install_line.success("Theme moved to system folder.")
|
||||
|
||||
|
||||
def remove(self):
|
||||
@@ -214,16 +221,19 @@ class GlobalTheme:
|
||||
|
||||
# use backup file if theme is installed
|
||||
if self.__is_installed():
|
||||
print("Theme is installed. Removing...")
|
||||
removing_line = Console.Line()
|
||||
removing_line.update("Theme is installed. Removing...")
|
||||
backup_path = os.path.join(self.destination_folder, self.backup_file)
|
||||
dest_path = os.path.join(self.destination_folder, self.destination_file)
|
||||
|
||||
if os.path.isfile(backup_path):
|
||||
subprocess.run(["sudo", "mv", backup_path, dest_path], check=True)
|
||||
removing_line.success("Global theme removed successfully. Restart GDM to apply changes.")
|
||||
|
||||
else:
|
||||
print("Backup file not found. Try reinstalling gnome-shell package.")
|
||||
formatted_shell = Console.format("gnome-shell", color=Color.BLUE, format_type=Format.BOLD)
|
||||
removing_line.error(f"Backup file not found. Try reinstalling {formatted_shell} package.")
|
||||
|
||||
else:
|
||||
print("Theme is not installed. Nothing to remove.")
|
||||
print("If theme is still installed globally, try reinstalling gnome-shell package.")
|
||||
Console.Line().error("Theme is not installed. Nothing to remove.")
|
||||
Console.Line().update("If theme is still installed globally, try reinstalling gnome-shell package.", icon="⚠️")
|
||||
|
||||
1
scripts/install/__init__.py
Normal file
1
scripts/install/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from scripts.install.arguments_definer import ArgumentsDefiner
|
||||
73
scripts/install/arguments_definer.py
Normal file
73
scripts/install/arguments_definer.py
Normal 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)
|
||||
17
scripts/install/colors_definer.py
Normal file
17
scripts/install/colors_definer.py
Normal 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"]
|
||||
39
scripts/install/global_theme_installer.py
Normal file
39
scripts/install/global_theme_installer.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import os
|
||||
|
||||
from scripts import config
|
||||
from scripts.gdm import GlobalTheme
|
||||
from scripts.install.theme_installer import ThemeInstaller
|
||||
from scripts.utils.console import Console, Color, Format
|
||||
|
||||
|
||||
class GlobalThemeInstaller(ThemeInstaller):
|
||||
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.install(hue, sat)
|
||||
|
||||
def _apply_tweaks_to_theme(self):
|
||||
for theme in self.theme.themes:
|
||||
self._apply_tweaks(theme.theme)
|
||||
|
||||
def _after_install(self):
|
||||
print()
|
||||
Console.Line().update(
|
||||
Console.format("GDM theme installed successfully.", color=Color.GREEN, format_type=Format.BOLD),
|
||||
icon="🥳"
|
||||
)
|
||||
Console.Line().update("You need to restart GDM to apply changes.", icon="ℹ️ ")
|
||||
|
||||
formatted_command = Console.format("systemctl restart gdm.service", color=Color.YELLOW, format_type=Format.BOLD)
|
||||
Console.Line().update(f"Run {formatted_command} to restart GDM.", icon="🔄")
|
||||
32
scripts/install/local_theme_installer.py
Normal file
32
scripts/install/local_theme_installer.py
Normal file
@@ -0,0 +1,32 @@
|
||||
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
|
||||
from scripts.utils.console import Console, Color, Format
|
||||
|
||||
|
||||
class LocalThemeInstaller(ThemeInstaller):
|
||||
theme: Theme
|
||||
|
||||
def remove(self):
|
||||
colors = self.colors.colors
|
||||
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)
|
||||
|
||||
def _apply_tweaks_to_theme(self):
|
||||
self._apply_tweaks(self.theme)
|
||||
|
||||
def _after_install(self):
|
||||
print()
|
||||
formatted_output = Console.format("Theme installed successfully.", color=Color.GREEN, format_type=Format.BOLD)
|
||||
Console.Line().update(formatted_output, icon="🥳")
|
||||
99
scripts/install/theme_installer.py
Normal file
99
scripts/install/theme_installer.py
Normal file
@@ -0,0 +1,99 @@
|
||||
import argparse
|
||||
import concurrent.futures
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from scripts.install.colors_definer import ColorsDefiner
|
||||
from scripts.theme import Theme
|
||||
from scripts.tweaks_manager import TweaksManager
|
||||
|
||||
|
||||
class ThemeInstaller(ABC):
|
||||
"""Base class for theme installers"""
|
||||
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()
|
||||
|
||||
@abstractmethod
|
||||
def remove(self):
|
||||
"""Method for removing already installed themes"""
|
||||
pass
|
||||
|
||||
def install(self):
|
||||
self.theme.prepare()
|
||||
self._apply_tweaks_to_theme()
|
||||
self._apply_colors()
|
||||
self._after_install()
|
||||
|
||||
@abstractmethod
|
||||
def _define_theme(self):
|
||||
"""Here is the place to define the theme object"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _apply_tweaks_to_theme(self):
|
||||
"""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"""
|
||||
pass
|
||||
|
||||
def _apply_tweaks(self, theme):
|
||||
"""This method should be called in the _apply_tweaks_to_theme method"""
|
||||
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
|
||||
|
||||
colors_to_install = []
|
||||
for color, values in colors.items():
|
||||
if args.all or getattr(args, color, False):
|
||||
hue = values.get('h')
|
||||
sat = values.get('s', args.sat)
|
||||
colors_to_install.append((hue, color, sat))
|
||||
|
||||
if self.stop_after_first_installed_color:
|
||||
break
|
||||
|
||||
if not colors_to_install:
|
||||
return False
|
||||
self._run_concurrent_installation(colors_to_install)
|
||||
return True
|
||||
|
||||
def _run_concurrent_installation(self, colors_to_install):
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
futures = [executor.submit(self._install_theme, hue, color, sat)
|
||||
for hue, color, sat in colors_to_install]
|
||||
concurrent.futures.wait(futures)
|
||||
175
scripts/theme.py
175
scripts/theme.py
@@ -2,11 +2,13 @@ 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:
|
||||
@@ -23,31 +25,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.modes = [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):
|
||||
"""
|
||||
@@ -74,9 +59,81 @@ class Theme:
|
||||
|
||||
return self
|
||||
|
||||
def __del__(self):
|
||||
# delete temp folder
|
||||
shutil.rmtree(self.temp_folder, ignore_errors=True)
|
||||
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):
|
||||
"""
|
||||
@@ -93,18 +150,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)]
|
||||
@@ -112,56 +169,4 @@ class Theme:
|
||||
replaced_colors.append((element, f"rgba({red}, {green}, {blue}, {alpha})"))
|
||||
|
||||
# replace colors
|
||||
replace_keywords(os.path.expanduser(f"{destination}/{apply_file}"), *replaced_colors)
|
||||
|
||||
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 install(self, hue, name, sat=None, destination=None):
|
||||
"""
|
||||
Copy files and generate theme with different accent color
|
||||
:param hue
|
||||
:param name: theme name
|
||||
:param sat: color saturation (optional)
|
||||
:param destination: folder where theme will be installed
|
||||
"""
|
||||
|
||||
is_dest = bool(destination)
|
||||
|
||||
print(f"Creating {name} {', '.join(self.mode)} theme...", end=" ")
|
||||
|
||||
try:
|
||||
for mode in self.mode:
|
||||
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)
|
||||
|
||||
except Exception as err:
|
||||
print("\nError: " + str(err))
|
||||
|
||||
else:
|
||||
print("Done.")
|
||||
|
||||
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)
|
||||
replace_keywords(os.path.expanduser(f"{destination}/{apply_file}"), *replaced_colors)
|
||||
@@ -3,7 +3,6 @@ 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 .label_files import label_files
|
||||
from .remove_files import remove_files
|
||||
from .remove_keywords import remove_keywords
|
||||
from .remove_properties import remove_properties
|
||||
|
||||
95
scripts/utils/console.py
Normal file
95
scripts/utils/console.py
Normal file
@@ -0,0 +1,95 @@
|
||||
import sys
|
||||
import threading
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Console:
|
||||
"""Manages console output for concurrent processes with line tracking"""
|
||||
_print_lock = threading.Lock()
|
||||
_line_mapping = {}
|
||||
_next_line = 0
|
||||
|
||||
class Line:
|
||||
def __init__(self, name: Optional[str]=None):
|
||||
"""Initialize a new managed line"""
|
||||
self.name = name or f"line_{Console._next_line}"
|
||||
self._reserve_line()
|
||||
|
||||
def update(self, message, icon="⏳"):
|
||||
"""Update the status message for the line"""
|
||||
with Console._print_lock:
|
||||
# Calculate how many lines to move up
|
||||
lines_up = Console._next_line - Console._line_mapping[self.name]
|
||||
# Move the cursor to correct line
|
||||
if lines_up > 0:
|
||||
sys.stdout.write(f"\033[{lines_up}F")
|
||||
# Clear line and write status
|
||||
if icon.strip() == "":
|
||||
sys.stdout.write(f"\r\033[K{message}")
|
||||
else:
|
||||
sys.stdout.write(f"\r\033[K{icon} {message}")
|
||||
# Move the cursor back down
|
||||
if lines_up > 0:
|
||||
sys.stdout.write(f"\033[{lines_up}E")
|
||||
sys.stdout.flush()
|
||||
|
||||
def success(self, message):
|
||||
self.update(message, "✅")
|
||||
|
||||
def error(self, message):
|
||||
self.update(message, "❌")
|
||||
|
||||
def warn(self, message):
|
||||
self.update(message, "⚠️")
|
||||
|
||||
def _reserve_line(self):
|
||||
"""Reserve a line for future updates"""
|
||||
with Console._print_lock:
|
||||
line_number = Console._next_line
|
||||
Console._next_line += 1
|
||||
Console._line_mapping[self.name] = line_number
|
||||
sys.stdout.write(f"\n") # Ensure we have a new line
|
||||
sys.stdout.flush()
|
||||
return line_number
|
||||
|
||||
@staticmethod
|
||||
def format(text: str, color: Optional['Color']=None, format_type: Optional['Format']=None):
|
||||
"""Apply color and formatting to text"""
|
||||
formatted_text = text
|
||||
|
||||
if color:
|
||||
formatted_text = color.value + formatted_text + Color.NORMAL.value
|
||||
if format_type:
|
||||
formatted_text = format_type.value + formatted_text + Format.NORMAL.value
|
||||
|
||||
return formatted_text
|
||||
|
||||
|
||||
class Color(Enum):
|
||||
"""ANSI color codes for terminal output"""
|
||||
NORMAL = '\033[0m' # Reset color
|
||||
BLACK = '\033[30m'
|
||||
RED = '\033[31m'
|
||||
GREEN = '\033[32m'
|
||||
YELLOW = '\033[33m'
|
||||
BLUE = '\033[34m'
|
||||
MAGENTA = '\033[35m'
|
||||
PURPLE = '\033[35m'
|
||||
CYAN = '\033[36m'
|
||||
WHITE = '\033[37m'
|
||||
GRAY = '\033[90m'
|
||||
|
||||
@classmethod
|
||||
def get(cls, color: str, default: Optional['Color']=None) -> Optional['Color']:
|
||||
return getattr(cls, color.upper(), default)
|
||||
|
||||
|
||||
class Format(Enum):
|
||||
"""ANSI formatting codes for terminal output"""
|
||||
NORMAL = '\033[0m' # Reset formatting
|
||||
BOLD = '\033[1m'
|
||||
ITALIC = '\033[3m'
|
||||
UNDERLINE = '\033[4m'
|
||||
BLINK = '\033[5m'
|
||||
REVERSE = '\033[7m'
|
||||
@@ -1,4 +1,6 @@
|
||||
import os
|
||||
import shutil
|
||||
|
||||
|
||||
def copy_files(source, destination):
|
||||
"""
|
||||
@@ -7,6 +9,7 @@ def copy_files(source, destination):
|
||||
:param destination: where files will be pasted
|
||||
"""
|
||||
|
||||
destination = os.path.expanduser(destination) # expand ~ to /home/user
|
||||
destination = os.path.expanduser(destination)
|
||||
os.makedirs(destination, exist_ok=True)
|
||||
os.system(f"cp -aT {source} {destination}")
|
||||
|
||||
shutil.copytree(source, destination, dirs_exist_ok=True)
|
||||
|
||||
52
scripts/utils/files_labeler.py
Normal file
52
scripts/utils/files_labeler.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import os
|
||||
from typing import Tuple
|
||||
|
||||
type LabeledFileGroup = Tuple[str, str]
|
||||
|
||||
class FilesLabeler:
|
||||
def __init__(self, directory: str, *args: str):
|
||||
"""
|
||||
Initialize the working directory and files to change
|
||||
"""
|
||||
self.directory = directory
|
||||
self.files = args
|
||||
|
||||
def append_label(self, label: str):
|
||||
"""
|
||||
Append a label to all files in the directory
|
||||
and update references in the files
|
||||
"""
|
||||
labeled_files = self._label_files(label)
|
||||
self._update_references(labeled_files)
|
||||
|
||||
def _label_files(self, label: str) -> list[LabeledFileGroup]:
|
||||
labeled_files = []
|
||||
for filename in os.listdir(self.directory):
|
||||
if label in filename: continue
|
||||
|
||||
name, extension = os.path.splitext(filename)
|
||||
new_filename = f"{name}-{label}{extension}"
|
||||
|
||||
old_filepath = os.path.join(self.directory, filename)
|
||||
new_filepath = os.path.join(self.directory, new_filename)
|
||||
os.rename(old_filepath, new_filepath)
|
||||
|
||||
labeled_files.append((filename, new_filename))
|
||||
return labeled_files
|
||||
|
||||
def _update_references(self, labeled_files: list[LabeledFileGroup]):
|
||||
for file_path in self.files:
|
||||
with open(file_path, 'r') as file:
|
||||
file_content = file.read()
|
||||
|
||||
file_content = self._update_references_in_file(file_content, labeled_files)
|
||||
|
||||
with open(file_path, 'w') as file:
|
||||
file.write(file_content)
|
||||
|
||||
@staticmethod
|
||||
def _update_references_in_file(file_content: str, labeled_files: list[LabeledFileGroup]) -> str:
|
||||
replaced_content = file_content
|
||||
for old_name, new_name in labeled_files:
|
||||
replaced_content = replaced_content.replace(old_name, new_name)
|
||||
return replaced_content
|
||||
@@ -1,10 +1,15 @@
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from scripts import config
|
||||
from scripts.utils.console import Console, Format, Color
|
||||
from scripts.utils.parse_folder import parse_folder
|
||||
|
||||
|
||||
def gnome_version() -> str | None:
|
||||
"""
|
||||
Get gnome-shell version
|
||||
"""
|
||||
|
||||
try:
|
||||
output = subprocess.check_output(['gnome-shell', '--version'], text=True).strip()
|
||||
return output.split(' ')[2]
|
||||
@@ -13,21 +18,40 @@ def gnome_version() -> str | None:
|
||||
|
||||
def apply_gnome_theme(theme=None) -> bool:
|
||||
"""
|
||||
Apply gnome-shell theme
|
||||
:param theme: theme name
|
||||
Applies the theme in user theme extension if it is Marble and extension installed.
|
||||
"""
|
||||
|
||||
try:
|
||||
if theme is None:
|
||||
current_theme = subprocess.check_output(['dconf', 'read', '/org/gnome/shell/extensions/user-theme/name'], text=True).strip().strip("'")
|
||||
if current_theme.startswith("Marble"):
|
||||
theme = current_theme
|
||||
else:
|
||||
return False
|
||||
theme = get_current_theme()
|
||||
|
||||
subprocess.run(['dconf', 'reset', '/org/gnome/shell/extensions/user-theme/name'], check=True)
|
||||
subprocess.run(['dconf', 'write', '/org/gnome/shell/extensions/user-theme/name', f"'{theme}'"], check=True)
|
||||
print(f"Theme '{theme}' applied.")
|
||||
except subprocess.CalledProcessError:
|
||||
line = Console.Line("apply_gnome_theme")
|
||||
(color, _) = parse_folder(theme)
|
||||
formatted_theme = Console.format(theme, color=Color.get(color, Color.GRAY), format_type=Format.BOLD)
|
||||
|
||||
line.update(f"Applying {formatted_theme} theme...")
|
||||
time.sleep(0.025) # applying the theme may freeze, so we need to wait a bit
|
||||
apply_user_theme(theme)
|
||||
line.success(f"Theme {formatted_theme} applied.")
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
return True
|
||||
|
||||
|
||||
def get_current_theme() -> str:
|
||||
"""
|
||||
Throws an error if theme is not Marble.
|
||||
"""
|
||||
try:
|
||||
output = subprocess.check_output(['dconf', 'read', config.user_themes_extension], text=True)
|
||||
output = output.strip().strip("'")
|
||||
|
||||
if not output.startswith("Marble"):
|
||||
raise Exception(f"Theme {output} doesn't appear to be a Marble theme")
|
||||
return output
|
||||
except subprocess.CalledProcessError:
|
||||
raise Exception("User theme extension not found.")
|
||||
|
||||
|
||||
def apply_user_theme(theme_name: str):
|
||||
subprocess.run(['dconf', 'reset', config.user_themes_extension], check=True)
|
||||
subprocess.run(['dconf', 'write', config.user_themes_extension, f"'{theme_name}'"], check=True)
|
||||
@@ -1,44 +0,0 @@
|
||||
import os
|
||||
|
||||
def label_files(directory, label, *args):
|
||||
"""
|
||||
Add a label to all files in a directory
|
||||
:param directory: folder where files are located
|
||||
:param label: label to add
|
||||
:param args: files to change links to labeled files
|
||||
:return:
|
||||
"""
|
||||
|
||||
# Open all files
|
||||
files = [open(file, 'r') for file in args]
|
||||
read_files = []
|
||||
|
||||
filenames = []
|
||||
|
||||
for filename in os.listdir(directory):
|
||||
# Skip if the file is already labeled
|
||||
if label in filename:
|
||||
continue
|
||||
|
||||
# Split the filename into name and extension
|
||||
name, extension = os.path.splitext(filename)
|
||||
|
||||
# Form the new filename and rename the file
|
||||
new_filename = f"{name}-{label}{extension}"
|
||||
os.rename(os.path.join(directory, filename), os.path.join(directory, new_filename))
|
||||
|
||||
filenames.append((filename, new_filename))
|
||||
|
||||
# Replace the filename in all files
|
||||
for i, file in enumerate(files):
|
||||
read_file = file.read()
|
||||
read_file.replace(filenames[i][0], filenames[i][1])
|
||||
read_files.append(read_file)
|
||||
file.close()
|
||||
|
||||
write_files = [open(file, 'w') for file in args]
|
||||
|
||||
# Write the changes to the files and close them
|
||||
for i, file in enumerate(write_files):
|
||||
file.write(read_files[i])
|
||||
file.close()
|
||||
11
scripts/utils/parse_folder.py
Normal file
11
scripts/utils/parse_folder.py
Normal file
@@ -0,0 +1,11 @@
|
||||
def parse_folder(folder: str) -> tuple[str, str] | None:
|
||||
"""Parse a folder name into color and mode"""
|
||||
folder_arr = folder.split("-")
|
||||
|
||||
if len(folder_arr) < 2 or folder_arr[0] != "Marble":
|
||||
return None
|
||||
|
||||
color = "-".join(folder_arr[1:-1])
|
||||
mode = folder_arr[-1]
|
||||
|
||||
return color, mode
|
||||
@@ -5,11 +5,14 @@
|
||||
import argparse
|
||||
import shutil
|
||||
from collections import defaultdict
|
||||
from typing import Any
|
||||
|
||||
from .console import Console, Color, Format
|
||||
from .parse_folder import parse_folder
|
||||
from .. import config
|
||||
import os
|
||||
|
||||
def remove_files(args: argparse.Namespace, colors: dict[str, str]):
|
||||
def remove_files(args: argparse.Namespace, formatted_colors: dict[str, Any]):
|
||||
"""Delete already installed Marble theme"""
|
||||
themes = detect_themes(config.themes_folder)
|
||||
|
||||
@@ -19,23 +22,26 @@ def remove_files(args: argparse.Namespace, colors: dict[str, str]):
|
||||
filtered_themes = themes
|
||||
if not args.all:
|
||||
args_dict = vars(args)
|
||||
arguments = [color for color in colors.keys() if args_dict.get(color)]
|
||||
arguments = [color for color in formatted_colors.keys() if args_dict.get(color)]
|
||||
filtered_themes = themes.filter(arguments)
|
||||
|
||||
if not filtered_themes:
|
||||
print("No matching themes found.")
|
||||
Console.Line().error("No matching themes found.")
|
||||
return
|
||||
|
||||
colors = [color for (color, modes) in filtered_themes]
|
||||
print(f"The following themes will be deleted: {', '.join(colors)}.")
|
||||
formatted_colors = [
|
||||
Console.format(color, color=Color.get(color), format_type=Format.BOLD)
|
||||
for (color, modes) in filtered_themes
|
||||
]
|
||||
Console.Line().warn(f"The following themes will be deleted: {', '.join(formatted_colors)}.")
|
||||
if args.mode:
|
||||
print(f"Theme modes to be deleted: {args.mode}.")
|
||||
Console.Line().warn(f"Theme modes to be deleted: {args.mode}.")
|
||||
|
||||
if input(f"Proceed? (y/N) ").lower() == "y":
|
||||
if proceed_input().lower() == "y":
|
||||
filtered_themes.remove(args.mode)
|
||||
print("Themes deleted successfully.")
|
||||
Console.Line().success("Themes deleted successfully.")
|
||||
else:
|
||||
print("Operation cancelled.")
|
||||
Console.Line().error("Operation cancelled.")
|
||||
|
||||
|
||||
def detect_themes(path: str) -> 'Themes':
|
||||
@@ -58,41 +64,12 @@ def detect_themes(path: str) -> 'Themes':
|
||||
return themes
|
||||
|
||||
|
||||
def parse_folder(folder: str) -> tuple[str, str] | None:
|
||||
"""Parse a folder name into color and mode"""
|
||||
folder_arr = folder.split("-")
|
||||
|
||||
if len(folder_arr) < 2 or folder_arr[0] != "Marble":
|
||||
return None
|
||||
|
||||
color = "-".join(folder_arr[1:-1])
|
||||
mode = folder_arr[-1]
|
||||
|
||||
return color, mode
|
||||
|
||||
|
||||
class ThemeMode:
|
||||
"""Concrete theme with mode and path"""
|
||||
mode: str
|
||||
path: str
|
||||
|
||||
def __init__(self, mode: str, path: str):
|
||||
self.mode = mode
|
||||
self.path = path
|
||||
|
||||
def remove(self):
|
||||
try:
|
||||
shutil.rmtree(self.path)
|
||||
except Exception as e:
|
||||
print(f"Error deleting {self.path}: {e}")
|
||||
|
||||
|
||||
class Themes:
|
||||
"""Collection of themes grouped by color"""
|
||||
def __init__(self):
|
||||
self.by_color: dict[str, list[ThemeMode]] = defaultdict(list) # color: list[ThemeMode]
|
||||
|
||||
def add_theme(self, color: str, theme_mode: ThemeMode):
|
||||
def add_theme(self, color: str, theme_mode: 'ThemeMode'):
|
||||
self.by_color[color].append(theme_mode)
|
||||
|
||||
def filter(self, colors: list[str]):
|
||||
@@ -120,3 +97,26 @@ class Themes:
|
||||
def __iter__(self):
|
||||
for color, modes in self.by_color.items():
|
||||
yield color, modes
|
||||
|
||||
|
||||
class ThemeMode:
|
||||
"""Concrete theme with mode and path"""
|
||||
mode: str
|
||||
path: str
|
||||
|
||||
def __init__(self, mode: str, path: str):
|
||||
self.mode = mode
|
||||
self.path = path
|
||||
|
||||
def remove(self):
|
||||
try:
|
||||
shutil.rmtree(self.path)
|
||||
except Exception as e:
|
||||
print(f"Error deleting {self.path}: {e}")
|
||||
|
||||
|
||||
def proceed_input():
|
||||
formatted_agree = Console.format("y", color=Color.GREEN, format_type=Format.BOLD)
|
||||
formatted_disagree = Console.format("N", color=Color.RED, format_type=Format.BOLD)
|
||||
formatted_proceed = Console.format("Proceed?", format_type=Format.BOLD)
|
||||
return input(f"{formatted_proceed} ({formatted_agree}/{formatted_disagree}) ")
|
||||
@@ -101,7 +101,7 @@
|
||||
/* that's much better than adding "margin: $base_padding * 0.5;" to .message-close-button */
|
||||
.message-close-button { margin: 0; }
|
||||
.message-header:ltr > :last-child { margin-right: 2px; }
|
||||
.messahe-header:rtl > :first-child { margin-left: 2px; }
|
||||
.message-header:rtl > :first-child { margin-left: 2px; }
|
||||
|
||||
/* close button, expand button (46+) */
|
||||
.message-close-button,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
from scripts.install.colors_definer import ColorsDefiner
|
||||
|
||||
|
||||
def define_arguments(parser):
|
||||
color_args = parser.add_argument_group('Color tweaks')
|
||||
color_args.add_argument('-O', '--opaque', action='store_true', help='make the background in menus/popovers opaque')
|
||||
|
||||
|
||||
def apply_tweak(args, theme, colors):
|
||||
def apply_tweak(args, theme, colors: ColorsDefiner):
|
||||
if args.opaque:
|
||||
colors["elements"]["BACKGROUND-COLOR"]["light"]["a"] = 1
|
||||
colors["elements"]["BACKGROUND-COLOR"]["dark"]["a"] = 1
|
||||
colors.replacers["BACKGROUND-COLOR"]["light"]["a"] = 1
|
||||
colors.replacers["BACKGROUND-COLOR"]["dark"]["a"] = 1
|
||||
|
||||
Reference in New Issue
Block a user