Compare commits

...

8 Commits

36 changed files with 5499 additions and 6325 deletions

View File

@ -1,38 +1,265 @@
name: EU-Utility CI/CD
name: CI/CD
on:
push:
branches: [ main, develop ]
branches: [main, develop]
tags: ["v*"]
pull_request:
branches: [ main ]
branches: [main, develop]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
PYTHON_LATEST: "3.12"
PYTHON_MINIMUM: "3.11"
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ['3.11', '3.12']
# ===========================================================================
# Code Quality Checks
# ===========================================================================
lint:
name: Lint and Format Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
python-version: ${{ env.PYTHON_LATEST }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
pip install -r requirements.txt
pip install black flake8 isort mypy
- name: Check code formatting with black
run: black --check --diff .
- name: Check import sorting with isort
run: isort --check-only --diff .
- name: Lint with flake8
run: |
# Stop on syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
# Treat warnings as errors for CI
flake8 . --count --max-complexity=10 --max-line-length=100 --statistics
- name: Test with pytest
- name: Type check with mypy
run: mypy core plugins --ignore-missing-imports
# ===========================================================================
# Security Analysis
# ===========================================================================
security:
name: Security Analysis
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_LATEST }}
- name: Install dependencies
run: |
pytest tests/ -v
python -m pip install --upgrade pip
pip install bandit[toml] safety
- name: Run Bandit security linter
run: bandit -r core plugins -f json -o bandit-report.json || true
- name: Upload Bandit report
uses: actions/upload-artifact@v4
if: always()
with:
name: bandit-report
path: bandit-report.json
- name: Run Safety check
run: safety check || true
# ===========================================================================
# Tests
# ===========================================================================
test:
name: Test (Python ${{ matrix.python-version }}, ${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ["3.11", "3.12"]
include:
- os: ubuntu-latest
python-version: "3.11"
coverage: true
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install system dependencies (Ubuntu)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libegl1 libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"
- name: Run tests with coverage
if: matrix.coverage
run: |
pytest tests/ -v --cov=core --cov=plugins --cov-report=xml --cov-report=html --cov-fail-under=80
env:
QT_QPA_PLATFORM: offscreen
- name: Run tests without coverage
if: '!matrix.coverage'
run: |
pytest tests/ -v -m "not slow and not ui"
env:
QT_QPA_PLATFORM: offscreen
- name: Upload coverage to Codecov
if: matrix.coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage.xml
fail_ci_if_error: true
verbose: true
- name: Upload coverage report
if: matrix.coverage
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: htmlcov/
# ===========================================================================
# Build and Package
# ===========================================================================
build:
name: Build Package
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_LATEST }}
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
pip install build twine
- name: Build package
run: python -m build
- name: Check package
run: twine check dist/*
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
# ===========================================================================
# Test Installation
# ===========================================================================
test-install:
name: Test Installation
runs-on: ${{ matrix.os }}
needs: build
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_LATEST }}
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Install from wheel
run: pip install dist/*.whl
- name: Test CLI entry point
run: |
eu-utility --help || true
python -c "from core import __version__; print(f'Version: {__version__}')"
# ===========================================================================
# Release
# ===========================================================================
release:
name: Create Release
runs-on: ubuntu-latest
needs: [build, test-install]
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
files: dist/*
generate_release_notes: true
draft: false
prerelease: ${{ contains(github.ref, 'alpha') || contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
# ===========================================================================
# Publish to PyPI (on tagged releases)
# ===========================================================================
publish:
name: Publish to PyPI
runs-on: ubuntu-latest
needs: release
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, 'dev')
environment:
name: pypi
url: https://pypi.org/p/eu-utility
permissions:
id-token: write
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

991
API.md Normal file
View File

@ -0,0 +1,991 @@
# EU-Utility API Reference
> Complete API documentation for plugin developers
**Version:** 2.1.0
**Last Updated:** 2026-02-16
---
## Table of Contents
1. [Getting Started](#getting-started)
2. [PluginAPI](#pluginapi) - Core services access
3. [WidgetAPI](#widgetapi) - Overlay widget management
4. [ExternalAPI](#externalapi) - REST endpoints and webhooks
5. [Event Bus](#event-bus) - Pub/sub event system
6. [BasePlugin Class](#baseplugin-class) - Plugin base class reference
7. [Nexus API](#nexus-api) - Entropia Nexus integration
8. [Code Examples](#code-examples) - Practical examples
9. [Best Practices](#best-practices)
---
## Getting Started
### Importing the API
```python
from core.api import get_api
class MyPlugin(BasePlugin):
def initialize(self):
self.api = get_api()
```
### API Availability
Always check if a service is available before using it:
```python
if self.api.ocr_available():
text = self.api.recognize_text()
```
---
## PluginAPI
The PluginAPI provides access to all core EU-Utility services.
### Window Manager
```python
# Get EU window information
window = api.get_eu_window()
if window:
print(f"Position: {window['x']}, {window['y']}")
print(f"Size: {window['width']}x{window['height']}")
print(f"Focused: {window['is_focused']}")
# Check if EU is focused
if api.is_eu_focused():
api.play_sound("alert.wav")
# Bring EU to front
api.bring_eu_to_front()
# Check if EU window is visible
is_visible = api.is_eu_visible()
```
### OCR Service
```python
# Check OCR availability
if api.ocr_available():
# Read text from screen region
text = api.recognize_text(region=(100, 100, 200, 50))
print(f"Found: {text}")
# Alternative: Use BasePlugin helper method
result = self.ocr_capture(region=(x, y, width, height))
# Returns: {'text': str, 'confidence': float, 'error': str or None}
```
### Screenshot Service
```python
# Check availability
if api.screenshot_available():
# Capture full screen
img = api.capture_screen()
# Capture specific region
img = api.capture_screen(region=(100, 100, 400, 300))
# Capture and save
img = api.capture_screen(
region=(100, 100, 400, 300),
save_path="screenshot.png"
)
# Alternative: Use BasePlugin helper
img = self.capture_screen(full_screen=True)
img = self.capture_region(x, y, width, height)
```
### Log Reader
```python
# Read recent log lines
lines = api.read_log_lines(count=100)
# Read logs since timestamp
from datetime import datetime, timedelta
recent = api.read_log_since(datetime.now() - timedelta(minutes=5))
# Alternative: Use BasePlugin helper
lines = self.read_log(lines=50, filter_text="loot")
```
### Data Store
```python
# Store data (scoped to plugin)
api.set_data("key", value)
# Retrieve with default
value = api.get_data("key", default=None)
# Delete data
api.delete_data("key")
# Alternative: Use BasePlugin helpers
self.save_data("key", value)
data = self.load_data("key", default=None)
self.delete_data("key")
```
### HTTP Client
```python
# GET request with caching
result = api.http_get(
"https://api.example.com/data",
cache=True,
cache_duration=3600 # 1 hour
)
if result['success']:
data = result['data']
else:
error = result['error']
# POST request
result = api.http_post(
"https://api.example.com/submit",
data={"key": "value"}
)
# Alternative: Use BasePlugin helper
response = self.http_get(url, cache_ttl=300, headers={})
```
### Audio
```python
# Play sound file
api.play_sound("assets/sounds/alert.wav", volume=0.7)
# Simple beep
api.beep()
# Alternative: Use BasePlugin helpers
self.play_sound("hof") # Predefined: 'hof', 'skill_gain', 'alert'
self.set_volume(0.8)
volume = self.get_volume()
```
### Notifications
```python
# Show toast notification
api.show_notification(
title="Loot Alert!",
message="You found something valuable!",
duration=5000, # milliseconds
sound=True
)
# Alternative: Use BasePlugin helpers
self.notify(title, message, notification_type='info', sound=False)
self.notify_info(title, message)
self.notify_success(title, message)
self.notify_warning(title, message)
self.notify_error(title, message, sound=True)
```
### Clipboard
```python
# Copy to clipboard
api.copy_to_clipboard("Text to copy")
# Paste from clipboard
text = api.paste_from_clipboard()
# Alternative: Use BasePlugin helpers
self.copy_to_clipboard(text)
text = self.paste_from_clipboard()
```
### Background Tasks
```python
# Run function in background
def heavy_computation(data):
# Long running task
import time
time.sleep(2)
return f"Processed: {data}"
def on_complete(result):
print(f"Done: {result}")
def on_error(error):
print(f"Error: {error}")
task_id = api.run_task(
heavy_computation,
"my data",
callback=on_complete,
error_handler=on_error
)
# Cancel task
api.cancel_task(task_id)
# Alternative: Use BasePlugin helper
self.run_in_background(func, *args, priority='normal',
on_complete=cb, on_error=err_cb)
```
---
## WidgetAPI
Manage overlay widgets - floating UI components.
### Getting Started
```python
from core.api import get_widget_api
widget_api = get_widget_api()
# Create widget
widget = widget_api.create_widget(
name="loot_tracker",
title="Loot Tracker",
size=(400, 300),
position=(100, 100)
)
widget.show()
```
### Widget Operations
```python
# Show/hide
widget.show()
widget.hide()
# Position
widget.move(500, 200)
x, y = widget.position
# Size
widget.resize(400, 300)
width, height = widget.size
# Opacity (0.0 - 1.0)
widget.set_opacity(0.8)
# Lock/unlock (prevent dragging)
widget.set_locked(True)
# Minimize/restore
widget.minimize()
widget.restore()
# Close
widget.close()
```
### Widget Management
```python
# Get existing widget
widget = widget_api.get_widget("loot_tracker")
# Show/hide specific widget
widget_api.show_widget("loot_tracker")
widget_api.hide_widget("loot_tracker")
# Close widget
widget_api.close_widget("loot_tracker")
# Global operations
widget_api.show_all_widgets()
widget_api.hide_all_widgets()
widget_api.close_all_widgets()
widget_api.set_all_opacity(0.8)
widget_api.lock_all()
widget_api.unlock_all()
```
### Layout Helpers
```python
# Arrange widgets
widget_api.arrange_widgets(layout="grid", spacing=10)
widget_api.arrange_widgets(layout="horizontal")
widget_api.arrange_widgets(layout="vertical")
widget_api.arrange_widgets(layout="cascade")
# Snap to grid
widget_api.snap_to_grid(grid_size=10)
```
### Widget Events
```python
# Handle events
widget.on('moved', lambda data: print(f"Moved to {data['x']}, {data['y']}"))
widget.on('resized', lambda data: print(f"Sized to {data['width']}x{data['height']}"))
widget.on('closing', lambda: print("Widget closing"))
widget.on('closed', lambda: print("Widget closed"))
widget.on('update', lambda data: print(f"Update: {data}"))
```
---
## ExternalAPI
REST endpoints, webhooks, and third-party integrations.
### Getting Started
```python
from core.api import get_external_api
ext = get_external_api()
# Start server
ext.start_server(port=8080)
# Check status
print(ext.get_status())
```
### REST Endpoints
```python
# Using decorator
@ext.endpoint("stats", methods=["GET"])
def get_stats():
return {"kills": 100, "loot": "50 PED"}
@ext.endpoint("loot", methods=["POST"])
def record_loot(data):
save_loot(data)
return {"status": "saved"}
# Programmatic registration
def get_stats_handler(params):
return {"kills": 100}
ext.register_endpoint("stats", get_stats_handler, methods=["GET"])
# Unregister
ext.unregister_endpoint("stats")
```
### Incoming Webhooks
```python
# Register webhook handler
def handle_discord(payload):
print(f"Discord: {payload}")
return {"status": "ok"}
ext.register_webhook(
name="discord",
handler=handle_discord,
secret="my_secret" # Optional HMAC verification
)
# POST to: http://localhost:8080/webhook/discord
```
### Outgoing Webhooks
```python
# POST to external webhook
result = ext.post_webhook(
"https://discord.com/api/webhooks/...",
{"content": "Hello from EU-Utility!"}
)
if result['success']:
print("Sent!")
else:
print(f"Error: {result['error']}")
```
---
## Event Bus
Typed publish-subscribe event system for inter-plugin communication.
### Event Types
```python
from core.event_bus import (
SkillGainEvent,
LootEvent,
DamageEvent,
GlobalEvent,
ChatEvent,
EconomyEvent,
SystemEvent
)
```
### Publishing Events
```python
from core.event_bus import LootEvent, SkillGainEvent
# Publish loot event
self.publish_typed(LootEvent(
mob_name="Daikiba",
items=[{"name": "Animal Oil", "value": 0.05}],
total_tt_value=0.05
))
# Publish skill gain
self.publish_typed(SkillGainEvent(
skill_name="Rifle",
skill_value=25.5,
gain_amount=0.01
))
# Legacy event publishing
api.publish("my_plugin.event", {"data": "value"})
```
### Subscribing to Events
```python
from core.event_bus import LootEvent, SkillGainEvent
class MyPlugin(BasePlugin):
def initialize(self):
# Subscribe to loot events
self.sub_id = self.subscribe_typed(
LootEvent,
self.on_loot
)
# Subscribe with filtering
self.sub_id2 = self.subscribe_typed(
LootEvent,
self.on_dragon_loot,
mob_types=["Dragon", "Drake"]
)
# Subscribe to specific skills
self.sub_id3 = self.subscribe_typed(
SkillGainEvent,
self.on_combat_skill,
skill_names=["Rifle", "Pistol", "Melee"]
)
def on_loot(self, event: LootEvent):
print(f"Loot from {event.mob_name}: {event.items}")
def on_dragon_loot(self, event: LootEvent):
print(f"Dragon loot! Total TT: {event.total_tt_value}")
def shutdown(self):
# Clean up subscriptions
self.unsubscribe_typed(self.sub_id)
self.unsubscribe_typed(self.sub_id2)
self.unsubscribe_typed(self.sub_id3)
# Or: self.unsubscribe_all_typed()
```
### Event Attributes
| Event | Attributes |
|-------|------------|
| `SkillGainEvent` | `skill_name`, `skill_value`, `gain_amount` |
| `LootEvent` | `mob_name`, `items`, `total_tt_value`, `position` |
| `DamageEvent` | `damage_amount`, `damage_type`, `is_critical`, `target_name` |
| `GlobalEvent` | `player_name`, `achievement_type`, `value`, `item_name` |
| `ChatEvent` | `channel`, `sender`, `message` |
| `EconomyEvent` | `transaction_type`, `amount`, `currency`, `description` |
| `SystemEvent` | `message`, `severity` |
### Legacy Event Bus
```python
# Subscribe
sub_id = api.subscribe("loot", on_loot_callback)
# Unsubscribe
api.unsubscribe(sub_id)
# Publish
api.publish("event_type", {"key": "value"})
# Get history
history = api.get_event_history("loot", limit=10)
```
---
## BasePlugin Class
Complete reference for the base plugin class.
### Required Attributes
```python
class MyPlugin(BasePlugin):
name = "My Plugin" # Display name
version = "1.0.0" # Version string
author = "Your Name" # Author name
description = "..." # Short description
```
### Optional Attributes
```python
class MyPlugin(BasePlugin):
icon = "path/to/icon.png" # Icon path
hotkey = "ctrl+shift+y" # Legacy single hotkey
hotkeys = [ # Multi-hotkey format
{
'action': 'toggle',
'description': 'Toggle My Plugin',
'default': 'ctrl+shift+m',
'config_key': 'myplugin_toggle'
}
]
enabled = True # Start enabled
dependencies = { # Dependencies
'pip': ['requests', 'numpy'],
'plugins': ['other_plugin'],
'optional': {'pillow': 'Image processing'}
}
```
### Lifecycle Methods
```python
def initialize(self) -> None:
"""Called when plugin is loaded."""
self.api = get_api()
self.log_info("Initialized!")
def get_ui(self) -> QWidget:
"""Return the plugin's UI widget."""
widget = QWidget()
# ... setup UI ...
return widget
def on_show(self) -> None:
"""Called when overlay becomes visible."""
pass
def on_hide(self) -> None:
"""Called when overlay is hidden."""
pass
def on_hotkey(self) -> None:
"""Called when hotkey is pressed."""
pass
def shutdown(self) -> None:
"""Called when app is closing. Cleanup resources."""
self.unsubscribe_all_typed()
super().shutdown()
```
### Config Methods
```python
# Get config value
theme = self.get_config('theme', default='dark')
# Set config value
self.set_config('theme', 'light')
```
### Logging Methods
```python
self.log_debug("Debug message")
self.log_info("Info message")
self.log_warning("Warning message")
self.log_error("Error message")
```
### Utility Methods
```python
# Format currency
ped_text = self.format_ped(123.45) # "123.45 PED"
pec_text = self.format_pec(50) # "50 PEC"
# Calculate DPP
dpp = self.calculate_dpp(damage=45.0, ammo=100, decay=2.5)
# Calculate markup
markup = self.calculate_markup(price=150.0, tt=100.0) # 150.0%
```
---
## Nexus API
Entropia Nexus integration for game data.
### Searching
```python
# Search for items
results = self.nexus_search("ArMatrix", entity_type="items")
# Search for mobs
mobs = self.nexus_search("Atrox", entity_type="mobs")
# Search for locations
locations = self.nexus_search("Fort", entity_type="locations")
```
### Entity Types
- `items`, `weapons`, `armors`
- `mobs`, `pets`
- `blueprints`, `materials`
- `locations`, `teleporters`, `shops`, `vendors`, `planets`, `areas`
- `skills`
- `enhancers`, `medicaltools`, `finders`, `excavators`, `refiners`
- `vehicles`, `decorations`, `furniture`
- `storagecontainers`, `strongboxes`
### Getting Details
```python
# Get item details
details = self.nexus_get_item_details("armatrix_lp-35")
if details:
print(f"Name: {details['name']}")
print(f"TT Value: {details['tt_value']} PED")
# Get market data
market = self.nexus_get_market_data("armatrix_lp-35")
if market:
print(f"Current markup: {market['current_markup']:.1f}%")
```
---
## Code Examples
### Example 1: Simple Calculator Plugin
```python
from plugins.base_plugin import BasePlugin
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton
)
class MarkupCalculatorPlugin(BasePlugin):
"""Calculate markup percentages."""
name = "Markup Calculator"
version = "1.0.0"
author = "Tutorial"
description = "Calculate item markup"
hotkey = "ctrl+shift+m"
def initialize(self):
self.log_info("Markup Calculator initialized!")
def get_ui(self):
widget = QWidget()
layout = QVBoxLayout(widget)
# Title
title = QLabel("Markup Calculator")
title.setStyleSheet("color: #4a9eff; font-size: 18px; font-weight: bold;")
layout.addWidget(title)
# TT Value input
layout.addWidget(QLabel("TT Value:"))
self.tt_input = QLineEdit()
self.tt_input.setPlaceholderText("100.00")
layout.addWidget(self.tt_input)
# Market Price input
layout.addWidget(QLabel("Market Price:"))
self.price_input = QLineEdit()
self.price_input.setPlaceholderText("150.00")
layout.addWidget(self.price_input)
# Calculate button
calc_btn = QPushButton("Calculate")
calc_btn.clicked.connect(self.calculate)
layout.addWidget(calc_btn)
# Result
self.result_label = QLabel("Markup: -")
self.result_label.setStyleSheet("color: #ffc107; font-size: 16px;")
layout.addWidget(self.result_label)
layout.addStretch()
return widget
def calculate(self):
try:
tt = float(self.tt_input.text() or 0)
price = float(self.price_input.text() or 0)
markup = self.calculate_markup(price, tt)
self.result_label.setText(f"Markup: {markup:.1f}%")
except ValueError:
self.result_label.setText("Invalid input")
```
### Example 2: Event-Driven Tracker
```python
from plugins.base_plugin import BasePlugin
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel, QListWidget
from core.event_bus import LootEvent, SkillGainEvent
class ActivityTrackerPlugin(BasePlugin):
"""Track recent activity from events."""
name = "Activity Tracker"
version = "1.0.0"
def initialize(self):
self.subscriptions = []
# Subscribe to multiple event types
sub1 = self.subscribe_typed(LootEvent, self.on_loot)
sub2 = self.subscribe_typed(SkillGainEvent, self.on_skill)
self.subscriptions.extend([sub1, sub2])
self.log_info("Activity Tracker initialized")
def get_ui(self):
widget = QWidget()
layout = QVBoxLayout(widget)
layout.addWidget(QLabel("Recent Activity:"))
self.activity_list = QListWidget()
layout.addWidget(self.activity_list)
return widget
def on_loot(self, event: LootEvent):
text = f"Loot: {event.mob_name} - {event.total_tt_value:.2f} PED"
self.activity_list.insertItem(0, text)
def on_skill(self, event: SkillGainEvent):
text = f"Skill: {event.skill_name} +{event.gain_amount:.4f}"
self.activity_list.insertItem(0, text)
def shutdown(self):
for sub_id in self.subscriptions:
self.unsubscribe_typed(sub_id)
```
### Example 3: Background Task Plugin
```python
from plugins.base_plugin import BasePlugin
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QPushButton,
QLabel, QProgressBar
)
class DataFetcherPlugin(BasePlugin):
"""Fetch data in background."""
name = "Data Fetcher"
version = "1.0.0"
def initialize(self):
self.log_info("Data Fetcher initialized")
def get_ui(self):
widget = QWidget()
layout = QVBoxLayout(widget)
self.status_label = QLabel("Ready")
layout.addWidget(self.status_label)
self.progress = QProgressBar()
self.progress.setRange(0, 0) # Indeterminate
self.progress.hide()
layout.addWidget(self.progress)
fetch_btn = QPushButton("Fetch Data")
fetch_btn.clicked.connect(self.fetch_data)
layout.addWidget(fetch_btn)
layout.addStretch()
return widget
def fetch_data(self):
self.status_label.setText("Fetching...")
self.progress.show()
# Run in background
self.run_in_background(
self._fetch_from_api,
priority='normal',
on_complete=self._on_fetch_complete,
on_error=self._on_fetch_error
)
def _fetch_from_api(self):
# This runs in background thread
import time
time.sleep(2) # Simulate API call
return {"items": ["A", "B", "C"], "count": 3}
def _on_fetch_complete(self, result):
# This runs in main thread - safe to update UI
self.progress.hide()
self.status_label.setText(f"Got {result['count']} items")
self.notify_success("Data Fetched", f"Retrieved {result['count']} items")
def _on_fetch_error(self, error):
self.progress.hide()
self.status_label.setText(f"Error: {error}")
self.notify_error("Fetch Failed", str(error))
```
---
## Best Practices
### 1. Always Use BasePlugin Helpers
```python
# ✅ Good - Uses helper methods
self.save_data("key", value)
data = self.load_data("key", default)
# ❌ Avoid - Direct API access when helper exists
self.api.set_data("key", value)
```
### 2. Handle Errors Gracefully
```python
def initialize(self):
try:
self.api = get_api()
self.log_info("API connected")
except Exception as e:
self.log_error(f"Failed to get API: {e}")
# Continue with limited functionality
```
### 3. Clean Up Resources
```python
def shutdown(self):
# Unsubscribe from events
self.unsubscribe_all_typed()
# Save any pending data
self.save_data("pending", self.pending_data)
super().shutdown()
```
### 4. Don't Block the Main Thread
```python
# ✅ Good - Use background tasks
def heavy_operation(self):
self.run_in_background(
self._do_heavy_work,
on_complete=self._update_ui
)
# ❌ Avoid - Blocks UI
def heavy_operation(self):
result = self._do_heavy_work() # Blocks!
self._update_ui(result)
```
### 5. Use Type Hints
```python
from typing import Optional, Dict, Any, List
from core.event_bus import LootEvent
def on_loot(self, event: LootEvent) -> None:
items: List[Dict[str, Any]] = event.items
total: float = event.total_tt_value
```
### 6. Follow Naming Conventions
```python
# ✅ Good
class LootTrackerPlugin(BasePlugin):
def on_loot_received(self):
pass
# ❌ Avoid
class lootTracker(BasePlugin):
def loot(self):
pass
```
### 7. Document Your Plugin
```python
class MyPlugin(BasePlugin):
"""
Brief description of what this plugin does.
Features:
- Feature 1
- Feature 2
Usage:
1. Step 1
2. Step 2
Hotkeys:
- Ctrl+Shift+Y: Toggle plugin
"""
```
---
## Error Handling
All APIs provide specific exceptions:
```python
from core.api import (
PluginAPIError,
ServiceNotAvailableError,
ExternalAPIError
)
try:
text = api.recognize_text((0, 0, 100, 100))
except ServiceNotAvailableError:
print("OCR not available")
except PluginAPIError as e:
print(f"API error: {e}")
```
---
## See Also
- [Plugin Development Guide](./docs/PLUGIN_DEVELOPMENT.md) - Detailed plugin tutorial
- [Architecture Overview](./ARCHITECTURE.md) - System architecture
- [Nexus API Reference](./docs/NEXUS_API_REFERENCE.md) - Entropia Nexus API
- [Contributing](./CONTRIBUTING.md) - How to contribute

533
ARCHITECTURE.md Normal file
View File

@ -0,0 +1,533 @@
# EU-Utility Architecture
> System architecture, component design, and data flow documentation
---
## System Overview
EU-Utility is built on a **modular plugin architecture** with clear separation of concerns. The system is organized into three main layers:
1. **Core Layer** - Foundation services and APIs
2. **Plugin Layer** - Extensible functionality modules
3. **UI Layer** - User interface components and overlays
```
┌─────────────────────────────────────────────────────────────────┐
│ UI Layer │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Overlay │ │ Widgets │ │ Activity Bar │ │
│ │ Window │ │ (Draggable)│ │ (Taskbar + App Drawer) │ │
│ └──────┬──────┘ └──────┬──────┘ └───────────┬─────────────┘ │
└─────────┼────────────────┼─────────────────────┼────────────────┘
│ │ │
└────────────────┴──────────┬──────────┘
┌─────────────────────────────────────┼─────────────────────────────┐
│ Plugin Layer │ │
│ ┌──────────────────────────────────┘ │
│ │ │
│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ │ Search │ │ Calculator│ │ Tracker │ │ Scanner │ │
│ │ │ Plugins │ │ Plugins │ │ Plugins │ │ Plugins │ │
│ │ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │
│ │ │ │ │ │ │
│ │ └─────────────┴──────┬──────┴─────────────┘ │
│ │ │ │
│ │ ┌────────┴────────┐ │
│ │ │ BasePlugin API │ │
│ │ │ (Base Class) │ │
│ │ └────────┬────────┘ │
│ └─────────────────────────────┼─────────────────────────────────┘
└────────────────────────────────┼─────────────────────────────────┘
┌────────────────────────────────┼─────────────────────────────────┐
│ Core Layer│ │
│ ┌─────────────────────────────┘ │
│ │ │
│ │ ┌──────────────────────────────────────────────────────┐ │
│ │ │ Three-Tier API System │ │
│ │ ├───────────────┬───────────────┬───────────────────────┤ │
│ │ │ PluginAPI │ WidgetAPI │ ExternalAPI │ │
│ │ │ (Services) │ (Overlays) │ (REST/Webhooks) │ │
│ │ └───────┬───────┴───────┬───────┴───────────┬───────────┘ │
│ │ │ │ │ │
│ │ ┌───────┴───────┐ ┌─────┴─────┐ ┌───────────┴──────────┐ │
│ │ │ Core Services │ │ Event │ │ Data Layer │ │
│ │ │ │ │ Bus │ │ │ │
│ │ │ • Window Mgr │ │ (Pub/Sub) │ │ • SQLite Store │ │
│ │ │ • OCR Service │ │ │ │ • Settings │ │
│ │ │ • Screenshot │ │ │ │ • Plugin States │ │
│ │ │ • Log Reader │ │ │ │ • Activity Log │ │
│ │ │ • Nexus API │ │ │ │ │ │
│ │ │ • HTTP Client │ │ │ │ │ │
│ │ │ • Audio │ │ │ │ │ │
│ │ └───────────────┘ └───────────┘ └──────────────────────┘ │
│ │ │
│ │ ┌──────────────────────────────────────────────────────┐ │
│ │ │ Plugin Manager │ │
│ │ │ (Discovery, Loading, Lifecycle, Dependencies) │ │
│ │ └──────────────────────────────────────────────────────┘ │
│ │ │
│ └────────────────────────────────────────────────────────────────┘
└────────────────────────────────────────────────────────────────────┘
```
---
## Core Layer
### Three-Tier API System
The Core provides a unified API organized into three tiers:
#### 1. PluginAPI
Primary interface for plugin developers to access core services.
```python
from core.api import get_api
api = get_api()
# Window Management
window = api.get_eu_window()
is_focused = api.is_eu_focused()
# OCR
if api.ocr_available():
text = api.recognize_text(region=(100, 100, 200, 50))
# Data Storage
api.set_data("key", value)
value = api.get_data("key", default)
```
**Available Services:**
- `LogReader` - Read and monitor Entropia Universe chat logs
- `WindowManager` - Detect and interact with EU window
- `OCRService` - Screen text recognition (EasyOCR/Tesseract/PaddleOCR)
- `Screenshot` - Screen capture with multiple backends
- `NexusAPI` - Entropia Nexus item database integration
- `HTTPClient` - Cached web requests with rate limiting
- `Audio` - Sound playback and volume control
- `Clipboard` - Cross-platform copy/paste
- `Notifications` - Desktop toast notifications
- `EventBus` - Pub/sub event system
- `DataStore` - Persistent key-value storage
- `TaskManager` - Background task execution
#### 2. WidgetAPI
Manages overlay widgets - floating UI components.
```python
from core.api import get_widget_api
widget_api = get_widget_api()
# Create widget
widget = widget_api.create_widget(
name="loot_tracker",
title="Loot Tracker",
size=(400, 300)
)
widget.show()
```
**Features:**
- Draggable overlay widgets
- Size and position persistence
- Opacity control
- Lock/unlock positioning
- Layout helpers (grid, cascade)
#### 3. ExternalAPI
Provides REST endpoints and webhook support for third-party integrations.
```python
from core.api import get_external_api
ext = get_external_api()
ext.start_server(port=8080)
# Register endpoint
@ext.endpoint("stats", methods=["GET"])
def get_stats():
return {"kills": 100, "loot": "50 PED"}
```
**Features:**
- REST API server with CORS
- Incoming/outgoing webhooks
- HMAC authentication
- IPC (Inter-Process Communication)
- Server-Sent Events (SSE)
---
## Plugin Layer
### Plugin Architecture
All plugins inherit from `BasePlugin` and implement a standard interface:
```
┌─────────────────────────────────────────┐
│ BasePlugin (Abstract) │
├─────────────────────────────────────────┤
│ Metadata: name, version, author, etc. │
├─────────────────────────────────────────┤
│ initialize() → Setup plugin │
│ get_ui() → Return QWidget │
│ on_show() → Overlay visible │
│ on_hide() → Overlay hidden │
│ on_hotkey() → Hotkey pressed │
│ shutdown() → Cleanup resources │
├─────────────────────────────────────────┤
│ Helper Methods: │
│ • OCR capture │
│ • Screenshot │
│ • Log reading │
│ • Data storage │
│ • Event publishing/subscribing │
│ • Nexus API access │
│ • Background tasks │
│ • Notifications │
└─────────────────────────────────────────┘
┌───────────┼───────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Search │ │ Tracker │ │Utility │
│ Plugins │ │ Plugins │ │ Plugins │
└─────────┘ └─────────┘ └─────────┘
```
### Plugin Categories
| Category | Purpose | Examples |
|----------|---------|----------|
| **Search** | Data lookup | Universal Search, Nexus Search, TP Runner |
| **Trackers** | Progress monitoring | Loot, Skills, Codex, Missions, Globals |
| **Calculators** | Game math | DPP, Crafting, Enhancer, Markup |
| **Scanners** | OCR-based tools | Game Reader, Skill Scanner |
| **Integration** | External services | Spotify Controller, Chat Logger |
| **System** | Core functionality | Dashboard, Settings, Plugin Store |
### Plugin Lifecycle
```
Discovery
┌──────────┐
│ Import │
└────┬─────┘
┌──────────┐ No ┌──────────┐
│ Enabled? │───────────▶│ Skip │
└────┬─────┘ └──────────┘
│ Yes
┌──────────┐
│ Load │
│ Module │
└────┬─────┘
┌──────────┐ Error ┌──────────┐
│Initialize│───────────▶│ Disable │
└────┬─────┘ └──────────┘
│ Success
┌──────────┐
│ Running │◀───────┐
└────┬─────┘ │
│ │
▼ │
┌──────────┐ │
│ Hotkey │ │
│ Pressed │────────┘
└──────────┘
┌──────────┐
│ Shutdown │
└──────────┘
```
---
## Event System
### Event Bus Architecture
The Event Bus provides typed, filterable pub/sub communication:
```
┌─────────────────────────────────────────────────┐
│ Event Bus │
├─────────────────────────────────────────────────┤
│ │
│ Publisher ─────┐ │
│ │ │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ Event Router │ │
│ │ (Type + Filter Match) │ │
│ └────────────┬────────────┘ │
│ │ │
│ ┌───────────┼───────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ Sub │ │ Sub │ │ Sub │ │
│ │ #1 │ │ #2 │ │ #3 │ │
│ └──────┘ └──────┘ └──────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ Plugin A Plugin B Plugin C │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ Event Types: │ │
│ │ • SkillGainEvent │ │
│ │ • LootEvent │ │
│ │ • DamageEvent │ │
│ │ • GlobalEvent │ │
│ │ • ChatEvent │ │
│ │ • EconomyEvent │ │
│ │ • SystemEvent │ │
│ └────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
```
### Event Flow Example
```python
# Plugin A publishes loot event
from core.event_bus import LootEvent
self.publish_typed(LootEvent(
mob_name="Daikiba",
items=[{"name": "Animal Oil", "value": 0.05}],
total_tt_value=0.05
))
# Plugin B subscribes with filter
self.subscribe_typed(
LootEvent,
on_loot,
mob_types=["Daikiba", "Atrox"] # Filter
)
```
---
## Data Flow
### Data Layer Architecture
```
┌─────────────────────────────────────────────────────────┐
│ Data Layer │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ SQLite Data Store │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │user_prefs │ │plugin_states│ │ │
│ │ ├─────────────┤ ├─────────────┤ │ │
│ │ │• settings │ │• enabled │ │ │
│ │ │• hotkeys │ │• version │ │ │
│ │ │• theme │ │• settings │ │ │
│ │ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │sessions │ │activity_log │ │ │
│ │ ├─────────────┤ ├─────────────┤ │ │
│ │ │• start_time │ │• timestamp │ │ │
│ │ │• end_time │ │• category │ │ │
│ │ │• stats │ │• action │ │ │
│ │ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │dashboard │ │hotkeys │ │ │
│ │ │_widgets │ │ │ │ │
│ │ ├─────────────┤ ├─────────────┤ │ │
│ │ │• position │ │• key_combo │ │ │
│ │ │• size │ │• action │ │ │
│ │ │• config │ │• plugin_id │ │ │
│ │ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Data Access Pattern │ │
│ │ │ │
│ │ Plugin ──▶ API ──▶ SQLiteStore ──▶ SQLite DB │ │
│ │ │ │
│ │ All data is JSON-serialized for flexibility │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
```
---
## UI Architecture
### Overlay System
```
┌─────────────────────────────────────────────────────────────┐
│ Desktop │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Main Overlay Window │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ Plugin Tab Bar │ │ │
│ │ │ [Dash][Search][Calc][Track][...] │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ │ ┌─────────┬──────────────────────────────────┐ │ │
│ │ │ Plugin │ │ │ │
│ │ │ List │ Active Plugin Content │ │ │
│ │ │ │ │ │ │
│ │ │ • Search│ ┌──────────────────────┐ │ │ │
│ │ │ • Calc │ │ │ │ │ │
│ │ │ • Track │ │ Plugin UI Here │ │ │ │
│ │ │ • ... │ │ │ │ │ │
│ │ │ │ └──────────────────────┘ │ │ │
│ │ └─────────┴──────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ Quick Actions Bar │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────┼─────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Floating │ │ Widget │ │ Activity │ │
│ │ Icon │ │ (Loot) │ │ Bar │ │
│ │ (Draggable)│ │ (Draggable)│ │(Bottom/Top) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
### Component Hierarchy
```
QApplication
└── EUUtilityApp
├── MainOverlayWindow
│ ├── PluginTabBar
│ ├── PluginSidebar
│ ├── PluginContentArea
│ └── QuickActionsBar
├── FloatingIcon (always on top)
├── ActivityBar (optional, bottom/top)
│ ├── StartButton (opens AppDrawer)
│ ├── PinnedPlugins
│ ├── SearchBox
│ └── Clock/Settings
├── WidgetOverlay
│ └── Individual Widgets
└── TrayIcon
└── ContextMenu
```
---
## Security Architecture
### Data Protection
```
┌─────────────────────────────────────────┐
│ Security Layer │
├─────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────┐ │
│ │ Input Sanitization │ │
│ │ • Path traversal protection │ │
│ │ • SQL injection prevention │ │
│ │ • XSS protection │ │
│ └─────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────┐ │
│ │ Data Encryption │ │
│ │ • AES-256 for sensitive data │ │
│ │ • Secure key storage │ │
│ │ • Encrypted settings │ │
│ └─────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────┐ │
│ │ File Operations │ │
│ │ • Atomic writes (temp files) │ │
│ │ • Proper file permissions │ │
│ │ • Safe temp file handling │ │
│ └─────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────┐ │
│ │ Plugin Isolation │ │
│ │ • Try/except around plugin ops │ │
│ │ • Separate error handling │ │
│ │ • Resource cleanup on crash │ │
│ └─────────────────────────────────┘ │
│ │
└─────────────────────────────────────────┘
```
---
## Performance Optimizations
### Caching Strategy
| Layer | Cache Type | TTL | Purpose |
|-------|------------|-----|---------|
| HTTP Client | Response cache | Configurable | API responses |
| Nexus API | Item data | 5 minutes | Item/market data |
| Icon Manager | Loaded icons | Session | UI performance |
| Plugin Manager | Loaded plugins | Session | Startup time |
| Settings | Config values | Session | Fast access |
### Resource Management
```python
# Lazy initialization pattern
class ExpensiveService:
def __init__(self):
self._initialized = False
self._resource = None
@property
def resource(self):
if not self._initialized:
self._resource = self._create_expensive_resource()
self._initialized = True
return self._resource
```
---
## Technology Stack
| Component | Technology |
|-----------|------------|
| **Language** | Python 3.11+ |
| **UI Framework** | PyQt6 |
| **Database** | SQLite |
| **HTTP Client** | `requests` with caching |
| **OCR** | EasyOCR / Tesseract / PaddleOCR |
| **Screenshot** | PIL / MSS / Win32 (Windows) |
| **Audio** | pygame.mixer / simpleaudio |
| **Task Queue** | ThreadPoolExecutor |
---
## See Also
- [API Reference](./API.md) - Complete API documentation
- [Plugin Development](./docs/PLUGIN_DEVELOPMENT.md) - Creating plugins
- [Contributing](./CONTRIBUTING.md) - Contribution guidelines

View File

@ -1,229 +0,0 @@
# EU-Utility Bug Fix Report
## Summary
This document details all bugs, errors, and issues fixed in the EU-Utility codebase during the bug hunting session.
---
## Fixed Issues
### 1. **Missing QAction Import in activity_bar.py**
**File:** `core/activity_bar.py`
**Line:** 5
**Issue:** `QAction` was used in `_show_context_menu()` method but not imported from `PyQt6.QtGui`.
**Fix:** Added `QAction` to the imports from `PyQt6.QtGui`.
```python
# Before:
from PyQt6.QtGui import QMouseEvent, QPainter, QColor, QFont, QIcon, QPixmap
# After:
from PyQt6.QtGui import QMouseEvent, QPainter, QColor, QFont, QIcon, QPixmap, QAction
```
---
### 2. **Invalid QPropertyAnimation Property (windowOpacity) in perfect_ux.py**
**File:** `core/perfect_ux.py`
**Line:** 904-930
**Issue:** The `_animate_transition()` method used `b"windowOpacity"` as a property for QPropertyAnimation, but `windowOpacity` is not a valid animatable property on QWidget in Qt6. This would cause runtime errors when switching views.
**Fix:** Added `QGraphicsOpacityEffect` and modified the animation to animate the `opacity` property of the effect instead of the widget directly.
```python
# Before:
fade_out = QPropertyAnimation(current, b"windowOpacity")
# After:
current._opacity_effect = QGraphicsOpacityEffect(current)
current.setGraphicsEffect(current._opacity_effect)
fade_out = QPropertyAnimation(current._opacity_effect, b"opacity")
```
---
### 3. **Invalid QPropertyAnimation Property (windowOpacity) in overlay_window.py**
**File:** `core/overlay_window.py`
**Line:** 527-540
**Issue:** Same issue as above - `windowOpacity` property cannot be animated directly on QWidget in Qt6.
**Fix:** Created a `QGraphicsOpacityEffect` for the window and animated its `opacity` property.
---
### 4. **Missing show()/hide() Methods in TrayIcon**
**File:** `core/tray_icon.py`
**Line:** 61-79
**Issue:** The `TrayIcon` class inherited from `QWidget` but didn't implement `show()` and `hide()` methods that delegate to the internal `QSystemTrayIcon`. Other code expected these methods to exist.
**Fix:** Added `show()`, `hide()`, and `isVisible()` methods that properly delegate to the internal tray icon.
```python
def show(self):
"""Show the tray icon."""
if self.tray_icon:
self.tray_icon.show()
def hide(self):
"""Hide the tray icon."""
if self.tray_icon:
self.tray_icon.hide()
def isVisible(self):
"""Check if tray icon is visible."""
return self.tray_icon.isVisible() if self.tray_icon else False
```
---
### 5. **Qt6 AA_EnableHighDpiScaling Deprecation Warning**
**File:** `core/main.py`
**Line:** 81-86
**Issue:** The `Qt.AA_EnableHighDpiScaling` attribute is deprecated in Qt6 and always enabled by default. While the existing code didn't cause errors due to the `hasattr` check, it was unnecessary.
**Fix:** Added proper try/except handling and comments explaining the Qt6 compatibility.
```python
# Enable high DPI scaling (Qt6 has this enabled by default)
# This block is kept for backwards compatibility with Qt5 if ever needed
if hasattr(Qt, 'AA_EnableHighDpiScaling'):
try:
self.app.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling)
except (AttributeError, TypeError):
pass # Qt6+ doesn't need this
```
---
### 6. **Unsafe Attribute Access in Activity Bar**
**File:** `core/activity_bar.py`
**Lines:** Multiple locations
**Issue:** Various methods accessed `plugin_class.name`, `self.drawer`, and other attributes without checking if they exist first. This could cause `AttributeError` exceptions.
**Fix:** Added `getattr()` calls with default values throughout:
```python
# Before:
plugin_name = plugin_class.name
# After:
plugin_name = getattr(plugin_class, 'name', plugin_id)
```
Also added `hasattr()` checks for `self.drawer` before accessing it.
---
### 7. **Missing Error Handling in Activity Bar Initialization**
**File:** `core/main.py`
**Line:** 127-139
**Issue:** Activity Bar initialization was not wrapped in try/except, so any error during creation would crash the entire application.
**Fix:** Wrapped the activity bar creation and initialization in a try/except block with proper error messages.
```python
try:
from core.activity_bar import get_activity_bar
self.activity_bar = get_activity_bar(self.plugin_manager)
if self.activity_bar:
if self.activity_bar.config.enabled:
# ... setup code ...
else:
print("[Core] Activity Bar disabled in config")
else:
print("[Core] Activity Bar not available")
self.activity_bar = None
except Exception as e:
print(f"[Core] Failed to create Activity Bar: {e}")
self.activity_bar = None
```
---
### 8. **Missing Error Handling in EU Focus Detection**
**File:** `core/main.py`
**Line:** 405-450
**Issue:** The `_check_eu_focus()` method had unsafe attribute access and could fail if `window_manager` or `activity_bar` were not properly initialized.
**Fix:** Added comprehensive error handling with `hasattr()` checks and try/except blocks around all UI operations.
---
### 9. **Unsafe Attribute Access in Plugin Manager**
**File:** `core/plugin_manager.py`
**Lines:** Multiple locations
**Issue:** Plugin loading code accessed `plugin_class.name` and `plugin_class.__name__` without checking if these attributes exist, and didn't handle cases where plugin classes might be malformed.
**Fix:** Added safe attribute access with `getattr()` and `hasattr()` checks throughout the plugin loading pipeline.
```python
# Before:
print(f"[PluginManager] Skipping disabled plugin: {plugin_class.name}")
# After:
plugin_name = getattr(plugin_class, 'name', plugin_class.__name__ if hasattr(plugin_class, '__name__') else 'Unknown')
print(f"[PluginManager] Skipping disabled plugin: {plugin_name}")
```
---
### 10. **Missing Error Handling in _toggle_activity_bar**
**File:** `core/main.py`
**Line:** 390-403
**Issue:** The `_toggle_activity_bar()` method didn't check if `activity_bar` and `tray_icon` exist before calling methods on them.
**Fix:** Added `hasattr()` checks and try/except blocks.
```python
def _toggle_activity_bar(self):
if hasattr(self, 'activity_bar') and self.activity_bar:
try:
if self.activity_bar.isVisible():
self.activity_bar.hide()
if hasattr(self, 'tray_icon') and self.tray_icon:
self.tray_icon.set_activity_bar_checked(False)
# ...
```
---
### 11. **Missing Error Handling in Drawer Methods**
**File:** `core/activity_bar.py`
**Lines:** 269-275, 373-377
**Issue:** The `_toggle_drawer()` and `_on_drawer_item_clicked()` methods didn't have error handling for drawer operations.
**Fix:** Added try/except blocks with error logging.
---
## Testing Recommendations
After applying these fixes, test the following critical paths:
1. **App Startup**
- Launch the application
- Verify no import errors occur
- Check that the dashboard opens correctly
2. **Dashboard Navigation**
- Click through all navigation items (Dashboard, Plugins, Widgets, Settings)
- Verify view transitions work without errors
3. **Activity Bar**
- Toggle activity bar visibility from tray menu
- Click on pinned plugins
- Open the drawer and click on plugins
- Test auto-hide functionality
4. **Tray Icon**
- Right-click tray icon to open menu
- Click "Dashboard" to toggle visibility
- Click "Quit" to exit the application
5. **Plugin Loading**
- Enable/disable plugins
- Verify plugins load without errors
- Check plugin UI displays correctly
---
## Summary
All identified bugs have been fixed. The codebase now has:
- ✅ Proper Qt6 imports
- ✅ Safe attribute access throughout
- ✅ Comprehensive error handling
- ✅ Graceful degradation when services are unavailable
- ✅ No runtime errors in critical paths
The application should now be stable and ready for use.

View File

@ -1,117 +0,0 @@
# EU-Utility Bug Fix Report
## Summary
- **Total Plugins Checked:** 24
- **Core Files Checked:** 39
- **Syntax Errors Found:** 0
- **Bugs Fixed:** 0 (No bugs were found - codebase is clean!)
- **Windows Compatibility Issues:** 0 (Properly handled)
## Detailed Analysis
### ✅ Syntax Check Results
All Python files compile successfully without syntax errors:
- All 24 plugins parse correctly
- All 39 core modules parse correctly
- No `SyntaxError` or `IndentationError` issues found
### ✅ Import Analysis
All imports are properly structured:
- No circular import issues detected
- All cross-module imports resolve correctly
- Optional dependencies have proper fallback handling
### ✅ Windows Compatibility Check
#### fcntl Module Handling
The `fcntl` module (Unix-only) is properly handled in:
- `core/data_store.py` - Lines 15-22: Wrapped in try/except with portalocker fallback
- `core/data_store_secure.py` - Lines 16-25: Wrapped in try/except with portalocker fallback
Both files implement cross-platform file locking with graceful fallback to threading locks.
#### Windows-Specific Code
All Windows-specific code is properly guarded:
- `core/window_manager.py` - Uses `IS_WINDOWS` and `WINDOWS_AVAILABLE` flags (lines 15, 18-26)
- `core/screenshot.py` - `capture_window()` method checks `self._platform != "windows"` before using win32gui
- `core/screenshot_secure.py` - Same platform checking as screenshot.py
- `plugins/spotify_controller/plugin.py` - Uses `platform.system()` checks before Windows-specific code
### ✅ Plugin Loading Verification
All 24 plugins load without errors:
1. ✅ auction_tracker
2. ✅ calculator
3. ✅ chat_logger
4. ✅ codex_tracker
5. ✅ crafting_calc
6. ✅ dashboard
7. ✅ dpp_calculator
8. ✅ enhancer_calc
9. ✅ event_bus_example
10. ✅ game_reader
11. ✅ global_tracker
12. ✅ inventory_manager
13. ✅ loot_tracker
14. ✅ mining_helper
15. ✅ mission_tracker
16. ✅ nexus_search
17. ✅ plugin_store_ui
18. ✅ profession_scanner
19. ✅ session_exporter
20. ✅ settings
21. ✅ skill_scanner
22. ✅ spotify_controller
23. ✅ tp_runner
24. ✅ universal_search
### ✅ Core Services Verified
All core services properly implemented:
- `main.py` - Entry point with proper dependency checking
- `plugin_manager.py` - Plugin discovery and lifecycle management
- `plugin_api.py` - Cross-plugin communication API
- `event_bus.py` - Typed event system with filtering
- `window_manager.py` - Windows window management with Linux fallback
- `screenshot.py` - Cross-platform screenshot capture
- `ocr_service.py` - OCR with multiple backend support
- `log_reader.py` - Game log monitoring
- `nexus_api.py` - Entropia Nexus API client
- `http_client.py` - Cached HTTP client
- `audio.py` - Cross-platform audio playback
- `clipboard.py` - Clipboard manager
- `tasks.py` - Background task execution
- `data_store.py` - Persistent data storage
- `security_utils.py` - Input validation and sanitization
### ✅ Code Quality Observations
#### Positive Findings:
1. **Proper Exception Handling:** All optional dependencies use try/except blocks
2. **Platform Detection:** `sys.platform` and `platform.system()` used correctly
3. **Singleton Pattern:** Properly implemented with thread-safety
4. **Type Hints:** Good use of type annotations throughout
5. **Documentation:** Comprehensive docstrings in all modules
#### Security Considerations (Already Implemented):
1. **Path Traversal Protection:** `data_store_secure.py` validates paths
2. **Input Sanitization:** `security_utils.py` provides validation functions
3. **Safe File Operations:** Atomic writes with temp files
## Files Noted but Not Issues:
### Vulnerable Test Files (Not Used in Production):
- `core/screenshot_vulnerable.py` - Not imported by any production code
- `core/data_store_vulnerable.py` - Not imported by any production code
These appear to be intentionally vulnerable versions for security testing/education.
## Conclusion
**The EU-Utility codebase is well-structured and bug-free.** All potential issues that were checked for:
- ❌ Syntax errors - None found
- ❌ Missing imports - None found
- ❌ Undefined variables - None found
- ❌ Windows compatibility issues - Properly handled
- ❌ fcntl/Unix-specific problems - Properly guarded
- ❌ Plugin loading failures - All 24 plugins load correctly
The codebase demonstrates good software engineering practices with proper error handling, cross-platform compatibility, and clean architecture.

View File

@ -7,46 +7,97 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
---
## [Unreleased]
### Added
- Comprehensive documentation overhaul with new API.md and ARCHITECTURE.md
- Consolidated plugin development guides into single reference
---
## [2.1.0] - 2026-02-16
### Added
- **Enhanced Dashboard Widgets** - Modular, draggable widgets with real-time data
- SystemStatusWidget - Monitor CPU, RAM, Disk usage
- QuickActionsWidget - One-click access to common actions
- RecentActivityWidget - Display recent system and plugin activity
- PluginGridWidget - Visual plugin status display
- **Activity Bar** - Windows 11-style taskbar with pinned plugins
- Start button with app drawer
- Drag-to-pin functionality
- Search box for quick plugin access
- Configurable auto-hide and position (top/bottom)
- **Plugin Store UI** - Browse, install, and manage community plugins
- **Widget Gallery** - Interface for adding and configuring dashboard widgets
- **SQLite Data Layer** - Thread-safe persistent storage for settings and states
- **Enhanced Settings Panel** - Full-featured settings with 6 organized tabs
- General settings
- Appearance (themes, colors, opacity)
- Plugin management
- Hotkey configuration
- Data & backup
- About section
### Changed
- Improved UI responsiveness with animation batching
- Enhanced plugin loading with caching and error recovery
- Better error messages throughout the application
### Fixed
- Plugin loading race conditions during startup
- Memory leaks in overlay system
- Hotkey conflicts with some applications
- Settings persistence issues
- Activity bar visibility on multi-monitor setups
### Security
- Added path traversal protection in data store
- Secure file permissions for sensitive data
- Improved input sanitization
---
## [2.0.0] - 2025-02-14
### 🎉 Major Release
This is a major release introducing the enhanced plugin architecture, improved UI, and comprehensive documentation.
### ✨ Added
### Added
#### Core Enhancements
- **Enhanced Plugin Manager** - Optimized plugin loading with caching and error recovery
- **Event Bus System** - Typed event system with filtering and persistence
- **Three-Tier API System** - PluginAPI, WidgetAPI, and ExternalAPI
- **Typed Event Bus** - Type-safe event system with filtering and persistence
- **Task Manager** - Thread pool-based background task execution
- **Secure Data Store** - Encrypted data storage for sensitive information
- **Performance Optimizations** - Caching, lazy loading, and resource pooling
- **UI Optimizations** - Responsive design, animation batching, style caching
- **Icon Manager** - Unified icon system with caching and scaling
#### New Plugins
- **Auction Tracker** - Track prices, markups, and market trends
- **Chat Logger** - Log, search, and filter chat messages with advanced features
- **Codex Tracker** - Track creature challenges and codex progress
- **Crafting Calculator** - Calculate crafting success rates and material costs
#### New Plugins (25 Total)
- **Dashboard** - Customizable start page with avatar statistics
- **DPP Calculator** - Calculate Damage Per PEC and weapon efficiency
- **Enhancer Calculator** - Calculate enhancer break rates and costs
- **Event Bus Example** - Demonstrates Enhanced Event Bus features
- **Game Reader** - OCR scanner for in-game menus and text
- **Global Tracker** - Track globals, HOFs, and ATHs
- **Inventory Manager** - Track items, TT value, and weight
- **Loot Tracker** - Track hunting loot with stats and ROI analysis
- **Mining Helper** - Track mining finds, claims, and hotspots
- **Mission Tracker** - Track missions, challenges, and objectives
- **Nexus Search** - Search items, users, and market data via Nexus API
- **Plugin Store UI** - Community plugin marketplace interface
- **Profession Scanner** - Track profession ranks and progress
- **Settings** - Configure EU-Utility preferences
- **Skill Scanner** - Uses core OCR and Log services for skill tracking
- **Spotify Controller** - Control Spotify and view current track info
- **TP Runner** - Teleporter locations and route planner
- **Universal Search** - Search items, mobs, locations, blueprints, and more
- **Nexus Search** - Search items and market data via Nexus API
- **DPP Calculator** - Calculate Damage Per PEC and weapon efficiency
- **Crafting Calculator** - Calculate crafting success rates and material costs
- **Enhancer Calculator** - Calculate enhancer break rates and costs
- **Loot Tracker** - Track hunting loot with stats and ROI analysis
- **Skill Scanner** - OCR-based skill tracking
- **Codex Tracker** - Track creature challenges and codex progress
- **Mission Tracker** - Track missions, challenges, and objectives
- **Global Tracker** - Track globals, HOFs, and ATHs
- **Mining Helper** - Track mining finds, claims, and hotspots
- **Auction Tracker** - Track prices, markups, and market trends
- **Inventory Manager** - Track items, TT value, and weight
- **Profession Scanner** - Track profession ranks and progress
- **Game Reader** - OCR scanner for in-game menus and text
- **Chat Logger** - Log, search, and filter chat messages
- **TP Runner** - Teleporter locations and route planner
- **Spotify Controller** - Control Spotify and view current track info
- **Settings** - Configure EU-Utility preferences
- **Plugin Store UI** - Community plugin marketplace interface
- **Event Bus Example** - Demonstrates Enhanced Event Bus features
#### API & Services
- **Nexus API Integration** - Full integration with Entropia Nexus API
@ -94,24 +145,21 @@ This is a major release introducing the enhanced plugin architecture, improved U
- **Security Hardening Guide** - Security best practices
- **Nexus API Reference** - Nexus integration documentation
### 🔧 Changed
### Changed
- **Plugin Architecture** - Complete rewrite for better modularity
- **BasePlugin API** - Enhanced with more convenience methods
- **Settings System** - More robust with encryption support
- **Data Store** - Improved with backup and recovery
- **Hotkey System** - More reliable global hotkey handling
### ⚡ Improved
### Improved
- **Startup Time** - Faster plugin loading with caching
- **Memory Usage** - Optimized resource management
- **UI Responsiveness** - Smoother interface interactions
- **Error Handling** - Better error reporting and recovery
- **Plugin Isolation** - Improved crash isolation between plugins
### 🐛 Fixed
### Fixed
- Plugin loading race conditions
- Memory leaks in overlay system
- Hotkey conflicts with other applications
@ -119,29 +167,35 @@ This is a major release introducing the enhanced plugin architecture, improved U
- Data corruption during concurrent writes
- Settings not persisting properly
### 🔒 Security
### Security
- Added data encryption for sensitive settings
- Implemented secure file permissions
- Added input sanitization
- Secure temporary file handling
### Removed
- Legacy plugin API (replaced with new three-tier system)
- Old settings storage format (migrated to encrypted storage)
---
## [1.1.0] - 2025-01-15
### Added
### Added
- Spotify controller plugin
- Skill scanner with OCR
- Basic loot tracking
- Skill scanner with OCR support
- Basic loot tracking functionality
- Nexus search integration improvements
### 🔧 Changed
- Improved overlay styling
- Better error messages
### Changed
- Improved overlay styling with new theme
- Better error messages throughout
- Enhanced calculator precision
### 🐛 Fixed
### Fixed
- Hotkey registration on Windows 11
- Memory leak in screenshot service
- Window detection edge cases
---
@ -151,36 +205,39 @@ This is a major release introducing the enhanced plugin architecture, improved U
First public release of EU-Utility.
### Added
- Basic plugin system
### Added
- Basic plugin system with BasePlugin class
- Nexus search integration
- DPP calculator
- Simple overlay UI
- Global hotkey support
- Floating icon for quick access
---
## Release Notes Template
When creating a new release, use this template:
```markdown
## [X.Y.Z] - YYYY-MM-DD
### Added
### Added
- New features
### 🔧 Changed
### Changed
- Changes to existing functionality
### 🗑️ Deprecated
### Deprecated
- Soon-to-be removed features
### 🗑️ Removed
### Removed
- Now removed features
### 🐛 Fixed
### Fixed
- Bug fixes
### 🔒 Security
### Security
- Security improvements
```
@ -188,29 +245,32 @@ First public release of EU-Utility.
## Future Roadmap
### Planned for 2.1.0
### Planned for 2.2.0
- [ ] Discord Rich Presence integration
- [ ] Advanced crafting calculator with blueprint data
- [ ] Hunting efficiency analyzer
- [ ] Export to spreadsheet functionality
- [ ] Plugin auto-updater
- [ ] Dark/Light theme toggle
### Planned for 2.2.0
### Planned for 2.3.0
- [ ] Web-based dashboard
- [ ] Mobile companion app
- [ ] Cloud sync for settings
- [ ] Advanced analytics
- [ ] Community plugin repository
- [ ] Multi-language support
### Planned for 3.0.0
- [ ] Full plugin SDK
- [ ] Plugin marketplace
- [ ] Theme system
- [ ] Scripting support
- [ ] Theme system with custom CSS
- [ ] Scripting support (Python/Lua)
- [ ] Multi-account support
---
[Unreleased]: https://github.com/ImpulsiveFPS/EU-Utility/compare/v2.1.0...HEAD
[2.1.0]: https://github.com/ImpulsiveFPS/EU-Utility/releases/tag/v2.1.0
[2.0.0]: https://github.com/ImpulsiveFPS/EU-Utility/releases/tag/v2.0.0
[1.1.0]: https://github.com/ImpulsiveFPS/EU-Utility/releases/tag/v1.1.0
[1.0.0]: https://github.com/ImpulsiveFPS/EU-Utility/releases/tag/v1.0.0

View File

@ -1,197 +0,0 @@
# EU-Utility Code Cleanup Summary
## Overview
This document summarizes the code cleanup and refactoring performed on the EU-Utility codebase to improve code quality, maintainability, and type safety.
## Changes Made
### 1. Core Module (`core/`)
#### `__init__.py`
- Added comprehensive module-level docstring
- Updated exports with proper type annotations
- Added version constants (VERSION, API_VERSION)
- Organized imports by category
#### `base_plugin.py`
- Added comprehensive docstrings to all methods
- Added type hints to all methods and attributes
- Fixed return type annotations
- Improved method documentation with Args/Returns/Examples
- Maintained backward compatibility
#### `event_bus.py`
- Added module-level docstring with usage examples
- Added type hints to all classes and methods
- Fixed generic type annotations (TypeVar usage)
- Documented all event types with attributes
- Added comprehensive class and method docstrings
#### `settings.py`
- Added module-level documentation
- Added type hints throughout
- Added proper handling for Qt/non-Qt environments
- Documented all methods with Args/Returns
- Added DEFAULTS constant documentation
### 2. Plugin API (`core/api/`)
#### `__init__.py`
- Added comprehensive package documentation
- Organized exports by API tier
- Added version information
- Documented the three-tier API architecture
#### `plugin_api.py`
- Already well-documented
- Maintained backward compatibility
- Added to __all__ exports
### 3. Plugins Package (`plugins/`)
#### `__init__.py`
- Added comprehensive docstring
- Documented plugin structure
- Added usage example
- Linked to documentation
#### `base_plugin.py`
- Simplified to re-export only
- Added deprecation note for preferring core import
### 4. Documentation
#### `core/README.md` (New)
- Created comprehensive module documentation
- Documented module structure
- Added usage examples for all key components
- Created service architecture overview
- Added best practices section
- Included version history
## Code Quality Improvements
### Type Hints
- Added to all public methods
- Used proper generic types where appropriate
- Fixed Optional[] annotations
- Added return type annotations
### Documentation
- All modules have comprehensive docstrings
- All public methods documented with Args/Returns/Examples
- Added module-level usage examples
- Created README for core module
### Organization
- Consistent file structure
- Clear separation of concerns
- Proper import organization
- Removed dead code paths
### Standards Compliance
- PEP 8 formatting throughout
- Consistent naming conventions (snake_case)
- Proper import ordering (stdlib, third-party, local)
- Type-safe default values
## Backward Compatibility
All changes maintain full backward compatibility:
- No public API changes
- Existing plugins continue to work
- Re-exports maintained for compatibility
- Deprecation notes added where appropriate
## Files Modified
### Core Module
- `core/__init__.py` - Updated exports and documentation
- `core/base_plugin.py` - Added type hints and docs
- `core/event_bus.py` - Added type hints and docs
- `core/settings.py` - Added type hints and docs
### API Module
- `core/api/__init__.py` - Added documentation
### Plugin Package
- `plugins/__init__.py` - Added documentation
- `plugins/base_plugin.py` - Simplified re-export
### Documentation
- `core/README.md` - Created comprehensive guide
## Verification
To verify the cleanup:
1. **Type checking** (if mypy available):
```bash
mypy core/ plugins/
```
2. **Import tests**:
```python
from core import get_event_bus, get_nexus_api
from core.base_plugin import BasePlugin
from core.api import get_api
from plugins import BasePlugin as PluginBase
```
3. **Documentation generation**:
```bash
pydoc core.base_plugin
pydoc core.event_bus
pydoc core.settings
```
## Recommendations for Future Work
1. **Add more type hints** to remaining core modules:
- `nexus_api.py`
- `http_client.py`
- `data_store.py`
- `log_reader.py`
2. **Create tests** for core functionality:
- Unit tests for EventBus
- Unit tests for Settings
- Mock tests for BasePlugin
3. **Add more documentation**:
- API usage guides
- Plugin development tutorials
- Architecture decision records
4. **Code cleanup** for remaining modules:
- Consolidate duplicate code
- Remove unused imports
- Optimize performance where needed
## Performance Notes
The cleanup focused on documentation and type safety without affecting runtime performance:
- No algorithmic changes
- Type hints are ignored at runtime
- Import structure maintained for lazy loading
## Security Considerations
- No security-sensitive code was modified
- Input validation preserved
- Security utilities in `security_utils.py` not affected
## Summary
The codebase is now:
- ✅ Better documented with comprehensive docstrings
- ✅ Type-hinted for better IDE support and type checking
- ✅ Organized with clear module structure
- ✅ Standards-compliant (PEP 8)
- ✅ Fully backward compatible
- ✅ Ready for future development
Total files modified: 8
Lines of documentation added: ~500+
Type hints added: ~200+

View File

@ -1,442 +0,0 @@
# EU-Utility Core Functionality
This document describes the core functionality implemented for EU-Utility v2.1.0.
## Overview
The core functionality provides a complete, working foundation for EU-Utility including:
1. **Dashboard Widgets** - Modular, draggable widgets with real-time data
2. **Widget Gallery** - Interface for adding and configuring widgets
3. **Plugin Store** - Browse, install, and manage plugins
4. **Settings Panel** - Full-featured settings with persistence
5. **Activity Bar** - Windows 11-style taskbar with pinned plugins
6. **Data Layer** - SQLite-based persistent storage
## 1. Dashboard Widgets
### Implemented Widgets
#### SystemStatusWidget
- **Purpose**: Monitor system resources (CPU, RAM, Disk)
- **Features**:
- Real-time resource monitoring via `psutil`
- Service status indicators
- Auto-updating progress bars
- Configurable update intervals
- **Size**: 2 columns x 1 row
- **Persistence**: Settings saved to SQLite
```python
from core.widgets import SystemStatusWidget
widget = SystemStatusWidget(parent)
widget.set_service("Overlay", True) # Update service status
```
#### QuickActionsWidget
- **Purpose**: One-click access to common actions
- **Features**:
- Configurable action buttons
- Icon support via icon_manager
- Action signal emission
- Activity logging
- **Size**: 2 columns x 1 row
- **Default Actions**: Search, Screenshot, Settings, Plugins
```python
from core.widgets import QuickActionsWidget
widget = QuickActionsWidget(parent)
widget.set_actions([
{'id': 'custom', 'name': 'Custom Action', 'icon': 'star'},
])
widget.action_triggered.connect(handle_action)
```
#### RecentActivityWidget
- **Purpose**: Display recent system and plugin activity
- **Features**:
- Auto-refresh from SQLite activity log
- Timestamp display
- Category icons
- Scrollable list
- **Size**: 1 column x 2 rows
- **Data Source**: `activity_log` table in SQLite
```python
from core.widgets import RecentActivityWidget
widget = RecentActivityWidget(parent)
# Auto-refreshes every 5 seconds
```
#### PluginGridWidget
- **Purpose**: Display installed plugins with status
- **Features**:
- Real-time plugin status
- Click to select
- Loaded/enabled indicators
- Scrollable grid layout
- **Size**: 2 columns x 2 rows
```python
from core.widgets import PluginGridWidget
widget = PluginGridWidget(plugin_manager, parent)
widget.plugin_clicked.connect(on_plugin_selected)
```
### Widget Base Class
All widgets inherit from `DashboardWidget`:
```python
class DashboardWidget(QFrame):
name = "Widget"
description = "Base widget"
icon_name = "target"
size = (1, 1) # (cols, rows)
```
## 2. Widget Gallery
### WidgetGallery
A popup gallery for browsing and adding widgets:
```python
from core.widgets import WidgetGallery
gallery = WidgetGallery(parent)
gallery.widget_added.connect(on_widget_added)
gallery.show()
```
### DashboardWidgetManager
Manages widget layout and persistence:
```python
from core.widgets import DashboardWidgetManager
manager = DashboardWidgetManager(plugin_manager, parent)
manager.widget_created.connect(on_widget_created)
# Add widget
manager.add_widget('system_status', 'widget_1', {
'position': {'row': 0, 'col': 0},
'size': {'width': 2, 'height': 1}
})
# Widget configurations are automatically saved to SQLite
```
## 3. Plugin Store
### PluginStoreUI
Complete plugin store interface:
```python
from core.plugin_store import PluginStoreUI
store = PluginStoreUI(plugin_manager, parent)
```
**Features**:
- Browse available plugins from repository
- Category filtering
- Search functionality
- Dependency resolution
- Install/uninstall with confirmation
- Enable/disable installed plugins
**Implementation Details**:
- Fetches manifest from remote repository
- Downloads plugins via raw file access
- Stores plugins in `plugins/` directory
- Tracks installed/enabled state in SQLite
## 4. Settings Panel
### EnhancedSettingsPanel
Full-featured settings with SQLite persistence:
```python
from core.ui.settings_panel import EnhancedSettingsPanel
settings = EnhancedSettingsPanel(overlay_window, parent)
settings.settings_changed.connect(on_setting_changed)
settings.theme_changed.connect(on_theme_changed)
```
**Tabs**:
1. **General**: Startup options, behavior settings, performance
2. **Appearance**: Theme selection, accent colors, opacity
3. **Plugins**: Enable/disable plugins, access plugin store
4. **Hotkeys**: Configure keyboard shortcuts
5. **Data & Backup**: Export/import, statistics, maintenance
6. **About**: Version info, system details, links
**Persistence**:
- All settings saved to SQLite via `user_preferences` table
- Hotkeys stored in `hotkeys` table
- Changes logged to `activity_log`
## 5. Activity Bar
### EnhancedActivityBar
Windows 11-style taskbar:
```python
from core.activity_bar_enhanced import EnhancedActivityBar
bar = EnhancedActivityBar(plugin_manager, parent)
bar.plugin_requested.connect(on_plugin_requested)
bar.search_requested.connect(on_search)
bar.settings_requested.connect(show_settings)
bar.show()
```
**Features**:
- **Start Button**: Opens app drawer
- **Search Box**: Quick plugin search
- **Pinned Plugins**: Drag-to-pin from app drawer
- **Clock**: Auto-updating time display
- **Settings Button**: Quick access to settings
- **Auto-hide**: Configurable auto-hide behavior
- **Position**: Top or bottom screen position
### AppDrawer
Start menu-style plugin launcher:
```python
from core.activity_bar_enhanced import AppDrawer
drawer = AppDrawer(plugin_manager, parent)
drawer.plugin_launched.connect(on_plugin_launch)
drawer.plugin_pin_requested.connect(pin_plugin)
```
**Features**:
- Grid of all plugins
- Real-time search filtering
- Context menu for pinning
- Frosted glass styling
## 6. Data Layer (SQLite)
### SQLiteDataStore
Thread-safe persistent storage:
```python
from core.data import get_sqlite_store
store = get_sqlite_store()
```
### Tables
#### plugin_states
```sql
CREATE TABLE plugin_states (
plugin_id TEXT PRIMARY KEY,
enabled INTEGER DEFAULT 0,
version TEXT,
settings TEXT, -- JSON
last_loaded TEXT,
load_count INTEGER DEFAULT 0,
error_count INTEGER DEFAULT 0,
created_at TEXT,
updated_at TEXT
);
```
#### user_preferences
```sql
CREATE TABLE user_preferences (
key TEXT PRIMARY KEY,
value TEXT, -- JSON
category TEXT DEFAULT 'general',
updated_at TEXT
);
```
#### sessions
```sql
CREATE TABLE sessions (
session_id TEXT PRIMARY KEY,
started_at TEXT,
ended_at TEXT,
plugin_stats TEXT, -- JSON
system_info TEXT -- JSON
);
```
#### activity_log
```sql
CREATE TABLE activity_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
category TEXT,
action TEXT,
details TEXT,
plugin_id TEXT
);
```
#### dashboard_widgets
```sql
CREATE TABLE dashboard_widgets (
widget_id TEXT PRIMARY KEY,
widget_type TEXT,
position_row INTEGER,
position_col INTEGER,
size_width INTEGER,
size_height INTEGER,
config TEXT, -- JSON
enabled INTEGER DEFAULT 1
);
```
#### hotkeys
```sql
CREATE TABLE hotkeys (
action TEXT PRIMARY KEY,
key_combo TEXT,
enabled INTEGER DEFAULT 1,
plugin_id TEXT
);
```
### API Examples
```python
# Plugin State
state = PluginState(
plugin_id='my_plugin',
enabled=True,
version='1.0.0',
settings={'key': 'value'}
)
store.save_plugin_state(state)
loaded_state = store.load_plugin_state('my_plugin')
# User Preferences
store.set_preference('theme', 'Dark Blue', category='appearance')
theme = store.get_preference('theme', default='Dark')
prefs = store.get_preferences_by_category('appearance')
# Activity Logging
store.log_activity('plugin', 'loaded', 'Plugin initialized', plugin_id='my_plugin')
recent = store.get_recent_activity(limit=50)
# Sessions
session_id = store.start_session()
store.end_session(session_id, plugin_stats={'loaded': 5})
# Widgets
store.save_widget_config(
widget_id='widget_1',
widget_type='system_status',
row=0, col=0,
width=2, height=1,
config={'update_interval': 1000}
)
widgets = store.load_widget_configs()
# Hotkeys
store.save_hotkey('toggle_overlay', 'Ctrl+Shift+U', enabled=True)
hotkeys = store.get_hotkeys()
# Maintenance
stats = store.get_stats()
store.vacuum() # Optimize database
```
## File Structure
```
core/
├── data/
│ ├── __init__.py
│ └── sqlite_store.py # SQLite data layer
├── widgets/
│ ├── __init__.py
│ ├── dashboard_widgets.py # Widget implementations
│ └── widget_gallery.py # Gallery and manager
├── ui/
│ ├── settings_panel.py # Enhanced settings
│ └── ...
├── dashboard_enhanced.py # Enhanced dashboard
├── activity_bar_enhanced.py # Enhanced activity bar
└── plugin_store.py # Plugin store
```
## Running the Demo
```bash
# Run the comprehensive demo
python core_functionality_demo.py
```
The demo showcases:
- All dashboard widgets
- Widget gallery functionality
- Activity bar features
- Settings panel with persistence
- Data layer statistics
## Integration Example
```python
from PyQt6.QtWidgets import QApplication, QMainWindow
from core.data import get_sqlite_store
from core.dashboard_enhanced import EnhancedDashboard
from core.activity_bar_enhanced import EnhancedActivityBar, get_activity_bar
from core.ui.settings_panel import EnhancedSettingsPanel
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# Initialize data store
self.store = get_sqlite_store()
# Create dashboard
self.dashboard = EnhancedDashboard(plugin_manager=self.plugin_manager)
self.setCentralWidget(self.dashboard)
# Create activity bar
self.activity_bar = get_activity_bar(self.plugin_manager)
self.activity_bar.show()
# Settings panel (can be shown as overlay)
self.settings = EnhancedSettingsPanel(self)
# Log startup
self.store.log_activity('system', 'app_started')
if __name__ == '__main__':
app = QApplication([])
window = MainWindow()
window.show()
app.exec()
```
## Summary
All critical tasks have been completed:
**Dashboard Widgets**: System Status, Quick Actions, Recent Activity, Plugin Grid
**Plugin Store**: Browse, install, uninstall, dependencies, versions
**Settings Panel**: General, plugins, hotkeys, data/backup - all with persistence
**Widget Gallery**: Browse, create, configure, position/size management
**Activity Bar**: Pinned plugins, app drawer, search, drag-to-pin
**Data Layer**: SQLite integration for settings, plugin state, preferences, sessions

View File

@ -1,175 +0,0 @@
# EU-Utility Feature Implementation Summary
**Date:** 2025-02-14
**Agent:** Feature Developer Agent
**Status:** ✅ COMPLETE
---
## Implemented Features
### 1. 📊 Session Exporter (`plugins/session_exporter/`)
**Purpose:** Export hunting/mining sessions to CSV/JSON formats for analysis
**Key Features:**
- Real-time session tracking via Event Bus
- Captures: Loot, Skill Gains, Globals, HOFs, Damage
- Export formats: JSON (structured) and CSV (spreadsheet-friendly)
- Auto-export at configurable intervals
- Session statistics and summaries
- Hotkey: `Ctrl+Shift+E`
**Files:**
- `plugins/session_exporter/__init__.py`
- `plugins/session_exporter/plugin.py` (23KB)
**Integration Points:**
- EventBus (subscribes to LootEvent, SkillGainEvent, GlobalEvent, DamageEvent)
- DataStore for settings persistence
- Notification service for user feedback
---
### 2. 🔔 Price Alert System (`plugins/price_alerts/`)
**Purpose:** Monitor Entropia Nexus API prices with smart alerts
**Key Features:**
- Search and monitor any Nexus item
- "Below" alerts for buy opportunities
- "Above" alerts for sell opportunities
- Auto-refresh at 1-60 minute intervals
- Price history tracking (7 days)
- Visual + sound notifications
- Hotkey: `Ctrl+Shift+P`
**Files:**
- `plugins/price_alerts/__init__.py`
- `plugins/price_alerts/plugin.py` (25KB)
**Integration Points:**
- NexusAPI for market data
- DataStore for alerts and price history
- Background task service for non-blocking API calls
- Notification service for alerts
---
### 3. 📸 Auto-Screenshot on Globals (`plugins/auto_screenshot/`)
**Purpose:** Automatically capture screenshots on Global/HOF and other events
**Key Features:**
- Triggers: Global, HOF, ATH, Discovery, high-value loot, skill gains
- Configurable capture delay (to hide overlay)
- Custom filename patterns with variables
- Organized folder structure
- Notification and sound options
- Hotkey: `Ctrl+Shift+C` (manual capture)
**Files:**
- `plugins/auto_screenshot/__init__.py`
- `plugins/auto_screenshot/plugin.py` (29KB)
**Integration Points:**
- EventBus (subscribes to GlobalEvent, LootEvent, SkillGainEvent)
- ScreenshotService for capture
- DataStore for settings
- Notification + Audio services
---
## Documentation
**Created:** `docs/FEATURE_PACK.md` (7KB)
Comprehensive documentation including:
- Feature descriptions
- Usage instructions
- Configuration options
- File format specifications
- Troubleshooting guide
- Hotkey reference
---
## Architecture Compliance
All plugins follow EU-Utility architecture:
✅ Extend `BasePlugin`
✅ Use typed Event Bus subscriptions
✅ Integrate with PluginAPI services
✅ Persistent storage via DataStore
✅ Qt6 UI with consistent styling
✅ Hotkey support
✅ Notification integration
✅ Background task support (where applicable)
---
## Code Quality
✅ All files pass Python syntax check
✅ Type hints used throughout
✅ Docstrings for all public methods
✅ Consistent error handling
✅ Resource cleanup on shutdown
✅ Thread-safe UI updates using signals
---
## File Structure
```
plugins/
├── session_exporter/
│ ├── __init__.py
│ └── plugin.py
├── price_alerts/
│ ├── __init__.py
│ └── plugin.py
└── auto_screenshot/
├── __init__.py
└── plugin.py
docs/
├── FEATURE_PACK.md
└── (existing docs...)
```
---
## Next Steps (Optional Enhancements)
1. **Session Exporter:**
- Add PDF report generation
- Session comparison/analytics
- Export to Google Sheets
2. **Price Alerts:**
- Price trend graphs
- Bulk import/export of alerts
- Discord webhook integration
3. **Auto-Screenshot:**
- Video capture option
- Upload to cloud storage
- Social media sharing
---
## Deliverables Summary
| Deliverable | Status | Size |
|-------------|--------|------|
| Session Exporter Plugin | ✅ Complete | 23 KB |
| Price Alerts Plugin | ✅ Complete | 26 KB |
| Auto-Screenshot Plugin | ✅ Complete | 29 KB |
| Feature Documentation | ✅ Complete | 7 KB |
| Plugin `__init__.py` files | ✅ Complete | 3 files |
**Total Code:** ~78 KB
**Total Documentation:** ~7 KB
**All Requirements:** ✅ Met

View File

@ -1,412 +0,0 @@
# EU-Utility Integration & Test Report
**Date:** 2024-02-15
**Engineer:** Integration & Test Engineer
**Project:** EU-Utility v2.0
---
## Executive Summary
I have completed a comprehensive test suite and documentation package for EU-Utility. This includes:
### Test Suite Deliverables
| Component | Status | Files | Test Count |
|-----------|--------|-------|------------|
| Unit Tests | ✅ Complete | 4 new + existing | 80+ |
| Integration Tests | ✅ Complete | 2 files | 20+ |
| UI Automation Tests | ✅ Complete | 1 file | 25+ |
| Performance Benchmarks | ✅ Complete | 1 file | 15+ |
| Test Runner | ✅ Complete | 1 script | - |
### Documentation Deliverables
| Document | Status | Pages |
|----------|--------|-------|
| User Guide | ✅ Complete | ~15 |
| Troubleshooting Guide | ✅ Complete | ~12 |
| API Documentation | ✅ Complete | ~18 |
| Setup Instructions | ✅ Complete | ~10 |
| Performance Report | ✅ Complete | ~8 |
| Test Suite README | ✅ Complete | ~6 |
---
## Test Suite Details
### 1. Unit Tests (`tests/unit/`)
#### New Test Files Created:
1. **`test_plugin_manager.py`** (250 lines)
- Plugin manager initialization
- Configuration loading
- Plugin enable/disable
- Discovery mechanisms
- Dependency management
2. **`test_window_manager.py`** (230 lines)
- Window manager singleton
- Window detection
- Focus tracking
- Multi-monitor support
- Activity bar functionality
3. **`test_api_integration.py`** (450 lines)
- Plugin API singleton
- All API methods
- Service registration
- Error handling
- Nexus API integration
- HTTP client
4. **`test_core_services.py`** (380 lines)
- Event bus
- Data store
- Settings
- Logger
- Hotkey manager
- Performance optimizations
**Existing Test Files Available:**
- test_audio.py
- test_clipboard.py
- test_data_store.py
- test_event_bus.py
- test_http_client.py
- test_log_reader.py
- test_nexus_api.py
- test_plugin_api.py
- test_security_utils.py
- test_settings.py
- test_tasks.py
### 2. Integration Tests (`tests/integration/`)
#### New Test Files Created:
1. **`test_plugin_workflows.py`** (500 lines)
- Plugin lifecycle tests
- Enable/disable workflow
- Settings persistence
- API workflows
- UI integration
- Error handling
**Existing Test Files Available:**
- test_plugin_lifecycle.py
### 3. UI Automation Tests (`tests/ui/`)
#### New Test Files Created:
1. **`test_ui_automation.py`** (380 lines)
- Dashboard UI tests
- Overlay window tests
- Activity bar tests
- Settings dialog tests
- Responsive UI tests
- Theme UI tests
- Accessibility tests
- Tray icon tests
### 4. Performance Tests (`tests/performance/`)
#### New Test Files Created:
1. **`test_benchmarks.py`** (320 lines)
- Plugin manager performance
- API performance
- UI performance
- Memory usage
- Startup performance
- Cache performance
- Concurrent operations
---
## Documentation Details
### 1. User Guide (`docs/USER_GUIDE.md`)
- Getting started
- Installation instructions
- First launch guide
- Main interface overview
- Plugin usage
- Hotkey reference
- Settings configuration
- Tips & tricks
- FAQ
### 2. Troubleshooting Guide (`docs/TROUBLESHOOTING.md`)
- Installation issues
- Startup problems
- Hotkey troubleshooting
- Plugin issues
- UI problems
- Performance optimization
- OCR issues
- Network problems
- Debug procedures
### 3. API Documentation (`docs/API_DOCUMENTATION.md`)
- Plugin API reference
- Window Manager API
- Event Bus API
- Data Store API
- Nexus API
- HTTP Client API
- Plugin development guide
- Code examples
- Best practices
### 4. Setup Instructions (`docs/SETUP_INSTRUCTIONS.md`)
- Prerequisites
- Windows setup
- Linux setup
- Development setup
- Configuration guide
- Verification steps
- Troubleshooting
- Updating procedures
### 5. Performance Report (`docs/PERFORMANCE_REPORT.md`)
- Executive summary
- Benchmark results
- Resource utilization
- Scalability testing
- Stress testing
- Optimization recommendations
- Configuration tuning
- Comparison with v1.0
---
## Test Infrastructure
### Shared Fixtures (`tests/conftest.py`)
Comprehensive fixtures for:
- `temp_dir`: Temporary directories
- `mock_overlay`: Mock overlay window
- `mock_plugin_manager`: Mock plugin manager
- `mock_qt_app`: Mock Qt application
- `sample_config`: Sample configurations
- `mock_nexus_response`: Mock API responses
- `mock_window_info`: Mock window data
- `mock_ocr_result`: Mock OCR results
- `sample_log_lines`: Sample log data
- `event_bus`: Fresh event bus instances
- `data_store`: Temporary data stores
- `mock_http_client`: Mock HTTP client
- `test_logger`: Test logging
### Test Runner (`run_tests.py`)
Features:
- Run all or specific test categories
- Coverage reporting (terminal, HTML, XML)
- Verbose output option
- Fail-fast mode
- Marker display
- Summary reporting
### Development Dependencies (`requirements-dev.txt`)
```
pytest>=7.4.0
pytest-cov>=4.1.0
pytest-mock>=3.11.0
pytest-benchmark>=4.0.0
pytest-qt>=4.2.0
pytest-xvfb>=2.0.0
```
---
## Test Coverage Summary
### UI Integration Tests
✅ **Tested:**
- Dashboard opens correctly
- Navigation between tabs works
- Activity bar shows/hides properly
- Tray icon is responsive
- No UI freezing or blocking (responsive tests)
### Plugin System Tests
✅ **Tested:**
- Plugins load correctly
- Plugin enable/disable works
- Plugin settings persist
- Plugin store functions
- Dependency management
### API Integration Tests
✅ **Tested:**
- Plugin API singleton
- All service registrations
- Log reading
- Window operations
- OCR functionality
- Screenshot capture
- Nexus API integration
- HTTP client
- Audio/Notifications
- Clipboard operations
- Event bus
- Data store
- Background tasks
### Window Management Tests
✅ **Tested:**
- EU window detection
- Focus tracking
- Overlay positioning
- Multi-monitor support
---
## File Structure
```
EU-Utility/
├── tests/
│ ├── __init__.py
│ ├── conftest.py # Shared fixtures
│ ├── README.md # Test documentation
│ ├── unit/
│ │ ├── test_plugin_manager.py ✅ NEW
│ │ ├── test_window_manager.py ✅ NEW
│ │ ├── test_api_integration.py ✅ NEW
│ │ ├── test_core_services.py ✅ NEW
│ │ └── [existing test files...]
│ ├── integration/
│ │ ├── test_plugin_workflows.py ✅ NEW
│ │ └── [existing test files...]
│ ├── ui/
│ │ └── test_ui_automation.py ✅ NEW
│ └── performance/
│ └── test_benchmarks.py ✅ NEW
├── docs/
│ ├── USER_GUIDE.md ✅ NEW
│ ├── TROUBLESHOOTING.md ✅ NEW
│ ├── API_DOCUMENTATION.md ✅ NEW
│ ├── SETUP_INSTRUCTIONS.md ✅ NEW
│ └── PERFORMANCE_REPORT.md ✅ NEW
├── run_tests.py ✅ NEW (enhanced)
└── requirements-dev.txt ✅ NEW
```
---
## Usage Examples
### Running Tests
```bash
# Run all tests
python run_tests.py --all
# Run with coverage
python run_tests.py --all --coverage --html
# Run specific category
python run_tests.py --unit
python run_tests.py --integration
python run_tests.py --ui
python run_tests.py --performance
# Using pytest directly
python -m pytest tests/unit/ -v
python -m pytest tests/integration/ -v -m integration
python -m pytest tests/ui/ -v -m ui
python -m pytest tests/performance/ --benchmark-only
```
### Accessing Documentation
```bash
# User Guide
cat docs/USER_GUIDE.md
# Troubleshooting
cat docs/TROUBLESHOOTING.md
# API Reference
cat docs/API_DOCUMENTATION.md
# Setup Instructions
cat docs/SETUP_INSTRUCTIONS.md
# Performance Report
cat docs/PERFORMANCE_REPORT.md
```
---
## Quality Metrics
### Code Quality
- ✅ Consistent naming conventions
- ✅ Comprehensive docstrings
- ✅ Type hints where appropriate
- ✅ Error handling
- ✅ Mock usage for isolation
### Test Quality
- ✅ Descriptive test names
- ✅ Clear test structure
- ✅ Appropriate fixtures
- ✅ Good coverage of edge cases
- ✅ Performance benchmarks
### Documentation Quality
- ✅ Clear structure
- ✅ Comprehensive coverage
- ✅ Code examples
- ✅ Troubleshooting steps
- ✅ Easy to navigate
---
## Recommendations
### Immediate Actions
1. Install development dependencies: `pip install -r requirements-dev.txt`
2. Run unit tests to verify setup: `python run_tests.py --unit`
3. Review test coverage report
### Short Term
1. Integrate tests into CI/CD pipeline
2. Add more edge case tests
3. Expand UI automation coverage
### Long Term
1. Add visual regression tests
2. Implement load testing
3. Create automated release testing
---
## Conclusion
All deliverables have been successfully created:
**Comprehensive test coverage** - 80+ unit tests, 20+ integration tests, 25+ UI tests, 15+ performance benchmarks
**Working integration tests** - All major workflows tested
**User documentation** - Complete user guide with examples
**Troubleshooting guide** - Comprehensive problem-solving guide
**Performance report** - Benchmarks and optimization recommendations
The test suite and documentation provide a solid foundation for maintaining EU-Utility quality and helping users get the most out of the application.
---
*Report completed by Integration & Test Engineer*
*Date: 2024-02-15*

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 ImpulsiveFPS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

120
MANIFEST.in Normal file
View File

@ -0,0 +1,120 @@
# EU-Utility - MANIFEST.in
# Controls what files are included in the source distribution (sdist)
# https://packaging.python.org/en/latest/guides/using-manifest-in/
# =============================================================================
# INCLUDE PATTERNS
# =============================================================================
# Core package files
recursive-include core *.py
recursive-include core *.json *.yaml *.yml
recursive-include core *.css *.qss *.ui
recursive-include core *.png *.jpg *.jpeg *.gif *.ico *.svg
# Plugin files
recursive-include plugins *.py
recursive-include plugins *.json *.yaml *.yml
recursive-include plugins *.css *.qss *.ui
recursive-include plugins/assets *.png *.jpg *.jpeg *.gif *.ico *.svg *.ttf *.woff *.woff2
recursive-include plugins/templates *.html *.txt *.md
# Documentation
recursive-include docs *.md *.rst *.txt
recursive-include docs *.png *.jpg *.jpeg *.gif *.svg
include README.md
include CONTRIBUTING.md
include CHANGELOG.md
include SECURITY_AUDIT_REPORT.md
include LICENSE
# Configuration files
include requirements.txt
include requirements-dev.txt
include pytest.ini
# Build and packaging
include setup.py
include pyproject.toml
include MANIFEST.in
include Makefile
# GitHub templates and workflows
recursive-include .github *.md *.yml *.yaml
# Assets
recursive-include assets *.png *.jpg *.jpeg *.gif *.ico *.svg
recursive-include benchmarks *.py *.md
# =============================================================================
# EXCLUDE PATTERNS
# =============================================================================
# Test files (excluded from distribution)
recursive-exclude tests *
recursive-exclude * test_*.py *_test.py
recursive-exclude * __pycache__
recursive-exclude * *.py[co]
recursive-exclude * *.so
recursive-exclude * .pytest_cache
recursive-exclude * .coverage
recursive-exclude * coverage.xml
recursive-exclude * htmlcov
# Development environment
recursive-exclude * .venv
recursive-exclude * venv
recursive-exclude * env
recursive-exclude * .env
recursive-exclude * .env.*
# Version control
recursive-exclude * .git
recursive-exclude * .gitignore
recursive-exclude * .gitattributes
# IDE and editor files
recursive-exclude * .vscode
recursive-exclude * .idea
recursive-exclude * *.swp
recursive-exclude * *.swo
recursive-exclude * *~
recursive-exclude * .DS_Store
recursive-exclude * Thumbs.db
# Build artifacts
recursive-exclude * build
recursive-exclude * dist
recursive-exclude * *.egg-info
recursive-exclude * .eggs
# CI/CD (excluded - these are for development only)
recursive-exclude .github/workflows *
# Temporary and cache files
recursive-exclude * .tox
recursive-exclude * .mypy_cache
recursive-exclude * .hypothesis
recursive-exclude * .ruff_cache
recursive-exclude * *.log
recursive-exclude * logs/*.log
recursive-exclude * *.db
recursive-exclude * *.sqlite
recursive-exclude * *.sqlite3
# Security and sensitive files (never include these)
exclude .env
exclude .env.local
exclude .env.production
exclude secrets.json
exclude credentials.json
exclude *secret*
exclude *password*
exclude *credential*
exclude *_key*
exclude private_*
# Debug and vulnerable files (exclude from distribution)
recursive-exclude * *_vulnerable.py
recursive-exclude * *_insecure.py
recursive-exclude * *_debug.py

263
Makefile Normal file
View File

@ -0,0 +1,263 @@
.PHONY: help install install-dev install-all update update-dev clean clean-all
.PHONY: test test-unit test-integration test-ui test-coverage test-fast
.PHONY: lint lint-flake8 lint-mypy lint-bandit lint-all
.PHONY: format format-check format-diff
.PHONY: build build-check build-dist upload-test upload-prod
.PHONY: docs docs-serve docs-build docs-clean
.PHONY: check security coverage-report
# Default target
.DEFAULT_GOAL := help
# Python executable
PYTHON := python3
PIP := pip3
PYTEST := pytest
BLACK := black
FLAKE8 := flake8
MYPY := mypy
ISORT := isort
BANDIT := bandit
SAFETY := safety
TWINE := twine
# Directories
SRC_DIRS := core plugins
TEST_DIR := tests
DOCS_DIR := docs
BUILD_DIR := build
DIST_DIR := dist
# =============================================================================
# HELP
# =============================================================================
help: ## Show this help message
@echo "EU-Utility Development Commands"
@echo "================================"
@echo ""
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
# =============================================================================
# INSTALLATION
# =============================================================================
install: ## Install production dependencies
$(PIP) install -e .
install-dev: ## Install development dependencies
$(PIP) install -e ".[dev]"
install-all: ## Install all dependencies including optional features
$(PIP) install -e ".[all,dev]"
install-spotify: ## Install with Spotify support
$(PIP) install -e ".[spotify]"
install-discord: ## Install with Discord Rich Presence support
$(PIP) install -e ".[discord]"
update: ## Update production dependencies
$(PIP) install --upgrade -e .
update-dev: ## Update development dependencies
$(PIP) install --upgrade -e ".[dev]"
update-all: ## Update all dependencies
$(PIP) install --upgrade -e ".[all,dev]"
# =============================================================================
# TESTING
# =============================================================================
test: ## Run all tests
$(PYTEST) $(TEST_DIR) -v
test-unit: ## Run unit tests only
$(PYTEST) $(TEST_DIR) -v -m unit
test-integration: ## Run integration tests only
$(PYTEST) $(TEST_DIR) -v -m integration
test-ui: ## Run UI tests only
$(PYTEST) $(TEST_DIR) -v -m ui
test-performance: ## Run performance benchmarks
$(PYTEST) $(TEST_DIR) -v -m performance --benchmark-only
test-coverage: ## Run tests with coverage report
$(PYTEST) $(TEST_DIR) -v --cov=$(SRC_DIRS) --cov-report=term-missing --cov-report=html
test-coverage-xml: ## Run tests with XML coverage report
$(PYTEST) $(TEST_DIR) -v --cov=$(SRC_DIRS) --cov-report=xml
test-fast: ## Run fast tests only (excludes slow and UI tests)
$(PYTEST) $(TEST_DIR) -v -m "not slow and not ui"
test-parallel: ## Run tests in parallel (requires pytest-xdist)
$(PYTEST) $(TEST_DIR) -v -n auto
# =============================================================================
# CODE QUALITY
# =============================================================================
lint: lint-flake8 lint-mypy ## Run all linters
lint-flake8: ## Run flake8 linter
$(FLAKE8) $(SRC_DIRS) $(TEST_DIR)
lint-mypy: ## Run mypy type checker
$(MYPY) $(SRC_DIRS)
lint-bandit: ## Run bandit security linter
$(BANDIT) -r $(SRC_DIRS) -f txt
lint-pydocstyle: ## Run pydocstyle docstring checker
pydocstyle $(SRC_DIRS)
lint-all: lint-flake8 lint-mypy lint-bandit lint-pydocstyle ## Run all linters
format: ## Format code with black and isort
$(BLACK) $(SRC_DIRS) $(TEST_DIR)
$(ISORT) $(SRC_DIRS) $(TEST_DIR)
format-check: ## Check code formatting without modifying files
$(BLACK) --check $(SRC_DIRS) $(TEST_DIR)
$(ISORT) --check-only $(SRC_DIRS) $(TEST_DIR)
format-diff: ## Show formatting changes without applying them
$(BLACK) --diff $(SRC_DIRS) $(TEST_DIR)
# =============================================================================
# SECURITY
# =============================================================================
security: security-bandit security-safety ## Run all security checks
security-bandit: ## Run bandit security analysis
$(BANDIT) -r $(SRC_DIRS) -f json -o bandit-report.json || true
$(BANDIT) -r $(SRC_DIRS) -f txt
security-safety: ## Run safety check for known vulnerabilities
$(SAFETY) check
security-pip-audit: ## Run pip-audit for dependency vulnerabilities
pip-audit --desc
# =============================================================================
# BUILD & DISTRIBUTION
# =============================================================================
build: clean-build ## Build source and wheel distributions
$(PYTHON) -m build
build-check: ## Check the built distributions
$(TWINE) check $(DIST_DIR)/*
build-dist: build build-check ## Build and check distributions
upload-test: build-dist ## Upload to TestPyPI
$(TWINE) upload --repository testpypi $(DIST_DIR)/*
upload-prod: build-dist ## Upload to PyPI (requires credentials)
$(TWINE) upload $(DIST_DIR)/*
# =============================================================================
# DOCUMENTATION
# =============================================================================
docs: docs-build ## Build documentation
docs-build: ## Build Sphinx documentation
cd $(DOCS_DIR) && $(MAKE) html
docs-serve: docs-build ## Serve documentation locally
cd $(DOCS_DIR)/_build/html && $(PYTHON) -m http.server 8000
docs-clean: ## Clean documentation build files
cd $(DOCS_DIR) && $(MAKE) clean
docs-live: ## Serve documentation with auto-reload (requires sphinx-autobuild)
sphinx-autobuild $(DOCS_DIR) $(DOCS_DIR)/_build/html --watch $(SRC_DIRS)
# =============================================================================
# CLEANUP
# =============================================================================
clean: ## Remove build artifacts and cache files
rm -rf $(BUILD_DIR)
rm -rf $(DIST_DIR)
rm -rf *.egg-info
rm -rf .eggs
rm -rf .pytest_cache
rm -rf .mypy_cache
rm -rf .coverage
rm -rf htmlcov
rm -rf coverage.xml
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
find . -type f -name "*.pyc" -delete
find . -type f -name "*.pyo" -delete
find . -type f -name "*.so" -delete
clean-all: clean ## Remove all generated files including virtual environments
rm -rf .venv
rm -rf venv
rm -rf env
rm -rf .tox
rm -rf bandit-report.json
# =============================================================================
# UTILITY
# =============================================================================
check: format-check lint test-fast ## Run all checks (format, lint, fast tests)
ci: clean install-dev format-check lint test-coverage security ## Full CI pipeline
coverage-report: ## Open coverage report in browser
$(PYTHON) -m webbrowser htmlcov/index.html
requirements: ## Generate requirements.txt from pyproject.toml
$(PIP) freeze > requirements.txt
dependency-tree: ## Show dependency tree
pipdeptree
pre-commit: ## Install and run pre-commit hooks
pre-commit install
pre-commit run --all-files
# =============================================================================
# DEVELOPMENT
# =============================================================================
run: ## Run EU-Utility
$(PYTHON) -m core.main
run-secure: ## Run EU-Utility (secure/optimized version)
$(PYTHON) -m core.main_optimized
run-tests: ## Run the test suite runner
$(PYTHON) run_tests.py
demo: ## Run the core functionality demo
$(PYTHON) core_functionality_demo.py
profile: ## Run with profiling
$(PYTHON) -m cProfile -o profile.stats -m core.main
# =============================================================================
# VERSION MANAGEMENT
# =============================================================================
version: ## Show current version
$(PYTHON) -c "from core import __version__; print(__version__)"
bump-patch: ## Bump patch version
bump2version patch
bump-minor: ## Bump minor version
bump2version minor
bump-major: ## Bump major version
bump2version major

226
PACKAGING.md Normal file
View File

@ -0,0 +1,226 @@
# EU-Utility Packaging Guide
This document describes the complete packaging setup for EU-Utility, designed for professional Python distribution and PyPI publication.
## 📦 Package Structure
```
EU-Utility/
├── setup.py # Traditional setuptools configuration
├── pyproject.toml # Modern Python packaging (PEP 517/518)
├── MANIFEST.in # Source distribution file inclusion rules
├── Makefile # Development task automation
├── requirements.txt # Production dependencies
├── requirements-dev.txt # Development dependencies
├── LICENSE # MIT License
└── .github/
└── workflows/
└── ci.yml # GitHub Actions CI/CD pipeline
```
## 🚀 Installation Methods
### For End Users
```bash
# Install from PyPI (when published)
pip install eu-utility
# Install with all optional features
pip install "eu-utility[all]"
# Install with specific features
pip install "eu-utility[spotify]"
pip install "eu-utility[discord]"
```
### For Developers
```bash
# Clone the repository
git clone https://github.com/ImpulsiveFPS/EU-Utility.git
cd EU-Utility
# Install in development mode with all dev dependencies
pip install -e ".[dev]"
# Or use the Makefile
make install-dev
```
## 🛠️ Development Commands (Makefile)
### Installation
```bash
make install # Install production dependencies
make install-dev # Install development dependencies
make install-all # Install all dependencies including optional features
```
### Testing
```bash
make test # Run all tests
make test-unit # Run unit tests only
make test-coverage # Run tests with coverage report
make test-fast # Run fast tests (excludes slow and UI tests)
```
### Code Quality
```bash
make lint # Run all linters (flake8, mypy)
make format # Format code with black and isort
make format-check # Check code formatting without modifying files
```
### Security
```bash
make security # Run all security checks
make security-bandit # Run Bandit security analysis
make security-safety # Run Safety vulnerability check
```
### Building and Distribution
```bash
make build # Build source and wheel distributions
make build-check # Check the built distributions
make upload-test # Upload to TestPyPI
make upload-prod # Upload to PyPI
```
### Cleanup
```bash
make clean # Remove build artifacts and cache files
make clean-all # Remove all generated files including virtual environments
```
## 🔧 Tool Configurations (pyproject.toml)
### Black (Code Formatter)
- Line length: 100 characters
- Target Python versions: 3.11, 3.12
### isort (Import Sorting)
- Profile: black (compatible with black formatting)
- Known first-party packages: core, plugins
### flake8 (Linter)
- Max line length: 100
- Max complexity: 10
- Extends ignore: E203, E501, W503 (black compatibility)
### mypy (Type Checker)
- Python version: 3.11
- Strict mode enabled
- Missing imports ignored for certain modules (PyQt6, OCR libraries)
### pytest (Testing)
- Test directory: tests/
- Coverage minimum: 80%
- Coverage reports: terminal, HTML, XML
### Bandit (Security)
- Excludes: tests, venv directories
- Skips: B101 (assert_used), B311 (random)
## 📋 CI/CD Pipeline (GitHub Actions)
The CI/CD pipeline runs on every push and pull request:
1. **Lint and Format Check**: Verifies code style with black, isort, flake8, and mypy
2. **Security Analysis**: Runs Bandit and Safety checks
3. **Tests**: Runs tests on multiple Python versions (3.11, 3.12) and OS (Ubuntu, Windows)
4. **Build**: Creates source and wheel distributions
5. **Test Installation**: Verifies the package can be installed
6. **Release**: Creates GitHub releases for tagged versions
7. **Publish**: Publishes to PyPI (only for tagged releases, not dev builds)
### Release Process
1. Update version in `core/__init__.py`
2. Update `CHANGELOG.md`
3. Commit and push changes
4. Create a git tag: `git tag v2.1.0`
5. Push the tag: `git push origin v2.1.0`
6. The CI pipeline will automatically:
- Run all tests
- Build the package
- Create a GitHub release
- Publish to PyPI
## 📦 Dependencies
### Production Dependencies
| Package | Version | Purpose |
|---------|---------|---------|
| PyQt6 | >=6.4.0 | GUI framework |
| keyboard | >=0.13.5 | Global hotkeys |
| psutil | >=5.9.0 | System monitoring |
| easyocr | >=1.7.0 | OCR (text recognition) |
| pytesseract | >=0.3.10 | Alternative OCR backend |
| pyautogui | >=0.9.54 | Screen capture |
| pillow | >=10.0.0 | Image processing |
| requests | >=2.28.0 | HTTP client |
| numpy | >=1.21.0 | Data processing |
### Development Dependencies
| Package | Version | Purpose |
|---------|---------|---------|
| pytest | >=7.4.0 | Testing framework |
| pytest-cov | >=4.1.0 | Coverage reporting |
| pytest-qt | >=4.2.0 | Qt testing |
| black | >=23.0.0 | Code formatting |
| flake8 | >=6.0.0 | Linting |
| mypy | >=1.5.0 | Type checking |
| bandit | >=1.7.5 | Security analysis |
| safety | >=2.3.0 | Vulnerability scanning |
| sphinx | >=7.0.0 | Documentation |
| build | >=0.10.0 | Package building |
| twine | >=4.0.0 | PyPI publishing |
## 🔒 Security Considerations
1. **Vulnerable files excluded**: Files ending with `_vulnerable.py`, `_insecure.py` are excluded from distribution
2. **Sensitive files excluded**: `.env`, `secrets.json`, `credentials.json` are never included
3. **Security scanning**: Bandit runs on every CI build
4. **Dependency scanning**: Safety checks for known vulnerabilities
## 📄 Entry Points
The package provides the following CLI commands after installation:
```bash
eu-utility # Main application entry point
eu-utility-secure # Secure/optimized version
eu-utility-gui # GUI entry point (Windows)
```
## 🌐 Platform Support
- **Windows 10/11**: Full support with all features
- **Linux**: Core features supported (some Windows-specific features disabled)
- **macOS**: Not officially supported (contributions welcome)
## 📝 Publishing Checklist
Before publishing a new release:
- [ ] Version bumped in `core/__init__.py`
- [ ] `CHANGELOG.md` updated with release notes
- [ ] All tests passing locally (`make test`)
- [ ] Code formatted (`make format`)
- [ ] Linting passes (`make lint`)
- [ ] Security checks pass (`make security`)
- [ ] Build succeeds (`make build`)
- [ ] Package installs correctly (`make test-install`)
- [ ] Git tag created and pushed
- [ ] GitHub release notes prepared
## 🔗 Resources
- [Python Packaging User Guide](https://packaging.python.org/)
- [setuptools documentation](https://setuptools.pypa.io/)
- [pyproject.toml specification](https://packaging.python.org/en/latest/specifications/declaring-project-metadata/)
- [GitHub Actions documentation](https://docs.github.com/en/actions)
- [PyPI publishing guide](https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/)

155
QUALITY_STANDARDS.md Normal file
View File

@ -0,0 +1,155 @@
# EU-Utility Quality Standards
This document defines the quality standards for EU-Utility to maintain peak code quality.
## Code Organization
```
EU-Utility/
├── core/ # Core functionality
│ ├── __init__.py
│ ├── main.py # Application entry point
│ ├── api/ # API layer
│ │ ├── __init__.py
│ │ ├── nexus_api.py
│ │ └── external_api.py
│ ├── services/ # Business logic
│ │ ├── __init__.py
│ │ ├── ocr_service.py
│ │ ├── screenshot.py
│ │ └── data_store.py
│ ├── ui/ # UI components
│ │ ├── __init__.py
│ │ ├── overlay.py
│ │ ├── dashboard.py
│ │ └── widgets/
│ └── utils/ # Utilities
│ ├── __init__.py
│ ├── security.py
│ └── helpers.py
├── plugins/ # Plugin system
│ ├── __init__.py
│ ├── base_plugin.py
│ ├── plugin_manager.py
│ └── builtin/ # Built-in plugins
├── tests/ # Test suite
│ ├── __init__.py
│ ├── test_core/
│ ├── test_plugins/
│ └── test_integration/
├── docs/ # Documentation
├── assets/ # Static assets
├── config/ # Configuration files
├── scripts/ # Utility scripts
├── setup.py # Package setup
├── pyproject.toml # Modern Python config
├── requirements.txt # Dependencies
├── requirements-dev.txt # Dev dependencies
├── Makefile # Build automation
├── LICENSE # MIT License
├── CHANGELOG.md # Version history
├── CONTRIBUTING.md # Contribution guide
└── README.md # Project readme
```
## Naming Conventions
- **Files**: `snake_case.py`
- **Classes**: `PascalCase`
- **Functions/Methods**: `snake_case()`
- **Constants**: `UPPER_SNAKE_CASE`
- **Private**: `_leading_underscore`
- **Protected**: `_single_leading_underscore`
## Code Style
- Follow PEP 8
- Maximum line length: 100 characters
- Use type hints for all function signatures
- Docstrings for all public APIs (Google style)
- Comments for complex logic
## Documentation Standards
### README.md
- Clear project description
- Installation instructions
- Quick start guide
- Feature list
- Screenshots/GIFs
- Contributing link
- License
### Function Docstrings
```python
def example_function(param1: str, param2: int) -> bool:
"""Short description.
Longer description if needed. Can span multiple
lines and provide detailed context.
Args:
param1: Description of param1.
param2: Description of param2.
Returns:
Description of return value.
Raises:
ValueError: When param2 is negative.
Example:
>>> example_function("test", 42)
True
"""
pass
```
## Testing Standards
- Unit tests for all functions
- Integration tests for workflows
- Minimum 80% code coverage
- Tests must pass before merging
## Security Standards
- No hardcoded credentials
- Input validation on all user inputs
- Use parameterized queries
- Secure defaults
- Regular dependency updates
## Performance Standards
- Profile before optimizing
- Use async for I/O operations
- Cache expensive operations
- Lazy loading for heavy resources
- Memory cleanup on exit
## Git Standards
- Commit messages: `type(scope): description`
- Types: feat, fix, docs, style, refactor, test, chore
- One logical change per commit
- Reference issues in commits
- Squash before merging to main
## Release Process
1. Update version in `__init__.py`
2. Update CHANGELOG.md
3. Run full test suite
4. Create git tag: `git tag vX.Y.Z`
5. Push to origin: `git push origin vX.Y.Z`
6. Create GitHub release
## Code Review Checklist
- [ ] Code follows style guide
- [ ] Tests added/updated
- [ ] Documentation updated
- [ ] No security vulnerabilities
- [ ] Performance acceptable
- [ ] Backwards compatible (or properly versioned)

View File

@ -1,292 +0,0 @@
# EU-Utility Code Cleanup & Refactoring - Final Report
**Date:** 2026-02-15
**Scope:** Core modules and plugin architecture
**Status:** ✅ Complete
---
## Executive Summary
Successfully cleaned and refactored the EU-Utility codebase with focus on:
- **Code Organization:** Proper module structure with clear separation of concerns
- **Documentation:** Comprehensive docstrings for all public APIs
- **Type Safety:** Full type hints throughout core modules
- **Standards Compliance:** PEP 8 formatting and naming conventions
- **Backward Compatibility:** All changes are non-breaking
---
## Files Refactored
| File | Lines | Changes |
|------|-------|---------|
| `core/__init__.py` | 200 | Complete rewrite with exports and docs |
| `core/base_plugin.py` | 893 | Added type hints and comprehensive docs |
| `core/event_bus.py` | 831 | Added type hints and comprehensive docs |
| `core/settings.py` | 284 | Added type hints and comprehensive docs |
| `core/api/__init__.py` | 94 | Added package documentation |
| `plugins/__init__.py` | 42 | Added module documentation |
| `core/README.md` | 194 | Created comprehensive guide |
**Total:** 2,538 lines of cleaned, documented code
---
## Improvements by Category
### 1. Code Organization ✅
**Before:**
- Inconsistent module exports
- Mixed import styles
- Unclear module boundaries
**After:**
- Clear module hierarchy
- Organized exports by category
- Consistent import patterns
- Well-defined module boundaries
### 2. Documentation ✅
**Before:**
- Minimal module-level docs
- Inconsistent docstring styles
- Missing examples
**After:**
- Comprehensive module docstrings
- Google-style docstrings (Args, Returns, Examples)
- Usage examples in all key modules
- Created core/README.md with detailed guide
### 3. Type Hints ✅
**Before:**
- No type annotations
- No type safety
- IDE support limited
**After:**
- Full type hints on all public methods
- Generic types (TypeVar) where appropriate
- Optional[] for nullable values
- Better IDE autocompletion
### 4. Standards Compliance ✅
**Before:**
- Inconsistent naming
- Mixed formatting styles
**After:**
- PEP 8 compliant formatting
- Consistent snake_case naming
- Proper import organization
- Clean code structure
### 5. Performance ✅
**Before:**
- Potential import overhead
**After:**
- TYPE_CHECKING for type-only imports
- Lazy loading maintained
- No runtime overhead from type hints
---
## Key Features Added
### Event Bus System
- Typed events with dataclasses
- Event filtering (mob types, damage thresholds)
- Event persistence (configurable history)
- Async event handling
- Event statistics and metrics
### Plugin Base Class
- Comprehensive API integration
- Service access methods (OCR, screenshot, audio, etc.)
- Event subscription management
- Data persistence helpers
- Notification methods
### Settings Manager
- Type-safe configuration access
- Automatic persistence
- Signal-based change notifications
- Plugin enablement tracking
- Qt/non-Qt environment support
---
## Architecture Improvements
### Three-Tier API System
```
┌─────────────────────────────────────┐
│ PluginAPI │ ← Core services
│ (Log, Window, OCR, Screenshot) │
├─────────────────────────────────────┤
│ WidgetAPI │ ← UI management
│ (Widget creation, positioning) │
├─────────────────────────────────────┤
│ ExternalAPI │ ← Integrations
│ (Webhooks, HTTP endpoints) │
└─────────────────────────────────────┘
```
### Event System Architecture
```
Publisher → EventBus → [Filters] → Subscribers
[History]
Statistics
```
---
## Backward Compatibility
All changes maintain 100% backward compatibility:
- ✅ No public API changes
- ✅ All existing imports work
- ✅ Re-exports maintained
- ✅ Default behavior unchanged
- ✅ Existing plugins unaffected
---
## Testing & Verification
### Syntax Validation
```bash
✓ python3 -m py_compile core/__init__.py
✓ python3 -m py_compile core/base_plugin.py
✓ python3 -m py_compile core/event_bus.py
✓ python3 -m py_compile core/settings.py
```
### Import Tests
```python
# Core imports
from core import get_event_bus, get_nexus_api, EventBus
from core.base_plugin import BasePlugin
from core.event_bus import LootEvent, DamageEvent
from core.settings import get_settings
# API imports
from core.api import get_api, get_widget_api, get_external_api
# Plugin imports
from plugins import BasePlugin
from plugins.base_plugin import BasePlugin
```
---
## Documentation Created
### Core Module README
- Module structure overview
- Quick start guides
- Service architecture explanation
- Best practices
- Version history
### Docstrings Added
- Module-level docstrings: 8
- Class docstrings: 15+
- Method docstrings: 100+
- Total documentation lines: ~500+
---
## Statistics
| Metric | Value |
|--------|-------|
| Files modified | 8 |
| Total lines | 2,538 |
| Type hints added | 200+ |
| Docstrings added | 100+ |
| Documentation lines | 500+ |
| Backward compatibility | 100% |
| Syntax errors | 0 |
---
## Recommendations for Future
### Immediate (High Priority)
1. Add type hints to remaining core modules:
- `nexus_api.py` (~600 lines)
- `http_client.py` (~500 lines)
- `data_store.py` (~500 lines)
2. Create unit tests for:
- EventBus functionality
- Settings persistence
- BasePlugin lifecycle
### Short-term (Medium Priority)
3. Clean up duplicate files:
- Consolidate OCR service versions
- Remove *_vulnerable.py files
- Merge optimized versions
4. Create additional documentation:
- Plugin development guide
- API cookbook with examples
- Troubleshooting guide
### Long-term (Low Priority)
5. Performance optimizations:
- Profile critical paths
- Optimize hot loops
- Add caching where appropriate
6. Additional features:
- Plugin dependency resolution
- Hot-reload for plugins
- Plugin marketplace integration
---
## Conclusion
The EU-Utility codebase has been successfully cleaned and refactored with:
- ✅ Comprehensive documentation
- ✅ Full type safety
- ✅ Clean architecture
- ✅ Standards compliance
- ✅ Backward compatibility
The codebase is now well-positioned for:
- Easier maintenance
- Better developer onboarding
- Improved IDE support
- Safer refactoring
- Future feature development
---
## Deliverables Checklist
- [x] Clean, organized codebase
- [x] Well-documented modules
- [x] Type-hinted throughout
- [x] Optimized performance (no regressions)
- [x] Standards compliant
- [x] Backward compatible
- [x] Syntax validated
- [x] Documentation created
---
**Report Generated:** 2026-02-15
**Refactoring Complete:** ✅

View File

@ -1,118 +0,0 @@
# EU-Utility v2.1.0 Release Notes
**Release Date:** 2026-02-14
**Version:** 2.1.0
**Codename:** "Nexus"
---
## 🎉 What's New
### 7 New Plugins
1. **Session Exporter** - Export hunting/mining sessions to CSV/JSON
2. **Price Alert System** - Monitor Nexus API prices with notifications
3. **Auto-Screenshot** - Capture screen on Global/HOF automatically
4. **Discord Rich Presence** - Show EU activity in Discord status
5. **Import/Export Tool** - Universal data backup and restore
6. **Analytics Dashboard** - Usage tracking and performance monitoring
7. **Auto-Updater** - Automatic update checking and installation
### New Core Systems
- **Theme System** - Dark, Light, and EU Classic themes
- **Logging System** - Structured logging with rotation
- **Security Hardening** - Path validation, input sanitization
- **CI/CD Pipeline** - GitHub Actions for testing
---
## 📊 Statistics
- **Total Plugins:** 31 (24 + 7 new)
- **Core Services:** 12
- **Test Coverage:** 75%
- **Documentation:** 15 files
- **Lines of Code:** ~25,000
- **Git Commits:** 6 major
---
## 🔒 Security Improvements
- Path traversal vulnerabilities patched
- Input sanitization on all user inputs
- URL validation in HTTP client
- Clipboard size limits
- Plugin sandboxing improvements
---
## 🚀 Performance Enhancements
- OCR lazy loading (faster startup)
- Database query optimization
- Memory leak fixes
- UI rendering improvements
- Background task efficiency
---
## 📚 Documentation
- Complete API Reference
- Plugin Development Guide
- User Manual
- Troubleshooting Guide
- FAQ (50+ questions)
- API Cookbook
- Migration Guide
- Security Hardening Guide
---
## 🐛 Bug Fixes
- Windows compatibility improvements
- Cross-platform file locking
- Plugin loading reliability
- Error handling robustness
- Memory management fixes
---
## 🎯 Known Issues
1. **Linux:** Window manager features limited
2. **macOS:** Not officially supported
3. **OCR:** First run downloads models (slow)
---
## 🔄 Upgrade Notes
### From v2.0
1. Backup your data (automatic on update)
2. Run Auto-Updater or `git pull`
3. Install new dependencies: `pip install -r requirements.txt`
4. Restart EU-Utility
### From Other Tools
See [Migration Guide](docs/MIGRATION_GUIDE.md)
---
## 🙏 Contributors
- **LemonNexus** - Lead Developer
- **Community** - Testing and feedback
---
## 📥 Download
- **Windows:** EU-Utility-Windows.zip
- **Linux:** EU-Utility-Linux.zip
- **Source:** `git clone https://github.com/ImpulsiveFPS/EU-Utility.git`
---
*Full changelog available in CHANGELOG.md*

55
ROADMAP.md Normal file
View File

@ -0,0 +1,55 @@
# EU-Utility Improvement Roadmap
## Immediate Priorities
### 1. Code Cleanup
- [ ] Remove duplicate files (screenshot_vulnerable.py, data_store_vulnerable.py)
- [ ] Consolidate multiple screenshot implementations into one secure version
- [ ] Remove or archive test plugins from main codebase
- [ ] Clean up root directory (too many markdown files)
### 2. Project Structure
- [ ] Create proper package structure with `__init__.py` files
- [ ] Organize core/ into logical subpackages (api, services, ui, utils)
- [ ] Move all plugins to plugins/builtin/
- [ ] Separate tests into unit, integration, and e2e
### 3. Documentation Consolidation
- [ ] Merge BUG_FIXES_APPLIED_DETAILED.md + BUG_FIX_REPORT.md → CHANGELOG.md
- [ ] Merge multiple refactoring reports into one
- [ ] Create single comprehensive README.md
- [ ] Archive old/duplicate documentation
### 4. Code Quality
- [ ] Add type hints to all public APIs
- [ ] Add comprehensive docstrings
- [ ] Fix any PEP8 violations
- [ ] Add proper error handling
### 5. Modern Python Packaging
- [ ] Create proper setup.py with all metadata
- [ ] Add pyproject.toml with build configuration
- [ ] Create MANIFEST.in for package distribution
- [ ] Separate requirements.txt into runtime and dev
### 6. CI/CD
- [ ] Set up GitHub Actions for automated testing
- [ ] Add code quality checks (black, flake8, mypy)
- [ ] Automated releases on tag push
### 7. Testing
- [ ] Ensure all existing tests pass
- [ ] Add missing test coverage
- [ ] Create integration test suite
- [ ] Add performance benchmarks
## Completed
- [x] Created QUALITY_STANDARDS.md
- [x] Spawned sub-agents for refactoring, docs, and packaging
## In Progress
- [ ] Code refactoring (eu-utility-refactor agent)
- [ ] Documentation improvements (eu-utility-docs agent)
- [ ] Packaging setup (eu-utility-packaging agent)

View File

@ -1,268 +0,0 @@
# EU-Utility Security Audit Report
**Date:** 2026-02-14
**Auditor:** Security Auditor Agent
**Scope:** `/home/impulsivefps/.openclaw/workspace/projects/EU-Utility/`
---
## Executive Summary
The EU-Utility codebase contains **several security vulnerabilities**, primarily around **path traversal**, **insufficient input validation**, and **unsafe plugin loading**. A hardened version exists for some components (data_store_secure.py, screenshot_secure.py) but the original vulnerable versions are still in use.
**Overall Risk Level:** MEDIUM-HIGH
---
## Findings
### 🔴 CRITICAL: Path Traversal in data_store.py
**File:** `core/data_store.py`
**Severity:** HIGH
**Status:** ⚠️ VULNERABLE (Secure version exists but unused)
**Issue:** The `_get_plugin_file()` method uses simple string replacement for sanitization:
```python
def _get_plugin_file(self, plugin_id: str) -> Path:
safe_name = plugin_id.replace(".", "_").replace("/", "_").replace("\\", "_")
return self.data_dir / f"{safe_name}.json"
```
**Attack Vector:** A malicious plugin could use `plugin_id="../../../etc/passwd"` to escape the data directory.
**Fix:** Replace with `data_store_secure.py` which includes:
- Proper path validation using `PathValidator`
- Resolved path verification against base path
- Security error handling
---
### 🔴 HIGH: Path Traversal in screenshot.py
**File:** `core/screenshot.py`
**Severity:** HIGH
**Status:** ⚠️ VULNERABLE (Secure version exists but unused)
**Issue:** The `save_screenshot()` method accepts arbitrary filenames without validation:
```python
def save_screenshot(self, image: Image.Image, filename: Optional[str] = None) -> Path:
if filename is None:
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S_%f")[:-3]
filename = f"screenshot_{timestamp}.{self._format.lower()}"
# NO VALIDATION HERE
filepath = self._save_path / filename
image.save(filepath, ...)
```
**Attack Vector:** A plugin could call `save_screenshot(image, "../../../malware.exe")` to write outside the screenshots directory.
**Fix:** Replace with `screenshot_secure.py` which includes:
- `PathValidator.sanitize_filename()` usage
- Resolved path verification
- Security error handling
---
### 🟡 MEDIUM: Insufficient HTTP Client Security
**File:** `core/http_client.py`
**Severity:** MEDIUM
**Status:** ⚠️ PARTIALLY VULNERABLE
**Issues:**
1. No SSL certificate verification control
2. `post()` method allows caching of POST requests (unusual/unsafe)
3. No URL scheme validation (could allow `file://` protocol)
**Recommendations:**
- Always verify SSL certificates
- Add URL scheme whitelist (`http://`, `https://`)
- Disable caching for POST by default
---
### 🟡 MEDIUM: Unvalidated Clipboard Storage
**File:** `core/clipboard.py`
**Severity:** MEDIUM
**Status:** ⚠️ VULNERABLE
**Issues:**
1. No maximum length validation for clipboard text
2. No sanitization before saving to history file
3. History file stored without encryption
**Attack Vector:** A malicious actor could copy extremely large text (GBs) causing DoS via memory exhaustion.
**Recommendations:**
- Add max length limits (e.g., 10KB per entry, 1000 entries max)
- Sanitize text before storage
- Consider encrypting sensitive clipboard history
---
### 🟠 HIGH: Unsafe Plugin Loading
**File:** `core/plugin_manager.py`
**Severity:** HIGH
**Status:** ⚠️ VULNERABLE
**Issues:**
1. Uses `exec_module()` which executes arbitrary Python code
2. No signature verification for plugins
3. No sandboxing or permission system
4. No validation of plugin metadata
**Attack Vector:** A malicious plugin in the `user_plugins` directory could execute arbitrary code with user privileges.
**Recommendations:**
- Implement plugin signature verification
- Add permission manifest system for plugins
- Consider using restricted Python execution environment
- Validate plugin metadata against schema
---
### 🟡 LOW: Subprocess Usage
**Files:** Multiple (window_manager.py, notifications.py, spotify_controller.py, game_reader.py)
**Severity:** LOW
**Status:** ✅ GENERALLY SAFE
**Analysis:** Subprocess usage found but:
- Uses hardcoded, safe commands
- No user input passed to shell commands
- Timeout protections in place
**No immediate action required** but continue to audit any new subprocess additions.
---
### 🟢 LOW: No Hardcoded Credentials Found
**Status:** ✅ PASS
Searched for:
- API keys
- Passwords
- Authentication tokens
- Secret keys
None found in the codebase. Good security practice maintained.
---
## Security Improvements Made
### 1. data_store_secure.py (EXISTS)
- Path traversal protection via `PathValidator`
- Input validation for plugin IDs and keys
- Data structure validation
- Secure backup path validation
### 2. screenshot_secure.py (EXISTS)
- Filename sanitization
- Path resolution validation
- Region coordinate validation
- Window handle validation
### 3. security_utils.py (EXISTS)
- `PathValidator` class for path sanitization
- `InputValidator` class for input validation
- `DataValidator` class for data structure validation
- `IntegrityChecker` for HMAC/hash operations
---
## Recommendations
### Immediate Actions (High Priority)
1. **Replace vulnerable modules with secure versions:**
```bash
mv core/data_store.py core/data_store_vulnerable.py
mv core/data_store_secure.py core/data_store.py
mv core/screenshot.py core/screenshot_vulnerable.py
mv core/screenshot_secure.py core/screenshot.py
```
2. **Add clipboard validation:**
- Implement max text length limits
- Sanitize clipboard content
3. **Implement plugin security:**
- Add plugin signature verification
- Create permission manifest system
### Medium Priority
4. **Enhance HTTP client:**
- Add URL scheme validation
- Enable SSL verification by default
- Add request/response size limits
5. **Add audit logging:**
- Log all file operations outside data directories
- Log plugin loading/unloading
- Log security violations
### Low Priority
6. **Implement data encryption:**
- Encrypt sensitive plugin data at rest
- Encrypt clipboard history
7. **Add rate limiting:**
- Rate limit screenshot captures
- Rate limit API calls per plugin
---
## Security Test Cases
```python
# Test Path Traversal Protection
def test_path_traversal():
# Should raise SecurityError
try:
data_store._get_plugin_file("../../../etc/passwd")
assert False, "Path traversal not blocked!"
except SecurityError:
pass # Expected
# Test Filename Sanitization
def test_filename_sanitization():
# Should sanitize dangerous characters
safe = PathValidator.sanitize_filename("../../../test.txt")
assert ".." not in safe
assert "/" not in safe
# Test Input Validation
def test_clipboard_limits():
# Should reject oversized input
large_text = "x" * (10 * 1024 * 1024) # 10MB
result = clipboard_manager.copy(large_text)
assert result == False # Should fail
```
---
## Conclusion
The EU-Utility project has a solid security foundation with `security_utils.py` providing comprehensive validation utilities. However, the **original vulnerable modules are still in use** instead of the hardened versions.
**Priority 1:** Switch to the secure versions of data_store and screenshot modules.
**Priority 2:** Implement plugin sandboxing and signature verification.
With these changes, the project risk level can be reduced from MEDIUM-HIGH to LOW-MEDIUM.
---
*Report generated by Security Auditor Agent*
*EU-Utility Security Audit 2026*

View File

@ -1,319 +0,0 @@
# EU-Utility Security Fixes Applied
**Date:** 2026-02-14
**Auditor:** Security Auditor Agent
---
## Summary
This document details the security fixes applied during the security audit of EU-Utility.
**Modules Fixed:** 4
**Security Improvements:** 15+
**Risk Level Reduced:** MEDIUM-HIGH → LOW-MEDIUM
---
## Fixes Applied
### 1. ✅ data_store.py - Path Traversal Protection
**Action:** Replaced vulnerable module with secure version
**Changes:**
- Backup created: `data_store_vulnerable.py`
- Active module now: `data_store_secure.py`
**Security Features Added:**
- Path validation using `PathValidator` class
- Resolved path verification against base directory
- Plugin ID validation (type checking, empty checks)
- Key validation against dangerous patterns
- Data structure validation before save
- Backup path traversal protection
**Code Example:**
```python
def _get_plugin_file(self, plugin_id: str) -> Path:
# Validate plugin_id
if not isinstance(plugin_id, str):
raise SecurityError("plugin_id must be a string")
# Sanitize and validate
safe_name = PathValidator.sanitize_filename(plugin_id, '_')
if '..' in safe_name or '/' in safe_name or '\\' in safe_name:
raise SecurityError(f"Invalid characters in plugin_id: {plugin_id}")
# Verify resolved path is within base directory
file_path = self.data_dir / f"{safe_name}.json"
resolved_path = file_path.resolve()
if not str(resolved_path).startswith(str(self._base_path)):
raise SecurityError(f"Path traversal detected: {plugin_id}")
return file_path
```
---
### 2. ✅ screenshot.py - Filename Sanitization & Path Validation
**Action:** Replaced vulnerable module with secure version
**Changes:**
- Backup created: `screenshot_vulnerable.py`
- Active module now: `screenshot_secure.py`
**Security Features Added:**
- Filename sanitization using `PathValidator.sanitize_filename()`
- Path resolution validation against base save path
- Region coordinate validation (prevent DoS via huge regions)
- Window handle validation (type and value checking)
- Dimension sanity checks
**Code Example:**
```python
def save_screenshot(self, image: Image.Image, filename: Optional[str] = None) -> Path:
# Sanitize filename
safe_filename = PathValidator.sanitize_filename(filename, '_')
filepath = self._save_path / safe_filename
# Security check: ensure resolved path is within save_path
try:
resolved_path = filepath.resolve()
if not str(resolved_path).startswith(str(self._base_save_path)):
raise SecurityError("Path traversal detected in filename")
except (OSError, ValueError) as e:
# Fallback to safe default
safe_filename = f"screenshot_{int(time.time())}.{self._format.lower()}"
filepath = self._save_path / safe_filename
```
---
### 3. ✅ clipboard.py - Input Validation & Size Limits
**Action:** Enhanced with security validation
**Security Features Added:**
- Maximum text length limit (10KB per entry)
- Maximum total storage limit (1MB)
- Null byte detection and rejection
- Text sanitization (control character removal)
- Source string length limiting
- Secure file permissions (0o600 - owner only)
- Temporary file atomic write pattern
**Code Example:**
```python
def _validate_text(self, text: str) -> tuple[bool, str]:
if not isinstance(text, str):
return False, "Text must be a string"
if '\x00' in text:
return False, "Text contains null bytes"
if len(text) > self._max_text_length:
return False, f"Text exceeds maximum length"
# Auto-cleanup to make room
current_size = sum(len(entry.text) for entry in self._history)
if current_size + len(text) > self._max_total_storage:
while self._history and current_size + len(text) > self._max_total_storage:
removed = self._history.popleft()
current_size -= len(removed.text)
return True, ""
def _save_history(self):
# Write with restricted permissions
temp_path = self._history_file.with_suffix('.tmp')
with open(temp_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2)
os.chmod(temp_path, 0o600) # Owner read/write only
temp_path.replace(self._history_file)
```
---
### 4. ✅ http_client.py - URL Validation & SSRF Protection
**Action:** Enhanced with security validation
**Security Features Added:**
- URL scheme validation (only http:// and https:// allowed)
- Path traversal pattern detection (`..`, `@`, `\`, null bytes)
- SSRF protection (blocks private, loopback, reserved, link-local IPs)
- Custom exception classes for security errors
- URL validation on both GET and POST requests
**Code Example:**
```python
def _validate_url(self, url: str) -> str:
if not url:
raise URLSecurityError("URL cannot be empty")
from urllib.parse import urlparse
parsed = urlparse(url)
# Check scheme
allowed_schemes = {'http', 'https'}
if parsed.scheme not in allowed_schemes:
raise URLSecurityError(f"URL scheme '{parsed.scheme}' not allowed")
# Check for dangerous patterns
dangerous_patterns = ['..', '@', '\\', '\x00']
for pattern in dangerous_patterns:
if pattern in url:
raise URLSecurityError(f"URL contains dangerous pattern")
# SSRF protection - block private IPs
hostname = parsed.hostname
if hostname:
try:
ip = ipaddress.ip_address(hostname)
if ip.is_private or ip.is_loopback or ip.is_reserved:
raise URLSecurityError(f"URL resolves to restricted IP")
except ValueError:
pass # Not an IP, it's a hostname
return url
```
---
## Files Modified
| File | Action | Status |
|------|--------|--------|
| `core/data_store.py` | Replaced with secure version | ✅ Fixed |
| `core/data_store_vulnerable.py` | Backup created | ✅ Archived |
| `core/screenshot.py` | Replaced with secure version | ✅ Fixed |
| `core/screenshot_vulnerable.py` | Backup created | ✅ Archived |
| `core/clipboard.py` | Enhanced with validation | ✅ Fixed |
| `core/http_client.py` | Added URL validation | ✅ Fixed |
---
## Remaining Recommendations
### Medium Priority (Not Yet Implemented)
1. **Plugin Manager Security**
- Implement plugin signature verification
- Add permission manifest system
- Consider sandboxed execution environment
2. **Audit Logging**
- Log all security violations
- Log plugin loading/unloading
- Log file operations outside data directories
### Low Priority (Future Enhancements)
3. **Data Encryption**
- Encrypt sensitive plugin data at rest
- Encrypt clipboard history with user password
4. **Rate Limiting**
- Per-plugin API rate limits
- Screenshot capture rate limiting
---
## Testing Security Fixes
### Path Traversal Test
```python
from core.data_store import get_data_store
data_store = get_data_store()
# Should raise SecurityError
try:
data_store.save("../../../etc/passwd", "key", "data")
except SecurityError:
print("✅ Path traversal blocked in data_store")
# Should raise SecurityError
try:
from core.screenshot import get_screenshot_service
service = get_screenshot_service()
service.save_screenshot(image, "../../../malware.exe")
except SecurityError:
print("✅ Path traversal blocked in screenshot")
```
### URL Validation Test
```python
from core.http_client import HTTPClient, URLSecurityError
client = HTTPClient()
# Should raise URLSecurityError
try:
client.get("file:///etc/passwd")
except URLSecurityError:
print("✅ File protocol blocked")
try:
client.get("http://127.0.0.1/admin")
except URLSecurityError:
print("✅ Localhost blocked (SSRF protection)")
try:
client.get("http://192.168.1.1/admin")
except URLSecurityError:
print("✅ Private IP blocked (SSRF protection)")
```
### Clipboard Validation Test
```python
from core.clipboard import get_clipboard_manager
clipboard = get_clipboard_manager()
# Should fail - too large
result = clipboard.copy("x" * (11 * 1024)) # 11KB
assert result == False, "Should reject oversized text"
print("✅ Size limit enforced")
# Should sanitize null bytes
result = clipboard.copy("hello\x00world")
assert result == False, "Should reject null bytes"
print("✅ Null byte protection working")
```
---
## Security Checklist
- [x] Path traversal protection (data_store)
- [x] Path traversal protection (screenshot)
- [x] Filename sanitization
- [x] Input validation (clipboard)
- [x] Size limits (clipboard)
- [x] URL validation (http_client)
- [x] SSRF protection (http_client)
- [x] Secure file permissions
- [x] Atomic file writes
- [x] Data structure validation
- [ ] Plugin signature verification (future)
- [ ] Plugin sandboxing (future)
- [ ] Audit logging (future)
- [ ] Data encryption (future)
---
## Contact
For questions about these security fixes, refer to:
- `SECURITY_AUDIT_REPORT.md` - Full audit report
- `core/security_utils.py` - Security utility classes
---
*Security fixes applied by Security Auditor Agent*
*EU-Utility Security Hardening 2026*

View File

@ -1,96 +0,0 @@
# UI/UX Excellence Transformation - Summary
## Overview
Successfully transformed EU-Utility's UI to be professional, polished, and completely emoji-free.
## Changes Made
### 1. SVG Icons Created (assets/icons/)
- `dashboard.svg` - Grid-based dashboard icon
- `plugins.svg` - Monitor/plugins icon
- `widgets.svg` - Widget grid icon
- `settings.svg` - Gear/cog icon (Phosphor style)
- `search.svg` - Magnifying glass icon
- `clock.svg` - Clock/time icon
- `menu.svg` - Hamburger menu icon
- `close.svg` - X close icon
- `minimize.svg` - Minimize dash icon
- `pin.svg` - Star/pin icon
- `check.svg` - Checkmark icon
- `warning.svg` - Triangle warning icon
- `info.svg` - Information circle icon
- `more.svg` - Three dots/more icon
All icons are:
- 32px optimized SVG files
- Monochrome (white stroke)
- Phosphor Icons style
- Consistent 2px stroke width
- Clean, minimalist design
### 2. Files Updated - Emojis Removed
#### core/perfect_ux.py
- NavigationRail: Replaced text emojis (◆, 🔌, 🎨, ⚙️) with SVG icons
- Button: Added icon support via `icon` parameter
- Activity items: Replaced emoji status icons with SVG icons (check, info, more)
- Quick Actions: Replaced emoji buttons with SVG icons (camera, package, globe, bar-chart)
- Added glassmorphism effects to Surface component
- Orange left border (3px) on active navigation items
- EU color scheme: dark blue (#141f23), orange accent (#ff8c42)
#### core/activity_bar.py
- Start button: Replaced ⊞ emoji with grid SVG icon
- Plugin buttons: Now use icon_name attribute with SVG icons
- Clock: Added clock SVG icon next to time display
- Context menu: Removed emoji from "Settings" and "Hide" actions
- Drawer items: Now display plugin icons
#### core/ui/dashboard_view.py
- Header: Added dashboard SVG icon
- Plugin Store button: Replaced 🔌 emoji with shopping-bag SVG icon
- Plugin Widgets section: Removed 🔌 emoji from label
- Updated styling to match EU color palette
#### core/ui/search_view.py
- Header: Added search SVG icon, removed 🔍 emoji
- Hint text: Removed 💡 emoji
- Updated border colors to use EU orange accent
#### core/ui/settings_view.py
- Header: Added settings SVG icon, removed ⚙️ emoji
- Tab labels: Removed emojis (🔌, 📦, ⌨️, 💾, 🔄)
- Button labels: Removed emojis (📤, 📥, 🗑, 🔍)
- Updated styling to EU color palette
### 3. Design System Improvements
#### Color Palette
- Primary Background: #141f23 (EU dark blue)
- Accent Color: #ff8c42 (EU orange)
- Surface: rgba(20, 31, 35, 0.95) with glassmorphism
- Borders: rgba(255, 140, 66, 0.1) subtle orange tint
#### Spacing (8dp Grid)
- XS: 4px
- S: 8px
- M: 16px
- L: 24px
- XL: 32px
#### Active State Indicators
- Orange left border (3px) on active navigation items
- Background highlight on hover
- Smooth transitions (150-350ms)
### 4. Integration
- All UI components now use `icon_manager.get_icon()` and `icon_manager.get_pixmap()`
- Consistent 24px icon size in navigation
- Consistent 20px icon size in buttons
- Tooltips for all icon-only buttons
## Verification
- All main UI files are now emoji-free
- 50 total SVG icons in assets/icons/
- Professional, consistent appearance throughout
- EU brand colors applied consistently

View File

@ -1,7 +1,4 @@
# EU-Utility Core Package
"""
EU-Utility Core Package
=======================
"""EU-Utility Core Package.
This package contains the core functionality for EU-Utility, including:
- Plugin management and base classes
@ -11,7 +8,6 @@ This package contains the core functionality for EU-Utility, including:
- Data persistence and settings
Quick Start:
------------
from core.api import get_api
from core.event_bus import get_event_bus, LootEvent
@ -19,130 +15,135 @@ Quick Start:
bus = get_event_bus()
Architecture:
-------------
- **api/**: Three-tier API system (PluginAPI, WidgetAPI, ExternalAPI)
- **services/**: Core services (OCR, screenshot, audio, etc.)
- **ui/**: UI components and views
- **utils/**: Utility modules (styles, security, etc.)
- **api/**: Three-tier API system (PluginAPI, WidgetAPI, ExternalAPI)
- **services/**: Core services (OCR, screenshot, audio, etc.)
- **ui/**: UI components and views
- **utils/**: Utility modules (styles, security, etc.)
See individual modules for detailed documentation.
"""
from __future__ import annotations
__version__ = "2.1.0"
# Safe imports (no PyQt6 dependency)
from core.nexus_api import NexusAPI, get_nexus_api
from core.nexus_api import EntityType, SearchResult, ItemDetails, MarketData
from core.log_reader import LogReader, get_log_reader
from core.event_bus import (
get_event_bus,
EventBus,
EventCategory,
BaseEvent,
SkillGainEvent,
LootEvent,
DamageEvent,
GlobalEvent,
ChatEvent,
EconomyEvent,
SystemEvent,
)
# Data Store (SQLite)
from core.data import (
SQLiteDataStore,
get_sqlite_store,
PluginState,
UserPreference,
SessionData,
)
# Dashboard Widgets
from core.widgets import (
DashboardWidget,
SystemStatusWidget,
QuickActionsWidget,
RecentActivityWidget,
PluginGridWidget,
WidgetGallery,
DashboardWidgetManager,
WIDGET_TYPES,
create_widget,
)
# Enhanced Components
from core.dashboard_enhanced import (
EnhancedDashboard,
DashboardContainer,
DashboardManager,
get_dashboard_manager,
)
from core.activity_bar_enhanced import (
EnhancedActivityBar,
AppDrawer,
PinnedPluginsArea,
get_activity_bar,
)
from core.ui.settings_panel import (
EnhancedSettingsPanel,
EnhancedSettingsView,
)
# Version info
VERSION = __version__
API_VERSION = "2.2"
# Data Store (SQLite)
# Safe imports (no PyQt6 dependency)
from core.nexus_api import (
EntityType,
ItemDetails,
MarketData,
NexusAPI,
SearchResult,
get_nexus_api,
)
from core.log_reader import LogReader, get_log_reader
from core.event_bus import (
BaseEvent,
ChatEvent,
DamageEvent,
EconomyEvent,
EventBus,
EventCategory,
GlobalEvent,
LootEvent,
SkillGainEvent,
SystemEvent,
get_event_bus,
)
from core.data import (
SQLiteDataStore,
get_sqlite_store,
PluginState,
UserPreference,
SessionData,
SQLiteDataStore,
UserPreference,
get_sqlite_store,
)
from core.security_utils import (
DataValidator,
InputValidator,
PathValidator,
SecurityError,
)
# Dashboard Widgets
from core.widgets import (
# Lazy imports for Qt-dependent components
# Use functions to avoid importing PyQt6 at module load time
def _get_widgets():
"""Lazy load widget components."""
from core.widgets import (
DashboardWidget,
SystemStatusWidget,
DashboardWidgetManager,
PluginGridWidget,
QuickActionsWidget,
RecentActivityWidget,
PluginGridWidget,
SystemStatusWidget,
WidgetGallery,
DashboardWidgetManager,
WIDGET_TYPES,
create_widget,
)
)
return {
'DashboardWidget': DashboardWidget,
'DashboardWidgetManager': DashboardWidgetManager,
'PluginGridWidget': PluginGridWidget,
'QuickActionsWidget': QuickActionsWidget,
'RecentActivityWidget': RecentActivityWidget,
'SystemStatusWidget': SystemStatusWidget,
'WidgetGallery': WidgetGallery,
'WIDGET_TYPES': WIDGET_TYPES,
'create_widget': create_widget,
}
# Enhanced Components
from core.dashboard_enhanced import (
EnhancedDashboard,
def _get_dashboard():
"""Lazy load dashboard components."""
from core.dashboard_enhanced import (
DashboardContainer,
DashboardManager,
EnhancedDashboard,
get_dashboard_manager,
)
)
return {
'DashboardContainer': DashboardContainer,
'DashboardManager': DashboardManager,
'EnhancedDashboard': EnhancedDashboard,
'get_dashboard_manager': get_dashboard_manager,
}
from core.activity_bar_enhanced import (
EnhancedActivityBar,
def _get_activity_bar():
"""Lazy load activity bar components."""
from core.activity_bar import (
AppDrawer,
EnhancedActivityBar,
PinnedPluginsArea,
get_activity_bar,
)
)
return {
'AppDrawer': AppDrawer,
'EnhancedActivityBar': EnhancedActivityBar,
'PinnedPluginsArea': PinnedPluginsArea,
'get_activity_bar': get_activity_bar,
}
from core.ui.settings_panel import (
def _get_settings_panel():
"""Lazy load settings panel components."""
from core.ui.settings_panel import (
EnhancedSettingsPanel,
EnhancedSettingsView,
)
)
return {
'EnhancedSettingsPanel': EnhancedSettingsPanel,
'EnhancedSettingsView': EnhancedSettingsView,
}
__all__ = [
# Version
'VERSION',
'API_VERSION',
# Nexus API
'NexusAPI',
'get_nexus_api',
@ -150,11 +151,9 @@ __all__ = [
'SearchResult',
'ItemDetails',
'MarketData',
# Log Reader
'LogReader',
'get_log_reader',
# Event Bus
'get_event_bus',
'EventBus',
@ -167,34 +166,20 @@ __all__ = [
'ChatEvent',
'EconomyEvent',
'SystemEvent',
# Data Store
'SQLiteDataStore',
'get_sqlite_store',
'PluginState',
'UserPreference',
'SessionData',
# Dashboard Widgets
'DashboardWidget',
'SystemStatusWidget',
'QuickActionsWidget',
'RecentActivityWidget',
'PluginGridWidget',
'WidgetGallery',
'DashboardWidgetManager',
'WIDGET_TYPES',
'create_widget',
# Enhanced Components
'EnhancedDashboard',
'DashboardContainer',
'DashboardManager',
'get_dashboard_manager',
'EnhancedActivityBar',
'AppDrawer',
'PinnedPluginsArea',
'get_activity_bar',
'EnhancedSettingsPanel',
'EnhancedSettingsView',
# Security
'PathValidator',
'InputValidator',
'DataValidator',
'SecurityError',
# Lazy loaders (documented but not directly exported)
'_get_widgets',
'_get_dashboard',
'_get_activity_bar',
'_get_settings_panel',
]

File diff suppressed because it is too large Load Diff

View File

@ -1,715 +0,0 @@
"""
EU-Utility - Enhanced Activity Bar
Windows 11-style taskbar with pinned plugins, app drawer, and search.
"""
import json
from pathlib import Path
from typing import Dict, List, Optional, Callable
from dataclasses import dataclass, asdict
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QFrame, QLineEdit, QMenu, QDialog, QSlider, QComboBox,
QCheckBox, QSpinBox, QApplication, QSizePolicy, QScrollArea,
QGridLayout, QMessageBox, QGraphicsDropShadowEffect
)
from PyQt6.QtCore import Qt, QPoint, QSize, QTimer, pyqtSignal, QPropertyAnimation, QEasingCurve
from PyQt6.QtGui import QMouseEvent, QPainter, QColor, QFont, QIcon, QPixmap, QDrag
from PyQt6.QtCore import QMimeData, QByteArray
from core.data.sqlite_store import get_sqlite_store
@dataclass
class ActivityBarConfig:
"""Activity bar configuration."""
enabled: bool = True
position: str = "bottom"
icon_size: int = 32
auto_hide: bool = False
auto_hide_delay: int = 3000
pinned_plugins: List[str] = None
def __post_init__(self):
if self.pinned_plugins is None:
self.pinned_plugins = []
def to_dict(self):
return {
'enabled': self.enabled,
'position': self.position,
'icon_size': self.icon_size,
'auto_hide': self.auto_hide,
'auto_hide_delay': self.auto_hide_delay,
'pinned_plugins': self.pinned_plugins
}
@classmethod
def from_dict(cls, data):
return cls(
enabled=data.get('enabled', True),
position=data.get('position', 'bottom'),
icon_size=data.get('icon_size', 32),
auto_hide=data.get('auto_hide', False),
auto_hide_delay=data.get('auto_hide_delay', 3000),
pinned_plugins=data.get('pinned_plugins', [])
)
class DraggablePluginButton(QPushButton):
"""Plugin button that supports drag-to-pin."""
drag_started = pyqtSignal(str)
def __init__(self, plugin_id: str, plugin_name: str, icon_text: str, parent=None):
super().__init__(parent)
self.plugin_id = plugin_id
self.plugin_name = plugin_name
self.icon_text = icon_text
self.setText(icon_text)
self.setFixedSize(40, 40)
self.setToolTip(plugin_name)
self._setup_style()
def _setup_style(self):
"""Setup button style."""
self.setStyleSheet("""
DraggablePluginButton {
background: transparent;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
}
DraggablePluginButton:hover {
background: rgba(255, 255, 255, 0.1);
}
DraggablePluginButton:pressed {
background: rgba(255, 255, 255, 0.05);
}
""")
self.setCursor(Qt.CursorShape.PointingHandCursor)
def mousePressEvent(self, event):
"""Start drag on middle click or with modifier."""
if event.button() == Qt.MouseButton.LeftButton:
self.drag_start_pos = event.pos()
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
"""Handle drag."""
if not (event.buttons() & Qt.MouseButton.LeftButton):
return
if not hasattr(self, 'drag_start_pos'):
return
# Check if dragged far enough
if (event.pos() - self.drag_start_pos).manhattanLength() < 10:
return
# Start drag
drag = QDrag(self)
mime_data = QMimeData()
mime_data.setText(self.plugin_id)
mime_data.setData('application/x-plugin-id', QByteArray(self.plugin_id.encode()))
drag.setMimeData(mime_data)
# Create drag pixmap
pixmap = QPixmap(40, 40)
pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap)
painter.setBrush(QColor(255, 140, 66, 200))
painter.drawRoundedRect(0, 0, 40, 40, 8, 8)
painter.setPen(Qt.GlobalColor.white)
painter.setFont(QFont("Segoe UI", 14))
painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, self.icon_text)
painter.end()
drag.setPixmap(pixmap)
drag.setHotSpot(QPoint(20, 20))
self.drag_started.emit(self.plugin_id)
drag.exec(Qt.DropAction.MoveAction)
class PinnedPluginsArea(QFrame):
"""Area for pinned plugins with drop support."""
plugin_pinned = pyqtSignal(str)
plugin_unpinned = pyqtSignal(str)
plugin_reordered = pyqtSignal(list) # New order of plugin IDs
def __init__(self, parent=None):
super().__init__(parent)
self.pinned_plugins: List[str] = []
self.buttons: Dict[str, DraggablePluginButton] = {}
self.setAcceptDrops(True)
self._setup_ui()
def _setup_ui(self):
"""Setup UI."""
self.setStyleSheet("background: transparent; border: none;")
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(4)
layout.addStretch()
def add_plugin(self, plugin_id: str, plugin_name: str, icon_text: str = ""):
"""Add a pinned plugin."""
if plugin_id in self.pinned_plugins:
return
self.pinned_plugins.append(plugin_id)
btn = DraggablePluginButton(plugin_id, plugin_name, icon_text)
btn.clicked.connect(lambda: self._on_plugin_clicked(plugin_id))
# Insert before stretch
layout = self.layout()
layout.insertWidget(layout.count() - 1, btn)
self.buttons[plugin_id] = btn
# Log
store = get_sqlite_store()
store.log_activity('ui', 'plugin_pinned', f"Plugin: {plugin_id}")
def remove_plugin(self, plugin_id: str):
"""Remove a pinned plugin."""
if plugin_id not in self.pinned_plugins:
return
self.pinned_plugins.remove(plugin_id)
if plugin_id in self.buttons:
btn = self.buttons[plugin_id]
self.layout().removeWidget(btn)
btn.deleteLater()
del self.buttons[plugin_id]
# Log
store = get_sqlite_store()
store.log_activity('ui', 'plugin_unpinned', f"Plugin: {plugin_id}")
def set_plugins(self, plugins: List[tuple]):
"""Set all pinned plugins."""
# Clear existing
for plugin_id in list(self.pinned_plugins):
self.remove_plugin(plugin_id)
# Add new
for plugin_id, plugin_name, icon_text in plugins:
self.add_plugin(plugin_id, plugin_name, icon_text)
def _on_plugin_clicked(self, plugin_id: str):
"""Handle plugin click."""
parent = self.window()
if parent and hasattr(parent, 'show_plugin'):
parent.show_plugin(plugin_id)
def dragEnterEvent(self, event):
"""Accept drag events."""
if event.mimeData().hasText():
event.acceptProposedAction()
def dropEvent(self, event):
"""Handle drop."""
plugin_id = event.mimeData().text()
self.plugin_pinned.emit(plugin_id)
event.acceptProposedAction()
class AppDrawer(QFrame):
"""App drawer popup with all plugins."""
plugin_launched = pyqtSignal(str)
plugin_pin_requested = pyqtSignal(str)
def __init__(self, plugin_manager, parent=None):
super().__init__(parent)
self.plugin_manager = plugin_manager
self.search_text = ""
self._setup_ui()
def _setup_ui(self):
"""Setup drawer UI."""
self.setWindowFlags(
Qt.WindowType.FramelessWindowHint |
Qt.WindowType.WindowStaysOnTopHint |
Qt.WindowType.Tool
)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.setFixedSize(420, 500)
# Frosted glass effect
self.setStyleSheet("""
AppDrawer {
background: rgba(32, 32, 32, 0.95);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
}
""")
# Shadow
shadow = QGraphicsDropShadowEffect()
shadow.setBlurRadius(30)
shadow.setColor(QColor(0, 0, 0, 100))
shadow.setOffset(0, 8)
self.setGraphicsEffect(shadow)
layout = QVBoxLayout(self)
layout.setContentsMargins(20, 20, 20, 20)
layout.setSpacing(15)
# Header
header = QLabel("All Plugins")
header.setStyleSheet("color: white; font-size: 18px; font-weight: bold;")
layout.addWidget(header)
# Search box
self.search_box = QLineEdit()
self.search_box.setPlaceholderText("🔍 Search plugins...")
self.search_box.setStyleSheet("""
QLineEdit {
background: rgba(255, 255, 255, 0.08);
color: white;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 10px 15px;
font-size: 14px;
}
QLineEdit:focus {
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 140, 66, 0.5);
}
""")
self.search_box.textChanged.connect(self._on_search)
layout.addWidget(self.search_box)
# Plugin grid
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.Shape.NoFrame)
scroll.setStyleSheet("background: transparent; border: none;")
self.grid_widget = QWidget()
self.grid_layout = QGridLayout(self.grid_widget)
self.grid_layout.setSpacing(10)
self.grid_layout.setContentsMargins(0, 0, 0, 0)
scroll.setWidget(self.grid_widget)
layout.addWidget(scroll)
self._refresh_plugins()
def _refresh_plugins(self):
"""Refresh plugin grid."""
# Clear existing
while self.grid_layout.count():
item = self.grid_layout.takeAt(0)
if item.widget():
item.widget().deleteLater()
if not self.plugin_manager:
return
all_plugins = self.plugin_manager.get_all_discovered_plugins()
# Filter by search
filtered = []
for plugin_id, plugin_class in all_plugins.items():
name = plugin_class.name.lower()
desc = plugin_class.description.lower()
search = self.search_text.lower()
if not search or search in name or search in desc:
filtered.append((plugin_id, plugin_class))
# Create items
cols = 3
for i, (plugin_id, plugin_class) in enumerate(filtered):
item = self._create_plugin_item(plugin_id, plugin_class)
row = i // cols
col = i % cols
self.grid_layout.addWidget(item, row, col)
self.grid_layout.setColumnStretch(cols, 1)
self.grid_layout.setRowStretch((len(filtered) // cols) + 1, 1)
def _create_plugin_item(self, plugin_id: str, plugin_class) -> QFrame:
"""Create a plugin item."""
frame = QFrame()
frame.setFixedSize(110, 110)
frame.setStyleSheet("""
QFrame {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
}
QFrame:hover {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
}
""")
frame.setCursor(Qt.CursorShape.PointingHandCursor)
layout = QVBoxLayout(frame)
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(6)
# Icon
icon = QLabel(getattr(plugin_class, 'icon', '📦'))
icon.setStyleSheet("font-size: 28px;")
icon.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(icon)
# Name
name = QLabel(plugin_class.name)
name.setStyleSheet("color: white; font-size: 11px; font-weight: bold;")
name.setAlignment(Qt.AlignmentFlag.AlignCenter)
name.setWordWrap(True)
layout.addWidget(name)
# Click handler
frame.mousePressEvent = lambda event, pid=plugin_id: self._on_plugin_clicked(pid)
# Context menu
frame.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
frame.customContextMenuRequested.connect(
lambda pos, pid=plugin_id: self._show_context_menu(pos, pid)
)
return frame
def _on_plugin_clicked(self, plugin_id: str):
"""Handle plugin click."""
self.plugin_launched.emit(plugin_id)
self.hide()
def _show_context_menu(self, pos, plugin_id: str):
"""Show context menu."""
menu = QMenu(self)
menu.setStyleSheet("""
QMenu {
background: rgba(40, 40, 40, 0.95);
color: white;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 8px;
}
QMenu::item {
padding: 8px 24px;
border-radius: 4px;
}
QMenu::item:selected {
background: rgba(255, 255, 255, 0.1);
}
""")
pin_action = menu.addAction("📌 Pin to Taskbar")
pin_action.triggered.connect(lambda: self.plugin_pin_requested.emit(plugin_id))
menu.exec(self.mapToGlobal(pos))
def _on_search(self, text: str):
"""Handle search."""
self.search_text = text
self._refresh_plugins()
class EnhancedActivityBar(QFrame):
"""Enhanced activity bar with drag-to-pin and search."""
plugin_requested = pyqtSignal(str)
search_requested = pyqtSignal(str)
settings_requested = pyqtSignal()
def __init__(self, plugin_manager, parent=None):
super().__init__(parent)
self.plugin_manager = plugin_manager
self.config = self._load_config()
self._setup_ui()
self._apply_config()
# Load pinned plugins
self._load_pinned_plugins()
def _setup_ui(self):
"""Setup activity bar UI."""
self.setWindowFlags(
Qt.WindowType.FramelessWindowHint |
Qt.WindowType.WindowStaysOnTopHint |
Qt.WindowType.Tool |
Qt.WindowType.WindowDoesNotAcceptFocus
)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.setFixedHeight(56)
# Main layout
layout = QHBoxLayout(self)
layout.setContentsMargins(12, 4, 12, 4)
layout.setSpacing(8)
# Style
self.setStyleSheet("""
EnhancedActivityBar {
background: rgba(30, 30, 35, 0.9);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 28px;
}
""")
# Start button
self.start_btn = QPushButton("")
self.start_btn.setFixedSize(40, 40)
self.start_btn.setStyleSheet("""
QPushButton {
background: rgba(255, 255, 255, 0.1);
color: white;
border: none;
border-radius: 8px;
font-size: 18px;
}
QPushButton:hover {
background: rgba(255, 255, 255, 0.2);
}
""")
self.start_btn.setToolTip("Open App Drawer")
self.start_btn.clicked.connect(self._toggle_drawer)
layout.addWidget(self.start_btn)
# Search box
self.search_box = QLineEdit()
self.search_box.setFixedSize(180, 36)
self.search_box.setPlaceholderText("Search...")
self.search_box.setStyleSheet("""
QLineEdit {
background: rgba(255, 255, 255, 0.08);
color: white;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 18px;
padding: 0 14px;
font-size: 13px;
}
QLineEdit:focus {
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 140, 66, 0.5);
}
""")
self.search_box.returnPressed.connect(self._on_search)
layout.addWidget(self.search_box)
# Separator
separator = QFrame()
separator.setFixedSize(1, 24)
separator.setStyleSheet("background: rgba(255, 255, 255, 0.1);")
layout.addWidget(separator)
# Pinned plugins area
self.pinned_area = PinnedPluginsArea()
self.pinned_area.plugin_pinned.connect(self._on_plugin_pinned)
self.pinned_area.setAcceptDrops(True)
layout.addWidget(self.pinned_area)
# Spacer
layout.addStretch()
# Clock
self.clock_label = QLabel("12:00")
self.clock_label.setStyleSheet("color: rgba(255, 255, 255, 0.7); font-size: 12px;")
self.clock_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(self.clock_label)
# Settings button
self.settings_btn = QPushButton("⚙️")
self.settings_btn.setFixedSize(36, 36)
self.settings_btn.setStyleSheet("""
QPushButton {
background: transparent;
color: rgba(255, 255, 255, 0.7);
border: none;
border-radius: 6px;
font-size: 14px;
}
QPushButton:hover {
background: rgba(255, 255, 255, 0.1);
color: white;
}
""")
self.settings_btn.setToolTip("Settings")
self.settings_btn.clicked.connect(self.settings_requested.emit)
layout.addWidget(self.settings_btn)
# Clock timer
self.clock_timer = QTimer(self)
self.clock_timer.timeout.connect(self._update_clock)
self.clock_timer.start(60000)
self._update_clock()
# Auto-hide timer
self.hide_timer = QTimer(self)
self.hide_timer.timeout.connect(self.hide)
# Drawer
self.drawer = None
# Enable drag-drop
self.setAcceptDrops(True)
def _toggle_drawer(self):
"""Toggle app drawer."""
if self.drawer is None:
self.drawer = AppDrawer(self.plugin_manager, self)
self.drawer.plugin_launched.connect(self.plugin_requested.emit)
self.drawer.plugin_pin_requested.connect(self._pin_plugin)
if self.drawer.isVisible():
self.drawer.hide()
else:
# Position drawer
bar_pos = self.pos()
if self.config.position == "bottom":
self.drawer.move(bar_pos.x(), bar_pos.y() - self.drawer.height() - 10)
else:
self.drawer.move(bar_pos.x(), bar_pos.y() + self.height() + 10)
self.drawer.show()
self.drawer.raise_()
def _on_search(self):
"""Handle search."""
text = self.search_box.text().strip()
if text:
self.search_requested.emit(text)
# Log
store = get_sqlite_store()
store.log_activity('ui', 'search', f"Query: {text}")
def _on_plugin_pinned(self, plugin_id: str):
"""Handle plugin pin."""
self._pin_plugin(plugin_id)
def _pin_plugin(self, plugin_id: str):
"""Pin a plugin to the activity bar."""
if not self.plugin_manager:
return
all_plugins = self.plugin_manager.get_all_discovered_plugins()
if plugin_id not in all_plugins:
return
plugin_class = all_plugins[plugin_id]
if plugin_id not in self.config.pinned_plugins:
self.config.pinned_plugins.append(plugin_id)
self._save_config()
icon_text = getattr(plugin_class, 'icon', '')
self.pinned_area.add_plugin(plugin_id, plugin_class.name, icon_text)
def _unpin_plugin(self, plugin_id: str):
"""Unpin a plugin."""
if plugin_id in self.config.pinned_plugins:
self.config.pinned_plugins.remove(plugin_id)
self._save_config()
self.pinned_area.remove_plugin(plugin_id)
def _load_pinned_plugins(self):
"""Load pinned plugins from config."""
if not self.plugin_manager:
return
all_plugins = self.plugin_manager.get_all_discovered_plugins()
plugins = []
for plugin_id in self.config.pinned_plugins:
if plugin_id in all_plugins:
plugin_class = all_plugins[plugin_id]
icon_text = getattr(plugin_class, 'icon', '')
plugins.append((plugin_id, plugin_class.name, icon_text))
self.pinned_area.set_plugins(plugins)
def _update_clock(self):
"""Update clock display."""
from datetime import datetime
self.clock_label.setText(datetime.now().strftime("%H:%M"))
def _apply_config(self):
"""Apply configuration."""
screen = QApplication.primaryScreen().geometry()
if self.config.position == "bottom":
self.move((screen.width() - 700) // 2, screen.height() - 70)
else:
self.move((screen.width() - 700) // 2, 20)
self.setFixedWidth(700)
def _load_config(self) -> ActivityBarConfig:
"""Load configuration."""
config_path = Path("config/activity_bar.json")
if config_path.exists():
try:
data = json.loads(config_path.read_text())
return ActivityBarConfig.from_dict(data)
except:
pass
return ActivityBarConfig()
def _save_config(self):
"""Save configuration."""
config_path = Path("config/activity_bar.json")
config_path.parent.mkdir(parents=True, exist_ok=True)
config_path.write_text(json.dumps(self.config.to_dict(), indent=2))
def enterEvent(self, event):
"""Mouse entered."""
self.hide_timer.stop()
super().enterEvent(event)
def leaveEvent(self, event):
"""Mouse left."""
if self.config.auto_hide:
self.hide_timer.start(self.config.auto_hide_delay)
super().leaveEvent(event)
def mousePressEvent(self, event: QMouseEvent):
"""Start dragging."""
if event.button() == Qt.MouseButton.LeftButton:
self._dragging = True
self._drag_offset = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
event.accept()
def mouseMoveEvent(self, event: QMouseEvent):
"""Drag window."""
if getattr(self, '_dragging', False):
new_pos = event.globalPosition().toPoint() - self._drag_offset
self.move(new_pos)
def mouseReleaseEvent(self, event: QMouseEvent):
"""Stop dragging."""
if event.button() == Qt.MouseButton.LeftButton:
self._dragging = False
# Global instance
_activity_bar_instance = None
def get_activity_bar(plugin_manager=None) -> Optional[EnhancedActivityBar]:
"""Get or create global activity bar instance."""
global _activity_bar_instance
if _activity_bar_instance is None and plugin_manager:
_activity_bar_instance = EnhancedActivityBar(plugin_manager)
return _activity_bar_instance

View File

@ -1,462 +0,0 @@
"""
EU-Utility - Data Store Service (Security Hardened)
Thread-safe persistent data storage for plugins with path validation.
"""
import json
import shutil
import threading
import platform
from pathlib import Path
from typing import Any, Dict, Optional
from datetime import datetime
from collections import OrderedDict
from core.security_utils import (
PathValidator, InputValidator, DataValidator, SecurityError
)
# Cross-platform file locking
try:
import fcntl # Unix/Linux/Mac
HAS_FCNTL = True
except ImportError:
HAS_FCNTL = False
# Windows fallback using portalocker or threading lock
try:
import portalocker
HAS_PORTALOCKER = True
except ImportError:
HAS_PORTALOCKER = False
class DataStore:
"""
Singleton data persistence service for plugins (Security Hardened).
Features:
- Thread-safe file operations with file locking
- Auto-backup on write (keeps last 5 versions)
- Per-plugin JSON storage
- Auto-create directories
- Path traversal protection
- Input validation
"""
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self, data_dir: str = "data/plugins"):
if self._initialized:
return
self.data_dir = Path(data_dir)
self.data_dir.mkdir(parents=True, exist_ok=True)
# Resolve and validate base path
self._base_path = self.data_dir.resolve()
# Memory cache for frequently accessed data
self._cache: Dict[str, Dict[str, Any]] = {}
self._cache_lock = threading.Lock()
# Backup settings
self.max_backups = 5
self._initialized = True
def _get_plugin_file(self, plugin_id: str) -> Path:
"""
Get the storage file path for a plugin with path validation.
Args:
plugin_id: Unique identifier for the plugin
Returns:
Safe file path
Raises:
SecurityError: If plugin_id is invalid or path traversal detected
"""
# Validate plugin_id
if not isinstance(plugin_id, str):
raise SecurityError("plugin_id must be a string")
if not plugin_id:
raise SecurityError("plugin_id cannot be empty")
# Sanitize plugin_id to create a safe filename
safe_name = PathValidator.sanitize_filename(plugin_id, '_')
# Additional check: ensure no path traversal remains
if '..' in safe_name or '/' in safe_name or '\\' in safe_name:
raise SecurityError(f"Invalid characters in plugin_id: {plugin_id}")
# Construct path
file_path = self.data_dir / f"{safe_name}.json"
# Security check: ensure resolved path is within data_dir
try:
resolved_path = file_path.resolve()
if not str(resolved_path).startswith(str(self._base_path)):
raise SecurityError(f"Path traversal detected: {plugin_id}")
except (OSError, ValueError) as e:
raise SecurityError(f"Invalid path for plugin_id: {plugin_id}") from e
return file_path
def _get_backup_dir(self, plugin_id: str) -> Path:
"""Get the backup directory for a plugin with validation."""
# Reuse validation from _get_plugin_file
safe_name = PathValidator.sanitize_filename(plugin_id, '_')
backup_dir = self.data_dir / ".backups" / safe_name
# Validate backup path
try:
resolved = backup_dir.resolve()
if not str(resolved).startswith(str(self._base_path)):
raise SecurityError(f"Backup path traversal detected: {plugin_id}")
except (OSError, ValueError) as e:
raise SecurityError(f"Invalid backup path: {plugin_id}") from e
backup_dir.mkdir(parents=True, exist_ok=True)
return backup_dir
def _load_plugin_data(self, plugin_id: str) -> Dict[str, Any]:
"""Load all data for a plugin from disk with validation."""
# Check cache first
with self._cache_lock:
if plugin_id in self._cache:
return self._cache[plugin_id].copy()
file_path = self._get_plugin_file(plugin_id)
if not file_path.exists():
return {}
try:
with open(file_path, 'r', encoding='utf-8') as f:
# Cross-platform file locking
self._lock_file(f, exclusive=False)
try:
data = json.load(f)
finally:
self._unlock_file(f)
# Validate loaded data
if not isinstance(data, dict):
print(f"[DataStore] Invalid data format for {plugin_id}, resetting")
return {}
# Validate data structure
try:
DataValidator.validate_data_structure(data)
except SecurityError as e:
print(f"[DataStore] Security error in {plugin_id} data: {e}")
return {}
# Update cache
with self._cache_lock:
self._cache[plugin_id] = data.copy()
return data
except (json.JSONDecodeError, IOError) as e:
print(f"[DataStore] Error loading data for {plugin_id}: {e}")
return {}
def _save_plugin_data(self, plugin_id: str, data: Dict[str, Any]) -> bool:
"""Save all data for a plugin to disk with backup."""
# Validate data before saving
try:
DataValidator.validate_data_structure(data)
except SecurityError as e:
print(f"[DataStore] Security error saving {plugin_id}: {e}")
return False
file_path = self._get_plugin_file(plugin_id)
try:
# Create backup if file exists
if file_path.exists():
self._create_backup(plugin_id, file_path)
# Write to temp file first, then move (atomic operation)
temp_path = file_path.with_suffix('.tmp')
with open(temp_path, 'w', encoding='utf-8') as f:
# Cross-platform file locking
self._lock_file(f, exclusive=True)
try:
json.dump(data, f, indent=2, ensure_ascii=False)
f.flush()
import os
os.fsync(f.fileno())
finally:
self._unlock_file(f)
# Atomic move
temp_path.replace(file_path)
# Update cache
with self._cache_lock:
self._cache[plugin_id] = data.copy()
return True
except IOError as e:
print(f"[DataStore] Error saving data for {plugin_id}: {e}")
# Clean up temp file if exists
temp_path = file_path.with_suffix('.tmp')
if temp_path.exists():
try:
temp_path.unlink()
except:
pass
return False
def _lock_file(self, f, exclusive: bool = False):
"""Cross-platform file locking."""
if HAS_FCNTL:
# Unix/Linux/Mac
lock_type = fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH
fcntl.flock(f.fileno(), lock_type)
elif HAS_PORTALOCKER:
# Windows with portalocker
import portalocker
lock_type = portalocker.LOCK_EX if exclusive else portalocker.LOCK_SH
portalocker.lock(f, lock_type)
else:
# Fallback: rely on threading lock (already held)
pass
def _unlock_file(self, f):
"""Cross-platform file unlock."""
if HAS_FCNTL:
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
elif HAS_PORTALOCKER:
import portalocker
portalocker.unlock(f)
else:
# Fallback: nothing to do
pass
def _create_backup(self, plugin_id: str, file_path: Path):
"""Create a backup of the current data file."""
backup_dir = self._get_backup_dir(plugin_id)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_path = backup_dir / f"{timestamp}.json"
try:
shutil.copy2(file_path, backup_path)
self._cleanup_old_backups(backup_dir)
except IOError as e:
print(f"[DataStore] Error creating backup for {plugin_id}: {e}")
def _cleanup_old_backups(self, backup_dir: Path):
"""Remove old backups, keeping only the last N versions."""
try:
backups = sorted(backup_dir.glob("*.json"), key=lambda p: p.stat().st_mtime)
while len(backups) > self.max_backups:
old_backup = backups.pop(0)
old_backup.unlink()
except IOError as e:
print(f"[DataStore] Error cleaning up backups: {e}")
def save(self, plugin_id: str, key: str, data: Any) -> bool:
"""
Save data for a plugin with validation.
Args:
plugin_id: Unique identifier for the plugin
key: Key under which to store the data
data: Data to store (must be JSON serializable)
Returns:
True if successful, False otherwise
"""
# Validate key
if not isinstance(key, str):
print(f"[DataStore] Invalid key type for {plugin_id}")
return False
if not key:
print(f"[DataStore] Empty key not allowed for {plugin_id}")
return False
if not InputValidator.validate_json_key(key):
print(f"[DataStore] Invalid key format for {plugin_id}: {key}")
return False
plugin_data = self._load_plugin_data(plugin_id)
plugin_data[key] = data
return self._save_plugin_data(plugin_id, plugin_data)
def load(self, plugin_id: str, key: str, default: Any = None) -> Any:
"""
Load data for a plugin.
Args:
plugin_id: Unique identifier for the plugin
key: Key of the data to load
default: Default value if key not found
Returns:
The stored data or default value
"""
# Validate key
if not isinstance(key, str):
return default
plugin_data = self._load_plugin_data(plugin_id)
return plugin_data.get(key, default)
def delete(self, plugin_id: str, key: str) -> bool:
"""
Delete data for a plugin.
Args:
plugin_id: Unique identifier for the plugin
key: Key of the data to delete
Returns:
True if key existed and was deleted, False otherwise
"""
# Validate key
if not isinstance(key, str):
return False
plugin_data = self._load_plugin_data(plugin_id)
if key in plugin_data:
del plugin_data[key]
return self._save_plugin_data(plugin_id, plugin_data)
return False
def get_all_keys(self, plugin_id: str) -> list:
"""
Get all keys stored for a plugin.
Args:
plugin_id: Unique identifier for the plugin
Returns:
List of keys
"""
plugin_data = self._load_plugin_data(plugin_id)
return list(plugin_data.keys())
def clear_plugin(self, plugin_id: str) -> bool:
"""
Clear all data for a plugin.
Args:
plugin_id: Unique identifier for the plugin
Returns:
True if successful, False otherwise
"""
file_path = self._get_plugin_file(plugin_id)
# Create backup before clearing
if file_path.exists():
self._create_backup(plugin_id, file_path)
# Clear cache
with self._cache_lock:
if plugin_id in self._cache:
del self._cache[plugin_id]
# Remove file
try:
if file_path.exists():
file_path.unlink()
return True
except IOError as e:
print(f"[DataStore] Error clearing data for {plugin_id}: {e}")
return False
def get_backups(self, plugin_id: str) -> list:
"""
Get list of available backups for a plugin.
Args:
plugin_id: Unique identifier for the plugin
Returns:
List of backup file paths
"""
backup_dir = self._get_backup_dir(plugin_id)
if not backup_dir.exists():
return []
backups = sorted(backup_dir.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True)
return [str(b) for b in backups]
def restore_backup(self, plugin_id: str, backup_path: str) -> bool:
"""
Restore data from a backup.
Args:
plugin_id: Unique identifier for the plugin
backup_path: Path to the backup file
Returns:
True if successful, False otherwise
"""
backup_file = Path(backup_path)
if not backup_file.exists():
print(f"[DataStore] Backup not found: {backup_path}")
return False
# Validate backup path is within backups directory
try:
backup_dir = self._get_backup_dir(plugin_id)
resolved_backup = backup_file.resolve()
resolved_backup_dir = backup_dir.resolve()
if not str(resolved_backup).startswith(str(resolved_backup_dir)):
print(f"[DataStore] Invalid backup path: {backup_path}")
return False
except (OSError, ValueError) as e:
print(f"[DataStore] Path validation error: {e}")
return False
file_path = self._get_plugin_file(plugin_id)
try:
# Create backup of current state before restoring
if file_path.exists():
self._create_backup(plugin_id, file_path)
# Copy backup to main file
shutil.copy2(backup_file, file_path)
# Invalidate cache
with self._cache_lock:
if plugin_id in self._cache:
del self._cache[plugin_id]
return True
except IOError as e:
print(f"[DataStore] Error restoring backup for {plugin_id}: {e}")
return False
# Singleton instance
_data_store = None
_data_store_lock = threading.Lock()
def get_data_store() -> DataStore:
"""Get the global DataStore instance."""
global _data_store
if _data_store is None:
with _data_store_lock:
if _data_store is None:
_data_store = DataStore()
return _data_store

View File

@ -1,355 +0,0 @@
"""
EU-Utility - Data Store Service
Thread-safe persistent data storage for plugins.
Provides file locking, auto-backup, and singleton access.
"""
import json
import shutil
import threading
import platform
from pathlib import Path
from typing import Any, Dict, Optional
from datetime import datetime
from collections import OrderedDict
# Cross-platform file locking
try:
import fcntl # Unix/Linux/Mac
HAS_FCNTL = True
except ImportError:
HAS_FCNTL = False
# Windows fallback using portalocker or threading lock
try:
import portalocker
HAS_PORTALOCKER = True
except ImportError:
HAS_PORTALOCKER = False
class DataStore:
"""
Singleton data persistence service for plugins.
Features:
- Thread-safe file operations with file locking
- Auto-backup on write (keeps last 5 versions)
- Per-plugin JSON storage
- Auto-create directories
"""
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self, data_dir: str = "data/plugins"):
if self._initialized:
return
self.data_dir = Path(data_dir)
self.data_dir.mkdir(parents=True, exist_ok=True)
# Memory cache for frequently accessed data
self._cache: Dict[str, Dict[str, Any]] = {}
self._cache_lock = threading.Lock()
# Backup settings
self.max_backups = 5
self._initialized = True
def _get_plugin_file(self, plugin_id: str) -> Path:
"""Get the storage file path for a plugin."""
# Sanitize plugin_id to create a safe filename
safe_name = plugin_id.replace(".", "_").replace("/", "_").replace("\\", "_")
return self.data_dir / f"{safe_name}.json"
def _get_backup_dir(self, plugin_id: str) -> Path:
"""Get the backup directory for a plugin."""
safe_name = plugin_id.replace(".", "_").replace("/", "_").replace("\\", "_")
backup_dir = self.data_dir / ".backups" / safe_name
backup_dir.mkdir(parents=True, exist_ok=True)
return backup_dir
def _load_plugin_data(self, plugin_id: str) -> Dict[str, Any]:
"""Load all data for a plugin from disk."""
# Check cache first
with self._cache_lock:
if plugin_id in self._cache:
return self._cache[plugin_id].copy()
file_path = self._get_plugin_file(plugin_id)
if not file_path.exists():
return {}
try:
with open(file_path, 'r', encoding='utf-8') as f:
# Cross-platform file locking
self._lock_file(f, exclusive=False)
try:
data = json.load(f)
finally:
self._unlock_file(f)
# Update cache
with self._cache_lock:
self._cache[plugin_id] = data.copy()
return data
except (json.JSONDecodeError, IOError) as e:
print(f"[DataStore] Error loading data for {plugin_id}: {e}")
return {}
def _save_plugin_data(self, plugin_id: str, data: Dict[str, Any]) -> bool:
"""Save all data for a plugin to disk with backup."""
file_path = self._get_plugin_file(plugin_id)
try:
# Create backup if file exists
if file_path.exists():
self._create_backup(plugin_id, file_path)
# Write to temp file first, then move (atomic operation)
temp_path = file_path.with_suffix('.tmp')
with open(temp_path, 'w', encoding='utf-8') as f:
# Cross-platform file locking
self._lock_file(f, exclusive=True)
try:
json.dump(data, f, indent=2, ensure_ascii=False)
f.flush()
import os
os.fsync(f.fileno())
finally:
self._unlock_file(f)
# Atomic move
temp_path.replace(file_path)
# Update cache
with self._cache_lock:
self._cache[plugin_id] = data.copy()
return True
except IOError as e:
print(f"[DataStore] Error saving data for {plugin_id}: {e}")
# Clean up temp file if exists
temp_path = file_path.with_suffix('.tmp')
if temp_path.exists():
temp_path.unlink()
return False
def _lock_file(self, f, exclusive: bool = False):
"""Cross-platform file locking."""
if HAS_FCNTL:
# Unix/Linux/Mac
lock_type = fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH
fcntl.flock(f.fileno(), lock_type)
elif HAS_PORTALOCKER:
# Windows with portalocker
import portalocker
lock_type = portalocker.LOCK_EX if exclusive else portalocker.LOCK_SH
portalocker.lock(f, lock_type)
else:
# Fallback: rely on threading lock (already held)
pass
def _unlock_file(self, f):
"""Cross-platform file unlock."""
if HAS_FCNTL:
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
elif HAS_PORTALOCKER:
import portalocker
portalocker.unlock(f)
else:
# Fallback: nothing to do
pass
def _create_backup(self, plugin_id: str, file_path: Path):
"""Create a backup of the current data file."""
backup_dir = self._get_backup_dir(plugin_id)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_path = backup_dir / f"{timestamp}.json"
try:
shutil.copy2(file_path, backup_path)
self._cleanup_old_backups(backup_dir)
except IOError as e:
print(f"[DataStore] Error creating backup for {plugin_id}: {e}")
def _cleanup_old_backups(self, backup_dir: Path):
"""Remove old backups, keeping only the last N versions."""
try:
backups = sorted(backup_dir.glob("*.json"), key=lambda p: p.stat().st_mtime)
while len(backups) > self.max_backups:
old_backup = backups.pop(0)
old_backup.unlink()
except IOError as e:
print(f"[DataStore] Error cleaning up backups: {e}")
def save(self, plugin_id: str, key: str, data: Any) -> bool:
"""
Save data for a plugin.
Args:
plugin_id: Unique identifier for the plugin
key: Key under which to store the data
data: Data to store (must be JSON serializable)
Returns:
True if successful, False otherwise
"""
plugin_data = self._load_plugin_data(plugin_id)
plugin_data[key] = data
return self._save_plugin_data(plugin_id, plugin_data)
def load(self, plugin_id: str, key: str, default: Any = None) -> Any:
"""
Load data for a plugin.
Args:
plugin_id: Unique identifier for the plugin
key: Key of the data to load
default: Default value if key not found
Returns:
The stored data or default value
"""
plugin_data = self._load_plugin_data(plugin_id)
return plugin_data.get(key, default)
def delete(self, plugin_id: str, key: str) -> bool:
"""
Delete data for a plugin.
Args:
plugin_id: Unique identifier for the plugin
key: Key of the data to delete
Returns:
True if key existed and was deleted, False otherwise
"""
plugin_data = self._load_plugin_data(plugin_id)
if key in plugin_data:
del plugin_data[key]
return self._save_plugin_data(plugin_id, plugin_data)
return False
def get_all_keys(self, plugin_id: str) -> list:
"""
Get all keys stored for a plugin.
Args:
plugin_id: Unique identifier for the plugin
Returns:
List of keys
"""
plugin_data = self._load_plugin_data(plugin_id)
return list(plugin_data.keys())
def clear_plugin(self, plugin_id: str) -> bool:
"""
Clear all data for a plugin.
Args:
plugin_id: Unique identifier for the plugin
Returns:
True if successful, False otherwise
"""
file_path = self._get_plugin_file(plugin_id)
# Create backup before clearing
if file_path.exists():
self._create_backup(plugin_id, file_path)
# Clear cache
with self._cache_lock:
if plugin_id in self._cache:
del self._cache[plugin_id]
# Remove file
try:
if file_path.exists():
file_path.unlink()
return True
except IOError as e:
print(f"[DataStore] Error clearing data for {plugin_id}: {e}")
return False
def get_backups(self, plugin_id: str) -> list:
"""
Get list of available backups for a plugin.
Args:
plugin_id: Unique identifier for the plugin
Returns:
List of backup file paths
"""
backup_dir = self._get_backup_dir(plugin_id)
if not backup_dir.exists():
return []
backups = sorted(backup_dir.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True)
return [str(b) for b in backups]
def restore_backup(self, plugin_id: str, backup_path: str) -> bool:
"""
Restore data from a backup.
Args:
plugin_id: Unique identifier for the plugin
backup_path: Path to the backup file
Returns:
True if successful, False otherwise
"""
backup_file = Path(backup_path)
if not backup_file.exists():
print(f"[DataStore] Backup not found: {backup_path}")
return False
file_path = self._get_plugin_file(plugin_id)
try:
# Create backup of current state before restoring
if file_path.exists():
self._create_backup(plugin_id, file_path)
# Copy backup to main file
shutil.copy2(backup_file, file_path)
# Invalidate cache
with self._cache_lock:
if plugin_id in self._cache:
del self._cache[plugin_id]
return True
except IOError as e:
print(f"[DataStore] Error restoring backup for {plugin_id}: {e}")
return False
# Singleton instance
_data_store = None
_data_store_lock = threading.Lock()
def get_data_store() -> DataStore:
"""Get the global DataStore instance."""
global _data_store
if _data_store is None:
with _data_store_lock:
if _data_store is None:
_data_store = DataStore()
return _data_store

868
core/modern_ui/__init__.py Normal file
View File

@ -0,0 +1,868 @@
"""
EU-Utility - Modern UI Design System
=====================================
A beautiful, modern design system for EU-Utility featuring:
- Dark gaming aesthetic with EU orange accents
- Glassmorphism effects throughout
- Smooth 60fps animations
- Responsive layouts
- Professional iconography
Design Principles:
1. Visual Hierarchy - Clear distinction between elements
2. Consistency - Unified design language
3. Feedback - Clear interactive states
4. Performance - GPU-accelerated animations
5. Accessibility - WCAG compliant contrast ratios
"""
from PyQt6.QtCore import Qt, QPropertyAnimation, QEasingCurve, QParallelAnimationGroup, QTimer
from PyQt6.QtCore import pyqtSignal, QSize, QPoint, QRectF
from PyQt6.QtWidgets import (
QWidget, QFrame, QPushButton, QLabel, QVBoxLayout, QHBoxLayout,
QGraphicsDropShadowEffect, QGraphicsOpacityEffect, QLineEdit,
QScrollArea, QStackedWidget, QProgressBar, QTextEdit, QComboBox
)
from PyQt6.QtGui import (
QColor, QPainter, QLinearGradient, QRadialGradient, QFont,
QFontDatabase, QIcon, QPixmap, QCursor, QPainterPath
)
from typing import Optional, Callable, List, Dict, Any
import math
# =============================================================================
# DESIGN TOKENS - Centralized Design System
# =============================================================================
class DesignTokens:
"""Central design tokens for consistent UI across the application."""
# Brand Colors
BRAND_ORANGE = "#FF6B35"
BRAND_ORANGE_LIGHT = "#FF8C5A"
BRAND_ORANGE_DARK = "#E55A2B"
BRAND_ORANGE_GLOW = "rgba(255, 107, 53, 0.4)"
# Extended Color Palette
COLORS = {
# Primary
'primary': '#FF6B35',
'primary_hover': '#FF8C5A',
'primary_pressed': '#E55A2B',
'primary_glow': 'rgba(255, 107, 53, 0.4)',
# Backgrounds - Deep space aesthetic
'bg_darkest': '#0A0C10',
'bg_dark': '#111318',
'bg_card': '#161920',
'bg_elevated': '#1D2129',
'bg_hover': '#252A33',
'bg_pressed': '#2D333D',
# Surfaces with glassmorphism
'surface': 'rgba(22, 25, 32, 0.85)',
'surface_hover': 'rgba(29, 33, 41, 0.9)',
'surface_active': 'rgba(37, 42, 51, 0.95)',
# Accents
'accent_teal': '#00D4AA',
'accent_blue': '#4D9CFF',
'accent_purple': '#A855F7',
'accent_yellow': '#FBBF24',
'accent_green': '#22C55E',
'accent_red': '#EF4444',
# Text
'text_primary': '#F0F4F8',
'text_secondary': '#9CA3AF',
'text_muted': '#6B7280',
'text_disabled': '#4B5563',
# Borders
'border_subtle': 'rgba(255, 255, 255, 0.06)',
'border_default': 'rgba(255, 255, 255, 0.1)',
'border_hover': 'rgba(255, 255, 255, 0.15)',
'border_focus': 'rgba(255, 107, 53, 0.5)',
'border_active': 'rgba(255, 107, 53, 0.8)',
# Status
'success': '#22C55E',
'warning': '#FBBF24',
'error': '#EF4444',
'info': '#4D9CFF',
}
# Typography
TYPOGRAPHY = {
'font_family': '"Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont, sans-serif',
'font_mono': '"JetBrains Mono", "Fira Code", monospace',
'size_xs': 11,
'size_sm': 12,
'size_base': 13,
'size_md': 14,
'size_lg': 16,
'size_xl': 18,
'size_2xl': 20,
'size_3xl': 24,
'size_4xl': 30,
'size_5xl': 36,
}
# Spacing (4px grid system)
SPACING = {
'0': 0,
'1': 4,
'2': 8,
'3': 12,
'4': 16,
'5': 20,
'6': 24,
'8': 32,
'10': 40,
'12': 48,
'16': 64,
}
# Border Radius
RADIUS = {
'none': 0,
'sm': 4,
'md': 8,
'lg': 12,
'xl': 16,
'2xl': 20,
'3xl': 24,
'full': 9999,
}
# Shadows
SHADOWS = {
'sm': '0 1px 2px rgba(0, 0, 0, 0.3)',
'md': '0 4px 6px rgba(0, 0, 0, 0.4)',
'lg': '0 10px 15px rgba(0, 0, 0, 0.5)',
'xl': '0 20px 25px rgba(0, 0, 0, 0.6)',
'glow': '0 0 20px rgba(255, 107, 53, 0.3)',
'glow_strong': '0 0 30px rgba(255, 107, 53, 0.5)',
}
# Animation Durations (ms)
DURATION = {
'fast': 150,
'normal': 250,
'slow': 350,
'slower': 500,
}
# Easing Curves
EASING = {
'default': QEasingCurve.Type.OutCubic,
'bounce': QEasingCurve.Type.OutBounce,
'elastic': QEasingCurve.Type.OutElastic,
'smooth': QEasingCurve.Type.InOutCubic,
}
@classmethod
def color(cls, name: str) -> str:
"""Get color by name."""
return cls.COLORS.get(name, '#FFFFFF')
@classmethod
def spacing(cls, name: str) -> int:
"""Get spacing value."""
return cls.SPACING.get(name, 0)
@classmethod
def radius(cls, name: str) -> int:
"""Get border radius value."""
return cls.RADIUS.get(name, 0)
# =============================================================================
# ANIMATION UTILITIES
# =============================================================================
class AnimationManager:
"""Manages smooth GPU-accelerated animations."""
_active_animations: List[QPropertyAnimation] = []
@classmethod
def fade_in(cls, widget: QWidget, duration: int = 250) -> QPropertyAnimation:
"""Fade in a widget smoothly."""
effect = QGraphicsOpacityEffect(widget)
widget.setGraphicsEffect(effect)
anim = QPropertyAnimation(effect, b"opacity")
anim.setDuration(duration)
anim.setStartValue(0.0)
anim.setEndValue(1.0)
anim.setEasingCurve(QEasingCurve.Type.OutCubic)
cls._active_animations.append(anim)
anim.finished.connect(lambda: cls._cleanup_animation(anim))
return anim
@classmethod
def fade_out(cls, widget: QWidget, duration: int = 200, on_finish: Optional[Callable] = None) -> QPropertyAnimation:
"""Fade out a widget smoothly."""
effect = widget.graphicsEffect()
if not isinstance(effect, QGraphicsOpacityEffect):
effect = QGraphicsOpacityEffect(widget)
widget.setGraphicsEffect(effect)
anim = QPropertyAnimation(effect, b"opacity")
anim.setDuration(duration)
anim.setStartValue(1.0)
anim.setEndValue(0.0)
anim.setEasingCurve(QEasingCurve.Type.InCubic)
if on_finish:
anim.finished.connect(on_finish)
cls._active_animations.append(anim)
anim.finished.connect(lambda: cls._cleanup_animation(anim))
return anim
@classmethod
def slide_in(cls, widget: QWidget, direction: str = "bottom", duration: int = 300) -> QPropertyAnimation:
"""Slide widget in from specified direction."""
anim = QPropertyAnimation(widget, b"pos")
anim.setDuration(duration)
anim.setEasingCurve(QEasingCurve.Type.OutCubic)
current_pos = widget.pos()
if direction == "left":
start_pos = current_pos - QPoint(widget.width() + 20, 0)
elif direction == "right":
start_pos = current_pos + QPoint(widget.width() + 20, 0)
elif direction == "top":
start_pos = current_pos - QPoint(0, widget.height() + 20)
else: # bottom
start_pos = current_pos + QPoint(0, widget.height() + 20)
anim.setStartValue(start_pos)
anim.setEndValue(current_pos)
cls._active_animations.append(anim)
anim.finished.connect(lambda: cls._cleanup_animation(anim))
return anim
@classmethod
def scale(cls, widget: QWidget, from_scale: float = 0.9, to_scale: float = 1.0, duration: int = 250) -> QPropertyAnimation:
"""Scale animation for widgets."""
anim = QPropertyAnimation(widget, b"minimumWidth")
anim.setDuration(duration)
anim.setEasingCurve(QEasingCurve.Type.OutBack)
base_width = widget.width()
anim.setStartValue(int(base_width * from_scale))
anim.setEndValue(int(base_width * to_scale))
cls._active_animations.append(anim)
anim.finished.connect(lambda: cls._cleanup_animation(anim))
return anim
@classmethod
def pulse_glow(cls, widget: QWidget, duration: int = 2000) -> QPropertyAnimation:
"""Create a pulsing glow effect."""
effect = QGraphicsDropShadowEffect(widget)
effect.setColor(QColor(255, 107, 53))
effect.setBlurRadius(20)
effect.setOffset(0, 0)
widget.setGraphicsEffect(effect)
anim = QPropertyAnimation(effect, b"blurRadius")
anim.setDuration(duration)
anim.setStartValue(20)
anim.setEndValue(40)
anim.setEasingCurve(QEasingCurve.Type.InOutSine)
anim.setLoopCount(-1)
return anim
@classmethod
def _cleanup_animation(cls, anim: QPropertyAnimation):
"""Remove completed animation from tracking."""
if anim in cls._active_animations:
cls._active_animations.remove(anim)
# =============================================================================
# MODERN COMPONENTS
# =============================================================================
class GlassCard(QFrame):
"""Glassmorphism card with frosted glass effect."""
clicked = pyqtSignal()
def __init__(self, parent=None, elevation: int = 1, hover_lift: bool = True):
super().__init__(parent)
self.elevation = elevation
self.hover_lift = hover_lift
self._hovered = False
self._setup_style()
self._setup_shadow()
if hover_lift:
self.setMouseTracking(True)
def _setup_style(self):
"""Apply glassmorphism styling."""
c = DesignTokens.COLORS
opacity = 0.85 + (self.elevation * 0.03)
self.setStyleSheet(f"""
GlassCard {{
background: rgba(22, 25, 32, {min(opacity, 0.95)});
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: {DesignTokens.radius('xl')}px;
}}
""")
def _setup_shadow(self):
"""Apply elevation shadow."""
self._shadow = QGraphicsDropShadowEffect(self)
self._shadow.setBlurRadius(self.elevation * 15)
self._shadow.setColor(QColor(0, 0, 0, int(80 + self.elevation * 20)))
self._shadow.setOffset(0, self.elevation * 3)
self.setGraphicsEffect(self._shadow)
def enterEvent(self, event):
"""Hover effect."""
if self.hover_lift:
self._hovered = True
self._shadow.setBlurRadius((self.elevation + 1) * 15)
self._shadow.setColor(QColor(0, 0, 0, int(100 + (self.elevation + 1) * 20)))
self.setStyleSheet(f"""
GlassCard {{
background: rgba(29, 33, 41, 0.9);
border: 1px solid rgba(255, 107, 53, 0.2);
border-radius: {DesignTokens.radius('xl')}px;
}}
""")
super().enterEvent(event)
def leaveEvent(self, event):
"""Reset hover effect."""
if self.hover_lift:
self._hovered = False
self._setup_shadow()
self._setup_style()
super().leaveEvent(event)
def mousePressEvent(self, event):
"""Handle click."""
if event.button() == Qt.MouseButton.LeftButton:
self.clicked.emit()
super().mousePressEvent(event)
class ModernButton(QPushButton):
"""Modern button with smooth animations and multiple variants."""
VARIANTS = ['primary', 'secondary', 'ghost', 'outline', 'danger', 'glass']
def __init__(self, text: str = "", variant: str = 'primary', icon: str = None, parent=None):
super().__init__(text, parent)
self.variant = variant
self.icon_text = icon
self.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
self._apply_style()
self._setup_animations()
def _apply_style(self):
"""Apply button styling based on variant."""
c = DesignTokens.COLORS
r = DesignTokens.radius('full')
styles = {
'primary': f"""
ModernButton {{
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 {c['primary']}, stop:1 {c['primary_hover']});
color: white;
border: none;
border-radius: {r}px;
padding: 12px 24px;
font-size: {DesignTokens.TYPOGRAPHY['size_md']}px;
font-weight: 600;
}}
ModernButton:hover {{
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 {c['primary_hover']}, stop:1 {c['primary']});
}}
ModernButton:pressed {{
background: {c['primary_pressed']};
}}
""",
'secondary': f"""
ModernButton {{
background: {c['bg_elevated']};
color: {c['text_primary']};
border: 1px solid {c['border_default']};
border-radius: {r}px;
padding: 12px 24px;
font-size: {DesignTokens.TYPOGRAPHY['size_md']}px;
font-weight: 600;
}}
ModernButton:hover {{
background: {c['bg_hover']};
border-color: {c['border_hover']};
}}
""",
'ghost': f"""
ModernButton {{
background: transparent;
color: {c['text_secondary']};
border: none;
border-radius: {r}px;
padding: 12px 24px;
font-size: {DesignTokens.TYPOGRAPHY['size_md']}px;
font-weight: 500;
}}
ModernButton:hover {{
background: rgba(255, 255, 255, 0.05);
color: {c['text_primary']};
}}
""",
'outline': f"""
ModernButton {{
background: transparent;
color: {c['primary']};
border: 2px solid {c['primary']};
border-radius: {r}px;
padding: 10px 22px;
font-size: {DesignTokens.TYPOGRAPHY['size_md']}px;
font-weight: 600;
}}
ModernButton:hover {{
background: rgba(255, 107, 53, 0.1);
}}
""",
'danger': f"""
ModernButton {{
background: {c['error']};
color: white;
border: none;
border-radius: {r}px;
padding: 12px 24px;
font-size: {DesignTokens.TYPOGRAPHY['size_md']}px;
font-weight: 600;
}}
ModernButton:hover {{
background: #DC2626;
}}
""",
'glass': f"""
ModernButton {{
background: rgba(255, 255, 255, 0.08);
color: white;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: {r}px;
padding: 12px 24px;
font-size: {DesignTokens.TYPOGRAPHY['size_md']}px;
font-weight: 600;
backdrop-filter: blur(10px);
}}
ModernButton:hover {{
background: rgba(255, 255, 255, 0.12);
border-color: rgba(255, 107, 53, 0.3);
}}
"""
}
self.setStyleSheet(styles.get(self.variant, styles['primary']))
self.setFixedHeight(44)
def _setup_animations(self):
"""Setup press animation."""
self._press_anim = QPropertyAnimation(self, b"minimumHeight")
self._press_anim.setDuration(100)
self._press_anim.setEasingCurve(QEasingCurve.Type.OutQuad)
def enterEvent(self, event):
"""Hover animation."""
super().enterEvent(event)
def mousePressEvent(self, event):
"""Press animation."""
if event.button() == Qt.MouseButton.LeftButton:
self._press_anim.setStartValue(44)
self._press_anim.setEndValue(42)
self._press_anim.start()
super().mousePressEvent(event)
def mouseReleaseEvent(self, event):
"""Release animation."""
self._press_anim.setStartValue(42)
self._press_anim.setEndValue(44)
self._press_anim.start()
super().mouseReleaseEvent(event)
class ModernInput(QLineEdit):
"""Modern input field with floating label and animations."""
def __init__(self, placeholder: str = "", parent=None):
super().__init__(parent)
self.setPlaceholderText(placeholder)
self._apply_style()
def _apply_style(self):
"""Apply modern input styling."""
c = DesignTokens.COLORS
r = DesignTokens.radius('lg')
self.setStyleSheet(f"""
ModernInput {{
background: {c['bg_elevated']};
color: {c['text_primary']};
border: 2px solid {c['border_default']};
border-radius: {r}px;
padding: 12px 16px;
font-size: {DesignTokens.TYPOGRAPHY['size_md']}px;
selection-background-color: {c['primary']};
}}
ModernInput:hover {{
border-color: {c['border_hover']};
}}
ModernInput:focus {{
border-color: {c['primary']};
background: {c['bg_card']};
}}
ModernInput::placeholder {{
color: {c['text_muted']};
}}
""")
self.setFixedHeight(48)
class ModernComboBox(QComboBox):
"""Modern dropdown with custom styling."""
def __init__(self, parent=None):
super().__init__(parent)
self._apply_style()
def _apply_style(self):
"""Apply modern combobox styling."""
c = DesignTokens.COLORS
r = DesignTokens.radius('lg')
self.setStyleSheet(f"""
ModernComboBox {{
background: {c['bg_elevated']};
color: {c['text_primary']};
border: 2px solid {c['border_default']};
border-radius: {r}px;
padding: 8px 12px;
font-size: {DesignTokens.TYPOGRAPHY['size_md']}px;
min-width: 150px;
}}
ModernComboBox:hover {{
border-color: {c['border_hover']};
}}
ModernComboBox:focus {{
border-color: {c['primary']};
}}
ModernComboBox::drop-down {{
border: none;
width: 30px;
}}
ModernComboBox::down-arrow {{
image: none;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 5px solid {c['text_secondary']};
}}
ModernComboBox QAbstractItemView {{
background: {c['bg_elevated']};
color: {c['text_primary']};
border: 1px solid {c['border_default']};
border-radius: {r}px;
selection-background-color: {c['bg_hover']};
selection-color: {c['text_primary']};
padding: 4px;
}}
""")
self.setFixedHeight(48)
class Badge(QLabel):
"""Status badge with various styles."""
STYLES = ['default', 'success', 'warning', 'error', 'info', 'primary']
def __init__(self, text: str = "", style: str = 'default', parent=None):
super().__init__(text, parent)
self.badge_style = style
self._apply_style()
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
def _apply_style(self):
"""Apply badge styling."""
c = DesignTokens.COLORS
colors = {
'default': ('rgba(255,255,255,0.1)', c['text_secondary']),
'success': ('rgba(34, 197, 94, 0.2)', c['accent_green']),
'warning': ('rgba(251, 191, 36, 0.2)', c['accent_yellow']),
'error': ('rgba(239, 68, 68, 0.2)', c['accent_red']),
'info': ('rgba(77, 156, 255, 0.2)', c['accent_blue']),
'primary': ('rgba(255, 107, 53, 0.2)', c['primary']),
}
bg, fg = colors.get(self.badge_style, colors['default'])
self.setStyleSheet(f"""
Badge {{
background: {bg};
color: {fg};
border-radius: {DesignTokens.radius('full')}px;
padding: 4px 12px;
font-size: {DesignTokens.TYPOGRAPHY['size_xs']}px;
font-weight: 600;
}}
""")
# Add subtle shadow
shadow = QGraphicsDropShadowEffect(self)
shadow.setBlurRadius(4)
shadow.setColor(QColor(0, 0, 0, 40))
shadow.setOffset(0, 1)
self.setGraphicsEffect(shadow)
class ProgressIndicator(QProgressBar):
"""Modern progress indicator with gradient."""
def __init__(self, parent=None):
super().__init__(parent)
self._apply_style()
self.setTextVisible(False)
self.setRange(0, 100)
self.setValue(0)
def _apply_style(self):
"""Apply modern progress styling."""
c = DesignTokens.COLORS
self.setStyleSheet(f"""
ProgressIndicator {{
background: {c['bg_elevated']};
border: none;
border-radius: {DesignTokens.radius('full')}px;
height: 6px;
}}
ProgressIndicator::chunk {{
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 {c['primary']}, stop:1 {c['primary_hover']});
border-radius: {DesignTokens.radius('full')}px;
}}
""")
self.setFixedHeight(6)
class IconButton(QPushButton):
"""Circular icon button with hover effects."""
def __init__(self, icon_text: str = "", size: int = 40, tooltip: str = "", parent=None):
super().__init__(icon_text, parent)
self.setFixedSize(size, size)
self.setToolTip(tooltip)
self.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
self._apply_style()
def _apply_style(self):
"""Apply icon button styling."""
c = DesignTokens.COLORS
size = self.width()
self.setStyleSheet(f"""
IconButton {{
background: transparent;
color: {c['text_secondary']};
border: none;
border-radius: {size // 2}px;
font-size: 16px;
}}
IconButton:hover {{
background: rgba(255, 255, 255, 0.1);
color: {c['text_primary']};
}}
IconButton:pressed {{
background: rgba(255, 255, 255, 0.15);
}}
""")
# =============================================================================
# LAYOUT HELPERS
# =============================================================================
def create_spacer(horizontal: bool = False, size: int = None):
"""Create a spacer item."""
from PyQt6.QtWidgets import QSpacerItem, QSizePolicy
if horizontal:
policy = QSizePolicy.Policy.Expanding
min_policy = QSizePolicy.Policy.Minimum
return QSpacerItem(size or 0, 0, policy, min_policy)
else:
policy = QSizePolicy.Policy.Expanding
min_policy = QSizePolicy.Policy.Minimum
return QSpacerItem(0, size or 0, min_policy, policy)
def create_separator(horizontal: bool = True):
"""Create a styled separator line."""
separator = QFrame()
if horizontal:
separator.setFrameShape(QFrame.Shape.HLine)
separator.setFixedHeight(1)
else:
separator.setFrameShape(QFrame.Shape.VLine)
separator.setFixedWidth(1)
separator.setStyleSheet(f"""
background: {DesignTokens.color('border_subtle')};
""")
return separator
# =============================================================================
# GLOBAL STYLESHEET
# =============================================================================
def get_global_stylesheet() -> str:
"""Get complete global stylesheet for the application."""
c = DesignTokens.COLORS
return f"""
/* Base */
QWidget {{
font-family: {DesignTokens.TYPOGRAPHY['font_family']};
font-size: {DesignTokens.TYPOGRAPHY['size_base']}px;
color: {c['text_primary']};
}}
/* Main Window */
QMainWindow {{
background: {c['bg_darkest']};
}}
/* Selection */
::selection {{
background: {c['primary']};
color: white;
}}
/* Scrollbars */
QScrollBar:vertical {{
background: transparent;
width: 8px;
margin: 0;
}}
QScrollBar::handle:vertical {{
background: {c['border_default']};
border-radius: 4px;
min-height: 40px;
}}
QScrollBar::handle:vertical:hover {{
background: {c['border_hover']};
}}
QScrollBar::add-line:vertical,
QScrollBar::sub-line:vertical {{
height: 0;
}}
QScrollBar:horizontal {{
background: transparent;
height: 8px;
margin: 0;
}}
QScrollBar::handle:horizontal {{
background: {c['border_default']};
border-radius: 4px;
min-width: 40px;
}}
/* Tooltips */
QToolTip {{
background: {c['bg_elevated']};
color: {c['text_primary']};
border: 1px solid {c['border_default']};
border-radius: 8px;
padding: 8px 12px;
font-size: {DesignTokens.TYPOGRAPHY['size_sm']}px;
}}
/* Menu */
QMenu {{
background: {c['bg_elevated']};
color: {c['text_primary']};
border: 1px solid {c['border_default']};
border-radius: 12px;
padding: 8px;
}}
QMenu::item {{
padding: 10px 20px;
border-radius: 8px;
}}
QMenu::item:selected {{
background: {c['bg_hover']};
}}
QMenu::separator {{
height: 1px;
background: {c['border_subtle']};
margin: 8px 0;
}}
/* Check Box */
QCheckBox {{
spacing: 8px;
}}
QCheckBox::indicator {{
width: 20px;
height: 20px;
border: 2px solid {c['border_default']};
border-radius: 6px;
background: {c['bg_elevated']};
}}
QCheckBox::indicator:hover {{
border-color: {c['border_hover']};
}}
QCheckBox::indicator:checked {{
background: {c['primary']};
border-color: {c['primary']};
}}
/* Slider */
QSlider::groove:horizontal {{
height: 4px;
background: {c['bg_elevated']};
border-radius: 2px;
}}
QSlider::handle:horizontal {{
width: 18px;
height: 18px;
background: {c['primary']};
border-radius: 9px;
margin: -7px 0;
}}
QSlider::sub-page:horizontal {{
background: {c['primary']};
border-radius: 2px;
}}
"""

View File

@ -12,9 +12,12 @@ Modes:
- OVERLAY_TEMPORARY: Show for 7-10 seconds on hotkey, then hide
"""
import threading
import time
from enum import Enum
from dataclasses import dataclass
from typing import Optional
from functools import lru_cache
from PyQt6.QtCore import QTimer, pyqtSignal, QObject
@ -33,7 +36,7 @@ class OverlayConfig:
# Default to game-focused mode (Blish HUD style)
mode: OverlayMode = OverlayMode.OVERLAY_GAME_FOCUSED
temporary_duration: int = 8000 # milliseconds (8 seconds default)
game_focus_poll_interval: int = 1000 # ms (1 second - faster response)
game_focus_poll_interval: int = 5000 # ms (5 seconds - reduces CPU load)
def to_dict(self):
return {
@ -53,7 +56,7 @@ class OverlayConfig:
return cls(
mode=mode,
temporary_duration=data.get('temporary_duration', 8000),
game_focus_poll_interval=data.get('game_focus_poll_interval', 2000)
game_focus_poll_interval=data.get('game_focus_poll_interval', 5000)
)
@ -80,9 +83,20 @@ class OverlayController(QObject):
self._is_visible = False
self._game_focused = False
# Caching for performance
self._cached_window_state = None
self._last_check_time = 0
self._cache_ttl = 2.0 # Cache window state for 2 seconds
# Threading for non-blocking focus checks
self._focus_check_thread = None
self._focus_check_lock = threading.Lock()
self._focus_check_running = False
self._pending_focus_result = None
# Timers
self._game_focus_timer = QTimer()
self._game_focus_timer.timeout.connect(self._check_game_focus)
self._game_focus_timer.timeout.connect(self._check_game_focus_async)
self._temporary_timer = QTimer()
self._temporary_timer.setSingleShot(True)
@ -101,6 +115,8 @@ class OverlayController(QObject):
"""Stop all timers and hide overlay."""
self._game_focus_timer.stop()
self._temporary_timer.stop()
# Stop any running focus check thread
self._focus_check_running = False
if self.activity_bar:
self.activity_bar.hide()
@ -118,9 +134,9 @@ class OverlayController(QObject):
self._show()
elif self._mode == OverlayMode.OVERLAY_GAME_FOCUSED:
# Show only when game focused
# Show only when game focused - use 5 second poll interval
self._game_focus_timer.start(self.config.game_focus_poll_interval)
self._check_game_focus() # Check immediately
self._check_game_focus_async() # Check immediately (non-blocking)
elif self._mode == OverlayMode.OVERLAY_HOTKEY_TOGGLE:
# Hotkey toggle - start hidden
@ -163,79 +179,106 @@ class OverlayController(QObject):
self._is_visible = False
self.visibility_changed.emit(False)
def _check_game_focus(self):
"""Check if EU game window is focused - non-blocking using QTimer.singleShot."""
# Run the actual check in next event loop iteration to not block
from PyQt6.QtCore import QTimer
QTimer.singleShot(0, self._do_check_game_focus)
def _check_game_focus_async(self):
"""Start focus check in background thread to avoid blocking UI."""
# Use cached result if available and recent
current_time = time.time()
if self._cached_window_state is not None and (current_time - self._last_check_time) < self._cache_ttl:
self._apply_focus_state(self._cached_window_state)
return
def _do_check_game_focus(self):
"""Actual focus check (non-blocking)."""
# Don't start a new thread if one is already running
with self._focus_check_lock:
if self._focus_check_running:
return
self._focus_check_running = True
# Start background thread for focus check
self._focus_check_thread = threading.Thread(
target=self._do_focus_check_threaded,
daemon=True,
name="FocusCheckThread"
)
self._focus_check_thread.start()
def _do_focus_check_threaded(self):
"""Background thread: Check focus without blocking main thread."""
start_time = time.perf_counter()
try:
# Check if window manager is available
if not self.window_manager:
return
# Track time for debugging
import time
start = time.perf_counter()
# Use fast cached window find
is_focused = self._fast_focus_check()
try:
# Set a flag to prevent re-entrancy
if getattr(self, '_checking_focus', False):
return
self._checking_focus = True
# Store result for main thread to apply
self._pending_focus_result = is_focused
# Quick check - limit window enumeration time
eu_window = self._quick_find_eu_window()
# Update cache
self._cached_window_state = is_focused
self._last_check_time = time.time()
if eu_window:
is_focused = eu_window.is_focused
if is_focused != self._game_focused:
self._game_focused = is_focused
if is_focused:
print("[OverlayController] EU focused - showing overlay")
self._show()
else:
print("[OverlayController] EU unfocused - hiding overlay")
self._hide()
else:
# No EU window found - hide if visible
if self._is_visible:
print("[OverlayController] EU window not found - hiding overlay")
self._hide()
self._game_focused = False
# Schedule UI update on main thread
from PyQt6.QtCore import QMetaObject, Qt, Q_ARG
QMetaObject.invokeMethod(
self,
"_apply_pending_focus_result",
Qt.ConnectionType.QueuedConnection
)
# Log if slow
elapsed = (time.perf_counter() - start) * 1000
if elapsed > 100:
# Log performance (only if slow, for debugging)
elapsed = (time.perf_counter() - start_time) * 1000
if elapsed > 50: # Log if >50ms (way above our 10ms target)
print(f"[OverlayController] Warning: Focus check took {elapsed:.1f}ms")
except Exception as e:
print(f"[OverlayController] Error checking focus: {e}")
# Silently handle errors in background thread
pass
finally:
self._checking_focus = False
def _quick_find_eu_window(self):
"""Quick window find with timeout protection."""
import time
start = time.perf_counter()
self._focus_check_running = False
def _fast_focus_check(self) -> bool:
"""Fast focus check using cached window handle and psutil."""
try:
# Try window manager first
if hasattr(self.window_manager, 'find_eu_window'):
# Wrap in timeout check
result = self.window_manager.find_eu_window()
# Use psutil for fast process-based detection (no window enumeration)
if hasattr(self.window_manager, 'is_eu_focused_fast'):
return self.window_manager.is_eu_focused_fast()
# If taking too long, skip future checks
elapsed = (time.perf_counter() - start) * 1000
if elapsed > 500: # More than 500ms is too slow
print(f"[OverlayController] Window find too slow ({elapsed:.1f}ms), disabling focus detection")
self._game_focus_timer.stop()
return None
# Fallback to window manager's optimized method
if hasattr(self.window_manager, 'find_eu_window_cached'):
window = self.window_manager.find_eu_window_cached()
if window:
return window.is_focused
return result
except Exception as e:
print(f"[OverlayController] Window find error: {e}")
return None
# Last resort - standard find (slow)
window = self.window_manager.find_eu_window()
if window:
return window.is_focused
return False
except Exception:
return False
def _apply_pending_focus_result(self):
"""Apply focus result from background thread (called on main thread)."""
if self._pending_focus_result is not None:
self._apply_focus_state(self._pending_focus_result)
self._pending_focus_result = None
def _apply_focus_state(self, is_focused: bool):
"""Apply focus state change (early exit if no change)."""
# Early exit - no change in state
if is_focused == self._game_focused:
return
# Update state and visibility
self._game_focused = is_focused
if is_focused:
self._show()
else:
self._hide()
# Singleton instance

View File

@ -1,547 +0,0 @@
"""
EU-Utility - Screenshot Service Core Module (Security Hardened)
Fast, reliable screen capture functionality with path validation.
"""
import io
import os
import time
import platform
import threading
from collections import deque
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Any, Union
from PIL import Image
from core.security_utils import PathValidator, InputValidator, SecurityError
class ScreenshotService:
"""
Core screenshot service with cross-platform support (Security Hardened).
Features:
- Singleton pattern for single instance across app
- Fast screen capture using PIL.ImageGrab (Windows) or pyautogui (cross-platform)
- Configurable auto-save with timestamps
- Screenshot history (last 20 in memory)
- PNG by default, JPG quality settings
- Thread-safe operations
- Path traversal protection
"""
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
if self._initialized:
return
self._initialized = True
self._lock = threading.Lock()
# Configuration
self._auto_save: bool = True
self._format: str = "PNG"
self._quality: int = 95 # For JPEG
self._history_size: int = 20
# Screenshot history (thread-safe deque)
self._history: deque = deque(maxlen=self._history_size)
self._last_screenshot: Optional[Image.Image] = None
# Platform detection - MUST be before _get_default_save_path()
self._platform = platform.system().lower()
self._use_pil = self._platform == "windows"
# Set save path AFTER platform detection
self._save_path: Path = self._get_default_save_path()
# Lazy init for capture backends
self._pil_available: Optional[bool] = None
self._pyautogui_available: Optional[bool] = None
# Resolve base path for validation
self._base_save_path = self._save_path.resolve()
# Ensure save directory exists
self._ensure_save_directory()
print(f"[Screenshot] Service initialized (auto_save={self._auto_save}, format={self._format})")
def _get_default_save_path(self) -> Path:
"""Get default save path for screenshots."""
# Use Documents/Entropia Universe/Screenshots/ as default
if self._platform == "windows":
documents = Path.home() / "Documents"
else:
documents = Path.home() / "Documents"
return documents / "Entropia Universe" / "Screenshots"
def _ensure_save_directory(self) -> None:
"""Ensure the save directory exists."""
try:
self._save_path.mkdir(parents=True, exist_ok=True)
except Exception as e:
print(f"[Screenshot] Warning: Could not create save directory: {e}")
def _check_pil_grab(self) -> bool:
"""Check if PIL.ImageGrab is available."""
if self._pil_available is not None:
return self._pil_available
try:
from PIL import ImageGrab
self._pil_available = True
return True
except ImportError:
self._pil_available = False
return False
def _check_pyautogui(self) -> bool:
"""Check if pyautogui is available."""
if self._pyautogui_available is not None:
return self._pyautogui_available
try:
import pyautogui
self._pyautogui_available = True
return True
except ImportError:
self._pyautogui_available = False
return False
def capture(self, full_screen: bool = True) -> Image.Image:
"""
Capture screenshot.
Args:
full_screen: If True, capture entire screen. If False, use default region.
Returns:
PIL Image object
Raises:
RuntimeError: If no capture backend is available
"""
with self._lock:
screenshot = self._do_capture(full_screen=full_screen)
# Store in history
self._last_screenshot = screenshot.copy()
self._history.append({
'image': screenshot.copy(),
'timestamp': datetime.now(),
'region': None if full_screen else 'custom'
})
# Auto-save if enabled
if self._auto_save:
self._auto_save_screenshot(screenshot)
return screenshot
def capture_region(self, x: int, y: int, width: int, height: int) -> Image.Image:
"""
Capture specific screen region.
Args:
x: Left coordinate
y: Top coordinate
width: Region width
height: Region height
Returns:
PIL Image object
Raises:
SecurityError: If region parameters are invalid
"""
# Validate region parameters
from core.security_utils import InputValidator
InputValidator.validate_region_coordinates(x, y, width, height)
with self._lock:
screenshot = self._do_capture(region=(x, y, x + width, y + height))
# Store in history
self._last_screenshot = screenshot.copy()
self._history.append({
'image': screenshot.copy(),
'timestamp': datetime.now(),
'region': (x, y, width, height)
})
# Auto-save if enabled
if self._auto_save:
self._auto_save_screenshot(screenshot)
return screenshot
def capture_window(self, window_handle: int) -> Optional[Image.Image]:
"""
Capture specific window by handle (Windows only).
Args:
window_handle: Window handle (HWND on Windows)
Returns:
PIL Image object or None if capture failed
"""
if self._platform != "windows":
print("[Screenshot] capture_window is Windows-only")
return None
# Validate window handle
if not isinstance(window_handle, int) or window_handle <= 0:
print("[Screenshot] Invalid window handle")
return None
try:
import win32gui
import win32ui
import win32con
from ctypes import windll
# Get window dimensions
left, top, right, bottom = win32gui.GetWindowRect(window_handle)
width = right - left
height = bottom - top
# Sanity check dimensions
if width <= 0 or height <= 0 or width > 7680 or height > 4320:
print("[Screenshot] Invalid window dimensions")
return None
# Create device context
hwndDC = win32gui.GetWindowDC(window_handle)
mfcDC = win32ui.CreateDCFromHandle(hwndDC)
saveDC = mfcDC.CreateCompatibleDC()
# Create bitmap
saveBitMap = win32ui.CreateBitmap()
saveBitMap.CreateCompatibleBitmap(mfcDC, width, height)
saveDC.SelectObject(saveBitMap)
# Copy screen into bitmap
result = windll.user32.PrintWindow(window_handle, saveDC.GetSafeHdc(), 3)
# Convert to PIL Image
bmpinfo = saveBitMap.GetInfo()
bmpstr = saveBitMap.GetBitmapBits(True)
screenshot = Image.frombuffer(
'RGB',
(bmpinfo['bmWidth'], bmpinfo['bmHeight']),
bmpstr, 'raw', 'BGRX', 0, 1
)
# Cleanup
win32gui.DeleteObject(saveBitMap.GetHandle())
saveDC.DeleteDC()
mfcDC.DeleteDC()
win32gui.ReleaseDC(window_handle, hwndDC)
if result != 1:
return None
with self._lock:
self._last_screenshot = screenshot.copy()
self._history.append({
'image': screenshot.copy(),
'timestamp': datetime.now(),
'region': 'window',
'window_handle': window_handle
})
if self._auto_save:
self._auto_save_screenshot(screenshot)
return screenshot
except Exception as e:
print(f"[Screenshot] Window capture failed: {e}")
return None
def _do_capture(self, full_screen: bool = True, region: Optional[Tuple[int, int, int, int]] = None) -> Image.Image:
"""Internal capture method."""
# Try PIL.ImageGrab first (Windows, faster)
if self._use_pil and self._check_pil_grab():
from PIL import ImageGrab
if region:
return ImageGrab.grab(bbox=region)
else:
return ImageGrab.grab()
# Fall back to pyautogui (cross-platform)
if self._check_pyautogui():
import pyautogui
if region:
x1, y1, x2, y2 = region
return pyautogui.screenshot(region=(x1, y1, x2 - x1, y2 - y1))
else:
return pyautogui.screenshot()
raise RuntimeError(
"No screenshot backend available. "
"Install pillow (Windows) or pyautogui (cross-platform)."
)
def _auto_save_screenshot(self, image: Image.Image) -> Optional[Path]:
"""Automatically save screenshot with timestamp."""
try:
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S_%f")[:-3]
filename = f"screenshot_{timestamp}.{self._format.lower()}"
return self.save_screenshot(image, filename)
except Exception as e:
print(f"[Screenshot] Auto-save failed: {e}")
return None
def save_screenshot(self, image: Image.Image, filename: Optional[str] = None) -> Path:
"""
Save screenshot to file with path validation.
Args:
image: PIL Image to save
filename: Optional filename (auto-generated if None)
Returns:
Path to saved file
Raises:
SecurityError: If filename is invalid
"""
if filename is None:
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S_%f")[:-3]
filename = f"screenshot_{timestamp}.{self._format.lower()}"
# Sanitize filename
safe_filename = PathValidator.sanitize_filename(filename, '_')
# Ensure correct extension
if not safe_filename.lower().endswith(('.png', '.jpg', '.jpeg')):
safe_filename += f".{self._format.lower()}"
filepath = self._save_path / safe_filename
# Security check: ensure resolved path is within save_path
try:
resolved_path = filepath.resolve()
if not str(resolved_path).startswith(str(self._base_save_path)):
raise SecurityError("Path traversal detected in filename")
except (OSError, ValueError) as e:
print(f"[Screenshot] Security error: {e}")
# Fallback to safe default
safe_filename = f"screenshot_{int(time.time())}.{self._format.lower()}"
filepath = self._save_path / safe_filename
# Save with appropriate settings
if safe_filename.lower().endswith('.jpg') or safe_filename.lower().endswith('.jpeg'):
image = image.convert('RGB') # JPEG doesn't support alpha
image.save(filepath, 'JPEG', quality=self._quality, optimize=True)
else:
image.save(filepath, 'PNG', optimize=True)
return filepath
def get_last_screenshot(self) -> Optional[Image.Image]:
"""
Get the most recent screenshot.
Returns:
PIL Image or None if no screenshots taken yet
"""
with self._lock:
return self._last_screenshot.copy() if self._last_screenshot else None
def get_history(self, limit: Optional[int] = None) -> List[Dict[str, Any]]:
"""
Get screenshot history.
Args:
limit: Maximum number of entries (default: all)
Returns:
List of dicts with 'timestamp', 'region', 'image' keys
"""
with self._lock:
history = list(self._history)
if limit:
history = history[-limit:]
return [
{
'timestamp': entry['timestamp'],
'region': entry['region'],
'image': entry['image'].copy()
}
for entry in history
]
def clear_history(self) -> None:
"""Clear screenshot history from memory."""
with self._lock:
self._history.clear()
self._last_screenshot = None
# ========== Configuration ==========
@property
def auto_save(self) -> bool:
"""Get auto-save setting."""
return self._auto_save
@auto_save.setter
def auto_save(self, value: bool) -> None:
"""Set auto-save setting."""
self._auto_save = bool(value)
@property
def save_path(self) -> Path:
"""Get current save path."""
return self._save_path
@save_path.setter
def save_path(self, path: Union[str, Path]) -> None:
"""Set save path."""
self._save_path = Path(path)
self._base_save_path = self._save_path.resolve()
self._ensure_save_directory()
@property
def format(self) -> str:
"""Get image format (PNG or JPEG)."""
return self._format
@format.setter
def format(self, fmt: str) -> None:
"""Set image format."""
fmt = fmt.upper()
if fmt in ('PNG', 'JPG', 'JPEG'):
self._format = 'PNG' if fmt == 'PNG' else 'JPEG'
else:
raise ValueError(f"Unsupported format: {fmt}")
@property
def quality(self) -> int:
"""Get JPEG quality (1-100)."""
return self._quality
@quality.setter
def quality(self, value: int) -> None:
"""Set JPEG quality (1-100)."""
self._quality = max(1, min(100, int(value)))
def configure(self,
auto_save: Optional[bool] = None,
save_path: Optional[Union[str, Path]] = None,
format: Optional[str] = None,
quality: Optional[int] = None) -> Dict[str, Any]:
"""
Configure screenshot service settings.
Args:
auto_save: Enable/disable auto-save
save_path: Directory to save screenshots
format: Image format (PNG or JPEG)
quality: JPEG quality (1-100)
Returns:
Current configuration as dict
"""
if auto_save is not None:
self.auto_save = auto_save
if save_path is not None:
self.save_path = save_path
if format is not None:
self.format = format
if quality is not None:
self.quality = quality
return self.get_config()
def get_config(self) -> Dict[str, Any]:
"""Get current configuration."""
return {
'auto_save': self._auto_save,
'save_path': str(self._save_path),
'format': self._format,
'quality': self._quality,
'history_size': self._history_size,
'platform': self._platform,
'backend': 'PIL' if self._use_pil else 'pyautogui'
}
# ========== Utility Methods ==========
def image_to_bytes(self, image: Image.Image, format: Optional[str] = None) -> bytes:
"""
Convert PIL Image to bytes.
Args:
image: PIL Image
format: Output format (default: current format setting)
Returns:
Image as bytes
"""
fmt = (format or self._format).upper()
buffer = io.BytesIO()
if fmt == 'JPEG':
image = image.convert('RGB')
image.save(buffer, 'JPEG', quality=self._quality)
else:
image.save(buffer, 'PNG')
return buffer.getvalue()
def get_available_backends(self) -> List[str]:
"""Get list of available capture backends."""
backends = []
if self._check_pil_grab():
backends.append('PIL.ImageGrab')
if self._check_pyautogui():
backends.append('pyautogui')
return backends
def is_available(self) -> bool:
"""Check if screenshot service is available (has working backend)."""
return self._check_pil_grab() or self._check_pyautogui()
# Singleton instance
_screenshot_service = None
def get_screenshot_service() -> ScreenshotService:
"""Get the global ScreenshotService instance."""
global _screenshot_service
if _screenshot_service is None:
_screenshot_service = ScreenshotService()
return _screenshot_service
# Convenience functions for quick screenshots
def quick_capture() -> Image.Image:
"""Quick full-screen capture."""
return get_screenshot_service().capture(full_screen=True)
def quick_capture_region(x: int, y: int, width: int, height: int) -> Image.Image:
"""Quick region capture."""
return get_screenshot_service().capture_region(x, y, width, height)
def quick_save(filename: Optional[str] = None) -> Path:
"""Quick capture and save."""
service = get_screenshot_service()
image = service.capture()
return service.save_screenshot(image, filename)

View File

@ -1,511 +0,0 @@
"""
EU-Utility - Screenshot Service Core Module
Fast, reliable screen capture functionality for all plugins.
Part of core - not a plugin. Plugins access via PluginAPI.
"""
import io
import os
import platform
import threading
from collections import deque
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Any, Union
try:
from PIL import Image
PIL_AVAILABLE = True
except ImportError:
PIL_AVAILABLE = False
Image = None
class ScreenshotService:
"""
Core screenshot service with cross-platform support.
Features:
- Singleton pattern for single instance across app
- Fast screen capture using PIL.ImageGrab (Windows) or pyautogui (cross-platform)
- Configurable auto-save with timestamps
- Screenshot history (last 20 in memory)
- PNG by default, JPG quality settings
- Thread-safe operations
"""
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
if self._initialized:
return
self._initialized = True
self._lock = threading.Lock()
# Configuration
self._auto_save: bool = True
self._format: str = "PNG"
self._quality: int = 95 # For JPEG
self._history_size: int = 20
# Screenshot history (thread-safe deque)
self._history: deque = deque(maxlen=self._history_size)
self._last_screenshot = None
# Platform detection - MUST be before _get_default_save_path()
self._platform = platform.system().lower()
self._use_pil = self._platform == "windows"
# Set save path AFTER platform detection
self._save_path: Path = self._get_default_save_path()
# Lazy init for capture backends
self._pil_available: Optional[bool] = None
self._pyautogui_available: Optional[bool] = None
# Ensure save directory exists
self._ensure_save_directory()
print(f"[Screenshot] Service initialized (auto_save={self._auto_save}, format={self._format})")
def _get_default_save_path(self) -> Path:
"""Get default save path for screenshots."""
# Use Documents/Entropia Universe/Screenshots/ as default
if self._platform == "windows":
documents = Path.home() / "Documents"
else:
documents = Path.home() / "Documents"
return documents / "Entropia Universe" / "Screenshots"
def _ensure_save_directory(self) -> None:
"""Ensure the save directory exists."""
try:
self._save_path.mkdir(parents=True, exist_ok=True)
except Exception as e:
print(f"[Screenshot] Warning: Could not create save directory: {e}")
def _check_pil_grab(self) -> bool:
"""Check if PIL.ImageGrab is available."""
if self._pil_available is not None:
return self._pil_available
try:
from PIL import ImageGrab
self._pil_available = True
return True
except ImportError:
self._pil_available = False
return False
def _check_pyautogui(self) -> bool:
"""Check if pyautogui is available."""
if self._pyautogui_available is not None:
return self._pyautogui_available
try:
import pyautogui
self._pyautogui_available = True
return True
except ImportError:
self._pyautogui_available = False
return False
def capture(self, full_screen: bool = True):
"""
Capture screenshot.
Args:
full_screen: If True, capture entire screen. If False, use default region.
Returns:
PIL Image object
Raises:
RuntimeError: If no capture backend is available
"""
with self._lock:
screenshot = self._do_capture(full_screen=full_screen)
# Store in history
self._last_screenshot = screenshot.copy()
self._history.append({
'image': screenshot.copy(),
'timestamp': datetime.now(),
'region': None if full_screen else 'custom'
})
# Auto-save if enabled
if self._auto_save:
self._auto_save_screenshot(screenshot)
return screenshot
def capture_region(self, x: int, y: int, width: int, height: int):
"""
Capture specific screen region.
Args:
x: Left coordinate
y: Top coordinate
width: Region width
height: Region height
Returns:
PIL Image object
"""
with self._lock:
screenshot = self._do_capture(region=(x, y, x + width, y + height))
# Store in history
self._last_screenshot = screenshot.copy()
self._history.append({
'image': screenshot.copy(),
'timestamp': datetime.now(),
'region': (x, y, width, height)
})
# Auto-save if enabled
if self._auto_save:
self._auto_save_screenshot(screenshot)
return screenshot
def capture_window(self, window_handle: int) -> Optional[Any]:
"""
Capture specific window by handle (Windows only).
Args:
window_handle: Window handle (HWND on Windows)
Returns:
PIL Image object or None if capture failed
"""
if self._platform != "windows":
print("[Screenshot] capture_window is Windows-only")
return None
try:
import win32gui
import win32ui
import win32con
from ctypes import windll
# Get window dimensions
left, top, right, bottom = win32gui.GetWindowRect(window_handle)
width = right - left
height = bottom - top
# Create device context
hwndDC = win32gui.GetWindowDC(window_handle)
mfcDC = win32ui.CreateDCFromHandle(hwndDC)
saveDC = mfcDC.CreateCompatibleDC()
# Create bitmap
saveBitMap = win32ui.CreateBitmap()
saveBitMap.CreateCompatibleBitmap(mfcDC, width, height)
saveDC.SelectObject(saveBitMap)
# Copy screen into bitmap
result = windll.user32.PrintWindow(window_handle, saveDC.GetSafeHdc(), 3)
# Convert to PIL Image
bmpinfo = saveBitMap.GetInfo()
bmpstr = saveBitMap.GetBitmapBits(True)
screenshot = Image.frombuffer(
'RGB',
(bmpinfo['bmWidth'], bmpinfo['bmHeight']),
bmpstr, 'raw', 'BGRX', 0, 1
)
# Cleanup
win32gui.DeleteObject(saveBitMap.GetHandle())
saveDC.DeleteDC()
mfcDC.DeleteDC()
win32gui.ReleaseDC(window_handle, hwndDC)
if result != 1:
return None
with self._lock:
self._last_screenshot = screenshot.copy()
self._history.append({
'image': screenshot.copy(),
'timestamp': datetime.now(),
'region': 'window',
'window_handle': window_handle
})
if self._auto_save:
self._auto_save_screenshot(screenshot)
return screenshot
except Exception as e:
print(f"[Screenshot] Window capture failed: {e}")
return None
def _do_capture(self, full_screen: bool = True, region: Optional[Tuple[int, int, int, int]] = None) -> Image.Image:
"""Internal capture method."""
# Try PIL.ImageGrab first (Windows, faster)
if self._use_pil and self._check_pil_grab():
from PIL import ImageGrab
if region:
return ImageGrab.grab(bbox=region)
else:
return ImageGrab.grab()
# Fall back to pyautogui (cross-platform)
if self._check_pyautogui():
import pyautogui
if region:
x1, y1, x2, y2 = region
return pyautogui.screenshot(region=(x1, y1, x2 - x1, y2 - y1))
else:
return pyautogui.screenshot()
raise RuntimeError(
"No screenshot backend available. "
"Install pillow (Windows) or pyautogui (cross-platform)."
)
def _auto_save_screenshot(self, image: Image.Image) -> Optional[Path]:
"""Automatically save screenshot with timestamp."""
try:
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S_%f")[:-3]
filename = f"screenshot_{timestamp}.{self._format.lower()}"
return self.save_screenshot(image, filename)
except Exception as e:
print(f"[Screenshot] Auto-save failed: {e}")
return None
def save_screenshot(self, image: Image.Image, filename: Optional[str] = None) -> Path:
"""
Save screenshot to file.
Args:
image: PIL Image to save
filename: Optional filename (auto-generated if None)
Returns:
Path to saved file
"""
if filename is None:
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S_%f")[:-3]
filename = f"screenshot_{timestamp}.{self._format.lower()}"
# Ensure correct extension
if not filename.lower().endswith(('.png', '.jpg', '.jpeg')):
filename += f".{self._format.lower()}"
filepath = self._save_path / filename
# Save with appropriate settings
if filename.lower().endswith('.jpg') or filename.lower().endswith('.jpeg'):
image = image.convert('RGB') # JPEG doesn't support alpha
image.save(filepath, 'JPEG', quality=self._quality, optimize=True)
else:
image.save(filepath, 'PNG', optimize=True)
return filepath
def get_last_screenshot(self) -> Optional[Image.Image]:
"""
Get the most recent screenshot.
Returns:
PIL Image or None if no screenshots taken yet
"""
with self._lock:
return self._last_screenshot.copy() if self._last_screenshot else None
def get_history(self, limit: Optional[int] = None) -> List[Dict[str, Any]]:
"""
Get screenshot history.
Args:
limit: Maximum number of entries (default: all)
Returns:
List of dicts with 'timestamp', 'region', 'image' keys
"""
with self._lock:
history = list(self._history)
if limit:
history = history[-limit:]
return [
{
'timestamp': entry['timestamp'],
'region': entry['region'],
'image': entry['image'].copy()
}
for entry in history
]
def clear_history(self) -> None:
"""Clear screenshot history from memory."""
with self._lock:
self._history.clear()
self._last_screenshot = None
# ========== Configuration ==========
@property
def auto_save(self) -> bool:
"""Get auto-save setting."""
return self._auto_save
@auto_save.setter
def auto_save(self, value: bool) -> None:
"""Set auto-save setting."""
self._auto_save = bool(value)
@property
def save_path(self) -> Path:
"""Get current save path."""
return self._save_path
@save_path.setter
def save_path(self, path: Union[str, Path]) -> None:
"""Set save path."""
self._save_path = Path(path)
self._ensure_save_directory()
@property
def format(self) -> str:
"""Get image format (PNG or JPEG)."""
return self._format
@format.setter
def format(self, fmt: str) -> None:
"""Set image format."""
fmt = fmt.upper()
if fmt in ('PNG', 'JPG', 'JPEG'):
self._format = 'PNG' if fmt == 'PNG' else 'JPEG'
else:
raise ValueError(f"Unsupported format: {fmt}")
@property
def quality(self) -> int:
"""Get JPEG quality (1-100)."""
return self._quality
@quality.setter
def quality(self, value: int) -> None:
"""Set JPEG quality (1-100)."""
self._quality = max(1, min(100, int(value)))
def configure(self,
auto_save: Optional[bool] = None,
save_path: Optional[Union[str, Path]] = None,
format: Optional[str] = None,
quality: Optional[int] = None) -> Dict[str, Any]:
"""
Configure screenshot service settings.
Args:
auto_save: Enable/disable auto-save
save_path: Directory to save screenshots
format: Image format (PNG or JPEG)
quality: JPEG quality (1-100)
Returns:
Current configuration as dict
"""
if auto_save is not None:
self.auto_save = auto_save
if save_path is not None:
self.save_path = save_path
if format is not None:
self.format = format
if quality is not None:
self.quality = quality
return self.get_config()
def get_config(self) -> Dict[str, Any]:
"""Get current configuration."""
return {
'auto_save': self._auto_save,
'save_path': str(self._save_path),
'format': self._format,
'quality': self._quality,
'history_size': self._history_size,
'platform': self._platform,
'backend': 'PIL' if self._use_pil else 'pyautogui'
}
# ========== Utility Methods ==========
def image_to_bytes(self, image: Image.Image, format: Optional[str] = None) -> bytes:
"""
Convert PIL Image to bytes.
Args:
image: PIL Image
format: Output format (default: current format setting)
Returns:
Image as bytes
"""
fmt = (format or self._format).upper()
buffer = io.BytesIO()
if fmt == 'JPEG':
image = image.convert('RGB')
image.save(buffer, 'JPEG', quality=self._quality)
else:
image.save(buffer, 'PNG')
return buffer.getvalue()
def get_available_backends(self) -> List[str]:
"""Get list of available capture backends."""
backends = []
if self._check_pil_grab():
backends.append('PIL.ImageGrab')
if self._check_pyautogui():
backends.append('pyautogui')
return backends
def is_available(self) -> bool:
"""Check if screenshot service is available (has working backend)."""
return self._check_pil_grab() or self._check_pyautogui()
# Singleton instance
_screenshot_service = None
def get_screenshot_service() -> ScreenshotService:
"""Get the global ScreenshotService instance."""
global _screenshot_service
if _screenshot_service is None:
_screenshot_service = ScreenshotService()
return _screenshot_service
# Convenience functions for quick screenshots
def quick_capture() -> Image.Image:
"""Quick full-screen capture."""
return get_screenshot_service().capture(full_screen=True)
def quick_capture_region(x: int, y: int, width: int, height: int) -> Image.Image:
"""Quick region capture."""
return get_screenshot_service().capture_region(x, y, width, height)
def quick_save(filename: Optional[str] = None) -> Path:
"""Quick capture and save."""
service = get_screenshot_service()
image = service.capture()
return service.save_screenshot(image, filename)

View File

@ -7,9 +7,11 @@ Windows-specific implementation using ctypes (no external dependencies).
import sys
import time
import threading
from typing import Optional, Tuple, Dict, Any
from dataclasses import dataclass
from pathlib import Path
from functools import lru_cache
# Platform detection
IS_WINDOWS = sys.platform == 'win32'
@ -26,6 +28,13 @@ if IS_WINDOWS:
else:
WINDOWS_AVAILABLE = False
# Optional psutil import for fast process detection
try:
import psutil
PSUTIL_AVAILABLE = True
except ImportError:
PSUTIL_AVAILABLE = False
@dataclass
class WindowInfo:
@ -50,6 +59,41 @@ class ProcessInfo:
cpu_percent: Optional[float]
class _WindowCache:
"""Thread-safe cache for window information."""
def __init__(self, ttl_seconds: float = 3.0):
self._cache: Dict[str, Any] = {}
self._timestamps: Dict[str, float] = {}
self._lock = threading.Lock()
self._ttl = ttl_seconds
def get(self, key: str) -> Any:
"""Get cached value if not expired."""
with self._lock:
if key not in self._cache:
return None
timestamp = self._timestamps.get(key, 0)
if time.time() - timestamp > self._ttl:
# Expired
del self._cache[key]
del self._timestamps[key]
return None
return self._cache[key]
def set(self, key: str, value: Any):
"""Set cached value."""
with self._lock:
self._cache[key] = value
self._timestamps[key] = time.time()
def clear(self):
"""Clear all cached values."""
with self._lock:
self._cache.clear()
self._timestamps.clear()
class WindowManager:
"""
Singleton Window Manager for EU-Utility.
@ -59,12 +103,15 @@ class WindowManager:
"""
_instance = None
_lock = threading.Lock()
# Window search criteria - look for actual game client, not the loader
EU_WINDOW_TITLE = "Entropia Universe Client" # Matches "Entropia Universe Client (64-bit) PlanetName"
EU_PROCESS_NAMES = ["entropia.exe", "entropiauniverse.exe"] # Excludes clientloader.exe (launcher only)
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
@ -74,24 +121,39 @@ class WindowManager:
if self._initialized:
return
# Core state (lazy initialization)
self._window_handle: Optional[int] = None
self._window_info: Optional[WindowInfo] = None
self._process_info: Optional[ProcessInfo] = None
self._last_update: float = 0
self._update_interval: float = 1.0 # seconds
self._update_interval: float = 3.0 # Increased to 3 seconds
# Windows API constants
if IS_WINDOWS and WINDOWS_AVAILABLE:
self._setup_windows_api()
# Caching
self._cache = _WindowCache(ttl_seconds=3.0)
self._window_handle_cache: Optional[int] = None
self._window_handle_cache_time: float = 0
self._window_handle_ttl: float = 5.0 # Cache window handle for 5 seconds
# Windows API (lazy initialization)
self._windows_api_ready = False
self._user32 = None
self._kernel32 = None
self._psutil_initialized = False
self._initialized = True
self._available = IS_WINDOWS and WINDOWS_AVAILABLE
if not self._available:
print("[WindowManager] Windows API not available - running in limited mode")
pass # Don't print here, print on first actual use if needed
def _ensure_windows_api(self):
"""Lazy initialization of Windows API."""
if self._windows_api_ready:
return
if not IS_WINDOWS or not WINDOWS_AVAILABLE:
return
def _setup_windows_api(self):
"""Setup Windows API constants and functions."""
# Window styles
self.GWL_STYLE = -16
self.GWL_EXSTYLE = -20
@ -99,8 +161,8 @@ class WindowManager:
self.WS_MINIMIZE = 0x20000000
# Load user32.dll functions
self.user32 = ctypes.windll.user32
self.kernel32 = ctypes.windll.kernel32
self._user32 = ctypes.windll.user32
self._kernel32 = ctypes.windll.kernel32
# EnumWindows callback type
self.EnumWindowsProc = ctypes.WINFUNCTYPE(
@ -109,12 +171,70 @@ class WindowManager:
wintypes.LPARAM
)
self._windows_api_ready = True
def _ensure_psutil(self):
"""Lazy initialization of psutil-based process detection."""
if self._psutil_initialized:
return
if PSUTIL_AVAILABLE:
# Pre-cache EU process info for fast lookups
self._refresh_eu_process_cache()
self._psutil_initialized = True
def _refresh_eu_process_cache(self):
"""Refresh cached EU process information."""
if not PSUTIL_AVAILABLE:
return
try:
for proc in psutil.process_iter(['pid', 'name', 'exe']):
try:
proc_name = proc.info['name'].lower()
if any(eu_name.lower() == proc_name for eu_name in self.EU_PROCESS_NAMES):
self._cache.set('eu_pid', proc.info['pid'])
self._cache.set('eu_process_name', proc.info['name'])
break
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
except Exception:
pass
# ========== Public API ==========
def is_available(self) -> bool:
"""Check if window manager is fully functional."""
return self._available
def find_eu_window_cached(self) -> Optional[WindowInfo]:
"""
Find EU window with aggressive caching for performance.
Returns cached result if available and recent.
"""
if not self._available:
return None
# Check cache first
cached = self._cache.get('eu_window_info')
if cached:
return cached
# Check if we have a cached window handle that's still valid
if self._window_handle_cache:
current_time = time.time()
if current_time - self._window_handle_cache_time < self._window_handle_ttl:
# Validate handle is still valid
if self._is_window_valid(self._window_handle_cache):
info = self._get_window_info_fast(self._window_handle_cache)
if info:
self._cache.set('eu_window_info', info)
return info
# Fall back to full search
return self.find_eu_window()
def find_eu_window(self) -> Optional[WindowInfo]:
"""
Find the Entropia Universe game window.
@ -125,24 +245,92 @@ class WindowManager:
if not self._available:
return None
# Try by window title first
hwnd = self._find_window_by_title(self.EU_WINDOW_TITLE)
self._ensure_windows_api()
# Try by window title first (faster)
hwnd = self._find_window_by_title_fast(self.EU_WINDOW_TITLE)
if hwnd:
self._window_handle = hwnd
self._window_info = self._get_window_info(hwnd)
self._window_handle_cache = hwnd
self._window_handle_cache_time = time.time()
self._window_info = self._get_window_info_fast(hwnd)
if self._window_info:
self._cache.set('eu_window_info', self._window_info)
return self._window_info
# Try by process name
# Try by process name if title fails
for proc_name in self.EU_PROCESS_NAMES:
hwnd = self._find_window_by_process(proc_name)
hwnd = self._find_window_by_process_fast(proc_name)
if hwnd:
self._window_handle = hwnd
self._window_info = self._get_window_info(hwnd)
self._window_handle_cache = hwnd
self._window_handle_cache_time = time.time()
self._window_info = self._get_window_info_fast(hwnd)
if self._window_info:
self._cache.set('eu_window_info', self._window_info)
return self._window_info
return None
def is_eu_focused_fast(self) -> bool:
"""
Ultra-fast check if EU is focused using psutil + cached window handle.
Target: <10ms execution time.
"""
if not self._available:
return False
self._ensure_windows_api()
self._ensure_psutil()
try:
# Fast path: Check if our cached window is still focused
if self._window_handle_cache:
# Quick foreground window check (no window enumeration)
foreground_hwnd = self._user32.GetForegroundWindow()
if foreground_hwnd == self._window_handle_cache:
# Verify window is still valid
if self._is_window_valid(self._window_handle_cache):
return True
else:
# Cache is stale, clear it
self._window_handle_cache = None
# Medium path: Use psutil to find EU process and check its windows
if PSUTIL_AVAILABLE:
eu_pid = self._cache.get('eu_pid')
if eu_pid:
try:
proc = psutil.Process(eu_pid)
# Check if process still exists
if proc.is_running():
# Get foreground window
foreground_hwnd = self._user32.GetForegroundWindow()
# Get PID of foreground window
fg_pid = wintypes.DWORD()
self._user32.GetWindowThreadProcessId(foreground_hwnd, ctypes.byref(fg_pid))
if fg_pid.value == eu_pid:
# Found it! Cache the handle
self._window_handle_cache = foreground_hwnd
self._window_handle_cache_time = time.time()
return True
else:
# Process died, refresh cache
self._refresh_eu_process_cache()
except (psutil.NoSuchProcess, psutil.AccessDenied):
self._refresh_eu_process_cache()
# Slow path: Find window and check focus
window = self.find_eu_window_cached()
if window:
return window.is_focused
return False
except Exception:
return False
def get_window_rect(self) -> Optional[Tuple[int, int, int, int]]:
"""
Get the window rectangle (left, top, right, bottom).
@ -167,18 +355,7 @@ class WindowManager:
Returns:
True if EU window is active, False otherwise
"""
if not self._available:
return False
if not self._window_handle:
self.find_eu_window()
if not self._window_handle:
return False
# Get foreground window
foreground_hwnd = self.user32.GetForegroundWindow()
return foreground_hwnd == self._window_handle
return self.is_eu_focused_fast()
def is_window_visible(self) -> bool:
"""
@ -207,6 +384,8 @@ class WindowManager:
if not self._available:
return False
self._ensure_windows_api()
if not self._window_handle:
self.find_eu_window()
@ -215,13 +394,13 @@ class WindowManager:
try:
# Show window if minimized
self.user32.ShowWindow(self._window_handle, 9) # SW_RESTORE = 9
self._user32.ShowWindow(self._window_handle, 9) # SW_RESTORE = 9
# Bring to front
result = self.user32.SetForegroundWindow(self._window_handle)
result = self._user32.SetForegroundWindow(self._window_handle)
# Force window to top
self.user32.SetWindowPos(
self._user32.SetWindowPos(
self._window_handle,
-1, # HWND_TOPMOST
0, 0, 0, 0,
@ -229,7 +408,7 @@ class WindowManager:
)
# Remove topmost flag but keep on top
self.user32.SetWindowPos(
self._user32.SetWindowPos(
self._window_handle,
-2, # HWND_NOTOPMOST
0, 0, 0, 0,
@ -238,7 +417,6 @@ class WindowManager:
return bool(result)
except Exception as e:
print(f"[WindowManager] Failed to bring window to front: {e}")
return False
def get_eu_process_info(self) -> Optional[ProcessInfo]:
@ -251,6 +429,13 @@ class WindowManager:
if not self._available:
return None
self._ensure_windows_api()
# Try cache first
cached = self._cache.get('eu_process_info')
if cached:
return cached
if not self._window_handle:
self.find_eu_window()
@ -259,20 +444,24 @@ class WindowManager:
# Get PID from window
pid = wintypes.DWORD()
self.user32.GetWindowThreadProcessId(self._window_handle, ctypes.byref(pid))
self._user32.GetWindowThreadProcessId(self._window_handle, ctypes.byref(pid))
if pid.value == 0:
return None
self._process_info = self._get_process_info(pid.value)
self._process_info = self._get_process_info_fast(pid.value)
if self._process_info:
self._cache.set('eu_process_info', self._process_info)
return self._process_info
def get_window_handle(self) -> Optional[int]:
"""Get the current window handle."""
return self._window_handle
return self._window_handle_cache or self._window_handle
def refresh(self) -> Optional[WindowInfo]:
"""Force refresh of window information."""
self._cache.clear()
self._window_handle_cache = None
self._last_update = 0
return self.find_eu_window()
@ -282,118 +471,179 @@ class WindowManager:
"""Update cached window info if needed."""
current_time = time.time()
if current_time - self._last_update > self._update_interval:
if self._window_handle:
self._window_info = self._get_window_info(self._window_handle)
if self._window_handle_cache and self._is_window_valid(self._window_handle_cache):
self._window_info = self._get_window_info_fast(self._window_handle_cache)
else:
self.find_eu_window()
self._last_update = current_time
def _find_window_by_title(self, title: str) -> Optional[int]:
"""Find window by title (partial match) - with timeout protection."""
def _is_window_valid(self, hwnd: int) -> bool:
"""Check if a window handle is still valid (fast)."""
if not self._available or not hwnd:
return False
try:
return bool(self._user32.IsWindow(hwnd))
except Exception:
return False
def _find_window_by_title_fast(self, title: str) -> Optional[int]:
"""
Find window by title with strict timeout and limits.
Target: <50ms execution time.
"""
found_hwnd = [None]
start_time = time.time()
start_time = time.perf_counter()
window_count = [0]
MAX_WINDOWS = 500 # Limit windows to check
MAX_TIME = 0.1 # 100ms timeout
MAX_WINDOWS = 200 # Reduced from 500
MAX_TIME = 0.025 # 25ms timeout (reduced from 100ms)
title_lower = title.lower()
def callback(hwnd, extra):
# Check timeout
if time.time() - start_time > MAX_TIME:
return False # Stop enumeration - timeout
# Check timeout first
if time.perf_counter() - start_time > MAX_TIME:
return False
# Check window limit
window_count[0] += 1
if window_count[0] > MAX_WINDOWS:
return False # Stop enumeration - too many windows
return False
if not self.user32.IsWindowVisible(hwnd):
# Quick visibility check
if not self._user32.IsWindowVisible(hwnd):
return True
# Quick check - skip windows with no title
text = ctypes.create_unicode_buffer(256)
self.user32.GetWindowTextW(hwnd, text, 256)
window_title = text.value
# Get window text with smaller buffer
text = ctypes.create_unicode_buffer(128) # Reduced from 256
length = self._user32.GetWindowTextW(hwnd, text, 128)
if not window_title: # Skip empty titles
if length == 0: # Skip empty titles quickly
return True
# Fast case-insensitive check
if title.lower() in window_title.lower():
# Fast lowercase comparison
if title_lower in text.value.lower():
found_hwnd[0] = hwnd
return False # Stop enumeration - found!
return True
proc = self.EnumWindowsProc(callback)
self.user32.EnumWindows(proc, 0)
self._user32.EnumWindows(proc, 0)
return found_hwnd[0]
def _find_window_by_process(self, process_name: str) -> Optional[int]:
"""Find window by process name - with timeout protection."""
def _find_window_by_process_fast(self, process_name: str) -> Optional[int]:
"""
Find window by process name with strict timeout.
Uses psutil if available for faster process detection.
"""
# Try psutil first for faster process-based lookup
if PSUTIL_AVAILABLE:
try:
for proc in psutil.process_iter(['pid', 'name']):
try:
if process_name.lower() in proc.info['name'].lower():
# Found process, now find its window
target_pid = proc.info['pid']
# Look for window with this PID
found = self._find_window_by_pid(target_pid)
if found:
return found
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
except Exception:
pass
# Fall back to enumeration
found_hwnd = [None]
start_time = time.time()
start_time = time.perf_counter()
window_count = [0]
MAX_WINDOWS = 300 # Lower limit for process check (slower)
MAX_TIME = 0.15 # 150ms timeout
MAX_WINDOWS = 150 # Reduced from 300
MAX_TIME = 0.030 # 30ms timeout (reduced from 150ms)
process_name_lower = process_name.lower()
def callback(hwnd, extra):
# Check timeout
if time.time() - start_time > MAX_TIME:
return False # Stop enumeration - timeout
if time.perf_counter() - start_time > MAX_TIME:
return False
# Check window limit
window_count[0] += 1
if window_count[0] > MAX_WINDOWS:
return False # Stop enumeration - too many windows
return False
if not self.user32.IsWindowVisible(hwnd):
if not self._user32.IsWindowVisible(hwnd):
return True
# Get process ID
pid = wintypes.DWORD()
self.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
self._user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
if pid.value == 0:
return True
# Check process name (this is slower, so we limit more)
try:
proc_info = self._get_process_info(pid.value)
if proc_info and process_name.lower() in proc_info.name.lower():
# Use cached process info if available
cache_key = f"proc_{pid.value}"
proc_info = self._cache.get(cache_key)
if proc_info is None:
# Get process info (cached)
proc_info = self._get_process_info_fast(pid.value)
if proc_info:
self._cache.set(cache_key, proc_info)
if proc_info and process_name_lower in proc_info.name.lower():
found_hwnd[0] = hwnd
return False # Stop enumeration - found!
except Exception:
pass # Skip on error
return False
return True
proc = self.EnumWindowsProc(callback)
self.user32.EnumWindows(proc, 0)
self._user32.EnumWindows(proc, 0)
return found_hwnd[0]
def _get_window_info(self, hwnd: int) -> Optional[WindowInfo]:
"""Get detailed information about a window."""
def _find_window_by_pid(self, target_pid: int) -> Optional[int]:
"""Find a window by its process ID."""
found_hwnd = [None]
def callback(hwnd, extra):
if not self._user32.IsWindowVisible(hwnd):
return True
pid = wintypes.DWORD()
self._user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
if pid.value == target_pid:
found_hwnd[0] = hwnd
return False
return True
proc = self.EnumWindowsProc(callback)
self._user32.EnumWindows(proc, 0)
return found_hwnd[0]
def _get_window_info_fast(self, hwnd: int) -> Optional[WindowInfo]:
"""Get window info with minimal overhead and timeout protection."""
try:
# Get window rect
rect = wintypes.RECT()
if not self.user32.GetWindowRect(hwnd, ctypes.byref(rect)):
if not self._user32.GetWindowRect(hwnd, ctypes.byref(rect)):
return None
# Get window text
text = ctypes.create_unicode_buffer(256)
self.user32.GetWindowTextW(hwnd, text, 256)
# Get window text (smaller buffer for speed)
text = ctypes.create_unicode_buffer(128)
self._user32.GetWindowTextW(hwnd, text, 128)
# Get PID
pid = wintypes.DWORD()
self.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
self._user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
# Check visibility
is_visible = bool(self.user32.IsWindowVisible(hwnd))
is_visible = bool(self._user32.IsWindowVisible(hwnd))
# Check if focused
foreground = self.user32.GetForegroundWindow()
foreground = self._user32.GetForegroundWindow()
is_focused = (foreground == hwnd)
return WindowInfo(
@ -406,26 +656,38 @@ class WindowManager:
is_visible=is_visible,
is_focused=is_focused
)
except Exception as e:
print(f"[WindowManager] Error getting window info: {e}")
except Exception:
return None
def _get_process_info(self, pid: int) -> Optional[ProcessInfo]:
"""Get process information."""
def _get_process_info_fast(self, pid: int) -> Optional[ProcessInfo]:
"""Get process info with caching and minimal overhead."""
# Try psutil first (much faster)
if PSUTIL_AVAILABLE:
try:
# Use Windows WMI or tasklist for process info
import subprocess
proc = psutil.Process(pid)
info = proc.as_dict(attrs=['pid', 'name', 'exe', 'memory_info'])
return ProcessInfo(
pid=info['pid'],
name=info['name'],
executable_path=info.get('exe'),
memory_usage=info['memory_info'].rss if info.get('memory_info') else None,
cpu_percent=None
)
except (psutil.NoSuchProcess, psutil.AccessDenied):
return None
except Exception:
pass
# Try tasklist first
# Fall back to tasklist (slower)
try:
result = subprocess.run(
['tasklist', '/FI', f'PID eq {pid}', '/FO', 'CSV', '/NH'],
capture_output=True,
text=True,
timeout=5
timeout=2 # Reduced from 5
)
if result.returncode == 0 and result.stdout.strip():
# Parse CSV output
lines = result.stdout.strip().split('\n')
for line in lines:
if str(pid) in line:
@ -447,16 +709,9 @@ class WindowManager:
memory_usage=None,
cpu_percent=None
)
except Exception as e:
print(f"[WindowManager] Error getting process info: {e}")
except Exception:
return None
def _check_window_exists(self, hwnd: int) -> bool:
"""Check if a window handle is still valid."""
if not self._available:
return False
return bool(self.user32.IsWindow(hwnd))
# Singleton instance
_window_manager = None
@ -473,7 +728,7 @@ def get_window_manager() -> WindowManager:
def is_eu_running() -> bool:
"""Quick check if Entropia Universe is running."""
wm = get_window_manager()
return wm.find_eu_window() is not None
return wm.find_eu_window_cached() is not None
def get_eu_window_rect() -> Optional[Tuple[int, int, int, int]]:
@ -496,7 +751,7 @@ def wait_for_eu(timeout: float = 30.0) -> bool:
start_time = time.time()
while time.time() - start_time < timeout:
if wm.find_eu_window():
if wm.find_eu_window_cached():
return True
time.sleep(0.5)

328
pyproject.toml Normal file
View File

@ -0,0 +1,328 @@
# EU-Utility - Modern Python Packaging Configuration
# https://github.com/ImpulsiveFPS/EU-Utility
[build-system]
requires = [
"setuptools>=65.0",
"wheel>=0.41.0",
"setuptools-scm>=7.0",
]
build-backend = "setuptools.build_meta"
[project]
name = "eu-utility"
version = "2.1.0"
description = "A versatile Entropia Universe utility suite with a modular plugin system"
readme = "README.md"
license = {text = "MIT"}
authors = [
{name = "ImpulsiveFPS", email = "dev@impulsivefps.com"},
]
maintainers = [
{name = "ImpulsiveFPS", email = "dev@impulsivefps.com"},
]
keywords = [
"entropia universe",
"game utility",
"overlay",
"plugin system",
"ocr",
"tracker",
"calculator",
"gaming",
"mmorpg",
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3 :: Only",
"Topic :: Games/Entertainment",
"Topic :: Utilities",
"Topic :: Desktop Environment",
"Environment :: X11 Applications :: Qt",
"Environment :: Win32 (MS Windows)",
"Natural Language :: English",
]
requires-python = ">=3.11"
dependencies = [
"PyQt6>=6.4.0,<7.0.0",
"keyboard>=0.13.5,<1.0.0",
"psutil>=5.9.0,<6.0.0",
"pyperclip>=1.8.2,<2.0.0",
"easyocr>=1.7.0,<2.0.0",
"pytesseract>=0.3.10,<1.0.0",
"pyautogui>=0.9.54,<1.0.0",
"pillow>=10.0.0,<11.0.0",
"requests>=2.28.0,<3.0.0",
"urllib3>=1.26.0,<3.0.0",
"numpy>=1.21.0,<2.0.0",
'portalocker>=2.7.0; platform_system=="Windows"',
'pywin32>=306; platform_system=="Windows"',
]
[project.optional-dependencies]
spotify = [
"spotipy>=2.23.0,<3.0.0",
'pycaw>=20230407; platform_system=="Windows"',
]
discord = [
"pypresence>=4.3.0,<5.0.0",
]
all = [
"spotipy>=2.23.0,<3.0.0",
'pycaw>=20230407; platform_system=="Windows"',
"pypresence>=4.3.0,<5.0.0",
]
dev = [
"pytest>=7.4.0,<8.0.0",
"pytest-cov>=4.1.0,<5.0.0",
"pytest-mock>=3.11.0,<4.0.0",
"pytest-benchmark>=4.0.0,<5.0.0",
"pytest-qt>=4.2.0,<5.0.0",
"pytest-xvfb>=2.0.0,<3.0.0",
"black>=23.0.0,<24.0.0",
"flake8>=6.0.0,<7.0.0",
"mypy>=1.5.0,<2.0.0",
"isort>=5.12.0,<6.0.0",
"pydocstyle>=6.3.0,<7.0.0",
"bandit>=1.7.5,<2.0.0",
"safety>=2.3.0,<3.0.0",
"sphinx>=7.0.0,<8.0.0",
"sphinx-rtd-theme>=1.3.0,<2.0.0",
"myst-parser>=2.0.0,<3.0.0",
"build>=0.10.0,<1.0.0",
"twine>=4.0.0,<5.0.0",
"wheel>=0.41.0,<1.0.0",
"pre-commit>=3.4.0,<4.0.0",
]
[project.scripts]
eu-utility = "core.main:main"
eu-utility-secure = "core.main_optimized:main"
[project.gui-scripts]
eu-utility-gui = "core.main:main"
[project.urls]
Homepage = "https://github.com/ImpulsiveFPS/EU-Utility"
Documentation = "https://github.com/ImpulsiveFPS/EU-Utility/tree/main/docs"
Repository = "https://github.com/ImpulsiveFPS/EU-Utility.git"
"Bug Tracker" = "https://github.com/ImpulsiveFPS/EU-Utility/issues"
Changelog = "https://github.com/ImpulsiveFPS/EU-Utility/blob/main/CHANGELOG.md"
# =============================================================================
# TOOL CONFIGURATIONS
# =============================================================================
# Black - Code Formatter
[tool.black]
line-length = 100
target-version = ["py311", "py312"]
include = '\.pyi?$'
extend-exclude = '''
/(
\.git
| \.venv
| venv
| env
| build
| dist
| __pycache__
| \.pytest_cache
| htmlcov
| \.tox
| migrations
)/
'''
skip-string-normalization = false
preview = false
# isort - Import Sorting
[tool.isort]
profile = "black"
line_length = 100
multi_line_output = 3
include_trailing_comma = true
force_grid_wrap = 0
use_parentheses = true
ensure_newline_before_comments = true
skip = [".git", "__pycache__", "venv", ".venv", "build", "dist"]
known_first_party = ["core", "plugins"]
known_third_party = ["PyQt6", "pytest"]
# flake8 - Linting
[tool.flake8]
max-line-length = 100
extend-ignore = [
"E203", # Whitespace before ':' (conflicts with black)
"E501", # Line too long (handled by black)
"W503", # Line break before binary operator (black style)
]
exclude = [
".git",
"__pycache__",
".venv",
"venv",
"build",
"dist",
".pytest_cache",
"htmlcov",
".tox",
"migrations",
]
per-file-ignores = [
"__init__.py:F401,F403",
"test_*.py:S101",
]
max-complexity = 10
# mypy - Type Checking
[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
disallow_untyped_decorators = false
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
warn_unreachable = true
strict_equality = true
show_error_codes = true
show_column_numbers = true
show_error_context = true
pretty = true
# Module-specific settings
[[tool.mypy.overrides]]
module = [
"pytesseract.*",
"pyautogui.*",
"easyocr.*",
"keyboard.*",
"psutil.*",
"PIL.*",
"win32con.*",
"win32gui.*",
"win32api.*",
"portalocker.*",
]
ignore_missing_imports = true
# pytest - Testing
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
"-v",
"--tb=short",
"--strict-markers",
"--cov=core",
"--cov=plugins",
"--cov-report=term-missing",
"--cov-report=html:htmlcov",
"--cov-report=xml:coverage.xml",
"--cov-fail-under=80",
]
markers = [
"unit: Unit tests for individual components",
"integration: Integration tests for plugin interactions",
"ui: UI automation tests",
"performance: Performance benchmarks",
"slow: Slow tests that may be skipped in CI",
"requires_qt: Tests requiring PyQt6",
"requires_network: Tests requiring network access",
]
filterwarnings = [
"ignore::DeprecationWarning",
"ignore::PendingDeprecationWarning",
]
# Coverage - Code Coverage
[tool.coverage.run]
source = ["core", "plugins"]
branch = true
omit = [
"*/tests/*",
"*/test_*",
"*/venv/*",
"*/.venv/*",
"setup.py",
"run_tests.py",
"code_review_report.py",
"*/benchmarks/*",
"*_vulnerable.py",
]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"if self.debug:",
"if settings.DEBUG",
"raise AssertionError",
"raise NotImplementedError",
"if 0:",
"if __name__ == .__main__.:",
"class .*\\bProtocol\\):",
"@(abc\\.)?abstractmethod",
"pass",
]
fail_under = 80
show_missing = true
skip_covered = false
[tool.coverage.html]
directory = "htmlcov"
# Bandit - Security Linter
[tool.bandit]
exclude_dirs = [
"tests",
"test",
".venv",
"venv",
"build",
"dist",
]
skips = [
"B101", # assert_used (common in Python code)
"B311", # random (acceptable for non-cryptographic use)
]
# Pydocstyle - Docstring Conventions
[tool.pydocstyle]
convention = "google"
match = "(?!test_).*\\.py"
match_dir = "^(?!tests|\\.venv|venv|build|dist).*"
add_ignore = [
"D100", # Missing docstring in public module
"D104", # Missing docstring in public package
"D107", # Missing docstring in __init__
]
# setuptools - Package Discovery
[tool.setuptools]
packages = ["core", "core.*", "plugins", "plugins.*"]
include-package-data = true
zip-safe = false
[tool.setuptools.package-data]
core = ["*.json", "*.yaml", "*.yml", "*.css", "*.qss"]
plugins = ["*/assets/*", "*/templates/*"]
[tool.setuptools.exclude-package-data]
core = ["tests/*", "*_test.py", "test_*.py"]
plugins = ["tests/*", "*_test.py", "test_*.py"]

View File

@ -7,12 +7,6 @@ addopts =
-v
--tb=short
--strict-markers
--cov=core
--cov=plugins
--cov-report=term-missing
--cov-report=html:htmlcov
--cov-report=xml:coverage.xml
--cov-fail-under=80
markers =
unit: Unit tests for individual components
integration: Integration tests for plugin interactions

View File

@ -1,6 +1,98 @@
pytest>=7.4.0
pytest-cov>=4.1.0
pytest-mock>=3.11.0
pytest-benchmark>=4.0.0
pytest-qt>=4.2.0
pytest-xvfb>=2.0.0
# EU-Utility - Development Dependencies
# ======================================
# These dependencies are required for development, testing, and CI/CD.
# They are NOT required for running EU-Utility in production.
#
# Install with: pip install -r requirements-dev.txt
# Or use: pip install -e ".[dev]"
# =============================================================================
# =============================================================================
# Testing Framework
# =============================================================================
# pytest is the main testing framework
pytest>=7.4.0,<8.0.0
# pytest-cov provides code coverage reporting
pytest-cov>=4.1.0,<5.0.0
# pytest-mock provides mocking utilities for pytest
pytest-mock>=3.11.0,<4.0.0
# pytest-benchmark provides performance benchmarking
pytest-benchmark>=4.0.0,<5.0.0
# pytest-qt provides Qt/PyQt6 testing support
pytest-qt>=4.2.0,<5.0.0
# pytest-xvfb allows running GUI tests headless on Linux
pytest-xvfb>=2.0.0,<3.0.0
# =============================================================================
# Code Quality and Linting
# =============================================================================
# black is the code formatter
black>=23.0.0,<24.0.0
# flake8 is the linter (style and error checking)
flake8>=6.0.0,<7.0.0
# mypy is the static type checker
mypy>=1.5.0,<2.0.0
# isort sorts imports automatically
isort>=5.12.0,<6.0.0
# pydocstyle checks docstring conventions
pydocstyle>=6.3.0,<7.0.0
# =============================================================================
# Security Analysis
# =============================================================================
# bandit finds common security issues in Python code
bandit>=1.7.5,<2.0.0
# safety checks for known security vulnerabilities in dependencies
safety>=2.3.0,<3.0.0
# pip-audit is an alternative to safety for vulnerability scanning
# pip-audit>=2.6.0,<3.0.0
# =============================================================================
# Documentation
# =============================================================================
# sphinx is the documentation generator
sphinx>=7.0.0,<8.0.0
# sphinx-rtd-theme is the Read the Docs theme
sphinx-rtd-theme>=1.3.0,<2.0.0
# myst-parser allows writing documentation in Markdown
myst-parser>=2.0.0,<3.0.0
# sphinx-autobuild provides live-reload for documentation development
# sphinx-autobuild>=2021.3.14,<2.0.0
# =============================================================================
# Build and Packaging
# =============================================================================
# build is the modern Python build frontend
build>=0.10.0,<1.0.0
# twine uploads packages to PyPI
twine>=4.0.0,<5.0.0
# wheel builds wheel distributions
wheel>=0.41.0,<1.0.0
# =============================================================================
# Development Tools
# =============================================================================
# pre-commit runs checks before commits
pre-commit>=3.4.0,<4.0.0
# bump2version manages version bumping
# bump2version>=1.0.1,<2.0.0
# pipdeptree shows dependency tree
# pipdeptree>=2.13.0,<3.0.0

View File

@ -1,40 +1,73 @@
# Core dependencies
PyQt6>=6.4.0
keyboard>=0.13.5
# EU-Utility - Production Dependencies
# =====================================
# These are the minimum dependencies required to run EU-Utility.
# For development dependencies, see requirements-dev.txt
# For all optional features, install with: pip install "eu-utility[all]"
# =============================================================================
# OCR and Image Processing (for Game Reader and Skill Scanner)
easyocr>=1.7.0
pytesseract>=0.3.10 # Tesseract OCR wrapper
pyautogui>=0.9.54
pillow>=10.0.0
# =============================================================================
# Core GUI Framework
# =============================================================================
# PyQt6 is the main GUI framework for EU-Utility.
# Provides the overlay window, widgets, and theming system.
PyQt6>=6.4.0,<7.0.0
# Windows-specific (auto-installs only on Windows)
# =============================================================================
# System Integration
# =============================================================================
# keyboard provides global hotkey support for triggering the overlay
# from anywhere, even when the game is focused.
keyboard>=0.13.5,<1.0.0
# psutil is used for system monitoring in the Analytics plugin
# and for process management.
psutil>=5.9.0,<6.0.0
# pyperclip provides cross-platform clipboard access for copy/paste
# functionality in various plugins.
pyperclip>=1.8.2,<2.0.0
# =============================================================================
# OCR and Image Processing
# =============================================================================
# easyocr is the recommended OCR engine for reading in-game text.
# It downloads models automatically on first use.
easyocr>=1.7.0,<2.0.0
# pytesseract provides an alternative OCR backend using Tesseract.
# Requires Tesseract OCR to be installed separately.
pytesseract>=0.3.10,<1.0.0
# pyautogui is used for screen capture and UI automation tasks.
pyautogui>=0.9.54,<1.0.0
# Pillow (PIL) is used for image processing in OCR and screenshot features.
pillow>=10.0.0,<11.0.0
# =============================================================================
# HTTP and Networking
# =============================================================================
# requests is used for API calls to Entropia Nexus and other services.
requests>=2.28.0,<3.0.0
# urllib3 is a dependency of requests but pinned for security.
urllib3>=1.26.0,<3.0.0
# =============================================================================
# Data Processing
# =============================================================================
# numpy is required by easyocr and used for data processing in calculators
# and trackers.
numpy>=1.21.0,<2.0.0
# =============================================================================
# Windows-Specific Dependencies
# =============================================================================
# These are only installed on Windows systems.
# They provide Windows API access for advanced features.
# portalocker provides file locking for Windows
portalocker>=2.7.0; platform_system=="Windows"
pywin32>=306; platform_system=="Windows" # Windows API access
# System monitoring (for Analytics plugin)
psutil>=5.9.0
# Clipboard support
pyperclip>=1.8.2
# HTTP requests
requests>=2.28.0
urllib3>=1.26.0
# Data processing
numpy>=1.21.0
# Optional plugin dependencies
# Uncomment if using specific plugins:
# For Spotify Controller (advanced features)
# spotipy>=2.23.0
# pycaw>=20230407; platform_system=="Windows"
# For Discord Rich Presence
# pypresence>=4.3.0
# Development
# pytest>=7.0.0
# black>=23.0.0
# pywin32 provides Windows API access for window management and system integration
pywin32>=306; platform_system=="Windows"

249
setup.py Normal file
View File

@ -0,0 +1,249 @@
#!/usr/bin/env python3
"""
EU-Utility - Setup Script
A versatile Entropia Universe utility suite with a modular plugin system.
For more information, visit: https://github.com/ImpulsiveFPS/EU-Utility
"""
import os
import sys
from pathlib import Path
# Ensure setuptools is available
from setuptools import setup, find_packages
# Project root directory
PROJECT_ROOT = Path(__file__).parent.resolve()
# Read version from core/__init__.py
def get_version():
"""Extract version from core/__init__.py."""
init_file = PROJECT_ROOT / "core" / "__init__.py"
with open(init_file, "r", encoding="utf-8") as f:
for line in f:
if line.startswith("__version__"):
return line.split("=")[1].strip().strip('"\'')
raise RuntimeError("Version not found in core/__init__.py")
# Read long description from README
def get_long_description():
"""Read README.md for long description."""
readme_file = PROJECT_ROOT / "README.md"
if readme_file.exists():
with open(readme_file, "r", encoding="utf-8") as f:
return f.read()
return ""
# Runtime dependencies
INSTALL_REQUIRES = [
# Core GUI Framework
"PyQt6>=6.4.0,<7.0.0",
# System Integration
"keyboard>=0.13.5,<1.0.0",
"psutil>=5.9.0,<6.0.0",
"pyperclip>=1.8.2,<2.0.0",
# OCR and Image Processing
"easyocr>=1.7.0,<2.0.0",
"pytesseract>=0.3.10,<1.0.0",
"pyautogui>=0.9.54,<1.0.0",
"pillow>=10.0.0,<11.0.0",
# HTTP and Networking
"requests>=2.28.0,<3.0.0",
"urllib3>=1.26.0,<3.0.0",
# Data Processing
"numpy>=1.21.0,<2.0.0",
# Windows-specific dependencies (installed only on Windows)
'portalocker>=2.7.0; platform_system=="Windows"',
'pywin32>=306; platform_system=="Windows"',
]
# Development dependencies
DEV_REQUIRES = [
# Testing
"pytest>=7.4.0,<8.0.0",
"pytest-cov>=4.1.0,<5.0.0",
"pytest-mock>=3.11.0,<4.0.0",
"pytest-benchmark>=4.0.0,<5.0.0",
"pytest-qt>=4.2.0,<5.0.0",
"pytest-xvfb>=2.0.0,<3.0.0",
# Code Quality
"black>=23.0.0,<24.0.0",
"flake8>=6.0.0,<7.0.0",
"mypy>=1.5.0,<2.0.0",
"isort>=5.12.0,<6.0.0",
"pydocstyle>=6.3.0,<7.0.0",
# Security
"bandit>=1.7.5,<2.0.0",
"safety>=2.3.0,<3.0.0",
# Documentation
"sphinx>=7.0.0,<8.0.0",
"sphinx-rtd-theme>=1.3.0,<2.0.0",
"myst-parser>=2.0.0,<3.0.0",
# Build and Packaging
"build>=0.10.0,<1.0.0",
"twine>=4.0.0,<5.0.0",
"wheel>=0.41.0,<1.0.0",
# Pre-commit hooks
"pre-commit>=3.4.0,<4.0.0",
]
# Optional plugin dependencies
EXTRAS_REQUIRE = {
# Advanced media control features
"spotify": [
"spotipy>=2.23.0,<3.0.0",
'pycaw>=20230407; platform_system=="Windows"',
],
# Discord Rich Presence integration
"discord": [
"pypresence>=4.3.0,<5.0.0",
],
# All optional features
"all": [
"spotipy>=2.23.0,<3.0.0",
'pycaw>=20230407; platform_system=="Windows"',
"pypresence>=4.3.0,<5.0.0",
],
# Development extras (includes all dev dependencies)
"dev": DEV_REQUIRES,
}
# Package classifiers
CLASSIFIERS = [
# Development Status
"Development Status :: 4 - Beta",
# Intended Audience
"Intended Audience :: End Users/Desktop",
"Intended Audience :: Gamers",
# License
"License :: OSI Approved :: MIT License",
# Operating System
"Operating System :: OS Independent",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX :: Linux",
# Programming Language
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3 :: Only",
# Topic
"Topic :: Games/Entertainment",
"Topic :: Utilities",
"Topic :: Desktop Environment",
# Environment
"Environment :: X11 Applications :: Qt",
"Environment :: Win32 (MS Windows)",
# Natural Language
"Natural Language :: English",
]
# Entry points for CLI usage
ENTRY_POINTS = {
"console_scripts": [
"eu-utility=core.main:main",
"eu-utility-secure=core.main_optimized:main",
],
"gui_scripts": [
"eu-utility-gui=core.main:main",
],
}
# Package data to include
PACKAGE_DATA = {
"core": ["*.json", "*.yaml", "*.yml", "*.css", "*.qss"],
"plugins": ["*/assets/*", "*/templates/*"],
}
def main():
"""Execute setup."""
setup(
# Basic metadata
name="eu-utility",
version=get_version(),
author="ImpulsiveFPS",
author_email="dev@impulsivefps.com",
maintainer="ImpulsiveFPS",
maintainer_email="dev@impulsivefps.com",
url="https://github.com/ImpulsiveFPS/EU-Utility",
project_urls={
"Bug Reports": "https://github.com/ImpulsiveFPS/EU-Utility/issues",
"Source": "https://github.com/ImpulsiveFPS/EU-Utility",
"Documentation": "https://github.com/ImpulsiveFPS/EU-Utility/tree/main/docs",
"Changelog": "https://github.com/ImpulsiveFPS/EU-Utility/blob/main/CHANGELOG.md",
},
# Descriptions
description="A versatile Entropia Universe utility suite with a modular plugin system",
long_description=get_long_description(),
long_description_content_type="text/markdown",
keywords=[
"entropia universe",
"game utility",
"overlay",
"plugin system",
"ocr",
"tracker",
"calculator",
"gaming",
"mmorpg",
],
# Classifiers
classifiers=CLASSIFIERS,
# License
license="MIT",
# Python version requirement
python_requires=">=3.11",
# Packages
packages=find_packages(
include=["core", "core.*", "plugins", "plugins.*"],
exclude=["tests", "tests.*", "benchmarks", "benchmarks.*", "docs", "docs.*"],
),
# Dependencies
install_requires=INSTALL_REQUIRES,
extras_require=EXTRAS_REQUIRE,
# Package data
include_package_data=True,
package_data=PACKAGE_DATA,
# Entry points
entry_points=ENTRY_POINTS,
# Zip safety
zip_safe=False,
# Platforms
platforms=["win32", "win64", "linux"],
)
if __name__ == "__main__":
main()