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

@@ -1,5 +1,30 @@
# Jackify Changelog
## v0.1.2 - About Dialog and System Information
**Release Date:** September 16, 2025
### New Features
- **About Dialog**: System information display with OS, kernel, desktop environment, and display server detection
- **Engine Version Detection**: Real-time jackify-engine version reporting
- **Update Integration**: Check for Updates functionality within About dialog
- **Support Tools**: Copy system info for troubleshooting
- **Configurable Jackify Directory**: Users can now customize the Jackify data directory location via Settings
### UX Improvements
- **Control Management**: Form controls are now disabled during install/configure workflows to prevent user conflicts (only Cancel remains active)
- **Auto-Accept Steam Restart**: Optional checkbox to automatically accept Steam restart dialogs for unattended workflows
- **Layout Optimization**: Resolution dropdown and Steam restart option share the same line for better space utilization
### Bug Fixes
- **Resolution Handler**: Fixed regression in resolution setting for Fallout 4 and other games when modlists use vanilla game directories instead of traditional "Stock Game" folders
- **DXVK Configuration**: Fixed dxvk.conf creation failure when modlists point directly to vanilla game installations
- **CLI Resolution Setting**: Fixed missing resolution prompting in CLI Install workflow
### Engine Updates
- **jackify-engine v0.3.14**: Updated to support configurable Jackify data directory, improved Nexus API error handling with better 404/403 responses, and enhanced error logging for troubleshooting
---
## v0.1.1 - Self-Updater Implementation
**Release Date:** September 17, 2025

View File

@@ -2,7 +2,7 @@
<div align="center">
[Wiki](https://github.com/Omni-guides/Jackify/wiki) | [Nexus](https://www.nexusmods.com/site/mods/1427) | [Download](https://www.nexusmods.com/Core/Libs/Common/Widgets/DownloadPopUp?id=5807&game_id=2295) | [Wabbajack Discord](https://discord.gg/wabbajack) | [Jackify Issues](https://github.com/Omni-guides/Jackify/issues) | [Legacy Guides](https://github.com/Omni-guides/Jackify/tree/master/Legacy) | [Ko-fi](https://ko-fi.com/omni1)
[Wiki](https://github.com/Omni-guides/Jackify/wiki) | [Nexus](https://www.nexusmods.com/site/mods/1427) | [Download](https://www.nexusmods.com/site/mods/1427?tab=files) | [Wabbajack Discord](https://discord.gg/wabbajack) | [Jackify Issues](https://github.com/Omni-guides/Jackify/issues) | [Legacy Guides](https://github.com/Omni-guides/Jackify/tree/master/Legacy) | [Ko-fi](https://ko-fi.com/omni1)
</div>
@@ -93,7 +93,7 @@ For a complete step-by-step guide with screenshots, see the [User Guide](https:/
### Quick Start
1. **Download**: Get the latest release from [NexusMods](https://www.nexusmods.com/Core/Libs/Common/Widgets/DownloadPopUp?id=5807&game_id=2295)
1. **Download**: Get the latest release from [NexusMods](https://www.nexusmods.com/site/mods/1427?tab=files)
2. **Extract**: Unzip the .7z archive to get `Jackify.AppImage`
3. **Run**: `chmod +x Jackify.AppImage && ./Jackify.AppImage`
4. **Install**: Choose "Install a Modlist", select your game and modlist, configure directories and API key

View File

@@ -5,4 +5,4 @@ This package provides both CLI and GUI interfaces for managing
Wabbajack modlists natively on Linux systems.
"""
__version__ = "0.1.1"
__version__ = "0.1.2"

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:

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -7,7 +7,7 @@
"targets": {
".NETCoreApp,Version=v8.0": {},
".NETCoreApp,Version=v8.0/linux-x64": {
"jackify-engine/0.3.13": {
"jackify-engine/0.3.14": {
"dependencies": {
"Markdig": "0.40.0",
"Microsoft.Extensions.Configuration.Json": "9.0.1",
@@ -22,16 +22,16 @@
"SixLabors.ImageSharp": "3.1.6",
"System.CommandLine": "2.0.0-beta4.22272.1",
"System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1",
"Wabbajack.CLI.Builder": "0.3.13",
"Wabbajack.Downloaders.Bethesda": "0.3.13",
"Wabbajack.Downloaders.Dispatcher": "0.3.13",
"Wabbajack.Hashing.xxHash64": "0.3.13",
"Wabbajack.Networking.Discord": "0.3.13",
"Wabbajack.Networking.GitHub": "0.3.13",
"Wabbajack.Paths.IO": "0.3.13",
"Wabbajack.Server.Lib": "0.3.13",
"Wabbajack.Services.OSIntegrated": "0.3.13",
"Wabbajack.VFS": "0.3.13",
"Wabbajack.CLI.Builder": "0.3.14",
"Wabbajack.Downloaders.Bethesda": "0.3.14",
"Wabbajack.Downloaders.Dispatcher": "0.3.14",
"Wabbajack.Hashing.xxHash64": "0.3.14",
"Wabbajack.Networking.Discord": "0.3.14",
"Wabbajack.Networking.GitHub": "0.3.14",
"Wabbajack.Paths.IO": "0.3.14",
"Wabbajack.Server.Lib": "0.3.14",
"Wabbajack.Services.OSIntegrated": "0.3.14",
"Wabbajack.VFS": "0.3.14",
"MegaApiClient": "1.0.0.0",
"runtimepack.Microsoft.NETCore.App.Runtime.linux-x64": "8.0.19"
},
@@ -1781,7 +1781,7 @@
}
}
},
"Wabbajack.CLI.Builder/0.3.13": {
"Wabbajack.CLI.Builder/0.3.14": {
"dependencies": {
"Microsoft.Extensions.Configuration.Json": "9.0.1",
"Microsoft.Extensions.DependencyInjection": "9.0.1",
@@ -1791,109 +1791,109 @@
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"System.CommandLine": "2.0.0-beta4.22272.1",
"System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1",
"Wabbajack.Paths": "0.3.13"
"Wabbajack.Paths": "0.3.14"
},
"runtime": {
"Wabbajack.CLI.Builder.dll": {}
}
},
"Wabbajack.Common/0.3.13": {
"Wabbajack.Common/0.3.14": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"System.Reactive": "6.0.1",
"Wabbajack.DTOs": "0.3.13",
"Wabbajack.Networking.Http": "0.3.13",
"Wabbajack.Paths.IO": "0.3.13"
"Wabbajack.DTOs": "0.3.14",
"Wabbajack.Networking.Http": "0.3.14",
"Wabbajack.Paths.IO": "0.3.14"
},
"runtime": {
"Wabbajack.Common.dll": {}
}
},
"Wabbajack.Compiler/0.3.13": {
"Wabbajack.Compiler/0.3.14": {
"dependencies": {
"F23.StringSimilarity": "6.0.0",
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Newtonsoft.Json": "13.0.3",
"SixLabors.ImageSharp": "3.1.6",
"Wabbajack.Downloaders.Dispatcher": "0.3.13",
"Wabbajack.Installer": "0.3.13",
"Wabbajack.VFS": "0.3.13",
"Wabbajack.Downloaders.Dispatcher": "0.3.14",
"Wabbajack.Installer": "0.3.14",
"Wabbajack.VFS": "0.3.14",
"ini-parser-netstandard": "2.5.2"
},
"runtime": {
"Wabbajack.Compiler.dll": {}
}
},
"Wabbajack.Compression.BSA/0.3.13": {
"Wabbajack.Compression.BSA/0.3.14": {
"dependencies": {
"K4os.Compression.LZ4.Streams": "1.3.8",
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"SharpZipLib": "1.4.2",
"Wabbajack.Common": "0.3.13",
"Wabbajack.DTOs": "0.3.13"
"Wabbajack.Common": "0.3.14",
"Wabbajack.DTOs": "0.3.14"
},
"runtime": {
"Wabbajack.Compression.BSA.dll": {}
}
},
"Wabbajack.Compression.Zip/0.3.13": {
"Wabbajack.Compression.Zip/0.3.14": {
"dependencies": {
"Wabbajack.IO.Async": "0.3.13"
"Wabbajack.IO.Async": "0.3.14"
},
"runtime": {
"Wabbajack.Compression.Zip.dll": {}
}
},
"Wabbajack.Configuration/0.3.13": {
"Wabbajack.Configuration/0.3.14": {
"runtime": {
"Wabbajack.Configuration.dll": {}
}
},
"Wabbajack.Downloaders.Bethesda/0.3.13": {
"Wabbajack.Downloaders.Bethesda/0.3.14": {
"dependencies": {
"LibAES-CTR": "1.1.0",
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"SharpZipLib": "1.4.2",
"Wabbajack.Common": "0.3.13",
"Wabbajack.Downloaders.Interfaces": "0.3.13",
"Wabbajack.Networking.BethesdaNet": "0.3.13"
"Wabbajack.Common": "0.3.14",
"Wabbajack.Downloaders.Interfaces": "0.3.14",
"Wabbajack.Networking.BethesdaNet": "0.3.14"
},
"runtime": {
"Wabbajack.Downloaders.Bethesda.dll": {}
}
},
"Wabbajack.Downloaders.Dispatcher/0.3.13": {
"Wabbajack.Downloaders.Dispatcher/0.3.14": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Newtonsoft.Json": "13.0.3",
"SixLabors.ImageSharp": "3.1.6",
"Wabbajack.Downloaders.Bethesda": "0.3.13",
"Wabbajack.Downloaders.GameFile": "0.3.13",
"Wabbajack.Downloaders.GoogleDrive": "0.3.13",
"Wabbajack.Downloaders.Http": "0.3.13",
"Wabbajack.Downloaders.IPS4OAuth2Downloader": "0.3.13",
"Wabbajack.Downloaders.Interfaces": "0.3.13",
"Wabbajack.Downloaders.Manual": "0.3.13",
"Wabbajack.Downloaders.MediaFire": "0.3.13",
"Wabbajack.Downloaders.Mega": "0.3.13",
"Wabbajack.Downloaders.ModDB": "0.3.13",
"Wabbajack.Downloaders.Nexus": "0.3.13",
"Wabbajack.Downloaders.VerificationCache": "0.3.13",
"Wabbajack.Downloaders.WabbajackCDN": "0.3.13",
"Wabbajack.Networking.WabbajackClientApi": "0.3.13"
"Wabbajack.Downloaders.Bethesda": "0.3.14",
"Wabbajack.Downloaders.GameFile": "0.3.14",
"Wabbajack.Downloaders.GoogleDrive": "0.3.14",
"Wabbajack.Downloaders.Http": "0.3.14",
"Wabbajack.Downloaders.IPS4OAuth2Downloader": "0.3.14",
"Wabbajack.Downloaders.Interfaces": "0.3.14",
"Wabbajack.Downloaders.Manual": "0.3.14",
"Wabbajack.Downloaders.MediaFire": "0.3.14",
"Wabbajack.Downloaders.Mega": "0.3.14",
"Wabbajack.Downloaders.ModDB": "0.3.14",
"Wabbajack.Downloaders.Nexus": "0.3.14",
"Wabbajack.Downloaders.VerificationCache": "0.3.14",
"Wabbajack.Downloaders.WabbajackCDN": "0.3.14",
"Wabbajack.Networking.WabbajackClientApi": "0.3.14"
},
"runtime": {
"Wabbajack.Downloaders.Dispatcher.dll": {}
}
},
"Wabbajack.Downloaders.GameFile/0.3.13": {
"Wabbajack.Downloaders.GameFile/0.3.14": {
"dependencies": {
"GameFinder.StoreHandlers.EADesktop": "4.5.0",
"GameFinder.StoreHandlers.EGS": "4.5.0",
@@ -1903,360 +1903,360 @@
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"SixLabors.ImageSharp": "3.1.6",
"Wabbajack.Downloaders.Interfaces": "0.3.13",
"Wabbajack.VFS": "0.3.13"
"Wabbajack.Downloaders.Interfaces": "0.3.14",
"Wabbajack.VFS": "0.3.14"
},
"runtime": {
"Wabbajack.Downloaders.GameFile.dll": {}
}
},
"Wabbajack.Downloaders.GoogleDrive/0.3.13": {
"Wabbajack.Downloaders.GoogleDrive/0.3.14": {
"dependencies": {
"HtmlAgilityPack": "1.11.72",
"Microsoft.AspNetCore.Http.Extensions": "2.3.0",
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Wabbajack.Common": "0.3.13",
"Wabbajack.DTOs": "0.3.13",
"Wabbajack.Downloaders.Interfaces": "0.3.13",
"Wabbajack.Networking.Http": "0.3.13",
"Wabbajack.Networking.Http.Interfaces": "0.3.13"
"Wabbajack.Common": "0.3.14",
"Wabbajack.DTOs": "0.3.14",
"Wabbajack.Downloaders.Interfaces": "0.3.14",
"Wabbajack.Networking.Http": "0.3.14",
"Wabbajack.Networking.Http.Interfaces": "0.3.14"
},
"runtime": {
"Wabbajack.Downloaders.GoogleDrive.dll": {}
}
},
"Wabbajack.Downloaders.Http/0.3.13": {
"Wabbajack.Downloaders.Http/0.3.14": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Wabbajack.Common": "0.3.13",
"Wabbajack.DTOs": "0.3.13",
"Wabbajack.Downloaders.Interfaces": "0.3.13",
"Wabbajack.Networking.BethesdaNet": "0.3.13",
"Wabbajack.Networking.Http.Interfaces": "0.3.13",
"Wabbajack.Paths.IO": "0.3.13"
"Wabbajack.Common": "0.3.14",
"Wabbajack.DTOs": "0.3.14",
"Wabbajack.Downloaders.Interfaces": "0.3.14",
"Wabbajack.Networking.BethesdaNet": "0.3.14",
"Wabbajack.Networking.Http.Interfaces": "0.3.14",
"Wabbajack.Paths.IO": "0.3.14"
},
"runtime": {
"Wabbajack.Downloaders.Http.dll": {}
}
},
"Wabbajack.Downloaders.Interfaces/0.3.13": {
"Wabbajack.Downloaders.Interfaces/0.3.14": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Wabbajack.Compression.Zip": "0.3.13",
"Wabbajack.DTOs": "0.3.13",
"Wabbajack.Paths.IO": "0.3.13"
"Wabbajack.Compression.Zip": "0.3.14",
"Wabbajack.DTOs": "0.3.14",
"Wabbajack.Paths.IO": "0.3.14"
},
"runtime": {
"Wabbajack.Downloaders.Interfaces.dll": {}
}
},
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.13": {
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.14": {
"dependencies": {
"F23.StringSimilarity": "6.0.0",
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Wabbajack.Common": "0.3.13",
"Wabbajack.Downloaders.Interfaces": "0.3.13",
"Wabbajack.Networking.Http": "0.3.13",
"Wabbajack.Networking.Http.Interfaces": "0.3.13"
"Wabbajack.Common": "0.3.14",
"Wabbajack.Downloaders.Interfaces": "0.3.14",
"Wabbajack.Networking.Http": "0.3.14",
"Wabbajack.Networking.Http.Interfaces": "0.3.14"
},
"runtime": {
"Wabbajack.Downloaders.IPS4OAuth2Downloader.dll": {}
}
},
"Wabbajack.Downloaders.Manual/0.3.13": {
"Wabbajack.Downloaders.Manual/0.3.14": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Wabbajack.Common": "0.3.13",
"Wabbajack.Downloaders.Interfaces": "0.3.13"
"Wabbajack.Common": "0.3.14",
"Wabbajack.Downloaders.Interfaces": "0.3.14"
},
"runtime": {
"Wabbajack.Downloaders.Manual.dll": {}
}
},
"Wabbajack.Downloaders.MediaFire/0.3.13": {
"Wabbajack.Downloaders.MediaFire/0.3.14": {
"dependencies": {
"HtmlAgilityPack": "1.11.72",
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Wabbajack.Common": "0.3.13",
"Wabbajack.Downloaders.Interfaces": "0.3.13",
"Wabbajack.Networking.Http.Interfaces": "0.3.13"
"Wabbajack.Common": "0.3.14",
"Wabbajack.Downloaders.Interfaces": "0.3.14",
"Wabbajack.Networking.Http.Interfaces": "0.3.14"
},
"runtime": {
"Wabbajack.Downloaders.MediaFire.dll": {}
}
},
"Wabbajack.Downloaders.Mega/0.3.13": {
"Wabbajack.Downloaders.Mega/0.3.14": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Newtonsoft.Json": "13.0.3",
"Wabbajack.Common": "0.3.13",
"Wabbajack.Downloaders.Interfaces": "0.3.13",
"Wabbajack.Paths.IO": "0.3.13"
"Wabbajack.Common": "0.3.14",
"Wabbajack.Downloaders.Interfaces": "0.3.14",
"Wabbajack.Paths.IO": "0.3.14"
},
"runtime": {
"Wabbajack.Downloaders.Mega.dll": {}
}
},
"Wabbajack.Downloaders.ModDB/0.3.13": {
"Wabbajack.Downloaders.ModDB/0.3.14": {
"dependencies": {
"HtmlAgilityPack": "1.11.72",
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Newtonsoft.Json": "13.0.3",
"Wabbajack.Common": "0.3.13",
"Wabbajack.Downloaders.Interfaces": "0.3.13",
"Wabbajack.Networking.Http": "0.3.13",
"Wabbajack.Networking.Http.Interfaces": "0.3.13"
"Wabbajack.Common": "0.3.14",
"Wabbajack.Downloaders.Interfaces": "0.3.14",
"Wabbajack.Networking.Http": "0.3.14",
"Wabbajack.Networking.Http.Interfaces": "0.3.14"
},
"runtime": {
"Wabbajack.Downloaders.ModDB.dll": {}
}
},
"Wabbajack.Downloaders.Nexus/0.3.13": {
"Wabbajack.Downloaders.Nexus/0.3.14": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Wabbajack.DTOs": "0.3.13",
"Wabbajack.Downloaders.Interfaces": "0.3.13",
"Wabbajack.Hashing.xxHash64": "0.3.13",
"Wabbajack.Networking.Http": "0.3.13",
"Wabbajack.Networking.Http.Interfaces": "0.3.13",
"Wabbajack.Networking.NexusApi": "0.3.13",
"Wabbajack.Paths": "0.3.13"
"Wabbajack.DTOs": "0.3.14",
"Wabbajack.Downloaders.Interfaces": "0.3.14",
"Wabbajack.Hashing.xxHash64": "0.3.14",
"Wabbajack.Networking.Http": "0.3.14",
"Wabbajack.Networking.Http.Interfaces": "0.3.14",
"Wabbajack.Networking.NexusApi": "0.3.14",
"Wabbajack.Paths": "0.3.14"
},
"runtime": {
"Wabbajack.Downloaders.Nexus.dll": {}
}
},
"Wabbajack.Downloaders.VerificationCache/0.3.13": {
"Wabbajack.Downloaders.VerificationCache/0.3.14": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Stub.System.Data.SQLite.Core.NetStandard": "1.0.119",
"Wabbajack.DTOs": "0.3.13",
"Wabbajack.Paths.IO": "0.3.13"
"Wabbajack.DTOs": "0.3.14",
"Wabbajack.Paths.IO": "0.3.14"
},
"runtime": {
"Wabbajack.Downloaders.VerificationCache.dll": {}
}
},
"Wabbajack.Downloaders.WabbajackCDN/0.3.13": {
"Wabbajack.Downloaders.WabbajackCDN/0.3.14": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Microsoft.Toolkit.HighPerformance": "7.1.2",
"Wabbajack.Common": "0.3.13",
"Wabbajack.Downloaders.Interfaces": "0.3.13",
"Wabbajack.Networking.Http": "0.3.13",
"Wabbajack.RateLimiter": "0.3.13"
"Wabbajack.Common": "0.3.14",
"Wabbajack.Downloaders.Interfaces": "0.3.14",
"Wabbajack.Networking.Http": "0.3.14",
"Wabbajack.RateLimiter": "0.3.14"
},
"runtime": {
"Wabbajack.Downloaders.WabbajackCDN.dll": {}
}
},
"Wabbajack.DTOs/0.3.13": {
"Wabbajack.DTOs/0.3.14": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Wabbajack.Hashing.xxHash64": "0.3.13",
"Wabbajack.Paths": "0.3.13"
"Wabbajack.Hashing.xxHash64": "0.3.14",
"Wabbajack.Paths": "0.3.14"
},
"runtime": {
"Wabbajack.DTOs.dll": {}
}
},
"Wabbajack.FileExtractor/0.3.13": {
"Wabbajack.FileExtractor/0.3.14": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"OMODFramework": "3.0.1",
"Wabbajack.Common": "0.3.13",
"Wabbajack.Compression.BSA": "0.3.13",
"Wabbajack.Hashing.PHash": "0.3.13",
"Wabbajack.Paths": "0.3.13"
"Wabbajack.Common": "0.3.14",
"Wabbajack.Compression.BSA": "0.3.14",
"Wabbajack.Hashing.PHash": "0.3.14",
"Wabbajack.Paths": "0.3.14"
},
"runtime": {
"Wabbajack.FileExtractor.dll": {}
}
},
"Wabbajack.Hashing.PHash/0.3.13": {
"Wabbajack.Hashing.PHash/0.3.14": {
"dependencies": {
"BCnEncoder.Net.ImageSharp": "1.1.1",
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Shipwreck.Phash": "0.5.0",
"SixLabors.ImageSharp": "3.1.6",
"Wabbajack.Common": "0.3.13",
"Wabbajack.DTOs": "0.3.13",
"Wabbajack.Paths": "0.3.13",
"Wabbajack.Paths.IO": "0.3.13"
"Wabbajack.Common": "0.3.14",
"Wabbajack.DTOs": "0.3.14",
"Wabbajack.Paths": "0.3.14",
"Wabbajack.Paths.IO": "0.3.14"
},
"runtime": {
"Wabbajack.Hashing.PHash.dll": {}
}
},
"Wabbajack.Hashing.xxHash64/0.3.13": {
"Wabbajack.Hashing.xxHash64/0.3.14": {
"dependencies": {
"Wabbajack.Paths": "0.3.13",
"Wabbajack.RateLimiter": "0.3.13"
"Wabbajack.Paths": "0.3.14",
"Wabbajack.RateLimiter": "0.3.14"
},
"runtime": {
"Wabbajack.Hashing.xxHash64.dll": {}
}
},
"Wabbajack.Installer/0.3.13": {
"Wabbajack.Installer/0.3.14": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Newtonsoft.Json": "13.0.3",
"Octopus.Octodiff": "2.0.548",
"SixLabors.ImageSharp": "3.1.6",
"Wabbajack.DTOs": "0.3.13",
"Wabbajack.Downloaders.Dispatcher": "0.3.13",
"Wabbajack.Downloaders.GameFile": "0.3.13",
"Wabbajack.FileExtractor": "0.3.13",
"Wabbajack.Networking.WabbajackClientApi": "0.3.13",
"Wabbajack.Paths": "0.3.13",
"Wabbajack.Paths.IO": "0.3.13",
"Wabbajack.VFS": "0.3.13",
"Wabbajack.DTOs": "0.3.14",
"Wabbajack.Downloaders.Dispatcher": "0.3.14",
"Wabbajack.Downloaders.GameFile": "0.3.14",
"Wabbajack.FileExtractor": "0.3.14",
"Wabbajack.Networking.WabbajackClientApi": "0.3.14",
"Wabbajack.Paths": "0.3.14",
"Wabbajack.Paths.IO": "0.3.14",
"Wabbajack.VFS": "0.3.14",
"ini-parser-netstandard": "2.5.2"
},
"runtime": {
"Wabbajack.Installer.dll": {}
}
},
"Wabbajack.IO.Async/0.3.13": {
"Wabbajack.IO.Async/0.3.14": {
"runtime": {
"Wabbajack.IO.Async.dll": {}
}
},
"Wabbajack.Networking.BethesdaNet/0.3.13": {
"Wabbajack.Networking.BethesdaNet/0.3.14": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Wabbajack.DTOs": "0.3.13",
"Wabbajack.Networking.Http": "0.3.13",
"Wabbajack.Networking.Http.Interfaces": "0.3.13"
"Wabbajack.DTOs": "0.3.14",
"Wabbajack.Networking.Http": "0.3.14",
"Wabbajack.Networking.Http.Interfaces": "0.3.14"
},
"runtime": {
"Wabbajack.Networking.BethesdaNet.dll": {}
}
},
"Wabbajack.Networking.Discord/0.3.13": {
"Wabbajack.Networking.Discord/0.3.14": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Wabbajack.Networking.Http.Interfaces": "0.3.13"
"Wabbajack.Networking.Http.Interfaces": "0.3.14"
},
"runtime": {
"Wabbajack.Networking.Discord.dll": {}
}
},
"Wabbajack.Networking.GitHub/0.3.13": {
"Wabbajack.Networking.GitHub/0.3.14": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Octokit": "14.0.0",
"Wabbajack.DTOs": "0.3.13",
"Wabbajack.Networking.Http.Interfaces": "0.3.13"
"Wabbajack.DTOs": "0.3.14",
"Wabbajack.Networking.Http.Interfaces": "0.3.14"
},
"runtime": {
"Wabbajack.Networking.GitHub.dll": {}
}
},
"Wabbajack.Networking.Http/0.3.13": {
"Wabbajack.Networking.Http/0.3.14": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Http": "9.0.1",
"Microsoft.Extensions.Logging": "9.0.1",
"Wabbajack.Configuration": "0.3.13",
"Wabbajack.Downloaders.Interfaces": "0.3.13",
"Wabbajack.Hashing.xxHash64": "0.3.13",
"Wabbajack.Networking.Http.Interfaces": "0.3.13",
"Wabbajack.Paths": "0.3.13",
"Wabbajack.Paths.IO": "0.3.13"
"Wabbajack.Configuration": "0.3.14",
"Wabbajack.Downloaders.Interfaces": "0.3.14",
"Wabbajack.Hashing.xxHash64": "0.3.14",
"Wabbajack.Networking.Http.Interfaces": "0.3.14",
"Wabbajack.Paths": "0.3.14",
"Wabbajack.Paths.IO": "0.3.14"
},
"runtime": {
"Wabbajack.Networking.Http.dll": {}
}
},
"Wabbajack.Networking.Http.Interfaces/0.3.13": {
"Wabbajack.Networking.Http.Interfaces/0.3.14": {
"dependencies": {
"Wabbajack.Hashing.xxHash64": "0.3.13"
"Wabbajack.Hashing.xxHash64": "0.3.14"
},
"runtime": {
"Wabbajack.Networking.Http.Interfaces.dll": {}
}
},
"Wabbajack.Networking.NexusApi/0.3.13": {
"Wabbajack.Networking.NexusApi/0.3.14": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Wabbajack.DTOs": "0.3.13",
"Wabbajack.Networking.Http": "0.3.13",
"Wabbajack.Networking.Http.Interfaces": "0.3.13",
"Wabbajack.Networking.WabbajackClientApi": "0.3.13"
"Wabbajack.DTOs": "0.3.14",
"Wabbajack.Networking.Http": "0.3.14",
"Wabbajack.Networking.Http.Interfaces": "0.3.14",
"Wabbajack.Networking.WabbajackClientApi": "0.3.14"
},
"runtime": {
"Wabbajack.Networking.NexusApi.dll": {}
}
},
"Wabbajack.Networking.WabbajackClientApi/0.3.13": {
"Wabbajack.Networking.WabbajackClientApi/0.3.14": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Octokit": "14.0.0",
"Wabbajack.Common": "0.3.13",
"Wabbajack.DTOs": "0.3.13",
"Wabbajack.Paths.IO": "0.3.13",
"Wabbajack.VFS.Interfaces": "0.3.13",
"Wabbajack.Common": "0.3.14",
"Wabbajack.DTOs": "0.3.14",
"Wabbajack.Paths.IO": "0.3.14",
"Wabbajack.VFS.Interfaces": "0.3.14",
"YamlDotNet": "16.3.0"
},
"runtime": {
"Wabbajack.Networking.WabbajackClientApi.dll": {}
}
},
"Wabbajack.Paths/0.3.13": {
"Wabbajack.Paths/0.3.14": {
"runtime": {
"Wabbajack.Paths.dll": {}
}
},
"Wabbajack.Paths.IO/0.3.13": {
"Wabbajack.Paths.IO/0.3.14": {
"dependencies": {
"Wabbajack.Paths": "0.3.13",
"Wabbajack.Paths": "0.3.14",
"shortid": "4.0.0"
},
"runtime": {
"Wabbajack.Paths.IO.dll": {}
}
},
"Wabbajack.RateLimiter/0.3.13": {
"Wabbajack.RateLimiter/0.3.14": {
"runtime": {
"Wabbajack.RateLimiter.dll": {}
}
},
"Wabbajack.Server.Lib/0.3.13": {
"Wabbajack.Server.Lib/0.3.14": {
"dependencies": {
"FluentFTP": "52.0.0",
"Microsoft.Extensions.DependencyInjection": "9.0.1",
@@ -2264,58 +2264,58 @@
"Nettle": "3.0.0",
"Newtonsoft.Json": "13.0.3",
"SixLabors.ImageSharp": "3.1.6",
"Wabbajack.Common": "0.3.13",
"Wabbajack.Networking.Http.Interfaces": "0.3.13",
"Wabbajack.Services.OSIntegrated": "0.3.13"
"Wabbajack.Common": "0.3.14",
"Wabbajack.Networking.Http.Interfaces": "0.3.14",
"Wabbajack.Services.OSIntegrated": "0.3.14"
},
"runtime": {
"Wabbajack.Server.Lib.dll": {}
}
},
"Wabbajack.Services.OSIntegrated/0.3.13": {
"Wabbajack.Services.OSIntegrated/0.3.14": {
"dependencies": {
"DeviceId": "6.8.0",
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Newtonsoft.Json": "13.0.3",
"SixLabors.ImageSharp": "3.1.6",
"Wabbajack.Compiler": "0.3.13",
"Wabbajack.Downloaders.Dispatcher": "0.3.13",
"Wabbajack.Installer": "0.3.13",
"Wabbajack.Networking.BethesdaNet": "0.3.13",
"Wabbajack.Networking.Discord": "0.3.13",
"Wabbajack.VFS": "0.3.13"
"Wabbajack.Compiler": "0.3.14",
"Wabbajack.Downloaders.Dispatcher": "0.3.14",
"Wabbajack.Installer": "0.3.14",
"Wabbajack.Networking.BethesdaNet": "0.3.14",
"Wabbajack.Networking.Discord": "0.3.14",
"Wabbajack.VFS": "0.3.14"
},
"runtime": {
"Wabbajack.Services.OSIntegrated.dll": {}
}
},
"Wabbajack.VFS/0.3.13": {
"Wabbajack.VFS/0.3.14": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"SixLabors.ImageSharp": "3.1.6",
"System.Data.SQLite.Core": "1.0.119",
"Wabbajack.Common": "0.3.13",
"Wabbajack.FileExtractor": "0.3.13",
"Wabbajack.Hashing.PHash": "0.3.13",
"Wabbajack.Hashing.xxHash64": "0.3.13",
"Wabbajack.Paths": "0.3.13",
"Wabbajack.Paths.IO": "0.3.13",
"Wabbajack.VFS.Interfaces": "0.3.13"
"Wabbajack.Common": "0.3.14",
"Wabbajack.FileExtractor": "0.3.14",
"Wabbajack.Hashing.PHash": "0.3.14",
"Wabbajack.Hashing.xxHash64": "0.3.14",
"Wabbajack.Paths": "0.3.14",
"Wabbajack.Paths.IO": "0.3.14",
"Wabbajack.VFS.Interfaces": "0.3.14"
},
"runtime": {
"Wabbajack.VFS.dll": {}
}
},
"Wabbajack.VFS.Interfaces/0.3.13": {
"Wabbajack.VFS.Interfaces/0.3.14": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Wabbajack.DTOs": "0.3.13",
"Wabbajack.Hashing.xxHash64": "0.3.13",
"Wabbajack.Paths": "0.3.13"
"Wabbajack.DTOs": "0.3.14",
"Wabbajack.Hashing.xxHash64": "0.3.14",
"Wabbajack.Paths": "0.3.14"
},
"runtime": {
"Wabbajack.VFS.Interfaces.dll": {}
@@ -2332,7 +2332,7 @@
}
},
"libraries": {
"jackify-engine/0.3.13": {
"jackify-engine/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
@@ -3021,202 +3021,202 @@
"path": "yamldotnet/16.3.0",
"hashPath": "yamldotnet.16.3.0.nupkg.sha512"
},
"Wabbajack.CLI.Builder/0.3.13": {
"Wabbajack.CLI.Builder/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Common/0.3.13": {
"Wabbajack.Common/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Compiler/0.3.13": {
"Wabbajack.Compiler/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Compression.BSA/0.3.13": {
"Wabbajack.Compression.BSA/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Compression.Zip/0.3.13": {
"Wabbajack.Compression.Zip/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Configuration/0.3.13": {
"Wabbajack.Configuration/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.Bethesda/0.3.13": {
"Wabbajack.Downloaders.Bethesda/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.Dispatcher/0.3.13": {
"Wabbajack.Downloaders.Dispatcher/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.GameFile/0.3.13": {
"Wabbajack.Downloaders.GameFile/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.GoogleDrive/0.3.13": {
"Wabbajack.Downloaders.GoogleDrive/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.Http/0.3.13": {
"Wabbajack.Downloaders.Http/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.Interfaces/0.3.13": {
"Wabbajack.Downloaders.Interfaces/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.13": {
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.Manual/0.3.13": {
"Wabbajack.Downloaders.Manual/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.MediaFire/0.3.13": {
"Wabbajack.Downloaders.MediaFire/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.Mega/0.3.13": {
"Wabbajack.Downloaders.Mega/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.ModDB/0.3.13": {
"Wabbajack.Downloaders.ModDB/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.Nexus/0.3.13": {
"Wabbajack.Downloaders.Nexus/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.VerificationCache/0.3.13": {
"Wabbajack.Downloaders.VerificationCache/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.WabbajackCDN/0.3.13": {
"Wabbajack.Downloaders.WabbajackCDN/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.DTOs/0.3.13": {
"Wabbajack.DTOs/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.FileExtractor/0.3.13": {
"Wabbajack.FileExtractor/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Hashing.PHash/0.3.13": {
"Wabbajack.Hashing.PHash/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Hashing.xxHash64/0.3.13": {
"Wabbajack.Hashing.xxHash64/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Installer/0.3.13": {
"Wabbajack.Installer/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.IO.Async/0.3.13": {
"Wabbajack.IO.Async/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Networking.BethesdaNet/0.3.13": {
"Wabbajack.Networking.BethesdaNet/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Networking.Discord/0.3.13": {
"Wabbajack.Networking.Discord/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Networking.GitHub/0.3.13": {
"Wabbajack.Networking.GitHub/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Networking.Http/0.3.13": {
"Wabbajack.Networking.Http/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Networking.Http.Interfaces/0.3.13": {
"Wabbajack.Networking.Http.Interfaces/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Networking.NexusApi/0.3.13": {
"Wabbajack.Networking.NexusApi/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Networking.WabbajackClientApi/0.3.13": {
"Wabbajack.Networking.WabbajackClientApi/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Paths/0.3.13": {
"Wabbajack.Paths/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Paths.IO/0.3.13": {
"Wabbajack.Paths.IO/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.RateLimiter/0.3.13": {
"Wabbajack.RateLimiter/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Server.Lib/0.3.13": {
"Wabbajack.Server.Lib/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Services.OSIntegrated/0.3.13": {
"Wabbajack.Services.OSIntegrated/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.VFS/0.3.13": {
"Wabbajack.VFS/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.VFS.Interfaces/0.3.13": {
"Wabbajack.VFS.Interfaces/0.3.14": {
"type": "project",
"serviceable": false,
"sha512": ""

Binary file not shown.

View File

@@ -32,9 +32,9 @@ class WabbajackMenuHandler:
print_section_header("Modlist and Wabbajack Tasks")
print(f"{COLOR_SELECTION}1.{COLOR_RESET} Install a Modlist (Automated)")
print(f" {COLOR_ACTION}Uses jackify-engine for a full install flow{COLOR_RESET}")
print(f" {COLOR_ACTION}Install a modlist in full: Select from a list or provide a .wabbajack file{COLOR_RESET}")
print(f"{COLOR_SELECTION}2.{COLOR_RESET} Configure New Modlist (Post-Download)")
print(f" {COLOR_ACTION}→ Modlist .wabbajack file downloaded? Configure it for Steam{COLOR_RESET}")
print(f" {COLOR_ACTION}→ Modlist already downloaded? Configure and add to Steam{COLOR_RESET}")
print(f"{COLOR_SELECTION}3.{COLOR_RESET} Configure Existing Modlist (In Steam)")
print(f" {COLOR_ACTION}→ Modlist already in Steam? Re-configure it here{COLOR_RESET}")
# HIDDEN FOR FIRST RELEASE - UNCOMMENT WHEN READY

View File

@@ -0,0 +1,400 @@
"""
About dialog for Jackify.
This dialog displays system information, version details, and provides
access to update checking and external links.
"""
import logging
import os
import platform
import subprocess
from pathlib import Path
from typing import Optional
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QGroupBox, QTextEdit, QApplication
)
from PySide6.QtCore import Qt, QThread, Signal
from PySide6.QtGui import QFont, QClipboard
from ....backend.services.update_service import UpdateService
from ....backend.models.configuration import SystemInfo
from .... import __version__
logger = logging.getLogger(__name__)
class UpdateCheckThread(QThread):
"""Background thread for checking updates."""
update_check_finished = Signal(object) # UpdateInfo or None
def __init__(self, update_service: UpdateService):
super().__init__()
self.update_service = update_service
def run(self):
"""Check for updates in background."""
try:
update_info = self.update_service.check_for_updates()
self.update_check_finished.emit(update_info)
except Exception as e:
logger.error(f"Error checking for updates: {e}")
self.update_check_finished.emit(None)
class AboutDialog(QDialog):
"""About dialog showing system info and app details."""
def __init__(self, system_info: SystemInfo, parent=None):
super().__init__(parent)
self.system_info = system_info
self.update_service = UpdateService(__version__)
self.update_check_thread = None
self.setup_ui()
self.setup_connections()
def setup_ui(self):
"""Set up the dialog UI."""
self.setWindowTitle("About Jackify")
self.setModal(True)
self.setFixedSize(520, 520)
layout = QVBoxLayout(self)
# Header
header_layout = QVBoxLayout()
# App icon/name
title_label = QLabel("Jackify")
title_font = QFont()
title_font.setPointSize(18)
title_font.setBold(True)
title_label.setFont(title_font)
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet("color: #3fd0ea; margin: 10px;")
header_layout.addWidget(title_label)
subtitle_label = QLabel(f"v{__version__}")
subtitle_font = QFont()
subtitle_font.setPointSize(12)
subtitle_label.setFont(subtitle_font)
subtitle_label.setAlignment(Qt.AlignCenter)
subtitle_label.setStyleSheet("color: #666; margin-bottom: 10px;")
header_layout.addWidget(subtitle_label)
tagline_label = QLabel("Simplifying Wabbajack modlist installation and configuration on Linux")
tagline_label.setAlignment(Qt.AlignCenter)
tagline_label.setStyleSheet("color: #888; margin-bottom: 20px;")
header_layout.addWidget(tagline_label)
layout.addLayout(header_layout)
# System Information Group
system_group = QGroupBox("System Information")
system_layout = QVBoxLayout(system_group)
system_info_text = self._get_system_info_text()
system_info_label = QLabel(system_info_text)
system_info_label.setStyleSheet("font-family: monospace; font-size: 10pt; color: #ccc;")
system_info_label.setWordWrap(True)
system_layout.addWidget(system_info_label)
layout.addWidget(system_group)
# Jackify Information Group
jackify_group = QGroupBox("Jackify Information")
jackify_layout = QVBoxLayout(jackify_group)
jackify_info_text = self._get_jackify_info_text()
jackify_info_label = QLabel(jackify_info_text)
jackify_info_label.setStyleSheet("font-family: monospace; font-size: 10pt; color: #ccc;")
jackify_layout.addWidget(jackify_info_label)
layout.addWidget(jackify_group)
# Update status
self.update_status_label = QLabel("")
self.update_status_label.setStyleSheet("color: #666; font-size: 10pt; margin: 5px;")
self.update_status_label.setAlignment(Qt.AlignCenter)
layout.addWidget(self.update_status_label)
# Buttons
button_layout = QHBoxLayout()
# Update check button
self.update_button = QPushButton("Check for Updates")
self.update_button.clicked.connect(self.check_for_updates)
self.update_button.setStyleSheet("""
QPushButton {
background-color: #23272e;
color: #3fd0ea;
font-weight: bold;
padding: 8px 16px;
border-radius: 4px;
border: 2px solid #3fd0ea;
}
QPushButton:hover {
background-color: #3fd0ea;
color: #23272e;
}
QPushButton:pressed {
background-color: #2bb8d6;
color: #23272e;
}
QPushButton:disabled {
background-color: #444;
color: #666;
border-color: #666;
}
""")
button_layout.addWidget(self.update_button)
button_layout.addStretch()
# Copy Info button
copy_button = QPushButton("Copy Info")
copy_button.clicked.connect(self.copy_system_info)
button_layout.addWidget(copy_button)
# External links
github_button = QPushButton("GitHub")
github_button.clicked.connect(self.open_github)
button_layout.addWidget(github_button)
nexus_button = QPushButton("Nexus")
nexus_button.clicked.connect(self.open_nexus)
button_layout.addWidget(nexus_button)
layout.addLayout(button_layout)
# Close button
close_layout = QHBoxLayout()
close_layout.addStretch()
close_button = QPushButton("Close")
close_button.setDefault(True)
close_button.clicked.connect(self.accept)
close_layout.addWidget(close_button)
layout.addLayout(close_layout)
def setup_connections(self):
"""Set up signal connections."""
pass
def _get_system_info_text(self) -> str:
"""Get formatted system information."""
try:
# OS info
os_info = self._get_os_info()
kernel = platform.release()
# Desktop environment
desktop = self._get_desktop_environment()
# Display server
display_server = self._get_display_server()
return f"• OS: {os_info}\n• Kernel: {kernel}\n• Desktop: {desktop}\n• Display: {display_server}"
except Exception as e:
logger.error(f"Error getting system info: {e}")
return "• System info unavailable"
def _get_jackify_info_text(self) -> str:
"""Get formatted Jackify information."""
try:
# Engine version
engine_version = self._get_engine_version()
# Python version
python_version = platform.python_version()
return f"• Engine: {engine_version}\n• Python: {python_version}"
except Exception as e:
logger.error(f"Error getting Jackify info: {e}")
return "• Jackify info unavailable"
def _get_os_info(self) -> str:
"""Get OS distribution name and version."""
try:
if os.path.exists("/etc/os-release"):
with open("/etc/os-release", "r") as f:
lines = f.readlines()
pretty_name = None
name = None
version = None
for line in lines:
line = line.strip()
if line.startswith("PRETTY_NAME="):
pretty_name = line.split("=", 1)[1].strip('"')
elif line.startswith("NAME="):
name = line.split("=", 1)[1].strip('"')
elif line.startswith("VERSION="):
version = line.split("=", 1)[1].strip('"')
# Prefer PRETTY_NAME, fallback to NAME + VERSION
if pretty_name:
return pretty_name
elif name and version:
return f"{name} {version}"
elif name:
return name
# Fallback to platform info
return f"{platform.system()} {platform.release()}"
except Exception as e:
logger.error(f"Error getting OS info: {e}")
return "Unknown Linux"
def _get_desktop_environment(self) -> str:
"""Get desktop environment."""
try:
# Try XDG_CURRENT_DESKTOP first
desktop = os.environ.get("XDG_CURRENT_DESKTOP")
if desktop:
return desktop
# Fallback to DESKTOP_SESSION
desktop = os.environ.get("DESKTOP_SESSION")
if desktop:
return desktop
# Try detecting common DEs
if os.environ.get("KDE_FULL_SESSION"):
return "KDE"
elif os.environ.get("GNOME_DESKTOP_SESSION_ID"):
return "GNOME"
elif os.environ.get("XFCE4_SESSION"):
return "XFCE"
return "Unknown"
except Exception as e:
logger.error(f"Error getting desktop environment: {e}")
return "Unknown"
def _get_display_server(self) -> str:
"""Get display server type (Wayland or X11)."""
try:
# Check XDG_SESSION_TYPE first
session_type = os.environ.get("XDG_SESSION_TYPE")
if session_type:
return session_type.capitalize()
# Check for Wayland display
if os.environ.get("WAYLAND_DISPLAY"):
return "Wayland"
# Check for X11 display
if os.environ.get("DISPLAY"):
return "X11"
return "Unknown"
except Exception as e:
logger.error(f"Error getting display server: {e}")
return "Unknown"
def _get_engine_version(self) -> str:
"""Get jackify-engine version."""
try:
# Try to execute jackify-engine --version
engine_path = Path(__file__).parent.parent.parent.parent / "engine" / "jackify-engine"
if engine_path.exists():
result = subprocess.run([str(engine_path), "--version"],
capture_output=True, text=True, timeout=5)
if result.returncode == 0:
version = result.stdout.strip()
# Extract just the version number (before the +commit hash)
if '+' in version:
version = version.split('+')[0]
return f"v{version}"
return "Unknown"
except Exception as e:
logger.error(f"Error getting engine version: {e}")
return "Unknown"
def check_for_updates(self):
"""Check for updates in background."""
if self.update_check_thread and self.update_check_thread.isRunning():
return
self.update_button.setEnabled(False)
self.update_button.setText("Checking...")
self.update_status_label.setText("Checking for updates...")
self.update_check_thread = UpdateCheckThread(self.update_service)
self.update_check_thread.update_check_finished.connect(self.update_check_finished)
self.update_check_thread.start()
def update_check_finished(self, update_info):
"""Handle update check completion."""
self.update_button.setEnabled(True)
self.update_button.setText("Check for Updates")
if update_info:
self.update_status_label.setText(f"Update available: v{update_info.version}")
self.update_status_label.setStyleSheet("color: #3fd0ea; font-size: 10pt; margin: 5px;")
# Show update dialog
from .update_dialog import UpdateDialog
update_dialog = UpdateDialog(update_info, self.update_service, self)
update_dialog.exec()
else:
self.update_status_label.setText("You're running the latest version")
self.update_status_label.setStyleSheet("color: #666; font-size: 10pt; margin: 5px;")
def copy_system_info(self):
"""Copy system information to clipboard."""
try:
info_text = f"""Jackify v{__version__} (Engine {self._get_engine_version()})
OS: {self._get_os_info()} ({platform.release()})
Desktop: {self._get_desktop_environment()} ({self._get_display_server()})
Python: {platform.python_version()}"""
clipboard = QApplication.clipboard()
clipboard.setText(info_text)
# Briefly update button text
sender = self.sender()
original_text = sender.text()
sender.setText("Copied!")
# Reset button text after delay
from PySide6.QtCore import QTimer
QTimer.singleShot(1000, lambda: sender.setText(original_text))
except Exception as e:
logger.error(f"Error copying system info: {e}")
def open_github(self):
"""Open GitHub repository."""
try:
import webbrowser
webbrowser.open("https://github.com/Omni-guides/Jackify")
except Exception as e:
logger.error(f"Error opening GitHub: {e}")
def open_nexus(self):
"""Open Nexus Mods page."""
try:
import webbrowser
webbrowser.open("https://www.nexusmods.com/site/mods/1427")
except Exception as e:
logger.error(f"Error opening Nexus: {e}")
def closeEvent(self, event):
"""Handle dialog close event."""
if self.update_check_thread and self.update_check_thread.isRunning():
self.update_check_thread.terminate()
self.update_check_thread.wait()
event.accept()

View File

@@ -325,6 +325,29 @@ class SettingsDialog(QDialog):
download_dir_row.addWidget(self.download_dir_edit)
download_dir_row.addWidget(self.download_dir_btn)
dir_layout.addRow(QLabel("Downloads Base Dir:"), download_dir_row)
# Jackify Data Directory
from jackify.shared.paths import get_jackify_data_dir
current_jackify_dir = str(get_jackify_data_dir())
self.jackify_data_dir_edit = QLineEdit(current_jackify_dir)
self.jackify_data_dir_edit.setToolTip("Directory for Jackify data (logs, downloads, temp files). Default: ~/Jackify")
self.jackify_data_dir_btn = QPushButton()
self.jackify_data_dir_btn.setIcon(QIcon.fromTheme("folder-open"))
self.jackify_data_dir_btn.setToolTip("Browse for directory")
self.jackify_data_dir_btn.setFixedWidth(32)
self.jackify_data_dir_btn.clicked.connect(lambda: self._pick_directory(self.jackify_data_dir_edit))
jackify_data_dir_row = QHBoxLayout()
jackify_data_dir_row.addWidget(self.jackify_data_dir_edit)
jackify_data_dir_row.addWidget(self.jackify_data_dir_btn)
# Reset to default button
reset_jackify_dir_btn = QPushButton("Reset")
reset_jackify_dir_btn.setToolTip("Reset to default (~/ Jackify)")
reset_jackify_dir_btn.setFixedWidth(50)
reset_jackify_dir_btn.clicked.connect(lambda: self.jackify_data_dir_edit.setText(str(Path.home() / "Jackify")))
jackify_data_dir_row.addWidget(reset_jackify_dir_btn)
dir_layout.addRow(QLabel("Jackify Data Dir:"), jackify_data_dir_row)
main_layout.addWidget(dir_group)
main_layout.addSpacing(12)
@@ -464,7 +487,14 @@ class SettingsDialog(QDialog):
# Save modlist base dirs
self.config_handler.set("modlist_install_base_dir", self.install_dir_edit.text().strip())
self.config_handler.set("modlist_downloads_base_dir", self.download_dir_edit.text().strip())
# Save jackify data directory (always store actual path, never None)
jackify_data_dir = self.jackify_data_dir_edit.text().strip()
self.config_handler.set("jackify_data_dir", jackify_data_dir)
self.config_handler.save_config()
# Refresh cached paths in GUI screens if Jackify directory changed
self._refresh_gui_paths()
# Check if debug mode changed and prompt for restart
new_debug_mode = self.debug_checkbox.isChecked()
if new_debug_mode != self._original_debug_mode:
@@ -484,6 +514,29 @@ class SettingsDialog(QDialog):
MessageService.information(self, "Settings Saved", "Settings have been saved successfully.", safety_level="low")
self.accept()
def _refresh_gui_paths(self):
"""Refresh cached paths in all GUI screens."""
try:
# Get the main window through parent relationship
main_window = self.parent()
if not main_window or not hasattr(main_window, 'stacked_widget'):
return
# Refresh paths in all screens that have the method
screens_to_refresh = [
getattr(main_window, 'install_modlist_screen', None),
getattr(main_window, 'configure_new_modlist_screen', None),
getattr(main_window, 'configure_existing_modlist_screen', None),
getattr(main_window, 'tuxborn_screen', None),
]
for screen in screens_to_refresh:
if screen and hasattr(screen, 'refresh_paths'):
screen.refresh_paths()
except Exception as e:
print(f"Warning: Could not refresh GUI paths: {e}")
def _bold_label(self, text):
label = QLabel(text)
label.setStyleSheet("font-weight: bold; color: #fff;")
@@ -655,7 +708,7 @@ class JackifyMainWindow(QMainWindow):
# Spacer
bottom_bar_layout.addStretch(1)
# Settings button (right)
# Settings button (right side)
settings_btn = QLabel('<a href="#" style="color:#6cf; text-decoration:none;">Settings</a>')
settings_btn.setStyleSheet("color: #6cf; font-size: 13px; padding-right: 8px;")
settings_btn.setTextInteractionFlags(Qt.TextBrowserInteraction)
@@ -663,6 +716,14 @@ class JackifyMainWindow(QMainWindow):
settings_btn.linkActivated.connect(self.open_settings_dialog)
bottom_bar_layout.addWidget(settings_btn, alignment=Qt.AlignRight)
# About button (right side)
about_btn = QLabel('<a href="#" style="color:#6cf; text-decoration:none;">About</a>')
about_btn.setStyleSheet("color: #6cf; font-size: 13px; padding-right: 8px;")
about_btn.setTextInteractionFlags(Qt.TextBrowserInteraction)
about_btn.setOpenExternalLinks(False)
about_btn.linkActivated.connect(self.open_about_dialog)
bottom_bar_layout.addWidget(about_btn, alignment=Qt.AlignRight)
# --- Main Layout ---
central_widget = QWidget()
main_layout = QVBoxLayout()
@@ -808,6 +869,16 @@ class JackifyMainWindow(QMainWindow):
import traceback
traceback.print_exc()
def open_about_dialog(self):
try:
from jackify.frontends.gui.dialogs.about_dialog import AboutDialog
dlg = AboutDialog(self.system_info, self)
dlg.exec()
except Exception as e:
print(f"[ERROR] Exception in open_about_dialog: {e}")
import traceback
traceback.print_exc()
def resource_path(relative_path):
if hasattr(sys, '_MEIPASS'):

View File

@@ -34,8 +34,7 @@ class ConfigureExistingModlistScreen(QWidget):
self.stacked_widget = stacked_widget
self.main_menu_index = main_menu_index
self.debug = DEBUG_BORDERS
self.modlist_log_path = os.path.expanduser('~/Jackify/logs/Configure_Existing_Modlist_workflow.log')
os.makedirs(os.path.dirname(self.modlist_log_path), exist_ok=True)
self.refresh_paths()
# --- Detect Steam Deck ---
steamdeck = os.path.exists('/etc/os-release') and 'steamdeck' in open('/etc/os-release').read().lower()
@@ -297,6 +296,41 @@ class ConfigureExistingModlistScreen(QWidget):
# Time tracking for workflow completion
self._workflow_start_time = None
# Initialize empty controls list - will be populated after UI is built
self._actionable_controls = []
# Now collect all actionable controls after UI is fully built
self._collect_actionable_controls()
def _collect_actionable_controls(self):
"""Collect all actionable controls that should be disabled during operations (except Cancel)"""
self._actionable_controls = [
# Main action button
self.start_btn,
# Form fields
self.shortcut_combo,
# Resolution controls
self.resolution_combo,
]
def _disable_controls_during_operation(self):
"""Disable all actionable controls during configure operations (except Cancel)"""
for control in self._actionable_controls:
if control:
control.setEnabled(False)
def _enable_controls_after_operation(self):
"""Re-enable all actionable controls after configure operations complete"""
for control in self._actionable_controls:
if control:
control.setEnabled(True)
def refresh_paths(self):
"""Refresh cached paths when config changes."""
from jackify.shared.paths import get_jackify_logs_dir
self.modlist_log_path = get_jackify_logs_dir() / 'Configure_Existing_Modlist_workflow.log'
os.makedirs(os.path.dirname(self.modlist_log_path), exist_ok=True)
def resizeEvent(self, event):
"""Handle window resize to prioritize form over console"""
@@ -382,17 +416,22 @@ class ConfigureExistingModlistScreen(QWidget):
log_handler = LoggingHandler()
log_handler.rotate_log_file_per_run(Path(self.modlist_log_path), backup_count=5)
# Disable controls during configuration
self._disable_controls_during_operation()
# Get selected shortcut
idx = self.shortcut_combo.currentIndex() - 1 # Account for 'Please Select...'
from jackify.frontends.gui.services.message_service import MessageService
if idx < 0 or idx >= len(self.shortcut_map):
MessageService.critical(self, "No Shortcut Selected", "Please select a ModOrganizer.exe Steam shortcut to configure.", safety_level="medium")
self._enable_controls_after_operation()
return
shortcut = self.shortcut_map[idx]
modlist_name = shortcut.get('AppName', '')
install_dir = shortcut.get('StartDir', '')
if not modlist_name or not install_dir:
MessageService.critical(self, "Invalid Shortcut", "The selected shortcut is missing required information.", safety_level="medium")
self._enable_controls_after_operation()
return
resolution = self.resolution_combo.currentText()
# Handle resolution saving
@@ -505,6 +544,9 @@ class ConfigureExistingModlistScreen(QWidget):
def on_configuration_complete(self, success, message, modlist_name):
"""Handle configuration completion"""
# Re-enable all controls when workflow completes
self._enable_controls_after_operation()
if success:
# Calculate time taken
time_taken = self._calculate_time_taken()
@@ -525,6 +567,9 @@ class ConfigureExistingModlistScreen(QWidget):
def on_configuration_error(self, error_message):
"""Handle configuration error"""
# Re-enable all controls on error
self._enable_controls_after_operation()
self._safe_append_text(f"Configuration error: {error_message}")
MessageService.critical(self, "Configuration Error", f"Configuration failed: {error_message}", safety_level="medium")
@@ -559,8 +604,8 @@ class ConfigureExistingModlistScreen(QWidget):
if self.config_process and self.config_process.state() == QProcess.Running:
self.config_process.terminate()
self.config_process.waitForFinished(2000)
# Reset button states
self.start_btn.setEnabled(True)
# Re-enable all controls
self._enable_controls_after_operation()
self.cancel_btn.setVisible(True)
def show_next_steps_dialog(self, message):

View File

@@ -1,7 +1,7 @@
"""
ConfigureNewModlistScreen for Jackify GUI
"""
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QComboBox, QHBoxLayout, QLineEdit, QPushButton, QGridLayout, QFileDialog, QTextEdit, QSizePolicy, QTabWidget, QDialog, QListWidget, QListWidgetItem, QMessageBox, QProgressDialog
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QComboBox, QHBoxLayout, QLineEdit, QPushButton, QGridLayout, QFileDialog, QTextEdit, QSizePolicy, QTabWidget, QDialog, QListWidget, QListWidgetItem, QMessageBox, QProgressDialog, QCheckBox
from PySide6.QtCore import Qt, QSize, QThread, Signal, QTimer, QProcess, QMetaObject
from PySide6.QtGui import QPixmap, QTextCursor
from ..shared_theme import JACKIFY_COLOR_BLUE, DEBUG_BORDERS
@@ -106,8 +106,7 @@ class ConfigureNewModlistScreen(QWidget):
self.protontricks_service = ProtontricksDetectionService()
# Path for workflow log
self.modlist_log_path = os.path.expanduser('~/Jackify/logs/Configure_New_Modlist_workflow.log')
os.makedirs(os.path.dirname(self.modlist_log_path), exist_ok=True)
self.refresh_paths()
# Scroll tracking for professional auto-scroll behavior
self._user_manually_scrolled = False
@@ -211,7 +210,6 @@ class ConfigureNewModlistScreen(QWidget):
"7680x4320"
])
form_grid.addWidget(resolution_label, 2, 0, alignment=Qt.AlignLeft | Qt.AlignVCenter)
form_grid.addWidget(self.resolution_combo, 2, 1)
# Load saved resolution if available
saved_resolution = self.resolution_service.get_saved_resolution()
@@ -236,6 +234,27 @@ class ConfigureNewModlistScreen(QWidget):
else:
self.resolution_combo.setCurrentIndex(0)
# Otherwise, default is 'Leave unchanged' (index 0)
# Horizontal layout for resolution dropdown and auto-restart checkbox
resolution_and_restart_layout = QHBoxLayout()
resolution_and_restart_layout.setSpacing(12)
# Resolution dropdown (made smaller)
self.resolution_combo.setMaximumWidth(280) # Constrain width but keep aesthetically pleasing
resolution_and_restart_layout.addWidget(self.resolution_combo)
# Add stretch to push checkbox to the right
resolution_and_restart_layout.addStretch()
# Auto-accept Steam restart checkbox (right-aligned)
self.auto_restart_checkbox = QCheckBox("Auto-accept Steam restart")
self.auto_restart_checkbox.setChecked(False) # Always default to unchecked per session
self.auto_restart_checkbox.setToolTip("When checked, Steam restart dialog will be automatically accepted, allowing unattended configuration")
resolution_and_restart_layout.addWidget(self.auto_restart_checkbox)
# Update the form grid to use the combined layout
form_grid.addLayout(resolution_and_restart_layout, 2, 1)
form_section_widget = QWidget()
form_section_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
form_section_widget.setLayout(form_grid)
@@ -338,6 +357,44 @@ class ConfigureNewModlistScreen(QWidget):
self.start_btn.clicked.connect(self.validate_and_start_configure)
# --- Connect steam_restart_finished signal ---
self.steam_restart_finished.connect(self._on_steam_restart_finished)
# Initialize empty controls list - will be populated after UI is built
self._actionable_controls = []
# Now collect all actionable controls after UI is fully built
self._collect_actionable_controls()
def _collect_actionable_controls(self):
"""Collect all actionable controls that should be disabled during operations (except Cancel)"""
self._actionable_controls = [
# Main action button
self.start_btn,
# Form fields
self.modlist_name_edit,
self.install_dir_edit,
# Resolution controls
self.resolution_combo,
# Checkboxes
self.auto_restart_checkbox,
]
def _disable_controls_during_operation(self):
"""Disable all actionable controls during configure operations (except Cancel)"""
for control in self._actionable_controls:
if control:
control.setEnabled(False)
def _enable_controls_after_operation(self):
"""Re-enable all actionable controls after configure operations complete"""
for control in self._actionable_controls:
if control:
control.setEnabled(True)
def refresh_paths(self):
"""Refresh cached paths when config changes."""
from jackify.shared.paths import get_jackify_logs_dir
self.modlist_log_path = get_jackify_logs_dir() / 'Configure_New_Modlist_workflow.log'
os.makedirs(os.path.dirname(self.modlist_log_path), exist_ok=True)
def resizeEvent(self, event):
"""Handle window resize to prioritize form over console"""
@@ -522,23 +579,38 @@ class ConfigureNewModlistScreen(QWidget):
# Start time tracking
self._workflow_start_time = time.time()
# Disable controls during configuration (after validation passes)
self._disable_controls_during_operation()
# Validate modlist name
modlist_name = self.modlist_name_edit.text().strip()
if not modlist_name:
MessageService.warning(self, "Missing Name", "Please specify a name for your modlist", safety_level="low")
self._enable_controls_after_operation()
return
# --- Shortcut creation will be handled by automated workflow ---
from jackify.backend.handlers.shortcut_handler import ShortcutHandler
steamdeck = os.path.exists('/etc/os-release') and 'steamdeck' in open('/etc/os-release').read().lower()
shortcut_handler = ShortcutHandler(steamdeck=steamdeck) # Still needed for Steam restart
# --- User confirmation before restarting Steam ---
reply = MessageService.question(
self, "Ready to Configure Modlist",
"Would you like to restart Steam and begin post-install configuration now? Restarting Steam could close any games you have open!",
safety_level="medium"
)
print(f"DEBUG: Steam restart dialog returned: {reply!r}")
# Check if auto-restart is enabled
auto_restart_enabled = hasattr(self, 'auto_restart_checkbox') and self.auto_restart_checkbox.isChecked()
if auto_restart_enabled:
# Auto-accept Steam restart - proceed without dialog
self._safe_append_text("Auto-accepting Steam restart (unattended mode enabled)")
reply = QMessageBox.Yes # Simulate user clicking Yes
else:
# --- User confirmation before restarting Steam ---
reply = MessageService.question(
self, "Ready to Configure Modlist",
"Would you like to restart Steam and begin post-install configuration now? Restarting Steam could close any games you have open!",
safety_level="medium"
)
debug_print(f"DEBUG: Steam restart dialog returned: {reply!r}")
if reply not in (QMessageBox.Yes, QMessageBox.Ok, QMessageBox.AcceptRole):
self._enable_controls_after_operation()
if self.stacked_widget:
self.stacked_widget.setCurrentIndex(0)
return
@@ -562,7 +634,6 @@ class ConfigureNewModlistScreen(QWidget):
progress.setMinimumDuration(0)
progress.setValue(0)
progress.show()
self.setEnabled(False)
def do_restart():
try:
ok = shortcut_handler.secure_steam_restart()
@@ -579,7 +650,7 @@ class ConfigureNewModlistScreen(QWidget):
if hasattr(self, '_steam_restart_progress'):
self._steam_restart_progress.close()
del self._steam_restart_progress
self.setEnabled(True)
self._enable_controls_after_operation()
if success:
self._safe_append_text("Steam restarted successfully.")
@@ -722,7 +793,7 @@ class ConfigureNewModlistScreen(QWidget):
"""Handle error from the automated prefix workflow"""
self._safe_append_text(f"Error during automated Steam setup: {error_message}")
self._safe_append_text("Please check the logs for details.")
self.start_btn.setEnabled(True)
self._enable_controls_after_operation()
def show_shortcut_conflict_dialog(self, conflicts):
"""Show dialog to resolve shortcut name conflicts"""
@@ -1162,8 +1233,8 @@ class ConfigureNewModlistScreen(QWidget):
def on_configuration_complete(self, success, message, modlist_name):
"""Handle configuration completion (same as Tuxborn)"""
# Always re-enable the start button when workflow completes
self.start_btn.setEnabled(True)
# Re-enable all controls when workflow completes
self._enable_controls_after_operation()
if success:
# Calculate time taken
@@ -1185,8 +1256,8 @@ class ConfigureNewModlistScreen(QWidget):
def on_configuration_error(self, error_message):
"""Handle configuration error"""
# Re-enable the start button on error
self.start_btn.setEnabled(True)
# Re-enable all controls on error
self._enable_controls_after_operation()
self._safe_append_text(f"Configuration error: {error_message}")
MessageService.critical(self, "Configuration Error", f"Configuration failed: {error_message}", safety_level="medium")

View File

@@ -355,9 +355,8 @@ class InstallModlistScreen(QWidget):
self.online_modlists = {} # {game_type: [modlist_dict, ...]}
self.modlist_details = {} # {modlist_name: modlist_dict}
# Path for workflow log
self.modlist_log_path = os.path.expanduser('~/Jackify/logs/Modlist_Install_workflow.log')
os.makedirs(os.path.dirname(self.modlist_log_path), exist_ok=True)
# Initialize log path (can be refreshed via refresh_paths method)
self.refresh_paths()
# Initialize services early
from jackify.backend.services.api_key_service import APIKeyService
@@ -459,11 +458,11 @@ class InstallModlistScreen(QWidget):
file_layout.setContentsMargins(0, 0, 0, 0)
self.file_edit = QLineEdit()
self.file_edit.setMinimumWidth(400)
file_btn = QPushButton("Browse")
file_btn.clicked.connect(self.browse_wabbajack_file)
self.file_btn = QPushButton("Browse")
self.file_btn.clicked.connect(self.browse_wabbajack_file)
file_layout.addWidget(QLabel(".wabbajack File:"))
file_layout.addWidget(self.file_edit)
file_layout.addWidget(file_btn)
file_layout.addWidget(self.file_btn)
self.file_group.setLayout(file_layout)
file_tab_vbox.addWidget(self.file_group)
file_tab.setLayout(file_tab_vbox)
@@ -484,22 +483,22 @@ class InstallModlistScreen(QWidget):
install_dir_label = QLabel("Install Directory:")
self.install_dir_edit = QLineEdit(self.config_handler.get_modlist_install_base_dir())
self.install_dir_edit.setMaximumHeight(25) # Force compact height
browse_install_btn = QPushButton("Browse")
browse_install_btn.clicked.connect(self.browse_install_dir)
self.browse_install_btn = QPushButton("Browse")
self.browse_install_btn.clicked.connect(self.browse_install_dir)
install_dir_hbox = QHBoxLayout()
install_dir_hbox.addWidget(self.install_dir_edit)
install_dir_hbox.addWidget(browse_install_btn)
install_dir_hbox.addWidget(self.browse_install_btn)
form_grid.addWidget(install_dir_label, 1, 0, alignment=Qt.AlignLeft | Qt.AlignVCenter)
form_grid.addLayout(install_dir_hbox, 1, 1)
# Downloads Dir
downloads_dir_label = QLabel("Downloads Directory:")
self.downloads_dir_edit = QLineEdit(self.config_handler.get_modlist_downloads_base_dir())
self.downloads_dir_edit.setMaximumHeight(25) # Force compact height
browse_downloads_btn = QPushButton("Browse")
browse_downloads_btn.clicked.connect(self.browse_downloads_dir)
self.browse_downloads_btn = QPushButton("Browse")
self.browse_downloads_btn.clicked.connect(self.browse_downloads_dir)
downloads_dir_hbox = QHBoxLayout()
downloads_dir_hbox.addWidget(self.downloads_dir_edit)
downloads_dir_hbox.addWidget(browse_downloads_btn)
downloads_dir_hbox.addWidget(self.browse_downloads_btn)
form_grid.addWidget(downloads_dir_label, 2, 0, alignment=Qt.AlignLeft | Qt.AlignVCenter)
form_grid.addLayout(downloads_dir_hbox, 2, 1)
# Nexus API Key
@@ -603,7 +602,25 @@ class InstallModlistScreen(QWidget):
self.resolution_combo.setCurrentIndex(0)
# Otherwise, default is 'Leave unchanged' (index 0)
form_grid.addWidget(resolution_label, 5, 0, alignment=Qt.AlignLeft | Qt.AlignVCenter)
form_grid.addWidget(self.resolution_combo, 5, 1)
# Horizontal layout for resolution dropdown and auto-restart checkbox
resolution_and_restart_layout = QHBoxLayout()
resolution_and_restart_layout.setSpacing(12)
# Resolution dropdown (made smaller)
self.resolution_combo.setMaximumWidth(280) # Constrain width but keep aesthetically pleasing
resolution_and_restart_layout.addWidget(self.resolution_combo)
# Add stretch to push checkbox to the right
resolution_and_restart_layout.addStretch()
# Auto-accept Steam restart checkbox (right-aligned)
self.auto_restart_checkbox = QCheckBox("Auto-accept Steam restart")
self.auto_restart_checkbox.setChecked(False) # Always default to unchecked per session
self.auto_restart_checkbox.setToolTip("When checked, Steam restart dialog will be automatically accepted, allowing unattended installation")
resolution_and_restart_layout.addWidget(self.auto_restart_checkbox)
form_grid.addLayout(resolution_and_restart_layout, 5, 1)
form_section_widget = QWidget()
form_section_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
form_section_widget.setLayout(form_grid)
@@ -723,6 +740,57 @@ class InstallModlistScreen(QWidget):
# Initialize process tracking
self.process = None
# Initialize empty controls list - will be populated after UI is built
self._actionable_controls = []
# Now collect all actionable controls after UI is fully built
self._collect_actionable_controls()
def _collect_actionable_controls(self):
"""Collect all actionable controls that should be disabled during operations (except Cancel)"""
self._actionable_controls = [
# Main action button
self.start_btn,
# Game/modlist selection
self.game_type_btn,
self.modlist_btn,
# Source tabs (entire tab widget)
self.source_tabs,
# Form fields
self.modlist_name_edit,
self.install_dir_edit,
self.downloads_dir_edit,
self.api_key_edit,
self.file_edit,
# Browse buttons
self.browse_install_btn,
self.browse_downloads_btn,
self.file_btn,
# Resolution controls
self.resolution_combo,
# Checkboxes
self.save_api_key_checkbox,
self.auto_restart_checkbox,
]
def _disable_controls_during_operation(self):
"""Disable all actionable controls during install/configure operations (except Cancel)"""
for control in self._actionable_controls:
if control:
control.setEnabled(False)
def _enable_controls_after_operation(self):
"""Re-enable all actionable controls after install/configure operations complete"""
for control in self._actionable_controls:
if control:
control.setEnabled(True)
def refresh_paths(self):
"""Refresh cached paths when config changes."""
from jackify.shared.paths import get_jackify_logs_dir
self.modlist_log_path = get_jackify_logs_dir() / 'Modlist_Install_workflow.log'
os.makedirs(os.path.dirname(self.modlist_log_path), exist_ok=True)
def _open_url_safe(self, url):
"""Safely open URL using subprocess to avoid Qt library conflicts in PyInstaller"""
@@ -1121,6 +1189,9 @@ class InstallModlistScreen(QWidget):
if not self._check_protontricks():
return
# Disable all controls during installation (except Cancel)
self._disable_controls_during_operation()
try:
tab_index = self.source_tabs.currentIndex()
install_mode = 'online'
@@ -1128,12 +1199,14 @@ class InstallModlistScreen(QWidget):
modlist = self.file_edit.text().strip()
if not modlist or not os.path.isfile(modlist) or not modlist.endswith('.wabbajack'):
MessageService.warning(self, "Invalid Modlist", "Please select a valid .wabbajack file.")
self._enable_controls_after_operation()
return
install_mode = 'file'
else:
modlist = self.modlist_btn.text().strip()
if not modlist or modlist in ("Select Modlist", "Fetching modlists...", "No modlists found.", "Error fetching modlists."):
MessageService.warning(self, "Invalid Modlist", "Please select a valid modlist.")
self._enable_controls_after_operation()
return
# For online modlists, use machine_url instead of display name
@@ -1159,6 +1232,7 @@ class InstallModlistScreen(QWidget):
missing_fields.append("Nexus API Key")
if missing_fields:
MessageService.warning(self, "Missing Required Fields", f"Please fill in all required fields before starting the install:\n- " + "\n- ".join(missing_fields))
self._enable_controls_after_operation()
return
validation_handler = ValidationHandler()
from pathlib import Path
@@ -1324,14 +1398,11 @@ class InstallModlistScreen(QWidget):
debug_print(f"DEBUG: Exception in validate_and_start_install: {e}")
import traceback
debug_print(f"DEBUG: Traceback: {traceback.format_exc()}")
# Re-enable the button in case of exception
self.start_btn.setEnabled(True)
# Re-enable all controls after exception
self._enable_controls_after_operation()
self.cancel_btn.setVisible(True)
self.cancel_install_btn.setVisible(False)
# Also re-enable the entire widget
self.setEnabled(True)
debug_print(f"DEBUG: Widget re-enabled in exception handler, widget enabled: {self.isEnabled()}")
print(f"DEBUG: Widget re-enabled in exception handler, widget enabled: {self.isEnabled()}") # Always print
debug_print(f"DEBUG: Controls re-enabled in exception handler")
def run_modlist_installer(self, modlist, install_dir, downloads_dir, api_key, install_mode='online'):
debug_print('DEBUG: run_modlist_installer called - USING THREADED BACKEND WRAPPER')
@@ -1501,12 +1572,21 @@ class InstallModlistScreen(QWidget):
self._safe_append_text(f"\nModlist installation completed successfully.")
self._safe_append_text(f"\nWarning: Post-install configuration skipped for unsupported game: {game_name or game_type}")
else:
# Show the normal install complete dialog for supported games
reply = MessageService.question(
self, "Modlist Install Complete!",
"Modlist install complete!\n\nWould you like to add this modlist to Steam and configure it now? Steam will restart, closing any game you have open!",
critical=False # Non-critical, won't steal focus
)
# Check if auto-restart is enabled
auto_restart_enabled = hasattr(self, 'auto_restart_checkbox') and self.auto_restart_checkbox.isChecked()
if auto_restart_enabled:
# Auto-accept Steam restart - proceed without dialog
self._safe_append_text("\nAuto-accepting Steam restart (unattended mode enabled)")
reply = QMessageBox.Yes # Simulate user clicking Yes
else:
# Show the normal install complete dialog for supported games
reply = MessageService.question(
self, "Modlist Install Complete!",
"Modlist install complete!\n\nWould you like to add this modlist to Steam and configure it now? Steam will restart, closing any game you have open!",
critical=False # Non-critical, won't steal focus
)
if reply == QMessageBox.Yes:
# --- Create Steam shortcut BEFORE restarting Steam ---
# Proceed directly to automated prefix creation
@@ -1522,6 +1602,8 @@ class InstallModlistScreen(QWidget):
"You can manually add the modlist to Steam later if desired.",
safety_level="medium"
)
# Re-enable controls since operation is complete
self._enable_controls_after_operation()
else:
# Check for user cancellation first
last_output = self.console.toPlainText()
@@ -1611,9 +1693,6 @@ class InstallModlistScreen(QWidget):
progress.setMinimumDuration(0)
progress.setValue(0)
progress.show()
self.setEnabled(False)
debug_print(f"DEBUG: Widget disabled in restart_steam_and_configure, widget enabled: {self.isEnabled()}")
print(f"DEBUG: Widget disabled in restart_steam_and_configure, widget enabled: {self.isEnabled()}") # Always print
def do_restart():
debug_print("DEBUG: do_restart thread started - using direct backend service")
@@ -1651,9 +1730,7 @@ class InstallModlistScreen(QWidget):
finally:
self._steam_restart_progress = None
self.setEnabled(True)
debug_print(f"DEBUG: Widget re-enabled in _on_steam_restart_finished, widget enabled: {self.isEnabled()}")
print(f"DEBUG: Widget re-enabled in _on_steam_restart_finished, widget enabled: {self.isEnabled()}") # Always print
# Controls are managed by the proper control management system
if success:
self._safe_append_text("Steam restarted successfully.")
@@ -1676,6 +1753,8 @@ class InstallModlistScreen(QWidget):
def start_automated_prefix_workflow(self):
"""Start the automated prefix creation workflow"""
try:
# Disable controls during installation
self._disable_controls_during_operation()
modlist_name = self.modlist_name_edit.text().strip()
install_dir = self.install_dir_edit.text().strip()
final_exe_path = os.path.join(install_dir, "ModOrganizer.exe")
@@ -1784,33 +1863,43 @@ class InstallModlistScreen(QWidget):
import traceback
debug_print(f"DEBUG: Traceback: {traceback.format_exc()}")
self._safe_append_text(f"ERROR: Failed to start automated workflow: {e}")
# Re-enable controls on exception
self._enable_controls_after_operation()
def on_automated_prefix_finished(self, success, prefix_path, new_appid_str, last_timestamp=None):
"""Handle completion of automated prefix creation"""
if success:
debug_print(f"SUCCESS: Automated prefix creation completed!")
debug_print(f"Prefix created at: {prefix_path}")
if new_appid_str and new_appid_str != "0":
debug_print(f"AppID: {new_appid_str}")
# Convert string AppID back to integer for configuration
new_appid = int(new_appid_str) if new_appid_str and new_appid_str != "0" else None
# Continue with configuration using the new AppID and timestamp
modlist_name = self.modlist_name_edit.text().strip()
install_dir = self.install_dir_edit.text().strip()
self.continue_configuration_after_automated_prefix(new_appid, modlist_name, install_dir, last_timestamp)
else:
self._safe_append_text(f"ERROR: Automated prefix creation failed")
self._safe_append_text("Please check the logs for details")
MessageService.critical(self, "Automated Setup Failed",
"Automated prefix creation failed. Please check the console output for details.")
try:
if success:
debug_print(f"SUCCESS: Automated prefix creation completed!")
debug_print(f"Prefix created at: {prefix_path}")
if new_appid_str and new_appid_str != "0":
debug_print(f"AppID: {new_appid_str}")
# Convert string AppID back to integer for configuration
new_appid = int(new_appid_str) if new_appid_str and new_appid_str != "0" else None
# Continue with configuration using the new AppID and timestamp
modlist_name = self.modlist_name_edit.text().strip()
install_dir = self.install_dir_edit.text().strip()
self.continue_configuration_after_automated_prefix(new_appid, modlist_name, install_dir, last_timestamp)
else:
self._safe_append_text(f"ERROR: Automated prefix creation failed")
self._safe_append_text("Please check the logs for details")
MessageService.critical(self, "Automated Setup Failed",
"Automated prefix creation failed. Please check the console output for details.")
# Re-enable controls on failure
self._enable_controls_after_operation()
finally:
# Always ensure controls are re-enabled when workflow truly completes
pass
def on_automated_prefix_error(self, error_msg):
"""Handle error in automated prefix creation"""
self._safe_append_text(f"ERROR: Error during automated prefix creation: {error_msg}")
MessageService.critical(self, "Automated Setup Error",
f"Error during automated prefix creation: {error_msg}")
# Re-enable controls on error
self._enable_controls_after_operation()
def on_automated_prefix_progress(self, progress_msg):
"""Handle progress updates from automated prefix creation"""
@@ -1831,7 +1920,6 @@ class InstallModlistScreen(QWidget):
self.steam_restart_progress.setMinimumDuration(0)
self.steam_restart_progress.setValue(0)
self.steam_restart_progress.show()
self.setEnabled(False)
def hide_steam_restart_progress(self):
"""Hide Steam restart progress dialog"""
@@ -1843,45 +1931,53 @@ class InstallModlistScreen(QWidget):
pass
finally:
self.steam_restart_progress = None
self.setEnabled(True)
# Controls are managed by the proper control management system
def on_configuration_complete(self, success, message, modlist_name):
"""Handle configuration completion on main thread"""
if success:
# Show celebration SuccessDialog after the entire workflow
from ..dialogs import SuccessDialog
import time
if not hasattr(self, '_install_workflow_start_time'):
self._install_workflow_start_time = time.time()
time_taken = int(time.time() - self._install_workflow_start_time)
mins, secs = divmod(time_taken, 60)
time_str = f"{mins} minutes, {secs} seconds" if mins else f"{secs} seconds"
display_names = {
'skyrim': 'Skyrim',
'fallout4': 'Fallout 4',
'falloutnv': 'Fallout New Vegas',
'oblivion': 'Oblivion',
'starfield': 'Starfield',
'oblivion_remastered': 'Oblivion Remastered',
'enderal': 'Enderal'
}
game_name = display_names.get(self._current_game_type, self._current_game_name)
success_dialog = SuccessDialog(
modlist_name=modlist_name,
workflow_type="install",
time_taken=time_str,
game_name=game_name,
parent=self
)
success_dialog.show()
elif hasattr(self, '_manual_steps_retry_count') and self._manual_steps_retry_count >= 3:
# Max retries reached - show failure message
MessageService.critical(self, "Manual Steps Failed",
"Manual steps validation failed after multiple attempts.")
else:
# Configuration failed for other reasons
MessageService.critical(self, "Configuration Failed",
"Post-install configuration failed. Please check the console output.")
try:
# Re-enable controls now that installation/configuration is complete
self._enable_controls_after_operation()
if success:
# Show celebration SuccessDialog after the entire workflow
from ..dialogs import SuccessDialog
import time
if not hasattr(self, '_install_workflow_start_time'):
self._install_workflow_start_time = time.time()
time_taken = int(time.time() - self._install_workflow_start_time)
mins, secs = divmod(time_taken, 60)
time_str = f"{mins} minutes, {secs} seconds" if mins else f"{secs} seconds"
display_names = {
'skyrim': 'Skyrim',
'fallout4': 'Fallout 4',
'falloutnv': 'Fallout New Vegas',
'oblivion': 'Oblivion',
'starfield': 'Starfield',
'oblivion_remastered': 'Oblivion Remastered',
'enderal': 'Enderal'
}
game_name = display_names.get(self._current_game_type, self._current_game_name)
success_dialog = SuccessDialog(
modlist_name=modlist_name,
workflow_type="install",
time_taken=time_str,
game_name=game_name,
parent=self
)
success_dialog.show()
elif hasattr(self, '_manual_steps_retry_count') and self._manual_steps_retry_count >= 3:
# Max retries reached - show failure message
MessageService.critical(self, "Manual Steps Failed",
"Manual steps validation failed after multiple attempts.")
else:
# Configuration failed for other reasons
MessageService.critical(self, "Configuration Failed",
"Post-install configuration failed. Please check the console output.")
except Exception as e:
# Ensure controls are re-enabled even on unexpected errors
self._enable_controls_after_operation()
raise
# Clean up thread
if hasattr(self, 'config_thread') and self.config_thread is not None:
# Disconnect all signals to prevent "Internal C++ object already deleted" errors
@@ -1940,8 +2036,8 @@ class InstallModlistScreen(QWidget):
else:
# User clicked Cancel or closed the dialog - cancel the workflow
self._safe_append_text("\n🛑 Manual steps cancelled by user. Workflow stopped.")
# Reset button states
self.start_btn.setEnabled(True)
# Re-enable all controls when workflow is cancelled
self._enable_controls_after_operation()
self.cancel_btn.setVisible(True)
self.cancel_install_btn.setVisible(False)
@@ -2513,8 +2609,8 @@ class InstallModlistScreen(QWidget):
# Cleanup any remaining processes
self.cleanup_processes()
# Reset button states
self.start_btn.setEnabled(True)
# Reset button states and re-enable all controls
self._enable_controls_after_operation()
self.cancel_btn.setVisible(True)
self.cancel_install_btn.setVisible(False)

View File

@@ -106,8 +106,7 @@ class TuxbornInstallerScreen(QWidget):
self.modlist_details = {} # {modlist_name: modlist_dict}
# Path for workflow log
self.modlist_log_path = os.path.expanduser('~/Jackify/logs/Tuxborn_Installer_workflow.log')
os.makedirs(os.path.dirname(self.modlist_log_path), exist_ok=True)
self.refresh_paths()
# Initialize services early
from jackify.backend.services.api_key_service import APIKeyService
@@ -440,6 +439,12 @@ class TuxbornInstallerScreen(QWidget):
self.start_btn.clicked.connect(self.validate_and_start_install)
self.steam_restart_finished.connect(self._on_steam_restart_finished)
def refresh_paths(self):
"""Refresh cached paths when config changes."""
from jackify.shared.paths import get_jackify_logs_dir
self.modlist_log_path = get_jackify_logs_dir() / 'Tuxborn_Installer_workflow.log'
os.makedirs(os.path.dirname(self.modlist_log_path), exist_ok=True)
def _open_url_safe(self, url):
"""Safely open URL using subprocess to avoid Qt library conflicts in PyInstaller"""
import subprocess

View File

@@ -14,15 +14,21 @@ import shutil
class LoggingHandler:
"""
Central logging handler for Jackify.
- Uses ~/Jackify/logs/ as the log directory.
- Uses configurable Jackify data directory for logs (default: ~/Jackify/logs/).
- Supports per-function log files (e.g., jackify-install-wabbajack.log).
- Handles log rotation and log directory creation.
Usage:
logger = LoggingHandler().setup_logger('install_wabbajack', 'jackify-install-wabbajack.log')
"""
def __init__(self):
self.log_dir = Path.home() / "Jackify" / "logs"
# Don't cache log_dir - use property to get fresh path each time
self.ensure_log_directory()
@property
def log_dir(self):
"""Get the current log directory (may change if config updated)."""
from jackify.shared.paths import get_jackify_logs_dir
return get_jackify_logs_dir()
def ensure_log_directory(self) -> None:
"""Ensure the log directory exists."""

65
jackify/shared/paths.py Normal file
View File

@@ -0,0 +1,65 @@
"""
Path utilities for Jackify.
This module provides standardized path resolution for Jackify directories,
supporting configurable data directory while keeping config in a fixed location.
"""
import os
from pathlib import Path
from typing import Optional
def get_jackify_data_dir() -> Path:
"""
Get the configurable Jackify data directory.
This directory contains:
- downloaded_mod_lists/
- logs/
- temporary proton prefixes during installation
Returns:
Path: The Jackify data directory (always set in config)
"""
try:
# Import here to avoid circular imports
from jackify.backend.handlers.config_handler import ConfigHandler
config_handler = ConfigHandler()
jackify_data_dir = config_handler.get('jackify_data_dir')
# Config handler now always ensures this is set, but fallback just in case
if jackify_data_dir:
return Path(jackify_data_dir).expanduser()
else:
return Path.home() / "Jackify"
except Exception:
# Emergency fallback if config system fails
return Path.home() / "Jackify"
def get_jackify_logs_dir() -> Path:
"""Get the logs directory within the Jackify data directory."""
return get_jackify_data_dir() / "logs"
def get_jackify_downloads_dir() -> Path:
"""Get the downloaded modlists directory within the Jackify data directory."""
return get_jackify_data_dir() / "downloaded_mod_lists"
def get_jackify_config_dir() -> Path:
"""
Get the Jackify configuration directory (always ~/.config/jackify).
This directory contains:
- config.json (settings)
- API keys and credentials
- Resource settings
Returns:
Path: Always ~/.config/jackify
"""
return Path.home() / ".config" / "jackify"

View File

@@ -51,9 +51,15 @@ def _clear_screen_fallback():
def print_jackify_banner():
"""Print the Jackify application banner"""
print("""
from jackify import __version__
version_text = f"Jackify CLI ({__version__})"
# Center the version text in the banner (72 chars content width)
padding = (72 - len(version_text)) // 2
centered_version = " " * padding + version_text + " " * (72 - len(version_text) - padding)
print(f"""
╔════════════════════════════════════════════════════════════════════════╗
Jackify CLI (pre-alpha)
{centered_version}
║ ║
║ A tool for installing and configuring modlists ║
║ & associated utilities on Linux ║