617 lines
26 KiB
Python
Executable File
617 lines
26 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Monocoque Manager - Simple TUI for managing monocoque telemetry system
|
|
"""
|
|
|
|
import subprocess
|
|
import sys
|
|
import os
|
|
import time
|
|
from pathlib import Path
|
|
|
|
try:
|
|
import curses
|
|
except ImportError:
|
|
print("Error: curses module not available")
|
|
sys.exit(1)
|
|
|
|
# Constants to avoid magic numbers
|
|
MIN_TERMINAL_HEIGHT = 20
|
|
MIN_TERMINAL_WIDTH = 60
|
|
PROCESS_STOP_WAIT_TIME = 1 # seconds
|
|
SIMD_START_DELAY = 2 # seconds
|
|
|
|
|
|
class MonocoqueManager:
|
|
def __init__(self):
|
|
# Mirror the shell script's robust path detection
|
|
xdg_data = os.environ.get('XDG_DATA_HOME', os.path.join(Path.home(), ".local/share"))
|
|
self.install_dir = Path(os.environ.get('MONOCOQUE_INSTALL_DIR', os.path.join(xdg_data, "monocoque")))
|
|
|
|
xdg_config = os.environ.get('XDG_CONFIG_HOME', os.path.join(Path.home(), ".config"))
|
|
self.config_dir = Path(xdg_config)
|
|
|
|
self.simd_running = False
|
|
self.monocoque_running = False
|
|
self.last_cleaned_pid_files = [] # Track cleaned stale PID files
|
|
|
|
def check_processes(self):
|
|
"""Check if simd and monocoque are running using careful detection"""
|
|
try:
|
|
manager_pid = os.getpid() # Our own PID to exclude
|
|
|
|
# Method 1: Exact name matching (safest)
|
|
result = subprocess.run(['pgrep', '-x', 'simd'],
|
|
capture_output=True, text=True)
|
|
simd_exact = bool(result.stdout.strip())
|
|
|
|
result = subprocess.run(['pgrep', '-x', 'monocoque'],
|
|
capture_output=True, text=True)
|
|
monocoque_exact = bool(result.stdout.strip())
|
|
|
|
# Method 2: Careful process scanning
|
|
simd_found = False
|
|
monocoque_found = False
|
|
|
|
ps_result = subprocess.run(['ps', 'axo', 'pid,comm,args'],
|
|
capture_output=True, text=True)
|
|
if ps_result.returncode == 0:
|
|
for line in ps_result.stdout.split('\n')[1:]: # Skip header
|
|
parts = line.strip().split(None, 2)
|
|
if len(parts) >= 3:
|
|
pid, comm, args = parts
|
|
|
|
# Skip our own process
|
|
if pid == str(manager_pid):
|
|
continue
|
|
|
|
# Look for simd executable
|
|
if (comm == 'simd' or
|
|
(comm.endswith('simd') and 'manager' not in comm) or
|
|
('/simd' in args and 'manager' not in args)):
|
|
simd_found = True
|
|
|
|
# Look for monocoque executable (not manager)
|
|
elif ((comm == 'monocoque' or
|
|
(comm.endswith('monocoque') and 'manager' not in comm)) and
|
|
'manager' not in args and
|
|
'monocoque-manager' not in args and
|
|
'python' not in args):
|
|
monocoque_found = True
|
|
|
|
# Set final status
|
|
self.simd_running = simd_exact or simd_found
|
|
self.monocoque_running = monocoque_exact or monocoque_found
|
|
|
|
# Check for stale PID files and clean them up
|
|
self.cleanup_stale_pid_files()
|
|
|
|
except Exception:
|
|
# Fallback: set both to False if detection fails
|
|
self.simd_running = False
|
|
self.monocoque_running = False
|
|
|
|
def cleanup_stale_pid_files(self):
|
|
"""Clean up stale PID files when processes aren't actually running"""
|
|
pid_files = [
|
|
('/tmp/simd.pid', self.simd_running),
|
|
('/tmp/monocoque.pid', self.monocoque_running),
|
|
]
|
|
|
|
cleaned_files = []
|
|
|
|
for pid_file_path, process_running in pid_files:
|
|
if os.path.exists(pid_file_path) and not process_running:
|
|
try:
|
|
# Read the PID and check if that process exists
|
|
with open(pid_file_path, 'r') as f:
|
|
pid_str = f.read().strip()
|
|
|
|
if pid_str.isdigit():
|
|
pid = int(pid_str)
|
|
# Check if this PID actually exists and is the right process
|
|
try:
|
|
ps_result = subprocess.run(['ps', '-p', str(pid), '-o', 'comm='],
|
|
capture_output=True, text=True)
|
|
if ps_result.returncode != 0:
|
|
# Process doesn't exist, PID file is stale
|
|
os.remove(pid_file_path)
|
|
cleaned_files.append(os.path.basename(pid_file_path))
|
|
else:
|
|
# Process exists, check if it's actually simd/monocoque
|
|
comm = ps_result.stdout.strip()
|
|
service_name = os.path.basename(pid_file_path).replace('.pid', '')
|
|
if service_name not in comm:
|
|
# Wrong process, PID file is stale
|
|
os.remove(pid_file_path)
|
|
cleaned_files.append(os.path.basename(pid_file_path))
|
|
except:
|
|
# Error checking process, assume stale and remove
|
|
os.remove(pid_file_path)
|
|
cleaned_files.append(os.path.basename(pid_file_path))
|
|
else:
|
|
# Invalid PID format, remove stale file
|
|
os.remove(pid_file_path)
|
|
cleaned_files.append(os.path.basename(pid_file_path))
|
|
|
|
except Exception:
|
|
# Error reading/processing PID file, try to remove it
|
|
try:
|
|
os.remove(pid_file_path)
|
|
cleaned_files.append(os.path.basename(pid_file_path))
|
|
except:
|
|
pass
|
|
|
|
# Store cleaned files for potential user feedback
|
|
self.last_cleaned_pid_files = cleaned_files
|
|
return cleaned_files
|
|
|
|
def check_installation(self):
|
|
"""Check if monocoque is installed"""
|
|
simd_exists = (self.install_dir / "simapi/simd/build/simd").exists() or \
|
|
subprocess.run(['which', 'simd'], capture_output=True).returncode == 0
|
|
monocoque_exists = (self.install_dir / "monocoque/build/monocoque").exists() or \
|
|
subprocess.run(['which', 'monocoque'], capture_output=True).returncode == 0
|
|
|
|
return simd_exists and monocoque_exists
|
|
|
|
def draw_header(self, stdscr):
|
|
"""Draw header"""
|
|
height, width = stdscr.getmaxyx()
|
|
title = "Monocoque Manager"
|
|
stdscr.attron(curses.color_pair(2) | curses.A_BOLD)
|
|
stdscr.addstr(0, (width - len(title)) // 2, title)
|
|
stdscr.attroff(curses.color_pair(2) | curses.A_BOLD)
|
|
stdscr.addstr(1, 0, "─" * width)
|
|
|
|
def draw_status(self, stdscr, row):
|
|
"""Draw status section"""
|
|
self.check_processes()
|
|
|
|
stdscr.attron(curses.A_BOLD)
|
|
stdscr.addstr(row, 2, "Status:")
|
|
stdscr.attroff(curses.A_BOLD)
|
|
|
|
# simd status
|
|
status_str = "● simd: "
|
|
stdscr.addstr(row + 1, 4, status_str)
|
|
if self.simd_running:
|
|
stdscr.attron(curses.color_pair(1))
|
|
stdscr.addstr("RUNNING")
|
|
stdscr.attroff(curses.color_pair(1))
|
|
else:
|
|
stdscr.attron(curses.color_pair(3))
|
|
stdscr.addstr("STOPPED")
|
|
stdscr.attroff(curses.color_pair(3))
|
|
|
|
# monocoque status
|
|
status_str = "● monocoque: "
|
|
stdscr.addstr(row + 2, 4, status_str)
|
|
if self.monocoque_running:
|
|
stdscr.attron(curses.color_pair(1))
|
|
stdscr.addstr("RUNNING")
|
|
stdscr.attroff(curses.color_pair(1))
|
|
else:
|
|
stdscr.attron(curses.color_pair(3))
|
|
stdscr.addstr("STOPPED")
|
|
stdscr.attroff(curses.color_pair(3))
|
|
|
|
# Show notification about cleaned PID files
|
|
current_row = row + 3
|
|
if self.last_cleaned_pid_files:
|
|
cleaned_msg = f"● Cleaned stale PID files: {', '.join(self.last_cleaned_pid_files)}"
|
|
stdscr.attron(curses.color_pair(4))
|
|
stdscr.addstr(current_row, 4, cleaned_msg)
|
|
stdscr.attroff(curses.color_pair(4))
|
|
current_row += 1
|
|
# Clear the notification after showing it once
|
|
self.last_cleaned_pid_files = []
|
|
|
|
return current_row
|
|
|
|
def draw_menu(self, stdscr, row, selected):
|
|
"""Draw menu options"""
|
|
height, width = stdscr.getmaxyx()
|
|
|
|
stdscr.addstr(row, 0, "─" * width)
|
|
row += 1
|
|
|
|
stdscr.attron(curses.A_BOLD)
|
|
stdscr.addstr(row, 2, "Actions:")
|
|
stdscr.attroff(curses.A_BOLD)
|
|
row += 1
|
|
|
|
menu_items = [
|
|
("1", "Start simd", not self.simd_running),
|
|
("2", "Start monocoque", not self.monocoque_running and self.simd_running),
|
|
("3", "Test configuration", True),
|
|
("4", "Edit monocoque config", True),
|
|
("5", "View logs", True),
|
|
("6", "Restart services", self.simd_running or self.monocoque_running),
|
|
("7", "Stop all", self.simd_running or self.monocoque_running),
|
|
("0", "Quit", True),
|
|
]
|
|
|
|
for i, (key, label, enabled) in enumerate(menu_items):
|
|
if enabled:
|
|
if i == selected:
|
|
stdscr.attron(curses.A_REVERSE)
|
|
stdscr.addstr(row + i, 4, f"[{key}] {label}")
|
|
if i == selected:
|
|
stdscr.attroff(curses.A_REVERSE)
|
|
else:
|
|
stdscr.attron(curses.color_pair(4))
|
|
stdscr.addstr(row + i, 4, f"[{key}] {label}")
|
|
stdscr.attroff(curses.color_pair(4))
|
|
|
|
return row + len(menu_items)
|
|
|
|
def get_enabled_menu_items(self):
|
|
"""Get list of currently enabled menu items with their indices"""
|
|
menu_items = [
|
|
("1", "Start simd", not self.simd_running),
|
|
("2", "Start monocoque", not self.monocoque_running and self.simd_running),
|
|
("3", "Test configuration", True),
|
|
("4", "Edit monocoque config", True),
|
|
("5", "View logs", True),
|
|
("6", "Restart services", self.simd_running or self.monocoque_running),
|
|
("7", "Stop all", self.simd_running or self.monocoque_running),
|
|
("0", "Quit", True),
|
|
]
|
|
|
|
enabled_items = []
|
|
for i, (key, label, enabled) in enumerate(menu_items):
|
|
if enabled:
|
|
enabled_items.append((i, key))
|
|
return enabled_items
|
|
|
|
def draw_footer(self, stdscr):
|
|
"""Draw footer"""
|
|
height, width = stdscr.getmaxyx()
|
|
footer = "↑↓/j/k: Navigate | Enter: Select | Numbers: Direct | q/ESC: Quit"
|
|
stdscr.addstr(height - 2, 0, "─" * width)
|
|
stdscr.attron(curses.color_pair(4))
|
|
stdscr.addstr(height - 1, (width - len(footer)) // 2, footer)
|
|
stdscr.attroff(curses.color_pair(4))
|
|
|
|
def run_command(self, cmd, terminal=True):
|
|
"""Run a command"""
|
|
if terminal:
|
|
# Try to find a terminal emulator
|
|
terminals = ['konsole', 'gnome-terminal', 'xfce4-terminal',
|
|
'alacritty', 'kitty', 'xterm']
|
|
|
|
for term in terminals:
|
|
if subprocess.run(['which', term], capture_output=True).returncode == 0:
|
|
if term == 'konsole':
|
|
subprocess.Popen([term, '-e', 'bash', '-c', cmd])
|
|
elif term == 'gnome-terminal':
|
|
subprocess.Popen([term, '--', 'bash', '-c', cmd])
|
|
elif term == 'kitty':
|
|
# Kitty prefers commands as positional arguments
|
|
subprocess.Popen([term, 'bash', '-c', cmd])
|
|
elif term in ['xfce4-terminal', 'alacritty', 'xterm']:
|
|
# Ensure the whole bash command is quoted correctly for -e
|
|
subprocess.Popen([term, '-e', f"bash -c '{cmd}'"])
|
|
return True
|
|
return False
|
|
else:
|
|
subprocess.run(cmd, shell=True)
|
|
return True
|
|
|
|
def handle_action(self, action):
|
|
"""Handle menu action"""
|
|
if action == '1': # Start simd
|
|
cmd = "start-simd || $HOME/.local/bin/start-simd; exec bash"
|
|
self.run_command(cmd)
|
|
|
|
elif action == '2': # Start monocoque
|
|
cmd = "start-monocoque || $HOME/.local/bin/start-monocoque; exec bash"
|
|
self.run_command(cmd)
|
|
|
|
elif action == '3': # Test configuration
|
|
cmd = "test-monocoque || $HOME/.local/bin/test-monocoque; read -p 'Press Enter to close...'"
|
|
self.run_command(cmd)
|
|
|
|
elif action == '4': # Edit config
|
|
editor = os.environ.get('EDITOR', 'nano')
|
|
config_file = self.config_dir / "monocoque/monocoque.config"
|
|
subprocess.run([editor, str(config_file)])
|
|
|
|
elif action == '5': # View logs
|
|
xdg_cache = os.environ.get('XDG_CACHE_HOME', os.path.join(Path.home(), ".cache"))
|
|
log_dir = Path(xdg_cache) / "monocoque"
|
|
if log_dir.exists():
|
|
cmd = f"find {log_dir} -name '*.log' -exec ls -la {{}} + 2>/dev/null; find {log_dir} -name '*.log' -exec tail -f {{}} + 2>/dev/null; read -p 'Press Enter to close...'"
|
|
self.run_command(cmd)
|
|
else:
|
|
# Create a simple message if no logs exist
|
|
cmd = f"echo 'No log directory found at {log_dir}'; echo 'Logs will appear here when monocoque runs'; read -p 'Press Enter to close...'"
|
|
self.run_command(cmd)
|
|
|
|
elif action == '6': # Restart services
|
|
stopped = self.stop_all_services()
|
|
if stopped:
|
|
print(f"Stopped: {', '.join(stopped)}")
|
|
# Wait a moment for processes to stop
|
|
time.sleep(PROCESS_STOP_WAIT_TIME)
|
|
# Restart simd first
|
|
cmd = f"start-simd || $HOME/.local/bin/start-simd; sleep {SIMD_START_DELAY}; start-monocoque || $HOME/.local/bin/start-monocoque; exec bash"
|
|
self.run_command(cmd)
|
|
print("Restarting services in terminal window...")
|
|
|
|
elif action == '7': # Stop all
|
|
stopped = self.stop_all_services()
|
|
if stopped:
|
|
print(f"Stopped: {', '.join(stopped)}")
|
|
else:
|
|
print("No running services found to stop")
|
|
|
|
def stop_all_services(self):
|
|
"""Carefully stop simd and monocoque processes while preserving the manager"""
|
|
stopped_services = []
|
|
manager_pid = os.getpid() # Get our own PID to exclude it
|
|
|
|
# Method 1: Try exact name matching (safest)
|
|
for service in ['simd', 'monocoque']:
|
|
result = subprocess.run(['pkill', '-x', service],
|
|
capture_output=True, text=True)
|
|
if result.returncode == 0:
|
|
stopped_services.append(f"{service} (exact)")
|
|
|
|
# Method 2: Carefully search ps output and target specific processes
|
|
try:
|
|
ps_result = subprocess.run(['ps', 'axo', 'pid,ppid,comm,args'],
|
|
capture_output=True, text=True)
|
|
if ps_result.returncode == 0:
|
|
lines = ps_result.stdout.split('\n')[1:] # Skip header
|
|
for line in lines:
|
|
parts = line.strip().split(None, 3) # Split into PID, PPID, COMM, ARGS
|
|
if len(parts) >= 4:
|
|
pid, ppid, comm, args = parts
|
|
|
|
# Skip our own process
|
|
if pid == str(manager_pid):
|
|
continue
|
|
|
|
# Target simd: look for executables ending in 'simd' or containing '/simd'
|
|
if (comm == 'simd' or
|
|
(comm.endswith('simd') and not 'manager' in comm) or
|
|
('/simd' in args and 'manager' not in args)):
|
|
try:
|
|
subprocess.run(['kill', '-TERM', pid], capture_output=True)
|
|
stopped_services.append(f"simd (PID {pid})")
|
|
time.sleep(0.1) # Brief pause between kills
|
|
except:
|
|
pass
|
|
|
|
# Target monocoque: be very specific to avoid manager
|
|
elif ((comm == 'monocoque' or
|
|
(comm.endswith('monocoque') and not 'manager' in comm)) and
|
|
'manager' not in args and
|
|
'monocoque-manager' not in args and
|
|
'python' not in args): # Avoid Python scripts
|
|
try:
|
|
subprocess.run(['kill', '-TERM', pid], capture_output=True)
|
|
stopped_services.append(f"monocoque (PID {pid})")
|
|
time.sleep(0.1) # Brief pause between kills
|
|
except:
|
|
pass
|
|
except:
|
|
pass
|
|
|
|
# Wait for graceful shutdown
|
|
time.sleep(PROCESS_STOP_WAIT_TIME)
|
|
|
|
# Method 3: Force kill stubborn processes (but still be careful)
|
|
try:
|
|
# Only force kill exact matches, never pattern matches
|
|
for service in ['simd', 'monocoque']:
|
|
subprocess.run(['pkill', '-9', '-x', service],
|
|
capture_output=True, text=True)
|
|
except:
|
|
pass
|
|
|
|
return stopped_services
|
|
|
|
def main(self, stdscr):
|
|
"""Main TUI loop"""
|
|
try:
|
|
curses.curs_set(0)
|
|
except curses.error:
|
|
pass # Some terminals don't support cursor visibility changes
|
|
|
|
try:
|
|
stdscr.keypad(True) # Enable keypad mode for arrow keys
|
|
except curses.error:
|
|
pass # Fallback if keypad mode fails
|
|
|
|
# Initialize colors if available
|
|
if curses.has_colors():
|
|
curses.start_color()
|
|
try:
|
|
curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK)
|
|
curses.init_pair(2, curses.COLOR_CYAN, curses.COLOR_BLACK)
|
|
curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK)
|
|
curses.init_pair(4, curses.COLOR_YELLOW, curses.COLOR_BLACK)
|
|
except curses.error:
|
|
pass # Fallback for terminals with limited color support
|
|
|
|
selected = 0
|
|
|
|
while True:
|
|
stdscr.clear()
|
|
height, width = stdscr.getmaxyx()
|
|
|
|
# Ensure selected index is valid for current state
|
|
self.check_processes()
|
|
enabled_items = self.get_enabled_menu_items()
|
|
enabled_indices = [item[0] for item in enabled_items]
|
|
if enabled_indices and selected not in enabled_indices:
|
|
selected = enabled_indices[0]
|
|
|
|
if height < MIN_TERMINAL_HEIGHT or width < MIN_TERMINAL_WIDTH:
|
|
stdscr.addstr(0, 0, "Terminal too small!")
|
|
stdscr.addstr(1, 0, f"Minimum: {MIN_TERMINAL_WIDTH}x{MIN_TERMINAL_HEIGHT}")
|
|
stdscr.refresh()
|
|
stdscr.getch()
|
|
continue
|
|
|
|
self.draw_header(stdscr)
|
|
row = self.draw_status(stdscr, 3)
|
|
row = self.draw_menu(stdscr, row + 1, selected)
|
|
self.draw_footer(stdscr)
|
|
|
|
stdscr.refresh()
|
|
|
|
key = stdscr.getch()
|
|
|
|
# Get currently enabled menu items
|
|
enabled_items = self.get_enabled_menu_items()
|
|
enabled_indices = [item[0] for item in enabled_items]
|
|
|
|
# Handle arrow key navigation (with multiple key codes for compatibility)
|
|
if key in [curses.KEY_UP, 259, ord('k'), ord('K')]: # UP arrow or k/K
|
|
if enabled_indices:
|
|
current_pos = enabled_indices.index(selected) if selected in enabled_indices else 0
|
|
new_pos = (current_pos - 1) % len(enabled_indices)
|
|
selected = enabled_indices[new_pos]
|
|
elif key in [curses.KEY_DOWN, 258, ord('j'), ord('J')]: # DOWN arrow or j/J
|
|
if enabled_indices:
|
|
current_pos = enabled_indices.index(selected) if selected in enabled_indices else 0
|
|
new_pos = (current_pos + 1) % len(enabled_indices)
|
|
selected = enabled_indices[new_pos]
|
|
elif key in [curses.KEY_ENTER, ord('\n'), ord('\r'), 10, 13]: # Various ENTER codes
|
|
# Execute currently selected action
|
|
action_key = None
|
|
for idx, key_char in enabled_items:
|
|
if idx == selected:
|
|
action_key = key_char
|
|
break
|
|
if action_key and action_key == '0':
|
|
break
|
|
elif action_key:
|
|
self.handle_action(action_key)
|
|
# Force a refresh after action
|
|
stdscr.clear()
|
|
elif key in [ord('q'), ord('Q'), 27]: # q/Q or ESC
|
|
break
|
|
elif key == ord('1'):
|
|
self.handle_action('1')
|
|
stdscr.clear() # Force refresh after action
|
|
elif key == ord('2'):
|
|
self.handle_action('2')
|
|
stdscr.clear() # Force refresh after action
|
|
elif key == ord('3'):
|
|
self.handle_action('3')
|
|
stdscr.clear() # Force refresh after action
|
|
elif key == ord('4'):
|
|
self.handle_action('4')
|
|
stdscr.clear() # Force refresh after action
|
|
elif key == ord('5'):
|
|
self.handle_action('5')
|
|
stdscr.clear() # Force refresh after action
|
|
elif key == ord('6'):
|
|
self.handle_action('6')
|
|
stdscr.clear() # Force refresh after action
|
|
elif key == ord('7'):
|
|
self.handle_action('7')
|
|
stdscr.clear() # Force refresh after action
|
|
elif key == ord('0'):
|
|
break
|
|
|
|
|
|
def text_interface(self):
|
|
"""Fallback text-based interface when curses is not available"""
|
|
print("Monocoque Manager - Text Mode")
|
|
print("=" * 40)
|
|
|
|
while True:
|
|
self.check_processes()
|
|
|
|
print("\nStatus:")
|
|
print(f" simd: {'RUNNING' if self.simd_running else 'STOPPED'}")
|
|
print(f" monocoque: {'RUNNING' if self.monocoque_running else 'STOPPED'}")
|
|
|
|
# Show notification about cleaned PID files
|
|
if self.last_cleaned_pid_files:
|
|
print(f" Note: Cleaned stale PID files: {', '.join(self.last_cleaned_pid_files)}")
|
|
# Clear the notification after showing it once
|
|
self.last_cleaned_pid_files = []
|
|
|
|
print("\nActions:")
|
|
menu_items = [
|
|
("1", "Start simd", not self.simd_running),
|
|
("2", "Start monocoque", not self.monocoque_running and self.simd_running),
|
|
("3", "Test configuration", True),
|
|
("4", "Edit monocoque config", True),
|
|
("5", "View logs", True),
|
|
("6", "Restart services", self.simd_running or self.monocoque_running),
|
|
("7", "Stop all", self.simd_running or self.monocoque_running),
|
|
("0", "Quit", True),
|
|
]
|
|
|
|
for key, label, enabled in menu_items:
|
|
if enabled:
|
|
print(f" [{key}] {label}")
|
|
else:
|
|
print(f" [{key}] {label} (disabled)")
|
|
|
|
try:
|
|
choice = input("\nEnter your choice: ").strip()
|
|
if choice == '0':
|
|
break
|
|
elif choice in ['1', '2', '3', '4', '5', '6', '7']:
|
|
# Check if action is enabled
|
|
for key, label, enabled in menu_items:
|
|
if key == choice and enabled:
|
|
self.handle_action(choice)
|
|
if choice in ['4']: # Actions that don't need terminal
|
|
print("Action completed.")
|
|
elif choice in ['6', '7']: # Stop/restart actions provide their own feedback
|
|
pass # Feedback already provided by handle_action
|
|
else:
|
|
print("Command executed. Check the terminal output.")
|
|
break
|
|
elif key == choice and not enabled:
|
|
print("That action is currently disabled.")
|
|
break
|
|
else:
|
|
print("Invalid choice. Please try again.")
|
|
except (KeyboardInterrupt, EOFError):
|
|
print("\nExiting...")
|
|
break
|
|
|
|
|
|
def main():
|
|
"""Entry point"""
|
|
manager = MonocoqueManager()
|
|
|
|
if not manager.check_installation():
|
|
print("Error: Monocoque does not appear to be installed")
|
|
print("Run the installer first: ./install.sh")
|
|
sys.exit(1)
|
|
|
|
# Check if we can use curses (proper terminal environment)
|
|
term = os.environ.get('TERM', 'dumb')
|
|
if term == 'dumb' or not sys.stdout.isatty():
|
|
print("Note: Using text mode (terminal doesn't support full TUI)")
|
|
manager.text_interface()
|
|
else:
|
|
try:
|
|
curses.wrapper(manager.main)
|
|
except KeyboardInterrupt:
|
|
print("\nExiting...")
|
|
except curses.error as e:
|
|
print(f"Terminal error: {e}")
|
|
print("Falling back to text mode...")
|
|
manager.text_interface()
|
|
except Exception as e:
|
|
print(f"Unexpected error: {e}")
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|