# 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__) class Selector: @staticmethod def select_objects( objects: List[Any], allow_multiple: bool = False, prompt_each: bool = False ) -> Union[Optional[Any], List[Any]]: if not objects: logger.warning("No objects available for selection.") return None # Sort objects alphabetically by their 'name' attribute sorted_objects = sorted(objects, key=lambda obj: getattr(obj, "name", str(obj)).lower()) # Display in 4 columns with extra spacing num_columns = 4 rows = (len(sorted_objects) + num_columns - 1) // num_columns print("\nAvailable Choices:") for row in range(rows): line = "" for col in range(num_columns): idx = row + col * rows if idx < len(sorted_objects): obj = sorted_objects[idx] name = getattr(obj, "name", str(obj)) line += f"{idx + 1}: {name:<30}" print(line) selected = [] if allow_multiple: while True: choice = input("Select an object by number (or Q to finish): ").strip().lower() if choice == "q": break try: index = int(choice) if 1 <= index <= len(sorted_objects): obj = sorted_objects[index - 1] if obj not in selected: selected.append(obj) if prompt_each: logger.info(f"Selected: {getattr(obj, 'name', str(obj))}") else: logger.warning("Object 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 object by number: ")) if 1 <= choice <= len(sorted_objects): selected_obj = sorted_objects[choice - 1] logger.info(f"Selected: {getattr(selected_obj, 'name', str(selected_obj))}") return selected_obj else: logger.warning("Selection out of range.") except ValueError: logger.warning("Invalid input.") return None @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'.")