Files
AirlockTools/utils/selector.py
T

138 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import logging
from typing import Any, Callable, List, Optional, Union
from utils.utils import get_sanitized_input # new async version
logger = logging.getLogger(__name__)
class Selector:
@staticmethod
def _display_choices(
items: List[Any],
label_func: Callable[[Any], str],
num_columns: int = 3,
header: str = "Available Choices:"
) -> None:
sorted_items = sorted(items, key=lambda item: label_func(item).lower())
rows = (len(sorted_items) + num_columns - 1) // num_columns
print(f"\n{header}")
for row in range(rows):
line = ""
for col in range(num_columns):
idx = row + col * rows
if idx < len(sorted_items):
label = label_func(sorted_items[idx])
line += f"{idx + 1}: {label:<30}"
print(line)
@staticmethod
async def _select_from_list(
items: List[Any],
label_func: Callable[[Any], str],
allow_multiple: bool = False,
prompt_each: bool = False,
header: str = "Available Choices:"
) -> Union[Optional[Any], List[Any]]:
if not items:
logger.warning("No items available for selection.")
return None
Selector._display_choices(items, label_func, header=header)
sorted_items = sorted(items, key=lambda item: label_func(item).lower())
selected = []
if allow_multiple:
while True:
choice = (await get_sanitized_input("Select an item by number (or Q to finish): ")).strip().lower()
if choice == "q":
break
try:
index = int(choice)
if 1 <= index <= len(sorted_items):
item = sorted_items[index - 1]
if item not in selected:
selected.append(item)
if prompt_each:
logger.info(f"Selected: {label_func(item)}")
else:
logger.warning("Item already selected.")
else:
logger.warning("Selection out of range. Try again.")
except ValueError:
logger.warning("Invalid input. Enter a number or 'Q' to quit.")
return selected if selected else None
else:
try:
choice = int(await get_sanitized_input("Select one item by number: "))
if 1 <= choice <= len(sorted_items):
selected_item = sorted_items[choice - 1]
logger.info(f"Selected: {label_func(selected_item)}")
return selected_item
else:
logger.warning("Selection out of range.")
except ValueError:
logger.warning("Invalid input.")
return None
@staticmethod
async def select_objects(objects: List[Any], allow_multiple: bool = False, prompt_each: bool = False) -> Union[Optional[Any], List[Any]]:
return await Selector._select_from_list(
objects,
label_func=lambda obj: getattr(obj, "name", str(obj)),
allow_multiple=allow_multiple,
prompt_each=prompt_each,
header="Available Objects:"
)
@staticmethod
async def select_string(options: List[str], allow_multiple: bool = False, prompt_each: bool = False) -> Union[Optional[str], List[str]]:
return await Selector._select_from_list(
options,
label_func=str,
allow_multiple=allow_multiple,
prompt_each=prompt_each,
header="Available Options:"
)
@staticmethod
async def select_int(options: List[int], allow_multiple: bool = False, prompt_each: bool = False) -> Union[Optional[int], List[int]]:
return await Selector._select_from_list(
options,
label_func=lambda x: str(x),
allow_multiple=allow_multiple,
prompt_each=prompt_each,
header="Available Integers:"
)
@staticmethod
async def select_value(prompt: str, value_type: type = int, valid_range: Optional[tuple] = None, allow_quit: bool = False) -> Optional[Any]:
while True:
user_input = (await get_sanitized_input(prompt)).strip().lower()
if allow_quit and user_input == "q":
logger.debug("User opted to quit value selection.")
return None
try:
value = value_type(user_input)
if valid_range:
min_val, max_val = valid_range
if not (min_val <= value <= max_val):
logger.warning(f"Value out of range ({min_val}{max_val}).")
continue
logger.debug(f"User selected value: {value}")
return value
except ValueError:
logger.warning(f"Invalid input. Expected a {value_type.__name__}.")
@staticmethod
async def confirm(prompt: str = "Are you sure? (Y/N): ") -> bool:
while True:
response = (await get_sanitized_input(prompt)).strip().lower()
if response in ["y", "yes"]:
logger.debug("User confirmed action.")
return True
elif response in ["n", "no"]:
logger.debug("User declined action.")
return False
else:
logger.warning("Invalid confirmation input. Expected 'Y' or 'N'.")