Fix TLDR causing Command not to run, Add placeholder prompt menu to Falyx
This commit is contained in:
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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."""
|
||||||
|
@ -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])
|
||||||
|
@ -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."""
|
||||||
|
@ -1 +1 @@
|
|||||||
__version__ = "0.1.70"
|
__version__ = "0.1.71"
|
||||||
|
@ -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"
|
||||||
|
Reference in New Issue
Block a user