From 4fbf03ad5cb7f5fab8376b67f42c41193bf37a3f Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 7 Jun 2026 21:43:33 -0500 Subject: [PATCH 01/19] Standardized printing. Organized color resources. Changed custom entry to accept multiple commands. Added systemd and fail2ban status. Added network usage by namespace. Added disk usage by pattern matching. Added G_TRUNCATE_MID. --- dietpi/func/dietpi-banner | 716 ++++++++++++++++++++++++++++++++----- dietpi/func/dietpi-globals | 32 +- 2 files changed, 651 insertions(+), 97 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 2da3f34cfb..338e4ac69b 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -5,6 +5,7 @@ # #//////////////////////////////////// # Created by Daniel Knight / daniel.knight@dietpi.com / dietpi.com + # Updated May 2026 by Tim Olson / timjolson@users.noreply.github.com # #//////////////////////////////////// # @@ -39,9 +40,92 @@ # Globals #///////////////////////////////////////////////////////////////////////////////////// readonly FP_SAVEFILE='/boot/dietpi/.dietpi-banner' - readonly FP_CUSTOM='/boot/dietpi/.dietpi-banner_custom' readonly FP_BANNERWRAP_AWK='/boot/dietpi/func/dietpi-banner-wrap.awk' + # ---------------------------------------------------------------------------- + # Custom preferences initialisation helper (can be called by external scripts after + # sourcing this file). It loads the user-saved settings file + # ($FP_SAVEFILE) so custom colours, commands, and other preferences are applied before they are used. + # Usage: DIETPI_BANNER_LOAD_PREFS + # ---------------------------------------------------------------------------- + DIETPI_BANNER_LOAD_PREFS() + { + declare -g COLOUR_RESET='\e[0m' + declare -gA aCOLOUR=( + [0]='\e[38;5;154m' # DietPi green | Lines, bullets and separators + [1]='\e[1m' # Bold white | Main descriptions + [2]='\e[90m' # Grey | Subdued text + [3]='\e[91m' # Red | Update notifications / Bad state + [4]='\e[1;32m' # Green | Good state + [5]='\e[1;33m' # Yellow | Warning state + [6]='\e[1;36m' # Blue | Dynamic match + + [DIETPI_GREEN]='\e[38;5;154m' + [GREY]='\e[90m' + [RED]='\e[91m' + [GREEN]='\e[1;32m' + [YELLOW]='\e[1;33m' + [BLUE]='\e[1;36m' + [GOLD]='\e[38;5;220m' + [PURPLE]='\e[38;5;141m' + [VIVID_RED]='\e[38;5;196m' + [CYAN]='\e[38;5;45m' + + [RESET]='\e[0m' + [BOLD]='\e[1m' + [DIM]='\e[2m' + [UNDERLINE]='\e[4m' + [INVERSE]='\e[7m' + [BLINK]='\e[5m' + [HIDDEN]='\e[8m' + + [FG_BLACK]='\e[30m' + [FG_RED]='\e[31m' + [FG_GREEN]='\e[32m' + [FG_YELLOW]='\e[33m' + [FG_BLUE]='\e[34m' + [FG_MAGENTA]='\e[35m' + [FG_CYAN]='\e[36m' + [FG_WHITE]='\e[37m' + + [BRIGHT_BLACK]='\e[90m' + [BRIGHT_RED]='\e[91m' + [BRIGHT_GREEN]='\e[92m' + [BRIGHT_YELLOW]='\e[93m' + [BRIGHT_BLUE]='\e[94m' + [BRIGHT_MAGENTA]='\e[95m' + [BRIGHT_CYAN]='\e[96m' + [BRIGHT_WHITE]='\e[97m' + + [BG_BLACK]='\e[40m' + [BG_RED]='\e[41m' + [BG_GREEN]='\e[42m' + [BG_YELLOW]='\e[43m' + [BG_BLUE]='\e[44m' + [BG_MAGENTA]='\e[45m' + [BG_CYAN]='\e[46m' + [BG_WHITE]='\e[47m' + [BG_DARK_GREY]='\e[48;5;236m' + + [BG_BRIGHT_BLACK]='\e[100m' + [BG_BRIGHT_RED]='\e[101m' + [BG_BRIGHT_GREEN]='\e[102m' + [BG_BRIGHT_YELLOW]='\e[103m' + [BG_BRIGHT_BLUE]='\e[104m' + [BG_BRIGHT_MAGENTA]='\e[105m' + [BG_BRIGHT_CYAN]='\e[106m' + [BG_BRIGHT_WHITE]='\e[107m' + + [TRUE_GREEN]='\e[38;2;46;204;113m' + [TRUE_BG]='\e[48;2;44;62;80m' + + ) + + # Load settings here, to have chosen ${aCOLOUR[N]} applied to below strings + # shellcheck disable=SC1090 + [[ -f $FP_SAVEFILE ]] && . "$FP_SAVEFILE" + } + aDESCRIPTION=( 'Device model' @@ -54,7 +138,7 @@ 'Disk usage (RootFS)' 'Disk usage (userdata)' 'Weather (wttr.in)' - 'Custom banner entry' + 'Custom commands' 'Display DietPi useful commands?' 'MOTD' 'VPN status' @@ -65,36 +149,45 @@ 'Load average' 'Word-wrap lines on small screens' 'Kernel' + 'Network Usage' + 'Disk Usage' + 'Systemd Status' + 'Fail2Ban Status' ) # Set defaults: Disable CPU temp by default in VMs if (( $G_HW_MODEL == 20 )) then - aENABLED=(1 0 0 0 0 1 0 1 0 0 0 1 1 0 0 1 0 0 0 0 0) + aENABLED=(1 0 0 0 0 1 0 1 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0 0 0) else - aENABLED=(1 0 1 0 0 1 0 0 0 0 0 1 1 0 0 1 0 0 0 0 0) + aENABLED=(1 0 1 0 0 1 0 0 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0 0 0) fi - COLOUR_RESET='\e[0m' - aCOLOUR=( - - '\e[38;5;154m' # DietPi green | Lines, bullets and separators - '\e[1m' # Bold white | Main descriptions - '\e[90m' # Grey | Credits - '\e[91m' # Red | Update notifications - ) - # Folding type (colon, dash, fixed), and fixed indent BW_INDENT_TYPE='colon' BW_INDENT_FIXED=3 - # Load settings here, to have chosen ${aCOLOUR[0]} applied to below strings - # shellcheck disable=SC1090 - [[ -f $FP_SAVEFILE ]] && . "$FP_SAVEFILE" + # Default custom command example + CUSTOM_COMMANDS[0]='echo -e "${aCOLOUR[RED]}Hello ${aCOLOUR[GREEN]}World! ${COLOUR_RESET} :)"' + # Default disk patterns + SPACE_BASE_PATTERNS=("/" "/mnt/dietpi_userdata") - GREEN_LINE=" ${aCOLOUR[0]}─────────────────────────────────────────────────────$COLOUR_RESET" - GREEN_BULLET=" ${aCOLOUR[0]}-$COLOUR_RESET" - GREEN_SEPARATOR="${aCOLOUR[0]}:$COLOUR_RESET" + # Load user preferences, overriding the above defaults as needed + DIETPI_BANNER_LOAD_PREFS + + # Put loaded colors into the used slots + aCOLOUR[DIETPI_GREEN]=${aCOLOUR[0]} + aCOLOUR[WHITE]=${aCOLOUR[1]} + aCOLOUR[GREY]=${aCOLOUR[2]} + aCOLOUR[RED]=${aCOLOUR[3]} + aCOLOUR[GREEN]=${aCOLOUR[4]} + aCOLOUR[YELLOW]=${aCOLOUR[5]} + aCOLOUR[BLUE]=${aCOLOUR[6]} + + # Derived convenience strings + declare -g GREEN_LINE=" ${aCOLOUR[DIETPI_GREEN]}─────────────────────────────────────────────────────$COLOUR_RESET" + declare -g GREEN_BULLET=" ${aCOLOUR[DIETPI_GREEN]}-$COLOUR_RESET" + declare -g GREEN_SEPARATOR="${aCOLOUR[DIETPI_GREEN]}:$COLOUR_RESET" DIETPI_VERSION="$G_DIETPI_VERSION_CORE.$G_DIETPI_VERSION_SUB.$G_DIETPI_VERSION_RC" if [[ $G_GITOWNER != 'MichaIng' ]] @@ -154,17 +247,36 @@ Save() { - # Custom entry description - echo "aDESCRIPTION[10]='${aDESCRIPTION[10]}'" - for i in "${!aDESCRIPTION[@]}" do echo "aENABLED[$i]=${aENABLED[$i]}" done - for i in "${!aCOLOUR[@]}" + # Persist only the primary colour slots (0..6). Convert any actual + # ESC bytes to the two-character sequence \e so the saved file is + # portable when sourced in different shells. + for i in {0..6} do - echo "aCOLOUR[$i]='${aCOLOUR[$i]}'" + val="${aCOLOUR[$i]}" + # Replace actual ESC bytes (0x1b) with literal \e + esc=$(printf '%s' "$val" | sed $'s/\x1b/\\e/g') + # Escape single quotes for safe embedding + esc=${esc//\'/\\\'} + echo "aCOLOUR[$i]='$esc'" + done + + # Persist SPACE_BASE_PATTERNS (indices 0..N) + for i in "${!SPACE_BASE_PATTERNS[@]}"; do + patt=${SPACE_BASE_PATTERNS[$i]} + patt=${patt//\'/\\\'} + printf "SPACE_BASE_PATTERNS[%s]='%s'\n" "$i" "$patt" + done + + # Persist CUSTOM_COMMANDS (indices 0..N) + for i in "${!CUSTOM_COMMANDS[@]}"; do + cmd=${CUSTOM_COMMANDS[$i]} + cmd=${cmd//\'/\\\'} + printf "CUSTOM_COMMANDS[%s]='%s'\n" "$i" "$cmd" done echo "BW_INDENT_TYPE='$BW_INDENT_TYPE'" @@ -176,57 +288,96 @@ # DietPi update available? if Check_DietPi_Update then - local text_update_available_date="${aCOLOUR[3]}Update available" + local text_update_available_date="${aCOLOUR[YELLOW]}Update available" # New DietPi live patches available? elif Check_DietPi_Live_Patches then - local text_update_available_date="${aCOLOUR[3]}New live patches available" + local text_update_available_date="${aCOLOUR[YELLOW]}New live patches available" # APT update available? elif Check_APT_Updates then - local text_update_available_date="${aCOLOUR[3]}$PACKAGE_COUNT APT updates available" + local text_update_available_date="${aCOLOUR[YELLOW]}$PACKAGE_COUNT APT updates available" # Reboot required to finalise kernel upgrade? elif Check_Reboot then - local text_update_available_date="${aCOLOUR[3]}Reboot required" + local text_update_available_date="${aCOLOUR[RED]}Reboot required" else local locale=$(sed -n '/^[[:blank:]]*AUTO_SETUP_LOCALE=/{s/^[^=]*=//p;q}' /boot/dietpi.txt) local text_update_available_date=$(LC_ALL=${locale:-C.UTF-8} date '+%R - %a %x') fi echo -e "$GREEN_LINE - ${aCOLOUR[1]}DietPi v$DIETPI_VERSION$COLOUR_RESET $GREEN_SEPARATOR $text_update_available_date$COLOUR_RESET + ${aCOLOUR[WHITE]}DietPi v$DIETPI_VERSION$COLOUR_RESET $GREEN_SEPARATOR $text_update_available_date$COLOUR_RESET $GREEN_LINE" } - Print_Local_Ip() + Print_Subheader() + { + # ON THE CURRENT LINE Output green line separator with subheader title "$1" + echo -e "\r$GREEN_LINE + ${aCOLOUR[WHITE]}$1 $GREEN_SEPARATOR" + } + + Get_Local_Ip() { (( ${aENABLED[5]} )) || return 0 local iface=$(G_GET_NET -q iface) local ip=$(G_GET_NET -q ip) - echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[5]} $GREEN_SEPARATOR ${ip:-Use dietpi-config to setup a connection} (${iface:-NONE})" + echo "${ip:-Use dietpi-config to setup a connection} (${iface:-NONE})" } - Print_Cert_Status() + Get_Cert_Status() { - # Let's Encrypt cert status - MUST be run as root - local i - for i in /etc/letsencrypt/live/*/cert.pem - do - # shellcheck disable=SC2292 - [ -f "$i" ] || continue - openssl x509 -enddate -noout -in "$i" | mawk '/notAfter=/{print "Valid until "$4"-"substr($1,10)"-"$2" "$3}' - return 0 - done - echo 'No certificate found' + Get_Cert_Status_Raw() + { + # helper function so that we can run it with sudo in non-root mode without needing to export all the above functions and variables. + # also is `dash` compatible + local i + for i in /etc/letsencrypt/live/*/cert.pem + do + # shellcheck disable=SC2292 + [ -f "$i" ] || continue + openssl x509 -enddate -noout -in "$i" | mawk '/notAfter=/{print "Valid until "$4"-"substr($1,10)"-"$2" "$3}' + return 0 + done + return 1 + } + + stat="" + code=0 + if [[ "$(id -u)" = "0" ]] + then + # Running as root + stat=$(Get_Cert_Status_Raw 2>/dev/null) + code=$? + else + # Running as non-root: Fail silently without NOPASSWD to avoid password prompt + stat=$(sudo -n dash -c "$(declare -f Get_Cert_Status_Raw); Get_Cert_Status_Raw 2>&1" 2> /dev/null) + code=$? + fi + + case $code in + 0) + case "$stat" in + Valid*) stat="${aCOLOUR[GREY]}$stat${COLOUR_RESET}" ;; + No\ certificate*) stat="${aCOLOUR[RED]}$stat${COLOUR_RESET}" ;; + *) stat="${aCOLOUR[YELLOW]}$stat${COLOUR_RESET}" ;; + esac + ;; # stat is valid + 1) stat="${aCOLOUR[RED]}No certificate found${COLOUR_RESET}" ;; + 2) stat="${aCOLOUR[YELLOW]}NOPASSWD sudo required${COLOUR_RESET}" ;; + *) stat="${aCOLOUR[YELLOW]}Unable to obtain cert status${COLOUR_RESET}" ;; + esac + + echo "$stat" } Print_Credits() { - echo -e " ${aCOLOUR[2]}DietPi Team : https://github.com/MichaIng/DietPi#the-dietpi-project-team" + echo -e " ${aCOLOUR[GREY]}DietPi Team : https://github.com/MichaIng/DietPi#the-dietpi-project-team" [[ -f '/boot/dietpi/.prep_info' ]] && mawk 'NR==1 {sub(/^0$/,"DietPi Core Team");a=$0} NR==2 {print " Image by : "a" (pre-image: "$0")"}' /boot/dietpi/.prep_info @@ -241,32 +392,330 @@ $GREEN_LINE" # DietPi update available? if [[ $AVAILABLE_UPDATE ]] then - echo -e " ${aCOLOUR[1]}dietpi-update$COLOUR_RESET $GREEN_SEPARATOR ${aCOLOUR[3]}Run now to update DietPi from v$DIETPI_VERSION to v$AVAILABLE_UPDATE$COLOUR_RESET\n" + echo -e " ${aCOLOUR[WHITE]}dietpi-update$COLOUR_RESET $GREEN_SEPARATOR ${aCOLOUR[YELLOW]}Run now to update DietPi from v$DIETPI_VERSION to v$AVAILABLE_UPDATE$COLOUR_RESET\n" # New DietPi live patches available? elif (( $LIVE_PATCHES )) then - echo -e " ${aCOLOUR[1]}dietpi-update$COLOUR_RESET $GREEN_SEPARATOR ${aCOLOUR[3]}Run now to check out new available DietPi live patches$COLOUR_RESET\n" + echo -e " ${aCOLOUR[WHITE]}dietpi-update$COLOUR_RESET $GREEN_SEPARATOR ${aCOLOUR[YELLOW]}Run now to check out new available DietPi live patches$COLOUR_RESET\n" # APT updates available? elif (( $PACKAGE_COUNT )) then - echo -e " ${aCOLOUR[1]}apt upgrade$COLOUR_RESET $GREEN_SEPARATOR ${aCOLOUR[3]}Run now to apply $PACKAGE_COUNT available APT package upgrades$COLOUR_RESET\n" + echo -e " ${aCOLOUR[WHITE]}apt upgrade$COLOUR_RESET $GREEN_SEPARATOR ${aCOLOUR[YELLOW]}Run now to apply $PACKAGE_COUNT available APT package upgrades$COLOUR_RESET\n" # Reboot required to finalise kernel upgrade? elif (( $REBOOT_REQUIRED )) then - echo -e " ${aCOLOUR[1]}reboot$COLOUR_RESET $GREEN_SEPARATOR ${aCOLOUR[3]}A reboot is required to finalise a recent kernel upgrade$COLOUR_RESET\n" + echo -e " ${aCOLOUR[WHITE]}reboot$COLOUR_RESET $GREEN_SEPARATOR ${aCOLOUR[RED]}A reboot is required to finalise a recent kernel upgrade$COLOUR_RESET\n" fi } Print_Useful_Commands() { - echo -e " ${aCOLOUR[1]}dietpi-launcher$COLOUR_RESET $GREEN_SEPARATOR All the DietPi programs in one place - ${aCOLOUR[1]}dietpi-config$COLOUR_RESET $GREEN_SEPARATOR Feature rich configuration tool for your device - ${aCOLOUR[1]}dietpi-software$COLOUR_RESET $GREEN_SEPARATOR Select optimised software for installation - ${aCOLOUR[1]}htop$COLOUR_RESET $GREEN_SEPARATOR Resource monitor - ${aCOLOUR[1]}cpu$COLOUR_RESET $GREEN_SEPARATOR Shows CPU information and stats\n" + echo -e " ${aCOLOUR[WHITE]}dietpi-launcher$COLOUR_RESET $GREEN_SEPARATOR All the DietPi programs in one place + ${aCOLOUR[WHITE]}dietpi-config$COLOUR_RESET $GREEN_SEPARATOR Feature rich configuration tool for your device + ${aCOLOUR[WHITE]}dietpi-software$COLOUR_RESET $GREEN_SEPARATOR Select optimised software for installation + ${aCOLOUR[WHITE]}htop$COLOUR_RESET $GREEN_SEPARATOR Resource monitor + ${aCOLOUR[WHITE]}cpu$COLOUR_RESET $GREEN_SEPARATOR Shows CPU information and stats\n" + } + + Print_Item_State() + { + # Print a single line item with green bullet, description "$1", green separator and state "$2" + # state should contain color and format codes as needed + local subtitle="$1" + local state="$2" + local width="${3:-}" + local color="${aCOLOUR[WHITE]}" + printf "%b%b %-${width}b %b %b\n" \ + "$GREEN_BULLET" "${color}" "$subtitle" "$GREEN_SEPARATOR" "$state" + } + + Get_Systemd_Status() + { + # Print systemd overall status based on "systemctl --failed" + FAILED_CNT=$(systemctl --failed --no-legend --no-pager | wc -l 2>/dev/null || true) + if [[ -z "$FAILED_CNT" || "$FAILED_CNT" -eq 0 ]]; then + state="${aCOLOUR[GREY]}No Services Failed${COLOUR_RESET}" + else + state="${aCOLOUR[RED]}$FAILED_CNT Service(s) Failed${COLOUR_RESET}" + fi + echo "$state" + } + + Get_Fail2Ban_Status() + { + Get_Fail2Ban_Status_Raw() + { + # # helper function so that we can run it with sudo in non-root mode without needing to export all the above functions and variables. + # # also is `dash` compatible + + # Count unique banned IPs via iptables/nftables compatibility. Try iptables first, + # fall back to nft if available. + COUNTIPS=0 + if command -v iptables >/dev/null 2>&1; then + COUNTIPS=$(iptables -L -n 2>/dev/null | awk '/REJECT/ {print $0}' | uniq -c | wc -l) + elif command -v nft >/dev/null 2>&1; then + COUNTIPS=$(nft list ruleset 2>/dev/null | awk '/reject/ {print $0}' | uniq -c | wc -l) + else + return 1 + fi + echo "$COUNTIPS" + return 0 + } + + COUNTIPS=0 + if [[ "$(id -u)" = "0" ]] + then + COUNTIPS="$(Get_Fail2Ban_Status_Raw 2>/dev/null || return 1)" + code=$? + else + # Running as non-root: Fail silently without NOPASSWD to avoid password prompt + COUNTIPS="$(sudo -n dash -c "$(declare -f Get_Fail2Ban_Status_Raw); Get_Fail2Ban_Status_Raw 2>&1" 2> /dev/null || return 2)" + code=$? + fi + + case $code in + 0) + if [[ $COUNTIPS -eq 0 ]]; then + # zero bans can be suspicious - something may not be working correctly + state="${aCOLOUR[YELLOW]}No IP(s) Banned${COLOUR_RESET}" + else + if [[ $COUNTIPS -gt 20 ]]; then + # high number of bans + state="${aCOLOUR[RED]}$COUNTIPS IP(s) Banned${COLOUR_RESET}" + else + # low number of bans + state="${aCOLOUR[GREY]}$COUNTIPS IP(s) Banned${COLOUR_RESET}" + fi + fi + ;; + 1) + state="${aCOLOUR[RED]}No Fail2Ban status available${COLOUR_RESET}" + ;; + 2) + state="${aCOLOUR[YELLOW]}NOPASSWD sudo required to obtain Fail2Ban status${COLOUR_RESET}" + ;; + *) + state="${aCOLOUR[RED]}Unknown state $COUNTIPS${COLOUR_RESET}" + ;; + esac + + echo "$state" + } + + Get_Network_Usage() + { + # IP address detection (IPv4) + local IP_re='([0-9]{1,3}\.){3}[0-9]{1,3}' + # Common error substrings to detect timeouts / DNS failures (lowercase for case-insensitive check) + local err_re='timed out|timeout|could not resolve|not known|unknown' + + DATA_USAGE(){ + local netns="$1" + + # Use an array prefix so we can run commands either in a netns + # (ip netns exec ...) or the default namespace, and with sudo if needed + local -a ns_cmd_prefix=() sudo_cmd_prefix=() + if [[ "$(id -u)" -ne "0" ]]; then + sudo_cmd_prefix+=(sudo -n) + fi + + if [[ -n "$netns" ]]; then + ns_cmd_prefix+=(ip netns exec "$netns") + fi + + # Query interface list once inside the (optional) namespace and match names + mapfile -t observed_ifaces < <("${sudo_cmd_prefix[@]}" "${ns_cmd_prefix[@]}" ip -o link show 2>/dev/null | awk -F': ' '{print $2}') + + for IFACE in "${observed_ifaces[@]}"; do + # strip kernel-style peer suffix (e.g. eth0@if8) and use base name + local ifname="${IFACE%%@*}" + # skip loopback + [[ "$ifname" == lo ]] && continue + + case "$ifname" in + # wired / predictable names + eth*|en*|lan*) handle_iface=1 ;; + # wireless + wlan*|wl*) handle_iface=1 ;; + # vpn/tunnel/virtual + tun*|wg*|vpn*) handle_iface=1 ;; + *) continue ;; + esac + + if (( handle_iface )); then + local RX="N/A" TX="N/A" rx tx ip_addr ip_addr_cidr + + # Get RX data + if "${sudo_cmd_prefix[@]}" "${ns_cmd_prefix[@]}" test -r "/sys/class/net/$ifname/statistics/rx_bytes"; then + rx=$("${sudo_cmd_prefix[@]}" "${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/rx_bytes" 2>/dev/null || :) + [[ $rx =~ ^[0-9]+$ ]] && RX="$(awk -v v="$rx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" + fi + + # Get TX data + if "${sudo_cmd_prefix[@]}" "${ns_cmd_prefix[@]}" test -r "/sys/class/net/$ifname/statistics/tx_bytes"; then + tx=$("${sudo_cmd_prefix[@]}" "${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/tx_bytes" 2>/dev/null || :) + [[ $tx =~ ^[0-9]+$ ]] && TX="$(awk -v v="$tx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" + fi + + # Get IP address (IPv4, global scope) for this interface in this namespace. If multiple are present, just take the first. If none, show "- No IP -" + ip_addr=" - No IP - " + ip_addr_cidr=$("${sudo_cmd_prefix[@]}" "${ns_cmd_prefix[@]}" ip -o -4 addr show dev "$ifname" scope global 2>/dev/null | awk '{print $4}' | head -n1) + [[ -n "$ip_addr_cidr" ]] && ip_addr=${ip_addr_cidr%%/*} + + if [[ "$RX" != "N/A" || "$TX" != "N/A" ]]; then + printf "%s %s%-20s %s%s TX= %12s RX= %12s%s\n" \ + "$GREEN_BULLET" "${aCOLOUR[GREY]}" "$ip_addr ($ifname)" "$GREEN_SEPARATOR" "${aCOLOUR[GREY]}" "$TX" "$RX" "$COLOUR_RESET" + fi + fi + done + } + + mapfile -t namespaces < <(ip netns ls | awk 'NF>1 { print $1 }') + # Prepend an empty entry so the default (root) namespace can be handled in the loop + namespaces=( "" "${namespaces[@]}" ) + + for ns in "${namespaces[@]}"; do + raw_ns="${ns:-default}" + display_ns="$(G_TRUNCATE_MID "$raw_ns" 30)" + + if [[ "$(id -u)" = "0" ]]; then + # Running as root, can call function directly + IP=$(G_GET_WAN_IP -t 1 -n "$ns" 2>&1) + else + # Running as non-root: Fail silently without NOPASSWD to avoid password prompt + # Get public IP, sourcing globals inside the sudo'd shell + # Pass namespace as positional arg to avoid expansion issues inside the subshell + IP=$(sudo -n bash -c '. /boot/dietpi/func/dietpi-globals >/dev/null 2>&1; G_GET_WAN_IP -t 1 -n "$1" 2>&1' _ "$ns") + fi + + if [[ $IP =~ $IP_re ]]; then + # Valid IP detected, keep as-is and print with namespace + + # # Extract just the IP portion (use BASH_REMATCH[0]). Removes the geographic info. + # IP="${BASH_REMATCH[0]}" + + printf " %s%s [%s]%s\n" \ + "${aCOLOUR[BLUE]}" "$IP" "$display_ns" "$COLOUR_RESET" + + elif [[ -z $IP ]]; then + # If the IP is empty, it likely means the namespace is disconnected or has no internet access. Print status message. + printf "%s %s %s %s%s%s\n" \ + "${aCOLOUR[RED]}" "Disconnected" "$GREEN_SEPARATOR" "${aCOLOUR[WHITE]}" "$display_ns" "$COLOUR_RESET" + + else + if [[ $IP =~ $err_re ]]; then + # Treat known curl/network error messages as timeout/DNS failures + IP="Timeout/DNS Failure" + fi + + # If the IP contains an unknown or error message, print it with the namespace + printf " %s%s [%s]%s\n" \ + "${aCOLOUR[YELLOW]}" "$IP" "$display_ns" "$COLOUR_RESET" + fi + + DATA_USAGE "$ns" + done + } + + Print_Custom_Commands() + { + for i in "${!CUSTOM_COMMANDS[@]}"; do + cmd="${CUSTOM_COMMANDS[$i]}" + if [[ -n "$cmd" ]]; then + # shellcheck disable=SC2086 + echo $(eval "$cmd" 2>&1) || echo -e "${aCOLOUR[RED]}Error executing custom command $((i+1))${COLOUR_RESET}" + fi + done + } + + Get_Disk_Usage() + { + # If no arguments are provided, default to '"/" "/mnt/*"'. + if (( $# > 0 )); then + SPACE_BASE_PATTERNS=("$@") + fi + + # TODO: is there a more efficient reader than df (stat?) + # Use df's output to parse numeric fields (kB units) for mounts matching the supplied patterns + length=0 + results=() + while read -r size_kb used_kb avail_kb mnt_target; do + matched=0 + basename= + for pattern in "${SPACE_BASE_PATTERNS[@]}"; do + # If the user supplies a pattern prefixed with 're:' treat it as regex + if [[ "$pattern" == re:* ]]; then + re="${pattern#re:}" + if [[ $mnt_target =~ $re ]]; then + matched=1 + basename=$(basename "$mnt_target") + break + fi + else + # Otherwise, treat it as a shell glob pattern and match using "case" + case "$mnt_target" in + $pattern) + matched=1 + basename=$(basename "$mnt_target") + break + ;; + *) ;; + esac + fi + done + if (( matched )); then + len=${#basename} + if (( len > length )); then + length=$len + fi + results+=("$size_kb|$used_kb|$avail_kb|$mnt_target|$basename") + fi + done < <(df -k --output=size,used,avail,target | tail -n +2) + + # Print results with aligned columns, converting kB to GB and calculating percentage used + for entry in "${results[@]}"; do + IFS='|' read -r size_kb used_kb avail_kb mnt_target name <<< "$entry" + # name=$(basename "$entry") + # Do all math in kB, convert to GB for display and round to 1 decimal + if [[ "$size_kb" =~ ^[0-9]+$ ]]; then + size_gb=$(awk "BEGIN {printf \"%.1f \", $size_kb/1024/1024}") + else + size_gb="N/A" + fi + if [[ "$used_kb" =~ ^[0-9]+$ ]]; then + used_gb=$(awk "BEGIN {printf \"%.1f \", $used_kb/1024/1024}") + else + used_gb="N/A" + fi + if [[ "$avail_kb" =~ ^[0-9]+$ ]]; then + avail_gb=$(awk "BEGIN {printf \"%.1f \", $avail_kb/1024/1024}") + else + avail_gb="N/A" + fi + if [[ "$size_kb" =~ ^[0-9]+$ && $size_kb -gt 0 ]]; then + perc=$(awk "BEGIN {printf \"%.1f%%\", ($used_kb/($used_kb+$avail_kb))*100}") + else + perc="N/A" + fi + # Print aligned columns: {basename} : {used} of {size} ({percent used}) + printf "%s %s%-${length}s %s%s %8sGiB of %8sGiB (%6b)%s\n" \ + "$GREEN_BULLET" "${aCOLOUR[BLUE]}" "$name" "$GREEN_SEPARATOR" "${aCOLOUR[GREY]}" "$used_gb" "$size_gb" "$perc" "$COLOUR_RESET" + done + + } + + Get_VPN_Status() + { + local state="$( /boot/dietpi/dietpi-vpn status 2>&1 )" + case "$state" in + Connected*) state="${aCOLOUR[GREY]}$state${COLOUR_RESET}" ;; + Disconnected*) state="${aCOLOUR[RED]}$state${COLOUR_RESET}" ;; + *) state="${aCOLOUR[YELLOW]}$state${COLOUR_RESET}" ;; + esac + echo "$state" } Print_Banner_raw() @@ -277,55 +726,59 @@ $GREEN_LINE" # shellcheck disable=SC1091 (( ${aENABLED[14]} )) && . /boot/dietpi/func/dietpi-print_large "$(&1)" + (( ${aENABLED[1]} )) && Print_Item_State "${aDESCRIPTION[1]}" "$(uptime -p 2>&1)" # Linux kernel version - (( ${aENABLED[20]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[20]} $GREEN_SEPARATOR $(uname -r 2>&1)" + (( ${aENABLED[20]} )) && Print_Item_State "${aDESCRIPTION[20]}" "$(uname -r 2>&1)" # CPU temp - (( ${aENABLED[2]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[2]} $GREEN_SEPARATOR $(print_full_info=1 G_OBTAIN_CPU_TEMP 2>&1)" + (( ${aENABLED[2]} )) && Print_Item_State "${aDESCRIPTION[2]}" "$(print_full_info=1 G_OBTAIN_CPU_TEMP 2>&1)" # RAM usage - (( ${aENABLED[17]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[17]} $GREEN_SEPARATOR $(free -b | mawk 'NR==2 {CONVFMT="%.0f"; print $3/1024^2" of "$2/1024^2" MiB ("$3/$2*100"%)"}')" + (( ${aENABLED[17]} )) && Print_Item_State "${aDESCRIPTION[17]}" "$(free -b | mawk 'NR==2 {CONVFMT="%.0f"; print $3/1024^2" of "$2/1024^2" MiB ("$3/$2*100"%)"}')" # Load average - (( ${aENABLED[18]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[18]} $GREEN_SEPARATOR $(mawk '{print $1 ", " $2 ", " $3}' /proc/loadavg) ($(nproc) cores)" + (( ${aENABLED[18]} )) && Print_Item_State "${aDESCRIPTION[18]}" "$(mawk '{print $1 ", " $2 ", " $3}' /proc/loadavg) ($(nproc) cores)" # Hostname - (( ${aENABLED[3]} && ! ${aENABLED[14]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[3]} $GREEN_SEPARATOR $(&1)" + (( ${aENABLED[4]} )) && Print_Item_State "${aDESCRIPTION[4]}" "$(hostname -y 2>&1)" # LAN IP - Print_Local_Ip + (( ${aENABLED[5]} )) && Print_Item_State "${aDESCRIPTION[5]}" "$(Get_Local_Ip 2>&1)" # WAN IP + location info - (( ${aENABLED[6]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[6]} $GREEN_SEPARATOR $(G_GET_WAN_IP 2>&1)" + (( ${aENABLED[6]} )) && Print_Item_State "${aDESCRIPTION[6]}" "$(G_GET_WAN_IP 2>&1)" # DietPi-VPN connection status - (( ${aENABLED[13]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[13]} $GREEN_SEPARATOR $(/boot/dietpi/dietpi-vpn status 2>&1)" + (( ${aENABLED[13]} )) && Print_Item_State "${aDESCRIPTION[13]}" "$(Get_VPN_Status 2>&1)" # Disk usage (RootFS) - (( ${aENABLED[7]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[7]} $GREEN_SEPARATOR $(df -h --output=used,size,pcent / | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" + (( ${aENABLED[7]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[WHITE]}${aDESCRIPTION[7]} $GREEN_SEPARATOR $(df -h --output=used,size,pcent / | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" # Disk usage (DietPi userdata) - (( ${aENABLED[8]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[8]} $GREEN_SEPARATOR $(df -h --output=used,size,pcent /mnt/dietpi_userdata | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" + (( ${aENABLED[8]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[WHITE]}${aDESCRIPTION[8]} $GREEN_SEPARATOR $(df -h --output=used,size,pcent /mnt/dietpi_userdata | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" # Weather - (( ${aENABLED[9]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[9]} $GREEN_SEPARATOR $(curl -sSfLm 3 'https://wttr.in/?format=4' 2>&1)" + (( ${aENABLED[9]} )) && Print_Item_State "${aDESCRIPTION[9]}" "$(curl -sSfLm 3 'https://wttr.in/?format=4' 2>&1)" # Let's Encrypt cert status - if (( ${aENABLED[16]} )) - then - echo -en "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[16]} $GREEN_SEPARATOR " - if [[ $EUID == 0 ]] - then - # Running as root - Print_Cert_Status 2>&1 - else - # Running as non-root: Fail silently without NOPASSWD to avoid password prompt, but print info instead - sudo -n dash -c "$(declare -f Print_Cert_Status); Print_Cert_Status 2>&1" 2> /dev/null || echo 'NOPASSWD sudo required to obtain cert status' - fi - fi - # Custom - (( ${aENABLED[10]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[10]} $GREEN_SEPARATOR $(bash "$FP_CUSTOM" 2>&1)" + (( ${aENABLED[16]} )) && Print_Item_State "Let's Encrypt Cert" "$(Get_Cert_Status 2>&1)" + # Systemd status + (( ${aENABLED[23]} )) && Print_Item_State "${aDESCRIPTION[23]}" "$(Get_Systemd_Status 2>&1)" + # Fail2Ban status + (( ${aENABLED[24]} )) && Print_Item_State "${aDESCRIPTION[24]}" "$(Get_Fail2Ban_Status 2>&1)" + + # Network usage by namespace + # %b required so that percent symbols are not interpreted as format specifiers by printf + (( ${aENABLED[21]} )) && Print_Subheader "Network Traffic" && printf '%b\n' "$(Get_Network_Usage 2>&1)" + # Disk Usage + (( ${aENABLED[22]} )) && Print_Subheader "Disk Usage" && printf '%b\n' "$(Get_Disk_Usage "${SPACE_BASE_PATTERNS[@]}" 2>&1)" + + # Custom commands + (( ${aENABLED[10]} )) && Print_Subheader "Custom Commands" && Print_Custom_Commands + + # Separator line + (( ${aENABLED[11]} || ${aENABLED[12]} || ${aENABLED[15]} )) && echo -e "$GREEN_LINE" + # MOTD if (( ${aENABLED[12]} )) then local motd fp_motd='/run/dietpi/.dietpi_motd' [[ -f $fp_motd ]] || curl -sSfLm 3 'https://dietpi.com/motd' -o "$fp_motd" # shellcheck disable=SC1090 - [[ -f $fp_motd ]] && . "$fp_motd" &> /dev/null && [[ $motd ]] && echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[12]} $GREEN_SEPARATOR $motd" + [[ -f $fp_motd ]] && . "$fp_motd" &> /dev/null && [[ $motd ]] && Print_Item_State "${aDESCRIPTION[12]}" "$motd" fi echo -e "$GREEN_LINE\n" @@ -370,17 +823,46 @@ $GREEN_LINE" if (( $i == 10 )) then - # Custom entry - [[ -f $FP_CUSTOM ]] && read -r G_WHIP_DEFAULT_ITEM < "$FP_CUSTOM" || G_WHIP_DEFAULT_ITEM='echo '\''Hello World!'\' - G_WHIP_INPUTBOX 'You have chosen to show a custom entry in the banner. -Please enter the desired command here.\n -NB: It is executed as bash script, so it needs to be in bash compatible syntax. - For multi-line or non-bash scripts, keep it separate and only add the script call here.' || continue - - echo "$G_WHIP_RETURNED_VALUE" > "$FP_CUSTOM" - - G_WHIP_DEFAULT_ITEM=${aDESCRIPTION[10]} - G_WHIP_INPUTBOX 'Please enter a meaningful name to be shown in front of your custom command output:' && aDESCRIPTION[10]=$G_WHIP_RETURNED_VALUE + example_command='echo -e "${aCOLOUR[RED]}Hello ${aCOLOUR[GREEN]}World! $COLOUR_RESET :)"\n\n-- or with the standard format --\n\nPrint_Item_State "Description" "value"' + + # Manage custom commands list + while true; do + G_WHIP_MENU_ARRAY=( 'Add' 'Add a new command' 'Edit' 'Edit an existing command' 'Remove' 'Remove a command' 'Done' 'Finish custom commands configuration' ) + G_WHIP_DEFAULT_ITEM='Done' + G_WHIP_MENU 'Manage custom banner commands.\n\nCAUTION!\nThese commands are executed with the same privileges as dietpi-banner :' || break + case "$G_WHIP_RETURNED_VALUE" in + Add) + G_WHIP_INPUTBOX "Enter new command, for example :\n$example_command" || continue + CUSTOM_COMMANDS+=("$G_WHIP_RETURNED_VALUE") + ;; + Edit) + if (( ${#CUSTOM_COMMANDS[@]} == 0 )); then G_WHIP_MSG 'No commands to edit'; continue; fi + G_WHIP_MENU_ARRAY=() + for idx in "${!CUSTOM_COMMANDS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${CUSTOM_COMMANDS[$idx]}"); done + G_WHIP_MENU 'Select command to edit:' || continue + sel=$G_WHIP_RETURNED_VALUE + G_WHIP_DEFAULT_ITEM=${CUSTOM_COMMANDS[$sel]} + G_WHIP_INPUTBOX "Edit command, for example :\n$example_command" || continue + CUSTOM_COMMANDS[$sel]="$G_WHIP_RETURNED_VALUE" + ;; + Remove) + if (( ${#CUSTOM_COMMANDS[@]} == 0 )); then G_WHIP_MSG 'No commands to remove'; continue; fi + G_WHIP_MENU_ARRAY=() + for idx in "${!CUSTOM_COMMANDS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${CUSTOM_COMMANDS[$idx]}"); done + G_WHIP_MENU 'Select command to remove:' || continue + sel=$G_WHIP_RETURNED_VALUE + unset 'CUSTOM_COMMANDS[$sel]' + # reindex array + CUSTOM_COMMANDS=("${CUSTOM_COMMANDS[@]}") + ;; + Done) + break + ;; + *) ;; + esac + done + + (( ${#CUSTOM_COMMANDS[@]} == 0 )) && aENABLED[10]=0 elif (( $i == 19 )) then @@ -397,11 +879,53 @@ NB: It is executed as bash script, so it needs to be in bash compatible syntax. G_WHIP_DEFAULT_ITEM=$BW_INDENT_FIXED G_WHIP_INPUTBOX_REGEX='^[1-9][0-9]*$' G_WHIP_INPUTBOX_REGEX_TEXT='be a number' G_WHIP_INPUTBOX 'Please set the fixed offset column to indent word-wrapped lines to:' && BW_INDENT_FIXED=$G_WHIP_RETURNED_VALUE + + elif (( $i == 22 )) + then + pattern_hint="The matching mount 'target's returned by the 'df' command will be reported. +To use regex, start the pattern with 're:', otherwise shell globbing is used. +For example: '/mnt/*' or 're:^/mnt/.*/*$'" + + # Manage SPACE_BASE_PATTERNS list + while true; do + G_WHIP_MENU_ARRAY=( 'Add' 'Add a new pattern' 'Edit' 'Edit an existing pattern' 'Remove' 'Remove a pattern' 'Done' 'Finish pattern configuration' ) + G_WHIP_DEFAULT_ITEM='Done' + G_WHIP_MENU "Manage disk space match patterns. $pattern_hint" || break + case "$G_WHIP_RETURNED_VALUE" in + Add) + G_WHIP_INPUTBOX "Enter new pattern :\n$pattern_hint" || continue + SPACE_BASE_PATTERNS+=("$G_WHIP_RETURNED_VALUE") + ;; + Edit) + if (( ${#SPACE_BASE_PATTERNS[@]} == 0 )); then G_WHIP_MSG 'No patterns to edit'; continue; fi + G_WHIP_MENU_ARRAY=() + for idx in "${!SPACE_BASE_PATTERNS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${SPACE_BASE_PATTERNS[$idx]}"); done + G_WHIP_MENU 'Select pattern to edit :' || continue + sel=$G_WHIP_RETURNED_VALUE + G_WHIP_DEFAULT_ITEM=${SPACE_BASE_PATTERNS[$sel]} + G_WHIP_INPUTBOX "Edit pattern :\n$pattern_hint" || continue + SPACE_BASE_PATTERNS[$sel]="$G_WHIP_RETURNED_VALUE" + ;; + Remove) + if (( ${#SPACE_BASE_PATTERNS[@]} == 0 )); then G_WHIP_MSG 'No patterns to remove'; continue; fi + G_WHIP_MENU_ARRAY=() + for idx in "${!SPACE_BASE_PATTERNS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${SPACE_BASE_PATTERNS[$idx]}"); done + G_WHIP_MENU 'Select pattern to remove :' || continue + sel=$G_WHIP_RETURNED_VALUE + unset 'SPACE_BASE_PATTERNS[$sel]' + # reindex array + SPACE_BASE_PATTERNS=("${SPACE_BASE_PATTERNS[@]}") + ;; + Done) + break + ;; + *) ;; + esac + done + (( ${#SPACE_BASE_PATTERNS[@]} == 0 )) && aENABLED[22]=0 fi done - [[ -f $FP_CUSTOM ]] || aENABLED[10]=0 - Save > "$FP_SAVEFILE" } @@ -409,7 +933,7 @@ NB: It is executed as bash script, so it needs to be in bash compatible syntax. # Main Loop #///////////////////////////////////////////////////////////////////////////////////// case $INPUT in - 0) Print_Header; Print_Local_Ip;; + 0) Print_Header; Print_Item_State "${aDESCRIPTION[5]}" "$(Get_Local_Ip 2>&1)";; 1) Print_Banner;; '') Menu_Main; Print_Banner;; *) G_DIETPI-NOTIFY 1 "Invalid input \"$*\"\n\nUsage:$USAGE"; exit 1;; @@ -418,4 +942,4 @@ NB: It is executed as bash script, so it needs to be in bash compatible syntax. #----------------------------------------------------------------------------------- exit 0 #----------------------------------------------------------------------------------- -} +} \ No newline at end of file diff --git a/dietpi/func/dietpi-globals b/dietpi/func/dietpi-globals index b84cf27827..69474923d0 100644 --- a/dietpi/func/dietpi-globals +++ b/dietpi/func/dietpi-globals @@ -171,6 +171,27 @@ $(ps f -eo pid,user,tty,cmd | grep -i 'dietpi')" && continue } + # Truncate a string in the middle to a maximum length, inserting "..." + # - If the string is shorter than or equal to the maximum length, it is returned unchanged. + # - If the maximum length is less than or equal to 3, the string is truncated to the maximum length without adding "...". + G_TRUNCATE_MID() { + local s="$1"; local max="$2" + local len=${#s} + if (( len <= max )); then + printf '%s' "$s" + return + fi + if (( max <= 3 )); then + printf '%.*s' "$max" "$s" + return + fi + local keep=$((max - 3)) + local pre=$(((keep + 1) / 2)) + local suf=$((keep / 2)) + local start=$((len - suf)) + printf '%s...%s' "${s:0:pre}" "${s:start}" + } + # DietPi-Notify # $1: # -2 = Processing @@ -1475,21 +1496,30 @@ Press any key to continue...' # Print public IP address and location info # - Optional arguments: # -t : Set timeout in seconds, supports floats, default: 3 + # -n : Run curl in the given network namespace, e.g. to obtain the WAN IP of a specific + # network namespace, default: empty (no namespace = the main default namespace) G_GET_WAN_IP() { # Defaults local timeout=3 + local ns="" # Inputs while (( $# )) do # shellcheck disable=SC2015 case $1 in '-t') shift; (( ${1/.} )) && timeout=$1 || { G_DIETPI-NOTIFY 1 "Invalid timeout \"$1\", aborting..."; return 1; };; + '-n') shift; ns="${1:-}";; *) G_DIETPI-NOTIFY 1 "Invalid argument \"$1\", aborting..."; return 1;; esac shift done - curl -sSfLm "$timeout" 'https://dietpi.com/geoip' + + if [[ -n "$ns" ]]; then + echo $(ip netns exec "$ns" curl -sSfLm "$timeout" 'https://dietpi.com/geoip' 2>&1) + else + echo $(curl -sSfLm "$timeout" 'https://dietpi.com/geoip' 2>&1) + fi } # $1 = directory to test permissions support From a922e514dec004033aea6c923089b83a644d57c0 Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 28 Jun 2026 09:09:54 -0500 Subject: [PATCH 02/19] removed extra `declare`s and -g `global` --- dietpi/func/dietpi-banner | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 338e4ac69b..364b18b5f3 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -185,9 +185,9 @@ aCOLOUR[BLUE]=${aCOLOUR[6]} # Derived convenience strings - declare -g GREEN_LINE=" ${aCOLOUR[DIETPI_GREEN]}─────────────────────────────────────────────────────$COLOUR_RESET" - declare -g GREEN_BULLET=" ${aCOLOUR[DIETPI_GREEN]}-$COLOUR_RESET" - declare -g GREEN_SEPARATOR="${aCOLOUR[DIETPI_GREEN]}:$COLOUR_RESET" + GREEN_LINE=" ${aCOLOUR[DIETPI_GREEN]}─────────────────────────────────────────────────────$COLOUR_RESET" + GREEN_BULLET=" ${aCOLOUR[DIETPI_GREEN]}-$COLOUR_RESET" + GREEN_SEPARATOR="${aCOLOUR[DIETPI_GREEN]}:$COLOUR_RESET" DIETPI_VERSION="$G_DIETPI_VERSION_CORE.$G_DIETPI_VERSION_SUB.$G_DIETPI_VERSION_RC" if [[ $G_GITOWNER != 'MichaIng' ]] From f97e44ab280a56fcdfb96d0366538d5300316621 Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 28 Jun 2026 09:17:36 -0500 Subject: [PATCH 03/19] adjusted example command for formatting and it serves as a test of colors inside custom commands --- dietpi/func/dietpi-banner | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 364b18b5f3..c5a2e337ed 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -168,7 +168,7 @@ BW_INDENT_FIXED=3 # Default custom command example - CUSTOM_COMMANDS[0]='echo -e "${aCOLOUR[RED]}Hello ${aCOLOUR[GREEN]}World! ${COLOUR_RESET} :)"' + CUSTOM_COMMANDS[0]='echo -e "$GREEN_BULLET ${aCOLOUR[RED]}Hello ${aCOLOUR[GREEN]}World! ${COLOUR_RESET} :)"' # Default disk patterns SPACE_BASE_PATTERNS=("/" "/mnt/dietpi_userdata") @@ -626,7 +626,7 @@ $GREEN_LINE" cmd="${CUSTOM_COMMANDS[$i]}" if [[ -n "$cmd" ]]; then # shellcheck disable=SC2086 - echo $(eval "$cmd" 2>&1) || echo -e "${aCOLOUR[RED]}Error executing custom command $((i+1))${COLOUR_RESET}" + echo "$(eval "$cmd" 2>&1)" || echo -e "${aCOLOUR[RED]}Error executing custom command $((i+1))${COLOUR_RESET}" fi done } @@ -823,13 +823,13 @@ $GREEN_LINE" if (( $i == 10 )) then - example_command='echo -e "${aCOLOUR[RED]}Hello ${aCOLOUR[GREEN]}World! $COLOUR_RESET :)"\n\n-- or with the standard format --\n\nPrint_Item_State "Description" "value"' + example_command='echo -e "$GREEN_BULLET ${aCOLOUR[RED]}Hello ${aCOLOUR[GREEN]}World! ${COLOUR_RESET} :)"\n\n-- or with the standard format --\n\nPrint_Item_State "Description" "value"' # Manage custom commands list while true; do G_WHIP_MENU_ARRAY=( 'Add' 'Add a new command' 'Edit' 'Edit an existing command' 'Remove' 'Remove a command' 'Done' 'Finish custom commands configuration' ) G_WHIP_DEFAULT_ITEM='Done' - G_WHIP_MENU 'Manage custom banner commands.\n\nCAUTION!\nThese commands are executed with the same privileges as dietpi-banner :' || break + G_WHIP_MENU 'Manage custom banner commands.\n\n!CAUTION!\nThese commands are executed with the same privileges as dietpi-banner, and DO NOT have a time limit :' || break case "$G_WHIP_RETURNED_VALUE" in Add) G_WHIP_INPUTBOX "Enter new command, for example :\n$example_command" || continue @@ -862,6 +862,7 @@ $GREEN_LINE" esac done + # If no commands are configured, disable the custom commands option (( ${#CUSTOM_COMMANDS[@]} == 0 )) && aENABLED[10]=0 elif (( $i == 19 )) From b2355e569b2a4af0be6aafeddbb93ff2af08e123 Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 28 Jun 2026 10:11:36 -0500 Subject: [PATCH 04/19] custom command failures provide the exit code --- dietpi/func/dietpi-banner | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index c5a2e337ed..8c82b196c5 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -623,10 +623,16 @@ $GREEN_LINE" Print_Custom_Commands() { for i in "${!CUSTOM_COMMANDS[@]}"; do - cmd="${CUSTOM_COMMANDS[$i]}" + local cmd="${CUSTOM_COMMANDS[$i]}" if [[ -n "$cmd" ]]; then # shellcheck disable=SC2086 - echo "$(eval "$cmd" 2>&1)" || echo -e "${aCOLOUR[RED]}Error executing custom command $((i+1))${COLOUR_RESET}" + output="$(eval "$cmd" 2>&1)" + status=$? + if [[ $status -ne 0 ]]; then + printf '%b\n' "${aCOLOUR[RED]}Error executing CUSTOM_COMMAND[$((i))] Exit Code: ${status}${COLOUR_RESET}" + else + printf '%b\n' "$output" + fi fi done } From c2d4f05b28d5f5e4f5b8eb945ef05a4699af31f8 Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 28 Jun 2026 13:58:47 -0500 Subject: [PATCH 05/19] Re-labeled colors for semantic indexing. removed extra formats. aCOLOUR saves just the relevant indexes. added some shellcheck bypasses and documentation. --- dietpi/func/dietpi-banner | 221 +++++++++++++++----------------------- 1 file changed, 85 insertions(+), 136 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 8c82b196c5..f7b0f76462 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -23,6 +23,8 @@ INPUT=$* # Import DietPi-Globals -------------------------------------------------------------- + # disable shellcheck for non-constant `source` (`.`) + # shellcheck disable=SC1090 . /boot/dietpi/func/dietpi-globals # - Allow concurrent banner prints but a single menu call only if [[ ! $INPUT ]] @@ -43,85 +45,31 @@ readonly FP_BANNERWRAP_AWK='/boot/dietpi/func/dietpi-banner-wrap.awk' # ---------------------------------------------------------------------------- - # Custom preferences initialisation helper (can be called by external scripts after - # sourcing this file). It loads the user-saved settings file + # Custom preferences initialisation helper. It loads the user-saved settings file # ($FP_SAVEFILE) so custom colours, commands, and other preferences are applied before they are used. - # Usage: DIETPI_BANNER_LOAD_PREFS + # Color and format references: + # `https://invisible-island.net/xterm/ctlseqs/ctlseqs.html` under section "Character Attributes (SGR)" + # `https://www.ditig.com/256-colors-cheat-sheet` + # `https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters` + # + # Usage: `DIETPI_BANNER_LOAD_PREFS` # ---------------------------------------------------------------------------- DIETPI_BANNER_LOAD_PREFS() { - declare -g COLOUR_RESET='\e[0m' declare -gA aCOLOUR=( - [0]='\e[38;5;154m' # DietPi green | Lines, bullets and separators - [1]='\e[1m' # Bold white | Main descriptions - [2]='\e[90m' # Grey | Subdued text - [3]='\e[91m' # Red | Update notifications / Bad state - [4]='\e[1;32m' # Green | Good state - [5]='\e[1;33m' # Yellow | Warning state - [6]='\e[1;36m' # Blue | Dynamic match - - [DIETPI_GREEN]='\e[38;5;154m' - [GREY]='\e[90m' - [RED]='\e[91m' - [GREEN]='\e[1;32m' - [YELLOW]='\e[1;33m' - [BLUE]='\e[1;36m' - [GOLD]='\e[38;5;220m' - [PURPLE]='\e[38;5;141m' - [VIVID_RED]='\e[38;5;196m' - [CYAN]='\e[38;5;45m' - - [RESET]='\e[0m' - [BOLD]='\e[1m' - [DIM]='\e[2m' - [UNDERLINE]='\e[4m' - [INVERSE]='\e[7m' - [BLINK]='\e[5m' - [HIDDEN]='\e[8m' - - [FG_BLACK]='\e[30m' - [FG_RED]='\e[31m' - [FG_GREEN]='\e[32m' - [FG_YELLOW]='\e[33m' - [FG_BLUE]='\e[34m' - [FG_MAGENTA]='\e[35m' - [FG_CYAN]='\e[36m' - [FG_WHITE]='\e[37m' - - [BRIGHT_BLACK]='\e[90m' - [BRIGHT_RED]='\e[91m' - [BRIGHT_GREEN]='\e[92m' - [BRIGHT_YELLOW]='\e[93m' - [BRIGHT_BLUE]='\e[94m' - [BRIGHT_MAGENTA]='\e[95m' - [BRIGHT_CYAN]='\e[96m' - [BRIGHT_WHITE]='\e[97m' - - [BG_BLACK]='\e[40m' - [BG_RED]='\e[41m' - [BG_GREEN]='\e[42m' - [BG_YELLOW]='\e[43m' - [BG_BLUE]='\e[44m' - [BG_MAGENTA]='\e[45m' - [BG_CYAN]='\e[46m' - [BG_WHITE]='\e[47m' - [BG_DARK_GREY]='\e[48;5;236m' - - [BG_BRIGHT_BLACK]='\e[100m' - [BG_BRIGHT_RED]='\e[101m' - [BG_BRIGHT_GREEN]='\e[102m' - [BG_BRIGHT_YELLOW]='\e[103m' - [BG_BRIGHT_BLUE]='\e[104m' - [BG_BRIGHT_MAGENTA]='\e[105m' - [BG_BRIGHT_CYAN]='\e[106m' - [BG_BRIGHT_WHITE]='\e[107m' - - [TRUE_GREEN]='\e[38;2;46;204;113m' - [TRUE_BG]='\e[48;2;44;62;80m' - + # Default colors + [ACCENT]='\e[38;5;154m' # DietPi green | Lines, bullets and separators + [STRONG]='\e[1m' # Bold white | Main descriptions + [WEAK]='\e[90m' # Grey | Subdued text + [ALERT]='\e[91m' # Red | Update notifications / Bad state + [GOOD]='\e[1;32m' # Green | Good state + [HIGHLIGHT]='\e[1;33m' # Yellow | Warning state + [ALT]='\e[1;36m' # Blue | Dynamic match ) + declare -g COLOUR_RESET='\e[0m' # Reset formatting to default (do not override this) - # Load settings here, to have chosen ${aCOLOUR[N]} applied to below strings + # Load settings here, to have chosen custom CLI colors applied + # disable shellcheck for non-constant `source` (`.`) # shellcheck disable=SC1090 [[ -f $FP_SAVEFILE ]] && . "$FP_SAVEFILE" } @@ -168,26 +116,17 @@ BW_INDENT_FIXED=3 # Default custom command example - CUSTOM_COMMANDS[0]='echo -e "$GREEN_BULLET ${aCOLOUR[RED]}Hello ${aCOLOUR[GREEN]}World! ${COLOUR_RESET} :)"' + CUSTOM_COMMANDS[0]='echo -e "$CLI_BULLET ${aCOLOUR[ALERT]}Hello ${aCOLOUR[GOOD]}World! ${COLOUR_RESET} :)"' # Default disk patterns SPACE_BASE_PATTERNS=("/" "/mnt/dietpi_userdata") # Load user preferences, overriding the above defaults as needed DIETPI_BANNER_LOAD_PREFS - # Put loaded colors into the used slots - aCOLOUR[DIETPI_GREEN]=${aCOLOUR[0]} - aCOLOUR[WHITE]=${aCOLOUR[1]} - aCOLOUR[GREY]=${aCOLOUR[2]} - aCOLOUR[RED]=${aCOLOUR[3]} - aCOLOUR[GREEN]=${aCOLOUR[4]} - aCOLOUR[YELLOW]=${aCOLOUR[5]} - aCOLOUR[BLUE]=${aCOLOUR[6]} - # Derived convenience strings - GREEN_LINE=" ${aCOLOUR[DIETPI_GREEN]}─────────────────────────────────────────────────────$COLOUR_RESET" - GREEN_BULLET=" ${aCOLOUR[DIETPI_GREEN]}-$COLOUR_RESET" - GREEN_SEPARATOR="${aCOLOUR[DIETPI_GREEN]}:$COLOUR_RESET" + CLI_LINE=" ${aCOLOUR[ACCENT]}─────────────────────────────────────────────────────$COLOUR_RESET" + CLI_BULLET=" ${aCOLOUR[ACCENT]}-$COLOUR_RESET" + CLI_SEPARATOR="${aCOLOUR[ACCENT]}:$COLOUR_RESET" DIETPI_VERSION="$G_DIETPI_VERSION_CORE.$G_DIETPI_VERSION_SUB.$G_DIETPI_VERSION_RC" if [[ $G_GITOWNER != 'MichaIng' ]] @@ -255,8 +194,11 @@ # Persist only the primary colour slots (0..6). Convert any actual # ESC bytes to the two-character sequence \e so the saved file is # portable when sourced in different shells. - for i in {0..6} + for i in "${!aCOLOUR[@]}" do + # Don't save the RESET colour slot + [[ "$i" == "RESET" ]] && continue + val="${aCOLOUR[$i]}" # Replace actual ESC bytes (0x1b) with literal \e esc=$(printf '%s' "$val" | sed $'s/\x1b/\\e/g') @@ -288,37 +230,37 @@ # DietPi update available? if Check_DietPi_Update then - local text_update_available_date="${aCOLOUR[YELLOW]}Update available" + local text_update_available_date="${aCOLOUR[HIGHLIGHT]}Update available" # New DietPi live patches available? elif Check_DietPi_Live_Patches then - local text_update_available_date="${aCOLOUR[YELLOW]}New live patches available" + local text_update_available_date="${aCOLOUR[HIGHLIGHT]}New live patches available" # APT update available? elif Check_APT_Updates then - local text_update_available_date="${aCOLOUR[YELLOW]}$PACKAGE_COUNT APT updates available" + local text_update_available_date="${aCOLOUR[HIGHLIGHT]}$PACKAGE_COUNT APT updates available" # Reboot required to finalise kernel upgrade? elif Check_Reboot then - local text_update_available_date="${aCOLOUR[RED]}Reboot required" + local text_update_available_date="${aCOLOUR[ALERT]}Reboot required" else local locale=$(sed -n '/^[[:blank:]]*AUTO_SETUP_LOCALE=/{s/^[^=]*=//p;q}' /boot/dietpi.txt) local text_update_available_date=$(LC_ALL=${locale:-C.UTF-8} date '+%R - %a %x') fi - echo -e "$GREEN_LINE - ${aCOLOUR[WHITE]}DietPi v$DIETPI_VERSION$COLOUR_RESET $GREEN_SEPARATOR $text_update_available_date$COLOUR_RESET -$GREEN_LINE" + echo -e "$CLI_LINE + ${aCOLOUR[STRONG]}DietPi v$DIETPI_VERSION$COLOUR_RESET $CLI_SEPARATOR $text_update_available_date$COLOUR_RESET +$CLI_LINE" } Print_Subheader() { # ON THE CURRENT LINE Output green line separator with subheader title "$1" - echo -e "\r$GREEN_LINE - ${aCOLOUR[WHITE]}$1 $GREEN_SEPARATOR" + echo -e "\r$CLI_LINE + ${aCOLOUR[STRONG]}$1 $CLI_SEPARATOR" } Get_Local_Ip() @@ -338,6 +280,7 @@ $GREEN_LINE" local i for i in /etc/letsencrypt/live/*/cert.pem do + # disable shellcheck for 'require-double-brackets' so it is `dash` compatible # shellcheck disable=SC2292 [ -f "$i" ] || continue openssl x509 -enddate -noout -in "$i" | mawk '/notAfter=/{print "Valid until "$4"-"substr($1,10)"-"$2" "$3}' @@ -362,14 +305,14 @@ $GREEN_LINE" case $code in 0) case "$stat" in - Valid*) stat="${aCOLOUR[GREY]}$stat${COLOUR_RESET}" ;; - No\ certificate*) stat="${aCOLOUR[RED]}$stat${COLOUR_RESET}" ;; - *) stat="${aCOLOUR[YELLOW]}$stat${COLOUR_RESET}" ;; + Valid*) stat="${aCOLOUR[WEAK]}$stat${COLOUR_RESET}" ;; + No\ certificate*) stat="${aCOLOUR[ALERT]}$stat${COLOUR_RESET}" ;; + *) stat="${aCOLOUR[HIGHLIGHT]}$stat${COLOUR_RESET}" ;; esac ;; # stat is valid - 1) stat="${aCOLOUR[RED]}No certificate found${COLOUR_RESET}" ;; - 2) stat="${aCOLOUR[YELLOW]}NOPASSWD sudo required${COLOUR_RESET}" ;; - *) stat="${aCOLOUR[YELLOW]}Unable to obtain cert status${COLOUR_RESET}" ;; + 1) stat="${aCOLOUR[ALERT]}No certificate found${COLOUR_RESET}" ;; + 2) stat="${aCOLOUR[HIGHLIGHT]}NOPASSWD sudo required${COLOUR_RESET}" ;; + *) stat="${aCOLOUR[HIGHLIGHT]}Unable to obtain cert status${COLOUR_RESET}" ;; esac echo "$stat" @@ -377,7 +320,7 @@ $GREEN_LINE" Print_Credits() { - echo -e " ${aCOLOUR[GREY]}DietPi Team : https://github.com/MichaIng/DietPi#the-dietpi-project-team" + echo -e " ${aCOLOUR[WEAK]}DietPi Team : https://github.com/MichaIng/DietPi#the-dietpi-project-team" [[ -f '/boot/dietpi/.prep_info' ]] && mawk 'NR==1 {sub(/^0$/,"DietPi Core Team");a=$0} NR==2 {print " Image by : "a" (pre-image: "$0")"}' /boot/dietpi/.prep_info @@ -392,32 +335,32 @@ $GREEN_LINE" # DietPi update available? if [[ $AVAILABLE_UPDATE ]] then - echo -e " ${aCOLOUR[WHITE]}dietpi-update$COLOUR_RESET $GREEN_SEPARATOR ${aCOLOUR[YELLOW]}Run now to update DietPi from v$DIETPI_VERSION to v$AVAILABLE_UPDATE$COLOUR_RESET\n" + echo -e " ${aCOLOUR[STRONG]}dietpi-update$COLOUR_RESET $CLI_SEPARATOR ${aCOLOUR[HIGHLIGHT]}Run now to update DietPi from v$DIETPI_VERSION to v$AVAILABLE_UPDATE$COLOUR_RESET\n" # New DietPi live patches available? elif (( $LIVE_PATCHES )) then - echo -e " ${aCOLOUR[WHITE]}dietpi-update$COLOUR_RESET $GREEN_SEPARATOR ${aCOLOUR[YELLOW]}Run now to check out new available DietPi live patches$COLOUR_RESET\n" + echo -e " ${aCOLOUR[STRONG]}dietpi-update$COLOUR_RESET $CLI_SEPARATOR ${aCOLOUR[HIGHLIGHT]}Run now to check out new available DietPi live patches$COLOUR_RESET\n" # APT updates available? elif (( $PACKAGE_COUNT )) then - echo -e " ${aCOLOUR[WHITE]}apt upgrade$COLOUR_RESET $GREEN_SEPARATOR ${aCOLOUR[YELLOW]}Run now to apply $PACKAGE_COUNT available APT package upgrades$COLOUR_RESET\n" + echo -e " ${aCOLOUR[STRONG]}apt upgrade$COLOUR_RESET $CLI_SEPARATOR ${aCOLOUR[HIGHLIGHT]}Run now to apply $PACKAGE_COUNT available APT package upgrades$COLOUR_RESET\n" # Reboot required to finalise kernel upgrade? elif (( $REBOOT_REQUIRED )) then - echo -e " ${aCOLOUR[WHITE]}reboot$COLOUR_RESET $GREEN_SEPARATOR ${aCOLOUR[RED]}A reboot is required to finalise a recent kernel upgrade$COLOUR_RESET\n" + echo -e " ${aCOLOUR[STRONG]}reboot$COLOUR_RESET $CLI_SEPARATOR ${aCOLOUR[ALERT]}A reboot is required to finalise a recent kernel upgrade$COLOUR_RESET\n" fi } Print_Useful_Commands() { - echo -e " ${aCOLOUR[WHITE]}dietpi-launcher$COLOUR_RESET $GREEN_SEPARATOR All the DietPi programs in one place - ${aCOLOUR[WHITE]}dietpi-config$COLOUR_RESET $GREEN_SEPARATOR Feature rich configuration tool for your device - ${aCOLOUR[WHITE]}dietpi-software$COLOUR_RESET $GREEN_SEPARATOR Select optimised software for installation - ${aCOLOUR[WHITE]}htop$COLOUR_RESET $GREEN_SEPARATOR Resource monitor - ${aCOLOUR[WHITE]}cpu$COLOUR_RESET $GREEN_SEPARATOR Shows CPU information and stats\n" + echo -e " ${aCOLOUR[STRONG]}dietpi-launcher$COLOUR_RESET $CLI_SEPARATOR All the DietPi programs in one place + ${aCOLOUR[STRONG]}dietpi-config$COLOUR_RESET $CLI_SEPARATOR Feature rich configuration tool for your device + ${aCOLOUR[STRONG]}dietpi-software$COLOUR_RESET $CLI_SEPARATOR Select optimised software for installation + ${aCOLOUR[STRONG]}htop$COLOUR_RESET $CLI_SEPARATOR Resource monitor + ${aCOLOUR[STRONG]}cpu$COLOUR_RESET $CLI_SEPARATOR Shows CPU information and stats\n" } Print_Item_State() @@ -427,9 +370,9 @@ $GREEN_LINE" local subtitle="$1" local state="$2" local width="${3:-}" - local color="${aCOLOUR[WHITE]}" + local color="${aCOLOUR[STRONG]}" printf "%b%b %-${width}b %b %b\n" \ - "$GREEN_BULLET" "${color}" "$subtitle" "$GREEN_SEPARATOR" "$state" + "$CLI_BULLET" "${color}" "$subtitle" "$CLI_SEPARATOR" "$state" } Get_Systemd_Status() @@ -437,9 +380,9 @@ $GREEN_LINE" # Print systemd overall status based on "systemctl --failed" FAILED_CNT=$(systemctl --failed --no-legend --no-pager | wc -l 2>/dev/null || true) if [[ -z "$FAILED_CNT" || "$FAILED_CNT" -eq 0 ]]; then - state="${aCOLOUR[GREY]}No Services Failed${COLOUR_RESET}" + state="${aCOLOUR[WEAK]}No Services Failed${COLOUR_RESET}" else - state="${aCOLOUR[RED]}$FAILED_CNT Service(s) Failed${COLOUR_RESET}" + state="${aCOLOUR[ALERT]}$FAILED_CNT Service(s) Failed${COLOUR_RESET}" fi echo "$state" } @@ -480,25 +423,25 @@ $GREEN_LINE" 0) if [[ $COUNTIPS -eq 0 ]]; then # zero bans can be suspicious - something may not be working correctly - state="${aCOLOUR[YELLOW]}No IP(s) Banned${COLOUR_RESET}" + state="${aCOLOUR[HIGHLIGHT]}No IP(s) Banned${COLOUR_RESET}" else if [[ $COUNTIPS -gt 20 ]]; then # high number of bans - state="${aCOLOUR[RED]}$COUNTIPS IP(s) Banned${COLOUR_RESET}" + state="${aCOLOUR[ALERT]}$COUNTIPS IP(s) Banned${COLOUR_RESET}" else # low number of bans - state="${aCOLOUR[GREY]}$COUNTIPS IP(s) Banned${COLOUR_RESET}" + state="${aCOLOUR[WEAK]}$COUNTIPS IP(s) Banned${COLOUR_RESET}" fi fi ;; 1) - state="${aCOLOUR[RED]}No Fail2Ban status available${COLOUR_RESET}" + state="${aCOLOUR[ALERT]}No Fail2Ban status available${COLOUR_RESET}" ;; 2) - state="${aCOLOUR[YELLOW]}NOPASSWD sudo required to obtain Fail2Ban status${COLOUR_RESET}" + state="${aCOLOUR[HIGHLIGHT]}NOPASSWD sudo required to obtain Fail2Ban status${COLOUR_RESET}" ;; *) - state="${aCOLOUR[RED]}Unknown state $COUNTIPS${COLOUR_RESET}" + state="${aCOLOUR[ALERT]}Unknown state $COUNTIPS${COLOUR_RESET}" ;; esac @@ -567,7 +510,7 @@ $GREEN_LINE" if [[ "$RX" != "N/A" || "$TX" != "N/A" ]]; then printf "%s %s%-20s %s%s TX= %12s RX= %12s%s\n" \ - "$GREEN_BULLET" "${aCOLOUR[GREY]}" "$ip_addr ($ifname)" "$GREEN_SEPARATOR" "${aCOLOUR[GREY]}" "$TX" "$RX" "$COLOUR_RESET" + "$CLI_BULLET" "${aCOLOUR[WEAK]}" "$ip_addr ($ifname)" "$CLI_SEPARATOR" "${aCOLOUR[WEAK]}" "$TX" "$RX" "$COLOUR_RESET" fi fi done @@ -598,12 +541,12 @@ $GREEN_LINE" # IP="${BASH_REMATCH[0]}" printf " %s%s [%s]%s\n" \ - "${aCOLOUR[BLUE]}" "$IP" "$display_ns" "$COLOUR_RESET" + "${aCOLOUR[ALT]}" "$IP" "$display_ns" "$COLOUR_RESET" elif [[ -z $IP ]]; then # If the IP is empty, it likely means the namespace is disconnected or has no internet access. Print status message. printf "%s %s %s %s%s%s\n" \ - "${aCOLOUR[RED]}" "Disconnected" "$GREEN_SEPARATOR" "${aCOLOUR[WHITE]}" "$display_ns" "$COLOUR_RESET" + "${aCOLOUR[ALERT]}" "Disconnected" "$CLI_SEPARATOR" "${aCOLOUR[STRONG]}" "$display_ns" "$COLOUR_RESET" else if [[ $IP =~ $err_re ]]; then @@ -613,7 +556,7 @@ $GREEN_LINE" # If the IP contains an unknown or error message, print it with the namespace printf " %s%s [%s]%s\n" \ - "${aCOLOUR[YELLOW]}" "$IP" "$display_ns" "$COLOUR_RESET" + "${aCOLOUR[HIGHLIGHT]}" "$IP" "$display_ns" "$COLOUR_RESET" fi DATA_USAGE "$ns" @@ -625,11 +568,10 @@ $GREEN_LINE" for i in "${!CUSTOM_COMMANDS[@]}"; do local cmd="${CUSTOM_COMMANDS[$i]}" if [[ -n "$cmd" ]]; then - # shellcheck disable=SC2086 output="$(eval "$cmd" 2>&1)" status=$? if [[ $status -ne 0 ]]; then - printf '%b\n' "${aCOLOUR[RED]}Error executing CUSTOM_COMMAND[$((i))] Exit Code: ${status}${COLOUR_RESET}" + printf '%b\n' "${aCOLOUR[ALERT]}Error executing CUSTOM_COMMAND[$((i))] Exit Code: ${status}${COLOUR_RESET}" else printf '%b\n' "$output" fi @@ -708,7 +650,7 @@ $GREEN_LINE" fi # Print aligned columns: {basename} : {used} of {size} ({percent used}) printf "%s %s%-${length}s %s%s %8sGiB of %8sGiB (%6b)%s\n" \ - "$GREEN_BULLET" "${aCOLOUR[BLUE]}" "$name" "$GREEN_SEPARATOR" "${aCOLOUR[GREY]}" "$used_gb" "$size_gb" "$perc" "$COLOUR_RESET" + "$CLI_BULLET" "${aCOLOUR[ALT]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[WEAK]}" "$used_gb" "$size_gb" "$perc" "$COLOUR_RESET" done } @@ -717,9 +659,9 @@ $GREEN_LINE" { local state="$( /boot/dietpi/dietpi-vpn status 2>&1 )" case "$state" in - Connected*) state="${aCOLOUR[GREY]}$state${COLOUR_RESET}" ;; - Disconnected*) state="${aCOLOUR[RED]}$state${COLOUR_RESET}" ;; - *) state="${aCOLOUR[YELLOW]}$state${COLOUR_RESET}" ;; + Connected*) state="${aCOLOUR[WEAK]}$state${COLOUR_RESET}" ;; + Disconnected*) state="${aCOLOUR[ALERT]}$state${COLOUR_RESET}" ;; + *) state="${aCOLOUR[HIGHLIGHT]}$state${COLOUR_RESET}" ;; esac echo "$state" } @@ -754,9 +696,9 @@ $GREEN_LINE" # DietPi-VPN connection status (( ${aENABLED[13]} )) && Print_Item_State "${aDESCRIPTION[13]}" "$(Get_VPN_Status 2>&1)" # Disk usage (RootFS) - (( ${aENABLED[7]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[WHITE]}${aDESCRIPTION[7]} $GREEN_SEPARATOR $(df -h --output=used,size,pcent / | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" + (( ${aENABLED[7]} )) && echo -e "$CLI_BULLET ${aCOLOUR[STRONG]}${aDESCRIPTION[7]} $CLI_SEPARATOR $(df -h --output=used,size,pcent / | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" # Disk usage (DietPi userdata) - (( ${aENABLED[8]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[WHITE]}${aDESCRIPTION[8]} $GREEN_SEPARATOR $(df -h --output=used,size,pcent /mnt/dietpi_userdata | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" + (( ${aENABLED[8]} )) && echo -e "$CLI_BULLET ${aCOLOUR[STRONG]}${aDESCRIPTION[8]} $CLI_SEPARATOR $(df -h --output=used,size,pcent /mnt/dietpi_userdata | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" # Weather (( ${aENABLED[9]} )) && Print_Item_State "${aDESCRIPTION[9]}" "$(curl -sSfLm 3 'https://wttr.in/?format=4' 2>&1)" # Let's Encrypt cert status @@ -776,17 +718,18 @@ $GREEN_LINE" (( ${aENABLED[10]} )) && Print_Subheader "Custom Commands" && Print_Custom_Commands # Separator line - (( ${aENABLED[11]} || ${aENABLED[12]} || ${aENABLED[15]} )) && echo -e "$GREEN_LINE" + (( ${aENABLED[11]} || ${aENABLED[12]} || ${aENABLED[15]} )) && echo -e "$CLI_LINE" # MOTD if (( ${aENABLED[12]} )) then local motd fp_motd='/run/dietpi/.dietpi_motd' [[ -f $fp_motd ]] || curl -sSfLm 3 'https://dietpi.com/motd' -o "$fp_motd" + # disable shellcheck for non-constant `source` (`.`) # shellcheck disable=SC1090 [[ -f $fp_motd ]] && . "$fp_motd" &> /dev/null && [[ $motd ]] && Print_Item_State "${aDESCRIPTION[12]}" "$motd" fi - echo -e "$GREEN_LINE\n" + echo -e "$CLI_LINE\n" (( ${aENABLED[15]} )) && Print_Credits Print_Updates @@ -829,7 +772,9 @@ $GREEN_LINE" if (( $i == 10 )) then - example_command='echo -e "$GREEN_BULLET ${aCOLOUR[RED]}Hello ${aCOLOUR[GREEN]}World! ${COLOUR_RESET} :)"\n\n-- or with the standard format --\n\nPrint_Item_State "Description" "value"' + # ignore check for single/double quote expansion issue; unassigned var is referenced; + # shellcheck disable=SC2016,SC2154 + example_command='echo -e "$CLI_BULLET ${aCOLOUR[ALERT]}Hello ${aCOLOUR[GOOD]}World! ${COLOUR_RESET} :)"\n\n-- or with the standard format --\n\nPrint_Item_State "Description" "value"' # Manage custom commands list while true; do @@ -889,6 +834,8 @@ $GREEN_LINE" elif (( $i == 22 )) then + # ignore check for single/double quote expansion issue; single/double quotes for glob; + # shellcheck disable=SC2016,SC2086 pattern_hint="The matching mount 'target's returned by the 'df' command will be reported. To use regex, start the pattern with 're:', otherwise shell globbing is used. For example: '/mnt/*' or 're:^/mnt/.*/*$'" @@ -929,6 +876,8 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" *) ;; esac done + + # If no patterns are configured, disable the disk usage option (( ${#SPACE_BASE_PATTERNS[@]} == 0 )) && aENABLED[22]=0 fi done From 8fd257098b3696c81af36951531821776ee9facb Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 28 Jun 2026 14:18:06 -0500 Subject: [PATCH 06/19] added colors reference --- dietpi/func/dietpi-banner | 1 + 1 file changed, 1 insertion(+) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index f7b0f76462..c800ab51ed 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -48,6 +48,7 @@ # Custom preferences initialisation helper. It loads the user-saved settings file # ($FP_SAVEFILE) so custom colours, commands, and other preferences are applied before they are used. # Color and format references: + # `https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit` # `https://invisible-island.net/xterm/ctlseqs/ctlseqs.html` under section "Character Attributes (SGR)" # `https://www.ditig.com/256-colors-cheat-sheet` # `https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters` From 46351ed1a07dcfddfbc3f58dcf856e86a1bba278 Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 28 Jun 2026 15:57:19 -0500 Subject: [PATCH 07/19] combined sudo and ns command prefixes since they were only ever used together --- dietpi/func/dietpi-banner | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index c800ab51ed..2123504a1b 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -461,9 +461,9 @@ $CLI_LINE" # Use an array prefix so we can run commands either in a netns # (ip netns exec ...) or the default namespace, and with sudo if needed - local -a ns_cmd_prefix=() sudo_cmd_prefix=() + local -a ns_cmd_prefix=() if [[ "$(id -u)" -ne "0" ]]; then - sudo_cmd_prefix+=(sudo -n) + ns_cmd_prefix+=(sudo -n) fi if [[ -n "$netns" ]]; then @@ -471,7 +471,7 @@ $CLI_LINE" fi # Query interface list once inside the (optional) namespace and match names - mapfile -t observed_ifaces < <("${sudo_cmd_prefix[@]}" "${ns_cmd_prefix[@]}" ip -o link show 2>/dev/null | awk -F': ' '{print $2}') + mapfile -t observed_ifaces < <("${ns_cmd_prefix[@]}" ip -o link show 2>/dev/null | awk -F': ' '{print $2}') for IFACE in "${observed_ifaces[@]}"; do # strip kernel-style peer suffix (e.g. eth0@if8) and use base name @@ -493,20 +493,20 @@ $CLI_LINE" local RX="N/A" TX="N/A" rx tx ip_addr ip_addr_cidr # Get RX data - if "${sudo_cmd_prefix[@]}" "${ns_cmd_prefix[@]}" test -r "/sys/class/net/$ifname/statistics/rx_bytes"; then - rx=$("${sudo_cmd_prefix[@]}" "${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/rx_bytes" 2>/dev/null || :) + if "${ns_cmd_prefix[@]}" test -r "/sys/class/net/$ifname/statistics/rx_bytes"; then + rx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/rx_bytes" 2>/dev/null || :) [[ $rx =~ ^[0-9]+$ ]] && RX="$(awk -v v="$rx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" fi # Get TX data - if "${sudo_cmd_prefix[@]}" "${ns_cmd_prefix[@]}" test -r "/sys/class/net/$ifname/statistics/tx_bytes"; then - tx=$("${sudo_cmd_prefix[@]}" "${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/tx_bytes" 2>/dev/null || :) + if "${ns_cmd_prefix[@]}" test -r "/sys/class/net/$ifname/statistics/tx_bytes"; then + tx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/tx_bytes" 2>/dev/null || :) [[ $tx =~ ^[0-9]+$ ]] && TX="$(awk -v v="$tx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" fi # Get IP address (IPv4, global scope) for this interface in this namespace. If multiple are present, just take the first. If none, show "- No IP -" ip_addr=" - No IP - " - ip_addr_cidr=$("${sudo_cmd_prefix[@]}" "${ns_cmd_prefix[@]}" ip -o -4 addr show dev "$ifname" scope global 2>/dev/null | awk '{print $4}' | head -n1) + ip_addr_cidr=$("${ns_cmd_prefix[@]}" ip -o -4 addr show dev "$ifname" scope global 2>/dev/null | awk '{print $4}' | head -n1) [[ -n "$ip_addr_cidr" ]] && ip_addr=${ip_addr_cidr%%/*} if [[ "$RX" != "N/A" || "$TX" != "N/A" ]]; then From 030a3c97da0d66fdbee35457214c05a7b5f0213e Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 28 Jun 2026 16:01:49 -0500 Subject: [PATCH 08/19] fixed syntax for shellcheck warning --- dietpi/func/dietpi-globals | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dietpi/func/dietpi-globals b/dietpi/func/dietpi-globals index 69474923d0..214bd82be0 100644 --- a/dietpi/func/dietpi-globals +++ b/dietpi/func/dietpi-globals @@ -1506,9 +1506,8 @@ Press any key to continue...' # Inputs while (( $# )) do - # shellcheck disable=SC2015 case $1 in - '-t') shift; (( ${1/.} )) && timeout=$1 || { G_DIETPI-NOTIFY 1 "Invalid timeout \"$1\", aborting..."; return 1; };; + '-t') shift; { (( ${1/.} )) && timeout=$1; } || { G_DIETPI-NOTIFY 1 "Invalid timeout \"$1\", aborting..."; return 1; };; '-n') shift; ns="${1:-}";; *) G_DIETPI-NOTIFY 1 "Invalid argument \"$1\", aborting..."; return 1;; esac From 0bab699dd38c8bb035392e10db3fa54dfb840f2a Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 28 Jun 2026 16:03:42 -0500 Subject: [PATCH 09/19] fixed indents --- dietpi/func/dietpi-banner | 160 +++++++++++++++++++------------------- 1 file changed, 80 insertions(+), 80 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 2123504a1b..4c4a17ae0e 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -457,64 +457,64 @@ $CLI_LINE" local err_re='timed out|timeout|could not resolve|not known|unknown' DATA_USAGE(){ - local netns="$1" + local netns="$1" - # Use an array prefix so we can run commands either in a netns - # (ip netns exec ...) or the default namespace, and with sudo if needed - local -a ns_cmd_prefix=() - if [[ "$(id -u)" -ne "0" ]]; then - ns_cmd_prefix+=(sudo -n) - fi + # Use an array prefix so we can run commands either in a netns + # (ip netns exec ...) or the default namespace, and with sudo if needed + local -a ns_cmd_prefix=() + if [[ "$(id -u)" -ne "0" ]]; then + ns_cmd_prefix+=(sudo -n) + fi - if [[ -n "$netns" ]]; then - ns_cmd_prefix+=(ip netns exec "$netns") - fi + if [[ -n "$netns" ]]; then + ns_cmd_prefix+=(ip netns exec "$netns") + fi - # Query interface list once inside the (optional) namespace and match names - mapfile -t observed_ifaces < <("${ns_cmd_prefix[@]}" ip -o link show 2>/dev/null | awk -F': ' '{print $2}') - - for IFACE in "${observed_ifaces[@]}"; do - # strip kernel-style peer suffix (e.g. eth0@if8) and use base name - local ifname="${IFACE%%@*}" - # skip loopback - [[ "$ifname" == lo ]] && continue - - case "$ifname" in - # wired / predictable names - eth*|en*|lan*) handle_iface=1 ;; - # wireless - wlan*|wl*) handle_iface=1 ;; - # vpn/tunnel/virtual - tun*|wg*|vpn*) handle_iface=1 ;; - *) continue ;; - esac + # Query interface list once inside the (optional) namespace and match names + mapfile -t observed_ifaces < <("${ns_cmd_prefix[@]}" ip -o link show 2>/dev/null | awk -F': ' '{print $2}') + + for IFACE in "${observed_ifaces[@]}"; do + # strip kernel-style peer suffix (e.g. eth0@if8) and use base name + local ifname="${IFACE%%@*}" + # skip loopback + [[ "$ifname" == lo ]] && continue + + case "$ifname" in + # wired / predictable names + eth*|en*|lan*) handle_iface=1 ;; + # wireless + wlan*|wl*) handle_iface=1 ;; + # vpn/tunnel/virtual + tun*|wg*|vpn*) handle_iface=1 ;; + *) continue ;; + esac - if (( handle_iface )); then - local RX="N/A" TX="N/A" rx tx ip_addr ip_addr_cidr + if (( handle_iface )); then + local RX="N/A" TX="N/A" rx tx ip_addr ip_addr_cidr - # Get RX data - if "${ns_cmd_prefix[@]}" test -r "/sys/class/net/$ifname/statistics/rx_bytes"; then - rx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/rx_bytes" 2>/dev/null || :) - [[ $rx =~ ^[0-9]+$ ]] && RX="$(awk -v v="$rx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" - fi + # Get RX data + if "${ns_cmd_prefix[@]}" test -r "/sys/class/net/$ifname/statistics/rx_bytes"; then + rx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/rx_bytes" 2>/dev/null || :) + [[ $rx =~ ^[0-9]+$ ]] && RX="$(awk -v v="$rx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" + fi - # Get TX data - if "${ns_cmd_prefix[@]}" test -r "/sys/class/net/$ifname/statistics/tx_bytes"; then - tx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/tx_bytes" 2>/dev/null || :) - [[ $tx =~ ^[0-9]+$ ]] && TX="$(awk -v v="$tx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" - fi + # Get TX data + if "${ns_cmd_prefix[@]}" test -r "/sys/class/net/$ifname/statistics/tx_bytes"; then + tx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/tx_bytes" 2>/dev/null || :) + [[ $tx =~ ^[0-9]+$ ]] && TX="$(awk -v v="$tx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" + fi - # Get IP address (IPv4, global scope) for this interface in this namespace. If multiple are present, just take the first. If none, show "- No IP -" - ip_addr=" - No IP - " - ip_addr_cidr=$("${ns_cmd_prefix[@]}" ip -o -4 addr show dev "$ifname" scope global 2>/dev/null | awk '{print $4}' | head -n1) - [[ -n "$ip_addr_cidr" ]] && ip_addr=${ip_addr_cidr%%/*} + # Get IP address (IPv4, global scope) for this interface in this namespace. If multiple are present, just take the first. If none, show "- No IP -" + ip_addr=" - No IP - " + ip_addr_cidr=$("${ns_cmd_prefix[@]}" ip -o -4 addr show dev "$ifname" scope global 2>/dev/null | awk '{print $4}' | head -n1) + [[ -n "$ip_addr_cidr" ]] && ip_addr=${ip_addr_cidr%%/*} - if [[ "$RX" != "N/A" || "$TX" != "N/A" ]]; then - printf "%s %s%-20s %s%s TX= %12s RX= %12s%s\n" \ - "$CLI_BULLET" "${aCOLOUR[WEAK]}" "$ip_addr ($ifname)" "$CLI_SEPARATOR" "${aCOLOUR[WEAK]}" "$TX" "$RX" "$COLOUR_RESET" - fi + if [[ "$RX" != "N/A" || "$TX" != "N/A" ]]; then + printf "%s %s%-20s %s%s TX= %12s RX= %12s%s\n" \ + "$CLI_BULLET" "${aCOLOUR[WEAK]}" "$ip_addr ($ifname)" "$CLI_SEPARATOR" "${aCOLOUR[WEAK]}" "$TX" "$RX" "$COLOUR_RESET" fi - done + fi + done } mapfile -t namespaces < <(ip netns ls | awk 'NF>1 { print $1 }') @@ -522,45 +522,45 @@ $CLI_LINE" namespaces=( "" "${namespaces[@]}" ) for ns in "${namespaces[@]}"; do - raw_ns="${ns:-default}" - display_ns="$(G_TRUNCATE_MID "$raw_ns" 30)" - - if [[ "$(id -u)" = "0" ]]; then - # Running as root, can call function directly - IP=$(G_GET_WAN_IP -t 1 -n "$ns" 2>&1) - else - # Running as non-root: Fail silently without NOPASSWD to avoid password prompt - # Get public IP, sourcing globals inside the sudo'd shell - # Pass namespace as positional arg to avoid expansion issues inside the subshell - IP=$(sudo -n bash -c '. /boot/dietpi/func/dietpi-globals >/dev/null 2>&1; G_GET_WAN_IP -t 1 -n "$1" 2>&1' _ "$ns") - fi + raw_ns="${ns:-default}" + display_ns="$(G_TRUNCATE_MID "$raw_ns" 30)" - if [[ $IP =~ $IP_re ]]; then - # Valid IP detected, keep as-is and print with namespace + if [[ "$(id -u)" = "0" ]]; then + # Running as root, can call function directly + IP=$(G_GET_WAN_IP -t 1 -n "$ns" 2>&1) + else + # Running as non-root: Fail silently without NOPASSWD to avoid password prompt + # Get public IP, sourcing globals inside the sudo'd shell + # Pass namespace as positional arg to avoid expansion issues inside the subshell + IP=$(sudo -n bash -c '. /boot/dietpi/func/dietpi-globals >/dev/null 2>&1; G_GET_WAN_IP -t 1 -n "$1" 2>&1' _ "$ns") + fi - # # Extract just the IP portion (use BASH_REMATCH[0]). Removes the geographic info. - # IP="${BASH_REMATCH[0]}" + if [[ $IP =~ $IP_re ]]; then + # Valid IP detected, keep as-is and print with namespace - printf " %s%s [%s]%s\n" \ - "${aCOLOUR[ALT]}" "$IP" "$display_ns" "$COLOUR_RESET" + # # Extract just the IP portion (use BASH_REMATCH[0]). Removes the geographic info. + # IP="${BASH_REMATCH[0]}" - elif [[ -z $IP ]]; then - # If the IP is empty, it likely means the namespace is disconnected or has no internet access. Print status message. - printf "%s %s %s %s%s%s\n" \ - "${aCOLOUR[ALERT]}" "Disconnected" "$CLI_SEPARATOR" "${aCOLOUR[STRONG]}" "$display_ns" "$COLOUR_RESET" + printf " %s%s [%s]%s\n" \ + "${aCOLOUR[ALT]}" "$IP" "$display_ns" "$COLOUR_RESET" - else - if [[ $IP =~ $err_re ]]; then - # Treat known curl/network error messages as timeout/DNS failures - IP="Timeout/DNS Failure" - fi + elif [[ -z $IP ]]; then + # If the IP is empty, it likely means the namespace is disconnected or has no internet access. Print status message. + printf "%s %s %s %s%s%s\n" \ + "${aCOLOUR[ALERT]}" "Disconnected" "$CLI_SEPARATOR" "${aCOLOUR[STRONG]}" "$display_ns" "$COLOUR_RESET" - # If the IP contains an unknown or error message, print it with the namespace - printf " %s%s [%s]%s\n" \ - "${aCOLOUR[HIGHLIGHT]}" "$IP" "$display_ns" "$COLOUR_RESET" + else + if [[ $IP =~ $err_re ]]; then + # Treat known curl/network error messages as timeout/DNS failures + IP="Timeout/DNS Failure" fi - DATA_USAGE "$ns" + # If the IP contains an unknown or error message, print it with the namespace + printf " %s%s [%s]%s\n" \ + "${aCOLOUR[HIGHLIGHT]}" "$IP" "$display_ns" "$COLOUR_RESET" + fi + + DATA_USAGE "$ns" done } From 1842ff38996dbbb978a04a54b9d1b0e31cf8a708 Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 28 Jun 2026 16:24:12 -0500 Subject: [PATCH 10/19] added BANNER_GET_WAN_IP so dietpi-banner can have it's own behavior --- dietpi/func/dietpi-banner | 45 ++++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 4c4a17ae0e..06e9751713 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -449,6 +449,39 @@ $CLI_LINE" echo "$state" } + # Print public IP address and location info + # - Optional arguments: + # -t : Set timeout in seconds, supports floats, default: 3 + # -n : Run curl in the given network namespace, e.g. to obtain the WAN IP of a specific + # network namespace, default: empty (no namespace = the main default namespace) + BANNER_GET_WAN_IP() + { + # Defaults + local timeout=3 + local ns="" + # Inputs + while (( $# )) + do + case $1 in + '-t') shift; { (( ${1/.} )) && timeout=$1; } || { G_DIETPI-NOTIFY 1 "Invalid timeout \"$1\", aborting..."; return 1; };; + '-n') shift; ns="${1:-}";; + *) G_DIETPI-NOTIFY 1 "Invalid argument \"$1\", aborting..."; return 1;; + esac + shift + done + + local -a ns_cmd_prefix=() + if [[ "$(id -u)" -ne "0" ]]; then + ns_cmd_prefix+=(sudo -n) + fi + + if [[ -n "$ns" ]]; then + ns_cmd_prefix+=(ip netns exec "$ns") + fi + + "${ns_cmd_prefix[@]}" curl -sSfLm "$timeout" 'https://dietpi.com/geoip' 2>&1 + } + Get_Network_Usage() { # IP address detection (IPv4) @@ -525,15 +558,7 @@ $CLI_LINE" raw_ns="${ns:-default}" display_ns="$(G_TRUNCATE_MID "$raw_ns" 30)" - if [[ "$(id -u)" = "0" ]]; then - # Running as root, can call function directly - IP=$(G_GET_WAN_IP -t 1 -n "$ns" 2>&1) - else - # Running as non-root: Fail silently without NOPASSWD to avoid password prompt - # Get public IP, sourcing globals inside the sudo'd shell - # Pass namespace as positional arg to avoid expansion issues inside the subshell - IP=$(sudo -n bash -c '. /boot/dietpi/func/dietpi-globals >/dev/null 2>&1; G_GET_WAN_IP -t 1 -n "$1" 2>&1' _ "$ns") - fi + IP=$(BANNER_GET_WAN_IP -t 1 -n "$ns" 2>&1) if [[ $IP =~ $IP_re ]]; then # Valid IP detected, keep as-is and print with namespace @@ -693,7 +718,7 @@ $CLI_LINE" # LAN IP (( ${aENABLED[5]} )) && Print_Item_State "${aDESCRIPTION[5]}" "$(Get_Local_Ip 2>&1)" # WAN IP + location info - (( ${aENABLED[6]} )) && Print_Item_State "${aDESCRIPTION[6]}" "$(G_GET_WAN_IP 2>&1)" + (( ${aENABLED[6]} )) && Print_Item_State "${aDESCRIPTION[6]}" "$(BANNER_GET_WAN_IP 2>&1)" # DietPi-VPN connection status (( ${aENABLED[13]} )) && Print_Item_State "${aDESCRIPTION[13]}" "$(Get_VPN_Status 2>&1)" # Disk usage (RootFS) From bd7d19358a033dd2fc5bcd5753854ff9ebc2e058 Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 29 Jun 2026 00:27:45 -0500 Subject: [PATCH 11/19] fail2ban status now uses 'fail2ban-client banned'. removed unneeded read tests from network data usage. Get_Fail2Ban_Status is cleaner without the complicated sudo handling. --- dietpi/func/dietpi-banner | 107 ++++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 56 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 06e9751713..0fcdbb0bfe 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -129,6 +129,10 @@ CLI_BULLET=" ${aCOLOUR[ACCENT]}-$COLOUR_RESET" CLI_SEPARATOR="${aCOLOUR[ACCENT]}:$COLOUR_RESET" + # IP address detection (IPv4) + IP4_re='([0-9]{1,3}\.){3}[0-9]{1,3}' + IP4_re_cidr='([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}' + DIETPI_VERSION="$G_DIETPI_VERSION_CORE.$G_DIETPI_VERSION_SUB.$G_DIETPI_VERSION_RC" if [[ $G_GITOWNER != 'MichaIng' ]] then @@ -192,7 +196,7 @@ echo "aENABLED[$i]=${aENABLED[$i]}" done - # Persist only the primary colour slots (0..6). Convert any actual + # Persist only the primary colour slots. Convert any actual # ESC bytes to the two-character sequence \e so the saved file is # portable when sourced in different shells. for i in "${!aCOLOUR[@]}" @@ -392,57 +396,56 @@ $CLI_LINE" { Get_Fail2Ban_Status_Raw() { - # # helper function so that we can run it with sudo in non-root mode without needing to export all the above functions and variables. - # # also is `dash` compatible - - # Count unique banned IPs via iptables/nftables compatibility. Try iptables first, - # fall back to nft if available. - COUNTIPS=0 - if command -v iptables >/dev/null 2>&1; then - COUNTIPS=$(iptables -L -n 2>/dev/null | awk '/REJECT/ {print $0}' | uniq -c | wc -l) - elif command -v nft >/dev/null 2>&1; then - COUNTIPS=$(nft list ruleset 2>/dev/null | awk '/reject/ {print $0}' | uniq -c | wc -l) + if command -v fail2ban-client >/dev/null 2>&1; then + if [[ "$(id -u)" = "0" ]]; then + f2b_output="$(fail2ban-client banned 2>/dev/null)" + else + f2b_output="$(sudo -n fail2ban-client banned 2>/dev/null || return 2)" + [ $? = 2 ] && return 2 + fi + + if ! command -v sort >/dev/null 2>&1; then + sort() { cat; } # fallback if sort is not available, simply passes input through + fi + + # get banned IP address count + IP_addrs="$(echo "$f2b_output" | grep -o '\[[^]]*\]' | grep -Eo "$IP4_re" | sort -u)" + count_banned_ips="$(echo "$IP_addrs" | wc -l)" # | grep -cve '^[[:space:]]*$')" + echo "$count_banned_ips" + + return 0 else + # fail2ban-client command is not available return 1 fi - echo "$COUNTIPS" - return 0 } - COUNTIPS=0 - if [[ "$(id -u)" = "0" ]] - then - COUNTIPS="$(Get_Fail2Ban_Status_Raw 2>/dev/null || return 1)" - code=$? - else - # Running as non-root: Fail silently without NOPASSWD to avoid password prompt - COUNTIPS="$(sudo -n dash -c "$(declare -f Get_Fail2Ban_Status_Raw); Get_Fail2Ban_Status_Raw 2>&1" 2> /dev/null || return 2)" - code=$? - fi + count_banned_ips=$(Get_Fail2Ban_Status_Raw) + code=$? case $code in 0) - if [[ $COUNTIPS -eq 0 ]]; then + if [[ $count_banned_ips -eq 0 ]]; then # zero bans can be suspicious - something may not be working correctly state="${aCOLOUR[HIGHLIGHT]}No IP(s) Banned${COLOUR_RESET}" else - if [[ $COUNTIPS -gt 20 ]]; then + if [[ $count_banned_ips -gt 20 ]]; then # high number of bans - state="${aCOLOUR[ALERT]}$COUNTIPS IP(s) Banned${COLOUR_RESET}" + state="${aCOLOUR[ALERT]}$count_banned_ips IP(s) Banned${COLOUR_RESET}" else # low number of bans - state="${aCOLOUR[WEAK]}$COUNTIPS IP(s) Banned${COLOUR_RESET}" + state="${aCOLOUR[WEAK]}$count_banned_ips IP(s) Banned${COLOUR_RESET}" fi fi ;; 1) - state="${aCOLOUR[ALERT]}No Fail2Ban status available${COLOUR_RESET}" + state="${aCOLOUR[ALERT]}fail2ban-client not available${COLOUR_RESET}" ;; 2) state="${aCOLOUR[HIGHLIGHT]}NOPASSWD sudo required to obtain Fail2Ban status${COLOUR_RESET}" ;; *) - state="${aCOLOUR[ALERT]}Unknown state $COUNTIPS${COLOUR_RESET}" + state="${aCOLOUR[ALERT]}Unknown state, code: $code count: $count_banned_ips${COLOUR_RESET}" ;; esac @@ -484,8 +487,6 @@ $CLI_LINE" Get_Network_Usage() { - # IP address detection (IPv4) - local IP_re='([0-9]{1,3}\.){3}[0-9]{1,3}' # Common error substrings to detect timeouts / DNS failures (lowercase for case-insensitive check) local err_re='timed out|timeout|could not resolve|not known|unknown' @@ -514,38 +515,32 @@ $CLI_LINE" case "$ifname" in # wired / predictable names - eth*|en*|lan*) handle_iface=1 ;; + eth*|en*|lan*) : ;; # wireless - wlan*|wl*) handle_iface=1 ;; + wlan*|wl*) : ;; # vpn/tunnel/virtual - tun*|wg*|vpn*) handle_iface=1 ;; + tun*|wg*|vpn*) : ;; *) continue ;; esac - if (( handle_iface )); then - local RX="N/A" TX="N/A" rx tx ip_addr ip_addr_cidr + local RX="N/A" TX="N/A" rx tx ip_addr ip_addr_cidr - # Get RX data - if "${ns_cmd_prefix[@]}" test -r "/sys/class/net/$ifname/statistics/rx_bytes"; then - rx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/rx_bytes" 2>/dev/null || :) - [[ $rx =~ ^[0-9]+$ ]] && RX="$(awk -v v="$rx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" - fi + # Get RX data + rx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/rx_bytes" 2>/dev/null || :) + [[ $rx =~ ^[0-9]+$ ]] && RX="$(awk -v v="$rx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" - # Get TX data - if "${ns_cmd_prefix[@]}" test -r "/sys/class/net/$ifname/statistics/tx_bytes"; then - tx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/tx_bytes" 2>/dev/null || :) - [[ $tx =~ ^[0-9]+$ ]] && TX="$(awk -v v="$tx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" - fi + # Get TX data + tx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/tx_bytes" 2>/dev/null || :) + [[ $tx =~ ^[0-9]+$ ]] && TX="$(awk -v v="$tx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" - # Get IP address (IPv4, global scope) for this interface in this namespace. If multiple are present, just take the first. If none, show "- No IP -" - ip_addr=" - No IP - " - ip_addr_cidr=$("${ns_cmd_prefix[@]}" ip -o -4 addr show dev "$ifname" scope global 2>/dev/null | awk '{print $4}' | head -n1) - [[ -n "$ip_addr_cidr" ]] && ip_addr=${ip_addr_cidr%%/*} + # Get IP address (IPv4) for this interface in this namespace. If multiple are present, just take the first. If none, show "- No IP -" + ip_addr=" - No IP - " + ip_addr_cidr=$("${ns_cmd_prefix[@]}" ip -o -4 addr show dev "$ifname" scope global 2>/dev/null | awk '{print $4}' | head -n1 | grep -E "$IP4_re_cidr") + [[ -n "$ip_addr_cidr" ]] && ip_addr=${ip_addr_cidr%%/*} - if [[ "$RX" != "N/A" || "$TX" != "N/A" ]]; then - printf "%s %s%-20s %s%s TX= %12s RX= %12s%s\n" \ - "$CLI_BULLET" "${aCOLOUR[WEAK]}" "$ip_addr ($ifname)" "$CLI_SEPARATOR" "${aCOLOUR[WEAK]}" "$TX" "$RX" "$COLOUR_RESET" - fi + if [[ "$RX" != "N/A" || "$TX" != "N/A" ]]; then + printf "%s %s%-20s %s%s TX= %11s RX= %11s%s\n" \ + "$CLI_BULLET" "${aCOLOUR[WEAK]}" "$ip_addr ($ifname)" "$CLI_SEPARATOR" "${aCOLOUR[WEAK]}" "$TX" "$RX" "$COLOUR_RESET" fi done } @@ -556,11 +551,11 @@ $CLI_LINE" for ns in "${namespaces[@]}"; do raw_ns="${ns:-default}" - display_ns="$(G_TRUNCATE_MID "$raw_ns" 30)" + display_ns="$(G_TRUNCATE_MID "$raw_ns" 24)" IP=$(BANNER_GET_WAN_IP -t 1 -n "$ns" 2>&1) - if [[ $IP =~ $IP_re ]]; then + if [[ $IP =~ $IP4_re ]]; then # Valid IP detected, keep as-is and print with namespace # # Extract just the IP portion (use BASH_REMATCH[0]). Removes the geographic info. From 90f93f74f7cff9fad4fcee4d8a0f04cfe82e61bc Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 29 Jun 2026 00:57:35 -0500 Subject: [PATCH 12/19] on network usage, only do sudo if netns is requested --- dietpi/func/dietpi-banner | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 0fcdbb0bfe..a56cf29059 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -461,25 +461,26 @@ $CLI_LINE" { # Defaults local timeout=3 - local ns="" + local netns="" # Inputs while (( $# )) do case $1 in '-t') shift; { (( ${1/.} )) && timeout=$1; } || { G_DIETPI-NOTIFY 1 "Invalid timeout \"$1\", aborting..."; return 1; };; - '-n') shift; ns="${1:-}";; + '-n') shift; netns="${1:-}";; *) G_DIETPI-NOTIFY 1 "Invalid argument \"$1\", aborting..."; return 1;; esac shift done local -a ns_cmd_prefix=() - if [[ "$(id -u)" -ne "0" ]]; then - ns_cmd_prefix+=(sudo -n) - fi - - if [[ -n "$ns" ]]; then - ns_cmd_prefix+=(ip netns exec "$ns") + if [[ -n "$netns" ]]; then + if [[ "$(id -u)" -ne "0" ]]; then + # add sudo if not root + ns_cmd_prefix+=(sudo -n) + fi + # add netns prefix + ns_cmd_prefix+=(ip netns exec "$netns") fi "${ns_cmd_prefix[@]}" curl -sSfLm "$timeout" 'https://dietpi.com/geoip' 2>&1 @@ -496,11 +497,12 @@ $CLI_LINE" # Use an array prefix so we can run commands either in a netns # (ip netns exec ...) or the default namespace, and with sudo if needed local -a ns_cmd_prefix=() - if [[ "$(id -u)" -ne "0" ]]; then - ns_cmd_prefix+=(sudo -n) - fi - if [[ -n "$netns" ]]; then + if [[ "$(id -u)" -ne "0" ]]; then + # add sudo if not root + ns_cmd_prefix+=(sudo -n) + fi + # add netns prefix ns_cmd_prefix+=(ip netns exec "$netns") fi From 31a892859a6dc9c43b2e28af9f62e4269cc32a9b Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 29 Jun 2026 12:26:26 -0500 Subject: [PATCH 13/19] Add example G_TRUNCATE_MID --- CONTRIBUTING.md | 10 ++++++++++ dietpi/func/dietpi-globals | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 34da9a7fd9..29c28614bc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -199,6 +199,16 @@ Below are minimal, copy-paste-ready examples that follow DietPi conventions. log=1 G_WHIP_VIEWFILE "$FP_LOG" || return ``` +- `G_TRUNCATE_MID` (shorten long strings by squishing the middle characters): + ``` + G_TRUNCATE_MID "Long text to be shortened by removing the middle" 26 + # -> "Long text to... the middle" + G_TRUNCATE_MID "Long text" 5 + # -> "L...t" + G_TRUNCATE_MID "Text" 3 + # -> "Tex" + ``` + - `Save()` persistence pattern (follow dietpi-banner conventions): ``` Save(){ diff --git a/dietpi/func/dietpi-globals b/dietpi/func/dietpi-globals index 214bd82be0..6043a74b3e 100644 --- a/dietpi/func/dietpi-globals +++ b/dietpi/func/dietpi-globals @@ -174,6 +174,11 @@ $(ps f -eo pid,user,tty,cmd | grep -i 'dietpi')" && continue # Truncate a string in the middle to a maximum length, inserting "..." # - If the string is shorter than or equal to the maximum length, it is returned unchanged. # - If the maximum length is less than or equal to 3, the string is truncated to the maximum length without adding "...". + # - E.g. + # For example: + # `G_TRUNCATE_MID "Long text to be shortened by removing the middle" 26` returns "Long text to... the middle" + # `G_TRUNCATE_MID "Long text" 5` returns "L...t" + # `G_TRUNCATE_MID "Text" 3` returns "Tex" G_TRUNCATE_MID() { local s="$1"; local max="$2" local len=${#s} From 00f821e297eb7d38204af6e2d32f7bf982f45934 Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 29 Jun 2026 13:04:29 -0500 Subject: [PATCH 14/19] improved disk space handling with fallbacks for missing data --- dietpi/func/dietpi-banner | 47 +++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index a56cf29059..71e2a5d087 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -627,6 +627,7 @@ $CLI_LINE" fi else # Otherwise, treat it as a shell glob pattern and match using "case" + # shellcheck disable=SC2254 # disable shellcheck for $pattern in case statement case "$mnt_target" in $pattern) matched=1 @@ -649,28 +650,56 @@ $CLI_LINE" # Print results with aligned columns, converting kB to GB and calculating percentage used for entry in "${results[@]}"; do IFS='|' read -r size_kb used_kb avail_kb mnt_target name <<< "$entry" - # name=$(basename "$entry") + + local values_count=0 # Do all math in kB, convert to GB for display and round to 1 decimal if [[ "$size_kb" =~ ^[0-9]+$ ]]; then - size_gb=$(awk "BEGIN {printf \"%.1f \", $size_kb/1024/1024}") + values_count=$((values_count+1)) + size_gb=$(awk "BEGIN {printf \"%.1f \", $size_kb/1024/1024}") else - size_gb="N/A" + if [[ "$used_kb" =~ ^[0-9]+$ ]] && [[ "$avail_kb" =~ ^[0-9]+$ ]]; then + # If used and available are available, calculate size as used + available + size_kb=$((used_kb+avail_kb)) + size_gb=$(awk "BEGIN {printf \"%.1f \", ($size_kb)/1024/1024}") + else + size_gb="???" + fi fi + if [[ "$used_kb" =~ ^[0-9]+$ ]]; then - used_gb=$(awk "BEGIN {printf \"%.1f \", $used_kb/1024/1024}") + values_count=$((values_count+1)) + used_gb=$(awk "BEGIN {printf \"%.1f \", $used_kb/1024/1024}") else - used_gb="N/A" + if [[ "$size_kb" =~ ^[0-9]+$ ]] && [[ "$avail_kb" =~ ^[0-9]+$ ]]; then + # If size and available are available, calculate used as size - available + used_kb=$((size_kb-avail_kb)) + used_gb=$(awk "BEGIN {printf \"%.1f \", ($used_kb)/1024/1024}") + else + used_gb="???" + fi fi + if [[ "$avail_kb" =~ ^[0-9]+$ ]]; then + values_count=$((values_count+1)) avail_gb=$(awk "BEGIN {printf \"%.1f \", $avail_kb/1024/1024}") else - avail_gb="N/A" + if [[ "$size_kb" =~ ^[0-9]+$ ]] && [[ "$used_kb" =~ ^[0-9]+$ ]]; then + # If size and used are available, calculate available as size - used + avail_kb=$((size_kb-used_kb)) + avail_gb=$(awk "BEGIN {printf \"%.1f \", ($avail_kb)/1024/1024}") + else + avail_gb="???" + fi fi - if [[ "$size_kb" =~ ^[0-9]+$ && $size_kb -gt 0 ]]; then - perc=$(awk "BEGIN {printf \"%.1f%%\", ($used_kb/($used_kb+$avail_kb))*100}") + + # Calculate percentage used based on available data: + if [[ "$values_count" -ge 2 ]]; then + # this should match the `df` output + perc=$(awk "BEGIN {printf \"%.1f%%\", ($used_kb/($used_kb+$avail_kb))*100}") else - perc="N/A" + perc="?? %" # default unknown value fi + # Print aligned columns: {basename} : {used} of {size} ({percent used}) printf "%s %s%-${length}s %s%s %8sGiB of %8sGiB (%6b)%s\n" \ "$CLI_BULLET" "${aCOLOUR[ALT]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[WEAK]}" "$used_gb" "$size_gb" "$perc" "$COLOUR_RESET" From 5dd9a971891992c02d6b6c3cd78ceec13daafecb Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 29 Jun 2026 13:06:31 -0500 Subject: [PATCH 15/19] fix indents --- dietpi/func/dietpi-banner | 62 +++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 71e2a5d087..7a35fae559 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -203,7 +203,7 @@ do # Don't save the RESET colour slot [[ "$i" == "RESET" ]] && continue - + val="${aCOLOUR[$i]}" # Replace actual ESC bytes (0x1b) with literal \e esc=$(printf '%s' "$val" | sed $'s/\x1b/\\e/g') @@ -614,37 +614,37 @@ $CLI_LINE" length=0 results=() while read -r size_kb used_kb avail_kb mnt_target; do - matched=0 - basename= - for pattern in "${SPACE_BASE_PATTERNS[@]}"; do - # If the user supplies a pattern prefixed with 're:' treat it as regex - if [[ "$pattern" == re:* ]]; then - re="${pattern#re:}" - if [[ $mnt_target =~ $re ]]; then - matched=1 - basename=$(basename "$mnt_target") - break - fi - else - # Otherwise, treat it as a shell glob pattern and match using "case" - # shellcheck disable=SC2254 # disable shellcheck for $pattern in case statement - case "$mnt_target" in - $pattern) - matched=1 - basename=$(basename "$mnt_target") - break - ;; - *) ;; - esac - fi - done - if (( matched )); then - len=${#basename} - if (( len > length )); then - length=$len - fi - results+=("$size_kb|$used_kb|$avail_kb|$mnt_target|$basename") + matched=0 + basename= + for pattern in "${SPACE_BASE_PATTERNS[@]}"; do + # If the user supplies a pattern prefixed with 're:' treat it as regex + if [[ "$pattern" == re:* ]]; then + re="${pattern#re:}" + if [[ $mnt_target =~ $re ]]; then + matched=1 + basename=$(basename "$mnt_target") + break + fi + else + # Otherwise, treat it as a shell glob pattern and match using "case" + # shellcheck disable=SC2254 # disable shellcheck for $pattern in case statement + case "$mnt_target" in + $pattern) + matched=1 + basename=$(basename "$mnt_target") + break + ;; + *) ;; + esac fi + done + if (( matched )); then + len=${#basename} + if (( len > length )); then + length=$len + fi + results+=("$size_kb|$used_kb|$avail_kb|$mnt_target|$basename") + fi done < <(df -k --output=size,used,avail,target | tail -n +2) # Print results with aligned columns, converting kB to GB and calculating percentage used From 53305d019fa7abe4726bc6863664ea8f1d6ec090 Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 29 Jun 2026 15:58:25 -0500 Subject: [PATCH 16/19] refactored for associative indexing. includes a migration feature --- dietpi/func/dietpi-banner | 295 ++++++++++++++++++++++++-------------- 1 file changed, 189 insertions(+), 106 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 7a35fae559..2ebe89a982 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -73,43 +73,108 @@ # disable shellcheck for non-constant `source` (`.`) # shellcheck disable=SC1090 [[ -f $FP_SAVEFILE ]] && . "$FP_SAVEFILE" + + # Migrate legacy numeric-indexed aENABLED (from older FP_SAVEFILE) to associative form + # Only perform migration when numeric indices exist in the loaded aENABLED + if declare -p aENABLED >/dev/null 2>&1; then + local has_numeric=0 + for k in "${!aENABLED[@]}"; do + if [[ "$k" =~ ^[0-9]+$ ]]; then has_numeric=1; break; fi + done + if (( has_numeric )); then + # Prefer LEGACY_INDEXED_BANNER_KEYS for a stable mapping; fallback to MENU_ITEMS + local -n key_map_ref=LEGACY_INDEXED_BANNER_KEYS + if [[ ${#LEGACY_INDEXED_BANNER_KEYS[@]} -eq 0 && ${#MENU_ITEMS[@]} -gt 0 ]]; then + key_map_ref=MENU_ITEMS + fi + declare -gA _tmp_enabled=() + # First, map numeric indices explicitly so legacy numeric prefs take precedence + for (( i=0; i<${#key_map_ref[@]}; i++ )); do + if [[ -n "${aENABLED[$i]+x}" ]]; then + key=${key_map_ref[$i]} + [[ -n "$key" ]] && _tmp_enabled[$key]=${aENABLED[$i]:-0} + fi + done + # Then copy any remaining non-numeric keys that weren't set by numeric mapping + for idx in "${!aENABLED[@]}"; do + if [[ ! "$idx" =~ ^[0-9]+$ ]]; then + key=$idx + # don't overwrite keys already set by numeric mapping + [[ -n "${_tmp_enabled[$key]+x}" ]] && continue + [[ -n "$key" ]] && _tmp_enabled[$key]=${aENABLED[$idx]:-0} + fi + done + # replace aENABLED with associative mapping + unset aENABLED + declare -gA aENABLED + for k in "${!_tmp_enabled[@]}"; do aENABLED[$k]=${_tmp_enabled[$k]}; done + + # Mark that a migration from legacy numeric indices occurred during this run + declare -g DIETPI_BANNER_MIGRATED_DURING_RUN=1 + unset _tmp_enabled + fi + fi } - aDESCRIPTION=( - - 'Device model' - 'Uptime' - 'CPU temp' - 'FQDN/hostname' - 'NIS domainname' - 'LAN IP' - 'WAN IP' - 'Disk usage (RootFS)' - 'Disk usage (userdata)' - 'Weather (wttr.in)' - 'Custom commands' - 'Display DietPi useful commands?' - 'MOTD' - 'VPN status' - 'Large hostname' - 'Print credits' - 'Let'\''s Encrypt cert status' - 'RAM usage' - 'Load average' - 'Word-wrap lines on small screens' - 'Kernel' - 'Network Usage' - 'Disk Usage' - 'Systemd Status' - 'Fail2Ban Status' + # Use associative arrays keyed by short names derived from the description + declare -gA aDESCRIPTION=( + [device_model]='Device model' + [uptime]='Uptime' + [cpu_temp]='CPU temp' + [hostname]='FQDN/hostname' + [nis_domainname]='NIS domainname' + [lan_ip]='LAN IP' + [wan_ip]='WAN IP' + [disk_rootfs]='Disk usage (RootFS)' + [disk_userdata]='Disk usage (userdata)' + [weather]='Weather (wttr.in)' + [custom_commands]='Custom commands' + [useful_commands]='Display DietPi useful commands?' + [motd]='MOTD' + [vpn_status]='VPN status' + [large_hostname]='Large hostname' + [print_credits]='Print credits' + [letsencrypt]='Let'\''s Encrypt cert status' + [ram_usage]='RAM usage' + [load_average]='Load average' + [word_wrap]='Word-wrap lines on small screens' + [kernel]='Kernel' + [network_usage]='Network Usage' + [disk_usage]='Disk Usage' + [systemd_status]='Systemd Status' + [fail2ban_status]='Fail2Ban Status' + ) + # Set the array order for the menu, items must be listed here to show up + declare -ga MENU_ITEMS=( + large_hostname device_model uptime kernel cpu_temp ram_usage load_average hostname nis_domainname \ + lan_ip wan_ip vpn_status disk_rootfs disk_userdata weather letsencrypt systemd_status fail2ban_status \ + network_usage disk_usage custom_commands useful_commands motd print_credits word_wrap kernel + ) + + # Keep a fixed index->key mapping to support numeric-index migration even if MENU_ITEMS changes later + # LEGACY_INDEXED_BANNER_KEYS maps legacy numeric indices to the current key names (0 -> device_model, 1 -> uptime, ...) + ############## + ## DO NOT CHANGE. THIS IS REQUIRED TO MAINTAIN LEGACY INDEX->KEY MAPPING FOR MIGRATION + ############## + declare -ga LEGACY_INDEXED_BANNER_KEYS=( + device_model uptime cpu_temp hostname nis_domainname lan_ip wan_ip disk_rootfs disk_userdata weather custom_commands useful_commands motd vpn_status large_hostname print_credits letsencrypt ram_usage load_average word_wrap kernel network_usage disk_usage systemd_status fail2ban_status ) + ############## + ## DO NOT CHANGE. THIS IS REQUIRED TO MAINTAIN LEGACY INDEX->KEY MAPPING FOR MIGRATION + ############## + # Set defaults: Disable CPU temp by default in VMs + declare -gA aENABLED if (( $G_HW_MODEL == 20 )) then - aENABLED=(1 0 0 0 0 1 0 1 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0 0 0) + aENABLED=( + [device_model]=1 [uptime]=0 [cpu_temp]=0 [hostname]=0 [nis_domainname]=0 [lan_ip]=1 [wan_ip]=0 [disk_rootfs]=1 [disk_userdata]=0 [weather]=0 [custom_commands]=0 [useful_commands]=1 [motd]=1 [vpn_status]=0 [large_hostname]=0 [print_credits]=1 [letsencrypt]=0 [ram_usage]=0 [load_average]=0 [word_wrap]=0 [kernel]=0 [network_usage]=0 [disk_usage]=0 [systemd_status]=0 [fail2ban_status]=0 + ) else - aENABLED=(1 0 1 0 0 1 0 0 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0 0 0) + aENABLED=( + [device_model]=1 [uptime]=0 [cpu_temp]=1 [hostname]=0 [nis_domainname]=0 [lan_ip]=1 [wan_ip]=0 [disk_rootfs]=0 [disk_userdata]=0 [weather]=0 [custom_commands]=0 [useful_commands]=1 [motd]=1 [vpn_status]=0 [large_hostname]=0 [print_credits]=1 [letsencrypt]=0 [ram_usage]=0 [load_average]=0 [word_wrap]=0 [kernel]=0 [network_usage]=0 [disk_usage]=0 [systemd_status]=0 [fail2ban_status]=0 + ) fi # Folding type (colon, dash, fixed), and fixed indent @@ -117,11 +182,13 @@ BW_INDENT_FIXED=3 # Default custom command example - CUSTOM_COMMANDS[0]='echo -e "$CLI_BULLET ${aCOLOUR[ALERT]}Hello ${aCOLOUR[GOOD]}World! ${COLOUR_RESET} :)"' + aCUSTOM_COMMANDS[0]='echo -e "$CLI_BULLET ${aCOLOUR[ALERT]}Hello ${aCOLOUR[GOOD]}World! ${COLOUR_RESET} :)"' # Default disk patterns - SPACE_BASE_PATTERNS=("/" "/mnt/dietpi_userdata") + aDISK_SPACE_PATTERNS=("/" "/mnt/dietpi_userdata") # Load user preferences, overriding the above defaults as needed + # Flag indicating whether a legacy numeric->associative migration happened during this run + declare -g DIETPI_BANNER_MIGRATED_DURING_RUN=0 DIETPI_BANNER_LOAD_PREFS # Derived convenience strings @@ -191,9 +258,10 @@ Save() { - for i in "${!aDESCRIPTION[@]}" - do - echo "aENABLED[$i]=${aENABLED[$i]}" + # Persist associative aENABLED as simple assignments (one per line) + for key in "${MENU_ITEMS[@]}"; do + val=${aENABLED[$key]:-0} + printf "aENABLED[%s]=%s\n" "$key" "$val" done # Persist only the primary colour slots. Convert any actual @@ -212,22 +280,27 @@ echo "aCOLOUR[$i]='$esc'" done - # Persist SPACE_BASE_PATTERNS (indices 0..N) - for i in "${!SPACE_BASE_PATTERNS[@]}"; do - patt=${SPACE_BASE_PATTERNS[$i]} + # Persist aDISK_SPACE_PATTERNS (indices 0..N) + for i in "${!aDISK_SPACE_PATTERNS[@]}"; do + patt=${aDISK_SPACE_PATTERNS[$i]} patt=${patt//\'/\\\'} - printf "SPACE_BASE_PATTERNS[%s]='%s'\n" "$i" "$patt" + printf "aDISK_SPACE_PATTERNS[%s]='%s'\n" "$i" "$patt" done - # Persist CUSTOM_COMMANDS (indices 0..N) - for i in "${!CUSTOM_COMMANDS[@]}"; do - cmd=${CUSTOM_COMMANDS[$i]} + # Persist aCUSTOM_COMMANDS (indices 0..N) + for i in "${!aCUSTOM_COMMANDS[@]}"; do + cmd=${aCUSTOM_COMMANDS[$i]} cmd=${cmd//\'/\\\'} - printf "CUSTOM_COMMANDS[%s]='%s'\n" "$i" "$cmd" + printf "aCUSTOM_COMMANDS[%s]='%s'\n" "$i" "$cmd" done + # Persist BW indent settings echo "BW_INDENT_TYPE='$BW_INDENT_TYPE'" echo "BW_INDENT_FIXED=$BW_INDENT_FIXED" + + # Clear migration flag now that prefs are persisted + DIETPI_BANNER_MIGRATED_DURING_RUN=0 + return 0 } Print_Header() @@ -270,7 +343,7 @@ $CLI_LINE" Get_Local_Ip() { - (( ${aENABLED[5]} )) || return 0 + (( ${aENABLED[lan_ip]} )) || return 0 local iface=$(G_GET_NET -q iface) local ip=$(G_GET_NET -q ip) echo "${ip:-Use dietpi-config to setup a connection} (${iface:-NONE})" @@ -588,8 +661,8 @@ $CLI_LINE" Print_Custom_Commands() { - for i in "${!CUSTOM_COMMANDS[@]}"; do - local cmd="${CUSTOM_COMMANDS[$i]}" + for i in "${!aCUSTOM_COMMANDS[@]}"; do + local cmd="${aCUSTOM_COMMANDS[$i]}" if [[ -n "$cmd" ]]; then output="$(eval "$cmd" 2>&1)" status=$? @@ -606,7 +679,7 @@ $CLI_LINE" { # If no arguments are provided, default to '"/" "/mnt/*"'. if (( $# > 0 )); then - SPACE_BASE_PATTERNS=("$@") + aDISK_SPACE_PATTERNS=("$@") fi # TODO: is there a more efficient reader than df (stat?) @@ -616,7 +689,7 @@ $CLI_LINE" while read -r size_kb used_kb avail_kb mnt_target; do matched=0 basename= - for pattern in "${SPACE_BASE_PATTERNS[@]}"; do + for pattern in "${aDISK_SPACE_PATTERNS[@]}"; do # If the user supplies a pattern prefixed with 're:' treat it as regex if [[ "$pattern" == re:* ]]; then re="${pattern#re:}" @@ -724,68 +797,71 @@ $CLI_LINE" # Large Format Hostname # shellcheck disable=SC1091 - (( ${aENABLED[14]} )) && . /boot/dietpi/func/dietpi-print_large "$(&1)" + (( ${aENABLED[uptime]} )) && Print_Item_State "${aDESCRIPTION[uptime]}" "$(uptime -p 2>&1)" # Linux kernel version - (( ${aENABLED[20]} )) && Print_Item_State "${aDESCRIPTION[20]}" "$(uname -r 2>&1)" + (( ${aENABLED[kernel]} )) && Print_Item_State "${aDESCRIPTION[kernel]}" "$(uname -r 2>&1)" # CPU temp - (( ${aENABLED[2]} )) && Print_Item_State "${aDESCRIPTION[2]}" "$(print_full_info=1 G_OBTAIN_CPU_TEMP 2>&1)" + (( ${aENABLED[cpu_temp]} )) && Print_Item_State "${aDESCRIPTION[cpu_temp]}" "$(print_full_info=1 G_OBTAIN_CPU_TEMP 2>&1)" # RAM usage - (( ${aENABLED[17]} )) && Print_Item_State "${aDESCRIPTION[17]}" "$(free -b | mawk 'NR==2 {CONVFMT="%.0f"; print $3/1024^2" of "$2/1024^2" MiB ("$3/$2*100"%)"}')" + (( ${aENABLED[ram_usage]} )) && Print_Item_State "${aDESCRIPTION[ram_usage]}" "$(free -b | mawk 'NR==2 {CONVFMT="%.0f"; print $3/1024^2" of "$2/1024^2" MiB ("$3/$2*100"%)"}')" # Load average - (( ${aENABLED[18]} )) && Print_Item_State "${aDESCRIPTION[18]}" "$(mawk '{print $1 ", " $2 ", " $3}' /proc/loadavg) ($(nproc) cores)" + (( ${aENABLED[load_average]} )) && Print_Item_State "${aDESCRIPTION[load_average]}" "$(mawk '{print $1 ", " $2 ", " $3}' /proc/loadavg) ($(nproc) cores)" # Hostname - (( ${aENABLED[3]} && ! ${aENABLED[14]} )) && Print_Item_State "${aDESCRIPTION[3]}" "$(&1)" + (( ${aENABLED[nis_domainname]} )) && Print_Item_State "${aDESCRIPTION[nis_domainname]}" "$(hostname -y 2>&1)" # LAN IP - (( ${aENABLED[5]} )) && Print_Item_State "${aDESCRIPTION[5]}" "$(Get_Local_Ip 2>&1)" + (( ${aENABLED[lan_ip]} )) && Print_Item_State "${aDESCRIPTION[lan_ip]}" "$(Get_Local_Ip 2>&1)" # WAN IP + location info - (( ${aENABLED[6]} )) && Print_Item_State "${aDESCRIPTION[6]}" "$(BANNER_GET_WAN_IP 2>&1)" + (( ${aENABLED[wan_ip]} )) && Print_Item_State "${aDESCRIPTION[wan_ip]}" "$(BANNER_GET_WAN_IP 2>&1)" # DietPi-VPN connection status - (( ${aENABLED[13]} )) && Print_Item_State "${aDESCRIPTION[13]}" "$(Get_VPN_Status 2>&1)" + (( ${aENABLED[vpn_status]} )) && Print_Item_State "${aDESCRIPTION[vpn_status]}" "$(Get_VPN_Status 2>&1)" # Disk usage (RootFS) - (( ${aENABLED[7]} )) && echo -e "$CLI_BULLET ${aCOLOUR[STRONG]}${aDESCRIPTION[7]} $CLI_SEPARATOR $(df -h --output=used,size,pcent / | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" + (( ${aENABLED[disk_rootfs]} )) && echo -e "$CLI_BULLET ${aCOLOUR[STRONG]}${aDESCRIPTION[disk_rootfs]} $CLI_SEPARATOR $(df -h --output=used,size,pcent / | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" # Disk usage (DietPi userdata) - (( ${aENABLED[8]} )) && echo -e "$CLI_BULLET ${aCOLOUR[STRONG]}${aDESCRIPTION[8]} $CLI_SEPARATOR $(df -h --output=used,size,pcent /mnt/dietpi_userdata | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" + (( ${aENABLED[disk_userdata]} )) && echo -e "$CLI_BULLET ${aCOLOUR[STRONG]}${aDESCRIPTION[disk_userdata]} $CLI_SEPARATOR $(df -h --output=used,size,pcent /mnt/dietpi_userdata | mawk 'NR==2 {print $1" of "$2" ("$3")"}' 2>&1)" # Weather - (( ${aENABLED[9]} )) && Print_Item_State "${aDESCRIPTION[9]}" "$(curl -sSfLm 3 'https://wttr.in/?format=4' 2>&1)" + (( ${aENABLED[weather]} )) && Print_Item_State "${aDESCRIPTION[weather]}" "$(curl -sSfLm 3 'https://wttr.in/?format=4' 2>&1)" # Let's Encrypt cert status - (( ${aENABLED[16]} )) && Print_Item_State "Let's Encrypt Cert" "$(Get_Cert_Status 2>&1)" + (( ${aENABLED[letsencrypt]} )) && Print_Item_State "Let's Encrypt Cert" "$(Get_Cert_Status 2>&1)" # Systemd status - (( ${aENABLED[23]} )) && Print_Item_State "${aDESCRIPTION[23]}" "$(Get_Systemd_Status 2>&1)" + (( ${aENABLED[systemd_status]} )) && Print_Item_State "${aDESCRIPTION[systemd_status]}" "$(Get_Systemd_Status 2>&1)" # Fail2Ban status - (( ${aENABLED[24]} )) && Print_Item_State "${aDESCRIPTION[24]}" "$(Get_Fail2Ban_Status 2>&1)" + (( ${aENABLED[fail2ban_status]} )) && Print_Item_State "${aDESCRIPTION[fail2ban_status]}" "$(Get_Fail2Ban_Status 2>&1)" # Network usage by namespace # %b required so that percent symbols are not interpreted as format specifiers by printf - (( ${aENABLED[21]} )) && Print_Subheader "Network Traffic" && printf '%b\n' "$(Get_Network_Usage 2>&1)" + (( ${aENABLED[network_usage]} )) && Print_Subheader "Network Traffic" && printf '%b\n' "$(Get_Network_Usage 2>&1)" # Disk Usage - (( ${aENABLED[22]} )) && Print_Subheader "Disk Usage" && printf '%b\n' "$(Get_Disk_Usage "${SPACE_BASE_PATTERNS[@]}" 2>&1)" + (( ${aENABLED[disk_usage]} )) && Print_Subheader "Disk Usage" && printf '%b\n' "$(Get_Disk_Usage "${aDISK_SPACE_PATTERNS[@]}" 2>&1)" # Custom commands - (( ${aENABLED[10]} )) && Print_Subheader "Custom Commands" && Print_Custom_Commands + (( ${aENABLED[custom_commands]} )) && Print_Subheader "Custom Commands" && Print_Custom_Commands # Separator line - (( ${aENABLED[11]} || ${aENABLED[12]} || ${aENABLED[15]} )) && echo -e "$CLI_LINE" + (( ${aENABLED[useful_commands]} || ${aENABLED[motd]} || ${aENABLED[print_credits]} )) && echo -e "$CLI_LINE" # MOTD - if (( ${aENABLED[12]} )) + if (( ${aENABLED[motd]} )) then local motd fp_motd='/run/dietpi/.dietpi_motd' [[ -f $fp_motd ]] || curl -sSfLm 3 'https://dietpi.com/motd' -o "$fp_motd" # disable shellcheck for non-constant `source` (`.`) # shellcheck disable=SC1090 - [[ -f $fp_motd ]] && . "$fp_motd" &> /dev/null && [[ $motd ]] && Print_Item_State "${aDESCRIPTION[12]}" "$motd" + [[ -f $fp_motd ]] && . "$fp_motd" &> /dev/null && [[ $motd ]] && Print_Item_State "${aDESCRIPTION[motd]}" "$motd" fi echo -e "$CLI_LINE\n" - (( ${aENABLED[15]} )) && Print_Credits + (( ${aENABLED[print_credits]} )) && Print_Credits Print_Updates - (( ${aENABLED[11]} )) && Print_Useful_Commands + (( ${aENABLED[useful_commands]} )) && Print_Useful_Commands + + # Notify user if prefs were migrated from legacy numeric indices during this run + (( DIETPI_BANNER_MIGRATED_DURING_RUN )) && echo -e " ${aCOLOUR[HIGHLIGHT]}Banner needs to be migrated, run dietpi-banner and validate your settings.$COLOUR_RESET" } Print_Banner() @@ -793,7 +869,7 @@ $CLI_LINE" G_TERM_CLEAR # Pipe to awk script for word-wrap if enabled - if (( ${aENABLED[19]} )) + if (( ${aENABLED[word_wrap]} )) then Print_Banner_raw | mawk -v "MAXCOL=$(tput cols)" -v "INDENT_TYPE=$BW_INDENT_TYPE" -v "INDENT_FIXED=$BW_INDENT_FIXED" -f "$FP_BANNERWRAP_AWK" else @@ -804,25 +880,25 @@ $CLI_LINE" Menu_Main() { G_WHIP_CHECKLIST_ARRAY=() - for i in "${!aDESCRIPTION[@]}" + for key in "${MENU_ITEMS[@]}" do local state='off' - (( ${aENABLED[$i]:=0} == 1 )) && state='on' - G_WHIP_CHECKLIST_ARRAY+=("$i" "${aDESCRIPTION[$i]}" "$state") + (( ${aENABLED[$key]:=0} == 1 )) && state='on' + G_WHIP_CHECKLIST_ARRAY+=("$key" "${aDESCRIPTION[$key]}" "$state") done G_WHIP_CHECKLIST "Please (de)select options via spacebar to be shown in the $G_PROGRAM_NAME:" || return 0 - for i in "${!aDESCRIPTION[@]}" + for key in "${MENU_ITEMS[@]}" do - aENABLED[$i]=0 + aENABLED[$key]=0 done - for i in $G_WHIP_RETURNED_VALUE + for selection in $G_WHIP_RETURNED_VALUE do - aENABLED[$i]=1 + aENABLED[$selection]=1 - if (( $i == 10 )) + if [[ "$selection" == "custom_commands" ]] then # ignore check for single/double quote expansion issue; unassigned var is referenced; # shellcheck disable=SC2016,SC2154 @@ -836,27 +912,27 @@ $CLI_LINE" case "$G_WHIP_RETURNED_VALUE" in Add) G_WHIP_INPUTBOX "Enter new command, for example :\n$example_command" || continue - CUSTOM_COMMANDS+=("$G_WHIP_RETURNED_VALUE") + aCUSTOM_COMMANDS+=("$G_WHIP_RETURNED_VALUE") ;; Edit) - if (( ${#CUSTOM_COMMANDS[@]} == 0 )); then G_WHIP_MSG 'No commands to edit'; continue; fi + if (( ${#aCUSTOM_COMMANDS[@]} == 0 )); then G_WHIP_MSG 'No commands to edit'; continue; fi G_WHIP_MENU_ARRAY=() - for idx in "${!CUSTOM_COMMANDS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${CUSTOM_COMMANDS[$idx]}"); done + for idx in "${!aCUSTOM_COMMANDS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${aCUSTOM_COMMANDS[$idx]}"); done G_WHIP_MENU 'Select command to edit:' || continue sel=$G_WHIP_RETURNED_VALUE - G_WHIP_DEFAULT_ITEM=${CUSTOM_COMMANDS[$sel]} + G_WHIP_DEFAULT_ITEM=${aCUSTOM_COMMANDS[$sel]} G_WHIP_INPUTBOX "Edit command, for example :\n$example_command" || continue - CUSTOM_COMMANDS[$sel]="$G_WHIP_RETURNED_VALUE" + aCUSTOM_COMMANDS[$sel]="$G_WHIP_RETURNED_VALUE" ;; Remove) - if (( ${#CUSTOM_COMMANDS[@]} == 0 )); then G_WHIP_MSG 'No commands to remove'; continue; fi + if (( ${#aCUSTOM_COMMANDS[@]} == 0 )); then G_WHIP_MSG 'No commands to remove'; continue; fi G_WHIP_MENU_ARRAY=() - for idx in "${!CUSTOM_COMMANDS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${CUSTOM_COMMANDS[$idx]}"); done + for idx in "${!aCUSTOM_COMMANDS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${aCUSTOM_COMMANDS[$idx]}"); done G_WHIP_MENU 'Select command to remove:' || continue sel=$G_WHIP_RETURNED_VALUE - unset 'CUSTOM_COMMANDS[$sel]' + unset 'aCUSTOM_COMMANDS[$sel]' # reindex array - CUSTOM_COMMANDS=("${CUSTOM_COMMANDS[@]}") + aCUSTOM_COMMANDS=("${aCUSTOM_COMMANDS[@]}") ;; Done) break @@ -866,9 +942,9 @@ $CLI_LINE" done # If no commands are configured, disable the custom commands option - (( ${#CUSTOM_COMMANDS[@]} == 0 )) && aENABLED[10]=0 + (( ${#aCUSTOM_COMMANDS[@]} == 0 )) && aENABLED[custom_commands]=0 - elif (( $i == 19 )) + elif [[ "$selection" == "word_wrap" ]] then # Banner word-wrap options G_WHIP_MENU_ARRAY=( @@ -884,7 +960,7 @@ $CLI_LINE" G_WHIP_INPUTBOX_REGEX='^[1-9][0-9]*$' G_WHIP_INPUTBOX_REGEX_TEXT='be a number' G_WHIP_INPUTBOX 'Please set the fixed offset column to indent word-wrapped lines to:' && BW_INDENT_FIXED=$G_WHIP_RETURNED_VALUE - elif (( $i == 22 )) + elif [[ "$selection" == "disk_usage" ]] then # ignore check for single/double quote expansion issue; single/double quotes for glob; # shellcheck disable=SC2016,SC2086 @@ -892,7 +968,7 @@ $CLI_LINE" To use regex, start the pattern with 're:', otherwise shell globbing is used. For example: '/mnt/*' or 're:^/mnt/.*/*$'" - # Manage SPACE_BASE_PATTERNS list + # Manage aDISK_SPACE_PATTERNS list while true; do G_WHIP_MENU_ARRAY=( 'Add' 'Add a new pattern' 'Edit' 'Edit an existing pattern' 'Remove' 'Remove a pattern' 'Done' 'Finish pattern configuration' ) G_WHIP_DEFAULT_ITEM='Done' @@ -900,27 +976,27 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" case "$G_WHIP_RETURNED_VALUE" in Add) G_WHIP_INPUTBOX "Enter new pattern :\n$pattern_hint" || continue - SPACE_BASE_PATTERNS+=("$G_WHIP_RETURNED_VALUE") + aDISK_SPACE_PATTERNS+=("$G_WHIP_RETURNED_VALUE") ;; Edit) - if (( ${#SPACE_BASE_PATTERNS[@]} == 0 )); then G_WHIP_MSG 'No patterns to edit'; continue; fi + if (( ${#aDISK_SPACE_PATTERNS[@]} == 0 )); then G_WHIP_MSG 'No patterns to edit'; continue; fi G_WHIP_MENU_ARRAY=() - for idx in "${!SPACE_BASE_PATTERNS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${SPACE_BASE_PATTERNS[$idx]}"); done + for idx in "${!aDISK_SPACE_PATTERNS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${aDISK_SPACE_PATTERNS[$idx]}"); done G_WHIP_MENU 'Select pattern to edit :' || continue sel=$G_WHIP_RETURNED_VALUE - G_WHIP_DEFAULT_ITEM=${SPACE_BASE_PATTERNS[$sel]} + G_WHIP_DEFAULT_ITEM=${aDISK_SPACE_PATTERNS[$sel]} G_WHIP_INPUTBOX "Edit pattern :\n$pattern_hint" || continue - SPACE_BASE_PATTERNS[$sel]="$G_WHIP_RETURNED_VALUE" + aDISK_SPACE_PATTERNS[$sel]="$G_WHIP_RETURNED_VALUE" ;; Remove) - if (( ${#SPACE_BASE_PATTERNS[@]} == 0 )); then G_WHIP_MSG 'No patterns to remove'; continue; fi + if (( ${#aDISK_SPACE_PATTERNS[@]} == 0 )); then G_WHIP_MSG 'No patterns to remove'; continue; fi G_WHIP_MENU_ARRAY=() - for idx in "${!SPACE_BASE_PATTERNS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${SPACE_BASE_PATTERNS[$idx]}"); done + for idx in "${!aDISK_SPACE_PATTERNS[@]}"; do G_WHIP_MENU_ARRAY+=("$idx" "${aDISK_SPACE_PATTERNS[$idx]}"); done G_WHIP_MENU 'Select pattern to remove :' || continue sel=$G_WHIP_RETURNED_VALUE - unset 'SPACE_BASE_PATTERNS[$sel]' + unset 'aDISK_SPACE_PATTERNS[$sel]' # reindex array - SPACE_BASE_PATTERNS=("${SPACE_BASE_PATTERNS[@]}") + aDISK_SPACE_PATTERNS=("${aDISK_SPACE_PATTERNS[@]}") ;; Done) break @@ -930,10 +1006,17 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" done # If no patterns are configured, disable the disk usage option - (( ${#SPACE_BASE_PATTERNS[@]} == 0 )) && aENABLED[22]=0 + (( ${#aDISK_SPACE_PATTERNS[@]} == 0 )) && aENABLED[disk_usage]=0 fi done + # Before saving, create a backup of the existing prefs if migrating. + # FP_SAVEFILE gets truncated when calling Save() below, so we need to backup first. + if [[ -f "$FP_SAVEFILE" ]] && [[ $DIETPI_BANNER_MIGRATED_DURING_RUN == 1 ]]; then + backup="$FP_SAVEFILE.bak-$(date -Iseconds)" + cp -a "$FP_SAVEFILE" "$backup" || true + fi + Save > "$FP_SAVEFILE" } @@ -941,7 +1024,7 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" # Main Loop #///////////////////////////////////////////////////////////////////////////////////// case $INPUT in - 0) Print_Header; Print_Item_State "${aDESCRIPTION[5]}" "$(Get_Local_Ip 2>&1)";; + 0) Print_Header; Print_Item_State "${aDESCRIPTION[lan_ip]}" "$(Get_Local_Ip 2>&1)";; 1) Print_Banner;; '') Menu_Main; Print_Banner;; *) G_DIETPI-NOTIFY 1 "Invalid input \"$*\"\n\nUsage:$USAGE"; exit 1;; From c43d44a252f71845f877c1046dcd6f463640716f Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 29 Jun 2026 16:46:07 -0500 Subject: [PATCH 17/19] fixed shellchecks --- dietpi/func/dietpi-banner | 8 ++++---- dietpi/func/dietpi-globals | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 2ebe89a982..c0856eeea7 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -47,7 +47,7 @@ # ---------------------------------------------------------------------------- # Custom preferences initialisation helper. It loads the user-saved settings file # ($FP_SAVEFILE) so custom colours, commands, and other preferences are applied before they are used. - # Color and format references: + # Color and format references: # `https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit` # `https://invisible-island.net/xterm/ctlseqs/ctlseqs.html` under section "Character Attributes (SGR)" # `https://www.ditig.com/256-colors-cheat-sheet` @@ -163,7 +163,6 @@ ## DO NOT CHANGE. THIS IS REQUIRED TO MAINTAIN LEGACY INDEX->KEY MAPPING FOR MIGRATION ############## - # Set defaults: Disable CPU temp by default in VMs declare -gA aENABLED if (( $G_HW_MODEL == 20 )) @@ -182,6 +181,7 @@ BW_INDENT_FIXED=3 # Default custom command example + # shellcheck disable=SC2016 # Disable warning about single quotes in echo -e aCUSTOM_COMMANDS[0]='echo -e "$CLI_BULLET ${aCOLOUR[ALERT]}Hello ${aCOLOUR[GOOD]}World! ${COLOUR_RESET} :)"' # Default disk patterns aDISK_SPACE_PATTERNS=("/" "/mnt/dietpi_userdata") @@ -474,7 +474,7 @@ $CLI_LINE" f2b_output="$(fail2ban-client banned 2>/dev/null)" else f2b_output="$(sudo -n fail2ban-client banned 2>/dev/null || return 2)" - [ $? = 2 ] && return 2 + [[ $? = 2 ]] && return 2 fi if ! command -v sort >/dev/null 2>&1; then @@ -859,7 +859,7 @@ $CLI_LINE" (( ${aENABLED[print_credits]} )) && Print_Credits Print_Updates (( ${aENABLED[useful_commands]} )) && Print_Useful_Commands - + # Notify user if prefs were migrated from legacy numeric indices during this run (( DIETPI_BANNER_MIGRATED_DURING_RUN )) && echo -e " ${aCOLOUR[HIGHLIGHT]}Banner needs to be migrated, run dietpi-banner and validate your settings.$COLOUR_RESET" } diff --git a/dietpi/func/dietpi-globals b/dietpi/func/dietpi-globals index 6043a74b3e..8bc94d39fd 100644 --- a/dietpi/func/dietpi-globals +++ b/dietpi/func/dietpi-globals @@ -174,7 +174,7 @@ $(ps f -eo pid,user,tty,cmd | grep -i 'dietpi')" && continue # Truncate a string in the middle to a maximum length, inserting "..." # - If the string is shorter than or equal to the maximum length, it is returned unchanged. # - If the maximum length is less than or equal to 3, the string is truncated to the maximum length without adding "...". - # - E.g. + # - E.g. # For example: # `G_TRUNCATE_MID "Long text to be shortened by removing the middle" 26` returns "Long text to... the middle" # `G_TRUNCATE_MID "Long text" 5` returns "L...t" @@ -1520,9 +1520,9 @@ Press any key to continue...' done if [[ -n "$ns" ]]; then - echo $(ip netns exec "$ns" curl -sSfLm "$timeout" 'https://dietpi.com/geoip' 2>&1) + ip netns exec "$ns" curl -sSfLm "$timeout" 'https://dietpi.com/geoip' 2>&1 else - echo $(curl -sSfLm "$timeout" 'https://dietpi.com/geoip' 2>&1) + curl -sSfLm "$timeout" 'https://dietpi.com/geoip' 2>&1 fi } From 2e6ab72f47a8f96774f36ab21a12f67f3a00b74e Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 29 Jun 2026 17:48:18 -0500 Subject: [PATCH 18/19] to accomodate associative indexing, added enumeration option to G_WHIP_CHECKLIST --- dietpi/func/dietpi-banner | 2 ++ dietpi/func/dietpi-globals | 40 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index c0856eeea7..0a96b369c2 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -887,7 +887,9 @@ $CLI_LINE" G_WHIP_CHECKLIST_ARRAY+=("$key" "${aDESCRIPTION[$key]}" "$state") done + G_WHIP_CHECKLIST_ENUM=1 # set the menu to use integer indices for the display G_WHIP_CHECKLIST "Please (de)select options via spacebar to be shown in the $G_PROGRAM_NAME:" || return 0 + G_WHIP_CHECKLIST_ENUM=0 for key in "${MENU_ITEMS[@]}" do diff --git a/dietpi/func/dietpi-globals b/dietpi/func/dietpi-globals index 8bc94d39fd..f3a184092d 100644 --- a/dietpi/func/dietpi-globals +++ b/dietpi/func/dietpi-globals @@ -486,6 +486,7 @@ $grey───────────────────────── # - G_WHIP_NOCANCEL=1 | Optional, hide the cancel button on inputbox, menu and checkbox dialogues # - G_WHIP_MENU_ARRAY | Required for G_WHIP_MENU to set available menu entries, 2 array indices per line: ('item' 'description') # - G_WHIP_CHECKLIST_ARRAY | Required for G_WHIP_CHECKLIST set available checklist options, 3 array indices per line: ('item' 'description' 'on'/'off') + # - G_WHIP_CHECKLIST_ENUM=1 | Optional, if set, the checklist will be displayed enumerated with numeric tags rather than the provided tags # Output: # - G_WHIP_RETURNED_VALUE | Returned value from inputbox/menu/checklist based whiptail items @@ -831,6 +832,7 @@ $grey───────────────────────── # G_WHIP_CHECKLIST "message" # - Prompt user to select multiple options from G_WHIP_CHECKLIST_ARRAY and save choice to G_WHIP_RETURNED_VALUE # - Exit code: 0=selection done, else=user cancelled or noninteractive + # - G_WHIP_CHECKLIST_ENUM=1 | Optional, if set, the checklist will be displayed enumerated with numeric tags rather than the provided tags G_WHIP_CHECKLIST() { local result=1 @@ -841,10 +843,48 @@ $grey───────────────────────── local WHIP_ERROR WHIP_BACKTITLE WHIP_SCROLLTEXT WHIP_SIZE_X WHIP_SIZE_Y WHIP_SIZE_Z WHIP_MESSAGE=$* NOCANCEL=() [[ $G_WHIP_NOCANCEL == 1 ]] && NOCANCEL=('--nocancel') G_WHIP_BUTTON_OK_TEXT=${G_WHIP_BUTTON_OK_TEXT:-Confirm} + + # Places to save original checklist and default + local orig_checklist=() original_default_item="$G_WHIP_DEFAULT_ITEM" + + #### + # If G_WHIP_CHECKLIST_ENUM=1, the checklist will be enumerated with numeric tags for whiptail, + # while preserving the original tags for mapping backwards after the checklist selection. + if [[ ${G_WHIP_CHECKLIST_ENUM:-0} == 1 && ${#G_WHIP_CHECKLIST_ARRAY[@]} -gt 0 ]] + then + # save the original checklist + orig_checklist=("${G_WHIP_CHECKLIST_ARRAY[@]}") + + # Build numeric-tagged checklist for whiptail while preserving original tags + local -a __tmp=() __idx=0 __map=() + for ((i=0;i<${#orig_checklist[@]};i+=3)); do + __map[$__idx]="${orig_checklist[$i]}" + __tmp+=("$__idx" "${orig_checklist[$i+1]}" "${orig_checklist[$i+2]}") + [[ -n "$original_default_item" && "$original_default_item" == "${orig_checklist[$i]}" ]] && G_WHIP_DEFAULT_ITEM=$__idx + ((__idx++)) + done + G_WHIP_CHECKLIST_ARRAY=("${__tmp[@]}") + fi + G_WHIP_INIT 3 # shellcheck disable=SC2086 G_WHIP_RETURNED_VALUE=$(whiptail ${G_PROGRAM_NAME:+--title "$G_PROGRAM_NAME"} --backtitle "$WHIP_BACKTITLE | Use spacebar to toggle selection" --checklist "$WHIP_MESSAGE" --separate-output --ok-button "$G_WHIP_BUTTON_OK_TEXT" --cancel-button "$G_WHIP_BUTTON_CANCEL_TEXT" "${NOCANCEL[@]}" --default-item "$G_WHIP_DEFAULT_ITEM" $WHIP_SCROLLTEXT "$WHIP_SIZE_Y" "$WHIP_SIZE_X" "$WHIP_SIZE_Z" -- "${G_WHIP_CHECKLIST_ARRAY[@]}" 3>&1 1>&2 2>&3-) result=$? + + # If enumeration was used, map numeric selections back to original tags + if [[ ${G_WHIP_CHECKLIST_ENUM:-0} == 1 && ${#orig_checklist[@]} -gt 0 && -n "$G_WHIP_RETURNED_VALUE" ]] + then + local sel mapped=() + for sel in $G_WHIP_RETURNED_VALUE; do + mapped+=("${__map[$sel]}") + done + G_WHIP_RETURNED_VALUE="${mapped[*]}" + # restore originals + G_WHIP_CHECKLIST_ARRAY=("${orig_checklist[@]}") + G_WHIP_DEFAULT_ITEM="$original_default_item" + unset -v __map __tmp __idx sel mapped + fi + G_WHIP_RETURNED_VALUE=$(echo -e "$G_WHIP_RETURNED_VALUE" | tr '\n' ' ') else G_WHIP_RETURNED_VALUE=$G_WHIP_DEFAULT_ITEM From 00626b09bfd6407bbc733eb13fba1080c315ef4d Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 29 Jun 2026 18:17:19 -0500 Subject: [PATCH 19/19] keys were set in 3 places, so made the defaults more automated/dynamic. housekeeping --- dietpi/func/dietpi-banner | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 0a96b369c2..a02041674d 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -163,32 +163,35 @@ ## DO NOT CHANGE. THIS IS REQUIRED TO MAINTAIN LEGACY INDEX->KEY MAPPING FOR MIGRATION ############## - # Set defaults: Disable CPU temp by default in VMs + ## Set default options + # Initialize aENABLED from aDESCRIPTION keys declare -gA aENABLED - if (( $G_HW_MODEL == 20 )) - then - aENABLED=( - [device_model]=1 [uptime]=0 [cpu_temp]=0 [hostname]=0 [nis_domainname]=0 [lan_ip]=1 [wan_ip]=0 [disk_rootfs]=1 [disk_userdata]=0 [weather]=0 [custom_commands]=0 [useful_commands]=1 [motd]=1 [vpn_status]=0 [large_hostname]=0 [print_credits]=1 [letsencrypt]=0 [ram_usage]=0 [load_average]=0 [word_wrap]=0 [kernel]=0 [network_usage]=0 [disk_usage]=0 [systemd_status]=0 [fail2ban_status]=0 - ) - else - aENABLED=( - [device_model]=1 [uptime]=0 [cpu_temp]=1 [hostname]=0 [nis_domainname]=0 [lan_ip]=1 [wan_ip]=0 [disk_rootfs]=0 [disk_userdata]=0 [weather]=0 [custom_commands]=0 [useful_commands]=1 [motd]=1 [vpn_status]=0 [large_hostname]=0 [print_credits]=1 [letsencrypt]=0 [ram_usage]=0 [load_average]=0 [word_wrap]=0 [kernel]=0 [network_usage]=0 [disk_usage]=0 [systemd_status]=0 [fail2ban_status]=0 - ) - fi - + for key in "${!aDESCRIPTION[@]}"; do + aENABLED[$key]=0 + done + # Set defaults: + aENABLED[device_model]=1 + aENABLED[lan_ip]=1 + aENABLED[disk_rootfs]=1 + aENABLED[useful_commands]=1 + aENABLED[motd]=1 + aENABLED[print_credits]=1 + # Enable CPU temp by default except in VMs + (( $G_HW_MODEL != 20 )) && aENABLED[cpu_temp]=1 # Folding type (colon, dash, fixed), and fixed indent BW_INDENT_TYPE='colon' BW_INDENT_FIXED=3 - # Default custom command example + ## Set default custom patterns and commands # shellcheck disable=SC2016 # Disable warning about single quotes in echo -e aCUSTOM_COMMANDS[0]='echo -e "$CLI_BULLET ${aCOLOUR[ALERT]}Hello ${aCOLOUR[GOOD]}World! ${COLOUR_RESET} :)"' # Default disk patterns aDISK_SPACE_PATTERNS=("/" "/mnt/dietpi_userdata") - # Load user preferences, overriding the above defaults as needed + ## Load user preferences # Flag indicating whether a legacy numeric->associative migration happened during this run declare -g DIETPI_BANNER_MIGRATED_DURING_RUN=0 + # Override the above defaults as needed DIETPI_BANNER_LOAD_PREFS # Derived convenience strings @@ -200,6 +203,7 @@ IP4_re='([0-9]{1,3}\.){3}[0-9]{1,3}' IP4_re_cidr='([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}' + # DietPi version string DIETPI_VERSION="$G_DIETPI_VERSION_CORE.$G_DIETPI_VERSION_SUB.$G_DIETPI_VERSION_RC" if [[ $G_GITOWNER != 'MichaIng' ]] then