diff --git a/.gitignore b/.gitignore index 6c4b97a..882e4fa 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ main.build main.dist main.onefile-build main.bin +__pycache__ diff --git a/config.py b/config.py new file mode 100644 index 0000000..3933335 --- /dev/null +++ b/config.py @@ -0,0 +1,85 @@ +import tkinter as tk +import os +from pathlib import Path +import sqlite3 + +APP_NAME = "SC KF" + +DEBUG = os.getenv("DEBUG") is not None + + +class PersistentConfig: + def __init__(self, db_path: Path): + self.table_name = "properties" + self.db = sqlite3.connect( + str(db_path), isolation_level=None, check_same_thread=False, autocommit=True + ) + self.db.execute("pragma journal_mode=wal;") + self.db.execute( + f"create table if not exists {self.table_name} (key text primary key, value_str text, value_int int) strict;" + ) + + def set_int(self, key: str, value: int) -> None: + self.db.execute( + f"insert into {self.table_name} (key, value_int) values (:key, :value) on conflict (key) do update set value_int = :value;", + {"key": key, "value": value}, + ) + + def get_int(self, key: str, default=None) -> int | None: + res = self.db.execute( + f"select value_int from {self.table_name} where key = ?;", (key,) + ) + return next(iter(res), (default,))[0] + + def set_str(self, key: str, value: str) -> None: + self.db.execute( + f"insert into {self.table_name} (key, value_str) values (:key, :value) on conflict (key) do update set value_str = :value;", + {"key": key, "value": value}, + ) + + def get_str(self, key: str, default=None) -> str | None: + res = self.db.execute( + f"select value_str from {self.table_name} where key = ?;", (key,) + ) + return next(iter(res), (default,))[0] + + def set_bool(self, key: str, value: bool) -> None: + self.set_int(key, int(value)) + + def get_bool(self, key: str, default=None) -> bool | None: + return bool(self.get_int(key, default)) + + def get_tk_boolean(self, name: str, default: bool) -> tk.BooleanVar: + value = self.get_bool(name, default) + var = tk.BooleanVar(name=name, value=value) + var.trace_add("write", lambda a, b, c: self.set_bool(name, var.get())) + return var + + def get_tk_str(self, name: str, default: str | None) -> tk.StringVar: + value = self.get_str(name, default) + var = tk.StringVar(name=name, value=value) + var.trace_add("write", lambda a, b, c: self.set_str(name, var.get())) + return var + + def get_tk_int(self, name: str, default: int | None) -> tk.IntVar: + value = self.get_int(name, default) + var = tk.IntVar(name=name, value=value) + var.trace_add("write", lambda a, b, c: self.set_int(name, var.get())) + return var + + +def get_app_data_dir() -> Path: + if os.name == "nt": + directory = Path.home() / "AppData/Roaming/SCKF" + + elif os.name == "posix": + directory = Path.home() / ".config/SCKF" + + else: + raise RuntimeError(f"Unsupported os: {os.name}") + + directory.mkdir(exist_ok=True, parents=True) + return directory + + +db = PersistentConfig(get_app_data_dir() / "config.sqlite") diff --git a/discord_subscriber.py b/discord_subscriber.py new file mode 100644 index 0000000..c8feb5c --- /dev/null +++ b/discord_subscriber.py @@ -0,0 +1,27 @@ +import requests +from monitor import Event, SubscriberABC +from loguru import logger + + +class DiscordSubscriber(SubscriberABC): + def __init__(self): + self.webhook_url: str | None = None + self.enabled: bool = True + + def set_url(self, webhook_url: str) -> None: + self.webhook_url = webhook_url + + def set_enabled(self, value: bool) -> None: + self.enabled = value + + def consume_event(self, event: Event) -> None: + if not self.enabled: + return + + try: + requests.post( + self.webhook_url, json={"content": event.to_str(include_ts=False)} + ) + + except Exception as e: + logger.opt(exception=True).warning(f"Error sending to Discord: {event}") diff --git a/main.py b/main.py index 0e1750e..4b252dc 100644 --- a/main.py +++ b/main.py @@ -1,166 +1,11 @@ -from pathlib import Path - -import requests -import time -from dataclasses import dataclass -from typing import Generator, Self - - -DISCORD_WEBHOOK_URL = "HOOOOOOOOOOOOOOOOK" -LOG_FILE_PATH = Path(r"E:\RSI\StarCitizen\LIVE\Game.log") -INCLUDE_NPC = False -ONLY_PERSONAL_FEED = True - -DEBUG = False - -damage_type_to_emoji = { - "ElectricArc": ":cloud_lightning:", - "Explosion": ":boom:", - "VehicleDestruction": ":boom: :airplane:", - "TakeDown": ":martial_arts_uniform: :face_with_peeking_eye:", -} - - -@dataclass(frozen=True) -class Line: - line: str - is_realtime: bool - - -@dataclass(frozen=True) -class Kill: - killer: str - victim: str - zone: str - killed_by: str - damage_type: str - - @staticmethod - def from_line(line: str) -> "Self": - # <2025-05-23T16:48:53.672Z> [Notice] CActor::Kill: 'Neon0ne' [1895570109956] in zone 'ANVL_Ballista_3700492224326' killed by 'Aleksey31' [1978962253870] using 'MRCK_S07_ANVL_Ballista_Duel_3700492224372' [Class unknown] with damage type 'VehicleDestruction' from direction x: 0.000000, y: 0.000000, z: 0.000000 [Team_ActorTech][Actor] - splited = [i.strip("'") for i in line.split()] - return Kill( - victim=splited[5], - zone=splited[9], - killer=splited[12], - killed_by=splited[15], - damage_type=splited[21], - ) - - -class App: - def __init__( - self, - log_file_path: Path, - discord_webhook_url: str, - do_backread: bool = False, - do_mock_discord: bool = False, - ): - self.log_file_path = log_file_path - self.do_backread: bool = do_backread - self.do_mock_discord: bool = do_mock_discord - self.discord_webhook_url: str = discord_webhook_url - self.nickname: str | None = None - - if not (discord_webhook_url.startswith("https://") or do_mock_discord): - msg = f"{discord_webhook_url} не вебхук, так то" - print(msg) - raise RuntimeError(msg) - - def try_set_self_name(self, line: str) -> None: - # <2025-05-23T16:19:10.244Z> [Notice] Character: createdAt 1747220301029 - updatedAt 1747220301703 - geid 1978962253870 - accountId 2261831 - name Aleksey31 - state STATE_CURRENT [Team_GameServices][Login] - if self.nickname is not None: - print("Warning, AccountLoginCharacterStatus_Character duplicate", line) - return - - try: - self.nickname = line.split()[17] - print(f"Extracted {self.nickname=}") - - except IndexError: - print("Failed to extract nickname from line", line) - - def process_lines(self) -> None: - for line in self.monitor_log_file(): - process_important_realtime_events = self.do_backread or line.is_realtime - - if "" in line.line: - self.try_set_self_name(line.line) - continue - - if ( - "[Notice] CActor::Kill:" in line.line - and process_important_realtime_events - ): - kill = Kill.from_line(line.line) - - if "_NPC_" in kill.killer + kill.victim and not INCLUDE_NPC: - continue - - if ( - self.nickname not in (kill.killer, kill.victim) - and ONLY_PERSONAL_FEED is True - ): - continue - - self.send_kill(kill) - - def send_kill(self, kill: Kill) -> None: - if kill.victim == kill.killer: - msg = f"**{kill.victim}** РКН 🤦" - - else: - emoji = damage_type_to_emoji.get(kill.damage_type, "") - msg = f"**{kill.victim}** killed by 🔫 **{kill.killer}** {emoji}" - - self.send(msg) - - def send(self, message: str) -> None: - try: - if self.do_mock_discord: - print("Sending message to discord", message) - - else: - requests.post(self.discord_webhook_url, json={"content": message}) - - except Exception as e: - print(f"Error sending to Discord: {e}") - - def monitor_log_file(self) -> Generator[Line, None, None]: - is_realtime = False - with self.log_file_path.open("r", encoding="utf-8") as file: - while True: - current_position = file.tell() - line = file.readline() - - if not line: - is_realtime = True - time.sleep(5) - file.seek(current_position) - if DEBUG is True: - return - - line = line.removesuffix("\n") - yield Line(line, is_realtime) +from ui import DeathLogGUI +import tkinter as tk def main(): - try: - if DEBUG is True: - app = App( - Path("./Game.log"), - DISCORD_WEBHOOK_URL, - do_mock_discord=True, - do_backread=True, - ) - - else: - app = App(LOG_FILE_PATH, DISCORD_WEBHOOK_URL) - - app.process_lines() - - except KeyboardInterrupt: - print("\nMonitoring stopped") + root = tk.Tk() + app = DeathLogGUI(root) + root.mainloop() if __name__ == "__main__": diff --git a/monitor.py b/monitor.py new file mode 100644 index 0000000..fab0717 --- /dev/null +++ b/monitor.py @@ -0,0 +1,182 @@ +import datetime +from pathlib import Path + +import time +from dataclasses import dataclass +from typing import Generator, Self + +import config +from loguru import logger + +from abc import ABC, abstractmethod + + +def parse_timestamp(line: str) -> datetime.datetime | None: + # <2025-05-23T16:48:53.672Z> + try: + line = line.strip("<>").replace("Z", "+00:00") + return datetime.datetime.fromisoformat(line) + + except Exception: + logger.warning(f"Failed to parse timestamp from {line!r}") + return None + + +damage_type_to_emoji = { + "ElectricArc": ":cloud_lightning:", + "Explosion": ":boom:", + "VehicleDestruction": ":boom: :airplane:", + "TakeDown": ":martial_arts_uniform: :face_with_peeking_eye:", +} + + +@dataclass(frozen=True) +class Line: + line: str + is_realtime: bool + + +@dataclass(frozen=True) +class Event: + timestamp: datetime.datetime | None + + def to_str(self, include_ts: bool = False) -> str: + return str(self) + + +class SubscriberABC(ABC): + @abstractmethod + def consume_event(self, event: Event) -> None: + raise NotImplementedError + + +@dataclass(frozen=True) +class Kill(Event): + killer: str + victim: str + zone: str + killed_by: str + damage_type: str + + @staticmethod + def from_line(line: str) -> "Self": + # <2025-05-23T16:48:53.672Z> [Notice] CActor::Kill: 'Neon0ne' [1895570109956] in zone 'ANVL_Ballista_3700492224326' killed by 'Aleksey31' [1978962253870] using 'MRCK_S07_ANVL_Ballista_Duel_3700492224372' [Class unknown] with damage type 'VehicleDestruction' from direction x: 0.000000, y: 0.000000, z: 0.000000 [Team_ActorTech][Actor] + splited = [i.strip("'") for i in line.split()] + return Kill( + timestamp=parse_timestamp(splited[0]), + victim=splited[5], + zone=splited[9], + killer=splited[12], + killed_by=splited[15], + damage_type=splited[21], + ) + + def to_str(self, include_ts: bool = False) -> str: + ts = (self.timestamp.strftime("%Y-%m-%d %H:%M:%S") + ": ") * include_ts + if self.victim == self.killer: + return f"{ts}**{self.victim}** РКН 🤦" + + emoji = damage_type_to_emoji.get(self.damage_type, "") + return f"{ts}**{self.victim}** killed by 🔫 **{self.killer}** {emoji}" + + +@dataclass +class Monitor: + log_file_path: Path + subscribers: set[SubscriberABC] + do_backread: bool = False + nickname: str | None = None + running: bool = False + + def try_set_self_name(self, line: str) -> None: + # <2025-05-23T16:19:10.244Z> [Notice] Character: createdAt 1747220301029 - updatedAt 1747220301703 - geid 1978962253870 - accountId 2261831 - name Aleksey31 - state STATE_CURRENT [Team_GameServices][Login] + if self.nickname is not None: + logger.warning( + "Warning, AccountLoginCharacterStatus_Character duplicate", line + ) + return + + try: + self.nickname = line.split()[17] + logger.info(f"Extracted {self.nickname=}") + + except IndexError: + logger.warning("Failed to extract nickname from line", line) + + def call_subscribers(self, event: Event) -> None: + logger.info(f"Passing event: {event}") + if len(self.subscribers) == 0: + logger.warning("set of subscribers is empty") + + for subscriber in self.subscribers: + subscriber.consume_event(event) + + def process_lines(self) -> None: + logger.info("Beginning lines consuming") + self.running = True + for line in self.monitor_log_file(): + process_important_realtime_events = self.do_backread or line.is_realtime + + if "" in line.line: + self.try_set_self_name(line.line) + continue + + if ( + "[Notice] CActor::Kill:" in line.line + and process_important_realtime_events + ): + kill = Kill.from_line(line.line) + + # if "_NPC_" in kill.killer + kill.victim and not self.include_npc: # TODO: + # continue + # + # if ( + # self.nickname not in (kill.killer, kill.victim) + # and self.only_personal_feed is True + # ): + # continue + + self.call_subscribers(kill) + + def monitor_log_file(self) -> Generator[Line, None, None]: + is_realtime = False + with self.log_file_path.open("r", encoding="utf-8") as file: + while self.running: + current_position = file.tell() + line = file.readline() + + if not line: + if not is_realtime: + logger.info("Reached edge of the file") + + is_realtime = True + time.sleep(0.15) + file.seek(current_position) + if config.DEBUG is True: + return + + line = line.removesuffix("\n") + yield Line(line, is_realtime) + + logger.info("Exiting reading loop") + + +# def main(): +# try: +# if config.DEBUG is True: +# app = Monitor( +# log_file_path=Path("./Game.log"), +# do_backread=True, +# ) +# +# else: +# app = Monitor(config.LOG_FILE_PATH, config.DISCORD_WEBHOOK_URL) +# +# app.process_lines() +# +# except KeyboardInterrupt: +# print("\nMonitoring stopped") +# +# +# if __name__ == "__main__": +# main() diff --git a/pyproject.toml b/pyproject.toml index d1ec3fd..9d9568e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,10 @@ version = "0.1.0" description = "Add your description here" requires-python = ">=3.13" dependencies = [ + "loguru>=0.7.3", + "pystray>=0.19.5", "requests>=2.32.3", + "sv-ttk>=2.6.0", ] [dependency-groups] diff --git a/ui.py b/ui.py new file mode 100644 index 0000000..c79754c --- /dev/null +++ b/ui.py @@ -0,0 +1,320 @@ +from functools import partial +from collections import deque +import pystray +from pystray import Menu, MenuItem +from PIL import Image, ImageDraw +import config +import threading +from loguru import logger +import sv_ttk +from pathlib import Path +import threading +import tkinter as tk +from tkinter import ttk, filedialog, scrolledtext +import config +from discord_subscriber import DiscordSubscriber +from monitor import Monitor, Event, SubscriberABC +from loguru import logger +from typing import Callable + + +class Feed(SubscriberABC): + def __init__(self, callback_data_changed: Callable | None = None): + self.ring_buffer: deque[Event] = deque(maxlen=150) + self.ring_buffer_lock = threading.Lock() + self.callback = callback_data_changed + + self.trace_callback = lambda name, b, c: self._notify_data_changed() + + self.include_ts: tk.BooleanVar = config.db.get_tk_boolean("INCLUDE_TS", True) + self.include_ts.trace_add("write", self.trace_callback) + + def consume_event(self, event: Event) -> None: + with self.ring_buffer_lock: + self.ring_buffer.append(event) + + self._notify_data_changed() + + def render(self) -> str: + with self.ring_buffer_lock: + return "\n".join( + event.to_str(include_ts=self.include_ts.get()) + for event in self.ring_buffer + ) + + def clear(self) -> None: + with self.ring_buffer_lock: + self.ring_buffer.clear() + + self._notify_data_changed() + + def _notify_data_changed(self) -> None: + if callable(self.callback): + self.callback() + + +def get_image( + width: int = 64, height: int = 64, color1: str = "black", color2: str = "green" +) -> Image: + image = Image.new("RGB", (width, height), color1) + dc = ImageDraw.Draw(image) + dc.rectangle((width // 2, 0, width, height // 2), fill=color2) + dc.rectangle((0, height // 2, width // 2, height), fill=color2) + + return image + + +class DeathLogGUI: + def __init__(self, root: tk.Tk): + self.root = root + self.root.title(config.APP_NAME) + self.root.geometry("850x600") + self.root.minsize(850, 600) + self.root.protocol("WM_DELETE_WINDOW", self.handle_close_window) + theme_mapping = {True: "dark", False: "light"} + sv_ttk.set_theme(theme_mapping.get(config.db.get_bool("IS_DARK_MODE", False))) + + self.log_path = config.db.get_tk_str( + "LOG_FILE_PATH", r"E:\RSI\StarCitizen\LIVE\Game.log" + ) + + self.do_backread = config.db.get_tk_boolean("DO_BACKREAD", False) + self.include_npc_kills = config.db.get_tk_boolean("INCLUDE_NPC_KILLS", True) + + self.is_dark_mode = config.db.get_tk_boolean("IS_DARK_MODE", False) + self.is_dark_mode.trace_add( + "write", + lambda a, b, c: sv_ttk.set_theme( + theme_mapping.get(self.is_dark_mode.get()), self.root + ), + ) + + self.feed = Feed( + callback_data_changed=partial(self.root.after, 0, self.update_feed_display) + ) + self.enable_discord = config.db.get_tk_boolean("ENABLE_DISCORD", False) + self.webhook_url = config.db.get_tk_str( + "DISCORD_WEBHOOK_URL", "https://discord.com/..." + ) + self.discord = DiscordSubscriber() + self.discord.enabled = self.enable_discord.get() + self.discord.webhook_url = self.webhook_url.get() + + self.webhook_url.trace_add( + "write", lambda a, b, c: self.discord.set_url(self.webhook_url.get()) + ) + + self.enable_discord.trace_add( + "read", lambda a, b, c: self.discord.set_enabled(self.enable_discord.get()) + ) + + self.monitor: Monitor | None = None + self.monitor_thread: threading.Thread | None = None + + self.create_widgets() + self.update_feed_display() + + self.tray = pystray.Icon( + name=config.APP_NAME, + icon=get_image(), + menu=Menu( + MenuItem("Show window", self.handle_open_window, default=True), + MenuItem("Exit", self.shutdown), + ), + ) + + self.tray.visible = False + self.tray_thread = threading.Thread( + target=self.tray.run, kwargs={"setup": lambda _: None} + ) + self.tray_thread.start() + + def shutdown(self): + logger.info("Beginning GUI shutdown") + self.stop_monitoring() + self.tray.stop() + self.root.quit() + + def handle_close_window(self) -> None: + self.root.withdraw() + self.tray.visible = True + + def handle_open_window(self) -> None: + self.root.deiconify() + self.tray.visible = False + + def create_widgets(self): + # Create main frame + main_frame = ttk.Frame(self.root, padding="10") + main_frame.pack(fill=tk.BOTH, expand=True) + + # Configuration section + config_frame = ttk.LabelFrame(main_frame, text="Configuration", padding="10") + config_frame.pack(fill=tk.X, pady=5) + + # Log file path + ttk.Label(config_frame, text="Log File Path:").grid( + row=0, column=0, sticky=tk.W, pady=2 + ) + ttk.Entry(config_frame, textvariable=self.log_path, width=50).grid( + row=0, column=1, sticky=tk.W + tk.E, pady=2, padx=5 + ) + ttk.Button(config_frame, text="Browse", command=self.browse_log_file).grid( + row=0, column=2, pady=2 + ) + + # Discord webhook URL + ttk.Label(config_frame, text="Discord Webhook URL:").grid( + row=1, column=0, sticky=tk.W, pady=2 + ) + ttk.Entry(config_frame, textvariable=self.webhook_url, width=50).grid( + row=1, column=1, sticky=tk.W + tk.E, pady=2, padx=5 + ) + + # Checkboxes + ttk.Checkbutton( + config_frame, text="Process existing log entries", variable=self.do_backread + ).grid(row=2, column=0, columnspan=2, sticky=tk.W, pady=2) + + ttk.Checkbutton( + config_frame, + text="Do send events to discord", + variable=self.enable_discord, + ).grid(row=3, column=0, columnspan=2, sticky=tk.W, pady=2) + ttk.Checkbutton( + config_frame, + text="Hide NPC kills (_NPC_ in name)", + variable=self.include_npc_kills, + ).grid(row=4, column=0, columnspan=2, sticky=tk.W, pady=2) + ttk.Checkbutton( + config_frame, + text="Include timestamps", + variable=self.feed.include_ts, + ).grid(row=5, column=0, sticky=tk.W, pady=2) + + ttk.Checkbutton( + config_frame, + text="Enable dark mode", + variable=self.is_dark_mode, + ).grid(row=6, column=0, sticky=tk.W, pady=2) + + # Control buttons + control_frame = ttk.Frame(main_frame) + control_frame.pack(fill=tk.X, pady=5) + + self.start_button = ttk.Button( + control_frame, text="Start Monitoring", command=self.start_monitoring + ) + self.start_button.pack(side=tk.LEFT, padx=5) + + self.stop_button = ttk.Button( + control_frame, + text="Stop Monitoring", + command=self.stop_monitoring, + state=tk.DISABLED, + ) + self.stop_button.pack(side=tk.LEFT, padx=5) + + # Status indicator + self.status_var = tk.StringVar(value="Not monitoring") + status_frame = ttk.Frame(main_frame) + status_frame.pack(fill=tk.X, pady=5) + ttk.Label(status_frame, text="Status:").pack(side=tk.LEFT) + self.status_label = ttk.Label( + status_frame, textvariable=self.status_var, foreground="red" + ) + self.status_label.pack(side=tk.LEFT, padx=5) + + kills_frame = ttk.LabelFrame( + main_frame, text="Recent Kill Events", padding="10" + ) + kills_frame.pack(fill=tk.BOTH, expand=True, pady=5) + + self.feed_display = scrolledtext.ScrolledText( + kills_frame, wrap=tk.WORD, height=10 + ) + self.feed_display.pack(fill=tk.BOTH, expand=True) + self.feed_display.config(state=tk.DISABLED) + + def browse_log_file(self): + filename = filedialog.askopenfilename( + title="Select Star Citizen Log File", + filetypes=[("Log Files", "*.log"), ("All Files", "*.*")], + ) + if filename: + self.log_path.set(filename) + + def start_monitoring(self): + if self.monitor_thread and self.monitor_thread.is_alive(): + return + + try: + self.monitor = Monitor( + log_file_path=Path(self.log_path.get()), + do_backread=self.do_backread.get(), + subscribers={self.feed}, + ) + + self.monitor_thread = threading.Thread( + target=self.monitor.process_lines, daemon=True + ) + self.monitor_thread.start() + self.feed.clear() + + self.status_var.set("Monitoring active") + self.status_label.config(foreground="green") + self.start_button.config(state=tk.DISABLED) + self.stop_button.config(state=tk.NORMAL) + + except Exception as e: + self.status_var.set(f"Error: {str(e)}") + self.status_label.config(foreground="red") + logger.opt(exception=True).exception("Failed to start monitoring") + + def stop_monitoring(self): + if self.monitor: + self.monitor.running = False + self.monitor_thread.join(timeout=1.0) + + self.status_var.set("Monitoring stopped") + self.status_label.config(foreground="red") + self.start_button.config(state=tk.NORMAL) + self.stop_button.config(state=tk.DISABLED) + + def update_feed_display(self): + self.feed_display.config(state=tk.NORMAL) + self.feed_display.delete("1.0", tk.END) + self.feed_display.insert("1.0", self.feed.render()) + self.feed_display.config(state=tk.DISABLED) + self.feed_display.see(tk.END) + + # try: + # # Check if we should run in GUI mode or CLI mode + # import sys + # + # # CLI mode + # if config.DEBUG is True: + # app = App( + # Path("./Game.log"), + # DEFAULT_DISCORD_WEBHOOK_URL, + # do_mock_discord=True, + # do_backread=True, + # hide_npc_kills=False, + # ) + # + # else: + # app = App( + # DEFAULT_LOG_FILE_PATH, + # DEFAULT_DISCORD_WEBHOOK_URL, + # hide_npc_kills=False, + # ) + # app.process_lines() + # + # else: + # # GUI mode + # root = tk.Tk() + # app = DeathLogGUI(root) + # root.mainloop() + # + # except KeyboardInterrupt: + # print("\nMonitoring stopped") diff --git a/uv.lock b/uv.lock index 629a86d..50af129 100644 --- a/uv.lock +++ b/uv.lock @@ -55,6 +55,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 }, ] +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + [[package]] name = "idna" version = "3.10" @@ -64,6 +73,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] +[[package]] +name = "loguru" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595 }, +] + [[package]] name = "nuitka" version = "2.7.3" @@ -83,6 +105,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/55/af02708f230eb77084a299d7b08175cff006dea4f2721074b92cdb0296c0/ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562", size = 7634 }, ] +[[package]] +name = "pillow" +version = "11.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/9c/447528ee3776e7ab8897fe33697a7ff3f0475bb490c5ac1456a03dc57956/pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28", size = 3190098 }, + { url = "https://files.pythonhosted.org/packages/b5/09/29d5cd052f7566a63e5b506fac9c60526e9ecc553825551333e1e18a4858/pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830", size = 3030166 }, + { url = "https://files.pythonhosted.org/packages/71/5d/446ee132ad35e7600652133f9c2840b4799bbd8e4adba881284860da0a36/pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0", size = 4408674 }, + { url = "https://files.pythonhosted.org/packages/69/5f/cbe509c0ddf91cc3a03bbacf40e5c2339c4912d16458fcb797bb47bcb269/pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1", size = 4496005 }, + { url = "https://files.pythonhosted.org/packages/f9/b3/dd4338d8fb8a5f312021f2977fb8198a1184893f9b00b02b75d565c33b51/pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f", size = 4518707 }, + { url = "https://files.pythonhosted.org/packages/13/eb/2552ecebc0b887f539111c2cd241f538b8ff5891b8903dfe672e997529be/pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155", size = 4610008 }, + { url = "https://files.pythonhosted.org/packages/72/d1/924ce51bea494cb6e7959522d69d7b1c7e74f6821d84c63c3dc430cbbf3b/pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14", size = 4585420 }, + { url = "https://files.pythonhosted.org/packages/43/ab/8f81312d255d713b99ca37479a4cb4b0f48195e530cdc1611990eb8fd04b/pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b", size = 4667655 }, + { url = "https://files.pythonhosted.org/packages/94/86/8f2e9d2dc3d308dfd137a07fe1cc478df0a23d42a6c4093b087e738e4827/pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2", size = 2332329 }, + { url = "https://files.pythonhosted.org/packages/6d/ec/1179083b8d6067a613e4d595359b5fdea65d0a3b7ad623fee906e1b3c4d2/pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691", size = 2676388 }, + { url = "https://files.pythonhosted.org/packages/23/f1/2fc1e1e294de897df39fa8622d829b8828ddad938b0eaea256d65b84dd72/pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c", size = 2414950 }, + { url = "https://files.pythonhosted.org/packages/c4/3e/c328c48b3f0ead7bab765a84b4977acb29f101d10e4ef57a5e3400447c03/pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22", size = 3192759 }, + { url = "https://files.pythonhosted.org/packages/18/0e/1c68532d833fc8b9f404d3a642991441d9058eccd5606eab31617f29b6d4/pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7", size = 3033284 }, + { url = "https://files.pythonhosted.org/packages/b7/cb/6faf3fb1e7705fd2db74e070f3bf6f88693601b0ed8e81049a8266de4754/pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16", size = 4445826 }, + { url = "https://files.pythonhosted.org/packages/07/94/8be03d50b70ca47fb434a358919d6a8d6580f282bbb7af7e4aa40103461d/pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b", size = 4527329 }, + { url = "https://files.pythonhosted.org/packages/fd/a4/bfe78777076dc405e3bd2080bc32da5ab3945b5a25dc5d8acaa9de64a162/pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406", size = 4549049 }, + { url = "https://files.pythonhosted.org/packages/65/4d/eaf9068dc687c24979e977ce5677e253624bd8b616b286f543f0c1b91662/pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91", size = 4635408 }, + { url = "https://files.pythonhosted.org/packages/1d/26/0fd443365d9c63bc79feb219f97d935cd4b93af28353cba78d8e77b61719/pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751", size = 4614863 }, + { url = "https://files.pythonhosted.org/packages/49/65/dca4d2506be482c2c6641cacdba5c602bc76d8ceb618fd37de855653a419/pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9", size = 4692938 }, + { url = "https://files.pythonhosted.org/packages/b3/92/1ca0c3f09233bd7decf8f7105a1c4e3162fb9142128c74adad0fb361b7eb/pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd", size = 2335774 }, + { url = "https://files.pythonhosted.org/packages/a5/ac/77525347cb43b83ae905ffe257bbe2cc6fd23acb9796639a1f56aa59d191/pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e", size = 2681895 }, + { url = "https://files.pythonhosted.org/packages/67/32/32dc030cfa91ca0fc52baebbba2e009bb001122a1daa8b6a79ad830b38d3/pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681", size = 2417234 }, +] + [[package]] name = "pycparser" version = "2.22" @@ -92,6 +144,69 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, ] +[[package]] +name = "pyobjc-core" +version = "11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/94/a111239b98260869780a5767e5d74bfd3a8c13a40457f479c28dcd91f89d/pyobjc_core-11.0.tar.gz", hash = "sha256:63bced211cb8a8fb5c8ff46473603da30e51112861bd02c438fbbbc8578d9a70", size = 994931 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/16/0c468e73dbecb821e3da8819236fe832dfc53eb5f66a11775b055a7589ea/pyobjc_core-11.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c338c1deb7ab2e9436d4175d1127da2eeed4a1b564b3d83b9f3ae4844ba97e86", size = 743900 }, + { url = "https://files.pythonhosted.org/packages/f3/88/cecec88fd51f62a6cd7775cc4fb6bfde16652f97df88d28c84fb77ca0c18/pyobjc_core-11.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b4e9dc4296110f251a4033ff3f40320b35873ea7f876bd29a1c9705bb5e08c59", size = 791905 }, +] + +[[package]] +name = "pyobjc-framework-cocoa" +version = "11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/32/53809096ad5fc3e7a2c5ddea642590a5f2cb5b81d0ad6ea67fdb2263d9f9/pyobjc_framework_cocoa-11.0.tar.gz", hash = "sha256:00346a8cb81ad7b017b32ff7bf596000f9faa905807b1bd234644ebd47f692c5", size = 6173848 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/a5/609281a7e89efefbef9db1d8fe66bc0458c3b4e74e2227c644f9c18926fa/pyobjc_framework_Cocoa-11.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:15b2bd977ed340074f930f1330f03d42912d5882b697d78bd06f8ebe263ef92e", size = 385889 }, + { url = "https://files.pythonhosted.org/packages/93/f6/2d5a863673ef7b85a3cba875c43e6c495fb1307427a6801001ae94bb5e54/pyobjc_framework_Cocoa-11.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5750001db544e67f2b66f02067d8f0da96bb2ef71732bde104f01b8628f9d7ea", size = 389831 }, +] + +[[package]] +name = "pyobjc-framework-quartz" +version = "11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/ad/f00f3f53387c23bbf4e0bb1410e11978cbf87c82fa6baff0ee86f74c5fb6/pyobjc_framework_quartz-11.0.tar.gz", hash = "sha256:3205bf7795fb9ae34747f701486b3db6dfac71924894d1f372977c4d70c3c619", size = 3952463 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/9e/54c48fe8faab06ee5eb80796c8c17ec61fc313d84398540ee70abeaf7070/pyobjc_framework_Quartz-11.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:973b4f9b8ab844574461a038bd5269f425a7368d6e677e3cc81fcc9b27b65498", size = 212478 }, + { url = "https://files.pythonhosted.org/packages/4a/28/456b54a59bfe11a91b7b4e94f8ffdcf174ffd1efa169f4283e5b3bc10194/pyobjc_framework_Quartz-11.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:66ab58d65348863b8707e63b2ec5cdc54569ee8189d1af90d52f29f5fdf6272c", size = 217973 }, +] + +[[package]] +name = "pystray" +version = "0.19.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pillow" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, + { name = "python-xlib", marker = "sys_platform == 'linux'" }, + { name = "six" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/64/927a4b9024196a4799eba0180e0ca31568426f258a4a5c90f87a97f51d28/pystray-0.19.5-py2.py3-none-any.whl", hash = "sha256:a0c2229d02cf87207297c22d86ffc57c86c227517b038c0d3c59df79295ac617", size = 49068 }, +] + +[[package]] +name = "python-xlib" +version = "0.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/f5/8c0653e5bb54e0cbdfe27bf32d41f27bc4e12faa8742778c17f2a71be2c0/python-xlib-0.33.tar.gz", hash = "sha256:55af7906a2c75ce6cb280a584776080602444f75815a7aff4d287bb2d7018b32", size = 269068 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/b8/ff33610932e0ee81ae7f1269c890f697d56ff74b9f5b2ee5d9b7fa2c5355/python_xlib-0.33-py2.py3-none-any.whl", hash = "sha256:c3534038d42e0df2f1392a1b30a15a4ff5fdc2b86cfa94f072bf11b10a164398", size = 182185 }, +] + [[package]] name = "requests" version = "2.32.3" @@ -112,7 +227,10 @@ name = "sc-death-log" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "loguru" }, + { name = "pystray" }, { name = "requests" }, + { name = "sv-ttk" }, ] [package.dev-dependencies] @@ -121,11 +239,34 @@ dev = [ ] [package.metadata] -requires-dist = [{ name = "requests", specifier = ">=2.32.3" }] +requires-dist = [ + { name = "loguru", specifier = ">=0.7.3" }, + { name = "pystray", specifier = ">=0.19.5" }, + { name = "requests", specifier = ">=2.32.3" }, + { name = "sv-ttk", specifier = ">=2.6.0" }, +] [package.metadata.requires-dev] dev = [{ name = "nuitka", specifier = ">=2.7.3" }] +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "sv-ttk" +version = "2.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/da/6ad667a4bad4d66ec2d15206c1a1ad81d572679e516aae078824a6f35870/sv_ttk-2.6.0.tar.gz", hash = "sha256:3fd440396c95e30e88f686fcf28be425480f7320d6bf346f9cea5d6f56702cc2", size = 47214 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/3d/be0abc3202e90f282ad465f4e7c6e41bc8dce810ce5d1611566a1e7dfba8/sv_ttk-2.6.0-py3-none-any.whl", hash = "sha256:4319c52edf2e14732fe84bdc9788e26f9e9a1ad79451ec0f89f0120ffc8105d9", size = 49097 }, +] + [[package]] name = "urllib3" version = "2.4.0" @@ -135,6 +276,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, ] +[[package]] +name = "win32-setctime" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083 }, +] + [[package]] name = "zstandard" version = "0.23.0"