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:
parent
a962d658a3
commit
d984e5ee86
30
README.md
30
README.md
|
|
@ -8,6 +8,8 @@ _ / / / / /_/ / / / / /_/ / /__ / /_/ / /_/ // /_/ // __/
|
|||
```
|
||||
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
|
||||
- Updates at 60 frames per seconds.
|
||||
- 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.
|
||||
- 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 )
|
||||
- Using [SimSHMBridge](https://github.com/spacefreak18/simshmbridge)
|
||||
- Asseto Corsa
|
||||
|
|
|
|||
|
|
@ -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 "$@"
|
||||
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
@ -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 ""
|
||||
|
||||
Loading…
Reference in New Issue