- Renamed all references from maxclaw to langcode across all files - Renamed maxclaw.py → langcode.py - Renamed completion scripts: maxclaw.bash/zsh → langcode.bash/zsh - Updated Dockerfile, README, all Python files, HTML, JSON, YAML - New Gitea repository: http://192.168.0.183:8085/truetype/langcode_agent.git
768 lines
26 KiB
Python
768 lines
26 KiB
Python
"""
|
|
Text User Interface for LangCode Agent configuration management
|
|
Enhanced with search/filter, export/import, and connection testing
|
|
"""
|
|
import curses
|
|
import sys
|
|
import json
|
|
import yaml
|
|
from pathlib import Path
|
|
from typing import Optional, List, Dict, Any
|
|
from config_manager import config_manager
|
|
|
|
|
|
def draw_menu(stdscr, selected_row_idx, options, title="LangCode Agent Configuration TUI"):
|
|
"""Draw a menu with options"""
|
|
stdscr.clear()
|
|
h, w = stdscr.getmaxyx()
|
|
|
|
# Title
|
|
stdscr.attron(curses.A_BOLD)
|
|
stdscr.addstr(0, (w - len(title)) // 2, title)
|
|
stdscr.attroff(curses.A_BOLD)
|
|
|
|
# Instructions
|
|
instructions = "Use arrow keys to navigate, Enter to select, 'q' to quit"
|
|
stdscr.addstr(2, (w - len(instructions)) // 2, instructions)
|
|
|
|
# Menu options
|
|
for idx, option in enumerate(options):
|
|
x = w // 2 - len(option) // 2
|
|
y = h // 2 - len(options) // 2 + idx
|
|
if idx == selected_row_idx:
|
|
stdscr.attron(curses.A_REVERSE)
|
|
stdscr.addstr(y, x, option)
|
|
stdscr.attroff(curses.A_REVERSE)
|
|
else:
|
|
stdscr.addstr(y, x, option)
|
|
|
|
stdscr.refresh()
|
|
|
|
|
|
def get_string_input(stdscr, prompt, y, x, max_length=50):
|
|
"""Get string input from user"""
|
|
curses.echo()
|
|
stdscr.addstr(y, x, prompt)
|
|
stdscr.refresh()
|
|
input_win = curses.newwin(1, max_length, y, x + len(prompt))
|
|
input_win.box()
|
|
input_win.refresh()
|
|
curses.curs_set(1)
|
|
result = input_win.getstr(0, 0, max_length).decode('utf-8')
|
|
curses.curs_set(0)
|
|
curses.noecho()
|
|
return result
|
|
|
|
|
|
def get_password_input(stdscr, prompt, y, x, max_length=50):
|
|
"""Get password input from user (hidden)"""
|
|
curses.noecho()
|
|
stdscr.addstr(y, x, prompt)
|
|
stdscr.refresh()
|
|
input_win = curses.newwin(1, max_length, y, x + len(prompt))
|
|
input_win.box()
|
|
input_win.refresh()
|
|
curses.curs_set(1)
|
|
result = ""
|
|
while True:
|
|
ch = input_win.getch()
|
|
if ch in [curses.KEY_ENTER, 10, 13]: # Enter key
|
|
break
|
|
elif ch in [curses.KEY_BACKSPACE, 127, 8]: # Backspace
|
|
if len(result) > 0:
|
|
result = result[:-1]
|
|
input_win.addstr(0, len(result), " ")
|
|
input_win.move(0, len(result))
|
|
elif ch >= 32 and ch <= 126 and len(result) < max_length: # Printable characters
|
|
result += chr(ch)
|
|
input_win.addstr(0, len(result)-1, "*")
|
|
input_win.refresh()
|
|
curses.curs_set(0)
|
|
return result
|
|
|
|
|
|
def show_message(stdscr, message, wait_for_key=True):
|
|
"""Show a message to the user"""
|
|
h, w = stdscr.getmaxyx()
|
|
stdscr.addstr(h//2, (w - len(message)) // 2, message)
|
|
stdscr.refresh()
|
|
if wait_for_key:
|
|
stdscr.getch()
|
|
|
|
|
|
def filter_configurations(configs: Dict[str, Any], search_term: str) -> Dict[str, Any]:
|
|
"""Filter configurations based on search term"""
|
|
if not search_term:
|
|
return configs
|
|
|
|
search_term = search_term.lower()
|
|
filtered = {}
|
|
|
|
for name, config in configs.items():
|
|
# Search in name
|
|
if search_term in name.lower():
|
|
filtered[name] = config
|
|
continue
|
|
|
|
# Search in provider
|
|
provider = config.get("provider", "").lower()
|
|
if search_term in provider:
|
|
filtered[name] = config
|
|
continue
|
|
|
|
# Search in model name
|
|
model_name = config.get("model_name", "").lower()
|
|
if search_term in model_name:
|
|
filtered[name] = config
|
|
continue
|
|
|
|
# Search in base URL
|
|
base_url = config.get("base_url", "").lower()
|
|
if search_term in base_url:
|
|
filtered[name] = config
|
|
continue
|
|
|
|
return filtered
|
|
|
|
|
|
def main_tui(stdscr):
|
|
"""Main TUI function"""
|
|
# Initialize colors if available
|
|
if curses.has_colors():
|
|
curses.start_color()
|
|
curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE)
|
|
curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK)
|
|
curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK)
|
|
|
|
# Main menu options
|
|
main_options = [
|
|
"View Configurations",
|
|
"Search Configurations",
|
|
"Add Configuration",
|
|
"Remove Configuration",
|
|
"Set Active Configuration",
|
|
"View Active Configuration",
|
|
"Test Configuration",
|
|
"Export Configurations",
|
|
"Import Configurations",
|
|
"Exit"
|
|
]
|
|
|
|
current_selection = 0
|
|
|
|
while True:
|
|
draw_menu(stdscr, current_selection, main_options)
|
|
|
|
key = stdscr.getch()
|
|
|
|
if key == curses.KEY_UP and current_selection > 0:
|
|
current_selection -= 1
|
|
elif key == curses.KEY_DOWN and current_selection < len(main_options) - 1:
|
|
current_selection += 1
|
|
elif key == curses.KEY_ENTER or key in [10, 13]:
|
|
# Enter key pressed
|
|
if current_selection == 0: # View Configurations
|
|
view_configurations_tui(stdscr)
|
|
elif current_selection == 1: # Search Configurations
|
|
search_configurations_tui(stdscr)
|
|
elif current_selection == 2: # Add Configuration
|
|
add_configuration_tui(stdscr)
|
|
elif current_selection == 3: # Remove Configuration
|
|
remove_configuration_tui(stdscr)
|
|
elif current_selection == 4: # Set Active Configuration
|
|
set_active_configuration_tui(stdscr)
|
|
elif current_selection == 5: # View Active Configuration
|
|
view_active_configuration_tui(stdscr)
|
|
elif current_selection == 6: # Test Configuration
|
|
test_configuration_tui(stdscr)
|
|
elif current_selection == 7: # Export Configurations
|
|
export_configurations_tui(stdscr)
|
|
elif current_selection == 8: # Import Configurations
|
|
import_configurations_tui(stdscr)
|
|
elif current_selection == 9: # Exit
|
|
break
|
|
elif key == ord('q') or key == ord('Q'):
|
|
break
|
|
|
|
|
|
def view_configurations_tui(stdscr, configs=None, title="Configurations"):
|
|
"""View all configurations in TUI"""
|
|
if configs is None:
|
|
configs = config_manager.get_configurations()
|
|
active = config_manager.get_active_configuration()
|
|
|
|
stdscr.clear()
|
|
h, w = stdscr.getmaxyx()
|
|
|
|
# Title
|
|
stdscr.attron(curses.A_BOLD)
|
|
stdscr.addstr(0, (w - len(title)) // 2, title)
|
|
stdscr.attroff(curses.A_BOLD)
|
|
|
|
if not configs:
|
|
stdscr.addstr(2, 0, "No configurations found.")
|
|
else:
|
|
# Header
|
|
header = f"{'Name':<20} {'Provider':<12} {'Model':<20} {'Active'}"
|
|
stdscr.addstr(2, 0, header)
|
|
stdscr.addstr(3, 0, "-" * len(header))
|
|
|
|
# Configurations
|
|
for idx, (name, config) in enumerate(configs.items()):
|
|
y = 4 + idx
|
|
if y >= h - 2: # Prevent overflow
|
|
break
|
|
|
|
marker = "*" if name == active else " "
|
|
provider = config.get("provider", "unknown")[:11]
|
|
model = config.get("model_name", "unknown")[:19]
|
|
line = f"{marker} {name:<19} {provider:<12} {model:<20}"
|
|
stdscr.addstr(y, 0, line)
|
|
|
|
# Instructions
|
|
instructions = "Press any key to return to main menu"
|
|
stdscr.addstr(h-2, (w - len(instructions)) // 2, instructions)
|
|
stdscr.refresh()
|
|
stdscr.getch()
|
|
|
|
|
|
def search_configurations_tui(stdscr):
|
|
"""Search/filter configurations"""
|
|
stdscr.clear()
|
|
h, w = stdscr.getmaxyx()
|
|
|
|
# Title
|
|
title = "Search Configurations"
|
|
stdscr.attron(curses.A_BOLD)
|
|
stdscr.addstr(0, (w - len(title)) // 2, title)
|
|
stdscr.attroff(curses.A_BOLD)
|
|
|
|
# Get search term
|
|
search_term = get_string_input(stdscr, "Search term (name, provider, model, URL): ", 2, 0, 50)
|
|
|
|
if not search_term:
|
|
show_message(stdscr, "Search cancelled.")
|
|
return
|
|
|
|
# Get all configurations and filter
|
|
all_configs = config_manager.get_configurations()
|
|
filtered_configs = filter_configurations(all_configs, search_term)
|
|
|
|
# Show filtered results
|
|
if not filtered_configs:
|
|
show_message(stdscr, f"No configurations found matching '{search_term}'.")
|
|
else:
|
|
view_configurations_tui(stdscr, filtered_configs, f"Search Results for '{search_term}'")
|
|
|
|
|
|
def add_configuration_tui(stdscr):
|
|
"""Add a new configuration via TUI"""
|
|
stdscr.clear()
|
|
h, w = stdscr.getmaxyx()
|
|
|
|
# Title
|
|
title = "Add New Configuration"
|
|
stdscr.attron(curses.A_BOLD)
|
|
stdscr.addstr(0, (w - len(title)) // 2, title)
|
|
stdscr.attroff(curses.A_BOLD)
|
|
|
|
# Get configuration name
|
|
name = get_string_input(stdscr, "Configuration name: ", 2, 0)
|
|
if not name:
|
|
show_message(stdscr, "Error: Configuration name cannot be empty")
|
|
return
|
|
|
|
if config_manager.configuration_exists(name):
|
|
overwrite_prompt = f"Configuration '{name}' already exists. Overwrite? (y/n): "
|
|
overwrite = get_string_input(stdscr, overwrite_prompt, 3, 0, 1)
|
|
if overwrite.lower() != 'y':
|
|
show_message(stdscr, "Cancelled.")
|
|
return
|
|
|
|
# Select provider
|
|
providers = ["OpenAI", "Ollama", "OpenRouter"]
|
|
provider_selection = 0
|
|
|
|
while True:
|
|
stdscr.clear()
|
|
stdscr.attron(curses.A_BOLD)
|
|
stdscr.addstr(0, (w - len(title)) // 2, title)
|
|
stdscr.attroff(curses.A_BOLD)
|
|
|
|
stdscr.addstr(2, 0, f"Name: {name}")
|
|
stdscr.addstr(3, 0, "Select provider:")
|
|
|
|
for idx, provider in enumerate(providers):
|
|
y = 4 + idx
|
|
if idx == provider_selection:
|
|
stdscr.attron(curses.A_REVERSE)
|
|
stdscr.addstr(y, 2, f"> {provider}")
|
|
stdscr.attroff(curses.A_REVERSE)
|
|
else:
|
|
stdscr.addstr(y, 2, f" {provider}")
|
|
|
|
stdscr.addstr(h-2, 0, "Use arrow keys to select, Enter to confirm")
|
|
stdscr.refresh()
|
|
|
|
key = stdscr.getch()
|
|
if key == curses.KEY_UP and provider_selection > 0:
|
|
provider_selection -= 1
|
|
elif key == curses.KEY_DOWN and provider_selection < len(providers) - 1:
|
|
provider_selection += 1
|
|
elif key == curses.KEY_ENTER or key in [10, 13]:
|
|
break
|
|
|
|
provider_map = {"OpenAI": "openai", "Ollama": "ollama", "OpenRouter": "openrouter"}
|
|
provider = provider_map[providers[provider_selection]]
|
|
config = {"provider": provider}
|
|
|
|
# Get model name
|
|
model_name = get_string_input(stdscr, f"Model name for {provider} (e.g., gpt-4o, gemma2:9b): ", 5, 0)
|
|
if not model_name:
|
|
show_message(stdscr, "Error: Model name cannot be empty")
|
|
return
|
|
config["model_name"] = model_name
|
|
|
|
# Get provider-specific settings
|
|
y_pos = 6
|
|
if provider == "openai":
|
|
api_key = get_password_input(stdscr, "OpenAI API key: ", y_pos, 0)
|
|
config["api_key"] = api_key
|
|
y_pos += 1
|
|
elif provider == "ollama":
|
|
base_url = get_string_input(stdscr, f"Ollama base URL (default: http://localhost:11434): ", y_pos, 0)
|
|
if not base_url:
|
|
base_url = "http://localhost:11434"
|
|
config["base_url"] = base_url
|
|
y_pos += 1
|
|
elif provider == "openrouter":
|
|
api_key = get_password_input(stdscr, "OpenRouter API key: ", y_pos, 0)
|
|
config["api_key"] = api_key
|
|
y_pos += 1
|
|
|
|
base_url = get_string_input(stdscr, f"OpenRouter base URL (default: https://openrouter.ai/api/v1): ", y_pos, 0)
|
|
if not base_url:
|
|
base_url = "https://openrouter.ai/api/v1"
|
|
config["base_url"] = base_url
|
|
y_pos += 1
|
|
|
|
# Ask if this should be the active configuration
|
|
set_active_prompt = "Set as active configuration? (y/n): "
|
|
set_active = get_string_input(stdscr, set_active_prompt, y_pos, 0, 1)
|
|
|
|
# Save the configuration
|
|
try:
|
|
config_manager.add_configuration(name, config)
|
|
message = f"Configuration '{name}' saved successfully."
|
|
if set_active.lower() == 'y':
|
|
config_manager.set_active_configuration(name)
|
|
message += f" Configuration '{name}' set as active."
|
|
show_message(stdscr, message)
|
|
except Exception as e:
|
|
show_message(stdscr, f"Error saving configuration: {e}")
|
|
|
|
|
|
def remove_configuration_tui(stdscr):
|
|
"""Remove a configuration via TUI"""
|
|
configs = config_manager.get_configurations()
|
|
if not configs:
|
|
show_message(stdscr, "No configurations to remove.")
|
|
return
|
|
|
|
stdscr.clear()
|
|
h, w = stdscr.getmaxyx()
|
|
|
|
# Title
|
|
title = "Remove Configuration"
|
|
stdscr.attron(curses.A_BOLD)
|
|
stdscr.addstr(0, (w - len(title)) // 2, title)
|
|
stdscr.attroff(curses.A_BOLD)
|
|
|
|
# List configurations
|
|
config_names = list(configs.keys())
|
|
selection = 0
|
|
|
|
while True:
|
|
stdscr.clear()
|
|
stdscr.attron(curses.A_BOLD)
|
|
stdscr.addstr(0, (w - len(title)) // 2, title)
|
|
stdscr.attroff(curses.A_BOLD)
|
|
|
|
stdscr.addstr(2, 0, "Select configuration to remove:")
|
|
|
|
for idx, name in enumerate(config_names):
|
|
y = 3 + idx
|
|
if y >= h - 4: # Prevent overflow
|
|
break
|
|
|
|
active_marker = " *"
|
|
if name == config_manager.get_active_configuration():
|
|
active_marker = " (active)"
|
|
|
|
if idx == selection:
|
|
stdscr.attron(curses.A_REVERSE)
|
|
stdscr.addstr(y, 2, f"> {name}{active_marker}")
|
|
stdscr.attroff(curses.A_REVERSE)
|
|
else:
|
|
stdscr.addstr(y, 2, f" {name}{active_marker}")
|
|
|
|
stdscr.addstr(h-2, 0, "Use arrow keys to select, Enter to confirm, 'c' to cancel")
|
|
stdscr.refresh()
|
|
|
|
key = stdscr.getch()
|
|
if key == curses.KEY_UP and selection > 0:
|
|
selection -= 1
|
|
elif key == curses.KEY_DOWN and selection < len(config_names) - 1:
|
|
selection += 1
|
|
elif key == curses.KEY_ENTER or key in [10, 13]:
|
|
# Confirm removal
|
|
name_to_remove = config_names[selection]
|
|
active = config_manager.get_active_configuration()
|
|
|
|
if name_to_remove == active:
|
|
confirm_prompt = f"'{name_to_remove}' is the active configuration. Remove it anyway? (y/n): "
|
|
confirm = get_string_input(stdscr, confirm_prompt, h-3, 0, 1)
|
|
if confirm.lower() != 'y':
|
|
show_message(stdscr, "Cancelled.")
|
|
return
|
|
|
|
confirm_prompt = f"Are you sure you want to remove configuration '{name_to_remove}'? (y/n): "
|
|
confirm = get_string_input(stdscr, confirm_prompt, h-3, 0, 1)
|
|
if confirm.lower() == 'y':
|
|
if config_manager.remove_configuration(name_to_remove):
|
|
show_message(stdscr, f"Configuration '{name_to_remove}' removed successfully.")
|
|
if name_to_remove == active:
|
|
show_message(stdscr, "Active configuration has been unset.")
|
|
else:
|
|
show_message(stdscr, f"Error: Failed to remove configuration '{name_to_remove}'.")
|
|
else:
|
|
show_message(stdscr, "Cancelled.")
|
|
return
|
|
elif key == ord('c') or key == ord('C'):
|
|
show_message(stdscr, "Cancelled.")
|
|
return
|
|
|
|
|
|
def set_active_configuration_tui(stdscr):
|
|
"""Set active configuration via TUI"""
|
|
configs = config_manager.get_configurations()
|
|
if not configs:
|
|
show_message(stdscr, "No configurations available.")
|
|
return
|
|
|
|
stdscr.clear()
|
|
h, w = stdscr.getmaxyx()
|
|
|
|
# Title
|
|
title = "Set Active Configuration"
|
|
stdscr.attron(curses.A_BOLD)
|
|
stdscr.addstr(0, (w - len(title)) // 2, title)
|
|
stdscr.attroff(curses.A_BOLD)
|
|
|
|
# List configurations
|
|
config_names = list(configs.keys())
|
|
selection = 0
|
|
|
|
while True:
|
|
stdscr.clear()
|
|
stdscr.attron(curses.A_BOLD)
|
|
stdscr.addstr(0, (w - len(title)) // 2, title)
|
|
stdscr.attroff(curses.A_BOLD)
|
|
|
|
stdscr.addstr(2, 0, "Select configuration to set as active:")
|
|
|
|
for idx, name in enumerate(config_names):
|
|
y = 3 + idx
|
|
if y >= h - 4: # Prevent overflow
|
|
break
|
|
|
|
current_active = config_manager.get_active_configuration()
|
|
active_marker = " (current)" if name == current_active else ""
|
|
|
|
if idx == selection:
|
|
stdscr.attron(curses.A_REVERSE)
|
|
stdscr.addstr(y, 2, f"> {name}{active_marker}")
|
|
stdscr.attroff(curses.A_REVERSE)
|
|
else:
|
|
stdscr.addstr(y, 2, f" {name}{active_marker}")
|
|
|
|
stdscr.addstr(h-2, 0, "Use arrow keys to select, Enter to confirm, 'c' to cancel")
|
|
stdscr.refresh()
|
|
|
|
key = stdscr.getch()
|
|
if key == curses.KEY_UP and selection > 0:
|
|
selection -= 1
|
|
elif key == curses.KEY_DOWN and selection < len(config_names) - 1:
|
|
selection += 1
|
|
elif key == curses.KEY_ENTER or key in [10, 13]:
|
|
# Set as active
|
|
name_to_set = config_names[selection]
|
|
if config_manager.set_active_configuration(name_to_set):
|
|
show_message(stdscr, f"Configuration '{name_to_set}' set as active.")
|
|
else:
|
|
show_message(stdscr, f"Error: Failed to set configuration '{name_to_set}' as active.")
|
|
return
|
|
elif key == ord('c') or key == ord('C'):
|
|
show_message(stdscr, "Cancelled.")
|
|
return
|
|
|
|
|
|
def view_active_configuration_tui(stdscr):
|
|
"""View active configuration via TUI"""
|
|
active_name = config_manager.get_active_configuration()
|
|
if not active_name:
|
|
show_message(stdscr, "No active configuration set.")
|
|
return
|
|
|
|
config = config_manager.get_configuration(active_name)
|
|
if not config:
|
|
show_message(stdscr, f"Error: Active configuration '{active_name}' not found.")
|
|
return
|
|
|
|
stdscr.clear()
|
|
h, w = stdscr.getmaxyx()
|
|
|
|
# Title
|
|
title = "Active Configuration"
|
|
stdscr.attron(curses.A_BOLD)
|
|
stdscr.addstr(0, (w - len(title)) // 2, title)
|
|
stdscr.attroff(curses.A_BOLD)
|
|
|
|
# Display configuration details
|
|
details = [
|
|
f"Name: {active_name}",
|
|
f"Provider: {config.get('provider', 'unknown')}",
|
|
f"Model: {config.get('model_name', 'unknown')}"
|
|
]
|
|
|
|
# Add provider-specific details
|
|
provider = config.get('provider')
|
|
if provider == "openai":
|
|
details.append(f"API Key: {'Set' if config.get('api_key') else 'Not set'}")
|
|
elif provider == "ollama":
|
|
base_url = config.get('base_url', 'http://localhost:11434')
|
|
details.append(f"Base URL: {base_url}")
|
|
elif provider == "openrouter":
|
|
details.append(f"API Key: {'Set' if config.get('api_key') else 'Not set'}")
|
|
base_url = config.get('base_url', 'https://openrouter.ai/api/v1')
|
|
details.append(f"Base URL: {base_url}")
|
|
|
|
# Display details
|
|
for idx, detail in enumerate(details):
|
|
y = 2 + idx
|
|
if y >= h - 2: # Prevent overflow
|
|
break
|
|
stdscr.addstr(y, 0, detail)
|
|
|
|
# Instructions
|
|
instructions = "Press any key to return to main menu"
|
|
stdscr.addstr(h-2, (w - len(instructions)) // 2, instructions)
|
|
stdscr.refresh()
|
|
stdscr.getch()
|
|
|
|
|
|
def test_configuration_tui(stdscr):
|
|
"""Test connection to a configuration"""
|
|
configs = config_manager.get_configurations()
|
|
if not configs:
|
|
show_message(stdscr, "No configurations to test.")
|
|
return
|
|
|
|
stdscr.clear()
|
|
h, w = stdscr.getmaxyx()
|
|
|
|
# Title
|
|
title = "Test Configuration Connection"
|
|
stdscr.attron(curses.A_BOLD)
|
|
stdscr.addstr(0, (w - len(title)) // 2, title)
|
|
stdscr.attroff(curses.A_BOLD)
|
|
|
|
# List configurations
|
|
config_names = list(configs.keys())
|
|
selection = 0
|
|
|
|
while True:
|
|
stdscr.clear()
|
|
stdscr.attron(curses.A_BOLD)
|
|
stdscr.addstr(0, (w - len(title)) // 2, title)
|
|
stdscr.attroff(curses.A_BOLD)
|
|
|
|
stdscr.addstr(2, 0, "Select configuration to test:")
|
|
|
|
for idx, name in enumerate(config_names):
|
|
y = 3 + idx
|
|
if y >= h - 4: # Prevent overflow
|
|
break
|
|
|
|
if idx == selection:
|
|
stdscr.attron(curses.A_REVERSE)
|
|
stdscr.addstr(y, 2, f"> {name}")
|
|
stdscr.attroff(curses.A_REVERSE)
|
|
else:
|
|
stdscr.addstr(y, 2, f" {name}")
|
|
|
|
stdscr.addstr(h-2, 0, "Use arrow keys to select, Enter to test, 'c' to cancel")
|
|
stdscr.refresh()
|
|
|
|
key = stdscr.getch()
|
|
if key == curses.KEY_UP and selection > 0:
|
|
selection -= 1
|
|
elif key == curses.KEY_DOWN and selection < len(config_names) - 1:
|
|
selection += 1
|
|
elif key == curses.KEY_ENTER or key in [10, 13]:
|
|
# Test configuration
|
|
name_to_test = config_names[selection]
|
|
show_message(stdscr, f"Testing connection to '{name_to_test}'...", False)
|
|
|
|
success, message = config_manager.test_configuration_connection(name_to_test)
|
|
|
|
if success:
|
|
show_message(stdscr, f"✅ {message}")
|
|
else:
|
|
show_message(stdscr, f"❌ {message}")
|
|
return
|
|
elif key == ord('c') or key == ord('C'):
|
|
show_message(stdscr, "Cancelled.")
|
|
return
|
|
|
|
|
|
def export_configurations_tui(stdscr):
|
|
"""Export configurations to JSON or YAML"""
|
|
configs = config_manager.get_configurations()
|
|
if not configs:
|
|
show_message(stdscr, "No configurations to export.")
|
|
return
|
|
|
|
stdscr.clear()
|
|
h, w = stdscr.getmaxyx()
|
|
|
|
# Title
|
|
title = "Export Configurations"
|
|
stdscr.attron(curses.A_BOLD)
|
|
stdscr.addstr(0, (w - len(title)) // 2, title)
|
|
stdscr.attroff(curses.A_BOLD)
|
|
|
|
# Select format
|
|
formats = ["JSON", "YAML"]
|
|
format_selection = 0
|
|
|
|
while True:
|
|
stdscr.clear()
|
|
stdscr.attron(curses.A_BOLD)
|
|
stdscr.addstr(0, (w - len(title)) // 2, title)
|
|
stdscr.attroff(curses.A_BOLD)
|
|
|
|
stdscr.addstr(2, 0, "Select export format:")
|
|
|
|
for idx, fmt in enumerate(formats):
|
|
y = 3 + idx
|
|
if idx == format_selection:
|
|
stdscr.attron(curses.A_REVERSE)
|
|
stdscr.addstr(y, 2, f"> {fmt}")
|
|
stdscr.attroff(curses.A_REVERSE)
|
|
else:
|
|
stdscr.addstr(y, 2, f" {fmt}")
|
|
|
|
stdscr.addstr(h-2, 0, "Use arrow keys to select, Enter to confirm, 'c' to cancel")
|
|
stdscr.refresh()
|
|
|
|
key = stdscr.getch()
|
|
if key == curses.KEY_UP and format_selection > 0:
|
|
format_selection -= 1
|
|
elif key == curses.KEY_DOWN and format_selection < len(formats) - 1:
|
|
format_selection += 1
|
|
elif key == curses.KEY_ENTER or key in [10, 13]:
|
|
break
|
|
elif key == ord('c') or key == ord('C'):
|
|
show_message(stdscr, "Cancelled.")
|
|
return
|
|
|
|
# Get filename
|
|
filename = get_string_input(stdscr, "Filename (without extension): ", 5, 0, 30)
|
|
if not filename:
|
|
show_message(stdscr, "Cancelled.")
|
|
return
|
|
|
|
# Export configurations
|
|
try:
|
|
if format_selection == 0: # JSON
|
|
export_file = Path(f"{filename}.json")
|
|
with open(export_file, 'w') as f:
|
|
json.dump(configs, f, indent=2)
|
|
else: # YAML
|
|
export_file = Path(f"{filename}.yaml")
|
|
with open(export_file, 'w') as f:
|
|
yaml.dump(configs, f, default_flow_style=False)
|
|
|
|
show_message(stdscr, f"Configurations exported to {export_file}")
|
|
except Exception as e:
|
|
show_message(stdscr, f"Error exporting configurations: {e}")
|
|
|
|
|
|
def import_configurations_tui(stdscr):
|
|
"""Import configurations from JSON or YAML"""
|
|
stdscr.clear()
|
|
h, w = stdscr.getmaxyx()
|
|
|
|
# Title
|
|
title = "Import Configurations"
|
|
stdscr.attron(curses.A_BOLD)
|
|
stdscr.addstr(0, (w - len(title)) // 2, title)
|
|
stdscr.attroff(curses.A_BOLD)
|
|
|
|
# Get filename
|
|
filename = get_string_input(stdscr, "Filename (with extension .json or .yaml): ", 2, 0, 50)
|
|
if not filename:
|
|
show_message(stdscr, "Cancelled.")
|
|
return
|
|
|
|
# Check if file exists
|
|
file_path = Path(filename)
|
|
if not file_path.exists():
|
|
show_message(stdscr, f"Error: File '{filename}' not found.")
|
|
return
|
|
|
|
# Import configurations
|
|
try:
|
|
with open(file_path, 'r') as f:
|
|
if filename.endswith('.json'):
|
|
imported_configs = json.load(f)
|
|
elif filename.endswith(('.yaml', '.yml')):
|
|
imported_configs = yaml.safe_load(f)
|
|
else:
|
|
show_message(stdscr, "Error: Unsupported file format. Use .json or .yaml")
|
|
return
|
|
|
|
# Validate imported configurations
|
|
if not isinstance(imported_configs, dict):
|
|
show_message(stdscr, "Error: Invalid configuration format.")
|
|
return
|
|
|
|
# Ask for confirmation
|
|
confirm_prompt = f"Import {len(imported_configs)} configurations? (y/n): "
|
|
confirm = get_string_input(stdscr, confirm_prompt, 4, 0, 1)
|
|
if confirm.lower() != 'y':
|
|
show_message(stdscr, "Cancelled.")
|
|
return
|
|
|
|
# Import configurations
|
|
imported_count = 0
|
|
for name, config in imported_configs.items():
|
|
try:
|
|
config_manager.add_configuration(name, config)
|
|
imported_count += 1
|
|
except Exception as e:
|
|
show_message(stdscr, f"Warning: Could not import '{name}': {e}")
|
|
|
|
show_message(stdscr, f"Successfully imported {imported_count} configurations.")
|
|
except Exception as e:
|
|
show_message(stdscr, f"Error importing configurations: {e}")
|
|
|
|
|
|
def main():
|
|
"""Main entry point for the TUI"""
|
|
curses.wrapper(main_tui)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |