-
Notifications
You must be signed in to change notification settings - Fork 0
iSteed/eqwhotracker
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
# Creating a Standalone EverQuest /who Tracker .exe
This guide shows how to create a standalone .exe file that users can simply double-click to run - no Python installation required!
## For the App Creator (One-Time Setup)
### Step 1: Install Python and PyInstaller
```bash
# Install Python from python.org (if not already installed)
# Then install PyInstaller
pip install pyinstaller
```
### Step 2: Save the Tracker Script
Save this as `eq_who_tracker.py`:
```python
#!/usr/bin/env python3
"""
EverQuest /who Tracker - Desktop Application
A reliable file monitor for capturing /who results during raids.
"""
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
import os
import time
import threading
from datetime import datetime
import re
import json
class EQWhoTracker:
def __init__(self):
self.root = tk.Tk()
self.root.title("EverQuest /who Tracker - Raid Edition")
self.root.geometry("1200x800")
self.root.configure(bg='#f0f0f0')
# Application state
self.log_file_path = None
self.monitoring = False
self.last_file_size = 0
self.initial_file_size = 0
self.who_results = []
self.selected_result_index = None
self.monitor_thread = None
self.stop_monitoring = False
# Load settings
self.settings_file = "eq_tracker_settings.json"
self.load_settings()
self.create_widgets()
self.setup_styles()
# Auto-load last used file if it exists
if self.log_file_path and os.path.exists(self.log_file_path):
self.load_log_file(self.log_file_path)
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
def setup_styles(self):
"""Configure custom styles for better appearance"""
style = ttk.Style()
style.theme_use('clam')
# Configure button styles
style.configure('Action.TButton', font=('Arial', 10, 'bold'))
style.configure('Success.TButton', background='#28a745', foreground='white')
style.configure('Danger.TButton', background='#dc3545', foreground='white')
style.configure('Primary.TButton', background='#007bff', foreground='white')
def create_widgets(self):
"""Create the main UI"""
# Main title
title_frame = tk.Frame(self.root, bg='#2c3e50', height=80)
title_frame.pack(fill='x', padx=0, pady=0)
title_frame.pack_propagate(False)
title_label = tk.Label(title_frame, text="🎮 EverQuest /who Tracker - Raid Edition",
font=('Arial', 18, 'bold'), fg='white', bg='#2c3e50')
title_label.pack(expand=True)
# Main container
main_frame = tk.Frame(self.root, bg='#f0f0f0')
main_frame.pack(fill='both', expand=True, padx=20, pady=20)
# File selection section
file_frame = tk.LabelFrame(main_frame, text="📁 Log File Setup",
font=('Arial', 12, 'bold'), bg='#f0f0f0', fg='#2c3e50')
file_frame.pack(fill='x', pady=(0, 15))
file_inner = tk.Frame(file_frame, bg='#f0f0f0')
file_inner.pack(fill='x', padx=15, pady=15)
ttk.Button(file_inner, text="Select EverQuest Log File",
command=self.select_log_file, style='Action.TButton').pack(side='left')
self.file_label = tk.Label(file_inner, text="No file selected",
font=('Arial', 10), bg='#f0f0f0', fg='#666')
self.file_label.pack(side='left', padx=(15, 0))
# Monitoring controls
control_frame = tk.LabelFrame(main_frame, text="📡 Monitoring Control",
font=('Arial', 12, 'bold'), bg='#f0f0f0', fg='#2c3e50')
control_frame.pack(fill='x', pady=(0, 15))
control_inner = tk.Frame(control_frame, bg='#f0f0f0')
control_inner.pack(fill='x', padx=15, pady=15)
self.start_btn = ttk.Button(control_inner, text="▶ Start Monitoring",
command=self.start_monitoring, style='Success.TButton')
self.start_btn.pack(side='left', padx=(0, 10))
self.stop_btn = ttk.Button(control_inner, text="⏹ Stop Monitoring",
command=self.stop_monitoring_cmd, style='Danger.TButton', state='disabled')
self.stop_btn.pack(side='left', padx=(0, 10))
self.clear_btn = ttk.Button(control_inner, text="🗑 Clear Results",
command=self.clear_results, style='Action.TButton')
self.clear_btn.pack(side='left', padx=(0, 10))
# Status display
self.status_label = tk.Label(control_inner, text="Status: Ready to select log file",
font=('Arial', 10, 'bold'), bg='#f0f0f0', fg='#007bff')
self.status_label.pack(side='left', padx=(20, 0))
# Results count
self.count_label = tk.Label(control_inner, text="Results: 0",
font=('Arial', 10, 'bold'), bg='#f0f0f0', fg='#28a745')
self.count_label.pack(side='right')
# Results section
results_frame = tk.LabelFrame(main_frame, text="📊 Captured /who Results",
font=('Arial', 12, 'bold'), bg='#f0f0f0', fg='#2c3e50')
results_frame.pack(fill='both', expand=True)
# Create paned window for split layout
paned = ttk.PanedWindow(results_frame, orient='horizontal')
paned.pack(fill='both', expand=True, padx=15, pady=15)
# Left panel - Results list
left_frame = tk.Frame(paned, bg='white', relief='sunken', bd=1)
paned.add(left_frame, weight=1)
list_label = tk.Label(left_frame, text="Results List", font=('Arial', 10, 'bold'),
bg='#f8f9fa', fg='#495057')
list_label.pack(fill='x', padx=2, pady=2)
# Listbox with scrollbar
list_scroll_frame = tk.Frame(left_frame, bg='white')
list_scroll_frame.pack(fill='both', expand=True, padx=2, pady=2)
self.results_listbox = tk.Listbox(list_scroll_frame, font=('Arial', 9),
selectmode='single', bg='white')
list_scrollbar = tk.Scrollbar(list_scroll_frame, orient='vertical', command=self.results_listbox.yview)
self.results_listbox.configure(yscrollcommand=list_scrollbar.set)
self.results_listbox.bind('<<ListboxSelect>>', self.on_result_select)
self.results_listbox.pack(side='left', fill='both', expand=True)
list_scrollbar.pack(side='right', fill='y')
# Right panel - Result details
right_frame = tk.Frame(paned, bg='white', relief='sunken', bd=1)
paned.add(right_frame, weight=2)
detail_header = tk.Frame(right_frame, bg='#f8f9fa')
detail_header.pack(fill='x', padx=2, pady=2)
detail_label = tk.Label(detail_header, text="Selected Result", font=('Arial', 10, 'bold'),
bg='#f8f9fa', fg='#495057')
detail_label.pack(side='left')
# Detail control buttons
button_frame = tk.Frame(detail_header, bg='#f8f9fa')
button_frame.pack(side='right')
self.copy_btn = ttk.Button(button_frame, text="📋 Copy",
command=self.copy_selected_result, state='disabled')
self.copy_btn.pack(side='left', padx=(0, 5))
self.save_btn = ttk.Button(button_frame, text="💾 Save As...",
command=self.save_selected_result, state='disabled')
self.save_btn.pack(side='left')
# Result content display
self.result_text = scrolledtext.ScrolledText(right_frame, wrap='word',
font=('Courier New', 9),
bg='#f8f9fa', state='normal')
self.result_text.pack(fill='both', expand=True, padx=2, pady=2)
# Make text area read-only but selectable
self.result_text.bind("<Key>", lambda e: "break") # Prevent editing
self.result_text.insert(1.0, "Select a result from the list to view details")
# Instructions
instructions = ("💡 Instructions: Select your EverQuest log file, click 'Start Monitoring', then use /who commands in-game. "
"Results will be captured automatically and appear in the list on the left. "
"Select any result to view details and copy/save.")
instr_label = tk.Label(main_frame, text=instructions, font=('Arial', 9),
bg='#e3f2fd', fg='#1565c0', wraplength=800, justify='left')
instr_label.pack(fill='x', pady=(15, 0))
def select_log_file(self):
"""Open file dialog to select EQ log file"""
initial_dir = os.path.expanduser("~/Documents")
if self.log_file_path:
initial_dir = os.path.dirname(self.log_file_path)
file_path = filedialog.askopenfilename(
title="Select EverQuest Log File",
filetypes=[("Log files", "*.txt"), ("All files", "*.*")],
initialdir=initial_dir
)
if file_path:
self.load_log_file(file_path)
def load_log_file(self, file_path):
"""Load the selected log file"""
try:
if not os.path.exists(file_path):
messagebox.showerror("Error", "Selected file does not exist!")
return
self.log_file_path = file_path
self.last_file_size = os.path.getsize(file_path)
# Update UI
filename = os.path.basename(file_path)
file_size = self.format_file_size(self.last_file_size)
self.file_label.config(text=f"📄 {filename} ({file_size})", fg='#28a745')
self.start_btn.config(state='normal')
self.update_status("File loaded - Ready to start monitoring", '#007bff')
# Save settings
self.save_settings()
except Exception as e:
messagebox.showerror("Error", f"Failed to load file: {str(e)}")
def start_monitoring(self):
"""Start monitoring the log file"""
if not self.log_file_path or not os.path.exists(self.log_file_path):
messagebox.showerror("Error", "Please select a valid log file first!")
return
try:
# Set initial file size baseline (only capture new content)
self.initial_file_size = os.path.getsize(self.log_file_path)
self.last_file_size = self.initial_file_size
# Update UI
self.monitoring = True
self.stop_monitoring = False
self.start_btn.config(state='disabled')
self.stop_btn.config(state='normal')
self.update_status("🟢 Monitoring Active - Watching for new /who results", '#28a745')
# Start monitoring thread
self.monitor_thread = threading.Thread(target=self.monitor_file, daemon=True)
self.monitor_thread.start()
except Exception as e:
messagebox.showerror("Error", f"Failed to start monitoring: {str(e)}")
def stop_monitoring_cmd(self):
"""Stop monitoring the log file"""
self.monitoring = False
self.stop_monitoring = True
self.start_btn.config(state='normal')
self.stop_btn.config(state='disabled')
self.update_status("⏹ Monitoring Stopped", '#dc3545')
def monitor_file(self):
"""Background thread to monitor the log file"""
while self.monitoring and not self.stop_monitoring:
try:
if not os.path.exists(self.log_file_path):
self.root.after(0, lambda: self.update_status("❌ Log file not found!", '#dc3545'))
break
current_size = os.path.getsize(self.log_file_path)
if current_size > self.last_file_size:
# Read only the new content
with open(self.log_file_path, 'r', encoding='utf-8', errors='ignore') as f:
f.seek(self.last_file_size)
new_content = f.read()
self.last_file_size = current_size
# Parse for /who results
self.parse_who_results(new_content)
time.sleep(1) # Check every second
except Exception as e:
self.root.after(0, lambda: self.update_status(f"❌ Monitoring error: {str(e)}", '#dc3545'))
break
self.monitoring = False
def parse_who_results(self, content):
"""Parse content for /who results"""
if not content.strip():
return
lines = content.split('\n')
current_who = []
in_who_result = False
who_timestamp = None
for line in lines:
line = line.strip()
if not line:
continue
# Look for start of /who result
if 'Players on EverQuest:' in line:
in_who_result = True
current_who = [line]
# Extract timestamp
timestamp_match = re.match(r'^\[([^\]]+)\]', line)
who_timestamp = timestamp_match.group(1) if timestamp_match else datetime.now().strftime("%a %b %d %H:%M:%S %Y")
continue
if in_who_result:
current_who.append(line)
# Look for end of /who result
if 'There are' in line and 'players in' in line:
# Complete /who result found
who_content = '\n'.join(current_who)
self.root.after(0, lambda w=who_content, t=who_timestamp: self.add_who_result(w, t))
in_who_result = False
current_who = []
who_timestamp = None
def add_who_result(self, content, timestamp):
"""Add a new /who result to the list"""
# Check for duplicates
for existing in self.who_results:
if existing['content'] == content and existing['timestamp'] == timestamp:
return # Duplicate found, don't add
# Extract location and player count for display
location_match = re.search(r'There are \d+ players in (.+)\.', content)
location = location_match.group(1) if location_match else "Unknown"
count_match = re.search(r'There are (\d+) players', content)
player_count = count_match.group(1) if count_match else "?"
result = {
'timestamp': timestamp,
'content': content,
'location': location,
'player_count': player_count,
'display_name': f"[{timestamp}] {player_count} players in {location}"
}
self.who_results.append(result)
# Update UI
self.results_listbox.insert(0, result['display_name']) # Add to top
self.update_count_label()
# Show brief notification in status
self.update_status(f"✅ New /who captured: {player_count} players in {location}", '#28a745')
def on_result_select(self, event):
"""Handle result selection from listbox"""
selection = self.results_listbox.curselection()
if not selection:
self.selected_result_index = None
self.result_text.config(state='normal')
self.result_text.delete(1.0, tk.END)
self.result_text.insert(1.0, "Select a result from the list to view details")
self.result_text.config(state='disabled')
self.copy_btn.config(state='disabled')
self.save_btn.config(state='disabled')
return
# Get selected result (remember list is reversed)
list_index = selection[0]
result_index = len(self.who_results) - 1 - list_index
if 0 <= result_index < len(self.who_results):
self.selected_result_index = result_index
result = self.who_results[result_index]
# Display result content
self.result_text.config(state='normal')
self.result_text.delete(1.0, tk.END)
self.result_text.insert(1.0, f"Timestamp: {result['timestamp']}\n")
self.result_text.insert(tk.END, f"Location: {result['location']}\n")
self.result_text.insert(tk.END, f"Player Count: {result['player_count']}\n")
self.result_text.insert(tk.END, "\n" + "="*50 + "\n\n")
self.result_text.insert(tk.END, result['content'])
self.result_text.config(state='disabled')
self.copy_btn.config(state='normal')
self.save_btn.config(state='normal')
def copy_selected_result(self):
"""Copy selected result to clipboard"""
if self.selected_result_index is None:
return
result = self.who_results[self.selected_result_index]
content = f"[{result['timestamp']}]\n{result['content']}"
self.root.clipboard_clear()
self.root.clipboard_append(content)
self.root.update() # Ensure clipboard is updated
self.update_status("📋 Result copied to clipboard!", '#28a745')
def save_selected_result(self):
"""Save selected result to file"""
if self.selected_result_index is None:
return
result = self.who_results[self.selected_result_index]
# Generate filename
safe_location = re.sub(r'[^\w\s-]', '', result['location']).strip()
safe_location = re.sub(r'[-\s]+', '_', safe_location)
timestamp_part = result['timestamp'].split()[1] + "_" + result['timestamp'].split()[2]
filename = f"who_{safe_location}_{timestamp_part}.txt"
file_path = filedialog.asksaveasfilename(
title="Save /who Result",
defaultextension=".txt",
filetypes=[("Text files", "*.txt"), ("All files", "*.*")],
initialvalue=filename
)
if file_path:
try:
content = f"[{result['timestamp']}]\n{result['content']}"
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
self.update_status(f"💾 Result saved to {os.path.basename(file_path)}", '#28a745')
except Exception as e:
messagebox.showerror("Error", f"Failed to save file: {str(e)}")
def clear_results(self):
"""Clear all captured results"""
if not self.who_results:
return
if messagebox.askyesno("Confirm", "Are you sure you want to clear all captured results?"):
self.who_results.clear()
self.results_listbox.delete(0, tk.END)
self.selected_result_index = None
self.result_text.delete(1.0, tk.END)
self.result_text.insert(1.0, "No results captured yet")
self.copy_btn.config(state='disabled')
self.save_btn.config(state='disabled')
self.update_count_label()
self.update_status("🗑 All results cleared", '#dc3545')
def update_status(self, message, color='#007bff'):
"""Update status label"""
self.status_label.config(text=f"Status: {message}", fg=color)
def update_count_label(self):
"""Update results count label"""
self.count_label.config(text=f"Results: {len(self.who_results)}")
def format_file_size(self, size_bytes):
"""Format file size in human readable format"""
if size_bytes == 0:
return "0 B"
size_names = ["B", "KB", "MB", "GB"]
i = 0
while size_bytes >= 1024 and i < len(size_names) - 1:
size_bytes /= 1024.0
i += 1
return f"{size_bytes:.1f} {size_names[i]}"
def load_settings(self):
"""Load application settings"""
try:
if os.path.exists(self.settings_file):
with open(self.settings_file, 'r') as f:
settings = json.load(f)
self.log_file_path = settings.get('last_log_file')
except:
pass # Ignore errors loading settings
def save_settings(self):
"""Save application settings"""
try:
settings = {
'last_log_file': self.log_file_path
}
with open(self.settings_file, 'w') as f:
json.dump(settings, f)
except:
pass # Ignore errors saving settings
def on_closing(self):
"""Handle application closing"""
self.stop_monitoring_cmd()
self.save_settings()
self.root.destroy()
def run(self):
"""Start the application"""
self.root.mainloop()
if __name__ == "__main__":
import sys
if sys.version_info < (3, 6):
print("Error: This application requires Python 3.6 or newer")
sys.exit(1)
try:
app = EQWhoTracker()
app.run()
except KeyboardInterrupt:
print("\nApplication interrupted by user")
except Exception as e:
print(f"Error starting application: {e}")
sys.exit(1)
```
### Step 3: Create the Executable
```bash
# Navigate to the folder containing eq_who_tracker.py
cd /path/to/your/script
# Create a standalone executable (includes everything users need)
pyinstaller --onefile --windowed --name "EQ_Who_Tracker" eq_who_tracker.py
# Optional: Add an icon (if you have one)
# pyinstaller --onefile --windowed --icon=eq_icon.ico --name "EQ_Who_Tracker" eq_who_tracker.py
```
### Step 4: Find Your Executable
After PyInstaller finishes, you'll find:
- `dist/EQ_Who_Tracker.exe` - This is your standalone executable!
## For Users (Super Simple!)
### What Users Get:
1. A single `.exe` file (around 50-100MB)
2. No Python installation needed
3. No command line knowledge required
4. Just double-click and it works!
### User Instructions:
1. **Download** the `EQ_Who_Tracker.exe` file
2. **Double-click** to run (Windows may ask "Do you want to run this file?" - click Yes)
3. **Select your EQ log file** (usually in `EverQuest/Logs/` folder)
4. **Click "Start Monitoring"**
5. **Done!** Use `/who` in-game and results are captured automatically
## Sharing Options
### Option A: Direct File Sharing
- Upload the `.exe` to Google Drive, Dropbox, or file hosting
- Share the download link with your guild/friends
- Users download and double-click to run
### Option B: Guild Website
- Host the `.exe` on your guild website
- Add a download page with simple instructions
### Option C: Discord/Forums
- Upload to Discord (if under 8MB) or use file hosting
- Post download link with instructions
## Benefits of This Approach
✅ **Zero Setup** - Users just double-click
✅ **No Python Required** - Everything is bundled
✅ **No Command Line** - Pure GUI application
✅ **Works Offline** - No internet needed after download
✅ **Professional** - Looks and feels like a real application
✅ **Raid-Friendly** - Set it once and forget it
## Potential Issues & Solutions
### Windows Security Warning
- Windows Defender might flag the .exe as suspicious (common with PyInstaller)
- **Solution**: Users can click "More info" → "Run anyway"
- **Better**: Get the .exe code-signed (costs money but removes warnings)
### Large File Size
- The .exe will be 50-100MB (includes Python runtime)
- **Solution**: Use file hosting or compress with 7-Zip
### Auto-Updates
- Users need to download new versions manually
- **Solution**: Include version info in filename (e.g., `EQ_Who_Tracker_v1.0.exe`)
This approach gives you a truly user-friendly solution that works exactly like any other Windows application!
About
tracks /who request in everquest logs
Resources
Stars
Watchers
Forks
Releases
No releases published
Packages 0
No packages published