Files
Jackify/jackify/frontends/cli/main.py
2025-09-14 21:54:18 +01:00

541 lines
24 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
"""
# Initialize update service
from jackify.backend.services.update_service import UpdateService
update_service = UpdateService(jackify_version)
services = {
'modlist_service': ModlistService(self.system_info),
'update_service': update_service
}
return services
def _check_for_updates_on_startup(self):
"""Check for updates on startup in background thread"""
try:
self._debug_print("Checking for updates on startup...")
def update_check_callback(update_info):
"""Handle update check results"""
try:
if update_info:
print(f"\n{COLOR_INFO}Update available: v{update_info.version}{COLOR_RESET}")
print(f"Current version: v{jackify_version}")
print(f"Release date: {update_info.release_date}")
if update_info.changelog:
print(f"Changelog: {update_info.changelog[:200]}...")
print(f"Download size: {update_info.file_size / (1024*1024):.1f} MB" if update_info.file_size else "Download size: Unknown")
print(f"\nTo update, run: jackify --update")
print("Or visit: https://github.com/Omni-guides/Jackify/releases")
else:
self._debug_print("No updates available")
except Exception as e:
self._debug_print(f"Error showing update info: {e}")
# Check for updates in background
self.backend_services['update_service'].check_for_updates_async(update_check_callback)
except Exception as e:
self._debug_print(f"Error checking for updates on startup: {e}")
# Continue anyway - don't block startup on update check errors
def _handle_update(self):
"""Handle manual update check and installation"""
try:
print("Checking for updates...")
update_service = self.backend_services['update_service']
# Check if updating is possible
if not update_service.can_update():
print(f"{COLOR_ERROR}Update not possible: not running as AppImage or insufficient permissions{COLOR_RESET}")
return 1
# Check for updates
update_info = update_service.check_for_updates()
if update_info:
print(f"{COLOR_INFO}Update available: v{update_info.version}{COLOR_RESET}")
print(f"Current version: v{jackify_version}")
print(f"Release date: {update_info.release_date}")
if update_info.changelog:
print(f"Changelog: {update_info.changelog}")
print(f"Download size: {update_info.file_size / (1024*1024):.1f} MB" if update_info.file_size else "Download size: Unknown")
# Ask for confirmation
response = input("\nDo you want to download and install this update? (y/N): ").strip().lower()
if response in ['y', 'yes']:
print("Downloading update...")
def progress_callback(downloaded, total):
if total > 0:
percentage = int((downloaded / total) * 100)
downloaded_mb = downloaded / (1024 * 1024)
total_mb = total / (1024 * 1024)
print(f"\rDownloaded {downloaded_mb:.1f} MB of {total_mb:.1f} MB ({percentage}%)", end='', flush=True)
downloaded_path = update_service.download_update(update_info, progress_callback)
if downloaded_path:
print(f"\nDownload completed. Installing update...")
if update_service.apply_update(downloaded_path):
print(f"{COLOR_INFO}Update applied successfully! Jackify will restart...{COLOR_RESET}")
return 0
else:
print(f"{COLOR_ERROR}Failed to apply update{COLOR_RESET}")
return 1
else:
print(f"\n{COLOR_ERROR}Failed to download update{COLOR_RESET}")
return 1
else:
print("Update cancelled.")
return 0
else:
print(f"{COLOR_INFO}You are already running the latest version (v{jackify_version}){COLOR_RESET}")
return 0
except Exception as e:
print(f"{COLOR_ERROR}Update failed: {e}{COLOR_RESET}")
return 1
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 update functionality
if getattr(self.args, 'update', False):
self._debug_print('Entering update workflow')
return self._handle_update()
# 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)
# Check for updates on startup (non-blocking)
self._check_for_updates_on_startup()
# 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)')
parser.add_argument('--update', action='store_true', help='Check for and install updates')
# 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)