diff --git a/README.md b/README.md index b8af15d..6e58c82 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,14 @@ ██╔══██╗██╔════╝██║ ██║██╔════╝██╔════╝╚══██╔══╝██║ ██║██╔══██╗ ██║ ██║█████╗ ██║ ██║███████╗█████╗ ██║ ██║ ██║██████╔╝ ██║ ██║██╔══╝ ╚██╗ ██╔╝╚════██║██╔══╝ ██║ ██║ ██║██╔═══╝ + ██║ ██║██╔══╝ ╚██╗ ██╔╝╚════██║██╔══╝ ██║ ██║ ██║██╔═╝ ██████╔╝███████╗ ╚████╔╝ ███████║███████╗ ██║ ╚██████╔╝██║ ╚═════╝ ╚══════╝ ╚═══╝ ╚══════╝╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ``` **One-command setup for your entire DevOps environment.** -[![Version](https://img.shields.io/badge/version-1.0.1-indigo?style=flat-square)](https://github.com/Silver595/DevSetUp/releases) +[![Version](https://img.shields.io/badge/version-1.0.5-indigo?style=flat-square)](https://github.com/Silver595/DevSetUp/releases) [![License](https://img.shields.io/badge/license-MIT-green?style=flat-square)](LICENSE) [![Shell](https://img.shields.io/badge/shell-bash%204%2B-blue?style=flat-square)](https://www.gnu.org/software/bash/) [![OS](https://img.shields.io/badge/OS-Debian%20·%20Ubuntu%20·%20Fedora%20·%20Arch-orange?style=flat-square)](#-supported-operating-systems) @@ -385,16 +386,16 @@ devsetup/ ## 🌐 TUI Backends -`devsetup` auto-detects the best available interface: +The pure-bash arrow-key TUI is the **default** — it works everywhere: sudo, SSH, tmux, any terminal. -| Backend | Experience | Install | +| Backend | Experience | How to use | |---|---|---| -| `whiptail` | ✔ Dialog boxes | `apt install whiptail` | -| `dialog` | ✔ Dialog boxes | `apt install dialog` | -| `fzf` | ✔ Fuzzy finder | `devsetup --install fzf` | -| **Pure bash** | ✔ Arrow-key TUI | **Built-in — always works** | +| **Pure bash** | ✔ Arrow keys, built-in | Default — always works | +| `whiptail` | ✔ Dialog boxes | `DEVSETUP_TUI=whiptail devsetup` | +| `dialog` | ✔ Dialog boxes | `DEVSETUP_TUI=dialog devsetup` | +| `fzf` | ✔ Fuzzy search | `DEVSETUP_TUI=fzf devsetup` | -The pure-bash fallback supports: `↑↓` / `j k` navigate · `Space` toggle · `Enter` confirm · `a` select all · `n` clear · `q` quit +**Pure-bash controls:** `↑↓` / `j k` navigate · `Space` toggle · `Enter` confirm · `a` select all · `n` clear · `q` quit --- diff --git a/devsetup b/devsetup index f6e5989..7a3896e 100755 --- a/devsetup +++ b/devsetup @@ -37,7 +37,7 @@ unset _lib DRY_RUN="${DRY_RUN:-false}" export DRY_RUN -DEVSETUP_VERSION="1.0.4" +DEVSETUP_VERSION="1.0.5" # ── Lock file (prevent concurrent runs) ────────────────────────────────────── LOCK_FILE="/tmp/devsetup.lock" @@ -268,9 +268,9 @@ run_interactive() { # Brief non-blocking doctor log_section "Pre-flight" - check_internet 2>/dev/null || log_warn "Internet may be offline — installs could fail." + check_internet || log_warn "Internet offline — installs will fail." check_sudo; check_disk_space /; check_pkg_lock - printf "\n" >&2 + echo >&2 # Temp file: all TUI backends write their selection here directly (no subshell tricks) _tui_out_file="$(mktemp /tmp/devsetup_sel.XXXXXX)" diff --git a/devsetup_1.0.5_all.deb b/devsetup_1.0.5_all.deb new file mode 100644 index 0000000..187d7aa Binary files /dev/null and b/devsetup_1.0.5_all.deb differ diff --git a/install.sh b/install.sh index fb5c781..77d4b64 100644 --- a/install.sh +++ b/install.sh @@ -1,14 +1,10 @@ #!/usr/bin/env bash # ============================================================================= -# install.sh — One-liner remote installer for devsetup +# install.sh — One-liner curl installer for devsetup v1.0.5 # -# Usage (once hosted on a server or GitHub): -# curl -fsSL https://raw.githubusercontent.com/YOU/devsetup/main/install.sh | bash -# -# What it does: -# 1. Downloads the devsetup repo (or just the required files) -# 2. Installs to /usr/local/bin/devsetup -# 3. Puts lib/ and config/ into /usr/share/devsetup/ +# Usage: +# curl -fsSL https://raw.githubusercontent.com/Silver595/DevSetUp/main/install.sh | bash +# curl -fsSL https://raw.githubusercontent.com/Silver595/DevSetUp/main/install.sh | sudo bash # ============================================================================= set -euo pipefail @@ -16,43 +12,89 @@ REPO_URL="https://github.com/Silver595/DevSetUp" RAW_URL="https://raw.githubusercontent.com/Silver595/DevSetUp/main" INSTALL_BIN="/usr/local/bin/devsetup" INSTALL_SHARE="/usr/share/devsetup" -INSTALLER_VERSION="1.0.3" +INSTALLER_VERSION="1.0.5" # ── Colors ──────────────────────────────────────────────────────────────────── -RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' -CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' +RED=$'\033[0;31m'; GREEN=$'\033[0;32m'; YELLOW=$'\033[1;33m' +CYAN=$'\033[0;36m'; BOLD=$'\033[1m'; DIM=$'\033[2m'; RESET=$'\033[0m' -info() { echo -e "${CYAN} ➜${RESET} $*"; } -ok() { echo -e "${GREEN} ✔${RESET} $*"; } -warn() { echo -e "${YELLOW} ⚠${RESET} $*"; } -die() { echo -e "${RED} ✘ ERROR:${RESET} $*" >&2; exit 1; } +info() { printf "${CYAN} ➜${RESET} %s\n" "$*"; } +ok() { printf "${GREEN} ✔${RESET} %s\n" "$*"; } +warn() { printf "${YELLOW} ⚠${RESET} %s\n" "$*"; } +die() { printf "${RED} ✘ ERROR:${RESET} %s\n" "$*" >&2; exit 1; } +step() { printf "\n${BOLD} ── %s${RESET}\n" "$*"; } +# ── Privilege detection ─────────────────────────────────────────────────────── SUDO_CMD="" -[[ "$EUID" -ne 0 ]] && SUDO_CMD="sudo" +if [[ "$EUID" -ne 0 ]]; then + command -v sudo &>/dev/null || die "This script must be run as root or with sudo." + SUDO_CMD="sudo" +fi + +# ── Pre-flight ──────────────────────────────────────────────────────────────── +printf "\n${BOLD} ╔══════════════════════════════════════╗\n" +printf " ║ devsetup Installer v%-12s ║\n" "$INSTALLER_VERSION" +printf " ╚══════════════════════════════════════╝${RESET}\n\n" + +step "Pre-flight checks" + +# curl is mandatory +command -v curl &>/dev/null || die "curl is required. Install it: sudo apt install curl" +ok "curl found" -# ── Preflight ───────────────────────────────────────────────────────────────── -command -v curl &>/dev/null || die "curl is required. Install it first: sudo apt install curl" +# Internet check +if curl -fsSL --max-time 6 --retry 2 https://1.1.1.1 &>/dev/null \ + || curl -fsSL --max-time 6 --retry 2 https://google.com &>/dev/null; then + ok "Internet connection OK" +else + die "No internet connection. Cannot download devsetup." +fi + +# OS detection +if [[ -f /etc/os-release ]]; then + # shellcheck source=/dev/null + source /etc/os-release + _os="${ID,,}" +else + _os="$(uname -s | tr '[:upper:]' '[:lower:]')" +fi -echo "" -echo -e "${BOLD} DevSetup Installer v${INSTALLER_VERSION}${RESET}" -echo " ─────────────────────────────────" -echo "" +case "$_os" in + ubuntu|debian|linuxmint|pop|kali|elementary|zorin|raspbian) + _pkg_mgr="apt" ;; + fedora|rhel|centos|almalinux|rocky) + command -v dnf &>/dev/null && _pkg_mgr="dnf" || _pkg_mgr="yum" ;; + arch|manjaro|endeavouros|garuda|artix) + _pkg_mgr="pacman" ;; + opensuse*|sles) + _pkg_mgr="zypper" ;; + darwin|macos) + _pkg_mgr="brew" ;; + *) + _pkg_mgr="unknown" + warn "Unknown OS: $_os — installation may have issues." + ;; +esac +ok "OS detected: $_os (package manager: $_pkg_mgr)" + +# ── Download ────────────────────────────────────────────────────────────────── +step "Downloading devsetup" -# ── Download method: prefer git clone, fall back to curl ───────────────────── TMP_DIR="$(mktemp -d)" trap 'rm -rf "$TMP_DIR"' EXIT +SRC="$TMP_DIR/devsetup" + if command -v git &>/dev/null; then - info "Cloning devsetup repository..." - git clone --depth=1 "$REPO_URL" "$TMP_DIR/devsetup" 2>/dev/null \ - || die "Failed to clone $REPO_URL — check the URL or your internet connection." - SRC="$TMP_DIR/devsetup" + info "Cloning repository (git)..." + git clone --depth=1 --quiet "$REPO_URL" "$SRC" \ + || die "Failed to clone $REPO_URL — check your internet connection." + ok "Repository cloned" else - info "git not found — downloading files directly..." - SRC="$TMP_DIR/devsetup" + info "git not found — downloading individual files..." mkdir -p "$SRC/lib" "$SRC/config" - FILES=( + _files=( "devsetup" "lib/logger.sh" "lib/detect.sh" @@ -64,19 +106,25 @@ else "config/aliases.conf" "config/folders.conf" ) - for f in "${FILES[@]}"; do - info " Downloading $f..." - mkdir -p "$SRC/$(dirname "$f")" - curl -fsSL "$RAW_URL/$f" -o "$SRC/$f" \ - || die "Failed to download $f" + for _f in "${_files[@]}"; do + mkdir -p "$SRC/$(dirname "$_f")" + curl -fsSL --retry 3 "$RAW_URL/$_f" -o "$SRC/$_f" \ + || die "Failed to download $_f — check your internet connection." done + ok "${#_files[@]} files downloaded" fi -# ── Install files ───────────────────────────────────────────────────────────── -info "Installing devsetup to $INSTALL_BIN ..." +# Basic sanity check — make sure we got the right thing +[[ -f "$SRC/devsetup" ]] || die "Download failed: main script not found in $SRC" +[[ -d "$SRC/lib" ]] || die "Download failed: lib/ directory not found in $SRC" +[[ -d "$SRC/config" ]] || die "Download failed: config/ directory not found in $SRC" + +# ── Install ─────────────────────────────────────────────────────────────────── +step "Installing to system" + $SUDO_CMD mkdir -p "$INSTALL_SHARE/lib" "$INSTALL_SHARE/config" -# Rewrite path vars so the installed binary finds its libraries +# Rewrite the path vars so the installed binary finds its libs correctly sed \ -e "s|^DEVSETUP_DIR=.*|DEVSETUP_DIR=\"${INSTALL_SHARE}\"|" \ -e "s|^LIB_DIR=.*|LIB_DIR=\"\${DEVSETUP_DIR}/lib\"|" \ @@ -84,17 +132,30 @@ sed \ "$SRC/devsetup" > "$TMP_DIR/devsetup_patched" $SUDO_CMD install -m 755 "$TMP_DIR/devsetup_patched" "$INSTALL_BIN" -$SUDO_CMD cp -r "$SRC/lib/." "$INSTALL_SHARE/lib/" -$SUDO_CMD cp -r "$SRC/config/." "$INSTALL_SHARE/config/" +$SUDO_CMD cp -r "$SRC/lib/." "$INSTALL_SHARE/lib/" +$SUDO_CMD cp -r "$SRC/config/." "$INSTALL_SHARE/config/" $SUDO_CMD chmod +x "$INSTALL_SHARE/lib/"*.sh -ok "Installed: $INSTALL_BIN" -ok "Data dir: $INSTALL_SHARE" +ok "Binary: $INSTALL_BIN" +ok "Data dir: $INSTALL_SHARE" -# ── Verify ──────────────────────────────────────────────────────────────────── -echo "" -devsetup --version && ok "devsetup is ready!" || warn "Installation may have issues — run 'devsetup --help' to check." +# ── Make sure /usr/local/bin is in PATH ─────────────────────────────────────── +if [[ ":$PATH:" != *":/usr/local/bin:"* ]]; then + warn "/usr/local/bin is not in your PATH — adding it..." + export PATH="/usr/local/bin:$PATH" + warn "Add this to your ~/.bashrc or ~/.zshrc to make it permanent:" + warn " export PATH=\"/usr/local/bin:\$PATH\"" +fi + +# ── Final verification ──────────────────────────────────────────────────────── +step "Verification" + +if "$INSTALL_BIN" --version &>/dev/null; then + _ver="$("$INSTALL_BIN" --version 2>/dev/null)" + ok "$_ver installed successfully" +else + die "Installation verification failed. Try running: $INSTALL_BIN --version" +fi -echo "" -echo -e "${BOLD} ✨ Run: devsetup${RESET}" -echo "" +printf "\n${BOLD}${GREEN} ✨ Done! Run:${RESET} ${CYAN}${BOLD}devsetup${RESET}\n\n" +printf " ${DIM}Tip: run 'devsetup --doctor' first to verify your system is ready.${RESET}\n\n" diff --git a/lib/tui.sh b/lib/tui.sh index 8822488..b886280 100755 --- a/lib/tui.sh +++ b/lib/tui.sh @@ -1,90 +1,104 @@ #!/usr/bin/env bash # ============================================================================= # lib/tui.sh — Interactive tool selector -# Chain: whiptail → dialog → fzf → pure-bash (always works, arrow keys) -# All backends write the selected tool list to a file (_tui_out_file). -# This avoids any subshell / fd-swap complications. +# Default: pure-bash arrow-key TUI (works everywhere — sudo, SSH, tmux) +# Override: DEVSETUP_TUI=whiptail|dialog|fzf devsetup # ============================================================================= -# ── Detect backend ──────────────────────────────────────────────────────────── -# Pure-bash TUI is the default — works in sudo, SSH, any terminal. -# Override with: DEVSETUP_TUI=whiptail devsetup +# ── Backend detection ───────────────────────────────────────────────────────── _tui_detect_backend() { local pref="${DEVSETUP_TUI:-}" if [[ -n "$pref" ]] && command -v "$pref" &>/dev/null; then echo "$pref"; return fi - echo "bash" # pure-bash always works + echo "bash" } TUI_BACKEND="$(_tui_detect_backend)" -# ── Confirm prompt (y/n) ────────────────────────────────────────────────────── +# ── Confirm prompt ──────────────────────────────────────────────────────────── tui_confirm() { local prompt="${1:-Are you sure?}" default="${2:-y}" local hint="[Y/n]"; [[ "$default" == "n" ]] && hint="[y/N]" printf "\n ${TEAL}?${RESET} ${BOLD}%s${RESET} ${DIM}%s${RESET} " "$prompt" "$hint" >&2 local reply - if [[ -t 0 ]]; then - IFS= read -r reply - else - IFS= read -r reply /dev/null || IFS= read -r reply reply="${reply:-$default}" [[ "${reply,,}" =~ ^y ]] } -# ── Simple labelled prompt ──────────────────────────────────────────────────── +# ── Labelled read prompt ────────────────────────────────────────────────────── tui_read() { local prompt="$1" default="${2:-}" printf " ${TEAL}?${RESET} ${BOLD}%s${RESET}${DIM}%s${RESET}: " \ "$prompt" "${default:+ [$default]}" >&2 local reply - IFS= read -r reply /dev/null || IFS= read -r reply printf '%s' "${reply:-$default}" } # ============================================================================= -# ── Pure-bash arrow-key multi-select ───────────────────────────────────────── -# Writes space-separated tool list to global _tui_out_file +# ── Pure-bash TUI ───────────────────────────────────────────────────────────── +# Writes space-separated selection to global $_tui_out_file # ============================================================================= _tui_bash() { - local -n _grps="$1" + local var_name="$1" + local -n _grps="$var_name" - # Build flat list - local -a items=() labels=() - local -a sorted_cats=() - IFS=$'\n' read -ra sorted_cats <<< "$(printf '%s\n' "${!_grps[@]}" | sort)" + # ── Build flat item list ────────────────────────────────────────────────── + local -a items=() labels=() sep_colors=() + local total cursor=1 scroll=0 + # Category colours/icons declare -A _CC=( [DevOps]="$TEAL" [IaC]="$ORANGE" [Cloud]="$INDIGO" [WebServer]="$LIME" [PHP]="$LAVENDER" [Database]="$CORAL" [Languages]="$YELLOW" [VCS]="$BCYAN" [Utils]="$PINK" ) - declare -A _CI=( - [DevOps]="⎈" [IaC]="⛌" [Cloud]="☁" [WebServer]=">" [PHP]="λ" - [Database]="▣" [Languages]="" [VCS]="⑂" [Utils]="⚒" - ) + local -a sorted_cats=() + mapfile -t sorted_cats < <(printf '%s\n' "${!_grps[@]}" | sort) + + # Pre-compute installed status — avoids repeated command -v on every redraw + declare -A _installed=() + for cat in "${sorted_cats[@]}"; do + for tool in ${_grps[$cat]}; do + command -v "$tool" &>/dev/null && _installed["$tool"]=1 || _installed["$tool"]=0 + done + done + + # Build flat items / labels / sep_color arrays + local -a sep_colors=() # colour code per item (empty for non-separators) for cat in "${sorted_cats[@]}"; do - items+=(""); labels+=("__SEP__:${_CC[$cat]:-$TEAL}:${_CI[$cat]:->} $cat") + # category header row + items+=("") + labels+=("${cat}") + sep_colors+=("${_CC[$cat]:-$TEAL}") for tool in ${_grps[$cat]}; do - items+=("$tool"); labels+=("$tool") + items+=("$tool") + labels+=("$tool") + sep_colors+=("") done done local total="${#items[@]}" + (( total == 0 )) && { log_warn "No tools found in tools.conf"; return 1; } + local -a selected=() local cursor=1 scroll=0 - local ROWS; ROWS=$(( $(tput lines 2>/dev/null || echo 24) - 8 )) - (( ROWS < 6 )) && ROWS=6 + local ROWS; ROWS=$(( $(tput lines 2>/dev/null || echo 24) - 9 )) + (( ROWS < 5 )) && ROWS=5 - # Save terminal state and switch to raw mode - local OLD_STTY; OLD_STTY="$(stty -g 2>/dev/null)" - stty -echo -icanon min 1 time 0 2>/dev/null - tput civis 2>/dev/null + # Save/restore terminal + local OLD_STTY; OLD_STTY="$(stty -g 2>/dev/null || true)" + stty -echo -icanon min 1 time 0 2>/dev/null || true + tput civis 2>/dev/null || true - _is_sep() { [[ "${labels[$1]:-}" == __SEP__* ]]; } - _is_sel() { local i; for i in "${selected[@]}"; do [[ "$i" == "$1" ]] && return 0; done; return 1; } + # ── Helpers ──────────────────────────────────────────────────────────────── + _is_sep() { [[ -n "${sep_colors[$1]:-}" ]]; } + _is_sel() { + local i; for i in "${selected[@]}"; do [[ "$i" == "$1" ]] && return 0; done + return 1 + } _toggle() { _is_sep "$1" && return if _is_sel "$1"; then @@ -104,97 +118,119 @@ _tui_bash() { while (( i >= 0 )); do _is_sep "$i" || { cursor=$i; return; }; (( i-- )); done } + # ── Draw entire TUI ─────────────────────────────────────────────────────── _draw() { - printf '\033[H\033[2J' >&2 # clear screen (tput clear >&2) + # Move cursor to top-left and clear (faster than `clear`) + printf '\033[H\033[2J\033[3J' >&2 + printf "${INDIGO}${BOLD} ╔══════════════════════════════════════════════════╗\n" >&2 printf " ║ ${BWHITE}devsetup — Select Tools to Install${INDIGO} ║\n" >&2 printf " ╚══════════════════════════════════════════════════╝${RESET}\n" >&2 - printf " ${DIM}↑/↓ move Space select Enter confirm a=all n=none q=quit${RESET}\n\n" >&2 + printf " ${DIM}↑/↓ move Space select Enter confirm a all n none q quit${RESET}\n\n" >&2 local end=$(( scroll + ROWS )) (( end > total )) && end=$total - local idx for (( idx = scroll; idx < end; idx++ )); do if _is_sep "$idx"; then - local si="${labels[$idx]#__SEP__:}" - local sc="${si%%:*}" sl="${si#*:}" - printf " ${sc}${BOLD} ▸ %-28s${RESET}\n" "$sl" >&2 + printf " %s${BOLD} ▸ %-24s${RESET}\n" \ + "${sep_colors[$idx]}" "${labels[$idx]}" >&2 else local t="${items[$idx]}" local mark="${DIM}[ ]${RESET}" - _is_sel "$idx" && mark="${BGREEN}[${ICON_OK}]${RESET}" + _is_sel "$idx" && mark="${BGREEN}[✔]${RESET}" local cur=" " [[ "$idx" == "$cursor" ]] && cur="${ORANGE}❯ ${RESET}" - if command -v "$t" &>/dev/null; then - printf " %s%s ${BOLD}%-16s${RESET} ${DIM}✔ installed${RESET}\n" \ + if [[ "${_installed[$t]:-0}" == "1" ]]; then + printf " %s%s ${BOLD}%-18s${RESET} ${DIM}installed${RESET}\n" \ "$cur" "$mark" "$t" >&2 else - printf " %s%s %-16s\n" "$cur" "$mark" "$t" >&2 + printf " %s%s %-18s\n" "$cur" "$mark" "$t" >&2 fi fi done - (( total > ROWS )) && \ - printf "\n ${DIM}[ %d–%d of %d ]${RESET}\n" "$scroll" "$end" "$total" >&2 - printf "\n ${TEAL}${BOLD}%d selected${RESET}\n" "${#selected[@]}" >&2 + # Scroll indicator + selection count + if (( total > ROWS + scroll )); then + printf "\n ${DIM}↓ more below (showing %d–%d of %d items)${RESET}\n" \ + "$scroll" "$end" "$total" >&2 + elif (( scroll > 0 )); then + printf "\n ${DIM}↑ more above (showing %d–%d of %d items)${RESET}\n" \ + "$scroll" "$end" "$total" >&2 + else + printf "\n" >&2 + fi + printf " ${TEAL}${BOLD}%d tool(s) selected${RESET} ${DIM}(scroll: %d/%d)${RESET}\n" \ + "${#selected[@]}" "$scroll" "$(( total - ROWS ))" >&2 } - # Main event loop + # ── Event loop ──────────────────────────────────────────────────────────── while true; do - while (( cursor < scroll )); do (( scroll-- )); done - while (( cursor >= scroll+ROWS )); do (( scroll++ )); done + # Keep cursor in scroll window + while (( cursor < scroll )); do (( scroll-- )); done + while (( cursor >= scroll+ROWS )); do (( scroll++ )); done + _draw local key="" esc="" IFS= read -r -s -n1 key /dev/null; tput cnorm 2>/dev/null - printf '\033[H\033[2J' >&2 + stty "$OLD_STTY" 2>/dev/null || true + tput cnorm 2>/dev/null || true + printf '\033[H\033[2J\033[3J' >&2 return 1 ;; esac fi done # Restore terminal - stty "$OLD_STTY" 2>/dev/null; tput cnorm 2>/dev/null - printf '\033[H\033[2J' >&2 + stty "$OLD_STTY" 2>/dev/null || true + tput cnorm 2>/dev/null || true + printf '\033[H\033[2J\033[3J' >&2 - # Write result to output file + # Collect tool names and write to output file local -a result=() local idx for idx in "${selected[@]}"; do [[ -n "${items[$idx]:-}" ]] && result+=("${items[$idx]}") done - printf '%s\n' "${result[*]}" > "$_tui_out_file" + + if [[ ${#result[@]} -gt 0 ]]; then + printf '%s ' "${result[@]}" > "$_tui_out_file" + fi } # ============================================================================= # ── whiptail backend ────────────────────────────────────────────────────────── # ============================================================================= _tui_whiptail() { - local -n _g="$1" + local var_name="$1" + local -n _g="$var_name" local -a args=() sorted_cats=() - IFS=$'\n' read -ra sorted_cats <<< "$(printf '%s\n' "${!_g[@]}" | sort)" + mapfile -t sorted_cats < <(printf '%s\n' "${!_g[@]}" | sort) for cat in "${sorted_cats[@]}"; do for tool in ${_g[$cat]}; do local state="OFF" @@ -204,7 +240,7 @@ _tui_whiptail() { done local _tmp; _tmp="$(mktemp)" whiptail --title "devsetup — Tool Selector" \ - --checklist "Space=toggle Enter=confirm Esc=cancel:" \ + --checklist "Space=toggle Enter=confirm Esc=cancel" \ 30 65 20 "${args[@]}" 2>"$_tmp" local rc=$? if [[ $rc -eq 0 ]]; then @@ -219,24 +255,24 @@ _tui_whiptail() { # ── dialog backend ──────────────────────────────────────────────────────────── # ============================================================================= _tui_dialog() { - local -n _g="$1" + local var_name="$1" + local -n _g="$var_name" local -a args=() sorted_cats=() - IFS=$'\n' read -ra sorted_cats <<< "$(printf '%s\n' "${!_g[@]}" | sort)" + mapfile -t sorted_cats < <(printf '%s\n' "${!_g[@]}" | sort) for cat in "${sorted_cats[@]}"; do - args+=("--- [$cat] ---" "" "off") for tool in ${_g[$cat]}; do local state="off" command -v "$tool" &>/dev/null && state="on" - args+=("$tool" "$cat" "$state") + args+=("$tool" "[$cat]" "$state") done done local _tmp; _tmp="$(mktemp)" dialog --title "devsetup — Tool Selector" \ - --checklist "Space to toggle, Enter to confirm:" \ + --checklist "Space=toggle Enter=confirm Esc=cancel" \ 30 65 20 "${args[@]}" 2>"$_tmp" local rc=$? if [[ $rc -eq 0 ]]; then - tr -d '"' < "$_tmp" | tr ' ' '\n' | grep -v '^---' | grep -v '^\[' \ + tr -d '"' < "$_tmp" | tr ' ' '\n' | grep -v '^\s*$' \ | tr '\n' ' ' > "$_tui_out_file" fi rm -f "$_tmp" @@ -247,14 +283,15 @@ _tui_dialog() { # ── fzf backend ─────────────────────────────────────────────────────────────── # ============================================================================= _tui_fzf() { - local -n _g="$1" + local var_name="$1" + local -n _g="$var_name" local -a all=() sorted_cats=() - IFS=$'\n' read -ra sorted_cats <<< "$(printf '%s\n' "${!_g[@]}" | sort)" + mapfile -t sorted_cats < <(printf '%s\n' "${!_g[@]}" | sort) for cat in "${sorted_cats[@]}"; do for tool in ${_g[$cat]}; do - local mark="" - command -v "$tool" &>/dev/null && mark=" ✔" - all+=("$(printf '%-12s %-20s%s' "[$cat]" "$tool" "$mark")") + local mark=" " + command -v "$tool" &>/dev/null && mark="✔" + all+=("$(printf '%-12s %-20s %s' "[$cat]" "$tool" "$mark")") done done printf '%s\n' "${all[@]}" \ @@ -267,22 +304,23 @@ _tui_fzf() { } # ============================================================================= -# ── Public: tui_select_tools ASSOC_VAR_NAME ───────────────────────────────── -# Sets $_tui_out_file (caller must declare it beforehand) -# Returns 0 if something selected, 1 if cancelled/empty +# ── Main dispatcher ─────────────────────────────────────────────────────────── +# Caller must set/export _tui_out_file before calling. +# Returns 0 if at least one tool selected, 1 if cancelled/empty. # ============================================================================= tui_select_tools() { local var_name="$1" - : > "$_tui_out_file" # truncate + : > "$_tui_out_file" # start empty case "$TUI_BACKEND" in - whiptail) _tui_whiptail "$var_name" ;; - dialog) _tui_dialog "$var_name" ;; + whiptail) _tui_whiptail "$var_name" || return 1 ;; + dialog) _tui_dialog "$var_name" || return 1 ;; fzf) _tui_fzf "$var_name" ;; - bash) _tui_bash "$var_name" ;; + bash) _tui_bash "$var_name" || return 1 ;; esac - local result; result="$(cat "$_tui_out_file" 2>/dev/null || echo '')" - result="${result// /}" # check if empty after stripping spaces + # Verify something was actually written + local result; result="$(cat "$_tui_out_file" 2>/dev/null)" + result="${result//[[:space:]]/}" # strip all whitespace [[ -n "$result" ]] }