diff --git a/commands/group.command.sh b/commands/group.command.sh index d71f756..d9d76dc 100644 --- a/commands/group.command.sh +++ b/commands/group.command.sh @@ -107,47 +107,38 @@ function cmd::group::run() { function cmd::group::list() { local groups_dir groups_dir="$(ctx::groups)" - + local groups=("${groups_dir}"/*.group) if [[ ! -f "${groups[0]}" ]]; then log::wg "No groups configured" return 0 fi - - log::section "Groups" - printf "\n %-20s %-35s %-8s %s\n" "NAME" "DESCRIPTION" "PEERS" "STATUS" - printf " %s\n" "$(printf '─%.0s' {1..75})" - + + local data + data=$(json::group_list_data "$groups_dir" "$(ctx::blocks)") + [[ -z "$data" ]] && log::wg "No groups configured" && return 0 + + # Measure column widths + local w_name=12 w_desc=16 while IFS="|" read -r name desc total blocked; do [[ -z "$name" ]] && continue - - local status_color="" status_str="active" - if [[ "$total" -gt 0 ]]; then - if [[ "$blocked" -eq "$total" ]]; then - status_color="\033[1;31m" - status_str="blocked" - elif [[ "$blocked" -gt 0 ]]; then - status_color="\033[1;33m" - status_str="blocked (${blocked}/${total})" - else - status_color="\033[1;32m" - status_str="active" - fi - fi - - local short_desc="${desc:0:33}" - [[ ${#desc} -gt 33 ]] && short_desc="${short_desc}..." - - local desc_col_width=35 - [[ "$desc" == "—" || -z "$desc" ]] && desc_col_width=37 - - printf " %-20s %-${desc_col_width}s %-8s %b\n" \ - "$name" "${short_desc:-—}" "$total" \ - "${status_color}${status_str}\033[0m" - - done < <(json::group_list_data "$groups_dir" "$(ctx::blocks)") - - printf "\n" + (( ${#name} > w_name )) && w_name=${#name} + local desc_len=${#desc} + [[ -z "$desc" ]] && desc_len=1 + (( desc_len > w_desc )) && w_desc=$desc_len + done <<< "$data" + (( w_name += 2 )) + (( w_desc += 2 )) + + log::section "Groups" + echo "" + + while IFS="|" read -r name desc total blocked; do + [[ -z "$name" ]] && continue + ui::group::list_row "$name" "$desc" "$total" "$blocked" "$w_name" "$w_desc" + done <<< "$data" + + echo "" } # ============================================ @@ -156,7 +147,7 @@ function cmd::group::list() { function cmd::group::show() { local name="" - + while [[ $# -gt 0 ]]; do case "$1" in --name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;; @@ -164,70 +155,72 @@ function cmd::group::show() { *) log::error "Unknown flag: $1"; return 1 ;; esac done - + [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1 group::require_exists "$name" || return 1 - + local group_file group_file="$(group::path "$name")" - + log::section "Group: ${name}" - + printf "\n" + local desc desc=$(json::get "$group_file" "desc") - printf "\n %-20s %s\n" "Description:" "${desc:-—}" - - # Load peers + ui::row "Description" "${desc:-—}" + local peers_list=() - mapfile -t peers_list < <(json::get "$group_file" "peers") - # Filter empty entries + mapfile -t peers_list < <(json::get "$group_file" "peers") || true local filtered=() for p in "${peers_list[@]:-}"; do [[ -n "$p" ]] && filtered+=("$p") done peers_list=("${filtered[@]:-}") local peer_count=${#peers_list[@]} - - [[ -z "${peers_list[0]}" ]] && peer_count=0 - - printf " %-20s %s\n" "Peers:" "$peer_count" - printf " %s\n" "$(printf '─%.0s' {1..50})" - + [[ -z "${peers_list[0]:-}" ]] && peer_count=0 + + local peer_word="peers" + [[ "$peer_count" -eq 1 ]] && peer_word="peer" + local valid_count=0 + for p in "${peers_list[@]}"; do + [[ -z "$p" ]] && continue + peers::require_exists "$p" > /dev/null 2>&1 && (( valid_count++ )) || true + done + local peer_word="peers" + [[ "$valid_count" -eq 1 ]] && peer_word="peer" + ui::row "Peers" "${valid_count} ${peer_word}" + printf "\n" + if [[ "$peer_count" -gt 0 ]]; then - printf "\n %-28s %-15s %-12s %s\n" "NAME" "IP" "RULE" "STATUS" - printf " %s\n" "$(printf '─%.0s' {1..65})" + # Measure name and IP widths + local w_name=16 w_ip=13 for peer_name in "${peers_list[@]}"; do [[ -z "$peer_name" ]] && continue - - # Skip if peer no longer exists + (( ${#peer_name} > w_name )) && w_name=${#peer_name} + done + (( w_name += 2 )) + + for peer_name in "${peers_list[@]}"; do + [[ -z "$peer_name" ]] && continue + if ! peers::require_exists "$peer_name" > /dev/null 2>&1; then - log::wg_warning "Peer '${peer_name}' no longer exists — skipping" + printf " \033[2m%-${w_name}s (no longer exists)\033[0m\n" "$peer_name" continue fi - - local ip rule status_str status_color + + local ip rule is_blocked ip=$(peers::get_ip "$peer_name") rule=$(peers::get_meta "$peer_name" "rule") - rule="${rule:-—}" - - if peers::is_blocked "$peer_name" 2>/dev/null; then - status_color="\033[1;31m" - status_str="blocked" - else - status_color="\033[1;32m" - status_str="active" - fi - - printf " %-28s %-15s %-12s %b\n" \ - "$peer_name" "0" "$rule" \ - "${status_str}\033[0m" + peers::is_blocked "$peer_name" 2>/dev/null && is_blocked="true" || is_blocked="false" + + ui::group::show_member_row "$peer_name" "$ip" "${rule:--}" \ + "$is_blocked" "$w_name" "$w_ip" done else - printf " —\n" + printf " \033[2m—\033[0m\n" fi - + printf "\n" - return 0 } # ============================================ diff --git a/commands/net.command.sh b/commands/net.command.sh index 811d623..2045a8e 100644 --- a/commands/net.command.sh +++ b/commands/net.command.sh @@ -77,7 +77,7 @@ function cmd::net::run() { function cmd::net::list() { local detailed=false filter_tag="" - + while [[ $# -gt 0 ]]; do case "$1" in --detailed) detailed=true; shift ;; @@ -86,62 +86,72 @@ function cmd::net::list() { *) log::error "Unknown flag: $1"; return 1 ;; esac done - + local net_file net_file="$(ctx::net)" - + if [[ ! -f "$net_file" ]]; then log::wg_warning "No services configured. Use 'wgctl net add' to add one." return 0 fi - - log::section "Network Services" - printf "\n %-20s %-16s %-6s %s\n" "NAME" "IP" "PORTS" "DESCRIPTION" - local divider - divider=$(printf '─%.0s' {1..72}) - printf " %s\n" "$divider" - - local found=false - while IFS="|" read -r name ip desc tags ports; do + + # Collect filtered data and build ports display per service + local filtered_data="" + while IFS="|" read -r name ip desc tags port_count; do [[ -z "$name" ]] && continue - - # Tag filter - if [[ -n "$filter_tag" ]]; then - [[ "$tags" != *"$filter_tag"* ]] && continue - fi - - found=true - local tag_display="" - [[ -n "$tags" ]] && tag_display=" \033[0;37m[${tags}]\033[0m" - - printf " %-20s %-16s %-6s %s%b\n" \ - "$name" "$ip" "${ports}p" "${desc:-—}" "$tag_display" - - if $detailed; then - local has_ports=false - # Show ports inline - while IFS="|" read -r ptype pname pport pproto pdesc; do - [[ "$ptype" != "port" ]] && continue - has_ports=true - local ann - ann=$(net::annotation "$ip" "$pport" "$pproto") - printf " \033[0;37m%-18s %s:%s%s\033[0m\n" \ - "${pname}" "$pport" "$pproto" \ - "${pdesc:+ # $pdesc}" - done < <(json::net_show "$net_file" "$name") - $has_ports && printf "\n" # newline after each service with ports - fi - + [[ -n "$filter_tag" && "$tags" != *"$filter_tag"* ]] && continue + + # Build ports display from json::net_show + local ports_display="" + while IFS="|" read -r ptype pname pport pproto pdesc; do + [[ "$ptype" != "port" ]] && continue + local port_str=":${pport}" + [[ -n "$pproto" && "$pproto" != "tcp" ]] && port_str="${port_str}/${pproto}" + ports_display+="${port_str}, " + done < <(json::net_show "$net_file" "$name") + ports_display="${ports_display%, }" + [[ -z "$ports_display" ]] && ports_display="-" + + filtered_data+="${name}|${ip}|${desc}|${tags}|${ports_display}"$'\n' done < <(json::net_list "$net_file") - - if ! $found; then + + [[ -z "$filtered_data" ]] && { [[ -n "$filter_tag" ]] && \ log::wg_warning "No services with tag: ${filter_tag}" || \ log::wg_warning "No services configured" - fi - - printf "\n" - return 0 + return 0 + } + + # Measure column widths + local w_name=12 w_ip=13 w_ports=16 + while IFS="|" read -r name ip desc tags ports; do + [[ -z "$name" ]] && continue + (( ${#name} > w_name )) && w_name=${#name} + (( ${#ip} > w_ip )) && w_ip=${#ip} + (( ${#ports} > w_ports )) && w_ports=${#ports} + done <<< "$filtered_data" + (( w_name += 2 )) + (( w_ip += 2 )) + (( w_ports += 2 )) + + log::section "Network Services" + echo "" + + while IFS="|" read -r name ip desc tags ports; do + [[ -z "$name" ]] && continue + ui::net::list_row "$name" "$ip" "$desc" "$tags" "$ports" \ + "$w_name" "$w_ip" "$w_ports" + + if $detailed; then + while IFS="|" read -r ptype pname pport pproto pdesc; do + [[ "$ptype" != "port" ]] && continue + ui::net::show_port_row "$pname" "$pport" "$pproto" "$pdesc" + done < <(json::net_show "$net_file" "$name") + echo "" + fi + done <<< "$filtered_data" + + echo "" } # ============================================ @@ -158,33 +168,31 @@ function cmd::net::show() { *) log::error "Unknown flag: $1"; return 1 ;; esac done - + [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1 net::require_exists "$name" || return 1 - + log::section "Service: ${name}" printf "\n" - + + local has_ports=false while IFS="|" read -r key val1 val2 val3 val4; do case "$key" in name) ui::row "Name" "$val1" ;; - ip) ui::row "IP" "$val1" ;; desc) ui::row "Description" "${val1:-—}" ;; tags) ui::row "Tags" "${val1:-—}" ;; + ip) ui::row "IP" "$val1" ;; port) - # val1=port_name val2=port val3=proto val4=desc - local ann - ann=$(net::annotation "$(json::net_resolve "$(ctx::net)" "$name")" \ - "$val2" "$val3" 2>/dev/null || true) - printf " %-20s \033[0;36m%s\033[0m %s:%s%s\n" \ - "${val1}:" "" "$val2" "$val3" \ - "${val4:+ # $val4}" + if ! $has_ports; then + printf " %-20s\n" "Ports:" + has_ports=true + fi + ui::net::show_port_row "$val1" "$val2" "$val3" "$val4" ;; esac done < <(json::net_show "$(ctx::net)" "$name") - + printf "\n" - return 0 } # ============================================ diff --git a/modules/ui/group.module.sh b/modules/ui/group.module.sh new file mode 100644 index 0000000..202a53f --- /dev/null +++ b/modules/ui/group.module.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +# ui/group.module.sh — rendering for groups + +# ====================================================== +# List rendering +# ====================================================== + +# ui::group::list_row +function ui::group::list_row() { + local name="${1:-}" desc="${2:-}" total="${3:-0}" blocked="${4:-0}" \ + w_name="${5:-16}" w_desc="${6:-30}" + + local name_pad desc_val desc_pad_n + name_pad=$(printf "%-${w_name}s" "$name") + desc_val="${desc:--}" + desc_pad_n=$(( w_desc - ${#desc_val} )) + [[ $desc_pad_n -lt 0 ]] && desc_pad_n=0 + + # Peer count — dim if zero + local peers_word="peers" + [[ "$total" -eq 1 ]] && peers_word="peer" + local peers_display + if [[ "$total" -eq 0 ]]; then + peers_display="\033[2m0 ${peers_word}\033[0m" + else + peers_display="${total} ${peers_word}" + fi + local peers_pad + peers_pad=$(printf "%-10s" "${total} ${peers_word}") + + # Status + local status_color status_str + IFS='|' read -r status_color status_str <<< "$(ui::group::status "$total" "$blocked")" + + local peers_str="${total} ${peers_word}" + local peers_pad_n=$(( 10 - ${#peers_str} )) + [[ $peers_pad_n -lt 0 ]] && peers_pad_n=0 + + if [[ "$total" -eq 0 ]]; then + printf " \033[2m%s %s%*s %s%*s %s\033[0m\n" \ + "$name_pad" "$desc_val" "$desc_pad_n" "" \ + "$peers_str" "$peers_pad_n" "" "inactive" + else + printf " %s %s%*s %s%*s %b%s\033[0m\n" \ + "$name_pad" "$desc_val" "$desc_pad_n" "" \ + "$peers_str" "$peers_pad_n" "" \ + "$status_color" "$status_str" + fi +} + +# Table version (kept for future display config) +function ui::group::list_header_table() { + printf "\n %-20s %-35s %-8s %s\n" "NAME" "DESCRIPTION" "PEERS" "STATUS" + printf " %s\n" "$(printf '─%.0s' {1..75})" +} + +function ui::group::list_row_table() { + local name="${1:-}" desc="${2:-}" total="${3:-0}" blocked="${4:-0}" + local status_color="" status_str="active" + if [[ "$total" -gt 0 ]]; then + if [[ "$blocked" -eq "$total" ]]; then + status_color="\033[1;31m"; status_str="blocked" + elif [[ "$blocked" -gt 0 ]]; then + status_color="\033[1;33m"; status_str="blocked (${blocked}/${total})" + else + status_color="\033[1;32m"; status_str="active" + fi + fi + printf " %-20s %-35s %-8s %b\n" \ + "$name" "${desc:-—}" "$total" \ + "${status_color}${status_str}\033[0m" +} + +# ====================================================== +# Show rendering +# ====================================================== + +# ui::group::show_member_row +function ui::group::show_member_row() { + local name="${1:-}" ip="${2:-}" rule="${3:--}" is_blocked="${4:-false}" \ + w_name="${5:-22}" w_ip="${6:-14}" + + local name_pad ip_pad rule_pad + name_pad=$(printf "%-${w_name}s" "$name") + ip_pad=$(printf "%-${w_ip}s" "$ip") + rule_pad=$(printf "%-12s" "${rule:--}") + + local status_color status_str + if [[ "$is_blocked" == "true" ]]; then + status_color="\033[1;31m"; status_str="blocked" + else + status_color="\033[1;32m"; status_str="active" + fi + + printf " %s %s \033[2mrule:\033[0m %s %b%s\033[0m\n" \ + "$name_pad" "$ip_pad" "$rule_pad" \ + "$status_color" "$status_str" +} + +# Table version +function ui::group::show_header_table() { + printf "\n %-28s %-15s %-12s %s\n" "NAME" "IP" "RULE" "STATUS" + printf " %s\n" "$(printf '─%.0s' {1..65})" +} + +function ui::group::show_member_row_table() { + local name="${1:-}" ip="${2:-}" rule="${3:--}" is_blocked="${4:-false}" + local status_color status_str + [[ "$is_blocked" == "true" ]] && { status_color="\033[1;31m"; status_str="blocked"; } \ + || { status_color="\033[1;32m"; status_str="active"; } + printf " %-28s %-15s %-12s %b\n" \ + "$name" "$ip" "${rule:--}" "${status_color}${status_str}\033[0m" +} + + +# ====================================================== +# Helpers +# ====================================================== + +# ui::group::status_color +# Returns color code and status string for a group +# Usage: IFS='|' read -r color str <<< "$(ui::group::status "$total" "$blocked")" +function ui::group::status() { + local total="${1:-0}" blocked="${2:-0}" + if [[ "$total" -eq 0 ]]; then + echo "\033[2;37m|inactive" + elif [[ "$blocked" -eq "$total" ]]; then + echo "\033[1;31m|blocked" + elif [[ "$blocked" -gt 0 ]]; then + echo "\033[1;33m|partial (${blocked}/${total})" + else + echo "\033[1;32m|active" + fi +} \ No newline at end of file diff --git a/modules/ui/net.module.sh b/modules/ui/net.module.sh new file mode 100644 index 0000000..4fb3ddd --- /dev/null +++ b/modules/ui/net.module.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# ui/net.module.sh — rendering for network services + +# ====================================================== +# List rendering +# ====================================================== + +# ui::net::list_row +function ui::net::list_row() { + local name="${1:-}" ip="${2:-}" desc="${3:-}" tags="${4:-}" ports="${5:-}" \ + w_name="${6:-16}" w_ip="${7:-14}" w_ports="${8:-20}" + + local name_pad ip_pad + name_pad=$(printf "%-${w_name}s" "$name") + ip_pad=$(printf "%-${w_ip}s" "$ip") + + local ports_pad_n=$(( w_ports - ${#ports} )) + [[ $ports_pad_n -lt 0 ]] && ports_pad_n=0 + + local tags_display="" + [[ -n "$tags" ]] && tags_display=" \033[2m[${tags}]\033[0m" + + printf " %s %s %s%*s %s%b\n" \ + "$name_pad" "$ip_pad" "$ports" "$ports_pad_n" "" \ + "${desc:--}" "$tags_display" +} + +# Table version (kept for future display config) +function ui::net::list_header_table() { + printf "\n %-20s %-16s %-6s %s\n" "NAME" "IP" "PORTS" "DESCRIPTION" + printf " %s\n" "$(printf '─%.0s' {1..72})" +} + +function ui::net::list_row_table() { + local name="${1:-}" ip="${2:-}" desc="${3:-}" tags="${4:-}" port_count="${5:-}" + local tag_display="" + [[ -n "$tags" ]] && tag_display=" \033[0;37m[${tags}]\033[0m" + printf " %-20s %-16s %-6s %s%b\n" \ + "$name" "$ip" "${port_count}p" "${desc:-—}" "$tag_display" +} + +# ====================================================== +# Show rendering +# ====================================================== + +# ui::net::show_port_row +function ui::net::show_port_row() { + local port_name="${1:-}" port="${2:-}" proto="${3:-}" desc="${4:-}" + local port_display=":${port}/${proto}" + local port_pad + port_pad=$(printf "%-14s" "$port_display") + if [[ -n "$desc" ]]; then + printf " %s \033[2m→\033[0m %-16s \033[2m# %s\033[0m\n" \ + "$port_pad" "$port_name" "$desc" + else + printf " %s \033[2m→\033[0m %s\n" \ + "$port_pad" "$port_name" + fi +} \ No newline at end of file