""" Interactive repository selector for migration tool """ import sys import termios import tty from typing import List, Set from colorama import Fore, Style, init from providers.base import Repository init() class InteractiveSelector: """Interactive repository selector with keyboard navigation""" def __init__(self, repositories: List[Repository], username: str): self.username = username # Sort repositories: user's repos first, then others, both alphabetically self.repositories = self._sort_repositories(repositories, username) # Only select user's own repositories by default self.selected = set(i for i, repo in enumerate(self.repositories) if repo.owner == username) self.current_index = 0 self.page_size = 15 # Number of repos to show per page self.current_page = 0 def get_key(self): """Get a single keypress from stdin""" fd = sys.stdin.fileno() old_settings = termios.tcgetattr(fd) try: tty.setraw(sys.stdin.fileno()) key = sys.stdin.read(1) # Handle arrow keys and special keys if key == '\x1b': # ESC sequence key += sys.stdin.read(2) finally: termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) return key def display_page(self): """Display current page of repositories""" # Clear screen print('\033[2J\033[H', end='') # Header print(f"{Fore.CYAN}╔═══════════════════════════════════════════════════════════════╗") print(f"║ 📋 SELECT REPOSITORIES ║") print(f"║ ║") print(f"║ 👤 = Your repos (selected by default) 👥 = Others' repos ║") print(f"║ ↑↓ navigate, SPACE toggle, A all, N none, ENTER confirm ║") print(f"╚═══════════════════════════════════════════════════════════════╝{Style.RESET_ALL}") print() # Calculate pagination start_idx = self.current_page * self.page_size end_idx = min(start_idx + self.page_size, len(self.repositories)) # Show page info total_pages = (len(self.repositories) + self.page_size - 1) // self.page_size selected_count = len(self.selected) total_count = len(self.repositories) print(f"{Fore.YELLOW}📊 Page {self.current_page + 1}/{total_pages} | " f"Selected: {selected_count}/{total_count} repositories{Style.RESET_ALL}") print() # Display repositories for current page last_owner_type = None # Track if we're switching from user repos to others for i in range(start_idx, end_idx): repo = self.repositories[i] is_selected = i in self.selected is_current = i == self.current_index # Check if we need to add a separator owner = repo.owner is_own_repo = owner == self.username current_owner_type = "own" if is_own_repo else "others" # Add separator when transitioning from own repos to others if last_owner_type == "own" and current_owner_type == "others": print(f" {Fore.LIGHTBLACK_EX}{'─' * 50} Autres repositories {'─' * 10}{Style.RESET_ALL}") last_owner_type = current_owner_type # Checkbox checkbox = "☑️ " if is_selected else "☐ " # Repository info name = repo.name private = "🔒" if repo.private else "🌐" ownership_indicator = "👤" if is_own_repo else "👥" description = (repo.description or 'No description')[:45] if len(repo.description or '') > 45: description += "..." # Highlight current selection if is_current: line = f"{Fore.BLACK}{Style.BRIGHT}> {checkbox}{ownership_indicator} {Fore.BLUE}{owner}/{name}{Style.RESET_ALL}" line += f"{Fore.BLACK}{Style.BRIGHT} {private} - {description}{Style.RESET_ALL}" else: if is_own_repo: color = Fore.GREEN if is_selected else Fore.WHITE else: color = Fore.YELLOW if is_selected else Fore.LIGHTBLACK_EX line = f" {checkbox}{ownership_indicator} {color}{owner}/{name}{Style.RESET_ALL}" line += f" {private} - {Fore.LIGHTBLACK_EX}{description}{Style.RESET_ALL}" print(line) # Navigation help at bottom print() nav_help = [] if self.current_page > 0: nav_help.append("← PREV PAGE") if self.current_page < total_pages - 1: nav_help.append("→ NEXT PAGE") if nav_help: print(f"{Fore.CYAN}Navigation: {' | '.join(nav_help)}{Style.RESET_ALL}") print(f"\n{Fore.GREEN}Press ENTER to continue with selected repositories{Style.RESET_ALL}") print(f"{Fore.RED}Press Q to quit{Style.RESET_ALL}") def move_up(self): """Move selection up""" if self.current_index > 0: self.current_index -= 1 # Check if we need to go to previous page if self.current_index < self.current_page * self.page_size: self.current_page = max(0, self.current_page - 1) def move_down(self): """Move selection down""" if self.current_index < len(self.repositories) - 1: self.current_index += 1 # Check if we need to go to next page total_pages = (len(self.repositories) + self.page_size - 1) // self.page_size if self.current_index >= (self.current_page + 1) * self.page_size: self.current_page = min(total_pages - 1, self.current_page + 1) def toggle_current(self): """Toggle selection of current repository""" if self.current_index in self.selected: self.selected.remove(self.current_index) else: self.selected.add(self.current_index) def select_all(self): """Select all repositories""" self.selected = set(range(len(self.repositories))) def select_none(self): """Deselect all repositories""" self.selected.clear() def prev_page(self): """Go to previous page""" if self.current_page > 0: self.current_page -= 1 self.current_index = self.current_page * self.page_size def next_page(self): """Go to next page""" total_pages = (len(self.repositories) + self.page_size - 1) // self.page_size if self.current_page < total_pages - 1: self.current_page += 1 self.current_index = self.current_page * self.page_size def _sort_repositories(self, repositories: List[Repository], username: str) -> List[Repository]: """Sort repositories: user's repos first, then others, both alphabetically""" def sort_key(repo): owner = repo.owner name = repo.name.lower() # Case-insensitive sorting is_user_repo = owner == username # Return tuple: (is_not_user_repo, owner.lower(), name) # This will sort user repos first (False < True), then alphabetically return (not is_user_repo, owner.lower(), name) return sorted(repositories, key=sort_key) def _rename_repositories_interface(self, selected_repos: List[Repository]) -> List[Repository]: """Interface for renaming selected repositories""" print('\033[2J\033[H', end='') # Clear screen print(f"{Fore.CYAN}╔═══════════════════════════════════════════════════════════════╗") print(f"║ ✏️ RENAME REPOSITORIES ║") print(f"║ ║") print(f"║ Press ENTER to keep current name, or type new name ║") print(f"║ Repository names should be valid GitHub repo names ║") print(f"╚═══════════════════════════════════════════════════════════════╝{Style.RESET_ALL}") print() renamed_repos = [] for i, repo in enumerate(selected_repos, 1): owner = repo.owner original_name = repo.name private = "🔒" if repo.private else "🌐" print(f"{Fore.YELLOW}📦 Repository {i}/{len(selected_repos)}:{Style.RESET_ALL}") print(f" Source: {Fore.BLUE}{owner}/{original_name}{Style.RESET_ALL} {private}") # Get new name from user new_name = input(f" GitHub name [{Fore.GREEN}{original_name}{Style.RESET_ALL}]: ").strip() # Validate and use new name if new_name: # Basic validation if not self._is_valid_repo_name(new_name): print(f" {Fore.RED}⚠️ Invalid repository name. Using original name: {original_name}{Style.RESET_ALL}") new_name = original_name else: print(f" {Fore.GREEN}✅ Will rename to: {new_name}{Style.RESET_ALL}") else: new_name = original_name print(f" {Fore.CYAN}ℹ️ Keeping original name: {original_name}{Style.RESET_ALL}") # Update repository with new GitHub name repo.github_name = new_name renamed_repos.append(repo) print() # Summary print(f"{Fore.GREEN}✅ Repository renaming complete!{Style.RESET_ALL}") print(f"\n{Fore.CYAN}📋 Migration summary:{Style.RESET_ALL}") for repo in renamed_repos: owner = repo.owner original_name = repo.name github_name = repo.github_name private = "🔒" if repo.private else "🌐" if original_name != github_name: print(f" • {Fore.BLUE}{owner}/{original_name}{Style.RESET_ALL} → {Fore.GREEN}{github_name}{Style.RESET_ALL} {private}") else: print(f" • {Fore.BLUE}{owner}/{original_name}{Style.RESET_ALL} {private}") input(f"\n{Fore.YELLOW}Press ENTER to continue...{Style.RESET_ALL}") return renamed_repos def _is_valid_repo_name(self, name: str) -> bool: """Validate GitHub repository name""" if not name: return False # GitHub repo name rules (simplified) if len(name) > 100: return False # Should not start or end with special characters if name.startswith('.') or name.startswith('-') or name.endswith('.'): return False # Should contain only alphanumeric, hyphens, underscores, and dots import re return bool(re.match(r'^[a-zA-Z0-9._-]+$', name)) def run(self) -> List[Repository]: """Run interactive selection and return selected repositories""" if not self.repositories: print(f"{Fore.YELLOW}⚠️ No repositories found.{Style.RESET_ALL}") return [] try: while True: self.display_page() key = self.get_key() if key == '\x1b[A': # Up arrow self.move_up() elif key == '\x1b[B': # Down arrow self.move_down() elif key == '\x1b[D': # Left arrow (previous page) self.prev_page() elif key == '\x1b[C': # Right arrow (next page) self.next_page() elif key == ' ': # Space - toggle selection self.toggle_current() elif key.lower() == 'a': # Select all self.select_all() elif key.lower() == 'n': # Select none self.select_none() elif key == '\r' or key == '\n': # Enter - confirm break elif key.lower() == 'q': # Quit print(f"\n{Fore.YELLOW}🚪 Migration cancelled by user.{Style.RESET_ALL}") sys.exit(0) except KeyboardInterrupt: print(f"\n{Fore.YELLOW}🚪 Migration cancelled by user.{Style.RESET_ALL}") sys.exit(0) # Return selected repositories selected_repos = [self.repositories[i] for i in sorted(self.selected)] # Clear screen and show summary print('\033[2J\033[H', end='') print(f"{Fore.GREEN}✅ Selected {len(selected_repos)} repositories for migration:{Style.RESET_ALL}\n") for repo in selected_repos: owner = repo.owner name = repo.name private = "🔒" if repo.private else "🌐" print(f" • {Fore.BLUE}{owner}/{name}{Style.RESET_ALL} {private}") # Ask if user wants to rename repositories print(f"\n{Fore.YELLOW}📝 Voulez-vous changer le nom de certains repos sur GitHub ?{Style.RESET_ALL}") print(f"{Fore.CYAN}[Y/y] Oui - Interface de renommage{Style.RESET_ALL}") print(f"{Fore.CYAN}[N/n ou ENTER] Non - Conserver les noms actuels{Style.RESET_ALL}") choice = input(f"\n{Fore.YELLOW}Votre choix: {Style.RESET_ALL}").strip().lower() if choice == 'y' or choice == 'yes' or choice == 'oui': selected_repos = self._rename_repositories_interface(selected_repos) print(f"\n{Fore.CYAN}🚀 Starting migration...{Style.RESET_ALL}\n") return selected_repos def select_repositories_interactive(repositories: List[Repository], username: str) -> List[Repository]: """ Interactive repository selection interface Args: repositories: List of Repository objects from source provider username: Current user's username to distinguish own repos Returns: List of selected repositories """ selector = InteractiveSelector(repositories, username) return selector.run()