Add filtering and options for History Command

This commit is contained in:
Roland Thomas Jr 2025-06-03 23:07:50 -04:00
parent 09eeb90dc6
commit ac82076511
Signed by: roland
GPG Key ID: 7C3C2B085A4C2872
17 changed files with 165 additions and 38 deletions

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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]:

View File

@ -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] = []

View File

@ -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):

View File

@ -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:

View File

@ -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

View File

@ -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)

View File

@ -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:

View File

@ -98,7 +98,7 @@ commands:
aliases: [clean, cleanup]
"""
console = Console(color_system="auto")
console = Console(color_system="truecolor")
def init_project(name: str) -> None:

View File

@ -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

View File

@ -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")

View File

@ -1 +1 @@
__version__ = "0.1.48"
__version__ = "0.1.49"

View File

@ -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 <roland@rtj.dev>"]
license = "MIT"