Files
Jackify/jackify/backend/handlers/resolution_handler.py
Omni cd591c14e3 Initial public release v0.1.0 - Linux Wabbajack Modlist Application
Jackify provides native Linux support for Wabbajack modlist installation
   and management with automated Steam integration and Proton configuration.

   Key Features:
   - Almost Native Linux implementation (texconv.exe run via proton)
   - Automated Steam shortcut creation and Proton prefix management
   - Both CLI and GUI interfaces, with Steam Deck optimization

   Supported Games:
   - Skyrim Special Edition
   - Fallout 4
   - Fallout New Vegas
   - Oblivion, Starfield, Enderal, and diverse other games

   Technical Architecture:
   - Clean separation between frontend and backend services
   - Powered by jackify-engine 0.3.x for Wabbajack-matching modlist installation
2025-09-05 20:46:24 +01:00

503 lines
22 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Resolution Handler Module
Handles setting resolution in various INI files
"""
import os
import re
import glob
import logging
import subprocess
from pathlib import Path
from typing import Optional, List, Dict
# Import colors from the new central location
from .ui_colors import COLOR_PROMPT, COLOR_RESET, COLOR_ERROR, COLOR_INFO
# Initialize logger
logger = logging.getLogger(__name__)
class ResolutionHandler:
"""
Handles resolution selection and configuration for games
"""
def __init__(self, modlist_dir=None, game_var=None, resolution=None):
self.modlist_dir = modlist_dir
self.game_var = game_var # Short version (e.g., "Skyrim")
self.game_var_full = None # Full version (e.g., "Skyrim Special Edition")
self.resolution = resolution
# Add logger initialization
self.logger = logging.getLogger(__name__)
# Set the full game name based on the short version
if self.game_var:
game_lookup = {
"Skyrim": "Skyrim Special Edition",
"Fallout": "Fallout 4",
"Fallout 4": "Fallout 4",
"Fallout New Vegas": "Fallout New Vegas",
"FNV": "Fallout New Vegas",
"Oblivion": "Oblivion"
}
self.game_var_full = game_lookup.get(self.game_var, self.game_var)
def set_resolution(self, resolution):
"""
Set the target resolution, e.g. "1280x800"
"""
self.resolution = resolution
logger.debug(f"Resolution set to: {self.resolution}")
return True
def get_resolution_components(self):
"""
Split resolution into width and height components
"""
if not self.resolution:
logger.error("Resolution not set")
return None, None
try:
width, height = self.resolution.split('x')
return width, height
except ValueError:
logger.error(f"Invalid resolution format: {self.resolution}")
return None, None
def detect_steamdeck_resolution(self):
"""
Set resolution to Steam Deck native if on a Steam Deck
"""
try:
if os.path.exists("/etc/os-release"):
with open("/etc/os-release", "r") as f:
if "steamdeck" in f.read():
self.resolution = "1280x800"
logger.debug("Steam Deck detected, setting resolution to 1280x800")
return True
return False
except Exception as e:
logger.error(f"Error detecting Steam Deck resolution: {e}")
return False
def select_resolution(self, steamdeck=False) -> Optional[str]:
"""
Ask the user if they want to set resolution, then prompt and validate.
Returns the selected resolution string (e.g., "1920x1080") or None if skipped/cancelled.
"""
if steamdeck:
logger.info("Steam Deck detected - Setting resolution to 1280x800")
return "1280x800"
# Ask user if they want to set resolution
response = input(f"{COLOR_PROMPT}Do you wish to set the display resolution now? (y/N): {COLOR_RESET}").lower()
if response == 'y':
while True:
user_res = input(f"{COLOR_PROMPT}Enter desired resolution (e.g., 1920x1080): {COLOR_RESET}").strip()
if self._validate_resolution_format(user_res):
# Optional: Add confirmation step here if desired
# confirm = input(f"{COLOR_PROMPT}Use resolution {user_res}? (Y/n): {COLOR_RESET}").lower()
# if confirm != 'n':
# return user_res
return user_res # Return validated resolution
else:
print(f"{COLOR_ERROR}Invalid format. Please use format WxH (e.g., 1920x1080){COLOR_RESET}")
else:
self.logger.info("Resolution setup skipped by user.")
return None
def _validate_resolution_format(self, resolution: str) -> bool:
"""Validates the resolution format WxH (e.g., 1920x1080)."""
if not resolution:
return False
# Simple regex to match one or more digits, 'x', one or more digits
if re.match(r"^[0-9]+x[0-9]+$", resolution):
self.logger.debug(f"Resolution format validated: {resolution}")
return True
else:
self.logger.warning(f"Invalid resolution format provided: {resolution}")
return False
@staticmethod
def get_available_resolutions() -> List[str]:
"""Gets available display resolutions using xrandr."""
resolutions = []
try:
result = subprocess.run(["xrandr"], capture_output=True, text=True, check=True)
# Regex to find lines like ' 1920x1080 59.96*+'
matches = re.finditer(r"^\s*(\d+x\d+)\s", result.stdout, re.MULTILINE)
for match in matches:
res = match.group(1)
if res not in resolutions:
resolutions.append(res)
# Add common resolutions if xrandr fails or doesn't list them
common_res = ["1280x720", "1280x800", "1920x1080", "1920x1200", "2560x1440"]
for res in common_res:
if res not in resolutions:
resolutions.append(res)
resolutions.sort(key=lambda r: tuple(map(int, r.split('x'))))
logger.debug(f"Detected resolutions: {resolutions}")
return resolutions
except (FileNotFoundError, subprocess.CalledProcessError, Exception) as e:
logger.warning(f"Could not detect resolutions via xrandr: {e}. Falling back to common list.")
# Fallback to a common list if xrandr is not available or fails
return ["1280x720", "1280x800", "1920x1080", "1920x1200", "2560x1440"]
@staticmethod
def update_ini_resolution(modlist_dir: str, game_var: str, set_res: str) -> bool:
"""
Updates the resolution in relevant INI files for the specified game.
Args:
modlist_dir (str): Path to the modlist directory.
game_var (str): The game identifier (e.g., "Skyrim Special Edition", "Fallout 4").
set_res (str): The desired resolution (e.g., "1920x1080").
Returns:
bool: True if successful or not applicable, False on error.
"""
logger.info(f"Attempting to set resolution to {set_res} for {game_var} in {modlist_dir}")
try:
isize_w, isize_h = set_res.split('x')
modlist_path = Path(modlist_dir)
success_count = 0
files_processed = 0
# 1. Handle SSEDisplayTweaks.ini (Skyrim SE only)
if game_var == "Skyrim Special Edition":
logger.debug("Processing SSEDisplayTweaks.ini...")
sse_tweaks_files = list(modlist_path.rglob("SSEDisplayTweaks.ini"))
if sse_tweaks_files:
for ini_file in sse_tweaks_files:
files_processed += 1
logger.debug(f"Updating {ini_file}")
if ResolutionHandler._modify_sse_tweaks(ini_file, set_res):
success_count += 1
else:
logger.debug("No SSEDisplayTweaks.ini found, skipping.")
# 1.5. Handle HighFPSPhysicsFix.ini (Fallout 4 only)
elif game_var == "Fallout 4":
logger.debug("Processing HighFPSPhysicsFix.ini...")
highfps_files = list(modlist_path.rglob("HighFPSPhysicsFix.ini"))
if highfps_files:
for ini_file in highfps_files:
files_processed += 1
logger.debug(f"Updating {ini_file}")
if ResolutionHandler._modify_highfps_physics_fix(ini_file, set_res):
success_count += 1
else:
logger.debug("No HighFPSPhysicsFix.ini found, skipping.")
# 2. Handle game-specific Prefs/INI files
prefs_filenames = []
if game_var == "Skyrim Special Edition":
prefs_filenames = ["skyrimprefs.ini"]
elif game_var == "Fallout 4":
prefs_filenames = ["Fallout4Prefs.ini"]
elif game_var == "Fallout New Vegas":
prefs_filenames = ["falloutprefs.ini"]
elif game_var == "Oblivion":
prefs_filenames = ["Oblivion.ini"]
else:
logger.warning(f"Resolution setting not implemented for game: {game_var}")
return True # Not an error, just not applicable
logger.debug(f"Processing {prefs_filenames}...")
prefs_files_found = []
# Search common locations: profiles/, stock game dirs
search_dirs = [modlist_path / "profiles"]
# Add potential stock game directories dynamically (case-insensitive)
potential_stock_dirs = [d for d in modlist_path.iterdir() if d.is_dir() and
d.name.lower() in ["stock game", "game root", "stock folder", "skyrim stock"]] # Add more if needed
search_dirs.extend(potential_stock_dirs)
for search_dir in search_dirs:
if search_dir.is_dir():
for fname in prefs_filenames:
prefs_files_found.extend(list(search_dir.rglob(fname)))
if not prefs_files_found:
logger.warning(f"No preference files ({prefs_filenames}) found in standard locations ({search_dirs}). Manual INI edit might be needed.")
# Consider this success as the main operation didn't fail?
return True
for ini_file in prefs_files_found:
files_processed += 1
logger.debug(f"Updating {ini_file}")
if ResolutionHandler._modify_prefs_resolution(ini_file, isize_w, isize_h, game_var == "Oblivion"):
success_count += 1
logger.info(f"Resolution update: processed {files_processed} files, {success_count} successfully updated.")
# Return True even if some updates failed, as the overall process didn't halt
return True
except ValueError:
logger.error(f"Invalid resolution format: {set_res}. Expected WxH (e.g., 1920x1080).")
return False
except Exception as e:
logger.error(f"Error updating INI resolutions: {e}", exc_info=True)
return False
@staticmethod
def _modify_sse_tweaks(ini_path: Path, resolution: str) -> bool:
"""Helper to modify SSEDisplayTweaks.ini"""
try:
with open(ini_path, 'r') as f:
lines = f.readlines()
new_lines = []
modified = False
for line in lines:
stripped_line = line.strip()
# Use regex for flexibility with spacing and comments
if re.match(r'^\s*(#?)\s*Resolution\s*=.*$', stripped_line, re.IGNORECASE):
new_lines.append(f"Resolution={resolution}\n")
modified = True
elif re.match(r'^\s*(#?)\s*Fullscreen\s*=.*$', stripped_line, re.IGNORECASE):
new_lines.append("Fullscreen=false\n")
modified = True
elif re.match(r'^\s*(#?)\s*Borderless\s*=.*$', stripped_line, re.IGNORECASE):
new_lines.append("Borderless=true\n")
modified = True
else:
new_lines.append(line)
if modified:
with open(ini_path, 'w') as f:
f.writelines(new_lines)
logger.debug(f"Successfully modified {ini_path} for SSEDisplayTweaks")
return True
except Exception as e:
logger.error(f"Failed to modify {ini_path}: {e}")
return False
@staticmethod
def _modify_highfps_physics_fix(ini_path: Path, resolution: str) -> bool:
"""Helper to modify HighFPSPhysicsFix.ini for Fallout 4"""
try:
with open(ini_path, 'r') as f:
lines = f.readlines()
new_lines = []
modified = False
for line in lines:
stripped_line = line.strip()
# Look for Resolution line (commonly commented out by default)
if re.match(r'^\s*(#?)\s*Resolution\s*=.*$', stripped_line, re.IGNORECASE):
new_lines.append(f"Resolution={resolution}\n")
modified = True
else:
new_lines.append(line)
if modified:
with open(ini_path, 'w') as f:
f.writelines(new_lines)
logger.debug(f"Successfully modified {ini_path} for HighFPSPhysicsFix")
return True
except Exception as e:
logger.error(f"Failed to modify {ini_path}: {e}")
return False
@staticmethod
def _modify_prefs_resolution(ini_path: Path, width: str, height: str, is_oblivion: bool) -> bool:
"""Helper to modify resolution in skyrimprefs.ini, Fallout4Prefs.ini, etc."""
try:
with open(ini_path, 'r') as f:
lines = f.readlines()
new_lines = []
modified = False
# Prepare the replacement strings for width and height
# Ensure correct spacing for Oblivion vs other games
# Corrected f-string syntax for conditional expression
equals_operator = "=" if is_oblivion else " = "
width_replace = f"iSize W{equals_operator}{width}\n"
height_replace = f"iSize H{equals_operator}{height}\n"
for line in lines:
stripped_line = line.strip()
if stripped_line.lower().endswith("isize w"):
new_lines.append(width_replace)
modified = True
elif stripped_line.lower().endswith("isize h"):
new_lines.append(height_replace)
modified = True
else:
new_lines.append(line)
if modified:
with open(ini_path, 'w') as f:
f.writelines(new_lines)
logger.debug(f"Successfully modified {ini_path} for resolution")
return True
except Exception as e:
logger.error(f"Failed to modify {ini_path}: {e}")
return False
def edit_resolution(self, modlist_dir, game_var, selected_resolution=None):
"""
Edit resolution in INI files
"""
if selected_resolution:
logger.debug(f"Applying resolution: {selected_resolution}")
return self.update_ini_resolution(modlist_dir, game_var, selected_resolution)
else:
logger.debug("Resolution setup skipped")
return True
def update_sse_display_tweaks(self):
"""
Update SSEDisplayTweaks.ini with the chosen resolution
Returns True on success, False on failure
"""
if not self.modlist_dir or not self.game_var or not self.resolution:
logger.error("Missing required parameters")
return False
if self.game_var != "Skyrim Special Edition":
logger.debug(f"Not Skyrim, skipping SSEDisplayTweaks")
return False
try:
# Find all SSEDisplayTweaks.ini files
ini_files = glob.glob(f"{self.modlist_dir}/**/SSEDisplayTweaks.ini", recursive=True)
if not ini_files:
logger.debug("No SSEDisplayTweaks.ini files found")
return False
for ini_file in ini_files:
# Read the file
with open(ini_file, 'r', encoding='utf-8', errors='ignore') as f:
content = f.readlines()
# Process and modify the content
modified_content = []
for line in content:
if line.strip().startswith("Resolution=") or line.strip().startswith("#Resolution="):
modified_content.append(f"Resolution={self.resolution}\n")
elif line.strip().startswith("Fullscreen=") or line.strip().startswith("#Fullscreen="):
modified_content.append(f"Fullscreen=false\n")
elif line.strip().startswith("Borderless=") or line.strip().startswith("#Borderless="):
modified_content.append(f"Borderless=true\n")
else:
modified_content.append(line)
# Write the modified content back
with open(ini_file, 'w', encoding='utf-8') as f:
f.writelines(modified_content)
logger.debug(f"Updated {ini_file} with Resolution={self.resolution}, Fullscreen=false, Borderless=true")
return True
except Exception as e:
logger.error(f"Error updating SSEDisplayTweaks.ini: {e}")
return False
def update_game_prefs_ini(self):
"""
Update game preference INI files with the chosen resolution
Returns True on success, False on failure
"""
if not self.modlist_dir or not self.game_var or not self.resolution:
logger.error("Missing required parameters")
return False
try:
# Get resolution components
width, height = self.get_resolution_components()
if not width or not height:
return False
# Define possible stock game folders to search
stock_folders = [
"profiles", "Stock Game", "Game Root", "STOCK GAME",
"Stock Game Folder", "Stock Folder", "Skyrim Stock"
]
# Define the appropriate INI file based on game type
ini_filename = None
if self.game_var == "Skyrim Special Edition":
ini_filename = "skyrimprefs.ini"
elif self.game_var == "Fallout 4":
ini_filename = "Fallout4Prefs.ini"
elif self.game_var == "Fallout New Vegas":
ini_filename = "falloutprefs.ini"
elif self.game_var == "Oblivion":
ini_filename = "Oblivion.ini"
else:
logger.error(f"Unsupported game: {self.game_var}")
return False
# Search for INI files in the appropriate directories
ini_files = []
for folder in stock_folders:
path_pattern = os.path.join(self.modlist_dir, folder, f"**/{ini_filename}")
ini_files.extend(glob.glob(path_pattern, recursive=True))
if not ini_files:
logger.warn(f"No {ini_filename} files found in specified directories")
return False
for ini_file in ini_files:
# Read the file
with open(ini_file, 'r', encoding='utf-8', errors='ignore') as f:
content = f.readlines()
# Process and modify the content
modified_content = []
for line in content:
line_lower = line.lower()
if "isize w" in line_lower:
# Handle different formats (with = or space)
if "=" in line and not " = " in line:
modified_content.append(f"iSize W={width}\n")
else:
modified_content.append(f"iSize W = {width}\n")
elif "isize h" in line_lower:
# Handle different formats (with = or space)
if "=" in line and not " = " in line:
modified_content.append(f"iSize H={height}\n")
else:
modified_content.append(f"iSize H = {height}\n")
else:
modified_content.append(line)
# Write the modified content back
with open(ini_file, 'w', encoding='utf-8') as f:
f.writelines(modified_content)
logger.debug(f"Updated {ini_file} with iSize W={width}, iSize H={height}")
return True
except Exception as e:
logger.error(f"Error updating game prefs INI: {e}")
return False
def update_all_resolution_settings(self):
"""
Update all resolution-related settings in all relevant INI files
Returns True if any files were updated, False if none were updated
"""
if not self.resolution:
logger.error("Resolution not set")
return False
success = False
# Update SSEDisplayTweaks.ini if applicable
sse_success = self.update_sse_display_tweaks()
# Update game preferences INI
prefs_success = self.update_game_prefs_ini()
return sse_success or prefs_success