|
#!/bin/bash |
|
# Generic AppImage Desktop Entry Installer |
|
# Creates desktop entries for any AppImage file |
|
# Usage: ./appimage-desktop-installer.sh /path/to/application.AppImage [AppName] [Category] |
|
|
|
set -e |
|
|
|
# Color codes for output |
|
RED='\033[0;31m' |
|
GREEN='\033[0;32m' |
|
YELLOW='\033[1;33m' |
|
BLUE='\033[0;34m' |
|
NC='\033[0m' # No Color |
|
|
|
# Function to print colored messages |
|
print_info() { echo -e "${GREEN}[INFO]${NC} $1"; } |
|
print_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } |
|
print_error() { echo -e "${RED}[ERROR]${NC} $1"; } |
|
print_debug() { echo -e "${BLUE}[DEBUG]${NC} $1"; } |
|
|
|
# Function to show usage |
|
show_usage() { |
|
cat << EOF |
|
Usage: $0 <AppImage-Path> [AppName] [Category] |
|
|
|
Arguments: |
|
AppImage-Path : Full path to the AppImage file (required) |
|
AppName : Name for the application (optional, auto-detected from filename) |
|
Category : Desktop category (optional, default: Utility) |
|
Common categories: Audio, Video, Development, Education, |
|
Game, Graphics, Network, Office, Science, Settings, System, Utility |
|
|
|
Examples: |
|
$0 ~/Downloads/Obsidian.AppImage |
|
$0 ~/Programs/MyApp.AppImage "My Application" "Development" |
|
$0 ./app.AppImage "Cool App" "Graphics" |
|
|
|
EOF |
|
exit 1 |
|
} |
|
|
|
# Check if help is requested |
|
if [[ "$1" == "-h" ]] || [[ "$1" == "--help" ]] || [[ -z "$1" ]]; then |
|
show_usage |
|
fi |
|
|
|
# Get AppImage path |
|
APPIMAGE_PATH="$1" |
|
|
|
# Validate AppImage exists |
|
if [[ ! -f "$APPIMAGE_PATH" ]]; then |
|
print_error "AppImage file not found: $APPIMAGE_PATH" |
|
exit 1 |
|
fi |
|
|
|
# Get absolute path |
|
APPIMAGE_PATH=$(realpath "$APPIMAGE_PATH") |
|
|
|
# Make sure AppImage is executable |
|
if [[ ! -x "$APPIMAGE_PATH" ]]; then |
|
print_warn "AppImage is not executable, making it executable..." |
|
chmod +x "$APPIMAGE_PATH" |
|
fi |
|
|
|
# Extract filename without extension for default app name |
|
FILENAME=$(basename "$APPIMAGE_PATH" .AppImage) |
|
|
|
# Get app name (use provided or extract from filename) |
|
if [[ -n "$2" ]]; then |
|
APP_NAME="$2" |
|
else |
|
APP_NAME="$FILENAME" |
|
fi |
|
|
|
# Get category (use provided or default to Utility) |
|
if [[ -n "$3" ]]; then |
|
CATEGORY="$3" |
|
else |
|
CATEGORY="Utility" |
|
fi |
|
|
|
# Generate lowercase ID for files |
|
APP_ID=$(echo "$APP_NAME" | tr '[:upper:]' '[:lower:]' | tr ' ' '-') |
|
|
|
# Set up directories |
|
ICON_DIR="$HOME/.local/share/icons" |
|
DESKTOP_DIR="$HOME/.local/share/applications" |
|
TMP_EXTRACT="/tmp/appimage-extract-$$" |
|
|
|
print_info "Starting desktop entry installation for: $APP_NAME" |
|
print_info "AppImage: $APPIMAGE_PATH" |
|
|
|
# Step 1: Test AppImage execution to detect issues |
|
print_info "Step 1: Testing AppImage for compatibility issues..." |
|
|
|
# Known GUI applications (Electron-based and others) |
|
KNOWN_GUI_APPS="obsidian|vscode|code|discord|slack|atom|postman|insomnia|gimp|inkscape|blender|krita|kdenlive|audacity" |
|
|
|
# Test if AppImage needs --no-sandbox flag |
|
TEST_OUTPUT=$(timeout 5 "$APPIMAGE_PATH" --version 2>&1 || true) |
|
NEEDS_NO_SANDBOX=false |
|
NEEDS_TERMINAL=false |
|
|
|
if echo "$TEST_OUTPUT" | grep -qi "SUID sandbox\|sandbox.*not configured\|sandboxing"; then |
|
print_warn " Detected sandbox issue - will use --no-sandbox flag" |
|
NEEDS_NO_SANDBOX=true |
|
fi |
|
|
|
# Check if this is a known GUI app (don't rely on --version output) |
|
APP_NAME_LOWER=$(echo "$APP_NAME" | tr '[:upper:]' '[:lower:]') |
|
if echo "$APP_NAME_LOWER" | grep -qiE "$KNOWN_GUI_APPS"; then |
|
print_debug " Recognized as known GUI application" |
|
NEEDS_TERMINAL=false |
|
else |
|
# Only check CLI detection for unknown apps |
|
if echo "$TEST_OUTPUT" | grep -qiE "^[A-Za-z0-9_-]+:.*version|v[0-9]|[0-9]+\.[0-9]" && ! echo "$TEST_OUTPUT" | grep -qi "error\|fatal\|sandbox"; then |
|
print_debug " App appears to be CLI-based" |
|
NEEDS_TERMINAL=true |
|
fi |
|
fi |
|
|
|
# Step 2: Extract AppImage to find icon |
|
print_info "Step 2: Extracting AppImage to locate icon..." |
|
mkdir -p "$TMP_EXTRACT" |
|
cd "$TMP_EXTRACT" |
|
|
|
if ! "$APPIMAGE_PATH" --appimage-extract > /dev/null 2>&1; then |
|
print_error "Failed to extract AppImage. Make sure it's a valid AppImage file." |
|
rm -rf "$TMP_EXTRACT" |
|
exit 1 |
|
fi |
|
|
|
print_info " ✓ AppImage extracted successfully" |
|
|
|
# Step 3: Detect Terminal requirement from desktop file |
|
print_info "Step 3: Analyzing application metadata..." |
|
|
|
# Check if there's an existing desktop file |
|
EXISTING_DESKTOP=$(find squashfs-root -name "*.desktop" | head -1) |
|
if [[ -n "$EXISTING_DESKTOP" ]]; then |
|
print_debug " Found existing desktop file: $EXISTING_DESKTOP" |
|
|
|
# Check Terminal setting - THIS HAS PRIORITY! |
|
if grep -q "^Terminal=true" "$EXISTING_DESKTOP"; then |
|
NEEDS_TERMINAL=true |
|
print_debug " Desktop file indicates Terminal=true (overriding detection)" |
|
elif grep -q "^Terminal=false" "$EXISTING_DESKTOP"; then |
|
NEEDS_TERMINAL=false |
|
print_debug " Desktop file indicates Terminal=false (overriding detection)" |
|
fi |
|
|
|
# Extract comment if available |
|
COMMENT=$(grep "^Comment=" "$EXISTING_DESKTOP" | cut -d'=' -f2- | head -1) |
|
if [[ -z "$COMMENT" ]]; then |
|
COMMENT="$APP_NAME - Installed via AppImage" |
|
fi |
|
|
|
# Extract categories if available |
|
EXISTING_CATEGORIES=$(grep "^Categories=" "$EXISTING_DESKTOP" | cut -d'=' -f2- | head -1) |
|
if [[ -n "$EXISTING_CATEGORIES" ]] && [[ "$CATEGORY" == "Utility" ]]; then |
|
CATEGORY="$EXISTING_CATEGORIES" |
|
print_debug " Using categories from desktop file: $CATEGORY" |
|
fi |
|
else |
|
COMMENT="$APP_NAME - Installed via AppImage" |
|
fi |
|
|
|
# Step 4: Find the best icon |
|
print_info "Step 4: Searching for application icon..." |
|
|
|
# Search for icons in common locations |
|
ICON_FILE="" |
|
for size in 512 256 128 scalable; do |
|
if [[ "$size" == "scalable" ]]; then |
|
FOUND_ICON=$(find squashfs-root -name "*.svg" -o -name "*.png" | grep -iE "(icon|logo)" | head -1) |
|
else |
|
FOUND_ICON=$(find squashfs-root -path "*/${size}x${size}/*" -name "*.png" | head -1) |
|
fi |
|
|
|
if [[ -n "$FOUND_ICON" ]]; then |
|
ICON_FILE="$FOUND_ICON" |
|
break |
|
fi |
|
done |
|
|
|
# Fallback: search for any PNG or SVG in common locations |
|
if [[ -z "$ICON_FILE" ]]; then |
|
ICON_FILE=$(find squashfs-root -name "*.png" -o -name "*.svg" | grep -iE "(icon|logo)" | head -1) |
|
fi |
|
|
|
# Last resort: any PNG file |
|
if [[ -z "$ICON_FILE" ]]; then |
|
ICON_FILE=$(find squashfs-root -name "*.png" | head -1) |
|
fi |
|
|
|
if [[ -z "$ICON_FILE" ]]; then |
|
print_warn "No icon found, desktop entry will be created without icon" |
|
ICON_PATH="" |
|
else |
|
ICON_EXT="${ICON_FILE##*.}" |
|
ICON_PATH="$ICON_DIR/${APP_ID}.${ICON_EXT}" |
|
|
|
mkdir -p "$ICON_DIR" |
|
cp "$ICON_FILE" "$ICON_PATH" |
|
print_info " ✓ Icon copied to $ICON_PATH" |
|
fi |
|
|
|
# Step 5: Find executable name for StartupWMClass |
|
EXEC_NAME=$(basename "$(find squashfs-root -type f -executable | grep -v '\.so' | head -1)" 2>/dev/null || echo "$APP_ID") |
|
print_info " ✓ Detected executable name: $EXEC_NAME" |
|
|
|
# Step 6: Determine final Exec command |
|
if [[ "$NEEDS_NO_SANDBOX" == true ]]; then |
|
EXEC_CMD="$APPIMAGE_PATH --no-sandbox %U" |
|
print_info " ✓ Using --no-sandbox flag for compatibility" |
|
else |
|
EXEC_CMD="$APPIMAGE_PATH %U" |
|
fi |
|
|
|
# Step 7: Create desktop entry file |
|
print_info "Step 5: Creating desktop entry file..." |
|
mkdir -p "$DESKTOP_DIR" |
|
|
|
DESKTOP_FILE="$DESKTOP_DIR/${APP_ID}.desktop" |
|
|
|
# Determine Terminal value |
|
if [[ "$NEEDS_TERMINAL" == true ]]; then |
|
TERMINAL_VALUE="true" |
|
print_info " ✓ Setting Terminal=true (CLI application detected)" |
|
else |
|
TERMINAL_VALUE="false" |
|
print_info " ✓ Setting Terminal=false (GUI application detected)" |
|
fi |
|
|
|
cat > "$DESKTOP_FILE" << EOF |
|
[Desktop Entry] |
|
Type=Application |
|
Name=$APP_NAME |
|
Comment=$COMMENT |
|
Exec=$EXEC_CMD |
|
Icon=$ICON_PATH |
|
Terminal=$TERMINAL_VALUE |
|
Categories=$CATEGORY; |
|
StartupWMClass=$EXEC_NAME |
|
X-AppImage-Version=1.0 |
|
X-AppImage-Path=$APPIMAGE_PATH |
|
EOF |
|
|
|
print_info " ✓ Desktop file created at $DESKTOP_FILE" |
|
|
|
# Step 8: Make desktop file executable |
|
print_info "Step 6: Setting permissions..." |
|
chmod +x "$DESKTOP_FILE" |
|
print_info " ✓ Desktop file made executable" |
|
|
|
# Step 9: Update desktop database |
|
print_info "Step 7: Updating desktop database..." |
|
if command -v update-desktop-database &> /dev/null; then |
|
update-desktop-database "$DESKTOP_DIR" 2>/dev/null || true |
|
print_info " ✓ Desktop database updated" |
|
else |
|
print_warn " update-desktop-database not available, changes may require logout" |
|
fi |
|
|
|
# Cleanup |
|
print_info "Step 8: Cleaning up temporary files..." |
|
cd "$HOME" |
|
rm -rf "$TMP_EXTRACT" |
|
print_info " ✓ Cleanup completed" |
|
|
|
echo "" |
|
print_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" |
|
print_info "✓ Installation completed successfully!" |
|
print_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" |
|
echo "" |
|
echo "Application: $APP_NAME" |
|
echo "Desktop File: $DESKTOP_FILE" |
|
echo "Icon: ${ICON_PATH:-Not available}" |
|
echo "Terminal: $TERMINAL_VALUE" |
|
if [[ "$NEEDS_NO_SANDBOX" == true ]]; then |
|
echo "Sandbox: Disabled (--no-sandbox)" |
|
fi |
|
echo "" |
|
print_info "$APP_NAME should now appear in your application menu." |
|
print_warn "If it doesn't appear immediately, try logging out and back in." |
|
echo "" |
|
|
|
# Final validation |
|
if [[ "$NEEDS_NO_SANDBOX" == true ]]; then |
|
print_warn "Note: This AppImage requires --no-sandbox flag due to sandbox configuration issues." |
|
print_warn "This is common with Electron-based apps (like Obsidian, VSCode, etc.)" |
|
fi |