#!/usr/bin/env bash
# ==============================================================
# appinstall - Run an AppImage from the current directory
# --------------------------------------------------------------
# Author : Kris Yotam
# Contact : krisyotam@protonmail.com
# License : GNU GPLv3
# Created : 2025-10-04
# --------------------------------------------------------------
# Description:
# - Finds *.AppImage files in a directory (current by default),
# makes them executable, and prompts the user to select one
# using fzf. If fzf is not available, the script launches the
# first found AppImage.
#
# - This file is intended to be installed in a user's bin
# directory (e.g. ~/.local/bin/appinstall) and used as a
# small helper to run AppImage files.
#
# Usage:
# appinstall [DIRECTORY]
# appinstall --help
#
# Notes:
# - Uses bash-specific behavior (nullglob). Keeps behavior
# simple and robust against directories with no AppImage files.
# ==============================================================
set -euo pipefail
# -------------------- Configuration (like config.py header) -----------------
# Default directory to scan for AppImage files. Can be overridden by
# passing a directory path as the first argument to the script.
DEFAULT_DIR="."
# fzf prompt text
FZF_PROMPT="Select AppImage: "
# ---------------------------------------------------------------------------
print_help() {
cat <<'HELP'
appinstall - Run an AppImage from the current directory
Usage:
appinstall [DIRECTORY]
appinstall --help
If DIRECTORY is omitted the current working directory is used. The
script will make all found *.AppImage files executable before offering a
selection (via fzf when available). If fzf is not installed the script
will run the first AppImage found.
HELP
}
# Make an array of AppImage files in the provided directory.
# Uses bash nullglob so that when there are no matches the array is empty.
find_appimages() {
local dir="$1"
# enable nullglob so the pattern expands to zero elements if no match
shopt -s nullglob
local -a arr=("$dir"/*.AppImage)
shopt -u nullglob
printf '%s\n' "${arr[@]}"
}
main() {
# Handle help request
if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
print_help
return 0
fi
local dir="${1:-$DEFAULT_DIR}"
# Collect AppImage files (full paths)
IFS=$'\n' read -r -d '' -a apps < <(find_appimages "$dir" | sed '/^$/d' && printf '\0') || true
if [ ${#apps[@]} -eq 0 ]; then
printf "No AppImage files found in %s\n" "$dir" >&2
return 1
fi
# Make each AppImage executable (best-effort)
for f in "${apps[@]}"; do
chmod +x -- "$f" 2>/dev/null || true
done
# If fzf is available, let the user choose; otherwise pick the first
if command -v fzf >/dev/null 2>&1; then
# Present only basenames to the user and map selection back to the full path
local sel_basename
sel_basename=$(printf '%s\n' "${apps[@]##*/}" | fzf --prompt="$FZF_PROMPT") || sel_basename=''
if [ -z "$sel_basename" ]; then
# no selection (user pressed ESC or similar)
printf "No selection made. Exiting.\n"
return 0
fi
local selected_path
# If the user supplied a directory path, selected basename may or may not include the dir
if [[ "$dir" == "." ]]; then
selected_path="$sel_basename"
else
selected_path="$dir/$sel_basename"
fi
# Run the selected AppImage (replace this shell with the AppImage)
exec -- "$selected_path"
else
# fzf not installed - run the first found AppImage
printf "fzf not found; launching first AppImage: %s\n" "${apps[0]}"
exec -- "${apps[0]}"
fi
}
# If the script is sourced, don't run main automatically. When executed,
# call main with positional arguments.
if [ "${BASH_SOURCE[0]:-}" = "$0" ]; then
main "$@"
fi