You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
223 lines
9.1 KiB
223 lines
9.1 KiB
"""
|
|
Interactive repository selector for migration tool
|
|
"""
|
|
import sys
|
|
import termios
|
|
import tty
|
|
from typing import List, Dict, Set
|
|
from colorama import Fore, Style, init
|
|
|
|
init()
|
|
|
|
class InteractiveSelector:
|
|
"""Interactive repository selector with keyboard navigation"""
|
|
|
|
def __init__(self, repositories: List[Dict], username: str):
|
|
self.repositories = repositories
|
|
self.username = username
|
|
# Only select user's own repositories by default
|
|
self.selected = set(i for i, repo in enumerate(repositories)
|
|
if repo['owner']['login'] == 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
|
|
for i in range(start_idx, end_idx):
|
|
repo = self.repositories[i]
|
|
is_selected = i in self.selected
|
|
is_current = i == self.current_index
|
|
|
|
# Checkbox
|
|
checkbox = "☑️ " if is_selected else "☐ "
|
|
|
|
# Repository info
|
|
owner = repo['owner']['login']
|
|
name = repo['name']
|
|
private = "🔒" if repo.get('private', False) else "🌐"
|
|
is_own_repo = owner == self.username
|
|
ownership_indicator = "👤" if is_own_repo else "👥"
|
|
description = repo.get('description', 'No description')[:45]
|
|
if len(repo.get('description', '')) > 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 run(self) -> List[Dict]:
|
|
"""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']['login']
|
|
name = repo['name']
|
|
private = "🔒" if repo.get('private', False) else "🌐"
|
|
print(f" • {Fore.BLUE}{owner}/{name}{Style.RESET_ALL} {private}")
|
|
|
|
print(f"\n{Fore.CYAN}🚀 Starting migration...{Style.RESET_ALL}\n")
|
|
|
|
return selected_repos
|
|
|
|
|
|
def select_repositories_interactive(repositories: List[Dict], username: str) -> List[Dict]:
|
|
"""
|
|
Interactive repository selection interface
|
|
|
|
Args:
|
|
repositories: List of repository dictionaries from Gitea API
|
|
username: Current user's username to distinguish own repos
|
|
|
|
Returns:
|
|
List of selected repositories
|
|
"""
|
|
selector = InteractiveSelector(repositories, username)
|
|
return selector.run() |