diff --git a/falyx/command.py b/falyx/command.py index 7894a18..8bca54d 100644 --- a/falyx/command.py +++ b/falyx/command.py @@ -22,6 +22,8 @@ from typing import Any, Awaitable, Callable from prompt_toolkit.formatted_text import FormattedText from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator +from rich.padding import Padding +from rich.panel import Panel from rich.tree import Tree from falyx.action.action import Action @@ -32,6 +34,7 @@ from falyx.debug import register_debug_hooks from falyx.execution_registry import ExecutionRegistry as er from falyx.hook_manager import HookManager, HookType from falyx.logger import logger +from falyx.mode import FalyxMode from falyx.options_manager import OptionsManager from falyx.parser.command_argument_parser import CommandArgumentParser from falyx.parser.signature import infer_args_from_func @@ -348,20 +351,37 @@ class Command(BaseModel): return f" {command_keys_text:<20} {options_text} " @property - def help_signature(self) -> str: + def help_signature(self) -> tuple[Panel, str]: """Generate a help signature for the command.""" + is_cli_mode = self.options_manager.get("mode") in { + FalyxMode.RUN, + FalyxMode.PREVIEW, + FalyxMode.RUN_ALL, + } + + program = f"{self.program} run " if is_cli_mode else "" + if self.arg_parser and not self.simple_help_signature: - signature = [self.arg_parser.get_usage()] - signature.append(f" {self.help_text or self.description}") + usage = Panel( + f"[{self.style}]{program}[/]{self.arg_parser.get_usage()}", + expand=False, + ) + description = [f" {self.help_text or self.description}"] if self.tags: - signature.append(f" [dim]Tags: {', '.join(self.tags)}[/dim]") - return "\n".join(signature).strip() + description.append(f" [dim]Tags: {', '.join(self.tags)}[/dim]") + return usage, "\n".join(description) command_keys = " | ".join( [f"[{self.style}]{self.key}[/{self.style}]"] + [f"[{self.style}]{alias}[/{self.style}]" for alias in self.aliases] ) - return f"{command_keys} {self.description}" + return ( + Panel( + f"[{self.style}]{program}[/]{command_keys} {self.description}", + expand=False, + ), + "", + ) def log_summary(self) -> None: if self._context: diff --git a/falyx/falyx.py b/falyx/falyx.py index a718743..2b17e90 100644 --- a/falyx/falyx.py +++ b/falyx/falyx.py @@ -125,6 +125,7 @@ class Falyx: title: str | Markdown = "Menu", *, program: str | None = "falyx", + program_style: str = OneColors.WHITE, usage: str | None = None, description: str | None = "Falyx CLI - Run structured async command workflows.", epilog: str | None = None, @@ -158,13 +159,6 @@ class Falyx: self.prompt: str | StyleAndTextTuples = rich_text_to_prompt_text(prompt) self.columns: int = columns self.commands: dict[str, Command] = CaseInsensitiveDict() - self.exit_command: Command = self._get_exit_command() - self.history_command: Command | None = ( - self._get_history_command() if include_history_command else None - ) - self.help_command: Command | None = ( - self._get_help_command() if include_help_command else None - ) self.console: Console = console self.welcome_message: str | Markdown | dict[str, Any] = welcome_message self.exit_message: str | Markdown | dict[str, Any] = exit_message @@ -182,6 +176,13 @@ class Falyx: self.validate_options(cli_args, options) self._prompt_session: PromptSession | None = None self.options.set("mode", FalyxMode.MENU) + self.exit_command: Command = self._get_exit_command() + self.history_command: Command | None = ( + self._get_history_command() if include_history_command else None + ) + self.help_command: Command | None = ( + self._get_help_command() if include_help_command else None + ) def validate_options( self, @@ -262,6 +263,8 @@ class Falyx: style=OneColors.DARK_RED, simple_help_signature=True, ignore_in_history=True, + options_manager=self.options, + program=self.program, ) def _get_history_command(self) -> Command: @@ -271,6 +274,7 @@ class Falyx: command_description="History", command_style=OneColors.DARK_YELLOW, aliases=["HISTORY"], + program=self.program, ) parser.add_argument( "-n", @@ -314,17 +318,19 @@ class Falyx: arg_parser=parser, help_text="View the execution history of commands.", ignore_in_history=True, + options_manager=self.options, + program=self.program, ) async def _show_help(self, tag: str = "") -> None: + is_cli_mode = self.options.get("mode") in { + FalyxMode.RUN, + FalyxMode.PREVIEW, + FalyxMode.RUN_ALL, + } + + program = f"{self.program} run " if is_cli_mode else "" if tag: - table = Table( - title=tag.upper(), - title_justify="left", - show_header=False, - box=box.SIMPLE, - show_footer=False, - ) tag_lower = tag.lower() commands = [ command @@ -332,27 +338,31 @@ class Falyx: if any(tag_lower == tag.lower() for tag in command.tags) ] for command in commands: - table.add_row(command.help_signature) - self.console.print(table) + usage, description = command.help_signature + self.console.print(usage) + if description: + self.console.print(description) return - else: - table = Table( - title="Help", - title_justify="left", - title_style=OneColors.LIGHT_YELLOW_b, - show_header=False, - show_footer=False, - box=box.SIMPLE, - ) - for command in self.commands.values(): - table.add_row(command.help_signature) + + for command in self.commands.values(): + usage, description = command.help_signature + self.console.print(usage) + if description: + self.console.print(description) if self.help_command: - table.add_row(self.help_command.help_signature) - if self.history_command: - table.add_row(self.history_command.help_signature) - table.add_row(self.exit_command.help_signature) - table.add_row(f"Tip: '[{OneColors.LIGHT_YELLOW}]?[KEY][/]' to preview a command ") - self.console.print(table) + usage, description = self.help_command.help_signature + self.console.print(usage) + self.console.print(description) + if not is_cli_mode: + if self.history_command: + usage, description = self.history_command.help_signature + self.console.print(usage) + self.console.print(description) + usage, _ = self.exit_command.help_signature + self.console.print(usage) + self.console.print( + f"Tip: '{program}[{OneColors.LIGHT_YELLOW}]?[KEY][/]' to preview a command " + ) def _get_help_command(self) -> Command: """Returns the help command for the menu.""" @@ -361,6 +371,7 @@ class Falyx: command_description="Help", command_style=OneColors.LIGHT_YELLOW, aliases=["?", "HELP", "LIST"], + program=self.program, ) parser.add_argument( "-t", @@ -378,6 +389,8 @@ class Falyx: style=OneColors.LIGHT_YELLOW, arg_parser=parser, ignore_in_history=True, + options_manager=self.options, + program=self.program, ) def _get_completer(self) -> FalyxCompleter: @@ -549,6 +562,8 @@ class Falyx: confirm=confirm, confirm_message=confirm_message, ignore_in_history=True, + options_manager=self.options, + program=self.program, ) def add_submenu( @@ -729,7 +744,7 @@ class Falyx: def build_placeholder_menu(self) -> StyleAndTextTuples: """ - Builds a menu placeholder for prompt_menu mode. + Builds a menu placeholder for show_placeholder_menu. """ visible_commands = [item for item in self.commands.items() if not item[1].hidden] if not visible_commands: diff --git a/falyx/parser/command_argument_parser.py b/falyx/parser/command_argument_parser.py index b37b07a..4a5eaa7 100644 --- a/falyx/parser/command_argument_parser.py +++ b/falyx/parser/command_argument_parser.py @@ -142,12 +142,13 @@ class CommandArgumentParser: Args: examples (list[tuple[str, str]]): List of (usage, description) tuples. """ - if not any( + if not all( isinstance(example, tuple) and len(example) == 2 for example in examples ): raise CommandArgumentError( "TLDR examples must be a list of (usage, description) tuples" ) + for usage, description in examples: self._tldr_examples.append(TLDRExample(usage=usage, description=description)) @@ -1362,7 +1363,7 @@ class CommandArgumentParser: command = self.aliases[0] if self.aliases else self.command_key if is_cli_mode: command = ( - f"{program} run [{self.command_style}]{command}[/{self.command_style}]" + f"[{self.command_style}]{program} run {command}[/{self.command_style}]" ) else: command = f"[{self.command_style}]{command}[/{self.command_style}]" diff --git a/falyx/parser/parsers.py b/falyx/parser/parsers.py index 1bf8716..07744c0 100644 --- a/falyx/parser/parsers.py +++ b/falyx/parser/parsers.py @@ -59,9 +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 ?[COMMAND]' to preview any command from the CLI.", + epilog: str | None = "Tip: Use 'falyx run ?' to show available commands.", parents: Sequence[ArgumentParser] | None = None, prefix_chars: str = "-", fromfile_prefix_chars: str | None = None, @@ -242,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 ?[COMMAND]' to preview any command from the CLI." + epilog = f"Tip: Use '{prog} run ?' to show available commands." if root_parser is None: parser = get_root_parser( prog=prog, diff --git a/falyx/version.py b/falyx/version.py index 4fef18a..aaedf09 100644 --- a/falyx/version.py +++ b/falyx/version.py @@ -1 +1 @@ -__version__ = "0.1.72" +__version__ = "0.1.73" diff --git a/pyproject.toml b/pyproject.toml index c913b16..95001e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "falyx" -version = "0.1.72" +version = "0.1.73" description = "Reliable and introspectable async CLI action framework." authors = ["Roland Thomas Jr "] license = "MIT" diff --git a/tests/test_parsers/test_argument.py b/tests/test_parsers/test_argument.py index f464dee..ef40a82 100644 --- a/tests/test_parsers/test_argument.py +++ b/tests/test_parsers/test_argument.py @@ -88,3 +88,11 @@ def test_argument_equality(): assert arg != "not an argument" assert arg is not None assert arg != object() + + +def test_argument_required(): + arg = Argument("--foo", dest="foo", required=True) + assert arg.required is True + + arg2 = Argument("--bar", dest="bar", required=False) + assert arg2.required is False diff --git a/tests/test_parsers/test_tldr.py b/tests/test_parsers/test_tldr.py new file mode 100644 index 0000000..89023d4 --- /dev/null +++ b/tests/test_parsers/test_tldr.py @@ -0,0 +1,47 @@ +import pytest + +from falyx.exceptions import CommandArgumentError +from falyx.parser.command_argument_parser import CommandArgumentParser + + +@pytest.mark.asyncio +async def test_add_tldr_examples(): + parser = CommandArgumentParser() + parser.add_tldr_examples( + [ + ("example1", "This is the first example."), + ("example2", "This is the second example."), + ] + ) + assert len(parser._tldr_examples) == 2 + assert parser._tldr_examples[0].usage == "example1" + assert parser._tldr_examples[0].description == "This is the first example." + assert parser._tldr_examples[1].usage == "example2" + assert parser._tldr_examples[1].description == "This is the second example." + + +@pytest.mark.asyncio +async def test_bad_tldr_examples(): + parser = CommandArgumentParser() + with pytest.raises(CommandArgumentError): + parser.add_tldr_examples( + [ + ("example1", "This is the first example.", "extra_arg"), + ("example2", "This is the second example."), + ] + ) + + +@pytest.mark.asyncio +async def test_add_tldr_examples_in_init(): + parser = CommandArgumentParser( + tldr_examples=[ + ("example1", "This is the first example."), + ("example2", "This is the second example."), + ] + ) + assert len(parser._tldr_examples) == 2 + assert parser._tldr_examples[0].usage == "example1" + assert parser._tldr_examples[0].description == "This is the first example." + assert parser._tldr_examples[1].usage == "example2" + assert parser._tldr_examples[1].description == "This is the second example."