Utoljára aktív 1 month ago

gistfile1.txt Eredeti
1#!/usr/bin/env bash
2
3# ============================================
4# Lifecycle
5# ============================================
6
7function cmd::rule::on_load() {
8 flag::register --name
9 flag::register --desc
10 flag::register --block-ip
11 flag::register --allow-ip
12 flag::register --block-port
13 flag::register --allow-port
14 flag::register --remove-block-ip
15 flag::register --remove-allow-ip
16 flag::register --remove-block-port
17 flag::register --remove-allow-port
18 flag::register --peer
19 flag::register --peers
20 flag::register --dns-redirect
21 flag::register --color
22}
23
24# ============================================
25# Help
26# ============================================
27
28function cmd::rule::help() {
29 cat <<EOF
30Usage: wgctl rule <subcommand> [options]
31
32Manage firewall rules for peers.
33
34Subcommands:
35 list, ls List all rules
36 show Show rule details and assigned peers
37 add, new, create Create a new rule
38 update, edit Update a rule and re-apply to all peers
39 remove, rm, del Remove a rule
40 assign Assign a rule to a peer
41 unassign Remove rule from a peer
42 migrate Apply default rules to all unassigned peers
43
44Options for add/update:
45 --name <name> Rule name (e.g. guest, user, dev-01)
46 --desc <description> Human readable description
47 --allow-ip <ip/cidr> Allow IP or subnet (repeatable)
48 --allow-port <ip:port:proto> Allow specific port (repeatable)
49 --block-ip <ip/cidr> Block IP or subnet (repeatable)
50 --block-port <ip:port:proto> Block specific port (repeatable)
51 --dns-redirect Force DNS through Pi-hole
52 --remove-allow-ip <ip> Remove allow IP entry (update only)
53 --remove-allow-port <entry> Remove allow port entry (update only)
54 --remove-block-ip <ip> Remove block IP entry (update only)
55 --remove-block-port <entry> Remove block port entry (update only)
56
57Options for assign/unassign:
58 --name <rule> Rule name
59 --peer <peer> Peer name (e.g. phone-nuno)
60 --type <type> Peer device type (optional)
61
62Examples:
63 wgctl rule list
64 wgctl rule show --name guest
65 wgctl rule add --name dev-01 --desc "Dev VM only" --allow-ip 10.0.0.50 --block-ip 10.0.0.0/24
66 wgctl rule update --name user --block-port 10.0.0.100:8006:tcp
67 wgctl rule update --name user --remove-block-ip 10.0.0.99
68 wgctl rule assign --name dev-01 --peer laptop-nuno
69 wgctl rule unassign --peer laptop-nuno --type laptop
70 wgctl rule migrate
71EOF
72}
73
74# ============================================
75# Run
76# ============================================
77
78function cmd::rule::run() {
79 local subcmd="${1:-help}"
80 shift || true
81
82 case "$subcmd" in
83 list|ls) cmd::rule::list "$@" ;;
84 show) cmd::rule::show "$@" ;;
85 add|new|create) cmd::rule::add "$@" ;;
86 update|edit) cmd::rule::update "$@" ;;
87 remove|rm|del|delete) cmd::rule::remove "$@" ;;
88 assign) cmd::rule::assign "$@" ;;
89 unassign) cmd::rule::unassign "$@" ;;
90 migrate) cmd::rule::migrate "$@" ;;
91 reapply) cmd::rule::reapply "$@" ;;
92 inspect) cmd::rule::inspect "$@" ;;
93 help) cmd::rule::help ;;
94 *)
95 log::error "Unknown subcommand: '${subcmd}'"
96 cmd::rule::help
97 return 1
98 ;;
99 esac
100}
101
102# ============================================
103# List
104# ============================================
105
106function cmd::rule::_pad() {
107 local text="$1" width="$2"
108 local visible
109 visible=$(printf "%s" "$text" | sed 's/\x1b\[[0-9;]*m//g')
110 local visible_len=${#visible}
111 local byte_len=${#text}
112 local extra=$(( byte_len - visible_len ))
113 printf "%-$(( width + extra ))s" "$text"
114}
115
116function cmd::rule::list() {
117 local rules_dir
118 rules_dir="$(ctx::rules)"
119
120 local rules=("${rules_dir}"/*.rule)
121 if [[ ! -f "${rules[0]}" ]]; then
122 log::wg "No rules configured"
123 return 0
124 fi
125
126 log::section "Firewall Rules"
127 printf "\n %-20s %-40s %-8s %-8s %s\n" \
128 "NAME" "DESCRIPTION" \
129 "$(ui::center "ALLOWS" 8)" \
130 "$(ui::center "BLOCKS" 8)" \
131 "PEERS"
132 local divider
133 divider=$(printf '─%.0s' {1..88})
134 printf " %s\n" "$divider"
135
136 local printing_base=false
137 local current_group=""
138
139 while IFS="|" read -r name desc n_allows n_blocks peer_count extends is_base group; do
140 [[ -z "$name" ]] && continue
141
142 # Base rules section header
143 if [[ "$is_base" == "True" && "$printing_base" == "false" ]]; then
144 local bdashes
145 bdashes=$(printf '─%.0s' {1..74})
146 printf "\n \033[0;37m── Base Rules \033[0m%s\n" "$bdashes"
147 printing_base=true
148 current_group="" # reset group tracking for base section
149 fi
150
151 # Group header — only for non-base rules
152 if [[ "$is_base" == "False" && "$group" != "$current_group" ]]; then
153 if [[ -n "$group" ]]; then
154 printf "\n \033[0;36m▸ %s\033[0m\n" "$group"
155 elif [[ -n "$current_group" ]]; then
156 # Switching from grouped back to ungrouped
157 printf "\n"
158 fi
159 current_group="$group"
160 fi
161
162 local short_desc="${desc:0:35}"
163 [[ ${#desc} -gt 35 ]] && short_desc="${short_desc}..."
164
165 local desc_col_width=40
166 [[ "${short_desc:-—}" == "—" ]] && desc_col_width=42
167
168 printf " %-20s %-${desc_col_width}s %-8s %-8s %s\n" \
169 "$name" "${short_desc:-—}" \
170 "$(ui::center "$n_allows" 8)" \
171 "$(ui::center "$n_blocks" 8)" \
172 "${peer_count} peers"
173
174 # Print extends IMMEDIATELY after the rule row
175 if [[ -n "$extends" ]]; then
176 IFS=',' read -ra extend_list <<< "$extends"
177 for base in "${extend_list[@]}"; do
178 [[ -z "$base" ]] && continue
179 printf " \033[0;37m ↳ %s\033[0m\n" "$base"
180 done
181 printf "\n" # blank line after rule with extends
182 fi
183
184 done < <(json::rule_list_data "$rules_dir" "$(ctx::meta)")
185
186 printf "\n"
187}
188
189# ============================================
190# Show
191# ============================================
192
193function cmd::rule::show() {
194 local name="" show_peers=false color=false
195
196 while [[ $# -gt 0 ]]; do
197 case "$1" in
198 --name) name="$2"; shift 2 ;;
199 --peers) show_peers=true; shift ;;
200 --color) color=true; shift ;;
201 --help) cmd::rule::help; return ;;
202 *) log::error "Unknown flag: $1"; return 1 ;;
203 esac
204 done
205
206 [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
207 rule::require_exists "$name" || return 1
208
209 local rule_file
210 rule_file="$(ctx::rule::path "${name}.rule")"
211
212 # Precompute peers before any operations
213 local peer_list=()
214 mapfile -t peer_list < <(peers::with_rule "$name")
215 local peer_count=${#peer_list[@]}
216
217 log::section "Rule: ${name}"
218
219 local desc dns_redirect
220 desc=$(json::get "$rule_file" "desc")
221 dns_redirect=$(json::get "$rule_file" "dns_redirect")
222
223 printf "\n"
224 ui::row "Description" "${desc:-—}"
225 ui::row "DNS Redirect" "${dns_redirect:-false}"
226
227 # Load all entries
228 local allow_ports allow_ips block_ips block_ports
229 allow_ports=$(json::get "$rule_file" "allow_ports")
230 allow_ips=$(json::get "$rule_file" "allow_ips")
231 block_ips=$(json::get "$rule_file" "block_ips")
232 block_ports=$(json::get "$rule_file" "block_ports")
233
234 # Allow section
235 if [[ -n "$allow_ports" || -n "$allow_ips" ]]; then
236 cmd::rule::_show_section "Allow" "green" "$color"
237 cmd::rule::_show_entries "Ports" "+" "$allow_ports" "$color" "green"
238 cmd::rule::_show_entries "IPs" "+" "$allow_ips" "$color" "green"
239 fi
240
241 # Block section
242 if [[ -n "$block_ips" || -n "$block_ports" ]]; then
243 cmd::rule::_show_section "Block" "red" "$color"
244 cmd::rule::_show_entries "IPs" "-" "$block_ips" "$color" "red"
245 cmd::rule::_show_entries "Ports" "-" "$block_ports" "$color" "red"
246 fi
247
248 if [[ -z "$allow_ports" && -z "$allow_ips" && -z "$block_ips" && -z "$block_ports" ]]; then
249 printf "\n"
250 ui::row "Access" "full (no restrictions)"
251 fi
252
253 # Peers section
254 cmd::rule::_show_section "Peers" "white" false
255 ui::row "Assigned" "$peer_count"
256
257 if $show_peers && [[ $peer_count -gt 0 ]]; then
258 printf "\n"
259 for peer_name in "${peer_list[@]}"; do
260 local ip
261 ip=$(peers::get_ip "$peer_name")
262 printf " %-28s %s\n" "$peer_name" "$ip"
263 done
264 fi
265
266 printf "\n"
267}
268
269# function cmd::rule::show() {
270# local name="" show_peers=false
271
272# while [[ $# -gt 0 ]]; do
273# case "$1" in
274# --name) name="$2"; shift 2 ;;
275# --peers) show_peers=true; shift ;;
276# --help) cmd::rule::help; return ;;
277# *) log::error "Unknown flag: $1"; return 1 ;;
278# esac
279# done
280
281# if [[ -z "$name" ]]; then
282# log::error "Missing required flag: --name"
283# return 1
284# fi
285
286# rule::require_exists "$name" || return 1
287
288# local rule_file
289# rule_file="$(ctx::rule::path "${name}.rule")"
290
291# log::section "Rule: ${name}"
292
293# local desc dns_redirect
294# desc=$(json::get "$rule_file" "desc")
295# dns_redirect=$(json::get "$rule_file" "dns_redirect")
296
297# printf "\n"
298# ui::row "Description" "${desc:-—}"
299# ui::row "DNS Redirect" "${dns_redirect:-false}"
300
301# # Allow ports
302# local allow_ports
303# allow_ports=$(json::get "$rule_file" "allow_ports")
304# if [[ -n "$allow_ports" ]]; then
305# printf "\n Allow Ports:\n"
306# ui::print_list "+" "$allow_ports"
307# fi
308
309# # Allow IPs
310# local allow_ips
311# allow_ips=$(json::get "$rule_file" "allow_ips")
312# if [[ -n "$allow_ips" ]]; then
313# printf "\n Allow IPs:\n"
314# while IFS= read -r ip; do
315# printf " + %s\n" "$ip"
316# done <<< "$allow_ips"
317# fi
318
319# # Block IPs
320# local block_ips
321# block_ips=$(json::get "$rule_file" "block_ips")
322# if [[ -n "$block_ips" ]]; then
323# printf "\n Block IPs:\n"
324# while IFS= read -r ip; do
325# printf " - %s\n" "$ip"
326# done <<< "$block_ips"
327# fi
328
329# # Block ports
330# local block_ports
331# block_ports=$(json::get "$rule_file" "block_ports")
332# if [[ -n "$block_ports" ]]; then
333# printf "\n Block Ports:\n"
334# while IFS= read -r entry; do
335# printf " - %s\n" "$entry"
336# done <<< "$block_ports"
337# fi
338
339# # Precompute peers before any other operations
340# local peer_list=()
341# mapfile -t peer_list < <(peers::with_rule "$name")
342# local peer_count=${#peer_list[@]}
343
344# # Peer count — always shown
345# printf "\n %-20s %s\n" "Assigned Peers:" "$peer_count"
346# printf " %s\n" "$(printf '─%.0s' {1..40})"
347
348# # Peer details — only with --peers flag
349# if $show_peers && [[ $peer_count -gt 0 ]]; then
350# for peer_name in "${peer_list[@]}"; do
351# local ip
352# ip=$(peers::get_ip "$peer_name")
353# printf " %-28s %s\n" "$peer_name" "$ip"
354# done
355# fi
356
357# printf "\n"
358# }
359
360# ============================================
361# Inspect
362# ============================================
363
364function cmd::rule::inspect() {
365 local name=""
366
367 while [[ $# -gt 0 ]]; do
368 case "$1" in
369 --name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
370 --help) cmd::rule::help; return ;;
371 *) log::error "Unknown flag: $1"; return 1 ;;
372 esac
373 done
374
375 [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
376 rule::require_exists "$name" || return 1
377
378 log::section "Rule Inspect: ${name}"
379
380 local prev_section=""
381 while IFS="|" read -r section key value; do
382 [[ -z "$section" ]] && continue
383
384 # Print section header when section changes
385 if [[ "$section" != "$prev_section" ]]; then
386 case "$section" in
387 own)
388 cmd::rule::_show_section "Own Rules" ;;
389 dns)
390 cmd::rule::_show_section "DNS" ;;
391 resolved)
392 cmd::rule::_show_section "Resolved (applied to peers)" ;;
393 inherited:*)
394 local base_name="${section#inherited:}"
395 cmd::rule::_show_section "Inherited: ${base_name}" ;;
396 esac
397 prev_section="$section"
398 fi
399
400 case "$key" in
401 allow_ip|allow_port)
402 printf " \033[0;32m+\033[0m %s\n" "$value" ;;
403 block_ip|block_port)
404 printf " \033[0;31m-\033[0m %s\n" "$value" ;;
405 dns_redirect)
406 printf " Redirect all DNS → %s\n" "$(config::dns)" ;;
407 esac
408
409 done < <(json::rule_inspect "$(ctx::rules)" "$name")
410
411 printf "\n"
412}
413
414# ============================================
415# Add
416# ============================================
417
418function cmd::rule::add() {
419 local name="" desc=""
420 local allow_ips=() block_ips=() block_ports=()
421 local dns_redirect=false
422
423 while [[ $# -gt 0 ]]; do
424 case "$1" in
425 --name) name="$2"; shift 2 ;;
426 --desc) desc="$2"; shift 2 ;;
427 --allow-ip) allow_ips+=("$2"); shift 2 ;;
428 --block-ip) block_ips+=("$2"); shift 2 ;;
429 --block-port) block_ports+=("$2"); shift 2 ;;
430 --dns-redirect) dns_redirect=true; shift ;;
431 --help) cmd::rule::help; return ;;
432 *)
433 log::error "Unknown flag: $1"
434 return 1
435 ;;
436 esac
437 done
438
439 if [[ -z "$name" ]]; then
440 log::error "Missing required flag: --name"
441 return 1
442 fi
443
444 if rule::exists "$name"; then
445 log::error "Rule already exists: ${name}"
446 return 1
447 fi
448
449 local rule_file
450 rule_file="$(ctx::rule::path "${name}.rule")"
451
452 local allow_str block_str port_str
453
454 allow_str=$(IFS=','; echo "${allow_ips[*]}")
455 block_str=$(IFS=','; echo "${block_ips[*]}")
456 port_str=$(IFS=','; echo "${block_ports[*]}")
457
458 json::create_rule "$rule_file" "$name" "$desc" \
459 "$($dns_redirect && echo true || echo false)" \
460 "$allow_str" "$block_str" "$port_str" || return 1
461
462 log::wg_success "Rule created: ${name}"
463}
464
465# ============================================
466# Update
467# ============================================
468
469function cmd::rule::update() {
470 local name="" desc=""
471 local allow_ips=() block_ips=() block_ports=()
472 local allow_ports=()
473 local rm_allow_ips=() rm_block_ips=() rm_block_ports=() rm_allow_ports=()
474 local dns_redirect=""
475
476 while [[ $# -gt 0 ]]; do
477 case "$1" in
478 --name) name="$2"; shift 2 ;;
479 --desc) desc="$2"; shift 2 ;;
480 --allow-ip) allow_ips+=("$2"); shift 2 ;;
481 --block-ip) block_ips+=("$2"); shift 2 ;;
482 --block-port) block_ports+=("$2"); shift 2 ;;
483 --allow-port) allow_ports+=("$2"); shift 2 ;;
484 --remove-allow-ip) rm_allow_ips+=("$2"); shift 2 ;;
485 --remove-block-ip) rm_block_ips+=("$2"); shift 2 ;;
486 --remove-block-port) rm_block_ports+=("$2"); shift 2 ;;
487 --remove-allow-port) rm_allow_ports+=("$2"); shift 2 ;;
488 --dns-redirect) dns_redirect=true; shift ;;
489 --help) cmd::rule::help; return ;;
490 *)
491 log::error "Unknown flag: $1"
492 return 1
493 ;;
494 esac
495 done
496
497 if [[ -z "$name" ]]; then
498 log::error "Missing required flag: --name"
499 return 1
500 fi
501
502 rule::require_exists "$name" || return 1
503
504 local rule_file
505 rule_file="$(ctx::rule::path "${name}.rule")"
506
507 # Update desc and dns_redirect
508 [[ -n "$desc" ]] && json::set "$rule_file" "desc" "\"$desc\""
509 [[ -n "$dns_redirect" ]] && json::set "$rule_file" "dns_redirect" "true"
510
511 # Add entries
512 for ip in "${allow_ips[@]}"; do json::append "$rule_file" "allow_ips" "$ip"; done
513 for ip in "${block_ips[@]}"; do json::append "$rule_file" "block_ips" "$ip"; done
514 for p in "${block_ports[@]}"; do json::append "$rule_file" "block_ports" "$p"; done
515 for p in "${allow_ports[@]}"; do json::append "$rule_file" "allow_ports" "$p"; done
516
517 # Remove entries
518 for ip in "${rm_allow_ips[@]}"; do json::remove "$rule_file" "allow_ips" "$ip"; done
519 for ip in "${rm_block_ips[@]}"; do json::remove "$rule_file" "block_ips" "$ip"; done
520 for p in "${rm_block_ports[@]}"; do json::remove "$rule_file" "block_ports" "$p"; done
521 for p in "${rm_allow_ports[@]}"; do json::remove "$rule_file" "allow_ports" "$p"; done
522
523 log::wg_success "Rule updated: ${name}"
524
525 # Re-apply to all assigned peers
526 rule::reapply_all "$name"
527}
528
529# ============================================
530# Remove
531# ============================================
532
533function cmd::rule::remove() {
534 local name="" force=false
535
536 while [[ $# -gt 0 ]]; do
537 case "$1" in
538 --name) name="$2"; shift 2 ;;
539 --force) force=true; shift ;;
540 --help) cmd::rule::help; return ;;
541 *)
542 log::error "Unknown flag: $1"
543 return 1
544 ;;
545 esac
546 done
547
548 if [[ -z "$name" ]]; then
549 log::error "Missing required flag: --name"
550 return 1
551 fi
552
553 rule::require_exists "$name" || return 1
554
555 # Check for assigned peers
556
557 local peer_list=()
558 mapfile -t peer_list < <(peers::with_rule "$name")
559 local peer_count=${#peer_list[@]}
560
561 if [[ "$peer_count" -gt 0 ]]; then
562 log::error "Rule '${name}' is assigned to ${peer_count} peer(s) — unassign first or use --force"
563 $force || return 1
564
565 # Force: unassign from all peers
566 for peer in "${peer_list[@]}"; do
567 local ip
568 ip=$(peers::get_ip "$peer")
569 rule::unapply "$name" "$ip"
570 done
571 fi
572
573 rm -f "$(ctx::rule::path "${name}.rule")"
574 log::wg_success "Rule removed: ${name}"
575}
576
577# ============================================
578# Assign
579# ============================================
580
581function cmd::rule::assign() {
582 local name="" peer="" type=""
583 while [[ $# -gt 0 ]]; do
584 case "$1" in
585 --name) name="$2"; shift 2 ;;
586 --peer) peer="$2"; shift 2 ;;
587 --type) type="$2"; shift 2 ;;
588 --help) cmd::rule::help; return ;;
589 *)
590 log::error "Unknown flag: $1"
591 return 1
592 ;;
593 esac
594 done
595
596 if [[ -z "$name" || -z "$peer" ]]; then
597 log::error "Missing required flags: --name and --peer"
598 return 1
599 fi
600
601 rule::require_exists "$name" || return 1
602 rule::require_assignable "$name" || return 1
603
604 # Support --type for peer resolution
605 peer=$(peers::resolve_and_require "$peer" "$type") || return 1
606
607 # Unapply existing rule first if any
608 local existing_rule
609 existing_rule=$(peers::get_meta "$peer" "rule")
610
611 local ip
612 ip=$(peers::get_ip "$peer")
613 log::debug "rule::assign: peer=$peer ip=$ip"
614 [[ -z "$ip" ]] && log::error "Could not resolve IP for: $peer" && return 1
615
616 log::debug "assign: peer=$peer ip=$ip clients=$(ctx::clients)"
617
618 if [[ -n "$existing_rule" && "$existing_rule" != "$name" ]]; then
619 rule::unapply "$existing_rule" "$ip"
620 log::wg "Removed existing rule '${existing_rule}' from: ${peer}"
621 fi
622
623 rule::apply "$name" "$ip"
624
625 log::wg_success "Assigned rule '${name}' to: ${peer}"
626}
627
628# ============================================
629# Unassign
630# ============================================
631
632function cmd::rule::unassign() {
633 local peer="" type=""
634
635 while [[ $# -gt 0 ]]; do
636 case "$1" in
637 --peer) peer="$2"; shift 2 ;;
638 --type) type="$2"; shift 2 ;;
639 --help) cmd::rule::help; return ;;
640 *)
641 log::error "Unknown flag: $1"
642 return 1
643 ;;
644 esac
645 done
646
647 if [[ -z "$peer" ]]; then
648 log::error "Missing required flag: --peer"
649 return 1
650 fi
651
652 peer=$(peers::resolve_and_require "$peer" "$type") || return 1
653
654 local existing_rule
655 existing_rule=$(peers::get_meta "$peer" "rule")
656
657 if [[ -z "$existing_rule" ]]; then
658 log::wg_warning "Peer '${peer}' has no assigned rule"
659 return 0
660 fi
661
662 local ip
663 ip=$(peers::get_ip "$peer")
664 rule::unapply "$existing_rule" "$ip"
665
666 log::wg_success "Unassigned rule from: ${peer}"
667}
668
669# ============================================
670# Migrate Rules
671# ============================================
672
673function cmd::rule::migrate() {
674 log::section "Migrating peers to default rules"
675
676 # Write migration plan to temp file to avoid fd conflicts
677 local tmp
678 tmp=$(mktemp)
679
680 while IFS= read -r peer_name; do
681 local existing
682 existing=$(peers::get_meta "$peer_name" "rule")
683 [[ -n "$existing" ]] && continue
684 local default_rule
685 default_rule=$(peers::default_rule "$peer_name")
686 rule::exists "$default_rule" || continue
687 local ip
688 ip=$(peers::get_ip "$peer_name")
689 echo "${peer_name} ${default_rule} ${ip}" >> "$tmp"
690 done < <(peers::all)
691
692 local count=0
693 local lines
694 mapfile -t lines < "$tmp"
695
696 for line in "${lines[@]}"; do
697 IFS=" " read -r peer_name default_rule ip <<< "$line"
698 rule::apply "$default_rule" "$ip" "$peer_name" </dev/null
699 (( count++ )) || true
700 done
701
702 cat "$tmp"
703
704 rm -f "$tmp"
705 log::wg_success "Migrated ${count} peers"
706}
707
708# ============================================
709# Reapply Rule
710# ============================================
711
712function cmd::rule::reapply() {
713 local name=""
714 while [[ $# -gt 0 ]]; do
715 case "$1" in
716 --name) name="$2"; shift 2 ;;
717 *) log::error "Unknown flag: $1"; return 1 ;;
718 esac
719 done
720 [[ -z "$name" ]] && log::error "Missing --name" && return 1
721 rule::require_exists "$name" || return 1
722 rule::reapply_all "$name"
723 log::wg_success "Rule '${name}' reapplied"
724}
725
726# ============================================
727# Show helpers
728# ============================================
729
730function cmd::rule::_show_section() {
731 local title="${1:-}" color="${2:-white}" use_color="${3:-false}"
732 local color_code=""
733 if $use_color; then
734 case "$color" in
735 green) color_code="\033[0;32m" ;;
736 red) color_code="\033[0;31m" ;;
737 esac
738 fi
739 printf "\n ${color_code}── %s ──────────────────────────────────\033[0m\n" "$title"
740}
741
742function cmd::rule::_show_entries() {
743 local label="${1:-}" prefix="${2:-}" entries="${3:-}"
744 local use_color="${4:-false}" color="${5:-white}"
745 [[ -z "$entries" ]] && return 0
746
747 local color_code="" reset=""
748 if $use_color; then
749 case "$color" in
750 green) color_code="\033[0;32m" ;;
751 red) color_code="\033[0;31m" ;;
752 esac
753 reset="\033[0m"
754 fi
755
756 printf " %-8s" "${label}:"
757 local first=true
758 while IFS= read -r entry; do
759 [[ -z "$entry" ]] && continue
760 if $first; then
761 printf "%b%s %s%b\n" "$color_code" "$prefix" "$entry" "$reset"
762 first=false
763 else
764 printf " %b%s %s%b\n" "$color_code" "$prefix" "$entry" "$reset"
765 fi
766 done <<< "$entries"
767}