diff --git a/falyx/completer.py b/falyx/completer.py index 5886cfb..caa5d5d 100644 --- a/falyx/completer.py +++ b/falyx/completer.py @@ -29,6 +29,26 @@ class FalyxCompleter(Completer): yield from self._suggest_commands(tokens[0] if tokens else "") return + # Identify command + command_key = tokens[0].upper() + command = self.falyx._name_map.get(command_key) + if not command or not command.arg_parser: + return + + # If at end of token, e.g., "--t" vs "--tag ", add a stub so suggest_next sees it + parsed_args = tokens[1:] if cursor_at_end_of_token else tokens[1:-1] + stub = "" if cursor_at_end_of_token else tokens[-1] + + try: + suggestions = command.arg_parser.suggest_next( + parsed_args + ([stub] if stub else []) + ) + for suggestion in suggestions: + if suggestion.startswith(stub): + yield Completion(suggestion, start_position=-len(stub)) + except Exception: + return + def _suggest_commands(self, prefix: str) -> Iterable[Completion]: prefix = prefix.upper() keys = [self.falyx.exit_command.key] diff --git a/falyx/falyx.py b/falyx/falyx.py index fe39635..c3aa88d 100644 --- a/falyx/falyx.py +++ b/falyx/falyx.py @@ -507,7 +507,7 @@ class Falyx: message=self.prompt, multiline=False, completer=self._get_completer(), - reserve_space_for_menu=1, + reserve_space_for_menu=5, validator=CommandValidator(self, self._get_validator_error_message()), bottom_toolbar=self._get_bottom_bar_render(), key_bindings=self.key_bindings, diff --git a/falyx/parser/command_argument_parser.py b/falyx/parser/command_argument_parser.py index dce3be7..51e5166 100644 --- a/falyx/parser/command_argument_parser.py +++ b/falyx/parser/command_argument_parser.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections import defaultdict from copy import deepcopy +from difflib import get_close_matches from typing import Any, Iterable from rich.console import Console @@ -914,6 +915,84 @@ class CommandArgumentParser: kwargs_dict[arg.dest] = parsed[arg.dest] return tuple(args_list), kwargs_dict + def suggest_next(self, args: list[str]) -> list[str]: + """ + Suggest the next possible flags or values given partially typed arguments. + + This does NOT raise errors. It is intended for completions, not validation. + + Returns: + A list of possible completions based on the current input. + """ + consumed_positionals = [] + positional_choices = [ + str(choice) + for arg in self._positional.values() + for choice in arg.choices + if arg.choices + ] + if not args: + # Nothing entered yet: suggest all top-level flags and positionals + if positional_choices: + return sorted(set(positional_choices)) + return sorted( + set( + flag + for arg in self._arguments + for flag in arg.flags + if not arg.positional + ) + ) + + last = args[-1] + suggestions: list[str] = [] + + # Case 1: Mid-flag (e.g., "--ver") + if last.startswith("-") and not last in self._flag_map: + possible_flags = [flag for flag in self._flag_map if flag.startswith(last)] + suggestions.extend(possible_flags) + + # Case 2: Flag that expects a value (e.g., ["--tag"]) + elif last in self._flag_map: + arg = self._flag_map[last] + if arg.choices: + suggestions.extend(arg.choices) + + # Case 3: Just completed a flag, suggest next + elif len(args) >= 2 and args[-2] in self._flag_map: + # Just gave value for a flag, now suggest next possible + used_dests = { + self._flag_map[arg].dest for arg in args if arg in self._flag_map + } + remaining_args = [ + a + for a in self._arguments + if not a.positional + and a.dest not in used_dests + and a.action != ArgumentAction.HELP + ] + for arg in remaining_args: + suggestions.extend(arg.flags) + + # Case 4: Positional values not yet filled + else: + consumed_positionals = [arg for arg in self._arguments if arg.positional][ + : len(args) + ] + remaining_positionals = [ + arg + for arg in self._arguments + if arg.positional and arg not in consumed_positionals + ] + if remaining_positionals: + arg = remaining_positionals[0] + if arg.choices: + suggestions.extend(arg.choices) + else: + suggestions.append(f"<{arg.dest}>") # generic placeholder + + return sorted(set(suggestions)) + def get_options_text(self, plain_text=False) -> str: # Options # Add all keyword arguments to the options list