diff --git a/examples/confirm_example.py b/examples/confirm_example.py index 1c5bee6..a491c3b 100644 --- a/examples/confirm_example.py +++ b/examples/confirm_example.py @@ -71,7 +71,7 @@ async def build_chain(dogs: list[Dog]) -> ChainedAction: ConfirmAction( name="test_confirm", message="Do you want to process the dogs?", - confirm_type="yes_no", + confirm_type="yes_no_cancel", return_last_result=True, inject_into="dogs", ), @@ -88,6 +88,7 @@ async def build_chain(dogs: list[Dog]) -> ChainedAction: factory = ActionFactory( name="Dog Post Factory", factory=build_chain, + preview_kwargs={"dogs": ["Buddy", "Max"]}, ) diff --git a/falyx/action/confirm_action.py b/falyx/action/confirm_action.py index e8ee2c8..83ce112 100644 --- a/falyx/action/confirm_action.py +++ b/falyx/action/confirm_action.py @@ -56,13 +56,14 @@ class ConfirmAction(BaseAction): prompt_session (PromptSession | None): The session to use for input. confirm (bool): Whether to prompt the user for confirmation. word (str): The word to type for TYPE_WORD confirmation. - return_last_result (bool): Whether to return the last result of the action. + return_last_result (bool): Whether to return the last result of the action + instead of a boolean. """ def __init__( self, name: str, - message: str = "Continue", + message: str = "Confirm?", confirm_type: ConfirmType | str = ConfirmType.YES_NO, prompt_session: PromptSession | None = None, confirm: bool = True, @@ -114,16 +115,19 @@ class ConfirmAction(BaseAction): session=self.prompt_session, ) case ConfirmType.YES_NO_CANCEL: + error_message = "Enter 'Y', 'y' to confirm, 'N', 'n' to decline, or 'C', 'c' to abort." answer = await self.prompt_session.prompt_async( - f"❓ {self.message} ([Y]es, [N]o, or [C]ancel to abort): ", - validator=words_validator(["Y", "N", "C"]), + f"❓ {self.message} [Y]es, [N]o, or [C]ancel to abort > ", + validator=words_validator( + ["Y", "N", "C"], error_message=error_message + ), ) if answer.upper() == "C": raise CancelSignal(f"Action '{self.name}' was cancelled by the user.") return answer.upper() == "Y" case ConfirmType.TYPE_WORD: answer = await self.prompt_session.prompt_async( - f"❓ {self.message} (type '{self.word}' to confirm or N/n): ", + f"❓ {self.message} [{self.word}] to confirm or [N/n] > ", validator=word_validator(self.word), ) return answer.upper().strip() != "N" @@ -138,9 +142,10 @@ class ConfirmAction(BaseAction): raise CancelSignal(f"Action '{self.name}' was cancelled by the user.") return answer case ConfirmType.OK_CANCEL: + error_message = "Enter 'O', 'o' to confirm or 'C', 'c' to abort." answer = await self.prompt_session.prompt_async( - f"❓ {self.message} ([O]k to continue, [C]ancel to abort): ", - validator=words_validator(["O", "C"]), + f"❓ {self.message} [O]k to confirm, [C]ancel to abort > ", + validator=words_validator(["O", "C"], error_message=error_message), ) if answer.upper() == "C": raise CancelSignal(f"Action '{self.name}' was cancelled by the user.") @@ -213,5 +218,5 @@ class ConfirmAction(BaseAction): def __str__(self) -> str: return ( f"ConfirmAction(name={self.name}, message={self.message}, " - f"confirm_type={self.confirm_type})" + f"confirm_type={self.confirm_type}, return_last_result={self.return_last_result})" ) diff --git a/falyx/action/signal_action.py b/falyx/action/signal_action.py index c07d291..7b22115 100644 --- a/falyx/action/signal_action.py +++ b/falyx/action/signal_action.py @@ -14,7 +14,7 @@ class SignalAction(Action): Useful for exiting a menu, going back, or halting execution gracefully. """ - def __init__(self, name: str, signal: Exception): + def __init__(self, name: str, signal: FlowSignal): self.signal = signal super().__init__(name, action=self.raise_signal) diff --git a/falyx/bottom_bar.py b/falyx/bottom_bar.py index 5f1c424..c1e7cb6 100644 --- a/falyx/bottom_bar.py +++ b/falyx/bottom_bar.py @@ -5,6 +5,7 @@ from typing import Any, Callable from prompt_toolkit.formatted_text import HTML, merge_formatted_text from prompt_toolkit.key_binding import KeyBindings +from rich.console import Console from falyx.console import console from falyx.options_manager import OptionsManager diff --git a/falyx/falyx.py b/falyx/falyx.py index 6fcd9cc..fe39635 100644 --- a/falyx/falyx.py +++ b/falyx/falyx.py @@ -1,7 +1,5 @@ # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed -"""falyx.py - -Main class for constructing and running Falyx CLI menus. +"""Main class for constructing and running Falyx CLI menus. Falyx provides a structured, customizable interactive menu system for running commands, actions, and workflows. It supports: diff --git a/falyx/parser/parsers.py b/falyx/parser/parsers.py index 532d43f..44fd58d 100644 --- a/falyx/parser/parsers.py +++ b/falyx/parser/parsers.py @@ -56,6 +56,39 @@ def get_root_parser( allow_abbrev: bool = True, exit_on_error: bool = True, ) -> ArgumentParser: + """ + Construct the root-level ArgumentParser for the Falyx CLI. + + This parser handles global arguments shared across subcommands and can serve + as the base parser for the Falyx CLI or standalone applications. It includes + options for verbosity, debug logging, and version output. + + Args: + prog (str | None): Name of the program (e.g., 'falyx'). + usage (str | None): Optional custom usage string. + description (str | None): Description shown in the CLI help. + epilog (str | None): Message displayed at the end of help output. + parents (Sequence[ArgumentParser] | None): Optional parent parsers. + prefix_chars (str): Characters to denote optional arguments (default: "-"). + fromfile_prefix_chars (str | None): Prefix to indicate argument file input. + argument_default (Any): Global default value for arguments. + conflict_handler (str): Strategy to resolve conflicting argument names. + add_help (bool): Whether to include help (`-h/--help`) in this parser. + allow_abbrev (bool): Allow abbreviated long options. + exit_on_error (bool): Exit immediately on error or raise an exception. + + Returns: + ArgumentParser: The root parser with global options attached. + + Notes: + ``` + Includes the following arguments: + --never-prompt : Run in non-interactive mode. + -v / --verbose : Enable debug logging. + --debug-hooks : Enable hook lifecycle debug logs. + --version : Print the Falyx version. + ``` + """ parser = ArgumentParser( prog=prog, usage=usage, @@ -92,7 +125,30 @@ def get_subparsers( title: str = "Falyx Commands", description: str | None = "Available commands for the Falyx CLI.", ) -> _SubParsersAction: - """Create and return a subparsers action for the given parser.""" + """ + Create and return a subparsers object for registering Falyx CLI subcommands. + + This function adds a `subparsers` block to the given root parser, enabling + structured subcommands such as `run`, `run-all`, `preview`, etc. + + Args: + parser (ArgumentParser): The root parser to attach the subparsers to. + title (str): Title used in help output to group subcommands. + description (str | None): Optional text describing the group of subcommands. + + Returns: + _SubParsersAction: The subparsers object that can be used to add new CLI subcommands. + + Raises: + TypeError: If `parser` is not an instance of `ArgumentParser`. + + Example: + ```python + >>> parser = get_root_parser() + >>> subparsers = get_subparsers(parser, title="Available Commands") + >>> subparsers.add_parser("run", help="Run a Falyx command") + ``` + """ if not isinstance(parser, ArgumentParser): raise TypeError("parser must be an instance of ArgumentParser") subparsers = parser.add_subparsers( @@ -122,7 +178,54 @@ def get_arg_parsers( root_parser: ArgumentParser | None = None, subparsers: _SubParsersAction | None = None, ) -> FalyxParsers: - """Returns the argument parser for the CLI.""" + """ + Create and return the full suite of argument parsers used by the Falyx CLI. + + This function builds the root parser and all subcommand parsers used for structured + CLI workflows in Falyx. It supports standard subcommands including `run`, `run-all`, + `preview`, `list`, and `version`, and integrates with registered `Command` objects + to populate dynamic help and usage documentation. + + Args: + prog (str | None): Program name to display in help and usage messages. + usage (str | None): Optional usage message to override the default. + description (str | None): Description for the CLI root parser. + epilog (str | None): Epilog message shown after the help text. + parents (Sequence[ArgumentParser] | None): Optional parent parsers. + prefix_chars (str): Characters that prefix optional arguments. + fromfile_prefix_chars (str | None): Prefix character for reading args from file. + argument_default (Any): Default value for arguments if not specified. + conflict_handler (str): Strategy for resolving conflicting arguments. + add_help (bool): Whether to add the `-h/--help` option to the root parser. + allow_abbrev (bool): Whether to allow abbreviated long options. + exit_on_error (bool): Whether the parser exits on error or raises. + commands (dict[str, Command] | None): Optional dictionary of registered commands + to populate help and subcommand descriptions dynamically. + root_parser (ArgumentParser | None): Custom root parser to use instead of building one. + subparsers (_SubParsersAction | None): Optional existing subparser object to extend. + + Returns: + FalyxParsers: A structured container of all parsers, including `run`, `run-all`, + `preview`, `list`, `version`, and the root parser. + + Raises: + TypeError: If `root_parser` is not an instance of ArgumentParser or + `subparsers` is not an instance of _SubParsersAction. + + Example: + ```python + >>> parsers = get_arg_parsers(commands=my_command_dict) + >>> args = parsers.root.parse_args() + ``` + + Notes: + - This function integrates dynamic command usage and descriptions if the + `commands` argument is provided. + - The `run` parser supports additional options for retry logic and confirmation + prompts. + - The `run-all` parser executes all commands matching a tag. + - Use `falyx run ?[COMMAND]` from the CLI to preview a command. + """ if epilog is None: epilog = f"Tip: Use '{prog} run ?[COMMAND]' to preview any command from the CLI." if root_parser is None: diff --git a/falyx/validators.py b/falyx/validators.py index 19b8ceb..f55e0a3 100644 --- a/falyx/validators.py +++ b/falyx/validators.py @@ -44,10 +44,12 @@ def yes_no_validator() -> Validator: return False return True - return Validator.from_callable(validate, error_message="Enter 'Y' or 'n'.") + return Validator.from_callable(validate, error_message="Enter 'Y', 'y' or 'N', 'n'.") -def words_validator(keys: Sequence[str] | KeysView[str]) -> Validator: +def words_validator( + keys: Sequence[str] | KeysView[str], error_message: str | None = None +) -> Validator: """Validator for specific word inputs.""" def validate(text: str) -> bool: @@ -55,9 +57,10 @@ def words_validator(keys: Sequence[str] | KeysView[str]) -> Validator: return False return True - return Validator.from_callable( - validate, error_message=f"Invalid input. Choices: {{{', '.join(keys)}}}." - ) + if error_message is None: + error_message = f"Invalid input. Choices: {{{', '.join(keys)}}}." + + return Validator.from_callable(validate, error_message=error_message) def word_validator(word: str) -> Validator: @@ -68,7 +71,7 @@ def word_validator(word: str) -> Validator: return True return text.upper().strip() == word.upper() - return Validator.from_callable(validate, error_message=f"Enter '{word}' or 'N'.") + return Validator.from_callable(validate, error_message=f"Enter '{word}' or 'N', 'n'.") class MultiIndexValidator(Validator): diff --git a/falyx/version.py b/falyx/version.py index 79d1955..12867ba 100644 --- a/falyx/version.py +++ b/falyx/version.py @@ -1 +1 @@ -__version__ = "0.1.58" +__version__ = "0.1.59" diff --git a/pyproject.toml b/pyproject.toml index 6c0c32b..c0f7c0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "falyx" -version = "0.1.58" +version = "0.1.59" description = "Reliable and introspectable async CLI action framework." authors = ["Roland Thomas Jr "] license = "MIT"