#!/usr/bin/env bash set -euo pipefail APP=gvim REPO="ryangerardwilson/gvim" APP_HOME="$HOME/.${APP}" INSTALL_DIR="$APP_HOME/bin" APP_DIR="$APP_HOME/app" VENV_DIR="$APP_HOME/venv" FILENAME="${APP}-linux-x64.tar.gz" MUTED='\033[0;2m' RED='\033[0;31m' NC='\033[0m' usage() { cat <] Install a specific release (e.g., 0.1.0 or v0.1.0) Without an argument, print the latest release version and exit -u Reinstall the latest release if it is newer (upgrade) -b, --binary Install from a local binary bundle EOF } info() { echo -e "${MUTED}$1${NC}"; } die() { echo -e "${RED}$1${NC}" >&2; exit 1; } require_sudo() { if [[ $EUID -ne 0 ]]; then command -v sudo >/dev/null 2>&1 || die "sudo is required to install system packages" fi } install_system_deps() { [[ -f /etc/os-release ]] || die "Unsupported OS: missing /etc/os-release" . /etc/os-release case "${ID}" in ubuntu|debian) require_sudo sudo apt-get update sudo apt-get install -y \ python3 \ python3-venv \ python3-gi \ gir1.2-gtk-4.0 \ libgirepository1.0-dev \ gcc \ pkg-config \ libcairo2-dev ;; fedora) require_sudo sudo dnf install -y \ python3 \ python3-gobject \ gtk4 \ gobject-introspection-devel \ gcc \ pkgconf-pkg-config \ cairo-gobject-devel ;; arch) require_sudo sudo pacman -S --noconfirm \ python \ python-gobject \ gtk4 \ gobject-introspection \ gcc \ pkgconf \ cairo ;; *) die "Unsupported distro for system deps: ${ID}" ;; esac } venv_ok() { [[ -x "$VENV_DIR/bin/python" ]] || return 1 "$VENV_DIR/bin/python" -c "import gi; gi.require_version('Gtk','4.0')" >/dev/null 2>&1 || return 1 "$VENV_DIR/bin/python" -c "import numpy, matplotlib, pandas" >/dev/null 2>&1 || return 1 return 0 } system_deps_ok() { command -v python3 >/dev/null 2>&1 || return 1 python3 -c "import gi; gi.require_version('Gtk','4.0')" >/dev/null 2>&1 || return 1 return 0 } setup_venv() { command -v python3 >/dev/null 2>&1 || die "python3 is required" if venv_ok; then info "Using existing venv at $VENV_DIR" return fi rm -rf "$VENV_DIR" python3 -m venv "$VENV_DIR" "$VENV_DIR/bin/python" -m pip install --upgrade pip setuptools wheel "$VENV_DIR/bin/pip" install -r "$APP_DIR/$APP/requirements.txt" "$VENV_DIR/bin/python" -c "import gi; gi.require_version('Gtk','4.0')" "$VENV_DIR/bin/python" -c "import numpy, matplotlib, pandas" } requested_version=${VERSION:-} bundle_path="" show_latest=false upgrade=false while [[ $# -gt 0 ]]; do case "$1" in -h|--help) usage exit 0 ;; -v) if [[ -n "${2:-}" && "${2:0:1}" != "-" ]]; then requested_version="$2" shift 2 else show_latest=true shift fi ;; -u|--upgrade) upgrade=true shift ;; -b|--binary) [[ -n "${2:-}" ]] || die "--binary requires a path" bundle_path="$2" shift 2 ;; --version) info "--version is deprecated. Use -v instead." if [[ -n "${2:-}" && "${2:0:1}" != "-" ]]; then requested_version="$2" shift 2 else show_latest=true shift fi ;; --upgrade) info "--upgrade is deprecated. Use -u instead." upgrade=true shift ;; --help) info "--help is deprecated. Use -h instead." usage exit 0 ;; *) info "Unknown option $1" shift ;; esac done _latest_version="" get_latest_version() { if [[ -z "${_latest_version}" ]]; then _latest_version=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" \ | sed -n 's/.*"tag_name": *"v\{0,1\}\([^"\\n]*\)".*/\1/p') [[ -n "${_latest_version}" ]] || die "Unable to determine latest release" fi printf '%s\n' "${_latest_version}" } if $show_latest; then [[ "$upgrade" == false && -z "$bundle_path" && -z "$requested_version" ]] || \ die "-v (no arg) cannot be combined with other options" get_latest_version exit 0 fi if $upgrade; then [[ -z "$bundle_path" ]] || die "-u cannot be used with -b/--binary" [[ -z "$requested_version" ]] || die "-u cannot be combined with -v" latest=$(get_latest_version) if command -v "$APP" >/dev/null 2>&1; then installed=$($APP --version 2>/dev/null || true) installed="${installed#v}" if [[ -n "$installed" && "$installed" == "$latest" ]]; then info "${APP} ${latest} already installed" exit 0 fi fi requested_version="$latest" fi mkdir -p "$INSTALL_DIR" rm -rf "$APP_DIR" rm -f "$INSTALL_DIR/$APP" if [[ -n "$bundle_path" ]]; then [[ -f "$bundle_path" ]] || die "Bundle not found: $bundle_path" info "Installing ${APP^^} from local bundle" tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/${APP}.XXXXXX") tar -xzf "$bundle_path" -C "$tmp_dir" [[ -f "$tmp_dir/${APP}/main.py" ]] || die "Archive missing ${APP}/main.py" mkdir -p "$APP_DIR" mv "$tmp_dir/${APP}" "$APP_DIR" rm -rf "$tmp_dir" installed_label="local" else raw_os=$(uname -s) arch=$(uname -m) [[ "$raw_os" == "Linux" ]] || die "Unsupported OS: $raw_os" [[ "$arch" == "x86_64" ]] || die "Unsupported arch: $arch" command -v curl >/dev/null 2>&1 || die "'curl' is required" command -v tar >/dev/null 2>&1 || die "'tar' is required" if [[ -z "$requested_version" ]]; then version_label=$(get_latest_version) url="https://github.com/${REPO}/releases/latest/download/${FILENAME}" else requested_version="${requested_version#v}" version_label="$requested_version" url="https://github.com/${REPO}/releases/download/v${requested_version}/${FILENAME}" http_status=$(curl -sI -o /dev/null -w "%{http_code}" \ "https://github.com/${REPO}/releases/tag/v${requested_version}") [[ "$http_status" != "404" ]] || die "Release v${requested_version} not found" fi if command -v "$APP" >/dev/null 2>&1 && [[ "$version_label" != "latest" ]]; then installed=$($APP --version 2>/dev/null || true) installed="${installed#v}" if [[ -n "$installed" && "$installed" == "$version_label" ]]; then info "${APP} ${version_label} already installed" exit 0 fi fi info "Installing ${APP^^} version ${version_label}" tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/${APP}.XXXXXX") curl -# -L -o "$tmp_dir/$FILENAME" "$url" tar -xzf "$tmp_dir/$FILENAME" -C "$tmp_dir" [[ -f "$tmp_dir/${APP}/main.py" ]] || die "Archive missing ${APP}/main.py" mkdir -p "$APP_DIR" mv "$tmp_dir/${APP}" "$APP_DIR" rm -rf "$tmp_dir" installed_label="$version_label" fi if system_deps_ok; then info "System deps already present; skipping system install" else install_system_deps fi setup_venv cat > "$INSTALL_DIR/$APP" <<'EOF' #!/usr/bin/env bash set -euo pipefail APP=gvim PYTHON_BIN="${HOME}/.${APP}/venv/bin/python" if [[ ! -x "$PYTHON_BIN" ]]; then if command -v python3 >/dev/null 2>&1; then PYTHON_BIN="python3" else echo "GVIM venv missing. Reinstall with install.sh." >&2 exit 1 fi fi case "${1:-}" in init|-v|--version|-h|--help|-u|--upgrade) exec "$PYTHON_BIN" "${HOME}/.${APP}/app/${APP}/main.py" "$@" ;; esac nohup "$PYTHON_BIN" "${HOME}/.${APP}/app/${APP}/main.py" "$@" >/dev/null 2>&1 & disown EOF chmod 755 "$INSTALL_DIR/$APP" maybe_add_path() { local command=$1 local rc_files=() local shell_name=$(basename "${SHELL:-bash}") case "$shell_name" in zsh) rc_files=("$HOME/.zshrc" "$HOME/.zshenv" "$HOME/.config/zsh/.zshrc") ;; bash) rc_files=("$HOME/.bashrc" "$HOME/.bash_profile" "$HOME/.profile") ;; fish) rc_files=("$HOME/.config/fish/config.fish") ;; *) rc_files=("$HOME/.profile") ;; esac for rc in "${rc_files[@]}"; do [[ -w "$rc" ]] || continue if grep -Fq "$command" "$rc" 2>/dev/null; then return fi printf '\n# %s\n%s\n' "${APP^^}" "$command" >> "$rc" info "Added ${APP^^} to PATH in $rc" return done info "Add to PATH manually: $command" } if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then if [[ $(basename "${SHELL:-bash}") == "fish" ]]; then maybe_add_path "fish_add_path $INSTALL_DIR" else maybe_add_path "export PATH=$INSTALL_DIR:\$PATH" fi fi info "Installed ${APP^^} (${installed_label:-unknown}) to $INSTALL_DIR/$APP" info "Run: ${APP} -h" if [[ -n "${BASH_VERSION:-}" ]]; then completion_dir="${XDG_CONFIG_HOME:-$HOME/.config}/bash_completion.d" mkdir -p "$completion_dir" if [[ -f "${HOME}/.${APP}/app/${APP}/completions_gvim.bash" ]]; then cp "${HOME}/.${APP}/app/${APP}/completions_gvim.bash" "$completion_dir/gvim" info "Installed bash completion to $completion_dir/gvim" fi if [[ -w "$HOME/.bashrc" ]]; then if ! grep -Fq "bash_completion.d/gvim" "$HOME/.bashrc" 2>/dev/null; then printf '\n# GVIM bash completion\nif [ -r "%s/gvim" ]; then\n . "%s/gvim"\nfi\n' \ "$completion_dir" "$completion_dir" >> "$HOME/.bashrc" info "Added gvim completion source to ~/.bashrc" fi fi fi