mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-01-17 19:47:00 +01:00
Initial public release v0.1.0 - Linux Wabbajack Modlist Application
Jackify provides native Linux support for Wabbajack modlist installation and management with automated Steam integration and Proton configuration. Key Features: - Almost Native Linux implementation (texconv.exe run via proton) - Automated Steam shortcut creation and Proton prefix management - Both CLI and GUI interfaces, with Steam Deck optimization Supported Games: - Skyrim Special Edition - Fallout 4 - Fallout New Vegas - Oblivion, Starfield, Enderal, and diverse other games Technical Architecture: - Clean separation between frontend and backend services - Powered by jackify-engine 0.3.x for Wabbajack-matching modlist installation
This commit is contained in:
441
jackify/frontends/cli/main.py
Executable file
441
jackify/frontends/cli/main.py
Executable file
@@ -0,0 +1,441 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user