feat: Add universal installer and TUI manager (#26)

* feat: Add universal installer and TUI manager

This vibe-coded project now includes a comprehensive installation system
that simplifies monocoque setup across all major Linux distributions.

Key Features:
- Universal installer (install.sh) with automatic distribution detection
- Installs binaries to ~/.local/share/monocoque (user-local, no root needed)
- Creates launcher scripts in ~/.local/bin (start-simd, start-monocoque, test-monocoque)
- Interactive TUI manager (monocoque-manager) for service management
- Complete uninstaller (tools/uninstall.sh) with selective removal
- Dynamic systemd service generation for auto-start capability

Installation Components:
- install.sh: One-command installation across Arch, Debian/Ubuntu, Fedora, openSUSE
- tools/monocoque-manager: Python TUI for real-time status monitoring
- tools/uninstall.sh: Clean removal with config/log preservation options
- Updated README.md with quick install instructions

Architecture:
- User-local installation (no system pollution)
- Binaries: ~/.local/share/monocoque/{monocoque,simapi,simshmbridge}/
- Configs: ~/.config/{monocoque,simd}/
- Launchers: ~/.local/bin/{start-*,test-*,monocoque-manager}
- Services: ~/.config/systemd/user/ (generated dynamically)

Distribution Support:
- AUR integration for Arch users (fastest installation path)
- Native package manager integration (pacman, apt, dnf, zypper)
- Comprehensive dependency handling and build automation

This reduces installation time from 30-60 minutes to 5-15 minutes while
providing better user experience and easier maintenance.

Tested on Garuda Linux (Arch-based) - successful installation, operation, and cleanup.

* refactor: address PR feedback and improve XDG compliance

- Consolidate installers: removed universal-install.sh in favor of install.sh
- Fix XDG compliance: updated install.sh, uninstall.sh, and monocoque-manager to respect XDG_DATA_HOME, XDG_CONFIG_HOME, and XDG_CACHE_HOME with proper fallbacks
- Remove --no-daemon flag from simd launcher and systemd service as requested
- Improve TUI compatibility: fixed popen execution for Kitty (confirmed working), xfce4-terminal, Alacritty, and xterm
- Add documentation: added references to spacefreak18.github.io/simapi/ in README and installer
This commit is contained in:
M4X1K02 2025-12-23 23:11:51 +01:00 committed by GitHub
parent a962d658a3
commit d984e5ee86
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 1328 additions and 0 deletions

View File

@ -8,6 +8,8 @@ _ / / / / /_/ / / / / /_/ / /__ / /_/ / /_/ // /_/ // __/
``` ```
Cross Platform device manager for driving and flight simulators, for use with common simulator software titles. Cross Platform device manager for driving and flight simulators, for use with common simulator software titles.
📚 **Documentation:** [spacefreak18.github.io/simapi/](https://spacefreak18.github.io/simapi/)
## Features ## Features
- Updates at 60 frames per seconds. - Updates at 60 frames per seconds.
- Modular design for support with various titles and devices. - Modular design for support with various titles and devices.
@ -18,6 +20,34 @@ Cross Platform device manager for driving and flight simulators, for use with co
- Convincing shaker effects for noise tranducers for wheel slip, wheel lock, and abs, as well as engine rpm and gear shifts. - Convincing shaker effects for noise tranducers for wheel slip, wheel lock, and abs, as well as engine rpm and gear shifts.
- Choice of Portaudio or Pulseaudio (libpulse) backend. - Choice of Portaudio or Pulseaudio (libpulse) backend.
## Quick Install
**One-Line Installation:**
```bash
curl -fsSL https://raw.githubusercontent.com/Spacefreak18/monocoque/master/install.sh | bash
```
**Or download and review first:**
```bash
wget https://raw.githubusercontent.com/Spacefreak18/monocoque/master/install.sh
chmod +x install.sh
./install.sh
```
**TUI Manager:**
After installation, use the interactive manager:
```bash
monocoque-manager
```
**Supported Distributions:**
- ✅ Arch Linux (with AUR support)
- ✅ Debian/Ubuntu
- ✅ Fedora/RHEL/CentOS
- ✅ openSUSE
For manual installation or troubleshooting, see [HOW-TO-USE.md](HOW-TO-USE.md).
## Supported Games ( see [simapi](https://github.com/spacefreak18/simapi) for more details of what is supported from each sim ) ## Supported Games ( see [simapi](https://github.com/spacefreak18/simapi) for more details of what is supported from each sim )
- Using [SimSHMBridge](https://github.com/spacefreak18/simshmbridge) - Using [SimSHMBridge](https://github.com/spacefreak18/simshmbridge)
- Asseto Corsa - Asseto Corsa

554
install.sh Executable file
View File

@ -0,0 +1,554 @@
#!/bin/bash
set -e
# Monocoque Universal Installer
# Works on: Arch, Debian/Ubuntu, Fedora, and other major distros
# Version: 1.0.0
SCRIPT_VERSION="1.0.0"
INSTALL_DIR="${MONOCOQUE_INSTALL_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/monocoque}"
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}"
BIN_DIR="${HOME}/.local/bin"
# Color output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Logging functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
print_header() {
echo ""
echo "╔══════════════════════════════════════════════════════════════════╗"
echo "║ Monocoque Universal Installer v${SCRIPT_VERSION}"
echo "╚══════════════════════════════════════════════════════════════════╝"
echo ""
}
# Detect distribution
detect_distro() {
if [ -f /etc/os-release ]; then
. /etc/os-release
DISTRO=$ID
DISTRO_VERSION=$VERSION_ID
DISTRO_LIKE=$ID_LIKE
elif [ -f /etc/arch-release ]; then
DISTRO="arch"
else
DISTRO="unknown"
fi
# Normalize Arch-based distributions
if [[ "$DISTRO_LIKE" == *"arch"* ]] || [[ "$DISTRO" == "manjaro" ]] || [[ "$DISTRO" == "garuda" ]] || [[ "$DISTRO" == "endeavouros" ]]; then
log_info "Detected distribution: $DISTRO (Arch-based)"
DISTRO="arch"
else
log_info "Detected distribution: $DISTRO"
fi
}
# Check if running as root
check_root() {
if [ "$EUID" -eq 0 ]; then
log_error "Please do not run this script as root"
exit 1
fi
}
# Check for required commands
check_requirements() {
local missing_commands=()
for cmd in git cmake make gcc; do
if ! command -v $cmd &> /dev/null; then
missing_commands+=($cmd)
fi
done
if [ ${#missing_commands[@]} -ne 0 ]; then
log_error "Missing required commands: ${missing_commands[*]}"
log_info "Please install them first"
exit 1
fi
}
# Install dependencies based on distro
install_dependencies() {
log_info "Installing dependencies for $DISTRO..."
case $DISTRO in
arch|manjaro)
local deps="yder libuv argtable libserialport libconfig hidapi lua libxdg-basedir mingw-w64-gcc"
# Check if yay is available for AUR
if command -v yay &> /dev/null; then
log_info "Using yay to check for AUR packages..."
echo ""
echo "You can install monocoque from AUR instead:"
echo " yay -S simapi-git simd-git monocoque-git"
echo ""
read -p "Install from AUR? (recommended) [Y/n]: " use_aur
if [[ ! $use_aur =~ ^[Nn]$ ]]; then
log_info "Installing from AUR..."
yay -S --needed simapi-git simd-git monocoque-git
# Still need simshmbridge
log_info "Building simshmbridge (not in AUR)..."
build_simshmbridge_only
# Setup configs and services
setup_configs
setup_systemd_services
# create_launcher_scripts uses SCRIPT_DIR which is set in main()
create_launcher_scripts
log_success "Installation complete (AUR method)!"
print_next_steps
exit 0
fi
fi
log_info "Installing build dependencies with pacman..."
sudo pacman -S --needed --noconfirm $deps
;;
ubuntu|debian|linuxmint|pop)
local deps="build-essential git cmake libyder-dev libuv1-dev libargtable2-dev libserialport-dev libconfig-dev libhidapi-dev lua5.3 liblua5.3-dev libxdg-basedir-dev mingw-w64"
log_info "Updating package lists..."
sudo apt-get update
log_info "Installing build dependencies..."
sudo apt-get install -y $deps
;;
fedora|rhel|centos)
local deps="git cmake gcc gcc-c++ yder-devel libuv-devel argtable-devel libserialport-devel libconfig-devel hidapi-devel lua-devel libxdg-basedir-devel mingw64-gcc"
log_info "Installing build dependencies..."
sudo dnf install -y $deps
;;
opensuse*)
local deps="git cmake gcc gcc-c++ libyder-devel libuv-devel argtable-devel libserialport-devel libconfig-devel hidapi-devel lua-devel libxdg-basedir-devel mingw64-gcc"
log_info "Installing build dependencies..."
sudo zypper install -y $deps
;;
*)
log_error "Unsupported distribution: $DISTRO"
log_info "Please install dependencies manually and try again"
log_info "Required: git, cmake, gcc, yder, libuv, argtable, libserialport, libconfig, hidapi, lua, libxdg-basedir, mingw-gcc"
exit 1
;;
esac
log_success "Dependencies installed"
}
# Clone repositories
clone_repositories() {
log_info "Creating installation directory: $INSTALL_DIR"
mkdir -p "$INSTALL_DIR"
cd "$INSTALL_DIR"
# Clone monocoque
if [ ! -d "monocoque" ]; then
log_info "Cloning monocoque..."
git clone https://github.com/Spacefreak18/monocoque.git
cd monocoque
git submodule sync --recursive
git submodule update --init --recursive
else
log_info "monocoque already cloned, updating..."
cd monocoque
git pull
git submodule sync --recursive
git submodule update --init --recursive
fi
cd "$INSTALL_DIR"
# Clone simapi
if [ ! -d "simapi" ]; then
log_info "Cloning simapi..."
git clone https://github.com/Spacefreak18/simapi.git
else
log_info "simapi already cloned, updating..."
cd simapi
git pull
fi
cd "$INSTALL_DIR"
# Clone simshmbridge
if [ ! -d "simshmbridge" ]; then
log_info "Cloning simshmbridge..."
git clone https://github.com/spacefreak18/simshmbridge.git
cd simshmbridge
git submodule sync --recursive
git submodule update --init --recursive
else
log_info "simshmbridge already cloned, updating..."
cd simshmbridge
git pull
git submodule sync --recursive
git submodule update --init --recursive
fi
log_success "Repositories cloned/updated"
}
# Build simapi
build_simapi() {
log_info "Building simapi..."
cd "$INSTALL_DIR/simapi"
mkdir -p build
cd build
cmake ..
make -j$(nproc)
log_info "Installing simapi library..."
sudo make install
# Update library cache
sudo ldconfig 2>/dev/null || true
log_success "simapi built and installed"
}
# Build simd
build_simd() {
log_info "Building simd..."
cd "$INSTALL_DIR/simapi/simd"
rm -rf build
mkdir -p build
cd build
cmake ..
make -j$(nproc)
log_success "simd built"
}
# Build simshmbridge
build_simshmbridge() {
log_info "Building simshmbridge..."
cd "$INSTALL_DIR/simshmbridge"
make clean || true
make -j$(nproc)
log_success "simshmbridge built"
}
build_simshmbridge_only() {
mkdir -p "$INSTALL_DIR"
cd "$INSTALL_DIR"
if [ ! -d "simshmbridge" ]; then
log_info "Cloning simshmbridge..."
git clone https://github.com/spacefreak18/simshmbridge.git
cd simshmbridge
git submodule sync --recursive
git submodule update --init --recursive
fi
build_simshmbridge
}
# Build monocoque
build_monocoque() {
log_info "Building monocoque..."
cd "$INSTALL_DIR/monocoque"
mkdir -p build
cd build
cmake ..
make -j$(nproc)
log_success "monocoque built"
}
# Setup configuration files
setup_configs() {
log_info "Setting up configuration files..."
# simd config
mkdir -p "$CONFIG_DIR/simd"
if [ ! -f "$CONFIG_DIR/simd/simd.config" ]; then
if [ -f "$INSTALL_DIR/simapi/simd/conf/simd.config" ]; then
cp "$INSTALL_DIR/simapi/simd/conf/simd.config" "$CONFIG_DIR/simd/simd.config"
log_success "Created simd config"
fi
else
log_info "simd config already exists, skipping"
fi
# monocoque config
mkdir -p "$CONFIG_DIR/monocoque"
if [ ! -f "$CONFIG_DIR/monocoque/monocoque.config" ]; then
cat > "$CONFIG_DIR/monocoque/monocoque.config" << 'EOF'
configs = (
{
sim = "default";
car = "default";
devices = (
// Add your devices here
// Example: Serial device (ESP32/Arduino)
/*
{
device = "Serial";
type = "Custom";
config = "None";
baud = 115200;
devpath = "/dev/ttyUSB0";
},
*/
// Example: Bass shaker
/*
{
device = "Sound";
effect = "Engine";
devid = "alsa_output.your_device_here";
pan = 0;
fps = 60;
threshold = 0.2;
channels = 2;
volume = 70;
modulation = "frequency";
frequency = 17;
frequencyMax = 37;
},
*/
);
}
);
EOF
log_success "Created monocoque config"
else
log_info "monocoque config already exists, skipping"
fi
}
# Create launcher scripts
create_launcher_scripts() {
log_info "Creating launcher scripts in $BIN_DIR..."
mkdir -p "$BIN_DIR"
# Find simd binary location
local SIMD_BIN=""
if [ -f "$INSTALL_DIR/simapi/simd/build/simd" ]; then
SIMD_BIN="$INSTALL_DIR/simapi/simd/build/simd"
elif command -v simd &> /dev/null; then
SIMD_BIN=$(which simd)
fi
# Find monocoque binary location
local MONOCOQUE_BIN=""
if [ -f "$INSTALL_DIR/monocoque/build/monocoque" ]; then
MONOCOQUE_BIN="$INSTALL_DIR/monocoque/build/monocoque"
elif command -v monocoque &> /dev/null; then
MONOCOQUE_BIN=$(which monocoque)
fi
# start-simd script
cat > "$BIN_DIR/start-simd" << EOF
#!/bin/bash
export LD_LIBRARY_PATH=\$LD_LIBRARY_PATH:/usr/local/lib:/usr/local/lib64
exec $SIMD_BIN -vv "\$@"
EOF
chmod +x "$BIN_DIR/start-simd"
# start-monocoque script
cat > "$BIN_DIR/start-monocoque" << EOF
#!/bin/bash
exec $MONOCOQUE_BIN play "\$@"
EOF
chmod +x "$BIN_DIR/start-monocoque"
# test-monocoque script
cat > "$BIN_DIR/test-monocoque" << EOF
#!/bin/bash
exec $MONOCOQUE_BIN test -vv "\$@"
EOF
chmod +x "$BIN_DIR/test-monocoque"
# monocoque-manager TUI
log_info "Looking for monocoque-manager to install..."
local MANAGER_INSTALLED=false
# Use the SCRIPT_DIR exported from main()
# Try multiple locations
local SEARCH_PATHS=(
"$SCRIPT_DIR/monocoque-manager"
"$SCRIPT_DIR/tools/monocoque-manager"
)
for MANAGER_PATH in "${SEARCH_PATHS[@]}"; do
log_info "Checking: $MANAGER_PATH"
if [ -f "$MANAGER_PATH" ]; then
log_info "Found monocoque-manager at: $MANAGER_PATH"
cp "$MANAGER_PATH" "$BIN_DIR/monocoque-manager"
chmod +x "$BIN_DIR/monocoque-manager"
log_success "✓ Installed monocoque-manager TUI to $BIN_DIR/monocoque-manager"
MANAGER_INSTALLED=true
break
fi
done
if [ "$MANAGER_INSTALLED" = false ]; then
log_error "monocoque-manager not found!"
log_warn "Searched in: ${SEARCH_PATHS[*]}"
log_warn "SCRIPT_DIR was: $SCRIPT_DIR"
log_info "Please copy it manually: cp $SCRIPT_DIR/monocoque-manager ~/.local/bin/"
fi
# Check if BIN_DIR is in PATH
if [[ ":$PATH:" != *":$BIN_DIR:"* ]]; then
log_warn "$BIN_DIR is not in your PATH"
log_info "Add this to your shell config:"
echo " # For bash/zsh:"
echo " export PATH=\"\$HOME/.local/bin:\$PATH\""
echo ""
echo " # For fish:"
echo " fish_add_path ~/.local/bin"
fi
log_success "Launcher scripts created"
}
# Setup systemd user services
setup_systemd_services() {
local SYSTEMD_DIR="$HOME/.config/systemd/user"
mkdir -p "$SYSTEMD_DIR"
log_info "Creating systemd service files..."
# Find binary locations
local SIMD_BIN=""
if [ -f "$INSTALL_DIR/simapi/simd/build/simd" ]; then
SIMD_BIN="$INSTALL_DIR/simapi/simd/build/simd"
elif command -v simd &> /dev/null; then
SIMD_BIN=$(which simd)
fi
# simd service
cat > "$SYSTEMD_DIR/simd.service" << EOF
[Unit]
Description=Sim Telemetry Daemon
Documentation=https://spacefreak18.github.io/simapi/
After=network.target
[Service]
Type=simple
Environment="LD_LIBRARY_PATH=/usr/local/lib:/usr/local/lib64"
ExecStart=$SIMD_BIN
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
EOF
log_success "systemd services created"
log_info "To enable auto-start on boot:"
echo " systemctl --user enable simd.service"
echo " systemctl --user start simd.service"
}
# Print next steps
print_next_steps() {
echo ""
echo "╔══════════════════════════════════════════════════════════════════╗"
echo "║ Installation Complete! ║"
echo "╚══════════════════════════════════════════════════════════════════╝"
echo ""
log_success "Monocoque and all components are installed!"
echo ""
echo "📁 Installation location: $INSTALL_DIR"
echo "⚙️ Configuration: $CONFIG_DIR/simd and $CONFIG_DIR/monocoque"
echo "🚀 Launcher scripts: $BIN_DIR"
echo ""
echo "Next steps:"
echo ""
echo "1⃣ Configure your games (REQUIRED!)"
echo " See: https://github.com/Spacefreak18/monocoque/blob/master/HOW-TO-USE.md"
echo ""
echo "2⃣ Start the services:"
echo " Terminal 1: start-simd"
echo " Terminal 2: start-monocoque"
echo ""
echo " OR enable auto-start:"
echo " systemctl --user enable --now simd.service"
echo ""
echo "3⃣ Configure your devices:"
echo " Edit: $CONFIG_DIR/monocoque/monocoque.config"
echo ""
echo "4⃣ Test your setup:"
echo " test-monocoque"
echo ""
echo "🎮 Supported games:"
echo " • Assetto Corsa / ACC"
echo " • Automobilista 2"
echo " • Project Cars 2"
echo " • RFactor 2"
echo " • Euro/American Truck Simulator"
echo ""
echo "📚 Documentation: https://spacefreak18.github.io/simapi/"
echo ""
}
# Main installation flow
main() {
# Save the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export SCRIPT_DIR
print_header
check_root
detect_distro
check_requirements
log_info "Starting installation..."
echo ""
install_dependencies
clone_repositories
log_info "Building components (this may take a few minutes)..."
build_simapi
build_simd
build_monocoque
build_simshmbridge
setup_configs
create_launcher_scripts
setup_systemd_services
print_next_steps
}
# Run main function
main "$@"

616
tools/monocoque-manager Executable file
View File

@ -0,0 +1,616 @@
#!/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()

128
tools/uninstall.sh Executable file
View File

@ -0,0 +1,128 @@
#!/bin/bash
# Monocoque Uninstaller
# Removes monocoque and all related components
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
INSTALL_DIR="${MONOCOQUE_INSTALL_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/monocoque}"
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}"
CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}"
BIN_DIR="$HOME/.local/bin"
SYSTEMD_DIR="$CONFIG_DIR/systemd/user"
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
print_header() {
echo ""
echo "╔══════════════════════════════════════════════════════════════════╗"
echo "║ Monocoque Uninstaller ║"
echo "╚══════════════════════════════════════════════════════════════════╝"
echo ""
}
print_header
log_warn "This will remove:"
echo " • Monocoque installation ($INSTALL_DIR)"
echo " • Configuration files ($CONFIG_DIR/monocoque, $CONFIG_DIR/simd)"
echo " • Launcher scripts ($BIN_DIR/start-*, test-monocoque)"
echo " • systemd service files ($SYSTEMD_DIR/simd.service)"
echo " • Log files ($CACHE_DIR/monocoque)"
echo ""
echo "This will NOT remove:"
echo " • System dependencies (yder, libuv, etc.)"
echo " • Compiled simapi library (/usr/local/lib/libsimapi.so)"
echo ""
read -p "Continue with uninstallation? [y/N]: " confirm
if [[ ! $confirm =~ ^[Yy]$ ]]; then
log_info "Uninstallation cancelled"
exit 0
fi
echo ""
log_info "Starting uninstallation..."
# Stop running services
log_info "Stopping running services..."
pkill -x simd 2>/dev/null || true
pkill -x monocoque 2>/dev/null || true
systemctl --user stop simd.service 2>/dev/null || true
systemctl --user disable simd.service 2>/dev/null || true
# Remove installation directory
if [ -d "$INSTALL_DIR" ]; then
log_info "Removing installation directory..."
rm -rf "$INSTALL_DIR"
log_success "Installation directory removed"
else
log_info "Installation directory not found, skipping"
fi
# Remove configuration (ask first)
if [ -d "$CONFIG_DIR/monocoque" ] || [ -d "$CONFIG_DIR/simd" ]; then
read -p "Remove configuration files? [y/N]: " remove_config
if [[ $remove_config =~ ^[Yy]$ ]]; then
rm -rf "$CONFIG_DIR/monocoque" 2>/dev/null || true
rm -rf "$CONFIG_DIR/simd" 2>/dev/null || true
log_success "Configuration files removed"
else
log_info "Keeping configuration files"
fi
fi
# Remove launcher scripts
log_info "Removing launcher scripts..."
rm -f "$BIN_DIR/start-simd" 2>/dev/null || true
rm -f "$BIN_DIR/start-monocoque" 2>/dev/null || true
rm -f "$BIN_DIR/test-monocoque" 2>/dev/null || true
rm -f "$BIN_DIR/monocoque-manager" 2>/dev/null || true
# Remove systemd services
if [ -f "$SYSTEMD_DIR/simd.service" ]; then
log_info "Removing systemd service files..."
rm -f "$SYSTEMD_DIR/simd.service"
systemctl --user daemon-reload 2>/dev/null || true
fi
# Remove logs
if [ -d "$CACHE_DIR/monocoque" ]; then
read -p "Remove log files? [y/N]: " remove_logs
if [[ $remove_logs =~ ^[Yy]$ ]]; then
rm -rf "$CACHE_DIR/monocoque"
log_success "Log files removed"
else
log_info "Keeping log files"
fi
fi
echo ""
log_success "Uninstallation complete!"
echo ""
log_info "To remove system dependencies and simapi library:"
echo " • Remove packages: yder, libuv, argtable, libserialport, etc."
echo " • Remove library: sudo rm /usr/local/lib/libsimapi.so*"
echo " • Run: sudo ldconfig"
echo ""