#!/usr/bin/env bash # The Orbit Installer for Linux: every Orbit desktop app in one go, each one # ticked to start with. # # curl -fsSL https://orbit.com.ai/install.sh | bash # # It lists the Orbit apps with a checkbox each, all ticked; untick what you # don't want. Each app is its newest release, downloaded through Orbit Mission # Control from GitHub: # # Ubuntu, Debian, Linux Mint, Pop!_OS… the .deb, installed with apt # Fedora, RHEL, Rocky, AlmaLinux… the .rpm, installed with dnf # openSUSE the .rpm, installed with zypper # Arch, Manjaro and everything else the AppImage, in ~/Applications with a menu entry # # Installing a package asks for your password through sudo. An AppImage # updates itself; a package is updated by running this again. # # Options (after `bash -s --` when piped from curl): # --all install every app without asking # --apps a,b install just these (mail, chat, pass, ide…) without asking # --appimage use the AppImage even where a package would do # --list show the apps and what this system would get, then stop # --dry-run say what would be downloaded and run, and do nothing # --help # # The website serves this file with the list of Orbit apps filled in; the copy # in the repository lists none. set -euo pipefail SERVER="${ORBIT_SERVER:-https://control.orbit.com.ai}" # One per line: || APPS=( "orbit-mail|Orbit Mail|Email and calendar" "orbit-chat|Orbit Chat|Encrypted team chat" "orbit-pass|Orbit Pass|Password manager" "orbit-ide|Orbit IDE|Code editor" ) # --- Saying things --------------------------------------------------------- if [ -t 1 ]; then BOLD=$'\033[1m'; DIM=$'\033[2m'; RED=$'\033[31m'; GREEN=$'\033[32m'; RESET=$'\033[0m' else BOLD=""; DIM=""; RED=""; GREEN=""; RESET=""; fi say() { printf '%s\n' "$*"; } step() { printf '%s==>%s %s\n' "$BOLD" "$RESET" "$*"; } warn() { printf '%sWarning:%s %s\n' "$BOLD" "$RESET" "$*" >&2; } fail() { printf '%sError:%s %s\n' "$RED" "$RESET" "$*" >&2; exit 1; } usage() { cat <<'HELP' The Orbit Installer for Linux: every Orbit desktop app, each one ticked to start with. curl -fsSL https://orbit.com.ai/install.sh | bash curl -fsSL https://orbit.com.ai/install.sh | bash -s -- [options] --all install every app without asking --apps a,b install just these (mail, chat, pass, ide…) without asking --appimage use the AppImage even where a package would do --list show the apps and what this system would get, then stop --dry-run say what would be downloaded and run, and do nothing HELP } # --- The system ------------------------------------------------------------ # How this system installs a package: deb (apt), dnf, zypper, or appimage. detect_family() { local release="${ORBIT_OS_RELEASE:-/etc/os-release}" id="" like="" if [ -r "$release" ]; then id="$(. "$release"; printf '%s' "${ID:-}")" like="$(. "$release"; printf '%s' "${ID_LIKE:-}")" PRETTY="$(. "$release"; printf '%s' "${PRETTY_NAME:-${NAME:-Linux}}")" else PRETTY="Linux" fi local words=" $id $like " case "$words" in *" ubuntu "*|*" debian "*) FAMILY=deb ;; *" fedora "*|*" rhel "*|*" centos "*|*" rocky "*|*" almalinux "*|*" ol "*|*" amzn "*) FAMILY=dnf ;; *" opensuse"*|*" suse "*|*" sles "*) FAMILY=zypper ;; *) FAMILY=appimage ;; esac # A family whose tool is missing falls back to the AppImage. case "$FAMILY" in deb) has apt-get || FAMILY=appimage ;; dnf) has dnf || has yum || FAMILY=appimage ;; zypper) has zypper || FAMILY=appimage ;; esac } has() { command -v "$1" >/dev/null 2>&1; } # The package Mission Control hands out for this family. package_kind() { case "$FAMILY" in deb) printf 'deb' ;; dnf|zypper) printf 'rpm' ;; *) printf 'appimage' ;; esac } describe_family() { case "$FAMILY" in deb) printf 'the .deb, installed with apt' ;; dnf) printf 'the .rpm, installed with dnf' ;; zypper) printf 'the .rpm, installed with zypper' ;; *) printf 'the AppImage, in ~/Applications' ;; esac } root_command() { if [ "$(id -u)" = "0" ]; then printf '' elif has sudo; then printf 'sudo' elif has doas; then printf 'doas' else printf 'none'; fi } # --- The apps -------------------------------------------------------------- field() { printf '%s' "$1" | cut -d'|' -f"$2"; } # Whether Mission Control has a build of this app in this package: it answers # 302 (to GitHub) when it does and 404 when there is none yet. available() { local status status="$(curl -s -o /dev/null -w '%{http_code}' --max-redirs 0 --connect-timeout 20 \ "$SERVER/download/$1/linux/$2/" 2>/dev/null || true)" [ "$status" = "302" ] || [ "$status" = "301" ] || [ "$status" = "303" ] || [ "$status" = "307" ] } # "mail" or "orbit-mail" or "Orbit Mail" to the app's entry, or nothing. find_app() { local wanted entry slug name wanted="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed 's/^orbit[- ]//')" for entry in ${APPS[@]+"${APPS[@]}"}; do slug="$(field "$entry" 1)"; name="$(field "$entry" 2 | tr '[:upper:]' '[:lower:]')" if [ "${slug#orbit-}" = "$wanted" ] || [ "${name#orbit }" = "$wanted" ]; then printf '%s' "$entry"; return 0 fi done return 1 } # Asks which of OFFERED to install, every one ticked; sets CHOSEN. choose() { local tags=() entry i=1 CHOSEN=() if [ ! -r /dev/tty ] || ! { : /dev/null; then say "No terminal to ask on, so every app is installed (as if you kept every box ticked)." CHOSEN=("${OFFERED[@]}") return fi if has whiptail || has dialog; then local tool=whiptail; has whiptail || tool=dialog for entry in "${OFFERED[@]}"; do tags+=("$(field "$entry" 1)" "$(field "$entry" 2): $(field "$entry" 3)" "ON") done local picked if ! picked="$("$tool" --title "Install Orbit" --separate-output --checklist \ "Untick the apps you don't want, then choose OK. Each is its newest release, as $(describe_family)." \ 20 78 "${#OFFERED[@]}" "${tags[@]}" 3>&1 1>&2 2>&3 /dev/null 2>&1 && "$target" --appimage-extract '*.png' >/dev/null 2>&1) || true if [ -f "$extracted/squashfs-root/.DirIcon" ] && cp -L "$extracted/squashfs-root/.DirIcon" "$icons/$slug.png" 2>/dev/null; then icon="$slug" fi rm -rf "$extracted" cat >"$menu" </dev/null 2>&1 || true; fi } main() { local all=0 wanted="" force_appimage=0 list=0 DRY_RUN=0 while [ $# -gt 0 ]; do case "$1" in --all) all=1 ;; --apps) shift; wanted="${1:-}" ;; --apps=*) wanted="${1#--apps=}" ;; --appimage) force_appimage=1 ;; --list) list=1 ;; --dry-run) DRY_RUN=1 ;; -h|--help) usage; exit 0 ;; *) fail "Unknown option $1 (try --help)." ;; esac shift done [ "$(uname -s)" = "Linux" ] || [ -n "${ORBIT_OS_RELEASE:-}" ] || fail "This is the installer for Linux. On a Mac or a PC, get the Orbit Installer from https://orbit.com.ai/download/installer/" [ ${#APPS[@]} -gt 0 ] || fail "This copy lists no apps. Run the one the website serves: curl -fsSL https://orbit.com.ai/install.sh | bash" has curl || fail "curl is needed to download the apps." local arch="${ORBIT_ARCH:-$(uname -m)}" case "$arch" in x86_64|amd64) ;; *) fail "The Orbit apps for Linux are built for 64-bit Intel and AMD processors (x86_64); this is $arch." ;; esac detect_family [ "$force_appimage" = 1 ] && FAMILY=appimage if [ "$FAMILY" != appimage ] && [ "$(root_command)" = "none" ]; then warn "There is no sudo or doas to install packages with, so the AppImages are used." FAMILY=appimage fi local kind; kind="$(package_kind)" step "Orbit for $PRETTY: $(describe_family)" # What there is a build of for this system. local entry slug OFFERED=() local missing=() for entry in "${APPS[@]}"; do slug="$(field "$entry" 1)" if [ "$DRY_RUN" = 1 ] || available "$slug" "$kind"; then OFFERED+=("$entry"); else missing+=("$(field "$entry" 2)"); fi done if [ ${#missing[@]} -gt 0 ]; then say "${DIM}Not out for Linux yet: $(printf '%s, ' "${missing[@]}" | sed 's/, $//').${RESET}" fi [ ${#OFFERED[@]} -gt 0 ] || fail "None of the Orbit apps has a Linux build yet. See https://orbit.com.ai/download/" if [ "$list" = 1 ]; then for entry in "${OFFERED[@]}"; do printf ' %-14s %s\n' "$(field "$entry" 2)" "$(field "$entry" 3)"; done exit 0 fi if [ -n "$wanted" ]; then CHOSEN=() local name found for name in $(printf '%s' "$wanted" | tr ',' ' '); do found="$(find_app "$name")" || fail "There is no Orbit app called $name." case " ${OFFERED[*]} " in *"$found"*) CHOSEN+=("$found") ;; *) warn "$(field "$found" 2) is not out for Linux yet." ;; esac done elif [ "$all" = 1 ]; then CHOSEN=("${OFFERED[@]}") else choose fi [ ${#CHOSEN[@]} -gt 0 ] || { say "Nothing to install."; exit 0; } if [ "$FAMILY" = appimage ] && ! has fusermount && ! has fusermount3; then warn "AppImages need FUSE to run: install it (fuse2 on Arch, fuse on most others) if an app doesn't open." fi WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT local done_apps=() failed=() name file for entry in "${CHOSEN[@]}"; do slug="$(field "$entry" 1)"; name="$(field "$entry" 2)" step "$name" file="$WORK/$slug.$kind" [ "$kind" = appimage ] && file="$WORK/$slug.AppImage" if ! download "$slug" "$kind" "$file"; then failed+=("$name"); continue; fi if [ "$FAMILY" = appimage ]; then install_appimage "$file" "$slug" "$name" || { failed+=("$name"); continue; } else install_package "$file" || { failed+=("$name"); continue; } fi done_apps+=("$name") done say "" if [ ${#done_apps[@]} -gt 0 ]; then say "${GREEN}Installed:${RESET} $(printf '%s, ' "${done_apps[@]}" | sed 's/, $//'). Open them from your applications menu and sign in with your Orbit account." fi if [ ${#failed[@]} -gt 0 ]; then fail "Didn't install: $(printf '%s, ' "${failed[@]}" | sed 's/, $//'). Run the installer again to try once more." fi } main "$@"