323 lines
11 KiB
Python
323 lines
11 KiB
Python
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)
|