diff --git a/falyx/command.py b/falyx/command.py index b677123..c829a58 100644 --- a/falyx/command.py +++ b/falyx/command.py @@ -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 diff --git a/falyx/context.py b/falyx/context.py index 035145f..f33e327 100644 --- a/falyx/context.py +++ b/falyx/context.py @@ -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__": diff --git a/falyx/falyx.py b/falyx/falyx.py index 5378728..c9b3962 100644 --- a/falyx/falyx.py +++ b/falyx/falyx.py @@ -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() diff --git a/falyx/parser/command_argument_parser.py b/falyx/parser/command_argument_parser.py index 785f57c..c213f84 100644 --- a/falyx/parser/command_argument_parser.py +++ b/falyx/parser/command_argument_parser.py @@ -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( diff --git a/falyx/routing.py b/falyx/routing.py index 22ac5f4..d9757aa 100644 --- a/falyx/routing.py +++ b/falyx/routing.py @@ -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