feat: support interactive sdl prompts

This commit is contained in:
Paweł Lidwin 2025-11-04 13:55:06 +01:00
parent 9ebe1f6e8b
commit 64b99ddb6f
No known key found for this signature in database
GPG key ID: C6EDF064F9FEE1E1
5 changed files with 86 additions and 27 deletions

View file

@ -27,7 +27,7 @@ from legendary.utils.custom_parser import HiddenAliasSubparsersAction
from legendary.utils.env import is_windows_mac_or_pyi
from legendary.lfs.eos import add_registry_entries, query_registry_entries, remove_registry_entries
from legendary.lfs.utils import validate_files, clean_filename
from legendary.utils.selective_dl import get_sdl_data
from legendary.utils.selective_dl import get_sdl_data, LGDEvaluationContext
from legendary.lfs.wine_helpers import read_registry, get_shell_folders, case_insensitive_file_search
# todo custom formatter for cli logger (clean info, highlighted error/warning)
@ -931,7 +931,9 @@ class LegendaryCLI:
sdl_enabled = False
if sdl_enabled:
# FIXME: Consider UpgradePathLogic - it lets automatically select options in new manifests when corresponding option was selected with older version
if not self.core.is_installed(game.app_name) or config_tags is None or args.reset_sdl:
context = LGDEvaluationContext(self.core)
sdl_data = get_sdl_data(self.core.lgd.egl_content_path, game.app_name, game.app_version(args.platform))
if sdl_data:
if args.skip_sdl:
@ -940,7 +942,7 @@ class LegendaryCLI:
if entry.get('IsRequired', 'false').lower() == 'true':
args.install_tag.extend(entry.get('Tags', []))
else:
args.install_tag = sdl_prompt(sdl_data, game.app_title)
args.install_tag = sdl_prompt(sdl_data, game.app_title, context)
# self.core.lgd.config.set(game.app_name, 'install_tags', ','.join(args.install_tag))
else:
logger.error(f'Unable to get SDL data for {game.app_name}')
@ -949,7 +951,6 @@ class LegendaryCLI:
elif args.install_tag and not game.is_dlc and not args.no_install:
config_tags = ','.join(args.install_tag)
logger.info(f'Saving install tags for "{game.app_name}" to config: {config_tags}')
self.core.lgd.config.set(game.app_name, 'install_tags', config_tags)
elif not game.is_dlc:
if config_tags and args.reset_sdl:
logger.info('Clearing install tags from config.')
@ -957,7 +958,8 @@ class LegendaryCLI:
elif config_tags:
logger.info(f'Using install tags from config: {config_tags}')
args.install_tag = config_tags.split(',')
logger.debug(f'Selected tags: {args.install_tag}')
logger.info(f'Preparing download for "{game.app_title}" ({game.app_name})...')
# todo use status queue to print progress from CLI
# This has become a little ridiculous hasn't it?

View file

@ -1,3 +1,10 @@
from InquirerPy import inquirer
from InquirerPy.base.control import Choice
from InquirerPy.separator import Separator
from epic_expreval import Tokenizer
from legendary.utils.selective_dl import LGDEvaluationContext, EXTRA_FUNCTIONS
def get_boolean_choice(prompt, default=True):
yn = 'Y/n' if default else 'y/N'
@ -43,33 +50,63 @@ def get_int_choice(prompt, default=None, min_choice=None, max_choice=None, retur
return choice
def sdl_prompt(sdl_data, title):
tags = ['']
if '__required' in sdl_data:
tags.extend(sdl_data['__required']['tags'])
def sdl_prompt(sdl_data, title, context):
tags = set()
print(f'You are about to install {title}, this application supports selective downloads.')
print('The following optional packs are available (tag - name):')
for tag, info in sdl_data.items():
if tag == '__required':
choices = []
required_categories = {}
for element in sdl_data['Data']:
if (element.get('IsRequired', 'false').lower() == 'true' and not 'Children' in element) or element.get('Invisible', 'false').lower() == 'true':
continue
print(' *', tag, '-', info['name'])
examples = ', '.join([g for g in sdl_data.keys() if g != '__required'][:2])
print(f'Please enter tags of pack(s) to install (space/comma-separated, e.g. "{examples}")')
print('Leave blank to use defaults (only required data will be downloaded).')
choices = input('Additional packs [Enter to confirm]: ')
if not choices:
return tags
for c in choices.strip('"').replace(',', ' ').split():
c = c.strip()
if c in sdl_data:
tags.extend(sdl_data[c]['tags'])
if element.get('ConfigHandler'):
choices.append(Separator(4 * '-' + ' ' + element['Title'] + ' ' + 4 * '-'))
is_required = element.get('IsRequired', 'false').lower() == 'true'
if is_required: required_categories[element['UniqueId']] = []
for child in element.get('Children', []):
enabled = element.get('IsDefaultSelected', 'false').lower() == 'true'
choices.append(Choice(child['UniqueId'], name=child['Title'], enabled=enabled))
if is_required: required_categories[element['UniqueId']].append(child['UniqueId'])
else:
print('Invalid tag:', c)
enabled = False
if element.get('IsDefaultSelected', 'false').lower() == 'true':
expression = element.get('DefaultSelectedExpression')
if expression:
tk = Tokenizer(expression, context)
tk.extend_functions(EXTRA_FUNCTIONS)
tk.compile()
if tk.execute(''):
enabled = True
else:
enabled = True
choices.append(Choice(element['UniqueId'], name=element['Title'], enabled=enabled))
return tags
selected_packs = inquirer.checkbox(message='Select optional packs to install',
choices=choices,
cycle=True,
validate=lambda selected: not required_categories or all(any(item in selected for item in category) for category in required_categories.values())).execute()
context.selection = set(selected_packs)
for element in sdl_data['Data']:
if element.get('IsRequired', 'false').lower() == 'true':
tags.update(element.get('Tags', []))
continue
if element.get('Invisible', 'false').lower() == 'true':
tk = Tokenizer(element['InvisibleSelectedExpression'], context)
tk.extend_functions(EXTRA_FUNCTIONS)
tk.compile()
if tk.execute(''):
tags.update(element.get('Tags', []))
if element['UniqueId'] in selected_packs:
tags.update(element.get('Tags', []))
if element.get('ConfigHandler'):
for child in element.get('Children', []):
if child['UniqueId'] in selected_packs:
tags.update(child.get('Tags', []))
return list(tags)
def strtobool(val):

View file

@ -5,6 +5,24 @@ import os
import json
from epic_expreval import Tokenizer, EvaluationContext
def has_access(context, app):
return bool(context.core.get_game(app))
def is_selected(context, input):
return input in context.selection
EXTRA_FUNCTIONS = {'HasAccess': has_access, "IsComponentSelected": is_selected}
class LGDEvaluationContext(EvaluationContext):
def __init__(self, core):
super().__init__()
self.core = core
self.selection = set()
def reset(self):
super().reset()
self.selection = set()
def run_expression(expression, input):
"""Runs expression with default EvauluationContext"""
tk = Tokenizer(expression, EvaluationContext())

View file

@ -1,3 +1,4 @@
requests<3.0
filelock
epic-expreval=0.2
epic-expreval=0.2
InquirerPy

View file

@ -37,6 +37,7 @@ setup(
install_requires=[
'requests<3.0',
'epic-expreval==0.2',
'InquirerPy',
'setuptools',
'wheel',
'filelock'