feat(help & spinners): improve help rendering, async spinner handling, and pipeline demo
- Refactored `Command.help_signature` to return `(usage, description, tags)` instead of a Rich `Padding`/`Panel`. - Replaced `show_help()` with `render_help()` in `Command` and `Falyx`. - Updated Falyx help rendering to use Rich `Panel`/`Padding` consistently for cleaner UI. - Swapped `print()` calls for `console.print()` for styled output. - Added hooks to `ProcessAction` to announce analysis start/finish. - Added spinners to test and deploy steps; simplified retry setup. - Converted `remove()` to `async def remove()` for consistency. - Added async lock to prevent concurrent Live loop start/stop races. - Added debug logging when starting/stopping the Live loop. - Updated `spinner_teardown_hook` to `await sm.remove(...)` to align with async `remove()`. - Removed `rich.panel`/`rich.padding` from `Command` since panels are now built in `Falyx` help rendering. - Bumped `rich` dependency to `^14.0`. - Bumped version to 0.1.78. This commit polishes help display, demo UX, and spinner lifecycle safety—making spinners thread/async safe and help output more structured and readable.
This commit is contained in:
@ -4,44 +4,44 @@ import time
|
|||||||
|
|
||||||
from falyx import Falyx
|
from falyx import Falyx
|
||||||
from falyx.action import Action, ActionGroup, ChainedAction, ProcessAction
|
from falyx.action import Action, ActionGroup, ChainedAction, ProcessAction
|
||||||
from falyx.retry import RetryHandler, RetryPolicy
|
from falyx.console import console
|
||||||
|
|
||||||
|
|
||||||
# Step 1: Fast I/O-bound setup (standard Action)
|
# Step 1: Fast I/O-bound setup (standard Action)
|
||||||
async def checkout_code():
|
async def checkout_code():
|
||||||
print("📥 Checking out code...")
|
console.print("🔄 Checking out code...")
|
||||||
await asyncio.sleep(0.5)
|
await asyncio.sleep(0.5)
|
||||||
|
console.print("📦 Code checked out successfully.")
|
||||||
|
|
||||||
|
|
||||||
# Step 2: CPU-bound task (ProcessAction)
|
# Step 2: CPU-bound task (ProcessAction)
|
||||||
def run_static_analysis():
|
def run_static_analysis():
|
||||||
print("🧠 Running static analysis (CPU-bound)...")
|
|
||||||
total = 0
|
total = 0
|
||||||
for i in range(10_000_000):
|
for i in range(10_000_000):
|
||||||
total += i % 3
|
total += i % 3
|
||||||
time.sleep(5)
|
time.sleep(2)
|
||||||
return total
|
return total
|
||||||
|
|
||||||
|
|
||||||
# Step 3: Simulated flaky test with retry
|
# Step 3: Simulated flaky test with retry
|
||||||
async def flaky_tests():
|
async def flaky_tests():
|
||||||
|
console.print("🧪 Running tests...")
|
||||||
await asyncio.sleep(0.3)
|
await asyncio.sleep(0.3)
|
||||||
if random.random() < 0.3:
|
if random.random() < 0.3:
|
||||||
raise RuntimeError("❌ Random test failure!")
|
raise RuntimeError("❌ Random test failure!")
|
||||||
print("🧪 Tests passed.")
|
console.print("🧪 Tests passed.")
|
||||||
return "ok"
|
return "ok"
|
||||||
|
|
||||||
|
|
||||||
# Step 4: Multiple deploy targets (parallel ActionGroup)
|
# Step 4: Multiple deploy targets (parallel ActionGroup)
|
||||||
async def deploy_to(target: str):
|
async def deploy_to(target: str):
|
||||||
print(f"🚀 Deploying to {target}...")
|
console.print(f"🚀 Deploying to {target}...")
|
||||||
await asyncio.sleep(random.randint(2, 6))
|
await asyncio.sleep(random.randint(2, 6))
|
||||||
|
console.print(f"✅ Deployment to {target} complete.")
|
||||||
return f"{target} complete"
|
return f"{target} complete"
|
||||||
|
|
||||||
|
|
||||||
def build_pipeline():
|
def build_pipeline():
|
||||||
retry_handler = RetryHandler(RetryPolicy(max_retries=3, delay=0.5))
|
|
||||||
|
|
||||||
# Base actions
|
# Base actions
|
||||||
checkout = Action("Checkout", checkout_code)
|
checkout = Action("Checkout", checkout_code)
|
||||||
analysis = ProcessAction(
|
analysis = ProcessAction(
|
||||||
@ -50,8 +50,17 @@ def build_pipeline():
|
|||||||
spinner=True,
|
spinner=True,
|
||||||
spinner_message="Analyzing code...",
|
spinner_message="Analyzing code...",
|
||||||
)
|
)
|
||||||
tests = Action("Run Tests", flaky_tests)
|
analysis.hooks.register(
|
||||||
tests.hooks.register("on_error", retry_handler.retry_on_error)
|
"before", lambda ctx: console.print("🧠 Running static analysis (CPU-bound)...")
|
||||||
|
)
|
||||||
|
analysis.hooks.register("after", lambda ctx: console.print("🧠 Analysis complete!"))
|
||||||
|
tests = Action(
|
||||||
|
"Run Tests",
|
||||||
|
flaky_tests,
|
||||||
|
retry=True,
|
||||||
|
spinner=True,
|
||||||
|
spinner_message="Running tests...",
|
||||||
|
)
|
||||||
|
|
||||||
# Parallel deploys
|
# Parallel deploys
|
||||||
deploy_group = ActionGroup(
|
deploy_group = ActionGroup(
|
||||||
@ -91,14 +100,18 @@ pipeline = build_pipeline()
|
|||||||
# Run the pipeline
|
# Run the pipeline
|
||||||
async def main():
|
async def main():
|
||||||
|
|
||||||
flx = Falyx()
|
flx = Falyx(
|
||||||
|
hide_menu_table=True, program="pipeline_demo.py", show_placeholder_menu=True
|
||||||
|
)
|
||||||
flx.add_command(
|
flx.add_command(
|
||||||
"A",
|
"P",
|
||||||
"Action Thing",
|
"Run Pipeline",
|
||||||
pipeline,
|
pipeline,
|
||||||
spinner=True,
|
spinner=True,
|
||||||
spinner_type="line",
|
spinner_type="line",
|
||||||
spinner_message="Running pipeline...",
|
spinner_message="Running pipeline...",
|
||||||
|
tags=["pipeline", "demo"],
|
||||||
|
help_text="Run the full CI/CD pipeline demo.",
|
||||||
)
|
)
|
||||||
|
|
||||||
await flx.run()
|
await flx.run()
|
||||||
|
@ -22,8 +22,6 @@ from typing import Any, Awaitable, Callable
|
|||||||
|
|
||||||
from prompt_toolkit.formatted_text import FormattedText
|
from prompt_toolkit.formatted_text import FormattedText
|
||||||
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator
|
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator
|
||||||
from rich.padding import Padding
|
|
||||||
from rich.panel import Panel
|
|
||||||
from rich.tree import Tree
|
from rich.tree import Tree
|
||||||
|
|
||||||
from falyx.action.action import Action
|
from falyx.action.action import Action
|
||||||
@ -342,7 +340,7 @@ class Command(BaseModel):
|
|||||||
return f" {command_keys_text:<20} {options_text} "
|
return f" {command_keys_text:<20} {options_text} "
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def help_signature(self) -> tuple[Padding, str]:
|
def help_signature(self) -> tuple[str, str, str]:
|
||||||
"""Generate a help signature for the command."""
|
"""Generate a help signature for the command."""
|
||||||
is_cli_mode = self.options_manager.get("mode") in {
|
is_cli_mode = self.options_manager.get("mode") in {
|
||||||
FalyxMode.RUN,
|
FalyxMode.RUN,
|
||||||
@ -353,30 +351,21 @@ class Command(BaseModel):
|
|||||||
program = f"{self.program} run " if is_cli_mode else ""
|
program = f"{self.program} run " if is_cli_mode else ""
|
||||||
|
|
||||||
if self.arg_parser and not self.simple_help_signature:
|
if self.arg_parser and not self.simple_help_signature:
|
||||||
usage = Padding(
|
usage = f"[{self.style}]{program}[/]{self.arg_parser.get_usage()}"
|
||||||
Panel(
|
description = f"[dim]{self.help_text or self.description}[/dim]"
|
||||||
f"[{self.style}]{program}[/]{self.arg_parser.get_usage()}",
|
|
||||||
expand=False,
|
|
||||||
),
|
|
||||||
(0, 2),
|
|
||||||
)
|
|
||||||
description = [f" [dim]{self.help_text or self.description}[/dim]"]
|
|
||||||
if self.tags:
|
if self.tags:
|
||||||
description.append(f" [dim]Tags: {', '.join(self.tags)}[/dim]")
|
tags = f"[dim]Tags: {', '.join(self.tags)}[/dim]"
|
||||||
return usage, "\n".join(description)
|
else:
|
||||||
|
tags = ""
|
||||||
|
return usage, description, tags
|
||||||
|
|
||||||
command_keys = " | ".join(
|
command_keys = " | ".join(
|
||||||
[f"[{self.style}]{self.key}[/{self.style}]"]
|
[f"[{self.style}]{self.key}[/{self.style}]"]
|
||||||
+ [f"[{self.style}]{alias}[/{self.style}]" for alias in self.aliases]
|
+ [f"[{self.style}]{alias}[/{self.style}]" for alias in self.aliases]
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
Padding(
|
f"[{self.style}]{program}[/]{command_keys}",
|
||||||
Panel(
|
f"[dim]{self.description}[/dim]",
|
||||||
f"[{self.style}]{program}[/]{command_keys} {self.description}",
|
|
||||||
expand=False,
|
|
||||||
),
|
|
||||||
(0, 2),
|
|
||||||
),
|
|
||||||
"",
|
"",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -384,7 +373,7 @@ class Command(BaseModel):
|
|||||||
if self._context:
|
if self._context:
|
||||||
self._context.log_summary()
|
self._context.log_summary()
|
||||||
|
|
||||||
def show_help(self) -> bool:
|
def render_help(self) -> bool:
|
||||||
"""Display the help message for the command."""
|
"""Display the help message for the command."""
|
||||||
if callable(self.custom_help):
|
if callable(self.custom_help):
|
||||||
output = self.custom_help()
|
output = self.custom_help()
|
||||||
|
108
falyx/falyx.py
108
falyx/falyx.py
@ -41,6 +41,8 @@ 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
|
||||||
|
from rich.padding import Padding
|
||||||
|
from rich.panel import Panel
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
|
|
||||||
from falyx.action.action import Action
|
from falyx.action.action import Action
|
||||||
@ -347,71 +349,97 @@ class Falyx:
|
|||||||
def get_tip(self) -> str:
|
def get_tip(self) -> str:
|
||||||
program = f"{self.program} run " if self.is_cli_mode else ""
|
program = f"{self.program} run " if self.is_cli_mode else ""
|
||||||
tips = [
|
tips = [
|
||||||
f"Tip: Use '{program}?[COMMAND]' to preview a command.",
|
f"Use '{program}?[COMMAND]' to preview a command.",
|
||||||
"Tip: Every command supports aliases—try abbreviating the name!",
|
"Every command supports aliases—try abbreviating the name!",
|
||||||
f"Tip: Use '{program}H' to reopen this help menu anytime.",
|
f"Use '{program}H' to reopen this help menu anytime.",
|
||||||
f"Tip: '{program}[COMMAND] --help' prints a detailed help message.",
|
f"'{program}[COMMAND] --help' prints a detailed help message.",
|
||||||
"Tip: [bold]CLI[/] and [bold]Menu[/] mode—commands run the same way in both.",
|
"[bold]CLI[/] and [bold]Menu[/] mode—commands run the same way in both.",
|
||||||
f"Tip: Use '{self.program} --never-prompt' to disable all prompts for the [bold italic]entire menu session[/].",
|
f"'{self.program} --never-prompt' to disable all prompts for the [bold italic]entire menu session[/].",
|
||||||
f"Tip: Use '{self.program} --verbose' to enable debug logging for a menu session.",
|
f"Use '{self.program} --verbose' to enable debug logging for a menu session.",
|
||||||
f"Tip: '{self.program} --debug-hooks' will trace every before/after hook in action.",
|
f"'{self.program} --debug-hooks' will trace every before/after hook in action.",
|
||||||
f"Tip: Run commands directly from the CLI: '{self.program} run [COMMAND] [OPTIONS]'.",
|
f"Run commands directly from the CLI: '{self.program} run [COMMAND] [OPTIONS]'.",
|
||||||
]
|
]
|
||||||
if self.is_cli_mode:
|
if self.is_cli_mode:
|
||||||
tips.extend(
|
tips.extend(
|
||||||
[
|
[
|
||||||
f"Tip: Use '{self.program} run ?' to list all commands at any time.",
|
f"Use '{self.program} run ?' to list all commands at any time.",
|
||||||
f"Tip: Use '{self.program} --never-prompt run [COMMAND] [OPTIONS]' to disable all prompts for [bold italic]just this command[/].",
|
f"Use '{self.program} --never-prompt run [COMMAND] [OPTIONS]' to disable all prompts for [bold italic]just this command[/].",
|
||||||
f"Tip: Use '{self.program} run --skip-confirm [COMMAND] [OPTIONS]' to skip confirmations.",
|
f"Use '{self.program} run --skip-confirm [COMMAND] [OPTIONS]' to skip confirmations.",
|
||||||
f"Tip: Use '{self.program} run --summary [COMMAND] [OPTIONS]' to print a post-run summary.",
|
f"Use '{self.program} run --summary [COMMAND] [OPTIONS]' to print a post-run summary.",
|
||||||
f"Tip: Use '{self.program} --verbose run [COMMAND] [OPTIONS]' to enable debug logging for any run.",
|
f"Use '{self.program} --verbose run [COMMAND] [OPTIONS]' to enable debug logging for any run.",
|
||||||
"Tip: Use '--skip-confirm' for automation scripts where no prompts are wanted.",
|
"Use '--skip-confirm' for automation scripts where no prompts are wanted.",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
tips.extend(
|
tips.extend(
|
||||||
[
|
[
|
||||||
"Tip: Use '[?]' alone to list all commands at any time.",
|
"Use '[?]' alone to list all commands at any time.",
|
||||||
"Tip: '[CTRL+KEY]' toggles are available in menu mode for quick switches.",
|
"'[CTRL+KEY]' toggles are available in menu mode for quick switches.",
|
||||||
"Tip: '[Y]' opens the command history viewer.",
|
"'[Y]' opens the command history viewer.",
|
||||||
"Tip: Use '[X]' in menu mode to exit.",
|
"Use '[X]' in menu mode to exit.",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
return choice(tips)
|
return choice(tips)
|
||||||
|
|
||||||
async def _show_help(self, tag: str = "") -> None:
|
async def _render_help(self, tag: str = "") -> None:
|
||||||
self.console.print("[bold]help:[/bold]")
|
|
||||||
if tag:
|
if tag:
|
||||||
tag_lower = tag.lower()
|
tag_lower = tag.lower()
|
||||||
|
self.console.print(f"[bold]{tag_lower}:[/bold]")
|
||||||
commands = [
|
commands = [
|
||||||
command
|
command
|
||||||
for command in self.commands.values()
|
for command in self.commands.values()
|
||||||
if any(tag_lower == tag.lower() for tag in command.tags)
|
if any(tag_lower == tag.lower() for tag in command.tags)
|
||||||
]
|
]
|
||||||
|
if not commands:
|
||||||
|
self.console.print(f"'{tag}'... Nothing to show here")
|
||||||
|
return
|
||||||
for command in commands:
|
for command in commands:
|
||||||
usage, description = command.help_signature
|
usage, description, _ = command.help_signature
|
||||||
self.console.print(usage)
|
self.console.print(usage)
|
||||||
if description:
|
if description:
|
||||||
self.console.print(description)
|
self.console.print(description)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self.console.print("[bold]help:[/bold]")
|
||||||
for command in self.commands.values():
|
for command in self.commands.values():
|
||||||
usage, description = command.help_signature
|
usage, description, tag = command.help_signature
|
||||||
self.console.print(usage)
|
self.console.print(
|
||||||
if description:
|
Padding(
|
||||||
self.console.print(description)
|
Panel(
|
||||||
|
usage,
|
||||||
|
expand=False,
|
||||||
|
title=description,
|
||||||
|
title_align="left",
|
||||||
|
subtitle=tag,
|
||||||
|
),
|
||||||
|
(0, 2),
|
||||||
|
)
|
||||||
|
)
|
||||||
if self.help_command:
|
if self.help_command:
|
||||||
usage, description = self.help_command.help_signature
|
usage, description, _ = self.help_command.help_signature
|
||||||
self.console.print(usage)
|
self.console.print(
|
||||||
self.console.print(description)
|
Padding(
|
||||||
|
Panel(usage, expand=False, title=description, title_align="left"),
|
||||||
|
(0, 2),
|
||||||
|
)
|
||||||
|
)
|
||||||
if not self.is_cli_mode:
|
if not self.is_cli_mode:
|
||||||
if self.history_command:
|
if self.history_command:
|
||||||
usage, description = self.history_command.help_signature
|
usage, description, _ = self.history_command.help_signature
|
||||||
self.console.print(usage)
|
self.console.print(
|
||||||
self.console.print(description)
|
Padding(
|
||||||
usage, _ = self.exit_command.help_signature
|
Panel(usage, expand=False, title=description, title_align="left"),
|
||||||
self.console.print(usage)
|
(0, 2),
|
||||||
self.console.print(self.get_tip())
|
)
|
||||||
|
)
|
||||||
|
usage, description, _ = self.exit_command.help_signature
|
||||||
|
self.console.print(
|
||||||
|
Padding(
|
||||||
|
Panel(usage, expand=False, title=description, title_align="left"),
|
||||||
|
(0, 2),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.console.print(f"[bold]tip:[/bold] {self.get_tip()}")
|
||||||
|
|
||||||
def _get_help_command(self) -> Command:
|
def _get_help_command(self) -> Command:
|
||||||
"""Returns the help command for the menu."""
|
"""Returns the help command for the menu."""
|
||||||
@ -434,7 +462,7 @@ class Falyx:
|
|||||||
aliases=["?", "HELP", "LIST"],
|
aliases=["?", "HELP", "LIST"],
|
||||||
description="Help",
|
description="Help",
|
||||||
help_text="Show this help menu",
|
help_text="Show this help menu",
|
||||||
action=Action("Help", self._show_help),
|
action=Action("Help", self._render_help),
|
||||||
style=OneColors.LIGHT_YELLOW,
|
style=OneColors.LIGHT_YELLOW,
|
||||||
arg_parser=parser,
|
arg_parser=parser,
|
||||||
ignore_in_history=True,
|
ignore_in_history=True,
|
||||||
@ -884,7 +912,7 @@ class Falyx:
|
|||||||
args, kwargs = await run_command.parse_args(input_args, from_validate)
|
args, kwargs = await run_command.parse_args(input_args, from_validate)
|
||||||
except (CommandArgumentError, Exception) as error:
|
except (CommandArgumentError, Exception) as error:
|
||||||
if not from_validate:
|
if not from_validate:
|
||||||
run_command.show_help()
|
run_command.render_help()
|
||||||
self.console.print(
|
self.console.print(
|
||||||
f"[{OneColors.DARK_RED}]❌ [{run_command.key}]: {error}"
|
f"[{OneColors.DARK_RED}]❌ [{run_command.key}]: {error}"
|
||||||
)
|
)
|
||||||
@ -955,7 +983,7 @@ class Falyx:
|
|||||||
async def process_command(self) -> bool:
|
async def process_command(self) -> bool:
|
||||||
"""Processes the action of the selected command."""
|
"""Processes the action of the selected command."""
|
||||||
app = get_app()
|
app = get_app()
|
||||||
await asyncio.sleep(0.01)
|
await asyncio.sleep(0.1)
|
||||||
app.invalidate()
|
app.invalidate()
|
||||||
with patch_stdout(raw=True):
|
with patch_stdout(raw=True):
|
||||||
choice = await self.prompt_session.prompt_async()
|
choice = await self.prompt_session.prompt_async()
|
||||||
@ -1171,7 +1199,7 @@ class Falyx:
|
|||||||
self.register_all_with_debug_hooks()
|
self.register_all_with_debug_hooks()
|
||||||
|
|
||||||
if self.cli_args.command == "list":
|
if self.cli_args.command == "list":
|
||||||
await self._show_help(tag=self.cli_args.tag)
|
await self._render_help(tag=self.cli_args.tag)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
if self.cli_args.command == "version" or self.cli_args.version:
|
if self.cli_args.command == "version" or self.cli_args.version:
|
||||||
@ -1210,7 +1238,7 @@ class Falyx:
|
|||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
except CommandArgumentError as error:
|
except CommandArgumentError as error:
|
||||||
self.console.print(f"[{OneColors.DARK_RED}]❌ ['{command.key}'] {error}")
|
self.console.print(f"[{OneColors.DARK_RED}]❌ ['{command.key}'] {error}")
|
||||||
command.show_help()
|
command.render_help()
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
try:
|
try:
|
||||||
await self.run_key(self.cli_args.name, args=args, kwargs=kwargs)
|
await self.run_key(self.cli_args.name, args=args, kwargs=kwargs)
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
Defines reusable lifecycle hooks for Falyx Actions and Commands.
|
Defines reusable lifecycle hooks for Falyx Actions and Commands.
|
||||||
|
|
||||||
This module includes:
|
This module includes:
|
||||||
|
- `spinner_before_hook`: Automatically starts a spinner before an action runs.
|
||||||
|
- `spinner_teardown_hook`: Stops and clears the spinner after the action completes.
|
||||||
- `ResultReporter`: A success hook that displays a formatted result with duration.
|
- `ResultReporter`: A success hook that displays a formatted result with duration.
|
||||||
- `CircuitBreaker`: A failure-aware hook manager that prevents repeated execution
|
- `CircuitBreaker`: A failure-aware hook manager that prevents repeated execution
|
||||||
after a configurable number of failures.
|
after a configurable number of failures.
|
||||||
@ -11,6 +13,7 @@ These hooks can be registered on `HookManager` instances via lifecycle stages
|
|||||||
(`before`, `on_error`, `after`, etc.) to enhance resiliency and observability.
|
(`before`, `on_error`, `after`, etc.) to enhance resiliency and observability.
|
||||||
|
|
||||||
Intended for use with:
|
Intended for use with:
|
||||||
|
- Actions that require user feedback during long-running operations.
|
||||||
- Retryable or unstable actions
|
- Retryable or unstable actions
|
||||||
- Interactive CLI feedback
|
- Interactive CLI feedback
|
||||||
- Safety checks prior to execution
|
- Safety checks prior to execution
|
||||||
@ -62,7 +65,7 @@ async def spinner_teardown_hook(context: ExecutionContext):
|
|||||||
else:
|
else:
|
||||||
cmd_name = cmd.key
|
cmd_name = cmd.key
|
||||||
sm = context.action.options_manager.spinners
|
sm = context.action.options_manager.spinners
|
||||||
sm.remove(cmd_name)
|
await sm.remove(cmd_name)
|
||||||
|
|
||||||
|
|
||||||
class ResultReporter:
|
class ResultReporter:
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
||||||
"""
|
"""
|
||||||
Defines `MenuOption` and `MenuOptionMap`, core components used to construct
|
Defines `MenuOption` and `MenuOptionMap`, core components used to construct
|
||||||
interactive menus within Falyx Actions such as `MenuAction` and `PromptMenuAction`.
|
interactive menus within Falyx Actions such as `MenuAction` and `PromptMenuAction`.
|
||||||
|
@ -24,6 +24,7 @@ Public Interface:
|
|||||||
- `parse_args(...)`: Parse CLI-style argument list into a `dict[str, Any]`.
|
- `parse_args(...)`: Parse CLI-style argument list into a `dict[str, Any]`.
|
||||||
- `parse_args_split(...)`: Return `(*args, **kwargs)` for Action invocation.
|
- `parse_args_split(...)`: Return `(*args, **kwargs)` for Action invocation.
|
||||||
- `render_help()`: Render a rich-styled help panel.
|
- `render_help()`: Render a rich-styled help panel.
|
||||||
|
- `render_tldr()`: Render quick usage examples.
|
||||||
- `suggest_next(...)`: Return suggested flags or values for completion.
|
- `suggest_next(...)`: Return suggested flags or values for completion.
|
||||||
|
|
||||||
Example Usage:
|
Example Usage:
|
||||||
@ -1379,8 +1380,12 @@ class CommandArgumentParser:
|
|||||||
usage = f"{command} {example.usage.strip()}"
|
usage = f"{command} {example.usage.strip()}"
|
||||||
description = example.description.strip()
|
description = example.description.strip()
|
||||||
block = f"[bold]{usage}[/bold]"
|
block = f"[bold]{usage}[/bold]"
|
||||||
self.console.print(Padding(Panel(block, expand=False), (0, 2)))
|
self.console.print(
|
||||||
self.console.print(f" {description}", style="dim")
|
Padding(
|
||||||
|
Panel(block, expand=False, title=description, title_align="left"),
|
||||||
|
(0, 2),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def __eq__(self, other: object) -> bool:
|
def __eq__(self, other: object) -> bool:
|
||||||
if not isinstance(other, CommandArgumentParser):
|
if not isinstance(other, CommandArgumentParser):
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
||||||
"""
|
"""
|
||||||
Centralized spinner rendering for Falyx CLI.
|
Centralized spinner rendering for Falyx CLI.
|
||||||
|
|
||||||
@ -43,13 +44,13 @@ Design Notes:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import threading
|
|
||||||
|
|
||||||
from rich.console import Group
|
from rich.console import Group
|
||||||
from rich.live import Live
|
from rich.live import Live
|
||||||
from rich.spinner import Spinner
|
from rich.spinner import Spinner
|
||||||
|
|
||||||
from falyx.console import console
|
from falyx.console import console
|
||||||
|
from falyx.logger import logger
|
||||||
from falyx.themes import OneColors
|
from falyx.themes import OneColors
|
||||||
|
|
||||||
|
|
||||||
@ -133,7 +134,7 @@ class SpinnerManager:
|
|||||||
self._task: asyncio.Task | None = None
|
self._task: asyncio.Task | None = None
|
||||||
self._running: bool = False
|
self._running: bool = False
|
||||||
|
|
||||||
self._start_lock = threading.Lock()
|
self._lock = asyncio.Lock()
|
||||||
|
|
||||||
async def add(
|
async def add(
|
||||||
self,
|
self,
|
||||||
@ -150,9 +151,10 @@ class SpinnerManager:
|
|||||||
spinner_style=spinner_style,
|
spinner_style=spinner_style,
|
||||||
spinner_speed=spinner_speed,
|
spinner_speed=spinner_speed,
|
||||||
)
|
)
|
||||||
with self._start_lock:
|
async with self._lock:
|
||||||
if not self._running:
|
if not self._running:
|
||||||
self._start_live()
|
logger.debug("[%s] Starting spinner manager Live loop.", name)
|
||||||
|
await self._start_live()
|
||||||
|
|
||||||
def update(
|
def update(
|
||||||
self,
|
self,
|
||||||
@ -174,13 +176,17 @@ class SpinnerManager:
|
|||||||
data.spinner_type = spinner_type
|
data.spinner_type = spinner_type
|
||||||
data.spinner = Spinner(spinner_type, text=data.text)
|
data.spinner = Spinner(spinner_type, text=data.text)
|
||||||
|
|
||||||
def remove(self, name: str):
|
async def remove(self, name: str):
|
||||||
"""Remove a spinner and stop the Live loop if no spinners remain."""
|
"""Remove a spinner and stop the Live loop if no spinners remain."""
|
||||||
self._spinners.pop(name, None)
|
self._spinners.pop(name, None)
|
||||||
if not self._spinners:
|
async with self._lock:
|
||||||
self._running = False
|
if not self._spinners:
|
||||||
|
logger.debug("[%s] Stopping spinner manager, no spinners left.", name)
|
||||||
|
if self._task:
|
||||||
|
self._task.cancel()
|
||||||
|
self._running = False
|
||||||
|
|
||||||
def _start_live(self):
|
async def _start_live(self):
|
||||||
"""Start the Live rendering loop in the background."""
|
"""Start the Live rendering loop in the background."""
|
||||||
self._running = True
|
self._running = True
|
||||||
self._task = asyncio.create_task(self._live_loop())
|
self._task = asyncio.create_task(self._live_loop())
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
|
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
||||||
"""
|
"""
|
||||||
colors.py
|
|
||||||
|
|
||||||
A Python module that integrates the Nord color palette with the Rich library.
|
A Python module that integrates the Nord color palette with the Rich library.
|
||||||
It defines a metaclass-based NordColors class allowing dynamic attribute lookups
|
It defines a metaclass-based NordColors class allowing dynamic attribute lookups
|
||||||
(e.g., NORD12bu -> "#D08770 bold underline") and provides a comprehensive Nord-based
|
(e.g., NORD12bu -> "#D08770 bold underline") and provides a comprehensive Nord-based
|
||||||
|
@ -1 +1 @@
|
|||||||
__version__ = "0.1.77"
|
__version__ = "0.1.78"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "falyx"
|
name = "falyx"
|
||||||
version = "0.1.77"
|
version = "0.1.78"
|
||||||
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"
|
||||||
@ -10,7 +10,7 @@ packages = [{ include = "falyx" }]
|
|||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = ">=3.10"
|
python = ">=3.10"
|
||||||
prompt_toolkit = "^3.0"
|
prompt_toolkit = "^3.0"
|
||||||
rich = "^13.0"
|
rich = "^14.0"
|
||||||
pydantic = "^2.0"
|
pydantic = "^2.0"
|
||||||
python-json-logger = "^3.3.0"
|
python-json-logger = "^3.3.0"
|
||||||
toml = "^0.10"
|
toml = "^0.10"
|
||||||
|
Reference in New Issue
Block a user