Tom_Neverwinter icon

koboldcpp android install

Tom_Neverwinter | PRO | 05/19/25 06:49:25 AM UTC (Edited) | 0 ⭐ | 386 👁️ | Never ⏰ | []
text |

100.21 KB

|

None

|

0 👍

/

0 👎

#!/data/data/com.termux/files/usr/bin/env bash
#
# KoboldCpp Termux Interactive Installer & Manager
# Version 3.1 (with Model Cache Management enhancements)
#
# --- Usage & Requirements ---
#
# Usage:
#    bash koboldcpp_manager.sh
#
# Requirements:
#    - Termux environment on Android (v0.118+ recommended)
#    - Bash shell
#    - Internet connection for downloading KoboldCpp and models
#    - Termux:API package (optional, but recommended for wake lock functionality)
#      Install with: pkg install termux-api
#
# For more advanced KoboldCpp setup and options, refer to the official documentation:
#    https://github.com/LostRuins/koboldcpp
#
 # --- Prevent Sourcing ---
if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then
  echo "This script is meant to be executed, not sourced." >&2
  if (return 0 2>/dev/null); then return 1; else exit 1; fi
fi
 # --- Script Configuration ---
set -euo pipefail # Exit on error, undefined variable, and pipe failure.
 # --- Early Initializations ---
# Global flag to ensure log directory creation is attempted only once.
LOG_FILE_DIR_INIT_ATTEMPTED=false
 # Function to ensure the log directory exists
_ensure_log_dir_exists_once() {
    if [ "$LOG_FILE_DIR_INIT_ATTEMPTED" = true ]; then
        return
    fi
    local current_log_file_path="$HOME/koboldcpp_install.log"
    local log_dir
    log_dir=$(dirname "$current_log_file_path")
     if ! mkdir -p "$log_dir"; then
        echo "CRITICAL ERROR: Could not create log directory '$log_dir'. Logging will fail. Aborting." >&2
        exit 1
    fi
    LOG_FILE_DIR_INIT_ATTEMPTED=true
}
_ensure_log_dir_exists_once # Call immediately
 # --- Global Readonly Variables ---
readonly KOBOLDCPP_DIR="$HOME/koboldcpp"
readonly MODEL_DIR="$KOBOLDCPP_DIR/models" # This is your model cache directory
readonly EXPECTED_MODEL_DIR_PATH="$HOME/koboldcpp/models"
readonly DOWNLOAD_DIR="$HOME/storage/downloads"
readonly BACKUP_DIR="$HOME/koboldcpp_backup"
readonly LOG_FILE="$HOME/koboldcpp_install.log"
readonly REPO_URL="https://github.com/LostRuins/koboldcpp.git"
readonly DEFAULT_BRANCH="main"
readonly CONFIG_FILE="$HOME/.koboldcpp_config"
readonly VERSION="3.1"
 # Colors for prettier output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[0;33m'
readonly BLUE='\033[0;34m'
readonly CYAN='\033[0;36m'
readonly BOLD='\033[1m'
readonly NC='\033[0m' # No Color
 # --- Portable File Size Command Initialization ---
DU_BYTES_CMD=""
DU_BYTES_TYPE="unknown" # 'du_b', 'du_bytes', 'stat', or 'unknown'
 _initialize_du_commands() {
    local test_file_path="/proc/self/exe" # Generally reliable link
    local temp_file_created=false
     if [ ! -r "$test_file_path" ] || [ ! -f "$test_file_path" ]; then
        test_file_path=$(mktemp)
        if [ -z "$test_file_path" ] || [ ! -f "$test_file_path" ]; then
             if typeset -f warn > /dev/null; then warn "mktemp failed for 'du' test."; else echo "Warning: mktemp failed for 'du' test." >&2; fi
             DU_BYTES_TYPE="unknown"
             return
        fi
        temp_file_created=true
        trap '_cleanup_temp_file "$test_file_path"; trap - EXIT' EXIT
    fi
     if du -b "$test_file_path" >/dev/null 2>&1; then
        DU_BYTES_CMD="du -b"
        DU_BYTES_TYPE="du_b"
    elif du --bytes "$test_file_path" >/dev/null 2>&1; then
        DU_BYTES_CMD="du --bytes"
        DU_BYTES_TYPE="du_bytes"
    elif stat -c %s "$test_file_path" >/dev/null 2>&1; then
        DU_BYTES_CMD="stat -c %s"
        DU_BYTES_TYPE="stat"
    else
        DU_BYTES_TYPE="unknown"
        if typeset -f warn > /dev/null; then warn "No reliable command for exact byte counts found."; else echo "Warning: No reliable command for exact byte counts found." >&2; fi
    fi
     if [ "$temp_file_created" = true ]; then
        _cleanup_temp_file "$test_file_path"
        trap - EXIT # Clear the specific trap if we cleaned up
    fi
}
 _cleanup_temp_file() {
    if [ -n "${1:-}" ] && [ -f "$1" ]; then
        rm -f "$1"
    fi
}
_initialize_du_commands # Initialize once
 # --- Utility functions ---
log() {
  echo -e "${GREEN}→${NC} $1"
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO: $1" >> "$LOG_FILE"
}
 error() {
  echo -e "${RED}❌ ERROR:${NC} $1" >&2
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $1" >> "$LOG_FILE"
  return 1
}
 success() {
  echo -e "${GREEN}✅${NC} $1"
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] SUCCESS: $1" >> "$LOG_FILE"
}
 warn() {
  echo -e "${YELLOW}⚠️ WARNING:${NC} $1"
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] WARNING: $1" >> "$LOG_FILE"
}
 info() {
  echo -e "${BLUE}ℹ️ INFO:${NC} $1"
}
 # Core pause logic.
_pause_script_execution() {
  local clear_behavior="${1:-clear}" # Default to "clear"
  echo
  echo -n "Press Enter to continue..."
  read -r
  if [ "$clear_behavior" = "clear" ]; then
    clear
  fi
}
 # Pauses script and keeps screen content.
wait_for_keypress_keep_screen() {
  _pause_script_execution "keep"
}
 # Pauses script and clears screen (default for progressing through menus/steps).
press_enter() {
  _pause_script_execution "clear"
}
 clear_screen() {
  clear
}
 # Function to save user configurations
save_config() {
    local key="$1"
    local value="$2"
     # Create config file if it doesn't exist
    touch "$CONFIG_FILE"
     # Check if key already exists, update if it does, add if it doesn't
    if grep -q "^$key=" "$CONFIG_FILE"; then
        sed -i "s/^$key=.*/$key=$value/" "$CONFIG_FILE"
    else
        echo "$key=$value" >> "$CONFIG_FILE"
    fi
}
 # Function to load user configurations
load_config() {
    local key="$1"
    local default="$2"
     if [ -f "$CONFIG_FILE" ] && grep -q "^$key=" "$CONFIG_FILE"; then
        grep "^$key=" "$CONFIG_FILE" | cut -d= -f2
    else
        echo "$default"
    fi
}
 get_file_size_bytes() {
    local filepath="${1:-}"
    if [ -z "$filepath" ] || [ ! -e "$filepath" ]; then
        echo "0"
        return
    fi
     local size_output=""
    case "$DU_BYTES_TYPE" in
        "du_b")      size_output=$(du -b "$filepath" 2>/dev/null | cut -f1) ;;
        "du_bytes")  size_output=$(du --bytes "$filepath" 2>/dev/null | cut -f1) ;;
        "stat")      size_output=$(stat -c %s "$filepath" 2>/dev/null) ;;
        *)
            local size_k
            size_k=$(du -k "$filepath" 2>/dev/null | cut -f1)
            if [[ "$size_k" =~ ^[0-9]+$ ]]; then
                size_output=$((size_k * 1024))
            else
                size_output="0"
            fi
            ;;
    esac
    echo "${size_output:-0}" # Ensure output, default to 0 if empty
}
 get_file_size_human() {
    local filepath="${1:-}"
    if [ -z "$filepath" ] || [ ! -e "$filepath" ]; then
        echo "N/A"
        return
    fi
    du -h "$filepath" 2>/dev/null | cut -f1 || echo "N/A"
}
 # Check if a model is corrupted (basic checks)
# Returns 0 for likely OK, 1 for likely corrupted.
check_model_integrity() {
    local model_path="$1"
    local model_name=$(basename "$model_path")
     # Simple size check - empty or very small files are likely corrupted
    local size_bytes=$(get_file_size_bytes "$model_path")
    if [ "$size_bytes" -lt 10240 ]; then # Less than 10KB
        warn "Model $model_name appears to be too small ($(get_file_size_human "$model_path")). It may be corrupted."
        return 1
    fi
     # Check file header for GGUF signature (basic check, requires hexdump)
    if command -v hexdump >/dev/null 2>&1; then
        # Read the first 8 bytes and check for the GGUF magic number (0x46554747)
        # This is "GGUF" in ASCII, followed by version number (typically 0x00000003 or 0x00000002)
        # We check for 47 47 55 46 (GGUF) followed by any 4 bytes.
        local header=$(hexdump -n 8 -e '1/1 "%02x"' "$model_path" 2>/dev/null)
        if [[ "$header" != "47475546"* ]]; then
             warn "Model $model_name does not start with a valid GGUF magic number. It may be corrupted or not a GGUF file."
             return 1
        fi
    else
        # If hexdump is not available, we can only do the size check
        if [ "$size_bytes" -lt 1000000 ]; then # Arbitrary larger size threshold if no hexdump
             warn "Could not verify GGUF header (hexdump not found). Model $model_name is relatively small ($(get_file_size_human "$model_path")) and might be incomplete or corrupted."
             return 1
        fi
    fi
     # If basic checks pass
    return 0
}
 # Validates critical directory paths against expected values and common pitfalls.
_validate_critical_directory_path() {
    local dir_to_check="${1:-}"
    local expected_path="${2:-}"
    local operation_name="${3:-operation}" # e.g., "backup", "restore"
     if [ -z "$dir_to_check" ]; then
        error "Safety check failed for $operation_name: Directory path is empty."
        return 1
    fi
    if [ "$dir_to_check" = "/" ] || [ "$dir_to_check" = "$HOME" ]; then
        error "Safety check failed for $operation_name: Directory path ('$dir_to_check') is a root or home directory."
        return 1
    fi
    # Disabled exact path check for more flexibility, rely on starts-with and critical path checks
    # if [ -n "$expected_path" ] && [ "$dir_to_check" != "$expected_path" ]; then
    #     error "Safety check failed for $operation_name: Directory path ('$dir_to_check') does not match expected path ('$expected_path')."
    #     return 1
    # fi
    # Add check to prevent operations outside HOME
    if [[ ! "$dir_to_check" == "$HOME"* ]]; then
         error "Safety check failed for $operation_name: Directory path ('$dir_to_check') is outside the home directory."
         return 1
    fi
     return 0
}
 # Function to check available storage space
check_available_space() {
    local required_space="$1" # in bytes
    local directory="$2"
     if [ ! -d "$directory" ]; then
        warn "Target directory '$directory' does not exist. Cannot check available space."
        return 0 # Assume enough space if dir doesn't exist yet (mkdir -p will create)
    fi
     # Get available space in the target directory
    local available_space=0
    if command -v df >/dev/null 2>&1; then
        available_space=$(df -B1 "$directory" | awk 'NR==2 {print $4}')
    else
        # If df is not available, assume there's enough space
        warn "df command not found. Cannot reliably check available storage space."
        return 0
    fi
     if [ -z "$available_space" ] || [ "$available_space" -lt "$required_space" ]; then
        local required_human=$(numfmt --to=iec-i --suffix=B "$required_space" 2>/dev/null || echo "$required_space bytes")
        local available_human=$(numfmt --to=iec-i --suffix=B "$available_space" 2>/dev/null || echo "$available_space bytes")
        warn "Not enough storage space in '$directory'."
        warn "Required: $required_human, Available: $available_human"
        return 1
    fi
     return 0
}
 # Find device CPU info
get_device_info() {
    local device_info=""
     # Get CPU model
    if [ -f "/proc/cpuinfo" ]; then
        local cpu_model=$(grep "model name\|Processor" /proc/cpuinfo | head -n1 | cut -d: -f2 | xargs)
        if [ -n "$cpu_model" ]; then
            device_info="CPU: $cpu_model"
        fi
    fi
     # Get total RAM
    if [ -f "/proc/meminfo" ]; then
        local total_ram=$(grep "MemTotal" /proc/meminfo | awk '{print $2}')
        if [ -n "$total_ram" ]; then
            local ram_gb=$(awk -v ram="$total_ram" 'BEGIN {printf "%.2f", ram/1024/1024}')
            device_info="$device_info, RAM: ${ram_gb}GB"
        fi
    fi
     # Get Android version if available
    if command -v getprop >/dev/null 2>&1; then
        local android_ver=$(getprop ro.build.version.release)
        if [ -n "$android_ver" ]; then
            device_info="$device_info, Android: $android_ver"
        fi
    fi
     echo "$device_info"
}
 # --- Prerequisite Checks ---
check_install() {
  local py_script_exists=false
  local binary_exists=false
  if [ -f "$KOBOLDCPP_DIR/koboldcpp.py" ]; then
    py_script_exists=true
  fi
  if [ -f "$KOBOLDCPP_DIR/koboldcpp-android-arm64" ] || [ -f "$KOBOLDCPP_DIR/koboldcpp" ]; then
    binary_exists=true
  fi
   if [ ! -d "$KOBOLDCPP_DIR" ] || { [ "$py_script_exists" = false ] && [ "$binary_exists" = false ]; }; then
    echo -e "${YELLOW}KoboldCpp is not installed or key components are missing.${NC}"
    echo
    echo -n "Would you like to install/reinstall it now? [Y/n]: "
    read -r INSTALL_CHOICE
    if [[ "${INSTALL_CHOICE:-Y}" =~ ^[Nn]$ ]]; then # Default to Y
      error "KoboldCpp installation is required to continue."
      wait_for_keypress_keep_screen
      return 1
    else
      install_all
      return $?
    fi
  fi
  return 0
}
 check_termux_storage() {
  if [ ! -d "$DOWNLOAD_DIR" ]; then
    log "Setting up Termux storage access..."
    if command -v termux-setup-storage &>/dev/null; then
      termux-setup-storage
    else
      error "termux-setup-storage command not found. Please ensure Termux:API is installed and configured."
      info "You might need to run: pkg install termux-api && termux-setup-storage"
      wait_for_keypress_keep_screen
      return 1
    fi
     for i in {1..10}; do
      if [ -d "$DOWNLOAD_DIR" ]; then
        break
      fi
      log "Waiting for storage access... ($i/10)"
      sleep 1
    done
     if [ ! -d "$DOWNLOAD_DIR" ]; then
      error "Storage access not granted or Downloads directory '$DOWNLOAD_DIR' not found."
      info "Please ensure you granted storage permission and the directory exists."
      wait_for_keypress_keep_screen
      return 1
    fi
  fi
  return 0
}
 # Check internet connectivity
check_internet() {
    log "Checking internet connectivity..."
    if ping -c 1 8.8.8.8 >/dev/null 2>&1 || ping -c 1 google.com >/dev/null 2>&1; then
        return 0
    else
        error "No internet connection detected. Some functions may not work properly."
        return 1
    fi
}
 # --- Core Functionality ---
 install_all() {
  clear_screen
  echo -e "${BOLD}${CYAN}=== Installing KoboldCpp ===${NC}"
  echo
  log "Starting KoboldCpp installation..."
   if command -v termux-wake-lock &>/dev/null; then
    log "Acquiring wake lock to prevent sleep..."
    termux-wake-lock
  else
    warn "termux-api not installed or termux-wake-lock not available. Consider 'pkg install termux-api'."
  fi
   check_termux_storage || { return 1; }
   # Check internet connection before proceeding
  check_internet
   log "Updating package lists..."
  pkg update -y || { error "Failed to update package lists"; wait_for_keypress_keep_screen; return 1; }
  log "Upgrading installed packages..."
  pkg upgrade -y || { warn "Failed to upgrade packages. Continuing, but this might lead to issues."; }
   log "Installing dependencies: openssl wget git python build-essential clang cmake libjpeg-turbo..."
  pkg install -y openssl wget git python build-essential clang cmake libjpeg-turbo || { error "Failed to install core packages"; wait_for_keypress_keep_screen; return 1; }
   if ! command -v termux-wake-lock &>/dev/null; then
    log "Attempting to install termux-api for better system integration..."
    pkg install -y termux-api || warn "Could not install termux-api, continuing anyway."
  fi
   if [ -d "$KOBOLDCPP_DIR/.git" ]; then
    log "KoboldCpp repository already exists, updating..."
    cd "$KOBOLDCPP_DIR" || { error "Could not cd to '$KOBOLDCPP_DIR'"; wait_for_keypress_keep_screen; return 1; }
    git fetch --all --prune || { error "Failed to fetch updates"; wait_for_keypress_keep_screen; return 1; }
    git reset --hard "origin/$DEFAULT_BRANCH" || { error "Failed to update to latest version of branch '$DEFAULT_BRANCH'"; wait_for_keypress_keep_screen; return 1; }
  else
    log "Cloning KoboldCpp repository (branch: $DEFAULT_BRANCH)..."
    git clone --depth 1 --branch "$DEFAULT_BRANCH" "$REPO_URL" "$KOBOLDCPP_DIR" || { error "Failed to clone repository"; wait_for_keypress_keep_screen; return 1; }
    cd "$KOBOLDCPP_DIR" || { error "Could not cd to '$KOBOLDCPP_DIR' after cloning"; wait_for_keypress_keep_screen; return 1; }
  fi
   mkdir -p "$MODEL_DIR" || { error "Failed to create models directory: '$MODEL_DIR'"; wait_for_keypress_keep_screen; return 1; }
   # Get system information for optimized build
  local device_info=$(get_device_info)
  log "Building for: $device_info"
   log "Building KoboldCpp with portable settings for Android..."
  export LLAMA_PORTABLE=1
  export KOBOLDCPP_PLATFORM="android"
   log "Cleaning previous build files (if any)..."
  make clean || warn "Failed to clean build files, continuing anyway."
   log "Building with optimizations for your device..."
  make -j$(nproc) || {
    warn "Build failed with default optimizations, trying with safer settings (LLAMA_NO_ACCELERATE=1)..."
    make clean
    make -j$(nproc) LLAMA_NO_ACCELERATE=1 || {
      warn "Build failed with first fallback, trying with fewer threads..."
      make clean
      make LLAMA_NO_ACCELERATE=1 || {
        error "Build failed with all fallback options. Please check the error messages above."
        wait_for_keypress_keep_screen
        return 1
      }
    }
  }
   local py_script_exists=false
  local binary_exists=false
  if [ -f "$KOBOLDCPP_DIR/koboldcpp.py" ]; then
    py_script_exists=true
  fi
  if [ -f "$KOBOLDCPP_DIR/koboldcpp-android-arm64" ] || [ -f "$KOBOLDCPP_DIR/koboldcpp" ]; then
    binary_exists=true
  fi
   if [ "$py_script_exists" = false ] && [ "$binary_exists" = false ]; then
      error "Build verification failed. Neither koboldcpp.py nor a compiled binary (e.g., koboldcpp-android-arm64 or koboldcpp) was found."
      info "Please check the build logs in '$LOG_FILE' for errors."
      wait_for_keypress_keep_screen
      return 1
  fi
   # Save build information to help with future builds
  if [ "$binary_exists" = true ]; then
    # local build_success="1" # This variable was declared but not used. Removed.
    save_config "last_successful_build" "$(date +%Y%m%d)"
    # Save the last used build option (0 for default, 1 for LLAMA_NO_ACCELERATE=1)
    # Check if LLAMA_NO_ACCELERATE is set to 1 in the environment or last command
    if [[ "${build_command:-}" == *"LLAMA_NO_ACCELERATE=1"* ]]; then
         save_config "build_options" "1"
    else
         save_config "build_options" "0"
    fi
  fi
   success "KoboldCpp installation complete!"
   if command -v termux-wake-unlock &>/dev/null; then
    log "Releasing wake lock..."
    termux-wake-unlock
  fi
   press_enter
  return 0
}
 add_model() {
  clear_screen
  echo -e "${BOLD}${CYAN}=== Add Model from Downloads ===${NC}"
  echo
  log "This copies a .gguf model file from your Termux Downloads directory ($DOWNLOAD_DIR) to the KoboldCpp models directory ($MODEL_DIR)."
  echo
   check_install || { return 1; }
  check_termux_storage || { return 1; }
   # Search for models recursively in Downloads
  echo -e "${YELLOW}Searching for .gguf files in Downloads directory...${NC}"
   local FILES=()
  local SEARCH_DEPTH=3  # Search up to 3 levels deep
   # Use a safer find command with -print0 and read -d $'\0'
  while IFS= read -r -d $'\0' file; do
    # Filter out files directly in DOWNLOAD_DIR if they don't end with .gguf - already done by -name "*.gguf"
    # Additional check to prevent adding files already in MODEL_DIR (unlikely if copying, but safe)
    # if [[ "$(dirname "$file")" == "$MODEL_DIR" ]]; then
    #     continue # Skip files already in the target directory
    # fi
    FILES+=("$file")
  done < <(find "$DOWNLOAD_DIR" -maxdepth "$SEARCH_DEPTH" -name "*.gguf" -type f -print0 2>/dev/null | sort -z)
    if [ ${#FILES[@]} -eq 0 ]; then
    error "No .gguf files found in your Downloads directory (searched $SEARCH_DEPTH levels deep)."
    echo "Please download a model (ending with .gguf) to that directory first."
    press_enter
    return 1
  fi
   echo -e "${YELLOW}Found models:${NC}"
  for i in "${!FILES[@]}"; do
    local FILE="${FILES[$i]}"
    local FILENAME
    FILENAME=$(basename "$FILE")
    local REL_PATH=${FILE#"$DOWNLOAD_DIR/"}
    local SIZE
    SIZE=$(get_file_size_human "$FILE")
    echo "[$i] $FILENAME ($SIZE)"
    echo "     Location: $REL_PATH"
  done
   echo
  echo -n "Enter number of the model to add (or 'q' to return to menu): "
  read -r CHOICE
   if [[ "${CHOICE:-}" == "q" || "${CHOICE:-}" == "Q" ]]; then # Handle empty CHOICE
    return 0
  fi
   if [[ ! "$CHOICE" =~ ^[0-9]+$ ]] || [ "$CHOICE" -lt 0 ] || [ "$CHOICE" -ge "${#FILES[@]}" ]; then
    error "Invalid selection."
    wait_for_keypress_keep_screen
    return 1
  fi
   local SRC="${FILES[$CHOICE]}"
  local FILENAME
  FILENAME=$(basename "$SRC")
  local DEST="$MODEL_DIR/$FILENAME"
   if [ -f "$DEST" ]; then
    echo -n "File '$FILENAME' already exists in models directory. Overwrite? [y/N]: "
    read -r CONFIRM
    if [[ ! "${CONFIRM:-N}" =~ ^[Yy]$ ]]; then # Default to N
      info "Operation cancelled."
      press_enter
      return 0
    fi
  fi
   # Check for available space before copying
  local file_size=$(get_file_size_bytes "$SRC")
  check_available_space "$file_size" "$MODEL_DIR" || {
    error "Not enough space to copy the model. Please free up some storage."
    wait_for_keypress_keep_screen
    return 1
  }
   log "Copying '$FILENAME' to models directory..."
  mkdir -p "$MODEL_DIR"
  if cp "$SRC" "$DEST"; then
    success "Model '$FILENAME' copied to '$MODEL_DIR'."
     # Verify the copied model integrity
    log "Verifying integrity of copied model..."
    if check_model_integrity "$DEST"; then
      success "Integrity check passed for $FILENAME."
    else
      warn "Integrity check failed for $FILENAME. The copied model may be corrupted."
      warn "Consider deleting it and adding it again, or re-downloading the original file."
      wait_for_keypress_keep_screen
    fi
  else
    error "Failed to copy model file '$FILENAME'. Check permissions and storage space."
    wait_for_keypress_keep_screen
    return 1
  fi
    press_enter
  return 0
}
 download_model() {
  clear_screen
  echo -e "${BOLD}${CYAN}=== Download Model ===${NC}"
  echo
  log "This downloads a .gguf model file directly to the KoboldCpp models directory ($MODEL_DIR)."
  echo
   check_install || { return 1; }
  check_internet || {
    error "Internet connection is required to download models."
    wait_for_keypress_keep_screen
    return 1
  }
   # Show recently used URLs if they exist
  local recent_urls=()
  local config_urls=$(grep "^recent_url_" "$CONFIG_FILE" 2>/dev/null | cut -d= -f2)
  if [ -n "$config_urls" ]; then
      echo -e "${YELLOW}Recently used URLs:${NC}"
      local url_index=0
      while IFS= read -r url; do
          recent_urls+=("$url")
          echo "[$url_index] $url"
          ((url_index++))
      done <<< "$config_urls"
      echo "[n] Enter a new URL"
      echo
      echo -n "Select an option: "
      read -r URL_CHOICE
       local URL=""
      if [[ "$URL_CHOICE" =~ ^[0-9]+$ ]] && [ "$URL_CHOICE" -ge 0 ] && [ "$URL_CHOICE" -lt "${#recent_urls[@]}" ]; then
          URL="${recent_urls[$URL_CHOICE]}"
          info "Selected recent URL: $URL"
      else
          echo "Enter a direct download URL for the model file (must be a .gguf file):"
          echo -n "> "
          read -r URL
          # Add new valid URL to recent list
          if [ -n "$URL" ] && [[ "$URL" == *.gguf* ]]; then
               save_config "recent_url_${#recent_urls[@]}" "$URL"
          fi
      fi
  else
    echo "Enter a direct download URL for the model file (must be a .gguf file):"
    echo -n "> "
    read -r URL
    # Add new valid URL to recent list
    if [ -n "$URL" ] && [[ "$URL" == *.gguf* ]]; then
         save_config "recent_url_0" "$URL"
    fi
  fi
    if [ -z "${URL:-}" ]; then
    info "Operation cancelled (empty URL)."
    press_enter
    return 0
  fi
   local FILENAME
  FILENAME=$(basename "${URL:-}") # Handle potentially empty URL
  FILENAME="${FILENAME%%\?*}" # Remove query string parameters
   if [ -z "$FILENAME" ] || [[ ! "$FILENAME" == *.gguf ]]; then
    warn "Couldn't determine a valid .gguf filename from the URL ('$FILENAME')."
    echo -n "Please enter a filename for this model (should end with .gguf): "
    read -r CUSTOM_FILENAME
     if [ -z "${CUSTOM_FILENAME:-}" ]; then
      info "Operation cancelled (empty filename)."
      press_enter
      return 0
    fi
    FILENAME="$CUSTOM_FILENAME"
  fi
   if [[ ! "$FILENAME" == *.gguf ]]; then
    FILENAME="${FILENAME}.gguf"
    info "Added .gguf extension. Filename will be: $FILENAME"
  fi
   local DEST="$MODEL_DIR/$FILENAME"
   if [ -f "$DEST" ]; then
    echo -n "File '$FILENAME' already exists in models directory. Overwrite? [y/N]: "
    read -r CONFIRM
    if [[ ! "${CONFIRM:-N}" =~ ^[Yy]$ ]]; then # Default N
      info "Operation cancelled."
      press_enter
      return 0
    fi
  fi
   # Check file size before downloading if possible
  local file_size=0
  if command -v curl >/dev/null 2>&1; then
    log "Checking file size before download..."
    # Use -s silent, -I head request, -L follow redirects, grep Content-Length, awk to get value, tr to remove carriage return
    local size_output=$(curl -sIL "$URL" 2>/dev/null | grep -i Content-Length | tail -n 1 | awk '{print $2}' | tr -d '\r')
    if [ -n "$size_output" ] && [[ "$size_output" =~ ^[0-9]+$ ]]; then
       file_size="$size_output"
    fi
     if [ "$file_size" -gt 0 ]; then
      local size_human=$(numfmt --to=iec-i --suffix=B "$file_size" 2>/dev/null || echo "$file_size bytes")
      info "Estimated model size: $size_human"
       # Check available space
      check_available_space "$file_size" "$MODEL_DIR" || {
        error "Not enough space to download the model. Please free up some storage."
        wait_for_keypress_keep_screen
        return 1
      }
    else
      warn "Could not reliably determine model size. Proceeding anyway, but ensure sufficient space is available."
      # As a fallback, check if there's at least 5GB free (a common minimum for larger models)
      check_available_space 5368709120 "$MODEL_DIR" || { # 5GB in bytes
          warn "Less than 5GB free space detected and could not get exact file size. Download might fail due to lack of space."
          echo -n "Continue download anyway? [y/N]: "
          read -r FORCE_DOWNLOAD
          if [[ ! "${FORCE_DOWNLOAD:-N}" =~ ^[Yy]$ ]]; then
              info "Download cancelled due to potential space issues."
              press_enter
              return 1
          fi
      }
    fi
  else
    warn "curl command not found. Cannot check file size before downloading. Proceeding anyway, but ensure sufficient space is available."
     # As a fallback, check if there's at least 5GB free (a common minimum for larger models)
     check_available_space 5368709120 "$MODEL_DIR" || { # 5GB in bytes
          warn "Less than 5GB free space detected and could not get exact file size. Download might fail due to lack of space."
          echo -n "Continue download anyway? [y/N]: "
          read -r FORCE_DOWNLOAD
          if [[ ! "${FORCE_DOWNLOAD:-N}" =~ ^[Yy]$ ]]; then
              info "Download cancelled due to potential space issues."
              press_enter
              return 1
          fi
      }
  fi
   log "Downloading model from: $URL"
  log "This may take a while depending on file size and your connection."
  log "Model will be saved to: $DEST"
   if command -v termux-wake-lock &>/dev/null; then
    termux-wake-lock
  fi
   mkdir -p "$MODEL_DIR"
   # Use curl with progress bar if available, otherwise fall back to wget
  if command -v curl >/dev/null 2>&1; then
    log "Downloading with curl..."
    if ! curl -L --progress-bar "$URL" -o "$DEST"; then
       error "Download failed. Please check the URL and your internet connection."
       # Clean up potentially incomplete file
       rm -f "$DEST" 2>/dev/null
       if command -v termux-wake-unlock &>/dev/null; then
         termux-wake-unlock
       fi
       wait_for_keypress_keep_screen
       return 1
    fi
  elif command -v wget >/dev/null 2>&1; then # Added explicit check for wget
    log "Downloading with wget..."
    if ! wget -c --show-progress "$URL" -O "$DEST"; then
       error "Download failed. Please check the URL and your internet connection."
       # Clean up potentially incomplete file (wget often cleans up .part files itself, but clean the target too)
       rm -f "$DEST" "$DEST.part" 2>/dev/null
       if command -v termux-wake-unlock &>/dev/null; then
         termux-wake-unlock
       fi
       wait_for_keypress_keep_screen
       return 1
    fi
  else
      error "Neither 'curl' nor 'wget' found. Cannot download the file."
      if command -v termux-wake-unlock &>/dev/null; then
        termux-wake-unlock
      fi
      wait_for_keypress_keep_screen
      return 1
  fi
    if command -v termux-wake-unlock &>/dev/null; then
    termux-wake-unlock
  fi
   if [ -f "$DEST" ] && [ -s "$DEST" ]; then
    local downloaded_size=$(get_file_size_human "$DEST")
    success "Model '$FILENAME' ($downloaded_size) downloaded successfully to '$DEST'"
     # Verify the downloaded model integrity
    log "Verifying integrity of downloaded model..."
    if check_model_integrity "$DEST"; then
      success "Integrity check passed for $FILENAME."
    else
      warn "Integrity check failed for $FILENAME. The downloaded model may be corrupted."
      warn "Consider deleting it and downloading it again."
      wait_for_keypress_keep_screen # Pause to show integrity warning
    fi
     # Show notification if available
    if command -v termux-notification &>/dev/null; then
      termux-notification --title "Download Complete" --content "Model $FILENAME has been downloaded successfully."
    fi
  else
    error "Download failed or resulted in an empty file: File not found or empty after download completion."
    rm -f "$DEST" 2>/dev/null
    wait_for_keypress_keep_screen
  fi
   press_enter
  return 0
}
 delete_model() {
  clear_screen
  echo -e "${BOLD}${CYAN}=== Delete Model ===${NC}"
  echo
  log "This will permanently delete a model file from your KoboldCpp models directory ($MODEL_DIR)."
  echo
   check_install || { return 1; }
   echo -e "${YELLOW}Available models in $MODEL_DIR:${NC}"
   local MODELS=()
  while IFS= read -r -d '' file; do # Use '' for null delimiter, portable
    MODELS+=("$file")
  done < <(find "$MODEL_DIR" -maxdepth 1 -name "*.gguf" -type f -print0 2>/dev/null | sort -z)
   if [ ${#MODELS[@]} -eq 0 ]; then
    error "No models found in '$MODEL_DIR'."
    wait_for_keypress_keep_screen
    return 1
  fi
   for i in "${!MODELS[@]}"; do
    local MODEL="${MODELS[$i]}"
    local FILENAME
    FILENAME=$(basename "$MODEL")
    local SIZE
    SIZE=$(get_file_size_human "$MODEL")
    echo "[$i] $FILENAME ($SIZE)"
  done
   echo
  echo -n "Enter number of the model to delete (or 'q' to return to menu): "
  read -r CHOICE
   if [[ "${CHOICE:-}" == "q" || "${CHOICE:-}" == "Q" ]]; then
    return 0
  fi
   if [[ ! "$CHOICE" =~ ^[0-9]+$ ]] || [ "$CHOICE" -lt 0 ] || [ "$CHOICE" -ge "${#MODELS[@]}" ]; then
    error "Invalid selection."
    wait_for_keypress_keep_screen
    return 1
  fi
   local TARGET="${MODELS[$CHOICE]}"
  local FILENAME
  FILENAME=$(basename "$TARGET")
  local SIZE
  SIZE=$(get_file_size_human "$TARGET")
   echo -e "${RED}WARNING: This action is irreversible!${NC}"
  echo -n "Are you sure you want to delete '$FILENAME' ($SIZE)? [y/N]: "
  read -r CONFIRM
   if [[ "${CONFIRM:-N}" =~ ^[Yy]$ ]]; then # Default N
    log "Deleting model file: $TARGET"
    if rm "$TARGET"; then
        success "Deleted model '$FILENAME' ($SIZE freed)."
    else
        error "Failed to delete '$FILENAME'. Check permissions."
        wait_for_keypress_keep_screen
        return 1
    fi
  else
    info "Deletion cancelled."
  fi
   press_enter
  return 0
}
 list_models() {
  clear_screen
  echo -e "${BOLD}${CYAN}=== Available Models ===${NC}"
  echo
  log "Listing .gguf models in the cache directory: $MODEL_DIR"
  echo
   check_install || { return 1; }
  mkdir -p "$MODEL_DIR"
   local MODELS=()
  while IFS= read -r -d '' file; do # Use '' for null delimiter, portable
    MODELS+=("$file")
  done < <(find "$MODEL_DIR" -maxdepth 1 -name "*.gguf" -type f -print0 2>/dev/null | sort -z)
   if [ ${#MODELS[@]} -eq 0 ]; then
    echo "No models found in '$MODEL_DIR'."
    echo
    echo "You can add models using the main menu options:"
    echo "2. Add model from Downloads"
    echo "3. Download model from URL"
    press_enter
    return 0
  fi
   echo -e "${YELLOW}Size\tStatus\tModel${NC}"
   local TOTAL_SIZE_BYTES=0
  local CORRUPT_COUNT=0
   for MODEL_PATH in "${MODELS[@]}"; do
    local SIZE_BYTES
    SIZE_BYTES=$(get_file_size_bytes "$MODEL_PATH")
    TOTAL_SIZE_BYTES=$((TOTAL_SIZE_BYTES + SIZE_BYTES))
    local HUMAN_SIZE
    HUMAN_SIZE=$(get_file_size_human "$MODEL_PATH")
    local FILENAME
    FILENAME=$(basename "$MODEL_PATH")
     # Check if model might be corrupted
    local status="✓ OK"
    local status_color="$GREEN"
    if ! check_model_integrity "$MODEL_PATH" >/dev/null 2>&1; then # Check silently for listing
      status="⚠️ Issues"
      status_color="$YELLOW"
      ((CORRUPT_COUNT++))
    fi
     # Use printf for consistent formatting
    printf "%s\t${status_color}%s${NC}\t%s\n" "$HUMAN_SIZE" "$status" "$FILENAME"
  done
   local HUMAN_TOTAL_SIZE="0B"
  if [ "$TOTAL_SIZE_BYTES" -gt 0 ]; then
    if command -v numfmt &>/dev/null; then
      HUMAN_TOTAL_SIZE=$(numfmt --to=iec-i --suffix=B "$TOTAL_SIZE_BYTES")
    else
       if [ "$TOTAL_SIZE_BYTES" -ge 1073741824 ]; then
          HUMAN_TOTAL_SIZE=$(awk -v ts="$TOTAL_SIZE_BYTES" 'BEGIN { printf "%.2fGiB", ts/1073741824 }')
       elif [ "$TOTAL_SIZE_BYTES" -ge 1048576 ]; then
          HUMAN_TOTAL_SIZE=$(awk -v ts="$TOTAL_SIZE_BYTES" 'BEGIN { printf "%.2fMiB", ts/1048576 }')
       elif [ "$TOTAL_SIZE_BYTES" -ge 1024 ]; then
          HUMAN_TOTAL_SIZE=$(awk -v ts="$TOTAL_SIZE_BYTES" 'BEGIN { printf "%.2fKiB", ts/1024 }')
       else
          HUMAN_TOTAL_SIZE="${TOTAL_SIZE_BYTES}B"
       fi
    fi
  fi
   echo -e "${YELLOW}------------------------------------${NC}"
  echo -e "${GREEN}Total Models:\t${#MODELS[@]}${NC}"
  echo -e "${GREEN}Total Size:\t$HUMAN_TOTAL_SIZE${NC}"
  if [ "$CORRUPT_COUNT" -gt 0 ]; then
      echo -e "${YELLOW}Models with Potential Issues:\t$CORRUPT_COUNT${NC}"
  fi
  echo
  echo "Legend: ✓ OK = Basic checks passed, ⚠️ Issues = Potential corruption or not a GGUF file"
   press_enter
  return 0
}
 # New function to interactively verify a specific model's integrity
verify_model_integrity_interactive() {
    clear_screen
    echo -e "${BOLD}${CYAN}=== Verify Model Integrity ===${NC}"
    echo
    log "This will perform basic checks on a selected model file to detect potential corruption."
    echo
     check_install || { return 1; }
    mkdir -p "$MODEL_DIR" # Ensure directory exists before checking
     local MODELS=()
    while IFS= read -r -d '' file; do
        MODELS+=("$file")
    done < <(find "$MODEL_DIR" -maxdepth 1 -name "*.gguf" -type f -print0 2>/dev/null | sort -z)
     if [ ${#MODELS[@]} -eq 0 ]; then
        error "No models found in '$MODEL_DIR' to verify."
        wait_for_keypress_keep_screen
        return 1
    fi
     echo -e "${YELLOW}Available models in $MODEL_DIR:${NC}"
    for i in "${!MODELS[@]}"; do
        local MODEL="${MODELS[$i]}"
        local FILENAME=$(basename "$MODEL")
        local SIZE=$(get_file_size_human "$MODEL")
        echo "[$i] $FILENAME ($SIZE)"
    done
     echo
    echo -n "Enter number of the model to verify (or 'q' to return to menu): "
    read -r CHOICE
     if [[ "${CHOICE:-}" == "q" || "${CHOICE:-}" == "Q" ]]; then
        return 0
    fi
     if [[ ! "$CHOICE" =~ ^[0-9]+$ ]] || [ "$CHOICE" -lt 0 ] || [ "$CHOICE" -ge "${#MODELS[@]}" ]; then
        error "Invalid selection."
        wait_for_keypress_keep_screen
        return 1
    fi
     local TARGET_MODEL_PATH="${MODELS[$CHOICE]}"
    local TARGET_MODEL_FILENAME=$(basename "$TARGET_MODEL_PATH")
     log "Verifying integrity of '$TARGET_MODEL_FILENAME'..."
    if check_model_integrity "$TARGET_MODEL_PATH"; then
        success "Model '$TARGET_MODEL_FILENAME' appears to be intact based on basic checks."
    else
        error "Model '$TARGET_MODEL_FILENAME' failed integrity checks. It may be corrupted or incomplete."
        info "This model might not load or run correctly."
    fi
     press_enter
    return 0
}
  run_model() {
  clear_screen
  echo -e "${BOLD}${CYAN}=== Run KoboldCpp ===${NC}"
  echo
  log "Starting the KoboldCpp server with a selected model from the cache directory ($MODEL_DIR)."
  echo
   check_install || { return 1; }
  mkdir -p "$MODEL_DIR"
   echo -e "${YELLOW}Available models:${NC}"
   local MODELS=()
  while IFS= read -r -d '' file; do # Use '' for null delimiter, portable
    MODELS+=("$file")
  done < <(find "$MODEL_DIR" -maxdepth 1 -name "*.gguf" -type f -print0 2>/dev/null | sort -z)
   if [ ${#MODELS[@]} -eq 0 ]; then
    error "No models found in '$MODEL_DIR'."
    echo
    echo "You need to add models first. Return to the main menu and select:"
    echo "2. Add model from Downloads"
    echo "3. Download model from URL"
    wait_for_keypress_keep_screen
    return 1
  fi
   # Sort models by last used time if available
  local last_used_model=""
  if [ -f "$CONFIG_FILE" ]; then
    last_used_model=$(grep "^last_used_model=" "$CONFIG_FILE" | cut -d= -f2)
     # Ensure last_used_model still exists
    if [ -n "$last_used_model" ] && [ ! -f "$last_used_model" ]; then
        warn "Last used model '$last_used_model' not found. Resetting last used model setting."
        last_used_model=""
        # Consider removing the invalid config entry
        # sed -i '/^last_used_model=/d' "$CONFIG_FILE"
    fi
     if [ -n "$last_used_model" ]; then
      for i in "${!MODELS[@]}"; do
        if [ "${MODELS[$i]}" = "$last_used_model" ]; then
          # Move last used model to the top
          local temp=${MODELS[0]}
          MODELS[0]=${MODELS[$i]}
          MODELS[$i]=$temp
          break
        fi
      done
    fi
  fi
   for i in "${!MODELS[@]}"; do
    local MODEL="${MODELS[$i]}"
    local FILENAME
    FILENAME=$(basename "$MODEL")
    local SIZE
    SIZE=$(get_file_size_human "$MODEL")
     # Mark the last used model
    local marker=""
    if [ "$MODEL" = "$last_used_model" ]; then
      marker=" (last used)"
    fi
     echo "[$i] $FILENAME ($SIZE)$marker"
  done
   echo
  echo -n "Enter number of the model to run (or 'q' to return to menu): "
  read -r CHOICE
   if [[ "${CHOICE:-}" == "q" || "${CHOICE:-}" == "Q" ]]; then
    return 0
  fi
   if [[ ! "$CHOICE" =~ ^[0-9]+$ ]] || [ "$CHOICE" -lt 0 ] || [ "$CHOICE" -ge "${#MODELS[@]}" ]; then
    error "Invalid selection."
    wait_for_keypress_keep_screen
    return 1
  fi
   local TARGET_MODEL_PATH="${MODELS[$CHOICE]}"
  local TARGET_MODEL_FILENAME=$(basename "$TARGET_MODEL_PATH")
  local TARGET_MODEL_SIZE=$(get_file_size_human "$TARGET_MODEL_PATH")
   # Save last used model
  save_config "last_used_model" "$TARGET_MODEL_PATH"
   # Check model integrity before running
  if ! check_model_integrity "$TARGET_MODEL_PATH"; then
    echo -e "${RED}WARNING: This model may have integrity issues.${NC}"
    echo -n "Do you still want to continue? [y/N]: "
    read -r INTEGRITY_CONFIRM
    if [[ ! "${INTEGRITY_CONFIRM:-N}" =~ ^[Yy]$ ]]; then
      info "Operation cancelled."
      press_enter
      return 0
    fi
  fi
   clear_screen
  echo -e "${BOLD}${CYAN}=== Run Configuration: $TARGET_MODEL_FILENAME ===${NC}"
  echo
  echo "Model: $TARGET_MODEL_FILENAME ($TARGET_MODEL_SIZE)"
  echo
  info "You can customize parameters like threads, context size, and port."
  info "For optimal performance, consider GPU acceleration if your device/build supports CLBlast (--useclblast 0 0)."
  echo
   # Load saved configuration if exists
  local saved_config=false
  local saved_threads=""
  local saved_context=""
  local saved_port=""
  local saved_extra=""
   local model_config_key="model_${TARGET_MODEL_FILENAME//[^a-zA-Z0-9_]/_}" # Sanitize filename for config key
   if [ -f "$CONFIG_FILE" ]; then
    saved_threads=$(grep "^${model_config_key}_threads=" "$CONFIG_FILE" | cut -d= -f2)
    saved_context=$(grep "^${model_config_key}_context=" "$CONFIG_FILE" | cut -d= -f2)
    saved_port=$(grep "^${model_config_key}_port=" "$CONFIG_FILE" | cut -d= -f2)
    saved_extra=$(grep "^${model_config_key}_extra=" "$CONFIG_FILE" | cut -d= -f2)
     if [ -n "$saved_threads" ] && [ -n "$saved_context" ] && [ -n "$saved_port" ]; then
      saved_config=true
      echo "Found saved configuration for this model:"
      echo "- Threads: ${YELLOW}$saved_threads${NC}"
      echo "- Context: ${YELLOW}$saved_context${NC}"
      echo "- Port: ${YELLOW}$saved_port${NC}"
      if [ -n "$saved_extra" ]; then
        echo "- Extra options: ${YELLOW}$saved_extra${NC}"
      fi
      echo
    fi
  fi
   echo "Select run configuration:"
  echo "[1] Quick start (recommended settings for this device)"
  if [ "$saved_config" = true ]; then
    echo "[2] Use saved configuration for this model"
    echo "[3] Custom settings"
  else
    echo "[2] Custom settings"
  fi
  echo "[q] Return to main menu"
  echo
  echo -n "Your choice: "
  read -r CONFIG_CHOICE
   local ARGS_STRING=""
  local PORT_NUMBER="5001"
  local NPROC_COUNT=4
  if command -v nproc &>/dev/null; then
    NPROC_COUNT=$(nproc)
    if [ "$NPROC_COUNT" -eq 0 ]; then NPROC_COUNT=1; fi # Avoid 0 threads
  fi
  local SUGGESTED_THREADS="$NPROC_COUNT"
  if [ "$NPROC_COUNT" -gt 8 ]; then # Suggest fewer threads on high-core count devices for balance
     SUGGESTED_THREADS=$((NPROC_COUNT / 2))
     if [ "$SUGGESTED_THREADS" -lt 4 ]; then SUGGESTED_THREADS=4; fi
  fi
    local current_choice="${CONFIG_CHOICE:-1}" # Default to 1 (Quick Start)
  if [ "$saved_config" = false ] && [ "$current_choice" = "3" ]; then
      # If saved_config is false, option 3 means custom, but the menu showed [2] Custom. Adjust choice.
      current_choice="2"
  fi
    case "$current_choice" in
    1)
      ARGS_STRING="--threads ${SUGGESTED_THREADS} --contextsize 2048 --port $PORT_NUMBER"
      info "Using Quick Start with $SUGGESTED_THREADS threads, context 2048, port $PORT_NUMBER."
      ;;
    2)
      if [ "$saved_config" = true ]; then
         # Use saved configuration (only if option 2 was explicitly chosen AND saved config exists)
         ARGS_STRING="--threads $saved_threads --contextsize $saved_context --port $saved_port"
         if [ -n "$saved_extra" ]; then
           ARGS_STRING="$ARGS_STRING $saved_extra"
         fi
         info "Using saved configuration with $saved_threads threads, context $saved_context, port $saved_port."
      else
         # Saved config doesn't exist, this was the "Custom settings" option
         # Fall through to custom settings block
         CONFIG_CHOICE="custom" # Internal flag
      fi
      ;;
    3) # This case is only reached if saved_config is true AND user chose 3
       CONFIG_CHOICE="custom" # Internal flag
       ;;
    q|Q)
      return 0
      ;;
    *)
      info "Invalid configuration choice, using Quick Start defaults."
      ARGS_STRING="--threads ${SUGGESTED_THREADS} --contextsize 2048 --port $PORT_NUMBER"
      ;;
  esac
   # Custom settings block (executed if CONFIG_CHOICE was "custom")
  if [ "${CONFIG_CHOICE:-}" = "custom" ]; then
    clear_screen
    echo -e "${BOLD}${CYAN}=== Custom Settings for $TARGET_MODEL_FILENAME ===${NC}"
    echo
     echo -n "Number of CPU threads (suggested: $SUGGESTED_THREADS, max detected: $NPROC_COUNT): "
    read -r THREADS_INPUT
    local THREADS=${THREADS_INPUT:-$SUGGESTED_THREADS}
    # Add validation for threads input
    if ! [[ "$THREADS" =~ ^[0-9]+$ ]] || [ "$THREADS" -le 0 ]; then
         warn "Invalid thread count '$THREADS_INPUT', using suggested count: $SUGGESTED_THREADS."
         THREADS="$SUGGESTED_THREADS"
    fi
    ARGS_STRING="--threads $THREADS"
     # Get model size to suggest appropriate context size
    local MODEL_SIZE_BYTES=$(get_file_size_bytes "$TARGET_MODEL_PATH")
    local SUGGESTED_CONTEXT=2048
    if [ "$MODEL_SIZE_BYTES" -gt 10737418240 ]; then # >10GB models (10 * 1024^3)
      SUGGESTED_CONTEXT=4096
    elif [ "$MODEL_SIZE_BYTES" -lt 5368709120 ]; then # <5GB models (5 * 1024^3)
      SUGGESTED_CONTEXT=8192
    fi
     echo -n "Context size (e.g., 2048, 4096, 8192; suggested: $SUGGESTED_CONTEXT): "
    read -r CONTEXT_INPUT
    local CONTEXT=${CONTEXT_INPUT:-$SUGGESTED_CONTEXT}
    # Add validation for context size input
    if ! [[ "$CONTEXT" =~ ^[0-9]+$ ]] || [ "$CONTEXT" -le 0 ]; then
        warn "Invalid context size '$CONTEXT_INPUT', using suggested size: $SUGGESTED_CONTEXT."
        CONTEXT="$SUGGESTED_CONTEXT"
    fi
     ARGS_STRING="$ARGS_STRING --contextsize $CONTEXT"
     echo -n "Server port (default: 5001): "
    read -r PORT_INPUT
    PORT_NUMBER=${PORT_INPUT:-5001}
    # Add validation for port number
    if ! [[ "$PORT_NUMBER" =~ ^[0-9]+$ ]] || [ "$PORT_NUMBER" -lt 1024 ] || [ "$PORT_NUMBER" -gt 65535 ]; then
        warn "Invalid port number '$PORT_INPUT', using default port: 5001."
        PORT_NUMBER="5001"
    fi
     ARGS_STRING="$ARGS_STRING --port $PORT_NUMBER"
     echo
    echo "Additional options:"
    echo "[1] No additional options - continue with settings above"
    echo "[2] Enable GPU acceleration (CLBlast - if supported by your device/build)"
    echo "[3] Use low VRAM mode (slower, uses less memory, implies --nostream)"
    echo "[4] Enable Mmapped IO (faster loading but more memory usage)"
    echo "[5] Set custom LORA files (if you have them)"
    echo "[6] Custom extra arguments (enter raw arguments)"
    echo
    echo -n "Your choice for additional options [1]: "
    read -r EXTRA_OPTIONS_CHOICE
     local EXTRA_ARGS_VAL=""
    case "${EXTRA_OPTIONS_CHOICE:-1}" in # Default to 1
      2)
        EXTRA_ARGS_VAL="--useclblast 0 0"
        info "GPU acceleration (CLBlast) enabled. Ensure your KoboldCpp is built with CLBlast support."
        ;;
      3)
        EXTRA_ARGS_VAL="--lowvram --nostream"
        info "Low VRAM mode enabled."
        ;;
      4)
        EXTRA_ARGS_VAL="--mmap"
        info "Memory-mapped IO enabled."
        ;;
      5)
        echo
        echo "Enter full path to LORA file:"
        read -r LORA_PATH
        if [ -n "$LORA_PATH" ] && [ -f "$LORA_PATH" ]; then
          echo "Enter LORA scale factor (default: 1.0):"
          read -r LORA_SCALE
          LORA_SCALE=${LORA_SCALE:-1.0}
          EXTRA_ARGS_VAL="--lora \"$LORA_PATH:$LORA_SCALE\"" # Quote path in case of spaces
          info "LORA '$LORA_PATH' will be loaded with scale $LORA_SCALE."
        else
          warn "LORA file not found or path empty. No LORA will be loaded."
        fi
        ;;
      6)
         echo
         echo "Enter custom additional arguments (e.g., --gpulayers 10 --unbantokens):"
         read -r RAW_EXTRA_ARGS
         EXTRA_ARGS_VAL="$RAW_EXTRA_ARGS"
         info "Using custom extra arguments: $RAW_EXTRA_ARGS"
         ;;
      *) # Includes 1 and any other input
        info "No additional special options selected."
        ;;
    esac
     if [ -n "$EXTRA_ARGS_VAL" ]; then
      ARGS_STRING="$ARGS_STRING $EXTRA_ARGS_VAL"
    fi
     # Save this configuration for future use
    echo -n "Save this configuration for future use with this model? [Y/n]: "
    read -r SAVE_CONFIG_CHOICE
    if [[ ! "${SAVE_CONFIG_CHOICE:-Y}" =~ ^[Nn]$ ]]; then # Default Y
      save_config "${model_config_key}_threads" "$THREADS"
      save_config "${model_config_key}_context" "$CONTEXT"
      save_config "${model_config_key}_port" "$PORT_NUMBER"
      save_config "${model_config_key}_extra" "$EXTRA_ARGS_VAL"
      success "Configuration saved for future use."
    fi
  fi # End Custom settings block
    clear_screen
  log "Starting KoboldCpp with model: $TARGET_MODEL_FILENAME ($TARGET_MODEL_SIZE)"
  if [ -n "$ARGS_STRING" ]; then
    log "Run arguments: $ARGS_STRING"
  fi
   # Show available network interfaces
  local DEVICE_IP_MSG=""
  if command -v ip &>/dev/null; then
    local interface_ip
    interface_ip=$(ip -o -4 addr show scope global | awk '!/ lo / && $4 !~ /^169\.254\./ {print $4}' | cut -d/ -f1 | head -n1)
    if [ -n "$interface_ip" ]; then
      DEVICE_IP_MSG=" (or http://$interface_ip:$PORT_NUMBER from another device on the same network)"
    fi
  fi
  log "Server will be available at: http://localhost:$PORT_NUMBER${DEVICE_IP_MSG}"
  log "Press Ctrl+C in this terminal to stop the server."
  echo
   cd "$KOBOLDCPP_DIR" || { error "Could not cd to '$KOBOLDCPP_DIR'"; wait_for_keypress_keep_screen; return 1; }
   local KOBOLDCPP_BASE_CMD=""
  if [ -f "./koboldcpp-android-arm64" ] && [ -x "./koboldcpp-android-arm64" ]; then
    KOBOLDCPP_BASE_CMD="./koboldcpp-android-arm64"
  elif [ -f "./koboldcpp" ] && [ -x "./koboldcpp" ]; then
    KOBOLDCPP_BASE_CMD="./koboldcpp"
  elif [ -f "./koboldcpp.py" ]; then
    KOBOLDCPP_BASE_CMD="python koboldcpp.py"
  else
    error "Could not find a runnable KoboldCpp executable (e.g., koboldcpp-android-arm64, koboldcpp) or koboldcpp.py script in $KOBOLDCPP_DIR."
    info "Please ensure KoboldCpp is installed and built correctly."
    wait_for_keypress_keep_screen
    return 1
  fi
   local cmd_array=()
  if [[ "$KOBOLDCPP_BASE_CMD" == "python koboldcpp.py" ]]; then
      cmd_array+=("python" "koboldcpp.py")
  else
      cmd_array+=("$KOBOLDCPP_BASE_CMD")
  fi
  cmd_array+=("--model" "$TARGET_MODEL_PATH")
   local args_temp_array=()
  # Safely split ARGS_STRING into arguments, handling quotes
  # This is tricky in pure bash without eval/set -f, but read -r -a is better than simple split
  # Using a temporary array and disabling/enabling globbing around read -a
  set -f # Disable globbing
  read -r -a args_temp_array <<< "$ARGS_STRING"
  set +f # Re-enable globbing
   if [ ${#args_temp_array[@]} -gt 0 ]; then
    cmd_array+=("${args_temp_array[@]}")
  fi
   info "Executing: ${cmd_array[*]}"
   if command -v termux-wake-lock &>/dev/null; then
    termux-wake-lock
  fi
   # Start time measurement
  local start_time=$(date +%s)
   # Execute the command array
  if ! "${cmd_array[@]}"; then
      local exit_code=$?
      local end_time=$(date +%s)
      local runtime=$((end_time - start_time))
      local hours=$((runtime / 3600))
      local minutes=$(( (runtime % 3600) / 60 ))
      local seconds=$((runtime % 60))
       warn "KoboldCpp server exited with code $exit_code. Runtime: ${hours}h ${minutes}m ${seconds}s"
      echo "Check the console output above for error messages."
      # Save partial runtime statistics on error
      save_config "last_runtime_hours" "$hours"
      save_config "last_runtime_minutes" "$minutes"
      save_config "last_runtime_seconds" "$seconds"
      save_config "last_runtime_model" "$TARGET_MODEL_FILENAME (exit $exit_code)"
       if command -v termux-wake-unlock &>/dev/null; then
        termux-wake-unlock
      fi
      wait_for_keypress_keep_screen
      return 1
  fi
   # End time measurement (only if command finished without error)
  local end_time=$(date +%s)
  local runtime=$((end_time - start_time))
  local hours=$((runtime / 3600))
  local minutes=$(( (runtime % 3600) / 60 ))
  local seconds=$((runtime % 60))
   # Save runtime statistics
  save_config "last_runtime_hours" "$hours"
  save_config "last_runtime_minutes" "$minutes"
  save_config "last_runtime_seconds" "$seconds"
  save_config "last_runtime_model" "$TARGET_MODEL_FILENAME"
   if command -v termux-wake-unlock &>/dev/null; then
    log "Releasing wake lock..."
    termux-wake-unlock
  fi
   echo
  info "KoboldCpp server stopped normally. Runtime: ${hours}h ${minutes}m ${seconds}s"
   press_enter
  return 0
}
 rebuild() {
  clear_screen
  echo -e "${BOLD}${CYAN}=== Rebuild KoboldCpp ===${NC}"
  echo
  log "Rebuilding KoboldCpp from source in $KOBOLDCPP_DIR..."
   # Check if KoboldCpp directory exists and is a git repo
  if [ ! -d "$KOBOLDCPP_DIR/.git" ]; then
     error "KoboldCpp directory '$KOBOLDCPP_DIR' is not a git repository. Cannot rebuild."
     info "Please use the 'Install / Update / Rebuild' option from the main menu to install first."
     wait_for_keypress_keep_screen
     return 1
  fi
   cd "$KOBOLDCPP_DIR" || { error "Could not cd to '$KOBOLDCPP_DIR'"; wait_for_keypress_keep_screen; return 1; }
   if command -v termux-wake-lock &>/dev/null; then
    termux-wake-lock
  fi
   log "Cleaning build files..."
  make clean || warn "Failed to clean build files, attempting rebuild anyway."
   # Get system information for optimized build
  local device_info=$(get_device_info)
  log "Building for: $device_info"
   # Try to use previously successful build options
  local build_options=$(load_config "build_options" "0") # Default to 0 (default build)
   export LLAMA_PORTABLE=1
  export KOBOLDCPP_PLATFORM="android"
   local build_command="make -j$(nproc)"
  if [ "$build_options" = "1" ]; then
    log "Using previously successful build options (LLAMA_NO_ACCELERATE=1)..."
    build_command="make -j$(nproc) LLAMA_NO_ACCELERATE=1"
  fi
   log "Starting build with command: $build_command"
  # Use eval to execute the build_command string
  if eval "$build_command"; then
      # If the build was successful, save the options used
      if [[ "$build_command" == *"LLAMA_NO_ACCELERATE=1"* ]]; then
          save_config "build_options" "1"
      else
          save_config "build_options" "0"
      fi
      success "KoboldCpp rebuilt successfully."
  else
      local build_exit_code=$?
      error "Rebuild failed with command: $build_command (Exit code $build_exit_code)."
      # If default build failed, try safer settings regardless of previous config
      if [ "$build_options" != "1" ]; then
          warn "Trying rebuild with safer settings (LLAMA_NO_ACCELERATE=1)..."
          make clean # Clean before retrying
          if make LLAMA_NO_ACCELERATE=1; then
              success "KoboldCpp rebuilt successfully with LLAMA_NO_ACCELERATE=1."
              save_config "build_options" "1" # Save the successful option
          else
              error "Rebuild failed even with safer settings. Please check error messages."
              wait_for_keypress_keep_screen
              return 1
          fi
      else
          # Safer settings were already tried or previously saved, and it failed again
          error "Rebuild failed with saved safer settings. Please check error messages."
          wait_for_keypress_keep_screen
          return 1
      fi
  fi
    if command -v termux-wake-unlock &>/dev/null; then
    termux-wake-unlock
  fi
   press_enter
  return 0
}
 update_koboldcpp() {
  clear_screen
  echo -e "${BOLD}${CYAN}=== Update KoboldCpp ===${NC}"
  echo
  log "Updating the KoboldCpp source code from the repository."
   # Check if KoboldCpp directory exists and is a git repo
  if [ ! -d "$KOBOLDCPP_DIR/.git" ]; then
     error "KoboldCpp directory '$KOBOLDCPP_DIR' is not a git repository. Cannot update."
     info "Please use the 'Install / Update / Rebuild' option from the main menu to install first."
     wait_for_keypress_keep_screen
     return 1
  fi
   check_install || { return 1; }
  check_internet || {
    warn "Internet connection is required to update KoboldCpp."
    echo -n "Do you want to continue anyway? [y/N]: "
    read -r CONTINUE
    if [[ ! "${CONTINUE:-N}" =~ ^[Yy]$ ]]; then
      press_enter
      return 0
    fi
  }
   log "Checking for KoboldCpp updates..."
  cd "$KOBOLDCPP_DIR" || { error "Could not cd to '$KOBOLDCPP_DIR'"; wait_for_keypress_keep_screen; return 1; }
   # Check for updates before proceeding
  # Use git fetch --dry-run or similar for a cleaner check without fetching
  # git fetch origin "$DEFAULT_BRANCH" --quiet --dry-run # --dry-run isn't always supported or clear
  # Alternative: fetch and compare
  if ! git fetch origin "$DEFAULT_BRANCH" --quiet; then
      warn "Failed to fetch updates from origin. Cannot determine if updates are available."
      echo -n "Attempt update anyway? [y/N]: "
      read -r ATTEMPT_UPDATE
      if [[ ! "${ATTEMPT_UPDATE:-N}" =~ ^[Yy]$ ]]; then
          press_enter
          return 0
      fi
      # If user wants to attempt, proceed with reset below which might fail or succeed with old data
      local updates_available=false # Assume no updates to skip the log output below
  else
      local LOCAL=$(git rev-parse HEAD)
      local REMOTE=$(git rev-parse origin/"$DEFAULT_BRANCH")
       if [ "$LOCAL" = "$REMOTE" ]; then
        info "KoboldCpp is already up to date (on branch '$DEFAULT_BRANCH')."
        echo -n "Force update/rebuild anyway? [y/N]: "
        read -r FORCE_UPDATE
        if [[ ! "${FORCE_UPDATE:-N}" =~ ^[Yy]$ ]]; then
          press_enter
          return 0
        fi
        local updates_available=false # No actual updates
      else
        echo -e "${YELLOW}Updates available:${NC}"
        git log --oneline --graph --decorate HEAD..origin/"$DEFAULT_BRANCH" | head -n 10
        if [ "$(git log --oneline HEAD..origin/"$DEFAULT_BRANCH" 2>/dev/null | wc -l)" -gt 10 ]; then
          echo "... and more changes"
        fi
        echo
        local updates_available=true
      fi
  fi
    echo -n "This will fetch the latest version of KoboldCpp from '$DEFAULT_BRANCH' branch and rebuild. Any local changes not committed might be lost (they will be stashed). Continue? [Y/n]: "
  read -r CONFIRM
  if [[ "${CONFIRM:-Y}" =~ ^[Nn]$ ]]; then # Default Y
    info "Update cancelled."
    press_enter
    return 0
  fi
   local stashed_changes=false
  local stash_pop_failed=false
  # Check for uncommitted changes that are tracked or untracked
  if ! git diff-index --quiet HEAD -- || git ls-files --others --exclude-standard | grep .; then
      log "Stashing local changes..."
      if git stash push -u -m "Auto-stash by KCPP Manager script $(date)"; then
          stashed_changes=true
          info "Local changes stashed."
      else
          warn "Failed to stash local changes. If you have modifications, they might be overwritten or cause conflicts."
      fi
  else
      info "No local changes to stash."
  fi
   log "Fetching updates from origin..."
  # Re-fetch in case --quiet fetch missed something or was skipped earlier
  if ! git fetch --all --prune; then
      error "Failed to fetch updates from repository."
      # Attempt to reapply stash before exiting on fetch failure
      if [ "$stashed_changes" = true ]; then
          info "Attempting to reapply stashed changes after fetch failure..."
          if git stash pop; then
              success "Successfully reapplied stashed local changes."
          else
              warn "Could not automatically reapply stashed local changes after fetch failure. Conflicts may have occurred."
              info "Your changes are still available in the stash. Use 'git stash list' to see them and 'git stash apply stash@{0}' (or similar) to reapply manually."
              stash_pop_failed=true
          fi
      fi
      if [ "$stash_pop_failed" = true ]; then wait_for_keypress_keep_screen; fi
      return 1
  fi
   log "Resetting to latest version of '$DEFAULT_BRANCH'..."
  if ! git reset --hard "origin/$DEFAULT_BRANCH"; then
      error "Failed to update to latest version of branch '$DEFAULT_BRANCH'. Your local repository might be in an inconsistent state."
       # Attempt to reapply stash before exiting on reset failure
      if [ "$stashed_changes" = true ]; then
          info "Attempting to reapply stashed changes after reset failure..."
          if git stash pop; then
              success "Successfully reapplied stashed local changes."
          else
              warn "Could not automatically reapply stashed local changes after reset failure. Conflicts may have occurred."
              info "Your changes are still available in the stash. Use 'git stash list' to see them and 'git stash apply stash@{0}' (or similar) to reapply manually."
              stash_pop_failed=true
          fi
      fi
      if [ "$stash_pop_failed" = true ]; then wait_for_keypress_keep_screen; fi
      return 1
  fi
   log "Rebuilding KoboldCpp after update..."
  if ! rebuild; then
    # rebuild() calls wait_for_keypress_keep_screen on its own error
    # Stash reapplying is handled below after rebuild attempt
    local rebuild_failed=true
  else
    local rebuild_failed=false
  fi
    if [ "$stashed_changes" = true ]; then
    info "Attempting to reapply stashed local changes..."
    if git stash pop; then
      success "Successfully reapplied stashed local changes."
    else
      stash_pop_failed=true
      warn "Could not automatically reapply stashed local changes. Conflicts may have occurred."
      info "Your changes are still available in the stash. Use 'git stash list' to see them and 'git stash apply stash@{0}' (or similar) to reapply manually."
    fi
  fi
   if [ "$rebuild_failed" = true ]; then
      error "KoboldCpp update completed (source fetched and reset), but rebuild failed."
      wait_for_keypress_keep_screen # Keep rebuild error message visible
      return 1 # Indicate failure
  elif [ "$updates_available" = true ]; then
      success "KoboldCpp updated to the latest version from '$DEFAULT_BRANCH' and rebuilt."
  else
      info "KoboldCpp source code was already up-to-date."
      success "Rebuild completed."
  fi
    if [ "$stash_pop_failed" = true ]; then
      wait_for_keypress_keep_screen # Keep complex stash message visible
  else
      press_enter
  fi
  return 0
}
 backup_models() {
  clear_screen
  echo -e "${BOLD}${CYAN}=== Backup Models ===${NC}"
  echo
  log "Creating a compressed archive of all .gguf models in '$MODEL_DIR'."
  echo
   check_install || { return 1; }
   _validate_critical_directory_path "$MODEL_DIR" "$EXPECTED_MODEL_DIR_PATH" "backup" || {
      wait_for_keypress_keep_screen; return 1;
  }
   local MODELS_EXIST_COUNT
  MODELS_EXIST_COUNT=$(find "$MODEL_DIR" -maxdepth 1 -name "*.gguf" -type f -print0 2>/dev/null | awk 'END{print NR}' RS='\0')
  if [ "${MODELS_EXIST_COUNT:-0}" -eq 0 ]; then
    warn "No models found in '$MODEL_DIR' to backup."
    press_enter
    return 0
  fi
   mkdir -p "$BACKUP_DIR" || { error "Failed to create backup directory: '$BACKUP_DIR'"; wait_for_keypress_keep_screen; return 1; }
  local BACKUP_FILE="$BACKUP_DIR/koboldcpp_models_$(date +%Y%m%d_%H%M%S).tar.gz"
   # Estimate backup size
  local TOTAL_MODELS_SIZE_BYTES=0
  while IFS= read -r -d '' model_file; do
    local file_size=$(get_file_size_bytes "$model_file")
    TOTAL_MODELS_SIZE_BYTES=$((TOTAL_MODELS_SIZE_BYTES + file_size))
  done < <(find "$MODEL_DIR" -maxdepth 1 -name "*.gguf" -type f -print0 2>/dev/null)
   if [ "$TOTAL_MODELS_SIZE_BYTES" -eq 0 ]; then
      warn "Found .gguf files, but their total size is zero. Skipping backup."
      press_enter
      return 0
  fi
   # Add 5% overhead for tar/gzip, minimum 1MB
  local ESTIMATED_BACKUP_SIZE=$((TOTAL_MODELS_SIZE_BYTES * 105 / 100))
  if [ "$ESTIMATED_BACKUP_SIZE" -lt 1048576 ] && [ "$TOTAL_MODELS_SIZE_BYTES" -gt 0 ]; then
      ESTIMATED_BACKUP_SIZE=1048576 # Minimum 1MB estimate if models exist
  fi
    # Check available space
  if ! check_available_space "$ESTIMATED_BACKUP_SIZE" "$BACKUP_DIR"; then
    error "Not enough space for backup. Please free up some storage or backup to an external device."
    wait_for_keypress_keep_screen
    return 1
  fi
   log "Backing up models from '$MODEL_DIR' to '$BACKUP_FILE'..."
  log "This may take a while for large models."
   if command -v termux-wake-lock &>/dev/null; then
    termux-wake-lock
  fi
   # Use tar with progress bar if 'pv' is installed
  if command -v pv &>/dev/null; then
      log "Using 'pv' for progress indication."
      if ! tar -c -C "$(dirname "$MODEL_DIR")" "$(basename "$MODEL_DIR")" 2>/dev/null | pv -s "$TOTAL_MODELS_SIZE_BYTES" -p -e -r -b -W | gzip > "$BACKUP_FILE"; then
         local tar_gzip_exit=$?
         error "Backup failed (tar/gzip with pv exited with code $tar_gzip_exit). Check permissions and available space."
         rm -f "$BACKUP_FILE" # Clean up failed backup file
         if command -v termux-wake-unlock &>/dev/null; then
           termux-wake-unlock
         fi
         wait_for_keypress_keep_screen
         return 1
      fi
  else
      log "Using 'tar' and 'gzip' without progress indicator (pv not found)."
      if ! tar -czf "$BACKUP_FILE" -C "$(dirname "$MODEL_DIR")" "$(basename "$MODEL_DIR")"; then
         local tar_gzip_exit=$?
         error "Backup failed (tar/gzip exited with code $tar_gzip_exit). Check permissions and available space."
         rm -f "$BACKUP_FILE" # Clean up failed backup file
         if command -v termux-wake-unlock &>/dev/null; then
           termux-wake-unlock
         fi
         wait_for_keypress_keep_screen
         return 1
      fi
  fi
   if command -v termux-wake-unlock &>/dev/null; then
    termux-wake-unlock
  fi
   if [ -f "$BACKUP_FILE" ]; then
      success "Models backed up successfully to '$BACKUP_FILE'"
      info "Backup size: $(get_file_size_human "$BACKUP_FILE")"
       # Ask if user wants to share/copy the backup elsewhere
      echo
      echo "What would you like to do with the backup file?"
      echo "[1] Keep the backup in the default location ($BACKUP_DIR)"
      echo "[2] Share the backup file via Android share menu"
      echo "[3] Copy the backup to external storage"
      echo "[4] Move the backup to external storage (removes from default location)"
      echo
      echo -n "Your choice [1]: "
      read -r BACKUP_ACTION
       case "${BACKUP_ACTION:-1}" in
        2)
          if command -v termux-share &>/dev/null; then
            log "Sharing backup file '$BACKUP_FILE'..."
            termux-share "$BACKUP_FILE" || warn "Failed to share file using termux-share."
          else
            warn "termux-api not installed. Cannot share the backup file. Install with 'pkg install termux-api'."
          fi
          ;;
        3|4)
          local action="Copy"
          if [ "${BACKUP_ACTION:-}" = "4" ]; then action="Move"; fi
          echo
          echo "Enter destination directory (e.g., /sdcard/Download or $DOWNLOAD_DIR):"
          read -r DEST_DIR
          # Basic validation
          if [ -z "$DEST_DIR" ]; then
              error "Destination directory cannot be empty. Operation cancelled."
          elif [ ! -d "$DEST_DIR" ]; then
              error "Destination directory '$DEST_DIR' does not exist. Operation cancelled."
          elif [ ! -w "$DEST_DIR" ]; then
               error "Destination directory '$DEST_DIR' is not writable. Check permissions. Operation cancelled."
          else
             log "${action}ing '$BACKUP_FILE' to '$DEST_DIR/'..."
             if [ "$action" = "Copy" ]; then
                 if cp "$BACKUP_FILE" "$DEST_DIR/"; then
                   success "Backup copied to $DEST_DIR/$(basename "$BACKUP_FILE")"
                 else
                   error "Failed to copy backup to $DEST_DIR. Check permissions or space."
                 fi
             else # Move
                 if mv "$BACKUP_FILE" "$DEST_DIR/"; then
                   success "Backup moved to $DEST_DIR/$(basename "$BACKUP_FILE")"
                 else
                   error "Failed to move backup to $DEST_DIR. Check permissions or space."
                   warn "Original backup might still be in '$BACKUP_DIR'."
                 fi
             fi
          fi
          ;;
        *)
          info "Backup kept in default location: $BACKUP_FILE"
          ;;
      esac
  else
      error "Backup file '$BACKUP_FILE' was not created successfully."
  fi
    press_enter
  return 0
}
 restore_models() {
  clear_screen
  echo -e "${BOLD}${CYAN}=== Restore Models ===${NC}"
  echo
  log "This will extract models from a backup archive (.tar.gz) into your KoboldCpp models directory ($MODEL_DIR)."
  echo -e "${RED}WARNING: This will REPLACE your current models in '$MODEL_DIR'.${NC}"
  echo
   check_install || { return 1; }
   _validate_critical_directory_path "$MODEL_DIR" "$EXPECTED_MODEL_DIR_PATH" "restore" || {
      wait_for_keypress_keep_screen; return 1;
  }
   # Check for backups in the default location
  if [ ! -d "$BACKUP_DIR" ]; then
    mkdir -p "$BACKUP_DIR"
    info "Backup directory '$BACKUP_DIR' created, but no backups found."
    echo
    echo "Options:"
    echo "[1] Import a backup file from elsewhere into the backup directory"
    echo "[2] Return to menu"
    echo
    echo -n "Your choice: "
    read -r IMPORT_CHOICE
     if [ "$IMPORT_CHOICE" = "1" ]; then
      echo
      echo "Enter full path to the backup file (.tar.gz) you want to import:"
      read -r IMPORT_FILE
      if [ -f "$IMPORT_FILE" ] && [[ "$IMPORT_FILE" == *.tar.gz ]]; then
        log "Importing '$IMPORT_FILE' to '$BACKUP_DIR/'..."
        if cp "$IMPORT_FILE" "$BACKUP_DIR/"; then
          success "Backup file imported to $BACKUP_DIR/$(basename "$IMPORT_FILE")"
        else
          error "Failed to import backup file '$IMPORT_FILE'. Check permissions or space."
          wait_for_keypress_keep_screen
          return 1
        fi
      else
        error "File doesn't exist, isn't a .tar.gz file, or path was empty."
        wait_for_keypress_keep_screen
        return 1
      fi
    else
      return 0 # User chose to return
    fi
  fi # End of backup directory check and import option
    echo -e "${YELLOW}Available backups in $BACKUP_DIR:${NC}"
  local BACKUPS=()
  # Sort by modification time, newest first
  while IFS= read -r -d '' file; do
    BACKUPS+=("$file")
  done < <(find "$BACKUP_DIR" -maxdepth 1 -name "*.tar.gz" -type f -print0 2>/dev/null | sort -rz)
   if [ ${#BACKUPS[@]} -eq 0 ]; then
    error "No backup archives (.tar.gz) found in '$BACKUP_DIR'."
    wait_for_keypress_keep_screen
    return 1
  fi
   for i in "${!BACKUPS[@]}"; do
    local BACKUP_ARCHIVE="${BACKUPS[$i]}"
    local SIZE
    SIZE=$(get_file_size_human "$BACKUP_ARCHIVE")
    local DATE_MODIFIED
    DATE_MODIFIED=$(date -r "$BACKUP_ARCHIVE" "+%Y-%m-%d %H:%M:%S")
    echo "[$i] $(basename "$BACKUP_ARCHIVE") ($SIZE) - $DATE_MODIFIED"
  done
   echo
  echo -n "Enter number of the backup to restore (or 'q' to return to menu): "
  read -r CHOICE
   if [[ "${CHOICE:-}" == "q" || "${CHOICE:-}" == "Q" ]]; then
    return 0
  fi
   if [[ ! "$CHOICE" =~ ^[0-9]+$ ]] || [ "$CHOICE" -lt 0 ] || [ "$CHOICE" -ge "${#BACKUPS[@]}" ]; then
    error "Invalid selection."
    wait_for_keypress_keep_screen
    return 1
  fi
   local BACKUP_TO_RESTORE="${BACKUPS[$CHOICE]}"
  log "Selected backup for restore: $(basename "$BACKUP_TO_RESTORE")"
   # Preview backup contents
  echo
  echo -e "${YELLOW}Contents of the backup (first 10 .gguf files):${NC}"
  local backup_contents=$(tar -tzf "$BACKUP_TO_RESTORE" 2>/dev/null | grep -E '\.gguf$' | head -n 10)
  if [ -n "$backup_contents" ]; then
      echo "$backup_contents"
      local total_items=$(tar -tzf "$BACKUP_TO_RESTORE" 2>/dev/null | grep -E '\.gguf$' | wc -l)
      if [ "$total_items" -gt 10 ]; then
          echo "... and $((total_items - 10)) more .gguf items"
      fi
  else
      warn "Could not list contents or no .gguf files found in the backup archive."
  fi
  echo
   # Check if backup has expected directory structure ('models/')
  if ! tar -tzf "$BACKUP_TO_RESTORE" 2>/dev/null | grep -q "^models/.*\.gguf$"; then
    warn "This backup may not have the expected directory structure ('models/*.gguf')."
    info "It might extract files directly into the parent directory of '$MODEL_DIR'."
    echo -n "Continue anyway? [y/N]: "
    read -r CONTINUE_RESTORE
    if [[ ! "${CONTINUE_RESTORE:-N}" =~ ^[Yy]$ ]]; then
      info "Restore operation cancelled."
      press_enter
      return 0
    fi
  fi
   # Check available space for restore
  local BACKUP_SIZE=$(get_file_size_bytes "$BACKUP_TO_RESTORE")
  # Estimate uncompressed size (gzip compression ratio varies, estimate 2x as a minimum)
  local ESTIMATED_RESTORE_SIZE=$((BACKUP_SIZE * 3)) # Use 3x for a slightly safer estimate
  if [ "$ESTIMATED_RESTORE_SIZE" -lt 1048576 ]; then ESTIMATED_RESTORE_SIZE=1048576; fi # Minimum 1MB estimate
   if ! check_available_space "$ESTIMATED_RESTORE_SIZE" "$(dirname "$MODEL_DIR")"; then
    error "Not enough space to restore the backup in '$(dirname "$MODEL_DIR")'. Required estimate: $(numfmt --to=iec-i --suffix=B "$ESTIMATED_RESTORE_SIZE" 2>/dev/null || echo "$ESTIMATED_RESTORE_SIZE bytes"). Please free up some storage."
    wait_for_keypress_keep_screen
    return 1
  fi
   echo -e "${RED}WARNING: This will DELETE all existing files in '$MODEL_DIR' before restoring!${NC}"
  echo -n "Are you absolutely sure you want to restore from this backup? [y/N]: "
  read -r CONFIRM
  if [[ ! "${CONFIRM:-N}" =~ ^[Yy]$ ]]; then # Default N
    info "Restore operation cancelled."
    press_enter
    return 0
  fi
   local PRE_RESTORE_BACKUP_DIR="" # Initialize as empty
  local current_models_count=$(find "$MODEL_DIR" -maxdepth 1 -name "*.gguf" -type f 2>/dev/null | wc -l)
  if [ -d "$MODEL_DIR" ] && [ "$current_models_count" -gt 0 ]; then
      log "Backing up current models to a temporary location before restoring..."
      PRE_RESTORE_BACKUP_DIR="$BACKUP_DIR/pre_restore_$(date +%Y%m%d_%H%M%S)_$$" # Add PID for uniqueness
      if mkdir -p "$PRE_RESTORE_BACKUP_DIR"; then
          if cp -a "$MODEL_DIR/." "$PRE_RESTORE_BACKUP_DIR/"; then
              info "Current models backed up to '$PRE_RESTORE_BACKUP_DIR'"
          else
              warn "Failed to backup current models to '$PRE_RESTORE_BACKUP_DIR'. Continuing with restore, but current models are NOT backed up by this operation."
              PRE_RESTORE_BACKUP_DIR="" # Clear path if backup failed
          fi
      else
          warn "Could not create directory '$PRE_RESTORE_BACKUP_DIR' for pre-restore backup. Skipping this step."
          PRE_RESTORE_BACKUP_DIR="" # Clear path if directory creation failed
      fi
  else
      info "No existing models in '$MODEL_DIR' to back up before restoring."
  fi
    log "Clearing existing models in '$MODEL_DIR'..."
  # Use find -delete for safer recursive deletion within the specific directory
  if [ -d "$MODEL_DIR" ]; then
      if find "$MODEL_DIR" -mindepth 1 -delete; then
          log "Existing models cleared from '$MODEL_DIR'."
      else
          error "Failed to clear existing models from '$MODEL_DIR'. Check permissions."
          _handle_restore_failure "$PRE_RESTORE_BACKUP_DIR" "$MODEL_DIR" # Try to restore pre-restore backup
          return 1
      fi
  else
      mkdir -p "$MODEL_DIR" || { error "Failed to create model directory: '$MODEL_DIR'"; wait_for_keypress_keep_screen; return 1; }
  fi
    log "Extracting backup: $(basename "$BACKUP_TO_RESTORE") to '$(dirname "$MODEL_DIR")'..."
  log "This may take a while for large backups."
  # Ensure the parent directory exists for extraction
  mkdir -p "$(dirname "$MODEL_DIR")" || { error "Failed to create parent directory for models: '$(dirname "$MODEL_DIR")'"; wait_for_keypress_keep_screen; return 1; }
   if command -v termux-wake-lock &>/dev/null; then
    termux-wake-lock
  fi
   # Extract with progress if available
  if command -v pv &>/dev/null; then
    log "Using 'pv' for progress indication during extraction."
    if ! pv "$BACKUP_TO_RESTORE" | tar -xzf - -C "$(dirname "$MODEL_DIR")"; then
      local extract_exit=$?
      error "Restore failed during extraction (Exit code $extract_exit). The backup might be corrupted or there might be permission/space issues."
      if command -v termux-wake-unlock &>/dev/null; then
        termux-wake-unlock
      fi
      _handle_restore_failure "$PRE_RESTORE_BACKUP_DIR" "$MODEL_DIR" # Try to restore the pre-restore backup
      return 1
    fi
  else
    log "Extracting with 'tar' without progress indicator (pv not found)."
    if ! tar -xzf "$BACKUP_TO_RESTORE" -C "$(dirname "$MODEL_DIR")"; then
      local extract_exit=$?
      error "Restore failed during extraction (Exit code $extract_exit). The backup might be corrupted or there might be permission/space issues."
      if command -v termux-wake-unlock &>/dev/null; then
        termux-wake-unlock
      fi
      _handle_restore_failure "$PRE_RESTORE_BACKUP_DIR" "$MODEL_DIR" # Try to restore the pre-restore backup
      return 1
    fi
  fi
   if command -v termux-wake-unlock &>/dev/null; then
    termux-wake-unlock
  fi
   # Verify restored models integrity
  log "Verifying restored models integrity..."
  local corrupt_models=0
  local restored_models_count=$(find "$MODEL_DIR" -maxdepth 1 -name "*.gguf" -type f 2>/dev/null | wc -l)
   if [ "$restored_models_count" -eq 0 ]; then
      warn "No .gguf files found in '$MODEL_DIR' after extraction. Restore may have failed or the backup was empty/incorrect."
      _handle_restore_failure "$PRE_RESTORE_BACKUP_DIR" "$MODEL_DIR" # Try to restore pre-restore backup
      return 1
  fi
   while IFS= read -r -d '' model_file; do
    if ! check_model_integrity "$model_file" >/dev/null 2>&1; then # Check silently
      warn "Model $(basename "$model_file") may have integrity issues after restoring."
      ((corrupt_models++))
    fi
  done < <(find "$MODEL_DIR" -maxdepth 1 -name "*.gguf" -type f -print0 2>/dev/null)
   if [ "$corrupt_models" -gt 0 ]; then
    warn "$corrupt_models models out of $restored_models_count may have integrity issues after restoring. Consider verifying them individually."
    wait_for_keypress_keep_screen # Pause to show warnings
  fi
   success "Models restored successfully from $(basename "$BACKUP_TO_RESTORE"). Total restored models: $restored_models_count."
   # Clean up the temporary pre-restore backup if it exists and restore was successful
  if [ -n "$PRE_RESTORE_BACKUP_DIR" ] && [ -d "$PRE_RESTORE_BACKUP_DIR" ]; then
      log "Cleaning up temporary pre-restore backup at '$PRE_RESTORE_BACKUP_DIR'..."
      rm -rf "$PRE_RESTORE_BACKUP_DIR" || warn "Failed to remove temporary pre-restore backup."
  fi
   press_enter
  return 0
}
 # Helper function for restore failures
_handle_restore_failure() {
  local pre_restore_dir="$1"
  local model_dir="$2"
   error "Restore operation failed."
  info "Attempting to restore the pre-restore backup if it exists and contains files..."
   if [ -n "$pre_restore_dir" ] && [ -d "$pre_restore_dir" ] && [ "$(ls -A "$pre_restore_dir" 2>/dev/null)" ]; then
      info "Clearing potentially inconsistent state in '$model_dir'..."
      find "$model_dir" -mindepth 1 -delete || warn "Failed to clear '$model_dir' before attempting pre-restore recovery."
      mkdir -p "$model_dir" || warn "Failed to recreate '$model_dir'."
       info "Copying models back from pre-restore backup '$pre_restore_dir'..."
      if cp -a "$pre_restore_dir/." "$model_dir/"; then
          success "Successfully restored models from the pre-restore backup at '$pre_restore_dir'."
          info "You may want to verify the integrity of these models."
          # Clean up the temporary backup after successful recovery
          log "Cleaning up temporary pre-restore backup..."
          rm -rf "$pre_restore_dir" || warn "Failed to remove temporary pre-restore backup."
      else
          warn "Failed to restore models from the pre-restore backup."
          warn "Model directory '$model_dir' might be in an inconsistent state."
          warn "Your original models might still be in '$pre_restore_dir'."
      fi
  else
    warn "No valid pre-restore backup found or it was empty. Model directory '$model_dir' might be empty or in an inconsistent state."
  fi
  wait_for_keypress_keep_screen # Ensure the failure message and recovery status are seen
}
  # Function to clean up unnecessary or temporary files
cleanup() {
  clear_screen
  echo -e "${BOLD}${CYAN}=== Clean Up Files ===${NC}"
  echo
  log "This utility helps free up disk space by removing temporary files, old backups, and build artifacts."
  echo
   log "Searching for files to clean up..."
   local SPACE_FREED=0
  local TEMP_SPACE_SAVED=0
   # Clean up build artifacts in KoboldCpp directory
  if [ -d "$KOBOLDCPP_DIR" ]; then
    cd "$KOBOLDCPP_DIR" || { error "Could not cd to '$KOBOLDCPP_DIR'"; wait_for_keypress_keep_screen; return 1; }
     # Clean build directory if it exists
    if [ -d "build" ]; then
      local BUILD_SIZE_KB=$(du -sk "build" 2>/dev/null | cut -f1)
      if [ -n "$BUILD_SIZE_KB" ] && [ "$BUILD_SIZE_KB" -gt 0 ]; then
          local BUILD_SIZE_BYTES=$((BUILD_SIZE_KB * 1024))
          echo -n "Clean build artifacts in '$KOBOLDCPP_DIR/build' ($(numfmt --to=iec-i --suffix=B "$BUILD_SIZE_BYTES" 2>/dev/null || echo "$BUILD_SIZE_BYTES bytes"))? [Y/n]: "
          read -r CLEAN_BUILD
          if [[ ! "${CLEAN_BUILD:-Y}" =~ ^[Nn]$ ]]; then
            rm -rf build/
            TEMP_SPACE_SAVED=$((TEMP_SPACE_SAVED + BUILD_SIZE_BYTES))
            success "Build artifacts cleaned."
          fi
      else
          info "No significant build artifacts found in '$KOBOLDCPP_DIR/build'."
      fi
    else
        info "Build directory '$KOBOLDCPP_DIR/build' not found."
    fi
     # Compact Git repository
    if [ -d ".git" ]; then
      local GIT_SIZE_BEFORE_KB=$(du -sk ".git" 2>/dev/null | cut -f1)
      if [ -n "$GIT_SIZE_BEFORE_KB" ] && [ "$GIT_SIZE_BEFORE_KB" -gt 0 ]; then
          local GIT_SIZE_BEFORE_BYTES=$((GIT_SIZE_BEFORE_KB * 1024))
          echo -n "Compact Git repository in '$KOBOLDCPP_DIR/.git' ($(numfmt --to=iec-i --suffix=B "$GIT_SIZE_BEFORE_BYTES" 2>/dev/null || echo "$GIT_SIZE_BEFORE_BYTES bytes"))? [Y/n]: "
          read -r CLEAN_GIT
          if [[ ! "${CLEAN_GIT:-Y}" =~ ^[Nn]$ ]]; then
            log "Running git gc --aggressive..."
            git gc --aggressive --prune=now 2>&1 | grep -v 'Counting objects:' | grep -v 'Delta compression' | grep -v 'Compressing objects' | grep -v 'Writing objects' | grep -v 'Total' # Suppress standard gc output
            local GIT_SIZE_AFTER_KB=$(du -sk ".git" 2>/dev/null | cut -f1)
            if [ -n "$GIT_SIZE_AFTER_KB" ] && [ "$GIT_SIZE_AFTER_KB" -ge 0 ]; then
                local GIT_SIZE_AFTER_BYTES=$((GIT_SIZE_AFTER_KB * 1024))
                local saved=$((GIT_SIZE_BEFORE_BYTES - GIT_SIZE_AFTER_BYTES))
                if [ "$saved" -gt 0 ]; then
                  TEMP_SPACE_SAVED=$((TEMP_SPACE_SAVED + saved))
                  success "Git repository compacted, saved $(numfmt --to=iec-i --suffix=B "$saved" 2>/dev/null || echo "$saved bytes")."
                else
                  info "Git repository already optimized, no significant space saved."
                fi
            else
                warn "Could not determine Git repository size after compaction."
            fi
          fi
      else
          info "No significant Git repository data found in '$KOBOLDCPP_DIR/.git'."
      fi
    else
        info "Git directory '$KOBOLDCPP_DIR/.git' not found."
    fi
  fi
   # Clean up temporary files and manage old backups in BACKUP_DIR
  if [ -d "$BACKUP_DIR" ]; then
    local temp_backups=$(find "$BACKUP_DIR" -maxdepth 1 -name "pre_restore_*" -type d 2>/dev/null)
    if [ -n "$temp_backups" ]; then
      local TEMP_SIZE_KB=0
      while IFS= read -r dir; do
        local size_kb=$(du -sk "$dir" 2>/dev/null | cut -f1)
        if [ -n "$size_kb" ]; then
            TEMP_SIZE_KB=$((TEMP_SIZE_KB + size_kb))
        fi
      done <<< "$temp_backups"
      local TEMP_SIZE_BYTES=$((TEMP_SIZE_KB * 1024))
       if [ "$TEMP_SIZE_BYTES" -gt 0 ]; then
          echo -n "Clean up temporary pre-restore backup directories ($(numfmt --to=iec-i --suffix=B "$TEMP_SIZE_BYTES" 2>/dev/null || echo "$TEMP_SIZE_BYTES bytes"))? [Y/n]: "
          read -r CLEAN_TEMP
          if [[ ! "${CLEAN_TEMP:-Y}" =~ ^[Nn]$ ]]; then
            while IFS= read -r dir; do
              rm -rf "$dir"
            done <<< "$temp_backups"
            SPACE_FREED=$((SPACE_FREED + TEMP_SIZE_BYTES)) # This is permanent space freed
            success "Temporary backup files cleaned."
          fi
      else
          info "No significant temporary backup directories found in '$BACKUP_DIR'."
      fi
    else
        info "No temporary pre-restore backup directories found in '$BACKUP_DIR'."
    fi
     # Manage old backups
    local backup_files=($(find "$BACKUP_DIR" -maxdepth 1 -name "*.tar.gz" -type f -print0 2>/dev/null | xargs -0))
    local backup_count=${#backup_files[@]}
     if [ "$backup_count" -gt 5 ]; then
      echo -n "You have $backup_count model backups. Keep only the 5 most recent? [y/N]: "
      read -r CLEAN_BACKUPS
      if [[ "${CLEAN_BACKUPS:-N}" =~ ^[Yy]$ ]]; then
        # Find backups to remove (older than the 5 most recent)
        local old_backups=($(find "$BACKUP_DIR" -maxdepth 1 -name "*.tar.gz" -type f -printf "%T@ %p\n" 2>/dev/null | sort -n | head -n $((backup_count - 5)) | cut -d' ' -f2-))
        local OLD_SIZE_BYTES=0
        local files_removed=0
        for file in "${old_backups[@]}"; do
          if [ -f "$file" ]; then # Ensure file still exists before attempting to delete
              local size=$(get_file_size_bytes "$file")
              OLD_SIZE_BYTES=$((OLD_SIZE_BYTES + size))
              if rm -f "$file"; then
                  ((files_removed++))
              else
                  warn "Failed to delete old backup file: $file"
              fi
          fi
        done
        SPACE_FREED=$((SPACE_FREED + OLD_SIZE_BYTES)) # This is permanent space freed
        success "Removed $files_removed old backups, freed $(numfmt --to=iec-i --suffix=B "$OLD_SIZE_BYTES" 2>/dev/null || echo "$OLD_SIZE_BYTES bytes")."
      fi
    elif [ "$backup_count" -gt 0 ]; then
        info "You have $backup_count model backups. No old backups to remove (keeping 5 most recent)."
    else
        info "No model backups found in '$BACKUP_DIR'."
    fi
  else
      info "Backup directory '$BACKUP_DIR' not found."
  fi
   # Log file rotation
  if [ -f "$LOG_FILE" ]; then
    local log_size=$(get_file_size_bytes "$LOG_FILE")
    if [ "$log_size" -gt 1048576 ]; then # 1MB
      echo -n "Rotate log file ($(numfmt --to=iec-i --suffix=B "$log_size" 2>/dev/null || echo "$log_size bytes"))? [Y/n]: "
      read -r ROTATE_LOG
      if [[ ! "${ROTATE_LOG:-Y}" =~ ^[Nn]$ ]]; then
        mv "$LOG_FILE" "${LOG_FILE}.old"
        touch "$LOG_FILE"
        echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO: Log rotated, old log saved as ${LOG_FILE}.old" >> "$LOG_FILE"
        success "Log file rotated, freed $(numfmt --to=iec-i --suffix=B "$log_size" 2>/dev/null || echo "$log_size bytes")."
        TEMP_SPACE_SAVED=$((TEMP_SPACE_SAVED + log_size)) # This space is potentially recoverable by deleting the .old file
      fi
    else
        info "Log file size is manageable ($(numfmt --to=iec-i --suffix=B "$log_size" 2>/dev/null || echo "$log_size bytes"))."
    fi
  else
      info "Log file '$LOG_FILE' not found."
  fi
    # Show summary
  echo
  if [ "$SPACE_FREED" -gt 0 ] || [ "$TEMP_SPACE_SAVED" -gt 0 ]; then
    success "Cleanup complete."
    if [ "$SPACE_FREED" -gt 0 ]; then
        info "Permanent space freed: $(numfmt --to=iec-i --suffix=B "$SPACE_FREED" 2>/dev/null || echo "$SPACE_FREED bytes")."
    fi
     if [ "$TEMP_SPACE_SAVED" -gt 0 ]; then
        info "Temporary/compressible space cleaned: $(numfmt --to=iec-i --suffix=B "$TEMP_SPACE_SAVED" 2>/dev/null || echo "$TEMP_SPACE_SAVED bytes")."
    fi
  else
    info "No files were cleaned up."
  fi
   press_enter
  return 0
}
 install_update_menu() {
  while true; do
    clear_screen
    echo -e "${BOLD}${CYAN}=== Installation, Updates, & Rebuild ===${NC}"
    echo
    local kobold_installed=false
    if [ -d "$KOBOLDCPP_DIR" ] && ([ -f "$KOBOLDCPP_DIR/koboldcpp.py" ] || [ -f "$KOBOLDCPP_DIR/koboldcpp-android-arm64" ] || [ -f "$KOBOLDCPP_DIR/koboldcpp" ]); then
        kobold_installed=true
        echo -e "${GREEN}KoboldCpp is currently installed in $KOBOLDCPP_DIR.${NC}"
        if [ -d "$KOBOLDCPP_DIR/.git" ]; then
             local current_commit=$(git -C "$KOBOLDCPP_DIR" rev-parse --short HEAD 2>/dev/null || echo "Unknown")
             echo -e "Current version (commit): ${YELLOW}$current_commit${NC}"
        fi
    else
        echo -e "${YELLOW}KoboldCpp is not currently installed.${NC}"
    fi
    echo
     echo -e "${BOLD}Options:${NC}"
    echo
    echo "1. Install / Reinstall KoboldCpp"
    if [ "$kobold_installed" = true ]; then
        echo "2. Update KoboldCpp (fetch latest code and rebuild)"
        echo "3. Rebuild KoboldCpp (from current source code)"
    fi
    echo "0. Back to Main Menu"
    echo
    echo -n "Enter your choice: "
    read -r CHOICE
     case "${CHOICE:-}" in
      1) install_all ;;
      2)
         if [ "$kobold_installed" = true ]; then
             update_koboldcpp
         else
             error "Option 2 is only available if KoboldCpp is installed."
             wait_for_keypress_keep_screen
         fi
         ;;
      3)
         if [ "$kobold_installed" = true ]; then
             rebuild
         else
             error "Option 3 is only available if KoboldCpp is installed."
             wait_for_keypress_keep_screen
         fi
         ;;
      0) return ;;
      *)
        error "Invalid choice. Please try again."
        wait_for_keypress_keep_screen
        ;;
    esac
  done
}
 backup_restore_menu() {
  while true; do
    clear_screen
    echo -e "${BOLD}${CYAN}=== Backup & Restore Models ===${NC}"
    echo
    local backup_count=$(find "$BACKUP_DIR" -maxdepth 1 -name "*.tar.gz" -type f 2>/dev/null | wc -l)
    echo -e "Backups found in $BACKUP_DIR: ${YELLOW}${backup_count:-0}${NC}"
    echo
     echo -e "${BOLD}Options:${NC}"
    echo
    echo "1. Backup current models"
    echo "2. Restore models from backup"
    echo "0. Back to Main Menu"
    echo
    echo -n "Enter your choice: "
    read -r CHOICE
     case "${CHOICE:-}" in
      1) backup_models ;;
      2) restore_models ;;
      0) return ;;
      *)
        error "Invalid choice. Please try again."
        wait_for_keypress_keep_screen
        ;;
    esac
  done
}
 advanced_menu() {
  while true; do
    clear_screen
    echo -e "${BOLD}${CYAN}=== Advanced Options ===${NC}"
    echo
    echo -e "${YELLOW}Use these options with caution.${NC}"
    echo
     echo -e "${BOLD}Options:${NC}"
    echo
    echo "1. Clean up temporary files and old backups"
    echo "2. Open log file ($LOG_FILE)"
    echo "3. Reset configuration file ($CONFIG_FILE)"
    echo "0. Back to Main Menu"
    echo
    echo -n "Enter your choice: "
    read -r CHOICE
     case "${CHOICE:-}" in
      1) cleanup ;;
      2)
        if [ -f "$LOG_FILE" ]; then
            log "Opening log file in default viewer..."
            termux-open "$LOG_FILE" || {
                warn "Failed to open log file. You might need to install a text editor or file viewer."
                info "You can view it manually using: less \"$LOG_FILE\""
                wait_for_keypress_keep_screen
            }
        else
            warn "Log file '$LOG_FILE' not found."
            wait_for_keypress_keep_screen
        fi
        ;;
      3)
        echo -e "${RED}WARNING: This will delete your saved configurations (last used model, run parameters, etc.).${NC}"
        echo -n "Are you sure you want to reset the configuration file '$CONFIG_FILE'? [y/N]: "
        read -r RESET_CONFIRM
        if [[ "${RESET_CONFIRM:-N}" =~ ^[Yy]$ ]]; then
            if rm -f "$CONFIG_FILE"; then
                success "Configuration file '$CONFIG_FILE' reset."
                # Re-initialize default build options
                save_config "build_options" "0"
                info "Default build options reset."
            else
                error "Failed to reset configuration file '$CONFIG_FILE'. Check permissions."
            fi
        else
            info "Configuration reset cancelled."
        fi
        press_enter
        ;;
      0) return ;;
      *)
        error "Invalid choice. Please try again."
        wait_for_keypress_keep_screen
        ;;
    esac
  done
}
  about() {
  clear_screen
  echo -e "${BOLD}${CYAN}=== About KoboldCpp Termux Manager ===${NC}"
  echo
  echo "KoboldCpp Termux Manager v$VERSION"
  echo
  echo "This tool helps you install, manage, and run KoboldCpp on Android using Termux."
  echo
  echo "Original KoboldCpp project: $REPO_URL"
  echo
  echo "Installation directory: ${BLUE}$KOBOLDCPP_DIR${NC}"
  echo "Model Cache directory:  ${BLUE}$MODEL_DIR${NC}" # Clarified as Model Cache
  echo "Downloads directory:    ${BLUE}$DOWNLOAD_DIR${NC}"
  echo "Backups directory:      ${BLUE}$BACKUP_DIR${NC}"
  echo "Log file:               ${BLUE}$LOG_FILE${NC}"
  echo "Config file:            ${BLUE}$CONFIG_FILE${NC}"
  echo
  log "The Model Cache directory ($MODEL_DIR) is where your downloaded or added model (.gguf) files are stored for quick access and management by this script."
  echo
   # Show system information
  local device_info=$(get_device_info)
  if [ -n "$device_info" ]; then
    echo "Device information: ${BLUE}$device_info${NC}"
    echo
  fi
   # Show usage statistics if available
  if [ -f "$CONFIG_FILE" ]; then
    local last_runtime_model=$(grep "^last_runtime_model=" "$CONFIG_FILE" | cut -d= -f2)
    local last_runtime_hours=$(grep "^last_runtime_hours=" "$CONFIG_FILE" | cut -d= -f2)
    local last_runtime_minutes=$(grep "^last_runtime_minutes=" "$CONFIG_FILE" | cut -d= -f2)
    local last_runtime_seconds=$(grep "^last_runtime_seconds=" "$CONFIG_FILE" | cut -d= -f2)
     if [ -n "$last_runtime_model" ]; then
      echo "Last session:"
      echo "- Model: ${CYAN}$last_runtime_model${NC}"
      if [ -n "$last_runtime_hours" ]; then
         echo "- Runtime: ${CYAN}${last_runtime_hours}h ${last_runtime_minutes}m ${last_runtime_seconds}s${NC}"
      fi
      echo
    fi
  fi
   echo "New in version $VERSION:"
  echo "- Completed Model Management menu."
  echo "- Added 'Verify model integrity' option to Model Management."
  echo "- Clarified '$MODEL_DIR' as the Model Cache directory."
  echo "- Improved input validation for run parameters."
  echo "- Enhanced file size checks and reporting."
  echo "- Better error handling and cleanup for downloads and restores."
  echo "- Added option to import backups in Restore menu."
  echo "- Option to move backups to external storage."
  echo "- More detailed output during cleanup."
  echo
  press_enter
}
 clean_exit() {
  clear_screen
  if command -v termux-wake-unlock &>/dev/null; then
    log "Releasing wake lock on exit (if active)..."
    termux-wake-unlock 2>/dev/null || true # Ignore error if no lock was active
  fi
  echo "--- KoboldCpp Termux Manager Session Ended: $(date '+%Y-%m-%d %H:%M:%S') ---" >> "$LOG_FILE"
  echo "Thank you for using KoboldCpp Termux Manager!"
  # Remove trap for temp file if it was set and not cleared by _initialize_du_commands
  # This is a general cleanup, though _initialize_du_commands tries to manage its own trap.
  trap - EXIT
  exit 0
}
 # --- Check for updates ---
check_script_updates() {
  # This is a placeholder function - in a real implementation, this would
  # check a repository or URL for newer versions of the script
  # For now, it just returns 0 to indicate no check is performed.
  return 0
}
 # --- Menus ---
 main_menu() {
  echo "--- KoboldCpp Termux Manager Session Started: $(date '+%Y-%m-%d %H:%M:%S') ---" >> "$LOG_FILE"
   # Check for script updates (placeholder)
  # check_script_updates
   trap clean_exit SIGINT SIGTERM
   while true; do
    clear_screen
    echo -e "${BOLD}${CYAN}=== KoboldCpp Termux Manager v$VERSION ===${NC}"
    echo
    log "Your model cache is located at: $MODEL_DIR"
    echo
     local MODEL_COUNT=0
    if [ -d "$MODEL_DIR" ]; then
      # Use find with null delimiter for safety
      MODEL_COUNT=$(find "$MODEL_DIR" -maxdepth 1 -name "*.gguf" -type f -print0 2>/dev/null | grep -c -z '\.gguf$')
    fi
     local py_script_exists=false
    local binary_exists=false
    local kobold_installed=false
     if [ -d "$KOBOLDCPP_DIR" ]; then
        if [ -f "$KOBOLDCPP_DIR/koboldcpp.py" ]; then
            py_script_exists=true
        fi
        if [ -f "$KOBOLDCPP_DIR/koboldcpp-android-arm64" ] || [ -f "$KOBOLDCPP_DIR/koboldcpp" ]; then
            binary_exists=true
        fi
        if [ "$py_script_exists" = true ] || [ "$binary_exists" = true ]; then
            kobold_installed=true
        fi
    fi
     if [ "$kobold_installed" = true ]; then
      echo -e "${GREEN}KoboldCpp is installed.${NC}"
      echo -e "Models in cache: ${YELLOW}${MODEL_COUNT:-0}${NC}"
       # Show last run information if available
      if [ -f "$CONFIG_FILE" ]; then
        local last_runtime_model=$(grep "^last_runtime_model=" "$CONFIG_FILE" | cut -d= -f2)
        local last_runtime_hours=$(grep "^last_runtime_hours=" "$CONFIG_FILE" | cut -d= -f2)
        if [ -n "$last_runtime_model" ]; then
          local last_model_base=$(basename "$last_runtime_model") # Show just filename
          local last_model_status=""
          if [ -n "$last_runtime_hours" ]; then
             last_model_status="(last ran)"
          else
             last_model_status="(last selected, possibly failed)" # Indicate if runtime wasn't saved
          fi
          echo -e "Last model used: ${CYAN}$last_model_base${NC} $last_model_status"
        fi
      fi
    else
      echo -e "${YELLOW}KoboldCpp is not installed (or key components are missing).${NC}"
    fi
     # Show device info
    local device_info=$(get_device_info)
    if [ -n "$device_info" ]; then
      echo -e "Device: ${BLUE}$device_info${NC}"
    fi
     echo
    echo -e "${BOLD}Main Menu:${NC}"
    echo
    echo "1. Run KoboldCpp (Select model from cache)"
    echo "2. Add model to cache (from Downloads)"
    echo "3. Download model to cache (from URL)"
    echo "4. Manage models in cache" # Renamed for clarity
    echo "5. Install / Update / Rebuild KoboldCpp"
    echo "6. Backup & Restore Models"
    echo "7. Advanced options"
    echo "8. About"
    echo "0. Exit"
    echo
    echo -n "Enter your choice: "
    read -r CHOICE
     case "${CHOICE:-}" in # Handle empty CHOICE
      1) run_model ;;
      2) add_model ;;
      3) download_model ;;
      4) models_menu ;;
      5) install_update_menu ;;
      6) backup_restore_menu ;;
      7) advanced_menu ;;
      8) about ;;
      0) clean_exit ;;
      *)
        error "Invalid choice. Please try again."
        wait_for_keypress_keep_screen
        ;;
    esac
  done
}
 models_menu() {
  while true; do
    clear_screen
    echo -e "${BOLD}${CYAN}=== Model Cache Management ===${NC}"
    echo
    log "Manage your .gguf models stored in the cache directory: $MODEL_DIR"
    echo
     local MODEL_COUNT=0
    if [ -d "$MODEL_DIR" ]; then
        # Use find with null delimiter for safety
        MODEL_COUNT=$(find "$MODEL_DIR" -maxdepth 1 -name "*.gguf" -type f -print0 2>/dev/null | grep -c -z '\.gguf$')
    fi
    echo -e "Models currently in cache: ${YELLOW}${MODEL_COUNT:-0}${NC}"
     echo
    echo -e "${BOLD}Options:${NC}"
    echo
    echo "1. List all models in cache"
    echo "2. Add model to cache (from Downloads)" # Re-added for convenience
    echo "3. Download model to cache (from URL)" # Re-added for convenience
    echo "4. Delete a model from cache"
    echo "5. Verify integrity of a model in cache" # Calls the new interactive function
    echo "0. Back to Main Menu"
    echo
    echo -n "Enter your choice: "
    read -r CHOICE
     case "${CHOICE:-}" in
      1) list_models ;;
      2) add_model ;;
      3) download_model ;;
      4) delete_model ;;
      5) verify_model_integrity_interactive ;; # Call the new function here
      0) return ;;
      *)
        error "Invalid choice. Please try again."
        wait_for_keypress_keep_screen
        ;;
    esac
  done
}
 # --- Script Entry Point ---
main_menu

Comments

  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎