@@ -280,16 +280,6 @@ color_for_list() {
280280# Formatting helpers
281281# ---------------------------------------------------------------------------
282282
283- truncate_str () {
284- local s=" $1 " max=" $2 "
285- local len=${# s}
286- if [ " $len " -le " $max " ]; then
287- printf ' %s' " $s "
288- else
289- printf ' %s...' " ${s: 0: max-3} "
290- fi
291- }
292-
293283# match_summary <rule-json>: "key1: pat1, key2: pat2" or "-".
294284match_summary () {
295285 local rule=" $1 "
@@ -314,47 +304,204 @@ rule_tool() {
314304 jq -r ' .tool // ""' <<< " $rule"
315305}
316306
307+ # Detect terminal width. Honors COLUMNS override (tests can set it).
308+ # Falls back to `tput cols`, then 120.
309+ term_width () {
310+ if [ -n " ${COLUMNS:- } " ] && [ " ${COLUMNS} " -gt 0 ] 2> /dev/null; then
311+ printf ' %s' " $COLUMNS "
312+ return
313+ fi
314+ local w=" "
315+ if command -v tput > /dev/null 2>&1 ; then
316+ w=" $( tput cols 2> /dev/null || true) "
317+ fi
318+ if [ -n " $w " ] && [ " $w " -gt 0 ] 2> /dev/null; then
319+ printf ' %s' " $w "
320+ else
321+ printf ' 120'
322+ fi
323+ }
324+
325+ # wrap_text <text> <width> [<out-var-array-name>]
326+ # Populates the named bash array with one element per wrapped line.
327+ # Empty input yields a single empty line (so every row still renders).
328+ # Prefers break points at space / `|` / `,` within the last quarter of <width>.
329+ wrap_text () {
330+ local text=" $1 " width=" $2 " arr_name=" ${3:- __wrap_out} "
331+ # Reset the output array to empty.
332+ eval " $arr_name =()"
333+ if [ " $width " -le 0 ]; then
334+ eval " $arr_name +=(\"\$ text\" )"
335+ return
336+ fi
337+ local remaining=" $text "
338+ while [ -n " $remaining " ]; do
339+ local len=${# remaining}
340+ if [ " $len " -le " $width " ]; then
341+ eval " $arr_name +=(\"\$ remaining\" )"
342+ break
343+ fi
344+ # Hunt for a friendly break within the last quarter of the width.
345+ local lookback=$(( width / 4 ))
346+ [ " $lookback " -lt 1 ] && lookback=1
347+ local low=$(( width - lookback ))
348+ [ " $low " -lt 1 ] && low=1
349+ local break_pos=-1
350+ local ch i
351+ for (( i = width - 1 ; i >= low; i-- )) ; do
352+ ch=" ${remaining: $i : 1} "
353+ case " $ch " in
354+ ' ' |' |' |' ,' ) break_pos=$i ; break ;;
355+ esac
356+ done
357+ local chunk rest_start
358+ if [ " $break_pos " -ge 0 ]; then
359+ # Keep the friendly break character on the current chunk so visual
360+ # continuity is preserved (e.g. the trailing ",").
361+ chunk=" ${remaining: 0: break_pos+1} "
362+ rest_start=$(( break_pos + 1 ))
363+ # Trim leading whitespace from the continuation so indentation is not
364+ # doubled-up.
365+ while [ " $rest_start " -lt " $len " ] && [ " ${remaining: $rest_start : 1} " = " " ]; do
366+ rest_start=$(( rest_start + 1 ))
367+ done
368+ else
369+ chunk=" ${remaining: 0: $width } "
370+ rest_start=$width
371+ fi
372+ eval " $arr_name +=(\"\$ chunk\" )"
373+ remaining=" ${remaining: $rest_start } "
374+ done
375+ # Text was originally empty; emit one empty cell.
376+ local __wrap_len
377+ # Use braces around the variable name so shellcheck sees this as a variable,
378+ # not an array index expansion: ${name}[@] -> ${<resolved>}[@] -> array ref.
379+ eval " __wrap_len=\$ {#${arr_name} [@]}"
380+ if [ " $__wrap_len " -eq 0 ]; then
381+ eval " $arr_name +=('')"
382+ fi
383+ }
384+
317385# ---------------------------------------------------------------------------
318386# Renderers
319387# ---------------------------------------------------------------------------
320388
389+ # Column layout for the flat renderer:
390+ # scope(8) list(6) source(9) #(4) tool(W_tool) match-summary(W_match) reason(rest)
391+ # Fixed columns sum: 8+1+6+1+9+1+4+2 = 32 chars (with single-space gaps and
392+ # the double-space after #). Then tool, match, reason share the rest.
321393render_flat_table () {
322394 local use_color=0
323395 tty_color && use_color=1
324396 local reset=" "
325397 [ " $use_color " -eq 1 ] && reset=' \033[0m'
326- printf ' %-8s %-6s %-9s %4s %-18s %-40s %s\n' \
327- ' scope' ' list' ' source' ' #' ' tool' ' match-summary' ' reason'
328- printf ' %s\n' ' ------------------------------------------------------------------------------------------------------'
329- local n i entry scope list source idx tool match_sum reason color
398+
399+ local total
400+ total=" $( term_width) "
401+
402+ # Fixed lead columns consume this many chars (see above).
403+ local fixed=32
404+
405+ local n i entry scope list source idx tool match_sum reason
330406 n=" $( jq -r ' length' <<< " $ANNOTATED" ) "
407+
408+ # First pass: widest tool.
409+ local widest_tool=4 t_len
410+ for (( i = 0 ; i < n; i++ )) ; do
411+ entry=" $( jq -c " .[${i} ]" <<< " $ANNOTATED" ) "
412+ tool=" $( rule_tool " $( jq -c ' .rule' <<< " $entry" ) " ) "
413+ [ -z " $tool " ] && tool=" -"
414+ t_len=${# tool}
415+ [ " $t_len " -gt " $widest_tool " ] && widest_tool=" $t_len "
416+ done
417+ local W_tool=$widest_tool
418+ [ " $W_tool " -lt 12 ] && W_tool=12
419+ [ " $W_tool " -gt 20 ] && W_tool=20
420+
421+ # Remaining budget for match + reason.
422+ local remaining=$(( total - fixed - W_tool - 1 ))
423+ [ " $remaining " -lt 50 ] && remaining=50
424+ local W_match=$(( remaining * 60 / 100 ))
425+ local W_reason=$(( remaining - W_match - 1 ))
426+ [ " $W_match " -lt 30 ] && W_match=30
427+ [ " $W_reason " -lt 20 ] && W_reason=20
428+
429+ local fmt_hdr=" %-8s %-6s %-9s %4s %-${W_tool} s %-${W_match} s %s"
430+ # shellcheck disable=SC2059
431+ printf " $fmt_hdr \n" ' scope' ' list' ' source' ' #' ' tool' ' match-summary' ' reason'
432+
433+ local dash_total=$(( fixed + W_tool + 1 + W_match + 1 + W_reason ))
434+ local dashes=" "
435+ local d
436+ for (( d = 0 ; d < dash_total; d++ )) ; do dashes=" ${dashes} -" ; done
437+ printf ' %s\n' " $dashes "
438+
439+ local match_lines=() reason_lines=()
440+ local max_lines li msum_line reason_line color pad_match pad_tool
331441 for (( i = 0 ; i < n; i++ )) ; do
332442 entry=" $( jq -c " .[${i} ]" <<< " $ANNOTATED" ) "
333443 scope=" $( jq -r ' .scope' <<< " $entry" ) "
334444 list=" $( jq -r ' .list' <<< " $entry" ) "
335445 source=" $( jq -r ' .source' <<< " $entry" ) "
336446 idx=" $( jq -r ' .index' <<< " $entry" ) "
337- tool=" $( truncate_str " $( rule_tool " $( jq -c ' .rule' <<< " $entry" ) " ) " 18 ) "
447+ tool=" $( rule_tool " $( jq -c ' .rule' <<< " $entry" ) " ) "
338448 [ -z " $tool " ] && tool=" -"
339- match_sum=" $( truncate_str " $( match_summary " $( jq -c ' .rule' <<< " $entry" ) " ) " 40 ) "
449+ match_sum=" $( match_summary " $( jq -c ' .rule' <<< " $entry" ) " ) "
340450 reason=" $( rule_reason " $( jq -c ' .rule' <<< " $entry" ) " ) "
451+
452+ wrap_text " $match_sum " " $W_match " match_lines
453+ wrap_text " $reason " " $W_reason " reason_lines
454+
455+ max_lines=${# match_lines[@]}
456+ [ " ${# reason_lines[@]} " -gt " $max_lines " ] && max_lines=${# reason_lines[@]}
457+
341458 if [ " $use_color " -eq 1 ]; then
342459 color=" $( color_for_list " $list " ) "
343- printf " ${color} %-8s %-6s %-9s %4s %-18s %-40s %s${reset} \n" \
344- " $scope " " $list " " $source " " $idx " " $tool " " $match_sum " " $reason "
345460 else
346- printf ' %-8s %-6s %-9s %4s %-18s %-40s %s\n' \
347- " $scope " " $list " " $source " " $idx " " $tool " " $match_sum " " $reason "
461+ color=" "
348462 fi
463+
464+ # Wrap tool only on the first line; padding on continuations.
465+ printf -v pad_tool ' %*s' " $W_tool " ' '
466+
467+ for (( li = 0 ; li < max_lines; li++ )) ; do
468+ msum_line=" ${match_lines[$li]:- } "
469+ reason_line=" ${reason_lines[$li]:- } "
470+ if [ " $li " -eq 0 ]; then
471+ if [ " $use_color " -eq 1 ]; then
472+ printf " ${color} %-8s %-6s %-9s %4s %-${W_tool} s %-${W_match} s %s${reset} \n" \
473+ " $scope " " $list " " $source " " $idx " " $tool " " $msum_line " " $reason_line "
474+ else
475+ printf " %-8s %-6s %-9s %4s %-${W_tool} s %-${W_match} s %s\n" \
476+ " $scope " " $list " " $source " " $idx " " $tool " " $msum_line " " $reason_line "
477+ fi
478+ else
479+ # Continuation line: pad scope/list/source/#/tool with spaces so the
480+ # wrapped tail sits under its column.
481+ if [ " $use_color " -eq 1 ]; then
482+ printf " ${color} %-8s %-6s %-9s %4s %s %-${W_match} s %s${reset} \n" \
483+ ' ' ' ' ' ' ' ' " $pad_tool " " $msum_line " " $reason_line "
484+ else
485+ printf " %-8s %-6s %-9s %4s %s %-${W_match} s %s\n" \
486+ ' ' ' ' ' ' ' ' " $pad_tool " " $msum_line " " $reason_line "
487+ fi
488+ fi
489+ done
349490 done
350491}
351492
493+ # Column layout for the grouped renderer:
494+ # #(4) tool(W_tool) match-summary(W_match) reason(rest)
495+ # Lead columns (before match) consume 4 + 2 + W_tool + 1 chars.
352496render_grouped_table () {
353497 local use_color=0
354498 tty_color && use_color=1
355499 local reset=" "
356500 [ " $use_color " -eq 1 ] && reset=' \033[0m'
357501
502+ local total
503+ total=" $( term_width) "
504+
358505 # Collect unique (scope, list, source) group keys in first-seen order.
359506 # Using jq to compute a stable set of tuples preserving the ANNOTATED order.
360507 local groups
@@ -402,24 +549,73 @@ render_grouped_table() {
402549 " $( printf ' %s' " $scope " | tr ' [:lower:]' ' [:upper:]' ) " \
403550 " $list " " $source " " $mcount "
404551
405- printf ' %4s %-18s %-50s %s\n' ' #' ' tool' ' match-summary' ' reason'
406-
407- local mi mentry midx mtool msum mreason
552+ # Per-group column budget.
553+ local widest_tool=4 t_len mi mentry mtool
554+ for (( mi = 0 ; mi < mcount; mi++ )) ; do
555+ mentry=" $( jq -c " .[${mi} ]" <<< " $members" ) "
556+ mtool=" $( rule_tool " $( jq -c ' .rule' <<< " $mentry" ) " ) "
557+ [ -z " $mtool " ] && mtool=" -"
558+ t_len=${# mtool}
559+ [ " $t_len " -gt " $widest_tool " ] && widest_tool=" $t_len "
560+ done
561+ local W_tool=$widest_tool
562+ [ " $W_tool " -lt 12 ] && W_tool=12
563+ [ " $W_tool " -gt 20 ] && W_tool=20
564+
565+ # Fixed lead consumes: 4 (#) + 2 (gap) + W_tool + 1 (gap) = 7 + W_tool.
566+ local fixed=$(( 7 + W_tool ))
567+ local remaining=$(( total - fixed ))
568+ [ " $remaining " -lt 50 ] && remaining=50
569+ local W_match=$(( remaining * 60 / 100 ))
570+ local W_reason=$(( remaining - W_match - 1 ))
571+ [ " $W_match " -lt 30 ] && W_match=30
572+ [ " $W_reason " -lt 20 ] && W_reason=20
573+
574+ printf " %4s %-${W_tool} s %-${W_match} s %s\n" ' #' ' tool' ' match-summary' ' reason'
575+
576+ local midx msum mreason match_lines=() reason_lines=() max_lines li msum_line reason_line pad_tool
577+ printf -v pad_tool ' %*s' " $W_tool " ' '
408578 for (( mi = 0 ; mi < mcount; mi++ )) ; do
409579 mentry=" $( jq -c " .[${mi} ]" <<< " $members" ) "
410580 midx=" $( jq -r ' .index' <<< " $mentry" ) "
411- mtool=" $( truncate_str " $( rule_tool " $( jq -c ' .rule' <<< " $mentry" ) " ) " 18 ) "
581+ mtool=" $( rule_tool " $( jq -c ' .rule' <<< " $mentry" ) " ) "
412582 [ -z " $mtool " ] && mtool=" -"
413- msum=" $( truncate_str " $( match_summary " $( jq -c ' .rule' <<< " $mentry" ) " ) " 50 ) "
583+ msum=" $( match_summary " $( jq -c ' .rule' <<< " $mentry" ) " ) "
414584 mreason=" $( rule_reason " $( jq -c ' .rule' <<< " $mentry" ) " ) "
585+
586+ wrap_text " $msum " " $W_match " match_lines
587+ wrap_text " $mreason " " $W_reason " reason_lines
588+
589+ max_lines=${# match_lines[@]}
590+ [ " ${# reason_lines[@]} " -gt " $max_lines " ] && max_lines=${# reason_lines[@]}
591+
415592 if [ " $use_color " -eq 1 ]; then
416593 color=" $( color_for_list " $list " ) "
417- printf " ${color} %4s %-18s %-50s %s${reset} \n" \
418- " $midx " " $mtool " " $msum " " $mreason "
419594 else
420- printf ' %4s %-18s %-50s %s\n' \
421- " $midx " " $mtool " " $msum " " $mreason "
595+ color=" "
422596 fi
597+
598+ for (( li = 0 ; li < max_lines; li++ )) ; do
599+ msum_line=" ${match_lines[$li]:- } "
600+ reason_line=" ${reason_lines[$li]:- } "
601+ if [ " $li " -eq 0 ]; then
602+ if [ " $use_color " -eq 1 ]; then
603+ printf " ${color} %4s %-${W_tool} s %-${W_match} s %s${reset} \n" \
604+ " $midx " " $mtool " " $msum_line " " $reason_line "
605+ else
606+ printf " %4s %-${W_tool} s %-${W_match} s %s\n" \
607+ " $midx " " $mtool " " $msum_line " " $reason_line "
608+ fi
609+ else
610+ if [ " $use_color " -eq 1 ]; then
611+ printf " ${color} %4s %s %-${W_match} s %s${reset} \n" \
612+ ' ' " $pad_tool " " $msum_line " " $reason_line "
613+ else
614+ printf " %4s %s %-${W_match} s %s\n" \
615+ ' ' " $pad_tool " " $msum_line " " $reason_line "
616+ fi
617+ fi
618+ done
423619 done
424620 done
425621}
0 commit comments