""" Interactive CLI interface for NimbusFlow. """ import sys import time from pathlib import Path from typing import TYPE_CHECKING if TYPE_CHECKING: from .base import NimbusFlowCLI from .commands import ( cmd_members_list, cmd_members_show, cmd_schedules_list, cmd_schedules_show, cmd_schedules_accept, cmd_schedules_decline, cmd_schedules_remove, cmd_schedules_schedule, cmd_services_list, ) # ANSI color codes class Colors: RESET = '\033[0m' BOLD = '\033[1m' DIM = '\033[2m' BLUE = '\033[94m' GREEN = '\033[92m' YELLOW = '\033[93m' RED = '\033[91m' CYAN = '\033[96m' WHITE = '\033[97m' GREY = '\033[90m' BG_GREY = '\033[100m' # Special combinations HEADER = '\033[1m\033[96m' # Bold Cyan SUCCESS = '\033[1m\033[92m' # Bold Green ERROR = '\033[1m\033[91m' # Bold Red WARNING = '\033[1m\033[93m' # Bold Yellow INPUT_BOX = '\033[90m' # Grey # Gold shimmer colors GOLD_DARK = '\033[38;5;130m' # Dark gold GOLD_MEDIUM = '\033[38;5;178m' # Medium gold GOLD_BRIGHT = '\033[38;5;220m' # Bright gold GOLD_SHINE = '\033[1m\033[38;5;226m' # Bright shining gold GOLD_WHITE = '\033[1m\033[97m' # Bright white for peak shine def create_input_box(prompt: str, width: int = 60) -> str: """Create a grey box around input prompt.""" box_top = f"{Colors.INPUT_BOX}┌" + "─" * (width - 2) + f"┐{Colors.RESET}" prompt_line = f"{Colors.INPUT_BOX}│{Colors.RESET} {prompt}" padding = width - len(prompt) - 3 if padding > 0: prompt_line += " " * padding + f"{Colors.INPUT_BOX}│{Colors.RESET}" else: prompt_line += f" {Colors.INPUT_BOX}│{Colors.RESET}" box_bottom = f"{Colors.INPUT_BOX}└" + "─" * (width - 2) + f"┘{Colors.RESET}" return f"\n{box_top}\n{prompt_line}\n{box_bottom}" def create_simple_input_box(prompt: str) -> str: """Create a simple grey box around input prompt.""" return f"\n{Colors.INPUT_BOX}┌─ {prompt} ─┐{Colors.RESET}" def clear_screen(): """Clear the terminal screen.""" print("\033[2J\033[H") def get_shimmer_color(position: int, shimmer_center: int, shimmer_width: int = 8) -> str: """Get the appropriate shimmer color based on distance from shimmer center.""" distance = abs(position - shimmer_center) if distance == 0: return Colors.GOLD_WHITE elif distance == 1: return Colors.GOLD_SHINE elif distance <= 3: return Colors.GOLD_BRIGHT elif distance <= 5: return Colors.GOLD_MEDIUM elif distance <= shimmer_width: return Colors.GOLD_DARK else: return Colors.GOLD_DARK def animate_nimbusflow_text() -> None: """Animate the NimbusFlow ASCII text and frame with a gold shimmer effect.""" # Complete welcome screen lines including borders welcome_lines = [ "╔════════════════════════════════════════════════════════════════════════════════════════════╗", "║ ║", "║ ███╗ ██╗██╗███╗ ███╗██████╗ ██╗ ██╗███████╗ ███████╗██╗ ██████╗ ██╗ ██╗ ║", "║ ████╗ ██║██║████╗ ████║██╔══██╗██║ ██║██╔════╝ ██╔════╝██║ ██╔═══██╗██║ ██║ ║", "║ ██╔██╗ ██║██║██╔████╔██║██████╔╝██║ ██║███████╗ █████╗ ██║ ██║ ██║██║ █╗ ██║ ║", "║ ██║╚██╗██║██║██║╚██╔╝██║██╔══██╗██║ ██║╚════██║ ██╔══╝ ██║ ██║ ██║██║███╗██║ ║", "║ ██║ ╚████║██║██║ ╚═╝ ██║██████╔╝╚██████╔╝███████║ ██║ ███████╗╚██████╔╝╚███╔███╔╝ ║", "║ ╚═╝ ╚═══╝╚═╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚══════╝ ╚═════╝ ╚══╝╚══╝ ║", "║ ║", "╚════════════════════════════════════════════════════════════════════════════════════════════╝" ] # Calculate max width for animation max_width = max(len(line) for line in welcome_lines) # Animation parameters shimmer_width = 12 total_steps = max_width + shimmer_width * 2 step_delay = 0.025 # Seconds between frames (even faster animation) # Animate the shimmer effect for step in range(total_steps): shimmer_center = step - shimmer_width # Move cursor up to overwrite previous frame (10 lines total) if step > 0: print(f"\033[{len(welcome_lines)}A", end="") for line in welcome_lines: for i, char in enumerate(line): if char.isspace(): print(char, end="") else: color = get_shimmer_color(i, shimmer_center, shimmer_width) print(f"{color}{char}{Colors.RESET}", end="") print() # New line after each row # Add a small delay for animation time.sleep(step_delay) def display_welcome(): """Display welcome screen with animated shimmer effect.""" print("\033[2J\033[H") # Clear screen and move cursor to top print() # Add some top padding animate_nimbusflow_text() print() def display_main_menu(): """Display the main menu options.""" print(f"{Colors.HEADER}Main Menu{Colors.RESET}") print(f"{Colors.GREY}─" * 50 + f"{Colors.RESET}") print() print(f" {Colors.CYAN}1.{Colors.RESET} {Colors.BOLD}Members{Colors.RESET} Manage choir members") print(f" {Colors.CYAN}2.{Colors.RESET} {Colors.BOLD}Schedules{Colors.RESET} View and manage schedules") print(f" {Colors.CYAN}3.{Colors.RESET} {Colors.BOLD}Services{Colors.RESET} Manage services and events") print(f" {Colors.CYAN}4.{Colors.RESET} {Colors.BOLD}Exit{Colors.RESET} Close NimbusFlow CLI") print() def display_members_menu(): """Display members submenu.""" print(f"\n{Colors.HEADER}Members{Colors.RESET}") print(f"{Colors.GREY}─" * 50 + f"{Colors.RESET}") print() print(f" {Colors.CYAN}1.{Colors.RESET} List all members") print(f" {Colors.CYAN}2.{Colors.RESET} List active members only") print(f" {Colors.CYAN}3.{Colors.RESET} List by classification") print(f" {Colors.CYAN}4.{Colors.RESET} Show member details") print(f" {Colors.CYAN}5.{Colors.RESET} {Colors.DIM}Back to main menu{Colors.RESET}") print() def display_schedules_menu(): """Display schedules submenu.""" print(f"\n{Colors.HEADER}Schedules{Colors.RESET}") print(f"{Colors.GREY}─" * 50 + f"{Colors.RESET}") print() print(f" {Colors.CYAN}1.{Colors.RESET} Browse schedules") print(f" {Colors.CYAN}2.{Colors.RESET} {Colors.GREEN}Accept a schedule{Colors.RESET}") print(f" {Colors.CYAN}3.{Colors.RESET} {Colors.RED}Decline a schedule{Colors.RESET}") print(f" {Colors.CYAN}4.{Colors.RESET} {Colors.ERROR}Remove scheduled members{Colors.RESET}") print(f" {Colors.CYAN}5.{Colors.RESET} {Colors.YELLOW}Schedule next member for service{Colors.RESET}") print(f" {Colors.CYAN}6.{Colors.RESET} {Colors.DIM}Back to main menu{Colors.RESET}") print() def display_services_menu(): """Display services submenu.""" print(f"\n{Colors.HEADER}Services{Colors.RESET}") print(f"{Colors.GREY}─" * 50 + f"{Colors.RESET}") print() print(f" {Colors.CYAN}1.{Colors.RESET} List all services") print(f" {Colors.CYAN}2.{Colors.RESET} List upcoming services") print(f" {Colors.CYAN}3.{Colors.RESET} List services by date") print(f" {Colors.CYAN}4.{Colors.RESET} {Colors.DIM}Back to main menu{Colors.RESET}") print() def get_user_choice(max_options: int) -> int: """Get user menu choice with validation.""" while True: try: print(create_simple_input_box(f"Enter your choice (1-{max_options})")) choice = input(f"{Colors.INPUT_BOX}└─> {Colors.RESET}").strip() if not choice: continue choice_int = int(choice) if 1 <= choice_int <= max_options: return choice_int else: print(f"{Colors.ERROR}Please enter a number between 1 and {max_options}{Colors.RESET}") except ValueError: print(f"{Colors.ERROR}Please enter a valid number{Colors.RESET}") except (KeyboardInterrupt, EOFError): print(f"\n{Colors.WARNING}Goodbye!{Colors.RESET}") sys.exit(0) def get_text_input(prompt: str, required: bool = True) -> str: """Get text input from user.""" while True: try: print(create_simple_input_box(prompt)) value = input(f"{Colors.INPUT_BOX}└─> {Colors.RESET}").strip() if value or not required: return value print(f"{Colors.ERROR}This field is required{Colors.RESET}") except (KeyboardInterrupt, EOFError): print(f"\n{Colors.WARNING}Operation cancelled{Colors.RESET}") return "" def get_date_input(prompt: str = "Enter date (YYYY-MM-DD)") -> str: """Get date input from user.""" while True: try: print(create_simple_input_box(prompt)) date_str = input(f"{Colors.INPUT_BOX}└─> {Colors.RESET}").strip() if not date_str: print(f"{Colors.ERROR}Date is required{Colors.RESET}") continue # Basic date format validation if len(date_str) == 10 and date_str.count('-') == 2: parts = date_str.split('-') if len(parts[0]) == 4 and len(parts[1]) == 2 and len(parts[2]) == 2: return date_str print(f"{Colors.ERROR}Please use format YYYY-MM-DD (e.g., 2025-09-07){Colors.RESET}") except (KeyboardInterrupt, EOFError): print(f"\n{Colors.WARNING}Operation cancelled{Colors.RESET}") return "" def get_date_input_optional(prompt: str = "Enter date (YYYY-MM-DD)") -> str: """Get optional date input from user (allows empty input).""" while True: try: print(create_simple_input_box(prompt)) date_str = input(f"{Colors.INPUT_BOX}└─> {Colors.RESET}").strip() if not date_str: return "" # Allow empty input # Basic date format validation if len(date_str) == 10 and date_str.count('-') == 2: parts = date_str.split('-') if len(parts[0]) == 4 and len(parts[1]) == 2 and len(parts[2]) == 2: return date_str print(f"{Colors.ERROR}Please use format YYYY-MM-DD (e.g., 2025-09-07){Colors.RESET}") except (KeyboardInterrupt, EOFError): print(f"\n{Colors.WARNING}Operation cancelled{Colors.RESET}") return "" class MockArgs: """Mock args object for interactive commands.""" def __init__(self, **kwargs): for key, value in kwargs.items(): setattr(self, key, value) def handle_members_menu(cli: "NimbusFlowCLI"): """Handle members menu interactions.""" while True: clear_screen() display_members_menu() choice = get_user_choice(5) if choice == 1: # List all members clear_screen() print(f"{Colors.SUCCESS}Listing all members...{Colors.RESET}\n") cmd_members_list(cli, MockArgs(active=False, classification=None)) input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}") elif choice == 2: # List active members clear_screen() print(f"{Colors.SUCCESS}Listing active members...{Colors.RESET}\n") cmd_members_list(cli, MockArgs(active=True, classification=None)) input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}") elif choice == 3: # List by classification clear_screen() classification = get_text_input("Enter classification (Soprano, Alto / Mezzo, Tenor, Baritone)", True) if classification: clear_screen() print(f"{Colors.SUCCESS}Listing {classification} members...{Colors.RESET}\n") cmd_members_list(cli, MockArgs(active=False, classification=classification)) input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}") elif choice == 4: # Show member details clear_screen() member_id = get_text_input("Enter member ID", True) if member_id.isdigit(): clear_screen() print(f"{Colors.SUCCESS}Showing details for member {member_id}...{Colors.RESET}\n") cmd_members_show(cli, MockArgs(member_id=int(member_id))) else: print(f"{Colors.ERROR}Invalid member ID{Colors.RESET}") input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}") elif choice == 5: # Back to main menu break def handle_schedules_menu(cli: "NimbusFlowCLI"): """Handle schedules menu interactions.""" while True: clear_screen() display_schedules_menu() choice = get_user_choice(6) if choice == 1: # List all schedules clear_screen() # Get date filter date = get_date_input_optional("Enter date to filter schedules (or press Enter to skip)") if not date: clear_screen() cmd_schedules_list(cli, MockArgs(service_id=None, status=None)) else: # Find services for the date try: from datetime import date as date_type target_date = date_type.fromisoformat(date) all_services = cli.service_repo.list_all() services_on_date = [s for s in all_services if s.ServiceDate == target_date] except ValueError: clear_screen() print(f"{Colors.ERROR}Invalid date format. Please use YYYY-MM-DD format.{Colors.RESET}") services_on_date = [] if not services_on_date: clear_screen() print(f"{Colors.ERROR}No services found for {date}{Colors.RESET}") else: clear_screen() # Show available services for selection service_type_map = {st.ServiceTypeId: st.TypeName for st in cli.service_type_repo.list_all()} print(f"\n{Colors.HEADER}Services available on {date}{Colors.RESET}") print(f"{Colors.GREY}─" * 50 + f"{Colors.RESET}") print() for i, service in enumerate(services_on_date, 1): type_name = service_type_map.get(service.ServiceTypeId, "Unknown") print(f" {Colors.CYAN}{i}.{Colors.RESET} {type_name}") print() # Get service selection try: print(create_simple_input_box(f"Select service (1-{len(services_on_date)}) or press Enter to show all")) choice_input = input(f"{Colors.INPUT_BOX}└─> {Colors.RESET}").strip() if not choice_input: # Empty input - show all services for this date clear_screen() cmd_schedules_list(cli, MockArgs(service_id=None, status=None, date=date)) elif not choice_input.isdigit(): print(f"{Colors.ERROR}Invalid selection{Colors.RESET}") else: service_choice = int(choice_input) if service_choice < 1 or service_choice > len(services_on_date): print(f"{Colors.ERROR}Please enter a number between 1 and {len(services_on_date)}{Colors.RESET}") else: clear_screen() selected_service = services_on_date[service_choice - 1] cmd_schedules_list(cli, MockArgs(service_id=selected_service.ServiceId, status=None)) except (KeyboardInterrupt, EOFError): print(f"\n{Colors.WARNING}Operation cancelled{Colors.RESET}") input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}") elif choice == 2: # Accept schedule clear_screen() date = get_date_input("Enter date for interactive accept") if date: clear_screen() cmd_schedules_accept(cli, MockArgs(date=date, schedule_id=None)) input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}") elif choice == 3: # Decline schedule clear_screen() date = get_date_input("Enter date for interactive decline") if date: clear_screen() reason = get_text_input("Enter decline reason (optional)", False) clear_screen() cmd_schedules_decline(cli, MockArgs(date=date, schedule_id=None, reason=reason)) input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}") elif choice == 4: # Remove scheduled members clear_screen() date = get_date_input("Enter date to remove schedules for") if date: clear_screen() cmd_schedules_remove(cli, MockArgs(date=date, schedule_id=None)) input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}") elif choice == 5: # Schedule next member clear_screen() date = get_date_input("Enter date to schedule for") if date: clear_screen() cmd_schedules_schedule(cli, MockArgs(service_id=None, date=date, classifications=None)) input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}") elif choice == 6: # Back to main menu break def handle_services_menu(cli: "NimbusFlowCLI"): """Handle services menu interactions.""" while True: clear_screen() display_services_menu() choice = get_user_choice(4) if choice == 1: # List all services clear_screen() print(f"{Colors.SUCCESS}Listing all services...{Colors.RESET}\n") cmd_services_list(cli, MockArgs(date=None, upcoming=False, limit=None)) input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}") elif choice == 2: # List upcoming services clear_screen() print(f"{Colors.WARNING}Listing upcoming services...{Colors.RESET}\n") cmd_services_list(cli, MockArgs(date=None, upcoming=True, limit=20)) input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}") elif choice == 3: # List by date clear_screen() date = get_date_input("Enter date to filter services") if date: clear_screen() print(f"{Colors.SUCCESS}Listing services for {date}...{Colors.RESET}\n") cmd_services_list(cli, MockArgs(date=date, upcoming=False, limit=None)) input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}") elif choice == 4: # Back to main menu break def run_interactive_mode(cli: "NimbusFlowCLI"): """Run the main interactive CLI mode.""" display_welcome() print(f"{Colors.HEADER}Welcome to the NimbusFlow Interactive CLI{Colors.RESET}") print(f"{Colors.DIM}Navigate through menus to manage your scheduling system.{Colors.RESET}") print() input(f"{Colors.DIM}Press Enter to continue...{Colors.RESET}") while True: clear_screen() # Clear screen display_main_menu() choice = get_user_choice(4) if choice == 1: # Members handle_members_menu(cli) elif choice == 2: # Schedules handle_schedules_menu(cli) elif choice == 3: # Services handle_services_menu(cli) elif choice == 4: # Exit clear_screen() print(f"\n{Colors.SUCCESS}Thank you for using NimbusFlow!{Colors.RESET}") print(f"{Colors.DIM}Goodbye!{Colors.RESET}") break