mirror of
https://github.com/GradienceTeam/Gradience.git
synced 2024-08-22 15:32:32 +00:00
Introduce a command-line interface (#675)
This PR introduces a CLI (command-line interface) for Gradience.
This commit is contained in:
commit
b5a4dc07fe
28 changed files with 1527 additions and 484 deletions
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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()
|
||||
|
|
58
gradience/backend/globals.py
Normal file
58
gradience/backend/globals.py
Normal 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
|
|
@ -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 = []
|
||||
|
|
|
@ -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'
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
0
gradience/backend/theming/__init__.py
Normal file
0
gradience/backend/theming/__init__.py
Normal file
8
gradience/backend/theming/meson.build
Normal file
8
gradience/backend/theming/meson.build
Normal 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)
|
63
gradience/backend/theming/monet.py
Normal file
63
gradience/backend/theming/monet.py
Normal 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
|
366
gradience/backend/theming/preset_utils.py
Normal file
366
gradience/backend/theming/preset_utils.py
Normal 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
|
31
gradience/backend/utils/colors.py
Normal file
31
gradience/backend/utils/colors.py
Normal 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)
|
|
@ -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):
|
||||
|
|
|
@ -2,6 +2,7 @@ utilsdir = 'gradience/backend/utils'
|
|||
|
||||
gradience_sources = [
|
||||
'__init__.py',
|
||||
'colors.py',
|
||||
'common.py'
|
||||
]
|
||||
PY_INSTALLDIR.install_sources(gradience_sources, subdir: utilsdir)
|
||||
|
|
0
gradience/frontend/cli/__init__.py
Normal file
0
gradience/frontend/cli/__init__.py
Normal file
416
gradience/frontend/cli/cli.in
Executable file
416
gradience/frontend/cli/cli.in
Executable 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()
|
22
gradience/frontend/cli/meson.build
Normal file
22
gradience/frontend/cli/meson.build
Normal 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)
|
|
@ -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,
|
||||
|
|
|
@ -13,6 +13,7 @@ configure_file(
|
|||
configuration: local_conf
|
||||
)
|
||||
|
||||
subdir('cli')
|
||||
subdir('dialogs')
|
||||
subdir('utils')
|
||||
subdir('views')
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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')}"
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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
21
local_cli.sh
Executable 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 "$@"
|
Loading…
Reference in a new issue