- ui::rule::list_row: inline padding math replaces ui::pad_mb (major perf gain)
- ui:⌚:fw_row/wg_row: drop ui::pad_mb for fw/wg labels (always 2 chars)
- watch: endpoint fallback via monitor::get_cached_endpoint
- watch: _poll_handshakes sorts by ts descending (most recent first)
- watch: empty endpoint uses - not — (avoids multi-byte padding issues)
- ui.sh: UTF-8 extra byte constants (_UI_EMDASH_EXTRA, _UI_ARROW_EXTRA, _UI_BULLET_EXTRA)
208 lines
No EOL
5.6 KiB
Bash
208 lines
No EOL
5.6 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
UI_ROW_WIDTH=${UI_ROW_WIDTH:-20}
|
|
UI_SECTION_WIDTH=${UI_SECTION_WIDTH:-44}
|
|
|
|
# UTF-8 multi-byte character extras (bash ${#} counts bytes, not chars)
|
|
# extra = byte_length - visible_char_length
|
|
_UI_EMDASH_EXTRA=2 # — (em dash) 3 bytes, 1 visible
|
|
_UI_ARROW_EXTRA=2 # → (right arrow) 3 bytes, 1 visible
|
|
_UI_BULLET_EXTRA=1 # · (middle dot) 2 bytes, 1 visible
|
|
|
|
function ui::row() {
|
|
local label="$1" value="$2" width="${3:-$UI_ROW_WIDTH}"
|
|
printf " %-${width}s %s\n" "${label}:" "$value"
|
|
}
|
|
|
|
function ui::section() {
|
|
local title="$1" width="${2:-$UI_SECTION_WIDTH}"
|
|
local dashes
|
|
dashes=$(printf '─%.0s' $(seq 1 $(( width - ${#title} - 4 ))))
|
|
printf "\n \033[0;37m── %s %s\033[0m\n" "$title" "$dashes"
|
|
}
|
|
|
|
function ui::list_item() {
|
|
local prefix="$1" value="$2"
|
|
printf " %s %s\n" "$prefix" "$value"
|
|
}
|
|
|
|
function ui::print_list() {
|
|
local prefix="$1" input="$2"
|
|
[[ -z "$input" ]] && return 0 # early return for empty input
|
|
while IFS= read -r e; do
|
|
[[ -n "$e" ]] && ui::list_item "$prefix" "$e"
|
|
done <<< "$input"
|
|
}
|
|
|
|
function ui::divider() {
|
|
local width="${1:-48}"
|
|
printf " %s\n" "$(printf '─%.0s' $(seq 1 $width))"
|
|
}
|
|
|
|
function ui::pad() {
|
|
local text="$1" width="${2:-20}"
|
|
local visible
|
|
visible=$(echo -e "$text" | sed 's/\x1b\[[0-9;]*m//g')
|
|
local pad=$(( width - ${#visible} ))
|
|
printf "%b%${pad}s" "$text" ""
|
|
}
|
|
|
|
function ui::pad_mb() {
|
|
local text="$1" width="${2:-20}"
|
|
local visible
|
|
visible=$(printf "%b" "$text" | sed 's/\x1b\[[0-9;]*m//g')
|
|
local vis_len
|
|
vis_len=$(python3 -c "import sys; print(len(sys.stdin.read().rstrip('\n')))" \
|
|
<<< "$visible")
|
|
local pad=$(( width - vis_len ))
|
|
[[ $pad -lt 0 ]] && pad=0
|
|
printf "%b%${pad}s" "$text" ""
|
|
}
|
|
|
|
function ui::vis_len_multi() {
|
|
# Get visible lengths of multiple strings in one Python call
|
|
# Returns newline-separated integers
|
|
python3 -c "
|
|
import sys, re
|
|
ansi = re.compile(r'\x1b\[[0-9;]*m')
|
|
for s in sys.argv[1:]:
|
|
print(len(ansi.sub('', s)))
|
|
" "$@"
|
|
}
|
|
|
|
# ui::vis_len <string>
|
|
# Returns the visible (character) length of a string,
|
|
# stripping ANSI codes and accounting for multi-byte UTF-8.
|
|
function ui::vis_len() {
|
|
local str="${1:-}"
|
|
# Strip ANSI codes first
|
|
local clean
|
|
clean=$(echo "$str" | sed 's/\x1b\[[0-9;]*m//g')
|
|
# Use Python for accurate Unicode character count
|
|
python3 -c "import sys; print(len('${clean//\'/\'\\\'\'}'))" 2>/dev/null || echo "${#clean}"
|
|
}
|
|
|
|
# ui::pad_to_col <string_bytes_printed> <target_visible_col>
|
|
# Returns padding needed accounting for UTF-8 byte/char difference.
|
|
# extra_bytes = bytes_printed - visible_chars_printed
|
|
function ui::utf8_extra_bytes() {
|
|
local str="${1:-}"
|
|
local byte_len=${#str}
|
|
local vis_len
|
|
vis_len=$(ui::vis_len "$str")
|
|
echo $(( byte_len - vis_len ))
|
|
}
|
|
|
|
|
|
function ui::pad_status() {
|
|
ui::pad "${1:-}" "${2:-25}"
|
|
}
|
|
|
|
function ui::center() {
|
|
local text="$1" width="${2:-8}"
|
|
local len=${#text}
|
|
local pad=$(( (width - len) / 2 ))
|
|
local rpad=$(( width - len - pad ))
|
|
printf "%${pad}s%s%${rpad}s" "" "$text" ""
|
|
}
|
|
|
|
# ui::measure_col <data> <field_index> [min_width]
|
|
# Scans pipe-delimited data and returns the max visible width
|
|
# of the field at field_index (1-based), with optional minimum.
|
|
# Strips ANSI codes before measuring.
|
|
# Usage:
|
|
# name_width=$(ui::measure_col "$data" 1 10)
|
|
# ip_width=$(ui::measure_col "$data" 2 14)
|
|
function ui::measure_col() {
|
|
local data="${1:-}" field_index="${2:-1}" min_width="${3:-0}"
|
|
local max=$min_width
|
|
|
|
while IFS='|' read -r line; do
|
|
local val
|
|
val=$(echo "$line" | cut -d'|' -f"$field_index")
|
|
# Strip ANSI codes for accurate measurement
|
|
local clean
|
|
clean=$(echo "$val" | sed 's/\x1b\[[0-9;]*m//g')
|
|
local len=${#clean}
|
|
(( len > max )) && max=$len
|
|
done <<< "$data"
|
|
|
|
echo $max
|
|
}
|
|
|
|
# ui::measure_cols <data> <field_indices...>
|
|
# Measure multiple columns at once, returns space-separated widths.
|
|
# Usage: read -r w1 w2 w3 <<< $(ui::measure_cols "$data" 1 2 3)
|
|
function ui::measure_cols() {
|
|
local data="${1:-}"
|
|
shift
|
|
local widths=()
|
|
for idx in "$@"; do
|
|
widths+=("$(ui::measure_col "$data" "$idx")")
|
|
done
|
|
echo "${widths[*]}"
|
|
}
|
|
|
|
function ui::sort_rows() {
|
|
local field="${1:-1}"
|
|
sort -t'|' -k"${field},${field}V"
|
|
}
|
|
|
|
function ui::firewall_rule() {
|
|
local rule="$1"
|
|
if [[ "$rule" =~ ACCEPT|DNAT ]]; then
|
|
printf "\033[0;32m%s\033[0m\n" "$rule"
|
|
elif [[ "$rule" =~ DROP ]]; then
|
|
printf "\033[0;31m%s\033[0m\n" "$rule"
|
|
elif [[ "$rule" =~ NFLOG|LOG ]]; then
|
|
printf "\033[0;37m%s\033[0m\n" "$rule"
|
|
else
|
|
printf "%s\n" "$rule"
|
|
fi
|
|
}
|
|
|
|
# ============================================
|
|
# Content Helpers
|
|
# ============================================
|
|
|
|
function ui::has_content() {
|
|
# Returns 0 (true) if content exists, 1 if empty
|
|
# Works with strings, arrays, or command output
|
|
local value="${1:-}"
|
|
[[ -n "$value" ]]
|
|
}
|
|
|
|
function ui::skip_if_empty() {
|
|
# Usage: ui::skip_if_empty "$var" || return 0
|
|
# Or: ui::skip_if_empty "${array[*]}" || return 0
|
|
local value="${1:-}"
|
|
[[ -z "${value// }" ]] && return 1 || return 0
|
|
}
|
|
|
|
function ui::empty() {
|
|
local val="${1:-}"
|
|
# Empty string or whitespace only
|
|
[[ -z "${val// }" ]] && return 0
|
|
# Numeric zero
|
|
[[ "$val" =~ ^[0-9]+$ ]] && [[ "$val" -eq 0 ]] && return 0
|
|
return 1
|
|
}
|
|
|
|
# Usage: ui::bool "$value" [yes_label] [no_label]
|
|
# Default labels: yes / no
|
|
function ui::bool() {
|
|
local val="${1:-}" yes="${2:-yes}" no="${3:-no}"
|
|
[[ "$val" == "true" ]] && echo "$yes" || echo "$no"
|
|
}
|
|
|
|
# ============================================
|
|
# Prompt
|
|
# ============================================
|
|
|
|
function ui::confirm() {
|
|
local prompt="${1:-Are you sure?}"
|
|
local response
|
|
printf " %s [y/N] " "$prompt"
|
|
read -r response
|
|
[[ "${response,,}" == "y" || "${response,,}" == "yes" ]]
|
|
} |