diff --git a/scripts/menus/menu_Helper_Scripts.sh b/scripts/menus/menu_Helper_Scripts.sh index b429f9ad..50ace89e 100644 --- a/scripts/menus/menu_Helper_Scripts.sh +++ b/scripts/menus/menu_Helper_Scripts.sh @@ -6,8 +6,8 @@ # Author : MacRimi # Copyright : (c) 2024 MacRimi # License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE) -# Version : 1.2 -# Last Updated: 14/11/2025 +# Version : 1.3 +# Last Updated: 14/03/2025 # ========================================================== # Description: # This script provides a simple and efficient way to access and execute Proxmox VE scripts @@ -33,8 +33,9 @@ load_language initialize_cache # ========================================================== +# New unified cache — categories and mirror URLs are embedded, +# metadata.json is no longer needed. HELPERS_JSON_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/refs/heads/main/json/helpers_cache.json" -METADATA_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/frontend/public/json/metadata.json" for cmd in curl jq dialog; do if ! command -v "$cmd" >/dev/null; then @@ -44,63 +45,78 @@ for cmd in curl jq dialog; do done CACHE_JSON=$(curl -s "$HELPERS_JSON_URL") -META_JSON=$(curl -s "$METADATA_URL") +# Validate that the JSON loaded correctly +if ! echo "$CACHE_JSON" | jq -e 'if type == "array" and length > 0 then true else false end' >/dev/null 2>&1; then + dialog --title "Helper Scripts" \ + --msgbox "Error: Could not load helpers cache.\nCheck your internet connection and try again.\n\nURL: $HELPERS_JSON_URL" 10 70 + exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh" +fi + +# --------------------------------------------------------------------------- +# Build category map directly from the cache (id → name). +# Uses transpose to pair categories[] and category_names[] arrays — no +# dependency on metadata.json, which no longer exists upstream. +# --------------------------------------------------------------------------- declare -A CATEGORY_NAMES -while read -r id name; do - CATEGORY_NAMES[$id]="$name" -done < <(echo "$META_JSON" | jq -r '.categories[] | "\(.id)\t\(.name)"') +while IFS=$'\t' read -r id name; do + [[ -n "$id" && -n "$name" ]] && CATEGORY_NAMES["$id"]="$name" +done < <(echo "$CACHE_JSON" | jq -r ' + [.[] | [.categories, .category_names] | transpose[] | @tsv] + | unique[]') +# Count scripts per category (deduplicated by slug) declare -A CATEGORY_COUNT -for id in $(echo "$CACHE_JSON" | jq -r ' - group_by(.slug) | map(.[0])[] | .categories[]'); do +while read -r id; do ((CATEGORY_COUNT[$id]++)) -done +done < <(echo "$CACHE_JSON" | jq -r ' + group_by(.slug) | map(.[0])[] | .categories[]') +# --------------------------------------------------------------------------- +# Type label — updated to match new type values (lxc instead of ct) +# --------------------------------------------------------------------------- get_type_label() { local type="$1" case "$type" in - ct) echo $'\Z1LXC\Zn' ;; - vm) echo $'\Z4VM\Zn' ;; - pve) echo $'\Z3PVE\Zn' ;; - addon) echo $'\Z2ADDON\Zn' ;; - *) echo $'\Z7GEN\Zn' ;; + lxc) echo $'\Z1LXC\Zn' ;; + vm) echo $'\Z4VM\Zn' ;; + pve) echo $'\Z3PVE\Zn' ;; + addon) echo $'\Z2ADDON\Zn' ;; + turnkey) echo $'\Z5TK\Zn' ;; + *) echo $'\Z7GEN\Zn' ;; esac } +# --------------------------------------------------------------------------- +# Download and execute a script URL, with optional mirror fallback +# --------------------------------------------------------------------------- download_script() { local url="$1" - local fallback_pve="${url/misc\/tools\/pve}" - local fallback_addon="${url/misc\/tools\/addon}" - local fallback_copydata="${url/misc\/tools\/copy-data}" if curl --silent --head --fail "$url" >/dev/null; then - bash <(curl -s "$url") - elif curl --silent --head --fail "$fallback_pve" >/dev/null; then - bash <(curl -s "$fallback_pve") - elif curl --silent --head --fail "$fallback_addon" >/dev/null; then - bash <(curl -s "$fallback_addon") - elif curl --silent --head --fail "$fallback_copydata" >/dev/null; then - bash <(curl -s "$fallback_copydata") + bash <(curl -s "$url") else - dialog --title "Helper Scripts" --msgbox "Error: Failed to download the script." 12 70 + dialog --title "Helper Scripts" --msgbox "$(translate "Error: Failed to download the script.")" 8 70 fi } RETURN_TO_MAIN=false +# --------------------------------------------------------------------------- +# Format default credentials for display +# --------------------------------------------------------------------------- format_credentials() { local script_info="$1" local credentials_info="" - + local has_credentials has_credentials=$(echo "$script_info" | base64 --decode | jq -r 'has("default_credentials")') - + if [[ "$has_credentials" == "true" ]]; then local username password username=$(echo "$script_info" | base64 --decode | jq -r '.default_credentials.username // empty') password=$(echo "$script_info" | base64 --decode | jq -r '.default_credentials.password // empty') - + if [[ -n "$username" && -n "$password" ]]; then credentials_info="Username: $username | Password: $password" elif [[ -n "$username" ]]; then @@ -109,30 +125,41 @@ format_credentials() { credentials_info="Password: $password" fi fi - + echo "$credentials_info" } +# --------------------------------------------------------------------------- +# Run a script identified by its slug. +# +# A slug can have multiple entries when a script supports several OS variants +# (e.g. Debian + Alpine). Each entry carries its own script_url / mirror and +# the os field already normalised to lowercase by generate_helpers_cache.py. +# The menu lets the user pick OS variant × source (GitHub / Mirror). +# --------------------------------------------------------------------------- run_script_by_slug() { local slug="$1" local -a script_infos - mapfile -t script_infos < <(echo "$CACHE_JSON" | jq -r --arg slug "$slug" '.[] | select(.slug == $slug) | @base64') + mapfile -t script_infos < <(echo "$CACHE_JSON" | jq -r --arg slug "$slug" \ + '.[] | select(.slug == $slug) | @base64') if [[ ${#script_infos[@]} -eq 0 ]]; then - dialog --title "Helper Scripts" --msgbox "Error: No script data found for slug: $slug" 8 60 + dialog --title "Helper Scripts" \ + --msgbox "$(translate "Error: No script data found for slug:") $slug" 8 60 return fi - decode() { - echo "$1" | base64 --decode | jq -r "$2" - } + decode() { echo "$1" | base64 --decode | jq -r "$2"; } local first="${script_infos[0]}" - local name desc notes + local name desc notes port website name=$(decode "$first" ".name") desc=$(decode "$first" ".desc") - notes=$(decode "$first" ".notes | join(\"\n\")") + notes=$(decode "$first" '.notes | join("\n")') + port=$(decode "$first" ".port // 0") + website=$(decode "$first" ".website // empty") + # Build notes block local notes_dialog="" if [[ -n "$notes" ]]; then while IFS= read -r line; do @@ -145,18 +172,21 @@ run_script_by_slug() { local credentials credentials=$(format_credentials "$first") - local msg="\Zb\Z4Descripción:\Zn\n$desc" - [[ -n "$notes_dialog" ]] && msg+="\n\n\Zb\Z4Notes:\Zn\n$notes_dialog" - [[ -n "$credentials" ]] && msg+="\n\n\Zb\Z4Default Credentials:\Zn\n$credentials" - - # Add separator before menu options + # Build info message + local msg="\Zb\Z4$(translate "Description"):\Zn\n$desc" + [[ -n "$notes_dialog" ]] && msg+="\n\n\Zb\Z4$(translate "Notes"):\Zn\n$notes_dialog" + [[ -n "$credentials" ]] && msg+="\n\n\Zb\Z4$(translate "Default Credentials"):\Zn\n$credentials" + [[ "$port" -gt 0 ]] && msg+="\n\n\Zb\Z4$(translate "Default Port"):\Zn $port" + [[ -n "$website" ]] && msg+="\n\Zb\Z4$(translate "Website"):\Zn $website" + msg+="\n\n$(translate "Choose how to run the script:"):" + # Build menu: one or two entries per script_info (GH + optional Mirror) declare -a MENU_OPTS=() local idx=0 for s in "${script_infos[@]}"; do local os script_url script_url_mirror script_name - os=$(decode "$s" ".os // empty") + os=$(decode "$s" '.os // empty') [[ -z "$os" ]] && os="$(translate "default")" script_name=$(decode "$s" ".name") script_url=$(decode "$s" ".script_url") @@ -196,7 +226,8 @@ run_script_by_slug() { if [[ -n "$mirror_url" ]]; then download_script "$mirror_url" else - dialog --title "Helper Scripts" --msgbox "$(translate "Mirror URL not available for this script.")" 8 60 + dialog --title "Helper Scripts" \ + --msgbox "$(translate "Mirror URL not available for this script.")" 8 60 RETURN_TO_MAIN=false return fi @@ -206,10 +237,10 @@ run_script_by_slug() { echo if [[ -n "$desc" || -n "$notes" || -n "$credentials" ]]; then - echo -e "$TAB\e[1;36mScript Information:\e[0m" + echo -e "$TAB\e[1;36m$(translate "Script Information"):\e[0m" if [[ -n "$notes" ]]; then - echo -e "$TAB\e[1;33mNotes:\e[0m" + echo -e "$TAB\e[1;33m$(translate "Notes"):\e[0m" while IFS= read -r line; do [[ -z "$line" ]] && continue echo -e "$TAB• $line" @@ -218,26 +249,30 @@ run_script_by_slug() { fi if [[ -n "$credentials" ]]; then - echo -e "$TAB\e[1;32mDefault Credentials:\e[0m" + echo -e "$TAB\e[1;32m$(translate "Default Credentials"):\e[0m" echo "$TAB$credentials" echo fi fi - msg_success "Press Enter to return to the main menu..." + msg_success "$(translate "Press Enter to return to the main menu...")" read -r RETURN_TO_MAIN=true } +# --------------------------------------------------------------------------- +# Search / filter scripts by name or description +# --------------------------------------------------------------------------- search_and_filter_scripts() { local search_term="" - + while true; do - search_term=$(dialog --inputbox "Enter search term (leave empty to show all scripts):" \ - 8 65 "$search_term" 3>&1 1>&2 2>&3) - + search_term=$(dialog --inputbox \ + "$(translate "Enter search term (leave empty to show all scripts):"):" \ + 8 65 "$search_term" 3>&1 1>&2 2>&3) + [[ $? -ne 0 ]] && return - + local filtered_json if [[ -z "$search_term" ]]; then filtered_json="$CACHE_JSON" @@ -250,12 +285,14 @@ search_and_filter_scripts() { (.desc | ascii_downcase | contains($term)) )]') fi - + local count count=$(echo "$filtered_json" | jq 'group_by(.slug) | length') - - if [[ $count -eq 0 ]]; then - dialog --msgbox "No scripts found for: '$search_term'\n\nTry a different search term." 8 50 + + if [[ "$count" -eq 0 ]]; then + dialog --msgbox \ + "$(translate "No scripts found for:") '$search_term'\n\n$(translate "Try a different search term.")" \ + 8 50 continue fi @@ -263,43 +300,41 @@ search_and_filter_scripts() { declare -A index_to_slug local menu_items=() local i=1 - + while IFS=$'\t' read -r slug name type; do index_to_slug[$i]="$slug" local label label=$(get_type_label "$type") local padded_name padded_name=$(printf "%-42s" "$name") - local entry="$padded_name $label" - menu_items+=("$i" "$entry") + menu_items+=("$i" "$padded_name $label") ((i++)) done < <(echo "$filtered_json" | jq -r ' - group_by(.slug) | map(.[0]) | sort_by(.name)[] | [.slug, .name, .type] | @tsv') - + group_by(.slug) | map(.[0]) | sort_by(.name)[] + | [.slug, .name, .type] | @tsv') + menu_items+=("" "") - menu_items+=("new_search" "New Search") - menu_items+=("show_all" "Show All Scripts") - - local title="Search Results" + menu_items+=("new_search" "$(translate "New Search")") + menu_items+=("show_all" "$(translate "Show All Scripts")") + + local title if [[ -n "$search_term" ]]; then - title="Search Results for: '$search_term' ($count found)" + title="$(translate "Search Results for:") '$search_term' ($count $(translate "found"))" else - title="All Available Scripts ($count total)" + title="$(translate "All Available Scripts") ($count $(translate "total"))" fi - + local selected selected=$(dialog --colors --backtitle "ProxMenux" \ --title "$title" \ - --menu "Select a script or action:" \ + --menu "$(translate "Select a script or action:"):" \ 22 75 15 "${menu_items[@]}" 3>&1 1>&2 2>&3) - - if [[ $? -ne 0 ]]; then - return - fi - + + [[ $? -ne 0 ]] && return + case "$selected" in "new_search") - break + break ;; "show_all") search_term="" @@ -308,7 +343,7 @@ search_and_filter_scripts() { continue ;; "back"|"") - return + return ;; *) if [[ -n "${index_to_slug[$selected]}" ]]; then @@ -321,48 +356,64 @@ search_and_filter_scripts() { done } +# --------------------------------------------------------------------------- +# Main loop — category list built from embedded category data. +# We map scriptcatXXXXX IDs to short numeric indices so dialog doesn't show +# the long ID string as the visible tag in the menu column. +# --------------------------------------------------------------------------- while true; do MENU_ITEMS=() - - MENU_ITEMS+=("search" "Search/Filter Scripts") + MENU_ITEMS+=("search" "$(translate "Search/Filter Scripts")") MENU_ITEMS+=("" "") - - for id in $(printf "%s\n" "${!CATEGORY_COUNT[@]}" | sort -n); do + + # Map scriptcatXXXXX IDs to short numeric indices (1, 2, 3…) so dialog + # doesn't render the long ID string as the visible tag column. + declare -A CAT_IDX_TO_ID + local_idx=1 + for id in $(printf "%s\n" "${!CATEGORY_COUNT[@]}" | sort); do + CAT_IDX_TO_ID[$local_idx]="$id" name="${CATEGORY_NAMES[$id]:-Category $id}" count="${CATEGORY_COUNT[$id]}" padded_name=$(printf "%-35s" "$name") padded_count=$(printf "(%2d)" "$count") - MENU_ITEMS+=("$id" "$padded_name $padded_count") + MENU_ITEMS+=("$local_idx" "$padded_name $padded_count") + ((local_idx++)) done - SELECTED=$(dialog --backtitle "ProxMenux" --title "Proxmox VE Helper-Scripts" --menu \ - "Select a category or search for scripts:" 20 70 14 \ - "${MENU_ITEMS[@]}" 3>&1 1>&2 2>&3) || { - dialog --clear --title "ProxMenux" \ - --msgbox "\n\n$(translate "Visit the website to discover more scripts, stay updated with the latest updates, and support the project:")\n\nhttps://community-scripts.github.io/ProxmoxVE" 15 70 + SELECTED_IDX=$(dialog --backtitle "ProxMenux" \ + --title "Proxmox VE Helper-Scripts" \ + --menu "$(translate "Select a category or search for scripts:"):" \ + 20 70 14 "${MENU_ITEMS[@]}" 3>&1 1>&2 2>&3) || { + dialog --clear --title "ProxMenux" \ + --msgbox "\n\n$(translate "Visit the website to discover more scripts, stay updated with the latest updates, and support the project:")\n\nhttps://community-scripts.github.io/ProxmoxVE" 15 70 exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh" } - - if [[ "$SELECTED" == "search" ]]; then + + if [[ "$SELECTED_IDX" == "search" ]]; then search_and_filter_scripts continue fi + # Resolve numeric index back to the real category ID + SELECTED="${CAT_IDX_TO_ID[$SELECTED_IDX]}" + [[ -z "$SELECTED" ]] && continue + + # ---- Scripts within the selected category -------------------------------- while true; do declare -A INDEX_TO_SLUG SCRIPTS=() i=1 + while IFS=$'\t' read -r slug name type; do INDEX_TO_SLUG[$i]="$slug" label=$(get_type_label "$type") padded_name=$(printf "%-42s" "$name") - entry="$padded_name $label" - SCRIPTS+=("$i" "$entry") + SCRIPTS+=("$i" "$padded_name $label") ((i++)) - done < <(echo "$CACHE_JSON" | jq -r --argjson id "$SELECTED" ' + done < <(echo "$CACHE_JSON" | jq -r --arg id "$SELECTED" ' [ - .[] - | select(.categories | index($id)) + .[] + | select(.categories | index($id)) | {slug, name, type} ] | group_by(.slug) @@ -371,13 +422,14 @@ while true; do | [.slug, .name, .type] | @tsv') - SCRIPT_INDEX=$(dialog --colors --backtitle "ProxMenux" --title "Scripts in ${CATEGORY_NAMES[$SELECTED]}" --menu \ - "Choose a script to execute:" 20 70 14 \ - "${SCRIPTS[@]}" 3>&1 1>&2 2>&3) || break + SCRIPT_INDEX=$(dialog --colors --backtitle "ProxMenux" \ + --title "$(translate "Scripts in") ${CATEGORY_NAMES[$SELECTED]}" \ + --menu "$(translate "Choose a script to execute:"):" \ + 20 70 14 "${SCRIPTS[@]}" 3>&1 1>&2 2>&3) || break SCRIPT_SELECTED="${INDEX_TO_SLUG[$SCRIPT_INDEX]}" run_script_by_slug "$SCRIPT_SELECTED" - + [[ "$RETURN_TO_MAIN" == true ]] && { RETURN_TO_MAIN=false; break; } done done