Add ability to set theme globally

This commit is contained in:
Vladyslav Hroshev
2023-12-10 22:47:34 +02:00
parent bdd74f94bf
commit 253913ae40
5 changed files with 322 additions and 46 deletions

View File

@@ -18,6 +18,7 @@
import json # working with json files import json # working with json files
import os # system commands, working with files import os # system commands, working with files
import argparse # command-line options import argparse # command-line options
import shutil
import textwrap # example text in argparse import textwrap # example text in argparse
from scripts import config # folder and files definitions from scripts import config # folder and files definitions
@@ -27,6 +28,7 @@ from scripts.utils import (
hex_to_rgba) # convert HEX to RGBA hex_to_rgba) # convert HEX to RGBA
from scripts.theme import Theme from scripts.theme import Theme
from scripts.gdm import GlobalTheme
def main(): def main():
@@ -70,6 +72,10 @@ def main():
color_tweaks.add_argument('--sat', type=int, choices=range(0, 251), color_tweaks.add_argument('--sat', type=int, choices=range(0, 251),
help='custom color saturation (<100%% - reduce, >100%% - increase)', metavar='(0 - 250)%') 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.')
panel_args = parser.add_argument_group('Panel tweaks') panel_args = parser.add_argument_group('Panel tweaks')
panel_args.add_argument('-Pds', '--panel_default_size', action='store_true', help='set default panel size') panel_args.add_argument('-Pds', '--panel_default_size', action='store_true', help='set default panel size')
panel_args.add_argument('-Pnp', '--panel_no_pill', action='store_true', help='remove panel button background') panel_args.add_argument('-Pnp', '--panel_no_pill', action='store_true', help='remove panel button background')
@@ -82,69 +88,106 @@ def main():
colors = json.load(open(config.colors_json)) colors = json.load(open(config.colors_json))
gnome_shell_theme = Theme("gnome-shell", colors, f"{config.raw_theme_folder}/{config.gnome_folder}", if args.gdm:
config.themes_folder, config.temp_folder, gdm_status = 1
mode=args.mode, is_filled=args.filled) if os.geteuid() != 0:
print("You must run this script as root to install GDM theme.")
return 1
# remove marble theme if args.all:
if args.remove: print("Error: You can't install all colors for GDM theme. Use specific color.")
remove_files() return 1
# panel tweaks gdm_theme = GlobalTheme(colors, f"{config.raw_theme_folder}/{config.gnome_folder}",
if args.panel_default_size: config.global_gnome_shell_theme, config.gnome_shell_gresource,
with open(f"{config.tweaks_folder}/panel/def-size.css", "r") as f: config.temp_folder, is_filled=args.filled)
gnome_shell_theme += f.read()
if args.panel_no_pill: if args.red or args.pink or args.purple or args.blue or args.green or args.yellow or args.gray:
with open(f"{config.tweaks_folder}/panel/no-pill.css", "r") as f: for color in colors["colors"]:
gnome_shell_theme += f.read() if getattr(args, color):
hue = colors["colors"][color]["h"]
sat = colors["colors"][color]["s"] if colors["colors"][color]["s"] is not None else args.sat
if args.panel_text_color: gdm_status = gdm_theme.install(hue, sat)
gnome_shell_theme += ".panel-button,\
.clock,\
.clock-display StIcon {\
color: rgba(" + ', '.join(map(str, hex_to_rgba(args.panel_text_color))) + ");\
}"
# dock tweaks elif args.hue:
if args.launchpad: hue = args.hue
with open(f"{config.tweaks_folder}/launchpad/launchpad.css", "r") as f:
gnome_shell_theme += f.read()
gnome_shell_theme *= f"{config.tweaks_folder}/launchpad/launchpad.png" gdm_status = gdm_theme.install(hue, args.sat)
# what argument colors defined else:
if args.all: print('No color arguments specified. Use -h or --help to see the available options.')
# install hue colors listed in colors.json
for color in colors["colors"]:
hue = colors["colors"][color]["h"]
# if saturation already defined in color (gray)
sat = colors["colors"][color]["s"] if colors["colors"][color]["s"] is not None else args.sat
gnome_shell_theme.install(hue, color, sat) if gdm_status == 0:
print("\nGDM theme installed successfully.")
print("You need to restart gdm.service to apply changes.")
print("Run \"systemctl restart gdm.service\" to restart GDM.")
elif args.red or args.pink or args.purple or args.blue or args.green or args.yellow or args.gray: # if not GDM theme
# install selected colors else:
for color in colors["colors"]: # remove marble theme
if getattr(args, color): # if argument name is in defined colors if args.remove:
remove_files()
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)
# panel tweaks
if args.panel_default_size:
with open(f"{config.tweaks_folder}/panel/def-size.css", "r") as f:
gnome_shell_theme += f.read()
if args.panel_no_pill:
with open(f"{config.tweaks_folder}/panel/no-pill.css", "r") as f:
gnome_shell_theme += f.read()
if args.panel_text_color:
gnome_shell_theme += ".panel-button,\
.clock,\
.clock-display StIcon {\
color: rgba(" + ', '.join(map(str, hex_to_rgba(args.panel_text_color))) + ");\
}"
# dock tweaks
if args.launchpad:
with open(f"{config.tweaks_folder}/launchpad/launchpad.css", "r") as f:
gnome_shell_theme += f.read()
gnome_shell_theme *= f"{config.tweaks_folder}/launchpad/launchpad.png"
# what argument colors defined
if args.all:
# install hue colors listed in colors.json
for color in colors["colors"]:
hue = colors["colors"][color]["h"] hue = colors["colors"][color]["h"]
# if saturation already defined in color (gray) # if saturation already defined in color (gray)
sat = colors["colors"][color]["s"] if colors["colors"][color]["s"] is not None else args.sat sat = colors["colors"][color]["s"] if colors["colors"][color]["s"] is not None else args.sat
gnome_shell_theme.install(hue, color, sat) gnome_shell_theme.install(hue, color, sat)
# custom color elif args.red or args.pink or args.purple or args.blue or args.green or args.yellow or args.gray:
elif args.hue: # install selected colors
hue = args.hue for color in colors["colors"]:
theme_name = args.name if args.name else f'hue{hue}' # if defined name if getattr(args, color): # if argument name is in defined colors
hue = colors["colors"][color]["h"]
# if saturation already defined in color (gray)
sat = colors["colors"][color]["s"] if colors["colors"][color]["s"] is not None else args.sat
gnome_shell_theme.install(hue, theme_name, args.sat) gnome_shell_theme.install(hue, color, sat)
else: # custom color
print('No arguments or no color arguments specified. Use -h or --help to see the available options.') elif args.hue:
hue = args.hue
theme_name = args.name if args.name else f'hue{hue}' # if defined name
gnome_shell_theme.install(hue, theme_name, args.sat)
else:
print('No arguments or no color arguments specified. Use -h or --help to see the available options.')
if __name__ == "__main__": if __name__ == "__main__":
main() main()
os.system(f"rm -r {config.temp_folder}") shutil.rmtree(config.temp_folder, ignore_errors=True)

View File

@@ -7,6 +7,11 @@ themes_folder = "~/.themes"
raw_theme_folder = "theme" raw_theme_folder = "theme"
scripts_folder = "scripts" scripts_folder = "scripts"
# GDM definitions
global_gnome_shell_theme = "/usr/share/gnome-shell"
gnome_shell_gresource = "gnome-shell-theme.gresource"
extracted_gdm_folder = "theme"
# files definitions # files definitions
gnome_shell_css = f"{temp_gnome_folder}/gnome-shell.css" gnome_shell_css = f"{temp_gnome_folder}/gnome-shell.css"
colors_json = "colors.json" colors_json = "colors.json"

179
scripts/gdm.py Normal file
View File

@@ -0,0 +1,179 @@
import os
import subprocess
import shutil
from .theme import Theme
from .utils import label_files
from . import config
class GlobalTheme:
def __init__(self, colors_json, theme_folder, destination_folder, destination_file, temp_folder,
is_filled=False):
"""
Initialize GlobalTheme class
:param colors_json: location of a json file with colors
:param theme_folder: raw theme location
:param destination_folder: folder where themes will be installed
:param temp_folder: folder where files will be collected
:param is_filled: if True, theme will be filled
"""
self.colors_json = colors_json
self.theme_folder = theme_folder
self.destination_folder = destination_folder
self.destination_file = destination_file
self.temp_folder = f"{temp_folder}/gdm"
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 = f"{self.temp_folder}/{config.extracted_gdm_folder}"
os.makedirs(self.temp_folder, exist_ok=True) # create temp folder
# create light and dark themes
self.light_theme = Theme("gnome-shell-light", self.colors_json, self.theme_folder,
self.extracted_theme, self.temp_folder, mode='light', is_filled=is_filled)
self.dark_theme = Theme("gnome-shell-dark", self.colors_json, self.theme_folder,
self.extracted_theme, self.temp_folder, mode='dark', is_filled=is_filled)
def __del__(self):
"""
Delete temp folder
"""
del self.light_theme
del self.dark_theme
shutil.rmtree(self.temp_folder)
def __is_installed(self):
"""
Check if theme is installed
:return: True if theme is installed, False otherwise
"""
with open(f"{self.destination_folder}/{self.destination_file}", "rb") as f:
content = f.read()
return self.backup_trigger.encode() in content
def __extract(self):
"""
Extract gresource files to temp folder
"""
print("Extracting gresource files...")
gst = self.gst
workdir = self.temp_folder
# Get the list of resources
resources = subprocess.getoutput(f"gresource list {gst}").split("\n")
# Create directories
for r in resources:
r = r.replace("/org/gnome/shell/", "")
directory = os.path.join(workdir, os.path.dirname(r))
os.makedirs(directory, exist_ok=True)
# Extract resources
for r in resources:
output_path = os.path.join(workdir, r.replace("/org/gnome/shell/", ""))
subprocess.run(f"gresource extract {gst} {r} > {output_path}", shell=True)
def __add_gnome_styles(self, theme):
"""
Add gnome styles to the start of the file
:param theme: Theme object
"""
with open(f"{self.extracted_theme}/{theme.theme_type}.css", 'r') as gnome_theme:
gnome_styles = gnome_theme.read() + self.backup_trigger
theme.add_to_start(gnome_styles)
def __prepare(self, hue, color, sat=None):
"""
Generate theme files for gnome-shell-theme.gresource.xml
:param hue: color hue
:param color: color name
:param sat: color saturation
"""
# add -light label to light theme files because they are installed to the same folder
label_files(self.light_theme.temp_folder, "light", self.light_theme.main_styles)
# add gnome styles to the start of the file
self.__add_gnome_styles(self.light_theme)
self.__add_gnome_styles(self.dark_theme)
# build code for gnome-shell-theme.gresource.xml
self.light_theme.install(hue, color, sat, destination=self.extracted_theme)
self.dark_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...")
os.system(f"cp -aT {self.gst} {self.gst}.backup")
def __generte_gresource_xml(self):
"""
Generates.gresource.xml
"""
# list of files to add to gnome-shell-theme.gresource.xml
files = list(f"<file>{file}</file>" for file in os.listdir(self.extracted_theme))
nl = "\n" # fstring doesn't support newline character
ready_xml = f"""<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/org/gnome/shell/theme">
{nl.join(files)}
</gresource>
</gresources>"""
return ready_xml
def install(self, hue, sat=None):
"""
Install theme globally
:param hue: color hue
:param sat: color saturation
"""
# use backup file if theme is installed
self.gst = f"{self.destination_folder}/{self.destination_file}"
if self.__is_installed():
print("Theme is installed. Reinstalling...")
self.gst += ".backup"
self.__extract()
# generate theme files for global theme
self.__prepare(hue, 'Marble', sat)
# generate gnome-shell-theme.gresource.xml
with open(f"{self.extracted_theme}/{self.destination_file}.xml", 'w') as gresource_xml:
generated_xml = self.__generte_gresource_xml()
gresource_xml.write(generated_xml)
# compile gnome-shell-theme.gresource.xml
print("Compiling theme...")
subprocess.run(f"glib-compile-resources {self.destination_file}.xml",
shell=True, cwd=self.extracted_theme)
# backup installed theme
self.__backup()
# install theme
print("Installing theme...")
os.system(f"sudo mv {self.extracted_theme}/{self.destination_file} "
f"{self.destination_folder}/{self.destination_file}")
return 0

View File

@@ -123,19 +123,23 @@ 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 install(self, hue, name, sat=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
:param hue :param hue
:param name: theme name :param name: theme name
:param sat: color saturation (optional) :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=" ") print(f"Creating {name} {', '.join(self.mode)} theme...", end=" ")
try: try:
for mode in self.mode: for mode in self.mode:
destination = destination_return(self.destination_folder, name, mode, self.theme_type) if not is_dest:
destination = destination_return(self.destination_folder, name, mode, self.theme_type)
copy_files(self.temp_folder + '/', destination) copy_files(self.temp_folder + '/', destination)
self.__apply_theme(hue, self.temp_folder, destination, mode, sat=sat) self.__apply_theme(hue, self.temp_folder, destination, mode, sat=sat)
@@ -145,3 +149,12 @@ class Theme:
else: else:
print("Done.") 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_styles.write(content + '\n' + main_styles.read())

View File

@@ -145,3 +145,39 @@ def hex_to_rgba(hex_color):
int(hex_color[2:4], 16), \ int(hex_color[2:4], 16), \
int(hex_color[4:6], 16), \ int(hex_color[4:6], 16), \
int(hex_color[6:8], 16) / 255 int(hex_color[6:8], 16) / 255
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 = [file.read() for file in files]
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))
# Replace the filename in all files
for file in read_files:
file.replace(filename, new_filename)
# Write the changes to the files and close them
for i, file in enumerate(files):
file.seek(0)
file.write(read_files[i])
file.close()