Compare commits

..

10 Commits

Author SHA1 Message Date
d8bf7239f6
Best effor scaling for windows 2025-05-25 17:53:38 +00:00
eabd2f0f78
Implement filtering 2025-05-25 17:51:29 +00:00
25da1593d1
UI 2025-05-25 15:22:57 +00:00
a10fa2be5c
ruff 2025-05-24 15:12:04 +00:00
18f52a2e47
Add nuitka 2025-05-24 14:08:30 +00:00
681b894384
Use more emojis to depict damage type 2025-05-24 00:58:09 +00:00
594767e4d4
Use killed by 2025-05-24 00:41:22 +00:00
5851c6bf22
Emojis in messages 2025-05-24 00:20:24 +00:00
de8630bd45
Implement ONLY_PERSONAL_FEED 2025-05-24 00:10:58 +00:00
e464078293
Console only: implement filters PERSONAL_FEED and INCLUDE_NPC 2025-05-23 23:51:33 +00:00
9 changed files with 843 additions and 439 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
main.build
main.dist
main.onefile-build
main.bin
__pycache__

2
.idea/vcs.xml generated
View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="" vcs="Git" />
</component>
</project>

83
config.py Normal file
View File

@ -0,0 +1,83 @@
import tkinter as tk
import os
from pathlib import Path
import sqlite3
APP_NAME = "SC KF"
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")

34
discord_subscriber.py Normal file
View File

@ -0,0 +1,34 @@
import requests
from monitor import Event, SubscriberABC
from loguru import logger
from monitor import Filter
class DiscordSubscriber(SubscriberABC):
def __init__(self, event_filter: Filter):
super().__init__(event_filter)
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
if not self.filter.filter_event(event):
return
try:
r = requests.post(
self.webhook_url,
json={"content": event.to_str(include_ts=False, rich_text=True)},
)
r.raise_for_status()
except Exception:
logger.opt(exception=True).warning(f"Error sending to Discord: {event}")

370
main.py
View File

@ -1,378 +1,18 @@
from pathlib import Path
import threading
from ui import DeathLogGUI
import tkinter as tk
from tkinter import ttk, filedialog, scrolledtext
import queue
import requests
import time
from dataclasses import dataclass
from typing import Generator, Self
# Default values
DEFAULT_DISCORD_WEBHOOK_URL = "HOOOOOOOOOOOOOOOOK"
DEFAULT_LOG_FILE_PATH = Path(r"E:\RSI\StarCitizen\LIVE\Game.log")
DEBUG = False
@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] <Actor Death> 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,
hide_npc_kills: bool = False,
kill_callback=None,
):
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
self.kill_callback = kill_callback
self.hide_npc_kills = hide_npc_kills
self.running = True
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] <AccountLoginCharacterStatus_Character> 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():
if not self.running:
break
process_important_realtime_events = self.do_backread or line.is_realtime
if "<AccountLoginCharacterStatus_Character>" in line.line:
self.try_set_self_name(line.line)
continue
if (
"[Notice] <Actor Death> CActor::Kill:" in line.line
and process_important_realtime_events
):
kill = Kill.from_line(line.line)
# Skip processing if this is an NPC kill and hide_npc_kills is enabled
if self.hide_npc_kills and "_NPC_" in kill.victim:
continue
self.send_kill(kill)
def send_kill(self, kill: Kill) -> None:
if kill.victim == kill.killer:
msg = f"🤦 **{kill.victim}** РКН"
else:
msg = f"💀 **{kill.victim}** killed by 🔫 **{kill.killer}**"
self.send(msg)
# Call the callback if provided
if self.kill_callback:
self.kill_callback(kill, 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
try:
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:
is_realtime = True
time.sleep(5)
file.seek(current_position)
if DEBUG is True:
return
line = line.removesuffix("\n")
yield Line(line, is_realtime)
except FileNotFoundError:
print(f"Log file not found: {self.log_file_path}")
return
except Exception as e:
print(f"Error monitoring log file: {e}")
return
def stop(self):
self.running = False
class DeathLogGUI:
def __init__(self, root):
self.root = root
self.root.title("Star Citizen Death Log Monitor")
self.root.geometry("800x600")
self.root.minsize(600, 400)
self.log_path = tk.StringVar(value=str(DEFAULT_LOG_FILE_PATH))
self.webhook_url = tk.StringVar(value=DEFAULT_DISCORD_WEBHOOK_URL)
self.do_backread = tk.BooleanVar(value=False)
self.do_mock_discord = tk.BooleanVar(value=False)
self.hide_npc_kills = tk.BooleanVar(value=True)
self.app = None
self.monitor_thread = None
self.kill_queue = queue.Queue()
self.recent_kills: list[str] = []
self.max_kills_to_display = 100
self.create_widgets()
self.update_kill_display()
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="Mock Discord (don't send actual messages)",
variable=self.do_mock_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.hide_npc_kills,
).grid(row=4, column=0, columnspan=2, 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)
# Kill events display
kills_frame = ttk.LabelFrame(
main_frame, text="Recent Kill Events", padding="10"
)
kills_frame.pack(fill=tk.BOTH, expand=True, pady=5)
self.kill_display = scrolledtext.ScrolledText(
kills_frame, wrap=tk.WORD, height=10
)
self.kill_display.pack(fill=tk.BOTH, expand=True)
self.kill_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:
log_path = Path(self.log_path.get())
webhook_url = self.webhook_url.get()
self.app = App(
log_file_path=log_path,
discord_webhook_url=webhook_url,
do_backread=self.do_backread.get(),
do_mock_discord=self.do_mock_discord.get(),
hide_npc_kills=self.hide_npc_kills.get(),
kill_callback=self.on_kill,
)
self.monitor_thread = threading.Thread(
target=self.app.process_lines, daemon=True
)
self.monitor_thread.start()
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")
def stop_monitoring(self):
if self.app:
self.app.stop()
if self.monitor_thread:
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 on_kill(self, kill: Kill, message: str):
# No need to filter here as it's already filtered in the App class
self.kill_queue.put((kill, message))
def update_kill_display(self):
while not self.kill_queue.empty():
try:
kill, message = self.kill_queue.get_nowait()
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
# Simplified message with only nicknames and emojis
if kill.victim == kill.killer:
formatted_message = f"[{timestamp}] 🤦 {kill.victim} (self-kill)\n"
else:
formatted_message = (
f"[{timestamp}] 💀 {kill.victim} killed by 🔫 {kill.killer}\n"
)
self.recent_kills.append(formatted_message)
if len(self.recent_kills) > self.max_kills_to_display:
self.recent_kills.pop(0)
self.kill_display.config(state=tk.NORMAL)
self.kill_display.delete(1.0, tk.END)
for msg in self.recent_kills:
self.kill_display.insert(tk.END, msg)
self.kill_display.config(state=tk.DISABLED)
self.kill_display.see(tk.END)
except queue.Empty:
break
self.root.after(100, self.update_kill_display)
from contextlib import suppress
def main():
try:
# Check if we should run in GUI mode or CLI mode
import sys
with suppress(Exception):
from ctypes import windll
if len(sys.argv) > 1 and sys.argv[1] == "--cli":
# CLI mode
if DEBUG is True:
app = App(
Path("./Game.log"),
DEFAULT_DISCORD_WEBHOOK_URL,
do_mock_discord=True,
do_backread=True,
hide_npc_kills=False,
)
windll.shcore.SetProcessDpiAwareness(1)
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")
if __name__ == "__main__":
main()

179
monitor.py Normal file
View File

@ -0,0 +1,179 @@
import datetime
from pathlib import Path
import time
from dataclasses import dataclass
from typing import Generator, Self
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, rich_text: bool = False) -> str:
return str(self)
@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] <Actor Death> 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, rich_text: bool = True) -> str:
ts = (self.timestamp.strftime("%Y-%m-%d %H:%M:%S") + ": ") * include_ts
if self.victim == self.killer:
return f"{ts}{'**' * rich_text}{self.victim}{'**' * rich_text} РКН{' 🤦' * rich_text}"
emoji = " " + damage_type_to_emoji.get(self.damage_type, "")
return f"{ts}{'**' * rich_text}{self.victim}{'**' * rich_text} killed by {'🔫 ' * rich_text}{'**' * rich_text}{self.killer}{'**' * rich_text}{emoji * rich_text}"
@dataclass
class Filter:
only_personal_feed: bool
include_npc: bool
nickname: str | None = None
def filter_event(self, event: Event) -> bool:
if isinstance(event, Kill):
if "_NPC_" in event.killer + event.victim and not self.include_npc:
return False
if all(
[
self.nickname not in (event.killer, event.victim),
self.only_personal_feed is True,
self.nickname is not None,
]
):
return False
return True
else:
logger.warning(
f"Don't know how to filter event {type(event)}, letting through"
)
return True
class SubscriberABC(ABC):
def __init__(self, event_filter: Filter):
self.filter = event_filter
@abstractmethod
def consume_event(self, event: Event) -> None:
raise NotImplementedError
@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] <AccountLoginCharacterStatus_Character> 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 "<AccountLoginCharacterStatus_Character>" in line.line:
self.try_set_self_name(line.line)
continue
if (
"[Notice] <Actor Death> CActor::Kill:" in line.line
and process_important_realtime_events
):
kill = Kill.from_line(line.line)
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)
line = line.removesuffix("\n")
yield Line(line, is_realtime)
logger.info("Exiting reading loop")

View File

@ -4,6 +4,13 @@ version = "0.1.0"
description = "Add your description here"
requires-python = ">=3.13"
dependencies = [
"pyinstaller>=6.13.0",
"loguru>=0.7.3",
"pystray>=0.19.5",
"requests>=2.32.3",
"sv-ttk>=2.6.0",
]
[dependency-groups]
dev = [
"nuitka>=2.7.3",
]

322
ui.py Normal file
View File

@ -0,0 +1,322 @@
from contextlib import suppress
from functools import partial
from collections import deque
import pystray
from pystray import Menu, MenuItem
from PIL import Image, ImageDraw
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, Filter
from loguru import logger
from typing import Callable
class Feed(SubscriberABC):
def __init__(
self, event_filter: Filter, callback_data_changed: Callable | None = None
):
super().__init__(event_filter)
self.ring_buffer: deque[Event] = deque(maxlen=512)
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(), rich_text=False)
for event in self.ring_buffer
if self.filter.filter_event(event)
)
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(config.db.get_str("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.personal_feed = config.db.get_tk_boolean("PERSONAL_FEED", False)
self.filter = Filter(
only_personal_feed=self.personal_feed.get(),
include_npc=self.include_npc_kills.get(),
)
def filter_changed(a, b, c) -> None:
self.filter.include_npc = self.include_npc_kills.get()
self.filter.only_personal_feed = self.personal_feed.get()
logger.debug("Filter props changed")
self.feed.callback()
self.include_npc_kills.trace_add("write", filter_changed)
self.personal_feed.trace_add("write", filter_changed)
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),
event_filter=self.filter,
)
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(event_filter=self.filter)
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(
"write", 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()
logger.info(self.root.winfo_geometry())
config.db.set_str("GEOMETRY", self.root.winfo_geometry())
self.tray.stop()
config.db.db.close()
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="Only personal events",
variable=self.personal_feed,
).grid(row=4, 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=5, column=0, columnspan=2, sticky=tk.W, pady=2)
ttk.Checkbutton(
config_frame,
text="Include timestamps",
variable=self.feed.include_ts,
).grid(row=6, column=0, sticky=tk.W, pady=2)
ttk.Checkbutton(
config_frame,
text="Enable dark mode",
variable=self.is_dark_mode,
).grid(row=7, 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.discord},
)
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):
with suppress(AttributeError):
self.filter.nickname = self.monitor.nickname
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)

271
uv.lock generated
View File

@ -2,15 +2,6 @@ version = 1
revision = 1
requires-python = ">=3.13"
[[package]]
name = "altgraph"
version = "0.17.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/de/a8/7145824cf0b9e3c28046520480f207df47e927df83aa9555fb47f8505922/altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406", size = 48418 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/3f/3bc3f1d83f6e4a7fcb834d3720544ca597590425be5ba9db032b2bf322a2/altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff", size = 21212 },
]
[[package]]
name = "certifi"
version = "2025.4.26"
@ -20,6 +11,28 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618 },
]
[[package]]
name = "cffi"
version = "1.17.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 },
{ url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 },
{ url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 },
{ url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 },
{ url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 },
{ url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 },
{ url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 },
{ url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 },
{ url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 },
{ url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 },
{ url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 },
]
[[package]]
name = "charset-normalizer"
version = "3.4.2"
@ -42,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"
@ -52,83 +74,137 @@ wheels = [
]
[[package]]
name = "macholib"
version = "1.16.3"
name = "loguru"
version = "0.7.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "altgraph" },
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "win32-setctime", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/95/ee/af1a3842bdd5902ce133bd246eb7ffd4375c38642aeb5dc0ae3a0329dfa2/macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30", size = 59309 }
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/d1/5d/c059c180c84f7962db0aeae7c3b9303ed1d73d76f2bfbc32bc231c8be314/macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c", size = 38094 },
{ url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595 },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 },
]
[[package]]
name = "pefile"
version = "2023.2.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/78/c5/3b3c62223f72e2360737fd2a57c30e5b2adecd85e70276879609a7403334/pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc", size = 74854 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/55/26/d0ad8b448476d0a1e8d3ea5622dc77b916db84c6aa3cb1e1c0965af948fc/pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6", size = 71791 },
]
[[package]]
name = "pyinstaller"
version = "6.13.0"
name = "nuitka"
version = "2.7.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "altgraph" },
{ name = "macholib", marker = "sys_platform == 'darwin'" },
{ name = "packaging" },
{ name = "pefile", marker = "sys_platform == 'win32'" },
{ name = "pyinstaller-hooks-contrib" },
{ name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
{ name = "setuptools" },
{ name = "ordered-set" },
{ name = "zstandard" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a8/b1/2949fe6d3874e961898ca5cfc1bf2cf13bdeea488b302e74a745bc28c8ba/pyinstaller-6.13.0.tar.gz", hash = "sha256:38911feec2c5e215e5159a7e66fdb12400168bd116143b54a8a7a37f08733456", size = 4276427 }
sdist = { url = "https://files.pythonhosted.org/packages/d6/40/7fd16fffa8bbf78f9f9b1abe75ac123e3cc46740c6142c4db4e29b32ce4a/Nuitka-2.7.3.tar.gz", hash = "sha256:bd3dd12c5f0dfec0208f88c14ef3bbfc4ba3495eafc62c31b4e1f71171536e47", size = 3884010 }
[[package]]
name = "ordered-set"
version = "4.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4c/ca/bfac8bc689799bcca4157e0e0ced07e70ce125193fc2e166d2e685b7e2fe/ordered-set-4.1.0.tar.gz", hash = "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8", size = 12826 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b4/02/d1a347d35b1b627da1e148159e617576555619ac3bb8bbd5fed661fc7bb5/pyinstaller-6.13.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:aa404f0b02cd57948098055e76ee190b8e65ccf7a2a3f048e5000f668317069f", size = 1001923 },
{ url = "https://files.pythonhosted.org/packages/6b/80/6da39f7aeac65c9ca5afad0fac37887d75fdfd480178a7077c9d30b0704c/pyinstaller-6.13.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:92efcf2f09e78f07b568c5cb7ed48c9940f5dad627af4b49bede6320fab2a06e", size = 718135 },
{ url = "https://files.pythonhosted.org/packages/05/2c/d21d31f780a489609e7bf6385c0f7635238dc98b37cba8645b53322b7450/pyinstaller-6.13.0-py3-none-manylinux2014_i686.whl", hash = "sha256:9f82f113c463f012faa0e323d952ca30a6f922685d9636e754bd3a256c7ed200", size = 728543 },
{ url = "https://files.pythonhosted.org/packages/e1/20/e6ca87bbed6c0163533195707f820f05e10b8da1223fc6972cfe3c3c50c7/pyinstaller-6.13.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:db0e7945ebe276f604eb7c36e536479556ab32853412095e19172a5ec8fca1c5", size = 726868 },
{ url = "https://files.pythonhosted.org/packages/20/d5/53b19285f8817ab6c4b07c570208d62606bab0e5a049d50c93710a1d9dc6/pyinstaller-6.13.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:92fe7337c5aa08d42b38d7a79614492cb571489f2cb0a8f91dc9ef9ccbe01ed3", size = 725037 },
{ url = "https://files.pythonhosted.org/packages/84/5b/08e0b305ba71e6d7cb247e27d714da7536895b0283132d74d249bf662366/pyinstaller-6.13.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:bc09795f5954135dd4486c1535650958c8218acb954f43860e4b05fb515a21c0", size = 721027 },
{ url = "https://files.pythonhosted.org/packages/1f/9c/d8d0a7120103471be8dbe1c5419542aa794b9b9ec2ef628b542f9e6f9ef0/pyinstaller-6.13.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:589937548d34978c568cfdc39f31cf386f45202bc27fdb8facb989c79dfb4c02", size = 723443 },
{ url = "https://files.pythonhosted.org/packages/52/c7/8a9d81569dda2352068ecc6ee779d5feff6729569dd1b4ffd1236ecd38fe/pyinstaller-6.13.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:b7260832f7501ba1d2ce1834d4cddc0f2b94315282bc89c59333433715015447", size = 719915 },
{ url = "https://files.pythonhosted.org/packages/d5/e6/cccadb02b90198c7ed4ffb8bc34d420efb72b996f47cbd4738067a602d65/pyinstaller-6.13.0-py3-none-win32.whl", hash = "sha256:80c568848529635aa7ca46d8d525f68486d53e03f68b7bb5eba2c88d742e302c", size = 1294997 },
{ url = "https://files.pythonhosted.org/packages/1a/06/15cbe0e25d1e73d5b981fa41ff0bb02b15e924e30b8c61256f4a28c4c837/pyinstaller-6.13.0-py3-none-win_amd64.whl", hash = "sha256:8d4296236b85aae570379488c2da833b28828b17c57c2cc21fccd7e3811fe372", size = 1352714 },
{ url = "https://files.pythonhosted.org/packages/83/ef/74379298d46e7caa6aa7ceccc865106d3d4b15ac487ffdda2a35bfb6fe79/pyinstaller-6.13.0-py3-none-win_arm64.whl", hash = "sha256:d9f21d56ca2443aa6a1e255e7ad285c76453893a454105abe1b4d45e92bb9a20", size = 1293589 },
{ 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 = "pyinstaller-hooks-contrib"
version = "2025.4"
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"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 }
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 = "packaging" },
{ name = "setuptools" },
{ name = "pyobjc-core" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e3/94/dfc5c7903306211798f990e6794c2eb7b8685ac487b26979e9255790419c/pyinstaller_hooks_contrib-2025.4.tar.gz", hash = "sha256:5ce1afd1997b03e70f546207031cfdf2782030aabacc102190677059e2856446", size = 162628 }
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/d3/e1/ed48c7074145898e5c5b0072e87be975c5bd6a1d0f08c27a1daa7064fca0/pyinstaller_hooks_contrib-2025.4-py3-none-any.whl", hash = "sha256:6c2d73269b4c484eb40051fc1acee0beb113c2cfb3b37437b8394faae6f0d072", size = 434451 },
{ 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 = "pywin32-ctypes"
version = "0.2.3"
name = "pyobjc-framework-quartz"
version = "11.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 }
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/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 },
{ 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]]
@ -151,23 +227,44 @@ name = "sc-death-log"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "pyinstaller" },
{ name = "loguru" },
{ name = "pystray" },
{ name = "requests" },
{ name = "sv-ttk" },
]
[package.dev-dependencies]
dev = [
{ name = "nuitka" },
]
[package.metadata]
requires-dist = [
{ name = "pyinstaller", specifier = ">=6.13.0" },
{ 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 = "setuptools"
version = "80.8.0"
name = "sv-ttk"
version = "2.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8d/d2/ec1acaaff45caed5c2dedb33b67055ba9d4e96b091094df90762e60135fe/setuptools-80.8.0.tar.gz", hash = "sha256:49f7af965996f26d43c8ae34539c8d99c5042fbff34302ea151eaa9c207cd257", size = 1319720 }
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/58/29/93c53c098d301132196c3238c312825324740851d77a8500a2462c0fd888/setuptools-80.8.0-py3-none-any.whl", hash = "sha256:95a60484590d24103af13b686121328cc2736bee85de8936383111e421b9edc0", size = 1201470 },
{ url = "https://files.pythonhosted.org/packages/0f/3d/be0abc3202e90f282ad465f4e7c6e41bc8dce810ce5d1611566a1e7dfba8/sv_ttk-2.6.0-py3-none-any.whl", hash = "sha256:4319c52edf2e14732fe84bdc9788e26f9e9a1ad79451ec0f89f0120ffc8105d9", size = 49097 },
]
[[package]]
@ -178,3 +275,39 @@ sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e
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"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation == 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/2ac0287b442160a89d726b17a9184a4c615bb5237db763791a7fd16d9df1/zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09", size = 681701 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/80/f1/8386f3f7c10261fe85fbc2c012fdb3d4db793b921c9abcc995d8da1b7a80/zstandard-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:576856e8594e6649aee06ddbfc738fec6a834f7c85bf7cadd1c53d4a58186ef9", size = 788975 },
{ url = "https://files.pythonhosted.org/packages/16/e8/cbf01077550b3e5dc86089035ff8f6fbbb312bc0983757c2d1117ebba242/zstandard-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38302b78a850ff82656beaddeb0bb989a0322a8bbb1bf1ab10c17506681d772a", size = 633448 },
{ url = "https://files.pythonhosted.org/packages/06/27/4a1b4c267c29a464a161aeb2589aff212b4db653a1d96bffe3598f3f0d22/zstandard-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2240ddc86b74966c34554c49d00eaafa8200a18d3a5b6ffbf7da63b11d74ee2", size = 4945269 },
{ url = "https://files.pythonhosted.org/packages/7c/64/d99261cc57afd9ae65b707e38045ed8269fbdae73544fd2e4a4d50d0ed83/zstandard-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ef230a8fd217a2015bc91b74f6b3b7d6522ba48be29ad4ea0ca3a3775bf7dd5", size = 5306228 },
{ url = "https://files.pythonhosted.org/packages/7a/cf/27b74c6f22541f0263016a0fd6369b1b7818941de639215c84e4e94b2a1c/zstandard-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:774d45b1fac1461f48698a9d4b5fa19a69d47ece02fa469825b442263f04021f", size = 5336891 },
{ url = "https://files.pythonhosted.org/packages/fa/18/89ac62eac46b69948bf35fcd90d37103f38722968e2981f752d69081ec4d/zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed", size = 5436310 },
{ url = "https://files.pythonhosted.org/packages/a8/a8/5ca5328ee568a873f5118d5b5f70d1f36c6387716efe2e369010289a5738/zstandard-0.23.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac184f87ff521f4840e6ea0b10c0ec90c6b1dcd0bad2f1e4a9a1b4fa177982ea", size = 4859912 },
{ url = "https://files.pythonhosted.org/packages/ea/ca/3781059c95fd0868658b1cf0440edd832b942f84ae60685d0cfdb808bca1/zstandard-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c363b53e257246a954ebc7c488304b5592b9c53fbe74d03bc1c64dda153fb847", size = 4936946 },
{ url = "https://files.pythonhosted.org/packages/ce/11/41a58986f809532742c2b832c53b74ba0e0a5dae7e8ab4642bf5876f35de/zstandard-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e7792606d606c8df5277c32ccb58f29b9b8603bf83b48639b7aedf6df4fe8171", size = 5466994 },
{ url = "https://files.pythonhosted.org/packages/83/e3/97d84fe95edd38d7053af05159465d298c8b20cebe9ccb3d26783faa9094/zstandard-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0817825b900fcd43ac5d05b8b3079937073d2b1ff9cf89427590718b70dd840", size = 4848681 },
{ url = "https://files.pythonhosted.org/packages/6e/99/cb1e63e931de15c88af26085e3f2d9af9ce53ccafac73b6e48418fd5a6e6/zstandard-0.23.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9da6bc32faac9a293ddfdcb9108d4b20416219461e4ec64dfea8383cac186690", size = 4694239 },
{ url = "https://files.pythonhosted.org/packages/ab/50/b1e703016eebbc6501fc92f34db7b1c68e54e567ef39e6e59cf5fb6f2ec0/zstandard-0.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd7699e8fd9969f455ef2926221e0233f81a2542921471382e77a9e2f2b57f4b", size = 5200149 },
{ url = "https://files.pythonhosted.org/packages/aa/e0/932388630aaba70197c78bdb10cce2c91fae01a7e553b76ce85471aec690/zstandard-0.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d477ed829077cd945b01fc3115edd132c47e6540ddcd96ca169facff28173057", size = 5655392 },
{ url = "https://files.pythonhosted.org/packages/02/90/2633473864f67a15526324b007a9f96c96f56d5f32ef2a56cc12f9548723/zstandard-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ce8b52c5987b3e34d5674b0ab529a4602b632ebab0a93b07bfb4dfc8f8a33", size = 5191299 },
{ url = "https://files.pythonhosted.org/packages/b0/4c/315ca5c32da7e2dc3455f3b2caee5c8c2246074a61aac6ec3378a97b7136/zstandard-0.23.0-cp313-cp313-win32.whl", hash = "sha256:a9b07268d0c3ca5c170a385a0ab9fb7fdd9f5fd866be004c4ea39e44edce47dd", size = 430862 },
{ url = "https://files.pythonhosted.org/packages/a2/bf/c6aaba098e2d04781e8f4f7c0ba3c7aa73d00e4c436bcc0cf059a66691d1/zstandard-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:f3513916e8c645d0610815c257cbfd3242adfd5c4cfa78be514e5a3ebb42a41b", size = 495578 },
]