2025-02-09 14:29:28 +01:00
#!/bin/bash
# ==========================================================
2025-09-10 18:47:55 +02:00
# ProxMenux - A menu-driven script for Proxmox VE management
2025-02-09 14:29:28 +01:00
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
2026-01-19 17:15:00 +01:00
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
2026-03-14 18:16:55 +01:00
# Version : 1.3
# Last Updated: 14/03/2025
2025-02-09 14:29:28 +01:00
# ==========================================================
# Description:
2025-06-06 17:29:06 +02:00
# This script provides a simple and efficient way to access and execute Proxmox VE scripts
2025-02-09 14:29:28 +01:00
# from the Community Scripts project (https://community-scripts.github.io/ProxmoxVE/).
#
# It serves as a convenient tool to run key automation scripts that simplify system management,
# continuing the great work and legacy of tteck in making Proxmox VE more accessible.
# A streamlined solution for executing must-have tools in Proxmox VE.
# ==========================================================
# Configuration ============================================
2025-11-03 01:06:04 +00:00
LOCAL_SCRIPTS = "/usr/local/share/proxmenux/scripts"
2025-02-09 14:29:28 +01:00
BASE_DIR = "/usr/local/share/proxmenux"
UTILS_FILE = " $BASE_DIR /utils.sh "
VENV_PATH = "/opt/googletrans-env"
if [ [ -f " $UTILS_FILE " ] ] ; then
source " $UTILS_FILE "
fi
load_language
initialize_cache
# ==========================================================
2026-03-14 18:16:55 +01:00
# New unified cache — categories and mirror URLs are embedded,
# metadata.json is no longer needed.
2025-06-06 17:29:06 +02:00
HELPERS_JSON_URL = "https://raw.githubusercontent.com/MacRimi/ProxMenux/refs/heads/main/json/helpers_cache.json"
for cmd in curl jq dialog; do
if ! command -v " $cmd " >/dev/null; then
echo " Missing required command: $cmd "
exit 1
fi
done
CACHE_JSON = $( curl -s " $HELPERS_JSON_URL " )
2026-03-14 18:16:55 +01:00
# 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.
# ---------------------------------------------------------------------------
2025-06-06 17:29:06 +02:00
declare -A CATEGORY_NAMES
2026-03-14 18:16:55 +01:00
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[ ] ' )
2025-06-06 17:29:06 +02:00
2026-03-14 18:16:55 +01:00
# Count scripts per category (deduplicated by slug)
2025-06-06 17:29:06 +02:00
declare -A CATEGORY_COUNT
2026-03-14 18:16:55 +01:00
while read -r id; do
2025-06-06 17:29:06 +02:00
( ( CATEGORY_COUNT[ $id ] ++) )
2026-03-14 18:16:55 +01:00
done < <( echo " $CACHE_JSON " | jq -r '
group_by( .slug) | map( .[ 0] ) [ ] | .categories[ ] ' )
2025-06-06 17:29:06 +02:00
2026-03-14 18:16:55 +01:00
# ---------------------------------------------------------------------------
# Type label — updated to match new type values (lxc instead of ct)
# ---------------------------------------------------------------------------
2025-06-06 17:29:06 +02:00
get_type_label( ) {
local type = " $1 "
case " $type " in
2026-03-14 18:16:55 +01:00
lxc) echo $'\Z1LXC\Zn' ; ;
vm) echo $'\Z4VM\Zn' ; ;
pve) echo $'\Z3PVE\Zn' ; ;
addon) echo $'\Z2ADDON\Zn' ; ;
turnkey) echo $'\Z5TK\Zn' ; ;
*) echo $'\Z7GEN\Zn' ; ;
2025-06-06 17:29:06 +02:00
esac
}
2025-02-09 14:29:28 +01:00
2026-03-14 18:16:55 +01:00
# ---------------------------------------------------------------------------
# Download and execute a script URL, with optional mirror fallback
# ---------------------------------------------------------------------------
2025-04-27 19:05:36 +02:00
download_script( ) {
2025-06-06 17:29:06 +02:00
local url = " $1 "
if curl --silent --head --fail " $url " >/dev/null; then
2026-03-14 18:16:55 +01:00
bash <( curl -s " $url " )
2025-06-06 17:29:06 +02:00
else
2026-03-14 18:16:55 +01:00
dialog --title "Helper Scripts" --msgbox " $( translate "Error: Failed to download the script." ) " 8 70
2025-06-06 17:29:06 +02:00
fi
}
2025-04-27 19:05:36 +02:00
2025-06-06 17:29:06 +02:00
RETURN_TO_MAIN = false
2026-03-14 18:16:55 +01:00
# ---------------------------------------------------------------------------
# Format default credentials for display
# ---------------------------------------------------------------------------
2025-06-06 17:29:06 +02:00
format_credentials( ) {
local script_info = " $1 "
local credentials_info = ""
2026-03-14 18:16:55 +01:00
2025-06-06 17:29:06 +02:00
local has_credentials
has_credentials = $( echo " $script_info " | base64 --decode | jq -r 'has("default_credentials")' )
2026-03-14 18:16:55 +01:00
2025-06-06 17:29:06 +02:00
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' )
2026-03-14 18:16:55 +01:00
2025-06-06 17:29:06 +02:00
if [ [ -n " $username " && -n " $password " ] ] ; then
credentials_info = " Username: $username | Password: $password "
elif [ [ -n " $username " ] ] ; then
credentials_info = " Username: $username "
elif [ [ -n " $password " ] ] ; then
credentials_info = " Password: $password "
2025-04-27 19:05:36 +02:00
fi
2025-06-06 17:29:06 +02:00
fi
2026-03-14 18:16:55 +01:00
2025-06-06 17:29:06 +02:00
echo " $credentials_info "
2025-04-27 19:05:36 +02:00
}
2026-03-14 18:16:55 +01:00
# ---------------------------------------------------------------------------
# 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).
# ---------------------------------------------------------------------------
2025-06-06 17:29:06 +02:00
run_script_by_slug( ) {
local slug = " $1 "
2025-11-14 18:54:32 +01:00
local -a script_infos
2026-03-14 18:16:55 +01:00
mapfile -t script_infos < <( echo " $CACHE_JSON " | jq -r --arg slug " $slug " \
'.[] | select(.slug == $slug) | @base64' )
2025-11-14 18:54:32 +01:00
if [ [ ${# script_infos [@] } -eq 0 ] ] ; then
2026-03-14 18:16:55 +01:00
dialog --title "Helper Scripts" \
--msgbox " $( translate "Error: No script data found for slug:" ) $slug " 8 60
2025-11-14 18:54:32 +01:00
return
fi
2025-06-06 17:29:06 +02:00
2026-03-14 18:16:55 +01:00
decode( ) { echo " $1 " | base64 --decode | jq -r " $2 " ; }
2025-06-06 17:29:06 +02:00
2025-11-14 18:54:32 +01:00
local first = " ${ script_infos [0] } "
2026-03-14 18:16:55 +01:00
local name desc notes port website
2025-11-14 18:54:32 +01:00
name = $( decode " $first " ".name" )
desc = $( decode " $first " ".desc" )
2026-03-14 18:16:55 +01:00
notes = $( decode " $first " '.notes | join("\n")' )
port = $( decode " $first " ".port // 0" )
website = $( decode " $first " ".website // empty" )
2025-06-06 17:29:06 +02:00
2026-03-14 18:16:55 +01:00
# Build notes block
2025-06-06 17:29:06 +02:00
local notes_dialog = ""
if [ [ -n " $notes " ] ] ; then
while IFS = read -r line; do
2025-11-14 18:54:32 +01:00
[ [ -z " $line " ] ] && continue
2025-06-06 17:29:06 +02:00
notes_dialog += " • $line \n "
done <<< " $notes "
2025-11-14 18:54:32 +01:00
notes_dialog = " ${ notes_dialog % \\ n } "
2025-06-06 17:29:06 +02:00
fi
local credentials
2025-11-14 18:54:32 +01:00
credentials = $( format_credentials " $first " )
2025-02-09 14:29:28 +01:00
2026-03-14 18:16:55 +01:00
# Build info message
2026-04-18 09:06:01 +02:00
local msg = " \Zb\Z4 $( translate "Description" ) :\Zn\n $desc "
if [ [ -n " $notes " ] ] ; then
local notes_short = ""
local char_count = 0
local max_chars = 400
while IFS = read -r line; do
[ [ -z " $line " ] ] && continue
char_count = $(( char_count + ${# line } ))
if [ [ $char_count -lt $max_chars ] ] ; then
notes_short += " • $line \n "
else
notes_short += "...\n"
break
fi
done <<< " $notes "
msg += " \n\n\Zb\Z4 $( translate "Notes" ) :\Zn\n $notes_short "
fi
2026-03-14 18:16:55 +01:00
[ [ -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 "
2026-04-18 09:06:01 +02:00
msg += " \n\n $( translate "Choose how to run the script:" ) "
2025-11-14 18:54:32 +01:00
2026-03-14 18:16:55 +01:00
# Build menu: one or two entries per script_info (GH + optional Mirror)
2025-11-14 18:54:32 +01:00
declare -a MENU_OPTS = ( )
local idx = 0
for s in " ${ script_infos [@] } " ; do
local os script_url script_url_mirror script_name
2026-03-14 18:16:55 +01:00
os = $( decode " $s " '.os // empty' )
2025-11-14 18:54:32 +01:00
[ [ -z " $os " ] ] && os = " $( translate "default" ) "
script_name = $( decode " $s " ".name" )
script_url = $( decode " $s " ".script_url" )
script_url_mirror = $( decode " $s " ".script_url_mirror // empty" )
MENU_OPTS += ( " ${ idx } _GH " " $os | $script_name | GitHub " )
if [ [ -n " $script_url_mirror " ] ] ; then
MENU_OPTS += ( " ${ idx } _MR " " $os | $script_name | Mirror " )
fi
2025-02-09 14:29:28 +01:00
2025-11-14 18:54:32 +01:00
( ( idx++) )
done
2025-06-06 17:29:06 +02:00
2025-11-14 18:54:32 +01:00
local choice
choice = $( dialog --clear --colors --backtitle "ProxMenux" \
--title " $name " \
--menu " $msg " 28 80 6 \
" ${ MENU_OPTS [@] } " 3>& 1 1>& 2 2>& 3)
2025-06-06 17:29:06 +02:00
2025-11-14 18:54:32 +01:00
if [ [ $? -ne 0 || -z " $choice " ] ] ; then
RETURN_TO_MAIN = false
return
fi
2025-06-06 17:29:06 +02:00
2025-11-14 18:54:32 +01:00
local sel_idx sel_src
IFS = "_" read -r sel_idx sel_src <<< " $choice "
2025-06-06 17:29:06 +02:00
2025-11-14 18:54:32 +01:00
local selected = " ${ script_infos [ $sel_idx ] } "
local gh_url mirror_url
gh_url = $( decode " $selected " ".script_url" )
mirror_url = $( decode " $selected " ".script_url_mirror // empty" )
2025-06-06 17:29:06 +02:00
2025-11-14 18:54:32 +01:00
if [ [ " $sel_src " = = "GH" ] ] ; then
download_script " $gh_url "
elif [ [ " $sel_src " = = "MR" ] ] ; then
if [ [ -n " $mirror_url " ] ] ; then
download_script " $mirror_url "
else
2026-03-14 18:16:55 +01:00
dialog --title "Helper Scripts" \
--msgbox " $( translate "Mirror URL not available for this script." ) " 8 60
2025-11-14 18:54:32 +01:00
RETURN_TO_MAIN = false
return
2025-06-06 17:29:06 +02:00
fi
2025-11-14 18:54:32 +01:00
fi
echo
echo
if [ [ -n " $desc " || -n " $notes " || -n " $credentials " ] ] ; then
2026-03-14 18:16:55 +01:00
echo -e " $TAB \e[1;36m $( translate "Script Information" ) :\e[0m "
2025-06-06 17:29:06 +02:00
2025-11-14 18:54:32 +01:00
if [ [ -n " $notes " ] ] ; then
2026-03-14 18:16:55 +01:00
echo -e " $TAB \e[1;33m $( translate "Notes" ) :\e[0m "
2025-11-14 18:54:32 +01:00
while IFS = read -r line; do
[ [ -z " $line " ] ] && continue
echo -e " $TAB • $line "
done <<< " $notes "
echo
fi
if [ [ -n " $credentials " ] ] ; then
2026-03-14 18:16:55 +01:00
echo -e " $TAB \e[1;32m $( translate "Default Credentials" ) :\e[0m "
2025-11-14 18:54:32 +01:00
echo " $TAB $credentials "
echo
fi
2025-06-06 17:29:06 +02:00
fi
2026-03-14 18:16:55 +01:00
msg_success " $( translate "Press Enter to return to the main menu..." ) "
2025-11-14 18:54:32 +01:00
read -r
RETURN_TO_MAIN = true
}
2025-06-06 17:29:06 +02:00
2026-03-14 18:16:55 +01:00
# ---------------------------------------------------------------------------
# Search / filter scripts by name or description
# ---------------------------------------------------------------------------
2025-06-06 17:29:06 +02:00
search_and_filter_scripts( ) {
local search_term = ""
2026-03-14 18:16:55 +01:00
2025-06-06 17:29:06 +02:00
while true; do
2026-03-14 18:16:55 +01:00
search_term = $( dialog --inputbox \
" $( translate "Enter search term (leave empty to show all scripts):" ) : " \
8 65 " $search_term " 3>& 1 1>& 2 2>& 3)
2025-06-06 17:29:06 +02:00
[ [ $? -ne 0 ] ] && return
2026-03-14 18:16:55 +01:00
2025-06-06 17:29:06 +02:00
local filtered_json
if [ [ -z " $search_term " ] ] ; then
filtered_json = " $CACHE_JSON "
else
local search_lower
search_lower = $( echo " $search_term " | tr '[:upper:]' '[:lower:]' )
filtered_json = $( echo " $CACHE_JSON " | jq --arg term " $search_lower " '
[ .[ ] | select (
( .name | ascii_downcase | contains( $term ) ) or
( .desc | ascii_downcase | contains( $term ) )
) ] ' )
fi
2026-03-14 18:16:55 +01:00
2025-06-06 17:29:06 +02:00
local count
2025-11-14 18:54:32 +01:00
count = $( echo " $filtered_json " | jq 'group_by(.slug) | length' )
2026-03-14 18:16:55 +01:00
if [ [ " $count " -eq 0 ] ] ; then
dialog --msgbox \
" $( translate "No scripts found for:" ) ' $search_term '\n\n $( translate "Try a different search term." ) " \
8 50
2025-06-06 17:29:06 +02:00
continue
fi
2025-02-09 14:29:28 +01:00
2025-02-09 15:56:14 +01:00
while true; do
2025-06-06 17:29:06 +02:00
declare -A index_to_slug
local menu_items = ( )
local i = 1
2026-03-14 18:16:55 +01:00
2025-06-06 17:29:06 +02:00
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 " )
2026-03-14 18:16:55 +01:00
menu_items += ( " $i " " $padded_name $label " )
2025-06-06 17:29:06 +02:00
( ( i++) )
done < <( echo " $filtered_json " | jq -r '
2026-03-14 18:16:55 +01:00
group_by( .slug) | map( .[ 0] ) | sort_by( .name) [ ]
| [ .slug, .name, .type] | @tsv' )
2025-06-06 17:29:06 +02:00
menu_items += ( "" "" )
2026-03-14 18:16:55 +01:00
menu_items += ( "new_search" " $( translate "New Search" ) " )
menu_items += ( "show_all" " $( translate "Show All Scripts" ) " )
local title
2025-06-06 17:29:06 +02:00
if [ [ -n " $search_term " ] ] ; then
2026-03-14 18:16:55 +01:00
title = " $( translate "Search Results for:" ) ' $search_term ' ( $count $( translate "found" ) ) "
2025-06-06 17:29:06 +02:00
else
2026-03-14 18:16:55 +01:00
title = " $( translate "All Available Scripts" ) ( $count $( translate "total" ) ) "
2025-06-06 17:29:06 +02:00
fi
2026-03-14 18:16:55 +01:00
2025-06-06 17:29:06 +02:00
local selected
selected = $( dialog --colors --backtitle "ProxMenux" \
--title " $title " \
2026-03-14 18:16:55 +01:00
--menu " $( translate "Select a script or action:" ) : " \
2025-06-06 17:29:06 +02:00
22 75 15 " ${ menu_items [@] } " 3>& 1 1>& 2 2>& 3)
2026-03-14 18:16:55 +01:00
[ [ $? -ne 0 ] ] && return
2025-06-06 17:29:06 +02:00
case " $selected " in
"new_search" )
2026-03-14 18:16:55 +01:00
break
2025-06-06 17:29:06 +02:00
; ;
"show_all" )
search_term = ""
filtered_json = " $CACHE_JSON "
2025-11-14 18:54:32 +01:00
count = $( echo " $filtered_json " | jq 'group_by(.slug) | length' )
2025-06-06 17:29:06 +02:00
continue
; ;
"back" | "" )
2026-03-14 18:16:55 +01:00
return
2025-06-06 17:29:06 +02:00
; ;
*)
if [ [ -n " ${ index_to_slug [ $selected ] } " ] ] ; then
run_script_by_slug " ${ index_to_slug [ $selected ] } "
[ [ " $RETURN_TO_MAIN " = = true ] ] && { RETURN_TO_MAIN = false; return ; }
fi
; ;
esac
2025-02-09 14:29:28 +01:00
done
2025-06-06 17:29:06 +02:00
done
2025-02-09 14:29:28 +01:00
}
2026-03-14 18:16:55 +01:00
# ---------------------------------------------------------------------------
# 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.
# ---------------------------------------------------------------------------
2025-06-06 17:29:06 +02:00
while true; do
MENU_ITEMS = ( )
2026-03-14 18:16:55 +01:00
MENU_ITEMS += ( "search" " $( translate "Search/Filter Scripts" ) " )
2025-06-06 17:29:06 +02:00
MENU_ITEMS += ( "" "" )
2026-03-14 18:16:55 +01:00
# 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 "
2025-06-06 17:29:06 +02:00
name = " ${ CATEGORY_NAMES [ $id ] :- Category $id } "
count = " ${ CATEGORY_COUNT [ $id ] } "
padded_name = $( printf "%-35s" " $name " )
padded_count = $( printf "(%2d)" " $count " )
2026-03-14 18:16:55 +01:00
MENU_ITEMS += ( " $local_idx " " $padded_name $padded_count " )
( ( local_idx++) )
2025-06-06 17:29:06 +02:00
done
2025-02-25 19:14:55 +01:00
2026-03-14 18:16:55 +01:00
SELECTED_IDX = $( dialog --backtitle "ProxMenux" \
--title "Proxmox VE Helper-Scripts" \
--menu " $( translate "Select a category or search for scripts:" ) : " \
2026-04-18 09:06:01 +02:00
22 75 15 " ${ MENU_ITEMS [@] } " 3>& 1 1>& 2 2>& 3) || {
2026-03-14 18:16:55 +01:00
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
2025-11-03 12:32:19 +00:00
exec bash " $LOCAL_SCRIPTS /menus/main_menu.sh "
2025-06-06 17:29:06 +02:00
}
2026-03-14 18:16:55 +01:00
if [ [ " $SELECTED_IDX " = = "search" ] ] ; then
2025-06-06 17:29:06 +02:00
search_and_filter_scripts
continue
fi
2026-03-14 18:16:55 +01:00
# Resolve numeric index back to the real category ID
SELECTED = " ${ CAT_IDX_TO_ID [ $SELECTED_IDX ] } "
[ [ -z " $SELECTED " ] ] && continue
# ---- Scripts within the selected category --------------------------------
2025-06-06 17:29:06 +02:00
while true; do
declare -A INDEX_TO_SLUG
SCRIPTS = ( )
i = 1
2026-03-14 18:16:55 +01:00
2025-06-06 17:29:06 +02:00
while IFS = $'\t' read -r slug name type; do
INDEX_TO_SLUG[ $i ] = " $slug "
label = $( get_type_label " $type " )
padded_name = $( printf "%-42s" " $name " )
2026-03-14 18:16:55 +01:00
SCRIPTS += ( " $i " " $padded_name $label " )
2025-06-06 17:29:06 +02:00
( ( i++) )
2026-03-14 18:16:55 +01:00
done < <( echo " $CACHE_JSON " | jq -r --arg id " $SELECTED " '
2025-11-14 18:54:32 +01:00
[
2026-03-14 18:16:55 +01:00
.[ ]
| select ( .categories | index( $id ) )
2025-11-14 18:54:32 +01:00
| { slug, name, type}
]
| group_by( .slug)
| map( .[ 0] )
| sort_by( .name) [ ]
| [ .slug, .name, .type]
| @tsv' )
2025-06-06 17:29:06 +02:00
2026-03-14 18:16:55 +01:00
SCRIPT_INDEX = $( dialog --colors --backtitle "ProxMenux" \
--title " $( translate "Scripts in" ) ${ CATEGORY_NAMES [ $SELECTED ] } " \
--menu " $( translate "Choose a script to execute:" ) : " \
2026-04-18 09:06:01 +02:00
22 75 15 " ${ SCRIPTS [@] } " 3>& 1 1>& 2 2>& 3) || break
2025-06-06 17:29:06 +02:00
SCRIPT_SELECTED = " ${ INDEX_TO_SLUG [ $SCRIPT_INDEX ] } "
run_script_by_slug " $SCRIPT_SELECTED "
2026-03-14 18:16:55 +01:00
2025-06-06 17:29:06 +02:00
[ [ " $RETURN_TO_MAIN " = = true ] ] && { RETURN_TO_MAIN = false; break; }
done
2025-06-10 14:34:30 +02:00
done