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.inject_into: str = inject_into
self._never_prompt: bool = never_prompt self._never_prompt: bool = never_prompt
self._skip_in_chain: bool = False self._skip_in_chain: bool = False
self.console = Console(color_system="auto") self.console = Console(color_system="truecolor")
self.options_manager: OptionsManager | None = None self.options_manager: OptionsManager | None = None
if logging_hooks: if logging_hooks:

View File

@ -51,7 +51,10 @@ class MenuAction(BaseAction):
self.columns = columns self.columns = columns
self.prompt_message = prompt_message self.prompt_message = prompt_message
self.default_selection = default_selection 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.prompt_session = prompt_session or PromptSession()
self.include_reserved = include_reserved self.include_reserved = include_reserved
self.show_table = show_table self.show_table = show_table

View File

@ -43,7 +43,10 @@ class PromptMenuAction(BaseAction):
self.menu_options = menu_options self.menu_options = menu_options
self.prompt_message = prompt_message self.prompt_message = prompt_message
self.default_selection = default_selection 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.prompt_session = prompt_session or PromptSession()
self.include_reserved = include_reserved self.include_reserved = include_reserved

View File

@ -76,7 +76,10 @@ class SelectFileAction(BaseAction):
self.prompt_message = prompt_message self.prompt_message = prompt_message
self.suffix_filter = suffix_filter self.suffix_filter = suffix_filter
self.style = style 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.prompt_session = prompt_session or PromptSession()
self.return_type = self._coerce_return_type(return_type) 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.return_type: SelectionReturnType = self._coerce_return_type(return_type)
self.title = title self.title = title
self.columns = columns 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.prompt_session = prompt_session or PromptSession()
self.default_selection = default_selection self.default_selection = default_selection
self.prompt_message = prompt_message self.prompt_message = prompt_message

View File

@ -40,7 +40,10 @@ class UserInputAction(BaseAction):
) )
self.prompt_text = prompt_text self.prompt_text = prompt_text
self.validator = validator 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() self.prompt_session = prompt_session or PromptSession()
def get_infer_target(self) -> tuple[None, None]: def get_infer_target(self) -> tuple[None, None]:

View File

@ -30,7 +30,7 @@ class BottomBar:
key_validator: Callable[[str], bool] | None = None, key_validator: Callable[[str], bool] | None = None,
) -> None: ) -> None:
self.columns = columns self.columns = columns
self.console = Console(color_system="auto") self.console = Console(color_system="truecolor")
self._named_items: dict[str, Callable[[], HTML]] = {} self._named_items: dict[str, Callable[[], HTML]] = {}
self._value_getters: dict[str, Callable[[], Any]] = CaseInsensitiveDict() self._value_getters: dict[str, Callable[[], Any]] = CaseInsensitiveDict()
self.toggle_keys: list[str] = [] self.toggle_keys: list[str] = []

View File

@ -44,7 +44,7 @@ from falyx.signals import CancelSignal
from falyx.themes import OneColors from falyx.themes import OneColors
from falyx.utils import ensure_async from falyx.utils import ensure_async
console = Console(color_system="auto") console = Console(color_system="truecolor")
class Command(BaseModel): class Command(BaseModel):

View File

@ -18,11 +18,10 @@ from falyx.action.base import BaseAction
from falyx.command import Command from falyx.command import Command
from falyx.falyx import Falyx from falyx.falyx import Falyx
from falyx.logger import logger from falyx.logger import logger
from falyx.parsers import CommandArgumentParser
from falyx.retry import RetryPolicy from falyx.retry import RetryPolicy
from falyx.themes import OneColors 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: def wrap_if_needed(obj: Any, name=None) -> BaseAction | Command:

View File

@ -80,8 +80,10 @@ class ExecutionContext(BaseModel):
start_wall: datetime | None = None start_wall: datetime | None = None
end_wall: datetime | None = None end_wall: datetime | None = None
index: int | None = None
extra: dict[str, Any] = Field(default_factory=dict) 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 shared_context: SharedContext | None = None

View File

@ -29,7 +29,8 @@ from __future__ import annotations
from collections import defaultdict from collections import defaultdict
from datetime import datetime from datetime import datetime
from typing import Dict, List from threading import Lock
from typing import Any, Literal
from rich import box from rich import box
from rich.console import Console from rich.console import Console
@ -70,23 +71,30 @@ class ExecutionRegistry:
ExecutionRegistry.summary() ExecutionRegistry.summary()
""" """
_store_by_name: Dict[str, List[ExecutionContext]] = defaultdict(list) _store_by_name: dict[str, list[ExecutionContext]] = defaultdict(list)
_store_all: List[ExecutionContext] = [] _store_by_index: dict[int, ExecutionContext] = {}
_console = Console(color_system="auto") _store_all: list[ExecutionContext] = []
_console = Console(color_system="truecolor")
_index = 0
_lock = Lock()
@classmethod @classmethod
def record(cls, context: ExecutionContext): def record(cls, context: ExecutionContext):
"""Record an execution context.""" """Record an execution context."""
logger.debug(context.to_log_line()) 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_by_name[context.name].append(context)
cls._store_all.append(context) cls._store_all.append(context)
@classmethod @classmethod
def get_all(cls) -> List[ExecutionContext]: def get_all(cls) -> list[ExecutionContext]:
return cls._store_all return cls._store_all
@classmethod @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, []) return cls._store_by_name.get(name, [])
@classmethod @classmethod
@ -97,11 +105,74 @@ class ExecutionRegistry:
def clear(cls): def clear(cls):
cls._store_by_name.clear() cls._store_by_name.clear()
cls._store_all.clear() cls._store_all.clear()
cls._store_by_index.clear()
@classmethod @classmethod
def summary(cls): def summary(
table = Table(title="📊 Execution History", expand=True, box=box.SIMPLE) 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("Name", style="bold cyan")
table.add_column("Start", justify="right", style="dim") table.add_column("Start", justify="right", style="dim")
table.add_column("End", 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("Status", style="bold")
table.add_column("Result / Exception", overflow="fold") table.add_column("Result / Exception", overflow="fold")
for ctx in cls.get_all(): for ctx in contexts:
start = ( start = (
datetime.fromtimestamp(ctx.start_time).strftime("%H:%M:%S") datetime.fromtimestamp(ctx.start_time).strftime("%H:%M:%S")
if ctx.start_time if ctx.start_time
@ -122,15 +193,19 @@ class ExecutionRegistry:
) )
duration = f"{ctx.duration:.3f}s" if ctx.duration else "n/a" duration = f"{ctx.duration:.3f}s" if ctx.duration else "n/a"
if ctx.exception: if ctx.exception and status.lower() in ["all", "error"]:
status = f"[{OneColors.DARK_RED}]❌ Error" final_status = f"[{OneColors.DARK_RED}]❌ Error"
result = repr(ctx.exception) 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: else:
status = f"[{OneColors.GREEN}]✅ Success" continue
result = repr(ctx.result)
if len(result) > 1000:
result = f"{result[:1000]}..."
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) cls._console.print(table)

View File

@ -201,7 +201,7 @@ class Falyx:
self.help_command: Command | None = ( self.help_command: Command | None = (
self._get_help_command() if include_help_command else 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.welcome_message: str | Markdown | dict[str, Any] = welcome_message
self.exit_message: str | Markdown | dict[str, Any] = exit_message self.exit_message: str | Markdown | dict[str, Any] = exit_message
self.hooks: HookManager = HookManager() self.hooks: HookManager = HookManager()
@ -300,6 +300,40 @@ class Falyx:
def _get_history_command(self) -> Command: def _get_history_command(self) -> Command:
"""Returns the history command for the menu.""" """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( return Command(
key="Y", key="Y",
description="History", description="History",
@ -307,6 +341,8 @@ class Falyx:
action=Action(name="View Execution History", action=er.summary), action=Action(name="View Execution History", action=er.summary),
style=OneColors.DARK_YELLOW, style=OneColors.DARK_YELLOW,
simple_help_signature=True, simple_help_signature=True,
arg_parser=parser,
help_text="View the execution history of commands.",
) )
async def _show_help(self, tag: str = "") -> None: async def _show_help(self, tag: str = "") -> None:

View File

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

View File

@ -159,7 +159,7 @@ class CommandArgumentParser:
aliases: list[str] | None = None, aliases: list[str] | None = None,
) -> None: ) -> None:
"""Initialize the CommandArgumentParser.""" """Initialize the CommandArgumentParser."""
self.console = Console(color_system="auto") self.console = Console(color_system="truecolor")
self.command_key: str = command_key self.command_key: str = command_key
self.command_description: str = command_description self.command_description: str = command_description
self.command_style: str = command_style self.command_style: str = command_style

View File

@ -273,7 +273,7 @@ async def prompt_for_index(
show_table: bool = True, show_table: bool = True,
) -> int: ) -> int:
prompt_session = prompt_session or PromptSession() prompt_session = prompt_session or PromptSession()
console = console or Console(color_system="auto") console = console or Console(color_system="truecolor")
if show_table: if show_table:
console.print(table, justify="center") console.print(table, justify="center")
@ -298,7 +298,7 @@ async def prompt_for_selection(
) -> str: ) -> str:
"""Prompt the user to select a key from a set of options. Return the selected key.""" """Prompt the user to select a key from a set of options. Return the selected key."""
prompt_session = prompt_session or PromptSession() prompt_session = prompt_session or PromptSession()
console = console or Console(color_system="auto") console = console or Console(color_system="truecolor")
if show_table: if show_table:
console.print(table, justify="center") console.print(table, justify="center")
@ -351,7 +351,7 @@ async def select_value_from_list(
highlight=highlight, highlight=highlight,
) )
prompt_session = prompt_session or PromptSession() 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( selection_index = await prompt_for_index(
len(selections) - 1, len(selections) - 1,
@ -376,7 +376,7 @@ async def select_key_from_dict(
) -> Any: ) -> Any:
"""Prompt for a key from a dict, returns the key.""" """Prompt for a key from a dict, returns the key."""
prompt_session = prompt_session or PromptSession() 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") console.print(table, justify="center")
@ -401,7 +401,7 @@ async def select_value_from_dict(
) -> Any: ) -> Any:
"""Prompt for a key from a dict, but return the value.""" """Prompt for a key from a dict, but return the value."""
prompt_session = prompt_session or PromptSession() 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") 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] [tool.poetry]
name = "falyx" name = "falyx"
version = "0.1.48" version = "0.1.49"
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"