feat(help): add invocation-aware path rendering for nested CLI help

- introduce InvocationContext and InvocationSegment for styled invocation paths
- thread invocation_context through command arg resolution and help/tldr rendering
- render CLI and namespace help from routed context instead of static program formatting
- support per-segment styling for nested namespaces and command paths
- rebase help target context for `help -k` so usage matches the target command path
- clean up context module docs and remove old invocation path formatting helper
This commit is contained in:
2026-04-11 20:00:01 -04:00
parent 30cb8b97b5
commit 8ece2a5de6
5 changed files with 344 additions and 132 deletions

View File

@@ -51,7 +51,7 @@ from rich.tree import Tree
from falyx.action.action import Action
from falyx.action.base_action import BaseAction
from falyx.console import console
from falyx.context import ExecutionContext
from falyx.context import ExecutionContext, InvocationContext
from falyx.debug import register_debug_hooks
from falyx.exceptions import CommandArgumentError, NotAFalyxError
from falyx.execution_option import ExecutionOption
@@ -213,7 +213,10 @@ class Command(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
async def resolve_args(
self, raw_args: list[str] | str, from_validate: bool = False
self,
raw_args: list[str] | str,
from_validate: bool = False,
invocation_context: InvocationContext | None = None,
) -> tuple[tuple, dict, dict]:
"""Parse CLI arguments into execution-ready components.
@@ -292,7 +295,9 @@ class Command(BaseModel):
)
return await self.arg_parser.parse_args_split(
raw_args, from_validate=from_validate
raw_args,
from_validate=from_validate,
invocation_context=invocation_context,
)
@field_validator("action", mode="before")
@@ -483,12 +488,15 @@ class Command(BaseModel):
if not self.arg_parser:
return "No arguments defined."
command_keys_text = self.arg_parser.get_command_keys_text(plain_text=True)
options_text = self.arg_parser.get_options_text(plain_text=True)
command_keys_text = self.arg_parser.get_command_keys_text()
options_text = self.arg_parser.get_options_text()
return f" {command_keys_text:<20} {options_text} "
@property
def help_signature(self) -> tuple[str, str, str]:
def help_signature(
self,
invocation_context: InvocationContext | None = None,
) -> tuple[str, str, str]:
"""Return a formatted help signature for display.
This property provides the core information used to render command help
@@ -519,7 +527,7 @@ class Command(BaseModel):
- Formatting may vary depending on CLI vs menu mode.
"""
if self.arg_parser and not self.simple_help_signature:
usage = self.arg_parser.get_usage()
usage = self.arg_parser.get_usage(invocation_context=invocation_context)
description = f"[dim]{self.help_text or self.description}[/dim]"
if self.tags:
tags = f"[dim]Tags: {', '.join(self.tags)}[/dim]"
@@ -541,7 +549,7 @@ class Command(BaseModel):
if self._context:
self._context.log_summary()
def render_help(self) -> bool:
def render_help(self, invocation_context: InvocationContext | None = None) -> bool:
"""Display the help message for the command."""
if callable(self.custom_help):
output = self.custom_help()
@@ -549,11 +557,11 @@ class Command(BaseModel):
console.print(output)
return True
if isinstance(self.arg_parser, CommandArgumentParser):
self.arg_parser.render_help()
self.arg_parser.render_help(invocation_context=invocation_context)
return True
return False
def render_tldr(self) -> bool:
def render_tldr(self, invocation_context: InvocationContext | None = None) -> bool:
"""Display the TLDR message for the command."""
if callable(self.custom_tldr):
output = self.custom_tldr()
@@ -561,7 +569,7 @@ class Command(BaseModel):
console.print(output)
return True
if isinstance(self.arg_parser, CommandArgumentParser):
self.arg_parser.render_tldr()
self.arg_parser.render_tldr(invocation_context=invocation_context)
return True
return False

View File

@@ -1,18 +1,24 @@
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
"""Context management for Falyx CLI.
"""Context models for Falyx execution and invocation state.
This module defines `ExecutionContext` and `SharedContext`, which are responsible for
capturing per-action and cross-action metadata during CLI workflow execution. These
context objects provide structured introspection, result tracking, error recording,
and time-based performance metrics.
This module defines the core context objects used throughout Falyx to track both
runtime execution metadata and routed invocation-path state.
- `ExecutionContext`: Captures runtime information for a single action execution,
including arguments, results, exceptions, timing, and logging.
- `SharedContext`: Maintains shared state and result propagation across
`ChainedAction` or `ActionGroup` executions.
It provides:
- `ExecutionContext` for per-action execution details such as arguments,
results, exceptions, timing, and summary logging.
- `SharedContext` for transient shared state across grouped or chained
actions, including propagated results, indexed errors, and arbitrary
shared data.
- `InvocationSegment` for representing a single styled token within a
rendered invocation path.
- `InvocationContext` for capturing the current routed command path as an
immutable value object that supports both plain-text and Rich-markup
rendering.
These contexts enable rich introspection, traceability, and workflow coordination,
supporting hook lifecycles, retries, and structured output generation.
Together, these models support Falyx lifecycle hooks, execution tracing,
history/introspection, and context-aware help and usage rendering across CLI
and menu modes.
"""
from __future__ import annotations
@@ -23,6 +29,7 @@ from typing import Any
from pydantic import BaseModel, ConfigDict, Field
from rich.console import Console
from rich.markup import escape
from falyx.console import console
from falyx.mode import FalyxMode
@@ -285,28 +292,161 @@ class SharedContext(BaseModel):
)
class InvocationSegment(BaseModel):
"""Styled path segment used to build an invocation display path.
`InvocationSegment` represents a single token within an `InvocationContext`,
such as a namespace key, command key, or alias. It stores the raw display
text and an optional Rich style so invocation paths can be rendered either
as plain text or styled markup.
Attributes:
text (str): Display text for this path segment.
style (str | None): Optional Rich style applied when rendering this
segment in markup output.
"""
text: str
style: str | None = None
class InvocationContext(BaseModel):
"""Immutable invocation-path context for routed Falyx help and execution.
`InvocationContext` captures the current displayable command path as the router
descends through namespaces and commands. It stores both the raw typed path
(`typed_path`) and a styled segment representation (`segments`) so the same
context can be rendered as plain text or Rich markup.
This model is intended to be treated as an immutable value object. Methods such
as `with_path_segment()` and `without_last_path_segment()` return new context
instances rather than mutating the existing one.
Attributes:
program (str): Root program name used in CLI-mode help and usage output.
program_style (str): Rich style applied to the program name when rendering
`markup_path`.
typed_path (list[str]): Raw invocation tokens collected during routing,
excluding the root program name.
segments (list[InvocationSegment]): Styled path segments used to render the
invocation path with Rich markup.
mode (FalyxMode): Active Falyx mode for this invocation context. This is
used to determine whether the path should include the program name.
is_preview (bool): Whether the current invocation is a preview flow rather
than a normal execution flow.
"""
program: str = ""
program_style: str = ""
typed_path: list[str] = Field(default_factory=list)
segments: list[InvocationSegment] = Field(default_factory=list)
mode: FalyxMode = FalyxMode.MENU
is_preview: bool = False
@property
def is_cli_mode(self) -> bool:
"""Whether this context should render using CLI path semantics.
Returns:
bool: `True` when the invocation is not in menu mode, meaning rendered
paths should include the program name. `False` when in menu mode.
"""
return self.mode != FalyxMode.MENU
def child(self, token: str) -> InvocationContext:
def with_path_segment(
self,
token: str,
*,
style: str | None = None,
) -> InvocationContext:
"""Return a new context with one additional path segment appended.
This method preserves the current context and creates a new
`InvocationContext` with the provided token added to both `typed_path` and
`segments`.
Args:
token (str): Raw path token to append, such as a namespace key,
command key, or alias.
style (str | None): Optional Rich style for the appended segment.
Returns:
InvocationContext: A new context containing the appended path segment.
"""
return InvocationContext(
program=self.program,
program_style=self.program_style,
typed_path=[*self.typed_path, token],
segments=[*self.segments, InvocationSegment(text=token, style=style)],
mode=self.mode,
is_preview=self.is_preview,
)
def display_path(self) -> str:
def without_last_path_segment(self) -> InvocationContext:
"""Return a new context with the last path segment removed.
This method preserves the current context and creates a new
`InvocationContext` with the last token removed from both `typed_path` and
`segments`.
Returns:
InvocationContext: A new context with the last path segment removed, or the
current context if no path segments are present.
"""
if not self.typed_path:
return self
return InvocationContext(
program=self.program,
program_style=self.program_style,
typed_path=self.typed_path[:-1],
segments=self.segments[:-1],
mode=self.mode,
is_preview=self.is_preview,
)
@property
def plain_path(self) -> str:
"""Render the invocation path as plain text.
In CLI mode, the rendered path includes the root program name followed by
all collected path segments. In menu mode, only the collected path segments
are rendered.
Returns:
str: Plain-text invocation path suitable for logs, comparisons, or
non-styled help output.
"""
parts = [seg.text for seg in self.segments]
if self.is_cli_mode:
return " ".join([self.program, *self.typed_path]).strip()
return " ".join(self.typed_path).strip()
return " ".join([self.program, *parts]).strip()
return " ".join(parts).strip()
@property
def markup_path(self) -> str:
"""Render the invocation path as escaped Rich markup.
In CLI mode, the root program name is included and styled with
`program_style` when provided. Each path segment is escaped and styled
using its associated `InvocationSegment.style` value when present.
Returns:
str: Rich-markup invocation path suitable for help and usage rendering.
"""
parts: list[str] = []
if self.is_cli_mode and self.program:
if self.program_style:
parts.append(
f"[{self.program_style}]{escape(self.program)}[/{self.program_style}]"
)
else:
parts.append(escape(self.program))
for seg in self.segments:
if seg.style:
parts.append(f"[{seg.style}]{escape(seg.text)}[/{seg.style}]")
else:
parts.append(escape(seg.text))
return " ".join(parts).strip()
if __name__ == "__main__":

View File

@@ -255,17 +255,11 @@ class Falyx:
"""Returns the current invocation context."""
return InvocationContext(
program=self.program,
program_style=self.program_style,
typed_path=[],
mode=self.options.get("mode"),
)
def format_invocation_path(
self, program: str, typed_path: list[str], *, cli_mode: bool
) -> str:
if cli_mode:
return " ".join([program, *typed_path]).strip()
return " ".join(typed_path).strip()
@property
def is_cli_mode(self) -> bool:
"""Checks if the current mode is a CLI mode."""
@@ -481,14 +475,18 @@ class Falyx:
)
return choice(tips)
async def _render_command_tldr(self, command: Command) -> None:
async def _render_command_tldr(
self,
command: Command,
context: InvocationContext | None = None,
) -> None:
"""Renders the TLDR examples for a command, if available."""
if not isinstance(command, Command):
self.console.print(
f"Entry '{command.key}' is not a command.", style=OneColors.DARK_RED
)
return None
if command.render_tldr():
if command.render_tldr(invocation_context=context):
if self.enable_help_tips:
self.console.print(f"[bold]tip:[/bold] {self.get_tip()}")
else:
@@ -496,7 +494,12 @@ class Falyx:
f"[bold]No TLDR examples available for '{command.description}'.[/bold]"
)
async def _render_command_help(self, command: Command, tldr: bool = False) -> None:
async def _render_command_help(
self,
command: Command,
tldr: bool = False,
context: InvocationContext | None = None,
) -> None:
"""Renders the detailed help for a command, if available."""
if not isinstance(command, Command):
self.console.print(
@@ -504,8 +507,8 @@ class Falyx:
)
return None
if tldr:
await self._render_command_tldr(command)
elif command.render_help():
await self._render_command_tldr(command, context=context)
elif command.render_help(invocation_context=context):
if self.enable_help_tips:
self.console.print(f"\n[bold]tip:[/bold] {self.get_tip()}")
else:
@@ -588,26 +591,25 @@ class Falyx:
)
return None
async def _render_namespace_tldr_help(self, context: InvocationContext) -> None:
# TODO: Create namespace tldr
console.print(context.markup_path)
async def render_namespace_help(
self, context: InvocationContext, tldr: bool = False
) -> None:
if context.mode is FalyxMode.MENU:
if tldr:
await self._render_namespace_tldr_help(context)
elif context.mode is FalyxMode.MENU:
await self._render_menu_help()
else:
print(
self.format_invocation_path(
context.program,
context.typed_path,
cli_mode=True,
)
)
await self._render_cli_help()
await self._render_cli_help(context)
async def _render_cli_help(self) -> None:
async def _render_cli_help(self, context: InvocationContext) -> None:
"""Renders the CLI help menu with all available commands and options."""
usage = self.usage or "[GLOBAL OPTIONS] [COMMAND] [OPTIONS]"
self.console.print(
f"[bold]usage:[/bold] [{self.program_style}]{self.program}[/{self.program_style}] [{self.usage_style}]{usage}[/{self.usage_style}]"
f"[bold]usage:[/bold] {context.markup_path} [{self.usage_style}]{usage}[/{self.usage_style}]"
)
if self.description:
self.console.print(
@@ -653,13 +655,27 @@ class Falyx:
if self.enable_help_tips:
self.console.print(f"\n[bold]tip:[/bold] {self.get_tip()}")
def _help_target_base_context(self, context: InvocationContext) -> InvocationContext:
if not context.typed_path:
return context
last_token = context.typed_path[-1]
entry, _ = self.resolve_entry(last_token)
if entry is self.help_command:
return context.without_last_path_segment()
return context
async def render_help(
self,
tag: str = "",
key: str | None = None,
tldr: bool = False,
invocation_context: InvocationContext | None = None,
) -> None:
"""Renders the help menu with command details, usage examples, and tips."""
context = invocation_context or self.get_current_invocation_context()
if key:
entry, suggestions = self.resolve_entry(key)
if suggestions:
@@ -667,22 +683,38 @@ class Falyx:
f"[{OneColors.LIGHT_YELLOW}]⚠️ Unknown entry '{key}'. Did you mean:[/]"
f"{', '.join(suggestions)[:10]}"
)
elif isinstance(entry, Command):
await self._render_command_help(entry, tldr)
return None
base_context = self._help_target_base_context(context)
if isinstance(entry, Command):
await self._render_command_help(
command=entry,
tldr=tldr,
context=base_context.with_path_segment(key, style=entry.style),
)
elif isinstance(entry, FalyxNamespace):
await entry.namespace.render_namespace_help(
self.get_current_invocation_context(), tldr
context=base_context.with_path_segment(key, style=entry.style),
tldr=tldr,
)
else:
# TODO: Should print something helpful here
self.console.print(
f"[{OneColors.DARK_RED}]❌ No entry found for '{key}'.[/]"
)
elif tldr:
await self._render_command_help(
self.help_command,
tldr,
context=context,
)
elif tag:
await self._render_tag_help(tag)
elif self.options.get("mode") == FalyxMode.MENU:
await self._render_menu_help()
else:
await self._render_cli_help()
await self._render_cli_help(context)
def _get_help_command(self) -> Command:
"""Returns the help command for the menu."""
@@ -693,8 +725,8 @@ class Falyx:
aliases=["HELP", "?"],
program=self.program,
options_manager=self.options,
_is_help_command=True,
)
parser.mark_as_help_command()
parser.add_argument(
"-t",
"--tag",
@@ -733,12 +765,24 @@ class Falyx:
async def _preview(self, key: str) -> None:
"""Previews the execution of a command without actually running it."""
command = await self.resolve_command(key)
if not command:
self.console.print(f"[{OneColors.DARK_RED}]❌ Command '{key}' not found.")
entry, suggestions = self.resolve_entry(key)
if suggestions:
self.console.print(
f"[{OneColors.LIGHT_YELLOW}]⚠️ Unknown entry '{key}'. Did you mean:[/]"
f"{', '.join(suggestions)[:10]}"
)
return None
self.console.print(f"Preview of command '{command.key}': {command.description}")
await command.preview()
if isinstance(entry, FalyxNamespace):
self.console.print(
f"❌ Entry '{key}' is a namespace. Please specify a command to preview.",
style=OneColors.DARK_RED,
)
return None
if not isinstance(entry, Command):
self.console.print(f"[{OneColors.DARK_RED}]❌ No entry found for '{key}'.[/]")
return None
self.console.print(f"Preview of command '{entry.key}': {entry.description}")
await entry.preview()
def _get_preview_command(self) -> Command:
"""Returns the preview command for Falyx."""
@@ -987,7 +1031,7 @@ class Falyx:
description: str,
submenu: Falyx,
*,
style: str = OneColors.CYAN,
style: str | None = None,
aliases: list[str] | None = None,
help_text: str = "",
) -> None:
@@ -1003,7 +1047,7 @@ class Falyx:
namespace=submenu,
aliases=aliases or [],
help_text=help_text or f"Open the {description} namespace.",
style=style,
style=style or submenu.program_style,
)
self.namespaces[key] = entry
@@ -1289,6 +1333,7 @@ class Falyx:
context = InvocationContext(
program=self.program,
program_style=self.program_style,
typed_path=[],
mode=mode or self.options.get("mode"),
is_preview=is_preview,
@@ -1304,7 +1349,9 @@ class Falyx:
assert route.command is not None
try:
args, kwargs, execution_args = await route.command.resolve_args(
route.leaf_argv, from_validate=from_validate
route.leaf_argv,
from_validate=from_validate,
invocation_context=route.context,
)
except CommandArgumentError as error:
if from_validate:
@@ -1312,7 +1359,7 @@ class Falyx:
cursor_position=len(raw_arguments), message=str(error)
) from error
else:
route.command.render_help()
route.command.render_help(invocation_context=route.context)
self.console.print(
f"[{OneColors.DARK_RED}]❌ [{route.command.key}]: {error}"
)
@@ -1368,6 +1415,10 @@ class Falyx:
await command.preview()
return None
if command is route.namespace.help_command:
kwargs = kwargs or {}
kwargs["invocation_context"] = route.context
logger.debug(
"Executing command '%s' with args=%s, kwargs=%s, execution_args=%s",
route.command.description,
@@ -1497,20 +1548,17 @@ class Falyx:
suggestions=suggestions,
)
child_context = context.child(head)
route_context = context.with_path_segment(head, style=entry.style)
# 4. Namespace entry -> recurse with remaining tokens
if isinstance(entry, FalyxNamespace):
return await entry.namespace.resolve_route(
tail,
context=child_context,
)
return await entry.namespace.resolve_route(tail, context=route_context)
# 5. Leaf command -> stop routing; leave tail untouched for leaf parser
return RouteResult(
kind=RouteKind.COMMAND,
namespace=self,
context=child_context,
context=route_context,
command=entry,
leaf_argv=tail,
)
@@ -1694,7 +1742,16 @@ class Falyx:
logger.info("[asyncio.CancelledError]. <- Exiting run.")
sys.exit(1)
if route.kind is RouteKind.NAMESPACE_MENU or not always_start_menu:
if (
route.kind
in (
RouteKind.NAMESPACE_MENU,
RouteKind.NAMESPACE_TLDR,
RouteKind.NAMESPACE_HELP,
)
or route.command is self.help_command
or not always_start_menu
):
sys.exit(0)
await self.menu()

View File

@@ -58,6 +58,7 @@ from rich.panel import Panel
from falyx.action.base_action import BaseAction
from falyx.console import console
from falyx.context import InvocationContext
from falyx.exceptions import CommandArgumentError, NotAFalyxError
from falyx.execution_option import ExecutionOption
from falyx.mode import FalyxMode
@@ -136,7 +137,6 @@ class CommandArgumentParser:
tldr_examples: list[tuple[str, str]] | None = None,
program: str | None = None,
options_manager: OptionsManager | None = None,
_is_help_command: bool = False,
) -> None:
"""Initialize the CommandArgumentParser."""
self.console: Console = console
@@ -162,11 +162,15 @@ class CommandArgumentParser:
self._arg_group_by_dest: dict[str, str] = {}
self._mutex_group_by_dest: dict[str, str] = {}
self._tldr_examples: list[TLDRExample] = []
self._is_help_command: bool = _is_help_command
self._is_help_command: bool = False
if tldr_examples:
self.add_tldr_examples(tldr_examples)
self.options_manager: OptionsManager = options_manager or OptionsManager()
def mark_as_help_command(self) -> None:
"""Mark this parser as the help command parser."""
self._is_help_command = True
def set_options_manager(self, options_manager: OptionsManager) -> None:
"""Set the options manager for the parser."""
if not isinstance(options_manager, OptionsManager):
@@ -1129,6 +1133,7 @@ class CommandArgumentParser:
consumed_indices: set[int],
arg_states: dict[str, ArgumentState],
from_validate: bool = False,
invocation_context: InvocationContext | None = None,
) -> int:
"""Handle a single token in the command line arguments."""
if token in self._keyword:
@@ -1137,7 +1142,7 @@ class CommandArgumentParser:
if action == ArgumentAction.HELP:
if not from_validate:
self.render_help()
self.render_help(invocation_context=invocation_context)
arg_states[spec.dest].set_consumed()
raise HelpSignal()
elif action == ArgumentAction.TLDR:
@@ -1147,7 +1152,7 @@ class CommandArgumentParser:
consumed_indices.add(index)
index += 1
elif not from_validate:
self.render_tldr()
self.render_tldr(invocation_context=invocation_context)
arg_states[spec.dest].set_consumed()
raise HelpSignal()
else:
@@ -1344,7 +1349,10 @@ class CommandArgumentParser:
)
async def parse_args(
self, args: list[str] | None = None, from_validate: bool = False
self,
args: list[str] | None = None,
from_validate: bool = False,
invocation_context: InvocationContext | None = None,
) -> dict[str, Any]:
"""Parse CLI arguments into a resolved mapping of values.
@@ -1416,6 +1424,7 @@ class CommandArgumentParser:
consumed_indices,
arg_states=arg_states,
from_validate=from_validate,
invocation_context=invocation_context,
)
# Compare length of args with length of required positional arguments to catch missing required positionals
@@ -1512,7 +1521,10 @@ class CommandArgumentParser:
return result
async def parse_args_split(
self, args: list[str], from_validate: bool = False
self,
args: list[str],
from_validate: bool = False,
invocation_context: InvocationContext | None = None,
) -> tuple[tuple[Any, ...], dict[str, Any], dict[str, Any]]:
"""Parse arguments and split them into execution-ready components.
@@ -1536,7 +1548,7 @@ class CommandArgumentParser:
- dict[str, Any]: Keyword arguments for execution.
- dict[str, Any]: Execution-specific arguments handled by Falyx.
"""
parsed = await self.parse_args(args, from_validate)
parsed = await self.parse_args(args, from_validate, invocation_context)
args_list = []
kwargs_dict = {}
execution_dict = {}
@@ -1961,7 +1973,7 @@ class CommandArgumentParser:
return sorted(set(suggestions))
def get_options_text(self, plain_text=False) -> str:
def get_options_text(self) -> str:
"""
Render all defined arguments as a help-style string.
@@ -1983,62 +1995,64 @@ class CommandArgumentParser:
choice_text = arg.get_choice_text()
if isinstance(arg.nargs, int):
choice_text = " ".join([choice_text] * arg.nargs)
if plain_text:
options_list.append(choice_text)
else:
options_list.append(escape(choice_text))
options_list.append(escape(choice_text))
return " ".join(options_list)
def get_command_keys_text(self, plain_text=False) -> str:
"""
Return formatted string showing the command key and aliases.
def get_command_keys_text(self) -> str:
"""Return formatted string showing the command key and aliases.
Used in help rendering and introspection.
Returns:
str: The visual command selector line.
"""
if plain_text:
command_keys = " | ".join(
[f"{self.command_key}"] + [f"{alias}" for alias in self.aliases]
)
else:
command_keys = " | ".join(
[f"[{self.command_style}]{self.command_key}[/{self.command_style}]"]
+ [
f"[{self.command_style}]{alias}[/{self.command_style}]"
for alias in self.aliases
]
)
command_keys = " | ".join(
[f"[{self.command_style}]{self.command_key}[/{self.command_style}]"]
+ [
f"[{self.command_style}]{alias}[/{self.command_style}]"
for alias in self.aliases
]
)
return command_keys
def get_usage(self, plain_text=False) -> str:
"""
Render the usage string for this parser.
def _get_invocation_prefix(
self,
invocation_context: InvocationContext | None = None,
) -> str:
if invocation_context is None:
command_keys = self.get_command_keys_text()
if self.options_manager.get("mode") == FalyxMode.MENU:
return command_keys
program = self.program or "falyx"
program_style = (
self.options_manager.get("program_style") or self.command_style
)
return f"[{program_style}]{program}[/{program_style}] {command_keys}"
if invocation_context.is_cli_mode:
return invocation_context.markup_path
return invocation_context.markup_path
def get_usage(
self,
invocation_context: InvocationContext | None = None,
) -> str:
"""Render the usage string for this parser.
Returns:
str: A formatted usage line showing syntax and argument structure.
"""
command_keys = self.get_command_keys_text(plain_text)
options_text = self.get_options_text(plain_text)
if options_text:
if self.options_manager.get("mode") == FalyxMode.MENU:
return f"{command_keys} {options_text}"
else:
program = self.program or "falyx"
program_style = (
self.options_manager.get("program_style") or self.command_style
)
return f"[{program_style}]{program}[/{program_style}] {command_keys} {options_text}"
return command_keys
prefix = self._get_invocation_prefix(invocation_context)
options_text = self.get_options_text()
return f"{prefix} {options_text}".strip() if options_text else prefix
def _iter_keyword_help_sections(
self,
) -> Generator[tuple[str, str, list[Argument]], None, None]:
"""
Yields (title, description, arguments)
"""
"""Yields (title, description, arguments)"""
assigned = set()
for group in self._argument_groups.values():
@@ -2059,7 +2073,11 @@ class CommandArgumentParser:
if ungrouped:
yield "options", "", ungrouped
def render_help(self) -> None:
def render_help(
self,
*,
invocation_context: InvocationContext | None = None,
) -> None:
"""Render full help output for the command.
This method displays a complete help view for the command, including
@@ -2076,7 +2094,7 @@ class CommandArgumentParser:
- Supports argument grouping and mutually exclusive groups
- Applies styling based on configured command style
"""
usage = self.get_usage()
usage = self.get_usage(invocation_context)
self.console.print(f"[bold]usage: {usage}[/bold]\n")
if self.help_text:
@@ -2131,7 +2149,7 @@ class CommandArgumentParser:
if self.help_epilog:
self.console.print("\n" + self.help_epilog, style="dim")
def render_tldr(self) -> None:
def render_tldr(self, *, invocation_context: InvocationContext | None = None) -> None:
"""Render concise example usage (TLDR) for the command.
This method displays a minimal, example-driven view of how to invoke
@@ -2148,18 +2166,8 @@ class CommandArgumentParser:
f"[bold]No TLDR examples available for {self.command_key}.[/bold]"
)
return
is_cli_mode = self.options_manager.get("mode") != FalyxMode.MENU
program = self.program or "falyx"
program_style = self.options_manager.get("program_style") or self.command_style
command = self.aliases[0] if self.aliases else self.command_key
if self._is_help_command and is_cli_mode:
command = f"[{program_style}]{program}[/{program_style}] [{self.command_style}]help[/{self.command_style}]"
elif is_cli_mode:
command = f"[{program_style}]{program}[/{program_style}] [{self.command_style}]{command}[/{self.command_style}]"
else:
command = f"[{self.command_style}]{command}[/{self.command_style}]"
usage = self.get_usage()
prefix = self._get_invocation_prefix(invocation_context)
usage = self.get_usage(invocation_context)
self.console.print(f"[bold]usage:[/] {usage}\n")
if self.help_text:
@@ -2167,7 +2175,7 @@ class CommandArgumentParser:
self.console.print("[bold]examples:[/bold]")
for example in self._tldr_examples:
usage = f"{command} {example.usage.strip()}"
usage = f"{prefix} {example.usage.strip()}"
description = example.description.strip()
block = f"[bold]{usage}[/bold]"
self.console.print(

View File

@@ -28,6 +28,5 @@ class RouteResult:
command: "Command | None" = None
namespace_entry: FalyxNamespace | None = None
leaf_argv: list[str] = field(default_factory=list)
typed_path: list[str] = field(default_factory=list)
suggestions: list[str] = field(default_factory=list)
is_preview: bool = False