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:
@@ -51,7 +51,7 @@ from rich.tree import Tree
|
|||||||
from falyx.action.action import Action
|
from falyx.action.action import Action
|
||||||
from falyx.action.base_action import BaseAction
|
from falyx.action.base_action import BaseAction
|
||||||
from falyx.console import console
|
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.debug import register_debug_hooks
|
||||||
from falyx.exceptions import CommandArgumentError, NotAFalyxError
|
from falyx.exceptions import CommandArgumentError, NotAFalyxError
|
||||||
from falyx.execution_option import ExecutionOption
|
from falyx.execution_option import ExecutionOption
|
||||||
@@ -213,7 +213,10 @@ class Command(BaseModel):
|
|||||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||||
|
|
||||||
async def resolve_args(
|
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]:
|
) -> tuple[tuple, dict, dict]:
|
||||||
"""Parse CLI arguments into execution-ready components.
|
"""Parse CLI arguments into execution-ready components.
|
||||||
|
|
||||||
@@ -292,7 +295,9 @@ class Command(BaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return await self.arg_parser.parse_args_split(
|
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")
|
@field_validator("action", mode="before")
|
||||||
@@ -483,12 +488,15 @@ class Command(BaseModel):
|
|||||||
if not self.arg_parser:
|
if not self.arg_parser:
|
||||||
return "No arguments defined."
|
return "No arguments defined."
|
||||||
|
|
||||||
command_keys_text = self.arg_parser.get_command_keys_text(plain_text=True)
|
command_keys_text = self.arg_parser.get_command_keys_text()
|
||||||
options_text = self.arg_parser.get_options_text(plain_text=True)
|
options_text = self.arg_parser.get_options_text()
|
||||||
return f" {command_keys_text:<20} {options_text} "
|
return f" {command_keys_text:<20} {options_text} "
|
||||||
|
|
||||||
@property
|
@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.
|
"""Return a formatted help signature for display.
|
||||||
|
|
||||||
This property provides the core information used to render command help
|
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.
|
- Formatting may vary depending on CLI vs menu mode.
|
||||||
"""
|
"""
|
||||||
if self.arg_parser and not self.simple_help_signature:
|
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]"
|
description = f"[dim]{self.help_text or self.description}[/dim]"
|
||||||
if self.tags:
|
if self.tags:
|
||||||
tags = f"[dim]Tags: {', '.join(self.tags)}[/dim]"
|
tags = f"[dim]Tags: {', '.join(self.tags)}[/dim]"
|
||||||
@@ -541,7 +549,7 @@ class Command(BaseModel):
|
|||||||
if self._context:
|
if self._context:
|
||||||
self._context.log_summary()
|
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."""
|
"""Display the help message for the command."""
|
||||||
if callable(self.custom_help):
|
if callable(self.custom_help):
|
||||||
output = self.custom_help()
|
output = self.custom_help()
|
||||||
@@ -549,11 +557,11 @@ class Command(BaseModel):
|
|||||||
console.print(output)
|
console.print(output)
|
||||||
return True
|
return True
|
||||||
if isinstance(self.arg_parser, CommandArgumentParser):
|
if isinstance(self.arg_parser, CommandArgumentParser):
|
||||||
self.arg_parser.render_help()
|
self.arg_parser.render_help(invocation_context=invocation_context)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def render_tldr(self) -> bool:
|
def render_tldr(self, invocation_context: InvocationContext | None = None) -> bool:
|
||||||
"""Display the TLDR message for the command."""
|
"""Display the TLDR message for the command."""
|
||||||
if callable(self.custom_tldr):
|
if callable(self.custom_tldr):
|
||||||
output = self.custom_tldr()
|
output = self.custom_tldr()
|
||||||
@@ -561,7 +569,7 @@ class Command(BaseModel):
|
|||||||
console.print(output)
|
console.print(output)
|
||||||
return True
|
return True
|
||||||
if isinstance(self.arg_parser, CommandArgumentParser):
|
if isinstance(self.arg_parser, CommandArgumentParser):
|
||||||
self.arg_parser.render_tldr()
|
self.arg_parser.render_tldr(invocation_context=invocation_context)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
170
falyx/context.py
170
falyx/context.py
@@ -1,18 +1,24 @@
|
|||||||
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
# 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
|
This module defines the core context objects used throughout Falyx to track both
|
||||||
capturing per-action and cross-action metadata during CLI workflow execution. These
|
runtime execution metadata and routed invocation-path state.
|
||||||
context objects provide structured introspection, result tracking, error recording,
|
|
||||||
and time-based performance metrics.
|
|
||||||
|
|
||||||
- `ExecutionContext`: Captures runtime information for a single action execution,
|
It provides:
|
||||||
including arguments, results, exceptions, timing, and logging.
|
- `ExecutionContext` for per-action execution details such as arguments,
|
||||||
- `SharedContext`: Maintains shared state and result propagation across
|
results, exceptions, timing, and summary logging.
|
||||||
`ChainedAction` or `ActionGroup` executions.
|
- `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,
|
Together, these models support Falyx lifecycle hooks, execution tracing,
|
||||||
supporting hook lifecycles, retries, and structured output generation.
|
history/introspection, and context-aware help and usage rendering across CLI
|
||||||
|
and menu modes.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -23,6 +29,7 @@ from typing import Any
|
|||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
from rich.markup import escape
|
||||||
|
|
||||||
from falyx.console import console
|
from falyx.console import console
|
||||||
from falyx.mode import FalyxMode
|
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):
|
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: str = ""
|
||||||
|
program_style: str = ""
|
||||||
typed_path: list[str] = Field(default_factory=list)
|
typed_path: list[str] = Field(default_factory=list)
|
||||||
|
segments: list[InvocationSegment] = Field(default_factory=list)
|
||||||
mode: FalyxMode = FalyxMode.MENU
|
mode: FalyxMode = FalyxMode.MENU
|
||||||
is_preview: bool = False
|
is_preview: bool = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_cli_mode(self) -> bool:
|
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
|
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(
|
return InvocationContext(
|
||||||
program=self.program,
|
program=self.program,
|
||||||
|
program_style=self.program_style,
|
||||||
typed_path=[*self.typed_path, token],
|
typed_path=[*self.typed_path, token],
|
||||||
|
segments=[*self.segments, InvocationSegment(text=token, style=style)],
|
||||||
mode=self.mode,
|
mode=self.mode,
|
||||||
is_preview=self.is_preview,
|
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:
|
if self.is_cli_mode:
|
||||||
return " ".join([self.program, *self.typed_path]).strip()
|
return " ".join([self.program, *parts]).strip()
|
||||||
return " ".join(self.typed_path).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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
145
falyx/falyx.py
145
falyx/falyx.py
@@ -255,17 +255,11 @@ class Falyx:
|
|||||||
"""Returns the current invocation context."""
|
"""Returns the current invocation context."""
|
||||||
return InvocationContext(
|
return InvocationContext(
|
||||||
program=self.program,
|
program=self.program,
|
||||||
|
program_style=self.program_style,
|
||||||
typed_path=[],
|
typed_path=[],
|
||||||
mode=self.options.get("mode"),
|
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
|
@property
|
||||||
def is_cli_mode(self) -> bool:
|
def is_cli_mode(self) -> bool:
|
||||||
"""Checks if the current mode is a CLI mode."""
|
"""Checks if the current mode is a CLI mode."""
|
||||||
@@ -481,14 +475,18 @@ class Falyx:
|
|||||||
)
|
)
|
||||||
return choice(tips)
|
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."""
|
"""Renders the TLDR examples for a command, if available."""
|
||||||
if not isinstance(command, Command):
|
if not isinstance(command, Command):
|
||||||
self.console.print(
|
self.console.print(
|
||||||
f"Entry '{command.key}' is not a command.", style=OneColors.DARK_RED
|
f"Entry '{command.key}' is not a command.", style=OneColors.DARK_RED
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
if command.render_tldr():
|
if command.render_tldr(invocation_context=context):
|
||||||
if self.enable_help_tips:
|
if self.enable_help_tips:
|
||||||
self.console.print(f"[bold]tip:[/bold] {self.get_tip()}")
|
self.console.print(f"[bold]tip:[/bold] {self.get_tip()}")
|
||||||
else:
|
else:
|
||||||
@@ -496,7 +494,12 @@ class Falyx:
|
|||||||
f"[bold]No TLDR examples available for '{command.description}'.[/bold]"
|
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."""
|
"""Renders the detailed help for a command, if available."""
|
||||||
if not isinstance(command, Command):
|
if not isinstance(command, Command):
|
||||||
self.console.print(
|
self.console.print(
|
||||||
@@ -504,8 +507,8 @@ class Falyx:
|
|||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
if tldr:
|
if tldr:
|
||||||
await self._render_command_tldr(command)
|
await self._render_command_tldr(command, context=context)
|
||||||
elif command.render_help():
|
elif command.render_help(invocation_context=context):
|
||||||
if self.enable_help_tips:
|
if self.enable_help_tips:
|
||||||
self.console.print(f"\n[bold]tip:[/bold] {self.get_tip()}")
|
self.console.print(f"\n[bold]tip:[/bold] {self.get_tip()}")
|
||||||
else:
|
else:
|
||||||
@@ -588,26 +591,25 @@ class Falyx:
|
|||||||
)
|
)
|
||||||
return None
|
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(
|
async def render_namespace_help(
|
||||||
self, context: InvocationContext, tldr: bool = False
|
self, context: InvocationContext, tldr: bool = False
|
||||||
) -> None:
|
) -> 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()
|
await self._render_menu_help()
|
||||||
else:
|
else:
|
||||||
print(
|
await self._render_cli_help(context)
|
||||||
self.format_invocation_path(
|
|
||||||
context.program,
|
|
||||||
context.typed_path,
|
|
||||||
cli_mode=True,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
await self._render_cli_help()
|
|
||||||
|
|
||||||
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."""
|
"""Renders the CLI help menu with all available commands and options."""
|
||||||
usage = self.usage or "[GLOBAL OPTIONS] [COMMAND] [OPTIONS]"
|
usage = self.usage or "[GLOBAL OPTIONS] [COMMAND] [OPTIONS]"
|
||||||
self.console.print(
|
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:
|
if self.description:
|
||||||
self.console.print(
|
self.console.print(
|
||||||
@@ -653,13 +655,27 @@ class Falyx:
|
|||||||
if self.enable_help_tips:
|
if self.enable_help_tips:
|
||||||
self.console.print(f"\n[bold]tip:[/bold] {self.get_tip()}")
|
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(
|
async def render_help(
|
||||||
self,
|
self,
|
||||||
tag: str = "",
|
tag: str = "",
|
||||||
key: str | None = None,
|
key: str | None = None,
|
||||||
tldr: bool = False,
|
tldr: bool = False,
|
||||||
|
invocation_context: InvocationContext | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Renders the help menu with command details, usage examples, and tips."""
|
"""Renders the help menu with command details, usage examples, and tips."""
|
||||||
|
context = invocation_context or self.get_current_invocation_context()
|
||||||
if key:
|
if key:
|
||||||
entry, suggestions = self.resolve_entry(key)
|
entry, suggestions = self.resolve_entry(key)
|
||||||
if suggestions:
|
if suggestions:
|
||||||
@@ -667,22 +683,38 @@ class Falyx:
|
|||||||
f"[{OneColors.LIGHT_YELLOW}]⚠️ Unknown entry '{key}'. Did you mean:[/]"
|
f"[{OneColors.LIGHT_YELLOW}]⚠️ Unknown entry '{key}'. Did you mean:[/]"
|
||||||
f"{', '.join(suggestions)[:10]}"
|
f"{', '.join(suggestions)[:10]}"
|
||||||
)
|
)
|
||||||
elif isinstance(entry, Command):
|
return None
|
||||||
await self._render_command_help(entry, tldr)
|
|
||||||
|
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):
|
elif isinstance(entry, FalyxNamespace):
|
||||||
await entry.namespace.render_namespace_help(
|
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:
|
else:
|
||||||
|
# TODO: Should print something helpful here
|
||||||
self.console.print(
|
self.console.print(
|
||||||
f"[{OneColors.DARK_RED}]❌ No entry found for '{key}'.[/]"
|
f"[{OneColors.DARK_RED}]❌ No entry found for '{key}'.[/]"
|
||||||
)
|
)
|
||||||
|
elif tldr:
|
||||||
|
await self._render_command_help(
|
||||||
|
self.help_command,
|
||||||
|
tldr,
|
||||||
|
context=context,
|
||||||
|
)
|
||||||
elif tag:
|
elif tag:
|
||||||
await self._render_tag_help(tag)
|
await self._render_tag_help(tag)
|
||||||
elif self.options.get("mode") == FalyxMode.MENU:
|
elif self.options.get("mode") == FalyxMode.MENU:
|
||||||
await self._render_menu_help()
|
await self._render_menu_help()
|
||||||
else:
|
else:
|
||||||
await self._render_cli_help()
|
await self._render_cli_help(context)
|
||||||
|
|
||||||
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."""
|
||||||
@@ -693,8 +725,8 @@ class Falyx:
|
|||||||
aliases=["HELP", "?"],
|
aliases=["HELP", "?"],
|
||||||
program=self.program,
|
program=self.program,
|
||||||
options_manager=self.options,
|
options_manager=self.options,
|
||||||
_is_help_command=True,
|
|
||||||
)
|
)
|
||||||
|
parser.mark_as_help_command()
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-t",
|
"-t",
|
||||||
"--tag",
|
"--tag",
|
||||||
@@ -733,12 +765,24 @@ class Falyx:
|
|||||||
|
|
||||||
async def _preview(self, key: str) -> None:
|
async def _preview(self, key: str) -> None:
|
||||||
"""Previews the execution of a command without actually running it."""
|
"""Previews the execution of a command without actually running it."""
|
||||||
command = await self.resolve_command(key)
|
entry, suggestions = self.resolve_entry(key)
|
||||||
if not command:
|
if suggestions:
|
||||||
self.console.print(f"[{OneColors.DARK_RED}]❌ Command '{key}' not found.")
|
self.console.print(
|
||||||
|
f"[{OneColors.LIGHT_YELLOW}]⚠️ Unknown entry '{key}'. Did you mean:[/]"
|
||||||
|
f"{', '.join(suggestions)[:10]}"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
self.console.print(f"Preview of command '{command.key}': {command.description}")
|
if isinstance(entry, FalyxNamespace):
|
||||||
await command.preview()
|
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:
|
def _get_preview_command(self) -> Command:
|
||||||
"""Returns the preview command for Falyx."""
|
"""Returns the preview command for Falyx."""
|
||||||
@@ -987,7 +1031,7 @@ class Falyx:
|
|||||||
description: str,
|
description: str,
|
||||||
submenu: Falyx,
|
submenu: Falyx,
|
||||||
*,
|
*,
|
||||||
style: str = OneColors.CYAN,
|
style: str | None = None,
|
||||||
aliases: list[str] | None = None,
|
aliases: list[str] | None = None,
|
||||||
help_text: str = "",
|
help_text: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -1003,7 +1047,7 @@ class Falyx:
|
|||||||
namespace=submenu,
|
namespace=submenu,
|
||||||
aliases=aliases or [],
|
aliases=aliases or [],
|
||||||
help_text=help_text or f"Open the {description} namespace.",
|
help_text=help_text or f"Open the {description} namespace.",
|
||||||
style=style,
|
style=style or submenu.program_style,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.namespaces[key] = entry
|
self.namespaces[key] = entry
|
||||||
@@ -1289,6 +1333,7 @@ class Falyx:
|
|||||||
|
|
||||||
context = InvocationContext(
|
context = InvocationContext(
|
||||||
program=self.program,
|
program=self.program,
|
||||||
|
program_style=self.program_style,
|
||||||
typed_path=[],
|
typed_path=[],
|
||||||
mode=mode or self.options.get("mode"),
|
mode=mode or self.options.get("mode"),
|
||||||
is_preview=is_preview,
|
is_preview=is_preview,
|
||||||
@@ -1304,7 +1349,9 @@ class Falyx:
|
|||||||
assert route.command is not None
|
assert route.command is not None
|
||||||
try:
|
try:
|
||||||
args, kwargs, execution_args = await route.command.resolve_args(
|
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:
|
except CommandArgumentError as error:
|
||||||
if from_validate:
|
if from_validate:
|
||||||
@@ -1312,7 +1359,7 @@ class Falyx:
|
|||||||
cursor_position=len(raw_arguments), message=str(error)
|
cursor_position=len(raw_arguments), message=str(error)
|
||||||
) from error
|
) from error
|
||||||
else:
|
else:
|
||||||
route.command.render_help()
|
route.command.render_help(invocation_context=route.context)
|
||||||
self.console.print(
|
self.console.print(
|
||||||
f"[{OneColors.DARK_RED}]❌ [{route.command.key}]: {error}"
|
f"[{OneColors.DARK_RED}]❌ [{route.command.key}]: {error}"
|
||||||
)
|
)
|
||||||
@@ -1368,6 +1415,10 @@ class Falyx:
|
|||||||
await command.preview()
|
await command.preview()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if command is route.namespace.help_command:
|
||||||
|
kwargs = kwargs or {}
|
||||||
|
kwargs["invocation_context"] = route.context
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Executing command '%s' with args=%s, kwargs=%s, execution_args=%s",
|
"Executing command '%s' with args=%s, kwargs=%s, execution_args=%s",
|
||||||
route.command.description,
|
route.command.description,
|
||||||
@@ -1497,20 +1548,17 @@ class Falyx:
|
|||||||
suggestions=suggestions,
|
suggestions=suggestions,
|
||||||
)
|
)
|
||||||
|
|
||||||
child_context = context.child(head)
|
route_context = context.with_path_segment(head, style=entry.style)
|
||||||
|
|
||||||
# 4. Namespace entry -> recurse with remaining tokens
|
# 4. Namespace entry -> recurse with remaining tokens
|
||||||
if isinstance(entry, FalyxNamespace):
|
if isinstance(entry, FalyxNamespace):
|
||||||
return await entry.namespace.resolve_route(
|
return await entry.namespace.resolve_route(tail, context=route_context)
|
||||||
tail,
|
|
||||||
context=child_context,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 5. Leaf command -> stop routing; leave tail untouched for leaf parser
|
# 5. Leaf command -> stop routing; leave tail untouched for leaf parser
|
||||||
return RouteResult(
|
return RouteResult(
|
||||||
kind=RouteKind.COMMAND,
|
kind=RouteKind.COMMAND,
|
||||||
namespace=self,
|
namespace=self,
|
||||||
context=child_context,
|
context=route_context,
|
||||||
command=entry,
|
command=entry,
|
||||||
leaf_argv=tail,
|
leaf_argv=tail,
|
||||||
)
|
)
|
||||||
@@ -1694,7 +1742,16 @@ class Falyx:
|
|||||||
logger.info("[asyncio.CancelledError]. <- Exiting run.")
|
logger.info("[asyncio.CancelledError]. <- Exiting run.")
|
||||||
sys.exit(1)
|
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)
|
sys.exit(0)
|
||||||
|
|
||||||
await self.menu()
|
await self.menu()
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ from rich.panel import Panel
|
|||||||
|
|
||||||
from falyx.action.base_action import BaseAction
|
from falyx.action.base_action import BaseAction
|
||||||
from falyx.console import console
|
from falyx.console import console
|
||||||
|
from falyx.context import InvocationContext
|
||||||
from falyx.exceptions import CommandArgumentError, NotAFalyxError
|
from falyx.exceptions import CommandArgumentError, NotAFalyxError
|
||||||
from falyx.execution_option import ExecutionOption
|
from falyx.execution_option import ExecutionOption
|
||||||
from falyx.mode import FalyxMode
|
from falyx.mode import FalyxMode
|
||||||
@@ -136,7 +137,6 @@ class CommandArgumentParser:
|
|||||||
tldr_examples: list[tuple[str, str]] | None = None,
|
tldr_examples: list[tuple[str, str]] | None = None,
|
||||||
program: str | None = None,
|
program: str | None = None,
|
||||||
options_manager: OptionsManager | None = None,
|
options_manager: OptionsManager | None = None,
|
||||||
_is_help_command: bool = False,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the CommandArgumentParser."""
|
"""Initialize the CommandArgumentParser."""
|
||||||
self.console: Console = console
|
self.console: Console = console
|
||||||
@@ -162,11 +162,15 @@ class CommandArgumentParser:
|
|||||||
self._arg_group_by_dest: dict[str, str] = {}
|
self._arg_group_by_dest: dict[str, str] = {}
|
||||||
self._mutex_group_by_dest: dict[str, str] = {}
|
self._mutex_group_by_dest: dict[str, str] = {}
|
||||||
self._tldr_examples: list[TLDRExample] = []
|
self._tldr_examples: list[TLDRExample] = []
|
||||||
self._is_help_command: bool = _is_help_command
|
self._is_help_command: bool = False
|
||||||
if tldr_examples:
|
if tldr_examples:
|
||||||
self.add_tldr_examples(tldr_examples)
|
self.add_tldr_examples(tldr_examples)
|
||||||
self.options_manager: OptionsManager = options_manager or OptionsManager()
|
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:
|
def set_options_manager(self, options_manager: OptionsManager) -> None:
|
||||||
"""Set the options manager for the parser."""
|
"""Set the options manager for the parser."""
|
||||||
if not isinstance(options_manager, OptionsManager):
|
if not isinstance(options_manager, OptionsManager):
|
||||||
@@ -1129,6 +1133,7 @@ class CommandArgumentParser:
|
|||||||
consumed_indices: set[int],
|
consumed_indices: set[int],
|
||||||
arg_states: dict[str, ArgumentState],
|
arg_states: dict[str, ArgumentState],
|
||||||
from_validate: bool = False,
|
from_validate: bool = False,
|
||||||
|
invocation_context: InvocationContext | None = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Handle a single token in the command line arguments."""
|
"""Handle a single token in the command line arguments."""
|
||||||
if token in self._keyword:
|
if token in self._keyword:
|
||||||
@@ -1137,7 +1142,7 @@ class CommandArgumentParser:
|
|||||||
|
|
||||||
if action == ArgumentAction.HELP:
|
if action == ArgumentAction.HELP:
|
||||||
if not from_validate:
|
if not from_validate:
|
||||||
self.render_help()
|
self.render_help(invocation_context=invocation_context)
|
||||||
arg_states[spec.dest].set_consumed()
|
arg_states[spec.dest].set_consumed()
|
||||||
raise HelpSignal()
|
raise HelpSignal()
|
||||||
elif action == ArgumentAction.TLDR:
|
elif action == ArgumentAction.TLDR:
|
||||||
@@ -1147,7 +1152,7 @@ class CommandArgumentParser:
|
|||||||
consumed_indices.add(index)
|
consumed_indices.add(index)
|
||||||
index += 1
|
index += 1
|
||||||
elif not from_validate:
|
elif not from_validate:
|
||||||
self.render_tldr()
|
self.render_tldr(invocation_context=invocation_context)
|
||||||
arg_states[spec.dest].set_consumed()
|
arg_states[spec.dest].set_consumed()
|
||||||
raise HelpSignal()
|
raise HelpSignal()
|
||||||
else:
|
else:
|
||||||
@@ -1344,7 +1349,10 @@ class CommandArgumentParser:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def parse_args(
|
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]:
|
) -> dict[str, Any]:
|
||||||
"""Parse CLI arguments into a resolved mapping of values.
|
"""Parse CLI arguments into a resolved mapping of values.
|
||||||
|
|
||||||
@@ -1416,6 +1424,7 @@ class CommandArgumentParser:
|
|||||||
consumed_indices,
|
consumed_indices,
|
||||||
arg_states=arg_states,
|
arg_states=arg_states,
|
||||||
from_validate=from_validate,
|
from_validate=from_validate,
|
||||||
|
invocation_context=invocation_context,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Compare length of args with length of required positional arguments to catch missing required positionals
|
# Compare length of args with length of required positional arguments to catch missing required positionals
|
||||||
@@ -1512,7 +1521,10 @@ class CommandArgumentParser:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
async def parse_args_split(
|
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]]:
|
) -> tuple[tuple[Any, ...], dict[str, Any], dict[str, Any]]:
|
||||||
"""Parse arguments and split them into execution-ready components.
|
"""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]: Keyword arguments for execution.
|
||||||
- dict[str, Any]: Execution-specific arguments handled by Falyx.
|
- 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 = []
|
args_list = []
|
||||||
kwargs_dict = {}
|
kwargs_dict = {}
|
||||||
execution_dict = {}
|
execution_dict = {}
|
||||||
@@ -1961,7 +1973,7 @@ class CommandArgumentParser:
|
|||||||
|
|
||||||
return sorted(set(suggestions))
|
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.
|
Render all defined arguments as a help-style string.
|
||||||
|
|
||||||
@@ -1983,62 +1995,64 @@ class CommandArgumentParser:
|
|||||||
choice_text = arg.get_choice_text()
|
choice_text = arg.get_choice_text()
|
||||||
if isinstance(arg.nargs, int):
|
if isinstance(arg.nargs, int):
|
||||||
choice_text = " ".join([choice_text] * arg.nargs)
|
choice_text = " ".join([choice_text] * arg.nargs)
|
||||||
if plain_text:
|
options_list.append(escape(choice_text))
|
||||||
options_list.append(choice_text)
|
|
||||||
else:
|
|
||||||
options_list.append(escape(choice_text))
|
|
||||||
|
|
||||||
return " ".join(options_list)
|
return " ".join(options_list)
|
||||||
|
|
||||||
def get_command_keys_text(self, plain_text=False) -> str:
|
def get_command_keys_text(self) -> str:
|
||||||
"""
|
"""Return formatted string showing the command key and aliases.
|
||||||
Return formatted string showing the command key and aliases.
|
|
||||||
|
|
||||||
Used in help rendering and introspection.
|
Used in help rendering and introspection.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: The visual command selector line.
|
str: The visual command selector line.
|
||||||
"""
|
"""
|
||||||
if plain_text:
|
command_keys = " | ".join(
|
||||||
command_keys = " | ".join(
|
[f"[{self.command_style}]{self.command_key}[/{self.command_style}]"]
|
||||||
[f"{self.command_key}"] + [f"{alias}" for alias in self.aliases]
|
+ [
|
||||||
)
|
f"[{self.command_style}]{alias}[/{self.command_style}]"
|
||||||
else:
|
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
|
return command_keys
|
||||||
|
|
||||||
def get_usage(self, plain_text=False) -> str:
|
def _get_invocation_prefix(
|
||||||
"""
|
self,
|
||||||
Render the usage string for this parser.
|
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:
|
Returns:
|
||||||
str: A formatted usage line showing syntax and argument structure.
|
str: A formatted usage line showing syntax and argument structure.
|
||||||
"""
|
"""
|
||||||
command_keys = self.get_command_keys_text(plain_text)
|
prefix = self._get_invocation_prefix(invocation_context)
|
||||||
options_text = self.get_options_text(plain_text)
|
options_text = self.get_options_text()
|
||||||
if options_text:
|
return f"{prefix} {options_text}".strip() if options_text else prefix
|
||||||
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
|
|
||||||
|
|
||||||
def _iter_keyword_help_sections(
|
def _iter_keyword_help_sections(
|
||||||
self,
|
self,
|
||||||
) -> Generator[tuple[str, str, list[Argument]], None, None]:
|
) -> Generator[tuple[str, str, list[Argument]], None, None]:
|
||||||
"""
|
"""Yields (title, description, arguments)"""
|
||||||
Yields (title, description, arguments)
|
|
||||||
"""
|
|
||||||
assigned = set()
|
assigned = set()
|
||||||
|
|
||||||
for group in self._argument_groups.values():
|
for group in self._argument_groups.values():
|
||||||
@@ -2059,7 +2073,11 @@ class CommandArgumentParser:
|
|||||||
if ungrouped:
|
if ungrouped:
|
||||||
yield "options", "", 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.
|
"""Render full help output for the command.
|
||||||
|
|
||||||
This method displays a complete help view for the command, including
|
This method displays a complete help view for the command, including
|
||||||
@@ -2076,7 +2094,7 @@ class CommandArgumentParser:
|
|||||||
- Supports argument grouping and mutually exclusive groups
|
- Supports argument grouping and mutually exclusive groups
|
||||||
- Applies styling based on configured command style
|
- 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")
|
self.console.print(f"[bold]usage: {usage}[/bold]\n")
|
||||||
|
|
||||||
if self.help_text:
|
if self.help_text:
|
||||||
@@ -2131,7 +2149,7 @@ class CommandArgumentParser:
|
|||||||
if self.help_epilog:
|
if self.help_epilog:
|
||||||
self.console.print("\n" + self.help_epilog, style="dim")
|
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.
|
"""Render concise example usage (TLDR) for the command.
|
||||||
|
|
||||||
This method displays a minimal, example-driven view of how to invoke
|
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]"
|
f"[bold]No TLDR examples available for {self.command_key}.[/bold]"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
is_cli_mode = self.options_manager.get("mode") != FalyxMode.MENU
|
prefix = self._get_invocation_prefix(invocation_context)
|
||||||
program = self.program or "falyx"
|
usage = self.get_usage(invocation_context)
|
||||||
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()
|
|
||||||
self.console.print(f"[bold]usage:[/] {usage}\n")
|
self.console.print(f"[bold]usage:[/] {usage}\n")
|
||||||
|
|
||||||
if self.help_text:
|
if self.help_text:
|
||||||
@@ -2167,7 +2175,7 @@ class CommandArgumentParser:
|
|||||||
|
|
||||||
self.console.print("[bold]examples:[/bold]")
|
self.console.print("[bold]examples:[/bold]")
|
||||||
for example in self._tldr_examples:
|
for example in self._tldr_examples:
|
||||||
usage = f"{command} {example.usage.strip()}"
|
usage = f"{prefix} {example.usage.strip()}"
|
||||||
description = example.description.strip()
|
description = example.description.strip()
|
||||||
block = f"[bold]{usage}[/bold]"
|
block = f"[bold]{usage}[/bold]"
|
||||||
self.console.print(
|
self.console.print(
|
||||||
|
|||||||
@@ -28,6 +28,5 @@ class RouteResult:
|
|||||||
command: "Command | None" = None
|
command: "Command | None" = None
|
||||||
namespace_entry: FalyxNamespace | None = None
|
namespace_entry: FalyxNamespace | None = None
|
||||||
leaf_argv: list[str] = field(default_factory=list)
|
leaf_argv: list[str] = field(default_factory=list)
|
||||||
typed_path: list[str] = field(default_factory=list)
|
|
||||||
suggestions: list[str] = field(default_factory=list)
|
suggestions: list[str] = field(default_factory=list)
|
||||||
is_preview: bool = False
|
is_preview: bool = False
|
||||||
|
|||||||
Reference in New Issue
Block a user