#!/bin/bash # Voculinux Uninstaller # This script removes Voculinux and cleans up the environment # Don't exit on error to allow for more robust cleanup # set -e # Function to display colored output print_info() { echo -e "\e[1;34m[INFO]\e[0m $1" } print_success() { echo -e "\e[1;32m[SUCCESS]\e[0m $1" } print_error() { echo -e "\e[1;31m[ERROR]\e[0m $1" } print_warning() { echo -e "\e[1;33m[WARNING]\e[0m $1" } # Function to check if a command exists command_exists() { command -v "$1" >/dev/null 2>&1 } # Read a numeric PID from a file (instance.lock / engine.pid). Empty if missing/invalid. read_pid_file() { local file="$1" local pid="" [ -f "$file" ] || return 0 pid=$(tr -d '[:space:]' < "$file" 2>/dev/null || true) if [[ "$pid" =~ ^[0-9]+$ ]]; then echo "$pid" fi } # True only for the Voculinux app / IBus engine — not editors, shells, or tests # whose argv merely contains the string "voculinux" (e.g. cwd under the repo). is_voculinux_process() { local pid="$1" if [ "$pid" = "$$" ] || [ "$pid" = "$PPID" ]; then return 1 fi [ -r "/proc/$pid/cmdline" ] || return 1 local stat stat=$(ps -o stat= -p "$pid" 2>/dev/null | awk '{print $1}') if [[ "$stat" == Z* ]]; then return 1 fi local cmdline cmdline=$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null) [ -n "$cmdline" ] || return 1 # Never target the installer/uninstaller themselves if [[ "$cmdline" == *"install.sh"* || "$cmdline" == *"uninstall.sh"* ]]; then return 1 fi # python -m vocalinux.main [--flags] if [[ "$cmdline" == *"-m vocalinux.main"* ]]; then return 0 fi # IBus engine: .../voculinux/.../ibus_engine.py if [[ "$cmdline" == *"ibus_engine.py"* && "$cmdline" == *"voculinux"* ]]; then return 0 fi # Console-script entrypoints. Setuptools scripts are executed as # `python …/bin/voculinux`, so argv0 is python — scan every arg for the # script path (require …/bin/voculinux to avoid matching a repo directory). local arg while IFS= read -r -d '' arg; do case "$arg" in voculinux|voculinux-gui|*/bin/voculinux|*/bin/voculinux-gui) return 0 ;; esac done < "/proc/$pid/cmdline" return 1 } # Prefer the PIDs the app writes; fall back to narrow entrypoint patterns only. # Deliberately does NOT use `pgrep -f voculinux` (matches editors/shells/cwd paths). get_voculinux_pids() { local data_home="${XDG_DATA_HOME:-$HOME/.local/share}" local -A seen=() local pid candidate local -a candidates=() for candidate in \ "$(read_pid_file "$data_home/voculinux/instance.lock")" \ "$(read_pid_file "$data_home/voculinux-ibus/engine.pid")" do [ -n "$candidate" ] && candidates+=("$candidate") done # Fallback when PID files are missing/stale: exact process name or known argv. while read -r candidate; do [ -n "$candidate" ] && candidates+=("$candidate") done < <( pgrep -u "$(id -u)" -x voculinux 2>/dev/null || true pgrep -u "$(id -u)" -x voculinux-gui 2>/dev/null || true pgrep -u "$(id -u)" -f -- '-m voculinux\.main' 2>/dev/null || true pgrep -u "$(id -u)" -f -- 'voculinux/.*/ibus_engine\.py' 2>/dev/null || true ) for pid in "${candidates[@]}"; do [ -n "${seen[$pid]:-}" ] && continue seen[$pid]=1 if is_voculinux_process "$pid"; then echo "$pid" fi done } # Function to safely remove a file or directory safe_remove() { local path="$1" local description="$2" # -e follows symlinks (false for a broken link), so also test -L to catch # dangling symlinks left behind by an install. if [ -e "$path" ] || [ -L "$path" ]; then print_info "Removing $description: $path" rm -rf "$path" && { print_success "Successfully removed $description" return 0 } || { print_error "Failed to remove $description: $path" return 1 } else print_info "$description not found: $path" return 0 fi } # Function to kill running Voculinux processes kill_voculinux_processes() { print_info "Checking for running Voculinux processes..." local PIDS PIDS=$(get_voculinux_pids || true) if [ -n "$PIDS" ]; then print_warning "Found running Voculinux process(es):" local pid for pid in $PIDS; do local cmd cmd=$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null || echo "?") print_warning " PID $pid: $cmd" done if [[ "$NON_INTERACTIVE" != "yes" ]]; then read -p "Kill running Voculinux process(es)? (Y/n) " -n 1 -r echo if [[ $REPLY =~ ^[Nn]$ ]]; then print_warning "Processes not killed. Uninstallation may be incomplete." return 0 fi fi print_info "Stopping Voculinux..." # shellcheck disable=SC2086 echo $PIDS | xargs -r kill -TERM 2>/dev/null || true sleep 2 local REMAINING_PIDS REMAINING_PIDS=$(get_voculinux_pids || true) if [ -n "$REMAINING_PIDS" ]; then print_warning "Some processes still running, forcing termination..." # shellcheck disable=SC2086 echo $REMAINING_PIDS | xargs -r kill -KILL 2>/dev/null || true sleep 1 fi local FINAL_PIDS FINAL_PIDS=$(get_voculinux_pids || true) if [ -n "$FINAL_PIDS" ]; then print_error "Warning: Could not terminate all Voculinux processes: $FINAL_PIDS" print_error "You may need to manually kill these processes before continuing." else print_success "All Voculinux processes stopped" fi else print_info "No running Voculinux processes found" fi } print_info "Voculinux Uninstaller" print_info "==============================" echo "" # Parse command line arguments KEEP_CONFIG="no" KEEP_DATA="no" NON_INTERACTIVE="no" # Repo-relative cleanup is only safe when this script lives in a source checkout. # The website command is `curl ... -o /tmp/vul.sh && bash /tmp/vul.sh`, so $PWD is # the user's directory and must not be walked. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SOURCE_CHECKOUT="" VENV_DIR="" if [[ -f "$SCRIPT_DIR/pyproject.toml" && -d "$SCRIPT_DIR/src/vocalinux" ]] \ && grep -q 'name = "voculinux"' "$SCRIPT_DIR/pyproject.toml" 2>/dev/null; then SOURCE_CHECKOUT="$SCRIPT_DIR" VENV_DIR="$SCRIPT_DIR/venv" fi # Detect if running non-interactively if [ ! -t 0 ]; then NON_INTERACTIVE="yes" fi while [[ $# -gt 0 ]]; do case $1 in --keep-config) KEEP_CONFIG="yes" shift ;; --keep-data) KEEP_DATA="yes" shift ;; --venv-dir=*) VENV_DIR="${1#*=}" shift ;; -y|--yes) NON_INTERACTIVE="yes" shift ;; --help) echo "Voculinux Uninstaller" echo "Usage: $0 [options]" echo "Options:" echo " --keep-config Keep configuration files" echo " --keep-data Keep application data (models, etc.)" echo " --venv-dir=PATH Virtual environment to remove. Absolute, or relative to a" echo " source checkout. Default: /venv when this script" echo " lives in a Voculinux tree; otherwise only the curl install" echo " at ~/.local/share/voculinux/venv is removed." echo " -y, --yes Non-interactive mode (no confirmation prompts)" echo " --help Show this help message" exit 0 ;; *) print_error "Unknown option: $1" echo "Use --help to see available options" exit 1 ;; esac done # A relative path is the checkout's venv, never $PWD. `bash /tmp/vul.sh --venv-dir=venv` # from $HOME must not delete ~/venv. if [[ -n "$VENV_DIR" && "$VENV_DIR" != /* ]]; then if [[ -n "$SOURCE_CHECKOUT" ]]; then VENV_DIR="$SOURCE_CHECKOUT/$VENV_DIR" else print_error "Refusing relative --venv-dir outside a source checkout: $VENV_DIR" print_error "Pass an absolute path. A curl uninstall already removes ~/.local/share/voculinux/venv." exit 1 fi fi # Define XDG directories CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/voculinux" DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/voculinux" IBUS_DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/voculinux-ibus" DESKTOP_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/applications" AUTOSTART_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/autostart" ICON_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor/scalable/apps" # Directories created by curl-based installation CURL_INSTALL_DIR="$HOME/.local/share/voculinux-install" CURL_VENV_DIR="$HOME/.local/share/voculinux/venv" CURL_BIN_DIR="$HOME/.local/bin" echo "Uninstallation options:" if [[ -n "$VENV_DIR" ]]; then echo "- Local virtual environment: $VENV_DIR" else echo "- Local virtual environment: (none; this script is not in a source checkout)" fi echo "- Curl-installed venv: $CURL_VENV_DIR" echo "- Curl-installed repo: $CURL_INSTALL_DIR" echo if [[ "$NON_INTERACTIVE" == "yes" ]]; then print_info "Non-interactive mode: proceeding with uninstallation..." else read -p "This will remove Voculinux from your system. Continue? (y/n) " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then print_info "Uninstallation cancelled." exit 0 fi if [[ "$KEEP_DATA" != "yes" ]]; then read -p "Do you want to keep your data (models, config)? (y/N) " -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]]; then KEEP_DATA="yes" KEEP_CONFIG="yes" print_info "Data and configuration will be preserved." fi fi fi # Function to handle virtual environment removal remove_virtual_environment() { # Deactivate the virtual environment if it's active if [[ -n "$VIRTUAL_ENV" ]]; then print_warning "Virtual environment is active. Deactivating..." deactivate 2>/dev/null || { print_warning "Failed to deactivate virtual environment. This is not critical." } fi # Checkout venv only. A curl install is removed via CURL_VENV_DIR below. if [[ -z "$VENV_DIR" ]]; then print_info "No source-checkout virtual environment to remove." elif [ -d "$VENV_DIR" ]; then print_info "Removing local virtual environment..." safe_remove "$VENV_DIR" "local virtual environment directory" else print_info "Local virtual environment not found: $VENV_DIR" fi # Remove curl-installed venv if [ -d "$CURL_VENV_DIR" ]; then print_info "Removing curl-installed virtual environment..." safe_remove "$CURL_VENV_DIR" "curl-installed virtual environment" fi # Remove the voculinux data directory if empty (contains venv) local VOCULINUX_DATA_DIR="$HOME/.local/share/voculinux" if [ -d "$VOCULINUX_DATA_DIR" ] && [ -z "$(ls -A "$VOCULINUX_DATA_DIR" 2>/dev/null)" ]; then safe_remove "$VOCULINUX_DATA_DIR" "empty voculinux data directory" fi } # Function to remove curl-installed files remove_curl_install_files() { print_info "Removing curl-installation files..." # Remove cloned repository safe_remove "$CURL_INSTALL_DIR" "cloned repository" # Launcher scripts/symlinks in ~/.local/bin are removed in # remove_application_files() (they exist for both curl and repo installs). # Remove activation script in ~/.local/bin safe_remove "$CURL_BIN_DIR/activate-voculinux.sh" "activation script in ~/.local/bin" } # Function to remove application files remove_application_files() { # Only the copy inside a source checkout. A relative path would delete # whatever activate-voculinux.sh happens to sit in $PWD. if [[ -n "$SOURCE_CHECKOUT" ]]; then safe_remove "$SOURCE_CHECKOUT/activate-voculinux.sh" "local activation script" fi # Remove launcher wrappers in ~/.local/bin created by install.sh. These are # regular files in a repo/dev install and may be symlinks in a curl install; # safe_remove handles both (and dangling symlinks). safe_remove "$CURL_BIN_DIR/voculinux" "voculinux launcher script" safe_remove "$CURL_BIN_DIR/voculinux-gui" "voculinux-gui launcher script" # Remove desktop entry safe_remove "$DESKTOP_DIR/voculinux.desktop" "desktop entry" safe_remove "$AUTOSTART_DIR/voculinux.desktop" "autostart desktop entry" # Remove icons print_info "Removing application icons..." local ICONS=( "voculinux.svg" "voculinux-microphone.svg" "voculinux-microphone-off.svg" "voculinux-microphone-process.svg" ) for icon in "${ICONS[@]}"; do safe_remove "$ICON_DIR/$icon" "icon" done # Update icon cache if command_exists gtk-update-icon-cache; then print_info "Updating icon cache..." gtk-update-icon-cache -f -t "${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor" 2>/dev/null || { print_warning "Failed to update icon cache. This is not critical." } fi } # Function to remove configuration and data remove_config_and_data() { # Remove configuration if not keeping it if [[ "$KEEP_CONFIG" != "yes" ]]; then safe_remove "$CONFIG_DIR" "configuration directory" else print_info "Keeping configuration directory as requested: $CONFIG_DIR" fi # Remove data if not keeping it if [[ "$KEEP_DATA" != "yes" ]]; then safe_remove "$DATA_DIR" "application data directory" else print_info "Keeping application data directory as requested: $DATA_DIR" fi # IBus runtime data (socket, PID file, logs) is recreated on next launch. safe_remove "$IBUS_DATA_DIR" "IBus runtime data directory" } # Clean build artifacts inside a Voculinux source checkout. # $1 must be an absolute directory. A relative path would follow the caller's # working directory ($HOME, when the website command runs this script from # /tmp). -xdev stays on one filesystem. -prune removes a directory without # find walking back into it afterwards. cleanup_build_artifacts() { local root="$1" local egg_info_status=0 # A relative path would follow the caller's working directory. A checkout # that an earlier step already removed (the curl clone) is not an error. if [[ "$root" != /* ]]; then print_error "Refusing to clean build artifacts outside a source checkout." return 1 fi if [[ ! -d "$root" ]]; then print_info "Source checkout already removed; skipping build-artifact cleanup." return 0 fi print_info "Cleaning up build artifacts..." safe_remove "$root/build" "build directory" safe_remove "$root/dist" "distribution directory" # voculinux metadata lives in src/ (setuptools src layout) and sometimes as # *.egg-info at the checkout root. Do not search the whole tree: .venv is # a separate dev environment and its egg-info has to stay. find "$root/src" -xdev -name "*.egg-info" -type d -prune -exec rm -rf {} + \ 2>/dev/null || egg_info_status=1 find "$root" -xdev -mindepth 1 -maxdepth 1 -name "*.egg-info" -type d \ -prune -exec rm -rf {} + 2>/dev/null || egg_info_status=1 if [[ "$egg_info_status" -ne 0 ]]; then print_warning "Failed to remove some egg-info directories." fi # -prune on .venv: same reason. Bytecode under the project is removed; # the dev environment is not. The parens keep -xdev on both sides of -o. find "$root" -xdev \( \ -name .venv -prune -o \ \( -name "__pycache__" -o -name ".pytest_cache" \) -type d -prune -exec rm -rf {} + \ \) 2>/dev/null || { print_warning "Failed to remove some cache directories." } print_info "Cleaning up temporary files..." # -delete implies -depth, which disables -prune, so .venv would be walked. find "$root" -xdev \( \ -name .venv -prune -o \ \( -name "*.pyc" -o -name "*.pyo" -o -name ".coverage" \) -type f -exec rm -f {} + \ \) 2>/dev/null || { print_warning "Failed to remove some temporary files." } safe_remove "$root/voculinux-run.py" "wrapper script" } # Function to verify uninstallation verify_uninstallation() { print_info "Verifying uninstallation..." local ISSUES=0 # Check if local virtual environment still exists if [[ -n "$VENV_DIR" && -d "$VENV_DIR" ]]; then print_warning "Local virtual environment still exists: $VENV_DIR" ((ISSUES++)) fi # Check if curl-installed virtual environment still exists if [ -d "$CURL_VENV_DIR" ]; then print_warning "Curl-installed venv still exists: $CURL_VENV_DIR" ((ISSUES++)) fi # Check if cloned repository still exists if [ -d "$CURL_INSTALL_DIR" ]; then print_warning "Cloned repository still exists: $CURL_INSTALL_DIR" ((ISSUES++)) fi # Check if launcher wrappers (files or symlinks) still exist for launcher in voculinux voculinux-gui; do if [ -e "$CURL_BIN_DIR/$launcher" ] || [ -L "$CURL_BIN_DIR/$launcher" ]; then print_warning "Launcher script still exists: $CURL_BIN_DIR/$launcher" ((ISSUES++)) fi done # Check if desktop entry still exists if [ -f "$DESKTOP_DIR/voculinux.desktop" ]; then print_warning "Desktop entry still exists: $DESKTOP_DIR/voculinux.desktop" ((ISSUES++)) fi if [ -f "$AUTOSTART_DIR/voculinux.desktop" ]; then print_warning "Autostart desktop entry still exists: $AUTOSTART_DIR/voculinux.desktop" ((ISSUES++)) fi # Check if any icons still exist local ICON_COUNT=0 for icon in voculinux.svg voculinux-microphone.svg voculinux-microphone-off.svg voculinux-microphone-process.svg; do if [ -f "$ICON_DIR/$icon" ]; then ((ICON_COUNT++)) fi done if [ "$ICON_COUNT" -gt 0 ]; then print_warning "$ICON_COUNT icons still exist in $ICON_DIR" ((ISSUES++)) fi # Check if configuration still exists (only if not keeping it) if [[ "$KEEP_CONFIG" != "yes" && -d "$CONFIG_DIR" ]]; then print_warning "Configuration directory still exists: $CONFIG_DIR" ((ISSUES++)) fi # Check if data still exists (only if not keeping it) if [[ "$KEEP_DATA" != "yes" && -d "$DATA_DIR" ]]; then print_warning "Application data directory still exists: $DATA_DIR" ((ISSUES++)) fi # IBus runtime data should always be removed if [ -d "$IBUS_DATA_DIR" ]; then print_warning "IBus runtime data directory still exists: $IBUS_DATA_DIR" ((ISSUES++)) fi # Return the number of issues found return $ISSUES } # Function to print uninstallation summary print_uninstallation_summary() { local ISSUES=$1 echo echo "==============================" echo " UNINSTALLATION SUMMARY" echo "==============================" echo if [ "$ISSUES" -eq 0 ]; then print_success "Uninstallation completed successfully with no issues!" else print_warning "Uninstallation completed with $ISSUES potential issue(s)." print_warning "Some files or directories may still exist." fi echo if [[ "$KEEP_CONFIG" == "yes" ]]; then print_info "Configuration directory was kept: $CONFIG_DIR" fi if [[ "$KEEP_DATA" == "yes" ]]; then print_info "Application data directory was kept: $DATA_DIR" fi echo print_info "Your system has been cleaned up and is ready for a fresh installation." if [ "$ISSUES" -gt 0 ]; then echo print_warning "If you encounter any problems, please report them at:" print_warning "https://github.com/VocaHQ/vocalinux/issues" fi } # Kill any running Voculinux processes first kill_voculinux_processes # Perform uninstallation steps remove_virtual_environment # Clean the checkout before remove_curl_install_files. A script that lives in # ~/.local/share/voculinux-install is both the checkout and the curl clone, and # that step deletes the directory. if [[ -n "$SOURCE_CHECKOUT" ]]; then cleanup_build_artifacts "$SOURCE_CHECKOUT" else print_info "Not running from a source checkout; skipping build-artifact cleanup." fi remove_curl_install_files remove_application_files remove_config_and_data # Verify uninstallation verify_uninstallation UNINSTALL_ISSUES=$? # Print uninstallation summary print_uninstallation_summary $UNINSTALL_ISSUES print_success "Uninstallation process completed!"