644 lines
24 KiB
Python
Executable File
644 lines
24 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
API Key Manager TUI - Interactive Terminal Interface
|
||
Beautiful and intuitive key management interface
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import json
|
||
from pathlib import Path
|
||
from typing import Dict, List, Optional, Tuple
|
||
from datetime import datetime
|
||
import argparse
|
||
from collections import defaultdict
|
||
import subprocess
|
||
import shutil
|
||
from getpass import getpass
|
||
|
||
# Rich TUI components
|
||
from rich.console import Console
|
||
from rich.table import Table
|
||
from rich.panel import Panel
|
||
from rich.layout import Layout
|
||
from rich.live import Live
|
||
from rich.prompt import Prompt, Confirm, IntPrompt
|
||
from rich.text import Text
|
||
from rich.style import Style
|
||
from rich.columns import Columns
|
||
from rich.syntax import Syntax
|
||
from rich.tree import Tree
|
||
from rich import box
|
||
from rich.align import Align
|
||
from rich.padding import Padding
|
||
|
||
console = Console()
|
||
|
||
class KeyManagerTUI:
|
||
"""Interactive TUI for API Key Management"""
|
||
|
||
def __init__(self):
|
||
self.workspace_root = Path(__file__).parent.parent.parent.parent
|
||
self.keys_file = self.workspace_root / "config" / "consolidated_keys.json"
|
||
self.keys_file.parent.mkdir(parents=True, exist_ok=True)
|
||
self.current_view = "main"
|
||
self.selected_category = None
|
||
self.keys_data = self.load_keys()
|
||
|
||
def load_keys(self) -> Dict:
|
||
"""Load consolidated keys from JSON"""
|
||
if self.keys_file.exists():
|
||
with open(self.keys_file, 'r') as f:
|
||
return json.load(f)
|
||
return {"keys": {}, "consolidated_at": None, "total_keys": 0}
|
||
|
||
def save_keys(self):
|
||
"""Save keys back to JSON"""
|
||
self.keys_data["consolidated_at"] = datetime.now().isoformat()
|
||
self.keys_data["total_keys"] = sum(len(cat) for cat in self.keys_data.get("keys", {}).values())
|
||
with open(self.keys_file, 'w') as f:
|
||
json.dump(self.keys_data, f, indent=2)
|
||
|
||
def clear_screen(self):
|
||
"""Clear the terminal screen"""
|
||
os.system('clear' if os.name == 'posix' else 'cls')
|
||
|
||
def display_header(self):
|
||
"""Display the application header"""
|
||
header = Panel(
|
||
Align.center(
|
||
Text("🔐 API Key Manager", style="bold cyan", justify="center")
|
||
),
|
||
box=box.DOUBLE,
|
||
style="cyan",
|
||
padding=(1, 2)
|
||
)
|
||
console.print(header)
|
||
|
||
# Display status bar
|
||
if self.keys_data.get("consolidated_at"):
|
||
last_update = self.keys_data["consolidated_at"][:19].replace("T", " ")
|
||
status = f"📊 Total Keys: {self.keys_data.get('total_keys', 0)} | 📅 Last Update: {last_update}"
|
||
else:
|
||
status = "⚠️ No keys consolidated yet"
|
||
|
||
console.print(Panel(status, style="dim", box=box.MINIMAL))
|
||
|
||
def display_main_menu(self):
|
||
"""Display the main menu"""
|
||
menu_items = [
|
||
("1", "🔍 View Keys", "Browse keys by category"),
|
||
("2", "➕ Add Key", "Add a new API key"),
|
||
("3", "✏️ Edit Key", "Modify an existing key"),
|
||
("4", "🗑️ Delete Key", "Remove a key"),
|
||
("5", "🔄 Scan Projects", "Scan all projects for .env files"),
|
||
("6", "📤 Export Keys", "Export to .env format"),
|
||
("7", "📊 Statistics", "View key statistics"),
|
||
("8", "🔍 Search", "Search for a specific key"),
|
||
("9", "⚙️ Settings", "Configure settings"),
|
||
("0", "🚪 Exit", "Exit the application")
|
||
]
|
||
|
||
table = Table(
|
||
title="Main Menu",
|
||
box=box.ROUNDED,
|
||
show_header=False,
|
||
padding=(0, 2),
|
||
style="cyan"
|
||
)
|
||
table.add_column("Option", style="bold yellow", width=8)
|
||
table.add_column("Action", style="bold white", width=20)
|
||
table.add_column("Description", style="dim")
|
||
|
||
for option, action, desc in menu_items:
|
||
table.add_row(option, action, desc)
|
||
|
||
console.print(Padding(table, (1, 0)))
|
||
|
||
def view_keys(self):
|
||
"""View keys organized by category"""
|
||
self.clear_screen()
|
||
self.display_header()
|
||
|
||
if not self.keys_data.get("keys"):
|
||
console.print(Panel("⚠️ No keys found. Use option 5 to scan projects.", style="yellow"))
|
||
Prompt.ask("\nPress Enter to continue")
|
||
return
|
||
|
||
# Create a tree view
|
||
tree = Tree("📁 API Keys", style="bold cyan")
|
||
|
||
categories = self.keys_data.get("keys", {})
|
||
for category, keys in categories.items():
|
||
branch = tree.add(f"📂 {category.upper()} ({len(keys)} keys)", style="yellow")
|
||
|
||
# Show first 5 keys in each category
|
||
for i, (key_name, value) in enumerate(sorted(keys.items())[:5]):
|
||
masked_value = value[:8] + "..." if value and len(value) > 8 else value or "(empty)"
|
||
branch.add(f"🔑 {key_name}: {masked_value}", style="dim white")
|
||
|
||
if len(keys) > 5:
|
||
branch.add(f"... and {len(keys) - 5} more", style="dim italic")
|
||
|
||
console.print(Padding(tree, (1, 2)))
|
||
|
||
# Category selection
|
||
console.print("\n[bold]Select a category to view all keys:[/bold]")
|
||
cat_list = list(categories.keys())
|
||
for i, cat in enumerate(cat_list, 1):
|
||
console.print(f" {i}. {cat.upper()} ({len(categories[cat])} keys)")
|
||
|
||
console.print(" 0. Back to main menu")
|
||
|
||
choice = Prompt.ask("\nYour choice", default="0")
|
||
|
||
if choice.isdigit() and 0 < int(choice) <= len(cat_list):
|
||
self.view_category_keys(cat_list[int(choice) - 1])
|
||
|
||
def view_category_keys(self, category: str):
|
||
"""View all keys in a specific category"""
|
||
self.clear_screen()
|
||
self.display_header()
|
||
|
||
keys = self.keys_data.get("keys", {}).get(category, {})
|
||
|
||
console.print(Panel(f"Category: {category.upper()}", style="bold yellow"))
|
||
|
||
table = Table(
|
||
title=f"{len(keys)} Keys",
|
||
box=box.SIMPLE,
|
||
show_lines=True,
|
||
style="cyan"
|
||
)
|
||
table.add_column("#", style="dim", width=4)
|
||
table.add_column("Key Name", style="bold white", width=30)
|
||
table.add_column("Value", style="green")
|
||
|
||
for i, (key_name, value) in enumerate(sorted(keys.items()), 1):
|
||
display_value = value[:50] + "..." if value and len(value) > 50 else value or "(empty)"
|
||
table.add_row(str(i), key_name, display_value)
|
||
|
||
console.print(table)
|
||
|
||
Prompt.ask("\nPress Enter to continue")
|
||
|
||
def add_key(self):
|
||
"""Add a new API key"""
|
||
self.clear_screen()
|
||
self.display_header()
|
||
|
||
console.print(Panel("➕ Add New API Key", style="bold green"))
|
||
|
||
# Get key details
|
||
key_name = Prompt.ask("\n[bold]Key name[/bold]").strip().upper()
|
||
|
||
if not key_name:
|
||
console.print("[red]Invalid key name[/red]")
|
||
Prompt.ask("\nPress Enter to continue")
|
||
return
|
||
|
||
# Check if key exists
|
||
for category, keys in self.keys_data.get("keys", {}).items():
|
||
if key_name in keys:
|
||
console.print(f"[yellow]⚠️ Key '{key_name}' already exists in category '{category}'[/yellow]")
|
||
if not Confirm.ask("Do you want to update it?"):
|
||
return
|
||
|
||
# Get value (hidden input for security)
|
||
console.print("[dim]Enter key value (input will be hidden):[/dim]")
|
||
key_value = getpass("Value: ")
|
||
|
||
# Select category
|
||
categories = ["ai", "services", "database", "settings", "custom"]
|
||
console.print("\n[bold]Select category:[/bold]")
|
||
for i, cat in enumerate(categories, 1):
|
||
console.print(f" {i}. {cat}")
|
||
|
||
cat_choice = IntPrompt.ask("Category", default=5)
|
||
category = categories[cat_choice - 1] if 1 <= cat_choice <= len(categories) else "custom"
|
||
|
||
# Add to data structure
|
||
if "keys" not in self.keys_data:
|
||
self.keys_data["keys"] = {}
|
||
if category not in self.keys_data["keys"]:
|
||
self.keys_data["keys"][category] = {}
|
||
|
||
self.keys_data["keys"][category][key_name] = key_value
|
||
self.save_keys()
|
||
|
||
console.print(f"\n[green]✅ Key '{key_name}' added to category '{category}'[/green]")
|
||
Prompt.ask("\nPress Enter to continue")
|
||
|
||
def edit_key(self):
|
||
"""Edit an existing key"""
|
||
self.clear_screen()
|
||
self.display_header()
|
||
|
||
console.print(Panel("✏️ Edit API Key", style="bold yellow"))
|
||
|
||
# Search for key
|
||
search = Prompt.ask("\n[bold]Enter key name to edit (partial match OK)[/bold]").strip().upper()
|
||
|
||
matches = []
|
||
for category, keys in self.keys_data.get("keys", {}).items():
|
||
for key_name in keys:
|
||
if search in key_name:
|
||
matches.append((category, key_name, keys[key_name]))
|
||
|
||
if not matches:
|
||
console.print(f"[red]No keys found matching '{search}'[/red]")
|
||
Prompt.ask("\nPress Enter to continue")
|
||
return
|
||
|
||
if len(matches) == 1:
|
||
category, key_name, old_value = matches[0]
|
||
else:
|
||
# Multiple matches, let user choose
|
||
console.print(f"\n[bold]Found {len(matches)} matches:[/bold]")
|
||
for i, (cat, name, val) in enumerate(matches, 1):
|
||
masked = val[:8] + "..." if val and len(val) > 8 else val or "(empty)"
|
||
console.print(f" {i}. {name} ({cat}): {masked}")
|
||
|
||
choice = IntPrompt.ask("Select key to edit", default=1)
|
||
if 1 <= choice <= len(matches):
|
||
category, key_name, old_value = matches[choice - 1]
|
||
else:
|
||
return
|
||
|
||
# Show current value
|
||
console.print(f"\n[bold]Editing: {key_name}[/bold]")
|
||
console.print(f"Category: {category}")
|
||
console.print(f"Current value: {old_value[:20]}..." if old_value and len(old_value) > 20 else f"Current value: {old_value or '(empty)'}")
|
||
|
||
# Get new value
|
||
console.print("\n[dim]Enter new value (input will be hidden, leave empty to keep current):[/dim]")
|
||
new_value = getpass("New value: ")
|
||
|
||
if new_value:
|
||
self.keys_data["keys"][category][key_name] = new_value
|
||
self.save_keys()
|
||
console.print(f"\n[green]✅ Key '{key_name}' updated[/green]")
|
||
else:
|
||
console.print("\n[yellow]No changes made[/yellow]")
|
||
|
||
Prompt.ask("\nPress Enter to continue")
|
||
|
||
def delete_key(self):
|
||
"""Delete a key"""
|
||
self.clear_screen()
|
||
self.display_header()
|
||
|
||
console.print(Panel("🗑️ Delete API Key", style="bold red"))
|
||
|
||
# Search for key
|
||
search = Prompt.ask("\n[bold]Enter key name to delete (partial match OK)[/bold]").strip().upper()
|
||
|
||
matches = []
|
||
for category, keys in self.keys_data.get("keys", {}).items():
|
||
for key_name in keys:
|
||
if search in key_name:
|
||
matches.append((category, key_name))
|
||
|
||
if not matches:
|
||
console.print(f"[red]No keys found matching '{search}'[/red]")
|
||
Prompt.ask("\nPress Enter to continue")
|
||
return
|
||
|
||
if len(matches) == 1:
|
||
category, key_name = matches[0]
|
||
else:
|
||
# Multiple matches
|
||
console.print(f"\n[bold]Found {len(matches)} matches:[/bold]")
|
||
for i, (cat, name) in enumerate(matches, 1):
|
||
console.print(f" {i}. {name} ({cat})")
|
||
|
||
choice = IntPrompt.ask("Select key to delete", default=1)
|
||
if 1 <= choice <= len(matches):
|
||
category, key_name = matches[choice - 1]
|
||
else:
|
||
return
|
||
|
||
# Confirm deletion
|
||
if Confirm.ask(f"\n[bold red]Delete '{key_name}' from '{category}'?[/bold red]"):
|
||
del self.keys_data["keys"][category][key_name]
|
||
|
||
# Remove category if empty
|
||
if not self.keys_data["keys"][category]:
|
||
del self.keys_data["keys"][category]
|
||
|
||
self.save_keys()
|
||
console.print(f"\n[green]✅ Key '{key_name}' deleted[/green]")
|
||
else:
|
||
console.print("\n[yellow]Deletion cancelled[/yellow]")
|
||
|
||
Prompt.ask("\nPress Enter to continue")
|
||
|
||
def scan_projects(self):
|
||
"""Scan all projects for .env files"""
|
||
self.clear_screen()
|
||
self.display_header()
|
||
|
||
console.print(Panel("🔄 Scanning Projects for API Keys", style="bold cyan"))
|
||
|
||
projects = {
|
||
"root": self.workspace_root,
|
||
"trax": self.workspace_root / "apps" / "trax",
|
||
"youtube-summarizer": self.workspace_root / "apps" / "youtube-summarizer",
|
||
"pdf-translator": self.workspace_root / "pdf-translator",
|
||
"directus-mcp": self.workspace_root / "tools" / "directus-mcp-server",
|
||
}
|
||
|
||
all_keys = defaultdict(dict)
|
||
found_count = 0
|
||
|
||
with console.status("[bold green]Scanning projects...") as status:
|
||
for project_name, project_path in projects.items():
|
||
env_file = project_path / ".env"
|
||
if env_file.exists():
|
||
status.update(f"[bold green]Scanning {project_name}...")
|
||
|
||
with open(env_file, 'r') as f:
|
||
for line in f:
|
||
line = line.strip()
|
||
if line and not line.startswith('#'):
|
||
if '=' in line:
|
||
key, value = line.split('=', 1)
|
||
key = key.strip()
|
||
value = value.strip().strip('"').strip("'")
|
||
all_keys[key][project_name] = value
|
||
found_count += 1
|
||
|
||
console.print(f" ✅ {project_name}: Found keys")
|
||
else:
|
||
console.print(f" ⚠️ {project_name}: No .env file")
|
||
|
||
# Consolidate keys
|
||
console.print(f"\n[bold]Found {len(all_keys)} unique keys across {found_count} total entries[/bold]")
|
||
|
||
if Confirm.ask("\nDo you want to consolidate these keys?"):
|
||
# Organize by category
|
||
categorized = {
|
||
"ai": {},
|
||
"services": {},
|
||
"database": {},
|
||
"settings": {},
|
||
"custom": {}
|
||
}
|
||
|
||
ai_prefixes = ["ANTHROPIC", "DEEPSEEK", "OPENAI", "PERPLEXITY", "OPENROUTER",
|
||
"GOOGLE_API", "XAI", "MISTRAL", "QWEN", "DASHSCOPE", "MODEL_STUDIO"]
|
||
service_prefixes = ["SLACK", "GITHUB", "GITEA", "YOUTUBE", "DIRECTUS", "MICROSOFT"]
|
||
db_prefixes = ["DATABASE", "REDIS", "POSTGRES"]
|
||
|
||
for key_name, values in all_keys.items():
|
||
# Pick the most common value or root
|
||
if "root" in values:
|
||
final_value = values["root"]
|
||
else:
|
||
final_value = max(values.values(), key=lambda x: list(values.values()).count(x))
|
||
|
||
# Categorize
|
||
if any(key_name.startswith(prefix) for prefix in ai_prefixes):
|
||
categorized["ai"][key_name] = final_value
|
||
elif any(key_name.startswith(prefix) for prefix in service_prefixes):
|
||
categorized["services"][key_name] = final_value
|
||
elif any(key_name.startswith(prefix) for prefix in db_prefixes):
|
||
categorized["database"][key_name] = final_value
|
||
elif any(keyword in key_name for keyword in ["JWT", "SECRET", "TOKEN", "KEY"]):
|
||
categorized["settings"][key_name] = final_value
|
||
else:
|
||
categorized["custom"][key_name] = final_value
|
||
|
||
self.keys_data["keys"] = categorized
|
||
self.save_keys()
|
||
|
||
console.print("\n[green]✅ Keys consolidated successfully![/green]")
|
||
|
||
Prompt.ask("\nPress Enter to continue")
|
||
|
||
def export_keys(self):
|
||
"""Export keys to .env format"""
|
||
self.clear_screen()
|
||
self.display_header()
|
||
|
||
console.print(Panel("📤 Export Keys", style="bold green"))
|
||
|
||
if not self.keys_data.get("keys"):
|
||
console.print("[red]No keys to export[/red]")
|
||
Prompt.ask("\nPress Enter to continue")
|
||
return
|
||
|
||
# Choose export options
|
||
console.print("\n[bold]Export Options:[/bold]")
|
||
console.print(" 1. All keys")
|
||
console.print(" 2. Specific category")
|
||
console.print(" 3. Specific project requirements")
|
||
|
||
choice = IntPrompt.ask("\nYour choice", default=1)
|
||
|
||
filter_category = None
|
||
if choice == 2:
|
||
categories = list(self.keys_data["keys"].keys())
|
||
console.print("\n[bold]Select category:[/bold]")
|
||
for i, cat in enumerate(categories, 1):
|
||
console.print(f" {i}. {cat}")
|
||
|
||
cat_choice = IntPrompt.ask("Category", default=1)
|
||
if 1 <= cat_choice <= len(categories):
|
||
filter_category = categories[cat_choice - 1]
|
||
|
||
# Get output file
|
||
default_path = "exported_keys.env"
|
||
output_path = Prompt.ask("\n[bold]Output file path[/bold]", default=default_path)
|
||
|
||
# Export
|
||
with open(output_path, 'w') as f:
|
||
f.write(f"# Exported API Keys\n")
|
||
f.write(f"# Generated: {datetime.now().isoformat()}\n\n")
|
||
|
||
for category, keys in self.keys_data["keys"].items():
|
||
if filter_category and category != filter_category:
|
||
continue
|
||
|
||
f.write(f"# {category.upper()} KEYS\n")
|
||
for key_name, value in sorted(keys.items()):
|
||
f.write(f"{key_name}={value}\n")
|
||
f.write("\n")
|
||
|
||
console.print(f"\n[green]✅ Exported to {output_path}[/green]")
|
||
|
||
# Show preview
|
||
if Confirm.ask("\nDo you want to preview the exported file?"):
|
||
with open(output_path, 'r') as f:
|
||
lines = f.readlines()[:20]
|
||
syntax = Syntax("".join(lines), "bash", theme="monokai", line_numbers=True)
|
||
console.print(syntax)
|
||
|
||
Prompt.ask("\nPress Enter to continue")
|
||
|
||
def show_statistics(self):
|
||
"""Show key statistics"""
|
||
self.clear_screen()
|
||
self.display_header()
|
||
|
||
console.print(Panel("📊 Key Statistics", style="bold cyan"))
|
||
|
||
if not self.keys_data.get("keys"):
|
||
console.print("[red]No keys found[/red]")
|
||
Prompt.ask("\nPress Enter to continue")
|
||
return
|
||
|
||
# Calculate statistics
|
||
categories = self.keys_data["keys"]
|
||
total_keys = sum(len(keys) for keys in categories.values())
|
||
|
||
# Category breakdown
|
||
table = Table(
|
||
title="Keys by Category",
|
||
box=box.SIMPLE_HEAD,
|
||
show_lines=True,
|
||
style="cyan"
|
||
)
|
||
table.add_column("Category", style="bold yellow", width=15)
|
||
table.add_column("Count", style="white", justify="right", width=10)
|
||
table.add_column("Percentage", style="green", justify="right", width=12)
|
||
table.add_column("Status", style="white", width=20)
|
||
|
||
for category, keys in sorted(categories.items()):
|
||
count = len(keys)
|
||
percentage = (count / total_keys * 100) if total_keys > 0 else 0
|
||
|
||
# Status indicator
|
||
if count == 0:
|
||
status = "❌ Empty"
|
||
elif count < 5:
|
||
status = "⚠️ Few keys"
|
||
else:
|
||
status = "✅ Good"
|
||
|
||
table.add_row(
|
||
category.upper(),
|
||
str(count),
|
||
f"{percentage:.1f}%",
|
||
status
|
||
)
|
||
|
||
table.add_section()
|
||
table.add_row(
|
||
"TOTAL",
|
||
str(total_keys),
|
||
"100.0%",
|
||
"📊 All Keys",
|
||
style="bold"
|
||
)
|
||
|
||
console.print(table)
|
||
|
||
# Key analysis
|
||
console.print("\n[bold]Key Analysis:[/bold]")
|
||
|
||
# Find empty keys
|
||
empty_keys = []
|
||
for category, keys in categories.items():
|
||
for key_name, value in keys.items():
|
||
if not value:
|
||
empty_keys.append(f"{key_name} ({category})")
|
||
|
||
if empty_keys:
|
||
console.print(f" ⚠️ Empty keys: {len(empty_keys)}")
|
||
for key in empty_keys[:5]:
|
||
console.print(f" - {key}", style="yellow")
|
||
if len(empty_keys) > 5:
|
||
console.print(f" ... and {len(empty_keys) - 5} more", style="dim")
|
||
else:
|
||
console.print(" ✅ No empty keys found", style="green")
|
||
|
||
# Last update
|
||
if self.keys_data.get("consolidated_at"):
|
||
last_update = self.keys_data["consolidated_at"][:19].replace("T", " ")
|
||
console.print(f"\n 📅 Last consolidation: {last_update}")
|
||
|
||
Prompt.ask("\nPress Enter to continue")
|
||
|
||
def search_keys(self):
|
||
"""Search for specific keys"""
|
||
self.clear_screen()
|
||
self.display_header()
|
||
|
||
console.print(Panel("🔍 Search Keys", style="bold cyan"))
|
||
|
||
search_term = Prompt.ask("\n[bold]Enter search term[/bold]").strip().upper()
|
||
|
||
if not search_term:
|
||
return
|
||
|
||
matches = []
|
||
for category, keys in self.keys_data.get("keys", {}).items():
|
||
for key_name, value in keys.items():
|
||
if search_term in key_name:
|
||
matches.append((category, key_name, value))
|
||
|
||
if not matches:
|
||
console.print(f"\n[red]No keys found matching '{search_term}'[/red]")
|
||
else:
|
||
console.print(f"\n[bold]Found {len(matches)} matches:[/bold]\n")
|
||
|
||
table = Table(box=box.SIMPLE, show_lines=True)
|
||
table.add_column("Category", style="yellow", width=12)
|
||
table.add_column("Key Name", style="bold white", width=30)
|
||
table.add_column("Value", style="green")
|
||
|
||
for category, key_name, value in matches:
|
||
display_value = value[:40] + "..." if value and len(value) > 40 else value or "(empty)"
|
||
table.add_row(category, key_name, display_value)
|
||
|
||
console.print(table)
|
||
|
||
Prompt.ask("\nPress Enter to continue")
|
||
|
||
def run(self):
|
||
"""Main TUI loop"""
|
||
while True:
|
||
self.clear_screen()
|
||
self.display_header()
|
||
self.display_main_menu()
|
||
|
||
choice = Prompt.ask("\n[bold cyan]Select an option[/bold cyan]", default="0")
|
||
|
||
if choice == "1":
|
||
self.view_keys()
|
||
elif choice == "2":
|
||
self.add_key()
|
||
elif choice == "3":
|
||
self.edit_key()
|
||
elif choice == "4":
|
||
self.delete_key()
|
||
elif choice == "5":
|
||
self.scan_projects()
|
||
elif choice == "6":
|
||
self.export_keys()
|
||
elif choice == "7":
|
||
self.show_statistics()
|
||
elif choice == "8":
|
||
self.search_keys()
|
||
elif choice == "9":
|
||
console.print("\n[yellow]Settings not implemented yet[/yellow]")
|
||
Prompt.ask("\nPress Enter to continue")
|
||
elif choice == "0":
|
||
if Confirm.ask("\n[bold red]Are you sure you want to exit?[/bold red]"):
|
||
self.clear_screen()
|
||
console.print("[bold green]Goodbye! 👋[/bold green]")
|
||
break
|
||
|
||
def main():
|
||
"""Entry point"""
|
||
try:
|
||
tui = KeyManagerTUI()
|
||
tui.run()
|
||
except KeyboardInterrupt:
|
||
console.print("\n\n[yellow]Interrupted by user[/yellow]")
|
||
except Exception as e:
|
||
console.print(f"\n[red]Error: {e}[/red]")
|
||
|
||
if __name__ == "__main__":
|
||
main() |