mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-01-17 19:47:00 +01:00
Jackify provides native Linux support for Wabbajack modlist installation and management with automated Steam integration and Proton configuration. Key Features: - Almost Native Linux implementation (texconv.exe run via proton) - Automated Steam shortcut creation and Proton prefix management - Both CLI and GUI interfaces, with Steam Deck optimization Supported Games: - Skyrim Special Edition - Fallout 4 - Fallout New Vegas - Oblivion, Starfield, Enderal, and diverse other games Technical Architecture: - Clean separation between frontend and backend services - Powered by jackify-engine 0.3.x for Wabbajack-matching modlist installation
442 lines
19 KiB
Python
Executable File
442 lines
19 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Jackify CLI Frontend - Main Entry Point
|
|
|
|
Command-line interface for Jackify that uses the backend services.
|
|
Extracted and refactored from the original jackify-cli.py.
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import argparse
|
|
import logging
|
|
|
|
# Import from our new backend structure
|
|
from jackify.backend.models.configuration import SystemInfo
|
|
from jackify.backend.services.modlist_service import ModlistService
|
|
from jackify.shared.colors import COLOR_INFO, COLOR_ERROR, COLOR_RESET
|
|
from jackify import __version__ as jackify_version
|
|
|
|
# Import our command handlers
|
|
from .commands.configure_modlist import ConfigureModlistCommand
|
|
from .commands.install_modlist import InstallModlistCommand
|
|
from .commands.tuxborn import TuxbornCommand
|
|
|
|
# Import our menu handlers
|
|
from .menus.main_menu import MainMenuHandler
|
|
from .menus.tuxborn_menu import TuxbornMenuHandler
|
|
from .menus.wabbajack_menu import WabbajackMenuHandler
|
|
from .menus.hoolamike_menu import HoolamikeMenuHandler
|
|
from .menus.additional_menu import AdditionalMenuHandler
|
|
|
|
# Import backend handlers for legacy compatibility
|
|
from jackify.backend.handlers.config_handler import ConfigHandler
|
|
from jackify.backend.handlers.filesystem_handler import FileSystemHandler
|
|
from jackify.backend.handlers.path_handler import PathHandler
|
|
from jackify.backend.handlers.shortcut_handler import ShortcutHandler
|
|
from jackify.backend.handlers.menu_handler import MenuHandler
|
|
from jackify.backend.handlers.mo2_handler import MO2Handler
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class JackifyCLI:
|
|
"""Main application class for Jackify CLI Frontend"""
|
|
|
|
def __init__(self, test_mode=False, dev_mode=False):
|
|
"""Initialize the JackifyCLI frontend.
|
|
|
|
Args:
|
|
test_mode (bool): If True, run in test mode with minimal side effects
|
|
dev_mode (bool): If True, enable development features
|
|
"""
|
|
# Initialize early (debug flag not yet available)
|
|
self._debug_mode = False
|
|
|
|
# Set test mode flag
|
|
self.test_mode = test_mode
|
|
self.dev_mode = dev_mode
|
|
self.verbose = False
|
|
|
|
# Configure logging to be quiet by default - will be adjusted after arg parsing
|
|
self._configure_logging_early()
|
|
|
|
# Determine system info
|
|
self.system_info = SystemInfo(is_steamdeck=self._is_steamdeck())
|
|
|
|
# Apply resource limits for optimal operation
|
|
self._apply_resource_limits()
|
|
|
|
# Initialize backend services
|
|
self.backend_services = self._initialize_backend_services()
|
|
|
|
# Initialize command handlers
|
|
self.commands = self._initialize_command_handlers()
|
|
|
|
# Initialize menu handlers with dev_mode
|
|
self.menus = self._initialize_menu_handlers()
|
|
|
|
# Initialize legacy compatibility attributes for menu bridge
|
|
self._initialize_legacy_compatibility()
|
|
|
|
# Initialize state variables
|
|
self.parser = None
|
|
self.subparsers = None
|
|
self.args = None
|
|
self.selected_modlist = None
|
|
self.setup_complete = False
|
|
|
|
def _debug_print(self, message):
|
|
"""Print debug message only if debug mode is enabled"""
|
|
if hasattr(self, '_debug_mode') and self._debug_mode:
|
|
logger.debug(message)
|
|
|
|
def _configure_logging_early(self):
|
|
"""Configure logging to be quiet during initialization, will be adjusted after arg parsing"""
|
|
# Set root logger to WARNING level initially to suppress INFO messages during init
|
|
logging.getLogger().setLevel(logging.WARNING)
|
|
|
|
# Configure basic logging format
|
|
if not logging.getLogger().handlers:
|
|
handler = logging.StreamHandler()
|
|
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
handler.setFormatter(formatter)
|
|
logging.getLogger().addHandler(handler)
|
|
|
|
def _configure_logging_final(self):
|
|
"""Configure final logging level based on parsed arguments"""
|
|
# Use the existing LoggingHandler for proper log rotation
|
|
from jackify.backend.handlers.logging_handler import LoggingHandler
|
|
|
|
# Set up CLI-specific logging with rotation
|
|
logging_handler = LoggingHandler()
|
|
logging_handler.rotate_log_for_logger('jackify-cli', 'Modlist_Install_workflow_cli.log')
|
|
cli_logger = logging_handler.setup_logger('jackify-cli', 'Modlist_Install_workflow_cli.log')
|
|
|
|
# Configure logging level
|
|
if self.args.debug:
|
|
cli_logger.setLevel(logging.DEBUG)
|
|
print("Debug logging enabled for console and file")
|
|
elif self.args.verbose:
|
|
cli_logger.setLevel(logging.INFO)
|
|
print("Verbose logging enabled for console and file")
|
|
else:
|
|
# Keep it at WARNING level for clean startup
|
|
cli_logger.setLevel(logging.WARNING)
|
|
|
|
def _is_steamdeck(self):
|
|
"""Check if running on Steam Deck"""
|
|
try:
|
|
if os.path.exists("/etc/os-release"):
|
|
with open("/etc/os-release", "r") as f:
|
|
content = f.read()
|
|
if "steamdeck" in content:
|
|
logger.info("Running on Steam Deck")
|
|
return True
|
|
logger.info("Not running on Steam Deck")
|
|
return False
|
|
except Exception as e:
|
|
logger.error(f"Error detecting Steam Deck: {e}")
|
|
return False
|
|
|
|
def _apply_resource_limits(self):
|
|
"""Apply recommended resource limits for optimal Jackify operation"""
|
|
try:
|
|
from jackify.backend.services.resource_manager import ResourceManager
|
|
|
|
resource_manager = ResourceManager()
|
|
success = resource_manager.apply_recommended_limits()
|
|
|
|
if success:
|
|
status = resource_manager.get_limit_status()
|
|
if status['target_achieved']:
|
|
logger.info(f"Resource limits optimized: file descriptors set to {status['current_soft']}")
|
|
else:
|
|
logger.info(f"Resource limits improved: file descriptors increased to {status['current_soft']} (target: {status['target_limit']})")
|
|
else:
|
|
# Log the issue but don't block startup
|
|
status = resource_manager.get_limit_status()
|
|
logger.warning(f"Could not optimize resource limits: current file descriptors={status['current_soft']}, target={status['target_limit']}")
|
|
|
|
# If we can't increase automatically, provide manual instructions in debug mode
|
|
if hasattr(self, '_debug_mode') and self._debug_mode:
|
|
instructions = resource_manager.get_manual_increase_instructions()
|
|
logger.debug(f"Manual increase instructions available for {instructions['distribution']}")
|
|
|
|
except Exception as e:
|
|
# Don't block startup on resource management errors
|
|
logger.warning(f"Error applying resource limits: {e}")
|
|
|
|
def _initialize_backend_services(self):
|
|
"""Initialize backend services.
|
|
|
|
Returns:
|
|
Dictionary of backend service instances
|
|
"""
|
|
# For now, create a basic modlist service
|
|
# TODO: Add other services as needed
|
|
services = {
|
|
'modlist_service': ModlistService(self.system_info)
|
|
}
|
|
return services
|
|
|
|
def _initialize_command_handlers(self):
|
|
"""Initialize command handler instances.
|
|
|
|
Returns:
|
|
Dictionary of command handler instances
|
|
"""
|
|
commands = {
|
|
'configure_modlist': ConfigureModlistCommand(self.backend_services),
|
|
'install_modlist': InstallModlistCommand(self.backend_services, self.system_info),
|
|
'tuxborn': TuxbornCommand(self.backend_services, self.system_info)
|
|
}
|
|
return commands
|
|
|
|
def _initialize_menu_handlers(self):
|
|
"""Initialize menu handler instances.
|
|
|
|
Returns:
|
|
Dictionary of menu handler instances
|
|
"""
|
|
menus = {
|
|
'main': MainMenuHandler(dev_mode=getattr(self, 'dev_mode', False)),
|
|
'tuxborn': TuxbornMenuHandler(),
|
|
'wabbajack': WabbajackMenuHandler(),
|
|
'hoolamike': HoolamikeMenuHandler(),
|
|
'additional': AdditionalMenuHandler()
|
|
}
|
|
|
|
# Set up logging for menu handlers
|
|
for menu in menus.values():
|
|
menu.logger = logger
|
|
|
|
return menus
|
|
|
|
def _initialize_legacy_compatibility(self):
|
|
"""
|
|
Initialize legacy compatibility attributes for menu bridge.
|
|
|
|
This provides the legacy attributes that menu handlers expect from cli_instance
|
|
until the backend migration is complete.
|
|
"""
|
|
# LEGACY BRIDGE: Add legacy imports to access original handlers
|
|
# Backend handlers are now imported directly from backend package
|
|
|
|
try:
|
|
# Initialize legacy handlers for compatibility
|
|
self.config_handler = ConfigHandler()
|
|
self.filesystem_handler = FileSystemHandler()
|
|
self.path_handler = PathHandler()
|
|
self.shortcut_handler = ShortcutHandler(self.config_handler.settings)
|
|
self.menu = MenuHandler() # Original menu handler for fallback
|
|
self.menu_handler = self.menu # Alias for backend compatibility
|
|
|
|
# Add MO2 handler to the menu handler for additional tasks menu
|
|
self.menu.mo2_handler = MO2Handler(self.menu)
|
|
|
|
# Set steamdeck attribute that menus expect
|
|
self.steamdeck = self.system_info.is_steamdeck
|
|
|
|
# Initialize settings that legacy code expects
|
|
if not hasattr(self.config_handler, 'settings'):
|
|
self.config_handler.settings = {}
|
|
self.config_handler.settings['steamdeck'] = self.steamdeck
|
|
|
|
logger.info("Legacy compatibility layer initialized successfully")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to initialize legacy compatibility layer: {e}")
|
|
# Continue anyway - some functionality might still work
|
|
self.config_handler = None
|
|
self.filesystem_handler = None
|
|
self.path_handler = None
|
|
self.shortcut_handler = None
|
|
self.menu = None
|
|
self.steamdeck = self.system_info.is_steamdeck
|
|
|
|
def run(self):
|
|
self.parser, self.subparsers, self.args = self._parse_args()
|
|
self._debug_mode = self.args.debug
|
|
self.verbose = self.args.verbose or self.args.debug
|
|
self.dev_mode = getattr(self.args, 'dev', False)
|
|
# Re-initialize menus with dev_mode after parsing args
|
|
self.menus = self._initialize_menu_handlers()
|
|
|
|
# Now that we have args, configure logging properly
|
|
self._configure_logging_final()
|
|
|
|
self._debug_print('Initializing Jackify CLI Frontend')
|
|
self._debug_print('JackifyCLI.run() called')
|
|
self._debug_print(f'Parsed args: {self.args}')
|
|
|
|
# Handle legacy restart-steam functionality (temporary)
|
|
if getattr(self.args, 'restart_steam', False):
|
|
self._debug_print('Entering restart_steam workflow')
|
|
return self._handle_restart_steam()
|
|
|
|
# Handle Tuxborn auto mode
|
|
if getattr(self.args, 'tuxborn_auto', False):
|
|
self._debug_print('Entering Tuxborn workflow')
|
|
return self.commands['tuxborn'].execute(self.args)
|
|
|
|
# Handle install-modlist top-level functionality
|
|
if getattr(self.args, 'install_modlist', False):
|
|
self._debug_print('Entering install_modlist workflow')
|
|
return self.commands['install_modlist'].execute_top_level(self.args)
|
|
|
|
# Handle subcommands
|
|
if getattr(self.args, 'command', None):
|
|
return self._run_command(self.args.command, self.args)
|
|
|
|
# Run interactive mode (legacy for now)
|
|
self._run_interactive()
|
|
|
|
def _parse_args(self):
|
|
"""Parse command-line arguments using command handlers"""
|
|
parser = argparse.ArgumentParser(description="Jackify: Wabbajack Modlist Manager for Linux/Steam Deck")
|
|
parser.add_argument("-V", "--version", action="store_true", help="Show Jackify version and exit")
|
|
parser.add_argument("-d", "--debug", action="store_true", help="Enable debug logging (implies verbose)")
|
|
parser.add_argument("-v", "--verbose", action="store_true", help="Enable informational console output")
|
|
parser.add_argument("--cli", action="store_true", help="Run in CLI mode (default if no GUI available)")
|
|
parser.add_argument("--resolution", type=str, help="Resolution to set (optional)")
|
|
parser.add_argument('--restart-steam', action='store_true', help='Restart Steam (native, for GUI integration)')
|
|
parser.add_argument('--dev', action='store_true', help='Enable development features (show hidden menu items)')
|
|
|
|
# Add command-specific arguments
|
|
self.commands['tuxborn'].add_args(parser)
|
|
self.commands['install_modlist'].add_top_level_args(parser)
|
|
|
|
# Add subcommands
|
|
subparsers = parser.add_subparsers(dest="command", help="Command to run")
|
|
self.commands['configure_modlist'].add_parser(subparsers)
|
|
self.commands['install_modlist'].add_parser(subparsers)
|
|
|
|
args = parser.parse_args()
|
|
if args.version:
|
|
print(f"Jackify version {jackify_version}")
|
|
sys.exit(0)
|
|
|
|
return parser, subparsers, args
|
|
|
|
def _run_command(self, command, args):
|
|
"""Run a specific command using command handlers"""
|
|
if command == "install-modlist":
|
|
return self.commands['install_modlist'].execute_subcommand(args)
|
|
elif command == "configure-modlist":
|
|
return self.commands['configure_modlist'].execute(args)
|
|
elif command == "install-wabbajack":
|
|
# Legacy functionality - TODO: extract to command handler
|
|
return self._handle_legacy_install_wabbajack()
|
|
elif command == "hoolamike":
|
|
# Legacy functionality - TODO: extract to command handler
|
|
return self._handle_legacy_hoolamike()
|
|
elif command == "install-mo2":
|
|
print("MO2 installation not yet implemented")
|
|
print("This functionality is coming soon!")
|
|
return 1
|
|
elif command == "configure-nxm":
|
|
print("NXM configuration not yet implemented")
|
|
print("This functionality is coming soon!")
|
|
return 1
|
|
elif command == "recovery":
|
|
return self._handle_legacy_recovery(args)
|
|
elif command == "test-protontricks":
|
|
return self._handle_legacy_protontricks_test()
|
|
else:
|
|
print(f"Unknown command: {command}")
|
|
return 1
|
|
|
|
def _run_interactive(self):
|
|
"""Run the CLI interface interactively using the new menu system"""
|
|
try:
|
|
while True:
|
|
# Show main menu and get user's choice
|
|
choice = self.menus['main'].show_main_menu(self)
|
|
|
|
if choice == "exit":
|
|
print(f"{COLOR_INFO}Thank you for using Jackify!{COLOR_RESET}")
|
|
return 0
|
|
elif choice == "wabbajack":
|
|
self.menus['wabbajack'].show_wabbajack_tasks_menu(self)
|
|
elif choice == "tuxborn":
|
|
self.menus['tuxborn'].show_tuxborn_installer_menu(self)
|
|
# HIDDEN FOR FIRST RELEASE - UNCOMMENT WHEN READY
|
|
# elif choice == "hoolamike":
|
|
# self.menus['hoolamike'].show_hoolamike_menu(self)
|
|
# elif choice == "additional":
|
|
# self.menus['additional'].show_additional_tasks_menu(self)
|
|
else:
|
|
logger.warning(f"Invalid choice '{choice}' received from show_main_menu.")
|
|
|
|
except KeyboardInterrupt:
|
|
print(f"\n{COLOR_INFO}Exiting Jackify...{COLOR_RESET}")
|
|
return 0
|
|
except Exception as e:
|
|
logger.error(f"Error in interactive mode: {e}")
|
|
print(f"{COLOR_ERROR}An error occurred: {e}{COLOR_RESET}")
|
|
return 1
|
|
|
|
def _handle_restart_steam(self):
|
|
"""Handle restart-steam command - now properly implemented"""
|
|
print("[Jackify] Attempting to restart Steam...")
|
|
logger.debug("About to call secure_steam_restart()")
|
|
|
|
try:
|
|
# Use the already initialized shortcut_handler
|
|
if self.shortcut_handler:
|
|
success = self.shortcut_handler.secure_steam_restart()
|
|
logger.debug(f"secure_steam_restart() returned: {success}")
|
|
|
|
if success:
|
|
print("[Jackify] Steam restart completed successfully.")
|
|
return 0
|
|
else:
|
|
print("[Jackify] Failed to restart Steam.")
|
|
return 1
|
|
else:
|
|
print("[Jackify] ERROR: ShortcutHandler not initialized")
|
|
return 1
|
|
|
|
except Exception as e:
|
|
print(f"[Jackify] ERROR: Exception during Steam restart: {e}")
|
|
logger.error(f"Steam restart failed with exception: {e}")
|
|
return 1
|
|
|
|
def _handle_legacy_install_wabbajack(self):
|
|
"""Handle install-wabbajack command (legacy functionality)"""
|
|
print("Install Wabbajack functionality not yet migrated to new structure")
|
|
return 1
|
|
|
|
def _handle_legacy_hoolamike(self):
|
|
"""Handle hoolamike command (legacy functionality)"""
|
|
print("Hoolamike functionality not yet migrated to new structure")
|
|
return 1
|
|
|
|
def _handle_legacy_recovery(self, args):
|
|
"""Handle recovery command (legacy functionality)"""
|
|
print("Recovery functionality not yet migrated to new structure")
|
|
return 1
|
|
|
|
def _handle_legacy_protontricks_test(self):
|
|
"""Handle test-protontricks command (legacy functionality)"""
|
|
print("Protontricks test functionality not yet migrated to new structure")
|
|
return 1
|
|
|
|
# LEGACY BRIDGE: Methods that menu handlers expect to find on cli_instance
|
|
def _cmd_install_wabbajack(self, args):
|
|
"""LEGACY BRIDGE: Install Wabbajack application"""
|
|
return self._handle_legacy_install_wabbajack()
|
|
|
|
|
|
def main():
|
|
"""Legacy main function (not used in new structure)"""
|
|
pass
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# This should not be called directly - use __main__.py instead
|
|
print("Please use: python -m jackify.frontends.cli")
|
|
sys.exit(1)
|