mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-08-14 02:33:42 +02:00
Release v0.7 - Tools Hub, Engine Choice, NXM Link Handling, Native Component Install, NSF/CSF Support
This commit is contained in:
@@ -1,164 +1,269 @@
|
||||
"""
|
||||
Configure Modlist Command
|
||||
"""Configure Modlist Command: CLI command for configuring a modlist post-install."""
|
||||
|
||||
CLI command for configuring a modlist post-install.
|
||||
Extracted from the original jackify-cli.py.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
from pathlib import Path
|
||||
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
|
||||
from jackify.shared.colors import (
|
||||
COLOR_INFO,
|
||||
COLOR_ERROR,
|
||||
COLOR_WARNING,
|
||||
COLOR_PROMPT,
|
||||
COLOR_RESET,
|
||||
COLOR_SUCCESS,
|
||||
)
|
||||
|
||||
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
|
||||
def __init__(self, backend_services):
|
||||
self.backend_services = backend_services
|
||||
self.test_mode = False
|
||||
|
||||
def add_parser(self, subparsers):
|
||||
p = subparsers.add_parser(
|
||||
"configure-modlist",
|
||||
help="Configure a modlist post-install (for GUI integration)",
|
||||
)
|
||||
p.add_argument("--modlist-name", type=str, required=True,
|
||||
help="Name of the modlist to configure (Steam shortcut name)")
|
||||
p.add_argument("--install-dir", type=str, required=True,
|
||||
help="Install directory of the modlist")
|
||||
p.add_argument("--download-dir", type=str, help="Downloads directory (optional)")
|
||||
p.add_argument("--nexus-api-key", type=str, help="Nexus API key (optional)")
|
||||
p.add_argument("--mo2-exe-path", type=str,
|
||||
help="Path to ModOrganizer.exe (for AppID lookup)")
|
||||
p.add_argument("--resolution", type=str, help="Resolution to set (optional)")
|
||||
p.add_argument("--skip-confirmation", action="store_true",
|
||||
help="Skip confirmation prompts")
|
||||
return p
|
||||
|
||||
def execute(self, args) -> int:
|
||||
logger.info("Starting non-interactive modlist configuration (CLI mode)")
|
||||
|
||||
try:
|
||||
context = self._build_context_from_args(args)
|
||||
result = self._execute_legacy_configuration(context)
|
||||
|
||||
if result is not True:
|
||||
logger.info("Finished non-interactive modlist configuration")
|
||||
return 1
|
||||
|
||||
logger.info("Finished non-interactive modlist configuration")
|
||||
|
||||
if not getattr(args, 'skip_confirmation', False) and context.get('install_dir'):
|
||||
from jackify.backend.handlers.modlist_install_cli_ttw import prompt_ttw_if_eligible
|
||||
prompt_ttw_if_eligible(context['install_dir'], context.get('modlist_name') or '')
|
||||
install_dir = context.get("install_dir", "") or ""
|
||||
modlist_name = context.get("modlist_name", "") or ""
|
||||
skip_confirm = bool(context.get("skip_confirmation", False))
|
||||
|
||||
if not install_dir:
|
||||
return 0
|
||||
|
||||
game_type = self._detect_game_type(install_dir)
|
||||
app_id = self._lookup_app_id(install_dir)
|
||||
|
||||
self._run_post_configure_hooks(
|
||||
install_dir, modlist_name, game_type, app_id, skip_confirm
|
||||
)
|
||||
|
||||
return 0
|
||||
|
||||
return 0 if result is True else 1
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to configure modlist: {e}")
|
||||
logger.error("Failed to configure modlist: %s", e)
|
||||
print(f"{COLOR_ERROR}Configuration failed: {e}{COLOR_RESET}")
|
||||
return 1
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Post-configure hooks (mirrors configuration_phase in install path)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _run_post_configure_hooks(
|
||||
self,
|
||||
install_dir: str,
|
||||
modlist_name: str,
|
||||
game_type: str,
|
||||
app_id: Optional[str],
|
||||
skip_confirm: bool,
|
||||
) -> None:
|
||||
# TTW
|
||||
try:
|
||||
from jackify.backend.handlers.modlist_install_cli_ttw import prompt_ttw_if_eligible
|
||||
prompt_ttw_if_eligible(install_dir, modlist_name)
|
||||
except Exception as e:
|
||||
logger.error("TTW post-install prompt failed: %s", e, exc_info=True)
|
||||
print(f"{COLOR_WARNING}TTW integration prompt failed. Check logs for details.{COLOR_RESET}")
|
||||
|
||||
# VNV
|
||||
try:
|
||||
from jackify.backend.services.vnv_integration_helper import (
|
||||
run_vnv_automation_if_applicable,
|
||||
should_offer_vnv_automation,
|
||||
)
|
||||
if should_offer_vnv_automation(modlist_name, Path(install_dir)):
|
||||
def _confirm_vnv(description: str) -> bool:
|
||||
print(f"\n{description}\n")
|
||||
try:
|
||||
ans = input(f"{COLOR_PROMPT}Run VNV post-install automation now? (Y/n): {COLOR_RESET}").strip().lower()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
return False
|
||||
return ans in ("", "y", "yes")
|
||||
|
||||
from jackify.backend.services.automated_prefix_service import AutomatedPrefixService
|
||||
automation_ran, vnv_error = run_vnv_automation_if_applicable(
|
||||
modlist_name=modlist_name,
|
||||
modlist_install_location=Path(install_dir),
|
||||
game_root=None,
|
||||
ttw_installer_path=AutomatedPrefixService.get_ttw_installer_path(),
|
||||
progress_callback=print,
|
||||
manual_file_callback=None,
|
||||
confirmation_callback=_confirm_vnv,
|
||||
)
|
||||
if automation_ran and not vnv_error:
|
||||
print(f"{COLOR_INFO}VNV post-install automation completed.{COLOR_RESET}")
|
||||
if vnv_error:
|
||||
print(f"{COLOR_WARNING}VNV automation encountered an error: {vnv_error}{COLOR_RESET}")
|
||||
except Exception as e:
|
||||
logger.error("VNV post-install automation failed: %s", e, exc_info=True)
|
||||
print(f"{COLOR_WARNING}VNV automation could not be completed. Check logs for details.{COLOR_RESET}")
|
||||
|
||||
# JContainers
|
||||
try:
|
||||
from jackify.backend.handlers.modlist_fixup_handler import (
|
||||
check_jcontainers_needs_fix,
|
||||
apply_jcontainers_fix,
|
||||
)
|
||||
if check_jcontainers_needs_fix(Path(install_dir), game_type):
|
||||
print(f"\n{COLOR_WARNING}JContainers Compatibility Fix{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}The mod JContainers has been detected. The Nexusmods version is known to cause crashes on Linux/Proton.{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}A fixed version is available from the mod's GitHub page. The original DLL will be backed up first.{COLOR_RESET}")
|
||||
try:
|
||||
user_input = input(f"{COLOR_PROMPT}Apply JContainers fix now? (Y/n): {COLOR_RESET}").strip().lower()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
user_input = "n"
|
||||
if user_input in ("", "y", "yes"):
|
||||
apply_jcontainers_fix(Path(install_dir), game_type)
|
||||
print(f"{COLOR_INFO}JContainers fix applied.{COLOR_RESET}")
|
||||
else:
|
||||
print(f"{COLOR_INFO}JContainers fix skipped.{COLOR_RESET}")
|
||||
except Exception as e:
|
||||
logger.warning("JContainers fix check failed (non-fatal): %s", e)
|
||||
|
||||
# Steam artwork
|
||||
if app_id:
|
||||
try:
|
||||
from jackify.backend.handlers.modlist_handler import ModlistHandler
|
||||
ModlistHandler().set_steam_grid_images(str(app_id), install_dir, game_type=game_type)
|
||||
logger.info("Steam artwork applied for app_id %s", app_id)
|
||||
except Exception as e:
|
||||
logger.warning("Steam artwork failed: %s", e)
|
||||
|
||||
# Install verification
|
||||
try:
|
||||
from jackify.backend.services.install_verifier_service import (
|
||||
run_install_verification,
|
||||
resolve_pfx_for_appid,
|
||||
)
|
||||
from jackify.frontends.cli.ui.indeterminate_status import CliIndeterminateStatus
|
||||
_pfx = resolve_pfx_for_appid(str(app_id)) if app_id else None
|
||||
if _pfx and _pfx.is_dir():
|
||||
_verif_result: list = [None]
|
||||
_spinner = CliIndeterminateStatus()
|
||||
_spinner.set("Running install verification...")
|
||||
|
||||
def _verif_worker() -> None:
|
||||
_verif_result[0] = run_install_verification(
|
||||
_pfx,
|
||||
Path(install_dir),
|
||||
game_type or "",
|
||||
str(app_id) if app_id else "",
|
||||
modlist_name,
|
||||
)
|
||||
|
||||
t = threading.Thread(target=_verif_worker, daemon=True)
|
||||
t.start()
|
||||
t.join()
|
||||
_spinner.stop()
|
||||
r = _verif_result[0]
|
||||
if r is not None:
|
||||
n_pass = len(r.passes) if hasattr(r, "passes") else 0
|
||||
n_warn = len(r.warnings) if hasattr(r, "warnings") else 0
|
||||
n_fail = len(r.failures) if hasattr(r, "failures") else 0
|
||||
print(f"{COLOR_INFO}Install verification: {n_pass} passed, {n_warn} warnings, {n_fail} failed{COLOR_RESET}")
|
||||
if hasattr(r, "failures"):
|
||||
for msg in r.failures:
|
||||
print(f" {COLOR_WARNING}[FAIL] {msg}{COLOR_RESET}")
|
||||
if hasattr(r, "warnings"):
|
||||
for msg in r.warnings:
|
||||
print(f" {COLOR_INFO}[WARN] {msg}{COLOR_RESET}")
|
||||
except Exception as e:
|
||||
logger.warning("Install verification failed: %s", e)
|
||||
|
||||
print(f"{COLOR_SUCCESS}Configuration completed successfully!{COLOR_RESET}")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _detect_game_type(self, install_dir: str) -> str:
|
||||
try:
|
||||
ini = os.path.join(install_dir, "ModOrganizer.ini")
|
||||
if os.path.isfile(ini):
|
||||
from jackify.backend.handlers.modlist_handler import ModlistHandler
|
||||
handler = ModlistHandler({})
|
||||
handler.modlist_ini = ini
|
||||
handler.modlist_dir = install_dir
|
||||
if handler._detect_game_variables():
|
||||
return handler.game_var_full or ""
|
||||
except Exception as e:
|
||||
logger.debug("Game type detection failed: %s", e)
|
||||
return ""
|
||||
|
||||
def _lookup_app_id(self, install_dir: str) -> Optional[str]:
|
||||
try:
|
||||
from jackify.backend.handlers.shortcut_handler import ShortcutHandler
|
||||
from jackify.backend.services.platform_detection_service import PlatformDetectionService
|
||||
platform_service = PlatformDetectionService.get_instance()
|
||||
sh = ShortcutHandler(steamdeck=platform_service.is_steamdeck, verbose=False)
|
||||
for sc in sh.find_shortcuts_by_exe("ModOrganizer.exe"):
|
||||
if os.path.realpath(sc.get("StartDir", "")) == os.path.realpath(install_dir):
|
||||
raw = sc.get("appid")
|
||||
if raw is not None:
|
||||
return str(int(raw) & 0xFFFFFFFF)
|
||||
except Exception as e:
|
||||
logger.debug("AppID lookup failed: %s", e)
|
||||
return None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Internals
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
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),
|
||||
"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
|
||||
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']
|
||||
return 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
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
"""List installed Wine components for a configured modlist prefix."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from jackify.shared.colors import (
|
||||
COLOR_ACTION,
|
||||
COLOR_ERROR,
|
||||
COLOR_INFO,
|
||||
COLOR_PROMPT,
|
||||
COLOR_RESET,
|
||||
COLOR_SELECTION,
|
||||
COLOR_WARNING,
|
||||
)
|
||||
from jackify.shared.ui_utils import clear_screen, print_jackify_banner, print_section_header
|
||||
|
||||
|
||||
class ListInstalledCommand:
|
||||
"""Show deduplicated component records for a modlist prefix."""
|
||||
|
||||
def run(self, appid: Optional[str] = None) -> None:
|
||||
clear_screen()
|
||||
print_jackify_banner()
|
||||
print_section_header("Installed Wine Components")
|
||||
|
||||
if appid:
|
||||
self._show_for_appid(appid, modlist_name=None)
|
||||
else:
|
||||
self._run_interactive()
|
||||
|
||||
def _run_interactive(self) -> None:
|
||||
from jackify.backend.handlers.modlist_handler import ModlistHandler
|
||||
|
||||
print(f"{COLOR_INFO}Discovering configured modlists...{COLOR_RESET}")
|
||||
try:
|
||||
handler = ModlistHandler()
|
||||
discovered = handler.discover_executable_shortcuts("ModOrganizer.exe")
|
||||
shortcuts = [
|
||||
{"name": m.get("name", "Unknown"), "appid": str(m.get("appid", ""))}
|
||||
for m in discovered
|
||||
if m.get("appid")
|
||||
]
|
||||
except Exception as exc:
|
||||
print(f"{COLOR_ERROR}Failed to discover modlists: {exc}{COLOR_RESET}")
|
||||
input("Press Enter to return to menu...")
|
||||
return
|
||||
|
||||
if not shortcuts:
|
||||
print(f"{COLOR_WARNING}No configured modlists found.{COLOR_RESET}")
|
||||
input("Press Enter to return to menu...")
|
||||
return
|
||||
|
||||
print()
|
||||
for i, s in enumerate(shortcuts, 1):
|
||||
print(f"{COLOR_SELECTION}{i}.{COLOR_RESET} {s['name']}")
|
||||
print(f" {COLOR_ACTION}AppID: {s['appid']}{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}0.{COLOR_RESET} Cancel")
|
||||
|
||||
selection = input(f"\n{COLOR_PROMPT}Select modlist (0-{len(shortcuts)}): {COLOR_RESET}").strip()
|
||||
if selection == "0" or not selection:
|
||||
return
|
||||
|
||||
try:
|
||||
idx = int(selection) - 1
|
||||
if idx < 0 or idx >= len(shortcuts):
|
||||
raise ValueError()
|
||||
except ValueError:
|
||||
print(f"{COLOR_ERROR}Invalid selection.{COLOR_RESET}")
|
||||
input("Press Enter to return to menu...")
|
||||
return
|
||||
|
||||
chosen = shortcuts[idx]
|
||||
self._show_for_appid(chosen["appid"], modlist_name=chosen["name"])
|
||||
|
||||
def _show_for_appid(self, appid: str, modlist_name: Optional[str]) -> None:
|
||||
from jackify.backend.handlers.path_handler import PathHandler
|
||||
|
||||
compat = PathHandler.find_compat_data(appid)
|
||||
if not compat:
|
||||
print(f"{COLOR_ERROR}Prefix not found for AppID {appid}.{COLOR_RESET}")
|
||||
input("Press Enter to continue...")
|
||||
return
|
||||
|
||||
pfx = compat / "pfx"
|
||||
|
||||
wt_log = pfx / "winetricks.log"
|
||||
wt_entries: List[str] = []
|
||||
if wt_log.is_file():
|
||||
seen: set = set()
|
||||
for line in wt_log.read_text(errors="replace").splitlines():
|
||||
entry = line.strip()
|
||||
if entry and entry not in seen:
|
||||
seen.add(entry)
|
||||
wt_entries.append(entry)
|
||||
|
||||
jc_path = pfx / "jackify_components.json"
|
||||
jc_data: Dict[str, dict] = {}
|
||||
if jc_path.is_file():
|
||||
try:
|
||||
jc_data = json.loads(jc_path.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
label = modlist_name or appid
|
||||
print(f"\n{COLOR_INFO}Installed components: {label}{COLOR_RESET}")
|
||||
if modlist_name:
|
||||
print(f"{COLOR_INFO}AppID: {appid}{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}Prefix: {pfx}{COLOR_RESET}\n")
|
||||
|
||||
if not wt_entries and not jc_data:
|
||||
print(f"{COLOR_WARNING}No component records found.{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}winetricks.log: {wt_log}{COLOR_RESET}")
|
||||
input("\nPress Enter to continue...")
|
||||
return
|
||||
|
||||
all_components: List[str] = list(wt_entries)
|
||||
for comp in jc_data:
|
||||
if comp not in all_components:
|
||||
all_components.append(comp)
|
||||
|
||||
col_w = max((len(c) for c in all_components), default=20)
|
||||
col_w = max(col_w, 20)
|
||||
header = f"{'Component':<{col_w}} {'Method':<12} Timestamp"
|
||||
print(f"{COLOR_SELECTION}{header}{COLOR_RESET}")
|
||||
print("-" * (col_w + 30))
|
||||
|
||||
for comp in all_components:
|
||||
if comp in jc_data:
|
||||
method = jc_data[comp].get("method", "native")
|
||||
ts = jc_data[comp].get("timestamp", "-")
|
||||
else:
|
||||
method = "winetricks"
|
||||
ts = "-"
|
||||
print(f"{comp:<{col_w}} {method:<12} {ts}")
|
||||
|
||||
wt_note = "[OK]" if wt_log.is_file() else "[MISSING]"
|
||||
jc_note = "[OK]" if jc_path.is_file() else "[MISSING]"
|
||||
print(f"\n{COLOR_INFO}Sources: {wt_note} winetricks.log {jc_note} jackify_components.json{COLOR_RESET}")
|
||||
input("\nPress Enter to continue...")
|
||||
@@ -370,7 +370,7 @@ class JackifyCLI:
|
||||
# Now that we have args, configure logging properly
|
||||
self._configure_logging_final()
|
||||
|
||||
logger.debug('Initializing Jackify CLI Frontend')
|
||||
logger.debug('Initialising Jackify CLI Frontend')
|
||||
logger.debug('JackifyCLI.run() called')
|
||||
logger.debug(f'Parsed args: {self.args}')
|
||||
|
||||
@@ -438,13 +438,10 @@ class JackifyCLI:
|
||||
print(" Then select: Additional Tasks > Install Wabbajack")
|
||||
return 0
|
||||
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
|
||||
from jackify.frontends.cli.commands.setup_mo2 import SetupMO2Command
|
||||
SetupMO2Command().run()
|
||||
return 0
|
||||
|
||||
elif command == "recovery":
|
||||
return self._handle_legacy_recovery(args)
|
||||
elif command == "test-protontricks":
|
||||
@@ -468,6 +465,8 @@ class JackifyCLI:
|
||||
# HIDDEN FOR FIRST RELEASE - UNCOMMENT WHEN READY
|
||||
elif choice == "additional":
|
||||
self.menus['additional'].show_additional_tasks_menu(self)
|
||||
elif choice == "tools_hub":
|
||||
self.menus['additional']._execute_tools_hub(self)
|
||||
else:
|
||||
logger.warning(f"Invalid choice '{choice}' received from show_main_menu.")
|
||||
|
||||
|
||||
@@ -29,33 +29,37 @@ class AdditionalMenuHandler:
|
||||
self._clear_screen()
|
||||
print_jackify_banner()
|
||||
print_section_header("Additional Tasks & Tools")
|
||||
print(f"{COLOR_INFO}Nexus Authentication, TTW Install & more{COLOR_RESET}\n")
|
||||
print(f"{COLOR_INFO}Modlist tools, diagnostics, and more{COLOR_RESET}\n")
|
||||
|
||||
print(f"{COLOR_SELECTION}1.{COLOR_RESET} Nexus Mods Authorization")
|
||||
print(f" {COLOR_ACTION}→ Authorize with Nexus using OAuth or manage API key{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}2.{COLOR_RESET} Tale of Two Wastelands (TTW) Installation")
|
||||
print(f" {COLOR_ACTION}→ Install TTW using TTW_Linux_Installer{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}3.{COLOR_RESET} Install Wabbajack Application")
|
||||
print(f" {COLOR_ACTION}→ Downloads and configures the Wabbajack app itself (via Proton){COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}4.{COLOR_RESET} Setup Mod Organizer 2")
|
||||
print(f" {COLOR_ACTION}→ Download and configure a standalone MO2 instance{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}5.{COLOR_RESET} Configure Tool Compatibility")
|
||||
print(f"{COLOR_SELECTION}1.{COLOR_RESET} Run Install Verifier")
|
||||
print(f" {COLOR_ACTION}→ Check an installed modlist for common configuration problems{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}2.{COLOR_RESET} Configure Tool Compatibility")
|
||||
print(f" {COLOR_ACTION}→ Apply Wine registry settings for xEdit, Synthesis, Pandora, Nemesis{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}3.{COLOR_RESET} Setup Mod Organizer 2")
|
||||
print(f" {COLOR_ACTION}→ Download and configure a standalone MO2 instance{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}4.{COLOR_RESET} Install Wabbajack Application")
|
||||
print(f" {COLOR_ACTION}→ Download the Wabbajack app under Proton — not needed for standard modlist installs{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}5.{COLOR_RESET} Create Diagnostic Bundle")
|
||||
print(f" {COLOR_ACTION}→ Package logs and system info for support{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}6.{COLOR_RESET} Nexus Mods Authorization")
|
||||
print(f" {COLOR_ACTION}→ Authorise with Nexus using OAuth or manage API key{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}0.{COLOR_RESET} Return to Main Menu")
|
||||
selection = input(f"\n{COLOR_PROMPT}Enter your selection (0-5): {COLOR_RESET}").strip()
|
||||
selection = input(f"\n{COLOR_PROMPT}Enter your selection (0-6): {COLOR_RESET}").strip()
|
||||
|
||||
if selection.lower() == 'q': # Allow 'q' to re-display menu
|
||||
continue
|
||||
if selection == "1":
|
||||
self._execute_nexus_authorization(cli_instance)
|
||||
self._execute_run_verifier()
|
||||
elif selection == "2":
|
||||
self._execute_ttw_install(cli_instance)
|
||||
elif selection == "3":
|
||||
self._execute_install_wabbajack(cli_instance)
|
||||
elif selection == "4":
|
||||
self._execute_setup_mo2(cli_instance)
|
||||
elif selection == "5":
|
||||
self._execute_configure_tool_compat(cli_instance)
|
||||
elif selection == "3":
|
||||
self._execute_setup_mo2(cli_instance)
|
||||
elif selection == "4":
|
||||
self._execute_install_wabbajack(cli_instance)
|
||||
elif selection == "5":
|
||||
self._execute_diagnostic_bundle()
|
||||
elif selection == "6":
|
||||
self._execute_nexus_authorization(cli_instance)
|
||||
elif selection == "0":
|
||||
break
|
||||
else:
|
||||
@@ -271,17 +275,17 @@ class AdditionalMenuHandler:
|
||||
|
||||
if authenticated:
|
||||
if method == 'oauth':
|
||||
print(f"\n{COLOR_SUCCESS}Status: Authorized via OAuth{COLOR_RESET}")
|
||||
print(f"\n{COLOR_SUCCESS}Status: Authorised via OAuth{COLOR_RESET}")
|
||||
if username:
|
||||
print(f"{COLOR_INFO}Logged in as: {username}{COLOR_RESET}")
|
||||
elif method == 'api_key':
|
||||
print(f"\n{COLOR_WARNING}Status: Using API Key (Legacy){COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}Consider switching to OAuth for better security{COLOR_RESET}")
|
||||
else:
|
||||
print(f"\n{COLOR_WARNING}Status: Not Authorized{COLOR_RESET}")
|
||||
print(f"\n{COLOR_WARNING}Status: Not Authorised{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}You need to authorize to download mods from Nexus{COLOR_RESET}")
|
||||
|
||||
print(f"\n{COLOR_SELECTION}1.{COLOR_RESET} Authorize with Nexus (OAuth)")
|
||||
print(f"\n{COLOR_SELECTION}1.{COLOR_RESET} Authorise with Nexus (OAuth)")
|
||||
print(f" {COLOR_ACTION}→ Opens browser for secure authorization{COLOR_RESET}")
|
||||
|
||||
if method == 'oauth':
|
||||
@@ -320,7 +324,7 @@ class AdditionalMenuHandler:
|
||||
# Get username
|
||||
_, _, username = auth_service.get_auth_status()
|
||||
if username:
|
||||
print(f"{COLOR_INFO}Authorized as: {username}{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}Authorised as: {username}{COLOR_RESET}")
|
||||
else:
|
||||
print(f"\n{COLOR_ERROR}OAuth authorization failed.{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}You can try again or use API key as fallback.{COLOR_RESET}")
|
||||
@@ -474,3 +478,122 @@ class AdditionalMenuHandler:
|
||||
print(f"\n{COLOR_ERROR}Tool compatibility configuration failed. Check logs for details.{COLOR_RESET}")
|
||||
|
||||
input("\nPress Enter to return to menu...")
|
||||
|
||||
def _execute_diagnostic_bundle(self) -> None:
|
||||
from jackify.backend.services.diagnostic_service import build_bundle
|
||||
from jackify.shared.colors import COLOR_ERROR, COLOR_SUCCESS
|
||||
|
||||
self._clear_screen()
|
||||
print_jackify_banner()
|
||||
print_section_header("Create Diagnostic Bundle")
|
||||
print(f"{COLOR_INFO}Collecting logs, system info, and prefix records...{COLOR_RESET}\n")
|
||||
|
||||
try:
|
||||
bundle_path = build_bundle()
|
||||
except Exception as exc:
|
||||
print(f"{COLOR_ERROR}Failed to create bundle: {exc}{COLOR_RESET}")
|
||||
input("Press Enter to return to menu...")
|
||||
return
|
||||
|
||||
print(f"{COLOR_SUCCESS}Bundle created:{COLOR_RESET} {bundle_path}")
|
||||
print(f"\n{COLOR_INFO}Attach this file when reporting an issue.{COLOR_RESET}")
|
||||
|
||||
input("\nPress Enter to return to menu...")
|
||||
|
||||
def _execute_run_verifier(self) -> None:
|
||||
from jackify.backend.services.install_verifier_service import (
|
||||
_load_verifier, run_install_verification, resolve_pfx_for_appid,
|
||||
)
|
||||
from jackify.shared.colors import COLOR_ERROR, COLOR_SUCCESS, COLOR_WARNING
|
||||
import threading
|
||||
|
||||
self._clear_screen()
|
||||
print_jackify_banner()
|
||||
print_section_header("Run Install Verifier")
|
||||
print(f"{COLOR_INFO}Discovering configured modlists...{COLOR_RESET}")
|
||||
|
||||
try:
|
||||
vmod = _load_verifier()
|
||||
modlists = vmod.discover_installed_modlists()
|
||||
except Exception as e:
|
||||
print(f"{COLOR_ERROR}Failed to discover modlists: {e}{COLOR_RESET}")
|
||||
input("Press Enter to return to menu...")
|
||||
return
|
||||
|
||||
if not modlists:
|
||||
print(f"{COLOR_WARNING}No configured modlists found.{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}Install and configure a modlist first.{COLOR_RESET}")
|
||||
input("Press Enter to return to menu...")
|
||||
return
|
||||
|
||||
print()
|
||||
for i, m in enumerate(modlists, 1):
|
||||
print(f"{COLOR_SELECTION}{i}.{COLOR_RESET} {m.get('name', 'Unknown')}")
|
||||
print(f"{COLOR_SELECTION}0.{COLOR_RESET} Cancel")
|
||||
|
||||
selection = input(f"\n{COLOR_PROMPT}Select modlist (0-{len(modlists)}): {COLOR_RESET}").strip()
|
||||
|
||||
if selection == "0" or not selection:
|
||||
return
|
||||
|
||||
try:
|
||||
idx = int(selection) - 1
|
||||
if idx < 0 or idx >= len(modlists):
|
||||
raise ValueError()
|
||||
except ValueError:
|
||||
print(f"{COLOR_ERROR}Invalid selection.{COLOR_RESET}")
|
||||
input("Press Enter to return to menu...")
|
||||
return
|
||||
|
||||
chosen = modlists[idx]
|
||||
appid = chosen.get("appid", "")
|
||||
modlist_dir = chosen.get("modlist_dir")
|
||||
game_type = chosen.get("game_type", "unknown")
|
||||
name = chosen.get("name", "Unknown")
|
||||
|
||||
pfx = resolve_pfx_for_appid(appid) if appid else None
|
||||
if not pfx or not pfx.is_dir():
|
||||
print(f"{COLOR_WARNING}No Wine prefix found for {name}. Cannot run verifier.{COLOR_RESET}")
|
||||
input("Press Enter to return to menu...")
|
||||
return
|
||||
|
||||
print(f"\n{COLOR_INFO}Running install verification for: {name}{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}This may take a moment...{COLOR_RESET}\n")
|
||||
|
||||
try:
|
||||
result_holder = [None]
|
||||
def _worker():
|
||||
result_holder[0] = run_install_verification(pfx, modlist_dir, game_type, appid, name)
|
||||
t = threading.Thread(target=_worker, daemon=True)
|
||||
t.start()
|
||||
t.join()
|
||||
r = result_holder[0]
|
||||
if r is None:
|
||||
print(f"{COLOR_WARNING}Verifier returned no results.{COLOR_RESET}")
|
||||
else:
|
||||
passes = r.passes if hasattr(r, 'passes') else []
|
||||
warnings = r.warnings if hasattr(r, 'warnings') else []
|
||||
failures = r.failures if hasattr(r, 'failures') else []
|
||||
total = len(passes) + len(warnings) + len(failures)
|
||||
print(f"--- Install Verification: {name} ---")
|
||||
print(f" {len(passes)} passed, {len(warnings)} warnings, {len(failures)} failed (of {total} checks)\n")
|
||||
for msg in failures:
|
||||
print(f"{COLOR_ERROR} [FAIL] {msg}{COLOR_RESET}")
|
||||
for msg in warnings:
|
||||
print(f"{COLOR_WARNING} [WARN] {msg}{COLOR_RESET}")
|
||||
if not failures and not warnings:
|
||||
print(f"{COLOR_SUCCESS} All checks passed.{COLOR_RESET}")
|
||||
if passes:
|
||||
show_all = input(f"\n{COLOR_PROMPT}Show all {len(passes)} passed checks? (y/N): {COLOR_RESET}").strip().lower()
|
||||
if show_all in ('y', 'yes'):
|
||||
print()
|
||||
for msg in passes:
|
||||
print(f"{COLOR_DISABLED} [OK] {msg}{COLOR_RESET}")
|
||||
except Exception as e:
|
||||
print(f"{COLOR_ERROR}Verifier error: {e}{COLOR_RESET}")
|
||||
|
||||
input("\nPress Enter to return to menu...")
|
||||
|
||||
def _execute_tools_hub(self, cli_instance) -> None:
|
||||
from jackify.frontends.cli.menus.tools_hub_menu import ToolsHubMenuHandler
|
||||
ToolsHubMenuHandler().show_tools_hub_menu(cli_instance)
|
||||
|
||||
@@ -43,16 +43,20 @@ class MainMenuHandler:
|
||||
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} Additional Tasks & Tools")
|
||||
print(f" {COLOR_ACTION}→ Nexus OAuth, TTW Installation, Install Wabbajack{COLOR_RESET}")
|
||||
print(f" {COLOR_ACTION}→ Verifier, Diagnostics, Nexus OAuth & more{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}3.{COLOR_RESET} Tools Hub")
|
||||
print(f" {COLOR_ACTION}→ Install, update, or switch engines and tools{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}0.{COLOR_RESET} Exit Jackify")
|
||||
choice = input(f"\n{COLOR_PROMPT}Enter your selection (0-2): {COLOR_RESET}").strip()
|
||||
|
||||
choice = input(f"\n{COLOR_PROMPT}Enter your selection (0-3): {COLOR_RESET}").strip()
|
||||
|
||||
if choice.lower() == 'q': # Allow 'q' to re-display menu
|
||||
continue
|
||||
if choice == "1":
|
||||
return "wabbajack"
|
||||
elif choice == "2":
|
||||
return "additional"
|
||||
elif choice == "3":
|
||||
return "tools_hub"
|
||||
elif choice == "0":
|
||||
return "exit"
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
"""
|
||||
Tools Hub Menu Handler for Jackify CLI Frontend
|
||||
"""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from jackify.shared.colors import (
|
||||
COLOR_SELECTION, COLOR_RESET, COLOR_ACTION, COLOR_PROMPT,
|
||||
COLOR_INFO, COLOR_DISABLED, COLOR_WARNING, COLOR_ERROR, COLOR_SUCCESS
|
||||
)
|
||||
from jackify.shared.ui_utils import print_jackify_banner, print_section_header, clear_screen
|
||||
from jackify.frontends.cli.ui.indeterminate_status import CliIndeterminateStatus
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ToolsHubMenuHandler:
|
||||
"""CLI menu for managing third-party tools via ToolRegistry."""
|
||||
|
||||
def show_tools_hub_menu(self, cli_instance) -> None:
|
||||
from jackify.backend.services.tool_registry import (
|
||||
ToolRegistry, get_active_engine_id
|
||||
)
|
||||
registry = ToolRegistry()
|
||||
|
||||
while True:
|
||||
clear_screen()
|
||||
print_jackify_banner()
|
||||
print_section_header("Third Party Tools Hub")
|
||||
|
||||
statuses = [s for s in registry.get_all_statuses() if not s.definition.hidden]
|
||||
active_engine_id = get_active_engine_id()
|
||||
|
||||
print(f"{COLOR_INFO}Active engine: {active_engine_id}{COLOR_RESET}\n")
|
||||
|
||||
for i, status in enumerate(statuses, 1):
|
||||
defn = status.definition
|
||||
if status.installed:
|
||||
ver = status.installed_version or "unknown"
|
||||
active_tag = (
|
||||
f" {COLOR_ACTION}[ACTIVE]{COLOR_RESET}"
|
||||
if defn.is_engine and defn.tool_id == active_engine_id
|
||||
else ""
|
||||
)
|
||||
print(f"{COLOR_SELECTION}{i}.{COLOR_RESET} {defn.display_name} "
|
||||
f"{COLOR_INFO}v{ver}{COLOR_RESET}{active_tag}")
|
||||
else:
|
||||
print(f"{COLOR_SELECTION}{i}.{COLOR_RESET} "
|
||||
f"{COLOR_DISABLED}{defn.display_name} [not installed]{COLOR_RESET}")
|
||||
print(f" {COLOR_ACTION}{defn.description}{COLOR_RESET}")
|
||||
|
||||
print(f"\n{COLOR_SELECTION}0.{COLOR_RESET} Return to Main Menu")
|
||||
selection = input(
|
||||
f"\n{COLOR_PROMPT}Select tool (0-{len(statuses)}): {COLOR_RESET}"
|
||||
).strip()
|
||||
|
||||
if selection == "0":
|
||||
break
|
||||
if selection.lower() == "q":
|
||||
continue
|
||||
|
||||
try:
|
||||
idx = int(selection) - 1
|
||||
if idx < 0 or idx >= len(statuses):
|
||||
raise ValueError()
|
||||
except ValueError:
|
||||
print(f"{COLOR_ERROR}Invalid selection.{COLOR_RESET}")
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
self._show_tool_detail(registry, statuses[idx], active_engine_id, cli_instance)
|
||||
|
||||
def _show_tool_detail(self, registry, status, active_engine_id: str, cli_instance=None) -> None:
|
||||
from jackify.backend.services.tool_registry import (
|
||||
get_active_engine_id, set_active_engine_id
|
||||
)
|
||||
|
||||
defn = status.definition
|
||||
|
||||
while True:
|
||||
clear_screen()
|
||||
print_jackify_banner()
|
||||
print_section_header(defn.display_name)
|
||||
print(f"{COLOR_INFO}{defn.description}{COLOR_RESET}\n")
|
||||
|
||||
if status.installed:
|
||||
ver = status.installed_version or "unknown"
|
||||
print(f"Installed: {COLOR_INFO}{ver}{COLOR_RESET}")
|
||||
if status.previous_version:
|
||||
print(f"Previous: {COLOR_DISABLED}{status.previous_version}{COLOR_RESET}")
|
||||
if defn.is_engine and defn.tool_id == active_engine_id:
|
||||
print(f"Engine: {COLOR_ACTION}[ACTIVE]{COLOR_RESET}")
|
||||
else:
|
||||
print(f"Status: {COLOR_DISABLED}Not installed{COLOR_RESET}")
|
||||
|
||||
latest = self._fetch_latest_with_spinner(registry, defn.tool_id)
|
||||
|
||||
if latest:
|
||||
latest_clean = latest.lstrip("v")
|
||||
current_clean = (status.installed_version or "").lstrip("v")
|
||||
update_available = status.installed and latest_clean != current_clean
|
||||
update_tag = (
|
||||
f" {COLOR_WARNING}[update available]{COLOR_RESET}" if update_available
|
||||
else f" {COLOR_ACTION}[up to date]{COLOR_RESET}" if status.installed
|
||||
else ""
|
||||
)
|
||||
print(f"Latest: {COLOR_INFO}{latest}{COLOR_RESET}{update_tag}")
|
||||
else:
|
||||
update_available = False
|
||||
print(f"Latest: {COLOR_DISABLED}(could not check){COLOR_RESET}")
|
||||
|
||||
options: dict = {}
|
||||
opt_num = 1
|
||||
print()
|
||||
|
||||
if not status.installed:
|
||||
print(f"{COLOR_SELECTION}{opt_num}.{COLOR_RESET} Install")
|
||||
options[str(opt_num)] = "install"
|
||||
opt_num += 1
|
||||
else:
|
||||
if latest and update_available:
|
||||
print(f"{COLOR_SELECTION}{opt_num}.{COLOR_RESET} Update to {latest}")
|
||||
options[str(opt_num)] = "update"
|
||||
opt_num += 1
|
||||
if status.previous_version and defn.can_uninstall:
|
||||
print(f"{COLOR_SELECTION}{opt_num}.{COLOR_RESET} "
|
||||
f"Downgrade to {status.previous_version}")
|
||||
options[str(opt_num)] = "downgrade"
|
||||
opt_num += 1
|
||||
if defn.can_uninstall:
|
||||
print(f"{COLOR_SELECTION}{opt_num}.{COLOR_RESET} Uninstall")
|
||||
options[str(opt_num)] = "uninstall"
|
||||
opt_num += 1
|
||||
if defn.is_engine and defn.tool_id != active_engine_id:
|
||||
print(f"{COLOR_SELECTION}{opt_num}.{COLOR_RESET} Set as active engine")
|
||||
options[str(opt_num)] = "set_active"
|
||||
opt_num += 1
|
||||
if defn.tool_id == "ttw_installer":
|
||||
print(f"{COLOR_SELECTION}{opt_num}.{COLOR_RESET} Run TTW Installation")
|
||||
options[str(opt_num)] = "run_ttw"
|
||||
opt_num += 1
|
||||
|
||||
print(f"{COLOR_SELECTION}0.{COLOR_RESET} Back")
|
||||
|
||||
if not options:
|
||||
input(f"\n{COLOR_PROMPT}Press Enter to go back...{COLOR_RESET}")
|
||||
break
|
||||
|
||||
selection = input(f"\n{COLOR_PROMPT}Enter selection: {COLOR_RESET}").strip()
|
||||
|
||||
if selection == "0":
|
||||
break
|
||||
|
||||
action = options.get(selection)
|
||||
if not action:
|
||||
print(f"{COLOR_ERROR}Invalid selection.{COLOR_RESET}")
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
if action == "run_ttw":
|
||||
from jackify.frontends.cli.menus.additional_menu import AdditionalMenuHandler
|
||||
AdditionalMenuHandler()._execute_ttw_install(cli_instance)
|
||||
status = registry.get_status(defn.tool_id) or status
|
||||
continue
|
||||
|
||||
if action == "set_active":
|
||||
try:
|
||||
set_active_engine_id(defn.tool_id)
|
||||
active_engine_id = defn.tool_id
|
||||
print(f"\n{COLOR_SUCCESS}Active engine set to {defn.display_name}.{COLOR_RESET}")
|
||||
except Exception as e:
|
||||
print(f"\n{COLOR_ERROR}Failed to set active engine: {e}{COLOR_RESET}")
|
||||
input(f"{COLOR_PROMPT}Press Enter to continue...{COLOR_RESET}")
|
||||
status = registry.get_status(defn.tool_id) or status
|
||||
continue
|
||||
|
||||
if action == "uninstall":
|
||||
confirm = input(
|
||||
f"\n{COLOR_WARNING}Uninstall {defn.display_name}? (y/N): {COLOR_RESET}"
|
||||
).strip().lower()
|
||||
if confirm not in ("y", "yes"):
|
||||
print(f"{COLOR_INFO}Cancelled.{COLOR_RESET}")
|
||||
time.sleep(0.8)
|
||||
status = registry.get_status(defn.tool_id) or status
|
||||
continue
|
||||
|
||||
ok, msg = self._run_with_spinner(registry, action, defn.tool_id, defn.display_name)
|
||||
|
||||
if ok:
|
||||
print(f"\n{COLOR_SUCCESS}{msg}{COLOR_RESET}")
|
||||
else:
|
||||
print(f"\n{COLOR_ERROR}{msg}{COLOR_RESET}")
|
||||
|
||||
input(f"{COLOR_PROMPT}Press Enter to continue...{COLOR_RESET}")
|
||||
status = registry.get_status(defn.tool_id) or status
|
||||
active_engine_id = get_active_engine_id()
|
||||
|
||||
def _fetch_latest_with_spinner(self, registry, tool_id: str) -> Optional[str]:
|
||||
result: list = [None]
|
||||
spinner = CliIndeterminateStatus()
|
||||
spinner.set("Checking latest version...")
|
||||
|
||||
def _worker():
|
||||
try:
|
||||
result[0] = registry.check_latest_version(tool_id)
|
||||
except Exception:
|
||||
result[0] = None
|
||||
|
||||
thread = threading.Thread(target=_worker, daemon=True)
|
||||
thread.start()
|
||||
thread.join()
|
||||
spinner.stop()
|
||||
return result[0]
|
||||
|
||||
def _run_with_spinner(
|
||||
self, registry, action: str, tool_id: str, display_name: str
|
||||
) -> Tuple[bool, str]:
|
||||
labels = {
|
||||
"install": f"Installing {display_name}",
|
||||
"update": f"Updating {display_name}",
|
||||
"downgrade": f"Downgrading {display_name}",
|
||||
"uninstall": f"Uninstalling {display_name}",
|
||||
}
|
||||
label = labels.get(action, f"Working on {display_name}")
|
||||
result: list = [False, ""]
|
||||
spinner = CliIndeterminateStatus()
|
||||
spinner.set(label)
|
||||
|
||||
def _worker():
|
||||
try:
|
||||
if action == "install":
|
||||
result[0], result[1] = registry.install(tool_id)
|
||||
elif action == "update":
|
||||
result[0], result[1] = registry.update(tool_id)
|
||||
elif action == "downgrade":
|
||||
result[0], result[1] = registry.downgrade(tool_id)
|
||||
elif action == "uninstall":
|
||||
result[0], result[1] = registry.uninstall(tool_id)
|
||||
except Exception as e:
|
||||
result[0], result[1] = False, str(e)
|
||||
|
||||
thread = threading.Thread(target=_worker, daemon=True)
|
||||
thread.start()
|
||||
thread.join()
|
||||
spinner.stop()
|
||||
return result[0], result[1]
|
||||
@@ -31,7 +31,7 @@ class WabbajackMenuHandler:
|
||||
# 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_SELECTION}1.{COLOR_RESET} Install a Modlist")
|
||||
print(f" {COLOR_ACTION}→ Install a modlist in full: Select from a list or provide a .wabbajack file{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}2.{COLOR_RESET} Configure New Modlist (Post-Download)")
|
||||
print(f" {COLOR_ACTION}→ Modlist already downloaded? Configure and add to Steam{COLOR_RESET}")
|
||||
|
||||
Reference in New Issue
Block a user