387 lines
10 KiB
Python
387 lines
10 KiB
Python
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
|
"""selection.py"""
|
|
from dataclasses import dataclass
|
|
from typing import Any, Callable, KeysView, Sequence
|
|
|
|
from prompt_toolkit import PromptSession
|
|
from rich import box
|
|
from rich.console import Console
|
|
from rich.markup import escape
|
|
from rich.table import Table
|
|
|
|
from falyx.themes import OneColors
|
|
from falyx.utils import chunks
|
|
from falyx.validators import int_range_validator, key_validator
|
|
|
|
|
|
@dataclass
|
|
class SelectionOption:
|
|
"""Represents a single selection option with a description and a value."""
|
|
|
|
description: str
|
|
value: Any
|
|
style: str = OneColors.WHITE
|
|
|
|
def __post_init__(self):
|
|
if not isinstance(self.description, str):
|
|
raise TypeError("SelectionOption description must be a string.")
|
|
|
|
def render(self, key: str) -> str:
|
|
"""Render the selection option for display."""
|
|
key = escape(f"[{key}]")
|
|
return f"[{OneColors.WHITE}]{key}[/] [{self.style}]{self.description}[/]"
|
|
|
|
|
|
def render_table_base(
|
|
title: str,
|
|
*,
|
|
caption: str = "",
|
|
columns: int = 4,
|
|
box_style: box.Box = box.SIMPLE,
|
|
show_lines: bool = False,
|
|
show_header: bool = False,
|
|
show_footer: bool = False,
|
|
style: str = "",
|
|
header_style: str = "",
|
|
footer_style: str = "",
|
|
title_style: str = "",
|
|
caption_style: str = "",
|
|
highlight: bool = True,
|
|
column_names: Sequence[str] | None = None,
|
|
) -> Table:
|
|
table = Table(
|
|
title=title,
|
|
caption=caption,
|
|
box=box_style,
|
|
show_lines=show_lines,
|
|
show_header=show_header,
|
|
show_footer=show_footer,
|
|
style=style,
|
|
header_style=header_style,
|
|
footer_style=footer_style,
|
|
title_style=title_style,
|
|
caption_style=caption_style,
|
|
highlight=highlight,
|
|
)
|
|
if column_names:
|
|
for column_name in column_names:
|
|
table.add_column(column_name)
|
|
else:
|
|
for _ in range(columns):
|
|
table.add_column()
|
|
return table
|
|
|
|
|
|
def render_selection_grid(
|
|
title: str,
|
|
selections: Sequence[str],
|
|
*,
|
|
columns: int = 4,
|
|
caption: str = "",
|
|
box_style: box.Box = box.SIMPLE,
|
|
show_lines: bool = False,
|
|
show_header: bool = False,
|
|
show_footer: bool = False,
|
|
style: str = "",
|
|
header_style: str = "",
|
|
footer_style: str = "",
|
|
title_style: str = "",
|
|
caption_style: str = "",
|
|
highlight: bool = False,
|
|
) -> Table:
|
|
"""Create a selection table with the given parameters."""
|
|
table = render_table_base(
|
|
title=title,
|
|
caption=caption,
|
|
columns=columns,
|
|
box_style=box_style,
|
|
show_lines=show_lines,
|
|
show_header=show_header,
|
|
show_footer=show_footer,
|
|
style=style,
|
|
header_style=header_style,
|
|
footer_style=footer_style,
|
|
title_style=title_style,
|
|
caption_style=caption_style,
|
|
highlight=highlight,
|
|
)
|
|
|
|
for chunk in chunks(selections, columns):
|
|
table.add_row(*chunk)
|
|
|
|
return table
|
|
|
|
|
|
def render_selection_indexed_table(
|
|
title: str,
|
|
selections: Sequence[str],
|
|
*,
|
|
columns: int = 4,
|
|
caption: str = "",
|
|
box_style: box.Box = box.SIMPLE,
|
|
show_lines: bool = False,
|
|
show_header: bool = False,
|
|
show_footer: bool = False,
|
|
style: str = "",
|
|
header_style: str = "",
|
|
footer_style: str = "",
|
|
title_style: str = "",
|
|
caption_style: str = "",
|
|
highlight: bool = False,
|
|
formatter: Callable[[int, str], str] | None = None,
|
|
) -> Table:
|
|
"""Create a selection table with the given parameters."""
|
|
table = render_table_base(
|
|
title=title,
|
|
caption=caption,
|
|
columns=columns,
|
|
box_style=box_style,
|
|
show_lines=show_lines,
|
|
show_header=show_header,
|
|
show_footer=show_footer,
|
|
style=style,
|
|
header_style=header_style,
|
|
footer_style=footer_style,
|
|
title_style=title_style,
|
|
caption_style=caption_style,
|
|
highlight=highlight,
|
|
)
|
|
|
|
for indexes, chunk in zip(
|
|
chunks(range(len(selections)), columns), chunks(selections, columns)
|
|
):
|
|
row = [
|
|
formatter(index, selection) if formatter else f"[{index}] {selection}"
|
|
for index, selection in zip(indexes, chunk)
|
|
]
|
|
table.add_row(*row)
|
|
|
|
return table
|
|
|
|
|
|
def render_selection_dict_table(
|
|
title: str,
|
|
selections: dict[str, SelectionOption],
|
|
*,
|
|
columns: int = 2,
|
|
caption: str = "",
|
|
box_style: box.Box = box.SIMPLE,
|
|
show_lines: bool = False,
|
|
show_header: bool = False,
|
|
show_footer: bool = False,
|
|
style: str = "",
|
|
header_style: str = "",
|
|
footer_style: str = "",
|
|
title_style: str = "",
|
|
caption_style: str = "",
|
|
highlight: bool = False,
|
|
) -> Table:
|
|
"""Create a selection table with the given parameters."""
|
|
table = render_table_base(
|
|
title=title,
|
|
caption=caption,
|
|
columns=columns,
|
|
box_style=box_style,
|
|
show_lines=show_lines,
|
|
show_header=show_header,
|
|
show_footer=show_footer,
|
|
style=style,
|
|
header_style=header_style,
|
|
footer_style=footer_style,
|
|
title_style=title_style,
|
|
caption_style=caption_style,
|
|
highlight=highlight,
|
|
)
|
|
|
|
for chunk in chunks(selections.items(), columns):
|
|
row = []
|
|
for key, option in chunk:
|
|
row.append(
|
|
f"[{OneColors.WHITE}][{key.upper()}] "
|
|
f"[{option.style}]{option.description}[/]"
|
|
)
|
|
table.add_row(*row)
|
|
|
|
return table
|
|
|
|
|
|
async def prompt_for_index(
|
|
max_index: int,
|
|
table: Table,
|
|
*,
|
|
min_index: int = 0,
|
|
default_selection: str = "",
|
|
console: Console | None = None,
|
|
prompt_session: PromptSession | None = None,
|
|
prompt_message: str = "Select an option > ",
|
|
show_table: bool = True,
|
|
):
|
|
prompt_session = prompt_session or PromptSession()
|
|
console = console or Console(color_system="auto")
|
|
|
|
if show_table:
|
|
console.print(table, justify="center")
|
|
|
|
selection = await prompt_session.prompt_async(
|
|
message=prompt_message,
|
|
validator=int_range_validator(min_index, max_index),
|
|
default=default_selection,
|
|
)
|
|
return int(selection)
|
|
|
|
|
|
async def prompt_for_selection(
|
|
keys: Sequence[str] | KeysView[str],
|
|
table: Table,
|
|
*,
|
|
default_selection: str = "",
|
|
console: Console | None = None,
|
|
prompt_session: PromptSession | None = None,
|
|
prompt_message: str = "Select an option > ",
|
|
show_table: bool = True,
|
|
) -> 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")
|
|
|
|
if show_table:
|
|
console.print(table, justify="center")
|
|
|
|
selected = await prompt_session.prompt_async(
|
|
message=prompt_message,
|
|
validator=key_validator(keys),
|
|
default=default_selection,
|
|
)
|
|
|
|
return selected
|
|
|
|
|
|
async def select_value_from_list(
|
|
title: str,
|
|
selections: Sequence[str],
|
|
*,
|
|
console: Console | None = None,
|
|
prompt_session: PromptSession | None = None,
|
|
prompt_message: str = "Select an option > ",
|
|
default_selection: str = "",
|
|
columns: int = 4,
|
|
caption: str = "",
|
|
box_style: box.Box = box.SIMPLE,
|
|
show_lines: bool = False,
|
|
show_header: bool = False,
|
|
show_footer: bool = False,
|
|
style: str = "",
|
|
header_style: str = "",
|
|
footer_style: str = "",
|
|
title_style: str = "",
|
|
caption_style: str = "",
|
|
highlight: bool = False,
|
|
):
|
|
"""Prompt for a selection. Return the selected item."""
|
|
table = render_selection_indexed_table(
|
|
title=title,
|
|
selections=selections,
|
|
columns=columns,
|
|
caption=caption,
|
|
box_style=box_style,
|
|
show_lines=show_lines,
|
|
show_header=show_header,
|
|
show_footer=show_footer,
|
|
style=style,
|
|
header_style=header_style,
|
|
footer_style=footer_style,
|
|
title_style=title_style,
|
|
caption_style=caption_style,
|
|
highlight=highlight,
|
|
)
|
|
prompt_session = prompt_session or PromptSession()
|
|
console = console or Console(color_system="auto")
|
|
|
|
selection_index = await prompt_for_index(
|
|
len(selections) - 1,
|
|
table,
|
|
default_selection=default_selection,
|
|
console=console,
|
|
prompt_session=prompt_session,
|
|
prompt_message=prompt_message,
|
|
)
|
|
|
|
return selections[selection_index]
|
|
|
|
|
|
async def select_key_from_dict(
|
|
selections: dict[str, SelectionOption],
|
|
table: Table,
|
|
*,
|
|
console: Console | None = None,
|
|
prompt_session: PromptSession | None = None,
|
|
prompt_message: str = "Select an option > ",
|
|
default_selection: str = "",
|
|
) -> 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.print(table, justify="center")
|
|
|
|
return await prompt_for_selection(
|
|
selections.keys(),
|
|
table,
|
|
default_selection=default_selection,
|
|
console=console,
|
|
prompt_session=prompt_session,
|
|
prompt_message=prompt_message,
|
|
)
|
|
|
|
|
|
async def select_value_from_dict(
|
|
selections: dict[str, SelectionOption],
|
|
table: Table,
|
|
*,
|
|
console: Console | None = None,
|
|
prompt_session: PromptSession | None = None,
|
|
prompt_message: str = "Select an option > ",
|
|
default_selection: str = "",
|
|
) -> 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.print(table, justify="center")
|
|
|
|
selection_key = await prompt_for_selection(
|
|
selections.keys(),
|
|
table,
|
|
default_selection=default_selection,
|
|
console=console,
|
|
prompt_session=prompt_session,
|
|
prompt_message=prompt_message,
|
|
)
|
|
|
|
return selections[selection_key].value
|
|
|
|
|
|
async def get_selection_from_dict_menu(
|
|
title: str,
|
|
selections: dict[str, SelectionOption],
|
|
*,
|
|
console: Console | None = None,
|
|
prompt_session: PromptSession | None = None,
|
|
prompt_message: str = "Select an option > ",
|
|
default_selection: str = "",
|
|
):
|
|
"""Prompt for a key from a dict, but return the value."""
|
|
table = render_selection_dict_table(
|
|
title,
|
|
selections,
|
|
)
|
|
|
|
return await select_value_from_dict(
|
|
selections=selections,
|
|
table=table,
|
|
console=console,
|
|
prompt_session=prompt_session,
|
|
prompt_message=prompt_message,
|
|
default_selection=default_selection,
|
|
)
|