Files
Jackify/jackify/backend/handlers/config_handler.py
2025-10-16 14:44:49 +01:00

585 lines
22 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Configuration Handler Module
Handles application settings and configuration
"""
import os
import json
import logging
import shutil
import re
import base64
from pathlib import Path
# Initialize logger
logger = logging.getLogger(__name__)
class ConfigHandler:
"""
Handles application configuration and settings
"""
def __init__(self):
"""Initialize configuration handler with default settings"""
self.config_dir = os.path.expanduser("~/.config/jackify")
self.config_file = os.path.join(self.config_dir, "config.json")
self.settings = {
"version": "0.0.5",
"last_selected_modlist": None,
"steam_libraries": [],
"resolution": None,
"protontricks_path": None,
"steam_path": None,
"nexus_api_key": None, # Base64 encoded API key
"default_install_parent_dir": None, # Parent directory for modlist installations
"default_download_parent_dir": None, # Parent directory for downloads
"modlist_install_base_dir": os.path.expanduser("~/Games"), # Configurable base directory for modlist installations
"modlist_downloads_base_dir": os.path.expanduser("~/Games/Modlist_Downloads"), # Configurable base directory for downloads
"jackify_data_dir": None, # Configurable Jackify data directory (default: ~/Jackify)
"use_winetricks_for_components": True, # True = use winetricks (faster), False = use protontricks for all (legacy)
"game_proton_path": None # Proton version for game shortcuts (can be any Proton 9+), separate from install proton
}
# Load configuration if exists
self._load_config()
# If steam_path is not set, detect it
if not self.settings["steam_path"]:
self.settings["steam_path"] = self._detect_steam_path()
# Auto-detect and set Proton version on first run
if not self.settings.get("proton_path"):
self._auto_detect_proton()
# If jackify_data_dir is not set, initialize it to default
if not self.settings.get("jackify_data_dir"):
self.settings["jackify_data_dir"] = os.path.expanduser("~/Jackify")
# Save the updated settings
self.save_config()
def _detect_steam_path(self):
"""
Detect the Steam installation path
Returns:
str: Path to the Steam installation or None if not found
"""
logger.info("Detecting Steam installation path...")
# Common Steam installation paths
steam_paths = [
os.path.expanduser("~/.steam/steam"),
os.path.expanduser("~/.local/share/Steam"),
os.path.expanduser("~/.steam/root")
]
# Check each path
for path in steam_paths:
if os.path.exists(path):
logger.info(f"Found Steam installation at: {path}")
return path
# If not found in common locations, try to find using libraryfolders.vdf
libraryfolders_vdf_paths = [
os.path.expanduser("~/.steam/steam/config/libraryfolders.vdf"),
os.path.expanduser("~/.local/share/Steam/config/libraryfolders.vdf"),
os.path.expanduser("~/.steam/root/config/libraryfolders.vdf")
]
for vdf_path in libraryfolders_vdf_paths:
if os.path.exists(vdf_path):
# Extract the Steam path from the libraryfolders.vdf path
steam_path = os.path.dirname(os.path.dirname(vdf_path))
logger.info(f"Found Steam installation at: {steam_path}")
return steam_path
logger.error("Steam installation not found")
return None
def _load_config(self):
"""Load configuration from file"""
try:
if os.path.exists(self.config_file):
with open(self.config_file, 'r') as f:
saved_config = json.load(f)
# Update settings with saved values while preserving defaults
self.settings.update(saved_config)
logger.debug("Loaded configuration from file")
else:
logger.debug("No configuration file found, using defaults")
self._create_config_dir()
except Exception as e:
logger.error(f"Error loading configuration: {e}")
def _create_config_dir(self):
"""Create configuration directory if it doesn't exist"""
try:
os.makedirs(self.config_dir, exist_ok=True)
logger.debug(f"Created configuration directory: {self.config_dir}")
except Exception as e:
logger.error(f"Error creating configuration directory: {e}")
def save_config(self):
"""Save current configuration to file"""
try:
self._create_config_dir()
with open(self.config_file, 'w') as f:
json.dump(self.settings, f, indent=2)
logger.debug("Saved configuration to file")
return True
except Exception as e:
logger.error(f"Error saving configuration: {e}")
return False
def get(self, key, default=None):
"""Get a configuration value by key"""
return self.settings.get(key, default)
def set(self, key, value):
"""Set a configuration value"""
self.settings[key] = value
return True
def update(self, settings_dict):
"""Update multiple configuration values"""
self.settings.update(settings_dict)
return True
def add_steam_library(self, path):
"""Add a Steam library path to configuration"""
if path not in self.settings["steam_libraries"]:
self.settings["steam_libraries"].append(path)
logger.debug(f"Added Steam library: {path}")
return True
return False
def remove_steam_library(self, path):
"""Remove a Steam library path from configuration"""
if path in self.settings["steam_libraries"]:
self.settings["steam_libraries"].remove(path)
logger.debug(f"Removed Steam library: {path}")
return True
return False
def set_resolution(self, width, height):
"""Set preferred resolution"""
resolution = f"{width}x{height}"
self.settings["resolution"] = resolution
logger.debug(f"Set resolution to: {resolution}")
return True
def get_resolution(self):
"""Get preferred resolution"""
return self.settings.get("resolution")
def set_last_modlist(self, modlist_name):
"""Save the last selected modlist"""
self.settings["last_selected_modlist"] = modlist_name
logger.debug(f"Set last selected modlist to: {modlist_name}")
return True
def get_last_modlist(self):
"""Get the last selected modlist"""
return self.settings.get("last_selected_modlist")
def set_protontricks_path(self, path):
"""Set the path to protontricks executable"""
self.settings["protontricks_path"] = path
logger.debug(f"Set protontricks path to: {path}")
return True
def get_protontricks_path(self):
"""Get the path to protontricks executable"""
return self.settings.get("protontricks_path")
def save_api_key(self, api_key):
"""
Save Nexus API key with base64 encoding
Args:
api_key (str): Plain text API key
Returns:
bool: True if saved successfully, False otherwise
"""
try:
if api_key:
# Encode the API key using base64
encoded_key = base64.b64encode(api_key.encode('utf-8')).decode('utf-8')
self.settings["nexus_api_key"] = encoded_key
logger.debug("API key saved successfully")
else:
# Clear the API key if empty
self.settings["nexus_api_key"] = None
logger.debug("API key cleared")
return self.save_config()
except Exception as e:
logger.error(f"Error saving API key: {e}")
return False
def get_api_key(self):
"""
Retrieve and decode the saved Nexus API key
Always reads fresh from disk to pick up changes from other instances
Returns:
str: Decoded API key or None if not saved
"""
try:
# Reload config from disk to pick up changes from Settings dialog
self._load_config()
encoded_key = self.settings.get("nexus_api_key")
if encoded_key:
# Decode the base64 encoded key
decoded_key = base64.b64decode(encoded_key.encode('utf-8')).decode('utf-8')
return decoded_key
return None
except Exception as e:
logger.error(f"Error retrieving API key: {e}")
return None
def has_saved_api_key(self):
"""
Check if an API key is saved in configuration
Always reads fresh from disk to pick up changes from other instances
Returns:
bool: True if API key exists, False otherwise
"""
# Reload config from disk to pick up changes from Settings dialog
self._load_config()
return self.settings.get("nexus_api_key") is not None
def clear_api_key(self):
"""
Clear the saved API key from configuration
Returns:
bool: True if cleared successfully, False otherwise
"""
try:
self.settings["nexus_api_key"] = None
logger.debug("API key cleared from configuration")
return self.save_config()
except Exception as e:
logger.error(f"Error clearing API key: {e}")
return False
def save_resolution(self, resolution):
"""
Save resolution setting to configuration
Args:
resolution (str): Resolution string (e.g., '1920x1080')
Returns:
bool: True if saved successfully, False otherwise
"""
try:
if resolution and resolution != 'Leave unchanged':
self.settings["resolution"] = resolution
logger.debug(f"Resolution saved: {resolution}")
else:
# Clear resolution if 'Leave unchanged' or empty
self.settings["resolution"] = None
logger.debug("Resolution cleared")
return self.save_config()
except Exception as e:
logger.error(f"Error saving resolution: {e}")
return False
def get_saved_resolution(self):
"""
Retrieve the saved resolution from configuration
Returns:
str: Saved resolution or None if not saved
"""
try:
resolution = self.settings.get("resolution")
if resolution:
logger.debug(f"Retrieved saved resolution: {resolution}")
else:
logger.debug("No saved resolution found")
return resolution
except Exception as e:
logger.error(f"Error retrieving resolution: {e}")
return None
def has_saved_resolution(self):
"""
Check if a resolution is saved in configuration
Returns:
bool: True if resolution exists, False otherwise
"""
return self.settings.get("resolution") is not None
def clear_saved_resolution(self):
"""
Clear the saved resolution from configuration
Returns:
bool: True if cleared successfully, False otherwise
"""
try:
self.settings["resolution"] = None
logger.debug("Resolution cleared from configuration")
return self.save_config()
except Exception as e:
logger.error(f"Error clearing resolution: {e}")
return False
def set_default_install_parent_dir(self, path):
"""
Save the parent directory for modlist installations
Args:
path (str): Parent directory path to save
Returns:
bool: True if saved successfully, False otherwise
"""
try:
if path and os.path.exists(path):
self.settings["default_install_parent_dir"] = path
logger.debug(f"Default install parent directory saved: {path}")
return self.save_config()
else:
logger.warning(f"Invalid or non-existent path for install parent directory: {path}")
return False
except Exception as e:
logger.error(f"Error saving install parent directory: {e}")
return False
def get_default_install_parent_dir(self):
"""
Retrieve the saved parent directory for modlist installations
Returns:
str: Saved parent directory path or None if not saved
"""
try:
path = self.settings.get("default_install_parent_dir")
if path and os.path.exists(path):
logger.debug(f"Retrieved default install parent directory: {path}")
return path
else:
logger.debug("No valid default install parent directory found")
return None
except Exception as e:
logger.error(f"Error retrieving install parent directory: {e}")
return None
def set_default_download_parent_dir(self, path):
"""
Save the parent directory for downloads
Args:
path (str): Parent directory path to save
Returns:
bool: True if saved successfully, False otherwise
"""
try:
if path and os.path.exists(path):
self.settings["default_download_parent_dir"] = path
logger.debug(f"Default download parent directory saved: {path}")
return self.save_config()
else:
logger.warning(f"Invalid or non-existent path for download parent directory: {path}")
return False
except Exception as e:
logger.error(f"Error saving download parent directory: {e}")
return False
def get_default_download_parent_dir(self):
"""
Retrieve the saved parent directory for downloads
Returns:
str: Saved parent directory path or None if not saved
"""
try:
path = self.settings.get("default_download_parent_dir")
if path and os.path.exists(path):
logger.debug(f"Retrieved default download parent directory: {path}")
return path
else:
logger.debug("No valid default download parent directory found")
return None
except Exception as e:
logger.error(f"Error retrieving download parent directory: {e}")
return None
def has_saved_install_parent_dir(self):
"""
Check if a default install parent directory is saved in configuration
Returns:
bool: True if directory exists and is valid, False otherwise
"""
path = self.settings.get("default_install_parent_dir")
return path is not None and os.path.exists(path)
def has_saved_download_parent_dir(self):
"""
Check if a default download parent directory is saved in configuration
Returns:
bool: True if directory exists and is valid, False otherwise
"""
path = self.settings.get("default_download_parent_dir")
return path is not None and os.path.exists(path)
def get_modlist_install_base_dir(self):
"""
Get the configurable base directory for modlist installations
Returns:
str: Base directory path for modlist installations
"""
return self.settings.get("modlist_install_base_dir", os.path.expanduser("~/Games"))
def set_modlist_install_base_dir(self, path):
"""
Set the configurable base directory for modlist installations
Args:
path (str): Base directory path to save
Returns:
bool: True if saved successfully, False otherwise
"""
try:
if path:
self.settings["modlist_install_base_dir"] = path
logger.debug(f"Modlist install base directory saved: {path}")
return self.save_config()
else:
logger.warning("Invalid path for modlist install base directory")
return False
except Exception as e:
logger.error(f"Error saving modlist install base directory: {e}")
return False
def get_modlist_downloads_base_dir(self):
"""
Get the configurable base directory for modlist downloads
Returns:
str: Base directory path for modlist downloads
"""
return self.settings.get("modlist_downloads_base_dir", os.path.expanduser("~/Games/Modlist_Downloads"))
def set_modlist_downloads_base_dir(self, path):
"""
Set the configurable base directory for modlist downloads
Args:
path (str): Base directory path to save
Returns:
bool: True if saved successfully, False otherwise
"""
try:
if path:
self.settings["modlist_downloads_base_dir"] = path
logger.debug(f"Modlist downloads base directory saved: {path}")
return self.save_config()
else:
logger.warning("Invalid path for modlist downloads base directory")
return False
except Exception as e:
logger.error(f"Error saving modlist downloads base directory: {e}")
return False
def get_proton_path(self):
"""
Retrieve the saved Install Proton path from configuration (for jackify-engine)
Always reads fresh from disk to pick up changes from Settings dialog
Returns:
str: Saved Install Proton path or 'auto' if not saved
"""
try:
# Reload config from disk to pick up changes from Settings dialog
self._load_config()
proton_path = self.settings.get("proton_path", "auto")
logger.debug(f"Retrieved fresh install proton_path from config: {proton_path}")
return proton_path
except Exception as e:
logger.error(f"Error retrieving install proton_path: {e}")
return "auto"
def get_game_proton_path(self):
"""
Retrieve the saved Game Proton path from configuration (for game shortcuts)
Falls back to install Proton path if game Proton not set
Always reads fresh from disk to pick up changes from Settings dialog
Returns:
str: Saved Game Proton path, Install Proton path, or 'auto' if not saved
"""
try:
# Reload config from disk to pick up changes from Settings dialog
self._load_config()
game_proton_path = self.settings.get("game_proton_path")
# If game proton not set or set to same_as_install, use install proton
if not game_proton_path or game_proton_path == "same_as_install":
game_proton_path = self.settings.get("proton_path", "auto")
logger.debug(f"Retrieved fresh game proton_path from config: {game_proton_path}")
return game_proton_path
except Exception as e:
logger.error(f"Error retrieving game proton_path: {e}")
return "auto"
def get_proton_version(self):
"""
Retrieve the saved Proton version from configuration
Always reads fresh from disk to pick up changes from Settings dialog
Returns:
str: Saved Proton version or 'auto' if not saved
"""
try:
# Reload config from disk to pick up changes from Settings dialog
self._load_config()
proton_version = self.settings.get("proton_version", "auto")
logger.debug(f"Retrieved fresh proton_version from config: {proton_version}")
return proton_version
except Exception as e:
logger.error(f"Error retrieving proton_version: {e}")
return "auto"
def _auto_detect_proton(self):
"""Auto-detect and set best Proton version (includes GE-Proton and Valve Proton)"""
try:
from .wine_utils import WineUtils
best_proton = WineUtils.select_best_proton()
if best_proton:
self.settings["proton_path"] = str(best_proton['path'])
self.settings["proton_version"] = best_proton['name']
proton_type = best_proton.get('type', 'Unknown')
logger.info(f"Auto-detected Proton: {best_proton['name']} ({proton_type})")
self.save_config()
else:
# Fallback to auto-detect mode
self.settings["proton_path"] = "auto"
self.settings["proton_version"] = "auto"
logger.info("No compatible Proton versions found, using auto-detect mode")
self.save_config()
except Exception as e:
logger.error(f"Failed to auto-detect Proton: {e}")
self.settings["proton_path"] = "auto"
self.settings["proton_version"] = "auto"