Introduce a command-line interface (#675)

This PR introduces a CLI (command-line interface) for Gradience.
This commit is contained in:
tfuxu 2022-12-21 00:15:36 +01:00 committed by GitHub
commit b5a4dc07fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 1527 additions and 484 deletions

View file

@ -14,6 +14,7 @@
"--filesystem=xdg-config/gtk-3.0",
"--filesystem=xdg-config/gtk-4.0",
"--filesystem=xdg-run/gvfsd",
"--filesystem=xdg-download:ro",
"--filesystem=~/.mozilla/firefox",
"--filesystem=~/.librewolf",
"--filesystem=~/.var/app/org.mozilla.firefox/.mozilla/firefox",

View file

@ -14,6 +14,7 @@
"--filesystem=xdg-config/gtk-3.0",
"--filesystem=xdg-config/gtk-4.0",
"--filesystem=xdg-run/gvfsd",
"--filesystem=xdg-download:ro",
"--filesystem=~/.mozilla/firefox",
"--filesystem=~/.librewolf",
"--filesystem=~/.var/app/org.mozilla.firefox/.mozilla/firefox",

View file

@ -20,6 +20,8 @@ import os
from gi.repository import GLib, Gio, Adw
from gradience.backend.constants import app_id
from gradience.backend.logger import Logger
logging = Logger()
@ -32,10 +34,10 @@ class InvalidGTKVersion(Exception):
pass
""" Internal helper functions (shouldn't be used outside this file) """
""" Internal helper functions (shouldn't be used outside this module) """
def get_system_flatpak_path():
def __get_system_flatpak_path():
systemPath = GLib.getenv("FLATPAK_SYSTEM_DIR")
logging.debug(f"systemPath: {systemPath}")
@ -47,7 +49,7 @@ def get_system_flatpak_path():
return GLib.build_filenamev([systemDataDir, "flatpak"])
def get_user_flatpak_path():
def __get_user_flatpak_path():
userPath = GLib.getenv("FLATPAK_USER_DIR")
logging.debug(f"userPath: {userPath}")
@ -60,49 +62,196 @@ def get_user_flatpak_path():
return GLib.build_filenamev([userDataDir, "flatpak"])
def user_save_keyfile(toast_overlay, settings, user_keyfile, filename, gtk_ver):
def __user_save_keyfile(user_keyfile, filename, settings=None, gtk_ver=None, toast_overlay=None):
try:
user_keyfile.save_to_file(filename)
except GLib.GError as e:
toast_overlay.add_toast(Adw.Toast(title=_("Failed to save override")))
if toast_overlay:
toast_overlay.add_toast(Adw.Toast(title=_("Failed to save override")))
logging.error(f"Failed to save keyfile structure to override. Exc: {e}")
else:
if gtk_ver == "gtk4":
if gtk_ver == "gtk4" and settings:
settings.set_boolean("user-flatpak-theming-gtk4", True)
logging.debug(
f"user-flatpak-theming-gtk4: {settings.get_boolean('user-flatpak-theming-gtk4')}"
)
elif gtk_ver == "gtk3":
elif gtk_ver == "gtk3" and settings:
settings.set_boolean("user-flatpak-theming-gtk3", True)
logging.debug(
f"user-flatpak-theming-gtk3: {settings.get_boolean('user-flatpak-theming-gtk3')}"
)
elif not gtk_ver and not settings:
logging.debug("DEV WARNING: 'gtk_ver' and 'settings' parameters aren't set for '__user_save_keyfile' function. Unless you aren't using '{create,remove}_*_override' functions, this is a bug.")
def global_save_keyfile(toast_overlay, settings, global_keyfile, filename, gtk_ver):
def __global_save_keyfile(global_keyfile, filename, settings=None, gtk_ver=None, toast_overlay=None):
try:
global_keyfile.save_to_file(filename)
except GLib.GError as e:
toast_overlay.add_toast(Adw.Toast(title=_("Failed to save override")))
if toast_overlay:
toast_overlay.add_toast(Adw.Toast(title=_("Failed to save override")))
logging.error(f"Failed to save keyfile structure to override. Exc: {e}")
else:
if gtk_ver == "gtk4":
if gtk_ver == "gtk4" and settings:
settings.set_boolean("global-flatpak-theming-gtk4", True)
logging.debug(
f"global-flatpak-theming-gtk4: {settings.get_boolean('global-flatpak-theming-gtk4')}"
)
elif gtk_ver == "gtk3":
elif gtk_ver == "gtk3" and settings:
settings.set_boolean("global-flatpak-theming-gtk3", True)
logging.debug(
f"global-flatpak-theming-gtk3: {settings.get_boolean('global-flatpak-theming-gtk3')}"
)
elif not gtk_ver and not settings:
logging.debug("DEV WARNING: 'gtk_ver' and 'settings' parameters aren't set for '__global_save_keyfile' function. Unless you aren't using '{create,remove}_*_override' functions, this is a bug.")
""" Main functions """
def create_gtk_user_override(toast_overlay, settings, gtk_ver):
override_dir = GLib.build_filenamev([get_user_flatpak_path(), "overrides"])
def list_file_access():
override_dir = GLib.build_filenamev([__get_user_flatpak_path(), "overrides"])
logging.debug(f"override_dir: {override_dir}")
filename = GLib.build_filenamev([override_dir, app_id])
user_keyfile = GLib.KeyFile.new()
try:
user_keyfile.load_from_file(filename, GLib.KeyFileFlags.NONE)
except GLib.GError as e:
if e.code == 4:
logging.debug("Gradience overrides file doesn't exist")
return False
else:
logging.error(f"Unhandled GLib.FileError error code. Exc: {e}")
raise
else:
try:
filesys_list = user_keyfile.get_string_list(
"Context", "filesystems")
except GLib.GError:
logging.debug("No values in 'filesystems' override")
return False
else:
return filesys_list
# TODO: Frontend: Show information to user to relaunch Gradience, as this function modifies \
# Gradience's overrides.
def allow_file_access(directory, toast_overlay=None):
override_dir = GLib.build_filenamev([__get_user_flatpak_path(), "overrides"])
logging.debug(f"override_dir: {override_dir}")
without_access_spec = (
not ":ro" in directory
and not ":rw" in directory
and not ":create" in directory
)
if without_access_spec:
directory += ":ro"
filename = GLib.build_filenamev([override_dir, app_id])
user_keyfile = GLib.KeyFile.new()
try:
user_keyfile.load_from_file(filename, GLib.KeyFileFlags.NONE)
except GLib.GError as e:
if e.code == 4:
logging.debug("File doesn't exist. Attempting to create one")
if not os.path.exists(override_dir):
try:
dirs = Gio.File.new_for_path(override_dir)
dirs.make_directory_with_parents(None)
except GLib.GError as e:
logging.error(f"Unable to create directories. Exc: {e}")
raise
else:
logging.debug("Directories created.")
file = Gio.File.new_for_path(filename)
file.create(Gio.FileCreateFlags.NONE, None)
user_keyfile.load_from_file(filename, GLib.KeyFileFlags.NONE)
user_keyfile.set_string("Context", "filesystems", directory)
__user_save_keyfile(user_keyfile, filename,
toast_overlay=toast_overlay)
else:
if toast_overlay:
toast_overlay.add_toast(
Adw.Toast(title=_("Unexpected file error occurred"))
)
logging.error(f"Unhandled GLib.FileError error code. Exc: {e}")
else:
try:
filesys_list = user_keyfile.get_string_list(
"Context", "filesystems")
except GLib.GError:
user_keyfile.set_string("Context", "filesystems", directory)
__user_save_keyfile(user_keyfile, filename,
toast_overlay=toast_overlay)
else:
if directory not in filesys_list:
user_keyfile.set_string_list(
"Context", "filesystems", filesys_list + [directory]
)
__user_save_keyfile(user_keyfile, filename,
toast_overlay=toast_overlay)
else:
logging.info("Path is already allowed")
# TODO: Frontend: Show information to user to relaunch Gradience, as this function modifies \
# Gradience's overrides.
def disallow_file_access(directory, toast_overlay=None):
override_dir = GLib.build_filenamev([__get_user_flatpak_path(), "overrides"])
logging.debug(f"override_dir: {override_dir}")
filename = GLib.build_filenamev([override_dir, app_id])
user_keyfile = GLib.KeyFile.new()
try:
user_keyfile.load_from_file(filename, GLib.KeyFileFlags.NONE)
except GLib.GError as e:
if e.code == 4:
logging.debug("File doesn't exist")
return
else:
if toast_overlay:
toast_overlay.add_toast(
Adw.Toast(title=_("Unexpected file error occurred"))
)
logging.error(f"Unhandled GLib.FileError error code. Exc: {e}")
raise
else:
try:
filesys_list = user_keyfile.get_string_list(
"Context", "filesystems")
except GLib.GError:
logging.debug("Group/key not found")
return
else:
if directory in filesys_list:
logging.debug(f"before: {filesys_list}")
filesys_list.remove(directory)
logging.debug(f"after: {filesys_list}")
user_keyfile.set_string_list(
"Context", "filesystems", filesys_list)
__user_save_keyfile(user_keyfile, filename,
toast_overlay=toast_overlay)
logging.debug("Path removed")
else:
logging.debug("Path doesn't exist in overrides")
return
def create_gtk_user_override(settings, gtk_ver, toast_overlay=None):
override_dir = GLib.build_filenamev([__get_user_flatpak_path(), "overrides"])
logging.debug(f"override_dir: {override_dir}")
filename = GLib.build_filenamev([override_dir, "global"])
@ -147,12 +296,13 @@ def create_gtk_user_override(toast_overlay, settings, gtk_ver):
user_keyfile.load_from_file(filename, GLib.KeyFileFlags.NONE)
user_keyfile.set_string("Context", "filesystems", gtk_path)
user_save_keyfile(toast_overlay, settings,
user_keyfile, filename, gtk_ver)
__user_save_keyfile(user_keyfile, filename,
settings, gtk_ver, toast_overlay)
else:
toast_overlay.add_toast(
Adw.Toast(title=_("Unexpected file error occurred"))
)
if toast_overlay:
toast_overlay.add_toast(
Adw.Toast(title=_("Unexpected file error occurred"))
)
logging.error(f"Unhandled GLib.FileError error code. Exc: {e}")
else:
try:
@ -160,16 +310,15 @@ def create_gtk_user_override(toast_overlay, settings, gtk_ver):
"Context", "filesystems")
except GLib.GError:
user_keyfile.set_string("Context", "filesystems", gtk_path)
user_save_keyfile(toast_overlay, settings,
user_keyfile, filename, gtk_ver)
__user_save_keyfile(user_keyfile, filename,
settings, gtk_ver, toast_overlay)
else:
if gtk_path not in filesys_list:
user_keyfile.set_string_list(
"Context", "filesystems", filesys_list + [gtk_path]
)
user_save_keyfile(
toast_overlay, settings, user_keyfile, filename, gtk_ver
)
__user_save_keyfile(user_keyfile, filename,
settings, gtk_ver, toast_overlay)
else:
if is_gtk4:
settings.set_boolean("user-flatpak-theming-gtk4", True)
@ -178,8 +327,8 @@ def create_gtk_user_override(toast_overlay, settings, gtk_ver):
logging.debug("Value already exists.")
def remove_gtk_user_override(toast_overlay, settings, gtk_ver):
override_dir = GLib.build_filenamev([get_user_flatpak_path(), "overrides"])
def remove_gtk_user_override(settings, gtk_ver, toast_overlay=None):
override_dir = GLib.build_filenamev([__get_user_flatpak_path(), "overrides"])
logging.debug(f"override_dir: {override_dir}")
filename = GLib.build_filenamev([override_dir, "global"])
@ -210,9 +359,10 @@ def remove_gtk_user_override(toast_overlay, settings, gtk_ver):
set_theming()
logging.warning("remove override: File doesn't exist")
else:
toast_overlay.add_toast(
Adw.Toast(title=_("Unexpected file error occurred"))
)
if toast_overlay:
toast_overlay.add_toast(
Adw.Toast(title=_("Unexpected file error occurred"))
)
logging.error(f"Unhandled GLib.FileError error code. Exc: {e}")
else:
try:
@ -229,9 +379,8 @@ def remove_gtk_user_override(toast_overlay, settings, gtk_ver):
user_keyfile.set_string_list(
"Context", "filesystems", filesys_list)
user_save_keyfile(
toast_overlay, settings, user_keyfile, filename, gtk_ver
)
__user_save_keyfile(user_keyfile, filename,
settings, gtk_ver, toast_overlay)
logging.debug("remove override: Value removed.")
else:
set_theming()
@ -242,9 +391,9 @@ def remove_gtk_user_override(toast_overlay, settings, gtk_ver):
# TODO: Implement user authentication using Polkit
def create_gtk_global_override(toast_overlay, settings, gtk_ver):
def create_gtk_global_override(settings, gtk_ver, toast_overlay=None):
override_dir = GLib.build_filenamev(
[get_system_flatpak_path(), "overrides"])
[__get_system_flatpak_path(), "overrides"])
logging.debug(f"override_dir: {override_dir}")
filename = GLib.build_filenamev([override_dir, "global"])
@ -289,13 +438,13 @@ def create_gtk_global_override(toast_overlay, settings, gtk_ver):
global_keyfile.load_from_file(filename, GLib.KeyFileFlags.NONE)
global_keyfile.set_string("Context", "filesystems", gtk_path)
global_save_keyfile(
toast_overlay, settings, global_keyfile, filename, gtk_ver
)
__global_save_keyfile(global_keyfile, filename,
settings, gtk_ver, toast_overlay)
else:
toast_overlay.add_toast(
Adw.Toast(title=_("Unexpected file error occurred"))
)
if toast_overlay:
toast_overlay.add_toast(
Adw.Toast(title=_("Unexpected file error occurred"))
)
logging.error(f"Unhandled GLib.FileError error code. Exc: {e}")
else:
try:
@ -303,17 +452,15 @@ def create_gtk_global_override(toast_overlay, settings, gtk_ver):
"Context", "filesystems")
except GLib.GError:
global_keyfile.set_string("Context", "filesystems", gtk_path)
global_save_keyfile(
toast_overlay, settings, global_keyfile, filename, gtk_ver
)
__global_save_keyfile(global_keyfile, filename,
settings, gtk_ver, toast_overlay)
else:
if gtk_path not in filesys_list:
global_keyfile.set_string_list(
"Context", "filesystems", filesys_list + [gtk_path]
)
global_save_keyfile(
toast_overlay, settings, global_keyfile, filename, gtk_ver
)
__global_save_keyfile(global_keyfile, filename,
settings, gtk_ver, toast_overlay)
else:
if is_gtk4:
settings.set_boolean("global-flatpak-theming-gtk4", True)
@ -322,9 +469,9 @@ def create_gtk_global_override(toast_overlay, settings, gtk_ver):
logging.debug("Value already exists.")
def remove_gtk_global_override(toast_overlay, settings, gtk_ver):
def remove_gtk_global_override(settings, gtk_ver, toast_overlay=None):
override_dir = GLib.build_filenamev(
[get_system_flatpak_path(), "overrides"])
[__get_system_flatpak_path(), "overrides"])
logging.debug(f"override_dir: {override_dir}")
filename = GLib.build_filenamev([override_dir, "global"])
@ -355,9 +502,10 @@ def remove_gtk_global_override(toast_overlay, settings, gtk_ver):
set_theming()
logging.warning("remove override: File doesn't exist")
else:
toast_overlay.add_toast(
Adw.Toast(title=_("Unexpected file error occurred"))
)
if toast_overlay:
toast_overlay.add_toast(
Adw.Toast(title=_("Unexpected file error occurred"))
)
logging.error(f"Unhandled GLib.FileError error code. Exc: {e}")
else:
try:
@ -374,9 +522,8 @@ def remove_gtk_global_override(toast_overlay, settings, gtk_ver):
global_keyfile.set_string_list(
"Context", "filesystems", filesys_list)
global_save_keyfile(
toast_overlay, settings, global_keyfile, filename, gtk_ver
)
__global_save_keyfile(global_keyfile, filename,
settings, gtk_ver, toast_overlay)
logging.debug("remove override: Value removed.")
else:
set_theming()

View file

@ -0,0 +1,58 @@
# globals.py
#
# Change the look of Adwaita, with ease
# Copyright (C) 2022 Gradience Team
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# 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 os
from gi.repository import Xdp
presets_dir = os.path.join(
os.environ.get("XDG_CONFIG_HOME", os.environ["HOME"] + "/.config"),
"presets"
)
preset_repos = {
"Official": "https://github.com/GradienceTeam/Community/raw/next/official.json",
"Curated": "https://github.com/GradienceTeam/Community/raw/next/curated.json"
}
def get_gtk_theme_dir(app_type):
if app_type == "gtk4":
theme_dir = os.path.join(
os.environ.get("XDG_CONFIG_HOME",
os.environ["HOME"] + "/.config"),
"gtk-4.0"
)
elif app_type == "gtk3":
theme_dir = os.path.join(
os.environ.get("XDG_CONFIG_HOME",
os.environ["HOME"] + "/.config"),
"gtk-3.0"
)
return theme_dir
def is_sandboxed():
portal = Xdp.Portal()
is_sandboxed = self.portal.running_under_sandbox()
return is_sandboxed
def get_available_sassc():
pass

View file

@ -25,9 +25,13 @@ class Logger(logging.getLoggerClass()):
"""
This is a wrapper of `logging` module. It provides
custom formatting for log messages.
Attributes:
logger_name (str): Custom name of the logger.
formatter (dict): Custom formatter for the logger.
"""
log_colors = {
"debug": 37,
"debug": 32,
"info": 36,
"warning": 33,
"error": 31,
@ -35,21 +39,31 @@ class Logger(logging.getLoggerClass()):
}
log_format = {
'fmt': '\033[1m[%(levelname)s]\033[0m [%(name)s] %(message)s'
'fmt': '[%(name)s] %(message)s'
}
def __set_color(self, level, message: str):
def __set_level_color(self, level, message: str):
if message is not None and "\n" in message:
message = message.replace("\n", "\n\t") + "\n"
color_id = self.log_colors[level]
return "\033[%dm%s\033[0m" % (color_id, message)
return "\033[1;%dm%s:\033[0m %s" % (color_id, level.upper(), message)
def __init__(self, formatter=None):
def __init__(self, logger_name=None, formatter=None):
"""
The constructor for Logger class.
When initializing this class, you should specify a logger name for debugging purposes,
even if you didn't wrote any debug messages in your code.
The logger name should usually be a name of your module's main class or module name.
"""
if formatter is None:
formatter = self.log_format
formatter = logging.Formatter(**formatter)
self.root.name = "gradience"
if logger_name:
self.root.name = "Gradience.%s" % (logger_name)
else:
self.root.name = "Gradience"
if build_type == "debug":
self.root.setLevel(logging.DEBUG)
@ -62,19 +76,19 @@ class Logger(logging.getLoggerClass()):
self.root.addHandler(handler)
def debug(self, message, **kwargs):
self.root.debug(self.__set_color("debug", str(message)), )
self.root.debug(self.__set_level_color("debug", str(message)), )
def info(self, message, **kwargs):
self.root.info(self.__set_color("info", str(message)), )
self.root.info(self.__set_level_color("info", str(message)), )
def warning(self, message, **kwargs):
self.root.warning(self.__set_color("warning", str(message)),)
self.root.warning(self.__set_level_color("warning", str(message)),)
def error(self, message, **kwargs):
self.root.error(self.__set_color("error", str(message)), )
self.root.error(self.__set_level_color("error", str(message)), )
def critical(self, message, **kwargs):
self.root.critical(self.__set_color("critical", str(message)), )
self.root.critical(self.__set_level_color("critical", str(message)), )
def set_silent(self):
self.root.handlers = []

View file

@ -20,12 +20,14 @@ configure_file(
)
subdir('models')
subdir('theming')
subdir('utils')
gradience_sources = [
'__init__.py',
'css_parser.py',
'flatpak_overrides.py',
'globals.py',
'logger.py',
'preset_downloader.py'
]

View file

@ -19,23 +19,90 @@
import json
import os
from gradience.frontend.settings_schema import settings_schema
from gradience.backend.utils.common import to_slug_case
from gradience.backend.globals import presets_dir
from gradience.backend.logger import Logger
logging = Logger()
presets_dir = os.path.join(
os.environ.get("XDG_CONFIG_HOME", os.environ["HOME"] + "/.config"),
"presets",
)
# Adwaita default colors palette
adw_palette = {
"blue_": {
"1": "#99c1f1",
"2": "#62a0ea",
"3": "#3584e4",
"4": "#1c71d8",
"5": "#1a5fb4",
},
"green_": {
"1": "#8ff0a4",
"2": "#57e389",
"3": "#33d17a",
"4": "#2ec27e",
"5": "#26a269",
},
"yellow_": {
"1": "#f9f06b",
"2": "#f8e45c",
"3": "#f6d32d",
"4": "#f5c211",
"5": "#e5a50a",
},
"orange_": {
"1": "#ffbe6f",
"2": "#ffa348",
"3": "#ff7800",
"4": "#e66100",
"5": "#c64600",
},
"red_": {
"1": "#f66151",
"2": "#ed333b",
"3": "#e01b24",
"4": "#c01c28",
"5": "#a51d2d",
},
"purple_": {
"1": "#dc8add",
"2": "#c061cb",
"3": "#9141ac",
"4": "#813d9c",
"5": "#613583",
},
"brown_": {
"1": "#cdab8f",
"2": "#b5835a",
"3": "#986a44",
"4": "#865e3c",
"5": "#63452c",
},
"light_": {
"1": "#ffffff",
"2": "#f6f5f4",
"3": "#deddda",
"4": "#c0bfbc",
"5": "#9a9996",
},
"dark_": {
"1": "#77767b",
"2": "#5e5c64",
"3": "#3d3846",
"4": "#241f31",
"5": "#000000",
}
}
custom_css_app_types = [
"gtk4",
"gtk3"
]
class Preset:
variables = {}
palette = {}
palette = adw_palette
custom_css = {
"gtk4": "",
"gtk3": "",
@ -43,33 +110,62 @@ class Preset:
plugins = {}
display_name = "New Preset"
preset_path = "new_preset"
plugins_list = {}
badges = {}
def __init__(self, preset_path=None, text=None, preset=None):
if preset_path:
self.load_preset(preset_path=preset_path)
elif text: # load from resource
self.load_preset(text=text)
elif preset: # css or dict
self.load_preset(preset=preset)
else:
raise Exception("Failed to create a new Preset object: Preset created without content")
def __init__(self):
pass
def new(self, variables: dict, display_name=None, palette=None, custom_css=None, badges=None):
self.variables = variables
if display_name:
self.display_name = display_name
if palette:
self.palette = palette
if custom_css:
self.custom_css = custom_css
if badges:
self.badges = badges
def new_from_path(self, preset_path: str):
self.preset_path = preset_path
def load_preset(self, preset_path=None, text=None, preset=None):
try:
if not preset:
if text:
preset_text = text
elif preset_path:
self.preset_path = preset_path
with open(self.preset_path, "r", encoding="utf-8") as file:
preset_text = file.read()
file.close()
else:
raise Exception("load_preset must be called with a path, text, or preset")
with open(self.preset_path, "r", encoding="utf-8") as file:
preset_text = file.read()
file.close()
except OSError as e:
logging.error(f"Failed to read contents of a preset in location: {self.preset_path}. Exc: {e}")
preset = json.loads(preset_text)
try:
preset = json.loads(preset_text)
except json.JSONDecodeError as e:
logging.error(f"Error while decoding JSON data. Exc: {e}")
self.__load_values(preset)
return self
def new_from_resource(self, text: str):
preset_text = text
try:
preset = json.loads(preset_text)
except json.JSONDecodeError as e:
logging.error(f"Error while decoding JSON data. Exc: {e}")
self.__load_values(preset)
return self
def new_from_dict(self, preset: dict):
self.__load_values(preset)
return self
def __load_values(self, preset):
try:
self.display_name = preset["name"]
self.variables = preset["variables"]
self.palette = preset["palette"]
@ -82,31 +178,40 @@ class Preset:
if "custom_css" in preset:
self.custom_css = preset["custom_css"]
else:
for app_type in settings_schema["custom_css_app_types"]:
for app_type in custom_css_app_types:
self.custom_css[app_type] = ""
except Exception as e:
if self.preset_path:
logging.error(f"Failed to load preset {self.preset_path}. Exc: {e}")
else:
logging.error(f"Failed to load preset with unknown path. Exc: {e}")
logging.error(f"Failed to create a new preset object. Exc: {e}")
# Rename an existing preset
def rename_preset(self, name):
def rename(self, name):
self.display_name = name
old_path = self.preset_path
self.preset_path = os.path.join(
os.path.dirname(self.preset_path),
to_slug_case(name) + ".json")
self.save_preset(to=self.preset_path)
self.save_to_file(to=self.preset_path)
os.remove(old_path)
def get_preset_json(self, indent=None):
preset_dict = {
"name": self.display_name,
"variables": self.variables,
"palette": self.palette,
"custom_css": self.custom_css,
"plugins": self.plugins_list
}
json_output = json.dumps(preset_dict, indent=indent)
return json_output
# Save a new user preset (or overwrite one)
def save_preset(self, name=None, plugins_list=None, to=None):
def save_to_file(self, name=None, plugins_list=None, to=None):
self.display_name = name if name else self.display_name
if to is None:
filename = to_slug_case(name) if name else "new_preset"
filename = to_slug_case(name) if name else to_slug_case(self.display_name)
self.preset_path = os.path.join(
presets_dir, "user", filename + ".json")
else:
@ -118,28 +223,29 @@ class Preset:
"user",
)
):
os.makedirs(
os.path.join(
presets_dir,
"user",
try:
os.makedirs(
os.path.join(
presets_dir,
"user",
)
)
)
except OSError as e:
logging.error(f"Failed to create a new preset directory. Exc: {e}")
raise
if plugins_list is None:
plugins_list = {}
else:
if plugins_list:
plugins_list = plugins_list.save()
with open(self.preset_path, "w", encoding="utf-8") as file:
object_to_write = {
"name": self.display_name,
"variables": self.variables,
"palette": self.palette,
"custom_css": self.custom_css,
"plugins": plugins_list,
}
file.write(json.dumps(object_to_write, indent=4))
file.close()
try:
with open(self.preset_path, "w", encoding="utf-8") as file:
content = self.get_preset_json(indent=4)
file.write(content)
file.close()
except OSError as e:
logging.error(f"Failed to save preset as a file. Exc: {e}")
raise
# TODO: Add validation
def validate(self):
return True

View file

@ -19,7 +19,8 @@
import os
from gradience.backend.utils.common import to_slug_case
from gradience.backend.models.preset import Preset, presets_dir
from gradience.backend.globals import presets_dir
from gradience.backend.models.preset import Preset
class Repo:
@ -34,5 +35,6 @@ class Repo:
presets = {}
for preset in os.listdir(self.path):
if preset.endswith(".json"):
presets[preset[:-5]] = Preset(os.path.join(self.path, preset))
preset_path = os.path.join(self.path, preset)
presets[preset[:-5]] = Preset().new_from_path(preset_path)
return presets

View file

@ -21,7 +21,7 @@ import json
from gi.repository import GLib, Soup
from gradience.backend.models.preset import presets_dir
from gradience.backend.globals import presets_dir
from gradience.backend.utils.common import to_slug_case
from gradience.backend.logger import Logger
@ -29,74 +29,77 @@ from gradience.backend.logger import Logger
logging = Logger()
# Open Soup3 session
session = Soup.Session()
class PresetDownloader:
def __init__(self):
# Open Soup3 session
self.session = Soup.Session()
def fetch_presets(repo) -> [dict, list]:
try:
request = Soup.Message.new("GET", repo)
body = session.send_and_read(request, None)
except GLib.GError as e: # offline
if e.code == 1:
logging.error(f"Failed to establish a new connection. Exc: {e}")
return False, False
else:
logging.error(f"Unhandled Libsoup3 GLib.GError error code {e.code}. Exc: {e}")
return False, False
try:
raw = json.loads(body.get_data())
except json.JSONDecodeError as e:
logging.error(f"Error with decoding JSON data. Exc: {e}")
return False, False
def fetch_presets(self, repo) -> [dict, list]:
try:
request = Soup.Message.new("GET", repo)
body = self.session.send_and_read(request, None)
except GLib.GError as e:
if e.code == 1: # offline
logging.error(f"Failed to establish a new connection. Exc: {e}")
raise
else:
logging.error(f"Unhandled Libsoup3 GLib.GError error code {e.code}. Exc: {e}")
raise
try:
raw = json.loads(body.get_data())
except json.JSONDecodeError as e:
logging.error(f"Error while decoding JSON data. Exc: {e}")
raise
preset_dict = {}
url_list = []
preset_dict = {}
url_list = []
for data in raw.items():
data = list(data)
data.insert(0, to_slug_case(data[0]))
for data in raw.items():
data = list(data)
data.insert(0, to_slug_case(data[0]))
url = data[2]
data.pop(2) # Remove preset URL from list
url = data[2]
data.pop(2) # Remove preset URL from list
to_dict = iter(data)
# Convert list back to dict
preset_dict.update(dict(zip(to_dict, to_dict)))
to_dict = iter(data)
# Convert list back to dict
preset_dict.update(dict(zip(to_dict, to_dict)))
url_list.append(url)
url_list.append(url)
return preset_dict, url_list
return preset_dict, url_list
def download_preset(name, repo_name, repo) -> None:
try:
request = Soup.Message.new("GET", repo)
body = session.send_and_read(request, None)
except GLib.GError as e: # offline
if e.code == 1:
logging.error(f"Failed to establish a new connection. Exc: {e}")
return False, False
else:
logging.error(f"Unhandled Libsoup3 GLib.GError error code {e.code}. Exc: {e}")
return False, False
try:
raw = json.loads(body.get_data())
except json.JSONDecodeError as e:
logging.error(f"Error with decoding JSON data. Exc: {e}")
return False, False
def download_preset(self, name, repo_name, repo) -> None:
try:
request = Soup.Message.new("GET", repo)
body = self.session.send_and_read(request, None)
except GLib.GError as e:
if e.code == 1: # offline
logging.error(f"Failed to establish a new connection. Exc: {e}")
raise
else:
logging.error(f"Unhandled Libsoup3 GLib.GError error code {e.code}. Exc: {e}")
raise
try:
raw = json.loads(body.get_data())
except json.JSONDecodeError as e:
logging.error(f"Error while decoding JSON data. Exc: {e}")
raise
data = json.dumps(raw, indent=4)
data = json.dumps(raw, indent=4)
try:
with open(
os.path.join(
presets_dir,
repo_name,
to_slug_case(name) + ".json",
),
"w",
encoding="utf-8",
) as f:
f.write(data)
f.close()
except OSError as e:
logging.error(f"Failed to write data to a file. Exc: {e}")
try:
with open(
os.path.join(
presets_dir,
repo_name,
to_slug_case(name) + ".json",
),
"w",
encoding="utf-8",
) as f:
f.write(data)
f.close()
except OSError as e:
logging.error(f"Failed to write data to a file. Exc: {e}")
raise

View file

View file

@ -0,0 +1,8 @@
themingdir = 'gradience/backend/theming'
gradience_sources = [
'__init__.py',
'monet.py',
'preset_utils.py'
]
PY_INSTALLDIR.install_sources(gradience_sources, subdir: themingdir)

View file

@ -0,0 +1,63 @@
# monet.py
#
# Change the look of Adwaita, with ease
# Copyright (C) 2022 Gradience Team
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# 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 os
from svglib.svglib import svg2rlg
from reportlab.graphics import renderPM
import material_color_utilities_python as monet
from gradience.backend.models.preset import Preset
from gradience.backend.logger import Logger
logging = Logger()
class Monet:
def __init__(self):
self.palette = None
def generate_from_image(self, image_path: str) -> dict:
if image_path.endswith(".svg"):
drawing = svg2rlg(image_path)
image_path = os.path.join(
os.environ.get("XDG_RUNTIME_DIR"), "gradience_bg.png"
)
renderPM.drawToFile(drawing, image_path, fmt="PNG")
if image_path.endswith(".xml"):
logging.error(f"XML files are unsupported by Gradience's Monet implementation.")
return False
try:
monet_img = monet.Image.open(image_path)
except Exception as e:
logging.error(f"An error occurred while generating a Monet palette. Exc: {e}")
return False
else:
basewidth = 64
wpercent = basewidth / float(monet_img.size[0])
hsize = int((float(monet_img.size[1]) * float(wpercent)))
monet_img = monet_img.resize(
(basewidth, hsize), monet.Image.Resampling.LANCZOS
)
self.palette = monet.themeFromImage(monet_img)
return self.palette

View file

@ -0,0 +1,366 @@
# preset_utils.py
#
# Change the look of Adwaita, with ease
# Copyright (C) 2022 Gradience Team
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# 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 os
import json
from pathlib import Path
from gi.repository import GLib, Gio
from gradience.backend.theming.monet import Monet
from gradience.backend.models.preset import Preset
from gradience.backend.utils.colors import rgba_from_argb
from gradience.backend.globals import presets_dir, get_gtk_theme_dir
from gradience.backend.logger import Logger
logging = Logger()
class PresetUtils:
def __init__(self):
self.preset = Preset()
def generate_gtk_css(self, app_type: str, preset: Preset) -> str:
variables = preset.variables
palette = preset.palette
custom_css = preset.custom_css
final_css = ""
for key in variables.keys():
final_css += f"@define-color {key} {variables[key]};\n"
for prefix_key in palette.keys():
for key in palette[prefix_key].keys():
final_css += f"@define-color {prefix_key + key} {palette[prefix_key][key]};\n"
final_css += custom_css.get(app_type, "")
return final_css
def new_preset_from_monet(self, name=None, monet_palette=None, props=None, obj_only=False) -> Preset or None:
if props:
tone = props[0]
theme = props[1]
else:
raise Exception("Properties 'tone' and/or 'theme' missing")
if not monet_palette:
raise Exception("Property 'monet_palette' missing")
if theme == "dark":
dark_theme = monet_palette["schemes"]["dark"]
variable = {
"accent_color": rgba_from_argb(dark_theme.primary),
"accent_bg_color": rgba_from_argb(dark_theme.primaryContainer),
"accent_fg_color": rgba_from_argb(dark_theme.onPrimaryContainer),
"destructive_color": rgba_from_argb(dark_theme.error),
"destructive_bg_color": rgba_from_argb(dark_theme.errorContainer),
"destructive_fg_color": rgba_from_argb(
dark_theme.onErrorContainer
),
"success_color": rgba_from_argb(dark_theme.tertiary),
"success_bg_color": rgba_from_argb(dark_theme.onTertiary),
"success_fg_color": rgba_from_argb(dark_theme.onTertiaryContainer),
"warning_color": rgba_from_argb(dark_theme.secondary),
"warning_bg_color": rgba_from_argb(dark_theme.onSecondary),
"warning_fg_color": rgba_from_argb(dark_theme.primary, "0.8"),
"error_color": rgba_from_argb(dark_theme.error),
"error_bg_color": rgba_from_argb(dark_theme.errorContainer),
"error_fg_color": rgba_from_argb(dark_theme.onError),
"window_bg_color": rgba_from_argb(dark_theme.surface),
"window_fg_color": rgba_from_argb(dark_theme.onSurface),
"view_bg_color": rgba_from_argb(dark_theme.surface),
"view_fg_color": rgba_from_argb(dark_theme.onSurface),
"headerbar_bg_color": rgba_from_argb(dark_theme.surface),
"headerbar_fg_color": rgba_from_argb(dark_theme.onSurface),
"headerbar_border_color": rgba_from_argb(
dark_theme.primary, "0.8"
),
"headerbar_backdrop_color": "@headerbar_bg_color",
"headerbar_shade_color": rgba_from_argb(dark_theme.shadow),
"card_bg_color": rgba_from_argb(dark_theme.primary, "0.05"),
"card_fg_color": rgba_from_argb(dark_theme.onSecondaryContainer),
"card_shade_color": rgba_from_argb(dark_theme.shadow),
"dialog_bg_color": rgba_from_argb(dark_theme.secondaryContainer),
"dialog_fg_color": rgba_from_argb(dark_theme.onSecondaryContainer),
"popover_bg_color": rgba_from_argb(dark_theme.secondaryContainer),
"popover_fg_color": rgba_from_argb(
dark_theme.onSecondaryContainer
),
"shade_color": rgba_from_argb(dark_theme.shadow),
"scrollbar_outline_color": rgba_from_argb(dark_theme.outline),
}
elif theme == "light":
light_theme = monet_palette["schemes"]["light"]
variable = {
"accent_color": rgba_from_argb(light_theme.primary),
"accent_bg_color": rgba_from_argb(light_theme.primary),
"accent_fg_color": rgba_from_argb(light_theme.onPrimary),
"destructive_color": rgba_from_argb(light_theme.error),
"destructive_bg_color": rgba_from_argb(light_theme.errorContainer),
"destructive_fg_color": rgba_from_argb(
light_theme.onErrorContainer
),
"success_color": rgba_from_argb(light_theme.tertiary),
"success_bg_color": rgba_from_argb(light_theme.tertiaryContainer),
"success_fg_color": rgba_from_argb(
light_theme.onTertiaryContainer
),
"warning_color": rgba_from_argb(light_theme.secondary),
"warning_bg_color": rgba_from_argb(light_theme.secondaryContainer),
"warning_fg_color": rgba_from_argb(
light_theme.onSecondaryContainer
),
"error_color": rgba_from_argb(light_theme.error),
"error_bg_color": rgba_from_argb(light_theme.errorContainer),
"error_fg_color": rgba_from_argb(light_theme.onError),
"window_bg_color": rgba_from_argb(light_theme.secondaryContainer),
"window_fg_color": rgba_from_argb(light_theme.onSurface),
"view_bg_color": rgba_from_argb(light_theme.secondaryContainer),
"view_fg_color": rgba_from_argb(light_theme.onSurface),
"headerbar_bg_color": rgba_from_argb(
light_theme.secondaryContainer
),
"headerbar_fg_color": rgba_from_argb(light_theme.onSurface),
"headerbar_border_color": rgba_from_argb(
light_theme.primary, "0.8"
),
"headerbar_backdrop_color": "@headerbar_bg_color",
"headerbar_shade_color": rgba_from_argb(
light_theme.secondaryContainer
),
"card_bg_color": rgba_from_argb(light_theme.primary, "0.05"),
"card_fg_color": rgba_from_argb(light_theme.onSecondaryContainer),
"card_shade_color": rgba_from_argb(light_theme.shadow),
"dialog_bg_color": rgba_from_argb(light_theme.secondaryContainer),
"dialog_fg_color": rgba_from_argb(
light_theme.onSecondaryContainer
),
"popover_bg_color": rgba_from_argb(light_theme.secondaryContainer),
"popover_fg_color": rgba_from_argb(
light_theme.onSecondaryContainer
),
"shade_color": rgba_from_argb(light_theme.shadow),
"scrollbar_outline_color": rgba_from_argb(light_theme.outline),
}
if obj_only == False and not name:
raise Exception("You either need to set 'obj_only' property to True, or add value to 'name' property")
if obj_only:
if name:
print("with name, obj_only")
self.preset.new(variables=variable, display_name=name)
else:
print("no name, obj_only")
self.preset.new(variables=variable)
return self.preset
if obj_only == False:
print("no obj_only, name")
self.preset.new(variables=variable, display_name=name)
try:
self.preset.save_to_file()
except Exception as e:
# TODO: Move exception handling to model/preset module
logging.error(f"Unexpected file error while trying to generate preset from Monet palette. Exc: {e}")
raise
def get_presets_list(self, json_output=False) -> dict:
presets_list = {}
for repo in Path(presets_dir).iterdir():
logging.debug(f"presets_dir.iterdir: {repo}")
if repo.is_dir():
for file_name in repo.iterdir():
file_name = str(file_name)
if file_name.endswith(".json"):
try:
with open(
os.path.join(presets_dir, file_name),
"r",
encoding="utf-8",
) as file:
preset_text = file.read()
file.close()
preset = json.loads(preset_text)
if preset.get("variables") is None:
raise KeyError("'variables' section missing in loaded preset file")
if preset.get("palette") is None:
raise KeyError("'palette' section missing in loaded preset file")
presets_list[file_name] = preset[
"name"
]
except (OSError, KeyError) as e:
logging.error(f"Failed to load an preset information. Exc: {e}")
raise
elif repo.is_file():
# this exists to keep compatibility with old presets
if repo.name.endswith(".json"):
logging.warning("Legacy preset found. Moving to new structure.")
if not os.path.isdir(os.path.join(presets_dir, "user")):
os.mkdir(os.path.join(presets_dir, "user"))
os.rename(repo, os.path.join(
presets_dir, "user", repo.name))
try:
with open(
os.path.join(presets_dir, "user", repo),
"r",
encoding="utf-8",
) as file:
preset_text = file.read()
preset = json.loads(preset_text)
if preset.get("variables") is None:
raise KeyError("'variables' section missing in loaded preset file")
if preset.get("palette") is None:
raise KeyError("'palette' section missing in loaded preset file")
presets_list["user"][file_name] = preset[
"name"
]
except (OSError, KeyError) as e:
logging.error(f"Failed to load an preset information. Exc: {e}")
raise
if json_output:
json_output = json.dumps(presets_list)
return json_output
return presets_list
def apply_preset(self, app_type: str, preset: Preset) -> None:
if app_type == "gtk4":
theme_dir = get_gtk_theme_dir(app_type)
if not os.path.exists(theme_dir):
os.makedirs(theme_dir)
gtk4_css = self.generate_gtk_css("gtk4", preset)
contents = ""
try:
with open(
os.path.join(theme_dir, "gtk.css"), "r", encoding="utf-8"
) as file:
contents = file.read()
except FileNotFoundError: # first run
pass
else:
with open(
os.path.join(theme_dir, "gtk.css.bak"), "w", encoding="utf-8"
) as file:
file.write(contents)
finally:
with open(
os.path.join(theme_dir, "gtk.css"), "w", encoding="utf-8"
) as file:
file.write(gtk4_css)
elif app_type == "gtk3":
theme_dir = get_gtk_theme_dir(app_type)
if not os.path.exists(theme_dir):
os.makedirs(theme_dir)
gtk3_css = self.generate_gtk_css("gtk3", preset)
contents = ""
try:
with open(
os.path.join(theme_dir, "gtk.css"), "r", encoding="utf-8"
) as file:
contents = file.read()
except FileNotFoundError: # first run
pass
else:
with open(
os.path.join(theme_dir, "gtk.css.bak"), "w", encoding="utf-8"
) as file:
file.write(contents)
finally:
with open(
os.path.join(theme_dir, "gtk.css"), "w", encoding="utf-8"
) as file:
file.write(gtk3_css)
def restore_gtk4_preset(self) -> None:
try:
with open(
os.path.join(
os.environ.get(
"XDG_CONFIG_HOME", os.environ["HOME"] +
"/.config"
),
"gtk-4.0/gtk.css.bak",
),
"r",
encoding="utf-8",
) as backup:
contents = backup.read()
backup.close()
with open(
os.path.join(
os.environ.get(
"XDG_CONFIG_HOME", os.environ["HOME"] +
"/.config"
),
"gtk-4.0/gtk.css",
),
"w",
encoding="utf-8",
) as gtk4css:
gtk4css.write(contents)
gtk4css.close()
except OSError as e:
logging.error(f"Unable to restore Gtk4 backup. Exc: {e}")
raise
def reset_preset(self, app_type: str) -> None:
if app_type == "gtk4":
file = Gio.File.new_for_path(
os.path.join(
os.environ.get(
"XDG_CONFIG_HOME", os.environ["HOME"] + "/.config"
),
"gtk-4.0/gtk.css",
)
)
try:
file.delete()
except GLib.GError as e:
logging.error(f"Unable to delete current preset. Exc: {e}")
raise
elif app_type == "gtk3":
file = Gio.File.new_for_path(
os.path.join(
os.environ.get(
"XDG_CONFIG_HOME", os.environ["HOME"] + "/.config"
),
"gtk-3.0/gtk.css",
)
)
try:
file.delete()
except GLib.GError as e:
logging.error(f"Unable to delete current preset. Exc: {e}")
raise

View file

@ -0,0 +1,31 @@
# colors.py
#
# Change the look of Adwaita, with ease
# Copyright (C) 2022 Gradience Team
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# 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 material_color_utilities_python as monet
def rgba_from_argb(argb, alpha=None) -> str:
base = "rgba({}, {}, {}, {})"
red = monet.redFromArgb(argb)
green = monet.greenFromArgb(argb)
blue = monet.blueFromArgb(argb)
if not alpha:
alpha = monet.alphaFromArgb(argb)
return base.format(red, green, blue, alpha)

View file

@ -23,7 +23,7 @@ import subprocess
from anyascii import anyascii
def to_slug_case(non_slug):
def to_slug_case(non_slug) -> str:
return re.sub(r"[^0-9a-z]+", "-", anyascii(non_slug).lower()).strip("-")
def run_command(command, *args, **kwargs):

View file

@ -2,6 +2,7 @@ utilsdir = 'gradience/backend/utils'
gradience_sources = [
'__init__.py',
'colors.py',
'common.py'
]
PY_INSTALLDIR.install_sources(gradience_sources, subdir: utilsdir)

View file

416
gradience/frontend/cli/cli.in Executable file
View file

@ -0,0 +1,416 @@
#!/usr/bin/env python3
# cli.in
#
# Change the look of Adwaita, with ease
# Copyright (C) 2022 Gradience Team
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# 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 os
import sys
import json
import shutil
import signal
import argparse
import warnings
version = "@VERSION@"
is_local = @local_build@
if is_local:
# In the local use case, use gradience module from the sourcetree
sys.path.insert(1, '@PYTHON@')
# In the local use case the installed schemas go in <builddir>/data
os.environ["XDG_DATA_DIRS"] = '@SCHEMAS_DIR@:' + os.environ.get("XDG_DATA_DIRS", "")
signal.signal(signal.SIGINT, signal.SIG_DFL)
warnings.filterwarnings("ignore") # suppress GTK warnings
from gi.repository import GLib, Gio
from gradience.backend.utils.common import to_slug_case
from gradience.backend.globals import preset_repos, presets_dir
from gradience.backend.theming.monet import Monet
from gradience.backend.models.preset import Preset
from gradience.backend.theming.preset_utils import PresetUtils
from gradience.backend.preset_downloader import PresetDownloader
from gradience.backend.flatpak_overrides import list_file_access, allow_file_access, disallow_file_access, create_gtk_user_override, remove_gtk_user_override
from gradience.backend.logger import Logger
logging = Logger()
class CLI:
settings = Gio.Settings.new("@APP_ID@")
def __init__(self):
self.parser = argparse.ArgumentParser(description="Gradience - Change the look of Adwaita, with ease")
self.parser.add_argument("-V", "--version", action="version", version=f"Gradience, version {version}")
#self.parser.add_argument("-j", "--json", action="store_true", help="print out a result of the command directly in JSON format")
#self.parser.add_argument('-J', '--pretty-json', dest='pretty_json', action='store_true', help='pretty-print JSON output')
subparsers = self.parser.add_subparsers(dest="command")
#info_parser = subparsers.add_parser("info", help="show information about Gradience")
presets_parser = subparsers.add_parser("presets", help="list installed presets")
#presets_parser.add_argument("-r", "--remove-preset", metavar="PRESET_NAME", help="remove a preset from the list")
presets_parser.add_argument("-j", "--json", action="store_true", help="print out a result of this command directly in JSON format")
favorites_parser = subparsers.add_parser("favorites", help="list favorite presets")
favorites_parser.add_argument("-a", "--add-preset", metavar="PRESET_NAME", help="add a preset to favorites")
favorites_parser.add_argument("-r", "--remove-preset", metavar="PRESET_NAME", help="remove a preset from favorites")
favorites_parser.add_argument("-j", "--json", action="store_true", help="print out a result of this command directly in JSON format")
import_parser = subparsers.add_parser("import", help="import a preset")
import_parser.add_argument("-p", "--preset-path", help="absolute path to a preset file", required=True)
apply_parser = subparsers.add_parser("apply", help="apply a preset")
apply_group = apply_parser.add_mutually_exclusive_group(required=True)
apply_group.add_argument("-n", "--preset-name", help="display name for a preset")
apply_group.add_argument("-p", "--preset-path", help="absolute path to a preset file")
apply_parser.add_argument("--gtk", choices=["gtk4", "gtk3", "both"], default="gtk4", help="types of applications you want to theme (default: gtk4)")
#apply_parser.add_argument("--flatpak", choices=["gtk4", "gtk3", "both"], help="types of Flatpak applications you want to theme (for GTK3 option, make sure you have adw-gtk3 installed as Flatpak)")
#new_parser = subparsers.add_parser("new", help="create a new preset")
#new_parser.add_argument("-i", "--interactive", action="store_true", help="")
#new_parser.add_argument("-n", "--name", help="display name for a preset", required=True)
#new_parser.add_argument("--colors", help="", required=True)
#new_parser.add_argument("--palette", help="")
#new_parser.add_argument("--custom-css", help="")
#new_parser.add_argument("-j", "--json", action="store_true", help="print out a result of this command directly in JSON format")
download_parser = subparsers.add_parser("download", help="download preset from a preset repository")
#new_parser.add_argument("-i", "--interactive", action="store_true", help="")
download_parser.add_argument("-n", "--preset-name", help="name of a preset you want to get", required=True)
#download_parser.add_argument("--custom-url", help="use custom repository's presets.json to download other presets")
monet_parser = subparsers.add_parser("monet", help="generate Material You preset from an image")
monet_parser.add_argument("-n", "--preset-name", help="name for a generated preset", required=True)
monet_parser.add_argument("-p", "--image-path", help="abosulte path to image", required=True)
monet_parser.add_argument("--tone", default=20, help="a tone for colors (default: 20)")
monet_parser.add_argument("--theme", choices=["light", "dark"], default="light", help="choose whatever it should be a light or dark theme (default: light)")
monet_parser.add_argument("-j", "--json", action="store_true", help="print out a result of this command directly in JSON format")
access_parser = subparsers.add_parser("access-file", help="allow or disallow Gradience to access a certain file or directory")
access_parser.add_argument("-l", "--list", action="store_true", help="list allowed directories and files")
access_group = access_parser.add_mutually_exclusive_group(required=False)
access_group.add_argument("-a", "--allow", metavar="PATH", help="allow Gradience access to this file or directory")
access_group.add_argument("-d", "--disallow", metavar="PATH", help="disallow Gradience access to this file or directory")
overrides_parser = subparsers.add_parser("flatpak-overrides", help="enable or disable Flatpak theming")
overrides_group = overrides_parser.add_mutually_exclusive_group(required=True)
overrides_group.add_argument("-e", "--enable-theming", choices=["gtk4", "gtk3", "both"], help="enable overrides for Flatpak theming")
overrides_group.add_argument("-d", "--disable-theming", choices=["gtk4", "gtk3", "both"], help="disable overrides for Flatpak theming")
self.__parse_args()
def __print_json(self, data, pretty=False):
if pretty:
print(json.dumps(data, indent=4))
else:
print(json.dumps(data))
def __parse_args(self):
args = self.parser.parse_args()
if not args.command:
print(self.parser.format_help())
if args.command == "presets":
self.list_presets(args)
elif args.command == "favorites":
self.favorite_presets(args)
elif args.command == "import":
self.import_preset(args)
elif args.command == "apply":
self.apply_preset(args)
elif args.command == "new":
self.new_preset(args)
elif args.command == "download":
self.download_preset(args)
elif args.command == "monet":
self.generate_monet(args)
elif args.command == "access-file":
self.access_file(args)
elif args.command == "flatpak-overrides":
self.flatpak_theming(args)
def list_presets(self, args):
#_remove_preset = args.remove_preset
_json = args.json
if _json:
presets_list = PresetUtils().get_presets_list(json_output=True)
print(presets_list)
exit(0)
presets_list = PresetUtils().get_presets_list()
# TODO: Modify this output to look more like a table (maybe use ncurses?)
print("\033[1;37mPreset name\033[0m | \033[1;37mPreset path\033[0m")
for key in presets_list:
print(f"{presets_list[key]} -> {key}")
def favorite_presets(self, args):
_add_preset = args.add_preset
_remove_preset = args.remove_preset
_json = args.json
favorite = set(self.settings.get_value("favourite"))
presets_list = PresetUtils().get_presets_list()
presets_name = list(presets_list.values())
if _json and not _add_preset and not _remove_preset:
favorites_json = {"favorites": list(favorite), "amount": len(favorite)}
json_output = json.dumps(favorites_json)
print(json_output)
exit(0)
elif _json and _add_preset or _json and _remove_preset:
logging.error("JSON output option isn't available for --add-preset and --remove-preset options.")
exit(1)
if _add_preset:
if _add_preset in presets_name:
favorite.add(_add_preset)
self.settings.set_value("favourite", GLib.Variant("as", favorite))
logging.info(f"Preset {_add_preset} has been added to favorites.")
exit(0)
else:
logging.error(f"Preset named {_add_preset} isn't installed in Gradience. "
"Check if you typed the correct preset name, or try importing your preset using 'import' command.")
exit(1)
if _remove_preset:
if _remove_preset in favorite:
favorite.remove(_remove_preset)
self.settings.set_value("favourite", GLib.Variant("as", favorite))
logging.info(f"Preset {_add_preset} has been removed from favorites.")
exit(0)
else:
logging.error(f"Preset named {_remove_preset} doesn't exist in favorites list. "
"Check if you typed the correct preset name.")
exit(1)
logging.info("Favorite presets list:")
for i, preset in enumerate(favorite):
print(preset)
logging.info(f"Favorites amount: {len(favorite)}")
exit(0)
def import_preset(self, args):
_preset_path = args.preset_path
preset_file = GLib.path_get_basename(_preset_path)
logging.info(f"Importing preset: {preset_file.strip()}")
# TODO: Check if preset is already imported
if _preset_path.endswith(".json"):
try:
shutil.copy(
_preset_path,
os.path.join(
presets_dir,
"user",
preset_file.strip()
)
)
except FileNotFoundError as e:
logging.error(f"Preset could not be imported. Exc: {e}")
exit(1)
logging.info("Preset imported successfully.")
else:
logging.error("Unsupported file format, must be .json")
exit(1)
def apply_preset(self, args):
_preset_name = args.preset_name
_preset_path = args.preset_path
_gtk = args.gtk
#_flatpak = args.flatpak
presets_list = PresetUtils().get_presets_list()
presets_name = list(presets_list.values())
if _preset_name:
if _preset_name in presets_name:
for path, name in presets_list.items():
if name == _preset_name:
preset = Preset().new_from_path(path)
elif _preset_path:
preset = Preset().new_from_path(_preset_path)
if _gtk == "gtk4":
PresetUtils().apply_preset("gtk4", preset)
logging.info(f"Preset {preset.display_name} applied successfully for Gtk 4 applications.")
elif _gtk == "gtk3":
PresetUtils().apply_preset("gtk3", preset)
logging.info(f"Preset {preset.display_name} applied successfully for Gtk 3 applications.")
logging.info("In order for changes to take full effect, you need to log out.")
def new_preset(self, args):
#_interactive = args.interactive
_name = args.name
_colors = args.colors
_palette = args.palette
_custom_css = args.custom_css
_json = args.json
# TODO: Do the logic code for `new` command
logging.error("This command isn't implemented yet")
exit(1)
def download_preset(self, args):
#_interactive = args.interactive
_preset_name = args.preset_name
#_custom_url = args.custom_url
repo_no = 1
repos_amount = len(preset_repos.items())
for repo_name, repo in preset_repos.items():
try:
explore_presets, urls = PresetDownloader().fetch_presets(repo)
except (GLib.GError, json.JSONDecodeError) as e:
logging.error(f"An error occurred while fetching presets from remote repository. Exc: {e}")
exit(1)
else:
preset_no = 1
presets_amount = len(explore_presets.items())
for (preset, preset_name), preset_url in zip(explore_presets.items(), urls):
# TODO: Add handling of two or more presets with the same elements in name
if _preset_name.lower() in preset_name.lower():
logging.info(f"Downloading preset: {preset_name}")
try:
PresetDownloader().download_preset(preset_name, to_slug_case(repo_name), preset_url)
except (GLib.GError, json.JSONDecodeError, OSError) as e:
logging.error(f"An error occurred while downloading a preset. Exc: {e}")
exit(1)
else:
logging.info("Preset downloaded successfully.")
exit(0)
else:
if repo_no == repos_amount and preset_no == presets_amount:
logging.error(f"No presets found with text: {_preset_name}")
exit(1)
preset_no += 1
continue
repo_no += 1
# NOTE: Possible useful portals to use in future: org.freedesktop.portal.Documents (support missing in libportal, only D-Bus calls), org.freedesktop.portal.FileChooser
def generate_monet(self, args):
_preset_name = args.preset_name
_image_path = args.image_path
_tone = args.tone
_theme = args.theme
_json = args.json
palette = Monet().generate_from_image(_image_path)
props = [_tone, _theme]
if _json:
preset = PresetUtils().new_preset_from_monet(name=_preset_name, monet_palette=palette,
props=props, obj_only=True)
preset_json = preset.get_preset_json()
print(preset_json)
exit(0)
PresetUtils().new_preset_from_monet(_preset_name, palette, props)
logging.info("In order for changes to take full effect, you need to log out.")
# TODO: Add path and xdg-* values parsing
def access_file(self, args):
_list = args.list
_allow = args.allow
_disallow = args.disallow
if _list:
try:
access_list = list_file_access()
except GLib.GError as e:
logging.error(f"An error occurred while accessing allowed files list. Exc: {e}")
exit(1)
else:
logging.info("Allowed files:")
if access_list:
for value in access_list:
print(value)
exit(0)
else:
print("0 paths found")
exit(0)
if _allow:
try:
allow_file_access(_allow)
except GLib.GError as e:
logging.error(f"An error occurred while setting file access. Exc: {e}")
exit(1)
else:
logging.info(f"Path {_allow} added to access list")
exit(0)
if _disallow:
try:
disallow_file_access(_disallow)
except GLib.GError as e:
logging.error(f"An error occurred while setting file access. Exc: {e}")
exit(1)
else:
logging.info(f"Path {_disallow} removed from access list")
exit(0)
def flatpak_theming(self, args):
_enable_theming = args.enable_theming
_disable_theming = args.disable_theming
if _enable_theming == "gtk4":
create_gtk_user_override(self.settings, "gtk4")
logging.info("Flatpak theming for Gtk 4 applications has been enabled.")
elif _enable_theming == "gtk3":
create_gtk_user_override(self.settings, "gtk3")
logging.info("Flatpak theming for Gtk 3 applications has been enabled.")
elif _enable_theming == "both":
create_gtk_user_override(self.settings, "gtk4")
create_gtk_user_override(self.settings, "gtk3")
logging.info("Flatpak theming for Gtk 4 and Gtk 3 applications has been enabled.")
if _disable_theming == "gtk4":
remove_gtk_user_override(self.settings, "gtk4")
logging.info("Flatpak theming for Gtk 4 applications has been disabled.")
elif _disable_theming == "gtk3":
remove_gtk_user_override(self.settings, "gtk3")
logging.info("Flatpak theming for Gtk 3 applications has been disabled.")
elif _disable_theming == "both":
remove_gtk_user_override(self.settings, "gtk4")
remove_gtk_user_override(self.settings, "gtk3")
logging.info("Flatpak theming for Gtk 4 and Gtk 3 applications has been disabled.")
if __name__ == "__main__":
cli = CLI()

View file

@ -0,0 +1,22 @@
clidir = 'gradience/frontend/cli'
configure_file(
input: 'cli.in',
output: 'gradience-cli',
configuration: conf,
install: true,
install_dir: get_option('bindir')
)
configure_file(
input: 'cli.in',
output: 'gradience-cli',
configuration: local_conf,
install: true,
install_dir: join_paths(meson.project_build_root(), 'gradience', 'frontend')
)
gradience_sources = [
'__init__.py'
]
PY_INSTALLDIR.install_sources(gradience_sources, subdir: clidir)

View file

@ -25,8 +25,11 @@ from pathlib import Path
from material_color_utilities_python import *
from gi.repository import Gtk, Gdk, Gio, Adw, GLib, Xdp, XdpGtk4
from gradience.backend.globals import presets_dir
from gradience.backend.css_parser import parse_css
from gradience.backend.models.preset import Preset, presets_dir
from gradience.backend.models.preset import Preset
from gradience.backend.theming.preset_utils import PresetUtils
from gradience.backend.utils.colors import rgba_from_argb
from gradience.backend.utils.common import to_slug_case
from gradience.backend.constants import *
@ -288,7 +291,7 @@ class GradienceApplication(Adw.Application):
"palette": palette,
"custom_css": {"gtk4": custom_css},
}
self.preset = Preset(preset=preset)
self.preset = Preset().new_from_dict(preset)
self.load_preset_variables_from_preset()
except OSError: # fallback to adwaita
logging.warning("Custom preset not found. Fallback to Adwaita")
@ -320,13 +323,13 @@ class GradienceApplication(Adw.Application):
def load_preset_from_file(self, preset_path):
logging.debug(f"load preset from file {preset_path}")
self.preset = Preset(preset_path=preset_path)
self.preset = Preset().new_from_path(preset_path)
self.load_preset_variables_from_preset()
def load_preset_from_resource(self, preset_path):
preset_text = Gio.resources_lookup_data(
preset_path, 0).get_data().decode()
self.preset = Preset(text=preset_text)
self.preset = Preset().new_from_resource(text=preset_text)
self.load_preset_variables_from_preset()
def load_preset_variables_from_preset(self, preset=None):
@ -374,131 +377,27 @@ class GradienceApplication(Adw.Application):
self.reload_variables()
@staticmethod
def rgba_from_argb(argb, alpha=None) -> str:
base = "rgba({}, {}, {}, {})"
red = redFromArgb(argb)
green = greenFromArgb(argb)
blue = blueFromArgb(argb)
if not alpha:
alpha = alphaFromArgb(argb)
return base.format(red, green, blue, alpha)
def update_theme_from_monet(self, theme, tone, monet_theme):
palettes = theme["palettes"]
def update_theme_from_monet(self, monet, tone, monet_theme):
palettes = monet["palettes"]
monet_theme = monet_theme.get_string().lower() # dark / light
palette = {}
i = 0
for color in palettes.values():
i += 1
for i, color in zip(range(1, 7), palettes.values()):
palette[str(i)] = hexFromArgb(color.tone(int(tone.get_string())))
self.pref_palette_shades["monet"].update_shades(palette)
if monet_theme == "auto":
if self.style_manager.get_dark():
monet_theme = "dark"
else:
monet_theme = "light"
if monet_theme == "dark":
dark_theme = theme["schemes"]["dark"]
variable = {
"accent_color": self.rgba_from_argb(dark_theme.primary),
"accent_bg_color": self.rgba_from_argb(dark_theme.primaryContainer),
"accent_fg_color": self.rgba_from_argb(dark_theme.onPrimaryContainer),
"destructive_color": self.rgba_from_argb(dark_theme.error),
"destructive_bg_color": self.rgba_from_argb(dark_theme.errorContainer),
"destructive_fg_color": self.rgba_from_argb(
dark_theme.onErrorContainer
),
"success_color": self.rgba_from_argb(dark_theme.tertiary),
"success_bg_color": self.rgba_from_argb(dark_theme.onTertiary),
"success_fg_color": self.rgba_from_argb(dark_theme.onTertiaryContainer),
"warning_color": self.rgba_from_argb(dark_theme.secondary),
"warning_bg_color": self.rgba_from_argb(dark_theme.onSecondary),
"warning_fg_color": self.rgba_from_argb(dark_theme.primary, "0.8"),
"error_color": self.rgba_from_argb(dark_theme.error),
"error_bg_color": self.rgba_from_argb(dark_theme.errorContainer),
"error_fg_color": self.rgba_from_argb(dark_theme.onError),
"window_bg_color": self.rgba_from_argb(dark_theme.surface),
"window_fg_color": self.rgba_from_argb(dark_theme.onSurface),
"view_bg_color": self.rgba_from_argb(dark_theme.surface),
"view_fg_color": self.rgba_from_argb(dark_theme.onSurface),
"headerbar_bg_color": self.rgba_from_argb(dark_theme.surface),
"headerbar_fg_color": self.rgba_from_argb(dark_theme.onSurface),
"headerbar_border_color": self.rgba_from_argb(
dark_theme.primary, "0.8"
),
"headerbar_backdrop_color": "@headerbar_bg_color",
"headerbar_shade_color": self.rgba_from_argb(dark_theme.shadow),
"card_bg_color": self.rgba_from_argb(dark_theme.primary, "0.05"),
"card_fg_color": self.rgba_from_argb(dark_theme.onSecondaryContainer),
"card_shade_color": self.rgba_from_argb(dark_theme.shadow),
"dialog_bg_color": self.rgba_from_argb(dark_theme.secondaryContainer),
"dialog_fg_color": self.rgba_from_argb(dark_theme.onSecondaryContainer),
"popover_bg_color": self.rgba_from_argb(dark_theme.secondaryContainer),
"popover_fg_color": self.rgba_from_argb(
dark_theme.onSecondaryContainer
),
"shade_color": self.rgba_from_argb(dark_theme.shadow),
"scrollbar_outline_color": self.rgba_from_argb(dark_theme.outline),
}
else: # light
light_theme = theme["schemes"]["light"]
variable = {
"accent_color": self.rgba_from_argb(light_theme.primary),
"accent_bg_color": self.rgba_from_argb(light_theme.primary),
"accent_fg_color": self.rgba_from_argb(light_theme.onPrimary),
"destructive_color": self.rgba_from_argb(light_theme.error),
"destructive_bg_color": self.rgba_from_argb(light_theme.errorContainer),
"destructive_fg_color": self.rgba_from_argb(
light_theme.onErrorContainer
),
"success_color": self.rgba_from_argb(light_theme.tertiary),
"success_bg_color": self.rgba_from_argb(light_theme.tertiaryContainer),
"success_fg_color": self.rgba_from_argb(
light_theme.onTertiaryContainer
),
"warning_color": self.rgba_from_argb(light_theme.secondary),
"warning_bg_color": self.rgba_from_argb(light_theme.secondaryContainer),
"warning_fg_color": self.rgba_from_argb(
light_theme.onSecondaryContainer
),
"error_color": self.rgba_from_argb(light_theme.error),
"error_bg_color": self.rgba_from_argb(light_theme.errorContainer),
"error_fg_color": self.rgba_from_argb(light_theme.onError),
"window_bg_color": self.rgba_from_argb(light_theme.secondaryContainer),
"window_fg_color": self.rgba_from_argb(light_theme.onSurface),
"view_bg_color": self.rgba_from_argb(light_theme.secondaryContainer),
"view_fg_color": self.rgba_from_argb(light_theme.onSurface),
"headerbar_bg_color": self.rgba_from_argb(
light_theme.secondaryContainer
),
"headerbar_fg_color": self.rgba_from_argb(light_theme.onSurface),
"headerbar_border_color": self.rgba_from_argb(
light_theme.primary, "0.8"
),
"headerbar_backdrop_color": "@headerbar_bg_color",
"headerbar_shade_color": self.rgba_from_argb(
light_theme.secondaryContainer
),
"card_bg_color": self.rgba_from_argb(light_theme.primary, "0.05"),
"card_fg_color": self.rgba_from_argb(light_theme.onSecondaryContainer),
"card_shade_color": self.rgba_from_argb(light_theme.shadow),
"dialog_bg_color": self.rgba_from_argb(light_theme.secondaryContainer),
"dialog_fg_color": self.rgba_from_argb(
light_theme.onSecondaryContainer
),
"popover_bg_color": self.rgba_from_argb(light_theme.secondaryContainer),
"popover_fg_color": self.rgba_from_argb(
light_theme.onSecondaryContainer
),
"shade_color": self.rgba_from_argb(light_theme.shadow),
"scrollbar_outline_color": self.rgba_from_argb(light_theme.outline),
}
preset_object = PresetUtils().new_preset_from_monet(monet_palette=monet,
props=[tone, monet_theme], obj_only=True)
variable = preset_object.variables
for key in variable:
if key in self.pref_variables:
@ -506,16 +405,6 @@ class GradienceApplication(Adw.Application):
self.reload_variables()
def generate_gtk_css(self, app_type):
final_css = ""
for key in self.variables.keys():
final_css += f"@define-color {key} {self.variables[key]};\n"
for prefix_key in self.palette.keys():
for key in self.palette[prefix_key].keys():
final_css += f"@define-color {prefix_key + key} {self.palette[prefix_key][key]};\n"
final_css += self.custom_css.get(app_type, "")
return final_css
def mark_as_dirty(self):
self.is_dirty = True
self.props.active_window.save_preset_button.get_child().set_icon_name(
@ -540,7 +429,7 @@ class GradienceApplication(Adw.Application):
def reload_variables(self):
parsing_errors = []
gtk_css = self.generate_gtk_css("gtk4")
gtk_css = PresetUtils().generate_gtk_css("gtk4", self.preset)
css_provider = Gtk.CssProvider()
def on_error(_, section, error):
@ -616,6 +505,7 @@ class GradienceApplication(Adw.Application):
Adw.ResponseAppearance.DESTRUCTIVE,
transient_for=self.props.active_window,
)
dialog.gtk3_app_type.set_sensitive(False)
dialog.connect("response", self.restore_color_scheme)
dialog.present()
@ -782,7 +672,7 @@ class GradienceApplication(Adw.Application):
def save_preset(self, _unused, response, preset_entry):
if response == "save":
self.preset.save_preset(preset_entry.get_text(), self.plugins_list)
self.preset.save_to_file(preset_entry.get_text(), self.plugins_list)
self.clear_dirty()
self.win.toast_overlay.add_toast(
Adw.Toast(title=_("Preset saved")))
@ -793,60 +683,10 @@ class GradienceApplication(Adw.Application):
def apply_color_scheme(self, widget, response):
if response == "apply":
if widget.get_app_types()["gtk4"]:
gtk4_dir = os.path.join(
os.environ.get("XDG_CONFIG_HOME",
os.environ["HOME"] + "/.config"),
"gtk-4.0",
)
if not os.path.exists(gtk4_dir):
os.makedirs(gtk4_dir)
gtk4_css = self.generate_gtk_css("gtk4")
contents = ""
try:
with open(
os.path.join(gtk4_dir, "gtk.css"), "r", encoding="utf-8"
) as file:
contents = file.read()
except FileNotFoundError: # first run
pass
else:
with open(
os.path.join(gtk4_dir, "gtk.css.bak"), "w", encoding="utf-8"
) as file:
file.write(contents)
finally:
with open(
os.path.join(gtk4_dir, "gtk.css"), "w", encoding="utf-8"
) as file:
file.write(gtk4_css)
PresetUtils().apply_preset("gtk4", self.preset)
if widget.get_app_types()["gtk3"]:
gtk3_dir = os.path.join(
os.environ.get("XDG_CONFIG_HOME",
os.environ["HOME"] + "/.config"),
"gtk-3.0",
)
if not os.path.exists(gtk3_dir):
os.makedirs(gtk3_dir)
gtk3_css = self.generate_gtk_css("gtk3")
contents = ""
try:
with open(
os.path.join(gtk3_dir, "gtk.css"), "r", encoding="utf-8"
) as file:
contents = file.read()
except FileNotFoundError: # first run
pass
else:
with open(
os.path.join(gtk3_dir, "gtk.css.bak"), "w", encoding="utf-8"
) as file:
file.write(contents)
finally:
with open(
os.path.join(gtk3_dir, "gtk.css"), "w", encoding="utf-8"
) as file:
file.write(gtk3_css)
PresetUtils().apply_preset("gtk3", self.preset)
self.reload_plugins()
self.plugins_list.apply()
@ -855,11 +695,12 @@ class GradienceApplication(Adw.Application):
Adw.Toast(title=_("Preset set successfully"))
)
# TODO: Make it as a seperate widget
dialog = Adw.MessageDialog(
transient_for=self.props.active_window,
heading=_("Log out"),
body=_(
"For the changes to take effect, you need to log out. "
"For the changes to take full effect, you need to log out."
),
body_use_markup=True,
)
@ -873,47 +714,14 @@ class GradienceApplication(Adw.Application):
def on_theme_set_dialog_response (self, dialog, response):
if response == "ok":
print("theme_set_dialog_ok")
logging.debug("theme_set_dialog_ok")
def restore_color_scheme(self, widget, response):
if response == "restore":
if widget.get_app_types()["gtk4"]:
file = Gio.File.new_for_path(
os.path.join(
os.environ.get(
"XDG_CONFIG_HOME", os.environ["HOME"] + "/.config"
),
"gtk-4.0/gtk.css.bak",
)
)
try:
backup = open(
os.path.join(
os.environ.get(
"XDG_CONFIG_HOME", os.environ["HOME"] +
"/.config"
),
"gtk-4.0/gtk.css.bak",
),
"r",
encoding="utf-8",
)
contents = backup.read()
backup.close()
gtk4css = open(
os.path.join(
os.environ.get(
"XDG_CONFIG_HOME", os.environ["HOME"] +
"/.config"
),
"gtk-4.0/gtk.css",
),
"w",
encoding="utf-8",
)
gtk4css.write(contents)
gtk4css.close()
except FileNotFoundError:
PresetUtils().restore_gtk4_preset()
except Exception:
self.win.toast_overlay.add_toast(
Adw.Toast(title=_("Unable to restore GTK 4 backup"))
)
@ -922,7 +730,7 @@ class GradienceApplication(Adw.Application):
transient_for=self.props.active_window,
heading=_("Log out"),
body=_(
"For the changes to take effect, you need to log out. "
"For the changes to take full effect, you need to log out."
),
body_use_markup=True,
)
@ -936,37 +744,21 @@ class GradienceApplication(Adw.Application):
def on_theme_restore_dialog_response (self, dialog, response):
if response == "ok":
print("theme_restore_dialog_ok")
logging.debug("theme_restore_dialog_ok")
def reset_color_scheme(self, widget, response):
if response == "reset":
if widget.get_app_types()["gtk4"]:
file = Gio.File.new_for_path(
os.path.join(
os.environ.get(
"XDG_CONFIG_HOME", os.environ["HOME"] + "/.config"
),
"gtk-4.0/gtk.css",
)
)
try:
file.delete()
PresetUtils().reset_preset("gtk4")
except Exception:
self.win.toast_overlay.add_toast(
Adw.Toast(title=_("Unable to delete current preset"))
)
if widget.get_app_types()["gtk3"]:
file = Gio.File.new_for_path(
os.path.join(
os.environ.get(
"XDG_CONFIG_HOME", os.environ["HOME"] + "/.config"
),
"gtk-3.0/gtk.css",
)
)
try:
file.delete()
PresetUtils().reset_preset("gtk3")
except Exception:
self.win.toast_overlay.add_toast(
Adw.Toast(title=_("Unable to delete current preset"))
@ -976,7 +768,7 @@ class GradienceApplication(Adw.Application):
transient_for=self.props.active_window,
heading=_("Log out"),
body=_(
"For the changes to take effect, you need to log out. "
"For the changes to take full effect, you need to log out."
),
body_use_markup=True,
)
@ -990,13 +782,14 @@ class GradienceApplication(Adw.Application):
def on_theme_reset_dialog_response (self, dialog, response):
if response == "ok":
print("theme_reset_dialog_ok")
logging.debug("theme_reset_dialog_ok")
def show_preferences(self, *_args):
prefs = GradiencePreferencesWindow(self.win)
prefs.set_transient_for(self.win)
prefs.present()
# TODO: Move it to seperate frontend module
def show_about_window(self, *_args):
about = Adw.AboutWindow(
transient_for=self.props.active_window,

View file

@ -13,6 +13,7 @@ configure_file(
configuration: local_conf
)
subdir('cli')
subdir('dialogs')
subdir('utils')
subdir('views')

View file

@ -23,6 +23,7 @@ from reportlab.graphics import renderPM
from material_color_utilities_python import *
from gi.repository import Gtk, Adw, Gio
from gradience.backend.theming.monet import Monet
from gradience.backend.constants import rootdir, app_id, build_type
from gradience.frontend.widgets.error_list_row import GradienceErrorListRow
@ -70,12 +71,12 @@ class GradienceMainWindow(Adw.ApplicationWindow):
self.connect("unrealize", self.save_window_props)
self.style_manager = self.get_application().style_manager
self.first_apply = True
#self.first_apply = True
self.get_default_wallpaper()
# FIXME: This function works only when building using meson, because Flatpak
# can't access host's dconf with current config/impl
# TODO: Check if org.freedesktop.portal.Settings portal will allow us to \
# read org.gnome.desktop.background DConf key
def get_default_wallpaper(self):
background_settings = Gio.Settings("org.gnome.desktop.background")
if self.style_manager.get_dark():
@ -210,39 +211,23 @@ class GradienceMainWindow(Adw.ApplicationWindow):
def on_apply_button(self, *_args):
if self.monet_image_file:
if self.monet_image_file.endswith(".svg"):
drawing = svg2rlg(self.monet_image_file)
self.monet_image_file = os.path.join(
os.environ.get("XDG_RUNTIME_DIR"), "gradience_bg.png"
)
renderPM.drawToFile(drawing, self.monet_image_file, fmt="PNG")
if self.monet_image_file.endswith(".xml"):
logging.debug("XML WIP")
try:
self.monet_img = Image.open(self.monet_image_file)
except Exception:
self.toast_overlay.add_toast(
Adw.Toast(title=_("Unsupported background type"))
)
else:
basewidth = 64
wpercent = basewidth / float(self.monet_img.size[0])
hsize = int((float(self.monet_img.size[1]) * float(wpercent)))
self.monet_img = self.monet_img.resize(
(basewidth, hsize), Image.Resampling.LANCZOS
)
self.theme = themeFromImage(self.monet_img)
self.theme = Monet().generate_from_image(self.monet_image_file)
self.tone = self.tone_row.get_selected_item()
self.monet_theme = self.monet_theme_row.get_selected_item()
self.get_application().update_theme_from_monet(
self.theme, self.tone, self.monet_theme
)
if not self.first_apply:
self.toast_overlay.add_toast(
Adw.Toast(title=_("Palette generated"))
)
except Exception as e:
logging.error(f"Failed to generate Monet palette. Exc: {e}")
self.toast_overlay.add_toast(
Adw.Toast(title=_("Failed to generate Monet palette"))
)
else:
self.toast_overlay.add_toast(
Adw.Toast(title=_("Palette generated"))
)
else:
self.toast_overlay.add_toast(
Adw.Toast(title=_("Select a background first"))

View file

@ -84,9 +84,9 @@ class GradiencePreferencesWindow(Adw.PreferencesWindow):
state = self.allow_gtk4_flatpak_theming_user.props.state
if not state:
create_gtk_user_override(self, self.settings, "gtk4")
create_gtk_user_override(self.settings, "gtk4", self)
else:
remove_gtk_user_override(self, self.settings, "gtk4")
remove_gtk_user_override(self.settings, "gtk4", self)
logging.debug(
f"user-flatpak-theming-gtk4: {self.settings.get_boolean('user-flatpak-theming-gtk4')}"
@ -96,9 +96,9 @@ class GradiencePreferencesWindow(Adw.PreferencesWindow):
state = self.allow_gtk3_flatpak_theming_user.props.state
if not state:
create_gtk_user_override(self, self.settings, "gtk3")
create_gtk_user_override(self.settings, "gtk3", self)
else:
remove_gtk_user_override(self, self.settings, "gtk3")
remove_gtk_user_override(self.settings, "gtk3", self)
logging.debug(
f"user-flatpak-theming-gtk3: {self.settings.get_boolean('user-flatpak-theming-gtk3')}"
@ -108,9 +108,9 @@ class GradiencePreferencesWindow(Adw.PreferencesWindow):
state = self.allow_gtk4_flatpak_theming_global.props.state
if not state:
create_gtk_global_override(self, self.settings, "gtk4")
create_gtk_global_override(self.settings, "gtk4", self)
else:
remove_gtk_global_override(self, self.settings, "gtk4")
remove_gtk_global_override(self.settings, "gtk4", self)
logging.debug(
f"global-flatpak-theming-gtk4: {self.settings.get_boolean('global-flatpak-theming-gtk4')}"
@ -120,9 +120,9 @@ class GradiencePreferencesWindow(Adw.PreferencesWindow):
state = self.allow_gtk3_flatpak_theming_global.props.state
if not state:
create_gtk_global_override(self, self.settings, "gtk3")
create_gtk_global_override(self.settings, "gtk3", self)
else:
remove_gtk_global_override(self, self.settings, "gtk3")
remove_gtk_global_override(self.settings, "gtk3", self)
logging.debug(
f"global-flatpak-theming-gtk3: {self.settings.get_boolean('global-flatpak-theming-gtk3')}"

View file

@ -24,8 +24,8 @@ from collections import OrderedDict
from pathlib import Path
from gi.repository import Gtk, Adw, GLib
from gradience.backend.preset_downloader import fetch_presets
from gradience.backend.models.preset import presets_dir
from gradience.backend.preset_downloader import PresetDownloader
from gradience.backend.globals import presets_dir, preset_repos
from gradience.backend.constants import rootdir
from gradience.frontend.widgets.preset_row import GradiencePresetRow
@ -65,11 +65,6 @@ class GradiencePresetWindow(Adw.Window):
custom_presets = {}
official_repositories = {
"Official": "https://github.com/GradienceTeam/Community/raw/next/official.json",
"Curated": "https://github.com/GradienceTeam/Community/raw/next/curated.json",
}
search_results_list = []
offline = False
@ -146,9 +141,19 @@ class GradiencePresetWindow(Adw.Window):
else:
badge = "white"
explore_presets, urls = fetch_presets(repo)
if explore_presets:
try:
explore_presets, urls = PresetDownloader().fetch_presets(repo)
except GLib.GError as e:
if e.code == 1:
self.offline = True
self.search_spinner.props.visible = False
self.search_stack.set_visible_child_name("page_offline")
else:
self.search_spinner.props.visible = False
# TODO: Create a new page to show for other errors eg. "page_error"
except json.JSONDecodeError as e:
self.search_spinner.props.visible = False
else:
self.search_spinner.props.visible = False
for (preset, preset_name), preset_url in zip(
@ -159,10 +164,6 @@ class GradiencePresetWindow(Adw.Window):
)
self.search_results.append(row)
self.search_results_list.append(row)
else:
self.offline = True
self.search_spinner.props.visible = False
self.search_stack.set_visible_child_name("page_offline")
def add_repo(self, _unused, response, name_entry, url_entry):
if response == "add":
@ -315,6 +316,7 @@ class GradiencePresetWindow(Adw.Window):
"pretty-purple": "Pretty Purple",
}
# TODO: Move this from frontend to backend
for repo in Path(presets_dir).iterdir():
logging.debug(f"presets_dir.iterdir: {repo}")
if repo.is_dir(): # repo
@ -426,7 +428,7 @@ class GradiencePresetWindow(Adw.Window):
self.repos_list = Adw.PreferencesGroup()
self.repos_list.set_title(_("Repositories"))
for repo_name, repo in self.official_repositories.items():
for repo_name, repo in preset_repos.items():
row = GradienceRepoRow(repo, repo_name, self, deletable=False)
self.repos_list.add(row)
@ -436,4 +438,4 @@ class GradiencePresetWindow(Adw.Window):
self.repos.add(self.repos_list)
self._repos = {**self.user_repositories, **self.official_repositories}
self._repos = {**self.user_repositories, **preset_repos}

View file

@ -176,7 +176,7 @@ class GradienceWelcomeWindow(Adw.Window):
self.allow_flatpak_theming_user_toggled()
def allow_flatpak_theming_user_toggled(self, *args):
create_gtk_user_override(self, self.gio_settings, "gtk4")
create_gtk_user_override(self.gio_settings, "gtk4", self)
def install_runner(self, widget):
def set_completed(result, error=False):

View file

@ -21,7 +21,7 @@ import os
from gi.repository import Gtk, Adw
from gradience.backend.utils.common import to_slug_case
from gradience.backend.preset_downloader import download_preset
from gradience.backend.preset_downloader import PresetDownloader
from gradience.backend.constants import rootdir
from gradience.backend.logger import Logger
@ -60,7 +60,7 @@ class GradienceExplorePresetRow(Adw.ActionRow):
@Gtk.Template.Callback()
def on_apply_button_clicked(self, *_args):
try:
download_preset(to_slug_case(self.name), self.prefix, self.url)
PresetDownloader().download_preset(to_slug_case(self.name), self.prefix, self.url)
except Exception as e:
self.toast_overlay.add_toast(
Adw.Toast(title=_("Preset could not be downloaded"))
@ -86,7 +86,7 @@ class GradienceExplorePresetRow(Adw.ActionRow):
@Gtk.Template.Callback()
def on_download_button_clicked(self, *_args):
try:
download_preset(to_slug_case(self.name), self.prefix, self.url)
PresetDownloader().download_preset(to_slug_case(self.name), self.prefix, self.url)
except Exception as e:
self.toast_overlay.add_toast(
Adw.Toast(title=_("Preset could not be downloaded"))

View file

@ -22,7 +22,7 @@ from gi.repository import Gtk, Adw, Xdp, XdpGtk4
from gradience.frontend.views.share_window import GradienceShareWindow
from gradience.backend.utils.common import to_slug_case
from gradience.backend.models.preset import Preset, presets_dir
from gradience.backend.models.preset import Preset
from gradience.backend.constants import rootdir
from gradience.backend.logger import Logger
@ -61,7 +61,7 @@ class GradiencePresetRow(Adw.ExpanderRow):
self.win = win
self.toast_overlay = self.win.toast_overlay
self.preset = Preset(preset_path)
self.preset = Preset().new_from_path(preset_path)
if self.preset.badges:
self.has_badges = True
@ -124,7 +124,7 @@ class GradiencePresetRow(Adw.ExpanderRow):
if self.name_entry_toggle.get_active():
self.value_stack.set_visible_child(self.name_entry)
else:
self.preset.rename_preset(self.name_entry.get_text())
self.preset.rename(self.name_entry.get_text())
self.value_stack.set_visible_child(self.apply_button)
def on_report_btn_clicked(self, *_args):

21
local_cli.sh Executable file
View file

@ -0,0 +1,21 @@
#!/usr/bin/bash
# local_cli.sh
#
# Change the look of Adwaita, with ease
# Copyright (C) 2022 Gradience Team
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
python builddir/gradience/frontend/gradience-cli "$@"