diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index c697af30..b34c9226 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,12 +1,12 @@ # These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username +patreon: L0drex # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: daehruoydeef +liberapay: # liberapay user name issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: # Replace with a single custom sponsorship URL diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 42253cd2..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -name: Bug report -about: Report if something doesn't work as expected -title: '' -labels: bug -assignees: '' - ---- - -### Describe the bug - - -### Enabled plugins - -What plugins did you have enabled? If not relevant, you can remove this. - -- [ ] Atom -- [ ] Brave -- [ ] Colors -- [ ] Custom Script -- [ ] Firefox -- [ ] Gedit -- [ ] GTK -- [ ] Konsole -- [ ] Kvantum -- [ ] Only Office -- [ ] System -- [ ] VSCode -- [ ] Wallpaper - -### Affected versions - -- Yin-Yang version: - -- Relevant application version[^1]: -- Python version: -- Qt version: - -[^1]: This refers to the application a plugin might use. For example, if you submit a bug report for the Firefox plugin, this refers to the Firefox version you are using. If the bug is in a plugin for a desktop environment, this refers to the DE (Plasma, Gnome, etc). - -### Additional notes - diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..811323b4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,75 @@ +name: Bug Report +description: File a bug report +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Also tell us, what did you expect to happen? + placeholder: Tell us what you see! + value: "A bug happened!" + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: What version of Yin & Yang are you running? + validations: + required: true + - type: dropdown + id: source + attributes: + label: How did you install Yin & Yang? + options: + - Git clone + running install script + - AUR + - Flatpak + validations: + required: true + - type: dropdown + id: desktop + attributes: + label: What desktop environments are you seeing the problem on? + multiple: true + options: + - KDE + - Gnome + - Xfce + - Mate + - Cinnamon + - other + - type: dropdown + id: plugins + attributes: + label: Which plugin causes the issue? + options: + - Atom + - Brave + - Colors + - Custom Script + - Firefox + - Gedit + - GTK + - Konsole + - Kvantum + - Only Office + - System + - VSCode + - Wallpaper + - type: input + id: plugin_version + attributes: + label: What software version do you use? + description: For example, if you see a problem with the VSCode plugin, this would refer to the version of VSCode you have installed. + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell diff --git a/designer/main_window.ui b/designer/main_window.ui index 22e8ed8d..3d6fad1a 100755 --- a/designer/main_window.ui +++ b/designer/main_window.ui @@ -296,13 +296,6 @@ - - - - Make a sound when switching the theme - - - @@ -374,8 +367,8 @@ 0 0 - 518 - 88 + 501 + 86 diff --git a/requirements.txt b/requirements.txt index 5e3f2fe9..c1ed9b88 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -psutil==5.9.6 -PySide6==6.6.0 -PySide6-Addons==6.6.0 +psutil==5.9.7 +PySide6==6.6.1 +PySide6-Addons==6.6.1 suntime==1.2.5 systemd-python==235 requests~=2.31.0 \ No newline at end of file diff --git a/resources/dark.wav b/resources/dark.wav deleted file mode 100755 index 2f2cee78..00000000 Binary files a/resources/dark.wav and /dev/null differ diff --git a/resources/light.wav b/resources/light.wav deleted file mode 100755 index b7ee33ca..00000000 Binary files a/resources/light.wav and /dev/null differ diff --git a/resources/translations/yin_yang.de_DE.qm b/resources/translations/yin_yang.de_DE.qm index dfe9e222..1cde15fe 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 ce9eecc2..a987ce5b 100644 --- a/resources/translations/yin_yang.de_DE.ts +++ b/resources/translations/yin_yang.de_DE.ts @@ -60,31 +60,26 @@ - 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 @@ -92,19 +87,19 @@ systray - + Open Yin Yang Context menu action in the systray Yin Yang öffnen - + Toggle theme Context menu action in the systray Farbschema wechseln - + 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 065f4499..96167969 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 ff50bcde..f3898afe 100644 --- a/resources/translations/yin_yang.nl_NL.ts +++ b/resources/translations/yin_yang.nl_NL.ts @@ -60,31 +60,26 @@ - 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 @@ -92,19 +87,19 @@ systray - + Open Yin Yang Context menu action in the systray - + Toggle theme Context menu action in the systray - + Quit Context menu action in the systray diff --git a/resources/translations/yin_yang.zh_TW.qm b/resources/translations/yin_yang.zh_TW.qm index b8a23ded..888f2cb2 100644 Binary files a/resources/translations/yin_yang.zh_TW.qm and b/resources/translations/yin_yang.zh_TW.qm differ diff --git a/resources/translations/yin_yang.zh_TW.ts b/resources/translations/yin_yang.zh_TW.ts index 7e7a5364..be8b9896 100644 --- a/resources/translations/yin_yang.zh_TW.ts +++ b/resources/translations/yin_yang.zh_TW.ts @@ -60,31 +60,26 @@ - Make a sound when switching the theme - 切換主題時播放音效 - - - Send a notification 推送通知 - + Time to wait until the system finished booting. Default value is 10 seconds. 開機後延遲秒數。預設為十秒。 - + Delay after boot: 延遲秒數: - + s - + Plugins 擴充功能 @@ -92,19 +87,19 @@ systray - + Open Yin Yang Context menu action in the systray - + Toggle theme Context menu action in the systray - + Quit Context menu action in the systray diff --git a/yin_yang/__main__.py b/yin_yang/__main__.py index c5d248f4..a4474c79 100755 --- a/yin_yang/__main__.py +++ b/yin_yang/__main__.py @@ -54,6 +54,7 @@ def setup_logger(use_systemd_journal: bool): ) logging.root.addHandler(file_handler) + def systray_icon_clicked(reason: QSystemTrayIcon.ActivationReason): match reason: case QSystemTrayIcon.ActivationReason.MiddleClick: @@ -68,6 +69,7 @@ def systray_icon_clicked(reason: QSystemTrayIcon.ActivationReason): help='toggles Yin-Yang', action='store_true') parser.add_argument('--systemd', help='uses systemd journal handler and applies desired theme', action='store_true') +parser.add_argument('--minimized', help='starts the program to tray bar', action='store_true') arguments = parser.parse_args() setup_logger(arguments.systemd) @@ -80,6 +82,7 @@ def systray_icon_clicked(reason: QSystemTrayIcon.ActivationReason): elif arguments.systemd: theme_switcher.set_desired_theme() + else: # load GUI config.add_event_listener(ConfigEvent.SAVE, daemon_handler.watcher) @@ -122,15 +125,24 @@ def systray_icon_clicked(reason: QSystemTrayIcon.ActivationReason): 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) + 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()) + if arguments.minimized: + sys.exit(app.exec()) + else: + window = main_window_connector.MainWindow() + window.show() + sys.exit(app.exec()) diff --git a/yin_yang/communicate.py b/yin_yang/communicate.py index 2fbb7222..39dc2962 100755 --- a/yin_yang/communicate.py +++ b/yin_yang/communicate.py @@ -20,7 +20,7 @@ logger = logging.getLogger(__name__) -def _move_times(time_now: datetime, time_light: dt_time, time_dark: dt_time) -> list[int, int]: +def _move_times(time_now: datetime, time_light: dt_time, time_dark: dt_time) -> list[int]: """ Converts a time string to seconds since the epoch :param time_now: the current time diff --git a/yin_yang/config.py b/yin_yang/config.py index c3fce903..f62d0d52 100755 --- a/yin_yang/config.py +++ b/yin_yang/config.py @@ -5,15 +5,12 @@ from abc import ABC, abstractmethod from datetime import time from functools import cache -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 .position import get_current_location from .meta import Modes, Desktop, PluginKey, ConfigEvent from .plugins import get_plugins @@ -116,36 +113,6 @@ def get_sun_time(latitude, longitude) -> tuple[time, time]: logger.error(f'Error: {e}.') -parent = QObject() -locationSource = QGeoPositionInfoSource.createDefaultSource(parent) - - -@cache -def get_current_location() -> QGeoCoordinate: - if locationSource is None: - logger.error("No location source is available") - return QGeoCoordinate(0, 0) - - pos: QGeoPositionInfo = locationSource.lastKnownPosition() - if pos is None: - locationSource.requestUpdate(10) - tries = 0 - while pos is None and tries < 10: - pos = locationSource.lastKnownPosition() - tries += 1 - sleep(1) - coordinate = pos.coordinate() - if not coordinate.isValid(): - 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 - - def get_desktop() -> Desktop: desktop = os.getenv('XDG_CURRENT_DESKTOP') if desktop is None: @@ -154,7 +121,7 @@ def get_desktop() -> Desktop: desktop = '' match desktop.lower(): - case 'gnome' | 'budgie': + case 'gnome': return Desktop.GNOME case 'kde' | 'plasma' | 'plasma5': return Desktop.KDE @@ -166,6 +133,8 @@ def get_desktop() -> Desktop: return Desktop.CINNAMON case 'sway' | 'hyprland': return Desktop.GNOME + case 'budgie:gnome' | 'budgie-desktop' | 'budgie': + return Desktop.BUDGIE case _: return Desktop.UNKNOWN @@ -253,12 +222,17 @@ def load(self) -> None: # check if config needs an update # if the default values are set, the version number is below 0 if config_loaded['version'] < self.defaults['version']: - config_loaded = update_config(config_loaded, self.defaults) + try: + config_loaded = update_config(config_loaded, self.defaults) + except Exception as e: + logger.error('An error ocurred while trying to update the config. Using defaults instead.') + logger.error(e) + config_loaded = self.defaults - for pl in plugins: - pl.theme_light = config_loaded['plugins'][pl.name.lower()]['light_theme'] - pl.theme_dark = config_loaded['plugins'][pl.name.lower()]['dark_theme'] - pl.enabled = config_loaded['plugins'][pl.name.lower()]['enabled'] + for p in plugins: + p.theme_light = config_loaded['plugins'][p.name.lower()]['light_theme'] + p.theme_dark = config_loaded['plugins'][p.name.lower()]['dark_theme'] + p.enabled = config_loaded['plugins'][p.name.lower()]['enabled'] self.update(config_loaded) @@ -354,7 +328,7 @@ def defaults(self) -> dict: 'running': False, 'dark_mode': False, 'mode': Modes.MANUAL.value, - 'coordinates': (0, 0), + 'coordinates': (0.0, 0.0), 'update_location': False, 'update_interval': 60, 'times': ('07:00', '20:00'), @@ -423,8 +397,17 @@ def mode(self, mode: Modes): @property def location(self) -> tuple[float, float]: if self.update_location: - coordinate = get_current_location() - return coordinate.latitude(), coordinate.longitude() + try: + coordinate = get_current_location() + return coordinate.latitude(), coordinate.longitude() + except TypeError as e: + logger.error('Unable to update position. Using config values as fallback.') + logger.error(e) + pass + except ValueError as e: + logger.error('Unable to update position. Using config values as fallback.') + logger.error(e) + pass return self['coordinates'] @@ -494,7 +477,7 @@ def boot_offset(self, value: int): logger.info('Detected desktop:', config.desktop) # set plugin themes -for p in filter(lambda pl: pl.available, plugins): - p.enabled = config.get_plugin_key(p.name, PluginKey.ENABLED) - p.theme_bright = config.get_plugin_key(p.name, PluginKey.THEME_LIGHT) - p.theme_dark = config.get_plugin_key(p.name, PluginKey.THEME_DARK) +for pl in filter(lambda pl: pl.available, plugins): + pl.enabled = config.get_plugin_key(pl.name, PluginKey.ENABLED) + pl.theme_bright = config.get_plugin_key(pl.name, PluginKey.THEME_LIGHT) + pl.theme_dark = config.get_plugin_key(pl.name, PluginKey.THEME_DARK) diff --git a/yin_yang/meta.py b/yin_yang/meta.py index cbfe421f..eae84667 100644 --- a/yin_yang/meta.py +++ b/yin_yang/meta.py @@ -16,6 +16,7 @@ class Desktop(Enum): UNKNOWN = 'unknown' MATE = 'mate' CINNAMON = 'cinnamon' + BUDGIE = 'budgie' class PluginKey(Enum): @@ -29,5 +30,11 @@ class ConfigEvent(Enum): SAVE = auto() +class FileFormat(Enum): + PLAIN = auto() + JSON = auto() + CONFIG = auto() + + class UnsupportedDesktopError(NotImplementedError): pass diff --git a/yin_yang/plugins/__init__.py b/yin_yang/plugins/__init__.py index c40941c2..f81da3f0 100755 --- a/yin_yang/plugins/__init__.py +++ b/yin_yang/plugins/__init__.py @@ -1,8 +1,8 @@ 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 +from . import firefox, only_office, okular +from . import vscode, konsole +from . import notify # NOTE initialize your plugin over here: # The order in the list specifies the order in the config gui @@ -18,15 +18,11 @@ def get_plugins(desktop: Desktop) -> [Plugin]: kvantum.Kvantum(), wallpaper.Wallpaper(desktop), firefox.Firefox(), - brave.Brave(), vscode.Vscode(), - atom.Atom(), - gedit.Gedit(), only_office.OnlyOffice(), okular.Okular(), konsole.Konsole(), custom.Custom(), - sound.Sound(), notify.Notification() ] diff --git a/yin_yang/plugins/_plugin.py b/yin_yang/plugins/_plugin.py index 05d43e59..ddce0895 100644 --- a/yin_yang/plugins/_plugin.py +++ b/yin_yang/plugins/_plugin.py @@ -1,12 +1,17 @@ +import copy +import json import logging import subprocess from abc import ABC, abstractmethod +from configparser import ConfigParser +from pathlib import Path from typing import Optional +from PySide6.QtDBus import QDBusConnection, QDBusMessage from PySide6.QtGui import QColor, QRgba64 from PySide6.QtWidgets import QGroupBox, QHBoxLayout, QLineEdit, QComboBox -from ..meta import UnsupportedDesktopError +from ..meta import UnsupportedDesktopError, FileFormat logger = logging.getLogger(__name__) @@ -89,7 +94,7 @@ def get_input(self, widget): # add all theme names for i, curComboBox in enumerate(inputs): themes = list(self.available_themes.values()) - themes.sort() + themes.sort(key=lambda s: s.casefold()) curComboBox.addItems(themes) curComboBox.setMinimumContentsLength(4) # set index @@ -248,22 +253,97 @@ def set_theme(self, theme: str): assert False, 'This is an external plugin, the mode can only be changed externally.' -def inplace_change(filename: str, old_string: str, new_string: str): - """Replaces a given string by a new string in a specific file - :param filename: the full path to the file that should be changed - :param old_string: the old string that should be found in the file - :param new_string: the string that should replace the old string - """ - # Safely read the input filename using 'with' - with open(filename, 'r') as file: - file_content = file.read() - if old_string not in file_content: - raise ValueError(f'{old_string} could not be found in {filename}') +class DBusPlugin(Plugin): + def __init__(self, base_message: QDBusMessage): + super().__init__() + self.connection = QDBusConnection.sessionBus() + self.base_message = base_message + + def set_theme(self, theme: str): + if not (self.available and self.enabled): + return + + if not theme: + raise ValueError(f'Theme \"{theme}\" is invalid') + + self.call(self.create_message(theme)) - # Safely write the changed content, if found in the file - with open(filename, 'w') as file: - file_content = file_content.replace(old_string, new_string) - file.write(file_content) + def create_message(self, theme: str) -> QDBusMessage: + message = copy.deepcopy(self.base_message) + message.setArguments([theme]) + return message + + def call(self, message) -> QDBusMessage: + return self.connection.call(message) + + +class ConfigFilePlugin(Plugin): + def __init__(self, config_paths: list[Path], file_format=FileFormat.PLAIN): + self.config_paths: list[Path] = config_paths + self.file_format = file_format + super().__init__() + + @property + def available(self) -> bool: + # check if any config file exists + for config in self.config_paths: + if config.is_file(): + return True + + return False + + def open_config(self, path: Path): + with open(path) as file: + match self.file_format.value: + case FileFormat.JSON.value: + try: + return json.load(file) + except json.decoder.JSONDecodeError as e: + return self.default_config + case FileFormat.CONFIG.value: + config = ConfigParser() + config.optionxform = str + config.read_file(file) + return config + case _: + return file.read() + + def write_config(self, value: str | ConfigParser, path: Path, **kwargs): + with open(path, 'w') as file: + if self.file_format.value == FileFormat.CONFIG.value: + value.write(file, **kwargs) + else: + file.write(value) + + def set_theme(self, theme: str, ignore_theme_check=False): + if not (self.available and self.enabled): + return + + if not ignore_theme_check and not theme: + raise ValueError(f'Theme \"{theme}\" is invalid') + + try: + for config_path in self.config_paths: + if not config_path.exists(): + continue + + config = self.open_config(config_path) + new_config = self.update_config(config, theme) + self.write_config(new_config, config_path) + except StopIteration: + raise FileNotFoundError( + 'No config file found. ' + 'If you see this error, try to set a custom theme manually once and try again.') + + @abstractmethod + def update_config(self, config, theme: str) -> str: + """Set the theme in the config.""" + raise NotImplementedError + + @property + def default_config(self): + """Fallback config file if active file should be empty.""" + raise FileNotFoundError('Config file is empty. Try to set a theme manually once and try again.') def get_qcolor_from_int(color_int: int) -> QColor: @@ -276,3 +356,15 @@ def get_int_from_qcolor(color: QColor) -> int: # ... - 2^32 converts uint to int color_int = color.rgba64().toArgb32() - 2 ** 32 return color_int + + +def flatpak_system(app_id: str) -> Path: + return Path(f'/var/lib/flatpak/app/{app_id}/current/active') + + +def flatpak_user(app_id: str) -> Path: + return Path.home() / f'.var/app/{app_id}' + + +def snap_path(app: str) -> Path: + return Path(f'/var/lib/snapd/snap/{app}/current') diff --git a/yin_yang/plugins/atom.py b/yin_yang/plugins/atom.py deleted file mode 100755 index cf17bf16..00000000 --- a/yin_yang/plugins/atom.py +++ /dev/null @@ -1,51 +0,0 @@ -import re -from os.path import isfile -from pathlib import Path - -from ._plugin import Plugin, inplace_change - - -def get_old_theme(settings): - """ - Returns the theme that is currently used. - Uses regex to find the currently used theme, I expect that themes follow this pattern: - XXXX-XXXX-ui XXXX-XXXX-syntax - """ - with open(settings, "r") as file: - string = file.read() - themes = re.findall(r'themes: \[[\s]*"([A-Za-z0-9\-]*)"[\s]*"([A-Za-z0-9\-]*)"', string) - if len(themes) >= 1: - ui_theme, _ = themes[0] - used_theme = re.findall('([A-z-A-z]*)-', ui_theme)[0] - return used_theme - - -class Atom(Plugin): - # noinspection SpellCheckingInspection - config_path = str(Path.home()) + "/.atom/config.cson" - - def __init__(self): - super().__init__() - self.theme_light = 'one-light' - self.theme_dark = 'one-dark' - - def set_theme(self, theme: str): - if not (self.available and self.enabled): - return - - if not theme: - raise ValueError(f'Theme \"{theme}\" is invalid') - - # getting the old theme first - current_theme: str = get_old_theme(self.config_path) - - if not current_theme: - raise ValueError("Current theme could not be determined." - "If you see this error, try to set a custom theme once and then try again") - - # updating the old theme with theme specified in config - inplace_change(self.config_path, current_theme, theme) - - @property - def available(self) -> bool: - return isfile(self.config_path) diff --git a/yin_yang/plugins/brave.py b/yin_yang/plugins/brave.py deleted file mode 100644 index 8b2cf451..00000000 --- a/yin_yang/plugins/brave.py +++ /dev/null @@ -1,51 +0,0 @@ -import json -from os.path import isfile -from pathlib import Path - -from PySide6.QtGui import QColor -from PySide6.QtWidgets import QWidget, QVBoxLayout, QLineEdit, QPushButton - -from ._plugin import Plugin, get_int_from_qcolor - -path = f'{Path.home()}/.config/BraveSoftware/Brave-Browser/Default/Preferences' - - -class Brave(Plugin): - def __init__(self): - super().__init__() - self.theme_light = '#ffffff' - self.theme_dark = '#000000' - - def set_theme(self, color_str: str): - with open(path, 'r') as file: - config = json.load(file) - - color = get_int_from_qcolor(QColor(color_str)) - config['autogenerated']['theme']['color'] = color - - def get_input(self, widget): - widgets = [] - - for dark_theme in [False, True]: - grp = QWidget(widget) - horizontal_layout = QVBoxLayout(grp) - - line = QLineEdit(grp) - color_str = self.theme_dark if dark_theme else self.theme_light - line.setText(color_str) - color = QColor(color_str) - line.setStyleSheet(f'background-color: {color_str};' - f' color: {"white" if color.lightness() <= 128 else "black"}') - horizontal_layout.addWidget(line) - - btn = QPushButton() - btn.setText(f'Pick {"dark" if dark_theme else "light"} color') - horizontal_layout.addWidget(btn) - - widgets.append(grp) - - return widgets - - @property - def available(self) -> bool: - return isfile(path) diff --git a/yin_yang/plugins/firefox.py b/yin_yang/plugins/firefox.py index 7967fac7..8407e0c4 100755 --- a/yin_yang/plugins/firefox.py +++ b/yin_yang/plugins/firefox.py @@ -11,14 +11,14 @@ logger = logging.getLogger(__name__) -def get_profile_paths() -> str: - path = str(Path.home()) + '/.mozilla/firefox/' +def get_profile_paths() -> Path: + path = Path.home() / '.mozilla/firefox/' config_parser = ConfigParser() - config_parser.read(path + '/profiles.ini') + config_parser.read(path / 'profiles.ini') for section in config_parser: if not section.startswith('Profile'): continue - yield path + config_parser[section]['Path'] + yield path / config_parser[section]['Path'] class Firefox(ExternalPlugin): @@ -34,7 +34,7 @@ def available_themes(self) -> dict: if not self.available: return {} - paths = (p + '/extensions.json' for p in get_profile_paths()) + paths = (p / 'extensions.json' for p in get_profile_paths()) themes: dict[str, str] = {} for path in paths: @@ -44,7 +44,7 @@ def available_themes(self) -> dict: for addon in content['addons']: if addon['type'] == 'theme': themes[addon['id']] = addon['defaultLocale']['name'] - except FileNotFoundError as e: + except FileNotFoundError as _: logger.warning(f'Firefox profile has no extensions installed: {path}') continue diff --git a/yin_yang/plugins/gedit.py b/yin_yang/plugins/gedit.py deleted file mode 100644 index 3fc45a70..00000000 --- a/yin_yang/plugins/gedit.py +++ /dev/null @@ -1,34 +0,0 @@ -import os -from os.path import isdir -from xml.etree import ElementTree - -from ._plugin import PluginCommandline -from .system import test_gnome_availability - -path = '/usr/share/gtksourceview-4/styles/' - - -class Gedit(PluginCommandline): - def __init__(self): - super(Gedit, self).__init__(['gsettings', 'set', 'org.gnome.gedit.preferences.editor', 'scheme', '{theme}']) - - @property - def available(self) -> bool: - return test_gnome_availability(self.command) - - @property - def available_themes(self) -> dict: - if not isdir(path): - return {} - - themes = {} - with os.scandir(path) as entries: - for file in (f.path for f in entries if f.is_file() and not f.name.endswith('.rng')): - config = ElementTree.parse(file) - attributes = config.getroot().attrib - - name = attributes.get('_name') - theme_id = attributes.get('id') - themes[theme_id] = name if name is not None else theme_id - - return themes diff --git a/yin_yang/plugins/gtk.py b/yin_yang/plugins/gtk.py index 153e552e..fa7a411f 100755 --- a/yin_yang/plugins/gtk.py +++ b/yin_yang/plugins/gtk.py @@ -1,13 +1,13 @@ import logging +import subprocess from os import scandir, path from pathlib import Path -import subprocess -from PySide6.QtDBus import QDBusConnection, QDBusMessage +from PySide6.QtDBus import QDBusMessage -from ..meta import Desktop -from ._plugin import PluginDesktopDependent, Plugin, PluginCommandline +from ._plugin import PluginDesktopDependent, PluginCommandline, DBusPlugin from .system import test_gnome_availability +from ..meta import Desktop logger = logging.getLogger(__name__) @@ -33,6 +33,8 @@ def __init__(self, desktop: Desktop): super().__init__(_Xfce()) case Desktop.CINNAMON: super().__init__(_Cinnamon()) + case Desktop.BUDGIE: + super().__init__(_Budgie()) case _: super().__init__(None) @@ -61,42 +63,59 @@ def __init__(self): @property def available(self) -> bool: return test_gnome_availability(self.command) + + +class _Budgie(PluginCommandline): + name = 'GTK' + + def __init__(self): + super().__init__(['gsettings', 'set', 'org.gnome.desktop.interface', 'gtk-theme', '{theme}']) + self.theme_light = 'Default' + self.theme_dark = 'Default' + + @property + def available(self) -> bool: + return test_gnome_availability(self.command) -class _Kde(Plugin): +class _Kde(DBusPlugin): name = 'GTK' def __init__(self): - super().__init__() - self.theme_light = 'Breeze' - self.theme_dark = 'Breeze' - - def set_theme(self, theme: str): - connection = QDBusConnection.sessionBus() message = QDBusMessage.createMethodCall( 'org.kde.GtkConfig', '/GtkConfig', 'org.kde.GtkConfig', 'setGtkTheme' ) - message.setArguments([theme]) - response = connection.call(message) - if response.type() == QDBusMessage.MessageType.ErrorMessage: - logger.warning('kde-gtk-config not available, try xsettingsd') - xsettingsd_conf_path = Path.home() / '.config' / 'xsettingsd' / 'xsettingsd.conf' - if not xsettingsd_conf_path.exists(): - logger.warning('xsettingsd not available') - with open(xsettingsd_conf_path, 'r') as f: - lines = f.readlines() - for i, line in enumerate(lines): - if line.startswith('Net/ThemeName'): - lines[i] = f'Net/ThemeName "{theme}"\n' - break - with open(xsettingsd_conf_path, 'w') as f: - f.writelines(lines) - subprocess.run(['killall', '-HUP', 'xsettingsd']) - else: - logger.debug('Success by kde-gtk-config') + super().__init__(message) + self.theme_light = 'Breeze' + self.theme_dark = 'Breeze' + + def set_theme(self, theme: str): + response = self.call(self.create_message(theme)) + + if response.type() != QDBusMessage.MessageType.ErrorMessage: + return + + logger.warning('kde-gtk-config not available, trying xsettingsd') + xsettingsd_conf_path = Path.home() / '.config/xsettingsd/xsettingsd.conf' + if not xsettingsd_conf_path.exists(): + logger.warning('xsettingsd not available') + return + + with open(xsettingsd_conf_path, 'r') as f: + lines = f.readlines() + for i, line in enumerate(lines): + if line.startswith('Net/ThemeName'): + lines[i] = f'Net/ThemeName "{theme}"\n' + break + + with open(xsettingsd_conf_path, 'w') as f: + f.writelines(lines) + + # send signal to read new config + subprocess.run(['killall', '-HUP', 'xsettingsd']) class _Xfce(PluginCommandline): diff --git a/yin_yang/plugins/icons.py b/yin_yang/plugins/icons.py index 3121e6c4..04cf65ec 100644 --- a/yin_yang/plugins/icons.py +++ b/yin_yang/plugins/icons.py @@ -1,6 +1,10 @@ from .system import test_gnome_availability from ..meta import Desktop from ._plugin import PluginDesktopDependent, PluginCommandline +from pathlib import Path +from os import scandir, path + +theme_directories = ['/usr/share/icons', f'{Path.home()}/.icons'] class Icons(PluginDesktopDependent): @@ -10,6 +14,8 @@ def __init__(self, desktop: Desktop): super().__init__(_Mate()) case Desktop.CINNAMON: super().__init__(_Cinnamon()) + case Desktop.BUDGIE: + super().__init__(_Budgie()) case _: super().__init__(None) @@ -34,3 +40,27 @@ def __init__(self): @property def available(self) -> bool: return test_gnome_availability(self.command) + + +class _Budgie(PluginCommandline): + def __init__(self): + super().__init__(['gsettings', 'set', 'org.gnome.desktop.interface', 'icon-theme', '\"{theme}\"']) + self.theme_light = 'Default' + self.theme_dark = 'Default' + + @property + def available(self) -> bool: + return test_gnome_availability(self.command) + + @property + def available_themes(self) -> dict: + themes = [] + + for directory in theme_directories: + if not path.isdir(directory): + continue + + with scandir(directory) as entries: + themes.extend(d.name for d in entries if d.is_dir() and path.isfile(d.path + '/index.theme')) + + return {t: t for t in themes} diff --git a/yin_yang/plugins/konsole.py b/yin_yang/plugins/konsole.py index ebe8a561..5d458138 100644 --- a/yin_yang/plugins/konsole.py +++ b/yin_yang/plugins/konsole.py @@ -74,7 +74,7 @@ def set_mode(self, dark: bool) -> bool: logger.debug(f'Changing profile in konsole session {proc_id}') set_profile(f'org.kde.konsole-{proc_id}', profile) - set_profile('org.kde.konsole', profile) # konsole may don't have session dbus like above + set_profile('org.kde.konsole', profile) # konsole may don't have session dbus like above set_profile('org.kde.yakuake', profile) process_ids = [ diff --git a/yin_yang/plugins/kvantum.py b/yin_yang/plugins/kvantum.py index 82854a49..f9befeb7 100755 --- a/yin_yang/plugins/kvantum.py +++ b/yin_yang/plugins/kvantum.py @@ -1,4 +1,4 @@ -import os +from os import walk from pathlib import Path from ._plugin import PluginCommandline @@ -11,9 +11,9 @@ def __init__(self): self.theme_dark = 'KvFlat' @classmethod - def get_kvantum_theme_from_dir(cls, dir): + def get_kvantum_theme_from_dir(cls, directory: Path): result = set() - for _, _, filenames in os.walk(dir): + for _, _, filenames in walk(directory): for filename in filenames: if filename.endswith('.kvconfig'): result.add(filename[:-9]) @@ -24,11 +24,10 @@ def available_themes(self) -> dict: if not self.available: return {} - paths = ['/usr/share/Kvantum', str(Path.home()) + '/.config/Kvantum'] + paths = [Path('/usr/share/Kvantum'), Path.home() / '.config/Kvantum'] themes = list() for path in paths: themes = themes + self.get_kvantum_theme_from_dir(path) - themes_dict: dict = {} assert len(themes) > 0, 'No themes were found' themes.sort() diff --git a/yin_yang/plugins/okular.py b/yin_yang/plugins/okular.py index 79832ef7..d9d3bbb6 100644 --- a/yin_yang/plugins/okular.py +++ b/yin_yang/plugins/okular.py @@ -1,40 +1,25 @@ import os -from configparser import ConfigParser from pathlib import Path import psutil from PySide6.QtDBus import QDBusConnection, QDBusMessage -from ._plugin import Plugin +from ..meta import FileFormat +from ._plugin import flatpak_user, ConfigFilePlugin -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""" +class Okular(ConfigFilePlugin): + """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__() + super().__init__([ + Path.home() / '.config/okularpartrc', + flatpak_user('org.kde.okular') / 'config/okularpartrc' + ], file_format=FileFormat.CONFIG) 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 + self._theme_dark = 'InvertLightness' def set_mode(self, dark: bool): if not self.enabled: @@ -59,25 +44,18 @@ def set_mode(self, dark: bool): 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 + self.set_theme(self.theme_dark if dark else self.theme_light, ignore_theme_check=True) + + def update_config(self, config, theme: str) -> str: + if theme == self.theme_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') + return config @property def available_themes(self) -> dict: @@ -108,11 +86,13 @@ def theme_dark(self): def theme_dark(self, value): self._theme_dark = value - for path in self.user_paths: - config = ConfigParser() - config.optionxform = str - config.read(path) + for config_path in self.config_paths: + if not config_path.exists(): + continue + + config = self.open_config(config_path) + # update rendering mode if value == '': if config.has_section('Document'): config.remove_option('Document', 'RenderMode') @@ -123,5 +103,4 @@ def theme_dark(self, value): config.add_section('Document') config['Document']['RenderMode'] = value - with open(path, 'w') as file: - config.write(file, space_around_delimiters=False) + self.write_config(config, config_path, space_around_delimiters=False) diff --git a/yin_yang/plugins/only_office.py b/yin_yang/plugins/only_office.py index 9b59dd07..66a82a1b 100644 --- a/yin_yang/plugins/only_office.py +++ b/yin_yang/plugins/only_office.py @@ -1,30 +1,22 @@ from configparser import ConfigParser -from os.path import isfile from pathlib import Path -from ._plugin import Plugin +from ..meta import FileFormat +from ._plugin import ConfigFilePlugin, flatpak_user -config_path = f'{Path.home()}/.config/onlyoffice/DesktopEditors.conf' - -class OnlyOffice(Plugin): +class OnlyOffice(ConfigFilePlugin): def __init__(self): - super().__init__() + super().__init__([ + Path.home() / '.config/onlyoffice/DesktopEditors.conf', + flatpak_user('org.onlyoffice.desktopeditors') / 'config/onlyoffice/DesktopEditors.conf' + ], file_format=FileFormat.CONFIG) self.theme_light = 'theme-light' self.theme_dark = 'theme-dark' - def set_theme(self, theme: str): - config = ConfigParser() - config.optionxform = str - config.read(config_path) - config['General']['UITheme2'] = theme - - with open(config_path, 'w') as file: - config.write(file) - - @property - def available(self) -> bool: - return isfile(config_path) + def update_config(self, config: ConfigParser, theme: str): + config['General']['UITheme'] = theme + return config @property def available_themes(self) -> dict: diff --git a/yin_yang/plugins/sound.py b/yin_yang/plugins/sound.py deleted file mode 100644 index a9b3fcc0..00000000 --- a/yin_yang/plugins/sound.py +++ /dev/null @@ -1,17 +0,0 @@ -import subprocess - -from ._plugin import PluginCommandline - - -class Sound(PluginCommandline): - def __init__(self): - super(Sound, self).__init__(["paplay", '{theme}']) - self.theme_light = './resources/light.wav' - self.theme_dark = './resources/dark.wav' - - @property - def available(self) -> bool: - try: - return subprocess.run(['paplay', '--help'], stdout=subprocess.DEVNULL).returncode == 0 - except FileNotFoundError: - return False diff --git a/yin_yang/plugins/system.py b/yin_yang/plugins/system.py index f5c2710d..c58bb14f 100644 --- a/yin_yang/plugins/system.py +++ b/yin_yang/plugins/system.py @@ -31,6 +31,8 @@ def __init__(self, desktop: Desktop): super().__init__(_Mate()) case Desktop.CINNAMON: super().__init__(_Cinnamon()) + case Desktop.BUDGIE: + super().__init__(_Budgie()) case _: super().__init__(None) @@ -48,6 +50,41 @@ def available(self) -> bool: return test_gnome_availability(self.command) +class _Budgie(PluginCommandline): + name = 'System' + + def __init__(self): + super().__init__(['gsettings', 'set', 'com.solus-project.budgie-panel', 'dark-theme', '{theme}']) + self.theme_light = 'light' + self.theme_dark = 'dark' + + @property + def available(self) -> bool: + return test_gnome_availability(self.command) + + # Override because budgie uses a switch for dark/light mode + def insert_theme(self, theme: str) -> list: + command = self.command.copy() + match theme.lower(): + case 'dark': + theme_bool = 'true' + case 'light': + theme_bool = 'false' + case _: + raise NotImplementedError + + for i, arg in enumerate(command): + command[i] = arg.format(theme=theme_bool) + + return command + + @property + def available_themes(self) -> dict: + themes: dict[str, str] = {'dark': 'Dark', 'light': 'Light'} + + return themes + + def get_readable_kde_theme_name(file) -> str: """Searches for the long_name in the file and maps it to the found short name""" diff --git a/yin_yang/plugins/vscode.py b/yin_yang/plugins/vscode.py index 4bbeb15f..c63de74d 100755 --- a/yin_yang/plugins/vscode.py +++ b/yin_yang/plugins/vscode.py @@ -1,35 +1,35 @@ -import os import json import logging +import os from os.path import isdir, isfile from pathlib import Path -from ._plugin import Plugin +from ..meta import FileFormat +from ._plugin import flatpak_system, flatpak_user, snap_path, ConfigFilePlugin logger = logging.getLogger(__name__) extension_paths = [ - str(Path.home()) + '/.vscode/extensions', - str(Path.home()) + '/.vscode-insiders/extensions', - str(Path.home()) + '/.vscode-oss/extensions', + str(Path.home() / '.vscode/extensions'), + str(Path.home() / '.vscode-insiders/extensions'), + str(Path.home() / '.vscode-oss/extensions'), '/usr/lib/code/extensions', '/usr/lib/code-insiders/extensions', '/usr/share/code/resources/app/extensions', '/usr/share/code-insiders/resources/app/extensions', '/opt/visual-studio-code/resources/app/extensions/', '/opt/visual-studio-code-insiders/resources/app/extensions/', - '/var/lib/snapd/snap/code/current/usr/share/code/resources/app/extensions/', - '/var/lib/snapd/snap/code-insiders/current/usr/share/code-insiders/resources/app/extensions/' + str(snap_path('code') / 'usr/share/code/resources/app/extensions/'), + str(snap_path('code-insiders') / 'usr/share/code-insiders/resources/app/extensions/'), + str(flatpak_user('com.visualstudio.code') / 'data/vscode/extensions/'), + str(flatpak_user('com.visualstudio.code-oss') / 'data/vscode/extensions/'), + str(flatpak_user('com.vscodium.codium') / 'data/codium/extensions/'), + str(flatpak_system('com.visualstudio.code') / 'files/extra/vscode/resources/app/extensions/'), + str(flatpak_system('com.visualstudio.code-oss') / 'files/main/resources/app/extensions/'), + str(flatpak_system('com.vscodium.codium') / 'files/share/codium/resources/app/extensions/') ] -def write_new_settings(settings, path): - # simple adds a new field to the settings - settings["workbench.colorTheme"] = "Default" - with open(path, 'w') as conf: - json.dump(settings, conf, indent=4) - - def get_theme_name(path): if not isfile(path): return [] @@ -54,55 +54,29 @@ def get_theme_name(path): return (theme['id'] if 'id' in theme else theme['label'] for theme in themes) -class Vscode(Plugin): +class Vscode(ConfigFilePlugin): name = 'VS Code' def __init__(self): - super(Vscode, self).__init__() - self.theme_light = 'Default Light+' - self.theme_dark = 'Default Dark+' - - def set_theme(self, theme: str): - if not theme: - raise ValueError(f'Theme \"{theme}\" is invalid') - - if not (self.available and self.enabled): - return - possible_editors = [ "VSCodium", "Code - OSS", "Code", "Code - Insiders", ] + paths = [Path.home() / f'.config/{name}/User/settings.json' for name in possible_editors] + paths += [ + flatpak_user('com.visualstudio.code') / 'config/Code/User/settings.json', + flatpak_user('com.visualstudio.code-oss') / 'config/Code - OSS/User/settings.json', + flatpak_user('com.vscodium.codium') / 'config/VSCodium/User/settings.json' + ] + super(Vscode, self).__init__(paths, file_format=FileFormat.JSON) + self.theme_light = 'Default Light Modern' + self.theme_dark = 'Default Dark Modern' - try: - for editor in filter( - os.path.isfile, - (f'{str(Path.home())}/.config/{name}/User/settings.json' for name in possible_editors)): - # load the settings - with open(editor, "r") as sett: - try: - settings = json.load(sett) - settings['workbench.colorTheme'] = theme - except json.decoder.JSONDecodeError as e: - # check if the file is completely empty - sett.seek(0) - first_char: str = sett.read(1) - if not first_char: - # file is empty - logger.info('File is empty') - settings = {"workbench.colorTheme": theme} - else: - # settings file is malformed - raise e - - # write changed settings into the file - with open(editor, 'w') as sett: - json.dump(settings, sett) - except StopIteration: - raise FileNotFoundError('No config file found. ' - 'If you see this error, try to set a custom theme manually once and try again.') + def update_config(self, config: dict, theme: str): + config['workbench.colorTheme'] = theme + return json.dumps(config) @property def available_themes(self) -> dict: @@ -129,8 +103,5 @@ def __str__(self): return 'code' @property - def available(self) -> bool: - for path in extension_paths: - if isdir(path): - return True - return False + def default_config(self): + return {'workbench.colorTheme': 'Default'} diff --git a/yin_yang/plugins/wallpaper.py b/yin_yang/plugins/wallpaper.py index d6c6c303..c91b7975 100755 --- a/yin_yang/plugins/wallpaper.py +++ b/yin_yang/plugins/wallpaper.py @@ -1,3 +1,4 @@ +import copy import logging import subprocess from pathlib import Path @@ -6,7 +7,7 @@ from PySide6.QtDBus import QDBusConnection, QDBusMessage from ..meta import Desktop -from ._plugin import PluginDesktopDependent, PluginCommandline, Plugin +from ._plugin import PluginDesktopDependent, PluginCommandline, Plugin, DBusPlugin from .system import test_gnome_availability logger = logging.getLogger(__name__) @@ -25,6 +26,8 @@ def __init__(self, desktop: Desktop): super().__init__(_Xfce()) case Desktop.CINNAMON: super().__init__(_Cinnamon()) + case Desktop.BUDGIE: + super().__init__(_Budgie()) case _: super().__init__(None) @@ -54,6 +57,18 @@ class _Gnome(PluginCommandline): def __init__(self): super().__init__(['gsettings', 'set', 'org.gnome.desktop.background', 'picture-uri', 'file://{theme}']) + @property + def available(self) -> bool: + return test_gnome_availability(self.command) + + +class _Budgie(PluginCommandline): + name = 'Wallpaper' + + def __init__(self): + super().__init__(['gsettings', 'set', 'org.gnome.desktop.background', 'picture-uri', 'file://{theme}']) + + @property def available(self) -> bool: return test_gnome_availability(self.command) @@ -72,11 +87,17 @@ def check_theme(theme: str) -> bool: return True -class _Kde(Plugin): +class _Kde(DBusPlugin): name = 'Wallpaper' def __init__(self): - super().__init__() + message = QDBusMessage.createMethodCall( + 'org.kde.plasmashell', + '/PlasmaShell', + 'org.kde.PlasmaShell', + 'evaluateScript', + ) + super().__init__(message) self._theme_light = None self._theme_dark = None @@ -98,14 +119,12 @@ def theme_dark(self, value: str): check_theme(value) self._theme_dark = value - def set_theme(self, theme: str): - connection = QDBusConnection.sessionBus() - message = QDBusMessage.createMethodCall( - 'org.kde.plasmashell', - '/PlasmaShell', - 'org.kde.PlasmaShell', - 'evaluateScript', - ) + @property + def available(self) -> bool: + return True + + def create_message(self, theme: str) -> QDBusMessage: + message = copy.deepcopy(self.base_message) message.setArguments([ 'string:' 'var Desktops = desktops();' @@ -116,11 +135,7 @@ def set_theme(self, theme: str): f' d.writeConfig("Image", "file:{theme}");' '}' ]) - connection.call(message) - - @property - def available(self) -> bool: - return True + return message class _Xfce(PluginCommandline): diff --git a/yin_yang/position.py b/yin_yang/position.py new file mode 100644 index 00000000..79fc891d --- /dev/null +++ b/yin_yang/position.py @@ -0,0 +1,71 @@ +import logging +from time import sleep + +import requests +from PySide6.QtCore import QObject +from PySide6.QtPositioning import QGeoPositionInfoSource, QGeoCoordinate, QGeoPositionInfo + +logger = logging.getLogger(__name__) + + +def get_current_location() -> QGeoCoordinate: + try: + return get_qt_position() + except TypeError as e: + logger.warning(e) + + try: + return get_ipinfo_position() + except TypeError as e: + logger.warning(e) + + raise TypeError('Unable to get current location') + + +parent = QObject() +location_source = QGeoPositionInfoSource.createDefaultSource(parent) + + +def get_qt_position() -> QGeoCoordinate: + if location_source is None: + raise TypeError('Location source is none') + + pos: QGeoPositionInfo = location_source.lastKnownPosition() + if pos is None: + location_source.requestUpdate(10) + tries = 0 + while pos is None and tries < 10: + pos = location_source.lastKnownPosition() + tries += 1 + sleep(1) + coordinate = pos.coordinate() + + if not coordinate.isValid(): + raise TypeError('Coordinates are not valid') + + return coordinate + +# there is a freedesktop portal for getting the location, +# but it's not implemented by KDE, so I have no use for it + + +def get_ipinfo_position() -> QGeoCoordinate: + # use the old method as a fallback + try: + response = requests.get('https://www.ipinfo.io/loc') + except Exception as e: + logger.error(e) + raise TypeError('Error while sending a request to get location') + + if not response.ok: + raise TypeError('Failed to get location from ipinfo.io') + + loc_response = response.text.removesuffix('\n').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]) + + if not coordinate.isValid(): + raise TypeError('Coordinates are not valid') + + return coordinate diff --git a/yin_yang/theme_switcher.py b/yin_yang/theme_switcher.py index 2a177b1f..01c2c7fa 100755 --- a/yin_yang/theme_switcher.py +++ b/yin_yang/theme_switcher.py @@ -13,7 +13,6 @@ from threading import Thread 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 @@ -40,7 +39,7 @@ 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)): + if force and isinstance(p, Notification): # skip sound and notify on apply settings continue try: diff --git a/yin_yang/ui/main_window.py b/yin_yang/ui/main_window.py index f4c1e205..a19cce38 100644 --- a/yin_yang/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.6.0 +## Created by: Qt User Interface Compiler version 6.6.1 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ @@ -229,11 +229,6 @@ def setupUi(self, main_window): self.settings_layout.addWidget(self.manual_buttons) - self.toggle_sound = QCheckBox(self.settings) - self.toggle_sound.setObjectName(u"toggle_sound") - - self.settings_layout.addWidget(self.toggle_sound) - self.toggle_notification = QCheckBox(self.settings) self.toggle_notification.setObjectName(u"toggle_notification") @@ -279,7 +274,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, 518, 88)) + self.plugins_scroll_content.setGeometry(QRect(0, 0, 501, 86)) 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) @@ -358,7 +353,6 @@ def retranslateUi(self, main_window): 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)) diff --git a/yin_yang/ui/main_window_connector.py b/yin_yang/ui/main_window_connector.py index ff7a45a3..4468fded 100755 --- a/yin_yang/ui/main_window_connector.py +++ b/yin_yang/ui/main_window_connector.py @@ -4,7 +4,7 @@ from PySide6 import QtWidgets from PySide6.QtCore import QStandardPaths from PySide6.QtGui import QScreen, QColor -from PySide6.QtWidgets import QFileDialog, QMessageBox, QDialogButtonBox, QColorDialog,QGroupBox +from PySide6.QtWidgets import QFileDialog, QMessageBox, QDialogButtonBox, QColorDialog, QGroupBox from .main_window import Ui_main_window from ..theme_switcher import set_desired_theme, set_mode @@ -81,7 +81,6 @@ def load(self): self.ui.btn_schedule.setChecked(True) self.ui.location.setVisible(False) - 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) @@ -111,17 +110,16 @@ def load_location(self): def load_plugins(self): # First, remove sample plugin - samplePlugin = cast(QGroupBox,self.ui.plugins_scroll_content.findChild(QGroupBox, 'samplePluginGroupBox')) - samplePlugin.hide() - + sample_plugin = cast(QGroupBox, self.ui.plugins_scroll_content.findChild(QGroupBox, 'samplePluginGroupBox')) + sample_plugin.hide() widget: QGroupBox for plugin in plugins: # filter out plugins for application - if plugin.name.casefold() in ['notification', 'sound']: + if plugin.name.casefold() == 'notification': continue - widget = cast(QGroupBox,self.ui.plugins_scroll_content.findChild(QGroupBox, 'group' + plugin.name)) + widget = cast(QGroupBox, self.ui.plugins_scroll_content.findChild(QGroupBox, 'group' + plugin.name)) if widget is None: widget = plugin.get_widget(self.ui.plugins_scroll_content) self.ui.plugins_scroll_content_layout.addWidget(widget) @@ -187,8 +185,6 @@ def setup_config_sync(self): # connect dialog buttons self.ui.btn_box.clicked.connect(self.save_config_to_file) - self.ui.toggle_sound.toggled.connect( - lambda enabled: config.update_plugin_key('sound', PluginKey.ENABLED, enabled)) self.ui.toggle_notification.toggled.connect( lambda enabled: config.update_plugin_key('notification', PluginKey.ENABLED, enabled)) diff --git a/yin_yang/ui/resources_rc.py b/yin_yang/ui/resources_rc.py index feafd0eb..f8be410e 100644 --- a/yin_yang/ui/resources_rc.py +++ b/yin_yang/ui/resources_rc.py @@ -1,133 +1,122 @@ # Resource object code (Python 3) # Created by: object code -# Created by: The Resource Compiler for Qt version 6.6.0 +# Created by: The Resource Compiler for Qt version 6.6.1 # WARNING! All changes made in this file will be lost! from PySide6 import QtCore qt_resource_data = b"\ -\x00\x00\x07w\ +\x00\x00\x06\xca\ <\ \xb8d\x18\xca\xef\x9c\x95\xcd!\x1c\xbf`\xa1\xbd\xdd\xa7\ -\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\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\ -\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`\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\ +\x00\x00\x00\x05de_DEB\x00\x00\x00\x90\x00\x04\ +\xa8\x8b\x00\x00\x00\xd6\x00\x05\x8c\x04\x00\x00\x05\x93\x00J\ +\x88\xea\x00\x00\x01\x06\x00R\xfd\xf4\x00\x00\x01\xd7\x00\x89\ +?\xc9\x00\x00\x04\xd3\x02\xcf6\x15\x00\x00\x05\xc1\x03^\ +\x05u\x00\x00\x00o\x05/\xdfz\x00\x00\x02\x04\x06\x99\ +\x04U\x00\x00\x03F\x07;\xe0\x03\x00\x00\x02t\x0ai\ +\xf3\xe7\x00\x00\x05L\x0a\xa0\x8cG\x00\x00\x00\x00\x0b\x0b\ +\xe8\x0a\x00\x00\x01\x96\x0b\xa1\xae>\x00\x00\x03\xb9\x0c\xbb\ +\x01s\x00\x00\x03\x04\x0e\x0e\x8c\xca\x00\x00\x024\x0f\x0a\ +g\xee\x00\x00\x02\x9b\x0fF^:\x00\x00\x019i\x00\ +\x00\x06\x0f\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\x19Automati\ +c theme switchin\ +g\x07\x00\x00\x00\x0bmain_windo\ +w\x01\x03\x00\x00\x008\x00B\x00e\x00n\x00u\x00\ +t\x00z\x00e\x00r\x00d\x00e\x00f\x00i\x00\ +n\x00i\x00e\x00r\x00t\x00e\x00r\x00 \x00\ +Z\x00e\x00i\x00t\x00r\x00a\x00u\x00m\x08\ +\x00\x00\x00\x00\x06\x00\x00\x00\x0fCustom \ +Schedule\x07\x00\x00\x00\x0bmai\ +n_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\x0bmai\ +n_window\x01\x03\x00\x00\x00\x0e\x00D\ +\x00u\x00n\x00k\x00e\x00l\x00:\x08\x00\x00\x00\ +\x00\x06\x00\x00\x00\x05Dark:\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\ +,\x00V\x00e\x00r\x00z\x00\xf6\x00g\x00e\x00\ +r\x00u\x00n\x00g\x00 \x00n\x00a\x00c\x00\ +h\x00 \x00S\x00t\x00a\x00r\x00t\x08\x00\x00\ +\x00\x00\x06\x00\x00\x00\x11Delay aft\ +er boot:\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\x08\x00\ +H\x00e\x00l\x00l\x08\x00\x00\x00\x00\x06\x00\x00\x00\ +\x05Light\x07\x00\x00\x00\x0bmain_\ +window\x01\x03\x00\x00\x00\x0a\x00H\x00e\ +\x00l\x00l\x00:\x08\x00\x00\x00\x00\x06\x00\x00\x00\x06\ +Light:\x07\x00\x00\x00\x0bmain_\ +window\x01\x03\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\x0aLong\ +itude:\x07\x00\x00\x00\x0bmain_\ +window\x01\x03\xff\xff\xff\xff\x08\x00\x00\x00\ +\x00\x06\x00\x00\x00\x07Plugins\x07\x00\x00\ +\x00\x0bmain_window\x01\x03\x00\ +\x00\x006\x00S\x00e\x00n\x00d\x00e\x00 \x00\ +e\x00i\x00n\x00e\x00 \x00B\x00e\x00n\x00\ +a\x00c\x00h\x00r\x00i\x00c\x00h\x00t\x00\ +i\x00g\x00u\x00n\x00g\x08\x00\x00\x00\x00\x06\x00\ +\x00\x00\x13Send a notifi\ +cation\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\x08\ +Settings\x07\x00\x00\x00\x0bmai\ +n_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 Sunrise\x07\x00\x00\x00\x0b\ +main_window\x01\x03\x00\x00\x00\ +\xae\x00Z\x00e\x00i\x00t\x00 \x00d\x00i\x00\ +e\x00 \x00g\x00e\x00w\x00a\x00r\x00t\x00\ +e\x00t\x00 \x00w\x00e\x00r\x00d\x00e\x00\ +n\x00 \x00s\x00o\x00l\x00l\x00 \x00w\x00\ +\xe4\x00h\x00r\x00e\x00n\x00d\x00 \x00d\x00\ +a\x00s\x00 \x00S\x00y\x00s\x00t\x00e\x00\ +m\x00 \x00s\x00t\x00a\x00r\x00t\x00e\x00\ +t\x00.\x00 \x00S\x00t\x00a\x00n\x00d\x00\ +a\x00r\x00d\x00w\x00e\x00r\x00t\x00 \x00\ +i\x00s\x00t\x00 \x001\x000\x00 \x00S\x00\ +e\x00k\x00u\x00n\x00d\x00e\x00n\x00.\x08\ +\x00\x00\x00\x00\x06\x00\x00\x00LTime to\ + wait until the \ +system finished \ +booting. Default\ + value is 10 sec\ +onds.\x07\x00\x00\x00\x0bmain_w\ +indow\x01\x03\x00\x00\x00<\x00P\x00o\x00\ +s\x00i\x00t\x00i\x00o\x00n\x00 \x00a\x00\ +u\x00t\x00o\x00m\x00a\x00t\x00i\x00s\x00\ +c\x00h\x00 \x00b\x00e\x00s\x00t\x00i\x00\ +m\x00m\x00e\x00n\x08\x00\x00\x00\x00\x06\x00\x00\x00\ +\x1dupdate location\ + automatically\x07\x00\ +\x00\x00\x0bmain_window\x01\x03\ +\x00\x00\x00\x1e\x00Y\x00i\x00n\x00 \x00Y\x00a\ +\x00n\x00g\x00 \x00\xf6\x00f\x00f\x00n\x00e\ +\x00n\x08\x00\x00\x00\x00\x06\x00\x00\x00\x0dOpen\ + Yin Yang\x07\x00\x00\x00\x07sy\ +stray\x01\x03\x00\x00\x00\x0e\x00B\x00e\x00\ +e\x00n\x00d\x00e\x00n\x08\x00\x00\x00\x00\x06\x00\ +\x00\x00\x04Quit\x07\x00\x00\x00\x07syst\ +ray\x01\x03\x00\x00\x00&\x00F\x00a\x00r\x00\ +b\x00s\x00c\x00h\x00e\x00m\x00a\x00 \x00\ +w\x00e\x00c\x00h\x00s\x00e\x00l\x00n\x08\ +\x00\x00\x00\x00\x06\x00\x00\x00\x0cToggle \ +theme\x07\x00\x00\x00\x07systra\ +y\x01\x88\x00\x00\x00\x02\x01\x01\ \x00\x00\x07\x22\ \x00\ \x00\x1dUx\xda\xcdYKs\xdb6\x10\xbe\xe7Wp\ @@ -274,9 +263,9 @@ \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\x8c^C\x1d\xc6\ -\x00\x00\x00P\x00\x01\x00\x00\x00\x01\x00\x00\x07{\ -\x00\x00\x01\x8cK\xf9\x0b4\ +\x00\x00\x01\x8d'?\x15:\ +\x00\x00\x00P\x00\x01\x00\x00\x00\x01\x00\x00\x06\xce\ +\x00\x00\x01\x8d#[_r\ " def qInitResources():