From 57e08e88c4ca3caca8657e43739f9df4bb178b85 Mon Sep 17 00:00:00 2001 From: Nuno Duque Nunes Date: Fri, 22 May 2026 23:12:27 +0000 Subject: [PATCH] feat: rule list tableless layout with inline extends and +all/-N indicators --- commands/rule.command.sh | 394 ++++++++++++++----------------------- core/ui.sh | 23 +++ daemon/endpoint_cache.json | 2 +- modules/rule.module.sh | 21 +- modules/ui/rule.module.sh | 151 +++++++++++--- 5 files changed, 300 insertions(+), 291 deletions(-) diff --git a/commands/rule.command.sh b/commands/rule.command.sh index 10ff3cf..cf1e5d4 100644 --- a/commands/rule.command.sh +++ b/commands/rule.command.sh @@ -23,10 +23,10 @@ function cmd::rule::on_load() { flag::register --peer flag::register --peers flag::register --dns-redirect - flag::register --color flag::register --base flag::register --no-base flag::register --tree + flag::register --detailed flag::register --resolved flag::register --force flag::register --type @@ -60,7 +60,7 @@ Options for list: --base Show only base rules --no-base Hide base rules section --group Filter by group (case insensitive) - --tree Show full inheritance tree inline + --detailed Show rule entries inline Options for add: --name Rule name @@ -72,8 +72,8 @@ Options for add: --allow-port Allow specific port (repeatable) --block-ip Block IP or subnet (repeatable) --block-port Block specific port (repeatable) - --block-service Block named service — resolved to IP/port at creation (repeatable) - --allow-service Allow named service — resolved to IP/port at creation (repeatable) + --block-service Block named service (repeatable) + --allow-service Allow named service (repeatable) --dns-redirect Force DNS through Pi-hole Options for update: @@ -85,7 +85,7 @@ Options for update: --remove-block-ip Remove block IP entry --remove-block-port Remove block port entry -Options for show/inspect: +Options for show: --name Rule name --resolved Show resolved/merged entries --no-peers Hide assigned peers @@ -96,14 +96,12 @@ Options for reapply: Examples: wgctl rule list - wgctl rule list --tree + wgctl rule list --detailed wgctl rule list --group "VM Rules" wgctl rule show --name guest wgctl rule show --name moonlight-02 --resolved wgctl rule add --name no-proxmox --base --block-service proxmox - wgctl rule add --name dev-01 --desc "Dev access" --group "Dev" --extends no-lan - wgctl rule add --name restricted-dns --allow-service pihole:dns --block-service pihole - wgctl rule update --name user --add-extends no-nginx + wgctl rule add --name dev-01 --desc "Dev access" --extends no-lan wgctl rule assign --name dev-01 --peer laptop-nuno wgctl rule reapply --all EOF @@ -118,17 +116,16 @@ function cmd::rule::run() { shift || true case "$subcmd" in - list|ls) cmd::rule::list "$@" ;; - show|inspect) cmd::rule::show "$@" ;; - inspect) cmd::rule::inspect "$@" ;; - add|new|create) cmd::rule::add "$@" ;; - update|edit) cmd::rule::update "$@" ;; - remove|rm|del|delete) cmd::rule::remove "$@" ;; - assign) cmd::rule::assign "$@" ;; + list|ls) cmd::rule::list "$@" ;; + show|inspect) cmd::rule::show "$@" ;; + add|new|create) cmd::rule::add "$@" ;; + update|edit) cmd::rule::update "$@" ;; + remove|rm|del|delete) cmd::rule::remove "$@" ;; + assign) cmd::rule::assign "$@" ;; unassign) cmd::rule::unassign "$@" ;; - migrate) cmd::rule::migrate "$@" ;; - reapply) cmd::rule::reapply "$@" ;; - help) cmd::rule::help ;; + migrate) cmd::rule::migrate "$@" ;; + reapply) cmd::rule::reapply "$@" ;; + help) cmd::rule::help ;; *) log::error "Unknown subcommand: '${subcmd}'" cmd::rule::help @@ -141,172 +138,98 @@ function cmd::rule::run() { # List # ============================================ -function cmd::rule::_pad() { - local text="$1" width="$2" - local visible - visible=$(printf "%s" "$text" | sed 's/\x1b\[[0-9;]*m//g') - local visible_len=${#visible} - local byte_len=${#text} - local extra=$(( byte_len - visible_len )) - printf "%-$(( width + extra ))s" "$text" -} - -function cmd::rule::_print_extends_tree() { - local extends="$1" indent="${2:-2}" rules_dir="$3" - [[ -z "$extends" ]] && return 0 - - local extend_list=() - IFS=',' read -ra extend_list <<< "$extends" - - for base in "${extend_list[@]}"; do - [[ -z "$base" ]] && continue - local spaces - spaces=$(printf '%*s' "$indent" '') - printf " \033[0;37m%s↳ %s\033[0m\n" "$spaces" "$base" - - if [[ "$indent" -lt 12 ]]; then - local sub_file="" - if sub_file=$(json::find_rule_file "$rules_dir" "$base" 2>/dev/null); then - local sub_extends="" - sub_extends=$(json::get "$sub_file" "extends" 2>/dev/null \ - | tr '\n' ',' | sed 's/,$//' || true) - if [[ -n "$sub_extends" ]]; then - cmd::rule::_print_extends_tree \ - "$sub_extends" $(( indent + 4 )) "$rules_dir" - fi - fi - fi - done -} - function cmd::rule::list() { local rules_dir rules_dir="$(ctx::rules)" - local show_base_only=false - local show_base=true - local filter_group="" - local show_tree=false - local found_any=false + local show_base_only=false show_base=true + local filter_group="" detailed=false while [[ $# -gt 0 ]]; do case "$1" in - --base) show_base_only=true; shift ;; - --no-base) show_base=false; shift ;; - --group) util::require_flag "--group" "${2:-}" || return 1 - filter_group="${2,,}"; shift 2 ;; - --tree) show_tree=true; shift ;; - --help) cmd::rule::help; return ;; + --base) show_base_only=true; shift ;; + --no-base) show_base=false; shift ;; + --group) filter_group="${2,,}"; shift 2 ;; + --detailed) detailed=true; shift ;; + --help) cmd::rule::help; return ;; *) log::error "Unknown flag: $1" return 1 ;; esac done - local rules=("${rules_dir}"/*.rule) - if [[ ! -f "${rules[0]}" ]]; then - log::wg "No rules configured" - return 0 - fi + local data + data=$(json::rule_list_data "$rules_dir" "$(ctx::meta)") + [[ -z "$data" ]] && log::wg "No rules configured" && return 0 - local header_printed=false - local printing_base=false - local current_group="" + # Measure max name width + local w_name=12 + while IFS='|' read -r name rest; do + [[ -z "$name" ]] && continue + (( ${#name} > w_name )) && w_name=${#name} + done <<< "$data" + (( w_name += 2 )) + + log::section "Firewall Rules" + echo "" + + local current_group="" printing_base=false found_any=false while IFS="|" read -r name desc n_allows n_blocks \ peer_count extends is_base group; do [[ -z "$name" ]] && continue - # --base: show ONLY base rules - if $show_base_only && [[ "$is_base" == "False" ]]; then - continue - fi + $show_base_only && [[ "$is_base" == "False" ]] && continue + ! $show_base && [[ "$is_base" == "True" ]] && continue + [[ -n "$filter_group" && "${group,,}" != "$filter_group" ]] && continue - # --no-base: hide base rules - if ! $show_base && [[ "$is_base" == "True" ]]; then - continue - fi - - # --group filter (case insensitive) - if [[ -n "$filter_group" && "${group,,}" != "$filter_group" ]]; then - continue - fi - - # Print header on first match - if ! $header_printed; then - log::section "Firewall Rules" - printf "\n %-20s %-40s %-8s %-8s %s\n" \ - "NAME" "DESCRIPTION" \ - "$(ui::center "ALLOWS" 8)" \ - "$(ui::center "BLOCKS" 8)" \ - "PEERS" - local divider - divider=$(printf '─%.0s' {1..88}) - printf " %s\n" "$divider" - header_printed=true - fi + found_any=true # Base rules section header if [[ "$is_base" == "True" && "$printing_base" == "false" ]]; then if ! $show_base_only; then - local bdashes - bdashes=$(printf '─%.0s' {1..74}) - printf "\n \033[0;37m── Base Rules \033[0m%s\n" "$bdashes" + ui::rule::list_base_header fi printing_base=true current_group="" fi - # Group header — only for non-base rules + # Group header — non-base rules only if [[ "$is_base" == "False" && "${group,,}" != "${current_group,,}" ]]; then if [[ -n "$group" ]]; then - printf "\n \033[0;36m▸ %s\033[0m\n" "$group" + ui::rule::list_group_header "$group" elif [[ -n "$current_group" ]]; then - printf "\n" + echo "" fi current_group="$group" fi - local short_desc="${desc:0:35}" - [[ ${#desc} -gt 35 ]] && short_desc="${short_desc}..." + # Rule row + # ui::rule::list_row "$name" "$n_allows" "$n_blocks" "$peer_count" "$w_name" - local desc_col_width=40 - [[ "${short_desc:-—}" == "—" ]] && desc_col_width=42 - - found_any=true - - printf " %-20s %-${desc_col_width}s %-8s %-8s %s\n" \ - "$name" "${short_desc:-—}" \ - "$(ui::center "$n_allows" 8)" \ - "$(ui::center "$n_blocks" 8)" \ - "${peer_count} peers" - - # Print extends - if [[ -n "$extends" ]]; then - if $show_tree; then - cmd::rule::_print_extends_tree "$extends" 2 "$rules_dir" - else - local extend_list=() - IFS=',' read -ra extend_list <<< "$extends" - for base in "${extend_list[@]}"; do - [[ -z "$base" ]] && continue - printf " \033[0;37m ↳ %s\033[0m\n" "$base" - done - fi - printf "\n" + # Extends + # Rule row — pass extends_csv for compact inline display + local compact_extends="" + if [[ -z "$detailed" ]] || ! $detailed; then + compact_extends="$extends" + fi + ui::rule::list_row "$name" "$n_allows" "$n_blocks" "$peer_count" "$w_name" "$compact_extends" + + # Detailed mode — show expanded entries + if $detailed && [[ -n "$extends" ]]; then + ui::rule::list_extends_detailed "$extends" "$rules_dir" + echo "" fi - done < <(json::rule_list_data "$rules_dir" "$(ctx::meta)") + done <<< "$data" - if ! $found_any; then - if [[ -n "$filter_group" ]]; then - log::wg_warning "No rules found in group: ${filter_group}" - else + $found_any || { + [[ -n "$filter_group" ]] && \ + log::wg_warning "No rules found in group: ${filter_group}" || \ log::wg_warning "No rules found" - fi - fi + } - printf "\n" + echo "" } # ============================================ @@ -318,11 +241,10 @@ function cmd::rule::show() { while [[ $# -gt 0 ]]; do case "$1" in - --name) util::require_flag "--name" "${2:-}" || return 1 - name="$2"; shift 2 ;; - --no-peers) show_peers=false; shift ;; - --resolved) show_resolved=true; shift ;; - --help) cmd::rule::help; return ;; + --name) name="$2"; shift 2 ;; + --no-peers) show_peers=false; shift ;; + --resolved) show_resolved=true; shift ;; + --help) cmd::rule::help; return ;; *) log::error "Unknown flag: $1"; return 1 ;; esac done @@ -333,7 +255,7 @@ function cmd::rule::show() { local rule_file rule_file="$(rule::path "$name")" - # ── DNS display ─────────────────────────────── + # DNS display local dns_redirect resolved_dns dns_display dns_redirect=$(rule::get_own "$name" "dns_redirect") dns_redirect="${dns_redirect:-false}" @@ -359,24 +281,21 @@ function cmd::rule::show() { ui::row "DNS" "$dns_display" printf "\n" - # ── Extends + own rules ──────────────────────── - if rule::render_extends_tree "$name"; then - # Has inheritance — tree already rendered + if ui::rule::tree "$name"; then : else - # No inheritance — flat view - rule::render_flat "$name" + ui::rule::flat "$name" printf "\n" fi - # ── Resolved ────────────────────────────────── + # Resolved view if $show_resolved; then - cmd::rule::_show_section "Resolved (applied to peers)" + ui::rule::section_header "Resolved (applied to peers)" printf "\n" local res_allow_ports res_allow_ips res_block_ips res_block_ports res_allow_ports=$(rule::get "$name" "allow_ports") - res_allow_ips=$(rule::get "$name" "allow_ips") - res_block_ips=$(rule::get "$name" "block_ips") + res_allow_ips=$(rule::get "$name" "allow_ips") + res_block_ips=$(rule::get "$name" "block_ips") res_block_ports=$(rule::get "$name" "block_ports") while IFS= read -r e; do [[ -n "$e" ]] && net::print_entry "+" "$e"; done \ <<< "$res_allow_ports"$'\n'"$res_allow_ips" @@ -385,26 +304,24 @@ function cmd::rule::show() { printf "\n" fi - # ── Peers ───────────────────────────────────── - local peer_list=() - mapfile -t peer_list < <(peers::with_rule "$name") + # Peers + $show_peers || return 0 + local peer_list=() + mapfile -t peer_list < <(peers::with_rule "$name") || true local peer_count=${#peer_list[@]} - ui::empty "$peer_count" && return 0 - printf "\n" - printf "\033[0;37m── Peers (%s) \033[0m%s\n\n" \ - "$(color::gray "${peer_count}")" \ - "$(printf '\033[0;37m─%.0s' {1..35})" + local peer_word="peers" + [[ "$peer_count" -eq 1 ]] && peer_word="peer" + printf "\n \033[0;37m── Peers (%s %s) \033[0m%s\n\n" \ + "$peer_count" "$peer_word" "$(printf '\033[0;37m─%.0s' {1..30})" - if $show_peers && [[ $peer_count -gt 0 ]]; then - for peer_name in "${peer_list[@]}"; do - local ip - ip=$(peers::get_ip "$peer_name") - printf " %-28s %s\n" "$peer_name" "$ip" - done - fi + for peer_name in "${peer_list[@]}"; do + local ip + ip=$(peers::get_ip "$peer_name") + printf " %-28s %s\n" "$peer_name" "$ip" + done printf "\n" return 0 } @@ -418,27 +335,26 @@ function cmd::rule::add() { local extends=() local allow_ips=() block_ips=() block_ports=() allow_ports=() local block_services=() allow_services=() - local dns_redirect=false - local is_base=false + local dns_redirect=false is_base=false while [[ $# -gt 0 ]]; do case "$1" in - --name) name="$2"; shift 2 ;; - --desc) desc="$2"; shift 2 ;; - --group) group="$2"; shift 2 ;; + --name) name="$2"; shift 2 ;; + --desc) desc="$2"; shift 2 ;; + --group) group="$2"; shift 2 ;; --extends) IFS=',' read -ra ext <<< "$2" extends+=("${ext[@]}") shift 2 ;; - --base) is_base=true; shift ;; - --allow-ip) allow_ips+=("$2"); shift 2 ;; - --allow-port) allow_ports+=("$2"); shift 2 ;; - --block-ip) block_ips+=("$2"); shift 2 ;; - --block-port) block_ports+=("$2"); shift 2 ;; - --block-service) block_services+=("$2"); shift 2 ;; - --allow-service) allow_services+=("$2"); shift 2 ;; - --dns-redirect) dns_redirect=true; shift ;; - --help) cmd::rule::help; return ;; + --base) is_base=true; shift ;; + --allow-ip) allow_ips+=("$2"); shift 2 ;; + --allow-port) allow_ports+=("$2"); shift 2 ;; + --block-ip) block_ips+=("$2"); shift 2 ;; + --block-port) block_ports+=("$2"); shift 2 ;; + --block-service) block_services+=("$2"); shift 2 ;; + --allow-service) allow_services+=("$2"); shift 2 ;; + --dns-redirect) dns_redirect=true; shift ;; + --help) cmd::rule::help; return ;; *) log::error "Unknown flag: $1" return 1 ;; @@ -452,12 +368,10 @@ function cmd::rule::add() { return 1 fi - # Validate extends for ext in "${extends[@]}"; do rule::require_exists "$ext" || return 1 done - # Determine target directory local rule_dir if $is_base; then rule_dir="$(ctx::rules)/base" @@ -487,7 +401,6 @@ function cmd::rule::add() { done local rule_file="${rule_dir}/${name}.rule" - local allow_str block_str port_str allow_port_str extends_str allow_str=$(IFS=','; echo "${allow_ips[*]}") block_str=$(IFS=','; echo "${block_ips[*]}") @@ -502,7 +415,6 @@ function cmd::rule::add() { local base_label="" $is_base && base_label=" (base)" - log::wg_success "Rule created: ${name}${base_label}" } @@ -519,9 +431,9 @@ function cmd::rule::update() { while [[ $# -gt 0 ]]; do case "$1" in - --name) name="$2"; shift 2 ;; - --desc) desc="$2"; shift 2 ;; - --group) group="$2"; shift 2 ;; + --name) name="$2"; shift 2 ;; + --desc) desc="$2"; shift 2 ;; + --group) group="$2"; shift 2 ;; --add-extends) IFS=',' read -ra ext <<< "$2" add_extends+=("${ext[@]}") @@ -530,16 +442,16 @@ function cmd::rule::update() { IFS=',' read -ra ext <<< "$2" rm_extends+=("${ext[@]}") shift 2 ;; - --allow-ip) allow_ips+=("$2"); shift 2 ;; - --allow-port) allow_ports+=("$2"); shift 2 ;; - --block-ip) block_ips+=("$2"); shift 2 ;; - --block-port) block_ports+=("$2"); shift 2 ;; - --remove-allow-ip) rm_allow_ips+=("$2"); shift 2 ;; - --remove-block-ip) rm_block_ips+=("$2"); shift 2 ;; - --remove-block-port) rm_block_ports+=("$2"); shift 2 ;; - --remove-allow-port) rm_allow_ports+=("$2"); shift 2 ;; - --dns-redirect) dns_redirect=true; shift ;; - --help) cmd::rule::help; return ;; + --allow-ip) allow_ips+=("$2"); shift 2 ;; + --allow-port) allow_ports+=("$2"); shift 2 ;; + --block-ip) block_ips+=("$2"); shift 2 ;; + --block-port) block_ports+=("$2"); shift 2 ;; + --remove-allow-ip) rm_allow_ips+=("$2"); shift 2 ;; + --remove-block-ip) rm_block_ips+=("$2"); shift 2 ;; + --remove-block-port) rm_block_ports+=("$2"); shift 2 ;; + --remove-allow-port) rm_allow_ports+=("$2"); shift 2 ;; + --dns-redirect) dns_redirect=true; shift ;; + --help) cmd::rule::help; return ;; *) log::error "Unknown flag: $1" return 1 ;; @@ -552,18 +464,15 @@ function cmd::rule::update() { local rule_file rule_file="$(rule::path "$name")" - # Update simple fields [[ -n "$desc" ]] && json::set "$rule_file" "desc" "\"$desc\"" [[ -n "$group" ]] && json::set "$rule_file" "group" "\"$group\"" [[ -n "$dns_redirect" ]] && json::set "$rule_file" "dns_redirect" "true" - # Add entries for ip in "${allow_ips[@]}"; do json::append "$rule_file" "allow_ips" "$ip"; done for ip in "${block_ips[@]}"; do json::append "$rule_file" "block_ips" "$ip"; done for p in "${block_ports[@]}"; do json::append "$rule_file" "block_ports" "$p"; done for p in "${allow_ports[@]}"; do json::append "$rule_file" "allow_ports" "$p"; done - # Add/remove extends for ext in "${add_extends[@]}"; do rule::require_exists "$ext" || return 1 json::append "$rule_file" "extends" "$ext" @@ -572,7 +481,6 @@ function cmd::rule::update() { json::remove "$rule_file" "extends" "$ext" done - # Remove entries for ip in "${rm_allow_ips[@]}"; do json::remove "$rule_file" "allow_ips" "$ip"; done for ip in "${rm_block_ips[@]}"; do json::remove "$rule_file" "block_ips" "$ip"; done for p in "${rm_block_ports[@]}"; do json::remove "$rule_file" "block_ports" "$p"; done @@ -583,8 +491,7 @@ function cmd::rule::update() { } # ============================================ -# Remove / Assign / Unassign / Migrate / Reapply -# (unchanged from original) +# Remove # ============================================ function cmd::rule::remove() { @@ -603,7 +510,7 @@ function cmd::rule::remove() { rule::require_exists "$name" || return 1 local peer_list=() - mapfile -t peer_list < <(peers::with_rule "$name") + mapfile -t peer_list < <(peers::with_rule "$name") || true local peer_count=${#peer_list[@]} if [[ "$peer_count" -gt 0 ]]; then @@ -620,6 +527,10 @@ function cmd::rule::remove() { log::wg_success "Rule removed: ${name}" } +# ============================================ +# Assign / Unassign +# ============================================ + function cmd::rule::assign() { local name="" peer="" type="" while [[ $# -gt 0 ]]; do @@ -635,15 +546,13 @@ function cmd::rule::assign() { [[ -z "$name" || -z "$peer" ]] && \ log::error "Missing required flags: --name and --peer" && return 1 - rule::require_exists "$name" || return 1 + rule::require_exists "$name" || return 1 rule::require_assignable "$name" || return 1 peer=$(peers::resolve_and_require "$peer" "$type") || return 1 - local existing_rule + local existing_rule ip existing_rule=$(peers::get_meta "$peer" "rule") - - local ip ip=$(peers::get_ip "$peer") [[ -z "$ip" ]] && log::error "Could not resolve IP for: $peer" && return 1 @@ -684,37 +593,41 @@ function cmd::rule::unassign() { log::wg_success "Unassigned rule from: ${peer}" } +# ============================================ +# Migrate +# ============================================ + function cmd::rule::migrate() { log::section "Migrating peers to default rules" - local tmp - tmp=$(mktemp) + local count=0 while IFS= read -r peer_name; do local existing existing=$(peers::get_meta "$peer_name" "rule") [[ -n "$existing" ]] && continue - local default_rule - default_rule=$(peers::default_rule "$peer_name") + + # Try to get default rule from subnet policy + local peer_type subnet_name default_rule + peer_type=$(peers::get_meta "$peer_name" "type") + subnet_name=$(peers::get_meta "$peer_name" "subnet") + default_rule=$(subnet::default_rule "$subnet_name" "$peer_type") + [[ -z "$default_rule" ]] && continue + rule::exists "$default_rule" || continue + local ip ip=$(peers::get_ip "$peer_name") - echo "${peer_name} ${default_rule} ${ip}" >> "$tmp" - done < <(peers::all) - - local count=0 - local lines=() - mapfile -t lines < "$tmp" - rm -f "$tmp" - - for line in "${lines[@]}"; do - IFS=" " read -r peer_name default_rule ip <<< "$line" - rule::apply "$default_rule" "$ip" "$peer_name" +# 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 +# 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}" diff --git a/daemon/endpoint_cache.json b/daemon/endpoint_cache.json index f7c1f06..04cae3f 100644 --- a/daemon/endpoint_cache.json +++ b/daemon/endpoint_cache.json @@ -8,6 +8,6 @@ "desktop-roboclean": "46.189.215.231", "laptop-nuno": "94.63.0.129", "phone-luis": "176.223.61.15", - "phone-helena-2": "148.69.192.130", + "phone-helena-2": "148.69.202.127", "desktop-zephyr": "86.120.152.74" } \ No newline at end of file diff --git a/modules/rule.module.sh b/modules/rule.module.sh index 58ba2b3..a3732bd 100644 --- a/modules/rule.module.sh +++ b/modules/rule.module.sh @@ -333,21 +333,14 @@ function rule::restore_all() { # Rendering # ============================================ -function rule::render_flat() { - ui::rule::flat "$1" -} +# ====================================================== +# Aliases (for backward compat — remove in cleanup pass) +# ====================================================== -function rule::render_entries() { - ui::rule::entries "$1" -} - -function rule::render_own_entries() { - ui::rule::own_entries "$1" -} - -function rule::render_extends_tree() { - ui::rule::tree "$1" -} +function rule::render_flat() { ui::rule::flat "$1"; } +function rule::render_entries() { ui::rule::entries "$1"; } +function rule::render_own_entries() { ui::rule::own_entries "$1"; } +function rule::render_extends_tree() { ui::rule::tree "$1"; } # ============================================ # DNS Redirect diff --git a/modules/ui/rule.module.sh b/modules/ui/rule.module.sh index c385775..83fde5c 100644 --- a/modules/ui/rule.module.sh +++ b/modules/ui/rule.module.sh @@ -11,24 +11,24 @@ # 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" } @@ -40,27 +40,27 @@ function ui::rule::own_entries() { 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" } @@ -69,22 +69,22 @@ function ui::rule::own_entries() { # 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 @@ -92,7 +92,7 @@ function ui::rule::flat() { 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 @@ -100,7 +100,7 @@ function ui::rule::flat() { net::print_entry "-" "$e" 2 done <<< "$block_ips"$'\n'"$block_ports" fi - + [[ "${dns,,}" == "true" ]] && \ net::print_dns_redirect "$(config::dns)" 6 "DNS" } @@ -117,7 +117,7 @@ function ui::rule::_render_bases() { 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 @@ -140,23 +140,23 @@ function ui::rule::tree() { 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 } @@ -194,16 +194,15 @@ function ui::rule::_identity_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 - # Rule has extends — render one level deep using shared helper 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 @@ -211,7 +210,6 @@ function ui::rule::_identity_rule_entry() { printf "%s\n" "$own_output" fi else - # Leaf rule — show own entries or note full access local own_output own_output=$(ui::rule::own_entries "$rule_name" 8) if [[ -n "$own_output" ]]; then @@ -254,4 +252,103 @@ function ui::rule::_peer_rule_entry() { 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 + + # Build allows and blocks — pad to fixed visible width of 5 + local allows_str blocks_str + if [[ "$n_allows" -eq 0 && "$n_blocks" -eq 0 ]]; then + allows_str=$(ui::pad_mb "\033[1;32m+all\033[0m" 5) + blocks_str=$(printf "%5s" "") + else + if [[ "$n_allows" -gt 0 ]]; then + allows_str=$(ui::pad_mb "\033[1;32m+${n_allows}\033[0m" 5) + else + allows_str=$(printf "%5s" "") + fi + if [[ "$n_blocks" -gt 0 ]]; then + blocks_str=$(ui::pad_mb "\033[1;31m-${n_blocks}\033[0m" 5) + else + blocks_str=$(printf "%5s" "") + fi + fi + + printf " %s %b%b %s%*s%b\n" \ + "$name_pad" "$allows_str" "$blocks_str" \ + "$peers_display" "$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 +} + +# ====================================================== +# Show helpers +# ====================================================== + +function ui::rule::section_header() { + local title="${1:-}" + printf "\n \033[0;37m── %s ──────────────────────────────────\033[0m\n" "$title" } \ No newline at end of file