diff --git a/legendary/cli.py b/legendary/cli.py index 06dd7c0..92c1df5 100644 --- a/legendary/cli.py +++ b/legendary/cli.py @@ -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? diff --git a/legendary/utils/cli.py b/legendary/utils/cli.py index c0fe14a..303b440 100644 --- a/legendary/utils/cli.py +++ b/legendary/utils/cli.py @@ -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): diff --git a/legendary/utils/selective_dl.py b/legendary/utils/selective_dl.py index 21e3d7c..5ada8b0 100644 --- a/legendary/utils/selective_dl.py +++ b/legendary/utils/selective_dl.py @@ -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()) diff --git a/requirements.txt b/requirements.txt index 5c9dce3..e8bc066 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ requests<3.0 filelock -epic-expreval=0.2 \ No newline at end of file +epic-expreval=0.2 +InquirerPy \ No newline at end of file diff --git a/setup.py b/setup.py index c58edd1..f30fd39 100644 --- a/setup.py +++ b/setup.py @@ -37,6 +37,7 @@ setup( install_requires=[ 'requests<3.0', 'epic-expreval==0.2', + 'InquirerPy', 'setuptools', 'wheel', 'filelock'