mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-06-08 00:07:45 +02:00
384 lines
15 KiB
Python
384 lines
15 KiB
Python
"""
|
|
Jackify GUI Frontend Main Application
|
|
|
|
Main entry point for the Jackify GUI application using PySide6.
|
|
This replaces the legacy jackify_gui implementation with a refactored architecture.
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
# Suppress xkbcommon locale errors (harmless but annoying)
|
|
os.environ['QT_LOGGING_RULES'] = '*.debug=false;qt.qpa.*=false;*.warning=false'
|
|
os.environ['QT_ENABLE_GLYPH_CACHE_WORKAROUND'] = '1'
|
|
|
|
# Hidden diagnostic flag for debugging AppImage/bundled environment issues - must be first
|
|
if '--env-diagnostic' in sys.argv:
|
|
import json
|
|
from datetime import datetime
|
|
|
|
print("Bundled Environment Diagnostic")
|
|
print("=" * 50)
|
|
|
|
# Check if we're running as AppImage
|
|
is_appimage = 'APPIMAGE' in os.environ or 'APPDIR' in os.environ
|
|
appdir = os.environ.get('APPDIR')
|
|
|
|
print(f"AppImage: {is_appimage}")
|
|
print(f"APPDIR: {appdir}")
|
|
|
|
# Capture environment data
|
|
env_data = {
|
|
'timestamp': datetime.now().isoformat(),
|
|
'context': 'appimage_runtime',
|
|
'appimage': is_appimage,
|
|
'appdir': appdir,
|
|
'python_executable': sys.executable,
|
|
'working_directory': os.getcwd(),
|
|
'sys_path': sys.path,
|
|
}
|
|
|
|
# Bundle-specific environment variables
|
|
bundle_vars = {}
|
|
for key, value in os.environ.items():
|
|
if any(term in key.lower() for term in ['mei', 'appimage', 'tmp']):
|
|
bundle_vars[key] = value
|
|
|
|
env_data['bundle_vars'] = bundle_vars
|
|
|
|
# Check LD_LIBRARY_PATH
|
|
ld_path = os.environ.get('LD_LIBRARY_PATH', '')
|
|
if ld_path:
|
|
suspicious = [p for p in ld_path.split(':') if 'mei' in p.lower() or 'tmp' in p.lower()]
|
|
env_data['ld_library_path'] = ld_path
|
|
env_data['ld_library_path_suspicious'] = suspicious
|
|
|
|
# Try to find jackify-engine from bundled context
|
|
engine_paths = []
|
|
if meipass:
|
|
meipass_path = Path(meipass)
|
|
potential_engine = meipass_path / "jackify" / "engine" / "jackify-engine"
|
|
if potential_engine.exists():
|
|
engine_paths.append(str(potential_engine))
|
|
|
|
env_data['engine_paths_found'] = engine_paths
|
|
|
|
# Output the results
|
|
print("\nEnvironment Data:")
|
|
print(json.dumps(env_data, indent=2))
|
|
|
|
# Save to file
|
|
try:
|
|
output_file = Path.cwd() / "bundle_env_capture.json"
|
|
with open(output_file, 'w') as f:
|
|
json.dump(env_data, f, indent=2)
|
|
print(f"\nData saved to: {output_file}")
|
|
except Exception as e:
|
|
print(f"\nCould not save data: {e}")
|
|
|
|
sys.exit(0)
|
|
|
|
from jackify import __version__ as jackify_version
|
|
|
|
# Initialize logger
|
|
logger = logging.getLogger(__name__)
|
|
|
|
if '--help' in sys.argv or '-h' in sys.argv:
|
|
print("""Jackify - Native Linux Modlist Manager\n\nUsage:\n jackify [--cli] [--debug] [--version] [--help]\n\nOptions:\n --cli Launch CLI frontend\n --debug Enable debug logging\n --version Show version and exit\n --help, -h Show this help message and exit\n\nIf no options are given, the GUI will launch by default.\n""")
|
|
sys.exit(0)
|
|
|
|
if '-v' in sys.argv or '--version' in sys.argv or '-V' in sys.argv:
|
|
print(f"Jackify version {jackify_version}")
|
|
sys.exit(0)
|
|
|
|
|
|
from jackify import __version__
|
|
|
|
# Add src directory to Python path
|
|
src_dir = Path(__file__).parent.parent.parent.parent
|
|
sys.path.insert(0, str(src_dir))
|
|
|
|
from PySide6.QtWidgets import (
|
|
QSizePolicy, QScrollArea,
|
|
QApplication, QMainWindow, QWidget, QLabel, QVBoxLayout, QPushButton,
|
|
QStackedWidget, QHBoxLayout, QDialog, QFormLayout, QLineEdit, QCheckBox, QSpinBox, QMessageBox, QGroupBox, QGridLayout, QFileDialog, QToolButton, QStyle, QComboBox, QTabWidget, QRadioButton, QButtonGroup
|
|
)
|
|
from PySide6.QtCore import Qt, QEvent, QTimer, QThread, Signal
|
|
from PySide6.QtGui import QIcon
|
|
import json
|
|
|
|
# Import backend services and models
|
|
from jackify.backend.models.configuration import SystemInfo
|
|
from jackify.backend.services.modlist_service import ModlistService
|
|
from jackify.frontends.gui.services.message_service import MessageService
|
|
from jackify.frontends.gui.shared_theme import DEBUG_BORDERS
|
|
from jackify.frontends.gui.utils import get_screen_geometry, set_responsive_minimum
|
|
from jackify.frontends.gui.dialogs.settings_dialog import SettingsDialog
|
|
from jackify.frontends.gui.mixins.main_window_geometry import MainWindowGeometryMixin
|
|
from jackify.frontends.gui.mixins.main_window_backend import MainWindowBackendMixin
|
|
from jackify.frontends.gui.mixins.main_window_ui import MainWindowUIMixin
|
|
from jackify.frontends.gui.mixins.main_window_startup import MainWindowStartupMixin
|
|
from jackify.frontends.gui.mixins.main_window_dialogs import MainWindowDialogsMixin
|
|
from jackify.frontends.gui.widgets.feature_placeholder import FeaturePlaceholder
|
|
|
|
ENABLE_WINDOW_HEIGHT_ANIMATION = False
|
|
|
|
def debug_print(message):
|
|
"""Print debug message only if debug mode is enabled"""
|
|
from jackify.backend.handlers.config_handler import ConfigHandler
|
|
config_handler = ConfigHandler()
|
|
if config_handler.get('debug_mode', False):
|
|
print(message)
|
|
|
|
# Constants for styling and disclaimer
|
|
DISCLAIMER_TEXT = (
|
|
"Disclaimer: Jackify is currently in an alpha state. This software is provided as-is, "
|
|
"without any warranty or guarantee of stability. By using Jackify, you acknowledge that you do so at your own risk. "
|
|
"The developers are not responsible for any data loss, system issues, or other problems that may arise from its use. "
|
|
"Please back up your data and use caution."
|
|
)
|
|
|
|
MENU_ITEMS = [
|
|
("Modlist Tasks", "modlist_tasks"),
|
|
("Hoolamike Tasks", "hoolamike_tasks"),
|
|
("Additional Tasks", "additional_tasks"),
|
|
("Exit Jackify", "exit_jackify"),
|
|
]
|
|
|
|
|
|
class JackifyMainWindow(
|
|
MainWindowGeometryMixin,
|
|
MainWindowBackendMixin,
|
|
MainWindowUIMixin,
|
|
MainWindowStartupMixin,
|
|
MainWindowDialogsMixin,
|
|
QMainWindow,
|
|
):
|
|
"""Main window for Jackify GUI application"""
|
|
|
|
def __init__(self, dev_mode=False):
|
|
super().__init__()
|
|
self.setWindowTitle("Jackify")
|
|
self._window_margin = 32
|
|
self._base_min_width = 900
|
|
self._base_min_height = 520
|
|
self._compact_height = 640
|
|
self._details_extra_height = 360
|
|
self._initial_show_adjusted = False
|
|
|
|
# Track open dialogs to prevent duplicates
|
|
self._settings_dialog = None
|
|
self._about_dialog = None
|
|
|
|
# Ensure GNOME/Ubuntu exposes full set of window controls (avoid hidden buttons)
|
|
self._apply_standard_window_flags()
|
|
try:
|
|
self.setSizeGripEnabled(True)
|
|
except AttributeError:
|
|
pass
|
|
|
|
# Set default responsive minimum constraints before restoring geometry
|
|
self.apply_responsive_minimum(self._base_min_width, self._base_min_height)
|
|
|
|
# Restore window geometry from QSettings (standard Qt approach)
|
|
self._restore_geometry()
|
|
self.apply_responsive_minimum(self._base_min_width, self._base_min_height)
|
|
|
|
# Initialize backend services
|
|
self._initialize_backend()
|
|
|
|
# Set up UI
|
|
self._setup_ui(dev_mode=dev_mode)
|
|
|
|
# Start background preload of gallery cache for instant gallery opening
|
|
self._start_gallery_cache_preload()
|
|
|
|
def resizeEvent(self, event):
|
|
self._on_resize_event_geometry(event)
|
|
|
|
def showEvent(self, event):
|
|
self._geometry_show_event(event)
|
|
|
|
|
|
|
|
def resource_path(relative_path):
|
|
"""Get path to resource file, handling both AppImage and dev modes."""
|
|
# AppImage mode - use APPDIR if available
|
|
appdir = os.environ.get('APPDIR')
|
|
if appdir:
|
|
# In AppImage, resources are in opt/jackify/ relative to APPDIR
|
|
# __file__ is at opt/jackify/frontends/gui/main.py, so go up to opt/jackify/
|
|
appimage_path = os.path.join(appdir, 'opt', 'jackify', relative_path)
|
|
if os.path.exists(appimage_path):
|
|
return appimage_path
|
|
|
|
# Dev mode or fallback - go up from frontends/gui to jackify, then to assets
|
|
# __file__ is at src/jackify/frontends/gui/main.py, so go up to src/jackify/
|
|
current_dir = os.path.abspath(os.path.dirname(__file__))
|
|
# Go up from frontends/gui to jackify
|
|
jackify_dir = os.path.dirname(os.path.dirname(current_dir))
|
|
return os.path.join(jackify_dir, relative_path)
|
|
|
|
|
|
def main():
|
|
"""Main entry point for the GUI application"""
|
|
# CRITICAL: Enable faulthandler for segfault debugging
|
|
# Print Python stack traces on segfault
|
|
import faulthandler
|
|
import signal
|
|
# Enable faulthandler to both stderr and file
|
|
try:
|
|
log_dir = Path.home() / '.local' / 'share' / 'jackify' / 'logs'
|
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
trace_file = open(log_dir / 'segfault_trace.txt', 'w')
|
|
faulthandler.enable(file=trace_file, all_threads=True)
|
|
except Exception:
|
|
# Fallback to stderr only if file can't be opened
|
|
faulthandler.enable(all_threads=True)
|
|
|
|
# Check for CLI mode argument
|
|
if len(sys.argv) > 1 and '--cli' in sys.argv:
|
|
# Launch CLI frontend instead of GUI
|
|
try:
|
|
from jackify.frontends.cli.__main__ import main as cli_main
|
|
print("CLI mode detected - switching to CLI frontend")
|
|
return cli_main()
|
|
except ImportError as e:
|
|
print(f"Error importing CLI frontend: {e}")
|
|
print("CLI mode not available. Falling back to GUI mode.")
|
|
|
|
# Load config and set debug mode if needed
|
|
from jackify.backend.handlers.config_handler import ConfigHandler
|
|
config_handler = ConfigHandler()
|
|
debug_mode = config_handler.get('debug_mode', False)
|
|
# Command-line --debug always takes precedence
|
|
if '--debug' in sys.argv or '-d' in sys.argv:
|
|
debug_mode = True
|
|
# Temporarily save CLI debug flag to config so engine can see it
|
|
config_handler.set('debug_mode', True)
|
|
import logging
|
|
|
|
# Initialize file logging on root logger so all modules inherit it
|
|
from jackify.shared.logging import LoggingHandler
|
|
logging_handler = LoggingHandler()
|
|
# Only rotate log file when debug mode is enabled
|
|
if debug_mode:
|
|
logging_handler.rotate_log_for_logger('jackify_gui', 'jackify-gui.log')
|
|
root_logger = logging_handler.setup_logger('', 'jackify-gui.log', is_general=True, debug_mode=debug_mode) # Empty name = root logger
|
|
|
|
# CRITICAL: Set root logger level BEFORE any child loggers are used
|
|
# DEBUG messages from child loggers must propagate
|
|
if debug_mode:
|
|
root_logger.setLevel(logging.DEBUG)
|
|
logging.getLogger().setLevel(logging.DEBUG) # Also set on root via getLogger() for compatibility
|
|
root_logger.debug("CLI --debug flag detected, saved debug_mode=True to config")
|
|
root_logger.info("Debug mode enabled (from config or CLI)")
|
|
else:
|
|
root_logger.setLevel(logging.WARNING)
|
|
logging.getLogger().setLevel(logging.WARNING)
|
|
|
|
# Root logger should not propagate (it's the top level)
|
|
# Child loggers will propagate to root logger by default (unless they explicitly set propagate=False)
|
|
root_logger.propagate = False
|
|
|
|
dev_mode = '--dev' in sys.argv
|
|
|
|
# Launch GUI application
|
|
app = QApplication(sys.argv)
|
|
# CRITICAL: Set application name before desktop file name to ensure proper window title/icon on PopOS/Ubuntu
|
|
app.setApplicationName("Jackify")
|
|
app.setApplicationDisplayName("Jackify")
|
|
app.setDesktopFileName("jackify.desktop")
|
|
|
|
# Global cleanup function for signal handling
|
|
def emergency_cleanup():
|
|
debug_print("Cleanup: terminating jackify-engine processes")
|
|
try:
|
|
import subprocess
|
|
subprocess.run(['pkill', '-f', 'jackify-engine'], timeout=5, capture_output=True)
|
|
except Exception:
|
|
pass
|
|
|
|
# Set up signal handlers for graceful shutdown
|
|
import signal
|
|
def signal_handler(sig, frame):
|
|
print(f"Received signal {sig}, cleaning up...")
|
|
emergency_cleanup()
|
|
app.quit()
|
|
|
|
signal.signal(signal.SIGINT, signal_handler) # Ctrl+C
|
|
signal.signal(signal.SIGTERM, signal_handler) # System shutdown
|
|
|
|
# Set the application icon
|
|
# Try multiple locations - AppImage build script places icon in standard locations
|
|
icon_path = None
|
|
icon = QIcon()
|
|
|
|
# Priority 1: Try resource_path (works in dev mode and if assets are in AppImage)
|
|
try_path = resource_path('assets/JackifyLogo_256.png')
|
|
if os.path.exists(try_path):
|
|
icon_path = try_path
|
|
icon = QIcon(try_path)
|
|
|
|
# Priority 2: Try standard AppImage icon locations (where build script actually places it)
|
|
if icon.isNull():
|
|
appdir = os.environ.get('APPDIR')
|
|
if appdir:
|
|
appimage_icon_paths = [
|
|
os.path.join(appdir, 'com.jackify.app.png'), # Root of AppDir
|
|
os.path.join(appdir, 'usr', 'share', 'icons', 'hicolor', '256x256', 'apps', 'com.jackify.app.png'), # Standard location
|
|
os.path.join(appdir, 'opt', 'jackify', 'assets', 'JackifyLogo_256.png'), # If assets are copied
|
|
]
|
|
for path in appimage_icon_paths:
|
|
if os.path.exists(path):
|
|
icon_path = path
|
|
icon = QIcon(path)
|
|
if not icon.isNull():
|
|
if debug_mode:
|
|
logging.getLogger().debug(f"Using AppImage icon: {path}")
|
|
break
|
|
|
|
# Priority 3: Fallback to any PNG in assets directory
|
|
if icon.isNull():
|
|
try_path = resource_path('assets/JackifyLogo_256.png')
|
|
if os.path.exists(try_path):
|
|
icon_path = try_path
|
|
icon = QIcon(try_path)
|
|
|
|
if debug_mode:
|
|
logging.getLogger().debug(f"Final icon path: {icon_path}")
|
|
logging.getLogger().debug(f"Icon is null: {icon.isNull()}")
|
|
|
|
app.setWindowIcon(icon)
|
|
window = JackifyMainWindow(dev_mode=dev_mode)
|
|
window.setWindowIcon(icon)
|
|
window.show()
|
|
|
|
# On Steam Deck, set window to maximized to prevent button overlap with Show Details console
|
|
if hasattr(window, 'system_info') and window.system_info.is_steamdeck:
|
|
window.showMaximized()
|
|
else:
|
|
# Position window after showing (so size is finalized)
|
|
# Center horizontally, position near top (10% from top) to leave room for expansion
|
|
screen = QApplication.primaryScreen()
|
|
if screen:
|
|
screen_geometry = screen.availableGeometry()
|
|
window_size = window.size()
|
|
x = (screen_geometry.width() - window_size.width()) // 2
|
|
y = int(screen_geometry.top() + (screen_geometry.height() * 0.1)) # 10% from top
|
|
window.move(x, y)
|
|
|
|
# Start background update check after window is shown
|
|
window._check_for_updates_on_startup()
|
|
|
|
# Ensure cleanup on exit
|
|
import atexit
|
|
atexit.register(emergency_cleanup)
|
|
|
|
return app.exec()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main()) |