#!/usr/bin/env bash # ui/rule.module.sh — rendering for rule data # Replaces rule::render_* functions from rule.module.sh. # All functions pure rendering — no writes, no state changes. # ====================================================== # Entry Rendering (shared primitives) # ====================================================== # ui::rule::entries [indent] # Renders the fully resolved entries for a rule (allow + block). function ui::rule::entries() { local rule_name="${1:-}" indent="${2:-4}" local allow_ports allow_ips block_ips block_ports dns allow_ports=$(rule::get "$rule_name" "allow_ports" 2>/dev/null || true) allow_ips=$(rule::get "$rule_name" "allow_ips" 2>/dev/null || true) block_ips=$(rule::get "$rule_name" "block_ips" 2>/dev/null || true) block_ports=$(rule::get "$rule_name" "block_ports" 2>/dev/null || true) dns=$(rule::get_own "$rule_name" "dns_redirect") while IFS= read -r e; do [[ -z "$e" ]] && continue net::print_entry "+" "$e" "$indent" done <<< "$allow_ports"$'\n'"$allow_ips" while IFS= read -r e; do [[ -z "$e" ]] && continue net::print_entry "-" "$e" "$indent" done <<< "$block_ips"$'\n'"$block_ports" [[ "${dns,,}" == "true" ]] && \ net::print_dns_redirect "$(config::dns)" 6 "DNS" } # ui::rule::own_entries [indent] # Renders only the rule's own (non-inherited) entries. function ui::rule::own_entries() { local rule_name="${1:-}" indent="${2:-4}" local rule_file rule_file="$(rule::path "$rule_name")" || return 0 [[ -z "$rule_file" ]] && return 0 local allow_ports allow_ips block_ips block_ports dns allow_ports=$(json::get "$rule_file" "allow_ports" 2>/dev/null || true) allow_ips=$(json::get "$rule_file" "allow_ips" 2>/dev/null || true) block_ips=$(json::get "$rule_file" "block_ips" 2>/dev/null || true) block_ports=$(json::get "$rule_file" "block_ports" 2>/dev/null || true) dns=$(json::get "$rule_file" "dns_redirect" 2>/dev/null || true) local combined="${allow_ports}${allow_ips}${block_ips}${block_ports}" [[ -z "${combined//[$'\n']/}" ]] && return 0 while IFS= read -r e; do [[ -z "$e" ]] && continue net::print_entry "+" "$e" "$indent" done <<< "$allow_ports"$'\n'"$allow_ips" while IFS= read -r e; do [[ -z "$e" ]] && continue net::print_entry "-" "$e" "$indent" done <<< "$block_ips"$'\n'"$block_ports" [[ "${dns,,}" == "true" ]] && \ net::print_dns_redirect "$(config::dns)" 6 "DNS" } # ui::rule::flat # Renders the full resolved entries as a flat list. function ui::rule::flat() { local rule_name="${1:-}" local allow_ports allow_ips block_ips block_ports dns allow_ports=$(rule::get "$rule_name" "allow_ports") allow_ips=$(rule::get "$rule_name" "allow_ips") block_ips=$(rule::get "$rule_name" "block_ips") block_ports=$(rule::get "$rule_name" "block_ports") dns=$(rule::get_own "$rule_name" "dns_redirect") local has_content=false [[ -n "${allow_ports}${allow_ips}${block_ips}${block_ports}" ]] && has_content=true if ! $has_content; then printf "\n full access (no restrictions)\n" return 0 fi if [[ -n "$allow_ports" || -n "$allow_ips" ]]; then printf "\n" while IFS= read -r e; do [[ -z "$e" ]] && continue net::print_entry "+" "$e" 2 done <<< "$allow_ports"$'\n'"$allow_ips" fi if [[ -n "$block_ips" || -n "$block_ports" ]]; then printf "\n" while IFS= read -r e; do [[ -z "$e" ]] && continue net::print_entry "-" "$e" 2 done <<< "$block_ips"$'\n'"$block_ports" fi [[ "${dns,,}" == "true" ]] && \ net::print_dns_redirect "$(config::dns)" 6 "DNS" } # ====================================================== # Shared Base Renderer # ====================================================== # ui::rule::_render_bases [entry_indent] [label_indent] # Renders a list of base rule names with newlines between them. # Used by both ui::rule::tree and ui::rule::_identity_rule_entry. function ui::rule::_render_bases() { local -n _bases="$1" local entry_indent="${2:-6}" label_indent="${3:-4}" local label_pad label_pad=$(printf '%*s' "$label_indent" '') local first=true for base_name in "${_bases[@]}"; do [[ -z "$base_name" ]] && continue $first || printf "\n" first=false printf "%s\033[0;37m↳ %s\033[0m\n" "$label_pad" "$base_name" ui::rule::entries "$base_name" "$entry_indent" done } # ====================================================== # Tree Rendering # ====================================================== # ui::rule::tree # Renders a rule's extends tree — one level deep with own entries. # Returns 1 if rule has no extends (caller can fall back to flat). function ui::rule::tree() { local rule_name="${1:-}" local rule_file rule_file="$(rule::path "$rule_name")" || return 1 [[ -z "$rule_file" ]] && return 1 local extends_raw=() mapfile -t extends_raw < <(json::get "$rule_file" "extends" 2>/dev/null || true) || true if [[ ${#extends_raw[@]} -eq 0 || -z "${extends_raw[0]:-}" ]]; then return 1 fi ui::rule::_render_bases extends_raw 6 4 local own_output own_output=$(ui::rule::own_entries "$rule_name" 6) if [[ -n "$own_output" ]]; then printf "\n \033[0;37mOwn:\033[0m\n" printf "%s\n" "$own_output" fi return 0 } # ====================================================== # Identity Rule Block # ====================================================== # ui::rule::identity_block # Renders the full identity rule block in inspect. function ui::rule::identity_block() { local identity_name="${1:-}" strict="${2:-false}" no_header=false # Parse optional flags shift 2 || true while [[ $# -gt 0 ]]; do case "$1" in --no-header) no_header=true; shift ;; *) shift ;; esac done local rules rules=$(identity::rules "$identity_name") [[ -z "$rules" ]] && return 0 if ! $no_header; then printf "\n \033[0;37m· identity:%s\033[0m\n" "$identity_name" fi # Indentation levels: # normal: entry=6 label=6 own=10 own_label=8 # no-header: entry=4 label=4 own=8 own_label=6 local entry_indent label_indent own_indent own_label_indent if $no_header; then entry_indent=4 label_indent=4 own_indent=8 own_label_indent=6 else entry_indent=6 label_indent=6 own_indent=10 own_label_indent=8 fi local first=true while IFS= read -r rule_name; do [[ -z "$rule_name" ]] && continue $first || printf "\n" first=false ui::rule::_identity_rule_entry "$rule_name" \ "$entry_indent" "$label_indent" "$own_indent" "$own_label_indent" done <<< "$rules" if [[ "$strict" == "true" ]]; then printf "\n \033[2m(strict — peer rule suppressed)\033[0m\n" fi } # ui::rule::_identity_rule_entry # Renders one rule within an identity block. function ui::rule::_identity_rule_entry() { local rule_name="${1:-}" local entry_indent="${2:-6}" local label_indent="${3:-6}" local own_indent="${4:-10}" local own_label_indent="${5:-8}" local rule_file rule_file="$(rule::path "$rule_name")" || return 0 local label_pad label_pad=$(printf '%*s' "$label_indent" '') printf "%s\033[0;37m↳ %s\033[0m\n" "$label_pad" "$rule_name" local extends_raw=() mapfile -t extends_raw < <(json::get "$rule_file" "extends" 2>/dev/null || true) || true if [[ ${#extends_raw[@]} -gt 0 && -n "${extends_raw[0]:-}" ]]; then ui::rule::_render_bases extends_raw "$own_indent" "$(( entry_indent + 2 ))" local own_output own_output=$(ui::rule::own_entries "$rule_name" "$own_indent") if [[ -n "$own_output" ]]; then local own_pad own_pad=$(printf '%*s' "$(( entry_indent + 2 ))" '') printf "\n%s\033[0;37mOwn:\033[0m\n" "$own_pad" printf "%s\n" "$own_output" fi else local own_output own_output=$(ui::rule::own_entries "$rule_name" "$entry_indent") if [[ -n "$own_output" ]]; then printf "%s\n" "$own_output" else local full_pad full_pad=$(printf '%*s' "$(( entry_indent + 2 ))" '') printf "%s\033[2mfull access (no restrictions)\033[0m\n" "$full_pad" fi fi } # ====================================================== # Peer Entry # ====================================================== function ui::rule::_peer_rule_entry() { local rule_name="${1:-}" local rule_file rule_file="$(rule::path "$rule_name")" || return 0 printf " \033[0;37m↳ %s\033[0m\n" "$rule_name" local extends_raw=() mapfile -t extends_raw < <(json::get "$rule_file" "extends" 2>/dev/null || true) || true if [[ ${#extends_raw[@]} -gt 0 && -n "${extends_raw[0]:-}" ]]; then ui::rule::_render_bases extends_raw 10 8 local own_output own_output=$(ui::rule::own_entries "$rule_name" 10) if [[ -n "$own_output" ]]; then printf "\n \033[0;37mOwn:\033[0m\n" printf "%s\n" "$own_output" fi else local own_output own_output=$(ui::rule::own_entries "$rule_name" 8) if [[ -n "$own_output" ]]; then printf "%s\n" "$own_output" else printf " \033[2mfull access (no restrictions)\033[0m\n" fi fi } # ====================================================== # List Rendering # ====================================================== # ui::rule::list_group_header function ui::rule::list_group_header() { local group="${1:-}" printf "\n \033[0;36m▸ %s\033[0m\n" "$group" } # ui::rule::list_base_header function ui::rule::list_base_header() { printf "\n \033[2m── Base Rules ──────────────────────\033[0m\n" } # ui::rule::list_row function ui::rule::list_row() { local name="${1:-}" n_allows="${2:-0}" n_blocks="${3:-0}" \ peer_count="${4:-0}" w_name="${5:-16}" extends_csv="${6:-}" local name_pad name_pad=$(printf "%-${w_name}s" "$name") local peer_word="peers" [[ "$peer_count" -eq 1 ]] && peer_word="peer" local peers_display peers_display=$(printf "%s %s" "$peer_count" "$peer_word") local peers_pad_n=$(( 10 - ${#peers_display} )) [[ $peers_pad_n -lt 0 ]] && peers_pad_n=0 local extends_indicator="" if [[ -n "$extends_csv" ]]; then local extends_display="${extends_csv//,/, }" extends_indicator=" \033[2m↳ ${extends_display}\033[0m" fi # Allows — green +N, padded to 5 visible chars # Append spaces after reset code so printf doesn't miscount local allows_str if [[ "$n_allows" -gt 0 ]]; then printf -v allows_str "\033[1;32m+%s\033[0m" "$n_allows" allows_str="${allows_str}$(printf '%*s' "$(( 4 - ${#n_allows} ))" '')" else printf -v allows_str "\033[2m+0\033[0m " # "+0" = 2 visible + 3 spaces = 5 fi # Blocks — red -N, padded to 5 visible chars local blocks_str if [[ "$n_blocks" -gt 0 ]]; then printf -v blocks_str "\033[1;31m-%s\033[0m" "$n_blocks" blocks_str="${blocks_str}$(printf '%*s' "$(( 4 - ${#n_blocks} ))" '')" else printf -v blocks_str "\033[2m-0\033[0m " # "-0" = 2 visible + 3 spaces = 5 fi # Peers — dim if zero local peers_colored if [[ "$peer_count" -eq 0 ]]; then peers_colored="\033[2m${peers_display}\033[0m" else peers_colored="$peers_display" fi printf " %s %b%b %b%*s%b\n" \ "$name_pad" "$allows_str" "$blocks_str" \ "$peers_colored" "$peers_pad_n" "" "$extends_indicator" } # ui::rule::list_extends # Renders the extends tree for a rule in list view (compact, one level) function ui::rule::list_extends() { local extends_csv="${1:-}" [[ -z "$extends_csv" ]] && return 0 local extend_list=() IFS=',' read -ra extend_list <<< "$extends_csv" for base in "${extend_list[@]}"; do [[ -z "$base" ]] && continue printf " \033[0;37m ↳ %s\033[0m\n" "$base" done } # ui::rule::list_extends_detailed # Renders the extends tree with entries expanded (--detailed mode) function ui::rule::list_extends_detailed() { local extends_csv="${1:-}" rules_dir="${2:-}" [[ -z "$extends_csv" ]] && return 0 local extend_list=() IFS=',' read -ra extend_list <<< "$extends_csv" for base in "${extend_list[@]}"; do [[ -z "$base" ]] && continue printf " \033[0;37m ↳ %s\033[0m\n" "$base" ui::rule::entries "$base" 6 done } # ====================================================== # Table view # ====================================================== function ui::rule::list_header_table() { printf "\n %-20s %-6s %-6s %-8s %-20s %s\n" \ "NAME" "ALLOW" "BLOCK" "PEERS" "EXTENDS" "GROUP" printf " %s\n" "$(printf '─%.0s' {1..75})" } function ui::rule::list_row_table() { local name="${1:-}" n_allows="${2:-0}" n_blocks="${3:-0}" \ peer_count="${4:-0}" extends="${5:-}" group="${6:-}" printf " %-20s %-6s %-6s %-8s %-20s %s\n" \ "$name" "+${n_allows}" "-${n_blocks}" "$peer_count" \ "${extends:--}" "${group:--}" } # ====================================================== # Show helpers # ====================================================== function ui::rule::section_header() { local title="${1:-}" printf "\n \033[0;37m── %s ──────────────────────────────────\033[0m\n" "$title" }