Fix TLDR causing Command not to run, Add placeholder prompt menu to Falyx

This commit is contained in:
2025-07-23 19:42:44 -04:00
parent 825ff60f08
commit 489d730755
7 changed files with 87 additions and 47 deletions

View File

@ -77,7 +77,12 @@ def default_config(parser: CommandArgumentParser) -> None:
) )
flx = Falyx("Argument Examples", program="argument_examples.py") flx = Falyx(
"Argument Examples",
program="argument_examples.py",
hide_menu_table=True,
show_placeholder_menu=True,
)
flx.add_command( flx.add_command(
key="T", key="T",
@ -88,7 +93,7 @@ flx.add_command(
name="test_args", name="test_args",
action=test_args, action=test_args,
), ),
style="bold blue", style="bold #B3EBF2",
argument_config=default_config, argument_config=default_config,
) )

View File

@ -19,6 +19,8 @@ flx = Falyx(
description="This example demonstrates how to select files using Falyx.", description="This example demonstrates how to select files using Falyx.",
version="1.0.0", version="1.0.0",
program="file_select.py", program="file_select.py",
hide_menu_table=True,
show_placeholder_menu=True,
) )
flx.add_command( flx.add_command(

View File

@ -33,7 +33,7 @@ from prompt_toolkit import PromptSession
from prompt_toolkit.formatted_text import StyleAndTextTuples from prompt_toolkit.formatted_text import StyleAndTextTuples
from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.patch_stdout import patch_stdout from prompt_toolkit.patch_stdout import patch_stdout
from prompt_toolkit.validation import ValidationError, Validator from prompt_toolkit.validation import ValidationError
from rich import box from rich import box
from rich.console import Console from rich.console import Console
from rich.markdown import Markdown from rich.markdown import Markdown
@ -66,41 +66,10 @@ from falyx.retry import RetryPolicy
from falyx.signals import BackSignal, CancelSignal, HelpSignal, QuitSignal from falyx.signals import BackSignal, CancelSignal, HelpSignal, QuitSignal
from falyx.themes import OneColors from falyx.themes import OneColors
from falyx.utils import CaseInsensitiveDict, _noop, chunks, ensure_async from falyx.utils import CaseInsensitiveDict, _noop, chunks, ensure_async
from falyx.validators import CommandValidator
from falyx.version import __version__ from falyx.version import __version__
class CommandValidator(Validator):
"""Validator to check if the input is a valid command."""
def __init__(self, falyx: Falyx, error_message: str) -> None:
super().__init__()
self.falyx = falyx
self.error_message = error_message
def validate(self, document) -> None:
if not document.text:
raise ValidationError(
message=self.error_message,
cursor_position=len(document.text),
)
async def validate_async(self, document) -> None:
text = document.text
if not text:
raise ValidationError(
message=self.error_message,
cursor_position=len(text),
)
is_preview, choice, _, __ = await self.falyx.get_command(text, from_validate=True)
if is_preview:
return None
if not choice:
raise ValidationError(
message=self.error_message,
cursor_position=len(text),
)
class Falyx: class Falyx:
""" """
Main menu controller for Falyx CLI applications. Main menu controller for Falyx CLI applications.
@ -176,6 +145,7 @@ class Falyx:
render_menu: Callable[[Falyx], None] | None = None, render_menu: Callable[[Falyx], None] | None = None,
custom_table: Callable[[Falyx], Table] | Table | None = None, custom_table: Callable[[Falyx], Table] | Table | None = None,
hide_menu_table: bool = False, hide_menu_table: bool = False,
show_placeholder_menu: bool = False,
) -> None: ) -> None:
"""Initializes the Falyx object.""" """Initializes the Falyx object."""
self.title: str | Markdown = title self.title: str | Markdown = title
@ -208,6 +178,7 @@ class Falyx:
self.render_menu: Callable[[Falyx], None] | None = render_menu self.render_menu: Callable[[Falyx], None] | None = render_menu
self.custom_table: Callable[[Falyx], Table] | Table | None = custom_table self.custom_table: Callable[[Falyx], Table] | Table | None = custom_table
self._hide_menu_table: bool = hide_menu_table self._hide_menu_table: bool = hide_menu_table
self.show_placeholder_menu: bool = show_placeholder_menu
self.validate_options(cli_args, options) self.validate_options(cli_args, options)
self._prompt_session: PromptSession | None = None self._prompt_session: PromptSession | None = None
self.options.set("mode", FalyxMode.MENU) self.options.set("mode", FalyxMode.MENU)
@ -492,6 +463,7 @@ class Falyx:
def prompt_session(self) -> PromptSession: def prompt_session(self) -> PromptSession:
"""Returns the prompt session for the menu.""" """Returns the prompt session for the menu."""
if self._prompt_session is None: if self._prompt_session is None:
placeholder = self.build_placeholder_menu()
self._prompt_session = PromptSession( self._prompt_session = PromptSession(
message=self.prompt, message=self.prompt,
multiline=False, multiline=False,
@ -502,6 +474,7 @@ class Falyx:
validate_while_typing=True, validate_while_typing=True,
interrupt_exception=QuitSignal, interrupt_exception=QuitSignal,
eof_exception=QuitSignal, eof_exception=QuitSignal,
placeholder=placeholder if self.show_placeholder_menu else None,
) )
return self._prompt_session return self._prompt_session
@ -724,16 +697,16 @@ class Falyx:
if self.help_command: if self.help_command:
bottom_row.append( bottom_row.append(
f"[{self.help_command.key}] [{self.help_command.style}]" f"[{self.help_command.key}] [{self.help_command.style}]"
f"{self.help_command.description}" f"{self.help_command.description}[/]"
) )
if self.history_command: if self.history_command:
bottom_row.append( bottom_row.append(
f"[{self.history_command.key}] [{self.history_command.style}]" f"[{self.history_command.key}] [{self.history_command.style}]"
f"{self.history_command.description}" f"{self.history_command.description}[/]"
) )
bottom_row.append( bottom_row.append(
f"[{self.exit_command.key}] [{self.exit_command.style}]" f"[{self.exit_command.key}] [{self.exit_command.style}]"
f"{self.exit_command.description}" f"{self.exit_command.description}[/]"
) )
return bottom_row return bottom_row
@ -754,6 +727,22 @@ class Falyx:
table.add_row(*row) table.add_row(*row)
return table return table
def build_placeholder_menu(self) -> StyleAndTextTuples:
"""
Builds a menu placeholder for prompt_menu mode.
"""
visible_commands = [item for item in self.commands.items() if not item[1].hidden]
if not visible_commands:
return [("", "")]
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():
placeholder.append(command_str)
return rich_text_to_prompt_text(" ".join(placeholder))
@property @property
def table(self) -> Table: def table(self) -> Table:
"""Creates or returns a custom table to display the menu commands.""" """Creates or returns a custom table to display the menu commands."""

View File

@ -101,6 +101,8 @@ class CommandArgumentParser:
- Render Help using Rich library. - Render Help using Rich library.
""" """
RESERVED_DESTS = frozenset(("help", "tldr"))
def __init__( def __init__(
self, self,
command_key: str = "", command_key: str = "",
@ -138,13 +140,13 @@ class CommandArgumentParser:
def _add_help(self): def _add_help(self):
"""Add help argument to the parser.""" """Add help argument to the parser."""
self.add_argument( help = Argument(
"-h", flags=("--help", "-h"),
"--help",
action=ArgumentAction.HELP, action=ArgumentAction.HELP,
help="Show this help message.", help="Show this help message.",
dest="help", dest="help",
) )
self._register_argument(help)
def add_tldr_examples(self, examples: list[tuple[str, str]]) -> None: def add_tldr_examples(self, examples: list[tuple[str, str]]) -> None:
""" """
@ -163,12 +165,13 @@ class CommandArgumentParser:
self._tldr_examples.append(TLDRExample(usage=usage, description=description)) self._tldr_examples.append(TLDRExample(usage=usage, description=description))
if "tldr" not in self._dest_set: if "tldr" not in self._dest_set:
self.add_argument( tldr = Argument(
"--tldr", ("--tldr",),
action=ArgumentAction.TLDR, action=ArgumentAction.TLDR,
help="Show quick usage examples and exit.", help="Show quick usage examples and exit.",
dest="tldr", dest="tldr",
) )
self._register_argument(tldr)
def _is_positional(self, flags: tuple[str, ...]) -> bool: def _is_positional(self, flags: tuple[str, ...]) -> bool:
"""Check if the flags are positional.""" """Check if the flags are positional."""
@ -529,6 +532,10 @@ class CommandArgumentParser:
"Merging multiple arguments into the same dest (e.g. positional + flagged) " "Merging multiple arguments into the same dest (e.g. positional + flagged) "
"is not supported. Define a unique 'dest' for each argument." "is not supported. Define a unique 'dest' for each argument."
) )
if dest in self.RESERVED_DESTS:
raise CommandArgumentError(
f"Destination '{dest}' is reserved and cannot be used."
)
action = self._validate_action(action, positional) action = self._validate_action(action, positional)
resolver = self._validate_resolver(action, resolver) resolver = self._validate_resolver(action, resolver)
@ -1050,6 +1057,7 @@ class CommandArgumentParser:
) )
result.pop("help", None) result.pop("help", None)
result.pop("tldr", None)
return result return result
async def parse_args_split( async def parse_args_split(
@ -1067,7 +1075,7 @@ class CommandArgumentParser:
args_list = [] args_list = []
kwargs_dict = {} kwargs_dict = {}
for arg in self._arguments: for arg in self._arguments:
if arg.dest == "help": if arg.dest in ("help", "tldr"):
continue continue
if arg.positional: if arg.positional:
args_list.append(parsed[arg.dest]) args_list.append(parsed[arg.dest])

View File

@ -7,6 +7,7 @@ user input during prompts—especially for selection actions, confirmations, and
argument parsing. argument parsing.
Included Validators: Included Validators:
- CommandValidator: Validates if the input matches a known command.
- int_range_validator: Enforces numeric input within a range. - int_range_validator: Enforces numeric input within a range.
- key_validator: Ensures the entered value matches a valid selection key. - key_validator: Ensures the entered value matches a valid selection key.
- yes_no_validator: Restricts input to 'Y' or 'N'. - yes_no_validator: Restricts input to 'Y' or 'N'.
@ -17,10 +18,45 @@ Included Validators:
These validators integrate directly into `PromptSession.prompt_async()` to These validators integrate directly into `PromptSession.prompt_async()` to
enforce correctness and provide helpful error messages. enforce correctness and provide helpful error messages.
""" """
from typing import KeysView, Sequence from typing import TYPE_CHECKING, KeysView, Sequence
from prompt_toolkit.validation import ValidationError, Validator from prompt_toolkit.validation import ValidationError, Validator
if TYPE_CHECKING:
from falyx.falyx import Falyx
class CommandValidator(Validator):
"""Validator to check if the input is a valid command."""
def __init__(self, falyx: "Falyx", error_message: str) -> None:
super().__init__()
self.falyx = falyx
self.error_message = error_message
def validate(self, document) -> None:
if not document.text:
raise ValidationError(
message=self.error_message,
cursor_position=len(document.text),
)
async def validate_async(self, document) -> None:
text = document.text
if not text:
raise ValidationError(
message=self.error_message,
cursor_position=len(text),
)
is_preview, choice, _, __ = await self.falyx.get_command(text, from_validate=True)
if is_preview:
return None
if not choice:
raise ValidationError(
message=self.error_message,
cursor_position=len(text),
)
def int_range_validator(minimum: int, maximum: int) -> Validator: def int_range_validator(minimum: int, maximum: int) -> Validator:
"""Validator for integer ranges.""" """Validator for integer ranges."""

View File

@ -1 +1 @@
__version__ = "0.1.70" __version__ = "0.1.71"

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "falyx" name = "falyx"
version = "0.1.70" version = "0.1.71"
description = "Reliable and introspectable async CLI action framework." description = "Reliable and introspectable async CLI action framework."
authors = ["Roland Thomas Jr <roland@rtj.dev>"] authors = ["Roland Thomas Jr <roland@rtj.dev>"]
license = "MIT" license = "MIT"