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:
5
jackify/frontends/cli/__init__.py
Normal file
5
jackify/frontends/cli/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""
|
||||
Jackify CLI Frontend
|
||||
|
||||
Command-line interface for Jackify that uses the backend services.
|
||||
"""
|
||||
45
jackify/frontends/cli/__main__.py
Normal file
45
jackify/frontends/cli/__main__.py
Normal file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Jackify CLI Frontend Entry Point
|
||||
|
||||
New entry point for the CLI frontend that uses the refactored structure.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import signal
|
||||
import logging
|
||||
|
||||
from .main import JackifyCLI
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
def terminate_children(signum, frame):
|
||||
"""Signal handler to terminate child processes on exit"""
|
||||
print("Received signal, shutting down...")
|
||||
sys.exit(0)
|
||||
|
||||
def main():
|
||||
"""Main entry point for the CLI frontend"""
|
||||
# Set up signal handlers
|
||||
signal.signal(signal.SIGTERM, terminate_children)
|
||||
signal.signal(signal.SIGINT, terminate_children)
|
||||
|
||||
try:
|
||||
cli = JackifyCLI()
|
||||
exit_code = cli.run()
|
||||
sys.exit(exit_code or 0)
|
||||
except KeyboardInterrupt:
|
||||
print("\nOperation cancelled by user")
|
||||
sys.exit(130) # Standard exit code for SIGINT
|
||||
except Exception as e:
|
||||
print(f"Fatal error: {e}")
|
||||
logging.exception("Fatal error in CLI frontend")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
5
jackify/frontends/cli/commands/__init__.py
Normal file
5
jackify/frontends/cli/commands/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""
|
||||
CLI Commands
|
||||
|
||||
Individual command implementations for the CLI interface.
|
||||
"""
|
||||
159
jackify/frontends/cli/commands/configure_modlist.py
Normal file
159
jackify/frontends/cli/commands/configure_modlist.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
Configure Modlist Command
|
||||
|
||||
CLI command for configuring a modlist post-install.
|
||||
Extracted from the original jackify-cli.py.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
# Import the backend services we'll need
|
||||
from jackify.backend.models.configuration import ConfigurationContext
|
||||
from jackify.shared.colors import COLOR_INFO, COLOR_ERROR, COLOR_RESET
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConfigureModlistCommand:
|
||||
"""Handler for the configure-modlist CLI command."""
|
||||
|
||||
def __init__(self, backend_services):
|
||||
"""Initialize with backend services.
|
||||
|
||||
Args:
|
||||
backend_services: Dictionary of backend service instances
|
||||
"""
|
||||
self.backend_services = backend_services
|
||||
self.test_mode = False # TODO: Get from global config
|
||||
|
||||
def add_parser(self, subparsers):
|
||||
"""Add the configure-modlist subcommand parser.
|
||||
|
||||
Args:
|
||||
subparsers: The ArgumentParser subparsers object
|
||||
"""
|
||||
configure_modlist_parser = subparsers.add_parser(
|
||||
"configure-modlist",
|
||||
help="Configure a modlist post-install (for GUI integration)"
|
||||
)
|
||||
configure_modlist_parser.add_argument(
|
||||
"--modlist-name",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Name of the modlist to configure (Steam shortcut name)"
|
||||
)
|
||||
configure_modlist_parser.add_argument(
|
||||
"--install-dir",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Install directory of the modlist"
|
||||
)
|
||||
configure_modlist_parser.add_argument(
|
||||
"--download-dir",
|
||||
type=str,
|
||||
help="Downloads directory (optional)"
|
||||
)
|
||||
configure_modlist_parser.add_argument(
|
||||
"--nexus-api-key",
|
||||
type=str,
|
||||
help="Nexus API key (optional)"
|
||||
)
|
||||
configure_modlist_parser.add_argument(
|
||||
"--mo2-exe-path",
|
||||
type=str,
|
||||
help="Path to ModOrganizer.exe (for AppID lookup)"
|
||||
)
|
||||
configure_modlist_parser.add_argument(
|
||||
"--resolution",
|
||||
type=str,
|
||||
help="Resolution to set (optional)"
|
||||
)
|
||||
configure_modlist_parser.add_argument(
|
||||
"--skip-confirmation",
|
||||
action='store_true',
|
||||
help="Skip confirmation prompts"
|
||||
)
|
||||
return configure_modlist_parser
|
||||
|
||||
def execute(self, args) -> int:
|
||||
"""Execute the configure-modlist command.
|
||||
|
||||
Args:
|
||||
args: Parsed command-line arguments
|
||||
|
||||
Returns:
|
||||
Exit code (0 for success, 1 for failure)
|
||||
"""
|
||||
logger.info("Starting non-interactive modlist configuration (CLI mode)")
|
||||
|
||||
try:
|
||||
# Build configuration context from args
|
||||
context = self._build_context_from_args(args)
|
||||
|
||||
# Use legacy implementation for now - will migrate to backend services later
|
||||
result = self._execute_legacy_configuration(context)
|
||||
|
||||
logger.info("Finished non-interactive modlist configuration")
|
||||
return 0 if result is not True else 1
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to configure modlist: {e}")
|
||||
print(f"{COLOR_ERROR}Configuration failed: {e}{COLOR_RESET}")
|
||||
return 1
|
||||
|
||||
def _build_context_from_args(self, args) -> dict:
|
||||
"""Build context dictionary from command arguments.
|
||||
|
||||
Args:
|
||||
args: Parsed command-line arguments
|
||||
|
||||
Returns:
|
||||
Context dictionary
|
||||
"""
|
||||
return {
|
||||
'modlist_name': getattr(args, 'modlist_name', None),
|
||||
'install_dir': getattr(args, 'install_dir', None),
|
||||
'download_dir': getattr(args, 'download_dir', None),
|
||||
'nexus_api_key': getattr(args, 'nexus_api_key', os.environ.get('NEXUS_API_KEY')),
|
||||
'mo2_exe_path': getattr(args, 'mo2_exe_path', None),
|
||||
'resolution': getattr(args, 'resolution', None),
|
||||
'skip_confirmation': getattr(args, 'skip_confirmation', False),
|
||||
'modlist_value': getattr(args, 'modlist_value', None),
|
||||
'modlist_source': getattr(args, 'modlist_source', None),
|
||||
}
|
||||
|
||||
def _execute_legacy_configuration(self, context: dict):
|
||||
"""Execute configuration using legacy implementation.
|
||||
|
||||
This is a temporary bridge - will be replaced with backend service calls.
|
||||
|
||||
Args:
|
||||
context: Configuration context dictionary
|
||||
|
||||
Returns:
|
||||
Result from legacy configuration
|
||||
"""
|
||||
# Import backend services
|
||||
from jackify.backend.handlers.menu_handler import ModlistMenuHandler
|
||||
from jackify.backend.handlers.config_handler import ConfigHandler
|
||||
|
||||
# Create legacy handler instances
|
||||
config_handler = ConfigHandler()
|
||||
modlist_menu = ModlistMenuHandler(
|
||||
config_handler=config_handler,
|
||||
test_mode=self.test_mode
|
||||
)
|
||||
|
||||
# Execute legacy configuration workflow
|
||||
# The _configure_new_modlist method already handles Steam restart, manual steps, and configuration
|
||||
result = modlist_menu._configure_new_modlist(
|
||||
default_modlist_dir=context['install_dir'],
|
||||
default_modlist_name=context['modlist_name']
|
||||
)
|
||||
|
||||
# The _configure_new_modlist method already calls run_modlist_configuration_phase internally
|
||||
# So we don't need to call it again here
|
||||
|
||||
return result
|
||||
363
jackify/frontends/cli/commands/install_modlist.py
Normal file
363
jackify/frontends/cli/commands/install_modlist.py
Normal file
@@ -0,0 +1,363 @@
|
||||
"""
|
||||
Install Modlist Command
|
||||
|
||||
CLI command for installing modlists.
|
||||
Extracted from the original jackify-cli.py.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
# Import the backend services we'll need
|
||||
from jackify.backend.models.modlist import ModlistContext
|
||||
from jackify.shared.colors import COLOR_INFO, COLOR_ERROR, COLOR_RESET
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InstallModlistCommand:
|
||||
"""Handler for the install-modlist CLI command."""
|
||||
|
||||
def __init__(self, backend_services, system_info):
|
||||
"""Initialize with backend services.
|
||||
|
||||
Args:
|
||||
backend_services: Dictionary of backend service instances
|
||||
system_info: System information (steamdeck flag, etc.)
|
||||
"""
|
||||
self.backend_services = backend_services
|
||||
self.system_info = system_info
|
||||
|
||||
def add_top_level_args(self, parser):
|
||||
"""Add top-level install-modlist arguments to the main parser.
|
||||
|
||||
Args:
|
||||
parser: The main ArgumentParser
|
||||
"""
|
||||
parser.add_argument(
|
||||
"--install-modlist",
|
||||
action="store_true",
|
||||
help="Enable modlist install/list feature (for GUI integration)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--list-modlists",
|
||||
action="store_true",
|
||||
help="List available modlists for a game type (with --install-modlist)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--install",
|
||||
action="store_true",
|
||||
help="Install a modlist non-interactively (with --install-modlist)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--game-type",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Game type to filter modlists (skyrim, fallout4, falloutnv, oblivion, starfield, oblivion_remastered, other)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--modlist-value",
|
||||
type=str,
|
||||
help="Modlist identifier for online modlists"
|
||||
)
|
||||
|
||||
def add_parser(self, subparsers):
|
||||
"""Add the install-modlist subcommand parser.
|
||||
|
||||
Args:
|
||||
subparsers: The ArgumentParser subparsers object
|
||||
"""
|
||||
install_modlist_parser = subparsers.add_parser(
|
||||
"install-modlist",
|
||||
help="Install or list available modlists"
|
||||
)
|
||||
install_modlist_parser.add_argument(
|
||||
"--list",
|
||||
action="store_true",
|
||||
help="List available modlists for a game type"
|
||||
)
|
||||
install_modlist_parser.add_argument(
|
||||
"--game-type",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Game type to filter modlists (skyrim, fallout4, falloutnv, oblivion, starfield, oblivion_remastered, other)"
|
||||
)
|
||||
return install_modlist_parser
|
||||
|
||||
def execute_top_level(self, args) -> int:
|
||||
"""Execute top-level install-modlist functionality.
|
||||
|
||||
Args:
|
||||
args: Parsed command-line arguments
|
||||
|
||||
Returns:
|
||||
Exit code (0 for success, 1 for failure)
|
||||
"""
|
||||
if getattr(args, 'list_modlists', False):
|
||||
return self.list_modlists(args)
|
||||
elif getattr(args, 'install', False):
|
||||
return self.install_modlist_auto(args)
|
||||
else:
|
||||
print(f"{COLOR_ERROR}No valid install-modlist operation specified{COLOR_RESET}")
|
||||
return 1
|
||||
|
||||
def execute_subcommand(self, args) -> int:
|
||||
"""Execute the install-modlist subcommand.
|
||||
|
||||
Args:
|
||||
args: Parsed command-line arguments
|
||||
|
||||
Returns:
|
||||
Exit code (0 for success, 1 for failure)
|
||||
"""
|
||||
if getattr(args, 'list', False):
|
||||
return self.list_modlists(args)
|
||||
else:
|
||||
# Default behavior: run interactive modlist installation
|
||||
logger.info("Starting interactive modlist installation via subcommand")
|
||||
|
||||
try:
|
||||
# Use the working ModlistInstallCLI for interactive installation
|
||||
from jackify.backend.core.modlist_operations import ModlistInstallCLI
|
||||
|
||||
# Use new SystemInfo pattern
|
||||
modlist_cli = ModlistInstallCLI(self.system_info)
|
||||
|
||||
# Run interactive discovery phase
|
||||
context = modlist_cli.run_discovery_phase()
|
||||
if context:
|
||||
# Run configuration phase (installation + Steam setup)
|
||||
modlist_cli.configuration_phase()
|
||||
logger.info("Interactive modlist installation completed successfully")
|
||||
return 0
|
||||
else:
|
||||
logger.info("Modlist installation cancelled by user")
|
||||
return 1
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to install modlist: {e}")
|
||||
print(f"{COLOR_ERROR}Installation failed: {e}{COLOR_RESET}")
|
||||
return 1
|
||||
|
||||
def list_modlists(self, args) -> int:
|
||||
"""List available modlists for a game type.
|
||||
|
||||
Args:
|
||||
args: Parsed command-line arguments
|
||||
|
||||
Returns:
|
||||
Exit code (0 for success, 1 for failure)
|
||||
"""
|
||||
logger.info("Listing available modlists")
|
||||
|
||||
try:
|
||||
# Use legacy implementation for now - will migrate to backend services later
|
||||
result = self._execute_legacy_list_modlists(args)
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list modlists: {e}")
|
||||
print(f"{COLOR_ERROR}Failed to list modlists: {e}{COLOR_RESET}")
|
||||
return 1
|
||||
|
||||
def install_modlist_auto(self, args) -> int:
|
||||
"""Install a modlist non-interactively.
|
||||
|
||||
Args:
|
||||
args: Parsed command-line arguments
|
||||
|
||||
Returns:
|
||||
Exit code (0 for success, 1 for failure)
|
||||
"""
|
||||
logger.info("Starting non-interactive modlist installation")
|
||||
|
||||
try:
|
||||
# Build context from args
|
||||
context = self._build_install_context_from_args(args)
|
||||
|
||||
# Validate required fields
|
||||
if not self._validate_install_context(context):
|
||||
return 1
|
||||
|
||||
# Use legacy implementation for now - will migrate to backend services later
|
||||
result = self._execute_legacy_install(context)
|
||||
|
||||
logger.info("Finished non-interactive modlist installation")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to install modlist: {e}")
|
||||
print(f"{COLOR_ERROR}Installation failed: {e}{COLOR_RESET}")
|
||||
return 1
|
||||
|
||||
def _build_install_context_from_args(self, args) -> dict:
|
||||
"""Build installation context from command arguments.
|
||||
|
||||
Args:
|
||||
args: Parsed command-line arguments
|
||||
|
||||
Returns:
|
||||
Context dictionary
|
||||
"""
|
||||
return {
|
||||
'modlist_name': getattr(args, 'modlist_name', None),
|
||||
'install_dir': getattr(args, 'install_dir', None),
|
||||
'download_dir': getattr(args, 'download_dir', None),
|
||||
'nexus_api_key': os.environ.get('NEXUS_API_KEY'),
|
||||
'game_type': getattr(args, 'game_type', None),
|
||||
'modlist_value': getattr(args, 'modlist_value', None),
|
||||
'skip_confirmation': True,
|
||||
'resolution': getattr(args, 'resolution', None),
|
||||
}
|
||||
|
||||
def _validate_install_context(self, context: dict) -> bool:
|
||||
"""Validate installation context.
|
||||
|
||||
Args:
|
||||
context: Installation context dictionary
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
is_gui_mode = os.environ.get('JACKIFY_GUI_MODE') == '1'
|
||||
required_keys = ['modlist_name', 'install_dir', 'download_dir', 'nexus_api_key', 'game_type']
|
||||
missing = [k for k in required_keys if not context.get(k)]
|
||||
|
||||
if is_gui_mode and missing:
|
||||
print(f"ERROR: Missing required arguments for GUI workflow: {', '.join(missing)}")
|
||||
print("This workflow must be fully non-interactive. Please report this as a bug if you see this message.")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _execute_legacy_list_modlists(self, args):
|
||||
"""Execute list modlists using backend implementation.
|
||||
|
||||
Args:
|
||||
args: Parsed command-line arguments
|
||||
"""
|
||||
# Import backend services
|
||||
from jackify.backend.core.modlist_operations import ModlistInstallCLI
|
||||
|
||||
# Use new SystemInfo pattern
|
||||
modlist_cli = ModlistInstallCLI(self.system_info)
|
||||
|
||||
# Get all modlists from engine
|
||||
raw_modlists = modlist_cli.get_all_modlists_from_engine()
|
||||
|
||||
# Group by game type as in original CLI
|
||||
game_type_map = {
|
||||
'skyrim': ['Skyrim', 'Skyrim Special Edition'],
|
||||
'fallout4': ['Fallout 4'],
|
||||
'falloutnv': ['Fallout New Vegas'],
|
||||
'oblivion': ['Oblivion'],
|
||||
'starfield': ['Starfield'],
|
||||
'oblivion_remastered': ['Oblivion Remastered', 'OblivionRemastered'],
|
||||
'other': None
|
||||
}
|
||||
|
||||
grouped_modlists = {k: [] for k in game_type_map}
|
||||
|
||||
for m_info in raw_modlists: # m_info is like {'id': ..., 'game': ...}
|
||||
found_category = False
|
||||
for cat_key, cat_keywords in game_type_map.items():
|
||||
if cat_key == 'other':
|
||||
continue
|
||||
if cat_keywords:
|
||||
for keyword in cat_keywords:
|
||||
if keyword.lower() in m_info.get('game', '').lower():
|
||||
grouped_modlists[cat_key].append(m_info)
|
||||
found_category = True
|
||||
break
|
||||
if found_category:
|
||||
break
|
||||
if not found_category:
|
||||
grouped_modlists['other'].append(m_info)
|
||||
|
||||
# Output modlists for the requested game type
|
||||
game_type = (getattr(args, 'game_type', '') or '').lower()
|
||||
if game_type and game_type in grouped_modlists:
|
||||
for m in grouped_modlists[game_type]:
|
||||
print(m.get('id', ''))
|
||||
else:
|
||||
# Output all modlists
|
||||
for cat_key in ['skyrim', 'fallout4', 'falloutnv', 'oblivion', 'starfield', 'oblivion_remastered', 'other']:
|
||||
for m in grouped_modlists[cat_key]:
|
||||
print(m.get('id', ''))
|
||||
|
||||
def _execute_legacy_install(self, context: dict) -> int:
|
||||
"""Execute installation using backend implementation.
|
||||
|
||||
Args:
|
||||
context: Installation context dictionary
|
||||
|
||||
Returns:
|
||||
Exit code
|
||||
"""
|
||||
# Import backend services
|
||||
from jackify.backend.core.modlist_operations import ModlistInstallCLI
|
||||
from jackify.shared.colors import COLOR_WARNING, COLOR_PROMPT
|
||||
|
||||
# Use new SystemInfo pattern
|
||||
modlist_cli = ModlistInstallCLI(self.system_info)
|
||||
|
||||
# Detect game type and check support
|
||||
game_type = None
|
||||
wabbajack_file_path = context.get('wabbajack_file_path')
|
||||
modlist_info = context.get('modlist_info')
|
||||
|
||||
if wabbajack_file_path:
|
||||
game_type = modlist_cli.detect_game_type(wabbajack_file_path=wabbajack_file_path)
|
||||
elif modlist_info:
|
||||
game_type = modlist_cli.detect_game_type(modlist_info=modlist_info)
|
||||
elif context.get('game_type'):
|
||||
game_type = context['game_type']
|
||||
|
||||
# Check if game is supported
|
||||
if game_type and not modlist_cli.check_game_support(game_type):
|
||||
# Show unsupported game warning
|
||||
supported_games = modlist_cli.wabbajack_parser.get_supported_games_display_names()
|
||||
supported_games_str = ", ".join(supported_games)
|
||||
|
||||
print(f"\n{COLOR_WARNING}Game Support Notice{COLOR_RESET}")
|
||||
print(f"{COLOR_WARNING}While any modlist can be downloaded with Jackify, the post-install configuration can only be automatically applied to: {supported_games_str}.{COLOR_RESET}")
|
||||
print(f"{COLOR_WARNING}We are working to add more automated support in future releases!{COLOR_RESET}")
|
||||
|
||||
# Ask for confirmation to continue
|
||||
response = input(f"{COLOR_PROMPT}Click Enter to continue with the modlist installation, or type 'cancel' to abort: {COLOR_RESET}").strip().lower()
|
||||
if response == 'cancel':
|
||||
print("[INFO] Modlist installation cancelled by user.")
|
||||
return 1
|
||||
|
||||
is_gui_mode = os.environ.get('JACKIFY_GUI_MODE') == '1'
|
||||
|
||||
if is_gui_mode:
|
||||
confirmed_context = modlist_cli.run_discovery_phase(context_override=context)
|
||||
if confirmed_context:
|
||||
# For unsupported games, skip post-install configuration
|
||||
if game_type and not modlist_cli.check_game_support(game_type):
|
||||
print(f"{COLOR_WARNING}Modlist installation completed successfully.{COLOR_RESET}")
|
||||
print(f"{COLOR_WARNING}Note: Post-install configuration was skipped for unsupported game type: {game_type}{COLOR_RESET}")
|
||||
return 0
|
||||
else:
|
||||
modlist_cli.configuration_phase()
|
||||
return 0
|
||||
else:
|
||||
print("[INFO] Modlist installation cancelled or not confirmed.")
|
||||
return 1
|
||||
else:
|
||||
# CLI mode: allow interactive prompts as before
|
||||
confirmed_context = modlist_cli.run_discovery_phase(context_override=context)
|
||||
if confirmed_context:
|
||||
# For unsupported games, skip post-install configuration
|
||||
if game_type and not modlist_cli.check_game_support(game_type):
|
||||
print(f"{COLOR_WARNING}Modlist installation completed successfully.{COLOR_RESET}")
|
||||
print(f"{COLOR_WARNING}Note: Post-install configuration was skipped for unsupported game type: {game_type}{COLOR_RESET}")
|
||||
return 0
|
||||
else:
|
||||
modlist_cli.configuration_phase()
|
||||
return 0
|
||||
else:
|
||||
print("[INFO] Modlist installation cancelled or not confirmed.")
|
||||
return 1
|
||||
247
jackify/frontends/cli/commands/tuxborn.py
Normal file
247
jackify/frontends/cli/commands/tuxborn.py
Normal file
@@ -0,0 +1,247 @@
|
||||
"""
|
||||
Tuxborn Command
|
||||
|
||||
CLI command for the Tuxborn Automatic Installer.
|
||||
Extracted from the original jackify-cli.py.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
# Import the backend services we'll need
|
||||
from jackify.backend.models.modlist import ModlistContext
|
||||
from jackify.shared.colors import COLOR_INFO, COLOR_ERROR, COLOR_RESET
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TuxbornCommand:
|
||||
"""Handler for the tuxborn-auto CLI command."""
|
||||
|
||||
def __init__(self, backend_services, system_info):
|
||||
"""Initialize with backend services.
|
||||
|
||||
Args:
|
||||
backend_services: Dictionary of backend service instances
|
||||
system_info: System information (steamdeck flag, etc.)
|
||||
"""
|
||||
self.backend_services = backend_services
|
||||
self.system_info = system_info
|
||||
|
||||
def add_args(self, parser):
|
||||
"""Add tuxborn-auto arguments to the main parser.
|
||||
|
||||
Args:
|
||||
parser: The main ArgumentParser
|
||||
"""
|
||||
parser.add_argument(
|
||||
"--tuxborn-auto",
|
||||
action="store_true",
|
||||
help="Run the Tuxborn Automatic Installer non-interactively (for GUI integration)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--install-dir",
|
||||
type=str,
|
||||
help="Install directory for Tuxborn (required with --tuxborn-auto)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--download-dir",
|
||||
type=str,
|
||||
help="Downloads directory for Tuxborn (required with --tuxborn-auto)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--modlist-name",
|
||||
type=str,
|
||||
default="Tuxborn",
|
||||
help="Modlist name (optional, defaults to 'Tuxborn')"
|
||||
)
|
||||
|
||||
def execute(self, args) -> int:
|
||||
"""Execute the tuxborn-auto command.
|
||||
|
||||
Args:
|
||||
args: Parsed command-line arguments
|
||||
|
||||
Returns:
|
||||
Exit code (0 for success, 1 for failure)
|
||||
"""
|
||||
logger.info("Starting Tuxborn Automatic Installer (GUI integration mode)")
|
||||
|
||||
try:
|
||||
# Set up logging redirection (copied from original)
|
||||
self._setup_tee_logging()
|
||||
|
||||
# Build context from args
|
||||
context = self._build_context_from_args(args)
|
||||
|
||||
# Validate required fields
|
||||
if not self._validate_context(context):
|
||||
return 1
|
||||
|
||||
# Use legacy implementation for now - will migrate to backend services later
|
||||
result = self._execute_legacy_tuxborn(context)
|
||||
|
||||
logger.info("Finished Tuxborn Automatic Installer")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to run Tuxborn installer: {e}")
|
||||
print(f"{COLOR_ERROR}Tuxborn installation failed: {e}{COLOR_RESET}")
|
||||
return 1
|
||||
finally:
|
||||
# Restore stdout/stderr
|
||||
self._restore_stdout_stderr()
|
||||
|
||||
def _build_context_from_args(self, args) -> dict:
|
||||
"""Build context dictionary from command arguments.
|
||||
|
||||
Args:
|
||||
args: Parsed command-line arguments
|
||||
|
||||
Returns:
|
||||
Context dictionary
|
||||
"""
|
||||
install_dir = getattr(args, 'install_dir', None)
|
||||
download_dir = getattr(args, 'download_dir', None)
|
||||
modlist_name = getattr(args, 'modlist_name', 'Tuxborn')
|
||||
machineid = 'Tuxborn/Tuxborn'
|
||||
|
||||
# Try to get API key from saved config first, then environment variable
|
||||
from jackify.backend.services.api_key_service import APIKeyService
|
||||
api_key_service = APIKeyService()
|
||||
api_key = api_key_service.get_saved_api_key()
|
||||
if not api_key:
|
||||
api_key = os.environ.get('NEXUS_API_KEY')
|
||||
|
||||
resolution = getattr(args, 'resolution', None)
|
||||
mo2_exe_path = getattr(args, 'mo2_exe_path', None)
|
||||
skip_confirmation = True # Always true in GUI mode
|
||||
|
||||
context = {
|
||||
'machineid': machineid,
|
||||
'modlist_name': modlist_name,
|
||||
'install_dir': install_dir,
|
||||
'download_dir': download_dir,
|
||||
'nexus_api_key': api_key,
|
||||
'skip_confirmation': skip_confirmation,
|
||||
'resolution': resolution,
|
||||
'mo2_exe_path': mo2_exe_path,
|
||||
}
|
||||
|
||||
# PATCH: Always set modlist_value and modlist_source for Tuxborn workflow
|
||||
context['modlist_value'] = 'Tuxborn/Tuxborn'
|
||||
context['modlist_source'] = 'identifier'
|
||||
|
||||
return context
|
||||
|
||||
def _validate_context(self, context: dict) -> bool:
|
||||
"""Validate Tuxborn context.
|
||||
|
||||
Args:
|
||||
context: Tuxborn context dictionary
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
required_keys = ['modlist_name', 'install_dir', 'download_dir', 'nexus_api_key']
|
||||
missing = [k for k in required_keys if not context.get(k)]
|
||||
|
||||
if missing:
|
||||
print(f"{COLOR_ERROR}Missing required arguments for --tuxborn-auto.\\n"
|
||||
f"--install-dir, --download-dir, and NEXUS_API_KEY (env, 32+ chars) are required.{COLOR_RESET}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _setup_tee_logging(self):
|
||||
"""Set up TEE logging (copied from original implementation)."""
|
||||
import shutil
|
||||
|
||||
# TEE logging setup & log rotation (copied from original)
|
||||
class TeeStdout:
|
||||
def __init__(self, *files):
|
||||
self.files = files
|
||||
def write(self, data):
|
||||
for f in self.files:
|
||||
f.write(data)
|
||||
f.flush()
|
||||
def flush(self):
|
||||
for f in self.files:
|
||||
f.flush()
|
||||
|
||||
log_dir = Path.home() / "Jackify" / "logs"
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
workflow_log_path = log_dir / "tuxborn_workflow.log"
|
||||
|
||||
# Log rotation: keep last 3 logs, 1KB each (for testing)
|
||||
max_logs = 3
|
||||
max_size = 1024 # 1KB for testing
|
||||
if workflow_log_path.exists() and workflow_log_path.stat().st_size > max_size:
|
||||
for i in range(max_logs, 0, -1):
|
||||
prev = log_dir / f"tuxborn_workflow.log.{i-1}" if i > 1 else workflow_log_path
|
||||
dest = log_dir / f"tuxborn_workflow.log.{i}"
|
||||
if prev.exists():
|
||||
if dest.exists():
|
||||
dest.unlink()
|
||||
prev.rename(dest)
|
||||
|
||||
self.workflow_log = open(workflow_log_path, 'a')
|
||||
self.orig_stdout, self.orig_stderr = sys.stdout, sys.stderr
|
||||
sys.stdout = TeeStdout(sys.stdout, self.workflow_log)
|
||||
sys.stderr = TeeStdout(sys.stderr, self.workflow_log)
|
||||
|
||||
def _restore_stdout_stderr(self):
|
||||
"""Restore original stdout/stderr."""
|
||||
if hasattr(self, 'orig_stdout'):
|
||||
sys.stdout = self.orig_stdout
|
||||
sys.stderr = self.orig_stderr
|
||||
if hasattr(self, 'workflow_log'):
|
||||
self.workflow_log.close()
|
||||
|
||||
def _execute_legacy_tuxborn(self, context: dict) -> int:
|
||||
"""Execute Tuxborn using legacy implementation.
|
||||
|
||||
Args:
|
||||
context: Tuxborn context dictionary
|
||||
|
||||
Returns:
|
||||
Exit code
|
||||
"""
|
||||
# Import backend services
|
||||
from jackify.backend.core.modlist_operations import ModlistInstallCLI
|
||||
from jackify.backend.handlers.menu_handler import MenuHandler
|
||||
|
||||
# Create legacy handler instances
|
||||
menu_handler = MenuHandler()
|
||||
modlist_cli = ModlistInstallCLI(
|
||||
menu_handler=menu_handler,
|
||||
steamdeck=self.system_info.get('is_steamdeck', False)
|
||||
)
|
||||
|
||||
confirmed_context = modlist_cli.run_discovery_phase(context_override=context)
|
||||
if confirmed_context:
|
||||
menu_handler.logger.info("Tuxborn discovery confirmed by GUI. Proceeding to configuration/installation.")
|
||||
modlist_cli.configuration_phase()
|
||||
|
||||
# Handle GUI integration prompts (copied from original)
|
||||
print('[PROMPT:RESTART_STEAM]')
|
||||
if os.environ.get('JACKIFY_GUI_MODE'):
|
||||
input() # Wait for GUI to send confirmation, no CLI prompt
|
||||
else:
|
||||
answer = input('Restart Steam automatically now? (Y/n): ')
|
||||
# ... handle answer as before ...
|
||||
|
||||
print('[PROMPT:MANUAL_STEPS]')
|
||||
if os.environ.get('JACKIFY_GUI_MODE'):
|
||||
input() # Wait for GUI to send confirmation, no CLI prompt
|
||||
else:
|
||||
input('Once you have completed ALL the steps above, press Enter to continue...')
|
||||
|
||||
return 0
|
||||
else:
|
||||
menu_handler.logger.info("Tuxborn discovery/confirmation cancelled or failed (GUI mode).")
|
||||
print(f"{COLOR_INFO}Tuxborn installation cancelled or not confirmed.{COLOR_RESET}")
|
||||
return 1
|
||||
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)
|
||||
20
jackify/frontends/cli/menus/__init__.py
Normal file
20
jackify/frontends/cli/menus/__init__.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
CLI Menu Components for Jackify Frontend
|
||||
Extracted from the legacy monolithic CLI system
|
||||
"""
|
||||
|
||||
from .main_menu import MainMenuHandler
|
||||
from .tuxborn_menu import TuxbornMenuHandler
|
||||
from .wabbajack_menu import WabbajackMenuHandler
|
||||
from .hoolamike_menu import HoolamikeMenuHandler
|
||||
from .additional_menu import AdditionalMenuHandler
|
||||
from .recovery_menu import RecoveryMenuHandler
|
||||
|
||||
__all__ = [
|
||||
'MainMenuHandler',
|
||||
'TuxbornMenuHandler',
|
||||
'WabbajackMenuHandler',
|
||||
'HoolamikeMenuHandler',
|
||||
'AdditionalMenuHandler',
|
||||
'RecoveryMenuHandler'
|
||||
]
|
||||
73
jackify/frontends/cli/menus/additional_menu.py
Normal file
73
jackify/frontends/cli/menus/additional_menu.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
Additional Tasks Menu Handler for Jackify CLI Frontend
|
||||
Extracted from src.modules.menu_handler.MenuHandler.show_additional_tasks_menu()
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from jackify.shared.colors import (
|
||||
COLOR_SELECTION, COLOR_RESET, COLOR_ACTION, COLOR_PROMPT, COLOR_INFO, COLOR_DISABLED
|
||||
)
|
||||
from jackify.shared.ui_utils import print_jackify_banner, print_section_header
|
||||
|
||||
class AdditionalMenuHandler:
|
||||
"""
|
||||
Handles the Additional Tasks menu (MO2, NXM Handling & Recovery)
|
||||
Extracted from legacy MenuHandler class
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = None # Will be set by CLI when needed
|
||||
|
||||
def _clear_screen(self):
|
||||
"""Clear the terminal screen"""
|
||||
import os
|
||||
os.system('cls' if os.name == 'nt' else 'clear')
|
||||
|
||||
def show_additional_tasks_menu(self, cli_instance):
|
||||
"""Show the MO2, NXM Handling & Recovery submenu"""
|
||||
while True:
|
||||
self._clear_screen()
|
||||
print_jackify_banner()
|
||||
print_section_header("Additional Utilities") # Broader title
|
||||
|
||||
print(f"{COLOR_SELECTION}1.{COLOR_RESET} Install Mod Organizer 2 (Base Setup)")
|
||||
print(f" {COLOR_ACTION}→ Proton setup for a standalone MO2 instance{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}2.{COLOR_RESET} Configure NXM Handling {COLOR_DISABLED}(Not Implemented){COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}3.{COLOR_RESET} Jackify Recovery Tools")
|
||||
print(f" {COLOR_ACTION}→ Restore files modified or backed up by Jackify{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}0.{COLOR_RESET} Return to Main Menu")
|
||||
selection = input(f"\n{COLOR_PROMPT}Enter your selection (0-3): {COLOR_RESET}").strip()
|
||||
|
||||
if selection.lower() == 'q': # Allow 'q' to re-display menu
|
||||
continue
|
||||
if selection == "1":
|
||||
self._execute_legacy_install_mo2(cli_instance)
|
||||
elif selection == "2":
|
||||
print(f"{COLOR_INFO}Configure NXM Handling is not yet implemented.{COLOR_RESET}")
|
||||
input("\nPress Enter to return to the Utilities menu...")
|
||||
elif selection == "3":
|
||||
self._execute_legacy_recovery_menu(cli_instance)
|
||||
elif selection == "0":
|
||||
break
|
||||
else:
|
||||
print("Invalid selection. Please try again.")
|
||||
time.sleep(1)
|
||||
|
||||
def _execute_legacy_install_mo2(self, cli_instance):
|
||||
"""LEGACY BRIDGE: Execute MO2 installation"""
|
||||
# LEGACY BRIDGE: Use legacy imports until backend migration complete
|
||||
if hasattr(cli_instance, 'menu') and hasattr(cli_instance.menu, 'mo2_handler'):
|
||||
cli_instance.menu.mo2_handler.install_mo2()
|
||||
else:
|
||||
print(f"{COLOR_INFO}MO2 handler not available - this will be implemented in Phase 2.3{COLOR_RESET}")
|
||||
input("\nPress Enter to continue...")
|
||||
|
||||
def _execute_legacy_recovery_menu(self, cli_instance):
|
||||
"""LEGACY BRIDGE: Execute recovery menu"""
|
||||
# This will be handled by the RecoveryMenuHandler
|
||||
from .recovery_menu import RecoveryMenuHandler
|
||||
|
||||
recovery_handler = RecoveryMenuHandler()
|
||||
recovery_handler.logger = self.logger
|
||||
recovery_handler.show_recovery_menu(cli_instance)
|
||||
32
jackify/frontends/cli/menus/hoolamike_menu.py
Normal file
32
jackify/frontends/cli/menus/hoolamike_menu.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""
|
||||
Hoolamike Menu Handler for Jackify CLI Frontend
|
||||
Extracted from src.modules.menu_handler.MenuHandler.show_hoolamike_menu()
|
||||
"""
|
||||
|
||||
from jackify.shared.colors import COLOR_INFO, COLOR_PROMPT, COLOR_RESET
|
||||
|
||||
class HoolamikeMenuHandler:
|
||||
"""
|
||||
Handles the Hoolamike Tasks menu
|
||||
Extracted from legacy MenuHandler class
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = None # Will be set by CLI when needed
|
||||
|
||||
def show_hoolamike_menu(self, cli_instance):
|
||||
"""
|
||||
LEGACY BRIDGE: Delegate to legacy menu handler until full backend migration
|
||||
|
||||
Args:
|
||||
cli_instance: Reference to main CLI instance for access to handlers
|
||||
"""
|
||||
print(f"{COLOR_INFO}Hoolamike menu functionality has been extracted but needs migration to backend services.{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}This will be implemented in Phase 2.3 (Menu Backend Integration).{COLOR_RESET}")
|
||||
|
||||
# LEGACY BRIDGE: Use the original menu handler's method
|
||||
if hasattr(cli_instance, 'menu') and hasattr(cli_instance.menu, 'show_hoolamike_menu'):
|
||||
cli_instance.menu.show_hoolamike_menu(cli_instance)
|
||||
else:
|
||||
print(f"{COLOR_INFO}Legacy menu handler not available - returning to main menu.{COLOR_RESET}")
|
||||
input(f"{COLOR_PROMPT}Press Enter to continue...{COLOR_RESET}")
|
||||
74
jackify/frontends/cli/menus/main_menu.py
Normal file
74
jackify/frontends/cli/menus/main_menu.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
Main Menu Handler for Jackify CLI Frontend
|
||||
Extracted from src.modules.menu_handler.MenuHandler.show_main_menu()
|
||||
"""
|
||||
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from jackify.shared.colors import (
|
||||
COLOR_SELECTION, COLOR_RESET, COLOR_ACTION, COLOR_PROMPT, COLOR_ERROR
|
||||
)
|
||||
from jackify.shared.ui_utils import print_jackify_banner
|
||||
|
||||
class MainMenuHandler:
|
||||
"""
|
||||
Handles the main interactive menu display and user input routing
|
||||
Extracted from legacy MenuHandler class
|
||||
"""
|
||||
|
||||
def __init__(self, dev_mode=False):
|
||||
self.logger = None # Will be set by CLI when needed
|
||||
self.dev_mode = dev_mode
|
||||
|
||||
def _clear_screen(self):
|
||||
"""Clear the terminal screen"""
|
||||
import os
|
||||
os.system('cls' if os.name == 'nt' else 'clear')
|
||||
|
||||
def show_main_menu(self, cli_instance) -> str:
|
||||
"""
|
||||
Show the main menu and return user selection
|
||||
|
||||
Args:
|
||||
cli_instance: Reference to main CLI instance for access to handlers
|
||||
|
||||
Returns:
|
||||
str: Menu choice ("wabbajack", "hoolamike", "additional", "exit", "tuxborn")
|
||||
"""
|
||||
while True:
|
||||
self._clear_screen()
|
||||
print_jackify_banner()
|
||||
print(f"{COLOR_SELECTION}Main Menu{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}{'-'*22}{COLOR_RESET}") # Standard separator
|
||||
print(f"{COLOR_SELECTION}1.{COLOR_RESET} Modlist Tasks")
|
||||
print(f" {COLOR_ACTION}→ Install & Configure Modlists{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}2.{COLOR_RESET} Tuxborn Automatic Installer")
|
||||
print(f" {COLOR_ACTION}→ Simple, fully automated Tuxborn installation{COLOR_RESET}")
|
||||
if self.dev_mode:
|
||||
print(f"{COLOR_SELECTION}3.{COLOR_RESET} Hoolamike Tasks")
|
||||
print(f" {COLOR_ACTION}→ Wabbajack alternative: Install Modlists, TTW, etc{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}4.{COLOR_RESET} Additional Tasks")
|
||||
print(f" {COLOR_ACTION}→ Install Wabbajack (via WINE), MO2, NXM Handling, Jackify Recovery{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}0.{COLOR_RESET} Exit Jackify")
|
||||
if self.dev_mode:
|
||||
choice = input(f"\n{COLOR_PROMPT}Enter your selection (0-4): {COLOR_RESET}").strip()
|
||||
else:
|
||||
choice = input(f"\n{COLOR_PROMPT}Enter your selection (0-2): {COLOR_RESET}").strip()
|
||||
|
||||
if choice.lower() == 'q': # Allow 'q' to re-display menu
|
||||
continue
|
||||
if choice == "1":
|
||||
return "wabbajack"
|
||||
elif choice == "2":
|
||||
return "tuxborn" # Will be handled by TuxbornMenuHandler
|
||||
if self.dev_mode:
|
||||
if choice == "3":
|
||||
return "hoolamike"
|
||||
elif choice == "4":
|
||||
return "additional"
|
||||
elif choice == "0":
|
||||
return "exit"
|
||||
else:
|
||||
print(f"{COLOR_ERROR}Invalid selection. Please try again.{COLOR_RESET}")
|
||||
time.sleep(1) # Brief pause for readability
|
||||
174
jackify/frontends/cli/menus/recovery_menu.py
Normal file
174
jackify/frontends/cli/menus/recovery_menu.py
Normal file
@@ -0,0 +1,174 @@
|
||||
"""
|
||||
Recovery Menu Handler for Jackify CLI Frontend
|
||||
Extracted from src.modules.menu_handler.MenuHandler._show_recovery_menu()
|
||||
"""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from jackify.shared.colors import (
|
||||
COLOR_SELECTION, COLOR_RESET, COLOR_PROMPT, COLOR_INFO, COLOR_ERROR
|
||||
)
|
||||
from jackify.shared.ui_utils import print_jackify_banner, print_section_header
|
||||
|
||||
class RecoveryMenuHandler:
|
||||
"""
|
||||
Handles the Recovery Tools menu
|
||||
Extracted from legacy MenuHandler class
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def _clear_screen(self):
|
||||
"""Clear the terminal screen"""
|
||||
import os
|
||||
os.system('cls' if os.name == 'nt' else 'clear')
|
||||
|
||||
def show_recovery_menu(self, cli_instance):
|
||||
"""Show the recovery tools menu."""
|
||||
while True:
|
||||
self._clear_screen()
|
||||
print_jackify_banner()
|
||||
print_section_header('Recovery Tools')
|
||||
print(f"{COLOR_INFO}This allows restoring original Steam configuration files from backups created by Jackify.{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}1.{COLOR_RESET} Restore all backups")
|
||||
print(f"{COLOR_SELECTION}2.{COLOR_RESET} Restore config.vdf only")
|
||||
print(f"{COLOR_SELECTION}3.{COLOR_RESET} Restore libraryfolders.vdf only")
|
||||
print(f"{COLOR_SELECTION}4.{COLOR_RESET} Restore shortcuts.vdf only")
|
||||
print(f"{COLOR_SELECTION}0.{COLOR_RESET} Return to Main Menu")
|
||||
|
||||
choice = input(f"\n{COLOR_PROMPT}Enter your selection (0-4): {COLOR_RESET}").strip()
|
||||
|
||||
if choice == "1":
|
||||
self._restore_all_backups(cli_instance)
|
||||
elif choice == "2":
|
||||
self._restore_config_vdf(cli_instance)
|
||||
elif choice == "3":
|
||||
self._restore_libraryfolders_vdf(cli_instance)
|
||||
elif choice == "4":
|
||||
self._restore_shortcuts_vdf(cli_instance)
|
||||
elif choice == "0":
|
||||
break
|
||||
else:
|
||||
print("Invalid selection. Please try again.")
|
||||
input("\nPress Enter to continue...")
|
||||
|
||||
def _restore_all_backups(self, cli_instance):
|
||||
"""Restore all supported Steam config files"""
|
||||
self.logger.info("Recovery selected: Restore all Steam config files")
|
||||
print("\nAttempting to restore all supported Steam config files...")
|
||||
|
||||
# LEGACY BRIDGE: Use legacy handlers until backend migration complete
|
||||
paths_to_check = {
|
||||
"libraryfolders": self._get_library_vdf_path(cli_instance),
|
||||
"config": self._get_config_vdf_path(cli_instance),
|
||||
"shortcuts": self._get_shortcuts_vdf_path(cli_instance)
|
||||
}
|
||||
|
||||
restored_count = 0
|
||||
for file_type, file_path in paths_to_check.items():
|
||||
if file_path:
|
||||
print(f"Restoring {file_type} ({file_path})...")
|
||||
latest_backup = self._find_latest_backup(cli_instance, Path(file_path))
|
||||
if latest_backup:
|
||||
if self._restore_backup(cli_instance, latest_backup, Path(file_path)):
|
||||
print(f"Successfully restored {file_type}.")
|
||||
restored_count += 1
|
||||
else:
|
||||
print(f"{COLOR_ERROR}Failed to restore {file_type} from {latest_backup}.{COLOR_RESET}")
|
||||
else:
|
||||
print(f"No backup found for {file_type}.")
|
||||
else:
|
||||
print(f"Could not locate original file for {file_type} to restore.")
|
||||
|
||||
print(f"\nRestore process completed. {restored_count}/{len(paths_to_check)} files potentially restored.")
|
||||
input("\nPress Enter to continue...")
|
||||
|
||||
def _restore_config_vdf(self, cli_instance):
|
||||
"""Restore config.vdf only"""
|
||||
self.logger.info("Recovery selected: Restore config.vdf only")
|
||||
print("\nAttempting to restore config.vdf...")
|
||||
|
||||
file_path = self._get_config_vdf_path(cli_instance)
|
||||
if file_path:
|
||||
latest_backup = self._find_latest_backup(cli_instance, Path(file_path))
|
||||
if latest_backup:
|
||||
if self._restore_backup(cli_instance, latest_backup, Path(file_path)):
|
||||
print(f"Successfully restored config.vdf from {latest_backup}.")
|
||||
else:
|
||||
print(f"{COLOR_ERROR}Failed to restore config.vdf from {latest_backup}.{COLOR_RESET}")
|
||||
else:
|
||||
print("No backup found for config.vdf.")
|
||||
else:
|
||||
print("Could not locate config.vdf.")
|
||||
input("\nPress Enter to continue...")
|
||||
|
||||
def _restore_libraryfolders_vdf(self, cli_instance):
|
||||
"""Restore libraryfolders.vdf only"""
|
||||
self.logger.info("Recovery selected: Restore libraryfolders.vdf only")
|
||||
print("\nAttempting to restore libraryfolders.vdf...")
|
||||
|
||||
file_path = self._get_library_vdf_path(cli_instance)
|
||||
if file_path:
|
||||
latest_backup = self._find_latest_backup(cli_instance, Path(file_path))
|
||||
if latest_backup:
|
||||
if self._restore_backup(cli_instance, latest_backup, Path(file_path)):
|
||||
print(f"Successfully restored libraryfolders.vdf from {latest_backup}.")
|
||||
else:
|
||||
print(f"{COLOR_ERROR}Failed to restore libraryfolders.vdf from {latest_backup}.{COLOR_RESET}")
|
||||
else:
|
||||
print("No backup found for libraryfolders.vdf.")
|
||||
else:
|
||||
print("Could not locate libraryfolders.vdf.")
|
||||
input("\nPress Enter to continue...")
|
||||
|
||||
def _restore_shortcuts_vdf(self, cli_instance):
|
||||
"""Restore shortcuts.vdf only"""
|
||||
self.logger.info("Recovery selected: Restore shortcuts.vdf only")
|
||||
print("\nAttempting to restore shortcuts.vdf...")
|
||||
|
||||
file_path = self._get_shortcuts_vdf_path(cli_instance)
|
||||
if file_path:
|
||||
latest_backup = self._find_latest_backup(cli_instance, Path(file_path))
|
||||
if latest_backup:
|
||||
if self._restore_backup(cli_instance, latest_backup, Path(file_path)):
|
||||
print(f"Successfully restored shortcuts.vdf from {latest_backup}.")
|
||||
else:
|
||||
print(f"{COLOR_ERROR}Failed to restore shortcuts.vdf from {latest_backup}.{COLOR_RESET}")
|
||||
else:
|
||||
print("No backup found for shortcuts.vdf.")
|
||||
else:
|
||||
print("Could not locate shortcuts.vdf.")
|
||||
input("\nPress Enter to continue...")
|
||||
|
||||
# LEGACY BRIDGE methods - delegate to existing handlers
|
||||
def _get_library_vdf_path(self, cli_instance):
|
||||
"""LEGACY BRIDGE: Get libraryfolders.vdf path"""
|
||||
if hasattr(cli_instance, 'path_handler'):
|
||||
return cli_instance.path_handler.find_steam_library_vdf_path()
|
||||
return None
|
||||
|
||||
def _get_config_vdf_path(self, cli_instance):
|
||||
"""LEGACY BRIDGE: Get config.vdf path"""
|
||||
if hasattr(cli_instance, 'path_handler'):
|
||||
return cli_instance.path_handler.find_steam_config_vdf()
|
||||
return None
|
||||
|
||||
def _get_shortcuts_vdf_path(self, cli_instance):
|
||||
"""LEGACY BRIDGE: Get shortcuts.vdf path"""
|
||||
if hasattr(cli_instance, 'shortcut_handler'):
|
||||
return cli_instance.shortcut_handler._find_shortcuts_vdf()
|
||||
return None
|
||||
|
||||
def _find_latest_backup(self, cli_instance, file_path: Path):
|
||||
"""LEGACY BRIDGE: Find latest backup file"""
|
||||
if hasattr(cli_instance, 'filesystem_handler'):
|
||||
return cli_instance.filesystem_handler.find_latest_backup(file_path)
|
||||
return None
|
||||
|
||||
def _restore_backup(self, cli_instance, backup_path, target_path: Path) -> bool:
|
||||
"""LEGACY BRIDGE: Restore backup file"""
|
||||
if hasattr(cli_instance, 'filesystem_handler'):
|
||||
return cli_instance.filesystem_handler.restore_backup(backup_path, target_path)
|
||||
return False
|
||||
194
jackify/frontends/cli/menus/tuxborn_menu.py
Normal file
194
jackify/frontends/cli/menus/tuxborn_menu.py
Normal file
@@ -0,0 +1,194 @@
|
||||
"""
|
||||
Tuxborn Menu Handler for Jackify CLI Frontend
|
||||
Extracted from src.modules.menu_handler.MenuHandler.show_tuxborn_installer_menu()
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from jackify.shared.colors import (
|
||||
COLOR_SELECTION, COLOR_RESET, COLOR_INFO, COLOR_PROMPT, COLOR_WARNING
|
||||
)
|
||||
from jackify.shared.ui_utils import print_jackify_banner
|
||||
from jackify.backend.handlers.config_handler import ConfigHandler
|
||||
|
||||
class TuxbornMenuHandler:
|
||||
"""
|
||||
Handles the Tuxborn Automatic Installer workflow
|
||||
Extracted from legacy MenuHandler class
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = None # Will be set by CLI when needed
|
||||
|
||||
def show_tuxborn_installer_menu(self, cli_instance):
|
||||
"""
|
||||
Implements the Tuxborn Automatic Installer workflow.
|
||||
Prompts for install path, downloads path, and Nexus API key, then runs the one-shot install from start to finish
|
||||
|
||||
Args:
|
||||
cli_instance: Reference to main CLI instance for access to handlers
|
||||
"""
|
||||
# Import backend service
|
||||
from jackify.backend.core.modlist_operations import ModlistInstallCLI
|
||||
|
||||
print_jackify_banner()
|
||||
print(f"{COLOR_SELECTION}Tuxborn Automatic Installer{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}{'-'*32}{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}This will install the Tuxborn modlist using the custom Jackify Install Engine in one automated flow.{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}You will be prompted for the install location, downloads directory, and your Nexus API key.{COLOR_RESET}\n")
|
||||
|
||||
tuxborn_machineid = "Tuxborn/Tuxborn"
|
||||
tuxborn_modlist_name = "Tuxborn"
|
||||
|
||||
# Prompt for install directory
|
||||
print("----------------------------")
|
||||
config_handler = ConfigHandler()
|
||||
base_install_dir = Path(config_handler.get_modlist_install_base_dir())
|
||||
default_install_dir = base_install_dir / "Skyrim" / "Tuxborn"
|
||||
print(f"{COLOR_PROMPT}Please enter the directory you wish to use for Tuxborn installation.{COLOR_RESET}")
|
||||
print(f"(Default: {default_install_dir})")
|
||||
install_dir_result = self._get_directory_path_legacy(
|
||||
cli_instance,
|
||||
prompt_message=f"{COLOR_PROMPT}Install directory (Enter for default, 'q' to cancel): {COLOR_RESET}",
|
||||
default_path=default_install_dir,
|
||||
create_if_missing=True,
|
||||
no_header=True
|
||||
)
|
||||
if not install_dir_result:
|
||||
print(f"{COLOR_INFO}Cancelled by user.{COLOR_RESET}")
|
||||
input("Press Enter to return to the main menu...")
|
||||
return
|
||||
if isinstance(install_dir_result, tuple):
|
||||
install_dir, _ = install_dir_result # We'll use the path, creation handled by engine or later
|
||||
else:
|
||||
install_dir = install_dir_result
|
||||
|
||||
# Prompt for download directory
|
||||
print("----------------------------")
|
||||
base_download_dir = Path(config_handler.get_modlist_downloads_base_dir())
|
||||
default_download_dir = base_download_dir / "Tuxborn"
|
||||
print(f"{COLOR_PROMPT}Please enter the directory you wish to use for Tuxborn downloads.{COLOR_RESET}")
|
||||
print(f"(Default: {default_download_dir})")
|
||||
download_dir_result = self._get_directory_path_legacy(
|
||||
cli_instance,
|
||||
prompt_message=f"{COLOR_PROMPT}Download directory (Enter for default, 'q' to cancel): {COLOR_RESET}",
|
||||
default_path=default_download_dir,
|
||||
create_if_missing=True,
|
||||
no_header=True
|
||||
)
|
||||
if not download_dir_result:
|
||||
print(f"{COLOR_INFO}Cancelled by user.{COLOR_RESET}")
|
||||
input("Press Enter to return to the main menu...")
|
||||
return
|
||||
if isinstance(download_dir_result, tuple):
|
||||
download_dir, _ = download_dir_result # We'll use the path, creation handled by engine or later
|
||||
else:
|
||||
download_dir = download_dir_result
|
||||
|
||||
# Prompt for Nexus API key
|
||||
print("----------------------------")
|
||||
from jackify.backend.services.api_key_service import APIKeyService
|
||||
api_key_service = APIKeyService()
|
||||
saved_key = api_key_service.get_saved_api_key()
|
||||
api_key = None
|
||||
|
||||
if saved_key:
|
||||
print(f"{COLOR_INFO}A Nexus API Key is already saved.{COLOR_RESET}")
|
||||
use_saved = input(f"{COLOR_PROMPT}Use the saved API key? [Y/n]: {COLOR_RESET}").strip().lower()
|
||||
if use_saved in ('', 'y', 'yes'):
|
||||
api_key = saved_key
|
||||
else:
|
||||
new_key = input(f"{COLOR_PROMPT}Enter a new Nexus API Key (or press Enter to keep the saved one): {COLOR_RESET}").strip()
|
||||
if new_key:
|
||||
api_key = new_key
|
||||
replace = input(f"{COLOR_PROMPT}Replace the saved key with this one? [y/N]: {COLOR_RESET}").strip().lower()
|
||||
if replace == 'y':
|
||||
if api_key_service.save_api_key(api_key):
|
||||
print(f"{COLOR_INFO}API key saved successfully.{COLOR_RESET}")
|
||||
else:
|
||||
print(f"{COLOR_WARNING}Failed to save API key. Using for this session only.{COLOR_RESET}")
|
||||
else:
|
||||
print(f"{COLOR_INFO}Using new key for this session only. Saved key unchanged.{COLOR_RESET}")
|
||||
else:
|
||||
api_key = saved_key
|
||||
else:
|
||||
print(f"{COLOR_PROMPT}A Nexus Mods API key is required for downloading mods.{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}You can get your personal key at: {COLOR_SELECTION}https://www.nexusmods.com/users/myaccount?tab=api{COLOR_RESET}")
|
||||
print(f"{COLOR_WARNING}Your API Key is NOT saved locally. It is used only for this session unless you choose to save it.{COLOR_RESET}")
|
||||
api_key = input(f"{COLOR_PROMPT}Enter Nexus API Key (or 'q' to cancel): {COLOR_RESET}").strip()
|
||||
if not api_key or api_key.lower() == 'q':
|
||||
print(f"{COLOR_INFO}Cancelled by user.{COLOR_RESET}")
|
||||
input("Press Enter to return to the main menu...")
|
||||
return
|
||||
save = input(f"{COLOR_PROMPT}Would you like to save this API key for future use? [y/N]: {COLOR_RESET}").strip().lower()
|
||||
if save == 'y':
|
||||
if api_key_service.save_api_key(api_key):
|
||||
print(f"{COLOR_INFO}API key saved successfully.{COLOR_RESET}")
|
||||
else:
|
||||
print(f"{COLOR_WARNING}Failed to save API key. Using for this session only.{COLOR_RESET}")
|
||||
else:
|
||||
print(f"{COLOR_INFO}Using API key for this session only. It will not be saved.{COLOR_RESET}")
|
||||
|
||||
# Context for ModlistInstallCLI
|
||||
context = {
|
||||
'machineid': tuxborn_machineid,
|
||||
'modlist_name': tuxborn_modlist_name, # Will be used for shortcut name
|
||||
'install_dir': install_dir_result, # Pass tuple (path, create_flag) or path
|
||||
'download_dir': download_dir_result, # Pass tuple (path, create_flag) or path
|
||||
'nexus_api_key': api_key,
|
||||
'resolution': None
|
||||
}
|
||||
|
||||
modlist_cli = ModlistInstallCLI(self, getattr(cli_instance, 'steamdeck', False))
|
||||
|
||||
# run_discovery_phase will use context_override, display summary, and ask for confirmation.
|
||||
# If user confirms, it returns the context, otherwise None.
|
||||
confirmed_context = modlist_cli.run_discovery_phase(context_override=context)
|
||||
|
||||
if confirmed_context:
|
||||
if self.logger:
|
||||
self.logger.info("Tuxborn discovery confirmed by user. Proceeding to configuration/installation.")
|
||||
# The modlist_cli instance now holds the confirmed context.
|
||||
# configuration_phase will use modlist_cli.context
|
||||
modlist_cli.configuration_phase()
|
||||
# After configuration_phase, messages about success or next steps are handled within it or by _configure_new_modlist
|
||||
else:
|
||||
if self.logger:
|
||||
self.logger.info("Tuxborn discovery/confirmation cancelled or failed.")
|
||||
print(f"{COLOR_INFO}Tuxborn installation cancelled or not confirmed.{COLOR_RESET}")
|
||||
input(f"{COLOR_PROMPT}Press Enter to return to the main menu...{COLOR_RESET}")
|
||||
return
|
||||
|
||||
def _get_directory_path_legacy(self, cli_instance, prompt_message: str, default_path: Optional[Path],
|
||||
create_if_missing: bool = True, no_header: bool = False) -> Optional[Path]:
|
||||
"""
|
||||
LEGACY BRIDGE: Delegate to legacy menu handler until full backend migration
|
||||
|
||||
Args:
|
||||
cli_instance: Reference to main CLI instance
|
||||
prompt_message: The prompt to show user
|
||||
default_path: Default path if user presses Enter
|
||||
create_if_missing: Whether to create directory if it doesn't exist
|
||||
no_header: Whether to skip header display
|
||||
|
||||
Returns:
|
||||
Path object or None if cancelled
|
||||
"""
|
||||
# LEGACY BRIDGE: Use the original menu handler's method
|
||||
if hasattr(cli_instance, 'menu') and hasattr(cli_instance.menu, 'get_directory_path'):
|
||||
return cli_instance.menu.get_directory_path(
|
||||
prompt_message=prompt_message,
|
||||
default_path=default_path,
|
||||
create_if_missing=create_if_missing,
|
||||
no_header=no_header
|
||||
)
|
||||
else:
|
||||
# Fallback: simple input for now (will be replaced in future phases)
|
||||
response = input(prompt_message).strip()
|
||||
if response.lower() == 'q':
|
||||
return None
|
||||
elif response == '':
|
||||
return default_path
|
||||
else:
|
||||
return Path(response)
|
||||
115
jackify/frontends/cli/menus/wabbajack_menu.py
Normal file
115
jackify/frontends/cli/menus/wabbajack_menu.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
Wabbajack Tasks Menu Handler for Jackify CLI Frontend
|
||||
Extracted from src.modules.menu_handler.MenuHandler.show_wabbajack_tasks_menu()
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from jackify.shared.colors import (
|
||||
COLOR_SELECTION, COLOR_RESET, COLOR_ACTION, COLOR_PROMPT, COLOR_INFO
|
||||
)
|
||||
from jackify.shared.ui_utils import print_jackify_banner, print_section_header
|
||||
|
||||
class WabbajackMenuHandler:
|
||||
"""
|
||||
Handles the Modlist and Wabbajack Tasks menu
|
||||
Extracted from legacy MenuHandler class
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = None # Will be set by CLI when needed
|
||||
|
||||
def _clear_screen(self):
|
||||
"""Clear the terminal screen"""
|
||||
import os
|
||||
os.system('cls' if os.name == 'nt' else 'clear')
|
||||
|
||||
def show_wabbajack_tasks_menu(self, cli_instance):
|
||||
"""Show the Modlist and Wabbajack Tasks menu"""
|
||||
while True:
|
||||
self._clear_screen()
|
||||
print_jackify_banner()
|
||||
# Use print_section_header for consistency
|
||||
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_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_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
|
||||
# print(f"{COLOR_SELECTION}4.{COLOR_RESET} Install Wabbajack Application")
|
||||
# print(f" {COLOR_ACTION}→ Downloads and configures the Wabbajack app itself (via WINE){COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}0.{COLOR_RESET} Return to Main Menu")
|
||||
selection = input(f"\n{COLOR_PROMPT}Enter your selection (0-3): {COLOR_RESET}").strip()
|
||||
|
||||
if selection.lower() == 'q': # Allow 'q' to re-display menu
|
||||
continue
|
||||
|
||||
if selection == "1":
|
||||
self._execute_legacy_install_modlist(cli_instance)
|
||||
elif selection == "2":
|
||||
self._execute_legacy_configure_new_modlist(cli_instance)
|
||||
elif selection == "3":
|
||||
self._execute_legacy_configure_existing_modlist(cli_instance)
|
||||
# HIDDEN FOR FIRST RELEASE - UNCOMMENT WHEN READY
|
||||
# elif selection == "4":
|
||||
# self._execute_legacy_install_wabbajack(cli_instance)
|
||||
elif selection == "0":
|
||||
break
|
||||
else:
|
||||
print("Invalid selection. Please try again.")
|
||||
time.sleep(1)
|
||||
|
||||
def _execute_legacy_install_modlist(self, cli_instance):
|
||||
"""LEGACY BRIDGE: Execute modlist installation workflow"""
|
||||
# Import backend services
|
||||
from jackify.backend.core.modlist_operations import ModlistInstallCLI
|
||||
from jackify.backend.handlers.menu_handler import MenuHandler
|
||||
|
||||
# Create a proper MenuHandler instance with the required methods
|
||||
menu_handler = MenuHandler()
|
||||
|
||||
# Pass the MenuHandler instance and steamdeck status
|
||||
steamdeck_status = getattr(cli_instance, 'steamdeck', False)
|
||||
installer = ModlistInstallCLI(menu_handler, steamdeck_status)
|
||||
if self.logger:
|
||||
self.logger.debug("MenuHandler: ModlistInstallCLI instance created for Install a Modlist.")
|
||||
context = installer.run_discovery_phase()
|
||||
if context:
|
||||
if self.logger:
|
||||
self.logger.info("MenuHandler: Discovery phase complete, proceeding to configuration phase.")
|
||||
installer.configuration_phase()
|
||||
else:
|
||||
if self.logger:
|
||||
self.logger.info("MenuHandler: Discovery phase did not return context. Skipping configuration.")
|
||||
input("\nPress Enter to return to the Modlist Tasks menu...") # Standard return prompt
|
||||
|
||||
def _execute_legacy_install_wabbajack(self, cli_instance):
|
||||
"""LEGACY BRIDGE: Execute Wabbajack application installation"""
|
||||
if self.logger:
|
||||
self.logger.info("User selected 'Install Wabbajack' from Modlist Tasks menu.")
|
||||
# Add introductory text before calling the Wabbajack installation workflow
|
||||
self._clear_screen()
|
||||
print_jackify_banner()
|
||||
print_section_header("Install Wabbajack Application")
|
||||
print(f"{COLOR_INFO}This process will guide you through downloading and setting up\nthe Wabbajack application itself.{COLOR_RESET}")
|
||||
print("\n") # Spacer
|
||||
cli_instance._cmd_install_wabbajack(None) # Pass the cli_instance itself
|
||||
|
||||
def _execute_legacy_configure_new_modlist(self, cli_instance):
|
||||
"""LEGACY BRIDGE: Execute new modlist configuration"""
|
||||
# Import backend service
|
||||
from jackify.backend.handlers.menu_handler import ModlistMenuHandler
|
||||
|
||||
modlist_menu = ModlistMenuHandler(cli_instance.config_handler)
|
||||
modlist_menu._configure_new_modlist()
|
||||
|
||||
def _execute_legacy_configure_existing_modlist(self, cli_instance):
|
||||
"""LEGACY BRIDGE: Execute existing modlist configuration"""
|
||||
# Import backend service
|
||||
from jackify.backend.handlers.menu_handler import ModlistMenuHandler
|
||||
|
||||
modlist_menu = ModlistMenuHandler(cli_instance.config_handler)
|
||||
modlist_menu._configure_existing_modlist()
|
||||
9
jackify/frontends/cli/ui/__init__.py
Normal file
9
jackify/frontends/cli/ui/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
CLI UI Components for Jackify Frontend
|
||||
Shared UI utilities and components for command-line interface
|
||||
"""
|
||||
|
||||
# Currently empty - will be populated with UI helpers as needed
|
||||
# Examples: input validators, progress indicators, etc.
|
||||
|
||||
__all__ = []
|
||||
Reference in New Issue
Block a user