# Copyright (C) 2025 James Brotosky, Brandon Wickline # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import logging from typing import Any, List, Optional, Union logger = logging.getLogger(__name__) from typing import List, Optional, Union, Any, Callable import logging logger = logging.getLogger(__name__) class Selector: @staticmethod def _display_choices( items: List[Any], label_func: Callable[[Any], str], num_columns: int = 4, 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 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 = 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(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 def select_objects( objects: List[Any], allow_multiple: bool = False, prompt_each: bool = False ) -> Union[Optional[Any], List[Any]]: return 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 def select_string( options: List[str], allow_multiple: bool = False, prompt_each: bool = False ) -> Union[Optional[str], List[str]]: return Selector._select_from_list( options, label_func=str, allow_multiple=allow_multiple, prompt_each=prompt_each, header="Available Options:" ) @staticmethod def select_int( options: List[int], allow_multiple: bool = False, prompt_each: bool = False ) -> Union[Optional[int], List[int]]: return Selector._select_from_list( options, label_func=lambda x: str(x), allow_multiple=allow_multiple, prompt_each=prompt_each, header="Available Integers:" ) @staticmethod def select_value( prompt: str, value_type: type = int, valid_range: Optional[tuple] = None, allow_quit: bool = False ) -> Optional[Any]: while True: user_input = input(prompt).strip().lower() if allow_quit and user_input == "q": logger.info("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.info(f"User selected value: {value}") return value except ValueError: logger.warning(f"Invalid input. Expected a {value_type.__name__}.") @staticmethod def confirm(prompt: str = "Are you sure? (Y/N): ") -> bool: while True: response = input(prompt).strip().lower() if response in ["y", "yes"]: logger.info("User confirmed action.") return True elif response in ["n", "no"]: logger.info("User declined action.") return False else: logger.warning("Invalid confirmation input. Expected 'Y' or 'N'.")