From 114f99b336689f708d8ba5be6013ed8c5e1d63bc Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Mon, 27 May 2024 07:03:39 +1000 Subject: [PATCH] textblock::table layout and width fixes, ansi fixes --- src/modules/punk-0.1.tm | 47 +- src/modules/punk/ansi-999999.0a1.0.tm | 50 +- src/modules/punk/lib-999999.0a1.0.tm | 32 +- src/modules/punk/repl-0.1.tm | 4 +- src/modules/textblock-999999.0a1.0.tm | 1257 +++++++++++++++++++++---- 5 files changed, 1187 insertions(+), 203 deletions(-) diff --git a/src/modules/punk-0.1.tm b/src/modules/punk-0.1.tm index 90e425b..5d231c4 100644 --- a/src/modules/punk-0.1.tm +++ b/src/modules/punk-0.1.tm @@ -6981,21 +6981,42 @@ namespace eval punk { set text "" if {$topic in [list env environment]} { set known $::punk::config::known_punk_env_vars - append text $linesep\n - append text "punk environment vars:\n" - append text $linesep\n - set col1 [string repeat " " 25] - set col2 [string repeat " " 50] - foreach v $known { - set c1 [overtype::left $col1 $v] - if {[info exists ::env($v)]} { - set c2 [overtype::left $col2 [set ::env($v)] - } else { - set c2 [overtype::right $col2 "(NOT SET)"] + append text \n + set usetable 1 + if {$usetable} { + set t [textblock::class::table new -show_hseps 0 -show_header 1 -ansiborder_header [a+ web-green]] + foreach v $known { + if {[info exists ::env($v)]} { + set c2 [set ::env($v)] + } else { + set c2 "(NOT SET)" + } + $t add_row [list $v $c2] + } + $t configure_column 0 -headers [list "Punk environment vars"] + $t configure_column 0 -minwidth [expr {[$t column_datawidth 0]+4}] -blockalign left -textalign left -header_colspans {all} + + append text [$t print]\n + $t destroy + } else { + + append text $linesep\n + append text "punk environment vars:\n" + append text $linesep\n + set col1 [string repeat " " 25] + set col2 [string repeat " " 50] + foreach v $known { + set c1 [overtype::left $col1 $v] + if {[info exists ::env($v)]} { + set c2 [overtype::left $col2 [set ::env($v)] + } else { + set c2 [overtype::right $col2 "(NOT SET)"] + } + append text "$c1 $c2\n" } - append text "$c1 $c2\n" + append text $linesep\n } - append text $linesep\n + lappend chunks [list stdout $text] } diff --git a/src/modules/punk/ansi-999999.0a1.0.tm b/src/modules/punk/ansi-999999.0a1.0.tm index d2b80fe..eacc1aa 100644 --- a/src/modules/punk/ansi-999999.0a1.0.tm +++ b/src/modules/punk/ansi-999999.0a1.0.tm @@ -1495,7 +1495,9 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu lappend rows $row set row [list] } - if {$i > 6} { + if {$i == 8} { + set fg "web-white" + } elseif {$i > 6} { set fg "web-black" } #lappend row "[a+ {*}$fg Term-$cname][format %3s $i] $cname " @@ -1773,6 +1775,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #$t configure_row [expr {[$t row_count]-1}] -ansibase [a+ $fg Rgb-$cdec] $t configure_row [expr {[$t row_count]-1}] -ansibase [a+ $fg Web-$cname] } + $t configure -frametype {} $t configure_column 0 -headers [list "[string totitle $g] colours"] $t configure_column 0 -header_colspans [list all] $t configure -ansibase_header [a+ web-black Web-white] @@ -1787,9 +1790,21 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu $displaytable destroy return $result } - proc colourtable_x11diff {} { + proc colourtable_x11diff {args} { variable X11_colour_map_diff variable WEB_colour_map + set defaults [dict create\ + -return "string"\ + ] + dict for {k v} $args { + switch -- $k { + -return {} + default { + error "colourtable_x11diff unrecognised option '$k'. Known options [dict keys $defaults]" + } + } + } + set opts [dict merge $defaults $args] set comparetables [list] ;# 2 side by side x11 and web @@ -1801,6 +1816,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set fg "web-white" $t configure_row [expr {[$t row_count]-1}] -ansibase [a+ $fg X11-$cname] } + $t configure -frametype block $t configure_column 0 -headers [list "X11"] $t configure_column 0 -header_colspans [list all] $t configure -ansibase_header [a+ web-black Web-white] @@ -1821,6 +1837,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set fg "web-white" $t configure_row [expr {[$t row_count]-1}] -ansibase [a+ $fg Web-$cname] } + $t configure -frametype block $t configure_column 0 -headers [list "Web"] $t configure_column 0 -header_colspans [list all] $t configure -ansibase_header [a+ web-black Web-white] @@ -1830,9 +1847,14 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set displaytable [textblock::list_as_table 2 $comparetables -return object] $displaytable configure -show_header 0 -show_vseps 0 - set result [$displaytable print] - $displaytable destroy - return $result + + if {[dict get $opts -return] eq "string"} { + set result [$displaytable print] + $displaytable destroy + return $result + } + + return $displaytable } proc a? {args} { #*** !doctools @@ -1947,7 +1969,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } x11 { set out "" - append out "Mostly same as web - known differences displayed" \n + append out " Mostly same as web - known differences displayed" \n append out [colourtable_x11diff] return $out } @@ -2141,9 +2163,10 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #[para]punk::ansi::a Red #[para]see [cmd punk::ansi::a?] to display a list of codes + #function name part of cache-key because a and a+ return slightly different results (a has leading reset) variable sgr_cache - if {[dict exists $sgr_cache $args]} { - return [dict get $sgr_cache $args] + if {[dict exists $sgr_cache a+$args]} { + return [dict get $sgr_cache a+$args] } #don't disable ansi here. @@ -2358,7 +2381,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } else { set result "\x1b\[[join $t {;}]m" } - dict set sgr_cache $args $result + dict set sgr_cache a+$args $result return $result } @@ -2373,9 +2396,10 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #[para]punk::ansi::a Red #[para]see [cmd punk::ansi::a?] to display a list of codes + #It's important to put the functionname in the cache-key because a and a+ return slightly different results variable sgr_cache - if {[dict exists $sgr_cache $args]} { - return [dict get $sgr_cache $args] + if {[dict exists $sgr_cache a_$args]} { + return [dict get $sgr_cache a_$args] } #don't disable ansi here. @@ -2589,7 +2613,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu # explicit reset at beginning of parameter list for a= (as opposed to a+) set t [linsert $t[unset t] 0 0] set result "\x1b\[[join $t {;}]m" - dict set sgr_cache $args $result + dict set sgr_cache a_$args $result return $result } @@ -4762,7 +4786,7 @@ namespace eval punk::ansi::class { set displaycode [ansistring VIEW $code] if {$col eq ""} { #row only move - set map [list H "H${arrow_lr}" f "f${arrow_lr}] + set map [list H "H${arrow_lr}" f "f${arrow_lr}"] } else { #row and col move set map [list H "H${arrow_lr}${arrow_du}" f "${arrow_lr}${arrow_du}"] diff --git a/src/modules/punk/lib-999999.0a1.0.tm b/src/modules/punk/lib-999999.0a1.0.tm index 53d586b..5360f93 100644 --- a/src/modules/punk/lib-999999.0a1.0.tm +++ b/src/modules/punk/lib-999999.0a1.0.tm @@ -1111,11 +1111,12 @@ namespace eval punk::lib { -block {trimhead1 trimtail1}\ -line {}\ -commandprefix ""\ - -ansiresets 0\ + -ansiresets auto\ + -ansireplays 0\ ] dict for {o v} $arglist { switch -- $o { - -block - -line - -commandprefix - -ansiresets {} + -block - -line - -commandprefix - -ansiresets - -ansireplays {} default { error "linelist: Unrecognized option '$o' usage:$usage" } @@ -1174,6 +1175,17 @@ namespace eval punk::lib { # -- --- --- --- --- --- set opt_ansiresets [dict get $opts -ansiresets] # -- --- --- --- --- --- + set opt_ansireplays [dict get $opts -ansireplays] + if {$opt_ansireplays} { + if {$opt_ansiresets eq "auto"} { + set opt_ansiresets 1 + } + } else { + if {$opt_ansiresets eq "auto"} { + set opt_ansiresets 0 + } + } + # -- --- --- --- --- --- set linelist [list] set nlsplit [split $text \n] if {![llength $opt_line]} { @@ -1260,17 +1272,23 @@ namespace eval punk::lib { #review - we need to make sure ansiresets don't accumulate/grow on any line #Each resulting line should have a reset of some type at start and a pure-reset at end to stop #see if we can find an ST sequence that most terminals will not display for marking sections? - if {$opt_ansiresets} { + if {$opt_ansireplays} { package require punk::ansi - set RST [punk::ansi::a] + if {$opt_ansiresets} { + set RST [punk::ansi::a] + } else { + set RST "" + } set replaycodes $RST ;#todo - default? set transformed [list] #shortcircuit common case of no ansi if {![punk::ansi::ta::detect $linelist]} { - foreach ln $linelist { - lappend transformed $RST$ln$RST + if {$opt_ansiresets} { + foreach ln $linelist { + lappend transformed $RST$ln$RST + } + set linelist $transformed } - set linelist $transformed } else { #INLINE punk::ansi::codetype::is_sgr_reset diff --git a/src/modules/punk/repl-0.1.tm b/src/modules/punk/repl-0.1.tm index 1599788..0cc2e9c 100644 --- a/src/modules/punk/repl-0.1.tm +++ b/src/modules/punk/repl-0.1.tm @@ -1879,7 +1879,7 @@ proc repl::repl_process_data {inputchan type chunk stdinlines prompt_config} { } else { set info [a+ green]$info[a] } - set lines [lines_as_list -ansiresets 1 $info] + set lines [lines_as_list -ansireplays 1 $info] if {[llength $lines] > 20} { set lines [lrange $lines end-19 end] set info [list_as_lines $lines] @@ -1901,7 +1901,7 @@ proc repl::repl_process_data {inputchan type chunk stdinlines prompt_config} { if {[catch { #set info [$editbuf view_lines] set info [$editbuf view_lines_numbered] - set lines [lines_as_list -ansiresets 1 $info] + set lines [lines_as_list -ansireplays 1 $info] if {[llength $lines] > 20} { set lines [lrange $lines end-19 end] set info [list_as_lines $lines] diff --git a/src/modules/textblock-999999.0a1.0.tm b/src/modules/textblock-999999.0a1.0.tm index 79f96ac..58906b1 100644 --- a/src/modules/textblock-999999.0a1.0.tm +++ b/src/modules/textblock-999999.0a1.0.tm @@ -35,6 +35,8 @@ namespace eval textblock { variable opts_table_defaults set opts_table_defaults [dict create\ -title ""\ + -titlealign "left"\ + -titletransparent 0\ -frametype "light"\ -frametype_header ""\ -ansibase_header ""\ @@ -229,9 +231,12 @@ namespace eval textblock { variable o_rowstates variable o_opts_table_defaults + variable o_opts_header_defaults ;# header data mostly stored in o_columndefs variable o_opts_column_defaults variable o_opts_row_defaults - variable TSUB + variable TSUB ;#make configurable so user isn't stopped from using our default PUA-unicode char in content (nerdfonts overlap) + variable o_calculated_column_widths + variable o_column_width_algorithm constructor {args} { #*** !doctools #[call class::table [method constructor] [arg args]] @@ -254,12 +259,31 @@ namespace eval textblock { my configure {*}[dict merge $o_opts_table_defaults $args] set o_columndefs [dict create] set o_columndata [dict create] ;#we store data by column even though it is often added row by row - set o_columnstates [dict create] ;#store the maxwidthbodyseen as we add rows and maxwidthheaderseen as we add headers - it is needed often and expensive to calculate repeatedly + set o_columnstates [dict create] ;#store the maxwidthbodyseen etc as we add rows and maxwidthheaderseen etc as we add headers - it is needed often and expensive to calculate repeatedly set o_headerstates [dict create] set o_rowdefs [dict create] ;#user requested row data e.g -minheight -maxheight set o_rowstates [dict create] ;#actual row data such as -minheight and -maxheight detected from supplied row data set TSUB \uF111 ;#should be BMP PUA code to show as either replacement char or nerdfont glyph. See FSUB for comments regarding choices. + set o_calculated_column_widths [list] + set o_column_width_algorithm "span" + set header_defaults [dict create\ + -colspans {}\ + -values {}\ + -ansibase {}\ + ] + set o_opts_header_defaults $header_defaults + } + + method width_algorithm {{alg ""}} { + if {$alg eq ""} { + return $o_column_width_algorithm + } + if {$alg ne $o_column_width_algorithm} { + #invlidate cached widths + set o_calculated_column_widths [list] + } + set o_column_width_algorithm $alg } method Get_seps {} { set requested_seps [dict get $o_opts_table -show_seps] @@ -495,6 +519,22 @@ namespace eval textblock { error "textblock::table::configure -ansireset is read-only. It is present only to prevent unwanted colourised output in configure commands" } } + -show_edge - -show_hseps { + if {![string is boolean $v]} { + error "textblock::table::configure invalid $k '$v'. Must be a boolean or empty string" + } + lappend checked_opts $k $v + #these don't affect column width calculations + } + -show_vseps { + #we allow empty string - so don't use -strict boolean check + if {![string is boolean $v]} { + error "textblock::table::configure invalid $k '$v'. Must be a boolean or empty string" + } + #affects width calculations + set o_calculated_column_widths [list] ;#invalidate cached column widths - a recalc will be forced when needed + lappend checked_opts $k $v + } default { lappend checked_opts $k $v } @@ -614,7 +654,8 @@ namespace eval textblock { dict set o_columndata $colcount [list] dict set o_columndefs $colcount $defaults ;#ensure record exists - dict set o_columnstates $colcount [dict create maxwidthbodyseen 0 maxwidthheaderseen 0] + dict set o_columnstates $colcount [dict create minwidthbodyseen 0 maxwidthbodyseen 0 maxwidthheaderseen 0] + set prev_calculated_column_widths $o_calculated_column_widths if {[catch { my configure_column $colcount {*}$opts } errMsg]} { @@ -622,8 +663,12 @@ namespace eval textblock { dict unset o_columndata $colcount dict unset o_columndefs $colcount dict unset o_columnstates $colcount + #undo cache invalidation + set o_calculated_column_widths $prev_calculated_column_widths error "add_column failed to configure with supplied options $opts. Err:\n$errMsg" } + #any add_column that succeeds should invalidate the calculated column widths + set o_calculated_column_widths [list] set numrows [my row_count] if {$numrows > 0} { #fill column with default values @@ -631,7 +676,8 @@ namespace eval textblock { set dval [dict get $opts -defaultvalue] set width [textblock::width $dval] dict set o_columndata $colcount [lrepeat $numrows $dval] - dict set o_columnstates $colcount [maxwidthbodyseen $width] + dict set o_columnstates $colcount maxwidthbodyseen $width + dict set o_columnstates $colcount minwidthbodyseen $width } return $colcount } @@ -646,6 +692,24 @@ namespace eval textblock { if {![llength $args]} { return [dict get $o_columndefs $cidx] } else { + if {[llength $args] == 1} { + if {[lindex $args 0] in [dict keys $o_opts_column_defaults]} { + #query single option + set k [lindex $args 0] + set val [dict get $o_columndefs $cidx $k] + set returndict [dict create option $k value $val ansireset "\x1b\[m"] + set infodict [dict create] + switch -- $k { + -ansibase { + dict set infodict debug [ansistring VIEW $val] + } + } + dict set returndict info $infodict + return $returndict + } else { + error "textblock::table configure_column - unrecognised option '[lindex $args 0]'. Known values [dict keys $o_opts_column_defaults]" + } + } if {[llength $args] %2 != 0} { error "textblock::table configure_column unexpected argument count. Require name value pairs. Known options: [dict keys $o_opts_column_defaults]" } @@ -660,23 +724,29 @@ namespace eval textblock { dict for {k v} $args { switch -- $k { -headers { - #todo - multiline header set i 0 + set maxseen 0 ;#don't compare with cached colstate maxwidthheaderseen - we have all the headers for the column right here and need to refresh the colstate maxwidthheaderseen values completely. foreach hdr $v { set currentmax [my header_height_calc $i $cidx] ;#exclude current column - ie look at heights for this header in all other columns #set this_header_height [textblock::height $hdr] lassign [textblock::size $hdr] _w this_header_width _h this_header_height if {$this_header_height >= $currentmax} { - dict set hstates $i -maxheight $this_header_height + dict set hstates $i maxheightseen $this_header_height } else { - dict set hstates $i -maxheight $currentmax + dict set hstates $i maxheightseen $currentmax } - if {$this_header_width > [dict get $colstate maxwidthheaderseen]} { - dict set colstate maxwidthheaderseen $this_header_width + if {$this_header_width >= $maxseen} { + set maxseen $this_header_width } + #if {$this_header_width > [dict get $colstate maxwidthheaderseen]} { + # dict set colstate maxwidthheaderseen $this_header_width + #} incr i } + dict set colstate maxwidthheaderseen $maxseen + #review - we could avoid some recalcs if we check current width range compared to previous + set o_calculated_column_widths [list] ;#invalidate cached column widths - a recalc will be forced when needed lappend checked_opts $k $v } -header_colspans { @@ -749,6 +819,16 @@ namespace eval textblock { } incr h } + #todo - avoid recalc if no change + set o_calculated_column_widths [list] ;#invalidate cached column widths - a recalc will be forced when needed + lappend checked_opts $k $v + } + -minwidth { + set o_calculated_column_widths [list] ;#invalidate cached column widths - a recalc will be forced when needed + lappend checked_opts $k $v + } + -maxwidth { + set o_calculated_column_widths [list] ;#invalidate cached column widths - a recalc will be forced when needed lappend checked_opts $k $v } -ansibase { @@ -830,7 +910,7 @@ namespace eval textblock { } method header_height {header_index} { set idx [lindex [dict keys $o_headerstates $header_index]] - return [dict get $o_headerstates $idx -maxheight] + return [dict get $o_headerstates $idx maxheightseen] } #review - use maxwidth (considering colspans) of each column to determine height after wrapping @@ -903,6 +983,252 @@ namespace eval textblock { } return $colspans_by_header } + + #should be configure_headerrow ? + method configure_header {index_expression args} { + #the header data being configured or returned here is mostly derived from the column defs and if necessary written to the column defs. + #It can also be set column by column - but it is much easier (especially for colspans) to configure them on a header-row basis + #e.g o_headerstates: 0 {maxheightseen 1} 1 {maxheightseen 2} + set num_headers [my header_count_calc] + set hidx [lindex [dict keys $o_headerstates] $index_expression] + if {$hidx eq ""} { + error "textblock::table::configure_header - no row defined at index '$hidx'." + } + if {$hidx > $num_headers -1} { + #assert - shouldn't happen + error "textblock::table::configure_header error headerstates data is out of sync" + } + + if {![llength $args]} { + set colspans_by_header [my header_colspans] + set result [dict create] + dict set result -colspans [dict get $colspans_by_header $hidx] + set header_row_items [list] + dict for {cidx cdef} $o_columndefs { + set colheaders [dict get $cdef -headers] + set relevant_header [lindex $colheaders $hidx] + #The -headers element of the column def is allowed to have less elements than the total, even be empty. Number of headers is defined by the longest -header list in the set of columns + lappend header_row_items $relevant_header ;#may be empty string because column had nothing at that index, or may be empty string stored in that column. We don't differentiate. + } + dict set result -values $header_row_items + return $result + } + if {[llength $args] == 1} { + if {[lindex $args 0] in [dict keys $o_opts_header_defaults]} { + #query single option + set k [lindex $args 0] + #set val [dict get $o_rowdefs $ridx $k] + + set infodict [dict create] + switch -- $k { + -values { + set header_row_items [list] + dict for {cidx cdef} $o_columndefs { + set colheaders [dict get $cdef -headers] + set relevant_header [lindex $colheaders $hidx] + #The -headers element of the column def is allowed to have less elements than the total, even be empty. Number of headers is defined by the longest -header list in the set of columns + lappend header_row_items $relevant_header ;#may be empty string because column had nothing at that index, or may be empty string stored in that column. We don't differentiate. + + } + set val $header_row_items + set returndict [dict create option $k value $val ansireset "\x1b\[m"] + } + -colspans { + set colspans_by_header [my header_colspans] + set result [dict create] + set val [dict get $colspans_by_header $hidx] + set returndict [dict create option $k value $val ansireset "\x1b\[m"] + } + -ansibase { + set val ??? + set returndict [dict create option $k value $val ansireset "\x1b\[m"] + dict set infodict debug [ansistring VIEW $val] + } + } + dict set returndict info $infodict + return $returndict + #return [dict create option $k value $val ansireset "\x1b\[m" info $infodict] + } else { + error "textblock::table configure_header - unrecognised option '[lindex $args 0]'. Known values [dict keys $o_opts_header_defaults]" + } + } + if {[llength $args] %2 != 0} { + error "textblock::table configure_header incorrect number of options following index_expression. Require name value pairs. Known options: [dict keys $o_opts_header_defaults]" + } + dict for {k v} $args { + if {$k ni [dict keys $o_opts_header_defaults]} { + error "[namespace current]::table configure_row unknown option '$k'. Known options: [dict keys $o_opts_header_defaults]" + } + } + + set checked_opts [list] + dict for {k v} $args { + switch -- $k { + -ansibase { + set parts [punk::ansi::ta::split_codes_single $v] ;#caller may have supplied separated codes eg "[a+ Yellow][a+ red]" + set header_ansibase_items [list] ; + foreach {pt code} $parts { + if {$pt ne ""} { + #we don't expect plaintext in an ansibase + error "Unable to interpret -ansibase value as ansi SGR codes. Plaintext detected. Consider using for example: '\[punk::ansi::a+ green]' (or alias '\[a+ green]') to build ansi. debug view: [punk::ansi::ansistring VIEW $v]" + } + if {$code ne ""} { + lappend header_ansibase_items $code + } + } + set header_ansibase [punk::ansi::codetype::sgr_merge_singles $row_ansibase_items] + error "sorry - -ansibase not yet implemented for header rows" + lappend checked_opts $k $header_ansibase + } + -ansireset { + if {$v eq "\uFFEF"} { + lappend checked_opts $k "\x1b\[m" ;# [a] + } else { + error "textblock::table::configure_header -ansireset is read-only. It is present only to prevent unwanted colourised output in configure commands" + } + } + -values { + if {[llength $v] > [dict size $o_columndefs]} { + error "textblock::table::configure_header -values length ([llength $v]) is longer than number of columns ([dict size $o_columndefs])" + } + lappend checked_opts $k $v + } + -colspans { + if {[llength $v] > [dict size $o_columndefs]} { + error "textblock::table::configure_header -colspans length ([llength $v]) is longer than number of columns ([dict size $o_columndefs])" + } + if {[llength $v]} { + set firstspan [lindex $v 0] + set first_is_ok 0 + if {$firstspan eq "all"} { + set first_is_ok 1 + } elseif {[string is integer -strict $firstspan] && $firstspan > 0} { + set first_is_ok 1 + } + if {!$first_is_ok} { + error "textblock::table::configure_header -colspans first value '$firstspan' must be integer > 0 or the string \"all\"" + } + #we don't mind if there are less colspans specified than columns.. the tail can be deduced from the leading ones specified (review) + set remaining $firstspan + if {$remaining ne "all"} { + incr remaining -1 + } + foreach span [lrange $v 1 end] { + if {$remaining eq "all"} { + if {$span eq "all"} { + set remaining "all" + } elseif {$span > 0} { + #ok to reset to higher val immediately or after an all and any number of following zeros + set remaining $span + incr remaining -1 + } else { + #zero following an all - leave remaining as all + incr remaining -1 + } + } else { + if {$span eq "0"} { + if {$remaining eq "0"} { + error "textblock::table::configure_header -colspans sequence incorrect at span '$span' remaining is $remaining - positive or \"all\" value span required" + } else { + incr remaining -1 + } + } else { + if {$remaining eq "0"} { + #ok for new span value of all or > 0 + set remaining $span + if {$remaining ne "all"} { + incr remaining -1 + } + } else { + error "textblock::table::configure_header -colspans sequence incorrect at span '$span' remaining is $remaining - zero value span required" + } + } + } + } + } + #empty -colspans list should be ok + + #error "sorry - -colspans not yet implemented for header rows - set manually in vertical order via configure_column for now" + lappend checked_opts $k $v + } + default { + lappend checked_opts $k $v + } + } + } + + #configured opts all good + + dict for {k v} $checked_opts { + switch -- $k { + -values { + set c 0 + foreach hval $v { + #retrieve -headers from relevant col, insert at header index, and write back. + set colheaders [dict get $o_columndefs $c -headers] + set missing [expr {($hidx +1) - [llength $colheaders]}] + if {$missing > 0} { + lappend colheaders {*}[lrepeat $missing ""] + } + lset colheaders $hidx $hval + dict set o_columndefs $c -headers $colheaders + #invalidate column width cache + set o_calculated_column_widths [list] + # -- -- -- -- -- -- + #also update maxwidthseen & maxheightseen + set i 0 + set maxwidthseen 0 + set maxheightseen 0 + foreach hdr $colheaders { + lassign [textblock::size $hdr] _w this_header_width _h this_header_height + if {$this_header_height >= $maxheightseen} { + dict set o_headerstates $i maxheightseen $this_header_height + } else { + dict set o_headerstates $i maxheightseen $maxheightseen + } + if {$this_header_width >= $maxwidthseen} { + set maxwidthseen $this_header_width + } + incr i + } + dict set o_columnstates $c maxwidthheaderseen $maxwidthseen + # -- -- -- -- -- -- + incr c + } + } + -colspans { + #sequence has been verified above - we need to split it and store across columns + set c 0 ;#column index + foreach span $v { + set colspans [dict get $o_columndefs $c -header_colspans] + if {$hidx > [llength $colspans]-1} { + set colspans_by_header [my header_colspans] + #puts ">>>>>?$colspans_by_header" + #sanity check - we are allowed to lset only one beyond the current length to append + #but there may be even less or no entries present in a column + #error "configure_header $hidx Unable to update -colspans for column $c with value $span from the set '$v' - review" + # - the ability to underspecify and calculate the missing values makes setting the values complicated. + #use the header_colspans calculation to update only those entries necessary + set spanlist [list] + for {set h 0} {$h < $hidx} {incr h} { + set cspans [dict get $colspans_by_header $h] + set requiredval [lindex $cspans $c] + lappend spanlist $requiredval + } + dict set o_columndefs $c -header_colspans $spanlist + + set colspans [dict get $o_columndefs $c -header_colspans] + } + + lset colspans $hidx $span + dict set o_columndefs $c -header_colspans $colspans + incr c + } + } + } + } + } + method add_row {valuelist args} { #*** !doctools #[call class::table [method add_row] [arg args]] @@ -970,11 +1296,13 @@ namespace eval textblock { error "add_row failed to configure with supplied options $opts. Err:\n$errMsg" } - set c 0 set max_height_seen 1 foreach v $valuelist { + set prev_maxwidth [dict get $o_columnstates $c maxwidthbodyseen] + set prev_minwidth [dict get $o_columnstates $c minwidthbodyseen] + dict lappend o_columndata $c $v set valheight [textblock::height $v] if {$valheight > $max_height_seen} { @@ -984,8 +1312,17 @@ namespace eval textblock { if {$width > [dict get $o_columnstates $c maxwidthbodyseen]} { dict set o_columnstates $c maxwidthbodyseen $width } + if {$width < [dict get $o_columnstates $c minwidthbodyseen]} { + dict set o_columnstates $c minwidthbodyseen $width + } + + if {[dict get $o_columnstates $c maxwidthbodyseen] > $prev_maxwidth || [dict get $o_columnstates $c minwidthbodyseen] < $prev_minwidth} { + #invalidate calculated column width cache if any new value was outside the previous range of widths + set o_calculated_column_widths [list] + } incr c } + set opt_maxh [dict get $o_rowdefs $rowcount -maxheight] if {$opt_maxh ne ""} { dict set o_rowstates $rowcount -maxheight [expr {min($opt_maxh,$max_height_seen)}] @@ -1003,6 +1340,25 @@ namespace eval textblock { if {![llength $args]} { return [dict get $o_rowdefs $ridx] } + if {[llength $args] == 1} { + if {[lindex $args 0] in [dict keys $o_opts_row_defaults]} { + #query single option + set k [lindex $args 0] + set val [dict get $o_rowdefs $ridx $k] + set returndict [dict create option $k value $val ansireset "\x1b\[m"] + set infodict [dict create] + switch -- $k { + -ansibase { + dict set infodict debug [ansistring VIEW $val] + } + } + dict set returndict info $infodict + return $returndict + #return [dict create option $k value $val ansireset "\x1b\[m" info $infodict] + } else { + error "textblock::table configure_row - unrecognised option '[lindex $args 0]'. Known values [dict keys $o_opts_row_defaults]" + } + } if {[llength $args] %2 != 0} { error "textblock::table configure_row incorrect number of options following index_expression. Require name value pairs. Known options: [dict keys $o_opts_row_defaults]" } @@ -1071,8 +1427,11 @@ namespace eval textblock { #The data values are stored by column regardless of whether added row by row dict for {cidx records} $o_columndata { dict set o_columndata $cidx [list] - dict set o_columnstates $cidx [dict create maxwidthbodyseen 0 maxwidthheaderseen 0] + #reset only the body fields in o_columnstates + dict set o_columnstates $cidx minwidthbodyseen 0 + dict set o_columnstates $cidx maxwidthbodyseen 0 } + set o_calculated_column_widths [list] } method clear {} { my row_clear @@ -1080,9 +1439,12 @@ namespace eval textblock { set o_columndata [dict create] set o_columnstates [dict create] } - method Get_columns_by_name {namematch_list} { - } + + + #method Get_columns_by_name {namematch_list} { + #} + #specify range with x..y method Get_columns_by_indices {index_list} { foreach spec $index_list { @@ -1130,6 +1492,7 @@ namespace eval textblock { return [dict create boxlimits $boxlimits_position boxlimits_top $boxlimits_toprow joins $joins bodyjoins $header_body_joins ] } method get_column_by_index {index_expression args} { + #puts "+++> get_column_by_index $index_expression $args [namespace current]" #index_expression may be something like end-1 or 2+2 - we can't directly use it as a lookup for dicts. set defaults [dict create\ -position "inner"\ @@ -1290,7 +1653,9 @@ namespace eval textblock { set return_headerheight 0 set return_headerwidth 0 set cidx [lindex [dict keys $o_columndefs] $index_expression] + set colwidth [my column_width $cidx] + set col_blockalign [dict get $o_columndefs $cidx -blockalign] if {$do_show_header} { @@ -1304,10 +1669,12 @@ namespace eval textblock { set ansiborder_final $ansibase_header$ansiborder_header } set RST [punk::ansi::a] - set hcell_line_blank [string repeat " " $colwidth] - set h 0 - set hmax [expr {[llength $header_list] -1}] + + set hcolwidth $colwidth + #set hcolwidth [my column_width_configured $cidx] + set hcell_line_blank [string repeat " " $hcolwidth] + set all_colspans [my header_colspans] #default span_extend_map - used as base to customise with specific joins @@ -1319,16 +1686,17 @@ namespace eval textblock { ] set framedef_leftbox [textblock::framedef $ftype_header -joins left] - set column_width_cache [dict create] #used for colspan-zero header frames set framesub_map [list hl $TSUB vll $TSUB vlr $TSUB tlc $TSUB blc $TSUB trc $TSUB brc $TSUB] ;# a debug test + set hrow 0 + set hmax [expr {[llength $header_list] -1}] foreach header $header_list { - set headerspans [dict get $all_colspans $h] + set headerspans [dict get $all_colspans $hrow] set this_span [lindex $headerspans $cidx] set hval $ansibase_header$header ;#no reset - set rowh [my header_height $h] + set rowh [my header_height $hrow] #set h_lines [lrepeat $rowh $hcell_line_blank] #set hcell_blank [join $h_lines \n] @@ -1337,23 +1705,23 @@ namespace eval textblock { #set hval_block [join $hval_lines \n] #set headercell [overtype::left -experimental test_mode $ansibase_header$hcell_blank$RST $hval_block] - if {$h == 0} { + if {$hrow == 0} { set hlims $header_boxlimits_toprow set rowpos "top" - if {$h == $hmax} { + if {$hrow == $hmax} { set rowpos "only" } } else { set hlims $header_boxlimits set rowpos "middle" - if {$h == $hmax} { + if {$hrow == $hmax} { set rowpos "bottom" } } if {!$show_seps_v} { set hlims [struct::set difference $hlims $headerseps_v] } - if {$h == $hmax} { + if {$hrow == $hmax} { set header_joins $header_body_joins } else { set header_joins $joins @@ -1362,14 +1730,13 @@ namespace eval textblock { set hlims [struct::set difference $hlims [dict get $::textblock::class::header_edge_parts $rowpos$opt_posn] ] } #puts ">>> headerspans: $headerspans cidx: $cidx" - if {$this_span eq "all" || $this_span > 0} { - + if {$this_span eq "all" || $this_span > 0} { set startmap [dict get $hmap $rowpos${opt_posn}] #look at spans in header below to determine joins required at blc if {$show_seps_v} { - if {[dict exists $all_colspans [expr {$h+1}]]} { - set next_spanlist [dict get $all_colspans [expr {$h+1}]] + if {[dict exists $all_colspans [expr {$hrow+1}]]} { + set next_spanlist [dict get $all_colspans [expr {$hrow+1}]] set spanbelow [lindex $next_spanlist $cidx] if {$spanbelow == 0} { #we don't want a down-join for blc - use a framedef with only left joins @@ -1390,15 +1757,14 @@ namespace eval textblock { #This ellipsis 0 makes a difference (unwanted ellipsis at rhs of header column that starts span) # -width is always +2 - as the boxlimits take into account show_vseps and show_edge - set header_cell_startspan [textblock::frame -ellipsis 0 -usecache 1 -width [expr {$colwidth+2}] -type [dict get $ftypes header]\ + set header_cell_startspan [textblock::frame -ellipsis 0 -usecache 1 -width [expr {$hcolwidth+2}] -type [dict get $ftypes header]\ -ansibase $ansibase_header -ansiborder $ansiborder_final\ -boxlimits $hlims -boxmap $startmap -joins $header_joins $hval\ ] - #JMN - #puts "===>\n$header_cell_startspan\n<===" - set spanned_parts [list $header_cell_startspan] if {$this_span ne "1"} { + #puts "===>\n$header_cell_startspan\n<===" + set spanned_parts [list $header_cell_startspan] #assert this_span == "all" or >1 ie a header that spans other columns #therefore more parts to append #set remaining_cols [lrange [dict keys $o_columndefs] $cidx end] @@ -1406,7 +1772,7 @@ namespace eval textblock { #puts ">> remaining_spans: $remaining_spans" set spancol [expr {$cidx + 1}] set h_lines [lrepeat $rowh ""] - set hcell_blank [::join $h_lines \n] ;#todo - just use -height option of frame? + set hcell_blank [::join $h_lines \n] ;#todo - just use -height option of frame? review - currently doesn't cache with -height and no contents - slow @@ -1421,6 +1787,8 @@ namespace eval textblock { set next_posn inner } + set next_headerseps_v [dict get $sep_elements_vertical top$next_posn] ;#static top ok + set limj [my Get_boxlimits_and_joins $next_posn $fname_body] set span_joins_body [dict get $limj bodyjoins] set span_joins [dict get $limj joins] @@ -1431,7 +1799,7 @@ namespace eval textblock { #set span_boxlimits_top [struct::set intersect [dict get $o_opts_table_effective -framelimits_body] $span_boxlimits_top] set header_span_boxlimits [struct::set intersect [dict get $o_opts_table_effective -framelimits_header] $span_boxlimits] set header_span_boxlimits_top [struct::set intersect [dict get $o_opts_table_effective -framelimits_header] $span_boxlimits_top] - if {$h == 0} { + if {$hrow == 0} { set hlims $header_span_boxlimits_top } else { set hlims $header_span_boxlimits @@ -1439,7 +1807,7 @@ namespace eval textblock { set this_span_map $span_extend_map if {!$show_seps_v} { - set hlims [struct::set difference $hlims $headerseps_v] + set hlims [struct::set difference $hlims $next_headerseps_v] } else { if {[llength $next_spanlist]} { set spanbelow [lindex $next_spanlist $spancol] @@ -1454,7 +1822,7 @@ namespace eval textblock { } } - if {$h == $hmax} { + if {$hrow == $hmax} { set header_joins $span_joins_body } else { set header_joins $span_joins @@ -1463,35 +1831,8 @@ namespace eval textblock { set hlims [struct::set difference $hlims [dict get $::textblock::class::header_edge_parts $rowpos$next_posn] ] } - if {![dict exists $column_width_cache $spancol]} { - #puts "-----> get_column_by_index $spancol -position $next_posn" - set spancolinfo [my get_column_by_index $spancol -position $next_posn -return dict] - set bwidth [dict get $spancolinfo bodywidth] - set hwidth [dict get $spancolinfo headerwidth] - dict set column_width_cache $spancol bodywidth $bwidth - dict set column_width_cache $spancol headerwidth $hwidth - } else { - set bwidth [dict get $column_width_cache $spancol bodywidth] - set hwidth [dict get $column_width_cache $spancol headerwidth] - } - #subsequent headers may also span columns - so we will get too wide if we use the headers directly - #but if we don't take into account header widths - they may get truncated. - - #This is an unintuitive edge case - review - #spans at tail end are too long when edges are shown if we use bwidth (vlr extends right beyond table) - #spans at tail end are too short if edges are hidden and we use bwidth-1 (short lower horizontal bar) - #test JMN - if {$next_posn eq "right" && [dict get $o_opts_table -show_edge]} { - set spanwidth [expr {$bwidth -1}] - } else { - set spanwidth [expr {$bwidth }] - } - - #JMN - review - set framewidth $spanwidth - incr framewidth 1 - - set header_cell [textblock::frame -ellipsis 0 -width $framewidth -type [dict get $ftypes header]\ + set contentwidth [my column_width $spancol] + set header_cell [textblock::frame -ellipsis 0 -width [expr {$contentwidth + 2}] -type [dict get $ftypes header]\ -ansibase $ansibase_header -ansiborder $ansiborder_final\ -boxlimits $hlims -boxmap $this_span_map -joins $header_joins $hcell_blank\ ] @@ -1502,10 +1843,10 @@ namespace eval textblock { incr spancol incr i } - } - set spanned_frame [textblock::join {*}$spanned_parts] - if {$this_span eq "all" || $this_span > 1} { - if {$h == 0} { + + set spanned_frame [textblock::join {*}$spanned_parts] + + if {$hrow == 0} { set hlims $header_boxlimits_toprow } else { set hlims $header_boxlimits @@ -1517,30 +1858,37 @@ namespace eval textblock { #use the edge_parts corresponding to the column being written to ie use opt_posn set hlims [struct::set difference $hlims [dict get $::textblock::class::header_edge_parts $rowpos$opt_posn] ] } - set spacemap [list hl " " vl " " tlc " " blc " " trc " " brc " "] - #set spacemap [list hl "\uFFFF" vl "\uFFFF" tlc "\uFFFF" blc "\uFFFF" trc "\uFFFF " brc "\uFFFF"] ;# a debug test + + set spacemap [list hl " " vl " " tlc " " blc " " trc " " brc " "] ;#transparent overlay elements + #set spacemap [list hl * vl * tlc * blc * trc * brc *] #-usecache 1 ok - set hblock [textblock::frame -ellipsis 0 -type $spacemap -boxlimits $hlims -ansibase $ansibase_header $hval] + #hval is not raw headerval - it has been padded to required width and has ansi applied + set hblock [textblock::frame -ellipsis 0 -type $spacemap -boxlimits $hlims -ansibase $ansibase_header $hval] ;#review -ansibase + #puts "==>boxlimits:'$hlims' hval_width:[string length $hval] blockwidth:[textblock::width $hblock]" + #puts $hblock + #puts "==>hval:'$hval'[a]" + #puts "==>hval:'[ansistring VIEW $hval]'" #set spanned_frame [overtype::renderspace -experimental test_mode -transparent 1 $spanned_frame $hblock] - set spanned_frame [overtype::block -blockalign left -transparent 1 $spanned_frame $hblock] + set spanned_frame [overtype::block -blockalign left -overflow 1 -transparent 1 $spanned_frame $hblock] + + } else { + #this_span == 1 + set spanned_frame [textblock::join $header_cell_startspan] } append part_header $spanned_frame append part_header \n } else { - #zero span header + #zero span header directly in this column ie one that is being colspanned by some column to our left + #previous col will already have built lines for this in it's own header rhs overhang + #we need a block of TSUB chars of the right height and width in this column so that we can join this column to the left ones with the TSUBs being transparent. - #test hack - wider helps stop the breaks - but leaves junk spaces and ansiresets beyond the rhs border of table - #print function overflow 0 fixes? - #set padwidth 20 - - #This sort of works - but doesn't cater for colspans that don't strictly decrease in size as we go down the header list - #we end up with breaks in some situations - #we don't know the width here, because we would need to look-ahead to see the widest section of frame - #We will adjust the padding below. - #we need the column data width as a minimum or we'll cut lines above from earlier columns - set padwidth [my column_datawidth $cidx -headers 0 -data 1 -cached 1] + #set padwidth [my column_datawidth $cidx -headers 0 -data 1 -cached 1] + + #if there are no header elements above then we will need a minimum of the column width + #may be extended to the widest portion of the header in the loop below + set padwidth [my column_width $cidx] #under assumption we are building table using L frame method and that horizontal borders are only ever 1 high @@ -1562,21 +1910,17 @@ namespace eval textblock { set h_lines [lrepeat $rowh $bline] set hcell_blank [::join $h_lines \n] # -usecache 1 ok - #set header_frame [textblock::frame -ellipsis 0 -width [expr {$padwidth+2}] -type [dict get $ftypes header]\ - # -ansibase $ansibase_header \ - # -boxlimits $hlims -boxmap $framesub_map $hcell_blank\ - # ] #frame borders will never display - so use the simplest frametype and don't apply any ansi + #puts "===>zerospan hlims: $hlims" set header_frame [textblock::frame -ellipsis 0 -width [expr {$padwidth+2}] -type ascii\ -boxlimits $hlims -boxmap $framesub_map $hcell_blank\ ] } - append part_header $header_frame\n } - incr h + incr hrow } if {![llength $header_list]} { #no headers - but we've been asked to show_header @@ -1589,7 +1933,7 @@ namespace eval textblock { set hlims [struct::set difference $hlims [dict get $::textblock::class::header_edge_parts only$opt_posn] ] } set header_joins $header_body_joins - set header_frame [textblock::frame -width [expr {$colwidth+2}] -type [dict get $ftypes header]\ + set header_frame [textblock::frame -width [expr {$hcolwidth+2}] -type [dict get $ftypes header]\ -ansibase $ansibase_header -ansiborder $ansiborder_final\ -boxlimits $hlims -boxmap $hdrmap -joins $header_joins\ ] @@ -1608,8 +1952,8 @@ namespace eval textblock { } } set part_header [join $adjusted_lines \n] + append output $part_header \n } - append output $part_header \n set r 0 set rmax [expr {[llength $cells]-1}] @@ -1669,8 +2013,6 @@ namespace eval textblock { #$c will always have ansi resets due to overtype behaviour ? #todo - review overtype if {[punk::ansi::ta::detect $c]} { - #if {[textblock::widthtopline $c] == $colwidth} {} - #use only the last ansi sequence in the cell value #Filter out foreground and use background for ansiborder override set parts [punk::ansi::ta::split_codes_single $c] @@ -1702,7 +2044,7 @@ namespace eval textblock { } } else { if {$ftblock} { - #no resets use cells bg to extend to the border - only for block frames + #no resets - use cell's bg to extend to the border - only for block frames set ansiborder_final $ansiborder_body_col_row$cell_bg } set cell_ansibase $cell_bg @@ -1815,9 +2157,8 @@ namespace eval textblock { #assert cidx is integer >=0 set cdef [dict get $o_columndefs $cidx] set headerlist [dict get $cdef -headers] - set num_headers [my header_count] + set num_header_rows [my header_count] - set RST [punk::ansi::a] set ansibase_body [dict get $o_opts_table -ansibase_body] set ansibase_col [dict get $cdef -ansibase] set textalign [dict get $cdef -textalign] @@ -1839,46 +2180,51 @@ namespace eval textblock { #store configured widths so we don't look up for each header line set configured_widths [list] foreach c [dict keys $o_columndefs] { + #lappend configured_widths [my column_width $c] + #we don't just want the width of the column in the body - or the headers will get truncated lappend configured_widths [my column_width_configured $c] } set output [dict create] dict set output headers [list] - for {set i 0} {$i < $num_headers} {incr i} { - set hdr [lindex $headerlist $i] - set header_maxdataheight [my header_height $i] ;#from cached headerstates - set header_colspans [dict get $all_colspans $i] - set this_span [lindex $header_colspans $cidx] - set hdrwidth 0 + for {set hrow 0} {$hrow < $num_header_rows} {incr hrow} { + set hdr [lindex $headerlist $hrow] + set header_maxdataheight [my header_height $hrow] ;#from cached headerstates + set headerrow_colspans [dict get $all_colspans $hrow] + set this_span [lindex $headerrow_colspans $cidx] + + set this_hdrwidth [lindex $configured_widths $cidx] + set spanned_hdrwidth 0 + if {$this_span eq "0"} { - set hdrwidth 0 + set this_hdrwidth 0 + set spanned_hdrwidth 0 } elseif {$this_span eq "all"} { #all means up to next non-zero set s "0" - set idx $cidx - while {$s eq "0" && $idx < [llength $header_colspans]} { - incr hdrwidth [lindex $configured_widths $idx] + set idx [expr {$cidx +1}] + while {$s eq "0" && $idx < [llength $headerrow_colspans]} { + incr spanned_hdrwidth [lindex $configured_widths $idx] incr idx - set s [lindex $header_colspans $idx] + set s [lindex $headerrow_colspans $idx] } } else { set spanned_cols [list] - for {set sc $cidx} {$sc < ($cidx + $this_span)} {incr sc} { + for {set sc [expr {$cidx+1}]} {$sc < ($cidx + $this_span)} {incr sc} { lappend spanned_cols $sc } - #spanned_cols here includes self foreach c $spanned_cols { - incr hdrwidth [lindex $configured_widths $c] + incr spanned_hdrwidth [lindex $configured_widths $c] } } - + set hdrwidth [expr {max($this_hdrwidth,$spanned_hdrwidth)}] set hdr_line_blank [string repeat " " $hdrwidth] - set header_underlay [lrepeat $header_maxdataheight $hdr_line_blank] - set header_underlay $ansibase_header[join $header_underlay \n] + set headercell_underlay [lrepeat $header_maxdataheight $hdr_line_blank] + set headercell_underlay $ansibase_header[join $headercell_underlay \n] if {$hdr ne ""} { - dict lappend output headers [overtype::renderspace -experimental test_mode $header_underlay $ansibase_header$hdr] + dict lappend output headers [overtype::renderspace -experimental test_mode $headercell_underlay $ansibase_header$hdr] } else { - dict lappend output headers $header_underlay + dict lappend output headers $headercell_underlay } } @@ -1893,13 +2239,13 @@ namespace eval textblock { set items [dict get $o_columndata $cidx] #puts "---> columndata $o_columndata" + #set opt_row_ansibase [dict get $o_rowdefs $r -ansibase] + #set cell_ansibase $ansibase_body$ansibase_col$opt_row_ansibase + dict set output cells [list];#ensure we return something for cells key if no items in list set r 0 foreach cval $items { - set opt_row_ansibase [dict get $o_rowdefs $r -ansibase] - set cell_ansibase $ansibase_body$ansibase_col$opt_row_ansibase - - #todo move to row_height method + #todo move to row_height method ? set maxdataheight [dict get $o_rowstates $r -maxheight] set rowdefminh [dict get $o_rowdefs $r -minheight] set rowdefmaxh [dict get $o_rowdefs $r -maxheight] @@ -1926,7 +2272,6 @@ namespace eval textblock { } } } - #set cval $cell_ansibase$cval ;#no reset set cell_lines [lrepeat $rowh $cell_line_blank] set cell_blank [join $cell_lines \n] @@ -1937,11 +2282,6 @@ namespace eval textblock { set cval_lines [lrange $cval_lines 0 $rowh-1] set cval_block [join $cval_lines \n] - #TODO! fix overtype library - #set cell [overtype::renderspace -experimental test_mode $cell_ansibase$cell_blank$RST $cval_block] - #set cell [overtype::renderspace -experimental test_mode $cell_blank $cval_block] - - #set cell [textblock::pad $cval_block -width $colwidth -padchar " " -within_ansi 0 -which right] set cell [textblock::pad $cval_block -width $datawidth -padchar " " -within_ansi 1 -which $pad] #set cell [textblock::pad $cval_block -width $colwidth -padchar " " -within_ansi 0 -which left] @@ -1958,10 +2298,103 @@ namespace eval textblock { } return [dict get $o_columndata $cidx] } - method debug {} { + method debug {args} { + #nice to lay this out with tables - but this is the one function where we really should provide the ability to avoid tables in case things get really broken (e.g major refactor) + set defaults [dict create\ + -usetables 1\ + ] + dict for {k v} $args { + switch -- $k { + -usetables {} + default { + error "table debug unrecognised option '$k'. Known options: [dict keys $defaults]" + } + } + } + set opts [dict merge $defaults $args] + set opt_usetables [dict get $opts -usetables] + puts stdout "rowdefs: $o_rowdefs" puts stdout "rowstates: $o_rowstates" - puts stdout "columndefs: $o_columndefs" + #puts stdout "columndefs: $o_columndefs" + puts stdout "columndefs:" + if {!$opt_usetables} { + dict for {k v} $o_columndefs { + puts " $k $v" + } + } else { + set t [textblock::class::table new] + $t add_column -headers "Col" + dict for {col coldef} $o_columndefs { + foreach property [dict keys $coldef] { + if {$property eq "-ansireset"} { + continue + } + $t add_column -headers $property + } + break + } + + #build our inner tables first so we can sync widths + set col_header_tables [dict create] + set max_widths [dict create 0 0 1 0 2 0 3 0] ;#max inner table column widths + dict for {col coldef} $o_columndefs { + set row [list $col] + set colheaders [dict get $coldef -headers] + #inner table probably overkill here ..but just as easy + set htable [textblock::class::table new] + $htable configure -show_header 1 -show_edge 0 -show_hseps 0 + $htable add_column -headers row + $htable add_column -headers text + $htable add_column -headers WxH + $htable add_column -headers span + set hnum 0 + set spans [dict get $o_columndefs $col -header_colspans] + foreach h $colheaders s $spans { + lassign [textblock::size $h] _w width _h height + $htable add_row [list "$hnum " $h "${width}x${height}" $s] + incr hnum + } + $htable configure_column 0 -ansibase [a+ web-dimgray] + dict set col_header_tables $col $htable + set colwidths [$htable column_widths] + set icol 0 + foreach w $colwidths { + if {$w > [dict get $max_widths $icol]} { + dict set max_widths $icol $w + } + incr icol + } + } + + dict for {col coldef} $o_columndefs { + set row [list $col] + dict for {property val} $coldef { + switch -- $property { + -ansireset {continue} + -headers { + set htable [dict get $col_header_tables $col] + dict for {innercol maxw} $max_widths { + $htable configure_column $innercol -minwidth $maxw -blockalign left + } + lappend row [$htable print] + $htable destroy + } + default { + lappend row $val + } + } + } + $t add_row $row + } + + + + + $t configure -show_header 1 + puts stdout [$t print] + $t destroy + } puts stdout "columnstates: $o_columnstates" puts stdout "headerstates: $o_headerstates" dict for {k coldef} $o_columndefs { @@ -1975,12 +2408,26 @@ namespace eval textblock { } else { set widest 0 } - append colinfo " widest: $widest" + append colinfo " widest of headers and data: $widest" } else { set colinfo "WARNING - no columndata record for column key '$k'" } puts stdout "column $k columndata info: $colinfo" } + set result "" + set cols [list] + set max [expr {[dict size $o_columndefs]-1}] + foreach c [dict keys $o_columndefs] { + if {$c == 0} { + lappend cols [my get_column_by_index $c -position left] " " + } elseif {$c == $max} { + lappend cols [my get_column_by_index $c -position right] + } else { + lappend cols [my get_column_by_index $c -position inner] " " + } + } + append result [textblock::join {*}$cols] + return $result } #column width including headers - but without colspan consideration method column_width_configured {index_expression} { @@ -2027,8 +2474,24 @@ namespace eval textblock { return $colwidth } - #column *body* content width method column_width {index_expression} { + if {[llength $o_calculated_column_widths] != [dict size $o_columndefs]} { + my calculate_column_widths -algorithm $o_column_width_algorithm + } + return [lindex $o_calculated_column_widths $index_expression] + } + method column_widths {} { + if {[llength $o_calculated_column_widths] != [dict size $o_columndefs]} { + my calculate_column_widths -algorithm $o_column_width_algorithm + } + return $o_calculated_column_widths + } + method width {} { + #calculate width based on assumption frame verticals are 1-wide - review - what about custom unicode double-wide frame? + } + + #column *body* content width + method basic_column_width {index_expression} { set cidx [lindex [dict keys $o_columndefs] $index_expression] if {$cidx eq ""} { return @@ -2040,6 +2503,7 @@ namespace eval textblock { set defmaxw [dict get $cdef -maxwidth] if {"$defminw$defmaxw" ne "" && $defminw eq $defmaxw} { #an exact width is defined for the column - no need to look at data width + #review - this can result in truncation of spanning headers - even though there is enough width in the span-cell to place the full header set colwidth $defminw } else { #set widest [my column_datawidth $cidx -headers 0 -data 1 -footers 0] @@ -2112,6 +2576,11 @@ namespace eval textblock { } set total_spanned_width [expr {$width_max + $others_width}] if {$thiscol_widest_header > $total_spanned_width} { + #this just allocates the extra space in the current column - which is not great. + #A proper algorithm for distributing width created by headers to all the spanned columns is needed. + #This is a tricky problem with multiple header lines and arbitrary spans. + #The calculation should probably be done on the table as a whole first and this function should just look up that result. + #Trying to calculate on a specific column only is unlikely to be easy or efficient. set needed [expr {$thiscol_widest_header - $total_spanned_width}] #puts "-->>col $cidx needed ($thiscol_widest_header - $total_spanned_width): $needed (spanned $spanned)" if {$defmaxw ne ""} { @@ -2263,7 +2732,248 @@ namespace eval textblock { return "No columns matched" } } + method columncalc_spans {allocmethod} { + set colwidths [dict create] ;# to use dict incr + set colspace_added [dict create] + + set ordered_spans [dict create] + dict for {col spandata} [my spangroups] { + set dwidth [my column_datawidth $col -data 1 -headers 0 -footers 0 -cached 1] + set minwidth [dict get $o_columndefs $col -minwidth] + set maxwidth [dict get $o_columndefs $col -maxwidth] + if {$minwidth ne ""} { + if {$dwidth < $minwidth} { + set dwidth $minwidth + } + } + if {$maxwidth ne ""} { + if {$dwidth > $maxwidth} { + set dwidth $maxwidth + } + } + dict set colwidths $col $dwidth ;#spangroups is ordered by column - so colwidths dict will be correctly ordered + dict set colspace_added $col 0 + + set spanlengths [dict get $spandata spanlengths] + foreach slen $spanlengths { + set spans [dict get $spandata spangroups $slen] + set spans [lsort -index 7 -integer $spans] + foreach s $spans { + set hwidth [dict get $s headerwidth] + set hrow [dict get $s hrow] + set scol [dict get $s startcol] + dict set ordered_spans $scol,$hrow membercols $col $dwidth + dict set ordered_spans $scol,$hrow headerwidth $hwidth + } + } + } + + dict for {spanid spandata} $ordered_spans { + lassign [split $spanid ,] startcol hrow + set memcols [dict get $spandata membercols] ;#dict with col and initial width - we ignore initial width, it's there in case we want to allocate space based on initial data width ratios + set colids [dict keys $memcols] + set hwidth [dict get $spandata headerwidth] + set num_cols_spanned [dict size $memcols] + if {$num_cols_spanned == 1} { + set col [lindex $memcols 0] + set space_to_alloc [expr {$hwidth - [dict get $colwidths $col]}] + if {$space_to_alloc > 0} { + dict set colwidths $col $hwidth + dict set colspace_added $col $space_to_alloc + } + } elseif {$num_cols_spanned > 1} { + set spannedwidth 0 + foreach col $colids { + incr spannedwidth [dict get $colwidths $col] + } + set space_to_alloc [expr {$hwidth - $spannedwidth}] + if {[my Showing_vseps]} { + set sepcount [expr {$num_cols_spanned -1}] + incr space_to_alloc -$sepcount + } + #review - we want to reduce overallocation to earlier or later columns - hence the use of colspace_added + switch -- $allocmethod { + 0 { + #add to least-expanded each time + #safer than method 1 - pretty balanced + if {$space_to_alloc > 0} { + for {set i 0} {$i < $space_to_alloc} {incr i} { + set ordered_colspace_added [lsort -stride 2 -index 1 -integer $colspace_added] + set ordered_all_colids [dict keys $ordered_colspace_added] + foreach testcolid $ordered_all_colids { + if {$testcolid in $colids} { + #assert - we will always find a match + set colid $testcolid + break + } + } + dict incr colwidths $colid + dict incr colspace_added $colid + } + } + } + 1 { + #adds space to all columns - not just those spanned - risk of underallocating and truncating some headers! + #probably not a good idea for tables with complex headers and spans + while {$space_to_alloc > 0} { + set ordered_colspace_added [lsort -stride 2 -index 1 -integer $colspace_added] + set ordered_colids [dict keys $ordered_colspace_added] + + foreach col $ordered_colids { + dict incr colwidths $col + dict incr colspace_added $col + incr space_to_alloc -1 + if {$space_to_alloc == 0} { + break + } + } + } + + } + } + } + } + + + return [list ordered_spans $ordered_spans colwidths [dict values $colwidths]] + } + + #spangroups keyed by column + method spangroups {} { + set column_count [dict size $o_columndefs] + set spangroups [dict create] + set headerwidths [dict create] ;#key on col,hrow + foreach c [dict keys $o_columndefs] { + dict set spangroups $c [list spanlengths {}] + set spanlist [my column_get_spaninfo $c] + set index_spanlen_val 5 + set spanlist [lsort -index $index_spanlen_val -integer $spanlist] + set ungrouped $spanlist + + while {[llength $ungrouped]} { + set spanlen [lindex $ungrouped 0 $index_spanlen_val] + set spangroup_posns [lsearch -all -index $index_spanlen_val $ungrouped $spanlen] + set sgroup [list] + foreach p $spangroup_posns { + set spaninfo [lindex $ungrouped $p] + set hcol [dict get $spaninfo startcol] + set hrow [dict get $spaninfo hrow] + set header [lindex [dict get $o_columndefs $hcol -headers] $hrow] + if {[dict exists $headerwidths $hcol,$hrow]} { + set hwidth [dict get $headerwidths $hcol,$hrow] + } else { + set hwidth [textblock::width $header] + dict set headerwidths $hcol,$hrow $hwidth + } + lappend spaninfo headerwidth $hwidth + lappend sgroup $spaninfo + } + set spanlengths [dict get $spangroups $c spanlengths] + lappend spanlengths $spanlen + dict set spangroups $c spanlengths $spanlengths + dict set spangroups $c spangroups $spanlen $sgroup + set ungrouped [lremove $ungrouped {*}$spangroup_posns] + } + } + return $spangroups + } + method column_get_own_spans {cidx} { + set colspans_for_column [dict get $o_columndefs $cidx -header_colspans] + } + method column_get_spaninfo {cidx} { + set spans_by_header [my header_colspans] + set colspans_for_column [dict get $o_columndefs $cidx -header_colspans] + set spaninfo [list] + set numcols [dict size $o_columndefs] + #note that 'all' can occur in positions other than column 0 - meaning all remaining + dict for {hrow rawspans} $spans_by_header { + set thiscol_spanval [lindex $rawspans $cidx] + if {$thiscol_spanval eq "all" || $thiscol_spanval > 0} { + set spanstartcol $cidx ;#own column + if {$thiscol_spanval eq "all"} { + set spanlen [expr {$numcols - $cidx}] + } else { + set spanlen $thiscol_spanval + } + } else { + #look left til we see an all or a non-zero value + for {set i $cidx} {$i > -1} {incr i -1} { + set s [lindex $rawspans $i] + if {$s eq "all" || $s > 0} { + set spanstartcol $i + if {$s eq "all"} { + set spanlen [expr {$numcols - $i}] + } else { + set spanlen $s + } + break + } + } + } + #assert - we should always find 1 answer for each header row + lappend spaninfo [list hrow $hrow startcol $spanstartcol spanlen $spanlen] + } + return $spaninfo + } + method calculate_column_widths {args} { + set column_count [dict size $o_columndefs] + + set defaults [dict create\ + -algorithm $o_column_width_algorithm\ + ] + dict for {k v} $args { + switch -- $k { + -algorithm {} + default { + error "Unknown option '$k'. Known options: [dict keys $defaults]" + } + } + } + set opts [dict merge $defaults $args] + set opt_algorithm [dict get $opts -algorithm] + #puts stderr "--- recalculating column widths -algorithm $opt_algorithm" + set known_algorithms [list basic simplistic span] + switch -- $opt_algorithm { + basic { + #basic column by column - This allocates extra space to first span/column as they're encountered. + #This can leave the table quite unbalanced with regards to whitespace and the table is also not as compact as it could be especially with regards to header colspans + #The header values can extend over some of the spanned columns - but not optimally so. + set o_calculated_column_widths [list] + for {set c 0} {$c < $column_count} {incr c} { + lappend o_calculated_column_widths [my basic_column_width $c] + } + } + simplistic { + #just uses the widest column data or header element. + #this is even less compact than basic and doesn't allow header colspan values to extend beyond their first spanned column + #This is a conservative option potentially useful in testing/debugging. + set o_calculated_column_widths [list] + for {set c 0} {$c < $column_count} {incr c} { + lappend o_calculated_column_widths [my column_width_configured $c] + } + } + span { + #widest of smallest spans first method + set calcresult [my columncalc_spans 0] + set o_calculated_column_widths [dict get $calcresult colwidths] + } + span2 { + #allocates more evenly - but truncates headers sometimes + set calcresult [my columncalc_spans 1] + set o_calculated_column_widths [dict get $calcresult colwidths] + } + default { + error "calculate_column_widths unknown algorithm $opt_algorithm" + } + } + #remember the last algorithm used + set o_column_width_algorithm $opt_algorithm + return $o_calculated_column_widths + } method print {args} { + variable full_column_cache + set full_column_cache [dict create] + if {![llength $args]} { set cols [dict keys $o_columndata] } else { @@ -2306,7 +3016,14 @@ namespace eval textblock { set flags [list -position inner] } #lappend blocks [my get_column_by_index $c {*}$flags] - set columninfo [my get_column_by_index $c -return dict {*}$flags] + #todo - only check and store in cache if table has header or footer colspans > 1 + if {[dict exists $full_column_cache $c]} { + #puts "!!print used full_column_cache for $c" + set columninfo [dict get $full_column_cache $c] + } else { + set columninfo [my get_column_by_index $c -return dict {*}$flags] + dict set full_column_cache $c $columninfo + } set nextcol [dict get $columninfo column] set bodywidth [dict get $columninfo bodywidth] @@ -2315,7 +3032,7 @@ namespace eval textblock { set height [textblock::height $table] ;#only need to get height once at start } else { set nextcol [textblock::join [textblock::block $padwidth $height $TSUB] $nextcol] - set table [overtype::renderspace -overflow 1 -experimental test_mode -transparent $TSUB $table $nextcol] + set table [overtype::renderspace -overflow 1 -experimental test_mode -transparent $TSUB $table[unset table] $nextcol] #JMN #set nextcol [textblock::join [textblock::block $padwidth $height "\uFFFF"] $nextcol] @@ -2327,6 +3044,42 @@ namespace eval textblock { if {[llength $cols]} { #return [textblock::join {*}$blocks] + if {[dict get $o_opts_table -show_edge]} { + #title is considered part of the edge ? + set offset 1 ;#make configurable? + set titlepad [string repeat $TSUB $offset] + if {[dict get $o_opts_table -title] ne ""} { + set titlealign [dict get $o_opts_table -titlealign] + switch -- $titlealign { + left { + set tstring $titlepad[dict get $o_opts_table -title] + } + right { + set tstring [dict get $o_opts_table -title]$titlepad + } + default { + set tstring [dict get $o_opts_table -title] + } + } + set opt_titletransparent [dict get $o_opts_table -titletransparent] + switch -- $opt_titletransparent { + 0 { + set mapchar "" + } + 1 { + set mapchar " " + } + default { + #won't work if not a single char - review - check also frame behaviour + set mapchar $opt_titletransparent + } + } + if {$mapchar ne ""} { + set tstring [string map [list $mapchar $TSUB] $tstring] + } + set table [overtype::block -blockalign $titlealign -transparent $TSUB $table[unset table] $tstring] + } + } return $table } else { return "No columns matched" @@ -2362,7 +3115,7 @@ namespace eval textblock { set t [list_as_table 5 {a b c d e aa bb cc dd ee X Y} -return object] $t configure_column 0 -headers {span3 span4 span5/5 "span-all etc blah 123 hmmmmm" span2} $t configure_column 0 -header_colspans {3 4 5 all 2} - $t configure_column 2 -headers {"" "" "" "" c2span2} + $t configure_column 2 -headers {"" "" "" "" c2span2_etc} $t configure_column 2 -header_colspans {0 0 0 0 2} $t configure -show_header 1 -ansiborder_header [a+ cyan] return $t @@ -2371,11 +3124,24 @@ namespace eval textblock { #more complex colspans proc spantest2 {} { set t [list_as_table 5 {a b c d e aa bb cc dd ee X Y} -return object] - $t configure_column 0 -headers {span3 span4 span5/5 "span-all etc blah 123 hmmmmm" span2} - $t configure_column 0 -header_colspans {3 4 1 all 1} + $t configure_column 0 -headers {c0span3 c0span4 c0span1 "c0span-all etc blah 123 hmmmmm" c0span2} + $t configure_column 0 -header_colspans {3 4 1 all 2} + $t configure_column 1 -header_colspans {0 0 2 0 0} $t configure_column 2 -headers {"" "" "" "" c2span2} - $t configure_column 1 -header_colspans {0 0 2 0 1} $t configure_column 2 -header_colspans {0 0 0 0 2} + $t configure_column 3 -header_colspans {1 0 2 0 0} + $t configure -show_header 1 -ansiborder_header [a+ cyan] + return $t + } + proc spantest3 {} { + set t [list_as_table 5 {a b c d e aa bb cc dd ee X Y} -return object] + $t configure_column 0 -headers {c0span3 c0span4 c0span1 "c0span-all etc blah 123 hmmmmm" "c0span2 etc blah" c0span1} + $t configure_column 0 -header_colspans {3 4 1 all 2 1} + $t configure_column 1 -header_colspans {0 0 4 0 0 1} + $t configure_column 1 -headers {"" "" "c1span4" "" "" "c1nospan"} + $t configure_column 2 -headers {"" "" "" "" "" c2span2} + $t configure_column 2 -header_colspans {0 0 0 0 1 2} + $t configure_column 4 -headers {"4" "444" "" "" "" "44"} $t configure -show_header 1 -ansiborder_header [a+ cyan] return $t } @@ -2493,7 +3259,7 @@ namespace eval textblock { #todo - keep simple table with symbols as base - map symbols to descriptions etc for more verbose table options - set header_0 [list "" 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18] + set header_0 [list 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18] set c 0 foreach h $header_0 { $t configure_column $c -headers [list $h] -minwidth 2 @@ -2667,12 +3433,29 @@ namespace eval textblock { lappend rainbow_list purple lappend rainbow_list cyan lappend rainbow_list {white Red} + + set rainbow_direction "horizontal" + set vpos [lsearch $colour vertical] + if {$vpos >= 0} { + set rainbow_direction vertical + set colour [lremove $colour $vpos] + } + set hpos [lsearch $colour horizontal] + if {$hpos >=0} { + #horizontal is the default and superfluous but allowed for symmetry + set colour [lremove $colour $hpos] + } + set chars [concat [punk::range 1 9] A B C D E F] set charsubset [lrange $chars 0 $size-1] - set RST [a] - if {"rainbow" in $colour} { + if {"noreset" in $colour} { + set RST "" + } else { + set RST [a] + } + if {"rainbow" in $colour && $rainbow_direction eq "vertical"} { #column first - colour change each column set c [::join $charsubset \n] @@ -2684,7 +3467,27 @@ namespace eval textblock { set ansicode [punk::ansi::codetype::sgr_merge_list "" $ansi] lappend clist ${ansicode}$c$RST } - return [textblock::join {*}$clist] + if {"noreset" in $colour} { + return [textblock::join -ansiresets 0 {*}$clist] + } else { + return [textblock::join {*}$clist] + } + } elseif {"rainbow" in $colour} { + #direction must be horizontal + set block "" + for {set r 0} {$r < $size} {incr r} { + set colour2 [string map [list rainbow [lindex $rainbow_list $r]] $colour] + set ansi [a+ {*}$colour2] + set ansicode [punk::ansi::codetype::sgr_merge_list "" $ansi] + set row "$ansicode" + foreach c $charsubset { + append row $c + } + append row $RST + append block $row\n + } + set block [string trimright $block \n] + return $block } else { #row first - set rows [list] @@ -2893,7 +3696,7 @@ namespace eval textblock { } #review - tcl format can only pad with zeros or spaces? - #experimental fallback to supposedly faster simpler 'format' but we still need to compensate for double-width or non-printing chars + #experimental fallback to supposedly faster simpler 'format' but we still need to compensate for double-width or non-printing chars - and it won't work on strings with ansi SGR codes if 0 { #review - surprisingly, this doesn't seem to be a performance win #No detectable diff on small blocks - slightly worse on large blocks @@ -2911,6 +3714,7 @@ namespace eval textblock { set lnwidth [punk::char::grapheme_width_cached $ln] set lnlen [string length $ln] set diff [expr $lnwidth - $lnlen] + #we need trickwidth to get format to pad a string with a different terminal width compared to string length set trickwidth [expr {$width - $diff}] ;#may 'subtract' a positive or negative int (ie will add to trickwidth if negative) lappend lines [format $fmt $trickwidth $ln] } @@ -2967,13 +3771,22 @@ namespace eval textblock { } } l-2 { - set line_chunks [linsert $line_chunks 0 $pad] + if {$lnum == 0} { + if {[lindex $line_chunks 0] eq ""} { + set line_chunks [linsert $line_chunks 2 $pad] + } else { + set line_chunks [linsert $line_chunks 0 $pad] + } + } else { + set line_chunks [linsert $line_chunks 0 $pad] + } } } } lappend lines [::join $line_chunks ""] set line_chunks [list] set line_len 0 + incr lnum } incr p } @@ -3002,14 +3815,20 @@ namespace eval textblock { } } r-2 { - lappend line_chunks $pad - } - l-0 { - if {[lindex $line_chunks 0] eq ""} { - set line_chunks [linsert $line_chunks 2 $pad] + if {[lindex $line_chunks end] eq ""} { + set line_chunks [linsert $line_chunks end-2 $pad] } else { - set line_chunks [linsert $line_chunks 0 $pad] + lappend line_chunks $pad } + #lappend line_chunks $pad + } + l-0 { + #if {[lindex $line_chunks 0] eq ""} { + # set line_chunks [linsert $line_chunks 2 $pad] + #} else { + # set line_chunks [linsert $line_chunks 0 $pad] + #} + set line_chunks [linsert $line_chunks 0 $pad] } l-1 { #set line_chunks [linsert $line_chunks 0 $pad] @@ -3074,12 +3893,38 @@ namespace eval textblock { return $t } - proc pad_test_blocklist {blocklist} { + proc pad_test_blocklist {blocklist args} { + set defaults [dict create\ + -description ""\ + -blockheaders ""\ + ] + foreach {k v} $args { + switch -- $k { + -description - -blockheaders {} + default { + error "pad_test_blocklist unrecognised option '$k'. Known options: [dict keys $defaults]" + } + } + } + set opts [dict merge $defaults $args] + set opt_blockheaders [dict get $opts -blockheaders] + set bheaders [dict create] + if {$opt_blockheaders ne ""} { + set b 0 + foreach h $opt_blockheaders { + if {$b < [llength $blocklist]} { + dict set bheaders $b $h + } + incr b + } + } + set b 0 set blockinfo [dict create] foreach block $blocklist { set width [textblock::width $block] - set padtowidth [expr {$width + 10}] + dict set blockinfo $b width $width + set padtowidth [expr {$width + 3}] dict set blockinfo $b left0 [textblock::pad $block -width $padtowidth -padchar . -which left -within_ansi 0] dict set blockinfo $b left1 [textblock::pad $block -width $padtowidth -padchar . -which left -within_ansi 1] dict set blockinfo $b left2 [textblock::pad $block -width $padtowidth -padchar . -which left -within_ansi 2] @@ -3092,21 +3937,43 @@ namespace eval textblock { set r0 [list "0"] set r1 [list "1"] set r2 [list "2"] + set r3 [list "column\ncolours"] + + #1 + #test without table padding + #we need to textblock::join each item to ensure we don't get the table's own textblock::pad interfering + #(basically a mechanism to add extra resets at start and end of each line) + #dict for {b bdict} $blockinfo { + # lappend r0 [textblock::join [dict get $blockinfo $b left0]] [textblock::join [dict get $blockinfo $b right0]] + # lappend r1 [textblock::join [dict get $blockinfo $b left1]] [textblock::join [dict get $blockinfo $b right1]] + # lappend r2 [textblock::join [dict get $blockinfo $b left2]] [textblock::join [dict get $blockinfo $b right2]] + #} + + #2 - the more useful one? dict for {b bdict} $blockinfo { - lappend r0 [dict get $blockinfo $b left0] [dict get $blockinfo $b right0] - lappend r1 [dict get $blockinfo $b left1] [dict get $blockinfo $b right1] + lappend r0 [dict get $blockinfo $b left0] [dict get $blockinfo $b right0] + lappend r1 [dict get $blockinfo $b left1] [dict get $blockinfo $b right1] lappend r2 [dict get $blockinfo $b left2] [dict get $blockinfo $b right2] + lappend r3 "" "" } - set rows [concat $r0 $r1 $r2] + + set rows [concat $r0 $r1 $r2 $r3] + + set column_ansi [a+ web-white Web-Gray] set t [textblock::list_as_table [expr {1 + (2 * [dict size $blockinfo])}] $rows -return object] - $t configure_column 0 -headers [list "" "within_ansi"] + $t configure_column 0 -headers [list [dict get $opts -description] "within_ansi"] -ansibase $column_ansi set col 1 dict for {b bdict} $blockinfo { - $t configure_column $col -headers [list "Block $b" "Left"] - $t configure_column $col -header_colspans 2 + if {[dict exists $bheaders $b]} { + set hdr [dict get $bheaders $b] + } else { + set hdr "Block $b" + } + $t configure_column $col -headers [list $hdr "Left"] -minwidth [expr {$padtowidth + 2}] + $t configure_column $col -header_colspans 2 -ansibase $column_ansi incr col - $t configure_column $col -headers [list "-" "Right"] + $t configure_column $col -headers [list "-" "Right"] -minwidth [expr {$padtowidth + 2}] -ansibase $column_ansi incr col } $t configure -show_header 1 @@ -3114,11 +3981,42 @@ namespace eval textblock { return $t } proc pad_example {} { - set b1 "[a+ green bold][textblock::block 4 4 x]\n[a]" - set b2 "[a+ green bold][textblock::block 4 4 x]\n[a+ Green]" - set b3 "[textblock::testblock 4 rainbow]\n[a]" - set b4 "[textblock::testblock 4 rainbow]\n[a+ Green]" - set t [textblock::pad_test_blocklist [list $b1 $b2 $b3 $b4]] + set headers [list] + set blocks [list] + + lappend blocks "[textblock::testblock 4 rainbow]" + lappend headers "rainbow 4x4\nresets at line extremes\nnothing trailing" + + lappend blocks "[textblock::testblock 4 rainbow][a]" + lappend headers "rainbow 4x4\nresets at line extremes\ntrailing reset" + + lappend blocks "[textblock::testblock 4 rainbow]\n[a+ Web-Green]" + lappend headers "rainbow 4x4\nresets at line extremes\ntrailing nl&green bg" + + lappend blocks "[textblock::testblock 4 {rainbow noreset}]" + lappend headers "rainbow 4x4\nno line resets\nnothing trailing" + + lappend blocks "[textblock::testblock 4 {rainbow noreset}][a]" + lappend headers "rainbow 4x4\nno line resets\ntrailing reset" + + lappend blocks "[textblock::testblock 4 {rainbow noreset}]\n[a+ Web-Green]" + lappend headers "rainbow 4x4\nno line resets\ntrailing nl&green bg" + + set t [textblock::pad_test_blocklist $blocks -description "trailing\nbg/reset\ntests" -blockheaders $headers] + } + proc pad_example2 {} { + set headers [list] + set blocks [list] + lappend blocks "[a+ web-red Web-steelblue][textblock::block 4 4 x]\n" + lappend headers "red on blue 4x4\nno inner resets\ntrailing nl" + + lappend blocks "[a+ web-red Web-steelblue][textblock::block 4 4 x]\n[a]" + lappend headers "red on blue 4x4\nno inner resets\ntrailing nl&reset" + + lappend blocks "[a+ web-red Web-steelblue][textblock::block 4 4 x]\n[a+ Web-Green]" + lappend headers "red on blue 4x4\nno inner resets\ntrailing nl&green bg" + + set t [textblock::pad_test_blocklist $blocks -description "trailing\nbg/reset\ntests" -blockheaders $headers] } @@ -3174,11 +4072,31 @@ namespace eval textblock { # blocks -type string -multiple 1 #} $args] _o opts _v values #set blocks [dict get $values blocks] - if {[lindex $args 0] eq "--"} { - set blocks [lrange $args 1 end] - } else { - set blocks $args + + #-ansireplays is always on (if ansi detected) + + #we will support -- at posn 0 and 2 only to allow an optional single option pair for -ansiresets + #textblock::join is already somewhat expensive - we don't want to do much argument processing + #also "--" is a legitimate joining block - so we need to support that too without too much risk of misinterpretation + #this makes for a somewhat messy api.. -- is required if first block is actually -- (or the very unlikely case the first block is intended to be -ansiresets) + set ansiresets auto + switch -- [lindex $args 0] { + -- { + set blocks [lrange $args 1 end] + } + -ansiresets { + if {[lindex $args 2] eq "--"} { + set blocks [lrange $args 3 end] + } else { + set blocks [lrange $args 2 end] + } + set ansiresets [lindex $args 1] + } + default { + set blocks $args + } } + if {![llength $blocks]} { return } @@ -3193,7 +4111,7 @@ namespace eval textblock { #for determining a shortcut to avoid unnecessary ta::detect - (e.g width <=1) we can't use result of textblock::width as it strips ansi. #testing of any decent size block line by line - even with max_string_length_line is slower than ta::detect anyway. if {[punk::ansi::ta::detect $b]} { - lappend fordata "v($idx)" [punk::lib::lines_as_list -ansiresets 1 -- $b] + lappend fordata "v($idx)" [punk::lib::lines_as_list -ansireplays 1 -ansiresets $ansiresets -- $b] } else { #each block is being rendered into its own empty column - we don't need resets if it has no ansi, even if blocks to left and right do have ansi lappend fordata "v($idx)" [split $b \n] @@ -4886,6 +5804,7 @@ namespace eval textblock { dict with framedef {} ;#extract vll,hlt,tlc etc vars #puts "---> $opt_boxmap" + #review - we handle double-wide in custom frames - what about for boxmaps? dict for {boxelement sub} $opt_boxmap { if {$boxelement eq "vl"} { set vll $sub @@ -4902,7 +5821,8 @@ namespace eval textblock { switch -- $frameset { custom { - + #REVIEW - textblock::table assumes that at least the vl elements are 1-wide + #generally supporting wider or taller custom frame elements would make for some interesting graphical possibilities though #if no ansi, these widths are reasonable to maintain in grapheme_width_cached indefinitely set vll_width [punk::ansi::printing_length $vll] set hlb_width [punk::ansi::printing_length $hlb] @@ -5030,6 +5950,7 @@ namespace eval textblock { set trc $opt_ansiborder$trc$rst set blc $opt_ansiborder$blc$rst set brc $opt_ansiborder$brc$rst + set lhs $opt_ansiborder$lhs$rst ;#wrap the whole block and let textblock::join figure it out set rhs $opt_ansiborder$rhs$rst }