Files
Jackify/jackify/backend/handlers/shortcut_launch_options.py
2026-02-25 20:54:28 +00:00

163 lines
7.5 KiB
Python

"""Launch options and icon methods for ShortcutHandler (Mixin)."""
import logging
import os
import shutil
import time
import vdf
logger = logging.getLogger(__name__)
class ShortcutLaunchOptionsMixin:
"""Mixin providing launch options and icon methods."""
def update_shortcut_launch_options(self, app_name, exe_path, new_launch_options):
"""
Updates the LaunchOptions for a specific existing shortcut in shortcuts.vdf by matching AppName and Exe.
Args:
app_name (str): The AppName of the shortcut to update (from config summary).
exe_path (str): The Exe path of the shortcut to update (from config summary, including quotes if present in VDF).
new_launch_options (str): The new string to set for LaunchOptions.
Returns:
bool: True if the update was successful, False otherwise.
"""
self.logger.info(f"Attempting to update launch options for shortcut with AppName '{app_name}' and Exe '{exe_path}' (no AppID matching)...")
shortcuts_file = self.path_handler._find_shortcuts_vdf()
if not shortcuts_file:
self.logger.error("Could not find shortcuts.vdf to update.")
return False
data = {'shortcuts': {}}
try:
if os.path.exists(shortcuts_file):
with open(shortcuts_file, 'rb') as f:
file_data = f.read()
if file_data:
data = vdf.binary_loads(file_data)
if 'shortcuts' not in data:
data['shortcuts'] = {}
else:
self.logger.error(f"shortcuts.vdf does not exist at {shortcuts_file}. Cannot update.")
return False
except Exception as e:
self.logger.error(f"Error reading or parsing shortcuts.vdf: {e}")
return False
def _normalize_path(p: str) -> str:
try:
p_clean = os.path.abspath(os.path.expanduser(p.strip().strip('"')))
return os.path.normpath(p_clean).lower()
except Exception:
return p.strip().strip('"').lower()
exe_norm = _normalize_path(exe_path)
target_index = None
for index, shortcut_data in data.get('shortcuts', {}).items():
shortcut_name = (shortcut_data.get('AppName', '') or '').strip()
shortcut_exe_raw = shortcut_data.get('Exe', '')
shortcut_exe_norm = _normalize_path(shortcut_exe_raw)
if shortcut_name == app_name and shortcut_exe_norm == exe_norm:
target_index = index
break
if target_index is None:
self.logger.error(f"Could not find shortcut with AppName '{app_name}' and Exe '{exe_path}' in shortcuts.vdf.")
for index, shortcut_data in data.get('shortcuts', {}).items():
shortcut_name = shortcut_data.get('AppName', '')
shortcut_exe = shortcut_data.get('Exe', '')
self.logger.error(f"Found shortcut: AppName='{shortcut_name}', Exe='{shortcut_exe}' -> norm='{_normalize_path(shortcut_exe)}'")
return False
if target_index in data['shortcuts']:
self.logger.info(f"Found shortcut at index {target_index}. Updating LaunchOptions...")
data['shortcuts'][target_index]['LaunchOptions'] = new_launch_options
else:
self.logger.error(f"Target index {target_index} not found in shortcuts dictionary after identification.")
return False
try:
temp_file = f"{shortcuts_file}.temp"
with open(temp_file, 'wb') as f:
vdf_data = vdf.binary_dumps(data)
f.write(vdf_data)
backup_dir = os.path.join(os.path.dirname(shortcuts_file), "backups")
os.makedirs(backup_dir, exist_ok=True)
timestamp = time.strftime("%Y%m%d_%H%M%S")
backup_path = os.path.join(backup_dir, f"shortcuts_update_{app_name}_{timestamp}.bak")
if os.path.exists(shortcuts_file):
shutil.copy2(shortcuts_file, backup_path)
self.logger.info(f"Created backup before update at {backup_path}")
shutil.move(temp_file, shortcuts_file)
self.logger.info(f"Successfully updated LaunchOptions for shortcut '{app_name}' in {shortcuts_file}.")
return True
except Exception as e:
self.logger.error(f"Error writing updated shortcuts.vdf: {e}")
if 'backup_path' in locals() and os.path.exists(backup_path):
try:
shutil.copy2(backup_path, shortcuts_file)
self.logger.warning(f"Restored shortcuts.vdf from backup {backup_path} after update failure.")
except Exception as restore_e:
self.logger.critical(f"CRITICAL: Failed to write updated shortcuts.vdf AND failed to restore backup! Error: {restore_e}")
return False
@staticmethod
def get_steam_shortcut_icon_path(exe_path, steamicons_dir=None, logger=None):
"""
Select the best icon for a Steam shortcut given an executable path and optional SteamIcons directory.
Prefers grid-tall.png, else any .png, else returns ''.
Logs selection steps if logger is provided.
"""
exe_dir = os.path.dirname(exe_path)
if not steamicons_dir:
steamicons_dir = os.path.join(exe_dir, "SteamIcons")
if logger:
logger.debug(f"[DEBUG] Looking for Steam shortcut icon in: {steamicons_dir}")
if os.path.isdir(steamicons_dir):
preferred_icon = os.path.join(steamicons_dir, "grid-tall.png")
if os.path.isfile(preferred_icon):
if logger:
logger.debug(f"[DEBUG] Using grid-tall.png as shortcut icon: {preferred_icon}")
return preferred_icon
pngs = [f for f in os.listdir(steamicons_dir) if f.lower().endswith('.png')]
if pngs:
icon_path = os.path.join(steamicons_dir, pngs[0])
if logger:
logger.debug(f"[DEBUG] Using fallback icon for shortcut: {icon_path}")
return icon_path
if logger:
logger.debug("[DEBUG] No .png icon found in SteamIcons directory.")
return ""
if logger:
logger.debug("[DEBUG] No SteamIcons directory found; shortcut will have no icon.")
return ""
def write_nxmhandler_ini(self, modlist_dir, mo2_exe_path):
"""
Create nxmhandler.ini in the modlist directory to suppress the NXM Handling popup on first MO2 launch.
If the file already exists, do nothing.
The executable path will be written as Z:\\<absolute path with double backslashes>, matching MO2's format.
"""
ini_path = os.path.join(modlist_dir, "nxmhandler.ini")
if os.path.exists(ini_path):
self.logger.info(f"nxmhandler.ini already exists at {ini_path}")
return
abs_path = os.path.abspath(mo2_exe_path)
z_path = f"Z:{abs_path}"
win_path = z_path.replace('/', '\\')
win_path = win_path.replace('\\', '\\\\')
content = (
"[handlers]\n"
"size=1\n"
"1\\games=\"skyrimse,skyrim,fallout4,falloutnv,fallout3,oblivion,enderal,starfield\"\n"
f"1\\executable={win_path}\n"
"1\\arguments=\n"
)
with open(ini_path, "w") as f:
f.write(content)
self.logger.info(f"[SUCCESS] nxmhandler.ini written to {ini_path}")