#!/usr/bin/env bash # menu.sh — Ubuntu/Debian Setup Manager # # Interactive menu that fetches and runs the hardened install scripts in this # Opengist. Each script is downloaded to a temp file (not blindly piped to bash) # and executed with the appropriate privilege level for that script: # # - "sudo" mode for system-wide installers (apt, /etc, /usr/local/bin) # - "user" mode for per-user installers that must NOT run as root # (gsettings, ~/.local, JetBrains Toolbox, fonts) # # Usage: # bash -c "$(curl -fsSL https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/menu.sh)" # # Or save and run locally: # curl -fsSLO https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/menu.sh # bash menu.sh # Note: NOT using `set -e` because we want the menu loop to survive a failed # sub-script. We do use -u and pipefail to catch real bugs in this file. set -uo pipefail IFS=$'\n\t' readonly SCRIPT_NAME="${0##*/}" readonly BASE_URL='https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD' log() { printf '\033[1;34m[menu]\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m[menu] WARN:\033[0m %s\n' "$*" >&2; } err() { printf '\033[1;31m[menu] ERROR:\033[0m %s\n' "$*" >&2; } hr() { printf '%s\n' "------------------------------------------------------"; } # Refuse to run as root: per-user scripts (jetbrains, fonts, ibus) need a real # desktop user. Sub-scripts elevate via sudo on their own. if (( EUID == 0 )); then err "Don't run $SCRIPT_NAME as root. Run as your normal user — it will call sudo for installers that need it." exit 1 fi # Sanity-check tools we depend on. for tool in curl bash mktemp; do command -v "$tool" >/dev/null 2>&1 || { err "Missing required tool: $tool"; exit 1; } done # Distro check (warn-only; sub-scripts enforce strictly). if [[ -r /etc/os-release ]]; then # shellcheck disable=SC1091 . /etc/os-release case "${ID:-}:${ID_LIKE:-}" in *ubuntu*|*debian*) : ;; *) warn "Detected ${PRETTY_NAME:-unknown}. These installers target Debian/Ubuntu; some will refuse to run." ;; esac fi # Cache sudo credentials up-front so sub-scripts that elevate don't keep # re-prompting in the middle of a multi-task run. prime_sudo() { if ! sudo -n true 2>/dev/null; then log "Caching sudo credentials (you may be prompted)..." sudo -v || { err "sudo authentication failed."; return 1; } fi # Keep sudo timestamp refreshed in the background while the menu runs. ( while true; do sudo -n true 2>/dev/null; sleep 60; done ) & SUDO_KEEPALIVE_PID=$! trap 'kill "${SUDO_KEEPALIVE_PID:-}" 2>/dev/null || true' EXIT } # run_script # mode: "sudo" (run as root via sudo) or "user" (run as current user) # Returns: 0 on success, non-zero on failure (does NOT exit the menu). run_script() { local name="$1" mode="$2" local url="$BASE_URL/$name" local tmp tmp="$(mktemp -t "${name%.sh}.XXXXXX.sh")" || { err "mktemp failed"; return 1; } log "Fetching $name..." if ! curl -fsSL --max-time 60 --retry 2 "$url" -o "$tmp"; then err "Download failed: $url" rm -f "$tmp" return 1 fi if [[ ! -s "$tmp" ]]; then err "Downloaded $name is empty." rm -f "$tmp" return 1 fi # Cheap sanity: confirm it looks like a shell script. if ! head -n1 "$tmp" | grep -qE '^#!.*sh'; then warn "$name doesn't start with a shebang; proceeding anyway." fi chmod +x "$tmp" local rc=0 if [[ "$mode" == "sudo" ]]; then sudo bash "$tmp" || rc=$? else bash "$tmp" || rc=$? fi rm -f "$tmp" if (( rc != 0 )); then err "$name exited with status $rc" fi return "$rc" } # Catalog: number | label | script-filename | mode # (Edit here to add/remove options — the menu loop is data-driven.) OPTIONS=( "1|Install Google Chrome|install-chrome.sh|sudo" "2|Install Firefox (Mozilla APT)|install-firefox.sh|sudo" "3|Install Thunderbird|install-thunderbird.sh|sudo" "4|Install 1Password|install-1password.sh|sudo" "5|Install Espanso (text expander)|install-espanso.sh|sudo" "6|Install LibreOffice (latest stable .deb)|install-libreoffice.sh|sudo" "7|Install Obsidian|install-obsidian.sh|sudo" "8|Install draw.io Desktop|install-drawio.sh|sudo" "9|Install Visual Studio Code|install-vscode.sh|sudo" "10|Install JetBrains Toolbox (per-user)|install-jetbrains-toolbox.sh|user" "11|Install Bruno (API client)|install-bruno.sh|sudo" "12|Install IPATool|install-ipatool.sh|sudo" "13|Install qBittorrent (latest AppImage)|install-qbittorrent.sh|sudo" "14|Mount Synology Network Drive|install-network_drive.sh|sudo" "15|Install IBus Intelligent Pinyin (per-user)|install-ibus-pinyin.sh|user" "16|Install Nerd Fonts (per-user)|install-font.sh|user" "17|Fix Dual-Boot Time (RTC to UTC)|timedatectl-fix.sh|sudo" "18|Install LocalSend|install-localsend.sh|sudo" "19|Install Telegram Desktop|install-telegram.sh|sudo" "20|Install Discord|install-discord.sh|sudo" ) print_menu() { hr echo " Ubuntu / Debian Setup Manager" hr echo "--- [ Browsers & Mail ] ---" echo " 1) Install Google Chrome" echo " 2) Install Firefox" echo " 3) Install Thunderbird" echo "--- [ Communication ] ---" echo " 18) Install LocalSend" echo " 19) Install Telegram Desktop" echo " 20) Install Discord" echo "--- [ Productivity & Security ] ---" echo " 4) Install 1Password" echo " 5) Install Espanso" echo " 6) Install LibreOffice" echo " 7) Install Obsidian" echo " 8) Install draw.io Desktop" echo "--- [ Development Tools ] ---" echo " 9) Install Visual Studio Code" echo " 10) Install JetBrains Toolbox (runs as you, not root)" echo " 11) Install Bruno" echo " 12) Install IPATool" echo " 13) Install qBittorrent" echo "--- [ System ] ---" echo " 14) Mount Synology Network Drive" echo " 15) Install IBus Intelligent Pinyin (runs as you, not root)" echo " 16) Install Nerd Fonts (runs as you, not root)" echo " 17) Fix Dual-Boot Time (RTC to UTC)" hr echo " 0) Run ALL options (1-20)" echo " -1) Exit" hr } # Resolve a numeric choice to its catalog entry; print "name|mode" to stdout. resolve_choice() { local want="$1" entry num for entry in "${OPTIONS[@]}"; do num="${entry%%|*}" if [[ "$num" == "$want" ]]; then # Strip the leading "N|label|"; what remains is "filename|mode" printf '%s' "${entry#*|*|}" return 0 fi done return 1 } ALL_NUMS=(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20) prime_sudo || exit 1 while true; do print_menu read -rp "Enter choices separated by spaces (e.g., 1 7 12), 0 for ALL, -1 to exit: " choices /dev/null; then bad+=" $c" fi done if [[ -n "$bad" ]]; then err "Invalid option(s):$bad" sleep 1 continue fi failures=() for c in $choices; do spec="$(resolve_choice "$c")" name="${spec%|*}" mode="${spec##*|}" hr log "[$c] Running $name ($mode)..." if ! run_script "$name" "$mode"; then failures+=("$c:$name") fi done hr if (( ${#failures[@]} == 0 )); then log "All selected tasks completed." else warn "Completed with ${#failures[@]} failure(s): ${failures[*]}" fi echo done