Update help formatting, allow help to be filtered by tag

This commit is contained in:
Roland Thomas Jr 2025-05-31 21:51:08 -04:00
parent 1585098513
commit 21af003bc7
Signed by: roland
GPG Key ID: 7C3C2B085A4C2872
7 changed files with 99 additions and 55 deletions

View File

@ -21,7 +21,8 @@ flx = Falyx("Deployment CLI")
flx.add_command( flx.add_command(
key="D", key="D",
aliases=["deploy"], aliases=["deploy"],
description="Deploy a service to a specified region.", description="Deploy",
help_text="Deploy a service to a specified region.",
action=Action( action=Action(
name="deploy_service", name="deploy_service",
action=deploy, action=deploy,
@ -31,6 +32,7 @@ flx.add_command(
"region": {"help": "Deployment region", "choices": ["us-east-1", "us-west-2"]}, "region": {"help": "Deployment region", "choices": ["us-east-1", "us-west-2"]},
"verbose": {"help": "Enable verbose mode"}, "verbose": {"help": "Enable verbose mode"},
}, },
tags=["deployment", "service"],
) )
deploy_chain = ChainedAction( deploy_chain = ChainedAction(
@ -48,8 +50,10 @@ deploy_chain = ChainedAction(
flx.add_command( flx.add_command(
key="N", key="N",
aliases=["notify"], aliases=["notify"],
description="Deploy a service and notify.", description="Deploy and Notify",
help_text="Deploy a service and notify.",
action=deploy_chain, action=deploy_chain,
tags=["deployment", "service", "notification"],
) )
asyncio.run(flx.run()) asyncio.run(flx.run())

View File

@ -135,6 +135,7 @@ class Command(BaseModel):
custom_help: Callable[[], str | None] | None = None custom_help: Callable[[], str | None] | None = None
auto_args: bool = True auto_args: bool = True
arg_metadata: dict[str, str | dict[str, Any]] = Field(default_factory=dict) arg_metadata: dict[str, str | dict[str, Any]] = Field(default_factory=dict)
simple_help_signature: bool = False
_context: ExecutionContext | None = PrivateAttr(default=None) _context: ExecutionContext | None = PrivateAttr(default=None)
@ -317,6 +318,22 @@ class Command(BaseModel):
options_text = self.arg_parser.get_options_text(plain_text=True) options_text = self.arg_parser.get_options_text(plain_text=True)
return f" {command_keys_text:<20} {options_text} " return f" {command_keys_text:<20} {options_text} "
@property
def help_signature(self) -> str:
"""Generate a help signature for the command."""
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}")
if self.tags:
signature.append(f" [dim]Tags: {', '.join(self.tags)}[/dim]")
return "\n".join(signature).strip()
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}"
def log_summary(self) -> None: def log_summary(self) -> None:
if self._context: if self._context:
self._context.log_summary() self._context.log_summary()

View File

@ -284,6 +284,7 @@ class Falyx:
action=Action("Exit", action=_noop), action=Action("Exit", action=_noop),
aliases=["EXIT", "QUIT"], aliases=["EXIT", "QUIT"],
style=OneColors.DARK_RED, style=OneColors.DARK_RED,
simple_help_signature=True,
) )
def _get_history_command(self) -> Command: def _get_history_command(self) -> Command:
@ -294,60 +295,71 @@ class Falyx:
aliases=["HISTORY"], aliases=["HISTORY"],
action=Action(name="View Execution History", action=er.summary), action=Action(name="View Execution History", action=er.summary),
style=OneColors.DARK_YELLOW, style=OneColors.DARK_YELLOW,
simple_help_signature=True,
) )
async def _show_help(self): async def _show_help(self, tag: str = "") -> None:
table = Table(title="[bold cyan]Help Menu[/]", box=box.SIMPLE) if tag:
table.add_column("Key", style="bold", no_wrap=True) table = Table(
table.add_column("Aliases", style="dim", no_wrap=True) title=tag.upper(),
table.add_column("Description", style="dim", overflow="fold") title_justify="left",
table.add_column("Tags", style="dim", no_wrap=True) show_header=False,
box=box.SIMPLE,
show_footer=False,
)
tag_lower = tag.lower()
commands = [
command
for command in self.commands.values()
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)
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(): for command in self.commands.values():
help_text = command.help_text or command.description table.add_row(command.help_signature)
table.add_row(
f"[{command.style}]{command.key}[/]",
", ".join(command.aliases) if command.aliases else "",
help_text,
", ".join(command.tags) if command.tags else "",
)
table.add_row(
f"[{self.exit_command.style}]{self.exit_command.key}[/]",
", ".join(self.exit_command.aliases),
"Exit this menu or program",
)
if self.history_command:
table.add_row(
f"[{self.history_command.style}]{self.history_command.key}[/]",
", ".join(self.history_command.aliases),
"History of executed actions",
)
if self.help_command: if self.help_command:
table.add_row( table.add_row(self.help_command.help_signature)
f"[{self.help_command.style}]{self.help_command.key}[/]", if self.history_command:
", ".join(self.help_command.aliases), table.add_row(self.history_command.help_signature)
"Show this help menu", 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)
self.console.print(table, justify="center")
if self.mode == FalyxMode.MENU:
self.console.print(
f"📦 Tip: '[{OneColors.LIGHT_YELLOW}]?[KEY][/]' to preview a command "
"before running it.\n",
justify="center",
)
def _get_help_command(self) -> Command: def _get_help_command(self) -> Command:
"""Returns the help command for the menu.""" """Returns the help command for the menu."""
parser = CommandArgumentParser(
command_key="H",
command_description="Help",
command_style=OneColors.LIGHT_YELLOW,
aliases=["?", "HELP", "LIST"],
)
parser.add_argument(
"-t",
"--tag",
nargs="?",
default="",
help="Optional tag to filter commands by.",
)
return Command( return Command(
key="H", key="H",
aliases=["HELP", "?"], aliases=["?", "HELP", "LIST"],
description="Help", description="Help",
help_text="Show this help menu",
action=Action("Help", self._show_help), action=Action("Help", self._show_help),
style=OneColors.LIGHT_YELLOW, style=OneColors.LIGHT_YELLOW,
arg_parser=parser,
auto_args=False,
) )
def _get_completer(self) -> WordCompleter: def _get_completer(self) -> WordCompleter:
@ -568,7 +580,9 @@ class Falyx:
if not isinstance(submenu, Falyx): if not isinstance(submenu, Falyx):
raise NotAFalyxError("submenu must be an instance of Falyx.") raise NotAFalyxError("submenu must be an instance of Falyx.")
self._validate_command_key(key) self._validate_command_key(key)
self.add_command(key, description, submenu.menu, style=style) self.add_command(
key, description, submenu.menu, style=style, simple_help_signature=True
)
if submenu.exit_command.key == "X": 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"])
@ -630,6 +644,7 @@ class Falyx:
custom_help: Callable[[], str | None] | None = None, custom_help: Callable[[], str | None] | None = None,
auto_args: bool = True, auto_args: bool = True,
arg_metadata: dict[str, str | dict[str, Any]] | None = None, arg_metadata: dict[str, str | dict[str, Any]] | None = None,
simple_help_signature: bool = False,
) -> Command: ) -> Command:
"""Adds an command to the menu, preventing duplicates.""" """Adds an command to the menu, preventing duplicates."""
self._validate_command_key(key) self._validate_command_key(key)
@ -682,6 +697,7 @@ class Falyx:
custom_help=custom_help, custom_help=custom_help,
auto_args=auto_args, auto_args=auto_args,
arg_metadata=arg_metadata or {}, arg_metadata=arg_metadata or {},
simple_help_signature=simple_help_signature,
) )
if hooks: if hooks:
@ -706,16 +722,16 @@ class Falyx:
def get_bottom_row(self) -> list[str]: def get_bottom_row(self) -> list[str]:
"""Returns the bottom row of the table for displaying additional commands.""" """Returns the bottom row of the table for displaying additional commands."""
bottom_row = [] bottom_row = []
if self.history_command:
bottom_row.append(
f"[{self.history_command.key}] [{self.history_command.style}]"
f"{self.history_command.description}"
)
if self.help_command: if self.help_command:
bottom_row.append( bottom_row.append(
f"[{self.help_command.key}] [{self.help_command.style}]" f"[{self.help_command.key}] [{self.help_command.style}]"
f"{self.help_command.description}" f"{self.help_command.description}"
) )
if self.history_command:
bottom_row.append(
f"[{self.history_command.key}] [{self.history_command.style}]"
f"{self.history_command.description}"
)
bottom_row.append( bottom_row.append(
f"[{self.exit_command.key}] [{self.exit_command.style}]" f"[{self.exit_command.key}] [{self.exit_command.style}]"
f"{self.exit_command.description}" f"{self.exit_command.description}"
@ -727,12 +743,14 @@ class Falyx:
Build the standard table layout. Developers can subclass or call this Build the standard table layout. Developers can subclass or call this
in custom tables. in custom tables.
""" """
table = Table(title=self.title, show_header=False, box=box.SIMPLE, expand=True) # type: ignore[arg-type] table = Table(title=self.title, show_header=False, box=box.SIMPLE) # type: ignore[arg-type]
visible_commands = [item for item in self.commands.items() if not item[1].hidden] visible_commands = [item for item in self.commands.items() if not item[1].hidden]
space = self.console.width // self.columns
for chunk in chunks(visible_commands, self.columns): for chunk in chunks(visible_commands, self.columns):
row = [] row = []
for key, command in chunk: for key, command in chunk:
row.append(f"[{key}] [{command.style}]{command.description}") cell = f"[{key}] [{command.style}]{command.description}"
row.append(f"{cell:<{space}}")
table.add_row(*row) table.add_row(*row)
bottom_row = self.get_bottom_row() bottom_row = self.get_bottom_row()
for row in chunks(bottom_row, self.columns): for row in chunks(bottom_row, self.columns):
@ -1076,7 +1094,7 @@ class Falyx:
self.register_all_with_debug_hooks() self.register_all_with_debug_hooks()
if self.cli_args.command == "list": if self.cli_args.command == "list":
await self._show_help() await self._show_help(tag=self.cli_args.tag)
sys.exit(0) sys.exit(0)
if self.cli_args.command == "version" or self.cli_args.version: if self.cli_args.command == "version" or self.cli_args.version:

View File

@ -255,6 +255,10 @@ def get_arg_parsers(
"list", help="List all available commands with tags" "list", help="List all available commands with tags"
) )
list_parser.add_argument(
"-t", "--tag", help="Filter commands by tag (case-insensitive)", default=None
)
version_parser = subparsers.add_parser("version", help="Show the Falyx version") version_parser = subparsers.add_parser("version", help="Show the Falyx version")
return FalyxParsers( return FalyxParsers(

View File

@ -24,7 +24,6 @@ def infer_args_from_func(
metadata = ( metadata = (
{"help": raw_metadata} if isinstance(raw_metadata, str) else raw_metadata {"help": raw_metadata} if isinstance(raw_metadata, str) else raw_metadata
) )
if param.kind not in ( if param.kind not in (
inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_ONLY,
inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.POSITIONAL_OR_KEYWORD,
@ -35,6 +34,8 @@ def infer_args_from_func(
arg_type = ( arg_type = (
param.annotation if param.annotation is not inspect.Parameter.empty else str param.annotation if param.annotation is not inspect.Parameter.empty else str
) )
if isinstance(arg_type, str):
arg_type = str
default = param.default if param.default is not inspect.Parameter.empty else None default = param.default if param.default is not inspect.Parameter.empty else None
is_required = param.default is inspect.Parameter.empty is_required = param.default is inspect.Parameter.empty
if is_required: if is_required:

View File

@ -1 +1 @@
__version__ = "0.1.43" __version__ = "0.1.44"

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "falyx" name = "falyx"
version = "0.1.43" version = "0.1.44"
description = "Reliable and introspectable async CLI action framework." description = "Reliable and introspectable async CLI action framework."
authors = ["Roland Thomas Jr <roland@rtj.dev>"] authors = ["Roland Thomas Jr <roland@rtj.dev>"]
license = "MIT" license = "MIT"