From 677fbfcaae53f6d8ab204eac865178fd790d0818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Hern=C3=A1ndez?= <65425622+fer-hnndz@users.noreply.github.com> Date: Wed, 2 Oct 2024 09:56:15 -0600 Subject: [PATCH] Add popups, system tray and improve codebase (#1) * Add GTK3 dialogs * Update dependencies * Added a simple system tray * Add enable/disable feature * Add system tray, and some notifications as popups * Improve config documentation * Improve code for mantainability and added dialogs for battery alerts - Created Settings class that is fed from load_settings to improve readability and typing for settings - Separated tresholds checks into a separate function for readability - Improve toml settings and comments - Remind now uses a timestamp to manage remind time instead of a time.sleep. More of that on the source * Add padding and increase font size to popups * Add title param to popups and added battery action popup * Dont report when battery is plugged --- README.md | 6 + battery_advisor/__init__.py | 3 +- battery_advisor/__main__.py | 4 - battery_advisor/battery_advisor.py | 134 +++++++++++++++++++ battery_advisor/entry.py | 84 +----------- battery_advisor/gui/alerts.py | 52 ++++++++ battery_advisor/notifications.py | 35 +++++ battery_advisor/settings_loader.py | 42 +++++- battery_advisor/tray.py | 26 ++++ battery_advisor/types.py | 11 +- battery_advisor/utils.py | 79 ++---------- defaultSettings.toml | 18 ++- poetry.lock | 198 ++++++++++++++++++++++++++++- pyproject.toml | 2 - 14 files changed, 531 insertions(+), 163 deletions(-) delete mode 100644 battery_advisor/__main__.py create mode 100644 battery_advisor/battery_advisor.py create mode 100644 battery_advisor/gui/alerts.py create mode 100644 battery_advisor/notifications.py create mode 100644 battery_advisor/tray.py diff --git a/README.md b/README.md index 5defb05..d544147 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,9 @@ A simple tool to monitor and notify about battery status. Built with Python. +# Dependencies (Arch only) +- libnotify +- python-gobject +- python-pillow +- python-toml +- python-psutil diff --git a/battery_advisor/__init__.py b/battery_advisor/__init__.py index 417ea48..6e2953c 100644 --- a/battery_advisor/__init__.py +++ b/battery_advisor/__init__.py @@ -1,3 +1,2 @@ -from .utils import get_battery_status, notify, notify_with_actions, execute_action -from .settings_loader import load_settings from .entry import main +from .settings_loader import load_settings diff --git a/battery_advisor/__main__.py b/battery_advisor/__main__.py deleted file mode 100644 index 50d868d..0000000 --- a/battery_advisor/__main__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .entry import main - -if __name__ == "__main__": - main() diff --git a/battery_advisor/battery_advisor.py b/battery_advisor/battery_advisor.py new file mode 100644 index 0000000..77f29bb --- /dev/null +++ b/battery_advisor/battery_advisor.py @@ -0,0 +1,134 @@ +import threading +import time + +from pystray import Menu, MenuItem +from .settings_loader import Settings +from .tray import get_icon +from .utils import execute_action, get_battery_status +from .notifications import notify, alert_with_options, alert +from .types import BatteryReport +from typing import Optional +from datetime import datetime + +settings = Settings.load() + + +class BatteryAdvisor: + """Base Program that manages SysTray Icon and the battery checker service.""" + + def __init__(self): + self.running = True + + def get_battery_reports(self) -> Optional[BatteryReport]: + """Returns thet status that needs to be reported to the user""" + + batt_percent, plugged = get_battery_status() + + if plugged: + return None + + if batt_percent <= settings.battery_action_treshold: + return BatteryReport.ACTION + + if batt_percent <= settings.critical_battery_treshold: + return BatteryReport.CRITICAL + + if batt_percent <= 100: + return BatteryReport.LOW + + return None + + def _battery_checker(self): + """ + Service that checks the battery status and notifies the user. + This one runs in a separate thread because the SysTray icon must run on the main thread. + """ + + print("Starting battery checker...") + + # Always get initial status to avoid false notifications + _, was_plugged = get_battery_status() + remind_timestamp = datetime.now() + + while True: + if not self.running: + time.sleep(3) + continue + + print("Checking battery status...") + _, plugged = get_battery_status() + + # Battery Plugged in notifications + if plugged != was_plugged: + was_plugged = plugged + if plugged and settings.notify_plugged: + notify("Battery Plugged In", "Battery is now charging") + elif not plugged and settings.notify_unplugged: + notify("Battery Unplugged", "Battery is now discharging.") + + report = self.get_battery_reports() + print("Battery Report:", report) + + if report is None: + time.sleep(settings.check_interval) + continue + + if report == BatteryReport.ACTION: + battery_action = settings.battery_action + alert(message=f"Your battery will {battery_action.capitalize()} soon.") + time.sleep(3) + execute_action(battery_action, settings.actions) + + if report == BatteryReport.CRITICAL: + alert_with_options( + message="Your battery is critically low. Please plug in your charger.", + options=settings.critical_battery_options, + title="Battery Critically Low", + ) + + # Don't sleep for the amount of time to remind the user again. + # Instead, check if the time has passed using timestamps. + + # This is to avoid excluding critical notifications or if a long remind time is set + # or plugged/unplugged notifications because of sleep. + # More like a QoL feature. ;) + if ( + report == BatteryReport.LOW + and remind_timestamp.timestamp() <= datetime.now().timestamp() + ): + r = alert_with_options( + "Battery is low. Please plug in your charger.", + settings.low_battery_options, + title="Battery Low", + ) + + selected_action = settings.low_battery_options[r] + + if selected_action == "remind": + remind_timestamp = datetime.fromtimestamp( + remind_timestamp.timestamp() + settings.remind_time + ) + + else: + execute_action(selected_action, settings.actions) + + time.sleep(settings.check_interval) + + def _on_enabled_click(self, icon, item): + self.running = not self.running + print("Battery Advisor is now", "enabled" if self.running else "disabled") + + def start(self): + batt_thread = threading.Thread(target=self._battery_checker, daemon=True) + batt_thread.start() + + menu = Menu( + MenuItem( + text="Enabled", + checked=lambda item: self.running, + action=self._on_enabled_click, + ) + ) + get_icon(menu).run() + print("Exiting...") + return 0 diff --git a/battery_advisor/entry.py b/battery_advisor/entry.py index 21029c6..5a29a5c 100644 --- a/battery_advisor/entry.py +++ b/battery_advisor/entry.py @@ -1,85 +1,5 @@ -import time -from .utils import get_battery_status, notify, notify_with_actions, execute_action -from .settings_loader import load_settings - -settings = load_settings() - -LOW_BATTERY_TRESHOLD = settings["tresholds"]["low_battery_treshold"] -CRITICAL_BATTERY_TRESHOLD = settings["tresholds"]["critical_battery_treshold"] -BATTERY_ACTION_TRESHOLD = settings["tresholds"]["battery_action_treshold"] -CHECK_INTERVAL = settings["advisor"]["check_interval"] - -# Configs -NOTIFY_PLUGGED = settings["advisor"]["notify_plugged"] -NOTIFY_UNPLUGGED = settings["advisor"]["notify_unplugged"] - -# Actions -LOW_BATTERY_OPTIONS = settings["advisor"]["low_battery_options"] -CRITICAL_BATTERY_OPTIONS = settings["advisor"]["critical_battery_options"] +from .battery_advisor import BatteryAdvisor def main(): - _, was_plugged = get_battery_status() - - while True: - print("Checking battery status...") - remind_time = 0 - batt_percent, plugged = get_battery_status() - - # Battery Plugged in notifications - if plugged != was_plugged: - was_plugged = plugged - if plugged and NOTIFY_PLUGGED: - notify("Battery Plugged In", "Battery is now charging") - elif not plugged and NOTIFY_UNPLUGGED: - notify("Battery Unplugged", "Battery is now discharging.") - - if plugged: - print("Battery is charging. Skipping checks.") - print("Sleeping...") - time.sleep(CHECK_INTERVAL) - continue - - # Battery Low notifications - if batt_percent <= BATTERY_ACTION_TRESHOLD: - notify( - "Battery Action", - f"Your battery is at {batt_percent}%. Your system will {settings['advisor']['battery_action'].capitalize()} in a few.", - ) - print("Reporting battery action.") - print("Sleeping...") - time.sleep(5) - configured_action = settings["advisor"]["battery_action"] - action_cmd = settings["actions"][configured_action] - print("Executing Battery Action. Goodbye...") - execute_action(action_cmd) - - if batt_percent <= CRITICAL_BATTERY_TRESHOLD: - print("Reporting critical battery.") - remind_time = notify_with_actions( - title="CRITICAL BATTERY", - message=f"Your battery is at {int(batt_percent)}%. Consider plugging your device.", - options=CRITICAL_BATTERY_OPTIONS, - actions=settings["actions"], - remind_time=round( - settings["advisor"]["remind_time"] / 2 - ), # Remind in half the remind time - ) - - elif batt_percent <= LOW_BATTERY_TRESHOLD: - print("Reporting low battery.") - remind_time = notify_with_actions( - title="Low Battery", - message=f"Consider plugging your device.", - options=LOW_BATTERY_OPTIONS, - actions=settings["actions"], - remind_time=settings["advisor"]["remind_time"], - ) - - if remind_time > 0: - print("A function returned a remind time!") - time.sleep(remind_time) - continue - - print("Sleeping...") - time.sleep(CHECK_INTERVAL) + BatteryAdvisor().start() diff --git a/battery_advisor/gui/alerts.py b/battery_advisor/gui/alerts.py new file mode 100644 index 0000000..4684369 --- /dev/null +++ b/battery_advisor/gui/alerts.py @@ -0,0 +1,52 @@ +import gi + +gi.require_version("Gtk", "3.0") +gi.require_version("Pango", "1.0") +from gi.repository import Gtk, Pango + +from ..utils import execute_action + + +class MessageAlert(Gtk.Dialog): + def __init__( + self, + title: str, + message: str, + ): + super().__init__(title, transient_for=None) + + content = self.get_content_area() + _title_label = Gtk.Label(margin_bottom=13, margin_top=10) + _title_label.set_markup(f"{title}") + _msg_label = Gtk.Label( + message, margin_bottom=12, margin_start=10, margin_end=10 + ) + + # Increase text size + _title_label.override_font(Pango.FontDescription("Ubuntu 12")) + _msg_label.override_font(Pango.FontDescription("Ubuntu 10")) + + content.add(_title_label) + content.add(_msg_label) + + if self.__class__ == MessageAlert: + self.add_button("Close", Gtk.ResponseType.CLOSE) + + self.show_all() + + def run(self): + response = super().run() + if response == -4 or response == -7 or response == -1: + response = 0 + + return response + + +class AlertWithButtons(MessageAlert): + def __init__(self, title: str, message, actions: list[str]): + super().__init__(message=message, title=title) + + for i, action in enumerate(actions): + self.add_button(action.capitalize(), i) + + self.show_all() diff --git a/battery_advisor/notifications.py b/battery_advisor/notifications.py new file mode 100644 index 0000000..67c0a4c --- /dev/null +++ b/battery_advisor/notifications.py @@ -0,0 +1,35 @@ +import subprocess + +from .utils import _get_path_icon, execute_action +from .gui.alerts import AlertWithButtons, MessageAlert + +EXPIRE_TIME = 160000 + + +def notify(title: str, message: str): + """Sends a notification to the user""" + subprocess.run(["notify-send", title, message, f"--icon={_get_path_icon()}"]) + + +def alert(message: str, title: str = "Battery Advisor"): + """Sends a popup to the user""" + dialog = MessageAlert(message=message, title=title) + dialog.run() + dialog.destroy() + + +def alert_with_options( + message: str, options: list[str], title: str = "Battery Advisor" +) -> int: + """Sends a popup with a close option to the user. + + Returns + ------- + int + The selected option index + """ + + dialog = AlertWithButtons(title=title, message=message, actions=options) + selection = dialog.run() + dialog.destroy() + return selection diff --git a/battery_advisor/settings_loader.py b/battery_advisor/settings_loader.py index ce50c8b..e9213f8 100644 --- a/battery_advisor/settings_loader.py +++ b/battery_advisor/settings_loader.py @@ -1,12 +1,13 @@ -import toml import os -from .types import Settings +import toml +from typing import Type +from .types import SettingsFile from .utils import _get_project_root user_settings_path = os.path.expanduser("~/.config/battery-advisor/settings.toml") -def load_settings() -> Settings: +def load_settings() -> SettingsFile: path = ( user_settings_path if os.path.exists(user_settings_path) @@ -18,3 +19,38 @@ def load_settings() -> Settings: with open(path) as f: return toml.load(f) + + +class Settings: + def __init__(self, settings: SettingsFile): + """Creates a settings object based on the settings.toml file. + NOTE: This class is not meant to be instantiated directly. Use the load() method instead. + """ + + self.check_interval: int = int(settings["advisor"]["check_interval"]) + self.remind_time: int = int(settings["advisor"]["remind_time"]) + + # Tresholds + # self.low_battery_treshold = settings["tresholds"]["low_battery_treshold"] + self.low_battery_treshold = 100 + self.critical_battery_treshold = settings["tresholds"][ + "critical_battery_treshold" + ] + self.battery_action_treshold = settings["tresholds"]["battery_action_treshold"] + + # Alert Options + self.low_battery_options = settings["advisor"]["low_battery_options"] + self.critical_battery_options = settings["advisor"]["critical_battery_options"] + self.battery_action = settings["advisor"]["battery_action"] + + self.actions: dict[str, list[str]] = settings["actions"] + + # Notification Settings + self.notify_plugged = settings["advisor"]["notify_plugged"] + self.notify_unplugged = settings["advisor"]["notify_unplugged"] + + @classmethod + def load(cls: Type["Settings"]) -> "Settings": + """Loads the settings from the settings.toml file and returns a Settings object""" + + return cls(load_settings()) diff --git a/battery_advisor/tray.py b/battery_advisor/tray.py new file mode 100644 index 0000000..80313ce --- /dev/null +++ b/battery_advisor/tray.py @@ -0,0 +1,26 @@ +import pystray +from PIL import Image +from PIL.ImageFile import ImageFile + +from .utils import _get_project_root + + +def _get_icon_from_image(image_path: str) -> ImageFile: + """Returns an image object from a file path""" + return Image.open(image_path) + + +def get_icon(menu: pystray.Menu): + icon_path = _get_project_root() + "/icon.png" + i = pystray.Icon( + name="Battery Advisor", + icon=_get_icon_from_image(icon_path), + title="Battery Advisor", + menu=menu, + ) + + if not i.HAS_MENU: + # Warn no menu + print("No menu available for this platform.") + + return i diff --git a/battery_advisor/types.py b/battery_advisor/types.py index 6759fc4..f346d34 100644 --- a/battery_advisor/types.py +++ b/battery_advisor/types.py @@ -1,4 +1,5 @@ -from typing import TypedDict, Literal +from typing import Literal, TypedDict +from enum import Enum class Tresholds(TypedDict): @@ -17,7 +18,13 @@ class Advisor(TypedDict): check_interval: int -class Settings(TypedDict): +class SettingsFile(TypedDict): tresholds: Tresholds advisor: Advisor actions: dict[str, list[str]] + + +class BatteryReport(Enum): + LOW = 0 + CRITICAL = 1 + ACTION = 2 diff --git a/battery_advisor/utils.py b/battery_advisor/utils.py index a97f03b..c1c8791 100644 --- a/battery_advisor/utils.py +++ b/battery_advisor/utils.py @@ -1,7 +1,7 @@ -import psutil -import subprocess import os -import time +import subprocess + +import psutil EXPIRE_TIME = 600000 # 10 minutes @@ -24,69 +24,16 @@ def get_battery_status() -> tuple[int, bool]: return batt.percent, batt.power_plugged -def execute_action(action: list[str]) -> None: - """Executes specified action""" - - try: - a = subprocess.run(action) - except Exception as e: - notify( - "Error", - f"Failed to perform action. Perhaps the actions is not valid.\n\n{e}", - ) - - -def notify(title: str, message: str): - """Sends a notification to the user""" - subprocess.run(["notify-send", title, message, f"--icon={_get_path_icon()}"]) +def execute_action(action_name: str, actions: dict[str, list[str]]) -> None: + """Executes specified action - -def notify_with_actions( - title: str, - message: str, - options: list[str], - actions: dict[str, list[str]], - remind_time: int = 180, -) -> int: - """Sends a notification with actions to the user""" - # Send notification command and retrieve selected action - - options_cmd: list[str] = [] - for option in options: - if option == "remind": - options_cmd.append(f"--action=Remind in {round(remind_time/60)} mins.") - continue - - options_cmd.append(f"--action={option.capitalize()}") - - notification_cmd = [ - "notify-send", - title, - message, - f"--icon={_get_path_icon()}", - f"--expire-time={EXPIRE_TIME}", # Show notification for 2 minutes - "--wait", - "--urgency=critical", - ] - - #! NOTE: If notification is ignored, the program will hang here. - #! Hence why notification is shown for 2 minutes. - notification_cmd.extend(options_cmd) - command = subprocess.run( - notification_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) + Parameters + ---------- + action : list[str] + The name of the action to execute + """ try: - action_index = int(command.stdout.decode().strip()) - except: - # User closed the notification - # Remind the user to charge device - return remind_time - - selected_action = options[action_index] - - if selected_action == "remind": - return remind_time - - execute_action(actions[selected_action]) - return 0 + subprocess.run(actions[action_name], stdout=subprocess.PIPE) + except Exception as e: + print("Failed to perform action: ", e) diff --git a/defaultSettings.toml b/defaultSettings.toml index 57a03e0..cc06125 100644 --- a/defaultSettings.toml +++ b/defaultSettings.toml @@ -3,18 +3,27 @@ # You can change these settings by copying this file to ~/.config/battery-advisor/settings.toml # and modifying the values there. +# These are the tresholds where battery-advisor will alert different battery statuses. +# The values are in percentage. Ex. 15%, 10%, 5%. +# The values must be in descending order. +# At battery action, the configured action will execute after 3 seconds of the notification. [tresholds] -low_battery_treshold = 25 +low_battery_treshold = 15 critical_battery_treshold = 10 battery_action_treshold = 5 [advisor] notify_plugged = true notify_unplugged = true + +# This is the action that will be executed when the battery reaches the battery_action_treshold. battery_action = "hibernate" + +# How many seconds to check between battery status. check_interval = 15 # Note, if you close the notification, the first action will be executed. +# Remind is only supported for low battery. low_battery_options = ["remind", "hibernate", "suspend"] critical_battery_options = ["hibernate", "shutdown"] @@ -22,6 +31,13 @@ critical_battery_options = ["hibernate", "shutdown"] # By default, it will remind in 3 minutes. remind_time = 180 +# Here you can define the available actions for the battery-advisor. +# The commands must be separated in lists as Python's subprocess library arguments. + +# As you can see `remind` is not defined here as the remind me later feature +# is done programatically and not with the terminal. + +# You can still use it on the low and critical options settings. [actions] hibernate = ["systemctl", "hibernate"] suspend = ["systemctl", "suspend"] diff --git a/poetry.lock b/poetry.lock index c39bf47..c5d02cb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -116,6 +116,103 @@ files = [ {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] +[[package]] +name = "pillow" +version = "10.4.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"}, + {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46"}, + {file = "pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984"}, + {file = "pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141"}, + {file = "pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696"}, + {file = "pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496"}, + {file = "pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91"}, + {file = "pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9"}, + {file = "pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42"}, + {file = "pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a"}, + {file = "pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309"}, + {file = "pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060"}, + {file = "pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea"}, + {file = "pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0"}, + {file = "pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e"}, + {file = "pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df"}, + {file = "pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef"}, + {file = "pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5"}, + {file = "pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"}, + {file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + [[package]] name = "platformdirs" version = "4.3.6" @@ -161,6 +258,105 @@ files = [ [package.extras] test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] +[[package]] +name = "pyobjc-core" +version = "10.3.1" +description = "Python<->ObjC Interoperability Module" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyobjc_core-10.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ea46d2cda17921e417085ac6286d43ae448113158afcf39e0abe484c58fb3d78"}, + {file = "pyobjc_core-10.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:899d3c84d2933d292c808f385dc881a140cf08632907845043a333a9d7c899f9"}, + {file = "pyobjc_core-10.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:6ff5823d13d0a534cdc17fa4ad47cf5bee4846ce0fd27fc40012e12b46db571b"}, + {file = "pyobjc_core-10.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2581e8e68885bcb0e11ec619e81ef28e08ee3fac4de20d8cc83bc5af5bcf4a90"}, + {file = "pyobjc_core-10.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ea98d4c2ec39ca29e62e0327db21418696161fb138ee6278daf2acbedf7ce504"}, + {file = "pyobjc_core-10.3.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:4c179c26ee2123d0aabffb9dbc60324b62b6f8614fb2c2328b09386ef59ef6d8"}, + {file = "pyobjc_core-10.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cb901fce65c9be420c40d8a6ee6fff5ff27c6945f44fd7191989b982baa66dea"}, + {file = "pyobjc_core-10.3.1.tar.gz", hash = "sha256:b204a80ccc070f9ab3f8af423a3a25a6fd787e228508d00c4c30f8ac538ba720"}, +] + +[[package]] +name = "pyobjc-framework-cocoa" +version = "10.3.1" +description = "Wrappers for the Cocoa frameworks on macOS" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyobjc_framework_Cocoa-10.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4cb4f8491ab4d9b59f5187e42383f819f7a46306a4fa25b84f126776305291d1"}, + {file = "pyobjc_framework_Cocoa-10.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5f31021f4f8fdf873b57a97ee1f3c1620dbe285e0b4eaed73dd0005eb72fd773"}, + {file = "pyobjc_framework_Cocoa-10.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:11b4e0bad4bbb44a4edda128612f03cdeab38644bbf174de0c13129715497296"}, + {file = "pyobjc_framework_Cocoa-10.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:de5e62e5ccf2871a94acf3bf79646b20ea893cc9db78afa8d1fe1b0d0f7cbdb0"}, + {file = "pyobjc_framework_Cocoa-10.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c5af24610ab639bd1f521ce4500484b40787f898f691b7a23da3339e6bc8b90"}, + {file = "pyobjc_framework_Cocoa-10.3.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:a7151186bb7805deea434fae9a4423335e6371d105f29e73cc2036c6779a9dbc"}, + {file = "pyobjc_framework_Cocoa-10.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:743d2a1ac08027fd09eab65814c79002a1d0421d7c0074ffd1217b6560889744"}, + {file = "pyobjc_framework_cocoa-10.3.1.tar.gz", hash = "sha256:1cf20714daaa986b488fb62d69713049f635c9d41a60c8da97d835710445281a"}, +] + +[package.dependencies] +pyobjc-core = ">=10.3.1" + +[[package]] +name = "pyobjc-framework-quartz" +version = "10.3.1" +description = "Wrappers for the Quartz frameworks on macOS" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyobjc_framework_Quartz-10.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5ef4fd315ed2bc42ef77fdeb2bae28a88ec986bd7b8079a87ba3b3475348f96e"}, + {file = "pyobjc_framework_Quartz-10.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:96578d4a3e70164efe44ad7dc320ecd4e211758ffcde5dcd694de1bbdfe090a4"}, + {file = "pyobjc_framework_Quartz-10.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ca35f92486869a41847a1703bb176aab8a53dbfd8e678d1f4d68d8e6e1581c71"}, + {file = "pyobjc_framework_Quartz-10.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:00a0933267e3a46ea4afcc35d117b2efb920f06de797fa66279c52e7057e3590"}, + {file = "pyobjc_framework_Quartz-10.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a161bedb4c5257a02ad56a910cd7eefb28bdb0ea78607df0d70ed4efe4ea54c1"}, + {file = "pyobjc_framework_Quartz-10.3.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:d7a8028e117a94923a511944bfa9daf9744e212f06cf89010c60934a479863a5"}, + {file = "pyobjc_framework_Quartz-10.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:de00c983b3267eb26fa42c6ed9f15e2bf006bde8afa7fe2b390646aa21a5d6fc"}, + {file = "pyobjc_framework_quartz-10.3.1.tar.gz", hash = "sha256:b6d7e346d735c9a7f147cd78e6da79eeae416a0b7d3874644c83a23786c6f886"}, +] + +[package.dependencies] +pyobjc-core = ">=10.3.1" +pyobjc-framework-Cocoa = ">=10.3.1" + +[[package]] +name = "pystray" +version = "0.19.5" +description = "Provides systray integration" +optional = false +python-versions = "*" +files = [ + {file = "pystray-0.19.5-py2.py3-none-any.whl", hash = "sha256:a0c2229d02cf87207297c22d86ffc57c86c227517b038c0d3c59df79295ac617"}, +] + +[package.dependencies] +Pillow = "*" +pyobjc-framework-Quartz = {version = ">=7.0", markers = "sys_platform == \"darwin\""} +python-xlib = {version = ">=0.17", markers = "sys_platform == \"linux\""} +six = "*" + +[[package]] +name = "python-xlib" +version = "0.33" +description = "Python X Library" +optional = false +python-versions = "*" +files = [ + {file = "python-xlib-0.33.tar.gz", hash = "sha256:55af7906a2c75ce6cb280a584776080602444f75815a7aff4d287bb2d7018b32"}, + {file = "python_xlib-0.33-py2.py3-none-any.whl", hash = "sha256:c3534038d42e0df2f1392a1b30a15a4ff5fdc2b86cfa94f072bf11b10a164398"}, +] + +[package.dependencies] +six = ">=1.10.0" + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + [[package]] name = "toml" version = "0.10.2" @@ -175,4 +371,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "b59897bb180b116200add93216a8b50fe47edf9cdc2d00695c23bff7c20985ff" +content-hash = "f7ccc3e07d50603254f5c1dd897bb1c14242772ca6540785bcd1d6bf67428023" diff --git a/pyproject.toml b/pyproject.toml index 7abc755..e6f1b27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,8 +12,6 @@ include = ["defaultSettings.toml", "icon.png", "LICENSE"] [tool.poetry.dependencies] python = "^3.11" -psutil = "^6.0.0" -toml = "^0.10.2" [tool.poetry.group.dev.dependencies] black = "^24.8.0"