#!/usr/bin/env bash # install-app — install executables (AppImage/ELF/script) to PATH with .desktop integration # Usage: install-app [OPTIONS] set -euo pipefail # ── Colors ──────────────────────────────────────────────────────────────────── RED='\033[0;31m' GRN='\033[0;32m' YLW='\033[1;33m' BLU='\033[0;34m' BLD='\033[1m' RST='\033[0m' info() { echo -e "${BLU}::${RST} $*"; } success() { echo -e "${GRN}✓${RST} $*"; } warn() { echo -e "${YLW}⚠${RST} $*"; } error() { echo -e "${RED}✗${RST} $*" >&2; } die() { error "$*"; exit 1; } # ── Defaults ────────────────────────────────────────────────────────────────── GLOBAL=false ICON="" APP_NAME="" CATEGORY="Utility" UPDATE_FILE="" FORCE=false UNINSTALL=false # ── Help ────────────────────────────────────────────────────────────────────── usage() { cat < ${BLD}OPTIONS${RST} -g, --global Install system-wide (/usr/local/bin, /usr/share/) Requires sudo / root -i, --icon Icon file (png/svg/xpm) -n, --name Display name (default: derived from filename) -c, --category .desktop category (default: Utility) --update Replace binary with new version (keeps .desktop & icon) --uninstall Remove binary, icon and .desktop entry --force Overwrite/remove without confirmation -h, --help Show this help ${BLD}EXAMPLES${RST} install-app ~/Downloads/MyApp.AppImage install-app -g -i ~/icon.png -n "My App" ~/Downloads/MyApp.AppImage install-app --update ~/Downloads/MyApp-v2.AppImage -n "My App" install-app --uninstall -n "My App" sudo install-app -g --uninstall -n "My App" EOF } # ── Argument parsing ────────────────────────────────────────────────────────── POSITIONAL=() while [[ $# -gt 0 ]]; do case "$1" in -g|--global) GLOBAL=true; shift ;; -i|--icon) ICON="$2"; shift 2 ;; -n|--name) APP_NAME="$2"; shift 2 ;; -c|--category) CATEGORY="$2"; shift 2 ;; --update) UPDATE_FILE="$2"; shift 2 ;; --uninstall) UNINSTALL=true; shift ;; --force) FORCE=true; shift ;; -h|--help) usage; exit 0 ;; -*) die "Unknown option: $1. Use -h for help." ;; *) POSITIONAL+=("$1"); shift ;; esac done # ── Resolve executable ──────────────────────────────────────────────────────── if $UNINSTALL; then # uninstall: -n required OR positional arg for name derivation if [[ -z "$APP_NAME" ]]; then if [[ ${#POSITIONAL[@]} -gt 0 ]]; then SRC_FILE="$(realpath "${POSITIONAL[0]}" 2>/dev/null || echo "${POSITIONAL[0]}")" else die "Provide app name via -n or pass the original executable as argument." fi else SRC_FILE="" fi elif [[ -n "$UPDATE_FILE" ]]; then SRC_FILE="$UPDATE_FILE" SRC_FILE="$(realpath "$SRC_FILE")" [[ -f "$SRC_FILE" ]] || die "File not found: $SRC_FILE" else [[ ${#POSITIONAL[@]} -eq 0 ]] && { usage; exit 1; } SRC_FILE="${POSITIONAL[0]}" SRC_FILE="$(realpath "$SRC_FILE")" [[ -f "$SRC_FILE" ]] || die "File not found: $SRC_FILE" fi # ── Derive app name ─────────────────────────────────────────────────────────── if [[ -z "$APP_NAME" ]]; then BASENAME="$(basename "${SRC_FILE:-}")" # Strip known extensions: .AppImage, .appimage, .sh, .py, .bin, .elf APP_NAME="${BASENAME%%.AppImage}" APP_NAME="${APP_NAME%%.appimage}" APP_NAME="${APP_NAME%%.sh}" APP_NAME="${APP_NAME%%.py}" APP_NAME="${APP_NAME%%.bin}" APP_NAME="${APP_NAME%%.elf}" # Strip version suffixes like -x86_64, -1.2.3, _amd64 APP_NAME="$(echo "$APP_NAME" | sed -E 's/[-_][vV]?[0-9]+[._0-9]*[-_]?(x86_64|amd64|arm64|aarch64)?$//')" APP_NAME="$(echo "$APP_NAME" | sed -E 's/[-_](x86_64|amd64|arm64|aarch64)$//')" fi DISPLAY_NAME="$APP_NAME" BIN_NAME="$(echo "$APP_NAME" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')" DESKTOP_NAME="$(echo "$BIN_NAME" | tr '[:upper:]' '[:lower:]')" # ── Paths ───────────────────────────────────────────────────────────────────── if $GLOBAL; then BIN_DIR="/usr/local/bin" ICON_DIR="/usr/share/icons/hicolor/256x256/apps" DESKTOP_DIR="/usr/share/applications" DESKTOP_DB_DIR="/usr/share/applications" else BIN_DIR="$HOME/.local/bin" ICON_DIR="$HOME/.local/share/icons/hicolor/256x256/apps" DESKTOP_DIR="$HOME/.local/share/applications" DESKTOP_DB_DIR="$HOME/.local/share/applications" fi DEST_BIN="$BIN_DIR/$BIN_NAME" DESKTOP_FILE="$DESKTOP_DIR/${DESKTOP_NAME}.desktop" # ── Sudo check ──────────────────────────────────────────────────────────────── if $GLOBAL && [[ $EUID -ne 0 ]]; then die "System-wide install requires root. Run: sudo install-app -g $*" fi # ── UNINSTALL MODE ──────────────────────────────────────────────────────────── if $UNINSTALL; then info "Uninstalling ${BLD}$DISPLAY_NAME${RST}" FOUND=false # Check both scopes USER_BIN="$HOME/.local/bin/$BIN_NAME" SYS_BIN="/usr/local/bin/$BIN_NAME" if $GLOBAL; then BINS_TO_CHECK=("$SYS_BIN") ICON_DIRS_TO_CHECK=("/usr/share/icons/hicolor") DESKTOP_FILES_TO_CHECK=("/usr/share/applications/${DESKTOP_NAME}.desktop") DB_DIR="/usr/share/applications" else BINS_TO_CHECK=("$USER_BIN") ICON_DIRS_TO_CHECK=("$HOME/.local/share/icons/hicolor") DESKTOP_FILES_TO_CHECK=("$HOME/.local/share/applications/${DESKTOP_NAME}.desktop") DB_DIR="$HOME/.local/share/applications" fi # Sudo check for global if $GLOBAL && [[ $EUID -ne 0 ]]; then die "System-wide uninstall requires root. Run: sudo install-app -g --uninstall -n \"$DISPLAY_NAME\"" fi if ! $FORCE; then warn "About to remove:" for b in "${BINS_TO_CHECK[@]}"; do [[ -f "$b" ]] && echo " $b"; done for d in "${DESKTOP_FILES_TO_CHECK[@]}"; do [[ -f "$d" ]] && echo " $d"; done for idir in "${ICON_DIRS_TO_CHECK[@]}"; do find "$idir" -name "${DESKTOP_NAME}.*" 2>/dev/null | while read -r f; do echo " $f"; done done read -rp " Continue? [y/N] " yn [[ "${yn,,}" == "y" ]] || die "Aborted." fi # Remove binary for b in "${BINS_TO_CHECK[@]}"; do if [[ -f "$b" ]]; then rm -f "$b" success "Removed binary: $b" FOUND=true fi done # Remove .desktop for d in "${DESKTOP_FILES_TO_CHECK[@]}"; do if [[ -f "$d" ]]; then rm -f "$d" success "Removed .desktop: $d" FOUND=true fi done # Remove icon(s) for idir in "${ICON_DIRS_TO_CHECK[@]}"; do while IFS= read -r iconfile; do rm -f "$iconfile" success "Removed icon: $iconfile" FOUND=true done < <(find "$idir" -name "${DESKTOP_NAME}.*" 2>/dev/null) done # Update desktop db if command -v update-desktop-database &>/dev/null; then update-desktop-database "$DB_DIR" 2>/dev/null || true fi $FOUND || warn "Nothing found to remove for '$BIN_NAME'" exit 0 fi # ── UPDATE MODE ─────────────────────────────────────────────────────────────── if [[ -n "$UPDATE_FILE" ]]; then info "Update mode for: ${BLD}$DISPLAY_NAME${RST}" # Find installed binary if [[ ! -f "$DEST_BIN" ]]; then # Try the other scope ALT_BIN="" if $GLOBAL; then ALT_BIN="$HOME/.local/bin/$BIN_NAME" else ALT_BIN="/usr/local/bin/$BIN_NAME" fi if [[ -f "$ALT_BIN" ]]; then DEST_BIN="$ALT_BIN" warn "Found binary in alternate location: $DEST_BIN" else die "No installed binary found for '$BIN_NAME'. Install it first." fi fi info "Replacing: $DEST_BIN" cp "$SRC_FILE" "$DEST_BIN" chmod +x "$DEST_BIN" success "Binary updated: $DEST_BIN" exit 0 fi # ── INSTALL MODE ────────────────────────────────────────────────────────────── info "Installing ${BLD}$DISPLAY_NAME${RST} $(if $GLOBAL; then echo '(system-wide)'; else echo "(user: $USER)"; fi)" # Confirm overwrite if [[ -f "$DEST_BIN" ]] && ! $FORCE; then warn "Binary already exists: $DEST_BIN" read -rp " Overwrite? [y/N] " yn [[ "${yn,,}" == "y" ]] || die "Aborted." fi # Create directories mkdir -p "$BIN_DIR" "$ICON_DIR" "$DESKTOP_DIR" # ── Copy binary ─────────────────────────────────────────────────────────────── cp "$SRC_FILE" "$DEST_BIN" chmod +x "$DEST_BIN" success "Binary installed: $DEST_BIN" # ── Handle icon ─────────────────────────────────────────────────────────────── ICON_VALUE="application-x-executable" if [[ -n "$ICON" ]]; then ICON="$(realpath "$ICON")" [[ -f "$ICON" ]] || die "Icon file not found: $ICON" ICON_EXT="${ICON##*.}" DEST_ICON="$ICON_DIR/${DESKTOP_NAME}.${ICON_EXT}" cp "$ICON" "$DEST_ICON" ICON_VALUE="$DEST_ICON" success "Icon installed: $DEST_ICON" else info "No icon provided — using default: application-x-executable" fi # ── Create .desktop file ────────────────────────────────────────────────────── cat > "$DESKTOP_FILE" </dev/null; then update-desktop-database "$DESKTOP_DB_DIR" 2>/dev/null && \ success "Desktop database updated" fi # Update icon cache (non-critical) if command -v gtk-update-icon-cache &>/dev/null; then ICON_THEME_DIR="$(dirname "$(dirname "$(dirname "$ICON_DIR")")")" gtk-update-icon-cache -q "$ICON_THEME_DIR" 2>/dev/null || true fi # ── Summary ─────────────────────────────────────────────────────────────────── echo "" echo -e "${BLD}── Summary ──────────────────────────────${RST}" echo -e " Name : ${BLD}$DISPLAY_NAME${RST}" echo -e " Binary : $DEST_BIN" echo -e " .desktop : $DESKTOP_FILE" echo -e " Icon : $ICON_VALUE" echo -e " Scope : $(if $GLOBAL; then echo 'system-wide'; else echo "user ($USER)"; fi)" echo "" echo -e " Run with : ${BLD}$BIN_NAME${RST}" echo -e "${BLD}─────────────────────────────────────────${RST}"