diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 34da9a7fd9..543fa35091 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -199,6 +199,19 @@ 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" + + # 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): ``` Save(){ diff --git a/dietpi/func/dietpi-banner b/dietpi/func/dietpi-banner index 2da3f34cfb..887cb19f8b 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,63 +40,152 @@ # 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' - aDESCRIPTION=( - - 'Device model' - 'Uptime' - 'CPU temp' - 'FQDN/hostname' - 'NIS domainname' - 'LAN IP' - 'WAN IP' - 'Disk usage (RootFS)' - 'Disk usage (userdata)' - 'Weather (wttr.in)' - 'Custom banner entry' - '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' + # Use associative arrays keyed by short names derived from the description + declare -A 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' + [dietpi_commands]='Display useful DietPi commands?' + [motd]='MOTD' + [vpn_status]='VPN status' + [large_hostname]='Large hostname' + [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]='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 fail2ban + network_usage disk_usage custom_commands dietpi_commands motd credits word_wrap ) - # 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) - else - aENABLED=(1 0 1 0 0 1 0 0 0 0 0 1 1 0 0 1 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 + # Initialize aENABLED from aDESCRIPTION keys + declare -A aENABLED + for k in "${!aDESCRIPTION[@]}" + do + aENABLED[$k]=0 + done + + # Set default options + aENABLED[device_model]=1 + aENABLED[lan_ip]=1 + aENABLED[disk_rootfs]=1 + aENABLED[dietpi_commands]=1 + aENABLED[motd]=1 + aENABLED[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 - # Load settings here, to have chosen ${aCOLOUR[0]} applied to below strings + # 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') + + # Load settings here, to have chosen custom CLI colours applied # shellcheck disable=SC1090 [[ -f $FP_SAVEFILE ]] && . "$FP_SAVEFILE" - GREEN_LINE=" ${aCOLOUR[0]}─────────────────────────────────────────────────────$COLOUR_RESET" - GREEN_BULLET=" ${aCOLOUR[0]}-$COLOUR_RESET" - GREEN_SEPARATOR="${aCOLOUR[0]}:$COLOUR_RESET" + # ---------------------------------------------------------------------------- + # 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 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 + # 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 + # 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" + + # 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" if [[ $G_GITOWNER != 'MichaIng' ]] then @@ -154,21 +244,47 @@ Save() { - # Custom entry description - echo "aDESCRIPTION[10]='${aDESCRIPTION[10]}'" - + # Persist associative aENABLED as simple assignments (one per line) for i in "${!aDESCRIPTION[@]}" do echo "aENABLED[$i]=${aENABLED[$i]}" done + # 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[@]}" 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 aDISK_SPACE_PATTERNS (indices 0..N) + for i in "${!aDISK_SPACE_PATTERNS[@]}" + do + patt=${aDISK_SPACE_PATTERNS[$i]} + patt=${patt//\'/\\\'} + echo "aDISK_SPACE_PATTERNS[$i]='$patt'" + done + + # Persist aCUSTOM_COMMANDS (indices 0..N) + for i in "${!aCUSTOM_COMMANDS[@]}" + do + cmd=${aCUSTOM_COMMANDS[$i]} + cmd=${cmd//\'/\\\'} + echo "aCUSTOM_COMMANDS[$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 } Print_Header() @@ -176,57 +292,400 @@ # DietPi update available? if Check_DietPi_Update then - local text_update_available_date="${aCOLOUR[3]}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[3]}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[3]}$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[3]}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[1]}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() + { + # Output green line separator with subheader title "$1" + echo -e "$CLI_LINE + ${aCOLOUR[strong]}$1 $CLI_SEPARATOR" } - Print_Local_Ip() + 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 -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})" + } + + Get_Cert_Status() + { + 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. + # Keep it 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 + echo 'No certificate found' + } + + local stat code + if [[ $EUID == 0 ]] + then + # Running as root + stat=$(Get_Cert_Status_Raw 2>&1) + 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) + # Get_Cert_Status_Raw executed, process output + case $stat in + '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 + ;; + *) + # sudo failed, print meaningful error message + stat="${aCOLOUR[highlight]}NOPASSWD sudo required to obtain cert status" + ;; + esac + + echo "$stat" + } + + 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 + echo "${aCOLOUR[weak]}No services failed" + else + echo "${aCOLOUR[alert]}$FAILED_CNT service(s) failed" + fi + } + + Get_Fail2Ban_Status() + { + Get_Fail2Ban_Status_Raw() + { + if command -v fail2ban-client > /dev/null + then + if [[ $EUID == 0 ]] + then + 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 20 )) + then + # high number of bans + state="${aCOLOUR[alert]}$count_banned_ips IP banned" + else + # low number of bans + state="${aCOLOUR[weak]}$count_banned_ips IP(s) banned" + fi + ;; + 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_Network_Usage() + { + + # 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' + } + + # Print usage stats for namespace $1 (default if empty) + Print_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 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 -br link 2> /dev/null | mawk '{print $1}') + + 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*) :;; + # wireless + wlan*|wl*) :;; + # vpn/tunnel/virtual + tun*|wg*|vpn*) :;; + *) continue;; + esac + + 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 [[ $netns ]]; + then + # 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 first IPv4 address for this interface in this namespace. If none, leave "- No IP -". + # 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 + printf '%b %b%-23s %b %bTX=%11s RX=%11s\n' "$CLI_BULLET" "${aCOLOUR[weak]}" "$ip_addr ($ifname)" "$CLI_SEPARATOR" "${aCOLOUR[weak]}" "$TX" "$RX" + done + } + + 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[@]}") + + for ns in "${namespaces[@]}" + do + raw_ns=${ns:-default} + display_ns=$(G_TRUNCATE_MID "$raw_ns" 24) + + IP=$(Get_WAN_IP "$ns" 2>&1) + + if [[ $IP =~ ^$IP4_re ]] + then + # Valid IP detected, keep as-is and print with namespace + 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. + echo -e "${aCOLOUR[alert]} Disconnected $CLI_SEPARATOR ${aCOLOUR[strong]}$display_ns" + + else + # Print curl errors as is + echo -e "${aCOLOUR[highlight]} $IP [$display_ns]" + fi + + Print_Data_Usage "$ns" + done + } + + Print_Disk_Usage() + { + # If no arguments are provided, default to '"/" "/mnt/*"'. + (( $# )) && 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 + 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 + basename= + 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 + 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} + (( $len > $length )) && length=$len + results+=("$size_kb|$used_kb|$avail_kb|$mnt_target|$basename") + fi + done < <(df -k --output=size,used,avail,target | tail +2) + + # 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 KiB, convert to GiB for display and round to 1 decimal + 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 and available are available, calculate size as used + available + size_kb=$(( $used_kb + $avail_kb )) + size_gb=$(mawk "BEGIN {printf \"%.1f \", ($size_kb)/1024/1024}") + else + size_gb='???' + fi + fi + + 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 and available are available, calculate used as size - available + used_kb=$(( $size_kb - $avail_kb )) + used_gb=$(mawk "BEGIN {printf \"%.1f \", ($used_kb)/1024/1024}") + else + used_gb='???' + fi + fi + + if [[ $avail_kb =~ ^[0-9]+$ ]] + then + ((values_count++)) + else + # 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 > 1 )) + then + # this should match the `df` output + perc=$(mawk "BEGIN {printf \"%.1f%%\", ($used_kb/($used_kb+$avail_kb))*100}") + else + perc='?? %' # default unknown value + fi + + # 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 + } + + 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}";; + esac + echo "$state" } - Print_Cert_Status() + Print_Custom_Commands() { - # Let's Encrypt cert status - MUST be run as root - local i - for i in /etc/letsencrypt/live/*/cert.pem + local output status + for i in "${!aCUSTOM_COMMANDS[@]}" 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 + [[ ${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 - echo 'No certificate found' } Print_Credits() { - echo -e " ${aCOLOUR[2]}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 @@ -241,32 +700,40 @@ $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[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[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[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[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[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[1]}reboot$COLOUR_RESET $GREEN_SEPARATOR ${aCOLOUR[3]}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[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[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 + printf "%b%b %b %b %b\n" "$CLI_BULLET" "${aCOLOUR[strong]}" "$subtitle" "$CLI_SEPARATOR" "$state" } Print_Banner_raw() @@ -275,63 +742,67 @@ $GREEN_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]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[20]} $GREEN_SEPARATOR $(uname -r 2>&1)" + (( ${aENABLED[kernel]} )) && Print_Item_State "${aDESCRIPTION[kernel]}" "$(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[cpu_temp]} )) && Print_Item_State "${aDESCRIPTION[cpu_temp]}" "$(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[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]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[18]} $GREEN_SEPARATOR $(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]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[3]} $GREEN_SEPARATOR $(&1)" + (( ${aENABLED[nis_domainname]} )) && Print_Item_State "${aDESCRIPTION[nis_domainname]}" "$(hostname -y 2>&1)" # LAN IP - Print_Local_Ip + (( ${aENABLED[lan_ip]} )) && Print_Item_State "${aDESCRIPTION[lan_ip]}" "$(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[wan_ip]} )) && Print_Item_State "${aDESCRIPTION[wan_ip]}" "$(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[vpn_status]} )) && Print_Item_State "${aDESCRIPTION[vpn_status]}" "$(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[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 "$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[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]} )) && echo -e "$GREEN_BULLET ${aCOLOUR[1]}${aDESCRIPTION[9]} $GREEN_SEPARATOR $(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 - 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[letsencrypt]} )) && Print_Item_State "Let's Encrypt Cert" "$(Get_Cert_Status 2>&1)" + # Systemd status + (( ${aENABLED[systemd]} )) && Print_Item_State "${aDESCRIPTION[systemd]}" "$(Get_Systemd_Status 2>&1)" + # Fail2Ban status + (( ${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[dietpi_commands]} || ${aENABLED[motd]} || ${aENABLED[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" # 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[motd]}" "$motd" fi - echo -e "$GREEN_LINE\n" + echo -e "$CLI_LINE\n" - (( ${aENABLED[15]} )) && Print_Credits + (( ${aENABLED[credits]} )) && Print_Credits Print_Updates - (( ${aENABLED[11]} )) && 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" } Print_Banner() @@ -339,7 +810,7 @@ $GREEN_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 @@ -350,39 +821,76 @@ $GREEN_LINE" Menu_Main() { G_WHIP_CHECKLIST_ARRAY=() - for i in "${!aDESCRIPTION[@]}" + for k in "${MENU_ITEMS[@]}" do local state='off' - (( ${aENABLED[$i]:=0} == 1 )) && state='on' - G_WHIP_CHECKLIST_ARRAY+=("$i" "${aDESCRIPTION[$i]}" "$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 - for i in "${!aDESCRIPTION[@]}" + for k in "${MENU_ITEMS[@]}" do - aENABLED[$i]=0 + aENABLED[$k]=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 - # 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 - - elif (( $i == 19 )) + # 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 : + 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\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 + aCUSTOM_COMMANDS+=("$G_WHIP_RETURNED_VALUE") + ;; + Edit) + (( ${#aCUSTOM_COMMANDS[@]} == 0 )) && { G_WHIP_MSG 'No commands to edit'; continue; } + G_WHIP_MENU_ARRAY=() + 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=${aCUSTOM_COMMANDS[$sel]} + G_WHIP_INPUTBOX "Edit command, for example:\n$example_command" || continue + aCUSTOM_COMMANDS[$sel]="$G_WHIP_RETURNED_VALUE" + ;; + Remove) + (( ${#aCUSTOM_COMMANDS[@]} == 0 )) && { G_WHIP_MSG 'No commands to remove'; continue; } + G_WHIP_MENU_ARRAY=() + 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 'aCUSTOM_COMMANDS[$sel]' + # reindex array + aCUSTOM_COMMANDS=("${aCUSTOM_COMMANDS[@]}") + ;; + Done) break;; + *) ;; + esac + done + + # If no commands are configured, disable the custom commands option + (( ${#aCUSTOM_COMMANDS[@]} )) || aENABLED[custom_commands]=0 + + elif [[ $selection == 'word_wrap' ]] then # Banner word-wrap options G_WHIP_MENU_ARRAY=( @@ -397,10 +905,63 @@ 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 [[ $selection == 'disk_usage' ]] + 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/.*/*$'" + + # Manage aDISK_SPACE_PATTERNS list + while : + 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 + 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 + sel=$G_WHIP_RETURNED_VALUE + G_WHIP_DEFAULT_ITEM=${aDISK_SPACE_PATTERNS[$sel]} + 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 + sel=$G_WHIP_RETURNED_VALUE + unset 'aDISK_SPACE_PATTERNS[$sel]' + # reindex array + aDISK_SPACE_PATTERNS=("${aDISK_SPACE_PATTERNS[@]}") + ;; + Done) break;; + *) ;; + esac + done + + # If no patterns are configured, disable the disk usage option + (( ${#aDISK_SPACE_PATTERNS[@]} == 0 )) && aENABLED[disk_usage]=0 fi done - [[ -f $FP_CUSTOM ]] || aENABLED[10]=0 + # 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 + local backup="$FP_SAVEFILE.bak-$(date -Iseconds)" + cp -a "$FP_SAVEFILE" "$backup" + fi Save > "$FP_SAVEFILE" } @@ -409,7 +970,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[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;; diff --git a/dietpi/func/dietpi-globals b/dietpi/func/dietpi-globals index b84cf27827..faf03d04ae 100644 --- a/dietpi/func/dietpi-globals +++ b/dietpi/func/dietpi-globals @@ -171,6 +171,40 @@ $(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 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 "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 + local len=${#s} + if (( $max >= $len )) + then + echo "$s" + return 0 + + elif (( $max < 7 )) + 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 # $1: # -2 = Processing @@ -460,11 +494,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 # 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 @@ -805,6 +840,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 @@ -815,10 +851,36 @@ $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} + + # If G_WHIP_CHECKLIST_ENUM=1 is set, map and replace original tags and default with numeric ones + if (( $G_WHIP_CHECKLIST_ENUM )) + then + 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 + 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 G_WHIP_CHECKLIST_ENUM=1 is set, replace numeric selections back to original tags + if (( $G_WHIP_CHECKLIST_ENUM )) + then + local mapped=() + for idx in $G_WHIP_RETURNED_VALUE + do + mapped+=("${map[$idx]}") + done + G_WHIP_RETURNED_VALUE=${mapped[*]} + fi + G_WHIP_RETURNED_VALUE=$(echo -e "$G_WHIP_RETURNED_VALUE" | tr '\n' ' ') else G_WHIP_RETURNED_VALUE=$G_WHIP_DEFAULT_ITEM