From 0417a06ee4008310f931a114adbeb33d5ae8b2eb Mon Sep 17 00:00:00 2001 From: Roland Thomas Date: Thu, 7 Aug 2025 19:27:59 -0400 Subject: [PATCH] feat: enhance help command UX, completions, and CLI tips - Expanded help command to accept: - `-k/--key` for detailed help on a specific command - `-t/--tag` for tag-filtered listings - `-T/--tldr` for quick usage examples - Updated TLDR flag to support `-T` short form and refined help text. - Improved `_render_help()` to show contextual CLI tips after help or TLDR output. - Adjusted completer to yield both upper and lower case completions without mutating the prefix. - Standardized CLI tip strings in root/arg parsers to reference `help` and `preview` subcommands instead of menu `run ?` syntax. - Passed `options_manager` to history/help/exit commands for consistency. - Allowed `help_command` to display TLDR examples when invoked without a key. - Added test assertions for help command key/alias consistency. - Bumped version to 0.1.82. --- falyx/command.py | 2 +- falyx/completer.py | 5 +-- falyx/falyx.py | 46 +++++++++++++++++++++---- falyx/parser/command_argument_parser.py | 9 +++-- falyx/parser/parsers.py | 16 ++++----- falyx/version.py | 2 +- pyproject.toml | 2 +- tests/test_falyx/test_help.py | 2 ++ 8 files changed, 62 insertions(+), 22 deletions(-) diff --git a/falyx/command.py b/falyx/command.py index 955272e..d697abb 100644 --- a/falyx/command.py +++ b/falyx/command.py @@ -366,7 +366,7 @@ class Command(BaseModel): ) return ( f"[{self.style}]{program}[/]{command_keys}", - f"[dim]{self.description}[/dim]", + f"[dim]{self.help_text or self.description}[/dim]", "", ) diff --git a/falyx/completer.py b/falyx/completer.py index 17c4b7f..52f86e3 100644 --- a/falyx/completer.py +++ b/falyx/completer.py @@ -120,7 +120,6 @@ class FalyxCompleter(Completer): Yields: Completion: Matching keys or aliases from all registered commands. """ - prefix = prefix.upper() keys = [self.falyx.exit_command.key] keys.extend(self.falyx.exit_command.aliases) if self.falyx.history_command: @@ -134,7 +133,9 @@ class FalyxCompleter(Completer): keys.extend(cmd.aliases) for key in keys: if key.upper().startswith(prefix): - yield Completion(key, start_position=-len(prefix)) + yield Completion(key.upper(), start_position=-len(prefix)) + elif key.lower().startswith(prefix): + yield Completion(key.lower(), start_position=-len(prefix)) def _ensure_quote(self, text: str) -> str: """ diff --git a/falyx/falyx.py b/falyx/falyx.py index a966b0a..bca7aad 100644 --- a/falyx/falyx.py +++ b/falyx/falyx.py @@ -291,6 +291,7 @@ class Falyx: ignore_in_history=True, options_manager=self.options, program=self.program, + help_text="Exit the program.", ) def _get_history_command(self) -> Command: @@ -301,6 +302,7 @@ class Falyx: command_style=OneColors.DARK_YELLOW, aliases=["HISTORY"], program=self.program, + options_manager=self.options, ) parser.add_argument( "-n", @@ -387,10 +389,16 @@ class Falyx: async def _render_help( self, tag: str = "", key: str | None = None, tldr: bool = False ) -> None: + if tldr and not key: + if self.help_command and self.help_command.arg_parser: + self.help_command.arg_parser.render_tldr() + self.console.print(f"[bold]tip:[/bold] {self.get_tip()}") + return None if key: _, command, args, kwargs = await self.get_command(key) if command and tldr and command.arg_parser: command.arg_parser.render_tldr() + self.console.print(f"[bold]tip:[/bold] {self.get_tip()}") return None elif command and tldr and not command.arg_parser: self.console.print( @@ -398,6 +406,7 @@ class Falyx: ) elif command and command.arg_parser: command.arg_parser.render_help() + self.console.print(f"[bold]tip:[/bold] {self.get_tip()}") return None elif command and not command.arg_parser: self.console.print( @@ -415,7 +424,7 @@ class Falyx: ] if not commands: self.console.print(f"'{tag}'... Nothing to show here") - return + return None for command in commands: usage, description, _ = command.help_signature self.console.print( @@ -424,7 +433,8 @@ class Falyx: (0, 2), ) ) - return + self.console.print(f"[bold]tip:[/bold] {self.get_tip()}") + return None self.console.print("[bold]help:[/bold]") for command in self.commands.values(): @@ -473,8 +483,9 @@ class Falyx: command_key="H", command_description="Help", command_style=OneColors.LIGHT_YELLOW, - aliases=["?", "HELP"], + aliases=["HELP", "?"], program=self.program, + options_manager=self.options, ) parser.add_argument( "-t", @@ -483,11 +494,27 @@ class Falyx: default="", help="Optional tag to filter commands by.", ) + parser.add_argument( + "-k", + "--key", + nargs="?", + default=None, + help="Optional command key or alias to get detailed help for.", + ) + parser.add_tldr_examples( + [ + ("", "Show all commands."), + ("-k [COMMAND]", "Show detailed help for a specific command."), + ("-Tk [COMMAND]", "Show quick usage examples for a specific command."), + ("--tldr", "Show these quick usage examples."), + ("--tag [TAG]", "Show commands with the specified tag."), + ] + ) return Command( key="H", - aliases=["?", "HELP"], + aliases=["HELP", "?"], description="Help", - help_text="Show this help menu", + help_text="Show this help menu.", action=Action("Help", self._render_help), style=OneColors.LIGHT_YELLOW, arg_parser=parser, @@ -652,6 +679,7 @@ class Falyx: style: str = OneColors.DARK_RED, confirm: bool = False, confirm_message: str = "Are you sure?", + help_text: str = "Exit the program.", ) -> None: """Updates the back command of the menu.""" self._validate_command_key(key) @@ -669,6 +697,7 @@ class Falyx: ignore_in_history=True, options_manager=self.options, program=self.program, + help_text=help_text, ) def add_submenu( @@ -682,7 +711,12 @@ class Falyx: key, description, submenu.menu, style=style, simple_help_signature=True ) if submenu.exit_command.key == "X": - submenu.update_exit_command(key="B", description="Back", aliases=["BACK"]) + submenu.update_exit_command( + key="B", + description="Back", + aliases=["BACK"], + help_text="Go back to the previous menu.", + ) def add_commands(self, commands: list[Command] | list[dict]) -> None: """Adds a list of Command instances or config dicts.""" diff --git a/falyx/parser/command_argument_parser.py b/falyx/parser/command_argument_parser.py index 07ccbf1..eca9483 100644 --- a/falyx/parser/command_argument_parser.py +++ b/falyx/parser/command_argument_parser.py @@ -156,9 +156,9 @@ class CommandArgumentParser: if "tldr" not in self._dest_set: tldr = Argument( - ("--tldr",), + ("--tldr", "-T"), action=ArgumentAction.TLDR, - help="Show quick usage examples and exit.", + help="Show quick usage examples.", dest="tldr", ) self._register_argument(tldr) @@ -1408,10 +1408,13 @@ class CommandArgumentParser: FalyxMode.RUN_ALL, FalyxMode.HELP, } + is_help_command = self.aliases[0] == "HELP" and self.command_key == "H" program = self.program or "falyx" command = self.aliases[0] if self.aliases else self.command_key - if is_cli_mode: + if is_help_command and is_cli_mode: + command = f"[{self.command_style}]{program} help[/{self.command_style}]" + elif is_cli_mode: command = ( f"[{self.command_style}]{program} run {command}[/{self.command_style}]" ) diff --git a/falyx/parser/parsers.py b/falyx/parser/parsers.py index 8e3e4fd..ce3e75f 100644 --- a/falyx/parser/parsers.py +++ b/falyx/parser/parsers.py @@ -59,7 +59,7 @@ def get_root_parser( prog: str | None = "falyx", usage: str | None = None, description: str | None = "Falyx CLI - Run structured async command workflows.", - epilog: str | None = "Tip: Use 'falyx run ?' to show available commands.", + epilog: str | None = "Tip: Use 'falyx help' to show available commands.", parents: Sequence[ArgumentParser] | None = None, prefix_chars: str = "-", fromfile_prefix_chars: str | None = None, @@ -178,7 +178,7 @@ def get_arg_parsers( description: str | None = "Falyx CLI - Run structured async command workflows.", epilog: ( str | None - ) = "Tip: Use 'falyx run ?[COMMAND]' to preview any command from the CLI.", + ) = "Tip: Use 'falyx preview [COMMAND]' to preview any command from the CLI.", parents: Sequence[ArgumentParser] | None = None, prefix_chars: str = "-", fromfile_prefix_chars: str | None = None, @@ -240,7 +240,7 @@ def get_arg_parsers( - Use `falyx run ?[COMMAND]` from the CLI to preview a command. """ if epilog is None: - epilog = f"Tip: Use '{prog} run ?' to show available commands." + epilog = f"Tip: Use '{prog} help' to show available commands." if root_parser is None: parser = get_root_parser( prog=prog, @@ -281,7 +281,7 @@ def get_arg_parsers( command_description = command.help_text or command.description run_description.append(f"{' '*24}{command_description}") run_epilog = ( - f"Tip: Use '{prog} run ?[COMMAND]' to preview commands by their key or alias." + f"Tip: Use '{prog} preview [COMMAND]' to preview commands by their key or alias." ) run_parser = subparsers.add_parser( "run", @@ -377,10 +377,6 @@ def get_arg_parsers( help_parser = subparsers.add_parser("help", help="List all available commands") - help_parser.add_argument( - "-t", "--tag", help="Filter commands by tag (case-insensitive)", default=None - ) - help_parser.add_argument( "-k", "--key", @@ -395,6 +391,10 @@ def get_arg_parsers( help="Show a simplified TLDR examples of a command if available", ) + help_parser.add_argument( + "-t", "--tag", help="Filter commands by tag (case-insensitive)", default=None + ) + version_parser = subparsers.add_parser("version", help=f"Show {prog} version") return FalyxParsers( diff --git a/falyx/version.py b/falyx/version.py index e5e0b9d..2db3392 100644 --- a/falyx/version.py +++ b/falyx/version.py @@ -1 +1 @@ -__version__ = "0.1.81" +__version__ = "0.1.82" diff --git a/pyproject.toml b/pyproject.toml index 4d675d3..1d9c9a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "falyx" -version = "0.1.81" +version = "0.1.82" description = "Reliable and introspectable async CLI action framework." authors = ["Roland Thomas Jr "] license = "MIT" diff --git a/tests/test_falyx/test_help.py b/tests/test_falyx/test_help.py index 948bead..9431e37 100644 --- a/tests/test_falyx/test_help.py +++ b/tests/test_falyx/test_help.py @@ -6,6 +6,8 @@ from falyx import Falyx @pytest.mark.asyncio async def test_help_command(capsys): flx = Falyx() + assert flx.help_command.arg_parser.aliases[0] == "HELP" + assert flx.help_command.arg_parser.command_key == "H" await flx.run_key("H") captured = capsys.readouterr()