From 7bf98e25bd31542cf928db9bc3161ffce54a182b Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 7 Jun 2026 21:43:33 -0500 Subject: [PATCH 01/39] 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 e898bd28f92d45ce6e31af47d6e08783f0add9c7 Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 28 Jun 2026 09:09:54 -0500 Subject: [PATCH 02/39] 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 5131bb566ff866e76f9b69630def1807255ede1f Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 28 Jun 2026 09:17:36 -0500 Subject: [PATCH 03/39] 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 7ae5764aeb0aaf0f0a3448463c5e62e8976302e3 Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 28 Jun 2026 10:11:36 -0500 Subject: [PATCH 04/39] 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 84b3bec1631d4a8e1df22c860603544dd8805e05 Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 28 Jun 2026 13:58:47 -0500 Subject: [PATCH 05/39] 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 b90f634faa2f52c4a513dec88269a99e2039d175 Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 28 Jun 2026 14:18:06 -0500 Subject: [PATCH 06/39] 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 d6d9996c482ffc9b9fcab00af2bb9cf916e1cdce Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 28 Jun 2026 15:57:19 -0500 Subject: [PATCH 07/39] 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 358b5fb997cc6f808f28f931a8026908215160de Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 28 Jun 2026 16:01:49 -0500 Subject: [PATCH 08/39] 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 bedf3d375ca8833d0c9e12c84d5d413120a75aea Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 28 Jun 2026 16:03:42 -0500 Subject: [PATCH 09/39] 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 59250e970e2be458388af20b1fe58c6936a78633 Mon Sep 17 00:00:00 2001 From: timjolson Date: Sun, 28 Jun 2026 16:24:12 -0500 Subject: [PATCH 10/39] 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 e73b847499862de80449b91e977dd8598956252f Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 29 Jun 2026 00:27:45 -0500 Subject: [PATCH 11/39] 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 754dbe0ebf7315bdb48f190d500caa95fd1ec8bb Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 29 Jun 2026 00:57:35 -0500 Subject: [PATCH 12/39] 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 3a75e1eebae1a593f33eda1798b3fd916a98759e Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 29 Jun 2026 12:26:26 -0500 Subject: [PATCH 13/39] 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 0665835b3a90fd132482dea4724b925eefd64552 Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 29 Jun 2026 13:04:29 -0500 Subject: [PATCH 14/39] 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 1dca26bad3261a45b6662835bfb697b3d017adb3 Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 29 Jun 2026 13:06:31 -0500 Subject: [PATCH 15/39] 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 ccc8ccdd29fb121fa1dfe122cfea0e6b61660ab5 Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 29 Jun 2026 15:58:25 -0500 Subject: [PATCH 16/39] 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 0599fb263739e1bd830419a6df14ffb7e9b83427 Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 29 Jun 2026 16:46:07 -0500 Subject: [PATCH 17/39] 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 57ce199537c83b767f69a77c1782fe32ce46f879 Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 29 Jun 2026 17:48:18 -0500 Subject: [PATCH 18/39] 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 956d0ff19598f7c3034dc66b4ca100d10464de88 Mon Sep 17 00:00:00 2001 From: timjolson Date: Mon, 29 Jun 2026 18:17:19 -0500 Subject: [PATCH 19/39] 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 From 577ee3e1e4b8da9acc33c91a79f4e1fcb84d3ae9 Mon Sep 17 00:00:00 2001 From: timjolson Date: Tue, 30 Jun 2026 10:56:19 -0500 Subject: [PATCH 20/39] removed duplicate entry --- dietpi/func/dietpi-banner | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index a02041674d..d69ec863da 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -148,7 +148,7 @@ 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 + network_usage disk_usage custom_commands useful_commands motd print_credits word_wrap ) # Keep a fixed index->key mapping to support numeric-index migration even if MENU_ITEMS changes later From 11df32ee1cd5298f9f1df52d3ac1bf8752af0676 Mon Sep 17 00:00:00 2001 From: timjolson Date: Tue, 30 Jun 2026 15:13:30 -0500 Subject: [PATCH 21/39] cleaned up shellcheck failures --- dietpi/func/dietpi-banner | 6 +----- dietpi/func/dietpi-globals | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index d69ec863da..f800625c1c 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -757,15 +757,11 @@ $CLI_LINE" fi if [[ "$avail_kb" =~ ^[0-9]+$ ]]; then - values_count=$((values_count+1)) - avail_gb=$(awk "BEGIN {printf \"%.1f \", $avail_kb/1024/1024}") + values_count=$((values_count+1)) else 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 diff --git a/dietpi/func/dietpi-globals b/dietpi/func/dietpi-globals index f3a184092d..e85fb08620 100644 --- a/dietpi/func/dietpi-globals +++ b/dietpi/func/dietpi-globals @@ -848,7 +848,7 @@ $grey───────────────────────── 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, + # 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 From ca2f16cb465266767596697ad286687ea9f2a9ff Mon Sep 17 00:00:00 2001 From: timjolson Date: Wed, 1 Jul 2026 11:51:11 -0500 Subject: [PATCH 22/39] added migration for legacy aCOLOUR preferences --- dietpi/func/dietpi-banner | 64 ++++++++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 8 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index f800625c1c..6b5faeac81 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -114,6 +114,44 @@ unset _tmp_enabled fi fi + + # Migrate legacy numeric-indexed aCOLOUR (from older FP_SAVEFILE) to associative form + # Only perform migration when numeric indices exist in the loaded aCOLOUR + if declare -p aCOLOUR >/dev/null 2>&1; then + local has_numeric_col=0 + for k in "${!aCOLOUR[@]}"; do + if [[ "$k" =~ ^[0-9]+$ ]]; then has_numeric_col=1; break; fi + done + if (( has_numeric_col )); then + # Prefer LEGACY_INDEXED_COLOUR_KEYS for a stable mapping + local -n key_map_ref_col=LEGACY_INDEXED_COLOUR_KEYS + declare -gA _tmp_colour=() + # First, map numeric indices explicitly so legacy numeric prefs take precedence + for (( i=0; i<${#key_map_ref_col[@]}; i++ )); do + if [[ -n "${aCOLOUR[$i]+x}" ]]; then + key=${key_map_ref_col[$i]} + [[ -n "$key" ]] && _tmp_colour[$key]=${aCOLOUR[$i]:-} + fi + done + # Then copy any remaining non-numeric keys that weren't set by numeric mapping + for idx in "${!aCOLOUR[@]}"; do + if [[ ! "$idx" =~ ^[0-9]+$ ]]; then + key=$idx + # don't overwrite keys already set by numeric mapping + [[ -n "${_tmp_colour[$key]+x}" ]] && continue + [[ -n "$key" ]] && _tmp_colour[$key]=${aCOLOUR[$idx]:-} + fi + done + # replace aCOLOUR with associative mapping + unset aCOLOUR + declare -gA aCOLOUR + for k in "${!_tmp_colour[@]}"; do aCOLOUR[$k]=${_tmp_colour[$k]}; done + + # Mark that a colour migration from legacy numeric indices occurred during this run + declare -g DIETPI_BANNER_COLOUR_MIGRATED_DURING_RUN=1 + unset _tmp_colour + fi + fi } # Use associative arrays keyed by short names derived from the description @@ -151,16 +189,21 @@ network_usage disk_usage custom_commands useful_commands motd print_credits word_wrap ) - # 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 + ## DO NOT CHANGE. THESE ARE REQUIRED TO MAINTAIN LEGACY INDEX->KEY MAPPING FOR MIGRATION + ## Keeps a fixed index->key mapping to support numeric-index migration of aCOLOUR and aENABLED preferences. ############## + # Fixed mapping for legacy numeric aENABLED indices -> associative keys 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 ) + # Fixed mapping for legacy numeric aCOLOUR indices -> associative keys + declare -ga LEGACY_INDEXED_COLOUR_KEYS=( + ACCENT STRONG WEAK ALERT GOOD HIGHLIGHT ALT + ) ############## - ## DO NOT CHANGE. THIS IS REQUIRED TO MAINTAIN LEGACY INDEX->KEY MAPPING FOR MIGRATION + ## DO NOT CHANGE. THESE ARE REQUIRED TO MAINTAIN LEGACY INDEX->KEY MAPPING FOR MIGRATION + ## Keeps a fixed index->key mapping to support numeric-index migration of aCOLOUR and aENABLED preferences. ############## ## Set default options @@ -189,8 +232,10 @@ aDISK_SPACE_PATTERNS=("/" "/mnt/dietpi_userdata") ## Load user preferences - # Flag indicating whether a legacy numeric->associative migration happened during this run + # Flag indicating whether a legacy numeric->associative preference migration happened during this run declare -g DIETPI_BANNER_MIGRATED_DURING_RUN=0 + # Flag indicating whether a legacy numeric->associative colour migration happened during this run + declare -g DIETPI_BANNER_COLOUR_MIGRATED_DURING_RUN=0 # Override the above defaults as needed DIETPI_BANNER_LOAD_PREFS @@ -304,6 +349,7 @@ # Clear migration flag now that prefs are persisted DIETPI_BANNER_MIGRATED_DURING_RUN=0 + DIETPI_BANNER_COLOUR_MIGRATED_DURING_RUN=0 return 0 } @@ -860,8 +906,10 @@ $CLI_LINE" 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" + # Notify user if prefs or colors were migrated from legacy numeric indices during this run + if (( DIETPI_BANNER_MIGRATED_DURING_RUN )) || (( DIETPI_BANNER_COLOUR_MIGRATED_DURING_RUN )); then + echo -e " ${aCOLOUR[HIGHLIGHT]}Banner needs to be migrated, run dietpi-banner and validate your settings.$COLOUR_RESET" + fi } Print_Banner() @@ -1014,7 +1062,7 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" # 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 + if [[ -f "$FP_SAVEFILE" ]] && { [[ $DIETPI_BANNER_MIGRATED_DURING_RUN == 1 ]] || [[ $DIETPI_BANNER_COLOUR_MIGRATED_DURING_RUN == 1 ]]; }; then backup="$FP_SAVEFILE.bak-$(date -Iseconds)" cp -a "$FP_SAVEFILE" "$backup" || true fi From f3b7962828cbbaf0d7015cbcb160d55226138f09 Mon Sep 17 00:00:00 2001 From: timjolson Date: Sat, 4 Jul 2026 07:59:58 -0500 Subject: [PATCH 23/39] added error handling for No IPs blocked (or Fail2ban not running) --- dietpi/func/dietpi-banner | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 6b5faeac81..00c09de967 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -533,7 +533,12 @@ $CLI_LINE" # 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:]]*$')" + if [[ -z "$IP_addrs" ]]; then + IP_addrs="" + count_banned_ips=0 + else + count_banned_ips="$(echo "$IP_addrs" | wc -l)" # | grep -cve '^[[:space:]]*$')" + fi echo "$count_banned_ips" return 0 @@ -550,7 +555,7 @@ $CLI_LINE" 0) 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}" + state="${aCOLOUR[HIGHLIGHT]}No IP(s) Banned, check Fail2Ban state${COLOUR_RESET}" else if [[ $count_banned_ips -gt 20 ]]; then # high number of bans @@ -717,7 +722,7 @@ $CLI_LINE" output="$(eval "$cmd" 2>&1)" status=$? if [[ $status -ne 0 ]]; then - printf '%b\n' "${aCOLOUR[ALERT]}Error executing CUSTOM_COMMAND[$((i))] Exit Code: ${status}${COLOUR_RESET}" + printf '%b\n' "${aCOLOUR[ALERT]}Error executing aCUSTOM_COMMANDS[$((i))] Exit Code: ${status}${COLOUR_RESET}" else printf '%b\n' "$output" fi @@ -820,8 +825,8 @@ $CLI_LINE" 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" + printf "%s %s%-${length}s %s %s%6b %s %s%8sGiB of %8sGiB%s\n" \ + "$CLI_BULLET" "${aCOLOUR[ALT]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[STRONG]}" "$perc" "$CLI_SEPARATOR" "${aCOLOUR[WEAK]}" "$used_gb" "$size_gb" "$COLOUR_RESET" done } @@ -1083,4 +1088,4 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" #----------------------------------------------------------------------------------- exit 0 #----------------------------------------------------------------------------------- -} \ No newline at end of file +} From 297080491bdc827b16af2fb32649117146f9d087 Mon Sep 17 00:00:00 2001 From: MichaIng Date: Tue, 7 Jul 2026 16:24:53 +0200 Subject: [PATCH 24/39] dietpi-banner: code alignments and simplification * align indentation * use "declare" only where needed (for assiciative arrays) * use "declare -g" only where needed: when defining global variables inside a function * use quotation in bash conditionals and variable assignment only where needed * use single-quotes consistently if no variable expansion or literal `'` is needed * prefer $EUID over external `id -u` call * prefer mawk over awk (which can be the slower gawk if installed) * prefer bash arithmetic context over `-ge` and alike operators * skip `|| true` as no `set -e` is used * merge bash conditionals where possible * simplify numeric => assiciative config array conversion: alter the new arrays in place instead of using a temporary conversion array: Both arrays are declared as assiciative arrays already before the config is sourced. Hence they remain assiciative arrays, just with the numberic indices added. If any index matches an existing old numeric key, it can be applied as its new assiciative key directly, and the numeric value unset. The mapping arrays were inlined, so ultimately this code can go into dietpi-update patches instead. --- dietpi/func/dietpi-banner | 514 +++++++++++++++++--------------------- 1 file changed, 229 insertions(+), 285 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 00c09de967..b0a3ef19c6 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -59,15 +59,15 @@ { declare -gA aCOLOUR=( # 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 + [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) + COLOUR_RESET='\e[0m' # Reset formatting to default (do not override this) # Load settings here, to have chosen custom CLI colors applied # disable shellcheck for non-constant `source` (`.`) @@ -75,87 +75,45 @@ [[ -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 + local k legacy_indexed_enabled_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 + ) + for k in "${!aENABLED[@]}" + do + # Leave non-numeric indices untouched + [[ $k =~ ^[0-9]+$ ]] || continue + + # Apply as new associative array index from legacy_indexed_enabled_keys if there is one + [[ ${legacy_indexed_enabled_keys[k]} ]] && aENABLED[${legacy_indexed_enabled_keys[k]}]=${aENABLED[$k]} || continue + + # Unset old numeric index + unset -v "aENABLED[$k]" + + # Mark that a migration from legacy numeric indices occurred during this run + DIETPI_BANNER_MIGRATED_DURING_RUN=1 + done # Migrate legacy numeric-indexed aCOLOUR (from older FP_SAVEFILE) to associative form - # Only perform migration when numeric indices exist in the loaded aCOLOUR - if declare -p aCOLOUR >/dev/null 2>&1; then - local has_numeric_col=0 - for k in "${!aCOLOUR[@]}"; do - if [[ "$k" =~ ^[0-9]+$ ]]; then has_numeric_col=1; break; fi - done - if (( has_numeric_col )); then - # Prefer LEGACY_INDEXED_COLOUR_KEYS for a stable mapping - local -n key_map_ref_col=LEGACY_INDEXED_COLOUR_KEYS - declare -gA _tmp_colour=() - # First, map numeric indices explicitly so legacy numeric prefs take precedence - for (( i=0; i<${#key_map_ref_col[@]}; i++ )); do - if [[ -n "${aCOLOUR[$i]+x}" ]]; then - key=${key_map_ref_col[$i]} - [[ -n "$key" ]] && _tmp_colour[$key]=${aCOLOUR[$i]:-} - fi - done - # Then copy any remaining non-numeric keys that weren't set by numeric mapping - for idx in "${!aCOLOUR[@]}"; do - if [[ ! "$idx" =~ ^[0-9]+$ ]]; then - key=$idx - # don't overwrite keys already set by numeric mapping - [[ -n "${_tmp_colour[$key]+x}" ]] && continue - [[ -n "$key" ]] && _tmp_colour[$key]=${aCOLOUR[$idx]:-} - fi - done - # replace aCOLOUR with associative mapping - unset aCOLOUR - declare -gA aCOLOUR - for k in "${!_tmp_colour[@]}"; do aCOLOUR[$k]=${_tmp_colour[$k]}; done - - # Mark that a colour migration from legacy numeric indices occurred during this run - declare -g DIETPI_BANNER_COLOUR_MIGRATED_DURING_RUN=1 - unset _tmp_colour - fi - fi + local legacy_indexed_colour_keys=(ACCENT STRONG WEAK ALERT GOOD HIGHLIGHT ALT) + for k in "${!aCOLOUR[@]}" + do + # Leave non-numeric indices untouched + [[ $k =~ ^[0-9]+$ ]] || continue + + # Apply as new associative array index from legacy_indexed_colour_keys if there is one + [[ ${legacy_indexed_colour_keys[k]} ]] && aCOLOUR[${legacy_indexed_colour_keys[k]}]=${aCOLOUR[$k]} || continue + + # Unset old numeric index + unset -v "aCOLOUR[$k]" + + # Mark that a colour migration from legacy numeric indices occurred during this run + DIETPI_BANNER_COLOUR_MIGRATED_DURING_RUN=1 + done } # Use associative arrays keyed by short names derived from the description - declare -gA aDESCRIPTION=( + declare -A aDESCRIPTION=( [device_model]='Device model' [uptime]='Uptime' [cpu_temp]='CPU temp' @@ -183,36 +141,20 @@ [fail2ban_status]='Fail2Ban Status' ) # Set the array order for the menu, items must be listed here to show up - declare -ga MENU_ITEMS=( + 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 ) - ############## - ## DO NOT CHANGE. THESE ARE REQUIRED TO MAINTAIN LEGACY INDEX->KEY MAPPING FOR MIGRATION - ## Keeps a fixed index->key mapping to support numeric-index migration of aCOLOUR and aENABLED preferences. - ############## - # Fixed mapping for legacy numeric aENABLED indices -> associative keys - 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 - ) - # Fixed mapping for legacy numeric aCOLOUR indices -> associative keys - declare -ga LEGACY_INDEXED_COLOUR_KEYS=( - ACCENT STRONG WEAK ALERT GOOD HIGHLIGHT ALT - ) - ############## - ## DO NOT CHANGE. THESE ARE REQUIRED TO MAINTAIN LEGACY INDEX->KEY MAPPING FOR MIGRATION - ## Keeps a fixed index->key mapping to support numeric-index migration of aCOLOUR and aENABLED preferences. - ############## - ## Set default options # Initialize aENABLED from aDESCRIPTION keys - declare -gA aENABLED - for key in "${!aDESCRIPTION[@]}"; do - aENABLED[$key]=0 + declare -A aENABLED + for k in "${!aDESCRIPTION[@]}" + do + aENABLED[$k]=0 done - # Set defaults: + # Set defaults aENABLED[device_model]=1 aENABLED[lan_ip]=1 aENABLED[disk_rootfs]=1 @@ -233,9 +175,9 @@ ## Load user preferences # Flag indicating whether a legacy numeric->associative preference migration happened during this run - declare -g DIETPI_BANNER_MIGRATED_DURING_RUN=0 + DIETPI_BANNER_MIGRATED_DURING_RUN=0 # Flag indicating whether a legacy numeric->associative colour migration happened during this run - declare -g DIETPI_BANNER_COLOUR_MIGRATED_DURING_RUN=0 + DIETPI_BANNER_COLOUR_MIGRATED_DURING_RUN=0 # Override the above defaults as needed DIETPI_BANNER_LOAD_PREFS @@ -308,9 +250,10 @@ Save() { # 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" + for k in "${MENU_ITEMS[@]}" + do + val=${aENABLED[$k]:-0} + printf 'aENABLED[%s]=%s\n' "$k" "$val" done # Persist only the primary colour slots. Convert any actual @@ -319,9 +262,9 @@ for i in "${!aCOLOUR[@]}" do # Don't save the RESET colour slot - [[ "$i" == "RESET" ]] && continue + [[ $i == 'RESET' ]] && continue - val="${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 @@ -330,14 +273,16 @@ done # Persist aDISK_SPACE_PATTERNS (indices 0..N) - for i in "${!aDISK_SPACE_PATTERNS[@]}"; do + for i in "${!aDISK_SPACE_PATTERNS[@]}" + do patt=${aDISK_SPACE_PATTERNS[$i]} patt=${patt//\'/\\\'} printf "aDISK_SPACE_PATTERNS[%s]='%s'\n" "$i" "$patt" done # Persist aCUSTOM_COMMANDS (indices 0..N) - for i in "${!aCUSTOM_COMMANDS[@]}"; do + for i in "${!aCUSTOM_COMMANDS[@]}" + do cmd=${aCUSTOM_COMMANDS[$i]} cmd=${cmd//\'/\\\'} printf "aCUSTOM_COMMANDS[%s]='%s'\n" "$i" "$cmd" @@ -410,16 +355,16 @@ $CLI_LINE" do # disable shellcheck for 'require-double-brackets' so it is `dash` compatible # shellcheck disable=SC2292 - [ -f "$i" ] || continue + [[ -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="" + stat= code=0 - if [[ "$(id -u)" = "0" ]] + if [[ $EUID == 0 ]] then # Running as root stat=$(Get_Cert_Status_Raw 2>/dev/null) @@ -432,15 +377,16 @@ $CLI_LINE" case $code in 0) - case "$stat" in - Valid*) stat="${aCOLOUR[WEAK]}$stat${COLOUR_RESET}" ;; - No\ certificate*) stat="${aCOLOUR[ALERT]}$stat${COLOUR_RESET}" ;; - *) stat="${aCOLOUR[HIGHLIGHT]}$stat${COLOUR_RESET}" ;; + # stat is valid + case $stat in + 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[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}" ;; + ;; + 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" @@ -495,19 +441,19 @@ $CLI_LINE" { # 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[STRONG]}" - printf "%b%b %-${width}b %b %b\n" \ - "$CLI_BULLET" "${color}" "$subtitle" "$CLI_SEPARATOR" "$state" + local subtitle=$1 + local state=$2 + local width=${3:-} + local color=${aCOLOUR[STRONG]} + printf "%b%b %-${width}b %b %b\n" "$CLI_BULLET" "${color}" "$subtitle" "$CLI_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 + FAILED_CNT=$(systemctl --failed --no-legend --no-pager | wc -l 2> /dev/null) + if [[ ! $FAILED_CNT || $FAILED_CNT == 0 ]] + then state="${aCOLOUR[WEAK]}No Services Failed${COLOUR_RESET}" else state="${aCOLOUR[ALERT]}$FAILED_CNT Service(s) Failed${COLOUR_RESET}" @@ -519,22 +465,26 @@ $CLI_LINE" { Get_Fail2Ban_Status_Raw() { - if command -v fail2ban-client >/dev/null 2>&1; then - if [[ "$(id -u)" = "0" ]]; then + if command -v fail2ban-client >/dev/null 2>&1 + then + if [[ $EUID == 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 + [[ $? == 2 ]] && return 2 fi - if ! command -v sort >/dev/null 2>&1; then + 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)" - if [[ -z "$IP_addrs" ]]; then - IP_addrs="" + if [[ ! "$IP_addrs" ]] + then + IP_addrs= count_banned_ips=0 else count_banned_ips="$(echo "$IP_addrs" | wc -l)" # | grep -cve '^[[:space:]]*$')" @@ -553,11 +503,13 @@ $CLI_LINE" case $code in 0) - if [[ $count_banned_ips -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, check Fail2Ban state${COLOUR_RESET}" else - if [[ $count_banned_ips -gt 20 ]]; then + if [[ $count_banned_ips -gt 20 ]] + then # high number of bans state="${aCOLOUR[ALERT]}$count_banned_ips IP(s) Banned${COLOUR_RESET}" else @@ -565,16 +517,10 @@ $CLI_LINE" state="${aCOLOUR[WEAK]}$count_banned_ips IP(s) Banned${COLOUR_RESET}" fi fi - ;; - 1) - 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, code: $code count: $count_banned_ips${COLOUR_RESET}" - ;; + ;; + 1) 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, code: $code count: $count_banned_ips${COLOUR_RESET}";; esac echo "$state" @@ -589,7 +535,7 @@ $CLI_LINE" { # Defaults local timeout=3 - local netns="" + local netns # Inputs while (( $# )) do @@ -601,12 +547,11 @@ $CLI_LINE" shift done - local -a ns_cmd_prefix=() - if [[ -n "$netns" ]]; then - if [[ "$(id -u)" -ne "0" ]]; then - # add sudo if not root - ns_cmd_prefix+=(sudo -n) - fi + local ns_cmd_prefix=() + if [[ $netns ]] + then + # add sudo if not root + (( $EUID )) && ns_cmd_prefix+=(sudo -n) # add netns prefix ns_cmd_prefix+=(ip netns exec "$netns") fi @@ -619,95 +564,92 @@ $CLI_LINE" # 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" + 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=() - if [[ -n "$netns" ]]; then - if [[ "$(id -u)" -ne "0" ]]; then - # add sudo if not root - ns_cmd_prefix+=(sudo -n) - fi + local ns_cmd_prefix=() + if [[ $netns ]] + then + # add sudo if not root + (( $EUID )) && ns_cmd_prefix+=(sudo -n) # add netns prefix 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}') + mapfile -t observed_ifaces < <("${ns_cmd_prefix[@]}" ip -o link show 2>/dev/null | mawk -F': ' '{print $2}') - for IFACE in "${observed_ifaces[@]}"; do + 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 + [[ $ifname == 'lo' ]] && continue - case "$ifname" in + case $ifname in # wired / predictable names - eth*|en*|lan*) : ;; + eth*|en*|lan*) :;; # wireless - wlan*|wl*) : ;; + wlan*|wl*) :;; # vpn/tunnel/virtual - tun*|wg*|vpn*) : ;; + tun*|wg*|vpn*) :;; *) continue ;; esac - 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 - 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" + rx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/rx_bytes" 2>/dev/null) + [[ $rx =~ ^[0-9]+$ ]] && RX="$(mawk -v v="$rx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" # 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" + tx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/tx_bytes" 2>/dev/null) + [[ $tx =~ ^[0-9]+$ ]] && TX="$(mawk -v v="$tx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" # 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%%/*} + ip_addr=' - No IP - ' + ip_addr_cidr=$("${ns_cmd_prefix[@]}" ip -o -4 addr show dev "$ifname" scope global 2>/dev/null | mawk '{print $4}' | head -n1 | grep -E "$IP4_re_cidr") + [[ "$ip_addr_cidr" ]] && ip_addr=${ip_addr_cidr%%/*} - 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 + [[ $RX == 'N/A' && $TX == 'N/A' ]] && continue + 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" done } - mapfile -t namespaces < <(ip netns ls | awk 'NF>1 { print $1 }') + mapfile -t namespaces < <(ip netns ls | mawk 'NF>1 { print $1 }') # Prepend an empty entry so the default (root) namespace can be handled in the loop - namespaces=( "" "${namespaces[@]}" ) + namespaces=('' "${namespaces[@]}") - for ns in "${namespaces[@]}"; do - raw_ns="${ns:-default}" - display_ns="$(G_TRUNCATE_MID "$raw_ns" 24)" + for ns in "${namespaces[@]}" + do + raw_ns=${ns:-default} + display_ns=$(G_TRUNCATE_MID "$raw_ns" 24) IP=$(BANNER_GET_WAN_IP -t 1 -n "$ns" 2>&1) - if [[ $IP =~ $IP4_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. # IP="${BASH_REMATCH[0]}" - printf " %s%s [%s]%s\n" \ - "${aCOLOUR[ALT]}" "$IP" "$display_ns" "$COLOUR_RESET" + printf ' %s%s [%s]%s\n' "${aCOLOUR[ALT]}" "$IP" "$display_ns" "$COLOUR_RESET" - elif [[ -z $IP ]]; then + elif [[ ! $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%s%s\n' "${aCOLOUR[ALERT]}" 'Disconnected' "$CLI_SEPARATOR" "${aCOLOUR[STRONG]}" "$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 + # Treat known curl/network error messages as timeout/DNS failures + [[ $IP =~ $err_re ]] && IP='Timeout/DNS Failure' # 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" + printf ' %s%s [%s]%s\n' "${aCOLOUR[HIGHLIGHT]}" "$IP" "$display_ns" "$COLOUR_RESET" fi DATA_USAGE "$ns" @@ -716,16 +658,16 @@ $CLI_LINE" Print_Custom_Commands() { - for i in "${!aCUSTOM_COMMANDS[@]}"; do - local cmd="${aCUSTOM_COMMANDS[$i]}" - if [[ -n "$cmd" ]]; then - output="$(eval "$cmd" 2>&1)" - status=$? - if [[ $status -ne 0 ]]; then - printf '%b\n' "${aCOLOUR[ALERT]}Error executing aCUSTOM_COMMANDS[$((i))] Exit Code: ${status}${COLOUR_RESET}" - else - printf '%b\n' "$output" - fi + for i in "${!aCUSTOM_COMMANDS[@]}" + do + [[ ${aCUSTOM_COMMANDS[$i]} ]] || continue + output=$(eval "${aCUSTOM_COMMANDS[$i]}" 2>&1) + status=$? + if (( $status )) + then + printf '%b\n' "${aCOLOUR[ALERT]}Error executing aCUSTOM_COMMANDS[$i] Exit Code: ${status}${COLOUR_RESET}" + else + printf '%b\n' "$output" fi done } @@ -733,22 +675,24 @@ $CLI_LINE" Get_Disk_Usage() { # If no arguments are provided, default to '"/" "/mnt/*"'. - if (( $# > 0 )); then - aDISK_SPACE_PATTERNS=("$@") - fi + (( $# )) && aDISK_SPACE_PATTERNS=("$@") # 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 + while read -r size_kb used_kb avail_kb mnt_target + do matched=0 basename= - for pattern in "${aDISK_SPACE_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:}" - if [[ $mnt_target =~ $re ]]; then + if [[ $pattern == 're:'* ]] + then + re=${pattern#re:} + if [[ $mnt_target =~ $re ]] + then matched=1 basename=$(basename "$mnt_target") break @@ -761,83 +705,86 @@ $CLI_LINE" matched=1 basename=$(basename "$mnt_target") break - ;; - *) ;; + ;; + *) :;; esac fi done - if (( matched )); then + if (( $matched )) + then len=${#basename} - if (( len > length )); then - length=$len - fi + (( $len > $length )) && length=$len 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 + for entry in "${results[@]}" + do IFS='|' read -r size_kb used_kb avail_kb mnt_target name <<< "$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 - values_count=$((values_count+1)) - size_gb=$(awk "BEGIN {printf \"%.1f \", $size_kb/1024/1024}") + if [[ $size_kb =~ ^[0-9]+$ ]] + then + ((values_count++)) + size_gb=$(mawk "BEGIN {printf \"%.1f \", $size_kb/1024/1024}") else - if [[ "$used_kb" =~ ^[0-9]+$ ]] && [[ "$avail_kb" =~ ^[0-9]+$ ]]; then + 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}") + size_kb=$(( $used_kb + $avail_kb )) + size_gb=$(mawk "BEGIN {printf \"%.1f \", ($size_kb)/1024/1024}") else - size_gb="???" + size_gb='???' fi fi - if [[ "$used_kb" =~ ^[0-9]+$ ]]; then - values_count=$((values_count+1)) - used_gb=$(awk "BEGIN {printf \"%.1f \", $used_kb/1024/1024}") + if [[ $used_kb =~ ^[0-9]+$ ]] + then + ((values_count++)) + used_gb=$(mawk "BEGIN {printf \"%.1f \", $used_kb/1024/1024}") else - if [[ "$size_kb" =~ ^[0-9]+$ ]] && [[ "$avail_kb" =~ ^[0-9]+$ ]]; then + 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}") + used_kb=$(( $size_kb - $avail_kb )) + used_gb=$(mawk "BEGIN {printf \"%.1f \", ($used_kb)/1024/1024}") else - used_gb="???" + used_gb='???' fi fi - if [[ "$avail_kb" =~ ^[0-9]+$ ]]; then - values_count=$((values_count+1)) + if [[ $avail_kb =~ ^[0-9]+$ ]] + then + ((values_count++)) else - 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)) - fi + # If size and used are available, calculate available as size - used + [[ $size_kb =~ ^[0-9]+$ && $used_kb =~ ^[0-9]+$ ]] && avail_kb=$(( $size_kb - $used_kb )) fi # Calculate percentage used based on available data: - if [[ "$values_count" -ge 2 ]]; then + if (( $values_count > 1 )) + then # this should match the `df` output - perc=$(awk "BEGIN {printf \"%.1f%%\", ($used_kb/($used_kb+$avail_kb))*100}") + perc=$(mawk "BEGIN {printf \"%.1f%%\", ($used_kb/($used_kb+$avail_kb))*100}") else - perc="?? %" # default unknown value + perc='?? %' # default unknown value fi # Print aligned columns: {basename} : {used} of {size} ({percent used}) - printf "%s %s%-${length}s %s %s%6b %s %s%8sGiB of %8sGiB%s\n" \ - "$CLI_BULLET" "${aCOLOUR[ALT]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[STRONG]}" "$perc" "$CLI_SEPARATOR" "${aCOLOUR[WEAK]}" "$used_gb" "$size_gb" "$COLOUR_RESET" + printf "%s %s%-${length}s %s %s%6b %s %s%8sGiB of %8sGiB%s\n" "$CLI_BULLET" "${aCOLOUR[ALT]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[STRONG]}" "$perc" "$CLI_SEPARATOR" "${aCOLOUR[WEAK]}" "$used_gb" "$size_gb" "$COLOUR_RESET" done } Get_VPN_Status() { - local state="$( /boot/dietpi/dietpi-vpn status 2>&1 )" - case "$state" in - Connected*) state="${aCOLOUR[WEAK]}$state${COLOUR_RESET}" ;; - Disconnected*) state="${aCOLOUR[ALERT]}$state${COLOUR_RESET}" ;; - *) state="${aCOLOUR[HIGHLIGHT]}$state${COLOUR_RESET}" ;; + local state=$(/boot/dietpi/dietpi-vpn status 2>&1) + case $state in + Connected*) state="${aCOLOUR[WEAK]}$state${COLOUR_RESET}";; + Disconnected*) state="${aCOLOUR[ALERT]}$state${COLOUR_RESET}";; + *) state="${aCOLOUR[HIGHLIGHT]}$state${COLOUR_RESET}";; esac echo "$state" } @@ -848,7 +795,7 @@ $CLI_LINE" # Large Format Hostname # shellcheck disable=SC1091 - (( ${aENABLED[large_hostname]} )) && . /boot/dietpi/func/dietpi-print_large "$( "$FP_SAVEFILE" From d49c0f57aa2fb93504fe31b3775905c96e265498 Mon Sep 17 00:00:00 2001 From: MichaIng Date: Tue, 7 Jul 2026 17:07:28 +0200 Subject: [PATCH 25/39] dietpi-banner: some restructure * some reordering to reduce the diff * use lower-case colour array keys * merge the two unnecessarily split array migration flags * some more cleanup --- dietpi/func/dietpi-banner | 287 ++++++++++++++++++-------------------- 1 file changed, 138 insertions(+), 149 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index b0a3ef19c6..ae4ab8fed0 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -23,8 +23,6 @@ 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 ]] @@ -44,74 +42,6 @@ readonly FP_SAVEFILE='/boot/dietpi/.dietpi-banner' readonly FP_BANNERWRAP_AWK='/boot/dietpi/func/dietpi-banner-wrap.awk' - # ---------------------------------------------------------------------------- - # 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` - # - # Usage: `DIETPI_BANNER_LOAD_PREFS` - # ---------------------------------------------------------------------------- - DIETPI_BANNER_LOAD_PREFS() - { - declare -gA aCOLOUR=( - # 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 - ) - COLOUR_RESET='\e[0m' # Reset formatting to default (do not override this) - - # Load settings here, to have chosen custom CLI colors applied - # 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 - local k legacy_indexed_enabled_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 - ) - for k in "${!aENABLED[@]}" - do - # Leave non-numeric indices untouched - [[ $k =~ ^[0-9]+$ ]] || continue - - # Apply as new associative array index from legacy_indexed_enabled_keys if there is one - [[ ${legacy_indexed_enabled_keys[k]} ]] && aENABLED[${legacy_indexed_enabled_keys[k]}]=${aENABLED[$k]} || continue - - # Unset old numeric index - unset -v "aENABLED[$k]" - - # Mark that a migration from legacy numeric indices occurred during this run - DIETPI_BANNER_MIGRATED_DURING_RUN=1 - done - - # Migrate legacy numeric-indexed aCOLOUR (from older FP_SAVEFILE) to associative form - local legacy_indexed_colour_keys=(ACCENT STRONG WEAK ALERT GOOD HIGHLIGHT ALT) - for k in "${!aCOLOUR[@]}" - do - # Leave non-numeric indices untouched - [[ $k =~ ^[0-9]+$ ]] || continue - - # Apply as new associative array index from legacy_indexed_colour_keys if there is one - [[ ${legacy_indexed_colour_keys[k]} ]] && aCOLOUR[${legacy_indexed_colour_keys[k]}]=${aCOLOUR[$k]} || continue - - # Unset old numeric index - unset -v "aCOLOUR[$k]" - - # Mark that a colour migration from legacy numeric indices occurred during this run - DIETPI_BANNER_COLOUR_MIGRATED_DURING_RUN=1 - done - } - # Use associative arrays keyed by short names derived from the description declare -A aDESCRIPTION=( [device_model]='Device model' @@ -142,19 +72,19 @@ ) # Set the array order for the menu, items must be listed here to show up 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 \ + 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 ) - ## Set default options # Initialize aENABLED from aDESCRIPTION keys declare -A aENABLED for k in "${!aDESCRIPTION[@]}" do aENABLED[$k]=0 done - # Set defaults + + # Set default options aENABLED[device_model]=1 aENABLED[lan_ip]=1 aENABLED[disk_rootfs]=1 @@ -163,28 +93,93 @@ aENABLED[print_credits]=1 # Enable CPU temp by default except in VMs (( $G_HW_MODEL != 20 )) && aENABLED[cpu_temp]=1 + + # Default colours + COLOUR_RESET='\e[0m' # Reset formatting to default (do not override this) + # Colour 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` + declare -A aCOLOUR=( + [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 + ) + # Folding type (colon, dash, fixed), and fixed indent BW_INDENT_TYPE='colon' BW_INDENT_FIXED=3 - ## 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} :)"' + # Set default custom patterns and commands + # shellcheck disable=SC2016 + aCUSTOM_COMMANDS[0]='echo -e "$CLI_BULLET ${aCOLOUR[alert]}Hello ${aCOLOUR[good]}World! ${COLOUR_RESET} :)"' # Default disk patterns - aDISK_SPACE_PATTERNS=("/" "/mnt/dietpi_userdata") + aDISK_SPACE_PATTERNS=('/' '/mnt/dietpi_userdata') - ## Load user preferences - # Flag indicating whether a legacy numeric->associative preference migration happened during this run + # Load settings here, to have chosen custom CLI colours applied + # shellcheck disable=SC1090 + [[ -f $FP_SAVEFILE ]] && . "$FP_SAVEFILE" + + # ---------------------------------------------------------------------------- + # Legacy config migration helper + # ---------------------------------------------------------------------------- + # Migration helper function + DIETPI_BANNER_LEGACY_MIGRATION() + { + # Migrate legacy numeric-indexed aENABLED (from older FP_SAVEFILE) to associative form + local k legacy_indexed_enabled_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 + ) + for k in "${!aENABLED[@]}" + do + # Leave non-numeric indices untouched + [[ $k =~ ^[0-9]+$ ]] || continue + + # Apply as new associative array index from legacy_indexed_enabled_keys if there is one + # shellcheck disable=SC2015 + [[ ${legacy_indexed_enabled_keys[k]} ]] && aENABLED[${legacy_indexed_enabled_keys[k]}]=${aENABLED[$k]} || continue + + # Unset old numeric index + unset -v "aENABLED[$k]" + + # Mark that a migration from legacy numeric indices occurred during this run + DIETPI_BANNER_MIGRATED_DURING_RUN=1 + done + + # Migrate legacy numeric-indexed aCOLOUR (from older FP_SAVEFILE) to associative form + local legacy_indexed_colour_keys=(accent strong weak alert good highlight alt) + for k in "${!aCOLOUR[@]}" + do + # Leave non-numeric indices untouched + [[ $k =~ ^[0-9]+$ ]] || continue + + # Apply as new associative array index from legacy_indexed_colour_keys if there is one + # shellcheck disable=SC2015 + [[ ${legacy_indexed_colour_keys[k]} ]] && aCOLOUR[${legacy_indexed_colour_keys[k]}]=${aCOLOUR[$k]} || continue + + # Unset old numeric index + unset -v "aCOLOUR[$k]" + + # Mark that a migration from legacy numeric indices occurred during this run + DIETPI_BANNER_MIGRATED_DURING_RUN=1 + done + } + # Flag indicating whether a legacy numeric->associative config migration happened during this run DIETPI_BANNER_MIGRATED_DURING_RUN=0 - # Flag indicating whether a legacy numeric->associative colour migration happened during this run - DIETPI_BANNER_COLOUR_MIGRATED_DURING_RUN=0 - # Override the above defaults as needed - DIETPI_BANNER_LOAD_PREFS + # Run migration + DIETPI_BANNER_LEGACY_MIGRATION + # ---------------------------------------------------------------------------- # Derived convenience strings - CLI_LINE=" ${aCOLOUR[ACCENT]}─────────────────────────────────────────────────────$COLOUR_RESET" - CLI_BULLET=" ${aCOLOUR[ACCENT]}-$COLOUR_RESET" - CLI_SEPARATOR="${aCOLOUR[ACCENT]}:$COLOUR_RESET" + CLI_LINE=" ${aCOLOUR[accent]}─────────────────────────────────────────────────────$COLOUR_RESET" + 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}' @@ -250,10 +245,9 @@ Save() { # Persist associative aENABLED as simple assignments (one per line) - for k in "${MENU_ITEMS[@]}" + for i in "${!aDESCRIPTION[@]}" do - val=${aENABLED[$k]:-0} - printf 'aENABLED[%s]=%s\n' "$k" "$val" + echo "aENABLED[$i]=${aENABLED[$i]}" done # Persist only the primary colour slots. Convert any actual @@ -261,9 +255,6 @@ # portable when sourced in different shells. 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') @@ -277,7 +268,7 @@ do patt=${aDISK_SPACE_PATTERNS[$i]} patt=${patt//\'/\\\'} - printf "aDISK_SPACE_PATTERNS[%s]='%s'\n" "$i" "$patt" + echo "aDISK_SPACE_PATTERNS[$i]='$patt'" done # Persist aCUSTOM_COMMANDS (indices 0..N) @@ -285,7 +276,7 @@ do cmd=${aCUSTOM_COMMANDS[$i]} cmd=${cmd//\'/\\\'} - printf "aCUSTOM_COMMANDS[%s]='%s'\n" "$i" "$cmd" + echo "aCUSTOM_COMMANDS[$i]='$cmd'" done # Persist BW indent settings @@ -294,8 +285,6 @@ # Clear migration flag now that prefs are persisted DIETPI_BANNER_MIGRATED_DURING_RUN=0 - DIETPI_BANNER_COLOUR_MIGRATED_DURING_RUN=0 - return 0 } Print_Header() @@ -303,29 +292,29 @@ # DietPi update available? if Check_DietPi_Update then - local text_update_available_date="${aCOLOUR[HIGHLIGHT]}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[HIGHLIGHT]}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[HIGHLIGHT]}$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[ALERT]}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 "$CLI_LINE - ${aCOLOUR[STRONG]}DietPi v$DIETPI_VERSION$COLOUR_RESET $CLI_SEPARATOR $text_update_available_date$COLOUR_RESET + ${aCOLOUR[strong]}DietPi v$DIETPI_VERSION$COLOUR_RESET $CLI_SEPARATOR $text_update_available_date$COLOUR_RESET $CLI_LINE" } @@ -333,7 +322,7 @@ $CLI_LINE" { # ON THE CURRENT LINE Output green line separator with subheader title "$1" echo -e "\r$CLI_LINE - ${aCOLOUR[STRONG]}$1 $CLI_SEPARATOR" + ${aCOLOUR[strong]}$1 $CLI_SEPARATOR" } Get_Local_Ip() @@ -379,14 +368,14 @@ $CLI_LINE" 0) # stat is valid case $stat in - Valid*) stat="${aCOLOUR[WEAK]}$stat${COLOUR_RESET}";; - No\ certificate*) stat="${aCOLOUR[ALERT]}$stat${COLOUR_RESET}";; - *) stat="${aCOLOUR[HIGHLIGHT]}$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 ;; - 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}";; + 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" @@ -394,7 +383,7 @@ $CLI_LINE" Print_Credits() { - echo -e " ${aCOLOUR[WEAK]}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 @@ -409,32 +398,32 @@ $CLI_LINE" # DietPi update available? if [[ $AVAILABLE_UPDATE ]] then - 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" + 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[STRONG]}dietpi-update$COLOUR_RESET $CLI_SEPARATOR ${aCOLOUR[HIGHLIGHT]}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[STRONG]}apt upgrade$COLOUR_RESET $CLI_SEPARATOR ${aCOLOUR[HIGHLIGHT]}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[STRONG]}reboot$COLOUR_RESET $CLI_SEPARATOR ${aCOLOUR[ALERT]}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[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" + 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() @@ -444,7 +433,7 @@ $CLI_LINE" local subtitle=$1 local state=$2 local width=${3:-} - local color=${aCOLOUR[STRONG]} + local color=${aCOLOUR[strong]} printf "%b%b %-${width}b %b %b\n" "$CLI_BULLET" "${color}" "$subtitle" "$CLI_SEPARATOR" "$state" } @@ -454,9 +443,9 @@ $CLI_LINE" FAILED_CNT=$(systemctl --failed --no-legend --no-pager | wc -l 2> /dev/null) if [[ ! $FAILED_CNT || $FAILED_CNT == 0 ]] then - state="${aCOLOUR[WEAK]}No Services Failed${COLOUR_RESET}" + state="${aCOLOUR[weak]}No Services Failed${COLOUR_RESET}" else - state="${aCOLOUR[ALERT]}$FAILED_CNT Service(s) Failed${COLOUR_RESET}" + state="${aCOLOUR[alert]}$FAILED_CNT Service(s) Failed${COLOUR_RESET}" fi echo "$state" } @@ -506,21 +495,21 @@ $CLI_LINE" 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, check Fail2Ban state${COLOUR_RESET}" + state="${aCOLOUR[highlight]}No IP(s) Banned, check Fail2Ban state${COLOUR_RESET}" else if [[ $count_banned_ips -gt 20 ]] then # high number of bans - state="${aCOLOUR[ALERT]}$count_banned_ips IP(s) Banned${COLOUR_RESET}" + state="${aCOLOUR[alert]}$count_banned_ips IP(s) Banned${COLOUR_RESET}" else # low number of bans - state="${aCOLOUR[WEAK]}$count_banned_ips IP(s) Banned${COLOUR_RESET}" + state="${aCOLOUR[weak]}$count_banned_ips IP(s) Banned${COLOUR_RESET}" fi fi ;; - 1) 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, code: $code count: $count_banned_ips${COLOUR_RESET}";; + 1) 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, code: $code count: $count_banned_ips${COLOUR_RESET}";; esac echo "$state" @@ -615,7 +604,7 @@ $CLI_LINE" [[ "$ip_addr_cidr" ]] && ip_addr=${ip_addr_cidr%%/*} [[ $RX == 'N/A' && $TX == 'N/A' ]] && continue - 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" + 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" done } @@ -637,19 +626,19 @@ $CLI_LINE" # # Extract just the IP portion (use BASH_REMATCH[0]). Removes the geographic info. # IP="${BASH_REMATCH[0]}" - printf ' %s%s [%s]%s\n' "${aCOLOUR[ALT]}" "$IP" "$display_ns" "$COLOUR_RESET" + printf ' %s%s [%s]%s\n' "${aCOLOUR[alt]}" "$IP" "$display_ns" "$COLOUR_RESET" elif [[ ! $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%s%s\n' "${aCOLOUR[alert]}" 'Disconnected' "$CLI_SEPARATOR" "${aCOLOUR[strong]}" "$display_ns" "$COLOUR_RESET" else # Treat known curl/network error messages as timeout/DNS failures [[ $IP =~ $err_re ]] && IP='Timeout/DNS Failure' # 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" + printf ' %s%s [%s]%s\n' "${aCOLOUR[highlight]}" "$IP" "$display_ns" "$COLOUR_RESET" fi DATA_USAGE "$ns" @@ -665,7 +654,7 @@ $CLI_LINE" status=$? if (( $status )) then - printf '%b\n' "${aCOLOUR[ALERT]}Error executing aCUSTOM_COMMANDS[$i] Exit Code: ${status}${COLOUR_RESET}" + printf '%b\n' "${aCOLOUR[alert]}Error executing aCUSTOM_COMMANDS[$i] Exit Code: ${status}${COLOUR_RESET}" else printf '%b\n' "$output" fi @@ -773,7 +762,7 @@ $CLI_LINE" fi # Print aligned columns: {basename} : {used} of {size} ({percent used}) - printf "%s %s%-${length}s %s %s%6b %s %s%8sGiB of %8sGiB%s\n" "$CLI_BULLET" "${aCOLOUR[ALT]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[STRONG]}" "$perc" "$CLI_SEPARATOR" "${aCOLOUR[WEAK]}" "$used_gb" "$size_gb" "$COLOUR_RESET" + printf "%s %s%-${length}s %s %s%6b %s %s%8sGiB of %8sGiB%s\n" "$CLI_BULLET" "${aCOLOUR[alt]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[strong]}" "$perc" "$CLI_SEPARATOR" "${aCOLOUR[weak]}" "$used_gb" "$size_gb" "$COLOUR_RESET" done } @@ -782,9 +771,9 @@ $CLI_LINE" { local state=$(/boot/dietpi/dietpi-vpn status 2>&1) case $state in - Connected*) state="${aCOLOUR[WEAK]}$state${COLOUR_RESET}";; - Disconnected*) state="${aCOLOUR[ALERT]}$state${COLOUR_RESET}";; - *) state="${aCOLOUR[HIGHLIGHT]}$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" } @@ -819,9 +808,9 @@ $CLI_LINE" # DietPi-VPN connection status (( ${aENABLED[vpn_status]} )) && Print_Item_State "${aDESCRIPTION[vpn_status]}" "$(Get_VPN_Status 2>&1)" # Disk usage (RootFS) - (( ${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)" + (( ${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[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)" + (( ${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[weather]} )) && Print_Item_State "${aDESCRIPTION[weather]}" "$(curl -sSfLm 3 'https://wttr.in/?format=4' 2>&1)" # Let's Encrypt cert status @@ -859,7 +848,7 @@ $CLI_LINE" (( ${aENABLED[useful_commands]} )) && Print_Useful_Commands # Notify user if prefs or colors were migrated from legacy numeric indices during this run - (( $DIETPI_BANNER_MIGRATED_DURING_RUN || $DIETPI_BANNER_COLOUR_MIGRATED_DURING_RUN )) && echo -e " ${aCOLOUR[HIGHLIGHT]}Banner needs to be migrated, run dietpi-banner and validate your settings.$COLOUR_RESET" + (( $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() @@ -878,20 +867,20 @@ $CLI_LINE" Menu_Main() { G_WHIP_CHECKLIST_ARRAY=() - for key in "${MENU_ITEMS[@]}" + for k in "${MENU_ITEMS[@]}" do local state='off' - (( ${aENABLED[$key]:=0} == 1 )) && state='on' - G_WHIP_CHECKLIST_ARRAY+=("$key" "${aDESCRIPTION[$key]}" "$state") + (( ${aENABLED[$k]:=0} == 1 )) && state='on' + G_WHIP_CHECKLIST_ARRAY+=("$k" "${aDESCRIPTION[$k]}" "$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[@]}" + for k in "${MENU_ITEMS[@]}" do - aENABLED[$key]=0 + aENABLED[$k]=0 done for selection in $G_WHIP_RETURNED_VALUE @@ -902,7 +891,7 @@ $CLI_LINE" then # 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"' + 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 : @@ -941,7 +930,7 @@ $CLI_LINE" done # If no commands are configured, disable the custom commands option - (( ${#aCUSTOM_COMMANDS[@]} == 0 )) && aENABLED[custom_commands]=0 + (( ${#aCUSTOM_COMMANDS[@]} )) || aENABLED[custom_commands]=0 elif [[ $selection == 'word_wrap' ]] then @@ -1010,9 +999,9 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" # 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 || $DIETPI_BANNER_COLOUR_MIGRATED_DURING_RUN == 1 ) ]] + if [[ -f $FP_SAVEFILE && $DIETPI_BANNER_MIGRATED_DURING_RUN == 1 ]] then - backup="$FP_SAVEFILE.bak-$(date -Iseconds)" + local backup="$FP_SAVEFILE.bak-$(date -Iseconds)" cp -a "$FP_SAVEFILE" "$backup" fi From 78d815c940d8d7a18a69c7fa4fee2958d1b9f46c Mon Sep 17 00:00:00 2001 From: MichaIng Date: Tue, 7 Jul 2026 17:42:12 +0200 Subject: [PATCH 26/39] dietpi-banner: some more alignment dietpi-globals: revert changes to G_GET_WAN_IP until we found a sane solution for namespaces with sudo, and since the banner does not currently use it. Align coding in G_TRUNCATE_MID --- dietpi/func/dietpi-banner | 60 +++++++++++++++++++++----------------- dietpi/func/dietpi-globals | 55 ++++++++++++++++------------------ 2 files changed, 58 insertions(+), 57 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index ae4ab8fed0..dcf1975c17 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -133,8 +133,9 @@ { # Migrate legacy numeric-indexed aENABLED (from older FP_SAVEFILE) to associative form local k legacy_indexed_enabled_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 + 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 ) for k in "${!aENABLED[@]}" do @@ -356,7 +357,7 @@ $CLI_LINE" if [[ $EUID == 0 ]] then # Running as root - stat=$(Get_Cert_Status_Raw 2>/dev/null) + stat=$(Get_Cert_Status_Raw 2> /dev/null) code=$? else # Running as non-root: Fail silently without NOPASSWD to avoid password prompt @@ -454,17 +455,17 @@ $CLI_LINE" { Get_Fail2Ban_Status_Raw() { - if command -v fail2ban-client >/dev/null 2>&1 + if command -v fail2ban-client > /dev/null then if [[ $EUID == 0 ]] then - f2b_output="$(fail2ban-client banned 2>/dev/null)" + f2b_output="$(fail2ban-client banned 2> /dev/null)" else - f2b_output="$(sudo -n fail2ban-client banned 2>/dev/null || return 2)" + f2b_output="$(sudo -n fail2ban-client banned 2> /dev/null || return 2)" [[ $? == 2 ]] && return 2 fi - if ! command -v sort >/dev/null 2>&1 + if ! command -v sort > /dev/null 2>&1 then sort() { cat; } # fallback if sort is not available, simply passes input through fi @@ -517,10 +518,10 @@ $CLI_LINE" # Print public IP address and location info # - Optional arguments: - # -t : Set timeout in seconds, supports floats, default: 3 + # -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() + # network namespace, default: empty (no namespace = the main default namespace) + Get_WAN_IP() { # Defaults local timeout=3 @@ -569,7 +570,7 @@ $CLI_LINE" 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 | mawk -F': ' '{print $2}') + mapfile -t observed_ifaces < <("${ns_cmd_prefix[@]}" ip -o link show 2> /dev/null | mawk -F': ' '{print $2}') for IFACE in "${observed_ifaces[@]}" do @@ -591,17 +592,17 @@ $CLI_LINE" local RX='N/A' TX='N/A' rx tx ip_addr ip_addr_cidr # Get RX data - rx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/rx_bytes" 2>/dev/null) + rx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/rx_bytes" 2> /dev/null) [[ $rx =~ ^[0-9]+$ ]] && RX="$(mawk -v v="$rx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" # Get TX data - tx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/tx_bytes" 2>/dev/null) + tx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/tx_bytes" 2> /dev/null) [[ $tx =~ ^[0-9]+$ ]] && TX="$(mawk -v v="$tx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" # 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 | mawk '{print $4}' | head -n1 | grep -E "$IP4_re_cidr") - [[ "$ip_addr_cidr" ]] && ip_addr=${ip_addr_cidr%%/*} + ip_addr_cidr=$("${ns_cmd_prefix[@]}" ip -o -4 addr show dev "$ifname" scope global 2> /dev/null | mawk '{print $4}' | head -1 | grep -E "$IP4_re_cidr") + [[ $ip_addr_cidr ]] && ip_addr=${ip_addr_cidr%%/*} [[ $RX == 'N/A' && $TX == 'N/A' ]] && continue 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" @@ -617,14 +618,14 @@ $CLI_LINE" raw_ns=${ns:-default} display_ns=$(G_TRUNCATE_MID "$raw_ns" 24) - IP=$(BANNER_GET_WAN_IP -t 1 -n "$ns" 2>&1) + IP=$(Get_WAN_IP -t 1 -n "$ns" 2>&1) 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. - # IP="${BASH_REMATCH[0]}" + # Extract just the IP portion (use BASH_REMATCH[0]). Removes the geographic info. + #IP=${BASH_REMATCH[0]} printf ' %s%s [%s]%s\n' "${aCOLOUR[alt]}" "$IP" "$display_ns" "$COLOUR_RESET" @@ -804,7 +805,7 @@ $CLI_LINE" # LAN IP (( ${aENABLED[lan_ip]} )) && Print_Item_State "${aDESCRIPTION[lan_ip]}" "$(Get_Local_Ip 2>&1)" # WAN IP + location info - (( ${aENABLED[wan_ip]} )) && Print_Item_State "${aDESCRIPTION[wan_ip]}" "$(BANNER_GET_WAN_IP 2>&1)" + (( ${aENABLED[wan_ip]} )) && Print_Item_State "${aDESCRIPTION[wan_ip]}" "$(Get_WAN_IP 2>&1)" # DietPi-VPN connection status (( ${aENABLED[vpn_status]} )) && Print_Item_State "${aDESCRIPTION[vpn_status]}" "$(Get_VPN_Status 2>&1)" # Disk usage (RootFS) @@ -896,12 +897,17 @@ $CLI_LINE" # Manage custom commands list while : 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_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\n!CAUTION!\nThese commands are executed with the same privileges as dietpi-banner, and DO NOT have a time limit :' || 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 + G_WHIP_INPUTBOX "Enter new command, for example:\n$example_command" || continue aCUSTOM_COMMANDS+=("$G_WHIP_RETURNED_VALUE") ;; Edit) @@ -911,7 +917,7 @@ $CLI_LINE" G_WHIP_MENU 'Select command to edit:' || continue sel=$G_WHIP_RETURNED_VALUE G_WHIP_DEFAULT_ITEM=${aCUSTOM_COMMANDS[$sel]} - G_WHIP_INPUTBOX "Edit command, for example :\n$example_command" || continue + G_WHIP_INPUTBOX "Edit command, for example:\n$example_command" || continue aCUSTOM_COMMANDS[$sel]="$G_WHIP_RETURNED_VALUE" ;; Remove) @@ -964,24 +970,24 @@ For example: '/mnt/*' or 're:^/mnt/.*/*$'" 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 + G_WHIP_INPUTBOX "Enter new pattern:\n$pattern_hint" || continue aDISK_SPACE_PATTERNS+=("$G_WHIP_RETURNED_VALUE") ;; Edit) (( ${#aDISK_SPACE_PATTERNS[@]} == 0 )) && { G_WHIP_MSG 'No patterns to edit'; continue; } G_WHIP_MENU_ARRAY=() 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 + G_WHIP_MENU 'Select pattern to edit:' || continue sel=$G_WHIP_RETURNED_VALUE G_WHIP_DEFAULT_ITEM=${aDISK_SPACE_PATTERNS[$sel]} - G_WHIP_INPUTBOX "Edit pattern :\n$pattern_hint" || continue + G_WHIP_INPUTBOX "Edit pattern:\n$pattern_hint" || continue aDISK_SPACE_PATTERNS[$sel]="$G_WHIP_RETURNED_VALUE" ;; Remove) (( ${#aDISK_SPACE_PATTERNS[@]} == 0 )) && { G_WHIP_MSG 'No patterns to remove'; continue; } G_WHIP_MENU_ARRAY=() 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 + G_WHIP_MENU 'Select pattern to remove:' || continue sel=$G_WHIP_RETURNED_VALUE unset 'aDISK_SPACE_PATTERNS[$sel]' # reindex array diff --git a/dietpi/func/dietpi-globals b/dietpi/func/dietpi-globals index e85fb08620..683853155e 100644 --- a/dietpi/func/dietpi-globals +++ b/dietpi/func/dietpi-globals @@ -176,25 +176,28 @@ $(ps f -eo pid,user,tty,cmd | grep -i 'dietpi')" && continue # - 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} - 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}" + # - `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 max=$2 + local len=${#s} + if (( $max < $len )) + then + echo "$s" + return 0 + + elif (( $max < 4 )) + then + echo "${s:0:$max}" + return 0 + fi + local keep=$(( $max - 3 )) + local pre=$(( ( $keep + 1 ) / 2 )) + local suf=$(( $keep / 2 )) + local start=$(( $len - $suf )) + echo "${s:0:pre}...${s:start}" } # DietPi-Notify @@ -1541,29 +1544,21 @@ 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:-}";; + '-t') shift; (( ${1/.} )) && timeout=$1 || { G_DIETPI-NOTIFY 1 "Invalid timeout \"$1\", aborting..."; return 1; };; *) G_DIETPI-NOTIFY 1 "Invalid argument \"$1\", aborting..."; return 1;; esac shift done - - if [[ -n "$ns" ]]; then - ip netns exec "$ns" curl -sSfLm "$timeout" 'https://dietpi.com/geoip' 2>&1 - else - curl -sSfLm "$timeout" 'https://dietpi.com/geoip' 2>&1 - fi + curl -sSfLm "$timeout" 'https://dietpi.com/geoip' } # $1 = directory to test permissions support From 8bc7a53908159d3ffe507b3ca9e58ce775137b54 Mon Sep 17 00:00:00 2001 From: MichaIng Date: Tue, 7 Jul 2026 18:47:17 +0200 Subject: [PATCH 27/39] dietpi-globals: simplify G_WHIP_CHECKLIST_ENUM handling --- dietpi/func/dietpi-banner | 1 - dietpi/func/dietpi-globals | 50 +++++++++++++++----------------------- 2 files changed, 19 insertions(+), 32 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index dcf1975c17..4aff02b989 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -877,7 +877,6 @@ $CLI_LINE" 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 k in "${MENU_ITEMS[@]}" do diff --git a/dietpi/func/dietpi-globals b/dietpi/func/dietpi-globals index 683853155e..781eeddfaf 100644 --- a/dietpi/func/dietpi-globals +++ b/dietpi/func/dietpi-globals @@ -489,12 +489,12 @@ $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 + # - 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 # G_WHIP_DESTROY | Clear vars after run of whiptail - G_WHIP_DESTROY(){ unset -v G_WHIP_DEFAULT_ITEM G_WHIP_SIZE_X_MAX G_WHIP_BUTTON_OK_TEXT G_WHIP_BUTTON_CANCEL_TEXT G_WHIP_NOCANCEL G_WHIP_MENU_ARRAY G_WHIP_CHECKLIST_ARRAY G_WHIP_INPUTBOX_REGEX G_WHIP_INPUTBOX_REGEX_TEXT; } + G_WHIP_DESTROY(){ unset -v G_WHIP_DEFAULT_ITEM G_WHIP_SIZE_X_MAX G_WHIP_BUTTON_OK_TEXT G_WHIP_BUTTON_CANCEL_TEXT G_WHIP_NOCANCEL G_WHIP_MENU_ARRAY G_WHIP_CHECKLIST_ARRAY G_WHIP_INPUTBOX_REGEX G_WHIP_INPUTBOX_REGEX_TEXT G_WHIP_CHECKLIST_ENUM; } # Run once, to be failsafe in case any exported/environment variables are left from originating shell G_WHIP_DESTROY @@ -835,7 +835,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_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 @@ -847,26 +847,17 @@ $grey───────────────────────── [[ $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 ]] + # If G_WHIP_CHECKLIST_ENUM=1 is set, map and replace original tags and default with numeric ones + if (( $G_WHIP_CHECKLIST_ENUM )) 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++)) + local map=() idx=0 + for ((i=0;i<${#G_WHIP_CHECKLIST_ARRAY[@]};i+=3)) + do + [[ $G_WHIP_DEFAULT_ITEM == "${G_WHIP_CHECKLIST_ARRAY[$i]}" ]] && G_WHIP_DEFAULT_ITEM=$idx + map[$idx]=${G_WHIP_CHECKLIST_ARRAY[$i]} + G_WHIP_CHECKLIST_ARRAY[$i]=$idx + ((idx++)) done - G_WHIP_CHECKLIST_ARRAY=("${__tmp[@]}") fi G_WHIP_INIT 3 @@ -874,18 +865,15 @@ $grey───────────────────────── 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" ]] + # If G_WHIP_CHECKLIST_ENUM=1 is set, replace numeric selections back to original tags + if (( $G_WHIP_CHECKLIST_ENUM )) then - local sel mapped=() - for sel in $G_WHIP_RETURNED_VALUE; do - mapped+=("${__map[$sel]}") + local mapped=() + for idx in $G_WHIP_RETURNED_VALUE + do + mapped+=("${map[$idx]}") 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 + G_WHIP_RETURNED_VALUE=${mapped[*]} fi G_WHIP_RETURNED_VALUE=$(echo -e "$G_WHIP_RETURNED_VALUE" | tr '\n' ' ') From a248de407e7e848eb4facd42205695401b8bc4a6 Mon Sep 17 00:00:00 2001 From: timjolson Date: Tue, 7 Jul 2026 22:55:53 -0500 Subject: [PATCH 28/39] fixed truncate length comparison and improved the output for shorter max lengths --- dietpi/func/dietpi-globals | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dietpi/func/dietpi-globals b/dietpi/func/dietpi-globals index 781eeddfaf..1e29695d06 100644 --- a/dietpi/func/dietpi-globals +++ b/dietpi/func/dietpi-globals @@ -183,12 +183,12 @@ $(ps f -eo pid,user,tty,cmd | grep -i 'dietpi')" && continue { local s=$1 max=$2 local len=${#s} - if (( $max < $len )) + if (( $max > $len )) then echo "$s" return 0 - elif (( $max < 4 )) + elif (( $max < 7 )) then echo "${s:0:$max}" return 0 From 4850cfff34e4b225d074b7bf442f0ea3c628c529 Mon Sep 17 00:00:00 2001 From: timjolson Date: Tue, 7 Jul 2026 22:57:18 -0500 Subject: [PATCH 29/39] cleaned up netns data and reduced sudo usage --- dietpi/func/dietpi-banner | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 4aff02b989..8a280c3ea0 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -590,13 +590,35 @@ $CLI_LINE" esac local RX='N/A' TX='N/A' rx tx ip_addr ip_addr_cidr + local rx_path="/sys/class/net/$ifname/statistics/rx_bytes" + local tx_path="/sys/class/net/$ifname/statistics/tx_bytes" - # Get RX data - rx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/rx_bytes" 2> /dev/null) - [[ $rx =~ ^[0-9]+$ ]] && RX="$(mawk -v v="$rx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" + if (( EUID != 0 )) && [ -z "$netns" ]; + then + # If we are not root and no netns is specified, check if we can read + # the RX/TX statistics files directly, otherwise fall back to sudo + if [ -r "$rx_path" ]; + then + rx=$(cat "$rx_path" 2> /dev/null) + else + # If we are not root and no netns is specified, use sudo + rx=$(sudo -n cat "$rx_path" 2> /dev/null) + fi + + if [ -r "$tx_path" ]; + then + tx=$(cat "$tx_path" 2> /dev/null) + else + tx=$(sudo -n cat "$tx_path" 2> /dev/null) + fi + else + # If we are root or a netns is specified, use the prefix to read the RX/TX statistics files + rx=$("${ns_cmd_prefix[@]}" cat "$rx_path" 2> /dev/null) + tx=$("${ns_cmd_prefix[@]}" cat "$tx_path" 2> /dev/null) + fi - # Get TX data - tx=$("${ns_cmd_prefix[@]}" cat "/sys/class/net/$ifname/statistics/tx_bytes" 2> /dev/null) + # Process the RX/TX values to convert to GiB if they are numeric, otherwise leave as 'N/A' + [[ $rx =~ ^[0-9]+$ ]] && RX="$(mawk -v v="$rx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" [[ $tx =~ ^[0-9]+$ ]] && TX="$(mawk -v v="$tx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" # Get IP address (IPv4) for this interface in this namespace. If multiple are present, just take the first. If none, show "- No IP -" From 284d4c7dd5bc1f558567efd4b4c0c2f9cf7a4051 Mon Sep 17 00:00:00 2001 From: MichaIng Date: Thu, 9 Jul 2026 18:10:40 +0200 Subject: [PATCH 30/39] dietpi-banner: some fixes and changes * restore dash-compatibility of Get_Cert_Status_Raw * cleanup Get_Cert_Status: `sudo -n` returns the exit code "1" if sudo permissions are missing, the "no cert found" error code would hence need to swap. However, there is already the exit code 0 handling based on the "No certificate found" output, so make us of that instead. If Get_Cert_Status_Raw runs at all, exit code 0 is now always returned, hence forge banner line based on its output. In case of another exit code, it must have come from a sudo failure, hence print the error about NOPASSWD sudo. * Move `Print_*` functions below `Get_*` functions, and rename `Get_*` functions which are supposed to print the formatted output to console directly to `Print_*` * Remove trailing COLOUR_RESET where not required * Simplify Get_Fail2Ban_Status: process `fail2ban-client` output with a single awk to avoid the large amount of external command calls, and print it directly. * create variables as local ones where appropriate * Simplify Print_Network_Usage: The `/sys/class/net/*/statistics` is always world-readable (unless the kernel is manipulated). Skip related checks, and always read it directly if no namespace is used, using the cheaper builtin `read` instead of external `cat` call. Tailore Get_WAN_IP. Parse simpler `ip -br` output instead of `ip -o`. Print curl errors as is. They should be short with `-sSf`, and leaving them untouched helps with debugging. --- dietpi/func/dietpi-banner | 348 +++++++++++++++---------------------- dietpi/func/dietpi-globals | 2 +- 2 files changed, 145 insertions(+), 205 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 8a280c3ea0..fb68e048d5 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -184,7 +184,6 @@ # 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 string DIETPI_VERSION="$G_DIETPI_VERSION_CORE.$G_DIETPI_VERSION_SUB.$G_DIETPI_VERSION_RC" @@ -339,25 +338,23 @@ $CLI_LINE" 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 + # Keep it dash-compatible 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 + [ -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 + echo 'No certificate found' } - stat= - code=0 + local stat code if [[ $EUID == 0 ]] then # Running as root - stat=$(Get_Cert_Status_Raw 2> /dev/null) + stat=$(Get_Cert_Status_Raw 2>&1) code=$? else # Running as non-root: Fail silently without NOPASSWD to avoid password prompt @@ -367,88 +364,32 @@ $CLI_LINE" case $code in 0) - # stat is valid + # Get_Cert_Status_Raw executed, process output case $stat in - Valid*) stat="${aCOLOUR[weak]}$stat${COLOUR_RESET}";; - No\ certificate*) stat="${aCOLOUR[alert]}$stat${COLOUR_RESET}";; - *) stat="${aCOLOUR[highlight]}$stat${COLOUR_RESET}";; + 'Valid until'*) stat="${aCOLOUR[weak]}$stat";; # valid cert, print line with weak color + 'No certificate found') stat="${aCOLOUR[highlight]}$stat";; # highlight if there is no cert + *) stat="${aCOLOUR[alert]}$stat";; # alert if cert expired or there was another error esac ;; - 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}";; + *) + # sudo failed, print meaningful error message + stat="${aCOLOUR[highlight]}NOPASSWD sudo required to obtain cert status" + ;; esac echo "$stat" } - Print_Credits() - { - 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 - - echo -e " Patreon Legends : ADSB.im, Oliver C. Radke - Website : https://dietpi.com/ | https://x.com/DietPi_ | Bsky: @dietpi.com - Contribute : https://dietpi.com/contribute.html - Web Hosting by : https://login-online.com/$COLOUR_RESET\n" - } - - Print_Updates() - { - # DietPi update available? - if [[ $AVAILABLE_UPDATE ]] - then - 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[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[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[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[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() - { - # 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[strong]} - printf "%b%b %-${width}b %b %b\n" "$CLI_BULLET" "${color}" "$subtitle" "$CLI_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) if [[ ! $FAILED_CNT || $FAILED_CNT == 0 ]] then - state="${aCOLOUR[weak]}No Services Failed${COLOUR_RESET}" + echo "${aCOLOUR[weak]}No services failed" else - state="${aCOLOUR[alert]}$FAILED_CNT Service(s) Failed${COLOUR_RESET}" + echo "${aCOLOUR[alert]}$FAILED_CNT service(s) failed" fi - echo "$state" } Get_Fail2Ban_Status() @@ -459,28 +400,10 @@ $CLI_LINE" then if [[ $EUID == 0 ]] then - f2b_output="$(fail2ban-client banned 2> /dev/null)" + fail2ban-client banned 2> /dev/null | mawk -F\' -v ip="^$IP4_re$" '{for(i=2;i /dev/null | mawk -F\' -v ip="^$IP4_re$" '{for(i=2;i /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)" - if [[ ! "$IP_addrs" ]] - then - IP_addrs= - count_banned_ips=0 - else - count_banned_ips="$(echo "$IP_addrs" | wc -l)" # | grep -cve '^[[:space:]]*$')" - fi - echo "$count_banned_ips" - return 0 else # fail2ban-client command is not available @@ -488,73 +411,53 @@ $CLI_LINE" fi } + local count_banned_ips count_banned_ips=$(Get_Fail2Ban_Status_Raw) code=$? case $code in 0) - if [[ $count_banned_ips -eq 0 ]] + if (( $count_banned_ips == 0 )) then # zero bans can be suspicious - something may not be working correctly - state="${aCOLOUR[highlight]}No IP(s) Banned, check Fail2Ban state${COLOUR_RESET}" + state="${aCOLOUR[highlight]}No IPs banned, check Fail2Ban state" + + elif (( $count_banned_ips > 20 )) + then + # high number of bans + state="${aCOLOUR[alert]}$count_banned_ips IP banned" else - if [[ $count_banned_ips -gt 20 ]] - then - # high number of bans - state="${aCOLOUR[alert]}$count_banned_ips IP(s) Banned${COLOUR_RESET}" - else - # low number of bans - state="${aCOLOUR[weak]}$count_banned_ips IP(s) Banned${COLOUR_RESET}" - fi + # low number of bans + state="${aCOLOUR[weak]}$count_banned_ips IP(s) banned" fi ;; - 1) 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, code: $code count: $count_banned_ips${COLOUR_RESET}";; + 1) state="${aCOLOUR[alert]}fail2ban-client not available";; + 2) state="${aCOLOUR[highlight]}NOPASSWD sudo required to obtain Fail2Ban status";; + *) state="${aCOLOUR[alert]}Unknown state, code: $code count: $count_banned_ips";; esac 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) - Get_WAN_IP() + Print_Network_Usage() { - # Defaults - local timeout=3 - 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; netns="${1:-}";; - *) G_DIETPI-NOTIFY 1 "Invalid argument \"$1\", aborting..."; return 1;; - esac - shift - done - - local ns_cmd_prefix=() - if [[ $netns ]] - then - # add sudo if not root - (( $EUID )) && ns_cmd_prefix+=(sudo -n) - # add netns prefix - ns_cmd_prefix+=(ip netns exec "$netns") - fi - "${ns_cmd_prefix[@]}" curl -sSfLm "$timeout" 'https://dietpi.com/geoip' 2>&1 - } - - Get_Network_Usage() - { - # 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' + # Get WAN IP for namespace $1 (default if empty) + Get_WAN_IP() + { + local ns_cmd_prefix=() + if [[ $1 ]] + then + # add sudo if not root + (( $EUID )) && ns_cmd_prefix+=(sudo -n) + # add netns prefix + ns_cmd_prefix+=(ip netns exec "$1") + fi + "${ns_cmd_prefix[@]}" curl -sSfLm 1 'https://dietpi.com/geoip' + } - DATA_USAGE() + # Print usage stats for namespace $1 (default if empty) + Print_Data_Usage() { local netns=$1 @@ -570,7 +473,7 @@ $CLI_LINE" 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 | mawk -F': ' '{print $2}') + mapfile -t observed_ifaces < <("${ns_cmd_prefix[@]}" ip -br link 2> /dev/null | mawk '{print $1}') for IFACE in "${observed_ifaces[@]}" do @@ -586,52 +489,38 @@ $CLI_LINE" wlan*|wl*) :;; # vpn/tunnel/virtual tun*|wg*|vpn*) :;; - *) continue ;; + *) continue;; esac - 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=' - No IP - ' ip_addr_cidr local rx_path="/sys/class/net/$ifname/statistics/rx_bytes" local tx_path="/sys/class/net/$ifname/statistics/tx_bytes" - if (( EUID != 0 )) && [ -z "$netns" ]; + if [[ $netns ]]; then - # If we are not root and no netns is specified, check if we can read - # the RX/TX statistics files directly, otherwise fall back to sudo - if [ -r "$rx_path" ]; - then - rx=$(cat "$rx_path" 2> /dev/null) - else - # If we are not root and no netns is specified, use sudo - rx=$(sudo -n cat "$rx_path" 2> /dev/null) - fi - - if [ -r "$tx_path" ]; - then - tx=$(cat "$tx_path" 2> /dev/null) - else - tx=$(sudo -n cat "$tx_path" 2> /dev/null) - fi - else - # If we are root or a netns is specified, use the prefix to read the RX/TX statistics files + # If a netns is specified, use the prefix to read the RX/TX statistics files. rx=$("${ns_cmd_prefix[@]}" cat "$rx_path" 2> /dev/null) tx=$("${ns_cmd_prefix[@]}" cat "$tx_path" 2> /dev/null) + else + # Else, read the RX/TX statistics files directly. + read -r rx < "$rx_path" + read -r tx < "$tx_path" fi # Process the RX/TX values to convert to GiB if they are numeric, otherwise leave as 'N/A' [[ $rx =~ ^[0-9]+$ ]] && RX="$(mawk -v v="$rx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" [[ $tx =~ ^[0-9]+$ ]] && TX="$(mawk -v v="$tx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" - # 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 | mawk '{print $4}' | head -1 | grep -E "$IP4_re_cidr") + # Get first IPv4 address for this interface in this namespace. If none, leave "- No IP -". + ip_addr_cidr=$("${ns_cmd_prefix[@]}" ip -br -4 addr show dev "$ifname" scope global 2> /dev/null | mawk -v re="^$IP4_re/[0-9]{1,2}$" '$3 ~ re {print $3;exit}') [[ $ip_addr_cidr ]] && ip_addr=${ip_addr_cidr%%/*} [[ $RX == 'N/A' && $TX == 'N/A' ]] && continue - 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" + echo -e "$CLI_BULLET ${aCOLOUR[weak]}$ip_addr ($ifname) $CLI_SEPARATOR ${aCOLOUR[weak]}TX=$TX RX=$RX" done } - mapfile -t namespaces < <(ip netns ls | mawk 'NF>1 { print $1 }') + mapfile -t namespaces < <(ip netns ls | mawk 'NF>1 {print $1}') # Prepend an empty entry so the default (root) namespace can be handled in the loop namespaces=('' "${namespaces[@]}") @@ -640,51 +529,32 @@ $CLI_LINE" raw_ns=${ns:-default} display_ns=$(G_TRUNCATE_MID "$raw_ns" 24) - IP=$(Get_WAN_IP -t 1 -n "$ns" 2>&1) + IP=$(Get_WAN_IP "$ns" 2>&1) - if [[ $IP =~ $IP4_re ]] + 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. #IP=${BASH_REMATCH[0]} - printf ' %s%s [%s]%s\n' "${aCOLOUR[alt]}" "$IP" "$display_ns" "$COLOUR_RESET" + echo -e "${aCOLOUR[alt]} $IP [$display_ns]" elif [[ ! $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 is empty, it likely means the namespace is disconnected or has no Internet access. Print status message. + echo -e "${aCOLOUR[alert]} Disconnected $CLI_SEPARATOR ${aCOLOUR[strong]}$display_ns" else - # Treat known curl/network error messages as timeout/DNS failures - [[ $IP =~ $err_re ]] && IP='Timeout/DNS Failure' - - # 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" + # Print curl errors as is + echo -e "${aCOLOUR[highlight]} $IP [$display_ns]" fi - DATA_USAGE "$ns" + Print_Data_Usage "$ns" done } - Print_Custom_Commands() - { - for i in "${!aCUSTOM_COMMANDS[@]}" - do - [[ ${aCUSTOM_COMMANDS[$i]} ]] || continue - output=$(eval "${aCUSTOM_COMMANDS[$i]}" 2>&1) - status=$? - if (( $status )) - then - printf '%b\n' "${aCOLOUR[alert]}Error executing aCUSTOM_COMMANDS[$i] Exit Code: ${status}${COLOUR_RESET}" - else - printf '%b\n' "$output" - fi - done - } - - Get_Disk_Usage() + Print_Disk_Usage() { # If no arguments are provided, default to '"/" "/mnt/*"'. (( $# )) && aDISK_SPACE_PATTERNS=("$@") @@ -801,6 +671,76 @@ $CLI_LINE" echo "$state" } + Print_Custom_Commands() + { + local output status + for i in "${!aCUSTOM_COMMANDS[@]}" + do + [[ ${aCUSTOM_COMMANDS[$i]} ]] || continue + output=$(eval "${aCUSTOM_COMMANDS[$i]}" 2>&1) + status=$? + if (( $status )) + then + echo -e "${aCOLOUR[alert]}Custom command $i failed ($status): $output" + else + echo -e "$output" + fi + done + } + + Print_Credits() + { + 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 + + echo -e " Patreon Legends : ADSB.im, Oliver C. Radke + Website : https://dietpi.com/ | https://x.com/DietPi_ | Bsky: @dietpi.com + Contribute : https://dietpi.com/contribute.html + Web Hosting by : https://login-online.com/$COLOUR_RESET\n" + } + + Print_Updates() + { + # DietPi update available? + if [[ $AVAILABLE_UPDATE ]] + then + 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[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[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[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[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() + { + # 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 state=$2 width=$3 + printf "%b%b %-${width}b %b %b\n" "$CLI_BULLET" "${aCOLOUR[strong]}" "$subtitle" "$CLI_SEPARATOR" "$state" + } + Print_Banner_raw() { Print_Header @@ -827,7 +767,7 @@ $CLI_LINE" # LAN IP (( ${aENABLED[lan_ip]} )) && Print_Item_State "${aDESCRIPTION[lan_ip]}" "$(Get_Local_Ip 2>&1)" # WAN IP + location info - (( ${aENABLED[wan_ip]} )) && Print_Item_State "${aDESCRIPTION[wan_ip]}" "$(Get_WAN_IP 2>&1)" + (( ${aENABLED[wan_ip]} )) && Print_Item_State "${aDESCRIPTION[wan_ip]}" "$(G_GET_WAN_IP 2>&1)" # DietPi-VPN connection status (( ${aENABLED[vpn_status]} )) && Print_Item_State "${aDESCRIPTION[vpn_status]}" "$(Get_VPN_Status 2>&1)" # Disk usage (RootFS) @@ -844,13 +784,13 @@ $CLI_LINE" (( ${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[network_usage]} )) && Print_Subheader "Network Traffic" && printf '%b\n' "$(Get_Network_Usage 2>&1)" - # Disk Usage - (( ${aENABLED[disk_usage]} )) && Print_Subheader "Disk Usage" && printf '%b\n' "$(Get_Disk_Usage "${aDISK_SPACE_PATTERNS[@]}" 2>&1)" + (( ${aENABLED[network_usage]} )) && Print_Subheader 'Network Traffic' && Print_Network_Usage 2>&1 + + # Disk usage + (( ${aENABLED[disk_usage]} )) && Print_Subheader 'Disk Usage' && Print_Disk_Usage "${aDISK_SPACE_PATTERNS[@]}" 2>&1 # Custom commands - (( ${aENABLED[custom_commands]} )) && Print_Subheader "Custom Commands" && Print_Custom_Commands + (( ${aENABLED[custom_commands]} )) && Print_Subheader 'Custom Commands' && Print_Custom_Commands # Separator line (( ${aENABLED[useful_commands]} || ${aENABLED[motd]} || ${aENABLED[print_credits]} )) && echo -e "$CLI_LINE" diff --git a/dietpi/func/dietpi-globals b/dietpi/func/dietpi-globals index 1e29695d06..f49becd7eb 100644 --- a/dietpi/func/dietpi-globals +++ b/dietpi/func/dietpi-globals @@ -183,7 +183,7 @@ $(ps f -eo pid,user,tty,cmd | grep -i 'dietpi')" && continue { local s=$1 max=$2 local len=${#s} - if (( $max > $len )) + if (( $max >= $len )) then echo "$s" return 0 From d0d588be1cf3e83f05ff7ba79fc5fdc7016ab67b Mon Sep 17 00:00:00 2001 From: MichaIng Date: Thu, 9 Jul 2026 19:01:05 +0200 Subject: [PATCH 31/39] dietpi-banner: shorten some option keys --- dietpi/func/dietpi-banner | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index fb68e048d5..83790f0680 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -55,11 +55,11 @@ [disk_userdata]='Disk usage (userdata)' [weather]='Weather (wttr.in)' [custom_commands]='Custom commands' - [useful_commands]='Display DietPi useful commands?' + [dietpi_commands]='Display useful DietPi commands?' [motd]='MOTD' [vpn_status]='VPN status' [large_hostname]='Large hostname' - [print_credits]='Print credits' + [credits]='Print credits' [letsencrypt]='Let'\''s Encrypt cert status' [ram_usage]='RAM usage' [load_average]='Load average' @@ -67,14 +67,14 @@ [kernel]='Kernel' [network_usage]='Network Usage' [disk_usage]='Disk Usage' - [systemd_status]='Systemd Status' - [fail2ban_status]='Fail2Ban Status' + [systemd]='Systemd Status' + [fail2ban]='Fail2Ban Status' ) # Set the array order for the menu, items must be listed here to show up 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 + lan_ip wan_ip vpn_status disk_rootfs disk_userdata weather letsencrypt systemd fail2ban + network_usage disk_usage custom_commands dietpi_commands motd credits word_wrap ) # Initialize aENABLED from aDESCRIPTION keys @@ -88,9 +88,9 @@ aENABLED[device_model]=1 aENABLED[lan_ip]=1 aENABLED[disk_rootfs]=1 - aENABLED[useful_commands]=1 + aENABLED[dietpi_commands]=1 aENABLED[motd]=1 - aENABLED[print_credits]=1 + aENABLED[credits]=1 # Enable CPU temp by default except in VMs (( $G_HW_MODEL != 20 )) && aENABLED[cpu_temp]=1 @@ -134,8 +134,8 @@ # Migrate legacy numeric-indexed aENABLED (from older FP_SAVEFILE) to associative form local k legacy_indexed_enabled_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 + weather custom_commands dietpi_commands motd vpn_status large_hostname credits letsencrypt + ram_usage load_average word_wrap kernel network_usage disk_usage systemd fail2ban ) for k in "${!aENABLED[@]}" do @@ -779,36 +779,32 @@ $CLI_LINE" # Let's Encrypt cert status (( ${aENABLED[letsencrypt]} )) && Print_Item_State "Let's Encrypt Cert" "$(Get_Cert_Status 2>&1)" # Systemd status - (( ${aENABLED[systemd_status]} )) && Print_Item_State "${aDESCRIPTION[systemd_status]}" "$(Get_Systemd_Status 2>&1)" + (( ${aENABLED[systemd]} )) && Print_Item_State "${aDESCRIPTION[systemd]}" "$(Get_Systemd_Status 2>&1)" # Fail2Ban status - (( ${aENABLED[fail2ban_status]} )) && Print_Item_State "${aDESCRIPTION[fail2ban_status]}" "$(Get_Fail2Ban_Status 2>&1)" - + (( ${aENABLED[fail2ban]} )) && Print_Item_State "${aDESCRIPTION[fail2ban]}" "$(Get_Fail2Ban_Status 2>&1)" # Network usage by namespace (( ${aENABLED[network_usage]} )) && Print_Subheader 'Network Traffic' && Print_Network_Usage 2>&1 - # Disk usage (( ${aENABLED[disk_usage]} )) && Print_Subheader 'Disk Usage' && Print_Disk_Usage "${aDISK_SPACE_PATTERNS[@]}" 2>&1 - # Custom commands (( ${aENABLED[custom_commands]} )) && Print_Subheader 'Custom Commands' && Print_Custom_Commands # Separator line - (( ${aENABLED[useful_commands]} || ${aENABLED[motd]} || ${aENABLED[print_credits]} )) && echo -e "$CLI_LINE" + (( ${aENABLED[dietpi_commands]} || ${aENABLED[motd]} || ${aENABLED[credits]} )) && echo -e "$CLI_LINE" # MOTD 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[motd]}" "$motd" fi echo -e "$CLI_LINE\n" - (( ${aENABLED[print_credits]} )) && Print_Credits + (( ${aENABLED[credits]} )) && Print_Credits Print_Updates - (( ${aENABLED[useful_commands]} )) && Print_Useful_Commands + (( ${aENABLED[dietpi_commands]} )) && Print_Useful_Commands # Notify user if prefs or colors 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" From f45fca59318831761daf45b7879bec67b0176334 Mon Sep 17 00:00:00 2001 From: MichaIng Date: Thu, 9 Jul 2026 19:39:37 +0200 Subject: [PATCH 32/39] dietpi-banner: fix disk usage and align new item descriptions --- dietpi/func/dietpi-banner | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 83790f0680..e6416ac209 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -65,10 +65,10 @@ [load_average]='Load average' [word_wrap]='Word-wrap lines on small screens' [kernel]='Kernel' - [network_usage]='Network Usage' - [disk_usage]='Disk Usage' - [systemd]='Systemd Status' - [fail2ban]='Fail2Ban Status' + [network_usage]='Network usage' + [disk_usage]='Disk usage' + [systemd]='systemd status' + [fail2ban]='Fail2Ban status' ) # Set the array order for the menu, items must be listed here to show up MENU_ITEMS=( @@ -561,8 +561,7 @@ $CLI_LINE" # 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=() + local length=0 results=() size_kb used_kb avail_kb mnt_target name matched basename while read -r size_kb used_kb avail_kb mnt_target do matched=0 @@ -598,15 +597,15 @@ $CLI_LINE" (( $len > $length )) && length=$len results+=("$size_kb|$used_kb|$avail_kb|$mnt_target|$basename") fi - done < <(df -k --output=size,used,avail,target | tail -n +2) + done < <(df -k --output=size,used,avail,target | tail +2) - # Print results with aligned columns, converting kB to GB and calculating percentage used + # Print results with aligned columns, converting KiB to GiB and calculating percentage used for entry in "${results[@]}" do IFS='|' read -r size_kb used_kb avail_kb mnt_target name <<< "$entry" local values_count=0 - # Do all math in kB, convert to GB for display and round to 1 decimal + # Do all math in KiB, convert to GiB for display and round to 1 decimal if [[ $size_kb =~ ^[0-9]+$ ]] then ((values_count++)) @@ -655,9 +654,8 @@ $CLI_LINE" fi # Print aligned columns: {basename} : {used} of {size} ({percent used}) - printf "%s %s%-${length}s %s %s%6b %s %s%8sGiB of %8sGiB%s\n" "$CLI_BULLET" "${aCOLOUR[alt]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[strong]}" "$perc" "$CLI_SEPARATOR" "${aCOLOUR[weak]}" "$used_gb" "$size_gb" "$COLOUR_RESET" + printf "%b %b%-${length}s %b %b%6b %b %b%8sGiB of %8sGiB\n" "$CLI_BULLET" "${aCOLOUR[alt]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[strong]}" "$perc" "$CLI_SEPARATOR" "${aCOLOUR[weak]}" "$used_gb" "$size_gb" done - } Get_VPN_Status() From e61bd4733cf0b4935376adaba427685670fd2b87 Mon Sep 17 00:00:00 2001 From: MichaIng Date: Thu, 9 Jul 2026 23:49:39 +0200 Subject: [PATCH 33/39] dietpi-banner: little cleanup * remove redundant carriage return: Print_Subheader isn't used anywhere inline. * remove obsolete comment about GeoIP removal, since this info is seen as benefitical addition to the IP address * remove doubled newline: dietpi-print_large has an empty line included already. Adding a second empty line makes the gap too big IMO --- dietpi/func/dietpi-banner | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index e6416ac209..573625bb6a 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -320,8 +320,8 @@ $CLI_LINE" Print_Subheader() { - # ON THE CURRENT LINE Output green line separator with subheader title "$1" - echo -e "\r$CLI_LINE + # Output green line separator with subheader title "$1" + echo -e "$CLI_LINE ${aCOLOUR[strong]}$1 $CLI_SEPARATOR" } @@ -534,10 +534,6 @@ $CLI_LINE" 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. - #IP=${BASH_REMATCH[0]} - echo -e "${aCOLOUR[alt]} $IP [$display_ns]" elif [[ ! $IP ]] @@ -745,7 +741,7 @@ $CLI_LINE" # Large Format Hostname # shellcheck disable=SC1091 - (( ${aENABLED[large_hostname]} )) && . /boot/dietpi/func/dietpi-print_large "$( Date: Thu, 9 Jul 2026 23:58:02 +0200 Subject: [PATCH 34/39] dietpi-banner: revert extra empty line Whoos, dietpi-print_large does NOT include an empty line. It just appears as if it did, if the hostname has not hanging character, like no "g", "q" etc. Those however make use of the last line, in which case the next banner entry looks too close. --- dietpi/func/dietpi-banner | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 573625bb6a..0b5e68c9d4 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -741,7 +741,7 @@ $CLI_LINE" # Large Format Hostname # shellcheck disable=SC1091 - (( ${aENABLED[large_hostname]} )) && . /boot/dietpi/func/dietpi-print_large "$( Date: Fri, 10 Jul 2026 13:00:21 +0200 Subject: [PATCH 35/39] dietpi-banner: align comment with what the line looks like --- dietpi/func/dietpi-banner | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 0b5e68c9d4..26016141c3 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -649,8 +649,8 @@ $CLI_LINE" perc='?? %' # default unknown value fi - # Print aligned columns: {basename} : {used} of {size} ({percent used}) - printf "%b %b%-${length}s %b %b%6b %b %b%8sGiB of %8sGiB\n" "$CLI_BULLET" "${aCOLOUR[alt]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[strong]}" "$perc" "$CLI_SEPARATOR" "${aCOLOUR[weak]}" "$used_gb" "$size_gb" + # Print aligned columns: {basename} : ({percent used}) : {used} of {size} + printf "%b %b%-${length}s %b %b%6s %b %b%8sGiB of %8sGiB\n" "$CLI_BULLET" "${aCOLOUR[alt]}" "$name" "$CLI_SEPARATOR" "${aCOLOUR[strong]}" "$perc" "$CLI_SEPARATOR" "${aCOLOUR[weak]}" "$used_gb" "$size_gb" done } From 9c67bbf07f90007a804c80ed041a8ded16b0ba6c Mon Sep 17 00:00:00 2001 From: timjolson Date: Thu, 9 Jul 2026 16:27:04 -0500 Subject: [PATCH 36/39] Made IP4_re mawk friendly. Fixed netns IPs not being detected due to being unfriendly. --- dietpi/func/dietpi-banner | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 26016141c3..5946f26114 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -182,8 +182,8 @@ 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}' + # IP address detection (IPv4) - mawk-friendly (avoid + and {m,n}) + IP4_re='[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*' # DietPi version string DIETPI_VERSION="$G_DIETPI_VERSION_CORE.$G_DIETPI_VERSION_SUB.$G_DIETPI_VERSION_RC" @@ -512,7 +512,8 @@ $CLI_LINE" [[ $tx =~ ^[0-9]+$ ]] && TX="$(mawk -v v="$tx" 'BEGIN{printf "%.2f", v/(1024^3)}') GiB" # Get first IPv4 address for this interface in this namespace. If none, leave "- No IP -". - ip_addr_cidr=$("${ns_cmd_prefix[@]}" ip -br -4 addr show dev "$ifname" scope global 2> /dev/null | mawk -v re="^$IP4_re/[0-9]{1,2}$" '$3 ~ re {print $3;exit}') + # Scan all fields for a matching IPv4/CIDR token. + ip_addr_cidr=$("${ns_cmd_prefix[@]}" ip -br -4 addr show dev "$ifname" scope global 2> /dev/null | mawk -v re="^$IP4_re/[0-9][0-9]*$" '{for(i=1;i<=NF;i++) if($i ~ re){print $i; exit}}') [[ $ip_addr_cidr ]] && ip_addr=${ip_addr_cidr%%/*} [[ $RX == 'N/A' && $TX == 'N/A' ]] && continue From 6ee763c2f2f43fc889b8c40289a8d0d5eb8518a1 Mon Sep 17 00:00:00 2001 From: timjolson Date: Thu, 9 Jul 2026 16:43:31 -0500 Subject: [PATCH 37/39] removed unused 'width' parameter --- dietpi/func/dietpi-banner | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 5946f26114..b6c46536de 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -732,8 +732,8 @@ $CLI_LINE" { # 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 state=$2 width=$3 - printf "%b%b %-${width}b %b %b\n" "$CLI_BULLET" "${aCOLOUR[strong]}" "$subtitle" "$CLI_SEPARATOR" "$state" + local subtitle=$1 state=$2 + printf "%b%b %b %b %b\n" "$CLI_BULLET" "${aCOLOUR[strong]}" "$subtitle" "$CLI_SEPARATOR" "$state" } Print_Banner_raw() From f45a1d9c51b7f8decc32da179ec7e7068db03aad Mon Sep 17 00:00:00 2001 From: timjolson Date: Fri, 10 Jul 2026 12:10:27 -0500 Subject: [PATCH 38/39] Updated truncat doc --- CONTRIBUTING.md | 11 +++++++---- dietpi/func/dietpi-globals | 11 ++++++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 29c28614bc..543fa35091 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -203,10 +203,13 @@ Below are minimal, copy-paste-ready examples that follow DietPi conventions. ``` 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" + + # And `G_TRUNCATE_MID "alphabetical" N` returns the below, as N decreases + # alp...al + # al...al + # alphab + # alpha + # ... and so on ``` - `Save()` persistence pattern (follow dietpi-banner conventions): diff --git a/dietpi/func/dietpi-globals b/dietpi/func/dietpi-globals index f49becd7eb..faf03d04ae 100644 --- a/dietpi/func/dietpi-globals +++ b/dietpi/func/dietpi-globals @@ -173,12 +173,17 @@ $(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 "...". + # - If the maximum length is less than 7, 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 "G_TRUNCATE_MID "alphabetical" N` returns the below, as N decreases + # alp...al + # al...al + # alphab + # alpha + # ... and so on + G_TRUNCATE_MID() { local s=$1 max=$2 From 8fd6f4bd2dc050803452addd31fd6a2dc7bc61ee Mon Sep 17 00:00:00 2001 From: MichaIng Date: Fri, 10 Jul 2026 20:10:33 +0200 Subject: [PATCH 39/39] dietpi-banner: re-apply fixed alignment for network stats I changed it to `echo -e` since the used `%s` format codes printed color codes literally. However, this can be solved with `%b` as well, keeping the min-length format codes of `printf`. Raised the "IP (iface)" min length to 23 characters, matchig complete IPv4 address + classic WiFi interface name. --- dietpi/func/dietpi-banner | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index b6c46536de..887cb19f8b 100755 --- a/dietpi/func/dietpi-banner +++ b/dietpi/func/dietpi-banner @@ -517,7 +517,7 @@ $CLI_LINE" [[ $ip_addr_cidr ]] && ip_addr=${ip_addr_cidr%%/*} [[ $RX == 'N/A' && $TX == 'N/A' ]] && continue - echo -e "$CLI_BULLET ${aCOLOUR[weak]}$ip_addr ($ifname) $CLI_SEPARATOR ${aCOLOUR[weak]}TX=$TX RX=$RX" + printf '%b %b%-23s %b %bTX=%11s RX=%11s\n' "$CLI_BULLET" "${aCOLOUR[weak]}" "$ip_addr ($ifname)" "$CLI_SEPARATOR" "${aCOLOUR[weak]}" "$TX" "$RX" done }