diff --git a/falyx/__init__.py b/falyx/__init__.py index 20019b5..b60e1d2 100644 --- a/falyx/__init__.py +++ b/falyx/__init__.py @@ -1,7 +1,6 @@ -""" -Falyx CLI Framework +"""Falyx CLI Framework -Copyright (c) 2025 rtj.dev LLC. +Copyright (c) 2026 rtj.dev LLC. Licensed under the MIT License. See LICENSE file for details. """ diff --git a/falyx/__main__.py b/falyx/__main__.py index 028e714..7b05393 100644 --- a/falyx/__main__.py +++ b/falyx/__main__.py @@ -1,7 +1,6 @@ -""" -Falyx CLI Framework +"""Falyx CLI Framework -Copyright (c) 2025 rtj.dev LLC. +Copyright (c) 2026 rtj.dev LLC. Licensed under the MIT License. See LICENSE file for details. """ diff --git a/falyx/action/__init__.py b/falyx/action/__init__.py index bcce9f4..6a552ec 100644 --- a/falyx/action/__init__.py +++ b/falyx/action/__init__.py @@ -1,7 +1,6 @@ -""" -Falyx CLI Framework +"""Falyx CLI Framework -Copyright (c) 2025 rtj.dev LLC. +Copyright (c) 2026 rtj.dev LLC. Licensed under the MIT License. See LICENSE file for details. """ diff --git a/falyx/action/action.py b/falyx/action/action.py index dc67180..52edcb6 100644 --- a/falyx/action/action.py +++ b/falyx/action/action.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Defines `Action`, the core atomic unit in the Falyx CLI framework, used to wrap and +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Defines `Action`, the core atomic unit in the Falyx CLI framework, used to wrap and execute a single callable or coroutine with structured lifecycle support. An `Action` is the simplest building block in Falyx's execution model, enabling @@ -50,8 +49,7 @@ from falyx.utils import ensure_async class Action(BaseAction): - """ - Action wraps a simple function or coroutine into a standard executable unit. + """Action wraps a simple function or coroutine into a standard executable unit. It supports: - Optional retry logic. @@ -148,8 +146,8 @@ class Action(BaseAction): self.enable_retry() def get_infer_target(self) -> tuple[Callable[..., Any], None]: - """ - Returns the callable to be used for argument inference. + """Returns the callable to be used for argument inference. + By default, it returns the action itself. """ return self.action, None diff --git a/falyx/action/action_factory.py b/falyx/action/action_factory.py index e9aae09..c26d89d 100644 --- a/falyx/action/action_factory.py +++ b/falyx/action/action_factory.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Defines `ActionFactory`, a dynamic Falyx Action that defers the construction of its +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Defines `ActionFactory`, a dynamic Falyx Action that defers the construction of its underlying logic to runtime using a user-defined factory function. This pattern is useful when the specific Action to execute cannot be determined until @@ -46,8 +45,7 @@ from falyx.utils import ensure_async class ActionFactory(BaseAction): - """ - Dynamically creates and runs another Action at runtime using a factory function. + """Dynamically creates and runs another Action at runtime using a factory function. This is useful for generating context-specific behavior (e.g., dynamic HTTPActions) where the structure of the next action depends on runtime values. diff --git a/falyx/action/action_group.py b/falyx/action/action_group.py index df8eaef..318790f 100644 --- a/falyx/action/action_group.py +++ b/falyx/action/action_group.py @@ -1,7 +1,6 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Defines `ActionGroup`, a Falyx Action that executes multiple sub-actions concurrently -using asynchronous parallelism. +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Defines `ActionGroup`, a Falyx Action that executes multiple sub-actions concurrently +using asynchronous concurrency. `ActionGroup` is designed for workflows where several independent actions can run simultaneously to improve responsiveness and reduce latency. It ensures robust error @@ -9,7 +8,7 @@ isolation, shared result tracking, and full lifecycle hook integration while pre Falyx's introspectability and chaining capabilities. Key Features: -- Executes all actions in parallel via `asyncio.gather` +- Executes all actions concurrently via `asyncio.gather` - Aggregates results as a list of `(name, result)` tuples - Collects and reports multiple errors without interrupting execution - Compatible with `SharedContext`, `OptionsManager`, and `last_result` injection @@ -27,11 +26,11 @@ Raises: Example: ActionGroup( - name="ParallelChecks", + name="ConcurrentChecks", actions=[Action(...), Action(...), ChainedAction(...)], ) -This module complements `ChainedAction` by offering breadth-wise (parallel) execution +This module complements `ChainedAction` by offering breadth-wise (concurrent) execution as opposed to depth-wise (sequential) execution. """ import asyncio @@ -54,14 +53,13 @@ from falyx.themes.colors import OneColors class ActionGroup(BaseAction, ActionListMixin): - """ - ActionGroup executes multiple actions concurrently in parallel. + """ActionGroup executes multiple actions concurrently. It is ideal for independent tasks that can be safely run simultaneously, improving overall throughput and responsiveness of workflows. Core features: - - Parallel execution of all contained actions. + - Concurrent execution of all contained actions. - Shared last_result injection across all actions if configured. - Aggregated collection of individual results as (name, result) pairs. - Hook lifecycle support (before, on_success, on_error, after, on_teardown). @@ -75,7 +73,7 @@ class ActionGroup(BaseAction, ActionListMixin): Best used for: - Batch processing multiple independent tasks. - - Reducing latency for workflows with parallelizable steps. + - Reducing latency for workflows with concurrent steps. - Isolating errors while maximizing successful execution. Args: @@ -173,7 +171,7 @@ class ActionGroup(BaseAction, ActionListMixin): combined_args = args + self.args combined_kwargs = {**self.kwargs, **kwargs} - shared_context = SharedContext(name=self.name, action=self, is_parallel=True) + shared_context = SharedContext(name=self.name, action=self, is_concurrent=True) if self.shared_context: shared_context.set_shared_result(self.shared_context.last_result()) updated_kwargs = self._maybe_inject_last_result(combined_kwargs) @@ -229,7 +227,7 @@ class ActionGroup(BaseAction, ActionListMixin): action.register_hooks_recursively(hook_type, hook) async def preview(self, parent: Tree | None = None): - label = [f"[{OneColors.MAGENTA_b}]⏩ ActionGroup (parallel)[/] '{self.name}'"] + label = [f"[{OneColors.MAGENTA_b}]⏩ ActionGroup (concurrent)[/] '{self.name}'"] if self.inject_last_result: label.append(f" [dim](receives '{self.inject_into}')[/dim]") tree = parent.add("".join(label)) if parent else Tree("".join(label)) diff --git a/falyx/action/action_mixins.py b/falyx/action/action_mixins.py index a0e237d..6784265 100644 --- a/falyx/action/action_mixins.py +++ b/falyx/action/action_mixins.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Provides reusable mixins for managing collections of `BaseAction` instances +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Provides reusable mixins for managing collections of `BaseAction` instances within composite Falyx actions such as `ActionGroup` or `ChainedAction`. The primary export, `ActionListMixin`, encapsulates common functionality for diff --git a/falyx/action/action_types.py b/falyx/action/action_types.py index 69e5052..83b6ba6 100644 --- a/falyx/action/action_types.py +++ b/falyx/action/action_types.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Defines strongly-typed enums used throughout the Falyx CLI framework for +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Defines strongly-typed enums used throughout the Falyx CLI framework for representing common structured values like file formats, selection return types, and confirmation modes. @@ -28,8 +27,7 @@ from enum import Enum class FileType(Enum): - """ - Represents supported file types for reading and writing in Falyx Actions. + """Represents supported file types for reading and writing in Falyx Actions. Used by `LoadFileAction` and `SaveFileAction` to determine how to parse or serialize file content. Includes alias resolution for common extensions like @@ -91,8 +89,7 @@ class FileType(Enum): class SelectionReturnType(Enum): - """ - Controls what is returned from a `SelectionAction` when using a selection map. + """Controls what is returned from a `SelectionAction` when using a selection map. Determines how the user's choice(s) from a `dict[str, SelectionOption]` are transformed and returned by the action. @@ -145,8 +142,7 @@ class SelectionReturnType(Enum): class ConfirmType(Enum): - """ - Enum for defining prompt styles in confirmation dialogs. + """Enum for defining prompt styles in confirmation dialogs. Used by confirmation actions to control user input behavior and available choices. diff --git a/falyx/action/base_action.py b/falyx/action/base_action.py index 83ace00..af8bd86 100644 --- a/falyx/action/base_action.py +++ b/falyx/action/base_action.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Core action system for Falyx. +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Core action system for Falyx. This module defines the building blocks for executable actions and workflows, providing a structured way to compose, execute, recover, and manage sequences of @@ -14,13 +13,13 @@ Core guarantees: - Consistent timing and execution context tracking for each run. - Unified, predictable result handling and error propagation. - Optional last_result injection to enable flexible, data-driven workflows. -- Built-in support for retries, rollbacks, parallel groups, chaining, and fallback +- Built-in support for retries, rollbacks, concurrent groups, chaining, and fallback recovery. Key components: - Action: wraps a function or coroutine into a standard executable unit. - ChainedAction: runs actions sequentially, optionally injecting last results. -- ActionGroup: runs actions in parallel and gathers results. +- ActionGroup: runs actions concurrently and gathers results. - ProcessAction: executes CPU-bound functions in a separate process. - LiteralInputAction: injects static values into workflows. - FallbackAction: gracefully recovers from failures or missing data. @@ -46,8 +45,7 @@ from falyx.themes import OneColors class BaseAction(ABC): - """ - Base class for actions. Actions can be simple functions or more + """Base class for actions. Actions can be simple functions or more complex actions like `ChainedAction` or `ActionGroup`. They can also be run independently or as part of Falyx. @@ -115,8 +113,8 @@ class BaseAction(ABC): @abstractmethod def get_infer_target(self) -> tuple[Callable[..., Any] | None, dict[str, Any] | None]: - """ - Returns the callable to be used for argument inference. + """Returns the callable to be used for argument inference. + By default, it returns None. """ raise NotImplementedError("get_infer_target must be implemented by subclasses") @@ -128,9 +126,7 @@ class BaseAction(ABC): self.shared_context = shared_context def get_option(self, option_name: str, default: Any = None) -> Any: - """ - Resolve an option from the OptionsManager if present, otherwise use the fallback. - """ + """Resolve an option from the OptionsManager if present, else default.""" if self.options_manager: return self.options_manager.get(option_name, default) return default @@ -158,8 +154,8 @@ class BaseAction(ABC): def prepare( self, shared_context: SharedContext, options_manager: OptionsManager | None = None ) -> BaseAction: - """ - Prepare the action specifically for sequential (ChainedAction) execution. + """Prepare the action specifically for sequential (ChainedAction) execution. + Can be overridden for chain-specific logic. """ self.set_shared_context(shared_context) diff --git a/falyx/action/chained_action.py b/falyx/action/chained_action.py index 1538f97..8c31039 100644 --- a/falyx/action/chained_action.py +++ b/falyx/action/chained_action.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Defines `ChainedAction`, a core Falyx construct for executing a sequence of actions +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Defines `ChainedAction`, a core Falyx construct for executing a sequence of actions in strict order, optionally injecting results from previous steps into subsequent ones. `ChainedAction` is designed for linear workflows where each step may depend on @@ -86,8 +85,7 @@ from falyx.themes import OneColors class ChainedAction(BaseAction, ActionListMixin): - """ - ChainedAction executes a sequence of actions one after another. + """ChainedAction executes a sequence of actions one after another. Features: - Supports optional automatic last_result injection (auto_inject). @@ -276,8 +274,7 @@ class ChainedAction(BaseAction, ActionListMixin): async def _rollback( self, rollback_stack: list[tuple[Action, tuple[Any, ...], dict[str, Any]]] ): - """ - Roll back all executed actions in reverse order. + """Roll back all executed actions in reverse order. Rollbacks run even if a fallback recovered from failure, ensuring consistent undo of all side effects. diff --git a/falyx/action/confirm_action.py b/falyx/action/confirm_action.py index 228b5c2..cae49e3 100644 --- a/falyx/action/confirm_action.py +++ b/falyx/action/confirm_action.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Defines `ConfirmAction`, a Falyx Action that prompts the user for confirmation +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Defines `ConfirmAction`, a Falyx Action that prompts the user for confirmation before continuing execution. `ConfirmAction` supports a wide range of confirmation strategies, including: @@ -62,8 +61,7 @@ from falyx.validators import word_validator, words_validator class ConfirmAction(BaseAction): - """ - Action to confirm an operation with the user. + """Action to confirm an operation with the user. There are several ways to confirm an action, such as using a simple yes/no prompt. You can also use a confirmation type that requires the user @@ -97,8 +95,7 @@ class ConfirmAction(BaseAction): inject_last_result: bool = True, inject_into: str = "last_result", ): - """ - Initialize the ConfirmAction. + """Initialize the ConfirmAction. Args: message (str): The confirmation message to display. diff --git a/falyx/action/fallback_action.py b/falyx/action/fallback_action.py index d74b291..9daa8bc 100644 --- a/falyx/action/fallback_action.py +++ b/falyx/action/fallback_action.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Defines `FallbackAction`, a lightweight recovery Action used within `ChainedAction` +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Defines `FallbackAction`, a lightweight recovery Action used within `ChainedAction` pipelines to gracefully handle errors or missing results from a preceding step. When placed immediately after a failing or null-returning Action, `FallbackAction` @@ -46,8 +45,7 @@ from falyx.themes import OneColors class FallbackAction(Action): - """ - FallbackAction provides a default value if the previous action failed or + """FallbackAction provides a default value if the previous action failed or returned None. It injects the last result and checks: diff --git a/falyx/action/http_action.py b/falyx/action/http_action.py index adb5e14..7099460 100644 --- a/falyx/action/http_action.py +++ b/falyx/action/http_action.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Defines an Action subclass for making HTTP requests using aiohttp within Falyx workflows. +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Defines `HTTPAction` for making HTTP requests using aiohttp. Features: - Automatic reuse of aiohttp.ClientSession via SharedContext @@ -32,8 +31,7 @@ async def close_shared_http_session(context: ExecutionContext) -> None: class HTTPAction(Action): - """ - An Action for executing HTTP requests using aiohttp with shared session reuse. + """An Action for executing HTTP requests using aiohttp with shared session reuse. This action integrates seamlessly into Falyx pipelines, with automatic session management, result injection, and lifecycle hook support. It is ideal for CLI-driven diff --git a/falyx/action/io_action.py b/falyx/action/io_action.py index d208715..b6b393c 100644 --- a/falyx/action/io_action.py +++ b/falyx/action/io_action.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -BaseIOAction: A base class for stream- or buffer-based IO-driven Actions. +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""BaseIOAction: A base class for stream- or buffer-based IO-driven Actions. This module defines `BaseIOAction`, a specialized variant of `BaseAction` that interacts with standard input and output, enabling command-line pipelines, @@ -29,8 +28,7 @@ from falyx.themes import OneColors class BaseIOAction(BaseAction): - """ - Base class for IO-driven Actions that operate on stdin/stdout input streams. + """Base class for IO-driven Actions that operate on stdin/stdout input streams. Designed for use in shell pipelines or programmatic workflows that pass data through chained commands. It handles reading input, transforming it, and diff --git a/falyx/action/literal_input_action.py b/falyx/action/literal_input_action.py index 552033f..2e4c3a9 100644 --- a/falyx/action/literal_input_action.py +++ b/falyx/action/literal_input_action.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Defines `LiteralInputAction`, a lightweight Falyx Action that injects a static, +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Defines `LiteralInputAction`, a lightweight Falyx Action that injects a static, predefined value into a `ChainedAction` workflow. This Action is useful for embedding literal values (e.g., strings, numbers, @@ -43,8 +42,7 @@ from falyx.themes import OneColors class LiteralInputAction(Action): - """ - LiteralInputAction injects a static value into a ChainedAction. + """LiteralInputAction injects a static value into a ChainedAction. This allows embedding hardcoded values mid-pipeline, useful when: - Providing default or fallback inputs. diff --git a/falyx/action/load_file_action.py b/falyx/action/load_file_action.py index a4c00fb..0265bb9 100644 --- a/falyx/action/load_file_action.py +++ b/falyx/action/load_file_action.py @@ -1,7 +1,6 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Defines `LoadFileAction`, a Falyx Action for reading and parsing the contents of a file -at runtime in a structured, introspectable, and lifecycle-aware manner. +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Defines `LoadFileAction`, a Falyx Action for reading and parsing the contents of a +file at runtime in a structured, introspectable, and lifecycle-aware manner. This action supports multiple common file types—including plain text, structured data formats (JSON, YAML, TOML), tabular formats (CSV, TSV), XML, and raw Path objects— @@ -57,8 +56,7 @@ from falyx.themes import OneColors class LoadFileAction(BaseAction): - """ - LoadFileAction loads and parses the contents of a file at runtime. + """LoadFileAction loads and parses the contents of a file at runtime. This action supports multiple common file formats—including plain text, JSON, YAML, TOML, XML, CSV, and TSV—and returns a parsed representation of the file. diff --git a/falyx/action/menu_action.py b/falyx/action/menu_action.py index 5174966..91184bb 100644 --- a/falyx/action/menu_action.py +++ b/falyx/action/menu_action.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Defines `MenuAction`, a one-shot, interactive menu-style Falyx Action that presents +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Defines `MenuAction`, a one-shot, interactive menu-style Falyx Action that presents a set of labeled options to the user and executes the corresponding action based on their selection. @@ -57,8 +56,7 @@ from falyx.utils import chunks class MenuAction(BaseAction): - """ - MenuAction displays a one-time interactive menu of predefined options, + """MenuAction displays a one-time interactive menu of predefined options, each mapped to a corresponding Action. Unlike the main Falyx menu system, `MenuAction` is intended for scoped, diff --git a/falyx/action/process_action.py b/falyx/action/process_action.py index 7af4405..14a1fa4 100644 --- a/falyx/action/process_action.py +++ b/falyx/action/process_action.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Defines `ProcessAction`, a Falyx Action that executes a blocking or CPU-bound function +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Defines `ProcessAction`, a Falyx Action that executes a blocking or CPU-bound function in a separate process using `concurrent.futures.ProcessPoolExecutor`. This is useful for offloading expensive computations or subprocess-compatible operations @@ -54,8 +53,7 @@ from falyx.themes import OneColors class ProcessAction(BaseAction): - """ - ProcessAction runs a function in a separate process using ProcessPoolExecutor. + """ProcessAction runs a function in a separate process using ProcessPoolExecutor. Features: - Executes CPU-bound or blocking tasks without blocking the main event loop. diff --git a/falyx/action/process_pool_action.py b/falyx/action/process_pool_action.py index 2ea3bdf..15437eb 100644 --- a/falyx/action/process_pool_action.py +++ b/falyx/action/process_pool_action.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Defines `ProcessPoolAction`, a parallelized action executor that distributes +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Defines `ProcessPoolAction`, a parallelized action executor that distributes tasks across multiple processes using Python's `concurrent.futures.ProcessPoolExecutor`. This module enables structured execution of CPU-bound tasks in parallel while @@ -37,8 +36,7 @@ from falyx.themes import OneColors @dataclass class ProcessTask: - """ - Represents a callable task with its arguments for parallel execution. + """Represents a callable task with its arguments for parallel execution. This lightweight container is used to queue individual tasks for execution inside a `ProcessPoolAction`. @@ -62,8 +60,7 @@ class ProcessTask: class ProcessPoolAction(BaseAction): - """ - Executes a set of independent tasks in parallel using a process pool. + """Executes a set of independent tasks in parallel using a process pool. `ProcessPoolAction` is ideal for CPU-bound tasks that benefit from concurrent execution in separate processes. Each task is wrapped in a @@ -147,7 +144,7 @@ class ProcessPoolAction(BaseAction): async def _run(self, *args, **kwargs) -> Any: if not self.actions: raise EmptyPoolError(f"[{self.name}] No actions to execute.") - shared_context = SharedContext(name=self.name, action=self, is_parallel=True) + shared_context = SharedContext(name=self.name, action=self, is_concurrent=True) if self.shared_context: shared_context.set_shared_result(self.shared_context.last_result()) if self.inject_last_result and self.shared_context: diff --git a/falyx/action/prompt_menu_action.py b/falyx/action/prompt_menu_action.py index 0d0b211..b423ce1 100644 --- a/falyx/action/prompt_menu_action.py +++ b/falyx/action/prompt_menu_action.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Defines `PromptMenuAction`, a Falyx Action that prompts the user to choose from +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Defines `PromptMenuAction`, a Falyx Action that prompts the user to choose from a list of labeled options using a single-line prompt input. Each option corresponds to a `MenuOption` that wraps a description and an executable action. @@ -29,8 +28,7 @@ from falyx.themes import OneColors class PromptMenuAction(BaseAction): - """ - Displays a single-line interactive prompt for selecting an option from a menu. + """Displays a single-line interactive prompt for selecting an option from a menu. `PromptMenuAction` is a lightweight alternative to `MenuAction`, offering a more compact selection interface. Instead of rendering a full table, it displays diff --git a/falyx/action/save_file_action.py b/falyx/action/save_file_action.py index 0132dc2..99493c3 100644 --- a/falyx/action/save_file_action.py +++ b/falyx/action/save_file_action.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Defines `SaveFileAction`, a Falyx Action for writing structured or unstructured data +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Defines `SaveFileAction`, a Falyx Action for writing structured or unstructured data to a file in a variety of supported formats. Supports overwrite control, automatic directory creation, and full lifecycle hook @@ -41,8 +40,7 @@ from falyx.themes import OneColors class SaveFileAction(BaseAction): - """ - Saves data to a file in the specified format. + """Saves data to a file in the specified format. `SaveFileAction` serializes and writes input data to disk using the format defined by `file_type`. It supports plain text and structured formats like @@ -101,8 +99,7 @@ class SaveFileAction(BaseAction): inject_last_result: bool = False, inject_into: str = "data", ): - """ - SaveFileAction allows saving data to a file. + """SaveFileAction allows saving data to a file. Args: name (str): Name of the action. diff --git a/falyx/action/select_file_action.py b/falyx/action/select_file_action.py index f59e05d..32730e1 100644 --- a/falyx/action/select_file_action.py +++ b/falyx/action/select_file_action.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Defines `SelectFileAction`, a Falyx Action that allows users to select one or more +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Defines `SelectFileAction`, a Falyx Action that allows users to select one or more files from a target directory and optionally return either their content or path, parsed based on a selected `FileType`. @@ -72,8 +71,7 @@ from falyx.themes import OneColors class SelectFileAction(BaseAction): - """ - SelectFileAction allows users to select a file(s) from a directory and return: + """SelectFileAction allows users to select a file(s) from a directory and return: - file content (as text, JSON, CSV, etc.) - or the file path itself. diff --git a/falyx/action/selection_action.py b/falyx/action/selection_action.py index 1047267..eb29856 100644 --- a/falyx/action/selection_action.py +++ b/falyx/action/selection_action.py @@ -1,7 +1,6 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Defines `SelectionAction`, a highly flexible Falyx Action for interactive or headless -selection from a list or dictionary of user-defined options. +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Defines `SelectionAction`, a highly flexible Falyx Action for interactive or +headless selection from a list or dictionary of user-defined options. This module powers workflows that require prompting the user for input, selecting configuration presets, branching execution paths, or collecting multiple values @@ -56,9 +55,8 @@ from falyx.themes import OneColors class SelectionAction(BaseAction): - """ - A Falyx Action for interactively or programmatically selecting one or more items - from a list or dictionary of options. + """A Falyx Action for interactively or programmatically selecting one or more + items from a list or dictionary of options. `SelectionAction` supports both `list[str]` and `dict[str, SelectionOption]` inputs. It renders a prompt (unless `never_prompt=True`), validates user input diff --git a/falyx/action/shell_action.py b/falyx/action/shell_action.py index 6b0f899..35797a8 100644 --- a/falyx/action/shell_action.py +++ b/falyx/action/shell_action.py @@ -1,4 +1,4 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed """Execute shell commands with input substitution.""" from __future__ import annotations @@ -16,8 +16,7 @@ from falyx.themes import OneColors class ShellAction(BaseIOAction): - """ - ShellAction wraps a shell command template for CLI pipelines. + """ShellAction wraps a shell command template for CLI pipelines. This Action takes parsed input (from stdin, literal, or last_result), substitutes it into the provided shell command template, and executes diff --git a/falyx/action/signal_action.py b/falyx/action/signal_action.py index f9625ba..9c27f96 100644 --- a/falyx/action/signal_action.py +++ b/falyx/action/signal_action.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Defines `SignalAction`, a lightweight Falyx Action that raises a `FlowSignal` +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Defines `SignalAction`, a lightweight Falyx Action that raises a `FlowSignal` (such as `BackSignal`, `QuitSignal`, or `BreakChainSignal`) during execution to alter or exit the CLI flow. @@ -33,8 +32,7 @@ from falyx.themes import OneColors class SignalAction(Action): - """ - A hook-compatible action that raises a control flow signal when invoked. + """A hook-compatible action that raises a control flow signal when invoked. `SignalAction` raises a `FlowSignal` (e.g., `BackSignal`, `QuitSignal`, `BreakChainSignal`) during execution. It is commonly used to exit menus, @@ -59,8 +57,7 @@ class SignalAction(Action): super().__init__(name, action=self.raise_signal, hooks=hooks) async def raise_signal(self, *args, **kwargs): - """ - Raises the configured `FlowSignal`. + """Raises the configured `FlowSignal`. This method is called internally by the Falyx runtime and is the core behavior of the action. All hooks surrounding execution are still triggered. @@ -74,8 +71,7 @@ class SignalAction(Action): @signal.setter def signal(self, value: FlowSignal): - """ - Validates that the provided value is a `FlowSignal`. + """Validates that the provided value is a `FlowSignal`. Raises: TypeError: If `value` is not an instance of `FlowSignal`. diff --git a/falyx/action/user_input_action.py b/falyx/action/user_input_action.py index e784495..da5d4f4 100644 --- a/falyx/action/user_input_action.py +++ b/falyx/action/user_input_action.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Defines `UserInputAction`, a Falyx Action that prompts the user for input using +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Defines `UserInputAction`, a Falyx Action that prompts the user for input using Prompt Toolkit and returns the result as a string. This action is ideal for interactive CLI workflows that require user input mid-pipeline. @@ -40,8 +39,7 @@ from falyx.themes.colors import OneColors class UserInputAction(BaseAction): - """ - Prompts the user for textual input and returns their response. + """Prompts the user for textual input and returns their response. `UserInputAction` uses Prompt Toolkit to gather input with optional validation, lifecycle hook compatibility, and support for default text. If `inject_last_result` diff --git a/falyx/bottom_bar.py b/falyx/bottom_bar.py index 46fc66f..cb66384 100644 --- a/falyx/bottom_bar.py +++ b/falyx/bottom_bar.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Provides the `BottomBar` class for managing a customizable bottom status bar in +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Provides the `BottomBar` class for managing a customizable bottom status bar in Falyx-based CLI applications. The bottom bar is rendered using `prompt_toolkit` and supports: diff --git a/falyx/command.py b/falyx/command.py index c829a58..a432732 100644 --- a/falyx/command.py +++ b/falyx/command.py @@ -1,4 +1,4 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed """Command abstraction for the Falyx CLI framework. This module defines the `Command` class, which represents a single executable @@ -302,14 +302,14 @@ class Command(BaseModel): @field_validator("action", mode="before") @classmethod - def wrap_callable_as_async(cls, action: Any) -> Any: + def _wrap_callable_as_async(cls, action: Any) -> Any: if isinstance(action, BaseAction): return action elif callable(action): return ensure_async(action) raise TypeError("Action must be a callable or an instance of BaseAction") - def get_argument_definitions(self) -> list[dict[str, Any]]: + def _get_argument_definitions(self) -> list[dict[str, Any]]: if self.arguments: return self.arguments elif callable(self.argument_config) and isinstance( @@ -361,7 +361,7 @@ class Command(BaseModel): program=self.program, options_manager=self.options_manager, ) - for arg_def in self.get_argument_definitions(): + for arg_def in self._get_argument_definitions(): self.arg_parser.add_argument(*arg_def.pop("flags"), **arg_def) if isinstance(self.arg_parser, CommandArgumentParser) and self.execution_options: @@ -429,7 +429,7 @@ class Command(BaseModel): if should_prompt_user(confirm=self.confirm, options=self.options_manager): if self.preview_before_confirm: await self.preview() - if not await confirm_async(self.confirmation_prompt): + if not await confirm_async(self._confirmation_prompt): logger.info("[Command:%s] Cancelled by user.", self.key) raise CancelSignal(f"[Command:{self.key}] Cancelled by confirmation.") @@ -458,7 +458,7 @@ class Command(BaseModel): return self._context.result if self._context else None @property - def confirmation_prompt(self) -> FormattedText: + def _confirmation_prompt(self) -> FormattedText: """Generate a styled prompt_toolkit FormattedText confirmation message.""" if self.confirm_message and self.confirm_message != "Are you sure?": return FormattedText([("class:confirm", self.confirm_message)]) diff --git a/falyx/command_executor.py b/falyx/command_executor.py index 145acb0..5077bea 100644 --- a/falyx/command_executor.py +++ b/falyx/command_executor.py @@ -1,4 +1,4 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed """Shared command execution engine for the Falyx CLI framework. This module defines `CommandExecutor`, a low-level execution service responsible diff --git a/falyx/command_runner.py b/falyx/command_runner.py index 34b440c..25cc42e 100644 --- a/falyx/command_runner.py +++ b/falyx/command_runner.py @@ -1,4 +1,4 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed """Standalone command runner for the Falyx CLI framework. This module defines `CommandRunner`, a developer-facing convenience wrapper for diff --git a/falyx/completer.py b/falyx/completer.py index 9b74f37..07868b0 100644 --- a/falyx/completer.py +++ b/falyx/completer.py @@ -1,4 +1,4 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed """Prompt Toolkit completion support for routed Falyx command input. This module defines `FalyxCompleter`, the interactive completion layer used by @@ -12,7 +12,7 @@ Completion behavior is split into two phases: 1. Namespace completion While the user is still selecting a command or namespace entry, completion candidates are derived from the active namespace via - `iter_completion_names`. Namespace-level help flags such as `-h`, `--help`, + `completion_names`. Namespace-level help flags such as `-h`, `--help`, `-T`, and `--tldr` are also suggested when appropriate. 2. Leaf-command completion @@ -131,7 +131,7 @@ class FalyxCompleter(Completer): committed_tokens, stub=stub, cursor_at_end_of_token=cursor_at_end, - context=context, + invocation_context=context, is_preview=is_preview, ) @@ -190,7 +190,7 @@ class FalyxCompleter(Completer): list[str]: Matching namespace entry keys and aliases. """ results: list[str] = [] - for name in namespace.iter_completion_names: + for name in namespace.completion_names: if name.upper().startswith(prefix.upper()): results.append(name.lower() if prefix.islower() else name) return results diff --git a/falyx/completer_types.py b/falyx/completer_types.py index 6b99c4f..cbe13a3 100644 --- a/falyx/completer_types.py +++ b/falyx/completer_types.py @@ -1,3 +1,4 @@ +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed """Completion route models for routed Falyx autocompletion. This module defines `CompletionRoute`, a lightweight value object used by the diff --git a/falyx/config.py b/falyx/config.py index 9dbc0d9..fd36eba 100644 --- a/falyx/config.py +++ b/falyx/config.py @@ -1,4 +1,4 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed """Configuration loader and schema definitions for the Falyx CLI framework. This module supports config-driven initialization of CLI commands and submenus diff --git a/falyx/console.py b/falyx/console.py index ab3f8c4..429f476 100644 --- a/falyx/console.py +++ b/falyx/console.py @@ -1,4 +1,4 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed """Global console instance for Falyx CLI applications.""" from rich.console import Console diff --git a/falyx/context.py b/falyx/context.py index f33e327..2d92783 100644 --- a/falyx/context.py +++ b/falyx/context.py @@ -1,4 +1,4 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed """Context models for Falyx execution and invocation state. This module defines the core context objects used throughout Falyx to track both @@ -229,9 +229,9 @@ class SharedContext(BaseModel): results (list[Any]): Captures results from each action, in order of execution. errors (list[tuple[int, BaseException]]): Indexed list of errors from failed actions. current_index (int): Index of the currently executing action (used in chains). - is_parallel (bool): Whether the context is used in parallel mode (ActionGroup). + is_concurrent (bool): Whether the context is used in concurrent mode (ActionGroup). shared_result (Any | None): Optional shared value available to all actions in - parallel mode. + concurrent mode. share (dict[str, Any]): Custom shared key-value store for user-defined communication between actions (e.g., flags, intermediate data, settings). @@ -254,7 +254,7 @@ class SharedContext(BaseModel): results: list[Any] = Field(default_factory=list) errors: list[tuple[int, BaseException]] = Field(default_factory=list) current_index: int = -1 - is_parallel: bool = False + is_concurrent: bool = False shared_result: Any | None = None share: dict[str, Any] = Field(default_factory=dict) @@ -269,11 +269,11 @@ class SharedContext(BaseModel): def set_shared_result(self, result: Any) -> None: self.shared_result = result - if self.is_parallel: + if self.is_concurrent: self.results.append(result) def last_result(self) -> Any: - if self.is_parallel: + if self.is_concurrent: return self.shared_result return self.results[-1] if self.results else None @@ -284,9 +284,9 @@ class SharedContext(BaseModel): self.share[key] = value def __str__(self) -> str: - parallel_label = "Parallel" if self.is_parallel else "Sequential" + concurrent_label = "Concurrent" if self.is_concurrent else "Sequential" return ( - f"<{parallel_label}SharedContext '{self.name}' | " + f"<{concurrent_label}SharedContext '{self.name}' | " f"Results: {self.results} | " f"Errors: {self.errors}>" ) diff --git a/falyx/debug.py b/falyx/debug.py index fbd3096..448176c 100644 --- a/falyx/debug.py +++ b/falyx/debug.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Provides debug logging hooks for Falyx action execution. +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Provides debug logging hooks for Falyx action execution. This module defines lifecycle hook functions (`log_before`, `log_success`, `log_after`, `log_error`) that can be registered with a `HookManager` to trace command execution. diff --git a/falyx/exceptions.py b/falyx/exceptions.py index 52f60b9..9652d63 100644 --- a/falyx/exceptions.py +++ b/falyx/exceptions.py @@ -1,4 +1,4 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed """Defines all custom exception classes used in the Falyx CLI framework. These exceptions provide structured error handling for common failure cases, diff --git a/falyx/execution_option.py b/falyx/execution_option.py index c11832c..42ae23b 100644 --- a/falyx/execution_option.py +++ b/falyx/execution_option.py @@ -1,10 +1,50 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Execution option enums for the Falyx command runtime. + +This module defines `ExecutionOption`, the enum used to represent optional +execution-scoped behaviors that a command may choose to expose through its +argument parser. + +Execution options are distinct from normal command inputs. They control runtime +behavior around command execution rather than the business-logic arguments +passed to the underlying action. Typical examples include summary output, +retry configuration, and confirmation handling. + +`ExecutionOption` is used by Falyx components such as `Command` and +`CommandArgumentParser` to declaratively enable execution-level flags and to +normalize user- or config-provided option names into a validated enum value. + +The enum also implements custom missing-value handling so string inputs can be +resolved case-insensitively with helpful error messages. +""" from __future__ import annotations from enum import Enum class ExecutionOption(Enum): + """Enumerates optional execution-scoped behaviors supported by Falyx. + + `ExecutionOption` identifies runtime features that can be enabled on a + command independently of its normal argument schema. When present, these + options typically cause `CommandArgumentParser` to expose additional flags + that affect how the command is executed rather than what the command does. + + Supported options: + SUMMARY: Enable summary-related execution flags and reporting behavior. + RETRY: Enable retry-related execution flags such as retry count, delay, + and backoff. + CONFIRM: Enable confirmation-related execution flags such as forcing or + skipping confirmation prompts. + + Notes: + - These values are intended for execution control, not domain-specific + command input. + - String values are normalized case-insensitively through `_missing_()` + so config and user input can be converted into enum members with + friendlier validation behavior. + """ + SUMMARY = "summary" RETRY = "retry" CONFIRM = "confirm" diff --git a/falyx/execution_registry.py b/falyx/execution_registry.py index 4f118f1..fe2f0de 100644 --- a/falyx/execution_registry.py +++ b/falyx/execution_registry.py @@ -1,7 +1,6 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Provides the `ExecutionRegistry`, a centralized runtime store for capturing and inspecting -the execution history of Falyx actions. +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Provides the `ExecutionRegistry`, a centralized runtime store for capturing and +inspecting the execution history of Falyx actions. The registry automatically records every `ExecutionContext` created during action execution—including context metadata, results, exceptions, duration, and tracebacks. @@ -63,8 +62,7 @@ from falyx.themes import OneColors class ExecutionRegistry: - """ - Global registry for recording and inspecting Falyx action executions. + """Global registry for recording and inspecting Falyx action executions. This class captures every `ExecutionContext` created by Falyx Actions, tracking metadata, results, exceptions, and performance metrics. It enables @@ -96,8 +94,7 @@ class ExecutionRegistry: @classmethod def record(cls, context: ExecutionContext): - """ - Record an execution context and assign a unique index. + """Record an execution context and assign a unique index. This method logs the context, appends it to the registry, and makes it available for future summary or filtering. @@ -115,8 +112,7 @@ class ExecutionRegistry: @classmethod def get_all(cls) -> list[ExecutionContext]: - """ - Return all recorded execution contexts in order of execution. + """Return all recorded execution contexts in order of execution. Returns: list[ExecutionContext]: All stored action contexts. @@ -125,8 +121,7 @@ class ExecutionRegistry: @classmethod def get_by_name(cls, name: str) -> list[ExecutionContext]: - """ - Retrieve all executions recorded under a given action name. + """Return all executions recorded under a given action name. Args: name (str): The name of the action. @@ -138,8 +133,7 @@ class ExecutionRegistry: @classmethod def get_latest(cls) -> ExecutionContext: - """ - Return the most recent execution context. + """Return the most recent execution context. Returns: ExecutionContext: The last recorded context. @@ -148,8 +142,7 @@ class ExecutionRegistry: @classmethod def clear(cls): - """ - Clear all stored execution data and reset internal indices. + """Clear all stored execution data and reset internal indices. This operation is destructive and cannot be undone. """ @@ -167,8 +160,7 @@ class ExecutionRegistry: last_result: bool = False, status: Literal["all", "success", "error"] = "all", ): - """ - Display a formatted Rich table of recorded executions. + """Display a formatted Rich table of recorded executions. Supports filtering by action name, index, or execution status. Can optionally show only the last result or a specific indexed result. diff --git a/falyx/falyx.py b/falyx/falyx.py index ad71a44..50d0ff7 100644 --- a/falyx/falyx.py +++ b/falyx/falyx.py @@ -1,30 +1,62 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -"""Falyx CLI framework core module. +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Core application runtime for the Falyx CLI framework. -This module defines the `Falyx` class, the primary orchestration layer for -building and running structured CLI applications. It integrates command -registration, interactive menu handling, CLI parsing, execution lifecycle -management, and option scoping into a unified runtime. +This module defines `Falyx`, the top-level orchestration layer used to build, +route, render, and execute Falyx applications. `Falyx` sits above individual +`Command` objects and their local argument parsers. -Core responsibilities: -- Manage command registration, alias resolution, and dispatch -- Coordinate interactive menu and non-interactive CLI execution modes -- Integrate with `FalyxParser` for root-level argument parsing and routing -- Apply execution-scoped overrides via `OptionsManager` -- Drive lifecycle hooks (`before`, `on_success`, `on_error`, `after`, `on_teardown`) -- Provide Rich-based rendering and Prompt Toolkit interaction -- Maintain execution history via `ExecutionRegistry` +Core Responsibilities: +- Registration of commands, builtins, and nested namespaces +- Root/session option parsing +- Recursive namespace-aware routing +- Interactive menu prompting and validation +- Routed autocompletion +- Namespace and command help/TLDR rendering +- Execution dispatch through `CommandExecutor` +- Shared option state and execution history -Execution Flow: -1. CLI arguments are parsed via `FalyxParser` -2. A `ParseResult` determines the execution mode (menu, command, help, etc.) -3. Execution options are applied through scoped namespaces -4. Commands are resolved and executed via `Command` and `Action` abstractions -5. Lifecycle hooks and context tracking are applied throughout execution +Architecture: + Falyx is the routing boundary of the framework. -This module serves as the entrypoint for most Falyx-based applications and -coordinates all major subsystems including parsing, execution, rendering, -and state management. + - `FalyxParser` parses only root-level/session flags and leaves the remaining + tokens untouched for routing. + - `Falyx.resolve_route()` walks the invocation path recursively across + nested `FalyxNamespace` entries until it reaches either a namespace help + target, a namespace menu target, an unknown entry, or a leaf `Command`. + - Once a leaf command is found, command-local parsing is delegated to that + command's `CommandArgumentParser` via `Command.resolve_args()`. + - Prepared inputs are then executed through `CommandExecutor`, which applies + shared outer execution behavior consistently across CLI and menu flows. + +Execution Model: + 1. Root CLI/session flags are parsed. + 2. The remaining tokens are routed across namespaces and commands. + 3. If a leaf command is reached, its remaining argv is parsed locally. + 4. The resolved route is rendered, previewed, or executed. + 5. Shared hooks, option overrides, and execution tracking are applied. + +Interactive Features: + In menu mode, `Falyx` integrates Rich and Prompt Toolkit to provide a + structured interactive runtime with: + + - persistent prompt history + - routed validation + - namespace-aware autocompletion + - bottom-bar rendering and key bindings + - preview flows and contextual help + - history and built-in utility commands + +Design Notes: + - `Falyx` owns routing; commands own leaf argument parsing; the executor owns + outer execution behavior. + - CLI mode and menu mode share the same routed execution semantics. + - Help, usage, and TLDR rendering are invocation-context aware so nested + namespaces display correctly scoped command paths. + - Builtins such as help, preview, version, history, and exit are registered + as first-class entries within the application runtime. + +This module is the primary entrypoint for assembling and running a Falyx +application. """ from __future__ import annotations @@ -96,75 +128,98 @@ from falyx.version import __version__ class Falyx: """Primary controller for Falyx CLI applications. - `Falyx` coordinates command registration, input parsing, execution dispatch, - and lifecycle management across both interactive (menu) and non-interactive - (CLI) modes. + `Falyx` manages the full runtime of a Falyx application, including command + registration, nested namespace traversal, interactive menu behavior, routed + help output, and execution dispatch. It acts as the central integration point between: - Command definitions (`Command`) + - Nested namespaces (`FalyxNamespace`) + - Root parser (`FalyxParser`) + - Leaf argument parsers (`CommandArgumentParser`) + - Execution dispatch (`CommandExecutor`) - Execution units (`Action`, `ChainedAction`, `ActionGroup`) - - CLI parsing (`FalyxParser`, `CommandArgumentParser`) - - Runtime configuration (`OptionsManager`) + - Shared runtime configuration (`OptionsManager`) - Lifecycle hooks (`HookManager`) - UI layers (Rich + Prompt Toolkit) Key Responsibilities: - - Maintain a registry of commands, aliases, and builtins - - Resolve user input to commands via exact match, prefix match, or fuzzy match - - Dispatch execution with full lifecycle hook support + - Maintain a registry of commands, aliases, builtins, and namespaces + - Parse root-level/session flags and delegate the rest to routing + - Resolve user input to a routed `RouteResult` + - Provide namespace-aware completion and validation + - Execute commands with full lifecycle hook support + - Provide prepared command executions through the shared executor + - Render help, usage, and TLDR output with invocation context - Apply execution-scoped option overrides (e.g. confirm, retries) - - Support both CLI-driven execution and interactive menu loops - - Provide structured help, preview, and history functionality + - Manage prompt session state, history, and bottom-bar integration + - Record and surface execution - Execution Modes: - - MENU: Interactive prompt loop using Prompt Toolkit - - COMMAND: Direct CLI command execution - - HELP: Render help output - - ERROR: Render error and exit + Routing Model: + `Falyx` performs recursive routing across visible entries in the current + namespace. - State Management: - - Uses `OptionsManager` with namespaced overrides (e.g. "execution") - - Tracks last executed command and execution context - - Integrates with `ExecutionRegistry` for history and summaries + - If no entry is selected, the route may target the current namespace + itself. + - If a help or TLDR flag is encountered before a leaf command, the route + targets namespace help for the current scope. + - If a namespace entry is selected, routing continues inside that nested + `Falyx` instance. + - If a leaf command is selected, the remaining argv is preserved and + delegated unchanged to that command's parser. + + This keeps namespace traversal separate from command-local parsing and + ensures completion, validation, help rendering, and execution all share + the same routing semantics. + + Execution Semantics: + `Falyx` does not parse command-local arguments itself once a leaf command + is resolved. Instead, it prepares the route, delegates leaf parsing to + the selected command, and forwards the prepared `(args, kwargs, + execution_args)` to `CommandExecutor`. + + This separation preserves a clean boundary: + + - `Falyx` routes + - `Command` parses + - `CommandExecutor` executes + + Interactive Semantics: + In menu mode, `Falyx` provides a prompt-driven interface with routed + validation and completion. In CLI mode, it applies the same routing and + execution pipeline to raw argv-style input. Both modes therefore share + the same command behavior, help model, and execution lifecycle. Design Notes: - Commands are first-class and may encapsulate complex workflows - Execution options are parsed separately from command arguments - All execution passes through a unified hook lifecycle - - CLI and menu modes share the same execution semantics - Args: - title (str | Markdown): Title displayed for the menu. - program (str | None): CLI program name used in help output. - usage (str | None): Optional usage string override. - description (str | None): Program description for CLI help. - epilog (str | None): Additional help text. - version (str): Program version string. - program_style (str): Rich style for program name in help. - usage_style (str): Rich style for usage string in help. - description_style (str): Rich style for description in help. - epilog_style (str): Rich style for epilog in help. - version_style (str): Rich style for version in help. - prompt (str | StyleAndTextTuples): Input prompt. - columns (int): Number of columns in menu display. - bottom_bar (BottomBar | str | Callable | None): Bottom bar renderer. - welcome_message (str | Markdown | dict): Message shown on startup. - exit_message (str | Markdown | dict): Message shown on exit. - key_bindings (KeyBindings | None): Custom Prompt Toolkit key bindings. - include_history_command (bool): Whether to include history command. - never_prompt (bool): Default prompt suppression setting. - force_confirm (bool): Default confirmation behavior. - options (OptionsManager | None): Initial options manager. - render_menu (Callable | None): Custom menu renderer. - custom_table (Callable | Table | None): Custom table builder. - hide_menu_table (bool): Whether to hide menu table. - show_placeholder_menu (bool): Show placeholder suggestions. - prompt_history_base_dir (Path): Directory for prompt history. - enable_prompt_history (bool): Enable persistent history. - enable_help_tips (bool): Show random tips in help output. + Attributes: + title (str | Markdown): Display title for the interactive menu. + program (str): Program name used in CLI-facing help and invocation paths. + commands (dict[str, Command]): Registered user commands. + builtins (dict[str, Command]): Registered built-in commands such as help, + preview, and version. + namespaces (dict[str, FalyxNamespace]): Registered nested namespaces. + options (OptionsManager): Shared runtime option manager. + hooks (HookManager): Application-level hook manager. + console (Console): Rich console used for rendering output. + key_bindings (KeyBindings): Prompt Toolkit key bindings for menu mode. + bottom_bar (BottomBar | str | Callable | None): Bottom toolbar renderer. + history (FileHistory | None): Optional persistent prompt history backend. Raises: FalyxError: If invalid configuration or command registration occurs. + CommandAlreadyExistsError: If a command, alias, or namespace identifier + collides with an existing entry. + + Notes: + - Entry names are resolved case-insensitively. + - Builtins and namespaces participate in the same routing surface as + normal commands. + - Help, TLDR, and usage rendering are scoped by `InvocationContext`, + which allows nested namespaces to render accurate command paths. """ def __init__( @@ -199,7 +254,91 @@ class Falyx: enable_prompt_history: bool = False, enable_help_tips: bool = True, ) -> None: - """Initializes the Falyx object.""" + """Initialize a Falyx application runtime. + + This constructor configures the top-level application object used to run a + Falyx CLI or interactive menu. It establishes the shared runtime state for + command registration, namespace routing, menu rendering, prompt behavior, + built-in command availability, and executor-backed dispatch. + + During initialization, `Falyx`: + + - stores application display metadata such as title, description, and version + - creates or validates the shared `OptionsManager` + - prepares key bindings, prompt rendering, and optional bottom-bar behavior + - initializes registries for commands, builtins, and namespaces + - registers default built-in commands such as help, preview, and version + - optionally enables persistent prompt history + - creates the shared `CommandExecutor` used for command dispatch + + The resulting instance is ready to have commands and namespaces added before + being executed in CLI or menu mode. + + Args: + title (str | Markdown): Title displayed for the interactive menu or top-level + application view. + program (str | None): Program name used in CLI usage text, invocation-path + rendering, and built-in help output. If `None`, an empty program name is + used. + usage (str | None): Optional usage override for namespace-level CLI help. When + omitted, usage text is derived from the current invocation context. + description (str | None): Short program description shown in top-level help + output. + epilog (str | None): Optional trailing help text rendered after the main help + sections. + version (str): Application version string used by the built-in version command. + program_style (str): Rich style used when rendering the program name. + usage_style (str): Rich style used for rendered usage text. + description_style (str): Rich style used for the program description. + epilog_style (str): Rich style used for the help epilog. + version_style (str): Rich style used for version output and version-related + rendering. + prompt (str | StyleAndTextTuples): Prompt text or Prompt Toolkit formatted text + shown in menu mode. + columns (int): Default column count used by menu-oriented UI components such as + the bottom bar. + bottom_bar (BottomBar | str | Callable[[], Any] | None): Bottom toolbar + configuration for menu mode. May be a `BottomBar` instance, a static + string, a callable renderer, or `None` to use the default bottom bar. + welcome_message (str | Markdown | dict[str, Any]): Optional welcome content + rendered when entering the interactive menu. + exit_message (str | Markdown | dict[str, Any]): Optional exit content rendered + when leaving the interactive menu. + key_bindings (KeyBindings | None): Optional Prompt Toolkit key bindings for + menu interaction. If omitted, a default `KeyBindings` object is created. + include_history_command (bool): Whether to register the built-in history + command. + never_prompt (bool): Default session-level value for the `never_prompt` + runtime option. + force_confirm (bool): Default session-level value for the `force_confirm` + runtime option. + options (OptionsManager | None): Shared options manager for the application. + If omitted, a new `OptionsManager` instance is created. + render_menu (Callable[[Falyx], None] | None): Optional custom menu renderer + used instead of the default table-based menu output. + custom_table (Callable[[Falyx], Table] | Table | None): Optional custom Rich + table or table factory used when rendering the default menu view. + hide_menu_table (bool): Whether the default menu table should be hidden. + show_placeholder_menu (bool): Whether prompt placeholder content should be + shown in the interactive prompt. + prompt_history_base_dir (Path): Base directory used to store persistent prompt + history files when history is enabled. + enable_prompt_history (bool): Whether to persist Prompt Toolkit input history + to disk. + enable_help_tips (bool): Whether to show contextual usage tips in rendered + help output. + + Raises: + FalyxError: If the provided options object is invalid or other core runtime + configuration is inconsistent. + + Notes: + - Initialization does not execute commands or parse user input. + - Default built-ins are registered immediately so they participate in routing, + completion, and help rendering from the start. + - The prompt session itself is created lazily, allowing UI-related state such + as bottom bars and key bindings to be finalized before first use. + """ self.title: str | Markdown = title self.program: str = program or "" self.usage: str | None = usage @@ -254,15 +393,34 @@ class Falyx: console=self.console, ) - def _print_suggestions_message(self, key: str, suggestions: list[str]) -> None: - """Prints a message with suggestions for the user.""" + def _print_suggestions_message( + self, + key: str, + suggestions: list[str], + message_context: str = "", + ) -> None: + """Render an unknown-entry message with optional suggestions. + + This helper standardizes the user-facing output shown when a command or + namespace token cannot be resolved. When suggestions are available, it + renders a "did you mean" style message; otherwise it prints a direct + not-found error. + + Args: + key (str): Raw token the user attempted to invoke. + suggestions (list[str]): Candidate entry names returned by resolution. + message_context (str): Optional label describing the lookup context, such as + "TLDR example". + """ + if message_context: + message_context = f"'{message_context}' " if not suggestions: self.console.print( - f"[{OneColors.DARK_RED}]❌ No command, alias, or namespace found for '{key}'.[/]" + f"[{OneColors.DARK_RED}]❌ {message_context}No command or namespace found for '{key}'.[/]" ) return None self.console.print( - f"[{OneColors.LIGHT_YELLOW}]⚠️ Unknown command, alias, or namespace '{key}'. Did you mean: [/]" + f"[{OneColors.LIGHT_YELLOW}]⚠️ {message_context}Unknown command or namespace '{key}'.\nDid you mean: [/]" f"{', '.join(suggestions)[:10]}" ) @@ -273,24 +431,54 @@ class Falyx: usage: str, description: str, ) -> None: - """Adds a TLDR example to the Falyx instance.""" + """Register a single namespace-level TLDR example. + + The referenced entry must resolve to a known command or namespace in the + current `Falyx` instance. Unknown entries are reported to the console and + are not added. + + Args: + entry_key (str): Command or namespace key the example is associated with. + usage (str): Example usage fragment shown after the resolved invocation path. + description (str): Short explanation displayed alongside the example. + """ + entry, suggestions = self.resolve_entry(entry_key) + if not entry: + self._print_suggestions_message( + entry_key, suggestions, message_context="TLDR example" + ) + return None self._tldr_examples.append( FalyxTLDRExample(entry_key=entry_key, usage=usage, description=description) ) def add_tldr_examples(self, examples: list[FalyxTLDRInput]) -> None: - """Adds TLDR examples to the Falyx instance.""" + """Register multiple namespace-level TLDR examples. + + Supports either `FalyxTLDRExample` objects or shorthand tuples of + `(entry_key, usage, description)`. + + Args: + examples (list[FalyxTLDRInput]): Example definitions to validate and append. + + Raises: + FalyxError: If an example has an unsupported shape. + """ for example in examples: if isinstance(example, FalyxTLDRExample): + entry, suggestions = self.resolve_entry(example.entry_key) + if not entry: + self._print_suggestions_message( + example.entry_key, suggestions, message_context="TLDR example" + ) + continue self._tldr_examples.append(example) elif len(example) == 3: entry_key, usage, description = example - self._tldr_examples.append( - FalyxTLDRExample( - entry_key=entry_key, - usage=usage, - description=description, - ) + self.add_tldr_example( + entry_key=entry_key, + usage=usage, + description=description, ) else: raise FalyxError( @@ -300,7 +488,15 @@ class Falyx: ) def get_current_invocation_context(self) -> InvocationContext: - """Returns the current invocation context.""" + """Build the default invocation context for this namespace. + + The returned context starts at the current namespace root and reflects the + runtime mode stored in the shared options manager. + + Returns: + InvocationContext: Fresh invocation context for help, routing, or + completion. + """ return InvocationContext( program=self.program, program_style=self.program_style, @@ -309,21 +505,40 @@ class Falyx: ) @property - def is_cli_mode(self) -> bool: - """Checks if the current mode is a CLI mode.""" + def _is_cli_mode(self) -> bool: + """Return whether the application is currently running outside menu mode. + + Returns: + bool: `True` when the active mode is not `FalyxMode.MENU`. + """ return self.options.get("mode") != FalyxMode.MENU def _validate_options( self, options: OptionsManager | None = None, ) -> None: - """Checks if the options are set correctly.""" + """Validate and install the shared options manager. + + If no options manager is provided, a new `OptionsManager` is created and + stored on the instance. + + Args: + options (OptionsManager | None): Optional options manager to reuse. + + Raises: + FalyxError: If `options` is provided but is not an `OptionsManager`. + """ self.options: OptionsManager = options or OptionsManager() if not isinstance(self.options, OptionsManager): raise FalyxError("Options must be an instance of OptionsManager.") def _register_options(self) -> None: - """Registers default options if they are not already set.""" + """Seed default application options and execution namespace values. + + This method ensures that core runtime flags such as mode, prompt behavior, + menu visibility, and program display metadata exist in the shared options + manager. + """ self.options.from_mapping(values={}, namespace_name="execution") if not self.options.get("never_prompt"): @@ -341,11 +556,17 @@ class Falyx: if not self.options.get("program_style"): self.options.set("program_style", self.program_style) - if not self.options.get("invocation_path"): - self.options.set("invocation_path", self.program) - @property - def iter_completion_names(self) -> list[str]: + def completion_names(self) -> list[str]: + """Return the visible names exposed for namespace completion. + + The result includes command keys, command aliases, namespace keys, + namespace aliases, builtins, and special entries such as history and exit, + while deduplicating names case-insensitively. + + Returns: + list[str]: Visible completion candidates for this namespace. + """ names: list[str] = [] seen: set[str] = set() @@ -386,10 +607,18 @@ class Falyx: @property def _entry_map(self) -> dict[str, Command | FalyxNamespace]: - """Builds a mapping of all valid input names to Command objects. + """Build a case-insensitive lookup map for all resolvable entries. - If a collision occurs, logs a warning and keeps the first - registered command. + The map includes commands, namespaces, builtins, history, and exit + entries. Descriptions are also registered for commands and builtins to + support friendly lookup behavior. + + Returns: + dict[str, Command | FalyxNamespace]: Normalized identifier-to-entry map. + + Raises: + CommandAlreadyExistsError: If two distinct entries claim the same + normalized identifier. """ mapping: dict[str, Command | FalyxNamespace] = {} @@ -432,16 +661,15 @@ class Falyx: return mapping - def get_title(self) -> str: - """Returns the string title of the menu.""" - if isinstance(self.title, str): - return self.title - elif isinstance(self.title, Markdown): - return self.title.markup - return self.title - def _get_exit_command(self) -> Command: - """Returns the back command for the menu.""" + """Create the default exit command for this namespace. + + The default entry emits a `QuitSignal`, is excluded from history-sensitive + behavior, and is rendered with the namespace's shared options manager. + + Returns: + Command: Configured exit command instance. + """ exit_command = Command( key="X", description="Exit", @@ -459,7 +687,14 @@ class Falyx: return exit_command def _get_history_command(self) -> Command: - """Returns the history command for the menu.""" + """Create the built-in execution-history command. + + The returned command wraps `ExecutionRegistry.summary` and includes a + purpose-built parser for history filtering, clearing, and result lookup. + + Returns: + Command: Configured history command instance. + """ parser = CommandArgumentParser( command_key="Y", command_description="History", @@ -528,8 +763,15 @@ class Falyx: ) def get_tip(self) -> str: - """Returns a random tip for the user about using Falyx.""" - program = f"{self.program} " if self.is_cli_mode else "" + """Return a random usage tip appropriate for the current runtime mode. + + Tips differ slightly between CLI and menu mode so the user sees examples + that match the active interface. + + Returns: + str: One formatted help tip. + """ + program = f"{self.program} " if self._is_cli_mode else "" tips = [ f"Use '{program}?[COMMAND]' to preview a command.", "Every command supports aliases—try abbreviating the name!", @@ -542,7 +784,7 @@ class Falyx: f"Run commands directly from the CLI: '{self.program} [COMMAND] [OPTIONS]'.", "All [COMMAND] keys and aliases are case-insensitive.", ] - if self.is_cli_mode: + if self._is_cli_mode: tips.extend( [ f"Use '{self.program} help' to list all commands at any time.", @@ -567,15 +809,24 @@ class Falyx: async def _render_command_tldr( self, command: Command, - context: InvocationContext | None = None, + invocation_context: InvocationContext | None = None, ) -> None: - """Renders the TLDR examples for a command, if available.""" + """Render TLDR examples for a resolved command. + + This helper validates that the supplied entry is a command, delegates TLDR + rendering to that command, and optionally appends a random usage tip. + + Args: + command (Command): Command whose TLDR output should be shown. + invocation_context (InvocationContext | None): Optional routed invocation context used to scope the + rendered usage path. + """ 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(invocation_context=context): + if command.render_tldr(invocation_context=invocation_context): if self.enable_help_tips: self.console.print(f"[bold]tip:[/bold] {self.get_tip()}") else: @@ -587,17 +838,26 @@ class Falyx: self, command: Command, tldr: bool = False, - context: InvocationContext | None = None, + invocation_context: InvocationContext | None = None, ) -> None: - """Renders the detailed help for a command, if available.""" + """Render detailed help or TLDR output for a resolved command. + + Args: + command (Command): Target command to render. + tldr (bool): When `True`, render TLDR output instead of full help. + invocation_context (InvocationContext | None): Optional routed invocation context used to scope the + rendered usage path. + """ if not isinstance(command, Command): self.console.print( f"Entry '{command.key}' is not a command.", style=OneColors.DARK_RED ) return None if tldr: - await self._render_command_tldr(command, context=context) - elif command.render_help(invocation_context=context): + await self._render_command_tldr( + command, invocation_context=invocation_context + ) + elif command.render_help(invocation_context=invocation_context): if self.enable_help_tips: self.console.print(f"\n[bold]tip:[/bold] {self.get_tip()}") else: @@ -606,7 +866,14 @@ class Falyx: ) async def _render_tag_help(self, tag: str) -> None: - """Renders a list of commands matching a specific tag.""" + """Render all visible commands associated with a tag. + + Matching is case-insensitive and only searches user-registered commands, + not namespaces or builtins. + + Args: + tag (str): Tag name to filter by. + """ tag_lower = tag.lower() self.console.print(f"[bold]{tag_lower}:[/bold]") commands = [ @@ -629,7 +896,11 @@ class Falyx: self.console.print(f"[bold]tip:[/bold] {self.get_tip()}") async def _render_menu_help(self) -> None: - """Renders the main menu help menu with all commands and menu builtins.""" + """Render the interactive menu-style help view for this namespace. + + The menu help view displays user commands plus the special help, history, + and exit entries using panel-based Rich rendering. + """ self.console.print("[bold]help:[/bold]") for command in self.commands.values(): usage, description, tag = command.help_signature @@ -671,23 +942,46 @@ class Falyx: if self.enable_help_tips: self.console.print(f"[bold]tip:[/bold] {self.get_tip()}") - def get_usage(self, context: InvocationContext) -> str: + def _get_usage(self, invocation_context: InvocationContext) -> str: + """Build the default namespace usage fragment for the given context. + + Usage text is aware of whether the current namespace exposes nested + namespaces and whether rendering is happening in CLI or menu mode. + + Args: + invocation_context (InvocationContext): Routed invocation context for the current help + target. + + Returns: + str: Escaped usage fragment suitable for Rich output. + """ has_namespaces = any(not ns.hidden for ns in self.namespaces.values()) target = "command" if not has_namespaces else "command or namespace" - if not context.typed_path and context.is_cli_mode: + if not invocation_context.typed_path and invocation_context.is_cli_mode: return escape(f"[-h] [-T] [-v] [-d] [-n] <{target}> [args...]") - elif not context.typed_path: + elif not invocation_context.typed_path: return escape(f"[-h] [-T] <{target}> [args...]") return escape(f"<{target}> [args...]") - async def _render_namespace_tldr_help(self, context: InvocationContext) -> None: + async def _render_namespace_tldr_help( + self, invocation_context: InvocationContext + ) -> None: + """Render namespace-level TLDR examples for the current scope. + + This prints usage, optional namespace description, and all registered TLDR + examples using the routed invocation path supplied by the context. + + Args: + invocation_context (InvocationContext): Routed invocation context for the namespace being + rendered. + """ if not self._tldr_examples: self.console.print( - f"[bold]No TLDR examples available for '{self.get_title()}'.[/bold]" + f"[bold]No TLDR examples available for '{self._get_title()}'.[/bold]" ) return None - usage = self.usage or self.get_usage(context) - prefix = context.markup_path + usage = self.usage or self._get_usage(invocation_context) + prefix = invocation_context.markup_path self.console.print( f"[bold]usage:[/bold] {prefix} [{self.usage_style}]{usage}[/{self.usage_style}]" ) @@ -698,10 +992,10 @@ class Falyx: if self._tldr_examples: self.console.print("\n[bold]examples:[/bold]") for example in self._tldr_examples: - entry, _ = self.resolve_entry(example.entry_key) + entry, suggestions = self.resolve_entry(example.entry_key) if not entry: - self.console.print( - f"[{OneColors.LIGHT_YELLOW}]⚠️ TLDR example references unknown entry '{example.entry_key}'.[/]" + self._print_suggestions_message( + example.entry_key, suggestions, message_context="TLDR example" ) continue command = f"[{entry.style}]{example.entry_key}[/{entry.style}]" @@ -716,20 +1010,41 @@ class Falyx: ) async def render_namespace_help( - self, context: InvocationContext, tldr: bool = False + self, + invocation_context: InvocationContext | None = None, + tldr: bool = False, ) -> None: + """Render help for the current namespace. + + Depending on the active mode and flags, this dispatches to namespace TLDR, + menu-style help, or CLI-style help rendering. + + Args: + invocation_context (InvocationContext | None): Optional routed invocation context. When omitted, a + fresh root context is created. + tldr (bool): Whether to render namespace TLDR output instead of standard help. + """ + invocation_context = invocation_context or self.get_current_invocation_context() if tldr: - await self._render_namespace_tldr_help(context) - elif context.mode is FalyxMode.MENU: + await self._render_namespace_tldr_help(invocation_context) + elif invocation_context.mode is FalyxMode.MENU: await self._render_menu_help() else: - await self._render_cli_help(context) + await self._render_cli_help(invocation_context) - async def _render_cli_help(self, context: InvocationContext) -> None: - """Renders the CLI help menu with all available commands and options.""" - usage = self.usage or self.get_usage(context) + async def _render_cli_help(self, invocation_context: InvocationContext) -> None: + """Render the CLI-style help view for this namespace. + + The output includes usage, description, global options, builtin commands, + user commands, and optional epilog content. + + Args: + invocation_context (InvocationContext): Routed invocation context used to render the current + invocation path. + """ + usage = self.usage or self._get_usage(invocation_context) self.console.print( - f"[bold]usage:[/bold] {context.markup_path} [{self.usage_style}]{usage}[/{self.usage_style}]" + f"[bold]usage:[/bold] {invocation_context.markup_path} [{self.usage_style}]{usage}[/{self.usage_style}]" ) if self.description: self.console.print( @@ -776,17 +1091,31 @@ 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 + def _help_target_base_context( + self, invocation_context: InvocationContext + ) -> InvocationContext: + """Normalize help context before rendering a nested target. - last_token = context.typed_path[-1] + This strips the trailing help-command segment from the routed path when the + help command itself is the active entry, preventing duplicated invocation + paths in nested help output. + + Args: + invocation_context (InvocationContext): Routed help context to normalize. + + Returns: + InvocationContext: Adjusted context for downstream help rendering. + """ + if not invocation_context.typed_path: + return invocation_context + + last_token = invocation_context.typed_path[-1] entry, _ = self.resolve_entry(last_token) if entry is self.help_command: - return context.without_last_path_segment() + return invocation_context.without_last_path_segment() - return context + return invocation_context async def render_help( self, @@ -796,7 +1125,23 @@ class Falyx: namespace_tldr: bool = False, invocation_context: InvocationContext | None = None, ) -> None: - """Renders the help menu with command details, usage examples, and tips.""" + """Render help for a namespace, tag, or specific entry. + + This is the main help dispatcher for `Falyx`. It can render: + + - namespace help for the current scope + - namespace TLDR output + - tag-filtered command help + - command help for a specific key + - namespace help for a specific nested namespace + + Args: + tag (str): Optional tag filter for command help. + key (str | None): Optional command or namespace identifier to render directly. + tldr (bool): Whether targeted command help should use TLDR output. + namespace_tldr (bool): Whether top-level namespace help should use TLDR output. + invocation_context (InvocationContext | None): Optional routed invocation context. + """ context = invocation_context or self.get_current_invocation_context() if key: base_context = self._help_target_base_context(context) @@ -806,11 +1151,15 @@ class Falyx: await self._render_command_help( command=entry, tldr=tldr, - context=base_context.with_path_segment(key, style=entry.style), + invocation_context=base_context.with_path_segment( + key, style=entry.style + ), ) elif isinstance(entry, FalyxNamespace): await entry.namespace.render_namespace_help( - context=base_context.with_path_segment(key, style=entry.style), + invocation_context=base_context.with_path_segment( + key, style=entry.style + ), tldr=tldr, ) else: @@ -821,7 +1170,7 @@ class Falyx: await self._render_command_help( self.help_command, tldr, - context=context, + invocation_context=context, ) elif tag: await self._render_tag_help(tag) @@ -829,7 +1178,14 @@ class Falyx: await self.render_namespace_help(context, namespace_tldr) def _get_help_command(self) -> Command: - """Returns the help command for the menu.""" + """Create the built-in help command for this namespace. + + The returned command wraps `render_help()` and installs a dedicated parser + that supports tag filtering, targeted key help, and TLDR behavior. + + Returns: + Command: Configured help command instance. + """ parser = CommandArgumentParser( command_key="H", command_description="Help", @@ -876,7 +1232,14 @@ class Falyx: ) async def _preview(self, key: str) -> None: - """Previews the execution of a command without actually running it.""" + """Render a preview for a specific command key. + + Namespaces are rejected because preview is only meaningful at the leaf + command boundary. + + Args: + key (str): Command key or alias to preview. + """ entry, suggestions = self.resolve_entry(key) if isinstance(entry, FalyxNamespace): self.console.print( @@ -890,7 +1253,14 @@ class Falyx: self._print_suggestions_message(key, suggestions) def _get_preview_command(self) -> Command: - """Returns the preview command for Falyx.""" + """Create the built-in preview command. + + The preview command accepts a command key or alias and delegates to + `_preview()`. + + Returns: + Command: Configured preview command instance. + """ preview_parser = CommandArgumentParser( command_key="preview", command_description="Preview", @@ -922,11 +1292,15 @@ class Falyx: return preview_command async def _render_version(self) -> None: - """Renders the program version.""" + """Render the program version string for this namespace.""" self.console.print(f"[{self.version_style}]{self.program} v{self.version}[/]") def _get_version_command(self) -> Command: - """Returns the version command for Falyx.""" + """Create the built-in version command. + + Returns: + Command: Configured version command instance. + """ version_command = Command( key="version", description="Version", @@ -945,24 +1319,43 @@ class Falyx: return version_command def _add_builtin(self, command: Command) -> None: - """Adds a built-in command to Falyx.""" + """Register a builtin command in the current namespace. + + Args: + command (Command): Builtin command to register. + + Raises: + CommandAlreadyExistsError: If the builtin key or aliases collide with an + existing identifier. + """ self._validate_command_aliases(command.key, command.aliases) self.builtins[command.key.upper()] = command _ = self._entry_map def _register_default_builtins(self) -> None: - """Registers the default built-in commands for Falyx.""" + """Register the default help, preview, and version builtins.""" self._add_builtin(self.help_command) self._add_builtin(self._get_preview_command()) self._add_builtin(self._get_version_command()) def _get_completer(self) -> FalyxCompleter: - """Completer to provide auto-completion for the menu commands.""" + """Create the Prompt Toolkit completer for this namespace. + + Returns: + FalyxCompleter: Routing-aware completer bound to this `Falyx` instance. + """ return FalyxCompleter(self) def _get_validator_error_message(self) -> str: - """Validator to check if the input is a valid command.""" - visible = self.iter_visible_entries( + """Build the validation error message shown by the prompt session. + + The message lists all currently visible entry keys and aliases that may be + invoked from the current namespace. + + Returns: + str: User-facing validation error text. + """ + visible = self._iter_visible_entries( include_help=True, include_history=True, include_exit=True, @@ -982,19 +1375,35 @@ class Falyx: return error_message def _invalidate_prompt_session_cache(self): - """Forces the prompt session to be recreated on the next access.""" + """Drop any cached prompt session so UI changes take effect. + + This is used when bottom-bar configuration or other prompt-session state + changes and a fresh `PromptSession` must be built on next access. + """ if hasattr(self, "prompt_session"): del self.prompt_session self._prompt_session = None @property def bottom_bar(self) -> BottomBar | str | Callable[[], Any] | None: - """Returns the bottom bar for the menu.""" + """Return the configured bottom-bar definition for menu mode.""" return self._bottom_bar @bottom_bar.setter def bottom_bar(self, bottom_bar: BottomBar | str | Callable[[], Any] | None) -> None: - """Sets the bottom bar for the menu.""" + """Install or normalize the bottom-bar configuration. + + `None` produces a default `BottomBar`. A `BottomBar` instance is rebound to + this namespace's key bindings. Strings and callables are stored directly as + alternate toolbar renderers. + + Args: + bottom_bar (BottomBar | str | Callable[[], Any] | None): Toolbar + configuration to install. + + Raises: + FalyxError: If the value is not a supported bottom-bar type. + """ if bottom_bar is None: self._bottom_bar = BottomBar(self.columns, self.key_bindings) elif isinstance(bottom_bar, BottomBar): @@ -1009,7 +1418,12 @@ class Falyx: self._invalidate_prompt_session_cache() def _get_bottom_bar_render(self) -> Callable[[], Any] | str | None: - """Returns the bottom bar for the menu.""" + """Return the actual toolbar renderer used by the prompt session. + + Returns: + Callable[[], Any] | str | None: Render callable, static toolbar string, + or `None` when no toolbar should be shown. + """ if isinstance(self.bottom_bar, BottomBar) and self.bottom_bar.has_items: return self.bottom_bar.render elif callable(self.bottom_bar): @@ -1020,7 +1434,15 @@ class Falyx: @cached_property def prompt_session(self) -> PromptSession: - """Returns the prompt session for the menu.""" + """Create and cache the interactive prompt session. + + The prompt session wires together completion, validation, history, + bottom-toolbar rendering, placeholder content, and quit behavior for menu + mode. + + Returns: + PromptSession: Configured prompt session for interactive input. + """ if self._prompt_session is None: placeholder = self.build_placeholder_menu() self._prompt_session = PromptSession( @@ -1040,7 +1462,19 @@ class Falyx: return self._prompt_session def register_all_hooks(self, hook_type: HookType, hooks: Hook | list[Hook]) -> None: - """Registers hooks for all commands in the menu and actions recursively.""" + """Register a hook across the namespace and all nested actions. + + Hooks are attached to the application hook manager, every registered + command, and any nested `BaseAction` or nested `Falyx` runtime reachable + through command actions. + + Args: + hook_type (HookType): Lifecycle slot to register against. + hooks (Hook | list[Hook]): Single hook or list of hooks to apply recursively. + + Raises: + InvalidActionError: If any supplied hook is not callable. + """ hook_list = hooks if isinstance(hooks, list) else [hooks] for hook in hook_list: if not callable(hook): @@ -1054,15 +1488,25 @@ class Falyx: command.action.register_hooks_recursively(hook_type, hook) def register_all_with_debug_hooks(self) -> None: - """Registers debug hooks for all commands in the menu and actions recursively.""" + """Install the standard debug hook set across all commands and actions.""" self.register_all_hooks(HookType.BEFORE, log_before) self.register_all_hooks(HookType.ON_SUCCESS, log_success) self.register_all_hooks(HookType.ON_ERROR, log_error) self.register_all_hooks(HookType.AFTER, log_after) - self.register_all_hooks(HookType.ON_TEARDOWN, log_after) def _validate_command_aliases(self, key: str, aliases: list[str] | None) -> None: - """Validates the command aliases to ensure they are unique.""" + """Validate that a new command or namespace identifier set is unique. + + Validation is case-insensitive and checks the proposed key and aliases + against existing commands, builtins, history, and exit entries. + + Args: + key (str): Proposed primary key. + aliases (list[str] | None): Proposed aliases for the same entry. + + Raises: + CommandAlreadyExistsError: If duplicates or collisions are found. + """ key = key.upper() aliases = [alias.upper() for alias in (aliases or [])] @@ -1109,7 +1553,24 @@ class Falyx: confirm_message: str = "Are you sure?", help_text: str = "Exit the program.", ) -> None: - """Updates the back command of the menu.""" + """Replace the namespace exit command with a custom one. + + This is commonly used by submenus to swap the default exit behavior for a + back-navigation command. + + Args: + key (str): New command key. + description (str): User-facing description. + aliases (list[str] | None): Optional aliases for the exit command. + action (Callable[..., Any] | None): Optional callable to execute. Defaults to raising `QuitSignal`. + style (str): Rich style used for menu/help rendering. + confirm (bool): Whether the command should require confirmation. + confirm_message (str): Confirmation prompt text. + help_text (str): Help text shown in command listings and help output. + + Raises: + InvalidActionError: If the supplied action is not callable. + """ self._validate_command_aliases(key, aliases) action = action or SignalAction(description, QuitSignal()) if not callable(action): @@ -1140,7 +1601,23 @@ class Falyx: aliases: list[str] | None = None, help_text: str = "", ) -> None: - """Adds a submenu to the menu.""" + """Register a nested `Falyx` instance as a namespace entry. + + The submenu becomes part of routing, completion, and help output in the + current namespace. When the submenu still uses the default exit command, it + is converted to a back command automatically. + + Args: + key (str): Namespace key used to enter the submenu. + description (str): User-facing namespace description. + submenu (Falyx): Nested `Falyx` instance to register. + style (str | None): Optional style override for the namespace entry. + aliases (list[str] | None): Optional aliases for the namespace. + help_text (str): Optional help text for namespace listings. + + Raises: + NotAFalyxError: If `submenu` is not a `Falyx` instance. + """ if not isinstance(submenu, Falyx): raise NotAFalyxError("submenu must be an instance of Falyx.") @@ -1166,7 +1643,16 @@ class Falyx: ) def add_commands(self, commands: list[Command] | list[dict]) -> None: - """Adds a list of Command instances or config dicts.""" + """Register multiple commands from instances or config dictionaries. + + Args: + commands (list[Command] | list[dict]): Sequence of `Command` objects or `add_command()` keyword + dictionaries. + + Raises: + FalyxError: If an element is neither a `Command` nor a configuration + dictionary. + """ for command in commands: if isinstance(command, dict): self.add_command(**command) @@ -1178,7 +1664,14 @@ class Falyx: ) def add_command_from_command(self, command: Command) -> None: - """Adds a command to the menu from an existing Command object.""" + """Register an already-built `Command` object. + + Args: + command (Command): Preconstructed command to add to this namespace. + + Raises: + FalyxError: If `command` is not a `Command`. + """ if not isinstance(command, Command): raise FalyxError("command must be an instance of Command.") self._validate_command_aliases(command.key, command.aliases) @@ -1228,7 +1721,57 @@ class Falyx: simple_help_signature: bool = False, ignore_in_history: bool = False, ) -> Command: - """Adds an command to the menu, preventing duplicates.""" + """Build and register a new command in the current namespace. + + This is the main command-registration API for `Falyx`. It forwards the + supplied configuration to `Command.build()`, injects shared runtime state, + validates identifier uniqueness, and stores the resulting command. + + Args: + key (str): Primary command key. + description (str): User-facing command description. + action (BaseAction | Callable[..., Any]): Underlying action or callable executed by the command. + args (tuple): Static positional arguments bound to the command. + kwargs (dict[str, Any] | None): Static keyword arguments bound to the command. + hidden (bool): Whether the command should be omitted from menu/help listings. + aliases (list[str] | None): Optional alternate invocation names. + help_text (str): Short help text shown in listings. + help_epilog (str): Extended help text shown in command help. + style (str): Rich style used for display. + confirm (bool): Whether confirmation should be required before execution. + confirm_message (str): Confirmation prompt text. + preview_before_confirm (bool): Whether preview should run before confirmation. + spinner (bool): Whether spinner hooks should be enabled. + spinner_message (str): Spinner label. + spinner_type (str): Rich spinner preset name. + spinner_style (str): Rich style for spinner output. + spinner_speed (float): Spinner speed multiplier. + hooks (HookManager | None): Optional command hook manager. + before_hooks (list[Callable] | None): Optional before hooks. + success_hooks (list[Callable] | None): Optional success hooks. + error_hooks (list[Callable] | None): Optional error hooks. + after_hooks (list[Callable] | None): Optional after hooks. + teardown_hooks (list[Callable] | None): Optional teardown hooks. + tags (list[str] | None): Optional tag labels for grouping and help filtering. + logging_hooks (bool): Whether debug hooks should be enabled. + retry (bool): Whether retry behavior should be enabled. + retry_all (bool): Whether retry should be applied recursively to nested actions. + retry_policy (RetryPolicy | None): Retry policy override. + arg_parser (CommandArgumentParser | None): Optional explicit command argument parser. + arguments (list[dict[str, Any]] | None): Optional declarative argument definitions. + argument_config (Callable[[CommandArgumentParser], None] | None): Optional callback that populates the parser. + execution_options (list[ExecutionOption | str] | None): Optional execution-level options to enable. + custom_parser (ArgParserProtocol | None): Optional parser override for full custom argument parsing. + custom_help (Callable[[], str | None] | None): Optional custom help renderer. + auto_args (bool): Whether argument inference should run automatically. + arg_metadata (dict[str, str | dict[str, Any]] | None): Optional metadata used during argument inference. + simple_help_signature (bool): Whether command listings should use compact help. + ignore_in_history (bool): Whether this command should be ignored by history-aware + result tracking. + + Returns: + Command: The newly built and registered command. + """ self._validate_command_aliases(key, aliases) command = Command.build( @@ -1279,8 +1822,12 @@ class Falyx: _ = self._entry_map return command - def get_bottom_row(self) -> list[str]: - """Returns the bottom row of the table for displaying additional commands.""" + def _get_bottom_row(self) -> list[str]: + """Build the special bottom-row entries for menu tables. + + Returns: + list[str]: Rendered help, history, and exit command labels. + """ bottom_row = [] if self.help_command: bottom_row.append( @@ -1298,7 +1845,7 @@ class Falyx: ) return bottom_row - def iter_visible_entries( + def _iter_visible_entries( self, *, include_builtins: bool = False, @@ -1306,6 +1853,17 @@ class Falyx: include_history: bool = False, include_exit: bool = False, ) -> list[Command | FalyxNamespace]: + """Collect visible entries for menu or validation message use. + + Args: + include_builtins (bool): Whether normal builtin commands should be included. + include_help (bool): Whether the help command should be appended. + include_history (bool): Whether the history command should be appended. + include_exit (bool): Whether the exit command should be appended. + + Returns: + list[Command | FalyxNamespace]: Visible entries in display order. + """ visible: list[Command | FalyxNamespace] = [] visible.extend([cmd for cmd in self.commands.values() if not cmd.hidden]) visible.extend([ns for ns in self.namespaces.values() if not ns.hidden]) @@ -1320,25 +1878,33 @@ class Falyx: return visible def build_default_table(self) -> Table: - """Build the standard table layout. + """Build the standard Rich table used for menu display. - Developers can subclass or call this in custom tables. + Returns: + Table: Default menu table for the current namespace. """ table = Table(title=self.title, show_header=False, box=box.SIMPLE) # type: ignore[arg-type] - visible = self.iter_visible_entries() + visible = self._iter_visible_entries() for chunk in chunks(visible, self.columns): row = [] for entry in chunk: escaped_key = escape(f"[{entry.key}]") row.append(f"{escaped_key} [{entry.style}]{entry.description}") table.add_row(*row) - bottom_row = self.get_bottom_row() + bottom_row = self._get_bottom_row() for row in chunks(bottom_row, self.columns): table.add_row(*row) return table def build_placeholder_menu(self) -> StyleAndTextTuples: - """Builds a menu placeholder for show_placeholder_menu.""" + """Build placeholder text for the interactive prompt. + + The placeholder summarizes visible commands and special bottom-row entries + and is used when `show_placeholder_menu` is enabled. + + Returns: + StyleAndTextTuples: Prompt Toolkit-compatible formatted placeholder. + """ visible_commands = [item for item in self.commands.items() if not item[1].hidden] if not visible_commands: return [("", "")] @@ -1346,14 +1912,25 @@ class Falyx: placeholder: list[str] = [] for key, command in visible_commands: placeholder.append(f"[{key}] [{command.style}]{command.description}[/]") - for command_str in self.get_bottom_row(): + for command_str in self._get_bottom_row(): placeholder.append(command_str) return rich_text_to_prompt_text(" ".join(placeholder)) @property def table(self) -> Table: - """Creates or returns a custom table to display the menu commands.""" + """Return the active menu table for this namespace. + + When `custom_table` is callable, it is invoked and must return a Rich + `Table`. When `custom_table` is already a `Table`, that instance is reused. + Otherwise the default menu table is built. + + Returns: + Table: Table used by menu rendering. + + Raises: + FalyxError: If a custom table factory returns a non-`Table` value. + """ if callable(self.custom_table): custom_table = self.custom_table(self) if not isinstance(custom_table, Table): @@ -1366,16 +1943,25 @@ class Falyx: else: return self.build_default_table() - def parse_preview_command(self, input_str: str) -> tuple[bool, str]: - """Checks if the input is a preview command and returns the command key if so.""" - if input_str.startswith("?"): - return True, input_str[1:].strip() - return False, input_str.strip() - def resolve_entry( self, token: str, ) -> tuple[Command | FalyxNamespace | None, list[str]]: + """Resolve a token to a command or namespace entry. + + Resolution is case-insensitive and proceeds in three stages: + + 1. Exact identifier match + 2. Unique prefix match + 3. Close-match suggestion lookup + + Args: + token (str): Raw user token to resolve. + + Returns: + tuple[Command | FalyxNamespace | None, list[str]]: Resolved entry, if + any, plus suggestion strings when resolution fails. + """ normalized = token.upper().strip() # exact match @@ -1393,6 +1979,7 @@ class Falyx: if len(prefix_matches) == 1: return prefix_matches[0], [] + # close match suggestions suggestions = get_close_matches( normalized, list(self._entry_map.keys()), n=3, cutoff=0.7 ) @@ -1405,6 +1992,27 @@ class Falyx: mode: FalyxMode | None = None, from_validate: bool = False, ) -> tuple[RouteResult | None, tuple, dict[str, Any], dict[str, Any]]: + """Tokenize input, resolve a route, and parse leaf-command arguments. + + This is the main preparation boundary between raw user input and executable + command dispatch. It: + + - tokenizes shell-style input + - detects preview-prefixed commands + - creates an initial `InvocationContext` + - resolves a `RouteResult` through namespace routing + - delegates leaf argument parsing to the resolved command when appropriate + + Args: + raw_arguments (list[str] | str): Raw argv-style input as a string or token list. + mode (FalyxMode | None): Optional mode override for the initial invocation context. + from_validate (bool): Whether errors should be surfaced as prompt validation + errors instead of normal runtime output. + + Returns: + tuple[RouteResult | None, tuple, dict[str, Any], dict[str, Any]]: + Resolved route, positional args, keyword args, and execution args. + """ args: tuple = () kwargs: dict[str, Any] = {} execution_args: dict[str, Any] = {} @@ -1444,7 +2052,7 @@ class Falyx: is_preview=is_preview, ) - route = await self.resolve_route(tokens, context=context) + route = await self.resolve_route(tokens, invocation_context=context) if is_preview: route.is_preview = True @@ -1477,6 +2085,11 @@ class Falyx: return route, args, kwargs, execution_args async def _render_unknown_route(self, route: RouteResult) -> None: + """Render help plus suggestions for an unresolved route. + + Args: + route (RouteResult): Unknown route returned by namespace resolution. + """ context = route.context typed_key = context.typed_path[0].upper() await route.namespace.render_namespace_help(context) @@ -1494,7 +2107,26 @@ class Falyx: wrap_errors: bool = True, summary_last_result: bool = False, ) -> Any | None: + """Dispatch a prepared route to help rendering, menu flow, or execution. + This method is the final route-handling stage after preparation. It knows + how to handle namespace menus, namespace help, namespace TLDR, unknown + routes, preview routes, and normal leaf-command execution. + + Args: + route (RouteResult): Prepared route to dispatch. + args (tuple): Positional arguments prepared for a leaf command. + kwargs (dict[str, Any] | None): Keyword arguments prepared for a leaf command. + execution_args (dict[str, Any] | None): Execution-only arguments such as confirmation or retry + overrides. + raise_on_error (bool): Whether executor errors should be re-raised. + wrap_errors (bool): Whether executor errors should be wrapped as `FalyxError`. + summary_last_result (bool): Whether summary output should only have the last + result when supported. + + Returns: + Any | None: Command result for executed leaf commands, otherwise `None`. + """ if route.kind is RouteKind.NAMESPACE_MENU: await route.namespace.menu() return None @@ -1568,8 +2200,6 @@ class Falyx: `execution_args` via `prepare_route()`. - Returns `None` when help output is triggered, argument parsing fails, the command cannot be found, or preview mode is requested. - - Raises `QuitSignal` if the resolved command is the configured exit - command. - For normal execution, forwards the resolved command and execution options to `_executor.execute()`. @@ -1582,6 +2212,7 @@ class Falyx: `FalyxError` by the underlying executor before being raised. summary_last_result (bool): Whether summary output should include the last result when execution summary reporting is requested. + mode (FalyxMode): Runtime mode used while preparing the route. Returns: Any | None: The command result returned by the underlying executor, or @@ -1625,18 +2256,27 @@ class Falyx: *, stub: str, cursor_at_end_of_token: bool, - context: InvocationContext, + invocation_context: InvocationContext, is_preview: bool = False, ) -> CompletionRoute: - """Route only until the leaf-command boundary. + """Resolve partial input for autocompletion. - Unlike resolve_route(), this method tolerates incomplete trailing input. - It stops either: - - inside a namespace, where the next token should be an entry, or - - at a leaf command, where remaining tokens belong to that command's argv. + Unlike full routing, completion routing tolerates incomplete trailing input. + It stops at the first point where completion must either suggest namespace + entries or delegate the remaining input to a leaf command's argument parser. + + Args: + committed_tokens (list[str]): Tokens fully committed before the active stub. + stub (str): Current token fragment under the cursor. + cursor_at_end_of_token (bool): Whether the cursor sits at a token boundary. + invocation_context (InvocationContext): Current routed invocation context. + is_preview (bool): Whether the input is preview-prefixed. + + Returns: + CompletionRoute: Partial route used by the completer. """ namespace = self - route_context = context + route_context = invocation_context remaining = list(committed_tokens) while remaining: @@ -1691,14 +2331,31 @@ class Falyx: self, tokens: list[str], *, - context: InvocationContext, + invocation_context: InvocationContext, ) -> RouteResult: + """Resolve an invocation path across namespaces until a leaf boundary. + + Routing is recursive and namespace-aware. It stops when one of the + following occurs: + + - no tokens remain, targeting the current namespace menu + - a namespace-level help or TLDR flag is encountered + - an unknown token is found + - a leaf command is reached + + Args: + tokens (list[str]): Remaining tokens to route. + invocation_context (InvocationContext): Routed context accumulated so far. + + Returns: + RouteResult: Final routed result for the supplied token path. + """ # 1. No more tokens -> this namespace itself was targeted if not tokens: return RouteResult( kind=RouteKind.NAMESPACE_MENU, namespace=self, - context=context, + context=invocation_context, ) head, *tail = tokens @@ -1708,14 +2365,14 @@ class Falyx: return RouteResult( kind=RouteKind.NAMESPACE_HELP, namespace=self, - context=context, + context=invocation_context, ) if head in {"-T", "--tldr"}: return RouteResult( kind=RouteKind.NAMESPACE_TLDR, namespace=self, - context=context, + context=invocation_context, ) # 3. Resolve the next entry in this namespace @@ -1724,15 +2381,17 @@ class Falyx: return RouteResult( kind=RouteKind.UNKNOWN, namespace=self, - context=context, + context=invocation_context, suggestions=suggestions, ) - route_context = context.with_path_segment(head, style=entry.style) + route_context = invocation_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=route_context) + return await entry.namespace.resolve_route( + tail, invocation_context=route_context + ) # 5. Leaf command -> stop routing; leave tail untouched for leaf parser return RouteResult( @@ -1743,8 +2402,12 @@ class Falyx: leaf_argv=tail, ) - async def process_command(self) -> None: - """Processes the action of the selected command.""" + async def _process_command(self) -> None: + """Read one prompt input from the interactive session and execute it. + + This helper refreshes the Prompt Toolkit app, collects raw input from the + cached prompt session, and forwards that input to `execute_command()`. + """ app = get_app() await asyncio.sleep(0.1) app.invalidate() @@ -1757,8 +2420,16 @@ class Falyx: summary_last_result=True, ) - def print_message(self, message: str | Markdown | dict[str, Any]) -> None: - """Prints a message to the console.""" + def _print_message(self, message: str | Markdown | dict[str, Any]) -> None: + """Print a startup or exit message using the configured console. + + Args: + message (str | Markdown | dict[str, Any]): Plain string, `Markdown`, + or a Rich-print argument dictionary. + + Raises: + TypeError: If the message is not a supported type. + """ if isinstance(message, (str, Markdown)): self.console.print(message) elif isinstance(message, dict): @@ -1771,12 +2442,32 @@ class Falyx: "Message must be a string, Markdown, or dictionary with args and kwargs." ) + def _get_title(self) -> str: + """Return the menu title as plain text. + + This normalizes string and `Markdown` title inputs into a single text value + for logging and display helpers. + + Returns: + str: Plain-text title for the current namespace. + """ + if isinstance(self.title, str): + return self.title + elif isinstance(self.title, Markdown): + return self.title.markup + return self.title + async def menu(self) -> None: - """Runs the menu and handles user input.""" - logger.info("Starting menu: %s", self.get_title()) + """Run the interactive menu loop for this namespace. + + The menu loop renders the current table view, reads commands from the prompt + session, handles navigation and cancellation signals, and prints optional + welcome and exit messages. + """ + logger.info("Starting menu: %s", self._get_title()) self.options.set("mode", FalyxMode.MENU) if self.welcome_message: - self.print_message(self.welcome_message) + self._print_message(self.welcome_message) try: while True: if not self.options.get("hide_menu_table", self._hide_menu_table): @@ -1785,7 +2476,7 @@ class Falyx: else: self.console.print(self.table, justify="center") try: - await self.process_command() + await self._process_command() except (EOFError, KeyboardInterrupt): logger.info("EOF or KeyboardInterrupt. Exiting menu.") break @@ -1799,12 +2490,19 @@ class Falyx: except asyncio.CancelledError: logger.info("[asyncio.CancelledError]. <- Returning to the menu.") finally: - logger.info("Exiting menu: %s", self.get_title()) + logger.info("Exiting menu: %s", self._get_title()) if self.exit_message: - self.print_message(self.exit_message) + self._print_message(self.exit_message) def _apply_parse_result(self, result: RootParseResult) -> None: - """Applies the parsed CLI arguments to the menu options.""" + """Apply parsed root/session options to runtime state. + + This updates the active mode, logging verbosity, debug-hook registration, + and prompt behavior based on the root parse result. + + Args: + result (RootParseResult): Parsed root CLI result to apply. + """ self.options.set("mode", result.mode) if result.verbose: @@ -1830,9 +2528,15 @@ class Falyx: ) -> None: """Execute the Falyx application using CLI-driven dispatch. - This method is the primary entrypoint for Falyx applications. It parses - CLI arguments, configures runtime state, and dispatches execution based - on the resolved mode. + This method is the primary entrypoint for Falyx applications. + + - parses root CLI flags using `FalyxParser` + - optionally invokes a post-parse callback + - applies session/runtime options + - renders help immediately when requested + - prepares and dispatches the routed command + - exits with CLI-appropriate status codes + - optionally falls through to interactive menu mode Callback Behavior: - If provided, `callback` is executed after parsing but before dispatch @@ -1842,9 +2546,9 @@ class Falyx: Args: callback (Callable[..., Any] | None): Optional function invoked after CLI parsing with the `ParseResult`. - always_start_menu (bool): - If True, launches the interactive menu after command execution - instead of exiting. + always_start_menu (bool): Whether to enter menu mode after a successful + command dispatch when the route itself does not already target help + or a namespace menu. Raises: FalyxError: @@ -1856,7 +2560,6 @@ class Falyx: - Most CLI execution paths terminate via `sys.exit()` - Interactive mode continues via `menu()` - Execution options are applied in a scoped "execution" namespace - - Preview mode (`?command`) bypasses execution and renders a preview Example: ``` diff --git a/falyx/hook_manager.py b/falyx/hook_manager.py index ca0a506..33f7328 100644 --- a/falyx/hook_manager.py +++ b/falyx/hook_manager.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Defines the `HookManager` and `HookType` used in the Falyx CLI framework to manage +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Defines the `HookManager` and `HookType` used in the Falyx CLI framework to manage execution lifecycle hooks around actions and commands. The hook system enables structured callbacks for important stages in a Falyx action's @@ -31,8 +30,7 @@ Hook = Union[ class HookType(Enum): - """ - Enum for supported hook lifecycle phases in Falyx. + """Enum for supported hook lifecycle phases in Falyx. HookType is used to classify lifecycle events that can be intercepted with user-defined callbacks. @@ -91,8 +89,7 @@ class HookType(Enum): class HookManager: - """ - Manages lifecycle hooks for a command or action. + """Manages lifecycle hooks for a command or action. `HookManager` tracks user-defined callbacks to be run at key points in a command's lifecycle: before execution, on success, on error, after completion, and during @@ -114,8 +111,7 @@ class HookManager: } def register(self, hook_type: HookType | str, hook: Hook): - """ - Register a new hook for a given lifecycle phase. + """Register a new hook for a given lifecycle phase. Args: hook_type (HookType | str): The hook category (e.g. "before", "on_success"). @@ -128,8 +124,7 @@ class HookManager: self._hooks[hook_type].append(hook) def clear(self, hook_type: HookType | None = None): - """ - Clear registered hooks for one or all hook types. + """Clear registered hooks for one or all hook types. Args: hook_type (HookType | None): If None, clears all hooks. @@ -141,8 +136,7 @@ class HookManager: self._hooks[ht] = [] async def trigger(self, hook_type: HookType, context: ExecutionContext): - """ - Invoke all hooks registered for a given lifecycle phase. + """Invoke all hooks registered for a given lifecycle phase. Args: hook_type (HookType): The lifecycle phase to trigger. diff --git a/falyx/hooks.py b/falyx/hooks.py index dd191d4..43db12d 100644 --- a/falyx/hooks.py +++ b/falyx/hooks.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Defines reusable lifecycle hooks for Falyx Actions and Commands. +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Defines reusable lifecycle hooks for Falyx Actions and Commands. This module includes: - `spinner_before_hook`: Automatically starts a spinner before an action runs. diff --git a/falyx/init.py b/falyx/init.py index ef5d7fb..ad8c48e 100644 --- a/falyx/init.py +++ b/falyx/init.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Project and global initializer for Falyx CLI environments. +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Project and global initializer for Falyx CLI environments. This module defines functions to bootstrap a new Falyx-based CLI project or create a global user-level configuration in `~/.config/falyx`. diff --git a/falyx/logger.py b/falyx/logger.py index c90f942..cd4af8c 100644 --- a/falyx/logger.py +++ b/falyx/logger.py @@ -1,4 +1,4 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed """Global logger instance for Falyx CLI applications.""" import logging diff --git a/falyx/menu.py b/falyx/menu.py index 134796c..2db157d 100644 --- a/falyx/menu.py +++ b/falyx/menu.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Defines `MenuOption` and `MenuOptionMap`, core components used to construct +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Defines `MenuOption` and `MenuOptionMap`, core components used to construct interactive menus within Falyx Actions such as `MenuAction` and `PromptMenuAction`. Each `MenuOption` represents a single actionable choice with a description, diff --git a/falyx/mode.py b/falyx/mode.py index 703703f..68ca9fc 100644 --- a/falyx/mode.py +++ b/falyx/mode.py @@ -1,9 +1,40 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -"""Defines `FalyxMode`, an enum representing the different modes of operation for Falyx.""" +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Runtime mode definitions for the Falyx CLI framework. + +This module defines `FalyxMode`, the enum used to represent the high-level +operating mode of a Falyx application during parsing, routing, rendering, and +execution. + +These modes describe the current intent of the runtime rather than any +particular command. They are used throughout Falyx to coordinate behavior such +as whether the application should show an interactive menu, execute a routed +command, render help output, preview a command, or surface an error state. + +`FalyxMode` is commonly stored in shared runtime state and passed through +invocation and parsing layers so UI rendering and execution flow remain +consistent across CLI and menu-driven entrypoints. +""" from enum import Enum class FalyxMode(Enum): + """Enumerates the high-level runtime modes used by Falyx. + + `FalyxMode` provides a small set of application-wide states that describe + how the current invocation should be handled. + + Attributes: + MENU: Interactive menu mode using Prompt Toolkit input and menu + rendering. + COMMAND: Direct command-execution mode for routed CLI or programmatic + invocation. + PREVIEW: Non-executing preview mode used to inspect a command before it + runs. + HELP: Help-rendering mode for namespace, command, or TLDR output. + ERROR: Error state used when invocation handling should surface a + failure condition. + """ + MENU = "menu" COMMAND = "command" PREVIEW = "preview" diff --git a/falyx/namespace.py b/falyx/namespace.py index c44d0a6..d817bed 100644 --- a/falyx/namespace.py +++ b/falyx/namespace.py @@ -1,3 +1,19 @@ +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Namespace entry model for nested Falyx applications. + +This module defines `FalyxNamespace`, the lightweight metadata container used to +register one `Falyx` instance inside another as a routed namespace entry. + +A `FalyxNamespace` describes how a nested application should appear and behave +from the perspective of its parent namespace. It stores the public-facing key, +description, aliases, styling, and visibility flags used for routing, +completion, help rendering, and menu display, while holding a reference to the +child `Falyx` runtime that should take over once the namespace is entered. + +This model is intentionally small and declarative. It does not implement +routing, rendering, or execution itself; those responsibilities remain with the +parent and child `Falyx` instances. +""" from __future__ import annotations from dataclasses import dataclass, field @@ -11,6 +27,26 @@ if TYPE_CHECKING: @dataclass class FalyxNamespace: + """Represents a nested `Falyx` application exposed as a namespace entry. + + `FalyxNamespace` is used by a parent `Falyx` instance to register and + describe a child `Falyx` runtime as a routable namespace. It provides the + metadata needed to expose that child namespace consistently across command + resolution, completion, help output, and menu rendering. + + Attributes: + key: Primary identifier used to enter the namespace. + description: User-facing description of the namespace. + namespace: Nested `Falyx` instance activated when this namespace is + selected. + aliases: Optional alternate names that may also resolve to the same + namespace. + help_text: Optional short help text used in listings or help output. + style: Rich style used when rendering the namespace key or aliases. + hidden: Whether the namespace should be omitted from visible menus and + help listings. + """ + key: str description: str namespace: Falyx diff --git a/falyx/options_manager.py b/falyx/options_manager.py index b2a5bd4..6728bf6 100644 --- a/falyx/options_manager.py +++ b/falyx/options_manager.py @@ -1,4 +1,4 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed """Manages global or scoped CLI options across namespaces for Falyx commands. The `OptionsManager` provides a centralized interface for retrieving, setting, toggling, diff --git a/falyx/parser/__init__.py b/falyx/parser/__init__.py index 0f35d88..7d19f46 100644 --- a/falyx/parser/__init__.py +++ b/falyx/parser/__init__.py @@ -1,7 +1,6 @@ -""" -Falyx CLI Framework +"""Falyx CLI Framework -Copyright (c) 2025 rtj.dev LLC. +Copyright (c) 2026 rtj.dev LLC. Licensed under the MIT License. See LICENSE file for details. """ diff --git a/falyx/parser/argument.py b/falyx/parser/argument.py index 8a99aed..ab0addd 100644 --- a/falyx/parser/argument.py +++ b/falyx/parser/argument.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Defines the `Argument` dataclass used by `CommandArgumentParser` to represent +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Defines the `Argument` dataclass used by `CommandArgumentParser` to represent individual command-line parameters in a structured, introspectable format. Each `Argument` instance describes one CLI input, including its flags, type, @@ -42,8 +41,7 @@ from falyx.parser.argument_action import ArgumentAction @dataclass class Argument: - """ - Represents a command-line argument. + """Represents a command-line argument. Attributes: flags (tuple[str, ...]): Short and long flags for the argument. diff --git a/falyx/parser/argument_action.py b/falyx/parser/argument_action.py index b4db0a2..8e72a67 100644 --- a/falyx/parser/argument_action.py +++ b/falyx/parser/argument_action.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Defines `ArgumentAction`, an enum used to standardize the behavior of CLI arguments +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Defines `ArgumentAction`, an enum used to standardize the behavior of CLI arguments defined within Falyx command configurations. Each member of this enum maps to a valid `argparse` like actions or Falyx-specific @@ -24,8 +23,7 @@ from enum import Enum class ArgumentAction(Enum): - """ - Defines the action to be taken when the argument is encountered. + """Defines the action to be taken when the argument is encountered. This enum mirrors the core behavior of Python's `argparse` actions, with a few Falyx-specific extensions. It is used when defining command-line arguments for diff --git a/falyx/parser/command_argument_parser.py b/falyx/parser/command_argument_parser.py index d73c953..94ce6e0 100644 --- a/falyx/parser/command_argument_parser.py +++ b/falyx/parser/command_argument_parser.py @@ -1,4 +1,4 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed """CommandArgumentParser implementation for the Falyx CLI framework. This module provides a structured, extensible argument parsing system designed @@ -273,8 +273,7 @@ class CommandArgumentParser: self._add_tldr() def add_tldr_examples(self, examples: list[TLDRInput]) -> None: - """ - Add TLDR examples to the parser. + """Add TLDR examples to the parser. Args: examples (list[TLDRInput]): List of TLDRExample instances or (usage, description) tuples. @@ -816,8 +815,7 @@ class CommandArgumentParser: self._register_argument(argument) def get_argument(self, dest: str) -> Argument | None: - """ - Return the Argument object for a given destination name. + """Return the Argument object for a given destination name. Args: dest (str): Destination key of the argument. @@ -830,8 +828,7 @@ class CommandArgumentParser: ) def to_definition_list(self) -> list[dict[str, Any]]: - """ - Convert argument metadata into a serializable list of dicts. + """Convert argument metadata into a serializable list of dicts. Returns: List of definitions for use in config introspection, documentation, or export. @@ -844,12 +841,13 @@ class CommandArgumentParser: "dest": arg.dest, "action": arg.action, "type": arg.type, + "default": arg.default, "choices": arg.choices, "required": arg.required, + "help": arg.help, "nargs": arg.nargs, "positional": arg.positional, - "default": arg.default, - "help": arg.help, + "suggestions": arg.suggestions, "group": arg.group, "mutex_group": arg.mutex_group, } @@ -1995,8 +1993,7 @@ class CommandArgumentParser: return sorted(set(suggestions)) def get_options_text(self) -> str: - """ - Render all defined arguments as a help-style string. + """Render all defined arguments as a help-style string. Returns: str: A visual description of argument flags and structure. diff --git a/falyx/parser/falyx_parser.py b/falyx/parser/falyx_parser.py index 146fddd..1b91e1b 100644 --- a/falyx/parser/falyx_parser.py +++ b/falyx/parser/falyx_parser.py @@ -1,8 +1,39 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Root parsing models and helpers for the Falyx CLI runtime. + +This module defines the minimal parsing layer used before namespace routing and +command-local argument parsing begin. + +It provides: + +- `RootOptions`, a lightweight container for session-scoped flags such as + verbose logging, help, TLDR, and prompt suppression. +- `FalyxParser`, a small root parser that consumes only leading global options + from argv and leaves the remaining tokens untouched for downstream routing. + +Unlike `CommandArgumentParser`, this module does not parse command-specific +arguments or attempt to resolve leaf-command inputs. Its responsibility is +intentionally narrow: identify root-level flags, determine the initial +application mode, and normalize the result into a `RootParseResult`. + +Parsing behavior is prefix-based. Root flags are consumed only from the start of +argv, and parsing stops at the first non-root token or an explicit `--` +separator. This allows the remaining arguments to be preserved exactly for later +namespace resolution and command-local parsing. + +Typical flow: + 1. Raw argv is passed to `FalyxParser.parse()`. + 2. Leading root/session flags are extracted into `RootOptions`. + 3. A `RootParseResult` is returned with either: + - `FalyxMode.HELP` when root help or TLDR was requested, or + - `FalyxMode.COMMAND` when normal routed execution should continue. + 4. Remaining argv is forwarded unchanged to the main Falyx routing layer. + +This module serves as the root-entry parsing boundary for Falyx applications. +""" from __future__ import annotations from dataclasses import dataclass -from typing import TYPE_CHECKING from falyx.mode import FalyxMode from falyx.parser.parse_result import RootParseResult @@ -10,6 +41,26 @@ from falyx.parser.parse_result import RootParseResult @dataclass(slots=True) class RootOptions: + """Container for root-level Falyx session flags. + + `RootOptions` stores the boolean flags recognized at the application + boundary before namespace routing and command-local parsing begin. These + values represent session-scoped behavior that applies to the overall Falyx + runtime rather than to any individual command. + + The model is intentionally small and lightweight. It is produced by + `FalyxParser._parse_root_options()` and then translated into a + `RootParseResult` that drives the initial execution mode and runtime + configuration. + + Attributes: + verbose: Whether verbose logging should be enabled for the session. + debug_hooks: Whether hook execution should be logged in detail. + never_prompt: Whether prompts should be suppressed for the session. + help: Whether root help output was requested. + tldr: Whether root TLDR output was requested. + """ + verbose: bool = False debug_hooks: bool = False never_prompt: bool = False @@ -18,13 +69,38 @@ class RootOptions: class FalyxParser: - """Root parser and command router for Falyx. + """Parse root-level Falyx CLI flags into an initial runtime result. + + `FalyxParser` is the narrow, top-level parser used before namespace routing + and command-local argument parsing begin. Its job is to inspect only the + leading session-scoped flags in argv, determine the initial application + mode, and return a normalized `RootParseResult`. Responsibilities: - - parse global/root flags - - resolve built-ins vs registered commands - - normalize CLI input into ParseResult - - delegate command-specific parsing to CommandArgumentParser + - Parse only root/session flags such as verbose logging, help, TLDR, + and prompt suppression. + - Stop parsing at the first non-root token or explicit `--` separator. + - Preserve the remaining argv exactly for downstream routing. + - Translate root help or TLDR requests into `FalyxMode.HELP`. + - Translate normal execution into `FalyxMode.COMMAND`. + + Design Notes: + - This parser does not resolve commands or namespaces. + - This parser does not parse command-specific arguments. + - Command-local parsing is delegated later to `CommandArgumentParser` + after Falyx routing has identified a leaf command. + - Root parsing is intentionally prefix-only so session flags apply at + the application boundary without mutating command-local argv. + + Typical Usage: + `Falyx.run()` or another top-level entrypoint passes raw argv into + `FalyxParser.parse()`, applies the returned session options, and then + forwards the untouched remaining argv into the routed Falyx execution + flow. + + Attributes: + ROOT_FLAG_ALIASES: Mapping of recognized root CLI flags to + `RootOptions` attribute names. """ ROOT_FLAG_ALIASES: dict[str, str] = { diff --git a/falyx/parser/group.py b/falyx/parser/group.py index eb8971b..81ee02a 100644 --- a/falyx/parser/group.py +++ b/falyx/parser/group.py @@ -1,4 +1,26 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Argument grouping models for the Falyx command argument parser. + +This module defines lightweight dataclasses used by +`CommandArgumentParser` to organize arguments into named help sections and +mutually exclusive sets. + +It provides: + +- `ArgumentGroup`, which represents a logical collection of related argument + destinations for grouped help rendering. +- `MutuallyExclusiveGroup`, which represents a set of argument destinations + where only one member may be selected, with optional group-level + requiredness. + +These models are metadata containers only. They do not perform parsing or +validation themselves. Instead, they are populated and enforced by +`CommandArgumentParser` during argument registration, parsing, and help +generation. + +This module exists to keep argument-group state explicit, structured, and easy +to introspect. +""" from __future__ import annotations from dataclasses import dataclass, field @@ -6,6 +28,22 @@ from dataclasses import dataclass, field @dataclass(slots=True) class ArgumentGroup: + """Represents a named group of related command argument destinations. + + `ArgumentGroup` is used by `CommandArgumentParser` to collect arguments that + belong together conceptually so they can be rendered under a shared section + in help output and tracked as a unit in parser metadata. + + This class stores only grouping metadata and does not implement any parsing + behavior on its own. + + Attributes: + name: User-facing name of the argument group. + description: Optional descriptive text for the group, typically used in + help rendering. + dests: Destination names of arguments assigned to this group. + """ + name: str description: str = "" dests: list[str] = field(default_factory=list) @@ -13,6 +51,25 @@ class ArgumentGroup: @dataclass(slots=True) class MutuallyExclusiveGroup: + """Represents a mutually exclusive set of argument destinations. + + `MutuallyExclusiveGroup` is used by `CommandArgumentParser` to model groups + of arguments where only one member may be provided at a time. It can also + mark the group as required, meaning that exactly one of the grouped + arguments must be present. + + This class stores group metadata only. Validation and enforcement are + performed by the parser. + + Attributes: + name: User-facing name of the mutually exclusive group. + required: Whether at least one argument in the group must be supplied. + description: Optional descriptive text for the group, typically used in + help rendering. + dests: Destination names of arguments assigned to this mutually + exclusive group. + """ + name: str required: bool = False description: str = "" diff --git a/falyx/parser/parse_result.py b/falyx/parser/parse_result.py index e84bb69..820ed14 100644 --- a/falyx/parser/parse_result.py +++ b/falyx/parser/parse_result.py @@ -1,4 +1,21 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Root parse result model for the Falyx CLI runtime. + +This module defines `RootParseResult`, the normalized output produced by the +root-level Falyx parsing stage. + +`RootParseResult` captures the session-scoped state derived from the initial +CLI parse before namespace routing or command-local argument parsing begins. It +records the selected top-level mode, the original argv, root option flags, and +any remaining argv that should be forwarded into the routed execution layer. + +This model is typically produced by `FalyxParser.parse()` and then consumed by +higher-level Falyx runtime entrypoints such as `Falyx.run()` to configure +logging, prompt behavior, help rendering, and routed command dispatch. + +The dataclass is intentionally lightweight and focused on root parsing only. It +does not perform parsing, validation, or execution itself. +""" from dataclasses import dataclass, field from falyx.mode import FalyxMode @@ -6,6 +23,28 @@ from falyx.mode import FalyxMode @dataclass(slots=True) class RootParseResult: + """Represents the normalized result of root-level Falyx argument parsing. + + `RootParseResult` stores the outcome of the initial CLI parse that occurs at + the application boundary. It separates session-level runtime settings from + the remaining argv that should continue into namespace routing and + command-local parsing. + + This model is used to communicate root parsing decisions cleanly to the + rest of the Falyx runtime, including whether the application should enter + help mode or continue with normal command execution. + + Attributes: + mode: Top-level runtime mode selected from the root parse. + raw_argv: Original argv passed into the root parser. + verbose: Whether verbose logging should be enabled for the session. + debug_hooks: Whether hook execution should be logged in detail. + never_prompt: Whether prompts should be suppressed for the session. + remaining_argv: Unconsumed argv that should be forwarded to routed + command resolution. + tldr_requested: Whether root TLDR output was requested. + """ + mode: FalyxMode raw_argv: list[str] = field(default_factory=list) verbose: bool = False diff --git a/falyx/parser/parser_types.py b/falyx/parser/parser_types.py index 93ba10a..6b275c2 100644 --- a/falyx/parser/parser_types.py +++ b/falyx/parser/parser_types.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Type utilities and argument state models for Falyx's custom CLI argument parser. +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Type utilities and argument state models for Falyx's custom CLI argument parser. This module provides specialized helpers and data structures used by the `CommandArgumentParser` to handle non-standard parsing behavior. diff --git a/falyx/parser/signature.py b/falyx/parser/signature.py index da1f28a..74f1a47 100644 --- a/falyx/parser/signature.py +++ b/falyx/parser/signature.py @@ -1,6 +1,5 @@ # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Provides utilities for introspecting Python callables and extracting argument +"""Provides utilities for introspecting Python callables and extracting argument metadata compatible with Falyx's `CommandArgumentParser`. This module is primarily used to auto-generate command argument definitions from @@ -20,8 +19,7 @@ def infer_args_from_func( func: Callable[[Any], Any] | None, arg_metadata: dict[str, str | dict[str, Any]] | None = None, ) -> list[dict[str, Any]]: - """ - Infer CLI-style argument definitions from a function signature. + """Infer CLI-style argument definitions from a function signature. This utility inspects the parameters of a function and returns a list of dictionaries, each of which can be passed to `CommandArgumentParser.add_argument()`. diff --git a/falyx/parser/utils.py b/falyx/parser/utils.py index 71396e6..bccb48d 100644 --- a/falyx/parser/utils.py +++ b/falyx/parser/utils.py @@ -1,6 +1,5 @@ # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Contains value coercion and signature comparison utilities for Falyx argument parsing. +"""Contains value coercion and signature comparison utilities for Falyx argument parsing. This module provides type coercion functions for converting string input into expected Python types, including `Enum`, `bool`, `datetime`, and `Literal`. It also supports @@ -25,8 +24,7 @@ from falyx.parser.signature import infer_args_from_func def coerce_bool(value: str) -> bool: - """ - Convert a string to a boolean. + """Convert a string to a boolean. Accepts various truthy and falsy representations such as 'true', 'yes', '0', 'off', etc. @@ -47,8 +45,7 @@ def coerce_bool(value: str) -> bool: def coerce_enum(value: Any, enum_type: EnumMeta) -> Any: - """ - Convert a raw value or string to an Enum instance. + """Convert a raw value or string to an Enum instance. Tries to resolve by name, value, or coerced base type. @@ -81,8 +78,7 @@ def coerce_enum(value: Any, enum_type: EnumMeta) -> Any: def coerce_value(value: str, target_type: type) -> Any: - """ - Attempt to convert a string to the given target type. + """Attempt to convert a string to the given target type. Handles complex typing constructs such as Union, Literal, Enum, and datetime. @@ -133,8 +129,7 @@ def same_argument_definitions( actions: list[Any], arg_metadata: dict[str, str | dict[str, Any]] | None = None, ) -> list[dict[str, Any]] | None: - """ - Determine if multiple callables resolve to the same argument definitions. + """Determine if multiple callables resolve to the same argument definitions. This is used to infer whether actions in an ActionGroup or ProcessPool can share a unified argument parser. diff --git a/falyx/prompt_utils.py b/falyx/prompt_utils.py index 4b895d3..0c1e97e 100644 --- a/falyx/prompt_utils.py +++ b/falyx/prompt_utils.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Utilities for user interaction prompts in the Falyx CLI framework. +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Utilities for user interaction prompts in the Falyx CLI framework. Provides asynchronous confirmation dialogs and helper logic to determine whether a user should be prompted based on command-line options. @@ -38,6 +37,15 @@ def should_prompt_user( flags that may override the need for confirmation, such as `--never-prompt`, `--force-confirm`, or `--skip-confirm`. The `override_namespace` is checked first for any explicit overrides, followed by the main `namespace` for defaults. + + Args: + confirm (bool): The initial confirmation flag (e.g., from a command argument). + options (OptionsManager): The options manager to check for override flags. + namespace (str): The primary namespace to check for options (default: "default"). + override_namespace (str): The secondary namespace for overrides (default: "execution"). + + Returns: + bool: True if the user should be prompted, False if confirmation can be bypassed. """ never_prompt = options.get("never_prompt", None, override_namespace) if never_prompt is None: @@ -74,9 +82,16 @@ async def confirm_async( def rich_text_to_prompt_text(text: Text | str | StyleAndTextTuples) -> StyleAndTextTuples: - """ - Convert a Rich Text object to a list of (style, text) tuples - compatible with prompt_toolkit. + """Convert a Rich Text object to prompt_toolkit formatted text. + + This function takes a Rich `Text` object (or a string or already formatted text) + and converts it in to a list of (style, text) tuples compatible with prompt_toolkit. + + Args: + text (Text | str | StyleAndTextTuples): The input text to convert. + + Returns: + StyleAndTextTuples: A list of (style, text) tuples for prompt_toolkit. """ if isinstance(text, list): if all(isinstance(pair, tuple) and len(pair) == 2 for pair in text): diff --git a/falyx/protocols.py b/falyx/protocols.py index a91253b..80b2b93 100644 --- a/falyx/protocols.py +++ b/falyx/protocols.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Defines structural protocols for advanced Falyx features. +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Defines structural protocols for advanced Falyx features. These runtime-checkable `Protocol` classes specify the expected interfaces for: - Factories that asynchronously return actions diff --git a/falyx/retry.py b/falyx/retry.py index e87cc7e..19eff29 100644 --- a/falyx/retry.py +++ b/falyx/retry.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Implements retry logic for Falyx Actions using configurable retry policies. +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Implements retry logic for Falyx Actions using configurable retry policies. This module defines: - `RetryPolicy`: A configurable model controlling retry behavior (delay, backoff, jitter). @@ -30,8 +29,7 @@ from falyx.logger import logger class RetryPolicy(BaseModel): - """ - Defines a retry strategy for Falyx `Action` objects. + """Defines a retry strategy for Falyx `Action` objects. This model controls whether an action should be retried on failure, and how: - `max_retries`: Maximum number of retry attempts. @@ -60,23 +58,16 @@ class RetryPolicy(BaseModel): enabled: bool = False def enable_policy(self) -> None: - """ - Enable the retry policy. - :return: None - """ + """Enable the retry policy.""" self.enabled = True def is_active(self) -> bool: - """ - Check if the retry policy is active. - :return: True if the retry policy is active, False otherwise. - """ + """Check if the retry policy is active.""" return self.max_retries > 0 and self.enabled class RetryHandler: - """ - Executes retry logic for Falyx actions using a provided `RetryPolicy`. + """Executes retry logic for Falyx actions using a provided `RetryPolicy`. This class is intended to be registered as an `on_error` hook. It will re-attempt the failed `Action`'s `action` method using the args/kwargs from diff --git a/falyx/retry_utils.py b/falyx/retry_utils.py index 14eefbc..51ec27c 100644 --- a/falyx/retry_utils.py +++ b/falyx/retry_utils.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Utilities for enabling retry behavior across Falyx actions. +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Utilities for enabling retry behavior across Falyx actions. This module provides a helper to recursively apply a `RetryPolicy` to an action and its nested children (e.g. `ChainedAction`, `ActionGroup`), and register the appropriate diff --git a/falyx/routing.py b/falyx/routing.py index d9757aa..ec98cfa 100644 --- a/falyx/routing.py +++ b/falyx/routing.py @@ -1,3 +1,24 @@ +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Routing result models for the Falyx CLI framework. + +This module defines the core types used to describe the outcome of namespace +routing in a `Falyx` application. + +It provides: + +- `RouteKind`, an enum describing the kind of routed target that was reached, + such as a leaf command, namespace help, namespace TLDR, namespace menu, or + an unknown entry. +- `RouteResult`, a structured value object that captures the resolved routing + state, including the active namespace, invocation context, optional leaf + command, remaining argv for command-local parsing, and any suggestions for + unresolved input. + +These types sit at the boundary between routing and execution. They do not +perform routing themselves. Instead, they are produced by Falyx routing logic +and then consumed by help rendering, completion, validation, preview, and +command dispatch flows. +""" from __future__ import annotations from dataclasses import dataclass, field @@ -13,6 +34,21 @@ if TYPE_CHECKING: class RouteKind(Enum): + """Enumerates the possible outcomes of Falyx namespace routing. + + `RouteKind` identifies what the routing layer resolved the current input + to, allowing downstream code to decide whether it should execute a command, + render namespace help, show TLDR output, display a namespace menu, or + surface an unknown-entry message. + + Attributes: + COMMAND: Routing reached a leaf command that may be parsed and executed. + NAMESPACE_MENU: Routing stopped at a namespace menu target. + NAMESPACE_HELP: Routing resolved to namespace help output. + NAMESPACE_TLDR: Routing resolved to namespace TLDR output. + UNKNOWN: Routing failed to resolve the requested entry. + """ + COMMAND = "command" NAMESPACE_MENU = "namespace_menu" NAMESPACE_HELP = "namespace_help" @@ -22,6 +58,30 @@ class RouteKind(Enum): @dataclass(slots=True) class RouteResult: + """Represents the resolved output of a Falyx routing operation. + + `RouteResult` captures the full state needed after namespace resolution + completes and before command execution or help rendering begins. It records + what kind of target was reached, where routing ended, the invocation path + used to reach it, and any leaf-command metadata needed for downstream + parsing. + + This model is used by Falyx execution, help, preview, completion, and + validation flows to make routing decisions explicit and easy to inspect. + + Attributes: + kind: The type of routed result that was resolved. + namespace: The `Falyx` namespace where routing ended. + context: Invocation context describing the routed path and current mode. + command: Resolved leaf command, if routing ended at a command. + namespace_entry: Resolved namespace entry, if the route corresponds to a + specific nested namespace. + leaf_argv: Remaining argv that should be delegated to the resolved + command's local parser. + suggestions: Suggested entry names for unresolved input. + is_preview: Whether the routed invocation is in preview mode. + """ + kind: RouteKind namespace: "Falyx" context: InvocationContext diff --git a/falyx/selection.py b/falyx/selection.py index 49a39d3..cc0c3d0 100644 --- a/falyx/selection.py +++ b/falyx/selection.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Provides interactive selection utilities for Falyx CLI actions. +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Provides interactive selection utilities for Falyx CLI actions. This module defines `SelectionOption` objects, selection maps, and rich-powered rendering functions to build interactive selection prompts using `prompt_toolkit`. @@ -46,9 +45,7 @@ class SelectionOption: class SelectionOptionMap(CaseInsensitiveDict): - """ - Manages selection options including validation and reserved key protection. - """ + """Manages selection options including validation and reserved key protection.""" RESERVED_KEYS: set[str] = set() @@ -118,6 +115,7 @@ def render_table_base( highlight: bool = True, column_names: Sequence[str] | None = None, ) -> Table: + """Render the base table for selection prompts.""" table = Table( title=title, caption=caption, @@ -288,6 +286,7 @@ async def prompt_for_index( allow_duplicates: bool = False, cancel_key: str = "", ) -> int | list[int]: + """Prompt the user to select an index from a table of options. Return the selected index.""" prompt_session = prompt_session or PromptSession() if show_table: diff --git a/falyx/signals.py b/falyx/signals.py index 191c61d..b113485 100644 --- a/falyx/signals.py +++ b/falyx/signals.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Defines flow control signals used internally by the Falyx CLI framework. +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Defines flow control signals used internally by the Falyx CLI framework. These signals are raised to interrupt or redirect CLI execution flow (e.g., returning to a menu, quitting, or displaying help) without diff --git a/falyx/spinner_manager.py b/falyx/spinner_manager.py index d80465f..fa01b1c 100644 --- a/falyx/spinner_manager.py +++ b/falyx/spinner_manager.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Centralized spinner rendering for Falyx CLI. +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Centralized spinner rendering for Falyx CLI. This module provides the `SpinnerManager` class, which manages a collection of Rich spinners that can be displayed concurrently during long-running tasks. @@ -55,8 +54,7 @@ from falyx.themes import OneColors class SpinnerData: - """ - Holds the configuration and Rich spinner object for a single task. + """Holds the configuration and Rich spinner object for a single task. This class is a lightweight container for spinner metadata, storing the message text, spinner type, style, and speed. It also initializes the @@ -92,8 +90,7 @@ class SpinnerData: class SpinnerManager: - """ - Manages multiple Rich spinners and handles their terminal rendering. + """Manages multiple Rich spinners and handles their terminal rendering. SpinnerManager maintains a registry of active spinners and a single Rich `Live` display loop to render them. When the first spinner is added, diff --git a/falyx/tagged_table.py b/falyx/tagged_table.py index fa2e094..62b0dfc 100644 --- a/falyx/tagged_table.py +++ b/falyx/tagged_table.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Generates a Rich table view of Falyx commands grouped by their tags. +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Generates a Rich table view of Falyx commands grouped by their tags. This module defines a utility function for rendering a custom CLI command table that organizes commands into groups based on their first tag. It is @@ -37,7 +36,7 @@ def build_tagged_table(flx: Falyx) -> Table: table.add_row("") # Add bottom row - for row in flx.get_bottom_row(): + for row in flx._get_bottom_row(): table.add_row(row) return table diff --git a/falyx/themes/__init__.py b/falyx/themes/__init__.py index 651f175..2073e95 100644 --- a/falyx/themes/__init__.py +++ b/falyx/themes/__init__.py @@ -1,7 +1,6 @@ -""" -Falyx CLI Framework +"""Falyx CLI Framework -Copyright (c) 2025 rtj.dev LLC. +Copyright (c) 2026 rtj.dev LLC. Licensed under the MIT License. See LICENSE file for details. """ diff --git a/falyx/themes/colors.py b/falyx/themes/colors.py index 7b5d0de..d4afaf0 100644 --- a/falyx/themes/colors.py +++ b/falyx/themes/colors.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -A Python module that integrates the Nord color palette with the Rich library. +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""A Python module that integrates the Nord color palette with the Rich library. It defines a metaclass-based NordColors class allowing dynamic attribute lookups (e.g., NORD12bu -> "#D08770 bold underline") and provides a comprehensive Nord-based Theme that customizes Rich's default styles. @@ -26,8 +25,7 @@ from rich.theme import Theme class ColorsMeta(type): - """ - A metaclass that catches attribute lookups like `NORD12buidrs` or `ORANGE_b` and returns + """A metaclass that catches attribute lookups like `NORD12buidrs` or `ORANGE_b` and returns a string combining the base color + bold/italic/underline/dim/reverse/strike flags. The color values are required to be uppercase with optional underscores and digits, @@ -152,8 +150,7 @@ class OneColors(metaclass=ColorsMeta): class NordColors(metaclass=ColorsMeta): - """ - Defines the Nord color palette as class attributes. + """Defines the Nord color palette as class attributes. Each color is labeled by its canonical Nord name (NORD0-NORD15) and also has useful aliases grouped by theme: @@ -212,8 +209,7 @@ class NordColors(metaclass=ColorsMeta): @classmethod def as_dict(cls): - """ - Returns a dictionary mapping every NORD* attribute + """Returns a dictionary mapping every NORD* attribute (e.g. 'NORD0') to its hex code. """ return { @@ -224,8 +220,7 @@ class NordColors(metaclass=ColorsMeta): @classmethod def aliases(cls): - """ - Returns a dictionary of *all* other aliases + """Returns a dictionary of *all* other aliases (Polar Night, Snow Storm, Frost, Aurora). """ skip_prefixes = ("NORD", "__") @@ -462,9 +457,7 @@ NORD_THEME_STYLES: dict[str, Style] = { def get_nord_theme() -> Theme: - """ - Returns a Rich Theme for the Nord color palette. - """ + """Returns a Rich Theme for the Nord color palette.""" return Theme(NORD_THEME_STYLES) diff --git a/falyx/utils.py b/falyx/utils.py index 9766e61..43a25f1 100644 --- a/falyx/utils.py +++ b/falyx/utils.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -General-purpose utilities and helpers for the Falyx CLI framework. +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""General-purpose utilities and helpers for the Falyx CLI framework. This module includes asynchronous wrappers, logging setup, formatting utilities, and small type-safe enhancements such as `CaseInsensitiveDict` and coroutine enforcement. @@ -130,8 +129,7 @@ def setup_logging( file_log_level: int = logging.DEBUG, console_log_level: int = logging.WARNING, ): - """ - Configure logging for Falyx with support for both CLI-friendly and structured + """Configure logging for Falyx with support for both CLI-friendly and structured JSON output. This function sets up separate logging handlers for console and file output, diff --git a/falyx/validators.py b/falyx/validators.py index 7440045..281a02b 100644 --- a/falyx/validators.py +++ b/falyx/validators.py @@ -1,6 +1,5 @@ -# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -""" -Input validators for use with Prompt Toolkit and interactive Falyx CLI workflows. +# Falyx CLI Framework — (c) 2026 rtj.dev LLC — MIT Licensed +"""Input validators for use with Prompt Toolkit and interactive Falyx CLI workflows. This module defines reusable `Validator` instances and subclasses that enforce valid user input during prompts—especially for selection actions, confirmations, and @@ -152,6 +151,8 @@ def word_validator(word: str) -> Validator: class MultiIndexValidator(Validator): + """Validator for multiple index selections (e.g. '1,2,3').""" + def __init__( self, minimum: int, @@ -202,6 +203,8 @@ class MultiIndexValidator(Validator): class MultiKeyValidator(Validator): + """Validator for multiple key selections (e.g. 'A,B,C').""" + def __init__( self, keys: Sequence[str] | KeysView[str],