mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-06-08 01:37:44 +02:00
398 lines
20 KiB
Python
398 lines
20 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Menu Handler Module
|
|
Handles CLI menu system for Jackify
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import logging
|
|
import time
|
|
import subprocess # Add subprocess import
|
|
import argparse
|
|
import re
|
|
from typing import List, Dict, Optional
|
|
from pathlib import Path
|
|
import glob # Add for the simpler tab completion
|
|
|
|
# Import colors from the new central location
|
|
from .ui_colors import (
|
|
COLOR_PROMPT, COLOR_SELECTION, COLOR_RESET, COLOR_INFO, COLOR_ERROR,
|
|
COLOR_SUCCESS, COLOR_WARNING, COLOR_DISABLED, COLOR_ACTION, COLOR_INPUT
|
|
)
|
|
|
|
# Import our modules
|
|
# Ensure these imports are correct based on your project structure
|
|
from .modlist_handler import ModlistHandler
|
|
from .shortcut_handler import ShortcutHandler
|
|
from .config_handler import ConfigHandler
|
|
from .filesystem_handler import FileSystemHandler
|
|
from .resolution_handler import ResolutionHandler
|
|
from .protontricks_handler import ProtontricksHandler
|
|
from .path_handler import PathHandler
|
|
from .vdf_handler import VDFHandler
|
|
from jackify.shared.ui_utils import print_section_header
|
|
from .completers import path_completer
|
|
|
|
try:
|
|
import readline
|
|
except ImportError:
|
|
readline = None
|
|
|
|
# Define exports for this module
|
|
__all__ = [
|
|
'MenuHandler',
|
|
'ModlistMenuHandler',
|
|
'simple_path_completer' # Export the function without underscore
|
|
]
|
|
|
|
# Initialize logger
|
|
logger = logging.getLogger(__name__)
|
|
|
|
from .menu_handler_input import (
|
|
basic_input_prompt, input_prompt, simple_path_completer,
|
|
READLINE_AVAILABLE, READLINE_HAS_PROMPT, READLINE_HAS_DISPLAY_HOOK,
|
|
)
|
|
from .menu_handler_modlist import ModlistMenuHandler
|
|
|
|
class MenuHandler:
|
|
"""
|
|
Handles CLI menu display and interaction
|
|
"""
|
|
|
|
def __init__(self, logger_instance=None):
|
|
if logger_instance:
|
|
self.logger = logger_instance
|
|
else:
|
|
self.logger = logging.getLogger(__name__)
|
|
self.config_handler = ConfigHandler()
|
|
self.shortcut_handler = ShortcutHandler(
|
|
steamdeck=self.config_handler.settings.get('steamdeck', False),
|
|
verbose=False
|
|
)
|
|
|
|
def display_banner(self):
|
|
"""Display the application banner - DEPRECATED: Banner display should be handled by frontend"""
|
|
os.system('cls' if os.name == 'nt' else 'clear')
|
|
# Banner display handled by frontend
|
|
|
|
|
|
|
|
|
|
|
|
def _show_recovery_menu(self, cli_instance):
|
|
"""Show the recovery tools menu."""
|
|
while True:
|
|
self._clear_screen()
|
|
# Banner display handled by frontend
|
|
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":
|
|
logger.info("Recovery selected: Restore all Steam config files")
|
|
print("\nAttempting to restore all supported Steam config files...")
|
|
# Logic to find and restore backups for all three files
|
|
paths_to_check = {
|
|
"libraryfolders": cli_instance.path_handler.find_steam_library_vdf_path(), # Need method to find vdf itself
|
|
"config": cli_instance.path_handler.find_steam_config_vdf(),
|
|
"shortcuts": cli_instance.shortcut_handler._find_shortcuts_vdf() # Assumes this returns the path
|
|
}
|
|
restored_count = 0
|
|
for file_type, file_path in paths_to_check.items():
|
|
if file_path:
|
|
print(f"Restoring {file_type} ({file_path})...")
|
|
# Find latest backup (needs helper function)
|
|
latest_backup = cli_instance.filesystem_handler.find_latest_backup(Path(file_path))
|
|
if latest_backup:
|
|
if cli_instance.filesystem_handler.restore_backup(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...")
|
|
elif choice == "2":
|
|
logger.info("Recovery selected: Restore config.vdf only")
|
|
print("\nAttempting to restore config.vdf...")
|
|
# Logic for config.vdf
|
|
file_path = cli_instance.path_handler.find_steam_config_vdf()
|
|
if file_path:
|
|
latest_backup = cli_instance.filesystem_handler.find_latest_backup(Path(file_path))
|
|
if latest_backup:
|
|
if cli_instance.filesystem_handler.restore_backup(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...")
|
|
elif choice == "3":
|
|
logger.info("Recovery selected: Restore libraryfolders.vdf only")
|
|
print("\nAttempting to restore libraryfolders.vdf...")
|
|
# Logic for libraryfolders.vdf
|
|
file_path = cli_instance.path_handler.find_steam_library_vdf_path()
|
|
if file_path:
|
|
latest_backup = cli_instance.filesystem_handler.find_latest_backup(Path(file_path))
|
|
if latest_backup:
|
|
if cli_instance.filesystem_handler.restore_backup(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...")
|
|
elif choice == "4":
|
|
logger.info("Recovery selected: Restore shortcuts.vdf only")
|
|
print("\nAttempting to restore shortcuts.vdf...")
|
|
# Logic for shortcuts.vdf
|
|
file_path = cli_instance.shortcut_handler._find_shortcuts_vdf()
|
|
if file_path:
|
|
latest_backup = cli_instance.filesystem_handler.find_latest_backup(Path(file_path))
|
|
if latest_backup:
|
|
if cli_instance.filesystem_handler.restore_backup(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...")
|
|
elif choice == "0":
|
|
logger.info("Returning to main menu from recovery.")
|
|
break # Exit recovery menu loop
|
|
else:
|
|
logger.warning(f"Invalid recovery menu selection: {choice}")
|
|
print("\nInvalid selection. Please try again.")
|
|
time.sleep(1)
|
|
|
|
def get_input_with_default(self, prompt, default=None):
|
|
"""
|
|
Get user input with an optional default value.
|
|
Returns the user input or the default value, or None if cancelled by 'q'.
|
|
"""
|
|
print("\n" + "-" * 28) # Separator
|
|
print(f"{COLOR_PROMPT}{prompt}{COLOR_RESET}") # Main prompt message
|
|
if default is not None:
|
|
print(f"{COLOR_INFO}(Default: {default}){COLOR_RESET}")
|
|
|
|
try:
|
|
# Consistent input line
|
|
user_input = input(f"{COLOR_PROMPT}Enter value (or 'q' to cancel, Enter for default): {COLOR_RESET}").strip()
|
|
if user_input.lower() == 'q':
|
|
self.logger.info(f"User cancelled input for prompt: '{prompt}'")
|
|
print(f"{COLOR_INFO}Input cancelled by user.{COLOR_RESET}")
|
|
return None # Explicit None for cancellation
|
|
return user_input if user_input else default
|
|
except KeyboardInterrupt:
|
|
self.logger.info(f"User cancelled input via Ctrl+C for prompt: '{prompt}'")
|
|
print("\nInput cancelled.")
|
|
return None # Explicit None for cancellation
|
|
|
|
def show_progress(self, step, percent, message):
|
|
"""
|
|
Display a progress bar with the current step and message
|
|
"""
|
|
# Ensure percent is within bounds
|
|
percent = max(0, min(100, int(percent)))
|
|
bar_length = 50
|
|
filled_length = int(bar_length * percent / 100)
|
|
bar = '=' * filled_length + ' ' * (bar_length - filled_length)
|
|
|
|
# Use \r to return to the beginning of the line, \033[K to clear the rest
|
|
print(f"\r\033[K[{bar}] {percent}% - {step}: {message}", end='')
|
|
if percent == 100:
|
|
print() # Add a newline when complete
|
|
sys.stdout.flush()
|
|
|
|
def _clear_screen(self):
|
|
"""Clears the terminal screen with fallbacks."""
|
|
self.logger.debug(f"_clear_screen: Detected os.name: {os.name}")
|
|
if os.name == 'nt':
|
|
self.logger.debug("_clear_screen: Clearing screen for NT by attempting command: cls via os.system")
|
|
os.system('cls')
|
|
else:
|
|
try:
|
|
# Attempt 1: Specific path to clear
|
|
self.logger.debug("_clear_screen: Attempting /usr/bin/clear")
|
|
subprocess.run(['/usr/bin/clear'], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
self.logger.debug("_clear_screen: /usr/bin/clear succeeded")
|
|
return
|
|
except FileNotFoundError:
|
|
self.logger.warning("_clear_screen: /usr/bin/clear not found.")
|
|
except subprocess.CalledProcessError as e:
|
|
self.logger.warning(f"_clear_screen: /usr/bin/clear failed: {e}")
|
|
except Exception as e:
|
|
self.logger.error(f"_clear_screen: Unexpected error with /usr/bin/clear: {e}")
|
|
|
|
try:
|
|
# Attempt 2: 'clear' command (relies on PATH)
|
|
self.logger.debug("_clear_screen: Attempting 'clear' from PATH")
|
|
subprocess.run(['clear'], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
self.logger.debug("_clear_screen: 'clear' from PATH succeeded")
|
|
return
|
|
except FileNotFoundError:
|
|
self.logger.warning("_clear_screen: 'clear' not found in PATH.")
|
|
except subprocess.CalledProcessError as e:
|
|
self.logger.warning(f"_clear_screen: 'clear' from PATH failed: {e}")
|
|
except Exception as e:
|
|
self.logger.error(f"_clear_screen: Unexpected error with 'clear' from PATH: {e}")
|
|
|
|
# Attempt 3: Fallback to printing newlines (guaranteed)
|
|
self.logger.debug("_clear_screen: Clearing screen for POSIX by printing 100 newlines.")
|
|
print("\n" * 100, flush=True)
|
|
|
|
|
|
|
|
|
|
def _ask_try_again(self):
|
|
"""Prompt the user to try again or cancel. Returns True to retry, False to cancel."""
|
|
while True:
|
|
choice = input(f"{COLOR_PROMPT}Try again? (Y/n/q): {COLOR_RESET}").strip().lower()
|
|
if choice == '' or choice.startswith('y'):
|
|
return True
|
|
elif choice == 'n' or choice == 'q':
|
|
return False
|
|
else:
|
|
print(f"{COLOR_ERROR}Invalid input. Please enter 'y', 'n', or 'q'.{COLOR_RESET}")
|
|
|
|
def get_directory_path(self, prompt_message: str, default_path: Optional[Path], create_if_missing: bool = True, no_header: bool = False) -> Optional[Path]:
|
|
"""
|
|
Prompts the user for a directory path. If the directory does not exist, asks if it should be created.
|
|
Returns a tuple (chosen_path, should_create) if creation is needed, or just the path if it exists.
|
|
The actual directory creation should be performed after summary confirmation.
|
|
"""
|
|
if not no_header:
|
|
print("\n" + "-" * 28)
|
|
print(f"{COLOR_PROMPT}{prompt_message}{COLOR_RESET}")
|
|
if default_path is not None: # Explicit check
|
|
print(f"{COLOR_INFO}(Default: {default_path}){COLOR_RESET}")
|
|
print(f"{COLOR_PROMPT}Enter path (or 'q' to cancel, Enter for default):{COLOR_RESET}")
|
|
else:
|
|
print(f"{COLOR_PROMPT}{prompt_message}{COLOR_RESET}")
|
|
if READLINE_AVAILABLE:
|
|
readline.set_completer_delims(' \t\n;')
|
|
readline.set_completer(path_completer)
|
|
readline.parse_and_bind('tab: complete')
|
|
elif not no_header:
|
|
print(f"{COLOR_INFO}Note: Tab completion is not available in this environment.{COLOR_RESET}")
|
|
try:
|
|
while True:
|
|
chosen_path: Optional[Path] = None
|
|
try:
|
|
user_input = input("Path: ").strip()
|
|
if user_input.lower() == 'q':
|
|
self.logger.info("User cancelled path input with 'q'.")
|
|
print(f"{COLOR_INFO}Input cancelled by user.{COLOR_RESET}")
|
|
return None
|
|
if not user_input: # User pressed Enter (empty input)
|
|
if default_path is not None: # Explicitly check if a default_path object was provided
|
|
self.logger.debug(f"User pressed Enter, using default_path: {default_path}")
|
|
chosen_path = default_path.expanduser().resolve()
|
|
else:
|
|
self.logger.warning("User pressed Enter, but no default_path was available.")
|
|
print(f"{COLOR_ERROR}No path entered and no default path was available.{COLOR_RESET}")
|
|
if not self._ask_try_again(): return None
|
|
continue
|
|
else:
|
|
self.logger.debug(f"User entered path: {user_input}")
|
|
chosen_path = Path(os.path.expanduser(user_input)).resolve()
|
|
if chosen_path.exists():
|
|
if chosen_path.is_dir():
|
|
self.logger.info(f"Selected directory (exists): {chosen_path}")
|
|
return chosen_path
|
|
else:
|
|
print(f"{COLOR_ERROR}Path exists but is not a directory: {chosen_path}{COLOR_RESET}")
|
|
if not self._ask_try_again(): return None
|
|
continue
|
|
elif create_if_missing:
|
|
self.logger.info(f"Directory does not exist: {chosen_path}. Prompting to create.")
|
|
print(f"{COLOR_WARNING}Directory does not exist: {chosen_path}{COLOR_RESET}")
|
|
print("\n" + "-" * 28)
|
|
print(f"{COLOR_PROMPT}Create this directory?{COLOR_RESET}")
|
|
create_choice = input(f"{COLOR_PROMPT}(Y/n/q): {COLOR_RESET}").strip().lower()
|
|
print("-" * 28)
|
|
if create_choice == '' or create_choice.startswith('y'):
|
|
self.logger.info(f"User chose to create directory: {chosen_path}")
|
|
return (chosen_path, True)
|
|
elif create_choice.startswith('n') or create_choice.startswith('q'):
|
|
self.logger.info(f"User chose not to create directory: {chosen_path}")
|
|
print("Directory creation skipped by user.")
|
|
if create_choice.startswith('q') or not self._ask_try_again(): return None
|
|
continue
|
|
else:
|
|
print(f"{COLOR_ERROR}Invalid input. Please enter 'y', 'n', or 'q'.{COLOR_RESET}")
|
|
if not self._ask_try_again(): return None
|
|
continue
|
|
except EOFError:
|
|
print("\nInput cancelled (EOF).")
|
|
return None
|
|
except KeyboardInterrupt:
|
|
print("\nInput cancelled (Ctrl+C).")
|
|
return None
|
|
finally:
|
|
if READLINE_AVAILABLE:
|
|
readline.set_completer(None)
|
|
|
|
def get_existing_file_path(self, prompt_message: str, extension_filter: str = ".wabbajack", no_header: bool = False) -> Optional[Path]:
|
|
if not no_header:
|
|
print("\n" + "-" * 28)
|
|
print(f"{COLOR_PROMPT}{prompt_message}{COLOR_RESET}")
|
|
print(f"Looking for files with extension: {extension_filter}")
|
|
print("You can also select a directory containing the file.")
|
|
print("")
|
|
print(f"{COLOR_PROMPT}Enter file path (or 'q' to cancel):{COLOR_RESET}")
|
|
if READLINE_AVAILABLE:
|
|
readline.set_completer_delims(' \t\n;')
|
|
readline.set_completer(path_completer)
|
|
readline.parse_and_bind('tab: complete')
|
|
else:
|
|
print(f"{COLOR_INFO}Note: Tab completion is not available in this environment.{COLOR_RESET}")
|
|
print(f"{COLOR_INFO}You'll need to manually type the full path to the file.{COLOR_RESET}")
|
|
try:
|
|
while True:
|
|
raw_path = input("File: ").strip()
|
|
if raw_path.lower() == 'q':
|
|
print(f"{COLOR_INFO}Input cancelled by user.{COLOR_RESET}")
|
|
print("")
|
|
return None
|
|
if not raw_path:
|
|
print("Input cancelled.")
|
|
print("")
|
|
return None
|
|
file_path = Path(os.path.expanduser(raw_path)).resolve()
|
|
if file_path.is_dir():
|
|
print("")
|
|
return file_path
|
|
if file_path.is_file() and file_path.name.lower().endswith(extension_filter.lower()):
|
|
print("")
|
|
return file_path
|
|
else:
|
|
print(f"{COLOR_ERROR}Path is not a valid '{extension_filter}' file or a directory: {file_path}{COLOR_RESET}")
|
|
print(f"{COLOR_INFO}Please check the path and try again, or press Ctrl+C or 'q' to cancel.{COLOR_RESET}")
|
|
if not self._ask_try_again():
|
|
print("")
|
|
return None
|
|
except KeyboardInterrupt:
|
|
print("\nInput cancelled.")
|
|
print("")
|
|
return None
|
|
finally:
|
|
if READLINE_AVAILABLE and readline:
|
|
readline.set_completer(None) |