From ac82076511b3d127682ec97cc6c8a0ce7f280f20 Mon Sep 17 00:00:00 2001 From: Roland Thomas Date: Tue, 3 Jun 2025 23:07:50 -0400 Subject: [PATCH] Add filtering and options for History Command --- falyx/action/base.py | 2 +- falyx/action/menu_action.py | 5 +- falyx/action/prompt_menu_action.py | 5 +- falyx/action/select_file_action.py | 5 +- falyx/action/selection_action.py | 5 +- falyx/action/user_input_action.py | 5 +- falyx/bottom_bar.py | 2 +- falyx/command.py | 2 +- falyx/config.py | 3 +- falyx/context.py | 4 +- falyx/execution_registry.py | 109 ++++++++++++++++++++++++----- falyx/falyx.py | 38 +++++++++- falyx/init.py | 2 +- falyx/parsers/argparse.py | 2 +- falyx/selection.py | 10 +-- falyx/version.py | 2 +- pyproject.toml | 2 +- 17 files changed, 165 insertions(+), 38 deletions(-) diff --git a/falyx/action/base.py b/falyx/action/base.py index 902a451..9d26ee6 100644 --- a/falyx/action/base.py +++ b/falyx/action/base.py @@ -74,7 +74,7 @@ class BaseAction(ABC): self.inject_into: str = inject_into self._never_prompt: bool = never_prompt self._skip_in_chain: bool = False - self.console = Console(color_system="auto") + self.console = Console(color_system="truecolor") self.options_manager: OptionsManager | None = None if logging_hooks: diff --git a/falyx/action/menu_action.py b/falyx/action/menu_action.py index 4dad4ad..2975f64 100644 --- a/falyx/action/menu_action.py +++ b/falyx/action/menu_action.py @@ -51,7 +51,10 @@ class MenuAction(BaseAction): self.columns = columns self.prompt_message = prompt_message self.default_selection = default_selection - self.console = console or Console(color_system="auto") + if isinstance(console, Console): + self.console = console + elif console: + raise ValueError("`console` must be an instance of `rich.console.Console`") self.prompt_session = prompt_session or PromptSession() self.include_reserved = include_reserved self.show_table = show_table diff --git a/falyx/action/prompt_menu_action.py b/falyx/action/prompt_menu_action.py index 6e490a6..ceece0b 100644 --- a/falyx/action/prompt_menu_action.py +++ b/falyx/action/prompt_menu_action.py @@ -43,7 +43,10 @@ class PromptMenuAction(BaseAction): self.menu_options = menu_options self.prompt_message = prompt_message self.default_selection = default_selection - self.console = console or Console(color_system="auto") + if isinstance(console, Console): + self.console = console + elif console: + raise ValueError("`console` must be an instance of `rich.console.Console`") self.prompt_session = prompt_session or PromptSession() self.include_reserved = include_reserved diff --git a/falyx/action/select_file_action.py b/falyx/action/select_file_action.py index 7673b86..3943a6d 100644 --- a/falyx/action/select_file_action.py +++ b/falyx/action/select_file_action.py @@ -76,7 +76,10 @@ class SelectFileAction(BaseAction): self.prompt_message = prompt_message self.suffix_filter = suffix_filter self.style = style - self.console = console or Console(color_system="auto") + if isinstance(console, Console): + self.console = console + elif console: + raise ValueError("`console` must be an instance of `rich.console.Console`") self.prompt_session = prompt_session or PromptSession() self.return_type = self._coerce_return_type(return_type) diff --git a/falyx/action/selection_action.py b/falyx/action/selection_action.py index 9f09ccd..add3f87 100644 --- a/falyx/action/selection_action.py +++ b/falyx/action/selection_action.py @@ -67,7 +67,10 @@ class SelectionAction(BaseAction): self.return_type: SelectionReturnType = self._coerce_return_type(return_type) self.title = title self.columns = columns - self.console = console or Console(color_system="auto") + if isinstance(console, Console): + self.console = console + elif console: + raise ValueError("`console` must be an instance of `rich.console.Console`") self.prompt_session = prompt_session or PromptSession() self.default_selection = default_selection self.prompt_message = prompt_message diff --git a/falyx/action/user_input_action.py b/falyx/action/user_input_action.py index 8fe31cf..54b5e5f 100644 --- a/falyx/action/user_input_action.py +++ b/falyx/action/user_input_action.py @@ -40,7 +40,10 @@ class UserInputAction(BaseAction): ) self.prompt_text = prompt_text self.validator = validator - self.console = console or Console(color_system="auto") + if isinstance(console, Console): + self.console = console + elif console: + raise ValueError("`console` must be an instance of `rich.console.Console`") self.prompt_session = prompt_session or PromptSession() def get_infer_target(self) -> tuple[None, None]: diff --git a/falyx/bottom_bar.py b/falyx/bottom_bar.py index e5c1535..9c7bfcb 100644 --- a/falyx/bottom_bar.py +++ b/falyx/bottom_bar.py @@ -30,7 +30,7 @@ class BottomBar: key_validator: Callable[[str], bool] | None = None, ) -> None: self.columns = columns - self.console = Console(color_system="auto") + self.console = Console(color_system="truecolor") self._named_items: dict[str, Callable[[], HTML]] = {} self._value_getters: dict[str, Callable[[], Any]] = CaseInsensitiveDict() self.toggle_keys: list[str] = [] diff --git a/falyx/command.py b/falyx/command.py index a77ff50..be66f52 100644 --- a/falyx/command.py +++ b/falyx/command.py @@ -44,7 +44,7 @@ from falyx.signals import CancelSignal from falyx.themes import OneColors from falyx.utils import ensure_async -console = Console(color_system="auto") +console = Console(color_system="truecolor") class Command(BaseModel): diff --git a/falyx/config.py b/falyx/config.py index 4e2a3ec..b035940 100644 --- a/falyx/config.py +++ b/falyx/config.py @@ -18,11 +18,10 @@ from falyx.action.base import BaseAction from falyx.command import Command from falyx.falyx import Falyx from falyx.logger import logger -from falyx.parsers import CommandArgumentParser from falyx.retry import RetryPolicy from falyx.themes import OneColors -console = Console(color_system="auto") +console = Console(color_system="truecolor") def wrap_if_needed(obj: Any, name=None) -> BaseAction | Command: diff --git a/falyx/context.py b/falyx/context.py index 93e6bd1..c35d5c4 100644 --- a/falyx/context.py +++ b/falyx/context.py @@ -80,8 +80,10 @@ class ExecutionContext(BaseModel): start_wall: datetime | None = None end_wall: datetime | None = None + index: int | None = None + extra: dict[str, Any] = Field(default_factory=dict) - console: Console = Field(default_factory=lambda: Console(color_system="auto")) + console: Console = Field(default_factory=lambda: Console(color_system="truecolor")) shared_context: SharedContext | None = None diff --git a/falyx/execution_registry.py b/falyx/execution_registry.py index 60b55e0..d78aa6b 100644 --- a/falyx/execution_registry.py +++ b/falyx/execution_registry.py @@ -29,7 +29,8 @@ from __future__ import annotations from collections import defaultdict from datetime import datetime -from typing import Dict, List +from threading import Lock +from typing import Any, Literal from rich import box from rich.console import Console @@ -70,23 +71,30 @@ class ExecutionRegistry: ExecutionRegistry.summary() """ - _store_by_name: Dict[str, List[ExecutionContext]] = defaultdict(list) - _store_all: List[ExecutionContext] = [] - _console = Console(color_system="auto") + _store_by_name: dict[str, list[ExecutionContext]] = defaultdict(list) + _store_by_index: dict[int, ExecutionContext] = {} + _store_all: list[ExecutionContext] = [] + _console = Console(color_system="truecolor") + _index = 0 + _lock = Lock() @classmethod def record(cls, context: ExecutionContext): """Record an execution context.""" logger.debug(context.to_log_line()) + with cls._lock: + context.index = cls._index + cls._store_by_index[cls._index] = context + cls._index += 1 cls._store_by_name[context.name].append(context) cls._store_all.append(context) @classmethod - def get_all(cls) -> List[ExecutionContext]: + def get_all(cls) -> list[ExecutionContext]: return cls._store_all @classmethod - def get_by_name(cls, name: str) -> List[ExecutionContext]: + def get_by_name(cls, name: str) -> list[ExecutionContext]: return cls._store_by_name.get(name, []) @classmethod @@ -97,11 +105,74 @@ class ExecutionRegistry: def clear(cls): cls._store_by_name.clear() cls._store_all.clear() + cls._store_by_index.clear() @classmethod - def summary(cls): - table = Table(title="📊 Execution History", expand=True, box=box.SIMPLE) + def summary( + cls, + name: str = "", + index: int = -1, + result: int = -1, + clear: bool = False, + last_result: bool = False, + status: Literal["all", "success", "error"] = "all", + ): + if clear: + cls.clear() + cls._console.print(f"[{OneColors.GREEN}]✅ Execution history cleared.") + return + if last_result: + for ctx in reversed(cls._store_all): + if ctx.name.upper() not in [ + "HISTORY", + "HELP", + "EXIT", + "VIEW EXECUTION HISTORY", + "BACK", + ]: + cls._console.print(ctx.result) + return + cls._console.print( + f"[{OneColors.DARK_RED}]❌ No valid executions found to display last result." + ) + return + + if result and result >= 0: + try: + result_context = cls._store_by_index[result] + except KeyError: + cls._console.print( + f"[{OneColors.DARK_RED}]❌ No execution found for index {index}." + ) + return + cls._console.print(result_context.result) + return + + if name: + contexts = cls.get_by_name(name) + if not contexts: + cls._console.print( + f"[{OneColors.DARK_RED}]❌ No executions found for action '{name}'." + ) + return + title = f"📊 Execution History for '{contexts[0].name}'" + elif index and index >= 0: + try: + contexts = [cls._store_by_index[index]] + except KeyError: + cls._console.print( + f"[{OneColors.DARK_RED}]❌ No execution found for index {index}." + ) + return + title = f"📊 Execution History for Index {index}" + else: + contexts = cls.get_all() + title = "📊 Execution History" + + table = Table(title=title, expand=True, box=box.SIMPLE) + + table.add_column("Index", justify="right", style="dim") table.add_column("Name", style="bold cyan") table.add_column("Start", justify="right", style="dim") table.add_column("End", justify="right", style="dim") @@ -109,7 +180,7 @@ class ExecutionRegistry: table.add_column("Status", style="bold") table.add_column("Result / Exception", overflow="fold") - for ctx in cls.get_all(): + for ctx in contexts: start = ( datetime.fromtimestamp(ctx.start_time).strftime("%H:%M:%S") if ctx.start_time @@ -122,15 +193,19 @@ class ExecutionRegistry: ) duration = f"{ctx.duration:.3f}s" if ctx.duration else "n/a" - if ctx.exception: - status = f"[{OneColors.DARK_RED}]❌ Error" - result = repr(ctx.exception) + if ctx.exception and status.lower() in ["all", "error"]: + final_status = f"[{OneColors.DARK_RED}]❌ Error" + final_result = repr(ctx.exception) + elif status.lower() in ["all", "success"]: + final_status = f"[{OneColors.GREEN}]✅ Success" + final_result = repr(ctx.result) + if len(final_result) > 1000: + final_result = f"{final_result[:1000]}..." else: - status = f"[{OneColors.GREEN}]✅ Success" - result = repr(ctx.result) - if len(result) > 1000: - result = f"{result[:1000]}..." + continue - table.add_row(ctx.name, start, end, duration, status, result) + table.add_row( + str(ctx.index), ctx.name, start, end, duration, final_status, final_result + ) cls._console.print(table) diff --git a/falyx/falyx.py b/falyx/falyx.py index 5dcfea6..f5183d5 100644 --- a/falyx/falyx.py +++ b/falyx/falyx.py @@ -201,7 +201,7 @@ class Falyx: self.help_command: Command | None = ( self._get_help_command() if include_help_command else None ) - self.console: Console = Console(color_system="auto", theme=get_nord_theme()) + self.console: Console = Console(color_system="truecolor", theme=get_nord_theme()) self.welcome_message: str | Markdown | dict[str, Any] = welcome_message self.exit_message: str | Markdown | dict[str, Any] = exit_message self.hooks: HookManager = HookManager() @@ -300,6 +300,40 @@ class Falyx: def _get_history_command(self) -> Command: """Returns the history command for the menu.""" + parser = CommandArgumentParser( + command_key="Y", + command_description="History", + command_style=OneColors.DARK_YELLOW, + aliases=["HISTORY"], + ) + parser.add_argument( + "-n", + "--name", + help="Filter by execution name.", + ) + parser.add_argument( + "-i", + "--index", + type=int, + help="Filter by execution index (0-based).", + ) + parser.add_argument( + "-s", + "--status", + choices=["all", "success", "error"], + default="all", + help="Filter by execution status (default: all).", + ) + parser.add_argument( + "-c", + "--clear", + action="store_true", + help="Clear the Execution History.", + ) + parser.add_argument("-r", "--result", type=int, help="Get the result by index") + parser.add_argument( + "-l", "--last-result", action="store_true", help="Get the last result" + ) return Command( key="Y", description="History", @@ -307,6 +341,8 @@ class Falyx: action=Action(name="View Execution History", action=er.summary), style=OneColors.DARK_YELLOW, simple_help_signature=True, + arg_parser=parser, + help_text="View the execution history of commands.", ) async def _show_help(self, tag: str = "") -> None: diff --git a/falyx/init.py b/falyx/init.py index f5f755c..65940a8 100644 --- a/falyx/init.py +++ b/falyx/init.py @@ -98,7 +98,7 @@ commands: aliases: [clean, cleanup] """ -console = Console(color_system="auto") +console = Console(color_system="truecolor") def init_project(name: str) -> None: diff --git a/falyx/parsers/argparse.py b/falyx/parsers/argparse.py index 5b7adec..751de27 100644 --- a/falyx/parsers/argparse.py +++ b/falyx/parsers/argparse.py @@ -159,7 +159,7 @@ class CommandArgumentParser: aliases: list[str] | None = None, ) -> None: """Initialize the CommandArgumentParser.""" - self.console = Console(color_system="auto") + self.console = Console(color_system="truecolor") self.command_key: str = command_key self.command_description: str = command_description self.command_style: str = command_style diff --git a/falyx/selection.py b/falyx/selection.py index 8f5f2b5..8ba63ef 100644 --- a/falyx/selection.py +++ b/falyx/selection.py @@ -273,7 +273,7 @@ async def prompt_for_index( show_table: bool = True, ) -> int: prompt_session = prompt_session or PromptSession() - console = console or Console(color_system="auto") + console = console or Console(color_system="truecolor") if show_table: console.print(table, justify="center") @@ -298,7 +298,7 @@ async def prompt_for_selection( ) -> str: """Prompt the user to select a key from a set of options. Return the selected key.""" prompt_session = prompt_session or PromptSession() - console = console or Console(color_system="auto") + console = console or Console(color_system="truecolor") if show_table: console.print(table, justify="center") @@ -351,7 +351,7 @@ async def select_value_from_list( highlight=highlight, ) prompt_session = prompt_session or PromptSession() - console = console or Console(color_system="auto") + console = console or Console(color_system="truecolor") selection_index = await prompt_for_index( len(selections) - 1, @@ -376,7 +376,7 @@ async def select_key_from_dict( ) -> Any: """Prompt for a key from a dict, returns the key.""" prompt_session = prompt_session or PromptSession() - console = console or Console(color_system="auto") + console = console or Console(color_system="truecolor") console.print(table, justify="center") @@ -401,7 +401,7 @@ async def select_value_from_dict( ) -> Any: """Prompt for a key from a dict, but return the value.""" prompt_session = prompt_session or PromptSession() - console = console or Console(color_system="auto") + console = console or Console(color_system="truecolor") console.print(table, justify="center") diff --git a/falyx/version.py b/falyx/version.py index d95a92e..a9d181d 100644 --- a/falyx/version.py +++ b/falyx/version.py @@ -1 +1 @@ -__version__ = "0.1.48" +__version__ = "0.1.49" diff --git a/pyproject.toml b/pyproject.toml index 9f2f095..c9a764e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "falyx" -version = "0.1.48" +version = "0.1.49" description = "Reliable and introspectable async CLI action framework." authors = ["Roland Thomas Jr "] license = "MIT"