507 lines
23 KiB
Python
507 lines
23 KiB
Python
"""
|
|
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 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()
|
|
|
|
# Ask if they want to schedule by name or use round-robin
|
|
print(f"\n{Colors.HEADER}Scheduling Options{Colors.RESET}")
|
|
print(f"{Colors.GREY}─" * 50 + f"{Colors.RESET}")
|
|
print()
|
|
print(f" {Colors.CYAN}1.{Colors.RESET} {Colors.YELLOW}Round-robin scheduling{Colors.RESET} (choose next available member)")
|
|
print(f" {Colors.CYAN}2.{Colors.RESET} {Colors.GREEN}Schedule by name{Colors.RESET} (choose specific member)")
|
|
print()
|
|
|
|
schedule_choice = get_user_choice(2)
|
|
clear_screen()
|
|
|
|
if schedule_choice == 1:
|
|
# Round-robin scheduling
|
|
cmd_schedules_schedule(cli, MockArgs(service_id=None, date=date, classifications=None, member_name=None))
|
|
else:
|
|
# Name-based scheduling
|
|
member_name = get_text_input("Enter member name to search for (first, last, or both)", True)
|
|
if member_name:
|
|
clear_screen()
|
|
cmd_schedules_schedule(cli, MockArgs(service_id=None, date=date, classifications=None, member_name=member_name))
|
|
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 |