diff --git a/.gitignore b/.gitignore index aa9a011a..a3ea2b16 100755 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ __pycache__ build-test* build-ui-* .vscode -src/build.py +yin_yang/build.py setup.py .idea/ diff --git a/README.md b/README.md index 815b838c..7798c536 100755 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Yin-Yang can be downloaded from AUR as [yin-yang](https://aur.archlinux.org/pack ### Source -Yin-Yang depends on `python-systemd` and `pyside6` from pypi. `python-systemd` requires you have installed the systemd-headers from your package manager. +Yin-Yang depends on `python-systemd` and `pyside6` from pypi. `python-systemd` requires you have installed the systemd-headers from your package manager. You also need python development headers (e.g. `python3-devel`). For CentOS, RHEL, and Fedora: ```bash @@ -68,21 +68,20 @@ Then you can install Yin-Yang in a python virtual environment: ```bash # bash is necessary to run the source command bash -# Removes any already present Yin-Yang code -rm -rf Yin-Yang # Clones the code to your local machine git clone https://github.com/oskarsh/Yin-Yang.git -# Enters the directory containing Yin-Yang's code -if pwd != "Yin-Yang"; then cd Yin-Yang; fi -## Creates a virtual environment for pypi (pip) packages -python3 -m venv .venv -source .venv/bin/activate -# Installs pip requirements specified in repository -pip3 install -r requirements.txt +cd Yin-Yang # Installs Yin-Yang ./scripts/install.sh ``` +For development, skip the install and instead create a venv in your home directory: +```bash +python -m venv .venv +source .venv/bin/activate # this is for bash, there are similar scripts in the that directory for other shells like fish +pip install -r requirements.txt +``` + ## Documentation Want to help out? Check out the wiki to learn how to contribute translations, plugins and more! diff --git a/designer/main_window.ui b/designer/main_window.ui index fabc5f4e..22e8ed8d 100755 --- a/designer/main_window.ui +++ b/designer/main_window.ui @@ -43,7 +43,7 @@ - 1 + 0 @@ -276,6 +276,26 @@ + + + + + + + Light + + + + + + + Dark + + + + + + @@ -290,6 +310,30 @@ + + + + + + Time to wait until the system finished booting. Default value is 10 seconds. + + + Delay after boot: + + + + + + + s + + + 10 + + + + + @@ -330,8 +374,8 @@ 0 0 - 523 - 663 + 518 + 88 @@ -340,16 +384,19 @@ - Sample Plugin + Sample Plugin + + + QComboBox::AdjustToContentsOnFirstShow - firefox-compact-light@mozilla.org + firefox-compact-light@mozilla.org @@ -362,7 +409,7 @@ QComboBox::AdjustToContentsOnFirstShow - firefox-compact-dark@mozilla.org + firefox-compact-dark@mozilla.org @@ -402,12 +449,12 @@ setVisible(bool) - 273 - 308 + 289 + 225 - 254 - 379 + 270 + 353 @@ -418,12 +465,12 @@ setVisible(bool) - 114 - 192 + 130 + 109 - 219 - 280 + 235 + 197 @@ -434,8 +481,8 @@ setVisible(bool) - 188 - 153 + 198 + 66 188 @@ -443,5 +490,21 @@ + + btn_enable + toggled(bool) + manual_buttons + setHidden(bool) + + + 109 + 58 + + + 274 + 398 + + + diff --git a/main.py b/main.py deleted file mode 100755 index a1a14084..00000000 --- a/main.py +++ /dev/null @@ -1,107 +0,0 @@ -#!/bin/env python3 - -import sys -import logging -from argparse import ArgumentParser -from logging.handlers import RotatingFileHandler -from pathlib import Path - -from PySide6 import QtWidgets -from PySide6.QtCore import QTranslator, QLibraryInfo, QLocale -from systemd import journal - -from src import daemon_handler -from src.meta import ConfigEvent -from src import yin_yang -from src.config import config, Modes -from src.ui import main_window_connector - -logger = logging.getLogger() - - -def setup_logger(use_systemd_journal: bool): - if use_systemd_journal: - logger.addHandler(journal.JournalHandler(SYSLOG_IDENTIFIER='yin_yang')) - - # __debug__ is true when you run main.py without the -O argument (python main.py) - # noinspection PyUnreachableCode - if __debug__: - # noinspection SpellCheckingInspection - logging.basicConfig( - level=logging.DEBUG, - format='%(asctime)s %(levelname)s - %(name)s: %(message)s' - ) - else: - # if you run it with "python -O main.py" instead, debug is false - - # let the default logger print to the console - # noinspection SpellCheckingInspection - logging.basicConfig( - level=logging.WARNING, - format='%(asctime)s %(levelname)s - %(name)s: %(message)s' - ) - # and add a handler that limits the size to 1 GB - file_handler = RotatingFileHandler( - str(Path.home()) + '/.local/share/yin_yang.log', - maxBytes=10**9, backupCount=1 - ) - logging.root.addHandler(file_handler) - - -def main(arguments): - # checks whether $ yin-yang is run without args - if len(sys.argv) == 1: - config.add_event_listener(ConfigEvent.SAVE, daemon_handler.watcher) - config.add_event_listener(ConfigEvent.CHANGE, daemon_handler.watcher) - # load GUI - app = QtWidgets.QApplication(sys.argv) - - # load translation - try: - lang = QLocale().name() - logger.debug(f'Using language {lang}') - - # system translations - path = QLibraryInfo.path(QLibraryInfo.TranslationsPath) - translator = QTranslator(app) - if translator.load(QLocale.system(), 'qtbase', '_', path): - app.installTranslator(translator) - else: - raise FileNotFoundError('Error while loading system translations!') - - # application translations - translator = QTranslator(app) - path = ':translations' - if translator.load(QLocale.system(), 'yin_yang', '.', path): - app.installTranslator(translator) - else: - raise FileNotFoundError('Error while loading application translations!') - - except Exception as e: - logger.error(str(e)) - print('Error while loading translation. Using default language.') - - window = main_window_connector.MainWindow() - window.show() - sys.exit(app.exec()) - - if arguments.toggle: - # terminate any running instances - config.running = False - config.mode = Modes.MANUAL - yin_yang.set_mode(not config.dark_mode) - - if arguments.systemd: - yin_yang.set_desired_theme() - - -if __name__ == "__main__": - # using ArgumentParser for parsing arguments - parser = ArgumentParser() - parser.add_argument("-t", "--toggle", - help="toggles Yin-Yang", - action="store_true") - parser.add_argument("--systemd", help="uses systemd journal handler and applies desired theme", action='store_true') - args = parser.parse_args() - setup_logger(args.systemd) - main(args) diff --git a/requirements.txt b/requirements.txt index ae0a672d..a8e71069 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,6 @@ -numpy==1.23.5 -psutil==5.9.4 -PySide6==6.4.1 -PySide6-Addons==6.4.1 -PySide6-Essentials==6.4.1 -python-dateutil==2.8.2 -shiboken6==6.4.1 -six==1.16.0 +psutil==5.9.5 +PySide6==6.5.3 +PySide6-Addons==6.5.3 suntime==1.2.5 -systemd-python==234 +systemd-python==235 +requests~=2.28.2 \ No newline at end of file diff --git a/resources/translations/yin_yang.de_DE.qm b/resources/translations/yin_yang.de_DE.qm index d7028c3a..dfe9e222 100644 Binary files a/resources/translations/yin_yang.de_DE.qm and b/resources/translations/yin_yang.de_DE.qm differ diff --git a/resources/translations/yin_yang.de_DE.ts b/resources/translations/yin_yang.de_DE.ts index 24f7312f..1a0a1935 100644 --- a/resources/translations/yin_yang.de_DE.ts +++ b/resources/translations/yin_yang.de_DE.ts @@ -1,39 +1,6 @@ - - MainWindow - - - You are using version {} - Sie verwenden Version {} - - - - Dark mode will be active between {} and {}. - Dunkler Modus wird zwischen {} und {} aktiv sein. - - - - Open light wallpaper - Öffne helles Hintergrundbild - - - - Open dark wallpaper - Öffne dunkles Hintergrundbild - - - - The settings have been modified. Do you want to save them? - Die Einstellungen wurden geändert. Möchten Sie sie speichern? - - - - Unsaved changes - Ungespeicherte Änderungen - - main_window @@ -79,37 +46,68 @@ update location automatically - + Position automatisch bestimmen - + + Light + Hell + + + + Dark + Dunkel + + + Make a sound when switching the theme Mache ein Geräusch, wenn das Thema geändert wird - + Send a notification Sende eine Benachrichtigung - + + Time to wait until the system finished booting. Default value is 10 seconds. + Zeit die gewartet werden soll während das System startet. Standardwert ist 10 Sekunden. + + + + Delay after boot: + Verzögerung nach Start + + + + s + + + + Plugins + + + systray - - Sample Plugin - + + Open Yin Yang + Context menu action in the systray + Yin Yang öffnen - - firefox-compact-light@mozilla.org - + + Toggle theme + Context menu action in the systray + Farbschema wechseln - - firefox-compact-dark@mozilla.org - + + Quit + Context menu action in the systray + Beenden diff --git a/resources/translations/yin_yang.nl_NL.qm b/resources/translations/yin_yang.nl_NL.qm index c0f17097..065f4499 100644 Binary files a/resources/translations/yin_yang.nl_NL.qm and b/resources/translations/yin_yang.nl_NL.qm differ diff --git a/resources/translations/yin_yang.nl_NL.ts b/resources/translations/yin_yang.nl_NL.ts index 73a65e07..067aec4d 100644 --- a/resources/translations/yin_yang.nl_NL.ts +++ b/resources/translations/yin_yang.nl_NL.ts @@ -1,39 +1,6 @@ - - MainWindow - - - You are using version {} - U maakt gebruik van versie {} - - - - Dark mode will be active between {} and {}. - Het donkere thema wordt ingeschakeld van {} tot {}. - - - - Open light wallpaper - Lichte achtergrond kiezen - - - - Open dark wallpaper - Donkere achtergrond kiezen - - - - The settings have been modified. Do you want to save them? - De instellingen zijn gewijzigd. Wilt u ze opslaan? - - - - Unsaved changes - Niet-opgeslagen wijzigingen - - main_window @@ -82,34 +49,65 @@ - + + Light + + + + + Dark + + + + Make a sound when switching the theme Geluid afspelen na instellen van ander thema - + Send a notification Melding tonen - + + Time to wait until the system finished booting. Default value is 10 seconds. + + + + + Delay after boot: + + + + + s + + + + Plugins Plug-ins + + + systray - - Sample Plugin - Voorbeeldplug-in + + Open Yin Yang + Context menu action in the systray + - - firefox-compact-light@mozilla.org - firefox-compact-licht@mozilla.org + + Toggle theme + Context menu action in the systray + - - firefox-compact-dark@mozilla.org - firefox-compact-donker@mozilla.org + + Quit + Context menu action in the systray + diff --git a/resources/yin-yang b/resources/yin-yang index 24d9d2f3..a5181b56 100755 --- a/resources/yin-yang +++ b/resources/yin-yang @@ -1,3 +1,5 @@ #!/bin/bash -cd /opt/yin-yang/ || exit -python3 -O /opt/yin-yang/main.py "$@" +cd /opt/yin-yang/ || exit 1 +# check whether the activate script is readable, then activate the venv +[[ -r .venv/bin/activate ]] && source .venv/bin/activate +python3 -Om yin_yang "$@" diff --git a/resources/yin_yang.service b/resources/yin_yang.service index d3d2b781..d091623f 100644 --- a/resources/yin_yang.service +++ b/resources/yin_yang.service @@ -1,5 +1,6 @@ [Unit] Description=Automatic light and dark mode +After=suspend.target [Service] ExecStart=/usr/bin/yin-yang --systemd diff --git a/scripts/build_ui.sh b/scripts/build_ui.sh index aebe92dc..c299d3ee 100755 --- a/scripts/build_ui.sh +++ b/scripts/build_ui.sh @@ -1,11 +1,11 @@ #!/bin/bash # resource file -pyside6-rcc ./resources/resources.qrc -o ./resources_rc.py +pyside6-rcc ./resources/resources.qrc -o ./yin_yang/ui/resources_rc.py # ui file from qt designer -pyside6-uic ./designer/main_window.ui > ./src/ui/main_window.py +pyside6-uic --from-imports ./designer/main_window.ui -o ./yin_yang/ui/main_window.py # extract strings to translate (doesn't work with .pro file unfortunately) -pyside6-lupdate ./designer/main_window.ui ./src/ui/main_window_connector.py \ +pyside6-lupdate ./designer/main_window.ui ./yin_yang/* \ -ts resources/translations/yin_yang.*.ts -no-obsolete # generate binary translation files pyside6-lrelease ./resources/translations/yin_yang.*.ts diff --git a/scripts/install.sh b/scripts/install.sh index 1f691161..f7801710 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -14,7 +14,12 @@ echo "Uninstalling old version, if it exists" ./scripts/uninstall.sh echo "Installing dependencies …" -pip3 install -r requirements.txt +# create virtual environment +python3 -m venv /opt/yin-yang/.venv +source .venv/bin/activate +/opt/yin-yang/.venv/bin/pip3 install --upgrade setuptools pip wheel +/opt/yin-yang/.venv/bin/pip3 install -r requirements.txt + echo "Installing yin yang" #check if /opt/ directory exists else create if [ ! -d /opt/ ]; then diff --git a/tests/test_communication.py b/tests/test_communication.py index 0fc33c6f..6c8d0aa2 100644 --- a/tests/test_communication.py +++ b/tests/test_communication.py @@ -5,10 +5,10 @@ from datetime import datetime, time from subprocess import Popen, PIPE -import communicate -from src.meta import PluginKey -from src.config import config -from src.yin_yang import should_be_dark +from yin_yang import communicate +from yin_yang.meta import PluginKey +from yin_yang.config import config +from yin_yang.theme_switcher import should_be_dark def should_be_dark_extensions(time_current: int, time_dark: int): diff --git a/tests/test_config.py b/tests/test_config.py index 6ac98c5c..79b13087 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -3,8 +3,8 @@ from pathlib import Path from typing import Optional -from src.config import config, ConfigWatcher, update_config -from src.meta import Desktop, Modes, PluginKey, ConfigEvent +from yin_yang.config import config, ConfigWatcher, update_config +from yin_yang.meta import Desktop, Modes, PluginKey, ConfigEvent config_path = f"{Path.home()}/.config/yin_yang/yin_yang_dev.json" diff --git a/tests/test_daemon.py b/tests/test_daemon.py index d57a77a7..4f34729f 100644 --- a/tests/test_daemon.py +++ b/tests/test_daemon.py @@ -1,7 +1,7 @@ import unittest from datetime import time -from src.yin_yang import should_be_dark +from yin_yang.theme_switcher import should_be_dark class DaemonTest(unittest.TestCase): diff --git a/tests/test_daemon_handler.py b/tests/test_daemon_handler.py index 8173340d..0d845b43 100644 --- a/tests/test_daemon_handler.py +++ b/tests/test_daemon_handler.py @@ -5,9 +5,9 @@ from datetime import time from os.path import isfile -from src import daemon_handler -from src.config import config -from src.meta import Modes, ConfigEvent +from yin_yang import daemon_handler +from yin_yang.config import config +from yin_yang.meta import Modes, ConfigEvent class DaemonTest(unittest.TestCase): diff --git a/tests/test_plugin_class.py b/tests/test_plugin_class.py index d91a6484..61a43f54 100644 --- a/tests/test_plugin_class.py +++ b/tests/test_plugin_class.py @@ -2,7 +2,7 @@ from PySide6.QtGui import QColor -from src.plugins._plugin import PluginCommandline, Plugin, get_qcolor_from_int, get_int_from_qcolor +from yin_yang.plugins._plugin import PluginCommandline, Plugin, get_qcolor_from_int, get_int_from_qcolor class MinimalPlugin(Plugin): diff --git a/tests/test_plugins.py b/tests/test_plugins.py index d10a6c28..f6fe3a5e 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -1,8 +1,8 @@ import unittest -from src.config import config -from src.config import plugins -from src.plugins._plugin import Plugin, ExternalPlugin +from yin_yang.config import config +from yin_yang.config import plugins +from yin_yang.plugins._plugin import Plugin, ExternalPlugin class PluginsTest(unittest.TestCase): diff --git a/yin_yang/NotificationHandler.py b/yin_yang/NotificationHandler.py new file mode 100644 index 00000000..6f5a3974 --- /dev/null +++ b/yin_yang/NotificationHandler.py @@ -0,0 +1,9 @@ +import subprocess +from logging import Handler + + +class NotificationHandler(Handler): + """Shows logs as notifications""" + def emit(self, record): + subprocess.call(['notify-send', record.levelname, record.msg, + '-a', 'Yin & Yang', '-u', 'low', '--icon', 'yin_yang']) diff --git a/src/__init__.py b/yin_yang/__init__.py similarity index 100% rename from src/__init__.py rename to yin_yang/__init__.py diff --git a/yin_yang/__main__.py b/yin_yang/__main__.py new file mode 100755 index 00000000..c5d248f4 --- /dev/null +++ b/yin_yang/__main__.py @@ -0,0 +1,136 @@ +#!/bin/env python3 + +import sys +import logging +from argparse import ArgumentParser +from logging.handlers import RotatingFileHandler +from pathlib import Path + +from PySide6 import QtWidgets +from PySide6.QtCore import QTranslator, QLibraryInfo, QLocale, QObject +from PySide6.QtGui import QIcon +from PySide6.QtWidgets import QSystemTrayIcon, QMenu +from systemd import journal + +from yin_yang.NotificationHandler import NotificationHandler +from yin_yang import daemon_handler +from yin_yang.meta import ConfigEvent +from yin_yang import theme_switcher +from yin_yang.config import config, Modes +from yin_yang.ui import main_window_connector + +logger = logging.getLogger() + + +def setup_logger(use_systemd_journal: bool): + notification_handler = NotificationHandler() + notification_handler.addFilter(lambda record: record.levelno > logging.WARNING) + logger.addHandler(notification_handler) + + if use_systemd_journal: + logger.addHandler(journal.JournalHandler(SYSLOG_IDENTIFIER='yin_yang')) + + # __debug__ is true when you run __main__.py without the -O argument (python __main__.py) + # noinspection PyUnreachableCode + if __debug__: + # noinspection SpellCheckingInspection + logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s %(levelname)s - %(name)s: %(message)s' + ) + else: + # if you run it with "python -O __main__.py" instead, debug is false + + # let the default logger print to the console + # noinspection SpellCheckingInspection + logging.basicConfig( + level=logging.WARNING, + format='%(asctime)s %(levelname)s - %(name)s: %(message)s' + ) + # and add a handler that limits the size to 1 GB + file_handler = RotatingFileHandler( + str(Path.home()) + '/.local/share/yin_yang.log', + maxBytes=10**9, backupCount=1 + ) + logging.root.addHandler(file_handler) + +def systray_icon_clicked(reason: QSystemTrayIcon.ActivationReason): + match reason: + case QSystemTrayIcon.ActivationReason.MiddleClick: + theme_switcher.set_mode(not config.dark_mode) + case QSystemTrayIcon.ActivationReason.Trigger: + window.show() + + +# using ArgumentParser for parsing arguments +parser = ArgumentParser() +parser.add_argument('-t', '--toggle', + help='toggles Yin-Yang', + action='store_true') +parser.add_argument('--systemd', help='uses systemd journal handler and applies desired theme', action='store_true') +arguments = parser.parse_args() +setup_logger(arguments.systemd) + +if arguments.toggle: + # terminate any running instances + config.running = False + config.mode = Modes.MANUAL + theme_switcher.set_mode(not config.dark_mode) + +elif arguments.systemd: + theme_switcher.set_desired_theme() + +else: + # load GUI + config.add_event_listener(ConfigEvent.SAVE, daemon_handler.watcher) + config.add_event_listener(ConfigEvent.CHANGE, daemon_handler.watcher) + app = QtWidgets.QApplication(sys.argv) + # fixes icon on wayland + app.setDesktopFileName('Yin-Yang') + + # load translation + try: + lang = QLocale().name() + logger.debug(f'Using language {lang}') + + # system translations + path = QLibraryInfo.path(QLibraryInfo.TranslationsPath) + translator = QTranslator(app) + if translator.load(QLocale.system(), 'qtbase', '_', path): + app.installTranslator(translator) + else: + raise FileNotFoundError('Error while loading system translations!') + + # application translations + translator = QTranslator(app) + path = ':translations' + if translator.load(QLocale.system(), 'yin_yang', '.', path): + app.installTranslator(translator) + else: + raise FileNotFoundError('Error while loading application translations!') + + except Exception as e: + logger.error(str(e)) + print('Error while loading translation. Using default language.') + + # show systray icon + if QSystemTrayIcon.isSystemTrayAvailable(): + app.setQuitOnLastWindowClosed(False) + + icon = QSystemTrayIcon(QIcon(u':icons/logo'), app) + icon.activated.connect(systray_icon_clicked) + icon.setToolTip('Yin & Yang') + + menu = QMenu('Yin & Yang') + menu.addAction(app.translate('systray', 'Open Yin Yang', 'Context menu action in the systray'), lambda: window.show()) + menu.addAction(app.translate('systray', 'Toggle theme', 'Context menu action in the systray'), lambda: theme_switcher.set_mode(not config.dark_mode)) + menu.addAction(QIcon.fromTheme('application-exit'), app.translate('systray', 'Quit', 'Context menu action in the systray'), app.quit) + + icon.setContextMenu(menu) + icon.show() + else: + logger.debug('System tray is unsupported') + + window = main_window_connector.MainWindow() + window.show() + sys.exit(app.exec()) diff --git a/communicate.py b/yin_yang/communicate.py similarity index 98% rename from communicate.py rename to yin_yang/communicate.py index c7bb04dd..2fbb7222 100755 --- a/communicate.py +++ b/yin_yang/communicate.py @@ -12,8 +12,8 @@ from datetime import datetime, time as dt_time from pathlib import Path -from src.meta import PluginKey -from src.config import config +from .meta import PluginKey +from .config import config logging.basicConfig(filename=str(Path.home()) + '/.local/share/yin_yang.log', level=logging.DEBUG, format='%(asctime)s %(levelname)s - %(name)s: %(message)s') diff --git a/src/config.py b/yin_yang/config.py similarity index 94% rename from src/config.py rename to yin_yang/config.py index fca6e507..c3fce903 100755 --- a/src/config.py +++ b/yin_yang/config.py @@ -8,13 +8,14 @@ from time import sleep from typing import Union, Optional +import requests from PySide6.QtCore import QObject from PySide6.QtPositioning import QGeoPositionInfoSource, QGeoPositionInfo, QGeoCoordinate from psutil import process_iter, NoSuchProcess from suntime import Sun, SunTimeException -from src.meta import Modes, Desktop, PluginKey, ConfigEvent -from src.plugins import get_plugins +from .meta import Modes, Desktop, PluginKey, ConfigEvent +from .plugins import get_plugins logger = logging.getLogger(__name__) @@ -119,6 +120,7 @@ def get_sun_time(latitude, longitude) -> tuple[time, time]: locationSource = QGeoPositionInfoSource.createDefaultSource(parent) +@cache def get_current_location() -> QGeoCoordinate: if locationSource is None: logger.error("No location source is available") @@ -134,8 +136,13 @@ def get_current_location() -> QGeoCoordinate: sleep(1) coordinate = pos.coordinate() if not coordinate.isValid(): - logger.error('Location could not be determined') - return QGeoCoordinate(0, 0) + logger.warning('Location could not be determined. Using ipinfo.io to get location') + # use the old method as a fallback + loc_response = requests.get('https://www.ipinfo.io/loc').text.split(',') + loc: [float] = [float(coordinate) for coordinate in loc_response] + assert len(loc) == 2, 'The returned location should have exactly 2 values.' + coordinate = QGeoCoordinate(loc[0], loc[1]) + assert coordinate.isValid() return coordinate @@ -153,6 +160,10 @@ def get_desktop() -> Desktop: return Desktop.KDE case 'xfce': return Desktop.XFCE + case 'mate': + return Desktop.MATE + case 'x-cinnamon': + return Desktop.CINNAMON case 'sway' | 'hyprland': return Desktop.GNOME case _: @@ -339,7 +350,7 @@ def defaults(self) -> dict: # NOTE: if you change or add new values here, make sure to update the version number and update_config() method conf_default = { - 'version': 3.2, + 'version': 3.3, 'running': False, 'dark_mode': False, 'mode': Modes.MANUAL.value, @@ -347,6 +358,7 @@ def defaults(self) -> dict: 'update_location': False, 'update_interval': 60, 'times': ('07:00', '20:00'), + 'boot_offset': 10, 'plugins': {} } @@ -465,10 +477,14 @@ def desktop(self) -> Desktop: return get_desktop() @property - def update_interval(self) -> int: + def boot_offset(self) -> int: """Seconds that should pass until next check""" - return self['update_interval'] + return self['boot_offset'] + + @boot_offset.setter + def boot_offset(self, value: int): + self['boot_offset'] = value # create global object with current version diff --git a/src/daemon_handler.py b/yin_yang/daemon_handler.py similarity index 94% rename from src/daemon_handler.py rename to yin_yang/daemon_handler.py index 765016ab..6b845bf8 100644 --- a/src/daemon_handler.py +++ b/yin_yang/daemon_handler.py @@ -4,8 +4,8 @@ from enum import Enum, auto from pathlib import Path -from src.config import ConfigWatcher, config -from src.meta import ConfigEvent, Modes +from .config import ConfigWatcher, config +from .meta import ConfigEvent, Modes logger = logging.getLogger(__name__) SYSTEMD_PATH = Path.home() / '.local/share/systemd/user' @@ -43,6 +43,7 @@ def update_times(): time_light, time_dark = config.times lines[4] = f'OnCalendar={time_light}\n' lines[5] = f'OnCalendar={time_dark}\n' + lines[6] = f'OnStartupSec={config.boot_offset}\n' with TIMER_PATH.open('w') as file: file.writelines(lines) @@ -73,7 +74,7 @@ def _set_needed_updates(self, change_values): self._next_timer_update = SaveWatcher._UpdateTimerStatus.STOP else: self._next_timer_update = SaveWatcher._UpdateTimerStatus.UPDATE_TIMES - case 'times' | 'coordinates': + case 'times' | 'coordinates' | 'boot_offset': self._next_timer_update = SaveWatcher._UpdateTimerStatus.UPDATE_TIMES def _update_timer(self): diff --git a/src/meta.py b/yin_yang/meta.py similarity index 92% rename from src/meta.py rename to yin_yang/meta.py index cc577c01..cbfe421f 100644 --- a/src/meta.py +++ b/yin_yang/meta.py @@ -14,6 +14,8 @@ class Desktop(Enum): GNOME = 'gnome' XFCE = 'xfce' UNKNOWN = 'unknown' + MATE = 'mate' + CINNAMON = 'cinnamon' class PluginKey(Enum): diff --git a/src/plugins/__init__.py b/yin_yang/plugins/__init__.py similarity index 59% rename from src/plugins/__init__.py rename to yin_yang/plugins/__init__.py index 0c682635..c40941c2 100755 --- a/src/plugins/__init__.py +++ b/yin_yang/plugins/__init__.py @@ -1,12 +1,12 @@ -from src.meta import Desktop -from src.plugins import system, colors, gtk, kvantum, wallpaper, custom -from src.plugins import firefox, brave, gedit, only_office -from src.plugins import vscode, atom, konsole -from src.plugins import sound, notify +from ..meta import Desktop +from . import system, colors, gtk, icons, kvantum, wallpaper, custom +from . import firefox, brave, gedit, only_office, okular +from . import vscode, atom, konsole +from . import sound, notify # NOTE initialize your plugin over here: # The order in the list specifies the order in the config gui -from src.plugins._plugin import Plugin, ExternalPlugin +from yin_yang.plugins._plugin import Plugin, ExternalPlugin def get_plugins(desktop: Desktop) -> [Plugin]: @@ -14,6 +14,7 @@ def get_plugins(desktop: Desktop) -> [Plugin]: system.System(desktop), colors.Colors(desktop), gtk.Gtk(desktop), + icons.Icons(desktop), kvantum.Kvantum(), wallpaper.Wallpaper(desktop), firefox.Firefox(), @@ -22,6 +23,7 @@ def get_plugins(desktop: Desktop) -> [Plugin]: atom.Atom(), gedit.Gedit(), only_office.OnlyOffice(), + okular.Okular(), konsole.Konsole(), custom.Custom(), sound.Sound(), @@ -29,5 +31,5 @@ def get_plugins(desktop: Desktop) -> [Plugin]: ] -# this lets us skip all external plugins in yin_yang.py while keeping _plugin "private" +# this lets us skip all external plugins in theme_switcher.py while keeping _plugin "private" ExternalPlugin = ExternalPlugin diff --git a/src/plugins/_plugin.py b/yin_yang/plugins/_plugin.py similarity index 94% rename from src/plugins/_plugin.py rename to yin_yang/plugins/_plugin.py index 430e2fc0..0568d08f 100644 --- a/src/plugins/_plugin.py +++ b/yin_yang/plugins/_plugin.py @@ -6,7 +6,7 @@ from PySide6.QtGui import QColor, QRgba64 from PySide6.QtWidgets import QGroupBox, QHBoxLayout, QLineEdit, QComboBox -from src.meta import UnsupportedDesktopError +from ..meta import UnsupportedDesktopError logger = logging.getLogger(__name__) @@ -144,14 +144,22 @@ def insert_theme(self, theme: str) -> list: return command - @property - def available(self) -> bool: - # Runs the first entry in the command list with --help + @staticmethod + def check_command(command) -> bool: + # Returns true if command execution succeeds try: - return subprocess.run([self.command[0], '--help'], stdout=subprocess.DEVNULL).returncode == 0 + subprocess.check_call(command, stdout=subprocess.DEVNULL) + return True except FileNotFoundError: # if no such command is available, the plugin is not available return False + except subprocess.CalledProcessError: + # command execution failed + return False + + @property + def available(self): + return self.check_command([self.command[0], '--help']) class PluginDesktopDependent(Plugin): diff --git a/src/plugins/atom.py b/yin_yang/plugins/atom.py similarity index 100% rename from src/plugins/atom.py rename to yin_yang/plugins/atom.py diff --git a/src/plugins/brave.py b/yin_yang/plugins/brave.py similarity index 96% rename from src/plugins/brave.py rename to yin_yang/plugins/brave.py index ac845668..8b2cf451 100644 --- a/src/plugins/brave.py +++ b/yin_yang/plugins/brave.py @@ -5,7 +5,7 @@ from PySide6.QtGui import QColor from PySide6.QtWidgets import QWidget, QVBoxLayout, QLineEdit, QPushButton -from src.plugins._plugin import Plugin, get_int_from_qcolor +from ._plugin import Plugin, get_int_from_qcolor path = f'{Path.home()}/.config/BraveSoftware/Brave-Browser/Default/Preferences' diff --git a/src/plugins/colors.py b/yin_yang/plugins/colors.py similarity index 90% rename from src/plugins/colors.py rename to yin_yang/plugins/colors.py index d58abd1a..5796d57d 100644 --- a/src/plugins/colors.py +++ b/yin_yang/plugins/colors.py @@ -1,8 +1,8 @@ import subprocess import re -from src.meta import Desktop -from src.plugins._plugin import Plugin, PluginDesktopDependent, PluginCommandline +from ..meta import Desktop +from ._plugin import Plugin, PluginDesktopDependent, PluginCommandline class Colors(PluginDesktopDependent): diff --git a/src/plugins/custom.py b/yin_yang/plugins/custom.py similarity index 94% rename from src/plugins/custom.py rename to yin_yang/plugins/custom.py index 4e477816..2f5547ce 100644 --- a/src/plugins/custom.py +++ b/yin_yang/plugins/custom.py @@ -2,7 +2,7 @@ from PySide6.QtWidgets import QLineEdit -from src.plugins._plugin import PluginCommandline +from ._plugin import PluginCommandline class Custom(PluginCommandline): diff --git a/src/plugins/firefox.py b/yin_yang/plugins/firefox.py similarity index 100% rename from src/plugins/firefox.py rename to yin_yang/plugins/firefox.py diff --git a/src/plugins/gedit.py b/yin_yang/plugins/gedit.py similarity index 90% rename from src/plugins/gedit.py rename to yin_yang/plugins/gedit.py index ceba099b..3fc45a70 100644 --- a/src/plugins/gedit.py +++ b/yin_yang/plugins/gedit.py @@ -2,8 +2,8 @@ from os.path import isdir from xml.etree import ElementTree -from src.plugins._plugin import PluginCommandline -from src.plugins.system import test_gnome_availability +from ._plugin import PluginCommandline +from .system import test_gnome_availability path = '/usr/share/gtksourceview-4/styles/' diff --git a/src/plugins/gtk.py b/yin_yang/plugins/gtk.py similarity index 70% rename from src/plugins/gtk.py rename to yin_yang/plugins/gtk.py index 61594451..7dad089c 100755 --- a/src/plugins/gtk.py +++ b/yin_yang/plugins/gtk.py @@ -4,9 +4,9 @@ from PySide6.QtDBus import QDBusConnection, QDBusMessage -from src.meta import Desktop -from src.plugins._plugin import PluginDesktopDependent, Plugin, PluginCommandline -from src.plugins.system import test_gnome_availability +from ..meta import Desktop +from ._plugin import PluginDesktopDependent, Plugin, PluginCommandline +from .system import test_gnome_availability logger = logging.getLogger(__name__) @@ -26,8 +26,12 @@ def __init__(self, desktop: Desktop): if not self.strategy.available: print('You need to install an extension for gnome to use it. \n' 'You can get it from here: https://extensions.gnome.org/extension/19/user-themes/') + case Desktop.MATE: + super().__init__(_Mate()) case Desktop.XFCE: super().__init__(_Xfce()) + case Desktop.CINNAMON: + super().__init__(_Cinnamon()) case _: super().__init__(None) @@ -83,3 +87,25 @@ def __init__(self): super(_Xfce, self).__init__(['xfconf-query', '-c', 'xsettings', '-p', '/Net/ThemeName', '-s', '{theme}']) self.theme_light = 'Adwaita' self.theme_dark = 'Adwaita-dark' + + +class _Mate(PluginCommandline): + def __init__(self): + super().__init__(['dconf', 'write', '/org/mate/desktop/interface/gtk-theme', '\'{theme}\'']) + self.theme_light = 'Yaru' + self.theme_dark = 'Yaru-dark' + + @property + def available(self) -> bool: + return self.check_command(['dconf', 'help']) + + +class _Cinnamon(PluginCommandline): + def __init__(self): + super().__init__(['gsettings', 'set', 'org.cinnamon.desktop.interface', 'gtk-theme', '\"{theme}\"']) + self.theme_light = 'Adwaita' + self.theme_dark = 'Adwaita-dark' + + @property + def available(self) -> bool: + return test_gnome_availability(self.command) diff --git a/yin_yang/plugins/icons.py b/yin_yang/plugins/icons.py new file mode 100644 index 00000000..3121e6c4 --- /dev/null +++ b/yin_yang/plugins/icons.py @@ -0,0 +1,36 @@ +from .system import test_gnome_availability +from ..meta import Desktop +from ._plugin import PluginDesktopDependent, PluginCommandline + + +class Icons(PluginDesktopDependent): + def __init__(self, desktop: Desktop): + match desktop: + case Desktop.MATE: + super().__init__(_Mate()) + case Desktop.CINNAMON: + super().__init__(_Cinnamon()) + case _: + super().__init__(None) + + +class _Mate(PluginCommandline): + def __init__(self): + super().__init__(['dconf', 'write', '/org/mate/desktop/interface/icon-theme', '\'{theme}\'']) + self.theme_light = 'Yaru' + self.theme_dark = 'Yaru-dark' + + @property + def available(self): + return self.check_command(['dconf', 'help']) + + +class _Cinnamon(PluginCommandline): + def __init__(self): + super().__init__(['gsettings', 'set', 'org.cinnamon.desktop.interface', 'icon-theme', '\"{theme}\"']) + self.theme_light = 'Mint-X' + self.theme_dark = 'gnome' + + @property + def available(self) -> bool: + return test_gnome_availability(self.command) diff --git a/src/plugins/konsole.py b/yin_yang/plugins/konsole.py similarity index 87% rename from src/plugins/konsole.py rename to yin_yang/plugins/konsole.py index a771248c..8f10d5d1 100644 --- a/src/plugins/konsole.py +++ b/yin_yang/plugins/konsole.py @@ -10,7 +10,7 @@ import psutil from PySide6.QtDBus import QDBusConnection, QDBusMessage -from src.plugins._plugin import Plugin +from ._plugin import Plugin logger = logging.getLogger(__name__) @@ -65,8 +65,8 @@ def set_mode(self, dark: bool) -> bool: # Get the process IDs of all running Konsole instances owned by the current user process_ids = [ - proc.pid for proc in psutil.process_iter(['name', 'username']) - if proc.info['name'] == 'konsole' and proc.info['username'] == os.getlogin() + proc.pid for proc in psutil.process_iter() + if proc.name() == 'konsole' and proc.username() == os.getlogin() ] # loop: console processes @@ -77,8 +77,8 @@ def set_mode(self, dark: bool) -> bool: 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() + proc.pid for proc in psutil.process_iter() + if proc.name() == 'dolphin' and proc.username() == os.getlogin() ] # loop: dolphin processes @@ -130,6 +130,8 @@ def default_profile(self): # If a match is found, return the content of the wildcard '*' if match: value = match.group(1) + if not os.path.isfile(self.user_path / value): + value = None if value is None: # use the first found profile @@ -139,8 +141,24 @@ def default_profile(self): break if value is not None: logger.warning(f'No default profile found, using {value} instead.') - else: - raise ValueError('No Konsole profile found.') + + if value is None: + # create a custom profile manually + file_content = """[Appearance] +ColorScheme=Breeze + +[General] +Command=/bin/bash +Name=Fish +Parent=FALLBACK/ +""" + + with (self.user_path / 'Default.profile').open('w') as file: + file.writelines(file_content) + + self.default_profile = 'Default.profile' + + return 'Default.profile' return value @@ -176,7 +194,13 @@ def update_profile(self, dark: bool, theme: str): profile_config = ConfigParser() profile_config.optionxform = str profile_config.read(file_path) - profile_config['Appearance']['ColorScheme'] = theme + + try: + profile_config['Appearance']['ColorScheme'] = theme + except KeyError: + profile_config.add_section('Appearance') + profile_config['Appearance']['ColorScheme'] = theme + with open(file_path, 'w') as file: profile_config.write(file) diff --git a/src/plugins/kvantum.py b/yin_yang/plugins/kvantum.py similarity index 95% rename from src/plugins/kvantum.py rename to yin_yang/plugins/kvantum.py index b02da05e..82854a49 100755 --- a/src/plugins/kvantum.py +++ b/yin_yang/plugins/kvantum.py @@ -1,7 +1,7 @@ import os from pathlib import Path -from src.plugins._plugin import PluginCommandline +from ._plugin import PluginCommandline class Kvantum(PluginCommandline): diff --git a/src/plugins/notify.py b/yin_yang/plugins/notify.py similarity index 85% rename from src/plugins/notify.py rename to yin_yang/plugins/notify.py index 423c947b..12707bbb 100644 --- a/src/plugins/notify.py +++ b/yin_yang/plugins/notify.py @@ -1,4 +1,4 @@ -from src.plugins._plugin import PluginCommandline +from ._plugin import PluginCommandline class Notification(PluginCommandline): diff --git a/yin_yang/plugins/okular.py b/yin_yang/plugins/okular.py new file mode 100644 index 00000000..79832ef7 --- /dev/null +++ b/yin_yang/plugins/okular.py @@ -0,0 +1,127 @@ +import os +from configparser import ConfigParser +from pathlib import Path + +import psutil +from PySide6.QtDBus import QDBusConnection, QDBusMessage + +from ._plugin import Plugin + + +class Okular(Plugin): + """Inspired by: https://gitlab.com/LADlSLAV/yabotss/-/blob/main/darkman_examples_kde_plasma/dark-mode.d/10_set_theme_okular_dark.sh""" + + def __init__(self): + super().__init__() + self._theme_light = '' + self._theme_dark = '' + + @property + def user_paths(self) -> [Path]: + path = Path.home() / '.config/okularpartrc' + if path.is_file(): + yield path + + path = Path.home() / '.var/app/org.kde.okular/config/okularpartrc' + if path.is_file(): + yield path + + return + + @property + def available(self) -> bool: + try: + next(self.user_paths) + return True + except StopIteration: + return False + + def set_mode(self, dark: bool): + if not self.enabled: + return False + + process_ids = [ + proc.pid for proc in psutil.process_iter(['name', 'username']) + if proc.name() == 'okular' and proc.username() == os.getlogin() + ] + # this is if okular is running in a flatpak + process_ids.append(2) + + connection = QDBusConnection.sessionBus() + for pid in process_ids: + message = QDBusMessage.createMethodCall( + f'org.kde.okular-{pid}', + '/okular', + 'org.kde.okular', + 'slotSetChangeColors' + ) + message.setArguments([dark]) + connection.call(message) + + # now change the config for future starts of the app + for path in self.user_paths: + config = ConfigParser() + config.optionxform = str + config.read(path) + + if dark: + if not config.has_section('Document'): + config.add_section('Document') + config['Document']['ChangeColors'] = 'true' + else: + config.remove_option('Document', 'ChangeColors') + if len(config.options('Document')) == 0: + config.remove_section('Document') + + with open(path, 'w') as file: + config.write(file, space_around_delimiters=False) + + def set_theme(self, theme: str): + pass + + @property + def available_themes(self) -> dict: + # these are color changing modes in Okulars accessibility settings + return { + '': 'Invert colors', + 'InvertLightness': 'Invert lightness', + 'InvertLuma': 'Invert luma (sRGB linear)', + 'InvertLumaSymmetric': 'Invert luma (symmetrical)' + } + + def get_input(self, widget): + inputs = super().get_input(widget) + n_items = len(self.available_themes) + + # modify light item to make it clear that this shows the original without modifications + for i in range(n_items): + inputs[0].removeItem(0) + inputs[0].addItem('Don\'t modify anything') + + return inputs + + @property + def theme_dark(self): + return self._theme_dark + + @theme_dark.setter + def theme_dark(self, value): + self._theme_dark = value + + for path in self.user_paths: + config = ConfigParser() + config.optionxform = str + config.read(path) + + if value == '': + if config.has_section('Document'): + config.remove_option('Document', 'RenderMode') + if len(config.options('Document')) == 0: + config.remove_section('Document') + else: + if not config.has_section('Document'): + config.add_section('Document') + config['Document']['RenderMode'] = value + + with open(path, 'w') as file: + config.write(file, space_around_delimiters=False) diff --git a/src/plugins/only_office.py b/yin_yang/plugins/only_office.py similarity index 96% rename from src/plugins/only_office.py rename to yin_yang/plugins/only_office.py index 18725854..9b59dd07 100644 --- a/src/plugins/only_office.py +++ b/yin_yang/plugins/only_office.py @@ -2,7 +2,7 @@ from os.path import isfile from pathlib import Path -from src.plugins._plugin import Plugin +from ._plugin import Plugin config_path = f'{Path.home()}/.config/onlyoffice/DesktopEditors.conf' diff --git a/src/plugins/sound.py b/yin_yang/plugins/sound.py similarity index 90% rename from src/plugins/sound.py rename to yin_yang/plugins/sound.py index 2b6e32cc..a9b3fcc0 100644 --- a/src/plugins/sound.py +++ b/yin_yang/plugins/sound.py @@ -1,6 +1,6 @@ import subprocess -from src.plugins._plugin import PluginCommandline +from ._plugin import PluginCommandline class Sound(PluginCommandline): diff --git a/src/plugins/system.py b/yin_yang/plugins/system.py similarity index 67% rename from src/plugins/system.py rename to yin_yang/plugins/system.py index ddd0ba4e..f5c2710d 100644 --- a/src/plugins/system.py +++ b/yin_yang/plugins/system.py @@ -3,29 +3,21 @@ import subprocess import pwd import os +from configparser import ConfigParser +from pathlib import Path from PySide6.QtCore import QLocale -from src.meta import Desktop -from src.plugins._plugin import PluginDesktopDependent, PluginCommandline +from ..meta import Desktop +from ._plugin import PluginDesktopDependent, PluginCommandline logger = logging.getLogger(__name__) def test_gnome_availability(command) -> bool: - # Runs the first entry in the command list with --help - try: - # if not available, you might want to run https://gist.github.com/atiensivu/fcc3183e9a6fd74ec1a283e3b9ad05f0 - # or you have to install that extension - process = subprocess.run( - [command[0], 'get', command[2], command[3]], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL - ) - return process.returncode == 0 - except FileNotFoundError: - # if no such command is available, the plugin is not available - return False + return PluginCommandline.check_command( + [command[0], 'get', command[2], command[3]] + ) class System(PluginDesktopDependent): @@ -35,6 +27,10 @@ def __init__(self, desktop: Desktop): super().__init__(_Kde()) case Desktop.GNOME: super().__init__(_Gnome()) + case Desktop.MATE: + super().__init__(_Mate()) + case Desktop.CINNAMON: + super().__init__(_Cinnamon()) case _: super().__init__(None) @@ -129,3 +125,50 @@ def available_themes(self) -> dict: self.translations[long_name] = long_name return self.translations + + +class _Mate(PluginCommandline): + theme_directories = [Path('/usr/share/themes'), Path.home() / '.themes'] + + def __init__(self): + super().__init__(['dconf', 'write', '/org/mate/marco/general/theme', '\'{theme}\'']) + self.theme_light = 'Yaru' + self.theme_dark = 'Yaru-dark' + + @property + def available_themes(self) -> dict: + themes = [] + + for directory in self.theme_directories: + if not directory.is_dir(): + continue + + for d in directory.iterdir(): + index = d / 'index.theme' + if not index.is_file(): + continue + + config = ConfigParser() + config.read(index) + try: + theme = config['X-GNOME-Metatheme']['MetacityTheme'] + themes.append(theme) + except KeyError: + continue + + return {t: t for t in themes} + + @property + def available(self): + return self.check_command(['dconf', 'help']) + + +class _Cinnamon(PluginCommandline): + def __init__(self): + super().__init__(['gsettings', 'set', 'org.cinnamon.theme', 'name', '\"{theme}\"']) + self.theme_light = 'Mint-X-Teal' + self.theme_dark = 'Mint-Y-Dark-Brown' + + @property + def available(self) -> bool: + return test_gnome_availability(self.command) diff --git a/src/plugins/vscode.py b/yin_yang/plugins/vscode.py similarity index 100% rename from src/plugins/vscode.py rename to yin_yang/plugins/vscode.py diff --git a/src/plugins/wallpaper.py b/yin_yang/plugins/wallpaper.py similarity index 71% rename from src/plugins/wallpaper.py rename to yin_yang/plugins/wallpaper.py index abc4086f..d6c6c303 100755 --- a/src/plugins/wallpaper.py +++ b/yin_yang/plugins/wallpaper.py @@ -1,10 +1,11 @@ import logging import subprocess +from pathlib import Path from PySide6.QtWidgets import QDialogButtonBox, QVBoxLayout, QWidget, QLineEdit from PySide6.QtDBus import QDBusConnection, QDBusMessage -from src.meta import Desktop +from ..meta import Desktop from ._plugin import PluginDesktopDependent, PluginCommandline, Plugin from .system import test_gnome_availability @@ -22,13 +23,11 @@ def __init__(self, desktop: Desktop): super().__init__(_Gnome()) case Desktop.XFCE: super().__init__(_Xfce()) + case Desktop.CINNAMON: + super().__init__(_Cinnamon()) case _: super().__init__(None) - @property - def available(self) -> bool: - return self.strategy is not None - def get_input(self, widget): widgets = [] @@ -59,11 +58,45 @@ def available(self) -> bool: return test_gnome_availability(self.command) +def check_theme(theme: str) -> bool: + if not theme: + return False + file = Path(theme) + if "#" in file.name: + logger.error('Image files that contain a \'#\' will not work.') + return False + if not file.exists(): + logger.error(f'Image {theme} does not exist!') + return False + + return True + + class _Kde(Plugin): name = 'Wallpaper' def __init__(self): super().__init__() + self._theme_light = None + self._theme_dark = None + + @property + def theme_light(self) -> str: + return self._theme_light + + @theme_light.setter + def theme_light(self, value: str): + check_theme(value) + self._theme_light = value + + @property + def theme_dark(self) -> str: + return self._theme_dark + + @theme_dark.setter + def theme_dark(self, value: str): + check_theme(value) + self._theme_dark = value def set_theme(self, theme: str): connection = QDBusConnection.sessionBus() @@ -97,3 +130,12 @@ def __init__(self): monitor = next(p for p in properties.split('\\n') if p.endswith('/workspace0/last-image')) super().__init__(['xfconf-query', '-c', 'xfce4-desktop', '-p', monitor, '-s', '{theme}']) + + +class _Cinnamon(PluginCommandline): + def __init__(self): + super().__init__(['gsettings', 'set', 'org.cinnamon.desktop.background', 'picture-uri', 'file://\"{theme}\"']) + + @property + def available(self) -> bool: + return test_gnome_availability(self.command) diff --git a/src/yin_yang.py b/yin_yang/theme_switcher.py similarity index 89% rename from src/yin_yang.py rename to yin_yang/theme_switcher.py index ebb2c971..2a177b1f 100755 --- a/src/yin_yang.py +++ b/yin_yang/theme_switcher.py @@ -12,11 +12,11 @@ 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 +from .plugins.notify import Notification +from .plugins.sound import Sound +from .daemon_handler import update_times +from .meta import PluginKey +from .config import config, plugins logger = logging.getLogger(__name__) diff --git a/src/ui/__init__.py b/yin_yang/ui/__init__.py similarity index 100% rename from src/ui/__init__.py rename to yin_yang/ui/__init__.py diff --git a/src/ui/main_window.py b/yin_yang/ui/main_window.py similarity index 84% rename from src/ui/main_window.py rename to yin_yang/ui/main_window.py index 865f1d9d..79c0e585 100644 --- a/src/ui/main_window.py +++ b/yin_yang/ui/main_window.py @@ -3,7 +3,7 @@ ################################################################################ ## Form generated from reading UI file 'main_window.ui' ## -## Created by: Qt User Interface Compiler version 6.4.1 +## Created by: Qt User Interface Compiler version 6.5.0 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ @@ -18,10 +18,10 @@ from PySide6.QtWidgets import (QAbstractButton, QApplication, QCheckBox, QComboBox, QDialogButtonBox, QDoubleSpinBox, QFormLayout, QFrame, QGroupBox, QHBoxLayout, QLabel, QMainWindow, - QRadioButton, QScrollArea, QSizePolicy, QSpacerItem, - QStatusBar, QTabWidget, QTimeEdit, QVBoxLayout, - QWidget) -import resources_rc + QPushButton, QRadioButton, QScrollArea, QSizePolicy, + QSpacerItem, QSpinBox, QStatusBar, QTabWidget, + QTimeEdit, QVBoxLayout, QWidget) +from . import resources_rc class Ui_main_window(object): def setupUi(self, main_window): @@ -210,6 +210,25 @@ def setupUi(self, main_window): self.settings_layout.addWidget(self.schedule_settings) + self.manual_buttons = QWidget(self.settings) + self.manual_buttons.setObjectName(u"manual_buttons") + self.horizontalLayout_3 = QHBoxLayout(self.manual_buttons) + self.horizontalLayout_3.setSpacing(6) + self.horizontalLayout_3.setContentsMargins(11, 11, 11, 11) + self.horizontalLayout_3.setObjectName(u"horizontalLayout_3") + self.button_light = QPushButton(self.manual_buttons) + self.button_light.setObjectName(u"button_light") + + self.horizontalLayout_3.addWidget(self.button_light) + + self.button_dark = QPushButton(self.manual_buttons) + self.button_dark.setObjectName(u"button_dark") + + self.horizontalLayout_3.addWidget(self.button_dark) + + + self.settings_layout.addWidget(self.manual_buttons) + self.toggle_sound = QCheckBox(self.settings) self.toggle_sound.setObjectName(u"toggle_sound") @@ -220,6 +239,23 @@ def setupUi(self, main_window): self.settings_layout.addWidget(self.toggle_notification) + self.bootOffsetSettings = QFormLayout() + self.bootOffsetSettings.setSpacing(6) + self.bootOffsetSettings.setObjectName(u"bootOffsetSettings") + self.bootOffsetLabel = QLabel(self.settings) + self.bootOffsetLabel.setObjectName(u"bootOffsetLabel") + + self.bootOffsetSettings.setWidget(0, QFormLayout.LabelRole, self.bootOffsetLabel) + + self.bootOffset = QSpinBox(self.settings) + self.bootOffset.setObjectName(u"bootOffset") + self.bootOffset.setValue(10) + + self.bootOffsetSettings.setWidget(0, QFormLayout.FieldRole, self.bootOffset) + + + self.settings_layout.addLayout(self.bootOffsetSettings) + self.label_active = QLabel(self.settings) self.label_active.setObjectName(u"label_active") self.label_active.setText(u"Darkmode will be active between") @@ -243,7 +279,7 @@ def setupUi(self, main_window): self.plugins_scroll.setWidgetResizable(True) self.plugins_scroll_content = QWidget() self.plugins_scroll_content.setObjectName(u"plugins_scroll_content") - self.plugins_scroll_content.setGeometry(QRect(0, 0, 523, 663)) + self.plugins_scroll_content.setGeometry(QRect(0, 0, 518, 88)) self.plugins_scroll_content_layout = QVBoxLayout(self.plugins_scroll_content) self.plugins_scroll_content_layout.setSpacing(6) self.plugins_scroll_content_layout.setContentsMargins(11, 11, 11, 11) @@ -253,13 +289,16 @@ def setupUi(self, main_window): self.horizontalLayout.setObjectName(u"horizontalLayout") self.samplePluginGroupBox = QGroupBox(self.plugins_scroll_content) self.samplePluginGroupBox.setObjectName(u"samplePluginGroupBox") + self.samplePluginGroupBox.setTitle(u"Sample Plugin") self.horizontalLayout_2 = QHBoxLayout(self.samplePluginGroupBox) self.horizontalLayout_2.setSpacing(6) self.horizontalLayout_2.setContentsMargins(11, 11, 11, 11) self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") self.comboBox = QComboBox(self.samplePluginGroupBox) self.comboBox.setObjectName(u"comboBox") + self.comboBox.setCurrentText(u"") self.comboBox.setSizeAdjustPolicy(QComboBox.AdjustToContentsOnFirstShow) + self.comboBox.setPlaceholderText(u"firefox-compact-light@mozilla.org") self.horizontalLayout_2.addWidget(self.comboBox) @@ -267,6 +306,7 @@ def setupUi(self, main_window): self.comboBox_2.setObjectName(u"comboBox_2") self.comboBox_2.setCurrentText(u"") self.comboBox_2.setSizeAdjustPolicy(QComboBox.AdjustToContentsOnFirstShow) + self.comboBox_2.setPlaceholderText(u"firefox-compact-dark@mozilla.org") self.horizontalLayout_2.addWidget(self.comboBox_2) @@ -299,8 +339,9 @@ def setupUi(self, main_window): self.btn_sun.toggled.connect(self.location.setVisible) self.btn_schedule.toggled.connect(self.time.setVisible) self.btn_enable.toggled.connect(self.schedule_settings.setVisible) + self.btn_enable.toggled.connect(self.manual_buttons.setHidden) - self.tab_widget.setCurrentIndex(1) + self.tab_widget.setCurrentIndex(0) QMetaObject.connectSlotsByName(main_window) @@ -315,12 +356,16 @@ def retranslateUi(self, main_window): self.label_longitude.setText(QCoreApplication.translate("main_window", u"Longitude:", None)) self.label_latitude.setText(QCoreApplication.translate("main_window", u"Latitude:", None)) self.btn_location.setText(QCoreApplication.translate("main_window", u"update location automatically", None)) + self.button_light.setText(QCoreApplication.translate("main_window", u"Light", None)) + self.button_dark.setText(QCoreApplication.translate("main_window", u"Dark", None)) self.toggle_sound.setText(QCoreApplication.translate("main_window", u"Make a sound when switching the theme", None)) self.toggle_notification.setText(QCoreApplication.translate("main_window", u"Send a notification", None)) +#if QT_CONFIG(tooltip) + self.bootOffsetLabel.setToolTip(QCoreApplication.translate("main_window", u"Time to wait until the system finished booting. Default value is 10 seconds.", None)) +#endif // QT_CONFIG(tooltip) + self.bootOffsetLabel.setText(QCoreApplication.translate("main_window", u"Delay after boot:", None)) + self.bootOffset.setSuffix(QCoreApplication.translate("main_window", u"s", None)) self.tab_widget.setTabText(self.tab_widget.indexOf(self.settings), QCoreApplication.translate("main_window", u"Settings", None)) - self.samplePluginGroupBox.setTitle(QCoreApplication.translate("main_window", u"Sample Plugin", None)) - self.comboBox.setPlaceholderText(QCoreApplication.translate("main_window", u"firefox-compact-light@mozilla.org", None)) - self.comboBox_2.setPlaceholderText(QCoreApplication.translate("main_window", u"firefox-compact-dark@mozilla.org", None)) self.tab_widget.setTabText(self.tab_widget.indexOf(self.plugins), QCoreApplication.translate("main_window", u"Plugins", None)) pass # retranslateUi diff --git a/src/ui/main_window_connector.py b/yin_yang/ui/main_window_connector.py similarity index 94% rename from src/ui/main_window_connector.py rename to yin_yang/ui/main_window_connector.py index fc571b41..ff7a45a3 100755 --- a/src/ui/main_window_connector.py +++ b/yin_yang/ui/main_window_connector.py @@ -6,12 +6,10 @@ from PySide6.QtGui import QScreen, QColor from PySide6.QtWidgets import QFileDialog, QMessageBox, QDialogButtonBox, QColorDialog,QGroupBox -from src.ui.main_window import Ui_main_window - -from src.yin_yang import set_desired_theme -from src.meta import ConfigEvent -from src.meta import PluginKey -from src.config import config, Modes, plugins, ConfigWatcher +from .main_window import Ui_main_window +from ..theme_switcher import set_desired_theme, set_mode +from ..meta import ConfigEvent, PluginKey +from ..config import config, Modes, plugins, ConfigWatcher logger = logging.getLogger(__name__) @@ -73,6 +71,7 @@ def load(self): # set the correct mode mode = config.mode self.ui.btn_enable.setChecked(mode != Modes.MANUAL) + self.ui.manual_buttons.setVisible(mode == Modes.MANUAL) if mode == Modes.FOLLOW_SUN: self.ui.time.setVisible(False) @@ -84,6 +83,7 @@ def load(self): self.ui.toggle_sound.setChecked(config.get_plugin_key('sound', PluginKey.ENABLED)) self.ui.toggle_notification.setChecked(config.get_plugin_key('notification', PluginKey.ENABLED)) + self.ui.bootOffset.setValue(config.boot_offset) # sets the correct time based on config self.load_times() @@ -192,6 +192,16 @@ def setup_config_sync(self): self.ui.toggle_notification.toggled.connect( lambda enabled: config.update_plugin_key('notification', PluginKey.ENABLED, enabled)) + self.ui.bootOffset.valueChanged.connect(self.update_boot_offset) + + # connect manual theme buttons + self.ui.button_light.clicked.connect(lambda: set_mode(False)) + self.ui.button_dark.clicked.connect(lambda: set_mode(True)) + + @staticmethod + def update_boot_offset(value: int): + config.boot_offset = value + def save_mode(self): if not self.ui.btn_enable.isChecked(): config.mode = Modes.MANUAL diff --git a/resources_rc.py b/yin_yang/ui/resources_rc.py similarity index 56% rename from resources_rc.py rename to yin_yang/ui/resources_rc.py index 94afa888..28b759c1 100644 --- a/resources_rc.py +++ b/yin_yang/ui/resources_rc.py @@ -1,141 +1,133 @@ # Resource object code (Python 3) # Created by: object code -# Created by: The Resource Compiler for Qt version 6.4.1 +# Created by: The Resource Compiler for Qt version 6.5.0 # WARNING! All changes made in this file will be lost! from PySide6 import QtCore qt_resource_data = b"\ -\x00\x00\x07\xfd\ +\x00\x00\x07w\ <\ \xb8d\x18\xca\xef\x9c\x95\xcd!\x1c\xbf`\xa1\xbd\xdd\xa7\ -\x00\x00\x00\x05de_DEB\x00\x00\x00\x90\x00\x0a\ -KE\x00\x00\x04\xd7\x00J\x88\xea\x00\x00\x03\xf3\x00l\ -\xa7\xf3\x00\x00\x02V\x00\x89?\xc9\x00\x00\x06\xc1\x00\xb6\ -\xd1\xae\x00\x00\x00\x00\x03$u=\x00\x00\x02\xb6\x03^\ -\x05u\x00\x00\x03\x8c\x05/\xdfz\x00\x00\x04g\x06\x99\ -\x04U\x00\x00\x06N\x07;\xe0\x03\x00\x00\x05|\x0a\x00\ -\x8c2\x00\x00\x01\x18\x0a\xa0\x8cG\x00\x00\x03\x1d\x0b\x0b\ -\xe8\x0a\x00\x00\x04&\x0c\xbb\x01s\x00\x00\x06\x0c\x0d\xd7\ -\xfdR\x00\x00\x00\xac\x0e\x0e\x8c\xca\x00\x00\x04\x97\x0f\x0a\ -g\xee\x00\x00\x05\xa3\x0f/\x19\xcf\x00\x00\x01\x83i\x00\ -\x00\x07B\x03\x00\x00\x00b\x00D\x00u\x00n\x00k\ -\x00l\x00e\x00r\x00 \x00M\x00o\x00d\x00u\ -\x00s\x00 \x00w\x00i\x00r\x00d\x00 \x00z\ -\x00w\x00i\x00s\x00c\x00h\x00e\x00n\x00 \ -\x00{\x00}\x00 \x00u\x00n\x00d\x00 \x00{\ -\x00}\x00 \x00a\x00k\x00t\x00i\x00v\x00 \ -\x00s\x00e\x00i\x00n\x00.\x08\x00\x00\x00\x00\x06\ -\x00\x00\x00+Dark mode wi\ -ll be active bet\ -ween {} and {}.\x07\ -\x00\x00\x00\x0aMainWindow\x01\x03\ -\x00\x00\x00:\x00\xd6\x00f\x00f\x00n\x00e\x00 \ -\x00d\x00u\x00n\x00k\x00l\x00e\x00s\x00 \ -\x00H\x00i\x00n\x00t\x00e\x00r\x00g\x00r\ -\x00u\x00n\x00d\x00b\x00i\x00l\x00d\x08\x00\ -\x00\x00\x00\x06\x00\x00\x00\x13Open dar\ -k wallpaper\x07\x00\x00\x00\x0a\ -MainWindow\x01\x03\x00\x00\x008\ -\x00\xd6\x00f\x00f\x00n\x00e\x00 \x00h\x00e\ -\x00l\x00l\x00e\x00s\x00 \x00H\x00i\x00n\ -\x00t\x00e\x00r\x00g\x00r\x00u\x00n\x00d\ -\x00b\x00i\x00l\x00d\x08\x00\x00\x00\x00\x06\x00\x00\ -\x00\x14Open light wal\ -lpaper\x07\x00\x00\x00\x0aMainW\ -indow\x01\x03\x00\x00\x00z\x00D\x00i\x00\ -e\x00 \x00E\x00i\x00n\x00s\x00t\x00e\x00\ -l\x00l\x00u\x00n\x00g\x00e\x00n\x00 \x00\ -w\x00u\x00r\x00d\x00e\x00n\x00 \x00g\x00\ -e\x00\xe4\x00n\x00d\x00e\x00r\x00t\x00.\x00\ - \x00M\x00\xf6\x00c\x00h\x00t\x00e\x00n\x00\ - \x00S\x00i\x00e\x00 \x00s\x00i\x00e\x00\ - \x00s\x00p\x00e\x00i\x00c\x00h\x00e\x00\ -r\x00n\x00?\x08\x00\x00\x00\x00\x06\x00\x00\x00:T\ -he settings have\ - been modified. \ -Do you want to s\ -ave them?\x07\x00\x00\x00\x0aMa\ -inWindow\x01\x03\x00\x00\x002\x00U\ -\x00n\x00g\x00e\x00s\x00p\x00e\x00i\x00c\ -\x00h\x00e\x00r\x00t\x00e\x00 \x00\xc4\x00n\ -\x00d\x00e\x00r\x00u\x00n\x00g\x00e\x00n\ -\x08\x00\x00\x00\x00\x06\x00\x00\x00\x0fUnsave\ -d changes\x07\x00\x00\x00\x0aMa\ -inWindow\x01\x03\x00\x00\x000\x00S\ -\x00i\x00e\x00 \x00v\x00e\x00r\x00w\x00e\ -\x00n\x00d\x00e\x00n\x00 \x00V\x00e\x00r\ -\x00s\x00i\x00o\x00n\x00 \x00{\x00}\x08\x00\ -\x00\x00\x00\x06\x00\x00\x00\x18You are \ -using version {}\ -\x07\x00\x00\x00\x0aMainWindow\x01\ -\x03\x00\x00\x006\x00A\x00u\x00t\x00o\x00m\x00\ -a\x00t\x00i\x00s\x00c\x00h\x00e\x00r\x00\ - \x00T\x00h\x00e\x00m\x00e\x00n\x00w\x00\ -e\x00c\x00h\x00s\x00e\x00l\x08\x00\x00\x00\x00\ -\x06\x00\x00\x00\x19Automatic t\ -heme switching\x07\x00\ +\x00\x00\x00\x05de_DEB\x00\x00\x00\x98\x00\x04\ +\xa8\x8b\x00\x00\x00\xd6\x00\x05\x8c\x04\x00\x00\x068\x00\x0a\ +KE\x00\x00\x02t\x00J\x88\xea\x00\x00\x01\x06\x00R\ +\xfd\xf4\x00\x00\x01\xd7\x00\x89?\xc9\x00\x00\x05x\x02\xcf\ +6\x15\x00\x00\x06f\x03^\x05u\x00\x00\x00o\x05/\ +\xdfz\x00\x00\x02\x04\x06\x99\x04U\x00\x00\x03\xeb\x07;\ +\xe0\x03\x00\x00\x03\x19\x0ai\xf3\xe7\x00\x00\x05\xf1\x0a\xa0\ +\x8cG\x00\x00\x00\x00\x0b\x0b\xe8\x0a\x00\x00\x01\x96\x0b\xa1\ +\xae>\x00\x00\x04^\x0c\xbb\x01s\x00\x00\x03\xa9\x0e\x0e\ +\x8c\xca\x00\x00\x024\x0f\x0ag\xee\x00\x00\x03@\x0fF\ +^:\x00\x00\x019i\x00\x00\x06\xb4\x03\x00\x00\x006\ +\x00A\x00u\x00t\x00o\x00m\x00a\x00t\x00i\ +\x00s\x00c\x00h\x00e\x00r\x00 \x00T\x00h\ +\x00e\x00m\x00e\x00n\x00w\x00e\x00c\x00h\ +\x00s\x00e\x00l\x08\x00\x00\x00\x00\x06\x00\x00\x00\x19\ +Automatic theme \ +switching\x07\x00\x00\x00\x0bma\ +in_window\x01\x03\x00\x00\x008\x00\ +B\x00e\x00n\x00u\x00t\x00z\x00e\x00r\x00\ +d\x00e\x00f\x00i\x00n\x00i\x00e\x00r\x00\ +t\x00e\x00r\x00 \x00Z\x00e\x00i\x00t\x00\ +r\x00a\x00u\x00m\x08\x00\x00\x00\x00\x06\x00\x00\x00\ +\x0fCustom Schedule\ +\x07\x00\x00\x00\x0bmain_window\ +\x01\x03\x00\x00\x00\x0c\x00D\x00u\x00n\x00k\x00e\ +\x00l\x08\x00\x00\x00\x00\x06\x00\x00\x00\x04Dark\ +\x07\x00\x00\x00\x0bmain_window\ +\x01\x03\x00\x00\x00\x0e\x00D\x00u\x00n\x00k\x00e\ +\x00l\x00:\x08\x00\x00\x00\x00\x06\x00\x00\x00\x05Da\ +rk:\x07\x00\x00\x00\x0bmain_win\ +dow\x01\x03\x00\x00\x00,\x00V\x00e\x00r\x00\ +z\x00\xf6\x00g\x00e\x00r\x00u\x00n\x00g\x00\ + \x00n\x00a\x00c\x00h\x00 \x00S\x00t\x00\ +a\x00r\x00t\x08\x00\x00\x00\x00\x06\x00\x00\x00\x11D\ +elay after boot:\ +\x07\x00\x00\x00\x0bmain_window\ +\x01\x03\x00\x00\x00\x18\x00B\x00r\x00e\x00i\x00t\ +\x00e\x00n\x00g\x00r\x00a\x00d\x00:\x08\x00\ +\x00\x00\x00\x06\x00\x00\x00\x09Latitude\ +:\x07\x00\x00\x00\x0bmain_windo\ +w\x01\x03\x00\x00\x00\x08\x00H\x00e\x00l\x00l\x08\ +\x00\x00\x00\x00\x06\x00\x00\x00\x05Light\x07\x00\ \x00\x00\x0bmain_window\x01\x03\ -\x00\x00\x008\x00B\x00e\x00n\x00u\x00t\x00z\ -\x00e\x00r\x00d\x00e\x00f\x00i\x00n\x00i\ -\x00e\x00r\x00t\x00e\x00r\x00 \x00Z\x00e\ -\x00i\x00t\x00r\x00a\x00u\x00m\x08\x00\x00\x00\ -\x00\x06\x00\x00\x00\x0fCustom Sch\ -edule\x07\x00\x00\x00\x0bmain_w\ -indow\x01\x03\x00\x00\x00\x0e\x00D\x00u\x00\ -n\x00k\x00e\x00l\x00:\x08\x00\x00\x00\x00\x06\x00\ -\x00\x00\x05Dark:\x07\x00\x00\x00\x0bmai\ -n_window\x01\x03\x00\x00\x00\x18\x00B\ -\x00r\x00e\x00i\x00t\x00e\x00n\x00g\x00r\ -\x00a\x00d\x00:\x08\x00\x00\x00\x00\x06\x00\x00\x00\x09\ -Latitude:\x07\x00\x00\x00\x0bma\ -in_window\x01\x03\x00\x00\x00\x0a\x00\ -H\x00e\x00l\x00l\x00:\x08\x00\x00\x00\x00\x06\x00\ -\x00\x00\x06Light:\x07\x00\x00\x00\x0bma\ -in_window\x01\x03\x00\x00\x00\x16\x00\ -L\x00\xe4\x00n\x00g\x00e\x00n\x00g\x00r\x00\ -a\x00d\x00:\x08\x00\x00\x00\x00\x06\x00\x00\x00\x0aL\ -ongitude:\x07\x00\x00\x00\x0bma\ -in_window\x01\x03\x00\x00\x00`\x00\ -M\x00a\x00c\x00h\x00e\x00 \x00e\x00i\x00\ -n\x00 \x00G\x00e\x00r\x00\xe4\x00u\x00s\x00\ -c\x00h\x00,\x00 \x00w\x00e\x00n\x00n\x00\ - \x00d\x00a\x00s\x00 \x00T\x00h\x00e\x00\ -m\x00a\x00 \x00g\x00e\x00\xe4\x00n\x00d\x00\ -e\x00r\x00t\x00 \x00w\x00i\x00r\x00d\x08\ -\x00\x00\x00\x00\x06\x00\x00\x00%Make a \ -sound when switc\ -hing the theme\x07\x00\ +\x00\x00\x00\x0a\x00H\x00e\x00l\x00l\x00:\x08\x00\ +\x00\x00\x00\x06\x00\x00\x00\x06Light:\x07\x00\ \x00\x00\x0bmain_window\x01\x03\ -\xff\xff\xff\xff\x08\x00\x00\x00\x00\x06\x00\x00\x00\x07Pl\ -ugins\x07\x00\x00\x00\x0bmain_w\ -indow\x01\x03\x00\x00\x006\x00S\x00e\x00\ -n\x00d\x00e\x00 \x00e\x00i\x00n\x00e\x00\ - \x00B\x00e\x00n\x00a\x00c\x00h\x00r\x00\ -i\x00c\x00h\x00t\x00i\x00g\x00u\x00n\x00\ -g\x08\x00\x00\x00\x00\x06\x00\x00\x00\x13Send \ -a notification\x07\x00\ +\x00\x00\x00\x16\x00L\x00\xe4\x00n\x00g\x00e\x00n\ +\x00g\x00r\x00a\x00d\x00:\x08\x00\x00\x00\x00\x06\ +\x00\x00\x00\x0aLongitude:\x07\x00\ \x00\x00\x0bmain_window\x01\x03\ -\x00\x00\x00\x1a\x00E\x00i\x00n\x00s\x00t\x00e\ -\x00l\x00l\x00u\x00n\x00g\x00e\x00n\x08\x00\ -\x00\x00\x00\x06\x00\x00\x00\x08Settings\ -\x07\x00\x00\x00\x0bmain_window\ -\x01\x03\x00\x00\x00B\x00S\x00o\x00n\x00n\x00e\ -\x00n\x00a\x00u\x00f\x00g\x00a\x00n\x00g\ -\x00 \x00b\x00i\x00s\x00 \x00S\x00o\x00n\ -\x00n\x00e\x00n\x00u\x00n\x00t\x00e\x00r\ -\x00g\x00a\x00n\x00g\x08\x00\x00\x00\x00\x06\x00\x00\ -\x00\x11Sunset to Sunr\ -ise\x07\x00\x00\x00\x0bmain_win\ -dow\x01\x03\x00\x00\x00D\x00S\x00t\x00a\x00\ -n\x00d\x00o\x00r\x00t\x00 \x00a\x00u\x00\ -t\x00o\x00m\x00a\x00t\x00i\x00s\x00c\x00\ -h\x00 \x00a\x00k\x00t\x00u\x00a\x00l\x00\ -i\x00s\x00i\x00e\x00r\x00e\x00n\x08\x00\x00\ -\x00\x00\x06\x00\x00\x00\x1dupdate lo\ -cation automatic\ -ally\x07\x00\x00\x00\x0bmain_wi\ -ndow\x01\x88\x00\x00\x00\x02\x01\x01\ +\x00\x00\x00`\x00M\x00a\x00c\x00h\x00e\x00 \ +\x00e\x00i\x00n\x00 \x00G\x00e\x00r\x00\xe4\ +\x00u\x00s\x00c\x00h\x00,\x00 \x00w\x00e\ +\x00n\x00n\x00 \x00d\x00a\x00s\x00 \x00T\ +\x00h\x00e\x00m\x00a\x00 \x00g\x00e\x00\xe4\ +\x00n\x00d\x00e\x00r\x00t\x00 \x00w\x00i\ +\x00r\x00d\x08\x00\x00\x00\x00\x06\x00\x00\x00%Ma\ +ke a sound when \ +switching the th\ +eme\x07\x00\x00\x00\x0bmain_win\ +dow\x01\x03\xff\xff\xff\xff\x08\x00\x00\x00\x00\x06\x00\ +\x00\x00\x07Plugins\x07\x00\x00\x00\x0bm\ +ain_window\x01\x03\x00\x00\x006\ +\x00S\x00e\x00n\x00d\x00e\x00 \x00e\x00i\ +\x00n\x00e\x00 \x00B\x00e\x00n\x00a\x00c\ +\x00h\x00r\x00i\x00c\x00h\x00t\x00i\x00g\ +\x00u\x00n\x00g\x08\x00\x00\x00\x00\x06\x00\x00\x00\x13\ +Send a notificat\ +ion\x07\x00\x00\x00\x0bmain_win\ +dow\x01\x03\x00\x00\x00\x1a\x00E\x00i\x00n\x00\ +s\x00t\x00e\x00l\x00l\x00u\x00n\x00g\x00\ +e\x00n\x08\x00\x00\x00\x00\x06\x00\x00\x00\x08Set\ +tings\x07\x00\x00\x00\x0bmain_w\ +indow\x01\x03\x00\x00\x00B\x00S\x00o\x00\ +n\x00n\x00e\x00n\x00a\x00u\x00f\x00g\x00\ +a\x00n\x00g\x00 \x00b\x00i\x00s\x00 \x00\ +S\x00o\x00n\x00n\x00e\x00n\x00u\x00n\x00\ +t\x00e\x00r\x00g\x00a\x00n\x00g\x08\x00\x00\ +\x00\x00\x06\x00\x00\x00\x11Sunset to\ + Sunrise\x07\x00\x00\x00\x0bmai\ +n_window\x01\x03\x00\x00\x00\xae\x00Z\ +\x00e\x00i\x00t\x00 \x00d\x00i\x00e\x00 \ +\x00g\x00e\x00w\x00a\x00r\x00t\x00e\x00t\ +\x00 \x00w\x00e\x00r\x00d\x00e\x00n\x00 \ +\x00s\x00o\x00l\x00l\x00 \x00w\x00\xe4\x00h\ +\x00r\x00e\x00n\x00d\x00 \x00d\x00a\x00s\ +\x00 \x00S\x00y\x00s\x00t\x00e\x00m\x00 \ +\x00s\x00t\x00a\x00r\x00t\x00e\x00t\x00.\ +\x00 \x00S\x00t\x00a\x00n\x00d\x00a\x00r\ +\x00d\x00w\x00e\x00r\x00t\x00 \x00i\x00s\ +\x00t\x00 \x001\x000\x00 \x00S\x00e\x00k\ +\x00u\x00n\x00d\x00e\x00n\x00.\x08\x00\x00\x00\ +\x00\x06\x00\x00\x00LTime to wa\ +it until the sys\ +tem finished boo\ +ting. Default va\ +lue is 10 second\ +s.\x07\x00\x00\x00\x0bmain_wind\ +ow\x01\x03\x00\x00\x00<\x00P\x00o\x00s\x00i\ +\x00t\x00i\x00o\x00n\x00 \x00a\x00u\x00t\ +\x00o\x00m\x00a\x00t\x00i\x00s\x00c\x00h\ +\x00 \x00b\x00e\x00s\x00t\x00i\x00m\x00m\ +\x00e\x00n\x08\x00\x00\x00\x00\x06\x00\x00\x00\x1dup\ +date location au\ +tomatically\x07\x00\x00\x00\x0b\ +main_window\x01\x03\x00\x00\x00\ +\x1e\x00Y\x00i\x00n\x00 \x00Y\x00a\x00n\x00\ +g\x00 \x00\xf6\x00f\x00f\x00n\x00e\x00n\x08\ +\x00\x00\x00\x00\x06\x00\x00\x00\x0dOpen Yi\ +n Yang\x07\x00\x00\x00\x07systr\ +ay\x01\x03\x00\x00\x00\x0e\x00B\x00e\x00e\x00n\ +\x00d\x00e\x00n\x08\x00\x00\x00\x00\x06\x00\x00\x00\x04\ +Quit\x07\x00\x00\x00\x07systray\ +\x01\x03\x00\x00\x00&\x00F\x00a\x00r\x00b\x00s\ +\x00c\x00h\x00e\x00m\x00a\x00 \x00w\x00e\ +\x00c\x00h\x00s\x00e\x00l\x00n\x08\x00\x00\x00\ +\x00\x06\x00\x00\x00\x0cToggle the\ +me\x07\x00\x00\x00\x07systray\x01\x88\ +\x00\x00\x00\x02\x01\x01\ \x00\x00\x07\x22\ \x00\ \x00\x1dUx\xda\xcdYKs\xdb6\x10\xbe\xe7Wp\ @@ -282,8 +274,8 @@ \x00\x00\x00\x10\x00\x02\x00\x00\x00\x01\x00\x00\x00\x03\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00.\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01\x87W\x9b\xe0\xc2\ -\x00\x00\x00P\x00\x01\x00\x00\x00\x01\x00\x00\x08\x01\ +\x00\x00\x01\x88)>\x08Z\ +\x00\x00\x00P\x00\x01\x00\x00\x00\x01\x00\x00\x07{\ \x00\x00\x01\x84\x01\xd5\x8cC\ "