Sync from development - prepare for v0.1.2

This commit is contained in:
Omni
2025-09-18 08:18:59 +01:00
parent 70b18004e1
commit 1cd4caf04b
61 changed files with 1349 additions and 503 deletions

View File

@@ -104,8 +104,8 @@ class ModlistInstallCLI:
if isinstance(menu_handler_or_system_info, SystemInfo):
# GUI frontend initialization pattern
system_info = menu_handler_or_system_info
self.steamdeck = system_info.is_steamdeck
self.system_info = menu_handler_or_system_info
self.steamdeck = self.system_info.is_steamdeck
# Initialize menu_handler for GUI mode
from ..handlers.menu_handler import MenuHandler
@@ -114,6 +114,9 @@ class ModlistInstallCLI:
# CLI frontend initialization pattern
self.menu_handler = menu_handler_or_system_info
self.steamdeck = steamdeck
# Create system_info for CLI mode
from ..models.configuration import SystemInfo
self.system_info = SystemInfo(is_steamdeck=steamdeck)
self.protontricks_handler = ProtontricksHandler(steamdeck=self.steamdeck)
self.shortcut_handler = ShortcutHandler(steamdeck=self.steamdeck)
@@ -914,6 +917,20 @@ class ModlistInstallCLI:
self.logger.debug("configuration_phase: Proceeding with Steam configuration...")
# Add resolution prompting for CLI mode (before Steam operations)
if not is_gui_mode:
from jackify.backend.handlers.resolution_handler import ResolutionHandler
resolution_handler = ResolutionHandler()
# Check if Steam Deck
is_steamdeck = self.steamdeck if hasattr(self, 'steamdeck') else False
# Prompt for resolution in CLI mode
selected_resolution = resolution_handler.select_resolution(steamdeck=is_steamdeck)
if selected_resolution:
self.context['resolution'] = selected_resolution
self.logger.info(f"Resolution set to: {selected_resolution}")
# Proceed with Steam configuration
self.logger.info(f"Starting Steam configuration for '{shortcut_name}'")
@@ -957,8 +974,8 @@ class ModlistInstallCLI:
shortcut_name, install_dir_str, mo2_exe_path, progress_callback, steamdeck=_is_steamdeck
)
# Handle the result
if isinstance(result, tuple) and len(result) == 3:
# Handle the result (same logic as GUI)
if isinstance(result, tuple) and len(result) == 4:
if result[0] == "CONFLICT":
# Handle conflict
conflicts = result[1]
@@ -984,8 +1001,8 @@ class ModlistInstallCLI:
result = prefix_service.continue_workflow_after_conflict_resolution(
shortcut_name, install_dir_str, mo2_exe_path, app_id, progress_callback
)
if isinstance(result, tuple) and len(result) == 3:
success, prefix_path, app_id = result
if isinstance(result, tuple) and len(result) >= 3:
success, prefix_path, app_id = result[0], result[1], result[2]
else:
success, prefix_path, app_id = False, None, None
else:
@@ -1000,10 +1017,58 @@ class ModlistInstallCLI:
print(f"{COLOR_ERROR}Invalid choice. Cancelling.{COLOR_RESET}")
return
else:
# Normal result
# Normal result with timestamp (4-tuple)
success, prefix_path, app_id, last_timestamp = result
elif isinstance(result, tuple) and len(result) == 3:
if result[0] == "CONFLICT":
# Handle conflict (3-tuple format)
conflicts = result[1]
print(f"\n{COLOR_WARNING}Found existing Steam shortcut(s) with the same name and path:{COLOR_RESET}")
for i, conflict in enumerate(conflicts, 1):
print(f" {i}. Name: {conflict['name']}")
print(f" Executable: {conflict['exe']}")
print(f" Start Directory: {conflict['startdir']}")
print(f"\n{COLOR_PROMPT}Options:{COLOR_RESET}")
print(" • Replace - Remove the existing shortcut and create a new one")
print(" • Cancel - Keep the existing shortcut and stop the installation")
print(" • Skip - Continue without creating a Steam shortcut")
choice = input(f"\n{COLOR_PROMPT}Choose an option (replace/cancel/skip): {COLOR_RESET}").strip().lower()
if choice == 'replace':
print(f"{COLOR_INFO}Replacing existing shortcut...{COLOR_RESET}")
success, app_id = prefix_service.replace_existing_shortcut(shortcut_name, mo2_exe_path, install_dir_str)
if success and app_id:
# Continue the workflow after replacement
result = prefix_service.continue_workflow_after_conflict_resolution(
shortcut_name, install_dir_str, mo2_exe_path, app_id, progress_callback
)
if isinstance(result, tuple) and len(result) >= 3:
success, prefix_path, app_id = result[0], result[1], result[2]
else:
success, prefix_path, app_id = False, None, None
else:
success, prefix_path, app_id = False, None, None
elif choice == 'cancel':
print(f"{COLOR_INFO}Cancelling installation.{COLOR_RESET}")
return
elif choice == 'skip':
print(f"{COLOR_INFO}Skipping Steam shortcut creation.{COLOR_RESET}")
success, prefix_path, app_id = True, None, None
else:
print(f"{COLOR_ERROR}Invalid choice. Cancelling.{COLOR_RESET}")
return
else:
# Normal result (3-tuple format)
success, prefix_path, app_id = result
else:
success, prefix_path, app_id = False, None, None
# Result is not a tuple, check if it's just a boolean success
if result is True:
success, prefix_path, app_id = True, None, None
else:
success, prefix_path, app_id = False, None, None
if success:
print(f"{COLOR_SUCCESS}Automated Steam setup completed successfully!{COLOR_RESET}")
@@ -1011,128 +1076,54 @@ class ModlistInstallCLI:
print(f"{COLOR_INFO}Proton prefix created at: {prefix_path}{COLOR_RESET}")
if app_id:
print(f"{COLOR_INFO}Steam AppID: {app_id}{COLOR_RESET}")
return
# Continue to configuration phase
else:
print(f"{COLOR_WARNING}Automated Steam setup failed. Falling back to manual setup...{COLOR_RESET}")
print(f"{COLOR_ERROR}Automated Steam setup failed. Result: {result}{COLOR_RESET}")
print(f"{COLOR_ERROR}Steam integration was not completed. Please check the logs for details.{COLOR_RESET}")
return
# Fallback to manual shortcut creation process
print(f"\n{COLOR_INFO}Using manual Steam setup workflow...{COLOR_RESET}")
# Step 3: Use SAME backend service as GUI
from jackify.backend.services.modlist_service import ModlistService
from jackify.backend.models.modlist import ModlistContext
from pathlib import Path
# Use the working shortcut creation process from legacy code
from ..handlers.shortcut_handler import ShortcutHandler
shortcut_handler = ShortcutHandler(steamdeck=self.steamdeck, verbose=False)
# Create nxmhandler.ini to suppress NXM popup
shortcut_handler.write_nxmhandler_ini(install_dir_str, mo2_exe_path)
# Create shortcut with working NativeSteamService
from ..services.native_steam_service import NativeSteamService
steam_service = NativeSteamService()
success, app_id = steam_service.create_shortcut_with_proton(
app_name=shortcut_name,
exe_path=mo2_exe_path,
start_dir=os.path.dirname(mo2_exe_path),
launch_options="%command%",
tags=["Jackify"],
proton_version="proton_experimental"
# Create ModlistContext with engine_installed=True (same as GUI)
modlist_context = ModlistContext(
name=shortcut_name,
install_dir=Path(install_dir_str),
download_dir=Path(install_dir_str) / "downloads", # Standard location
game_type=self.context.get('detected_game', 'Unknown'),
nexus_api_key='', # Not needed for configuration
modlist_value=self.context.get('modlist_value', ''),
modlist_source=self.context.get('modlist_source', 'identifier'),
resolution=self.context.get('resolution'),
mo2_exe_path=Path(mo2_exe_path),
skip_confirmation=True, # Always skip confirmation in CLI
engine_installed=True # Skip path manipulation for engine workflows
)
if not success or not app_id:
self.logger.error("Failed to create Steam shortcut")
print(f"{COLOR_ERROR}Failed to create Steam shortcut. Check logs for details.{COLOR_RESET}")
return
# Add app_id to context
modlist_context.app_id = app_id
# Step 2: Handle Steam restart and manual steps (if not in GUI mode)
if not is_gui_mode:
print(f"\n{COLOR_INFO}Steam shortcut created successfully!{COLOR_RESET}")
print("Steam needs to restart to detect the new shortcut. WARNING: This will close all running Steam instances, and games.")
restart_choice = input("\nRestart Steam automatically now? (Y/n): ").strip().lower()
if restart_choice == 'n':
print("\nPlease restart Steam manually and complete the Proton setup steps.")
print("You can configure this modlist later using 'Configure Existing Modlist'.")
return
# Restart Steam
print("\nRestarting Steam...")
if shortcut_handler.secure_steam_restart():
print(f"{COLOR_INFO}Steam restarted successfully.{COLOR_RESET}")
# Display manual Proton steps
from ..handlers.menu_handler import ModlistMenuHandler
from ..handlers.config_handler import ConfigHandler
config_handler = ConfigHandler()
menu_handler = ModlistMenuHandler(config_handler)
menu_handler._display_manual_proton_steps(shortcut_name)
retry_count = 0
max_retries = 3
while retry_count < max_retries:
input(f"\n{COLOR_PROMPT}Once you have completed ALL the steps above, press Enter to continue...{COLOR_RESET}")
print(f"\n{COLOR_INFO}Verifying manual steps...{COLOR_RESET}")
new_app_id = shortcut_handler.get_appid_for_shortcut(shortcut_name, mo2_exe_path)
if new_app_id and new_app_id.isdigit() and int(new_app_id) > 0:
app_id = new_app_id
from ..handlers.modlist_handler import ModlistHandler
modlist_handler = ModlistHandler({}, steamdeck=self.steamdeck)
verified, status_code = modlist_handler.verify_proton_setup(app_id)
if verified:
print(f"{COLOR_SUCCESS}Manual steps verification successful!{COLOR_RESET}")
break
else:
retry_count += 1
if retry_count < max_retries:
print(f"\n{COLOR_ERROR}Verification failed: {status_code}{COLOR_RESET}")
print(f"{COLOR_WARNING}Please ensure you have completed all manual steps correctly.{COLOR_RESET}")
menu_handler._display_manual_proton_steps(shortcut_name)
else:
print(f"\n{COLOR_ERROR}Manual steps verification failed after {max_retries} attempts.{COLOR_RESET}")
print(f"{COLOR_WARNING}Configuration may not work properly.{COLOR_RESET}")
return
else:
retry_count += 1
if retry_count < max_retries:
print(f"\n{COLOR_ERROR}Could not find valid AppID after launch.{COLOR_RESET}")
print(f"{COLOR_WARNING}Please ensure you have launched the shortcut from Steam.{COLOR_RESET}")
menu_handler._display_manual_proton_steps(shortcut_name)
else:
print(f"\n{COLOR_ERROR}Could not find valid AppID after {max_retries} attempts.{COLOR_RESET}")
print(f"{COLOR_WARNING}Configuration may not work properly.{COLOR_RESET}")
return
else:
print(f"{COLOR_ERROR}Steam restart failed. Please restart manually and configure later.{COLOR_RESET}")
return
# Step 3: Build configuration context with the AppID
config_context = {
'name': shortcut_name,
'appid': app_id,
'path': install_dir_str,
'mo2_exe_path': mo2_exe_path,
'resolution': self.context.get('resolution'),
'skip_confirmation': is_gui_mode,
'manual_steps_completed': not is_gui_mode # True if we did manual steps above
}
# Step 4: Use ModlistMenuHandler to run the complete configuration
from ..handlers.menu_handler import ModlistMenuHandler
from ..handlers.config_handler import ConfigHandler
config_handler = ConfigHandler()
modlist_menu = ModlistMenuHandler(config_handler)
# Step 4: Configure modlist using SAME service as GUI
modlist_service = ModlistService(self.system_info)
# Add section header for configuration phase if progress callback is available
if 'progress_callback' in locals() and progress_callback:
progress_callback("") # Blank line for spacing
progress_callback("=== Configuring Modlist ===")
progress_callback("=== Configuration Phase ===")
self.logger.info("Running post-installation configuration phase")
configuration_success = modlist_menu.run_modlist_configuration_phase(config_context)
print(f"\n{COLOR_INFO}=== Configuration Phase ==={COLOR_RESET}")
self.logger.info("Running post-installation configuration phase using ModlistService")
# Configure modlist using SAME method as GUI
configuration_success = modlist_service.configure_modlist_post_steam(modlist_context)
if configuration_success:
print(f"{COLOR_SUCCESS}Configuration completed successfully!{COLOR_RESET}")
self.logger.info("Post-installation configuration completed successfully")
else:
print(f"{COLOR_WARNING}Configuration had some issues but completed.{COLOR_RESET}")
self.logger.warning("Post-installation configuration had issues")
else:
# Game not supported for automated configuration
@@ -1162,10 +1153,9 @@ class ModlistInstallCLI:
# Section header now provided by GUI layer to avoid duplication
try:
# Set GUI mode for backend operations
# CLI Install: keep original GUI mode (don't force GUI mode)
import os
original_gui_mode = os.environ.get('JACKIFY_GUI_MODE')
os.environ['JACKIFY_GUI_MODE'] = '1'
try:
# Build context for configuration
@@ -1176,7 +1166,7 @@ class ModlistInstallCLI:
'modlist_value': context.get('modlist_value'),
'modlist_source': context.get('modlist_source'),
'resolution': context.get('resolution'),
'skip_confirmation': True, # GUI mode is non-interactive
'skip_confirmation': True, # CLI Install is non-interactive
'manual_steps_completed': False
}

View File

@@ -37,7 +37,8 @@ class ConfigHandler:
"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
"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)
}
# Load configuration if exists
@@ -48,6 +49,12 @@ class ConfigHandler:
self.settings["steam_path"] = self._detect_steam_path()
# Save the updated settings
self.save_config()
# 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):
"""

View File

@@ -788,10 +788,16 @@ class ModlistHandler:
status_callback(f"{self._get_progress_timestamp()} Updating resolution settings")
# Ensure resolution_handler call uses correct args if needed
# Assuming it uses modlist_dir (str) and game_var_full (str)
# Construct vanilla game directory path for fallback
vanilla_game_dir = None
if self.steam_library and self.game_var_full:
vanilla_game_dir = str(Path(self.steam_library) / "steamapps" / "common" / self.game_var_full)
if not self.resolution_handler.update_ini_resolution(
modlist_dir=self.modlist_dir,
game_var=self.game_var_full,
set_res=self.selected_resolution
set_res=self.selected_resolution,
vanilla_game_dir=vanilla_game_dir
):
self.logger.warning("Failed to update resolution settings in some INI files.")
print("Warning: Failed to update resolution settings.")
@@ -818,12 +824,18 @@ class ModlistHandler:
status_callback(f"{self._get_progress_timestamp()} Creating dxvk.conf file")
self.logger.info("Step 10: Creating dxvk.conf file...")
# Assuming create_dxvk_conf still uses string paths
# Construct vanilla game directory path for fallback
vanilla_game_dir = None
if self.steam_library and self.game_var_full:
vanilla_game_dir = str(Path(self.steam_library) / "steamapps" / "common" / self.game_var_full)
if not self.path_handler.create_dxvk_conf(
modlist_dir=self.modlist_dir,
modlist_sdcard=self.modlist_sdcard,
steam_library=str(self.steam_library) if self.steam_library else None, # Pass as string or None
basegame_sdcard=self.basegame_sdcard,
game_var_full=self.game_var_full
game_var_full=self.game_var_full,
vanilla_game_dir=vanilla_game_dir
):
self.logger.warning("Failed to create dxvk.conf file.")
print("Warning: Failed to create dxvk.conf file.")

View File

@@ -616,7 +616,8 @@ class ModlistInstallCLI:
if machineid:
# Convert machineid to filename (e.g., "Tuxborn/Tuxborn" -> "Tuxborn.wabbajack")
modlist_name = machineid.split('/')[-1] if '/' in machineid else machineid
cached_wabbajack_path = os.path.expanduser(f"~/Jackify/downloaded_mod_lists/{modlist_name}.wabbajack")
from jackify.shared.paths import get_jackify_downloads_dir
cached_wabbajack_path = get_jackify_downloads_dir() / f"{modlist_name}.wabbajack"
self.logger.debug(f"Checking for cached .wabbajack file: {cached_wabbajack_path}")
if modlist_value and modlist_value.endswith('.wabbajack') and os.path.isfile(modlist_value):

View File

@@ -251,7 +251,7 @@ class PathHandler:
return False
@staticmethod
def create_dxvk_conf(modlist_dir, modlist_sdcard, steam_library, basegame_sdcard, game_var_full):
def create_dxvk_conf(modlist_dir, modlist_sdcard, steam_library, basegame_sdcard, game_var_full, vanilla_game_dir=None):
"""
Create dxvk.conf file in the appropriate location
@@ -261,6 +261,7 @@ class PathHandler:
steam_library (str): Path to the Steam library
basegame_sdcard (bool): Whether the base game is on an SD card
game_var_full (str): Full name of the game (e.g., "Skyrim Special Edition")
vanilla_game_dir (str): Optional path to vanilla game directory for fallback
Returns:
bool: True on success, False on failure
@@ -271,25 +272,35 @@ class PathHandler:
# Determine the location for dxvk.conf
dxvk_conf_path = None
# Check for common stock game directories
# Check for common stock game directories first, then vanilla as fallback
stock_game_paths = [
os.path.join(modlist_dir, "Stock Game"),
os.path.join(modlist_dir, "STOCK GAME"),
os.path.join(modlist_dir, "Game Root"),
os.path.join(modlist_dir, "STOCK GAME"),
os.path.join(modlist_dir, "Stock Game Folder"),
os.path.join(modlist_dir, "Stock Folder"),
os.path.join(modlist_dir, "Skyrim Stock"),
os.path.join(modlist_dir, "root", "Skyrim Special Edition"),
os.path.join(steam_library, game_var_full)
os.path.join(modlist_dir, "root", "Skyrim Special Edition")
]
# Add vanilla game directory as fallback if steam_library and game_var_full are provided
if steam_library and game_var_full:
stock_game_paths.append(os.path.join(steam_library, "steamapps", "common", game_var_full))
for path in stock_game_paths:
if os.path.exists(path):
dxvk_conf_path = os.path.join(path, "dxvk.conf")
break
if not dxvk_conf_path:
logger.error("Could not determine location for dxvk.conf")
return False
# Fallback: Try vanilla game directory if provided
if vanilla_game_dir and os.path.exists(vanilla_game_dir):
logger.info(f"Attempting fallback to vanilla game directory: {vanilla_game_dir}")
dxvk_conf_path = os.path.join(vanilla_game_dir, "dxvk.conf")
logger.info(f"Using vanilla game directory for dxvk.conf: {dxvk_conf_path}")
else:
logger.error("Could not determine location for dxvk.conf")
return False
# The required line that Jackify needs
required_line = "dxvk.enableGraphicsPipelineLibrary = False"
@@ -773,6 +784,21 @@ class PathHandler:
return False
with open(modlist_ini_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
# Extract existing gamePath to use as source of truth for vanilla game location
existing_game_path = None
for line in lines:
if re.match(r'^\s*gamepath\s*=.*@ByteArray\(([^)]+)\)', line, re.IGNORECASE):
match = re.search(r'@ByteArray\(([^)]+)\)', line)
if match:
raw_path = match.group(1)
# Convert Windows path back to Linux path
if raw_path.startswith(('Z:', 'D:')):
linux_path = raw_path[2:].replace('\\\\', '/').replace('\\', '/')
existing_game_path = linux_path
logger.debug(f"Extracted existing gamePath: {existing_game_path}")
break
game_path_updated = False
binary_paths_updated = 0
working_dirs_updated = 0
@@ -791,9 +817,16 @@ class PathHandler:
backslash_style = wd_match.group(2)
working_dir_lines.append((i, stripped, index, backslash_style))
binary_paths_by_index = {}
# Use provided steam_libraries if available, else detect
if steam_libraries is None or not steam_libraries:
# Use existing gamePath to determine correct Steam library, fallback to detection
if existing_game_path and '/steamapps/common/' in existing_game_path:
# Extract the Steam library root from the existing gamePath
steamapps_index = existing_game_path.find('/steamapps/common/')
steam_lib_root = existing_game_path[:steamapps_index]
steam_libraries = [Path(steam_lib_root)]
logger.info(f"Using Steam library from existing gamePath: {steam_lib_root}")
elif steam_libraries is None or not steam_libraries:
steam_libraries = PathHandler.get_all_steam_library_paths()
logger.debug(f"Fallback to detected Steam libraries: {steam_libraries}")
for i, line, index, backslash_style in binary_lines:
parts = line.split('=', 1)
if len(parts) != 2:

View File

@@ -149,7 +149,7 @@ class ResolutionHandler:
return ["1280x720", "1280x800", "1920x1080", "1920x1200", "2560x1440"]
@staticmethod
def update_ini_resolution(modlist_dir: str, game_var: str, set_res: str) -> bool:
def update_ini_resolution(modlist_dir: str, game_var: str, set_res: str, vanilla_game_dir: str = None) -> bool:
"""
Updates the resolution in relevant INI files for the specified game.
@@ -157,6 +157,7 @@ class ResolutionHandler:
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").
vanilla_game_dir (str): Optional path to vanilla game directory for fallback.
Returns:
bool: True if successful or not applicable, False on error.
@@ -211,22 +212,30 @@ class ResolutionHandler:
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)))
# Search entire modlist directory recursively for all target files
logger.debug(f"Searching entire modlist directory for: {prefs_filenames}")
for fname in prefs_filenames:
found_files = list(modlist_path.rglob(fname))
prefs_files_found.extend(found_files)
if found_files:
logger.debug(f"Found {len(found_files)} {fname} files: {[str(f) for f in found_files]}")
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
logger.warning(f"No preference files ({prefs_filenames}) found in modlist directory.")
# Fallback: Try vanilla game directory if provided
if vanilla_game_dir:
logger.info(f"Attempting fallback to vanilla game directory: {vanilla_game_dir}")
vanilla_path = Path(vanilla_game_dir)
for fname in prefs_filenames:
vanilla_files = list(vanilla_path.rglob(fname))
prefs_files_found.extend(vanilla_files)
if vanilla_files:
logger.info(f"Found {len(vanilla_files)} {fname} files in vanilla game directory")
if not prefs_files_found:
logger.warning("No preference files found in modlist or vanilla game directory. Manual INI edit might be needed.")
return True
for ini_file in prefs_files_found:
files_processed += 1
@@ -314,19 +323,23 @@ class ResolutionHandler:
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"):
if stripped_line.lower().startswith("isize w"):
# Preserve original spacing around equals sign
if " = " in stripped_line:
width_replace = f"iSize W = {width}\n"
else:
width_replace = f"iSize W={width}\n"
new_lines.append(width_replace)
modified = True
elif stripped_line.lower().endswith("isize h"):
elif stripped_line.lower().startswith("isize h"):
# Preserve original spacing around equals sign
if " = " in stripped_line:
height_replace = f"iSize H = {height}\n"
else:
height_replace = f"iSize H={height}\n"
new_lines.append(height_replace)
modified = True
else: