#!/usr/bin/bash set -euo pipefail # Constants REPO="vicinaehq/vicinae" BINARY_NAME="vicinae" TEMP_DIR="/tmp" SYSTEMD_SERVICE_NAME="vicinae.service" # Installation prefix - can be overridden via environment or --prefix flag PREFIX="${PREFIX:-/usr/local}" # systemd searches a fixed set of user unit directories. "$PREFIX/lib/systemd/user" # is one of them for /usr and /usr/local, but not for a prefix under $HOME, where # the XDG data directory is the one it looks at. resolve_systemd_user_dir() { if [[ "$PREFIX" == "$HOME" || "$PREFIX" == "$HOME"/* ]]; then echo "${XDG_DATA_HOME:-$HOME/.local/share}/systemd/user" else echo "$PREFIX/lib/systemd/user" fi } # Derived paths based on PREFIX INSTALL_DIR="$PREFIX/lib/vicinae" BIN_DIR="$PREFIX/bin" THEMES_DIR="$PREFIX/share/vicinae/themes" APPLICATIONS_DIR="$PREFIX/share/applications" SYSTEMD_USER_DIR="$(resolve_systemd_user_dir)" VICINAE_SCRIPT_PATH="$TEMP_DIR/vicinae-install-script.sh" SCRIPT_DOWNLOAD_URL="https://vicinae.com/install" DOCS_URL="https://docs.vicinae.com/install/script" TEMP_FILES=() PRESERVE_FILES=() WARNINGS=() warn() { echo -e " \033[1;33m⚠ WARNING:\033[0m $1" >&2 WARNINGS+=("$1") } ok() { echo -e " \033[0;32m✓\033[0m $1" >&2; } cleanup() { rm -f $VICINAE_SCRIPT_PATH for file in "${TEMP_FILES[@]}"; do if [[ -e "$file" ]]; then rm -rf "$file" fi done # PRESERVE_FILES are kept for future runs and only cleaned up after successful installation } trap cleanup EXIT ERR renderIcon() { cols=$(tput cols) icon=$( cat <<'EOF' ............ ..... .......... ... ............. ... ................. ... ................... .. .................... .. .................... .. .................... ... ................... .. ................... ... ................. ... ............. .... .......... ............. EOF ) tagline="vicinae - A focused launcher for your desktop — native, fast, extensible" while IFS= read -r line; do line_len=${#line} padding=$(((cols - line_len) / 2)) printf "%*s%s\n" "$padding" "" "$line" done <<<"$icon" echo tagline_len=${#tagline} padding=$(((cols - tagline_len) / 2)) printf "%*s%s\n\n" "$padding" "" "$tagline" } check_dependencies() { echo "Checking dependencies..." if ! command -v curl >/dev/null 2>&1; then echo "Error: curl is required but not installed." echo "Please install curl and try again." exit 1 fi if ! command -v jq >/dev/null 2>&1; then echo "Error: jq is required but not installed." echo "Please install jq and try again." exit 1 fi ok "All dependencies found" echo } check_permissions() { echo "Checking installation permissions..." # Check if we can write to the target directories local test_dir="$BIN_DIR" if [[ ! -w "$test_dir" && ! -w "$(dirname "$test_dir")" ]]; then echo "" echo "Warning: Installation to $PREFIX requires elevated permissions." echo "" if [[ $EUID -eq 0 ]]; then ok "Running as root" else # Check for available privilege escalation tools local escalation_cmd="" local escalation_name="" if command -v doas >/dev/null 2>&1; then escalation_cmd="doas" escalation_name="doas" elif command -v sudo >/dev/null 2>&1; then escalation_cmd="sudo -E" escalation_name="sudo" else echo "Error: Neither doas nor sudo is available." echo "Please install doas or sudo, or use --prefix ~/.local for a user installation." exit 1 fi echo "This script will need root privileges in order to install to $PREFIX" echo "" echo "You have the following options:" echo " 1. Re-run this script with $escalation_name: we will prompt you for your password (recommended)" echo " 2. Install to a custom, local directory: full documentation available at ${DOCS_URL}" echo "" read -p "Would you like to continue with $escalation_name? [y/N] " -n 1 -r < /dev/tty echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then echo "Installation cancelled." exit 0 fi echo "Preparing script to execute with elevated privileges..." self_download # Re-execute with privilege escalation echo "Re-executing with $escalation_name..." exec $escalation_cmd "$VICINAE_SCRIPT_PATH" "${ORIGINAL_ARGS[@]}" fi else ok "Have write permissions to $PREFIX" echo fi } get_latest_release_info() { echo "Fetching latest release info from GitHub..." >&2 local api_url="https://api.github.com/repos/$REPO/releases/latest" local response if [[ -n "${GITHUB_TOKEN:-}" ]]; then response=$(curl -s --header "Authorization: Bearer $GITHUB_TOKEN" "$api_url") else response=$(curl -s "$api_url") fi local tag_name tag_name=$(echo "$response" | jq -r '.tag_name') if [[ "$tag_name" == "null" || -z "$tag_name" ]]; then echo "Error: Failed to fetch latest version from GitHub API" >&2 exit 1 fi local arch case "$(uname -m)" in x86_64 | amd64) arch="x86_64" ;; aarch64 | arm64) arch="aarch64" ;; *) echo "Error: unsupported architecture: $(uname -m)" >&2 exit 1 ;; esac local appimage_name appimage_name=$(echo "$response" | jq -r --arg arch "$arch" '.assets[] | select(.name | endswith(".AppImage")) | select(.name | contains($arch)) | .name' | head -1) if [[ "$appimage_name" == "null" || -z "$appimage_name" ]]; then echo "Error: Failed to find $arch AppImage asset in latest release" >&2 exit 1 fi ok "Latest version: $tag_name" ok "AppImage asset: $appimage_name" echo "$tag_name|$appimage_name" echo } get_installed_version() { if [[ ! -f "$BIN_DIR/$BINARY_NAME" ]]; then echo "none" return fi local version_output if version_output=$("$BIN_DIR/$BINARY_NAME" version 2>/dev/null); then local version version=$(echo "$version_output" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) if [[ -n "$version" ]]; then echo "v$version" else echo "unknown" fi else echo "unknown" fi } compare_versions() { local installed="$1" local latest="$2" if [[ "$installed" == "none" || "$installed" == "unknown" ]]; then return 0 # Need to install fi local installed_clean="${installed#v}" local latest_clean="${latest#v}" if [[ "$installed_clean" != "$latest_clean" ]]; then return 0 # Need to update fi return 1 # Already up to date } download_appimage() { local version="$1" local appimage_name="$2" local download_path="$TEMP_DIR/$appimage_name" local download_url="https://github.com/$REPO/releases/download/$version/$appimage_name" if [[ -f "$download_path" && -x "$download_path" ]]; then ok "Using cached AppImage: $download_path" PRESERVE_FILES+=("$download_path") echo "$download_path" return 0 fi echo "Downloading Vicinae $version..." >&2 echo "Asset: $appimage_name" >&2 echo "URL: $download_url" >&2 if curl -L --progress-bar "$download_url" -o "$download_path"; then chmod +x "$download_path" PRESERVE_FILES+=("$download_path") # Keep for future runs ok "Download completed: $download_path" echo "$download_path" else echo "Error: Failed to download AppImage" >&2 exit 1 fi } extract_appimage() { local appimage_path="$1" local keep_appimage="${2:-}" local extract_dir="$INSTALL_DIR.extract" local extract_logs="$TEMP_DIR/appimage-extract.log" echo "Extracting AppImage..." >&2 rm -rf "$extract_dir" mkdir -p "$extract_dir" cd "$extract_dir" echo "This may take a moment..." >&2 "$appimage_path" --appimage-extract > $extract_logs 2>&1 local extract_status=$? if [[ $extract_status -eq 0 ]]; then if [[ -d "squashfs-root" ]]; then mv squashfs-root/* squashfs-root/.[!.]* . 2>/dev/null || true rmdir squashfs-root 2>/dev/null || rm -rf squashfs-root ok "Extraction completed" echo "$extract_dir" rm $extract_logs if [[ -z "$keep_appimage" ]]; then rm "$appimage_path" fi else echo "Error: squashfs-root directory not found after extraction" >&2 ls -la >&2 exit 1 fi else echo "Error: Failed to extract AppImage (exit code: $extract_status)" >&2 echo "Note: Make sure you have a compatible AppImage runtime installed" >&2 exit 1 fi } install_themes() { echo "Installing themes..." >&2 local themes_source="$INSTALL_DIR/usr/share/vicinae/themes" if [[ -d "$themes_source" ]]; then mkdir -p "$THEMES_DIR" cp -r "$themes_source"/* "$THEMES_DIR/" 2>/dev/null || true if [[ -n "$(ls -A "$THEMES_DIR" 2>/dev/null)" ]]; then ok "Themes installed to $THEMES_DIR" echo else echo "Note: No themes found in $themes_source" >&2 fi else echo "Note: Themes directory not found at $themes_source" >&2 fi } install_desktop_files() { echo "Installing desktop application files..." >&2 local applications_source="$INSTALL_DIR/usr/share/applications" if [[ -d "$applications_source" ]]; then mkdir -p "$APPLICATIONS_DIR" # Copy only vicinae-related desktop files if ls "$applications_source"/vicinae*.desktop >/dev/null 2>&1; then cp "$applications_source"/vicinae*.desktop "$APPLICATIONS_DIR/" 2>/dev/null || true ok "Desktop files installed to $APPLICATIONS_DIR" # Update desktop database if available if command -v update-desktop-database >/dev/null 2>&1; then update-desktop-database "$APPLICATIONS_DIR" 2>/dev/null || true ok "Desktop database updated" echo fi else echo "Note: No vicinae desktop files found in $applications_source" >&2 fi else echo "Note: Applications directory not found at $applications_source" >&2 fi } install_icons() { echo "Installing application icons..." >&2 local icon_source="$INSTALL_DIR/usr/share/icons/hicolor/512x512/apps/vicinae.png" local icon_dest_dir="$PREFIX/share/icons/hicolor/512x512/apps" if [[ -f "$icon_source" ]]; then mkdir -p "$icon_dest_dir" cp "$icon_source" "$icon_dest_dir/" if command -v gtk-update-icon-cache >/dev/null 2>&1; then gtk-update-icon-cache -f -t "$PREFIX/share/icons/hicolor" 2>/dev/null || true fi ok "Application icon installed to $icon_dest_dir/vicinae.png" echo else warn "Application icon not found at $icon_source" fi } install_input_server_capabilities() { echo "Setting input server capabilities (for keyboard monitoring and injection)..." >&2 if [[ $EUID -ne 0 ]]; then warn "Skipping input server capabilities (not root)" warn " Snippet expansion and paste require evdev/uinput access. Run: sudo setcap cap_dac_override=ep $INSTALL_DIR/usr/libexec/vicinae/vicinae-input-server" return fi local input_bin="$INSTALL_DIR/usr/libexec/vicinae/vicinae-input-server" if [[ -f "$input_bin" ]]; then if command -v setcap >/dev/null 2>&1; then setcap "cap_dac_override=ep" "$input_bin" ok "Input server capabilities set (cap_dac_override)" else warn "setcap not found — snippet expansion and paste will not work without manual permission setup" fi else echo "Note: Input server binary not found at $input_bin" >&2 fi } install_modules_load() { echo "Installing modules-load config (for uinput kernel module)..." >&2 if [[ $EUID -ne 0 ]]; then warn "Skipping modules-load config installation (not root)" return fi local modules_source="$INSTALL_DIR/usr/lib/modules-load.d" local modules_dest="$PREFIX/lib/modules-load.d" if [[ -d "$modules_source" ]]; then mkdir -p "$modules_dest" cp "$modules_source"/* "$modules_dest/" 2>/dev/null || true ok "modules-load config installed to $modules_dest" if ! lsmod | grep -q "^uinput"; then modprobe uinput 2>/dev/null || true ok "uinput module loaded" fi fi } install_systemd_service() { echo "Installing systemd user service..." >&2 local service_source="$INSTALL_DIR/usr/lib/systemd/user/$SYSTEMD_SERVICE_NAME" if [[ -f "$service_source" ]]; then mkdir -p "$SYSTEMD_USER_DIR" # Copy the service file and replace vicinae with absolute path local service_dest="$SYSTEMD_USER_DIR/$SYSTEMD_SERVICE_NAME" cp "$service_source" "$service_dest" # Replace 'vicinae' with absolute path in ExecStart sed -i "s|ExecStart=vicinae|ExecStart=$BIN_DIR/$BINARY_NAME|g" "$service_dest" ok "Systemd service installed to $service_dest" # Reload systemd user daemon if available if command -v systemctl >/dev/null 2>&1; then systemctl --user daemon-reload 2>/dev/null || true ok "Systemd user daemon reloaded" echo fi else echo "Note: Systemd service file not found at $service_source" >&2 fi } install_vicinae() { local extract_dir="$1" echo "Installing Vicinae..." >&2 mkdir -p "$BIN_DIR" local binary_path="$extract_dir/usr/bin/$BINARY_NAME" if [[ ! -f "$binary_path" ]]; then binary_path="$extract_dir/AppRun" fi if [[ -f "$binary_path" ]]; then rm -rf "$INSTALL_DIR" mv "$extract_dir" "$INSTALL_DIR" binary_path="${binary_path/$extract_dir/$INSTALL_DIR}" ln -sf "$binary_path" "$BIN_DIR/$BINARY_NAME" # Older releases bundle node in the AppImage; keep symlinking it so this # script can still install them local node_path="$INSTALL_DIR/usr/bin/node" if [[ -f "$node_path" ]]; then ln -sf "$node_path" "$BIN_DIR/vicinae-node" ok "Node.js binary symlinked to $BIN_DIR/vicinae-node" fi # After successful installation, mark the AppImage for cleanup # (we pass the appimage_path as a second argument now) if [[ -n "${2:-}" ]]; then TEMP_FILES+=("$2") fi ok "Installation completed" echo # Install themes, desktop files, icons, systemd service, and system configs install_themes install_desktop_files install_icons install_systemd_service install_modules_load install_input_server_capabilities else echo "Error: Vicinae binary not found in extracted files" >&2 echo "Looking in: $extract_dir" >&2 ls -la "$extract_dir/" 2>&1 | head -10 >&2 exit 1 fi } uninstall_vicinae() { echo "Uninstalling Vicinae..." if [[ -d "$INSTALL_DIR" ]]; then rm -rf "$INSTALL_DIR" ok "Removed installation directory: $INSTALL_DIR" else echo "No installation directory found" fi if [[ -L "$BIN_DIR/$BINARY_NAME" ]]; then rm -f "$BIN_DIR/$BINARY_NAME" ok "Removed binary symlink: $BIN_DIR/$BINARY_NAME" else echo "No binary symlink found" fi if [[ -L "$BIN_DIR/vicinae-node" ]]; then rm -f "$BIN_DIR/vicinae-node" ok "Removed Node.js symlink: $BIN_DIR/vicinae-node" fi if [[ -d "$THEMES_DIR" ]]; then rm -rf "$THEMES_DIR" ok "Removed themes directory: $THEMES_DIR" fi if ls "$APPLICATIONS_DIR"/vicinae*.desktop >/dev/null 2>&1; then rm -f "$APPLICATIONS_DIR"/vicinae*.desktop ok "Removed desktop files from: $APPLICATIONS_DIR" # Update desktop database if available if command -v update-desktop-database >/dev/null 2>&1; then update-desktop-database "$APPLICATIONS_DIR" 2>/dev/null || true fi fi local icon_file="$PREFIX/share/icons/hicolor/512x512/apps/vicinae.png" if [[ -f "$icon_file" ]]; then rm -f "$icon_file" ok "Removed application icon: $icon_file" if command -v gtk-update-icon-cache >/dev/null 2>&1; then gtk-update-icon-cache -f -t "$PREFIX/share/icons/hicolor" 2>/dev/null || true fi fi if [[ -f "$SYSTEMD_USER_DIR/$SYSTEMD_SERVICE_NAME" ]]; then # Stop and disable the service if it's running if command -v systemctl >/dev/null 2>&1; then systemctl --user stop "$SYSTEMD_SERVICE_NAME" 2>/dev/null || true systemctl --user disable "$SYSTEMD_SERVICE_NAME" 2>/dev/null || true fi rm -f "$SYSTEMD_USER_DIR/$SYSTEMD_SERVICE_NAME" ok "Removed systemd service: $SYSTEMD_USER_DIR/$SYSTEMD_SERVICE_NAME" # Reload systemd user daemon if command -v systemctl >/dev/null 2>&1; then systemctl --user daemon-reload 2>/dev/null || true fi fi if [[ -f "$PREFIX/lib/udev/rules.d/70-vicinae.rules" ]]; then rm -f "$PREFIX/lib/udev/rules.d/70-vicinae.rules" ok "Removed udev rules" if command -v udevadm >/dev/null 2>&1; then udevadm control --reload-rules 2>/dev/null || true fi fi if [[ -f "$PREFIX/lib/modules-load.d/vicinae.conf" ]]; then rm -f "$PREFIX/lib/modules-load.d/vicinae.conf" ok "Removed modules-load config" fi if [[ $EUID -eq 0 ]]; then if [[ -f "/etc/chromium/native-messaging-hosts/com.vicinae.vicinae.json" ]]; then rm -f "/etc/chromium/native-messaging-hosts/com.vicinae.vicinae.json" ok "Removed Chromium native messaging manifest" fi if [[ -f "/usr/lib/mozilla/native-messaging-hosts/com.vicinae.vicinae.json" ]]; then rm -f "/usr/lib/mozilla/native-messaging-hosts/com.vicinae.vicinae.json" ok "Removed Firefox native messaging manifest" fi fi echo ok "Vicinae has been uninstalled" } show_usage() { echo "Usage: $0 [OPTIONS]" echo "" echo "Options:" echo " --prefix PATH Installation prefix (default: /usr/local)" echo " --appimage PATH Install from a local AppImage instead of downloading the latest release" echo " --token TOKEN GitHub token for the release API call (avoids rate limiting)" echo " --uninstall Uninstall Vicinae" echo " --help, -h Show this help message" echo "" echo "Environment variables:" echo " PREFIX Installation prefix (overridden by --prefix)" echo " GITHUB_TOKEN GitHub token for the release API call (overridden by --token)" echo "" echo "Without options, the script will install or update Vicinae to the latest version." echo "" echo "Examples:" echo " $0 # Install to /usr/local (requires sudo)" echo " $0 --prefix ~/.local # Install to ~/.local (user install)" echo " PREFIX=/opt/vicinae $0 # Install to /opt/vicinae" echo " GITHUB_TOKEN=ghp_xxx $0 # Use a GitHub token (avoids API rate limiting)" } # we need the script stored on disk to re-execute it with privilege elevation, if needed. # when piped from curl there is no file to copy, so we download it again self_download() { if [[ -f "$0" ]]; then cp "$0" "$VICINAE_SCRIPT_PATH" else curl -fsSL $SCRIPT_DOWNLOAD_URL > $VICINAE_SCRIPT_PATH fi chmod +x $VICINAE_SCRIPT_PATH } main() { if [[ "$(uname -s)" == "Darwin" ]]; then echo "Error: this installer exclusively supports linux based systems." >&2 echo "See https://docs.vicinae.com for macOS instructions." >&2 exit 1 fi renderIcon # Save original arguments for potential re-execution with sudo/doas ORIGINAL_ARGS=("$@") # Parse arguments local action="" local local_appimage="" while [[ $# -gt 0 ]]; do case "$1" in --appimage) if [[ -z "${2:-}" ]]; then echo "Error: --appimage requires a path argument" show_usage exit 1 fi if [[ ! -f "$2" ]]; then echo "Error: AppImage not found at $2" exit 1 fi local_appimage=$(readlink -f "$2") shift 2 ;; --prefix) if [[ -z "${2:-}" ]]; then echo "Error: --prefix requires a path argument" show_usage exit 1 fi PREFIX="$2" # Recalculate derived paths INSTALL_DIR="$PREFIX/lib/vicinae" BIN_DIR="$PREFIX/bin" THEMES_DIR="$PREFIX/share/vicinae/themes" APPLICATIONS_DIR="$PREFIX/share/applications" SYSTEMD_USER_DIR="$(resolve_systemd_user_dir)" shift 2 ;; --token) if [[ -z "${2:-}" ]]; then echo "Error: --token requires a GitHub token argument" show_usage exit 1 fi export GITHUB_TOKEN="$2" shift 2 ;; --uninstall) action="uninstall" shift ;; --help | -h) show_usage exit 0 ;; *) echo "Error: Unknown option '$1'" show_usage exit 1 ;; esac done # Handle actions if [[ "$action" == "uninstall" ]]; then check_permissions uninstall_vicinae exit 0 fi mkdir -p $PREFIX check_dependencies check_permissions local appimage_path local latest_version if [[ -n "$local_appimage" ]]; then appimage_path="$local_appimage" latest_version="(local)" # artifact zips downloaded from CI do not preserve the executable bit chmod +x "$appimage_path" 2>/dev/null || true echo "Installing from local AppImage: $appimage_path" else local release_info release_info=$(get_latest_release_info) local appimage_name IFS='|' read -r latest_version appimage_name <<<"$release_info" local installed_version installed_version=$(get_installed_version) echo echo " Installed version: $installed_version" echo " Latest version: $latest_version" echo if ! compare_versions "$installed_version" "$latest_version"; then ok "Vicinae is already up to date" return 0 fi if [[ "$installed_version" == "none" ]]; then echo "Installing Vicinae for the first time..." else echo "Updating Vicinae from $installed_version to $latest_version..." fi appimage_path=$(download_appimage "$latest_version" "$appimage_name") fi # local AppImages are never deleted, only downloaded ones are cleaned up local extract_dir if [[ -n "$local_appimage" ]]; then extract_dir=$(extract_appimage "$appimage_path" keep) install_vicinae "$extract_dir" else extract_dir=$(extract_appimage "$appimage_path") install_vicinae "$extract_dir" "$appimage_path" fi if (( ${#WARNINGS[@]} > 0 )); then echo echo -e "\033[1;33mInstallation completed with ${#WARNINGS[@]} warning(s):\033[0m" for w in "${WARNINGS[@]}"; do echo " • $w" done fi echo echo "🎉 Vicinae $latest_version has been successfully installed!" echo # Check if binary directory is in PATH if [[ ":$PATH:" == *":$BIN_DIR:"* ]]; then ok "$BIN_DIR is already in your PATH. You can now run 'vicinae' from anywhere in your terminal." else echo "To use Vicinae, add $BIN_DIR to your PATH:" if [[ "$BIN_DIR" == "$HOME"* ]]; then # User installation - show unexpanded path local_path="${BIN_DIR/#$HOME/\$HOME}" echo " export PATH=\"$local_path:\$PATH\"" else # System installation echo " export PATH=\"$BIN_DIR:\$PATH\"" fi echo "" echo "You can add this to your shell profile (~/.bashrc, ~/.zshrc, etc.) for permanent access." echo "Then restart your terminal or run the export command above." fi echo "" echo "Check the quickstart section for your Desktop Environment at https://docs.vicinae.com" } main "$@"