diff --git a/main.py b/main.py index 5ae5e72c..a1a14084 100755 --- a/main.py +++ b/main.py @@ -62,7 +62,7 @@ def main(arguments): logger.debug(f'Using language {lang}') # system translations - path = QLibraryInfo.location(QLibraryInfo.TranslationsPath) + path = QLibraryInfo.path(QLibraryInfo.TranslationsPath) translator = QTranslator(app) if translator.load(QLocale.system(), 'qtbase', '_', path): app.installTranslator(translator) diff --git a/scripts/install.sh b/scripts/install.sh index 4965d3ed..1f691161 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -44,10 +44,7 @@ cp ./resources/yin-yang /usr/bin/ cp ./resources/Yin-Yang.desktop "$USER_HOME/.local/share/applications/Yin-Yang.desktop" # copy icon cp ./resources/logo.svg /usr/share/icons/hicolor/scalable/apps/yin_yang.svg -# systemd unit files -mkdir -p "$USER_HOME/.local/share/systemd/user/" -cp ./resources/yin_yang.service "$USER_HOME/.local/share/systemd/user/yin_yang.service" -cp ./resources/yin_yang.timer "$USER_HOME/.local/share/systemd/user/yin_yang.timer" +# systemd unit files will be installed by the app cat << "EOF" __ ___ __ __ diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index d31cd5ab..4f73420c 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -23,5 +23,9 @@ rm -rf /opt/yin-yang /usr/bin/yin-yang echo "Removing manifest" rm -f /usr/lib/mozilla/native-messaging-hosts/yin_yang.json +echo "Removing systemd units" +rm -f "$HOME/.local/share/systemd/user/yin_yang.timer" +rm -f "$HOME/.local/share/systemd/user/yin_yang.service" + echo Yin-Yang uninstalled succesfully echo have a nice day ... diff --git a/src/config.py b/src/config.py index 5e953e28..8db1eaca 100755 --- a/src/config.py +++ b/src/config.py @@ -3,18 +3,18 @@ import os import pathlib from abc import ABC, abstractmethod +from datetime import time from functools import cache from time import sleep +from typing import Union, Optional from PySide6.QtCore import QObject from PySide6.QtPositioning import QGeoPositionInfoSource, QGeoPositionInfo, QGeoCoordinate from psutil import process_iter, NoSuchProcess -from datetime import time -from typing import Union, Optional - from suntime import Sun, SunTimeException -from src.plugins import get_plugins + from src.meta import Modes, Desktop, PluginKey, ConfigEvent +from src.plugins import get_plugins logger = logging.getLogger(__name__) @@ -71,6 +71,7 @@ def update_config(config_old: dict, defaults: dict): plugin_settings: dict = defaults['plugins'] for plugin_name, plugin_config in plugin_settings.items(): update_plugin_config(config_old, plugin_config, plugin_name) + return config_new diff --git a/src/plugins/konsole.py b/src/plugins/konsole.py index 70183d89..a771248c 100644 --- a/src/plugins/konsole.py +++ b/src/plugins/konsole.py @@ -1,7 +1,11 @@ import logging import os +import re import subprocess +from configparser import ConfigParser +from itertools import chain from pathlib import Path +from shutil import copyfile import psutil from PySide6.QtDBus import QDBusConnection, QDBusMessage @@ -18,6 +22,7 @@ class Konsole(Plugin): This is necessary to allow live theme changes. """ global_path = Path('/usr/share/konsole') + config_path = Path.home() / '.config/konsolerc' @property def user_path(self) -> Path: @@ -25,10 +30,37 @@ def user_path(self) -> Path: def __init__(self): super().__init__() - self.theme_light = 'BlackOnWhite' - self.theme_dark = 'Breeze' + self._theme_light = 'BlackOnWhite' + self._theme_dark = 'Breeze' + + @property + def theme_light(self): + return self._theme_light + + @theme_light.setter + def theme_light(self, value): + self.update_profile(False, value) + self._theme_light = value + + @property + def theme_dark(self): + return self._theme_dark + + @theme_dark.setter + def theme_dark(self, value): + self.update_profile(True, value) + self._theme_dark = value + + def set_mode(self, dark: bool) -> bool: + # run checks + if not super().set_mode(dark): + return False + + profile = 'Dark' if dark else 'Light' + + # update default profile, if application is started afterward + self.default_profile = profile + '.profile' - def set_theme(self, theme: str): # Set Konsole profile for all sessions # Get the process IDs of all running Konsole instances owned by the current user @@ -39,47 +71,157 @@ def set_theme(self, theme: str): # loop: console processes for proc_id in process_ids: - set_profile(f'org.kde.konsole-{proc_id}', theme) + logger.debug(f'Changing profile in konsole session {proc_id}') + set_profile(f'org.kde.konsole-{proc_id}', profile) + + set_profile('org.kde.yakuake', profile) + + process_ids = [ + proc.pid for proc in psutil.process_iter(['name', 'username']) + if proc.info['name'] == 'dolphin' and proc.info['username'] == os.getlogin() + ] + + # loop: dolphin processes + for proc_id in process_ids: + logger.debug(f'Changing profile in dolphin session {proc_id}') + set_profile(f'org.kde.dolphin-{proc_id}', profile) - set_profile('org.kde.yakuake', theme) + return True + + def set_theme(self, theme: str): + # everything is done in set_mode (above) + pass @property def available_themes(self) -> dict: if not self.available: return {} - profile_paths = [ - p.name.removesuffix('.profile') for p in self.user_path.iterdir() - if p.is_file() and p.suffix == '.profile' - ] + themes = dict(sorted([ + (p.with_suffix('').name, p) + for p in chain(self.global_path.iterdir(), self.user_path.iterdir()) + if p.is_file() and p.suffix == '.colorscheme' + ])) - return {profile: profile for profile in profile_paths} + themes_dict = {} + config_parser = ConfigParser() - def get_input(self, widget): - input_widgets = super().get_input(widget) - for widget in input_widgets: - widget.setToolTip( - 'Select a profile. ' - 'Create new profiles or edit existing ones within Konsole to change the color scheme.' - ) + for theme, theme_path in themes.items(): + config_parser.read(theme_path) + theme_name = config_parser['General']['Description'] + themes_dict[theme] = theme_name - return input_widgets + assert themes_dict != {}, 'No themes found!' + return themes_dict @property def available(self) -> bool: return self.global_path.is_dir() + @property + def default_profile(self): + value = None + # cant use config parser because of weird file structure + with self.config_path.open('r') as file: + for line in file: + # Search for the pattern "DefaultProfile=*" + match = re.search(r'DefaultProfile=(.*)', line) + + # If a match is found, return the content of the wildcard '*' + if match: + value = match.group(1) + + if value is None: + # use the first found profile + for file in self.user_path.iterdir(): + if file.suffix == '.profile': + value = file.name + break + if value is not None: + logger.warning(f'No default profile found, using {value} instead.') + else: + raise ValueError('No Konsole profile found.') + + return value + + @default_profile.setter + def default_profile(self, value: str): + assert value.endswith('.profile') + + with self.config_path.open('r') as file: + lines = file.readlines() + for i, line in enumerate(lines): + # Search for the pattern "DefaultProfile=*" + match = re.search(r'DefaultProfile=(.*)', line) + + # If a match is found, return the content of the wildcard '*' + if match: + lines[i] = f'DefaultProfile={value}\n' + break + with self.config_path.open('w') as file: + file.writelines(lines) + + def update_profile(self, dark: bool, theme: str): + if not self.available or theme == '': + # theme is empty string on super init + return + + # update the color scheme setting in either dark or light profile + logger.debug('Updating konsole profile') + + file_path = self.user_path / ('Dark.profile' if dark else 'Light.profile') + if not file_path.exists(): + self.create_profiles() + + profile_config = ConfigParser() + profile_config.optionxform = str + profile_config.read(file_path) + profile_config['Appearance']['ColorScheme'] = theme + with open(file_path, 'w') as file: + profile_config.write(file) + + def create_profiles(self): + logger.debug('Creating new profiles for live-switching between light and dark themes.') + # copy default profile to create theme profiles + light_profile = self.user_path / 'Light.profile' + dark_profile = self.user_path / 'Dark.profile' + # TODO there is a parent profile section in the profile file, maybe we can use that (in a later version)? + copyfile(self.user_path / self.default_profile, light_profile) + copyfile(self.user_path / self.default_profile, dark_profile) + + # Change name in file + profile_config = ConfigParser() + profile_config.optionxform = str + + profile_config.read(light_profile) + profile_config['General']['Name'] = light_profile.stem + + with open(light_profile, 'w') as file: + profile_config.write(file) + + profile_config.read(dark_profile) + profile_config['General']['Name'] = dark_profile.stem + + with open(dark_profile, 'w') as file: + profile_config.write(file) + def set_profile(service: str, profile: str): # connect to the session bus connection = QDBusConnection.sessionBus() # maybe it's possible with pyside6 dbus packages, but this was simpler and worked - sessions = subprocess.check_output(f'qdbus {service} | grep "Sessions/"', shell=True) + try: + sessions = subprocess.check_output(f'qdbus {service} | grep "Sessions/"', shell=True) + except subprocess.CalledProcessError: + # happens when dolphins konsole is not opened + logger.debug(f'No Konsole sessions available in service {service}, skipping') + return sessions = sessions.decode('utf-8').removesuffix('\n').split('\n') # loop: process sessions for session in sessions: + logger.debug(f'Changing profile of session {session} to {profile}') # set profile message = QDBusMessage.createMethodCall( service, diff --git a/src/yin_yang.py b/src/yin_yang.py index 7106077b..ebb2c971 100755 --- a/src/yin_yang.py +++ b/src/yin_yang.py @@ -1,4 +1,3 @@ - """ title: yin_yang description: yin_yang provides an easy way to toggle between light and dark @@ -13,6 +12,8 @@ import time from threading import Thread +from src.plugins.notify import Notification +from src.plugins.sound import Sound from src.daemon_handler import update_times from src.meta import PluginKey from src.config import config, plugins @@ -39,6 +40,9 @@ def set_mode(dark: bool, force=False): logger.info(f'Switching to {"dark" if dark else "light"} mode.') for p in plugins: if config.get_plugin_key(p.name, PluginKey.ENABLED): + if force and (isinstance(p, Sound) or isinstance(p, Notification)): + # skip sound and notify on apply settings + continue try: logger.info(f'Changing theme in plugin {p.name}') p_thread = Thread(target=p.set_mode, args=[dark], name=p.name)