# -*- tcl -*- # Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt # # Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. # Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ # (C) 2023 # # @@ Meta Begin # Application textblock 999999.0a1.0 # Meta platform tcl # Meta license # @@ Meta End # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ ## Requirements ##e.g package require frobz #package require punk package require punk::args package require punk::char package require punk::ansi package require punk::lib catch {package require patternpunk} package require overtype package require term::ansi::code::macros ;#required for frame if old ansi g0 used - review - make package optional? package require textutil namespace eval textblock { #review - what about ansi off in punk::console? namespace import ::punk::ansi::a ::punk::ansi::a+ namespace eval class { variable opts_table_defaults set opts_table_defaults [dict create\ -title ""\ -titlealign "left"\ -titletransparent 0\ -frametype "light"\ -frametype_header ""\ -ansibase_header ""\ -ansibase_body ""\ -ansibase_footer ""\ -ansiborder_header ""\ -ansiborder_body ""\ -ansiborder_footer ""\ -ansireset "\uFFeF"\ -framelimits_header "vll vlr hlb hlt tlc trc blc brc"\ -frametype_body ""\ -framelimits_body "vll vlr hlb hlt tlc trc blc brc"\ -framemap_body [list\ topleft {} topinner {} topright {} topsolo {}\ middleleft {} middleinner {} middleright {} middlesolo {}\ bottomleft {} bottominner {} bottomright {} bottomsolo {}\ onlyleft {} onlyinner {} onlyright {} onlysolo {}\ ]\ -framemap_header [list\ topleft {} topinner {} topright {} topsolo {}\ middleleft {} middleinner {} middleright {} middlesolo {}\ bottomleft {} bottominner {} bottomright {} bottomsolo {}\ onlyleft {} onlyinner {} onlyright {} onlysolo {}\ ]\ -show_edge 1\ -show_seps 1\ -show_hseps ""\ -show_vseps ""\ -show_header ""\ -show_footer ""\ -minwidth ""\ -maxwidth ""\ ] #for 'L' shaped table building pattern (tables bigger than 4x4 will be mostly 'L' patterns) #ie only vll,blc,hlb used for cells except top row and right column #top right cell uses all 'O' shape, other top cells use 'C' shape (hlt,tlc,vll,blc,hlb) #right cells use 'U' shape (vll,blc,hlb,brc,vlr) #e.g for 4x4 # C C C O # L L L U # L L L U #anti-clockwise elements set C [list hlt tlc vll blc hlb] set O [list trc hlt tlc vll blc hlb brc vlr] set L [list vll blc hlb] set U [list vll blc hlb brc vlr] set tops [list trc hlt tlc] set lefts [list tlc vll blc] set bottoms [list blc hlb brc] set rights [list trc brc vlr] variable table_edge_parts set table_edge_parts [dict create\ topleft [struct::set intersect $C [concat $tops $lefts]]\ topinner [struct::set intersect $C [concat $tops]]\ topright [struct::set intersect $O [concat $tops $rights]]\ topsolo [struct::set intersect $O [concat $tops $lefts $rights]]\ middleleft [struct::set intersect $L $lefts]\ middleinner [list]\ middleright [struct::set intersect $U $rights]\ middlesolo [struct::set intersect $U [concat $lefts $rights]]\ bottomleft [struct::set intersect $L [concat $lefts $bottoms]]\ bottominner [struct::set intersect $L $bottoms]\ bottomright [struct::set intersect $U [concat $bottoms $rights]]\ bottomsolo [struct::set intersect $U [concat $lefts $bottoms $rights]]\ onlyleft [struct::set intersect $C [concat $tops $lefts $bottoms]]\ onlyinner [struct::set intersect $C [concat $tops $bottoms]]\ onlyright [struct::set intersect $O [concat $tops $bottoms $rights]]\ onlysolo [struct::set intersect $O [concat $tops $lefts $bottoms $rights]]\ ] #for header rows - we don't consider the bottom border as part of the edge - even if table body has no rows #The usual-case of a single header line is the 'onlyleft,onlyinner,onlyright,onlysolo' set. variable header_edge_parts set header_edge_parts [dict create\ topleft [struct::set intersect $C [concat $tops $lefts]]\ topinner [struct::set intersect $C [concat $tops]]\ topright [struct::set intersect $O [concat $tops $rights]]\ topsolo [struct::set intersect $O [concat $tops $lefts $rights]]\ middleleft [struct::set intersect $L $lefts]\ middleinner [list]\ middleright [struct::set intersect $U $rights]\ middlesolo [struct::set intersect $U [concat $lefts $rights]]\ bottomleft [struct::set intersect $L [concat $lefts]]\ bottominner [list]\ bottomright [struct::set intersect $U $rights]\ bottomsolo [struct::set intersect $U [concat $lefts $rights]]\ onlyleft [struct::set intersect $C [concat $tops $lefts]]\ onlyinner [struct::set intersect $C $tops]\ onlyright [struct::set intersect $O [concat $tops $rights]]\ onlysolo [struct::set intersect $O [concat $tops $lefts $rights]]\ ] variable table_hseps set table_hseps [dict create\ topleft [list blc hlb]\ topinner [list blc hlb]\ topright [list blc hlb brc]\ topsolo [list blc hlb brc]\ middleleft [list blc hlb]\ middleinner [list blc hlb]\ middleright [list blc hlb brc]\ middlesolo [list blc hlb brc]\ bottomleft [list]\ bottominner [list]\ bottomright [list]\ bottomsolo [list]\ onlyleft [list]\ onlyinner [list]\ onlyright [list]\ onlysolo [list]\ ] variable table_vseps set table_vseps [dict create\ topleft [list]\ topinner [list vll tlc blc]\ topright [list vll tlc blc]\ topsolo [list]\ middleleft [list]\ middleinner [list vll tlc blc]\ middleright [list vll tlc blc]\ middlesolo [list]\ bottomleft [list]\ bottominner [list vll tlc blc]\ bottomright [list vll tlc blc]\ bottomsolo [list]\ onlyleft [list]\ onlyinner [list vll tlc blc]\ onlyright [list vll tlc blc]\ onlysolo [list]\ ] #e.g $t configure -framemap_body [table_edge_map " "] proc table_edge_map {char} { variable table_edge_parts set map [list] dict for {celltype parts} $table_edge_parts { set tmap [list] foreach p $parts { dict set tmap $p $char } dict set map $celltype $tmap } return $map } proc table_sep_map {char} { variable table_hseps set map [list] dict for {celltype parts} $table_hseps { set tmap [list] foreach p $parts { dict set tmap $p $char } dict set map $celltype $tmap } return $map } proc header_edge_map {char} { variable header_edge_parts set map [list] dict for {celltype parts} $header_edge_parts { set tmap [list] foreach p $parts { dict set tmap $p $char } dict set map $celltype $tmap } return $map } if {[info commands [namespace current]::table] eq ""} { #*** !doctools #[subsection {Namespace textblock::class}] #[para] class definitions #[list_begin itemized] [comment {- textblock::class groupings -}] # [item] # [para] [emph {handler_classes}] # [list_begin enumerated] oo::class create [namespace current]::table { #*** !doctools #[enum] CLASS [class interface_caphandler.registry] #[list_begin definitions] # [para] [emph METHODS] variable o_opts_table ;#options as configured by user (with exception of -ansireset) variable o_opts_table_effective; #options in effect - e.g with defaults merged in. variable o_columndefs variable o_columndata variable o_columnstates variable o_headerstates variable o_rowdefs 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 ;#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]] upvar ::textblock::class::opts_table_defaults tdefaults set o_opts_table_defaults $tdefaults if {[llength $args] == 1} { set args [list -title [lindex $args 0]] } if {[llength $args] %2 !=0} { error "[namespace current]::table constructor - unexpected argument count. Require single value being title, or name value pairs" } dict for {k v} $args { if {$k ni [dict keys $o_opts_table_defaults]} { error "[namespace current]::table unrecognised option '$k'. Known values [dict keys $o_opts_table_defaults]" } } #set o_opts_table [dict merge $o_opts_table_defaults $args] set o_opts_table $o_opts_table_defaults set o_opts_table_effective $o_opts_table_defaults 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 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] set requested_seps_h [dict get $o_opts_table -show_hseps] set requested_seps_v [dict get $o_opts_table -show_vseps] set seps $requested_seps set seps_h $requested_seps_h set seps_v $requested_seps_v if {$requested_seps eq ""} { if {$requested_seps_h eq ""} { set seps_h 1 } if {$requested_seps_v eq ""} { set seps_v 1 } } else { if {$requested_seps_h eq ""} { set seps_h $seps } if {$requested_seps_v eq ""} { set seps_v $seps } } return [dict create horizontal $seps_h vertical $seps_v] } method Get_frametypes {} { set requested_ft [dict get $o_opts_table -frametype] set requested_ft_header [dict get $o_opts_table -frametype_header] set requested_ft_body [dict get $o_opts_table -frametype_body] set ft $requested_ft set ft_header $requested_ft_header set ft_body $requested_ft_body switch -- $requested_ft { light { if {$requested_ft_header eq ""} { set ft_header heavy } if {$requested_ft_body eq ""} { set ft_body light } } default { if {$requested_ft_header eq ""} { set ft_header $requested_ft } if {$requested_ft_body eq ""} { set ft_body $requested_ft } } } return [dict create header $ft_header body $ft_body] } method Set_effective_framelimits {} { upvar ::textblock::class::opts_table_defaults tdefaults set default_blims [dict get $tdefaults -framelimits_body] set default_hlims [dict get $tdefaults -framelimits_header] set eff_blims [dict get $o_opts_table_effective -framelimits_body] set eff_hlims [dict get $o_opts_table_effective -framelimits_header] set requested_blims [dict get $o_opts_table -framelimits_body] set requested_hlims [dict get $o_opts_table -framelimits_header] set blims $eff_blims set hlims $eff_hlims switch -- $requested_blims { "default" { set blims $default_blims } default { #set blims $requested_blims set blims [list] foreach lim $requested_blims { switch -- $lim { hl { lappend blims hlt hlb } vl { lappend blims vll vlr } default { lappend blims $lim } } } set blims [lsort -unique $blims] } } dict set o_opts_table_effective -framelimits_body $blims switch -- $requested_hlims { "default" { set hlims $default_hlims } default { #set hlims $requested_hlims set hlims [list] foreach lim $requested_hlims { switch -- $lim { hl { lappend hlims hlt hlb } vl { lappend hlims vll vlr } default { lappend hlims $lim } } } set hlims [lsort -unique $hlims] } } dict set o_opts_table_effective -framelimits_header $hlims return [dict create body $blims header $hlims] } method configure args { if {![llength $args]} { return $o_opts_table } if {[llength $args] == 1} { if {[lindex $args 0] in [dict keys $o_opts_table_defaults]} { #query single option set k [lindex $args 0] set val [dict get $o_opts_table $k] set returndict [dict create option $k value $val ansireset "\x1b\[m"] set infodict [dict create] switch -- $k { -ansibase_header - -ansibase_body - -ansiborder_header - -ansiborder_body - -ansiborder_footer { dict set infodict debug [ansistring VIEW $val] } -framemap_body - -framemap_header - -framelimits_body - -framelimits_header { dict set returndict effective [dict get $o_opts_table_effective $k] } } dict set returndict info $infodict return $returndict #return [dict create option $k value $val ansireset "\x1b\[m" info $infodict] } else { error "textblock::table configure - unrecognised option '[lindex $args 0]'. Known values [dict keys $o_opts_table_defaults]" } } if {[llength $args] %2 != 0} { error "[namespace current]::table configure - unexpected argument count. Require name value pairs" } dict for {k v} $args { if {$k ni [dict keys $o_opts_table_defaults]} { error "[namespace current]::table configure - unrecognised option '$k'. Known values [dict keys $o_opts_table_defaults]" } } set checked_opts [list] dict for {k v} $args { switch -- $k { -ansibase_header - -ansibase_body - -ansiborder_header - -ansiborder-body - -ansiborder_footer { set parts [punk::ansi::ta::split_codes_single $v] ;#caller may have supplied separated codes eg "[a+ Yellow][a+ red]" set ansi_codes [list] ; foreach {pt code} $parts { if {$pt ne ""} { #we don't expect plaintext in an ansibase error "Unable to interpret $k 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 ansi_codes $code } } set ansival [punk::ansi::codetype::sgr_merge_singles $ansi_codes] lappend checked_opts $k $ansival } -frametype - -frametype_header - -frametype_body { #frametype will raise an error if v is not a valid custom dict or one of the known predefined types such as light,heavy,double etc lassign [textblock::frametype $v] _cat category _type ftype lappend checked_opts $k $v } -framemap_body - -framemap_header { #upvar ::textblock::class::opts_table_defaults tdefaults #set default_bmap [dict get $tdefaults -framemap_body] #todo - check keys and map if {[llength $v] == 1} { if {$v eq "default"} { upvar ::textblock::class::opts_table_defaults tdefaults set default_map [dict get $tdefaults $k] lappend checked_opts $k $default_map } else { error "textblock::table::configure invalid $k value $v. Expected the value 'default' or a dict e.g topleft {hl *}" } } else { dict for {subk subv} $v { switch -- $subk { topleft - topinner - topright - topsolo - middleleft - middleinner - middleright - middlesolo - bottomleft - bottominner - bottomright - bottomsolo - onlyleft - onlyinner - onlyright - onlysolo {} default { error "textblock::table::configure invalid $subk. Known values {topleft topinner topright topsolo middleleft middleinner middleright middlesolo bottomleft bottominner bottomright bottomsolo onlyleft onlyinner onlyright onlysolo}" } } dict for {seg subst} $subv { switch -- $seg { hl - hlt - hlb - vl - vll - vlr - trc - tlc - blc - brc {} default { error "textblock::table::configure invalid $subk value $seg. Known values {hl hlt hlb vl vll vlr trc tlc blc brc}" } } } } lappend checked_opts $k $v } } -framelimits_body - -framelimits_header { set specific_framelimits [list] foreach fl $v { switch -- $fl { "default" { lappend specific_framelimits trc hlt tlc vll blc hlb brc vlr } hl { lappend specific_framelimits hlt hlb } vl { lappend specific_framelimits vll vlr } hlt - hlb - vll - vlr - trc - tlc - blc - brc { lappend specific_framelimits $fl } default { error "textblock::table::configure invalid $k '$fl'. Known values {hl hlb hlt vl vll vlr trc tlc blc brc} (or default for all)" } } } lappend checked_opts $k $specific_framelimits } -ansireset { if {$v eq "\uFFEF"} { set RST "\x1b\[m" ;#[a] lappend checked_opts $k $RST } else { error "textblock::table::configure -ansireset is read-only. It is present only to prevent unwanted colourised output in configure commands" } } -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_edge { 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 - except if table -minwidth/-maxwidth come into play set o_calculated_column_widths [list] ;#invalidate cached column widths - a recalc will be forced when needed } -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] lappend checked_opts $k $v } -minwidth - -maxwidth { set o_calculated_column_widths [list] lappend checked_opts $k $v } default { lappend checked_opts $k $v } } } #all options checked - ok to update o_opts_table and o_opts_table_effective #set o_opts_table [dict merge $o_opts_table $checked_opts] dict for {k v} $args { switch -- $k { -framemap_header - -framemap_body { #framemaps don't require setting every key to update. #e.g configure -framemaps {topleft } #needs to merge with existing unspecified keys such as topright middleleft etc. if {$v eq "default"} { dict set o_opts_table $k default } else { if {[dict get $o_opts_table $k] eq "default"} { dict set o_opts_table $k $v } else { dict set o_opts_table $k [dict merge [dict get $o_opts_table $k] $v] } } } default { dict set o_opts_table $k $v } } } #use values from checked_opts for the effective opts dict for {k v} $checked_opts { switch -- $k { -framemap_body - -framemap_header { set existing [dict get $o_opts_table_effective $k] set updated $existing dict for {subk subv} $v { dict set updated $subk $subv } dict set o_opts_table_effective $k $updated } -framelimits_body - -framelimits_header { #my Set_effective_framelimits dict set o_opts_table_effective $k $v } default { dict set o_opts_table_effective $k $v } } } #ansireset exception dict set o_opts_table -ansireset [dict get $o_opts_table_effective -ansireset] return $o_opts_table } #integrate with struct::matrix - allows ::m format 2string $table method printmatrix {matrix} { set matrix_rowcount [$matrix rows] set matrix_colcount [$matrix columns] set table_colcount [my column_count] if {$table_colcount == 0} { for {set c 0} {$c < $matrix_colcount} {incr c} { my add_column -headers "" } } set table_colcount [my column_count] if {$table_colcount != $matrix_colcount} { error "textblock::table::printmatrix column count of table doesn't match column count of matrix" } if {[my row_count] > 0} { my row_clear } for {set r 0} {$r < $matrix_rowcount} {incr r} { my add_row [$matrix get row $r] } my print } method as_matrix {{cmd ""}} { if {$cmd eq ""} { set m [struct::matrix] } else { set m [struct::matrix $cmd] } $m add columns [dict size $o_columndata] $m add rows [dict size $o_rowdefs] dict for {k v} $o_columndata { $m set column $k $v } return $m } method add_column {args} { #*** !doctools #[call class::table [method add_column] [arg args]] set defaults [dict create\ -headers [list]\ -header_colspans [list]\ -footers [list]\ -defaultvalue ""\ -ansibase ""\ -ansireset "\uFFEF"\ -minwidth ""\ -maxwidth ""\ -blockalign centre\ -textalign left\ ] #initialise -ansireset with replacement char so we can keep in appropriate dict position for initial configure and then treat as read-only set o_opts_column_defaults $defaults if {[llength $args] %2 != 0} { error "[namespace current]::table::add_column unexpected argument count. Require name value pairs. Known options: [dict keys $defaults]" } dict for {k v} $args { if {$k ni [dict keys $defaults]} { error "[namespace current]::table::add_column unknown option '$k'. Known options: [dict keys $defaults]" } } set opts [dict merge $defaults $args] set colcount [dict size $o_columndefs] dict set o_columndata $colcount [list] dict set o_columndefs $colcount $defaults ;#ensure record exists 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]} { #configure failed - ensure o_columndata and o_columndefs entries are removed 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 #puts ">>> adding default values for column $colcount" 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 minwidthbodyseen $width } return $colcount } method column_count {} { return [dict size $o_columndefs] } method configure_column {index_expression args} { set cidx [lindex [dict keys $o_columndefs] $index_expression] if {$cidx eq ""} { error "textblock::table::configure_column - no column defined at index '$cidx'.Use add_column to define columns" } 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]" } dict for {k v} $args { if {$k ni [dict keys $o_opts_column_defaults]} { error "[namespace current]::table configure_column unknown option '$k'. Known options: [dict keys $o_opts_column_defaults]" } } set checked_opts [list] set hstates $o_headerstates ;#operate on a copy set colstate [dict get $o_columnstates $cidx] dict for {k v} $args { switch -- $k { -headers { 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 maxheightseen $this_header_height } else { dict set hstates $i maxheightseen $currentmax } 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 { #check columns to left to make sure each new colspan for this column makes sense in the overall context #user may have to adjust colspans in order left to right to avoid these check errors #note that 'all' represents span all up to the next non-zero defined colspan. set cspans [my header_colspans] set h 0 if {[llength $v] > [dict size $cspans]} { error "configure_column $cidx -header_colspans. Only [dict size $cspans] headers exist. Too many values supplied" } foreach s $v { if {$cidx == 0} { if {[string is integer -strict $s]} { if {$s < 1} { error "configure_column $cidx -header_colspans bad first value '$s' for header '$h' . First column cannot have span less than 1. use 'all' or a positive integer" } } else { if {$s ne "all" && $s ne ""} { error "configure_column $cidx -header_colspans unrecognised value '$s' for header '$h' - must be a positive integer or the keyword 'all'" } } } else { #if {![string is integer -strict $s]} { # if {$s ne "all" && $s ne ""} { # error "configure_column $cidx -header_colspans unrecognised value '$s' for header '$h' - must be a positive integer or the keyword 'all'" # } #} else { set header_spans [dict get $cspans $h] set remaining [lindex $header_spans 0] if {$remaining ne "all"} { incr remaining -1 } #look at spans defined for previous cols #we are presuming previous column entries are valid - and only validating if our new entry is ok under that assumption for {set c 0} {$c < $cidx} {incr c} { set span [lindex $header_spans $c] if {$span eq "all"} { set remaining "all" } else { if {$remaining eq "all"} { if {$span ne "0"} { #a previous column has ended the 'all' span set remaining [expr {$span -1}] } } else { if {$span eq "0"} { incr remaining -1 } else { set remaining [expr {$span -1}] } #allow to go negative } } } if {$remaining eq "all"} { #any int >0 ok - what about 'all' immediately following all? } else { if {$remaining > 0} { if {$s ne "0" && $s ne ""} { error "configure_column $cidx -header_colspans bad span $s for header '$h'. Remaining span is '$remaining' - so a zero colspan is required" } } else { if {$s == 0} { error "configure_column $cidx -header_colspans bad span $s for header '$h'. No span in place - need >=1 or 'all'" } } } #} } 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 { set parts [punk::ansi::ta::split_codes_single $v] ;#caller may have supplied separated codes eg "[a+ Yellow][a+ red]" set col_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 col_ansibase_items $code } } set col_ansibase [punk::ansi::codetype::sgr_merge_singles $col_ansibase_items] lappend checked_opts $k $col_ansibase } -ansireset { if {$v eq "\uFFEF"} { lappend checked_opts $k "\x1b\[m" ;# [a] } else { error "textblock::table::configure_column -ansireset is read-only. It is present only to prevent unwanted colourised output in configure commands" } } -blockalign - -textalign { switch -- $v { left - right { lappend checked_opts $k $v } centre - centre { lappend checked_opts $k centre } } } default { lappend checked_opts $k $v } } } #args checked - ok to update headerstates and columndefs and columnstates set o_headerstates $hstates dict set o_columnstates $cidx $colstate set current_opts [dict get $o_columndefs $cidx] set opts [dict merge $current_opts $checked_opts] dict set o_columndefs $cidx $opts if {"-headers" in [dict keys $args]} { #if the headerlist length for this column has shrunk,and it was the longest - we may now have excess entries in o_headerstates set zero_heights [list] dict for {hidx _v} $o_headerstates { #pass empty string for exclude_column so we don't exclude our own column if {[my header_height_calc $hidx ""] == 0} { lappend zero_heights $hidx } } foreach zidx $zero_heights { dict unset o_headerstates $zidx } } if {"-headers" in [dict keys $args] || "-header_colspans" in [dict keys $args]} { #check and adjust header_colspans for all columns } return [dict get $o_columndefs $cidx] } } method header_count {} { return [dict size $o_headerstates] } method header_count_calc {} { set max_headers 0 dict for {k cdef} $o_columndefs { set num_headers [llength [dict get $cdef -headers]] set max_headers [expr {max($max_headers,$num_headers)}] } return $max_headers } method header_height {header_index} { set idx [lindex [dict keys $o_headerstates $header_index]] return [dict get $o_headerstates $idx maxheightseen] } #review - use maxwidth (considering colspans) of each column to determine height after wrapping # -need to consider whether vertical expansion allowed / maxheight? method header_height_calc {header_index {exclude_column ""}} { set dataheight 0 if {$exclude_column eq ""} { set exclude_colidx "" } else { set exclude_colidx [lindex [dict keys $o_columndefs] $exclude_column] } dict for {cidx cdef} $o_columndefs { if {$exclude_colidx == $cidx} { continue } set headerlist [dict get $cdef -headers] if {$header_index < [llength $headerlist]} { set this_height [textblock::height [lindex $headerlist $header_index]] set dataheight [expr {max($dataheight,$this_height)}] } } return $dataheight } #return a dict keyed on header index with values representing colspans #e.g # 0 {all 0 0 0} 1 {1 1 1 1} 2 {2 0 1 1} 3 {3 0 0 1} # method header_colspans {} { set num_headers [my header_count_calc] set colspans_by_header [dict create] dict for {cidx cdef} $o_columndefs { set headerlist [dict get $cdef -headers] set colspans_for_column [dict get $cdef -header_colspans] for {set h 0} {$h < $num_headers} {incr h} { set headerspans [punk::lib::dict_getdef $colspans_by_header $h [list]] set defined_span [lindex $colspans_for_column $h] set i 0 set spanremaining [lindex $headerspans 0] if {$spanremaining ne "all"} { if {$spanremaining eq ""} { set spanremaining 1 } incr spanremaining -1 } foreach s $headerspans { if {$s eq "all"} { set spanremaining "all" } elseif {$s == 0} { if {$spanremaining ne "all"} { incr spanremaining -1 } } else { set spanremaining [expr {$s - 1}] } incr i } if {$defined_span eq ""} { if {$spanremaining eq "0"} { lappend headerspans 1 } else { #"all" or an integer lappend headerspans 0 } } else { lappend headerspans $defined_span } dict set colspans_by_header $h $headerspans } } 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] < [dict size $o_columndefs]} { puts stderr "textblock::table::configure_header warning - only [llength $v] spans specified for [dict size $o_columndefs] columns." puts stderr "It is recommended to set all spans explicitly. (auto-calc not implemented)" } 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" #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 # - 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]] if {[dict size $o_columndefs] > 0 && ([llength $valuelist] && [llength $valuelist] != [dict size $o_columndefs])} { set msg "" append msg "add_row - invalid number ([llength $valuelist]) of values in row - Must match existing column count: [dict size $o_columndefs]" \n append msg "rowdata: $valuelist" error $msg } if {[dict size $o_columndefs] == 0 && ![llength $valuelist]} { error "add_row - no values supplied, and no columns defined, so cannot use default column values" } set defaults [dict create\ -minheight 1\ -maxheight ""\ -ansibase ""\ -ansireset "\uFFEF"\ ] set o_opts_row_defaults $defaults if {[llength $args] %2 !=0} { error "[namespace current]::table::add_row unexpected argument count. Require name value pairs. Known options: [dict keys $defaults]" } dict for {k v} $args { switch -- $k { -minheight - -maxheight - -ansibase - -ansireset {} default { error "Invalid option '$k' Known options: [dict keys $defaults] (-ansireset is read-only)" } } } set opts [dict merge $defaults $args] set auto_columns 0 if {[dict size $o_columndefs] == 0} { set auto_columns 1 #no columns defined - auto define with defaults for each column in first supplied row #auto define columns only valid if no existing columns #the autocolumns must be added before configure_row - but if configure_row fails - we need to remove them! foreach el $valuelist { my add_column } } else { if {![llength $valuelist]} { dict for {k coldef} $o_columndefs { lappend valuelist [dict get $coldef -defaultvalue] } } } set rowcount [dict size $o_rowdefs] dict set o_rowdefs $rowcount $defaults ;# ensure record exists before configure if {[catch { my configure_row $rowcount {*}$opts } errMsg]} { #undo anything we saved before configure_row dict unset o_rowdefs $rowcount #remove auto_columns if {$auto_columns} { set o_columndata [dict create] set o_columndefs [dict create] set o_columnstate [dict create] } 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} { set max_height_seen $valheight } set width [textblock::width $v] 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)}] } else { dict set o_rowstates $rowcount -maxheight $max_height_seen } return $rowcount } method configure_row {index_expression args} { set ridx [lindex [dict keys $o_rowdefs] $index_expression] if {$ridx eq ""} { error "textblock::table::configure_row - no row defined at index '$ridx'.Use add_row to define rows" } 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]" } dict for {k v} $args { if {$k ni [dict keys $o_opts_row_defaults]} { error "[namespace current]::table configure_row unknown option '$k'. Known options: [dict keys $o_opts_row_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 row_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 row_ansibase_items $code } } set row_ansibase [punk::ansi::codetype::sgr_merge_singles $row_ansibase_items] lappend checked_opts $k $row_ansibase } -ansireset { if {$v eq "\uFFEF"} { lappend checked_opts $k "\x1b\[m" ;# [a] } else { error "textblock::table::configure_row -ansireset is read-only. It is present only to prevent unwanted colourised output in configure commands" } } default { lappend checked_opts $k $v } } } set current_opts [dict get $o_rowdefs $ridx] set opts [dict merge $current_opts $checked_opts] #check minheight and maxheight together set opt_minh [dict get $opts -minheight] set opt_maxh [dict get $opts -maxheight] if {![string is integer $opt_minh] || ($opt_maxh ne "" && ![string is integer -strict $opt_maxh])} { error "[namespace current]::table::add_row error -minheight '$opt_minh' and -maxheight '$opt_maxh' must be integers greater than or equal to 1" } if {$opt_minh < 1 || ($opt_maxh ne "" && $opt_maxh < 1)} { error "[namespace current]::table::add_row error -minheight '$opt_minh' and -maxheight '$opt_maxh' must both be 1 or greater" } if {$opt_maxh ne "" && $opt_maxh < $opt_minh} { error "[namespace current]::table::add_row error -maxheight '$opt_maxh' must greater than -minheight '$opt_minh'" } dict set o_rowstates $ridx -minheight $opt_minh dict set o_rowdefs $ridx $opts } method row_count {} { return [dict size $o_rowdefs] } method row_clear {} { set o_rowdefs [dict create] set o_rowstates [dict create] #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] #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 set o_columndefs [dict create] set o_columndata [dict create] set o_columnstates [dict create] } #method Get_columns_by_name {namematch_list} { #} #specify range with x..y method Get_columns_by_indices {index_list} { foreach spec $index_list { if {[string is integer -strict $c]} { set colidx $c } else { dict for {colidx coldef} $o_columndefs { #if {[string match x x]} {} } } } } method Get_boxlimits_and_joins {position fname_body} { #fname_body will be "custom" or one of the predefined types light,heavy etc switch -- $position { left { #set header_boxlimits {hlb hlt tlc blc vll} set header_body_joins [list down-$fname_body] set boxlimits_position {hlb blc vll} set boxlimits_toprow [concat $boxlimits_position {hlt tlc}] set joins {down} } inner { #set header_boxlimits {hlb hlt tlc blc vll} set header_body_joins [list left down-$fname_body] set boxlimits_position {hlb blc vll} set boxlimits_toprow [concat $boxlimits_position {hlt tlc}] set joins {down left} } right { #set header_boxlimits {hlb hlt tlc blc vll vlr trc brc} set header_body_joins [list left down-$fname_body] set boxlimits_position {hlb blc vll vlr brc} set boxlimits_toprow [concat $boxlimits_position {hlt tlc trc}] set joins {down left} } solo { #set header_boxlimits {hlb hlt tlc blc vll vlr trc brc} set header_body_joins [list down-$fname_body] set boxlimits_position {hlb blc vll vlr brc} set boxlimits_toprow [concat $boxlimits_position {hlt tlc trc}] set joins {down} } } 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"\ -return "string"\ ] dict for {k v} $args { switch -- $k { -position - -return {} default { error "[namespace current]::table::get_column_by_index error invalid option '$k'. Known options [dict keys $defaults]" } } } set opts [dict merge $defaults $args] set opt_posn [dict get $opts -position] set opt_return [dict get $opts -return] set valid_positions [list left inner right solo] switch -- $opt_posn { left - inner - right - solo {} default { error "[namespace current]::table::get_column_by_index error invalid value '$opt_posn' for -position. Valid values: $valid_positions" } } switch -- $opt_return { string - dict {} default { error "[namespace current]::table::get_column_by_index error invalid value '$opt_return' for -return. Valid values: string dict" } } set columninfo [my get_column_cells_by_index $index_expression] set header_list [dict get $columninfo headers] #puts "===== header_list: $header_list" set cells [dict get $columninfo cells] set topt_show_header [dict get $o_opts_table -show_header] if {$topt_show_header eq ""} { set allheaders "" set all_cols [dict keys $o_columndefs] foreach c $all_cols { set headerset [dict get $o_columndefs $c -headers] foreach hdr $headerset { append allheaders $hdr } } if {$allheaders eq ""} { set do_show_header 0 } else { set do_show_header 1 } } else { set do_show_header $topt_show_header } set topt_show_footer [dict get $o_opts_table -show_footer] set output "" set part_header "" set part_body "" set part_footer "" set boxlimits "" set joins "" set header_boxlimits [list] set header_body_joins [list] set ftypes [my Get_frametypes] set ftype_body [dict get $ftypes body] if {[llength $ftype_body] >= 2} { set fname_body "custom" } else { set fname_body $ftype_body } set ftype_header [dict get $ftypes header] if {[llength $ftype_header] >= 2} { set fname_header "custom" } else { set fname_header $ftype_header } switch -- $opt_posn { left { #set header_boxlimits {hlb hlt tlc blc vll} set header_body_joins [list down-$fname_body] set boxlimits_position {hlb blc vll} set boxlimits_toprow [concat $boxlimits_position {hlt tlc}] set joins {down} } inner { #set header_boxlimits {hlb hlt tlc blc vll} set header_body_joins [list left down-$fname_body] set boxlimits_position {hlb blc vll} set boxlimits_toprow [concat $boxlimits_position {hlt tlc}] set joins {down left} } right { #set header_boxlimits {hlb hlt tlc blc vll vlr trc brc} set header_body_joins [list left down-$fname_body] set boxlimits_position {hlb blc vll vlr brc} set boxlimits_toprow [concat $boxlimits_position {hlt tlc trc}] set joins {down left} } solo { #set header_boxlimits {hlb hlt tlc blc vll vlr trc brc} set header_body_joins [list down-$fname_body] set boxlimits_position {hlb blc vll vlr brc} set boxlimits_toprow [concat $boxlimits_position {hlt tlc trc}] set joins {down} } } set limj [my Get_boxlimits_and_joins $opt_posn $fname_body] set header_body_joins [dict get $limj bodyjoins] set joins [dict get $limj joins] set boxlimits_position [dict get $limj boxlimits] set boxlimits_toprow [dict get $limj boxlimits_top] #use struct::set instead of simple for loop - will be faster at least when critcl available set boxlimits [struct::set intersect [dict get $o_opts_table_effective -framelimits_body] $boxlimits_position] set boxlimits_headerless [struct::set intersect [dict get $o_opts_table_effective -framelimits_body] $boxlimits_toprow] set header_boxlimits [struct::set intersect [dict get $o_opts_table_effective -framelimits_header] $boxlimits_position] set header_boxlimits_toprow [struct::set intersect [dict get $o_opts_table_effective -framelimits_header] $boxlimits_toprow] set fmap [dict get $o_opts_table_effective -framemap_body] set hmap [dict get $o_opts_table_effective -framemap_header] #if {![dict get $o_opts_table -show_edge]} { # set body_edgemap [textblock::class::table_edge_map ""] # dict for {k v} $fmap { # #dict set fmap $k [dict merge $v [dict get $body_edgemap $k]] # } # set header_edgemap [textblock::class::header_edge_map ""] # dict for {k v} $hmap { # #dict set hmap $k [dict merge $v [dict get $header_edgemap $k]] # } #} set sep_elements_horizontal $::textblock::class::table_hseps set sep_elements_vertical $::textblock::class::table_vseps set topmap [dict get $fmap top$opt_posn] set botmap [dict get $fmap bottom$opt_posn] set midmap [dict get $fmap middle$opt_posn] set onlymap [dict get $fmap only$opt_posn] set hdrmap [dict get $hmap only${opt_posn}] set topseps_h [dict get $sep_elements_horizontal top$opt_posn] set topseps_v [dict get $sep_elements_vertical top$opt_posn] set midseps_h [dict get $sep_elements_horizontal middle$opt_posn] set midseps_v [dict get $sep_elements_vertical middle$opt_posn] set botseps_v [dict get $sep_elements_vertical bottom$opt_posn] set onlyseps_v [dict get $sep_elements_vertical only$opt_posn] #top should work for all header rows regarding vseps - as we only use it to reduce the box elements, and lower headers have same except for tlc which isn't present anyway set headerseps_v [dict get $sep_elements_vertical top$opt_posn] lassign [my Get_seps] _h show_seps_h _v show_seps_v 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} { #puts "boxlimitsinfo header $opt_posn: -- boxlimits $header_boxlimits -- boxmap $hdrmap" set ansibase_header [dict get $o_opts_table -ansibase_header] ;#merged to single during configure set ansiborder_header [dict get $o_opts_table -ansiborder_header] if {[dict get $o_opts_table -frametype_header] eq "block"} { set extrabg [punk::ansi::codetype::sgr_merge_singles [list $ansibase_header] -filter_fg 1] set ansiborder_final $ansibase_header$ansiborder_header$extrabg } else { set ansiborder_final $ansibase_header$ansiborder_header } set RST [punk::ansi::a] 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 set fdef_header [textblock::framedef $ftype_header] set span_extend_map [dict create \ vll " "\ tlc [dict get $fdef_header hlt]\ blc [dict get $fdef_header hlb]\ ] set framedef_leftbox [textblock::framedef $ftype_header -joins left] #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 $hrow] set this_span [lindex $headerspans $cidx] set hval $ansibase_header$header ;#no reset set rowh [my header_height $hrow] #set h_lines [lrepeat $rowh $hcell_line_blank] #set hcell_blank [join $h_lines \n] #set hval_lines [split $hval \n] #set hval_lines [lrange $hval_lines 0 $rowh-1] #set hval_block [join $hval_lines \n] #set headercell [overtype::left -experimental test_mode $ansibase_header$hcell_blank$RST $hval_block] if {$hrow == 0} { set hlims $header_boxlimits_toprow set rowpos "top" if {$hrow == $hmax} { set rowpos "only" } } else { set hlims $header_boxlimits set rowpos "middle" if {$hrow == $hmax} { set rowpos "bottom" } } if {!$show_seps_v} { set hlims [struct::set difference $hlims $headerseps_v] } if {$hrow == $hmax} { set header_joins $header_body_joins } else { set header_joins $joins } if {![dict get $o_opts_table -show_edge]} { 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} { 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 {$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 dict set startmap blc [dict get $framedef_leftbox blc] } } else { set next_spanlist [list] } } #supporting wrapping in headers might be a step too difficult for little payoff. #we would need a flag to enable/disable - plus language-based wrapping alg (see tcllib) #The interaction with colspans and width calculations makes it likely to have a heavy performance impact and make the code even more complex. #May be better to require user to pre-wrap as needed ##set hval [textblock::renderspace -width $colwidth -wrap 1 "" $hval] #review - contentwidth of hval can be greater than $colwidth+2 - if so frame function will detect and frame_cache will not be used #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 {$hcolwidth+2}] -type [dict get $ftypes header]\ # -ansibase $ansibase_header -ansiborder $ansiborder_final\ # -boxlimits $hlims -boxmap $startmap -joins $header_joins $hval\ # ] if {$this_span eq "1"} { #write the actual value now set cellcontents $hval } else { #just write an empty vertical placeholder. The spanned value will be overtyped below set cellcontents [join [lrepeat [llength [split $hval \n]] ""] \n] } set header_cell_startspan [textblock::frame -blockalign $col_blockalign -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 $cellcontents\ ] 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] set remaining_spans [lrange $headerspans $cidx+1 end] #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? review - currently doesn't cache with -height and no contents - slow set last [expr {[llength $remaining_spans] -1}] set i 0 foreach s $remaining_spans { if {$s == 0} { if {$i == $last} { set next_posn right #set next_posn inner } else { 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] set span_boxlimits [dict get $limj boxlimits] set span_boxlimits_top [dict get $limj boxlimits_top] #use struct::set instead of simple for loop - will be faster at least when critcl available #set span_boxlimits [struct::set intersect [dict get $o_opts_table_effective -framelimits_body] $span_boxlimits] #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 {$hrow == 0} { set hlims $header_span_boxlimits_top } else { set hlims $header_span_boxlimits } set this_span_map $span_extend_map if {!$show_seps_v} { set hlims [struct::set difference $hlims $next_headerseps_v] } else { if {[llength $next_spanlist]} { set spanbelow [lindex $next_spanlist $spancol] if {$spanbelow != 0} { set downbox [textblock::framedef $ftype_header -joins {down}] dict set this_span_map blc [dict get $downbox hlbj] ;#horizontal line bottom with down join - to same frametype } } else { #join to body set downbox [textblock::framedef $ftype_header -joins [list down-$fname_body]] dict set this_span_map blc [dict get $downbox hlbj] ;#horizontal line bottom with down join - from header frametype to body frametype } } if {$hrow == $hmax} { set header_joins $span_joins_body } else { set header_joins $span_joins } if {![dict get $o_opts_table -show_edge]} { set hlims [struct::set difference $hlims [dict get $::textblock::class::header_edge_parts $rowpos$next_posn] ] } 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\ ] lappend spanned_parts $header_cell } else { break } incr spancol incr i } set spanned_frame [textblock::join {*}$spanned_parts] if {$hrow == 0} { set hlims $header_boxlimits_toprow } else { set hlims $header_boxlimits } if {!$show_seps_v} { set hlims [struct::set difference $hlims $headerseps_v] } if {![dict get $o_opts_table -show_edge]} { #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 " "] ;#transparent overlay elements #set spacemap [list hl * vl * tlc * blc * trc * brc *] #-usecache 1 ok #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] #spanned values default left - todo make configurable set spanned_frame [overtype::block -blockalign $col_blockalign -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 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. #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 # - we can avoid using a frame - but we potentially need to manually adjust for show_hpos show_edge etc #avoiding frame here is faster.. but not by much (<10%? on textblock::spantest2 ) if 0 { #breaks -show_edge 0 if {$rowpos eq "top" && [dict get $o_opts_table -show_edge]} { set padheight [expr {$rowh + 2}] } else { set padheight [expr {$rowh + 1}] } set bline [string repeat $TSUB [expr {$padwidth +1}]] set h_lines [lrepeat $padheight $bline] set hcell_blank [::join $h_lines \n] set header_frame $hcell_blank } else { set bline [string repeat $TSUB $padwidth] set h_lines [lrepeat $rowh $bline] set hcell_blank [::join $h_lines \n] # -usecache 1 ok #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 hrow } if {![llength $header_list]} { #no headers - but we've been asked to show_header #display a zero content-height header (ie outline if edge is being shown - or bottom bar) set hlims $header_boxlimits_toprow if {!$show_seps_v} { set hlims [struct::set difference $hlims $headerseps_v] } if {![dict get $o_opts_table -show_edge]} { 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 {$hcolwidth+2}] -type [dict get $ftypes header]\ -ansibase $ansibase_header -ansiborder $ansiborder_final\ -boxlimits $hlims -boxmap $hdrmap -joins $header_joins\ ] append part_header $header_frame\n } set part_header [string trimright $part_header \n] lassign [textblock::size $part_header] _w return_headerwidth _h return_headerheight set padline [string repeat $TSUB $return_headerwidth] set adjusted_lines [list] foreach ln [split $part_header \n] { if {[string first $TSUB $ln] >=0} { lappend adjusted_lines $padline } else { lappend adjusted_lines $ln } } set part_header [join $adjusted_lines \n] append output $part_header \n } set r 0 set rmax [expr {[llength $cells]-1}] set blims_mid $boxlimits set blims_top $boxlimits set blims_bot $boxlimits set blims_top_headerless $boxlimits_headerless set blims_only $boxlimits set blims_only_headerless $boxlimits_headerless if {!$show_seps_h} { set blims_mid [struct::set difference $blims_mid $midseps_h] set blims_top [struct::set difference $blims_top $topseps_h] set blims_top_headerless [struct::set difference $blims_top_headerless $topseps_h] } if {!$show_seps_v} { set blims_mid [struct::set difference $blims_mid $midseps_v] set blims_top [struct::set difference $blims_top $topseps_v] set blims_top_headerless [struct::set difference $blims_top_headerless $topseps_v] set blims_bot [struct::set difference $blims_bot $botseps_v] set blims_only [struct::set difference $blims_only $onlyseps_v] set blims_only_headerless [struct::set difference $blims_only_headerless $onlyseps_v] } set colidx [lindex [dict keys $o_columndefs] $index_expression] ;#convert possible end-1,2+2 etc expression to >= 0 integer in dict range set opt_col_ansibase [dict get $o_columndefs $colidx -ansibase] ;#ordinary merge of codes already done in configure_column #set colwidth [my column_width $colidx] set body_ansibase [dict get $o_opts_table -ansibase_body] #set ansibase [punk::ansi::codetype::sgr_merge_singles [list $body_ansibase $opt_col_ansibase]] ;#allow col to override body set body_ansiborder [dict get $o_opts_table -ansiborder_body] if {[dict get $o_opts_table -frametype] eq "block"} { #block is the only style where bg colour can fill the frame content area exactly if the L-shaped border elements are styled #we need to only accept background ansi codes from the columndef ansibase for this set col_bg [punk::ansi::codetype::sgr_merge_singles [list $opt_col_ansibase] -filter_fg 1] ;#special merge for block borders - don't override fg colours set border_ansi $body_ansibase$body_ansiborder$col_bg } else { set border_ansi $body_ansibase$body_ansiborder } set r 0 set ftblock [expr {[dict get $o_opts_table -frametype] eq "block"}] foreach c $cells { set ansibase $body_ansibase$opt_col_ansibase set row_ansibase [dict get $o_rowdefs $r -ansibase] #todo - joinleft,joinright,joindown based on opts in args #append output [textblock::frame -boxlimits {vll blc hlb} $c]\n set cell_ansibase "" set row_bg "" if {$row_ansibase ne ""} { set row_bg [punk::ansi::codetype::sgr_merge_singles [list $row_ansibase] -filter_fg 1] } set ansiborder_body_col_row $border_ansi$row_bg set ansiborder_final $ansiborder_body_col_row #$c will always have ansi resets due to overtype behaviour ? #todo - review overtype if {[punk::ansi::ta::detect $c]} { #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] #we have detected ansi - so there will always be at least 3 parts beginning and ending with pt pt,ansi,pt,ansi...,pt set codes [list] foreach {pt cd} $parts { if {$cd ne ""} { lappend codes $cd } } #set takebg [lindex $parts end-1] #set cell_bg [punk::ansi::codetype::sgr_merge_singles [list $takebg] -filter_fg 1] set cell_bg [punk::ansi::codetype::sgr_merge_singles $codes -filter_fg 1 -filter_reset 1] #puts --->[ansistring VIEW $codes] if {[punk::ansi::codetype::is_sgr_reset [lindex $codes end]]} { if {[punk::ansi::codetype::is_sgr_reset [lindex $codes end-1]]} { #special case double reset at end of content set cell_ansi_tail [punk::ansi::codetype::sgr_merge_singles $codes] ;#no filters set ansibase "" set row_ansibase "" if {$ftblock} { set ansiborder_final [punk::ansi::codetype::sgr_merge [list $ansiborder_body_col_row] -filter_bg 1] } set cell_ansibase $cell_ansi_tail } else { #single trailing reset in content set cell_ansibase "" ;#cell doesn't contribute to frame's ansibase } } else { if {$ftblock} { #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 } } set ansibase_final $ansibase$row_ansibase$cell_ansibase if {$r == 0} { if {$r == $rmax} { set joins [lremove $joins [lsearch $joins down*]] set bmap $onlymap if {$do_show_header} { set blims $blims_only } else { set blims $blims_only_headerless } if {![dict get $o_opts_table -show_edge]} { set blims [struct::set difference $blims [dict get $::textblock::class::table_edge_parts only$opt_posn] ] } } else { set bmap $topmap if {$do_show_header} { set blims $blims_top } else { set blims $blims_top_headerless } if {![dict get $o_opts_table -show_edge]} { set blims [struct::set difference $blims [dict get $::textblock::class::table_edge_parts top$opt_posn] ] } } set rowframe [textblock::frame -type [dict get $ftypes body] -width [expr {$colwidth+2}] -blockalign $col_blockalign -ansibase $ansibase_final -ansiborder $ansiborder_final -boxlimits $blims -boxmap $bmap -joins $joins $c] set return_bodywidth [textblock::width $rowframe] append part_body $rowframe \n } else { if {$r == $rmax} { set joins [lremove $joins [lsearch $joins down*]] set bmap $botmap set blims $blims_bot if {![dict get $o_opts_table -show_edge]} { set blims [struct::set difference $blims [dict get $::textblock::class::table_edge_parts bottom$opt_posn] ] } } else { set bmap $midmap set blims $blims_mid ;#will only be reduced from boxlimits if -show_seps was processed above if {![dict get $o_opts_table -show_edge]} { set blims [struct::set difference $blims [dict get $::textblock::class::table_edge_parts middle$opt_posn] ] } } append part_body [textblock::frame -type [dict get $ftypes body] -width [expr {$colwidth+2}] -blockalign $col_blockalign -ansibase $ansibase_final -ansiborder $ansiborder_final -boxlimits $blims -boxmap $bmap -joins $joins $c]\n } incr r } #return empty (zero content height) row if no rows if {![llength $cells]} { set joins [lremove $joins [lsearch $joins down*]] #we need to know the width of the column to setup the empty cell properly #even if no header displayed - we should take account of any defined column widths set colwidth [my column_width $index_expression] if {$do_show_header} { set blims $blims_only } else { append part_body \n set blims $blims_only_headerless } #note that if show_edge is 0 - then for this empty line - we will not expect to see any bars #This is because the frame with no data had vertical components made entirely of corner elements #we could just append a newline when -show_edge is zero, but we want the correct width even when empty - for proper column joins when there are colspanned headers. # if {![dict get $o_opts_table -show_edge]} { #set blims [struct::set difference $blims [dict get $::textblock::class::table_edge_parts only$opt_posn] ] #append output [textblock::frame -width [expr {$colwidth + 2}] -type [dict get $ftypes body] -boxlimits $blims -boxmap $onlymap -joins $joins]\n append part_body [string repeat " " $colwidth] \n set return_bodywidth $colwidth } else { set emptyframe [textblock::frame -width [expr {$colwidth + 2}] -type [dict get $ftypes body] -boxlimits $blims -boxmap $onlymap -joins $joins] append part_body $emptyframe \n set return_bodywidth [textblock::width $emptyframe] } } #assert bodywidth is integer >=0 whether there are rows or not #trim only 1 newline if {[string index $part_body end] eq "\n"} { set part_body [string range $part_body 0 end-1] } set return_bodyheight [textblock::height $part_body] append output $part_body if {$opt_return eq "string"} { return $output } else { return [dict create column $output headerwidth $return_headerwidth headerheight $return_headerheight bodywidth $return_bodywidth bodyheight $return_bodyheight] } } method get_column_cells_by_index {index_expression} { set cidx [lindex [dict keys $o_columndefs] $index_expression] if {$cidx eq ""} { set range "" if {[dict size $o_columndefs] > 0} { set range "0..[expr {[dict size $o_columndefs] -1}]" } else { set range empty } error "table::get_column_cells_by_index no such index $index_expression. Valid range is $range" } #assert cidx is integer >=0 set cdef [dict get $o_columndefs $cidx] set headerlist [dict get $cdef -headers] set num_header_rows [my header_count] set ansibase_body [dict get $o_opts_table -ansibase_body] set ansibase_col [dict get $cdef -ansibase] set textalign [dict get $cdef -textalign] switch -- $textalign { left {set pad right} right {set pad left} default { set pad "centre" ;#todo? } } set ansibase_header [dict get $o_opts_table -ansibase_header] #set header_underlay $ansibase_header$cell_line_blank #set hdrwidth [my column_width_configured $cidx] set all_colspans [my header_colspans] #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] set showing_vseps [my Showing_vseps] 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 this_hdrwidth [my column_datawidth $cidx -headers 1 -colspan $this_span] ;#widest of headers in this col with same span - allows textalign to work with blockalign set hcell_line_blank [string repeat " " $this_hdrwidth] set hcell_lines [lrepeat $header_maxdataheight $hcell_line_blank] set hval_lines [split $hdr \n] set hval_lines [concat $hval_lines $hcell_lines] set hval_lines [lrange $hval_lines 0 $header_maxdataheight-1] ;#vertical align top set hval_block [::join $hval_lines \n] set hcell [textblock::pad $hval_block -width $this_hdrwidth -padchar " " -within_ansi 1 -which $pad] dict lappend output headers $hcell } #set colwidth [my column_width $cidx] #set cell_line_blank [string repeat " " $colwidth] set datawidth [my column_datawidth $cidx -headers 0 -footers 0 -data 1 -cached 1] set cell_line_blank [string repeat " " $datawidth] 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 { #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] if {"$rowdefminh$rowdefmaxh" ne "" && $rowdefminh eq $rowdefmaxh} { #an exact height is defined for the row set rowh $rowdefminh } else { if {$rowdefminh eq ""} { if {$rowdefmaxh eq ""} { #both defs empty set rowh $maxdataheight } else { set rowh [expr {min(1,$rowdefmaxh,$maxdataheight)}] } } else { if {$rowdefmaxh eq ""} { set rowh [expr {max($rowdefminh,$maxdataheight)}] } else { if {$maxdataheight < $rowdefminh} { set rowh $rowdefminh } else { set rowh [expr {max($rowdefminh,$maxdataheight)}] } } } } set cell_lines [lrepeat $rowh $cell_line_blank] #set cell_blank [join $cell_lines \n] set cval_lines [split $cval \n] set cval_lines [concat $cval_lines $cell_lines] set cval_lines [lrange $cval_lines 0 $rowh-1] set cval_block [::join $cval_lines \n] 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] dict lappend output cells $cell incr r } return $output } method get_column_values_by_index {index_expression} { set cidx [lindex [dict keys $o_columndefs] $index_expression] if {$cidx eq ""} { return } return [dict get $o_columndata $cidx] } 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:" 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 { if {[dict exists $o_columndata $k]} { set headerlist [dict get $coldef -headers] set coldata [dict get $o_columndata $k] set colinfo "rowcount: [llength $coldata]" set allfields [concat $headerlist $coldata] if {[llength $allfields]} { set widest [tcl::mathfunc::max {*}[lmap v $allfields {textblock::width $v}]] } else { set widest 0 } 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} { set cidx [lindex [dict keys $o_columndefs] $index_expression] if {$cidx eq ""} { return } #assert cidx is now >=0 integer within the range of defined columns set cdef [dict get $o_columndefs $cidx] set defminw [dict get $cdef -minwidth] 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 set colwidth $defminw } else { #set widest [my column_datawidth $cidx -headers 1 -data 1 -footers 1] set hwidest [dict get $o_columnstates $cidx maxwidthheaderseen] #set hwidest [my column_datawidth $cidx -headers 1 -colspan 1 -data 0 -footers 1] #set hwidest_singlespan ?? set bwidest [dict get $o_columnstates $cidx maxwidthbodyseen] set widest [expr {max($hwidest,$bwidest)}] #todo - predetermine if any items in column contain newlines and are truncated vertically due to o_rowdefs configuration. #if so - a truncated line shouldn't be included in our width calculation if {$defminw eq ""} { if {$defmaxw eq ""} { set colwidth $widest } else { set colwidth [expr {min($defmaxw,$widest)}] } } else { if {$defmaxw eq ""} { set colwidth [expr {max($defminw,$widest)}] } else { if {$widest < $defminw} { set colwidth $defminw } else { if {$widest > $defmaxw} { set colwidth $defmaxw } else { set colwidth [expr {max($defminw,$widest)}] } } } } } return $colwidth } 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 } #width of a table includes borders and seps #whereas width of a column refers to the borderless width (inner width) method width {} { #calculate width based on assumption frame verticals are 1-wide - review - what about custom unicode double-wide frame? set colwidths [my column_widths] set contentwidth [tcl::mathop::+ {*}$colwidths] set twidth $contentwidth if {[my Showing_vseps]} { incr twidth [llength $colwidths] incr twidth -1 } if {[dict get $o_opts_table -show_edge]} { incr twidth 2 } return $twidth } #column *body* content width method basic_column_width {index_expression} { set cidx [lindex [dict keys $o_columndefs] $index_expression] if {$cidx eq ""} { return } #puts "===column_width $index_expression" #assert cidx is now >=0 integer within the range of defined columns set cdef [dict get $o_columndefs $cidx] set defminw [dict get $cdef -minwidth] 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] set widest [dict get $o_columnstates $cidx maxwidthbodyseen] #todo - predetermine if any items in column contain newlines and are truncated vertically due to o_rowdefs configuration. #if so - a truncated line shouldn't be included in our width calculation if {$defminw eq ""} { if {$defmaxw eq ""} { set colwidth $widest } else { set colwidth [expr {min($defmaxw,$widest)}] } } else { if {$defmaxw eq ""} { set colwidth [expr {max($defminw,$widest)}] } else { if {$widest < $defminw} { set colwidth $defminw } else { if {$widest > $defmaxw} { set colwidth $defmaxw } else { set colwidth [expr {max($defminw,$widest)}] } } } } } set configured_widths [list] foreach c [dict keys $o_columndefs] { lappend configured_widths [my column_width_configured $c] } set header_colspans [my header_colspans] set width_max $colwidth set test_width $colwidth set showing_vseps [my Showing_vseps] dict for {h colspans} $header_colspans { set spanc [lindex $colspans $cidx] #set headers [dict get $cdef -headers] #set thiscol_widest_header 0 #if {[llength $headers] > 0} { # set thiscol_widest_header [tcl::mathfunc::max {*}[lmap v $headers {textblock::width $v}]] #} set thiscol_widest_header [dict get $o_columnstates $cidx maxwidthheaderseen] if {$spanc eq "1"} { if {$thiscol_widest_header > $colwidth} { set test_width [expr {max($thiscol_widest_header,$colwidth)}] if {$defmaxw ne ""} { set test_width [expr {min($colwidth,$defmaxw)}] } } set width_max [expr {max($test_width,$width_max)}] continue } if {$spanc eq "all" || $spanc > 1} { set spanned [list] ;#spanned is other columns spanned - not including this one set cnext [expr {$cidx +1}] set spanlength [lindex $colspans $cnext] while {$spanlength eq "0" && $cnext < [llength $colspans]} { lappend spanned $cnext incr cnext set spanlength [lindex $colspans $cnext] } set others_width 0 foreach col $spanned { incr others_width [lindex $configured_widths $col] if {$showing_vseps} { incr others_width 1 } } 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 ""} { set test_width [expr {min($colwidth+$needed,$defmaxw)}] } else { set test_width [expr {$colwidth + $needed}] } } } set width_max [expr {max($test_width,$width_max)}] } #alternative to all the above shennanigans.. let the body cell data determine the size and just wrap the headers #could also split the needed width amongst the spanned columns? configurable for whether cells expand? set expand_first_column 1 if {$expand_first_column} { set colwidth $width_max } #puts "---column_width $cidx = $colwidth" return $colwidth } method Showing_vseps {} { #review - show_seps and override mechanism for show_vseps show_hseps - document. set seps [dict get $o_opts_table -show_seps] set vseps [dict get $o_opts_table -show_vseps] if {$seps eq ""} { if {$vseps eq "" || $vseps} { return true } } elseif {$seps} { if {$vseps eq "" || $vseps} { return true } } else { if {$vseps ne "" && $vseps} { return true } } return false } method column_datawidth {index_expression args} { set defaults [dict create\ -headers 0\ -footers 0\ -colspan *\ -data 1\ -cached 1\ ] #-colspan is relevant to header/footer data only dict for {k v} $args { switch -- $k { -headers - -footers - -colspan - -data - -cached {} default { error "column_datawidth unrecognised flag '$k'. Known flags: [dict keys $defaults]" } } } set opts [dict merge $defaults $args] set opt_colspan [dict get $opts -colspan] set cidx [lindex [dict keys $o_columndefs] $index_expression] if {$cidx eq ""} { return } if {[dict get $opts -cached]} { set hwidest 0 set bwidest 0 set fwidest 0 if {[dict get $opts -headers]} { if {$opt_colspan eq "*"} { set hwidest [dict get $o_columnstates $cidx maxwidthheaderseen] } else { set colheaders [dict get $o_columndefs $cidx -headers] set all_colspans_by_header [my header_colspans] set hlist [list] dict for {hrow cspans} $all_colspans_by_header { set s [lindex $cspans $cidx] #todo - map 'all' entries to a number? #we should build a version of header_colspans that does this if {$s eq $opt_colspan} { lappend hlist [lindex $colheaders $hrow] } } #set widest1 [tcl::mathfunc::max {*}[lmap v $cmds {string length $v}]] if {[llength $hlist]} { set hwidest [tcl::mathfunc::max {*}[lmap v $hlist {textblock::width $v}]] } else { set hwidest 0 } } } if {[dict get $opts -data]} { set bwidest [dict get $o_columnstates $cidx maxwidthbodyseen] } if {[dict get $opts -footers]} { #TODO! #set bwidest [dict get $o_columnstates $cidx maxwidthfooterseen] } return [expr {max($hwidest,$bwidest,$fwidest)}] } #assert cidx is >=0 integer in valid range of keys for o_columndefs set values [list] if {[dict get $opts -headers]} { lappend values {*}[dict get $o_columndefs $cidx -headers] } if {[dict get $opts -data]} { if {[dict exists $o_columndata $cidx]} { lappend values {*}[dict get $o_columndata $cidx] } } if {[dict get $opts -footers]} { lappend values {*}[dict get $o_columndefs $cidx -footers] } if {[llength $values]} { set widest [tcl::mathfunc::max {*}[lmap v $values {textblock::width $v}]] } else { set widest 0 } return $widest } #print1 uses basic column joining - useful for testing/debug especially with colspans method print1 {args} { if {![llength $args]} { set cols [dict keys $o_columndata] } else { set cols [list] foreach colspec $args { set allcols [dict keys $o_columndata] if {[string first .. $colspec] >=0} { set parts [punk::ansi::ta::_perlish_split {\.\.} $colspec] if {[llength $parts] != 3} { error "[namespace::current]::table error invalid print specification '$colspec'" } lassign $parts from _dd to if {$from eq ""} {set from 0 } if {$to eq ""} {set to end} set indices [lrange $allcols $from $to] lappend cols {*}$indices } else { set c [lindex $allcols $colspec] if {$c ne ""} { lappend cols $c } } } } set blocks [list] set colposn 0 set numposns [llength $cols] foreach c $cols { set flags [list] if {$colposn == 0 && $colposn == $numposns-1} { set flags [list -position solo] } elseif {$colposn == 0} { set flags [list -position left] } elseif {$colposn == $numposns-1} { set flags [list -position right] } else { set flags [list -position inner] } lappend blocks [my get_column_by_index $c {*}$flags] incr colposn } if {[llength $blocks]} { return [textblock::join {*}$blocks] } else { 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 } } } } } } } set column_widths [dict values $colwidths] #todo - -maxwidth etc set table_minwidth [dict get $o_opts_table -minwidth] ;#min width including frame elements if {[string is integer -strict $table_minwidth]} { set contentwidth [tcl::mathop::+ {*}$column_widths] set twidth $contentwidth if {[my Showing_vseps]} { incr twidth [llength $column_widths] incr twidth -1 } if {[dict get $o_opts_table -show_edge]} { incr twidth 2 } # set shortfall [expr {$table_minwidth - $twidth}] if {$shortfall > 0} { set space_to_alloc $shortfall 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 } } } set column_widths [dict values $colwidths] } } return [list ordered_spans $ordered_spans colwidths $column_widths adjustments $colspace_added] } #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 { set cols [list] foreach colspec $args { set allcols [dict keys $o_columndata] if {[string first .. $colspec] >=0} { set parts [punk::ansi::ta::_perlish_split {\.\.} $colspec] if {[llength $parts] != 3} { error "[namespace::current]::table error invalid print specification '$colspec'" } lassign $parts from _dd to if {$from eq ""} {set from 0} if {$to eq ""} {set to end} set indices [lrange $allcols $from $to] lappend cols {*}$indices } else { set c [lindex $allcols $colspec] if {$c ne ""} { lappend cols $c } } } } set blocks [list] set colposn 0 set numposns [llength $cols] set padwidth 0 set table "" foreach c $cols { set flags [list] if {$colposn == 0 && $colposn == $numposns-1} { set flags [list -position solo] } elseif {$colposn == 0} { set flags [list -position left] } elseif {$colposn == $numposns-1} { set flags [list -position right] } else { set flags [list -position inner] } #lappend blocks [my get_column_by_index $c {*}$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] if {$table eq ""} { set table $nextcol 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[unset table] $nextcol] #JMN #set nextcol [textblock::join [textblock::block $padwidth $height "\uFFFF"] $nextcol] #set table [overtype::renderspace -overflow 1 -experimental test_mode -transparent \uFFFF $table $nextcol] } incr padwidth $bodywidth incr colposn } 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" } } method print_bodymatrix {} { set m [my as_matrix] $m format 2string } #*** !doctools #[list_end] } #*** !doctools # [list_end] [comment {- end enumeration provider_classes }] #[list_end] [comment {- end itemized list textblock::class groupings -}] } } } # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ # #Note: A textblock does not necessarily have lines the same length - either in number of characters or print-width # namespace eval textblock { namespace export block width namespace eval cd { #todo - save and restore existing namespace export in case macros::cd has default exports in future namespace eval ::term::ansi::code::macros::cd {namespace export *} namespace import ::term::ansi::code::macros::cd::* namespace eval ::term::ansi::code::macros::cd {namespace export -clear} } proc spantest {} { 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_etc} $t configure_column 2 -header_colspans {0 0 0 0 2} $t configure -show_header 1 -ansiborder_header [a+ cyan] return $t } #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 {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 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 } proc periodic {args} { #For an impressive interactive terminal app (javascript) # see: https://github.com/spirometaxas/periodic-table-cli set defaults [dict create\ -return "string"\ -compact 1\ ] dict for {k v} $args { switch -- $k { -return - -compact {} default { "textblock::periodic unknown option '$k'. Known options: [dict keys $defaults]" } } } set opts [dict merge $defaults $args] set opt_return [dict get $opts -return] #examples ptable.com set elements [list\ 1 H "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" He\ 2 Li Be "" "" "" "" "" "" "" "" "" "" B C N O F Ne\ 3 Na Mg "" "" "" "" "" "" "" "" "" "" Al Si P S Cl Ar\ 4 K Ca Sc Ti V Cr Mn Fe Co Ni Cu Zn Ga Ge As Se Br Kr\ 5 Rb Sr Y Zr Nb Mo Tc Ru Rh Pd Ag Cd In Sn Sb Te I Xe\ 6 Cs Ba "" Hf Ta W Re Os Ir Pt Au Hg Tl Pb Bi Po At Rn\ 7 Fr Ra "" Rf Db Sg Bh Hs Mt Ds Rg Cn Nh Fl Mc Lv Ts Og\ " " " " " " " " " " " " " " " " " " " " " " " " "" "" "" "" "" "" ""\ "" "" "" 6 La Ce Pr Nd Pm Sm Eu Gd Tb Dy Ho Er Tm Yb Lu\ "" "" "" 7 Ac Th Pa U Np Pu Am Cm Bk Cf Es Fm Md No Lr\ ] set type_colours [list] set ecat [dict create] set cat_alkaline_earth [list Be Mg Ca Sr Ba Ra] set ansi [a+ Web-gold web-black] foreach e $cat_alkaline_earth { dict set ecat $e [list ansi $ansi cat alkaline_earth] } set cat_reactive_nonmetal [list H C N O F P S Cl Se Br I] set ansi [a+ Web-lightgreen web-black] foreach e $cat_reactive_nonmetal { dict set ecat $e [list ansi $ansi cat reactive_nonmetal] } set cat [list Li Na K Rb Cs Fr] set ansi [a+ Web-Khaki web-black] foreach e $cat { dict set ecat $e [list ansi $ansi cat alkali_metals] } set cat [list Sc Ti V Cr Mn Fe Co Ni Cu Zn Y Zr Nb Mo Tc Ru Rh Pd Ag Cd Hf Ta W Re Os Ir Pt Au Hg Rf Db Sg Bh Hs] set ansi [a+ Web-lightsalmon web-black] foreach e $cat { dict set ecat $e [list ansi $ansi cat transition_metals] } set cat [list Al Ga In Sn Tl Pb Bi Po] set ansi [a+ Web-lightskyblue web-black] foreach e $cat { dict set ecat $e [list ansi $ansi cat post_transition_metals] } set cat [list B Si Ge As Sb Te At] set ansi [a+ Web-turquoise web-black] foreach e $cat { dict set ecat $e [list ansi $ansi cat metalloids] } set cat [list He Ne Ar Kr Xe Rn] set ansi [a+ Web-orchid web-black] foreach e $cat { dict set ecat $e [list ansi $ansi cat noble_gases] } set cat [list Ac Th Pa U Np Pu Am Cm Bk Cf Es Fm Md No Lr] set ansi [a+ Web-plum web-black] foreach e $cat { dict set ecat $e [list ansi $ansi cat actinoids] } set cat [list La Ce Pr Nd Pm Sm Eu Gd Tb Dy Ho Er Tm Yb Lu] set ansi [a+ Web-tan web-black] foreach e $cat { dict set ecat $e [list ansi $ansi cat lanthanoids] } set cat [list Mt Ds Rg Cn Nh Fl Mc Lv Ts Og] set ansi [a+ Web-whitesmoke web-black] foreach e $cat { dict set ecat $e [list ansi $ansi cat other] } set elements1 [list] foreach e $elements { if {[dict exists $ecat $e]} { set ansi [dict get $ecat $e ansi] #lappend elements1 [textblock::pad $ansi$e -width 2 -which right] #no need to pad here - table column_configure -textalign default of left will do the work of padding right anyway lappend elements1 $ansi$e } else { lappend elements1 $e } } set t [list_as_table 19 $elements1 -return obj] #todo - keep simple table with symbols as base - map symbols to descriptions etc for more verbose table options 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 incr c } for {set c 0} {$c < [$t column_count]} {incr c} { $t configure_column $c -minwidth 3 } if {[dict get $opts -compact]} { $t configure -show_hseps 0 $t configure -show_header 0 $t configure -show_edge 0 } else { $t configure -show_header 1 } if {$opt_return eq "string"} { $t configure -frametype_header light $t configure -ansiborder_header [a+ web-white] $t configure -ansibase_header [a+ Web-black] $t configure -ansibase_body [a+ Web-black] $t configure -ansiborder_body [a+ web-black] $t configure -frametype block set output [textblock::frame -ansiborder [a+ Web-black web-cornflowerblue] -type heavy -title "[a+ Web-black] Periodic Table " [$t print]] return $output } return $t } proc list_as_table {table_or_colcount datalist args} { set defaults [dict create\ -return string\ -frametype \uFFEF\ -show_edge \uFFEF\ -show_seps \uFFEF\ ] foreach {k v} $args { switch -- $k { -return - -show_edge - -show_seps - -frametype {} default { error "unrecognised option '$k'. Known options [dict keys $defaults]" } } } set opts [dict merge $defaults $args] set count [llength $datalist] set is_new_table 0 if {[string is integer -strict $table_or_colcount]} { set cols $table_or_colcount set is_new_table 1 #defaults for new table only if {[dict get $opts -frametype] eq "\uFFEF"} { dict set opts -frametype "light" } if {[dict get $opts -show_edge] eq "\uFFEF"} { dict set opts -show_edge 1 } if {[dict get $opts -show_seps] eq "\uFFEF"} { dict set opts -show_seps 1 } set t [textblock::class::table new -show_header 0 -show_edge [dict get $opts -show_edge] -frametype [dict get $opts -frametype] -show_seps [dict get $opts -show_seps]] for {set c 0} {$c < $cols} {incr c} { $t add_column -headers [list $c] } } else { if {[namespace tail [info object class $table_or_colcount]] ne "table"} { error "textblock::list_as_table error - table_or_colcount must be an integer or an existing table object" } set t $table_or_colcount if {[dict get $opts -frametype] ne "\uFFEF"} { $t configure -frametype [dict get $opts -frametype] } if {[dict get $opts -show_edge] ne "\uFFEF"} { $t configure -show_edge [dict get $opts -show_edge] } $t row_clear set cols [$t column_count] } set full_rows [expr {$count / $cols}] set last_items [expr {$count % $cols}] set rowdata [list] set row [list] set i 0 if {$full_rows > 0} { for {set r 0} {$r < $full_rows} {incr r} { set j [expr {$i + ($cols -1)}] set row [lrange $datalist $i $j] incr i $cols lappend rowdata $row } } if {$last_items > 0} { set idx [expr {$last_items -1}] lappend rowdata [lrange $datalist end-$idx end] } foreach row $rowdata { set shortfall [expr {$cols - [llength $row]}] if {$shortfall > 0} { set row [concat $row [lrepeat $shortfall ""]] } $t add_row $row } #puts stdout $rowdata if {[dict get $opts -return] eq "string"} { set result [$t print] if {$is_new_table} { $t destroy } return $result } else { return $t } } #return a homogenous block of characters - ie lines all same length, all same character #printing-width in terminal columns is not necessarily same as blockwidth even if a single char is passed (could be full-width unicode character) #This can have ansi SGR codes - but to repeat blocks containing ansi-movements, the block should first be rendered to a static output with something like overtype::left proc block {blockwidth blockheight {char " "}} { if {$blockwidth < 0} { error "textblock::block blockwidth must be an integer greater than or equal to zero" } if {$blockheight <= 0} { error "textblock::block blockheight must be a positive integer" } if {$char eq ""} {return ""} #using string length is ok if {[string length $char] == 1} { set row [string repeat $char $blockwidth] set mtrx [lrepeat $blockheight $row] return [::join $mtrx \n] } else { set charblock [string map [list \r\n \n] $char] if {[string last \n $charblock] >= 0} { if {$blockwidth > 1} { #set row [.= val $charblock {*}[lrepeat [expr {$blockwidth -1}] |> piper_blockjoin $charblock]] ;#building a repeated "|> command arg" list to evaluate as a pipeline. (from before textblock::join could take arbitrary num of blocks ) set row [textblock::join {*}[lrepeat $blockwidth $charblock]] } else { set row $charblock } } else { set row [string repeat $char $blockwidth] } set mtrx [lrepeat $blockheight $row] return [::join $mtrx \n] } } proc testblock {size {colour ""}} { if {$size <1 || $size > 15} { error "textblock::testblock only sizes between 1 and 15 inclusive supported" } set rainbow_list [list] lappend rainbow_list {30 47} ;#black White lappend rainbow_list {31 46} ;#red Cyan lappend rainbow_list {32 45} ;#green Purple lappend rainbow_list {33 44} ;#yellow Blue lappend rainbow_list {34 43} ;#blue Yellow lappend rainbow_list {35 42} ;#purple Green lappend rainbow_list {36 41} ;#cyan Red lappend rainbow_list {37 40} ;#white Black lappend rainbow_list {black Yellow} lappend rainbow_list red lappend rainbow_list green lappend rainbow_list yellow lappend rainbow_list blue 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] 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] set clist [list] for {set i 0} {$i <$size} {incr i} { set colour2 [string map [list rainbow [lindex $rainbow_list $i]] $colour] set ansi [a+ {*}$colour2] set ansicode [punk::ansi::codetype::sgr_merge_list "" $ansi] lappend clist ${ansicode}$c$RST } 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] foreach ch $charsubset { lappend rows [string repeat $ch $size] } set block [::join $rows \n] if {$colour ne ""} { set block [a+ {*}$colour]$block$RST } return $block } } interp alias {} testblock {} textblock::testblock #todo - consider 'elastic tabstops' for textblocks where tab acts as a column separator and adjacent lines with the same number of tabs form a sort of table proc width {textblock} { #backspaces, vertical tabs ? if {$textblock eq ""} { return 0 } #textutil::tabify is a reasonable hack when there are no ansi SGR codes - but probably not always what we want even then - review if {[string last \t $textblock] >= 0} { if {[info exists punk::console::tabwidth]} { set tw $::punk::console::tabwidth } else { set tw 8 } set textblock [textutil::tabify::untabify2 $textblock $tw] } if {[punk::ansi::ta::detect $textblock]} { set textblock [punk::ansi::stripansi $textblock] } if {[string last \n $textblock] >= 0} { return [tcl::mathfunc::max {*}[lmap v [split $textblock \n] {::punk::char::ansifreestring_width $v}]] } return [punk::char::ansifreestring_width $textblock] } #when we know the block is uniform in width - just examine topline proc widthtopline {textblock} { set firstnl [string first \n $textblock] if {$firstnl >= 0} { set tl [string range $textblock 0 $firstnl] } else { set tl $textblock } if {[punk::ansi::ta::detect $tl]} { set tl [punk::ansi::stripansi $tl] } return [punk::char::ansifreestring_width $tl] } #uses tcl's string length on each line. Length will include chars in ansi codes etc - this is not a 'width' determining function. proc string_length_line_max textblock { tcl::mathfunc::max {*}[lmap v [split $textblock \n] {string length $v}] } proc string_length_line_min textblock { tcl::mathfunc::min {*}[lmap v [split $textblock \n] {string length $v}] } proc height {textblock} { #This is the height as it will/would-be rendered - not the number of input lines purely in terms of le #empty string still has height 1 (at least for left-right/right-left languages) #vertical tab on a proper terminal should move directly down. #Whether or not the terminal in use actually does this - we need to calculate as if it does. (there might not even be a terminal) set num_le [expr {[string length $textblock]-[string length [string map [list \n {} \v {}] $textblock]]}] ;#faster than splitting into single-char list return [expr {$num_le + 1}] ;# one line if no le - 2 if there is one trailing le even if no data follows le } #MAINTENANCE - same as overtype::blocksize? proc size {textblock} { if {$textblock eq ""} { return [dict create width 0 height 1] ;#no such thing as zero-height block - for consistency with non-empty strings having no line-endings } #strangely - string last (windows tcl8.7 anway) is faster than string first for large strings when the needle not in the haystack if {[string last \t $textblock] >= 0} { if {[info exists punk::console::tabwidth]} { set tw $::punk::console::tabwidth } else { set tw 8 } set textblock [textutil::tabify::untabify2 $textblock $tw] } #stripansi on entire block in one go rather than line by line - result should be the same - review - make tests if {[punk::ansi::ta::detect $textblock]} { set textblock [punk::ansi::stripansi $textblock] } if {[string last \n $textblock] >= 0} { #set width [tcl::mathfunc::max {*}[lmap v [punk::lib::lines_as_list -- $textblock] {::punk::char::ansifreestring_width $v}]] set width [tcl::mathfunc::max {*}[lmap v [split $textblock \n] {::punk::char::ansifreestring_width $v}]] } else { set width [punk::char::ansifreestring_width $textblock] } set num_le [expr {[string length $textblock]-[string length [string map [list \n {} \v {}] $textblock]]}] ;#faster than splitting into single-char list #our concept of block-height is likely to be different to other line-counting mechanisms set height [expr {$num_le + 1}] ;# one line if no le - 2 if there is one trailing le even if no data follows le return [dict create width $width height $height] ;#maintain order in 'image processing' standard width then height - caller may use lassign [dict values [blocksize ]] width height } #must be able to handle block as string with or without newlines #if no newlines - attempt to treat as a list #must handle whitespace-only string,list elements, and/or lines. #reviewing 2024 - this seems like too much magic! proc width1 {block} { if {$block eq ""} { return 0 } if {[info exists punk::console::tabwidth]} { set tw $::punk::console::tabwidth } else { set tw 8 } set block [textutil::tabify::untabify2 $block $tw] if {[string last \n $block] >= 0} { return [tcl::mathfunc::max {*}[lmap v [punk::lib::lines_as_list -- $block] {::punk::char::string_width [stripansi $v]}]] } if {[catch {llength $block}]} { return [::punk::char::string_width [stripansi $block]] } if {[llength $block] == 0} { #could be just a whitespace string return [string length $block] } return [tcl::mathfunc::max {*}[lmap v $block {::punk::char::string_width [stripansi $v]}]] } #we shouldn't make textblock depend on the punk pipeline system #pipealias ::textblock::padleft .= {list $input [string repeat " " $indent]} |/0,padding/1> punk:lib::lines_as_list -- |> .= {lmap v $data {overtype::right $padding $v}} |> punk::lib::list_as_lines -- punk::lib::lines_as_list -- |> .= {lmap v $data {overtype::left $padding $v}} |> punk::lib::list_as_lines -- |? ?-which right|left|centre? ?-width auto|? ?-within_ansi 1|0?" foreach {k v} $args { switch -- $k { -padchar - -which - -width - -overflow - -within_ansi {} default { error "textblock::pad unrecognised option '$k'. Usage: $usage" } } } set opts [dict merge $defaults $args] # -- --- --- --- --- --- --- --- --- --- set padchar [dict get $opts -padchar] #if padchar width (screen width) > 1 - length calculations will not be correct #we will allow tokens longer than 1 - as the caller may want to post-process on the token whilst preserving previous leading/trailing spaces, e.g with a string map #The caller may also use ansi within the padchar - although it's unlikely to be efficient. # -- --- --- --- --- --- --- --- --- --- set known_whiches [list l left r right c center centre] set opt_which [string tolower [dict get $opts -which]] switch -- $opt_which { center - centre - c { set which c } left - l { set which l } right - r { set which r } default { error "textblock::pad unrecognised value for -which option. Known values $known_whiches" } } # -- --- --- --- --- --- --- --- --- --- set opt_width [dict get $opts -width] switch -- $opt_width { "" - auto { set width auto } default { if {![string is integer -strict $opt_width] || $opt_width < 0} { error "textblock::pad -width must be an integer >=0" } set width $opt_width } } # -- --- --- --- --- --- --- --- --- --- set opt_withinansi [dict get $opts -within_ansi] switch -- $opt_withinansi { 0 - 1 {} default { set opt_withinansi 2 } } # -- --- --- --- --- --- --- --- --- --- set datawidth [textblock::width $block] if {$width eq "auto"} { set width $datawidth } set lines [list] if {$block eq ""} { #we need to treat as a line return [string repeat $padchar $width] } #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 - 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 if {($padchar eq " " || $padchar eq "0") && ![punk::ansi::ta::detect $block]} { #This will still be wrong for example with diacritics on terminals that don't collapse the space following a diacritic, and don't correctly report cursor position #(i.e as at 2024 - lots of them) wezterm on windows at least does the right thing. set block [string map [list \r\n \n] $block] if {$which eq "l"} { set fmt "%+${padchar}*s" } else { set fmt "%-${padchar}*s" } foreach ln [split $block \n] { #set lnwidth [punk::char::ansifreestring_width $ln] 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] } return [::join $lines \n] } } set lnum 0 set parts [punk::ansi::ta::split_codes $block] set line_chunks [list] set line_len 0 foreach {pt ansi} $parts { if {$pt ne ""} { set has_nl [expr {[string last \n $pt]>=0}] if {$has_nl} { set pt [string map [list \r\n \n] $pt] set partlines [split $pt \n] } else { set partlines [list $pt] } set last [expr {[llength $partlines]-1}] set p 0 foreach pl $partlines { lappend line_chunks $pl #incr line_len [punk::char::ansifreestring_width $pl] incr line_len [punk::char::grapheme_width_cached $pl] ;#memleak if {$p != $last} { #do padding set missing [expr {$width - $line_len}] if {$missing > 0} { set pad [string repeat $padchar $missing] switch -- $which-$opt_withinansi { r-0 { lappend line_chunks $pad } r-1 { if {[lindex $line_chunks end] eq ""} { set line_chunks [linsert $line_chunks end-2 $pad] } else { lappend line_chunks $pad } } r-2 { lappend line_chunks $pad } l-0 { set line_chunks [linsert $line_chunks 0 $pad] } l-1 { if {[lindex $line_chunks 0] eq ""} { set line_chunks [linsert $line_chunks 2 $pad] } else { set line_chunks [linsert $line_chunks 0 $pad] } } l-2 { 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 } } else { #we need to store empties in order to insert text in the correct position relative to leading/trailing ansi codes lappend line_chunks "" } #don't let trailing empty ansi affect the line_chunks length if {$ansi ne ""} { lappend line_chunks $ansi ;#don't update line_len - review - ansi codes with visible content? } } #pad last line set missing [expr {$width - $line_len}] if {$missing > 0} { set pad [string repeat $padchar $missing] switch -- $which-$opt_withinansi { r-0 { lappend line_chunks $pad } r-1 { if {[lindex $line_chunks end] eq ""} { set line_chunks [linsert $line_chunks end-2 $pad] } else { lappend line_chunks $pad } } r-2 { if {[lindex $line_chunks end] eq ""} { set line_chunks [linsert $line_chunks end-2 $pad] } else { 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] set line_chunks [_insert_before_text_or_last_ansi $pad $line_chunks] } l-2 { set line_chunks [linsert $line_chunks 0 $pad] } } } lappend lines [::join $line_chunks ""] return [::join $lines \n] } #left insertion into a list resulting from punk::ansi::ta::split_codes or split_codes_single #resulting list is no longer a valid ansisplit list proc _insert_before_text_or_last_ansi {str ansisplits} { if {[llength $ansisplits] == 1} { #ansisplits was a split on plaintext only return [list $str [lindex $ansisplits 0]] } elseif {[llength $ansisplits] == 0} { return [list $str] } if {[llength $ansisplits] %2 != 1} { error "_insert_before_text_or_last_ansi ansisplits list is not a valid resultlist from an ansi split - must be odd number of elements pt,ansi,pt,ansi...pt" } set out [list] set i 0 set i_last_code [expr {[llength $ansisplits]-3}] ;#would normally be -2 - but our i is jumping to each pt - not every element foreach {pt code} $ansisplits { if {$pt ne ""} { return [lappend out $str {*}[lrange $ansisplits $i end]] } if {$i == $i_last_code} { return [lappend out $str {*}[lrange $ansisplits $i end]] } #code being empty can only occur when we have reached last pt #we have returned by then. lappend out $code incr i 2 } error "_insert_before_text_or_last_ansi failed on input str:[ansistring VIEW $str] ansisplits:[ansistring VIEW $ansisplits]" } proc pad_test {block} { set width [textblock::width $block] set padtowidth [expr {$width + 10}] set left0 [textblock::pad $block -width $padtowidth -padchar . -which left -within_ansi 0] set left1 [textblock::pad $block -width $padtowidth -padchar . -which left -within_ansi 1] set left2 [textblock::pad $block -width $padtowidth -padchar . -which left -within_ansi 2] set right0 [textblock::pad $block -width $padtowidth -padchar . -which right -within_ansi 0] set right1 [textblock::pad $block -width $padtowidth -padchar . -which right -within_ansi 1] set right2 [textblock::pad $block -width $padtowidth -padchar . -which right -within_ansi 2] set testlist [list "within_ansi 0" $left0 $right0 "within_ansi 1" $left1 $right1 "within_ansi 2" $left2 $right2] set t [textblock::list_as_table 3 $testlist -return object] $t configure_column 0 -headers [list "ansi"] $t configure_column 1 -headers [list "Left"] $t configure_column 2 -headers [list "Right"] $t configure -show_header 1 puts stdout [$t print] return $t } 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] 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] dict set blockinfo $b right0 [textblock::pad $block -width $padtowidth -padchar . -which right -within_ansi 0] dict set blockinfo $b right1 [textblock::pad $block -width $padtowidth -padchar . -which right -within_ansi 1] dict set blockinfo $b right2 [textblock::pad $block -width $padtowidth -padchar . -which right -within_ansi 2] incr b } 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 r2 [dict get $blockinfo $b left2] [dict get $blockinfo $b right2] lappend r3 "" "" } 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 [dict get $opts -description] "within_ansi"] -ansibase $column_ansi set col 1 dict for {b bdict} $blockinfo { 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"] -minwidth [expr {$padtowidth + 2}] -ansibase $column_ansi incr col } $t configure -show_header 1 puts stdout [$t print] return $t } proc pad_example {} { 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] } #playing with syntax # pipealias ::textblock::join_width .= {list $lhs [string repeat " " $w1] $rhs [string repeat " " $w2]} {| # /2,col1/1,col2/3 # >} punk::lib::lines_as_list -- {| # data2 # >} .=lhs> punk::lib::lines_as_list -- {| # >} .= {lmap v $data w $data2 {val "[overtype::left $col1 $v][overtype::left $col2 $w]"}} {| # >} punk::lib::list_as_lines -- } .=> punk::lib::lines_as_list -- {| # data2 # >} .=lhs> punk::lib::lines_as_list -- {| # >} .= {lmap v $data w $data2 {val "[overtype::left $col1 $v][overtype::left $col2 $w]"}} {| # >} punk::lib::list_as_lines -- } .=> punk::lib::lines_as_list -- {| # data2 # >} .=lhs> punk::lib::lines_as_list -- {| # >} .= {lmap v $data w $data2 {val "[overtype::right $col1 $v][overtype::right $col2 $w]"}} {| # >} punk::lib::list_as_lines punk . rhs] set pright [>punk . lhs] set prightair [>punk . lhs_air] set red [a+ red]; set redb [a+ red bold] set green [a+ green]; set greenb [a+ green bold] set cyan [a+ cyan];set cyanb [a+ cyan bold] set blue [a+ blue];set blueb [a+ blue bold] set RST [a] set gr0 [punk::ansi::g0 abcdefghijklm\nnopqrstuvwxyz] set punks [textblock::join $pleft $pright] set pleft_greenb $greenb$pleft$RST set pright_redb $redb$pright$RST set prightair_cyanb $cyanb$prightair$RST set cpunks [textblock::join $pleft_greenb $pright_redb] set out "" append out $punks \n append out $cpunks \n append out [textblock::join $punks $cpunks] \n set 2frames_a [textblock::join [textblock::frame $cpunks] [textblock::frame $punks]] append out $2frames_a \n set 2frames_b [textblock::join [textblock::frame -ansiborder $cyanb -title "plainpunks" $punks] [textblock::frame -ansiborder $greenb -title "fancypunks" $cpunks]] append out [textblock::frame -title "punks" $2frames_b\n$RST$2frames_a] \n set fancy [overtype::right [overtype::left [textblock::frame -ansiborder [a+ green bold] -type heavy -title ${redb}PATTERN$RST -subtitle ${redb}PUNK$RST $prightair_cyanb] "$blueb\n\n\P\nU\nN\nK$RST"] "$blueb\n\nL\nI\nF\nE"] set spantable [[spantest] print] append out [textblock::join $fancy " " $spantable] \n #append out [textblock::frame -title gr $gr0] append out [textblock::periodic] return $out } proc example3 {{text "test\netc\nmore text"}} { package require patternpunk .= textblock::join [punk::lib::list_as_lines -- [list 1 2 3 4 5 6 7]] [>punk . lhs] |> .=>1 textblock::join " " |> .=>1 textblock::join $text |> .=>1 textblock::join [>punk . rhs] |> .=>1 textblock::join [punk::lib::list_as_lines -- [lrepeat 7 " | "]] } proc example2 {{text "test\netc\nmore text"}} { package require patternpunk .= textblock::join\ [punk::lib::list_as_lines -- [list 1 2 3 4 5 6 7 8]]\ [>punk . lhs]\ " "\ $text\ [>punk . rhs]\ [punk::lib::list_as_lines -- [lrepeat 8 " | "]] } proc table {args} { upvar ::textblock::class::opts_table_defaults toptdefaults set defaults [dict create\ -rows [list]\ -headers [list]\ -return string\ ] set defaults [dict merge $defaults $toptdefaults] ;# -title -frametype -show_header etc set opts [dict merge $defaults $args] # -- --- --- --- set opt_return [dict get $opts -return] set opt_rows [dict get $opts -rows] set opt_headers [dict get $opts -headers] # -- --- --- --- set topts [dict create] set toptkeys [dict keys $toptdefaults] dict for {k v} $opts { if {$k in $toptkeys} { dict set topts $k $v } } set t [textblock::class::table new {*}$topts] foreach h $opt_headers { $t add_column -headers [list $h] } if {[$t column_count] == 0} { if {[llength $opt_rows]} { set r0 [lindex $opt_rows 0] foreach c $r0 { $t add_column } } } foreach r $opt_rows { $t add_row $r } if {$opt_return eq "string"} { set result [$t print] $t destroy return $result } else { return $t } } variable frametypes set frametypes [list light heavy arc double block block1 ascii altg] #class::table needs to be able to determine valid frametypes proc frametypes {} { variable frametypes return $frametypes } proc frametype {f} { variable frametypes set default_custom [dict create hl " " vl " " tlc " " trc " " blc " " brc " "] set custom_keys [list hl hlt hlb vl vll vlr tlc trc blc brc] if {$f ni $frametypes} { set is_custom_dict_ok 1 if {[llength $f] %2 == 0} { #custom dict may leave out keys - but cannot have unknown keys dict for {k v} $f { switch -- $k { hl - hlt - hlb - vl - vll - vlr - tlc - trc - blc - brc {} default { #k not in custom_keys set is_custom_dict_ok 0 break } } } } else { set is_custom_dict_ok 0 } if {!$is_custom_dict_ok} { error "frame option -type must be one of known types: $frametypes or a dictionary with any of keys hl,hlt,hlb,vl,vll,vlr,tlc,trc,blc,brc" } set custom_frame [dict merge $default_custom $f] return [dict create category custom type $custom_frame] } else { return [dict create category predefined type $f] } } variable framedef_cache [dict create] proc framedef {f args} { #unicode box drawing only provides enough characters for seamless joining of unicode boxes light and heavy. #e.g with characters such as \u2539 Box Drawings Right Light and Left Up Heavy. #the double glyphs in box drawing can do a limited set of joins to light lines - but not enough for seamless table layouts. #the arc set can't even join to itself e.g with curved equivalents of T-like shapes variable framedef_cache set cache_key [concat $f $args] if {[dict exists $framedef_cache $cache_key]} { return [dict get $framedef_cache $cache_key] } set defaults [dict create\ -joins ""\ -boxonly 0\ ] dict for {k v} $args { switch -- $k { -joins - -boxonly {} default { error "framedef unknown option '$k'. Known options [dict keys $args]" } } } set opts [dict merge $defaults $args] set joins [dict get $opts -joins] set boxonly [dict get $opts -boxonly] #sorted order down left right up #1 x choose 4 #4 x choose 3 #6 x choose 2 #4 x choose 1 #15 combos set join_directions [list] #targets - light,heavy (double?) - seem to be some required glyphs missing from unicode #e.g down-light, up-heavy set join_targets [dict create left "" down "" right "" up ""] foreach jt $joins { lassign [split $jt -] direction target if {$target ne ""} { dict set join_targets $direction $target } lappend join_directions $direction } set join_directions [lsort -unique $join_directions] set do_joins [::join $join_directions _] switch -- $f { "altg" { #old style ansi escape sequences with alternate graphics page G0 set hl [cd::hl] set hlt $hl set hlb $hl set vl [cd::vl] set vll $vl set vlr $vl set tlc [cd::tlc] set trc [cd::trc] set blc [cd::blc] set brc [cd::brc] #horizontal and vertical bar joins set hltj $hlt set hlbj $hlb set vllj $vll set vlrj $vlr #No join targets available to join altg to other box styles switch -- $do_joins { down { #1 set blc [punk::ansi::g0 t] ;#(ltj) set brc [punk::ansi::g0 u] ;#(rtj) set hlbj [punk::ansi::g0 w] ;#(ttj) } left { #2 set tlc [punk::ansi::g0 w] ;#(ttj) set blc [punk::ansi::g0 v] ;#(btj) set vllj [punk::ansi::g0 u] ;#(rtj) } right { #3 set trc [punk::ansi::g0 w] ;#(ttj) set brc [punk::ansi::g0 v] ;#(btj) set vlrj [punk::ansi::g0 t] ;#(ltj) } up { #4 set tlc [punk::ansi::g0 t] ;#(ltj) set trc [punk::ansi::g0 u] ;#(rtj) set hltj [punk::ansi::g0 v];#(btj) } down_left { #5 set blc [punk::ansi::g0 n] ;#(fwj) set tlc [punk::ansi::g0 w] ;#(ttj) set brc [punk::ansi::g0 u] ;#(rtj) set hlbj [punk::ansi::g0 w] ;#(ttj) set vllj [punk::ansi::g0 u] ;#(rtj) } down_right { #6 set blc [punk::ansi::g0 t] ;#(ltj) set trc [punk::ansi::g0 w] ;#(ttj) set brc [punk::ansi::g0 n] ;#(fwj) set hlbj [punk::ansi::g0 w] ;#(ttj) set vlrj [punk::ansi::g0 t] ;#(ltj) } down_up { #7 set blc [punk::ansi::g0 t] ;#(ltj) set brc [punk::ansi::g0 u] ;#(rtj) set tlc [punk::ansi::g0 t] ;#(ltj) set trc [punk::ansi::g0 u] ;#(rtj) set hlbj [punk::ansi::g0 w] ;#(ttj) set hltj [punk::ansi::g0 v];#(btj) } left_right { #8 #from 2 set tlc [punk::ansi::g0 w] ;#(ttj) set blc [punk::ansi::g0 v] ;#(btj) #from3 set trc [punk::ansi::g0 w] ;#(ttj) set brc [punk::ansi::g0 v] ;#(btj) set vllj [punk::ansi::g0 u] ;#(rtj) set vlrj [punk::ansi::g0 t] ;#(ltj) } left_up { #9 set tlc [punk::ansi::g0 n] ;#(fwj) set trc [punk::ansi::g0 u] ;#(rtj) set blc [punk::ansi::g0 v] ;#(btj) set vllj [punk::ansi::g0 u] ;#(rtj) } right_up { #10 set tlc [punk::ansi::g0 t] ;#(ltj) set trc [punk::ansi::g0 n] ;#(fwj) set brc [punk::ansi::g0 v] ;#(btj) set hltj [punk::ansi::g0 v];#(btj) set vlrj [punk::ansi::g0 t] ;#(ltj) } down_left_right { #11 set blc [punk::ansi::g0 n] ;#(fwj) set brc [punk::ansi::g0 n] ;#(fwj) set trc [punk::ansi::g0 w] ;#(ttj) set tlc [punk::ansi::g0 w] ;#(ttj) set vllj [punk::ansi::g0 u] ;#(rtj) set vlrj [punk::ansi::g0 t] ;#(ltj) set hlbj [punk::ansi::g0 w] ;#(ttj) } down_left_up { #12 set tlc [punk::ansi::g0 n] ;#(fwj) set blc [punk::ansi::g0 n] ;#(fwj) set trc [punk::ansi::g0 u] ;#(rtj) set brc [punk::ansi::g0 u] ;#(rtj) set vllj [punk::ansi::g0 u] ;#(rtj) set hltj [punk::ansi::g0 v];#(btj) set hlbj [punk::ansi::g0 w] ;#(ttj) } down_right_up { #13 set tlc [punk::ansi::g0 t] ;#(ltj) set blc [punk::ansi::g0 t] ;#(ltj) set trc [punk::ansi::g0 n] ;#(fwj) set brc [punk::ansi::g0 n] ;#(fwj) set hltj [punk::ansi::g0 v];#(btj) set vlrj [punk::ansi::g0 t] ;#(ltj) set hlbj [punk::ansi::g0 w] ;#(ttj) } left_right_up { #14 set tlc [punk::ansi::g0 n] ;#(fwj) set trc [punk::ansi::g0 n] ;#(fwj) set blc [punk::ansi::g0 v] ;#(btj) set brc [punk::ansi::g0 v] ;#(btj) set vllj [punk::ansi::g0 u] ;#(rtj) set hltj [punk::ansi::g0 v];#(btj) set vlrj [punk::ansi::g0 t] ;#(ltj) } down_left_right_up { #15 set tlc [punk::ansi::g0 n] ;#(fwj) set blc [punk::ansi::g0 n] ;#(fwj) set trc [punk::ansi::g0 n] ;#(fwj) set brc [punk::ansi::g0 n] ;#(fwj) set vllj [punk::ansi::g0 u] ;#(rtj) set hltj [punk::ansi::g0 v];#(btj) set vlrj [punk::ansi::g0 t] ;#(ltj) set hlbj [punk::ansi::g0 w] ;#(ttj) } } } "ascii" { set hl - set hlt - set hlb - set vl | set vll | set vlr | set tlc + set trc + set blc + set brc + #horizontal and vertical bar joins #set hltj $hlt #set hlbj $hlb #set vllj $vll #set vlrj $vlr #ascii + is small - can reasonably be considered a join to anything? set hltj + set hlbj + set vllj + set vlrj + #our corners are all + already - so we won't do anything for directions or targets } "light" { #unicode box drawing set set hl [punk::char::charshort boxd_lhz] ;# light horizontal set hlt $hl set hlb $hl set vl [punk::char::charshort boxd_lv] ;#light vertical set vll $vl set vlr $vl set tlc [punk::char::charshort boxd_ldr] set trc [punk::char::charshort boxd_ldl] set blc [punk::char::charshort boxd_lur] set brc [punk::char::charshort boxd_lul] #horizontal and vertical bar joins set hltj $hlt set hlbj $hlb set vllj $vll set vlrj $vlr #15 combos #sort order: down left right up #ltj,rtj,ttj,btj e.g left T junction etc. #Look at from the perspective of a frame/table outline with a clean border and arms pointing inwards #set targetdown,targetleft,targetright,targetup vars #default empty targets to current box type 'light' foreach dir {down left right up} { set target [dict get $join_targets $dir] switch -- $target { "" - light { set target$dir light } ascii - altg - arc { set target$dir light } heavy { set target$dir $target } default { set target$dir other } } } switch -- $do_joins { down { #1 switch -- $targetdown { heavy { set blc [punk::char::charshort boxd_dhrul] ;#\u251f down hieavy and right up light (ltj) set brc \u2527 ;#boxd_dhlul down heavy and left up light (rtj) set hlbj \u2530 ;# down heavy (ttj) } light { set blc \u251c ;#[punk::char::charshort boxd_lvr] light vertical and right (ltj) set brc [punk::char::charshort boxd_lvl] ;#light vertical and left (rtj) set hlbj \u252c ;# (ttj) } } } left { #2 switch -- $targetleft { heavy { set tlc \u252d ;# Left Heavy and Right Down Light (ttj) set blc \u2535 ;# Left Heavy and Right Up Light (btj) set vllj \u2525 ;# left heavy (rtj) } light { set tlc [punk::char::charshort boxd_ldhz] ;#T shape (ttj) - joinleft set blc [punk::char::charshort boxd_luhz] ;#upside down T shape (btj) set vllj \u2524 ;# (rtj) } } } right { #3 switch -- $targetright { heavy { set trc \u252e ;#Right Heavy and Left Down Light (ttj) set brc \u2536 ;#Right Heavy and Left up Light (btj) set vlrj \u251d;#right heavy (ltj) } light { set trc [punk::char::charshort boxd_ldhz] ;#T shape (ttj) set brc [punk::char::charshort boxd_luhz] ;#upside down T shape (btj) set vlrj \u251c;# (ltj) } } } up { #4 switch -- $targetup { heavy { set tlc \u251e ;#up heavy (ltj) set trc \u2526 ;#up heavy (rtj) } light { set tlc [punk::char::charshort boxd_lvr] ;#light vertical and right (ltj) set trc [punk::char::charshort boxd_lvl] ;#light vertical and left (rtj) } } } down_left { #5 switch -- $targetdown-$targetleft { other-light { set blc \u2534 ;#(btj) set tlc \u252c ;#(ttj) #brc - default corner set vllj \u2524 ;# (rtj) } other-other { #default corners } other-heavy { set blc \u2535 ;# heavy left (btj) set tlc \u252d ;#heavy left (ttj) #brc default corner set vllj \u2525 ;# heavy left (rtj) } heavy-light { set blc \u2541 ;# heavy down (fwj) set tlc \u252c ;# light (ttj) set brc \u2527 ;# heavy down (rtj) set vllj \u2524 ;# (rtj) set hlbj \u2530 ;# heavy down (ttj) } heavy-other { set blc \u251f ;#heavy down (ltj) #tlc - default corner set brc \u2527 ;#heavy down (rtj) set hlbj \u2530 ;# heavy down (ttj) } heavy-heavy { set blc \u2545 ;#heavy down and left (fwj) set tlc \u252d ;#heavy left (ttj) set brc \u2527 ;#heavy down (rtj) set vllj \u2525 ;# heavy left (rtj) set hlbj \u2530 ;# heavy down (ttj) } light-light { set blc \u253c ;# [punk::char::charshort boxd_lvhz] ;#light vertical and horizontal (fwj) set tlc \u252c ;# boxd_ldhz (ttj) set brc \u2524 ;# boxd_lvl light vertical and left(rtj) set vllj \u2524 ;# (rtj) set hlbj \u252c ;# (ttj) } light-other { set blc \u251c ;# (ltj) #tlc - default corner set brc \u2524 ;# boxd_lvl (rtj) set hlbj \u252c ;# (ttj) } light-heavy { set blc \u253d ;# heavy left (fwj) set tlc \u252d ;# heavy left (ttj) set brc \u2524 ;# light (rtj) set vllj \u2525 ;# heavy left (rtj) set hlbj \u252c ;# (ttj) } default { set blc \u253c ;# [punk::char::charshort boxd_lvhz] ;#light vertical and horizontal (fwj) set tlc \u252c ;# [punk::char::charshort boxd_ldhz] ;#T shape (ttj) set brc \u2524 ;# [punk::char::charshort boxd_lvl] ;#light vertical and left (rtj) } } } down_right { #6 set blc [punk::char::charshort boxd_lvr] ;#light vertical and right (ltj) set trc [punk::char::charshort boxd_ldhz] ;#T shape (ttj) set brc [punk::char::charshort boxd_lvhz] ;#light vertical and horizontal (fwj) } down_up { #7 set blc [punk::char::charshort boxd_lvr] ;#light vertical and right (ltj) set brc [punk::char::charshort boxd_lvl] ;#light vertical and left (rtj) set tlc [punk::char::charshort boxd_lvr] ;#light vertical and right (ltj) set trc [punk::char::charshort boxd_lvl] ;#light vertical and left (rtj) } left_right { #8 #from 2 set tlc [punk::char::charshort boxd_ldhz] ;#T shape (ttj) - joinleft set blc [punk::char::charshort boxd_luhz] ;#upside down T shape (btj) #from3 set trc [punk::char::charshort boxd_ldhz] ;#T shape (ttj) set brc [punk::char::charshort boxd_luhz] ;#upside down T shape (btj) } left_up { #9 set tlc [punk::char::charshort boxd_lvhz] ;#light vertical and horizontal (fwj) set trc [punk::char::charshort boxd_lvl] ;#light vertical and left (rtj) set blc [punk::char::charshort boxd_luhz] ;#upside down T shape (btj) } right_up { #10 set tlc [punk::char::charshort boxd_lvr] ;#light vertical and right (ltj) set trc [punk::char::charshort boxd_lvhz] ;#light vertical and horizontal (fwj) set brc [punk::char::charshort boxd_luhz] ;#upside down T shape (btj) } down_left_right { #11 set blc [punk::char::charshort boxd_lvhz] ;#light vertical and horizontal (fwj) set brc [punk::char::charshort boxd_lvhz] ;#light vertical and horizontal (fwj) set trc [punk::char::charshort boxd_ldhz] ;#T shape (ttj) set tlc [punk::char::charshort boxd_ldhz] ;#T shape (ttj) } down_left_up { #12 set tlc [punk::char::charshort boxd_lvhz] ;#light vertical and horizontal (fwj) set blc [punk::char::charshort boxd_lvhz] ;#light vertical and horizontal (fwj) set trc [punk::char::charshort boxd_lvl] ;#light vertical and left (rtj) set brc [punk::char::charshort boxd_lvl] ;#light vertical and left (rtj) } down_right_up { #13 set tlc [punk::char::charshort boxd_lvr] ;#light vertical and right (ltj) set blc [punk::char::charshort boxd_lvr] ;#light vertical and right (ltj) set trc [punk::char::charshort boxd_lvhz] ;#light vertical and horizontal (fwj) set brc [punk::char::charshort boxd_lvhz] ;#light vertical and horizontal (fwj) } left_right_up { #14 set tlc [punk::char::charshort boxd_lvhz] ;#light vertical and horizontal (fwj) set trc [punk::char::charshort boxd_lvhz] ;#light vertical and horizontal (fwj) set blc [punk::char::charshort boxd_luhz] ;#upside down T shape (btj) set brc [punk::char::charshort boxd_luhz] ;#upside down T shape (btj) } down_left_right_up { #15 set tlc [punk::char::charshort boxd_lvhz] ;#light vertical and horizontal (fwj) set blc [punk::char::charshort boxd_lvhz] ;#light vertical and horizontal (fwj) set trc [punk::char::charshort boxd_lvhz] ;#light vertical and horizontal (fwj) set brc [punk::char::charshort boxd_lvhz] ;#light vertical and horizontal (fwj) } } #four way junction (cd::fwj) (punk::ansi::g0 n) (punk::char::charshort lvhz) (+) } "heavy" { #unicode box drawing set set hl [punk::char::charshort boxd_hhz] ;# light horizontal set hlt $hl set hlb $hl set vl [punk::char::charshort boxd_hv] ;#light vertical set vll $vl set vlr $vl set tlc [punk::char::charshort boxd_hdr] set trc [punk::char::charshort boxd_hdl] set blc [punk::char::charshort boxd_hur] set brc [punk::char::charshort boxd_hul] #horizontal and vertical bar joins set hltj $hlt set hlbj $hlb set vllj $vll set vlrj $vlr #set targetdown,targetleft,targetright,targetup vars #default empty targets to current box type 'heavy' foreach dir {down left right up} { set target [dict get $join_targets $dir] switch -- $target { "" - heavy { set target$dir heavy } light - ascii - altg - arc { set target$dir light } default { set target$dir other } } } switch -- $do_joins { down { #1 switch -- $targetdown { light { set blc [punk::char::charshort boxd_dlruh] ;#\u2521 down light and right up heavy (ltj) set brc [punk::char::charshort boxd_dlluh] ;#\u2529 down light and left up heavy (rtj) set hlbj \u252F ;#down light (ttj) } heavy { set blc [punk::char::charshort boxd_hvr] ;# (ltj) set brc [punk::char::charshort boxd_hvl] ;# (rtj) set hlbj \u2533 ;# down heavy (ttj) } } } left { #2 switch -- $targetleft { light { set tlc [punk::char::charshort boxd_llrdh] ;#\u2532 Left Light and Right Down Heavy (ttj) set blc [punk::char::charshort boxd_llruh] ;#\u253a Left Light and Right Up Heavy (btj) set vllj \u2528 ;# left light (rtj) } heavy { set tlc [punk::char::charshort boxd_hdhz] ;# (ttj) set blc [punk::char::charshort boxd_huhz] ;# (btj) set vllj \u252b ;#(rtj) } } } right { #3 switch -- $targetright { light { set trc [punk::char::charshort boxd_rlldh] ;#\u2531 Right Light and Left Down Heavy (ttj) set brc [punk::char::charshort boxd_rlluh] ;#\u2539 Right Light and Left Up Heavy(btj) set vlrj \u2520 ;#right light (ltj) } heavy { set trc [punk::char::charshort boxd_hdhz] ;#T shape (ttj) set brc [punk::char::charshort boxd_huhz] ;# (btj) set vlrj \u2523 ;# (ltj) } } } up { #4 switch -- $targetup { light { set tlc [punk::char::charshort boxd_ulrdh] ;#\u2522 Up Light and Right Down Heavy (ltj) set trc [punk::char::charshort boxd_ulldh] ;#\u252a Up Light and Left Down Heavy (rtj) set hltj \u2537 ;# up light (btj) } heavy { set tlc [punk::char::charshort boxd_hvr] ;# (ltj) set trc [punk::char::charshort boxd_hvl] ;# (rtj) set hltj \u253b ;# (btj) } } } down_left { #down_left is the main join type used for 'L' shaped cell border table building where we boxlimit to {vll hlb blc} #5 switch -- down-$targetdown-left-$targetleft { down-light-left-heavy { set blc [punk::char::charshort boxd_dluhzh] ;#down light and up horizontal heavy (fwj) set brc [punk::char::charshort boxd_dlluh] ;#\u2529 down light and left up heavy (rtj) set tlc [punk::char::charshort boxd_hdhz] ;# (ttj) (at tlc down stays heavy) set hlbj \u252F ;# down light (ttj) set vllj \u252b ;#(rtj) } down-heavy-left-light { set blc [punk::char::charshort boxd_llrvh] ;# left light and right Vertical Heavy (fwj) set brc [punk::char::charshort boxd_hvl] ;# (rtj) (at brc left must stay heavy) set tlc [punk::char::charshort boxd_llrdh] ;#\u2532 Left Light and Right Down Heavy (ttj) set hlbj \u2533 ;# down heavy (ttj) set vllj \u2528 ;# left light (rtj) } down-light-left-light { set blc [punk::char::charshort boxd_ruhldl] ;#\u2544 right up heavy and left down light (fwj) set brc [punk::char::charshort boxd_dlluh] ;#\u2529 Down Light and left Up Heavy (rtj) (left must stay heavy) set tlc [punk::char::charshort boxd_llrdh] ;#\u2532 Left Light and Right Down Heavy (ttj) (we are in heavy - so down must stay heavy at tlc) set hlbj \u252F ;# down light (ttj) set vllj \u2528 ;# left light (rtj) } down-heavy-left-heavy { set blc [punk::char::charshort boxd_hvhz] ;# (fwj) set brc [punk::char::charshort boxd_hvl] ;# (rtj) set tlc [punk::char::charshort boxd_hdhz] ;# (ttj) set hlbj \u2533 ;#(ttj) set vllj \u252b ;#(rtj) } down-other-left-heavy { set blc \u253b ;#boxd_huhz Heavy Up and Horizontal (btj) #leave brc default corner set tlc \u2533 ;#hdhz Heavy Down and Horizontal (ttj) set vllj \u252b ;#(rtj) } down-other-left-light { set blc \u253a ;#llruh Left Light and Right Up Heavy (btj) #leave brc default corner set tlc \u2532 ;#llrdh Left Light and Right Down Heavy (ttj) set vllj \u2528 ;# left light (rtj) } down-heavy-left-other { set blc \u2523 ;#hvr Heavy Vertical and Right (ltj) set brc \u252b ;#hvl Heavy Veritcal and Left (rtj) #leave tlc default corner set hlbj \u2533 ;#(ttj) } down-light-left-other { set blc \u2521 ;#dlruh Down Light and Right Up Heavy (ltj) set brc \u2529 ;#dlluh Down Light and Left Up Heavy (rtj) #leave tlc default corner set hlbj \u252F ;# down light (ttj) } } } down_right { #6 set blc [punk::char::charshort boxd_hvr] ;# (ltj) (blc right&up stay heavy) set trc [punk::char::charshort boxd_hdhz] ;# (ttj) (tlc down&left stay heavy) set brc [punk::char::charshort boxd_hvhz] ;# (fwj) (brc left&up heavy) } down_up { #7 set blc [punk::char::charshort boxd_hvr] ;# (ltj) set brc [punk::char::charshort boxd_hvl] ;# (rtj) set tlc [punk::char::charshort boxd_hvr] ;# (ltj) set trc [punk::char::charshort boxd_hvl] ;# (rtj) } left_right { #8 #from 2 set tlc [punk::char::charshort boxd_hdhz] ;# (ttj) set blc [punk::char::charshort boxd_huhz] ;# (btj) #from3 set trc [punk::char::charshort boxd_hdhz] ;# (ttj) set brc [punk::char::charshort boxd_huhz] ;# (btj) } left_up { #9 set tlc [punk::char::charshort boxd_hvhz] ;# (fwj) set trc [punk::char::charshort boxd_hvl] ;# (rtj) set blc [punk::char::charshort boxd_huhz] ;# (btj) } right_up { #10 set tlc [punk::char::charshort boxd_hvr] ;# (ltj) set trc [punk::char::charshort boxd_hvhz] ;# (fwj) set brc [punk::char::charshort boxd_huhz] ;# (btj) } down_left_right { #11 set blc [punk::char::charshort boxd_hvhz] ;# (fwj) set brc [punk::char::charshort boxd_hvhz] ;# (fwj) set trc [punk::char::charshort boxd_hdhz] ;# (ttj) set tlc [punk::char::charshort boxd_hdhz] ;# (ttj) } down_left_up { #12 set tlc [punk::char::charshort boxd_hvhz] ;# (fwj) set blc [punk::char::charshort boxd_hvhz] ;# (fwj) set trc [punk::char::charshort boxd_hvl] ;# (rtj) set brc [punk::char::charshort boxd_hvl] ;# (rtj) } down_right_up { #13 set tlc [punk::char::charshort boxd_hvr] ;# (ltj) set blc [punk::char::charshort boxd_hvr] ;# (ltj) set trc [punk::char::charshort boxd_hvhz] ;# (fwj) set brc [punk::char::charshort boxd_hvhz] ;# (fwj) } left_right_up { #14 set tlc [punk::char::charshort boxd_hvhz] ;# (fwj) set trc [punk::char::charshort boxd_hvhz] ;# (fwj) set blc [punk::char::charshort boxd_huhz] ;# (btj) set brc [punk::char::charshort boxd_huhz] ;# (btj) } down_left_right_up { #15 set tlc [punk::char::charshort boxd_hvhz] ;# (fwj) set blc [punk::char::charshort boxd_hvhz] ;# (fwj) set trc [punk::char::charshort boxd_hvhz] ;# (fwj) set brc [punk::char::charshort boxd_hvhz] ;# (fwj) } } } "double" { #unicode box drawing set set hl [punk::char::charshort boxd_dhz] ;# double horizontal \U2550 set hlt $hl set hlb $hl set vl [punk::char::charshort boxd_dv] ;#double vertical \U2551 set vll $vl set vlr $vl set tlc [punk::char::charshort boxd_ddr] ;#double down and right \U2554 set trc [punk::char::charshort boxd_ddl] ;#double down and left \U2557 set blc [punk::char::charshort boxd_dur] ;#double up and right \U255A set brc [punk::char::charshort boxd_dul] ;#double up and left \U255D #horizontal and vertical bar joins set hltj $hlt set hlbj $hlb set vllj $vll set vlrj $vlr # \u256c (fwj) #set targetdown,targetleft,targetright,targetup vars #default empty targets to current box type 'double' foreach dir {down left right up} { set target [dict get $join_targets $dir] switch -- $target { "" - double { set target$dir double } light { set target$dir light } default { set target$dir other } } } #unicode provides no joining for double to anything else #better to leave a gap by using default double corners if join target is not empty or double switch -- $do_joins { down { #1 switch -- $targetdown { double { set blc \u2560 ;# (ltj) set brc \u2563 ;# (rtj) set hlbj \u2566 ;# (ttj) } light { set hlbj \u2564 ;# down light (ttj) } } } left { #2 switch -- $targetleft { double { set tlc \u2566 ;# (ttj) set blc \u2569 ;# (btj) set vllj \u2563 ;# (rtj) } light { set vllj \u2562 ;# light left (rtj) } } } right { #3 switch -- $targetright { double { set trc \u2566 ;# (ttj) set brc \u2569 ;# (btj) } light { set vlrj \u255F ;# light right (ltj) } } } up { #4 switch -- $targetup { double { set tlc \u2560 ;# (ltj) set trc \u2563 ;# (rtj) set hltj \u2569 ;# (btj) } light { set hltj \u2567 ;#up light (btj) } } } down_left { #5 switch -- $targetdown-$targetleft { double-double { set blc \u256c ;# (fwj) set brc \u2563 ;# (rtj) set tlc \u2566 ;# (ttj) set hlbj \u2566 ;# (ttj) } double-light { #no corner joins treat corners like 'other' set blc \u2560 ;# (ltj) set brc \u2563 ;# (rtj) set hlbj \u2566 ;# (ttj) set vllj \u2562 ;# light left (rtj) } double-other { set blc \u2560 ;# (ltj) set brc \u2563 ;# (rtj) #leave tlc as ordinary double corner } light-double { set vllj \u2563 ;# (rtj) set hlbj \u2564 ;# light down (ttj) } light-light { set vllj \u2562 ;# light left (rtj) set hlbj \u2564 ;# light down (ttj) } other-light { set vllj \u2562 ;# light left (rtj) } other-double { set blc \u2569 ;# (btj) #leave brc as ordinary double corner set tlc \u2566 ;# (ttj) } } } down_right { #6 switch -- $targetdown-$targetright { double-double { set blc \u2560 ;# (ltj) set trc \u2566 ;# (ttj) set brc \u256c ;# (fwj) set hlbj \u2566 ;# (ttj) } double-other { set blc \u2560 ;# (ltj) #leave trc default set brc \u2563 ;# (rtj) } other-double { #leave blc default set trc \u2566 ;# (ttj) set brc \u2569 ;#(btj) } } } down_up { #7 switch -- $targetdown-$targetup { double-double { set blc \u2560 ;# (ltj) set brc \u2563 ;# (rtj) set tlc \u2560 ;# (ltj) set trc \u2563 ;# (rtj) set hltj \u2569 ;# (btj) set hlbj \u2566 ;# (ttj) } } } left_right { #8 #from 2 set tlc [punk::char::charshort boxd_hdhz] ;# (ttj) set blc [punk::char::charshort boxd_huhz] ;# (btj) #from3 set trc [punk::char::charshort boxd_hdhz] ;# (ttj) set brc [punk::char::charshort boxd_huhz] ;# (btj) } left_up { #9 set tlc [punk::char::charshort boxd_hvhz] ;# (fwj) set trc [punk::char::charshort boxd_hvl] ;# (rtj) set blc [punk::char::charshort boxd_huhz] ;# (btj) set hltj \u2569 ;# (btj) set vllj \u2563 ;# (rtj) } right_up { #10 set tlc [punk::char::charshort boxd_hvr] ;# (ltj) set trc [punk::char::charshort boxd_hvhz] ;# (fwj) set brc [punk::char::charshort boxd_huhz] ;# (btj) set hltj \u2569 ;# (btj) set vlrj \u2560 ;# (ltj) } down_left_right { #11 set blc [punk::char::charshort boxd_hvhz] ;# (fwj) set brc [punk::char::charshort boxd_hvhz] ;# (fwj) set trc [punk::char::charshort boxd_hdhz] ;# (ttj) set tlc [punk::char::charshort boxd_hdhz] ;# (ttj) set hlbj \u2566 ;# (ttj) set vlrj \u2560 ;# (ltj) } down_left_up { #12 set tlc [punk::char::charshort boxd_hvhz] ;# (fwj) set blc [punk::char::charshort boxd_hvhz] ;# (fwj) set trc [punk::char::charshort boxd_hvl] ;# (rtj) set brc [punk::char::charshort boxd_hvl] ;# (rtj) set hltj \u2569 ;# (btj) set hlbj \u2566 ;# (ttj) } down_right_up { #13 set tlc [punk::char::charshort boxd_hvr] ;# (ltj) set blc [punk::char::charshort boxd_hvr] ;# (ltj) set trc [punk::char::charshort boxd_hvhz] ;# (fwj) set brc [punk::char::charshort boxd_hvhz] ;# (fwj) set hltj \u2569 ;# (btj) set hlbj \u2566 ;# (ttj) } left_right_up { #14 set tlc [punk::char::charshort boxd_hvhz] ;# (fwj) set trc [punk::char::charshort boxd_hvhz] ;# (fwj) set blc [punk::char::charshort boxd_huhz] ;# (btj) set brc [punk::char::charshort boxd_huhz] ;# (btj) set hltj \u2569 ;# (btj) } down_left_right_up { #15 set tlc [punk::char::charshort boxd_hvhz] ;# (fwj) set blc [punk::char::charshort boxd_hvhz] ;# (fwj) set trc [punk::char::charshort boxd_hvhz] ;# (fwj) set brc [punk::char::charshort boxd_hvhz] ;# (fwj) set hltj \u2569 ;# (btj) set hlbj \u2566 ;# (ttj) } } } "arc" { #unicode box drawing set set hl [punk::char::charshort boxd_lhz] ;# light horizontal set hlt $hl set hlb $hl set vl [punk::char::charshort boxd_lv] ;#light vertical set vll $vl set vlr $vl set tlc [punk::char::charshort boxd_ladr] ;#light arc down and right \U256D set trc [punk::char::charshort boxd_ladl] ;#light arc down and left \U256E set blc [punk::char::charshort boxd_laur] ;#light arc up and right \U2570 set brc [punk::char::charshort boxd_laul] ;#light arc up and left \U256F #horizontal and vertical bar joins set hltj $hlt set hlbj $hlb set vllj $vll set vlrj $vlr #set targetdown,targetleft,targetright,targetup vars #default empty targets to current box type 'arc' foreach dir {down left right up} { set target [dict get $join_targets $dir] switch -- $target { "" - arc { set target$dir self } default { set target$dir other } } } switch -- $do_joins { down { #1 switch -- $targetdown { self { set blc \u251c ;# *light (ltj) #set blc \u29FD ;# misc math right-pointing curved angle bracket (ltj) - positioned too far left #set blc \u227b ;# math succeeds char. joins on right ok - gaps top and bottom - almost passable #set blc \u22b1 ;#math succeeds under relation - looks 'ornate', sits too low to join horizontal #set brc \u2524 ;# *light(rtj) #set brc \u29FC ;# misc math left-pointing curved angle bracket (rtj) } } } left { #2 switch -- $targetleft { self { set tlc \u252c ;# *light(ttj) - need a low positioned curved y shape - nothing apparent #set blc \u2144 ;# (btj) - upside down y - positioning too low for arc set blc \u2534 ;# *light (btj) } } } right { #3 switch -- $targetright { self { set trc \u252c ;# *light (ttj) #set brc \u2144 ;# (btj) set brc \u2534 ;# *light (btj) } } } up { #4 switch -- $targetup { self { set tlc \u251c ;# *light (ltj) set trc \u2524 ;# *light(rtj) } } } down_left { #5 switch -- $targetdown-$targetleft { self-self { #set blc \u27e1 ;# white concave-sided diamond - positioned too far right #set blc \u27e3 ;# concave sided diamond with rightwards tick - positioned too low, too right - big gaps set brc \u2524 ;# *light (rtj) set tlc \u252c ;# *light (ttj) } self-other { #set blc \u2560 ;# (ltj) #set brc \u2563 ;# (rtj) #leave tlc as ordinary double corner } other-self { #set blc \u2569 ;# (btj) #leave brc as ordinary double corner #set tlc \u2566 ;# (ttj) } } } } } block1 { set hlt \u2581 ;# lower one eighth block set hlb \u2594 ;# upper one eighth block set vll \u258f ;# left one eighth block set vlr \u2595 ;# right one eighth block set tlc \u2581 ;# lower one eighth block set trc \u2581 ;# lower one eighth block set blc \u2594 ;# upper one eighth block set brc \u2594 ;# upper one eight block #horizontal and vertical bar joins set hltj $hlt set hlbj $hlb set vllj $vll set vlrj $vlr } blockxx { set hlt \u2594 ;# upper one eighth block set hlb \u2581 ;# lower one eighth block set vll \u2595 ;# right one eighth block set vlr \u258f ;# left one eighth block set tlc \u2595 ;# right one eighth block set trc \u258f ;# left one eighth block set blc \u2595 ;# right one eighth block set brc \u258f ;# left one eighth block #horizontal and vertical bar joins set hltj $hlt set hlbj $hlb set vllj $vll set vlrj $vlr } block { set hlt \u2580 ;#upper half set hlb \u2584 ;#lower half set vll \u258c ;#left half set vlr \u2590 ;#right half set tlc \u259b ;#upper left corner half set trc \u259c set blc \u2599 set brc \u259f #horizontal and vertical bar joins set hltj $hlt set hlbj $hlb set vllj $vll set vlrj $vlr } default { set default_custom [dict create hl " " vl " " tlc " " trc " " blc " " brc " "] set custom_frame [dict merge $default_custom $f] dict with custom_frame {} ;#extract keys as vars if {[dict exists $custom_frame hlt]} { set hlt [dict get $custom_frame hlt] } else { set hlt $hl } if {[dict exists $custom_frame hlb]} { set hlb [dict get $custom_frame hlb] } else { set hlb $hl } if {[dict exists $custom_frame vll]} { set vll [dict get $custom_frame vll] } else { set vll $vl } if {[dict exists $custom_frame vlr]} { set vlr [dict get $custom_frame vlr] } else { set vlr $vl } #horizontal and vertical bar joins set hltj $hlt set hlbj $hlb set vllj $vll set vlrj $vlr } } if {$boxonly} { set result [dict create\ tlc $tlc hlt $hlt trc $trc\ vll $vll vlr $vlr\ blc $blc hlb $hlb brc $brc\ ] dict set framedef_cache $cache_key $result return $result } else { set result [dict create\ tlc $tlc hlt $hlt trc $trc\ vll $vll vlr $vlr\ blc $blc hlb $hlb brc $brc\ hltj $hltj\ hlbj $hlbj\ vllj $vllj\ vlrj $vlrj\ ] dict set framedef_cache $cache_key $result return $result } } variable frame_cache set frame_cache [dict create] proc frame_cache {{action ""}} { if {$action ni [list clear ""]} { error "frame_cache action '$action' not understood. Valid actions: clear" } variable frame_cache set out "" if {[catch { set termwidth [dict get [punk::console::get_size] columns] }]} { set termwidth 80 } dict for {k v} $frame_cache { lassign $v _f frame _used used #set fwidth [textblock::widthtopline $frame] #review - are cached frames uniform width lines? set fwidth [textblock::width $frame] set frameinfo "$k used:$used " set allinone_width [expr {[string length $frameinfo] + $fwidth}] if {$allinone_width >= $termwidth} { #split across 2 lines append out "$frameinfo\n" append out $frame \n } else { append out [textblock::join $frameinfo $frame]\n } append out \n ;# frames used to build tables often have joins - keep a line in between for clarity } if {$action eq "clear"} { set frame_cache [dict create] append out \nCLEARED } return $out } #options before content argument - which is allowed to be absent #frame performance (noticeable with complex tables even of modest size) is improved significantly by frame_cache - but is still (2024) a fairly expensive operation. proc frame {args} { variable frametypes set expect_optval 0 set argposn 0 set pmax [expr {[llength $args]-1}] set has_contents 0 ;#differentiate between empty string and no content supplied set contents "" set arglist [list] foreach a $args { if {!$expect_optval} { if {$argposn < $pmax} { if {[string match -* $a]} { set expect_optval 1 lappend arglist $a } else { error "textblock::frame expects -option pairs" } } else { set has_contents 1 set contents $a } } else { lappend arglist $a set expect_optval 0 } incr argposn } #set contents [lindex $args end] #set arglist [lrange $args 0 end-1] if {[llength $arglist] % 2 != 0} { error "Usage frame ?-type unicode|altg|ascii|? ?-title ? ?-subtitle ? ?-width ? ?-ansiborder ? ?-boxlimits hl|hlt|hlb|vl|vll|vlr|tlc|blc|brc? ?-joins left|right|up|down? " } #todo args -justify left|centre|right (center) set defaults [dict create\ -etabs 0\ -type light\ -boxlimits [list hl vl tlc blc trc brc]\ -boxmap {}\ -joins [list]\ -title ""\ -subtitle ""\ -width ""\ -height ""\ -ansiborder ""\ -ansibase ""\ -blockalign "centre"\ -textalign "left"\ -ellipsis 1\ -usecache 1\ -buildcache 1\ ] #todo -blockalignbias -textalignbias? #use -buildcache 1 with -usecache 0 for debugging cache issues so we can inspect using textblock::frame_cache set opts [dict merge $defaults $arglist] foreach {k v} $opts { switch -- $k { -etabs - -type - -boxlimits - -boxmap - -joins - -title - -subtitle - -width - -height - -ansiborder - -ansibase - -blockalign - -textalign - -ellipsis - -usecache - -buildcache {} default { error "frame option '$k' not understood. Valid options are [dict keys $defaults]" } } } # -- --- --- --- --- --- set opt_etabs [dict get $opts -etabs] set opt_type [dict get $opts -type] set opt_boxlimits [dict get $opts -boxlimits] set opt_joins [dict get $opts -joins] set opt_boxmap [dict get $opts -boxmap] set opt_usecache [dict get $opts -usecache] set opt_buildcache [dict get $opts -buildcache] set usecache $opt_usecache ;#may need to override set buildcache $opt_buildcache set custom_keys [list hl hlt hlb vl vll vlr tlc trc blc brc] set known_frametypes $frametypes ;# light, heavey etc as defined in textblock::frametypes variable set default_custom [dict create hl " " vl " " tlc " " trc " " blc " " brc " "] lassign [textblock::frametype $opt_type] _cat category _type ftype if {$category eq "custom"} { set custom_frame $ftype set frameset "custom" set framedef $custom_frame } else { #category = predefined set frameset $ftype ;# light,heavy etc set framedef $ftype } set is_boxlimits_ok 1 set exact_boxlimits [list] foreach v $opt_boxlimits { switch -- $v { hl { lappend exact_boxlimits hlt hlb } vl { lappend exact_boxlimits vll vlr } hlt - hlb - vll - vlr - tlc - trc - blc - brc { lappend exact_boxlimits $v } default { #k not in custom_keys set is_boxlimits_ok 0 break } } } if {!$is_boxlimits_ok} { error "frame option -boxlimits '$opt_boxlimits' must contain only values from the set: hl,hlt,hlb,vl,vll,vlr,tlc,trc,blc,brc" } set exact_boxlimits [lsort -unique $exact_boxlimits] set is_joins_ok 1 foreach v $opt_joins { lassign [split $v -] direction target switch -- $direction { left - right - up - down {} default { set is_joins_ok 0 break } } switch -- $target { "" - light - heavy - ascii - altg - arc - double - custom - block - block1 {} default { set is_joins_ok 0 break } } } if {!$is_joins_ok} { error "frame option -joins '$opt_joins' must contain only values from the set: left,right,up,down with targets heavy,light,ascii,altg,arc,double,block,block1,custom e.g down-light" } set is_boxmap_ok 1 dict for {boxelement subst} $opt_boxmap { switch -- $boxelement { hl - hlt - hlb - vl - vll - vlr - tlc - trc - blc - brc {} default { set is_boxmap_ok 0 break } } } if {!$is_boxmap_ok} { error "frame option -boxmap '$opt_boxmap' must contain only keys from the set: hl,hlt,hlb,vl,vll,vlr,tlc,trc,blc,brc" } #sorted order down left right up #1 x choose 4 #4 x choose 3 #6 x choose 2 #4 x choose 1 #15 combos set join_directions [list] #targets - light,heavy (double?) - seem to be some required glyphs missing from unicode #e.g down-light, up-heavy set join_targets [dict create left "" down "" right "" up ""] foreach jt $opt_joins { lassign [split $jt -] direction target if {$target ne ""} { dict set join_targets $direction $target } lappend join_directions $direction } set join_directions [lsort -unique $join_directions] set do_joins [::join $join_directions _] # -- --- --- --- --- --- set opt_title [dict get $opts -title] set opt_subtitle [dict get $opts -subtitle] set opt_width [dict get $opts -width] set opt_height [dict get $opts -height] # -- --- --- --- --- --- set opt_blockalign [dict get $opts -blockalign] switch -- $opt_blockalign { left - right - centre - center {} default { error "frame option -blockalign must be left|right|centre|center - received: $opt_blockalign" } } #these are all valid commands for overtype:: # -- --- --- --- --- --- set opt_textalign [dict get $opts -textalign] switch -- $opt_textalign { left - right - centre - center {} default { error "frame option -textalign must be left|right|centre|center - received: $opt_textalign" } } # -- --- --- --- --- --- set opt_ansiborder [dict get $opts -ansiborder] set opt_ansibase [dict get $opts -ansibase] ;#experimental set opt_ellipsis [dict get $opts -ellipsis] # -- --- --- --- --- --- if {$has_contents} { if {[string last \t $contents] >= 0} { if {[info exists punk::console::tabwidth]} { set tw $::punk::console::tabwidth } else { set tw 8 } if {$opt_etabs} { set contents [textutil::tabify::untabify2 $contents $tw] } } set contents [string map [list \r\n \n] $contents] set actual_contentwidth [textblock::width $contents] ;#length of longest line in contents (contents can be ragged) set actual_contentheight [textblock::height $contents] } else { set actual_contentwidth 0 set actual_contentheight 0 } if {$opt_title ne ""} { set titlewidth [punk::ansi::printing_length $opt_title] set content_or_title_width [expr {max($actual_contentwidth,$titlewidth)}] } else { set titlewith 0 set content_or_title_width $actual_contentwidth } if {$opt_width eq ""} { set frame_inner_width $content_or_title_width } else { set frame_inner_width [expr {max(0,$opt_width - 2)}] ;#default } if {$opt_height eq ""} { set frame_inner_height $actual_contentheight } else { set frame_inner_height [expr {max(0,$opt_height -2)}] ;#default } if {$frame_inner_height == 0 && $frame_inner_width == 0} { set has_contents 0 } #todo - render it with vertical overflow so we can process ansi moves? #set linecount [textblock::height $contents] set linecount $frame_inner_height # -- --- --- --- --- --- --- --- --- variable frame_cache #review - custom frame affects frame_inner_width - exclude from caching? #set cache_key [concat $arglist $frame_inner_width $frame_inner_height] set hashables [concat $arglist $frame_inner_width $frame_inner_height] package require md5 set hash [md5::md5 -hex $hashables] set cache_key "$hash-$frame_inner_width-$frame_inner_height-actualcontentwidth:$actual_contentwidth" #should be in a unicode private range different to that used in table construction #e.g BMP PUA U+E000 -> U+F8FF - although this is commonly used for example by nerdfonts #also supplementary private use blocks #however these display double wide on for example cmd terminal despite having wcswidth 1 (makes layout debugging difficult) #U+F0000 -> U+FFFD #U+100000 -> U+10FFFD #FSUB options: \uf0ff \uF1FF \uf2ff (no nerdfont glyphs - should look like standard replacement char) \uF2DD (circular thingy) #should be something someone is unlikely to use as part of a custom frame character. #ideally a glyph that doesn't auto-expand into whitespace and is countable when in a string (narrower is better) #As nerdfont glyphs tend to be mostly equal height & width - circular glyphs tend to be more distinguishable in a string #terminal settings may need to be adjusted to stop auto glyph resizing - a rather annoying misfeature that some people seem to think they like. #e.g in wezterm config: allow_square_glyphs_to_overflow_width = "Never" #review - we could consider wasting a few cycles to check for a conflict and use a different FSUB set FSUB \uF2DD #this occurs commonly in table building with colspans - review if {$actual_contentwidth > $frame_inner_width || $actual_contentheight != $frame_inner_height} { set usecache 0 #set buildcache 0 ;#comment out for debug/analysis so we can see #puts "--->> frame_inner_width:$frame_inner_width actual_contentwidth:$actual_contentwidth contents: '$contents'" set cache_key [a+ Web-red web-white]$cache_key[a] } if {$buildcache && $actual_contentwidth < $frame_inner_width} { #colourise cache_key to warn if {$actual_contentwidth == 0} { #we can still substitute with right length set cache_key [a+ Web-steelblue web-black]$cache_key[a] } else { #actual_contentwidth is narrower than frame - check template's patternwidth if {[dict exists $frame_cache $cache_key]} { set cache_patternwidth [dict get $frame_cache $cache_key patternwidth] } else { set cache_patternwidth [$actual_contentwidth] } if {$actual_contentwidth < $cache_patternwidth} { set usecache 0 set cache_key [a+ Web-orange web-black]$cache_key[a] } elseif {$actual_contentwidth == $cache_patternwidth} { #set usecache 1 } else { #actual_contentwidth > pattern set usecache 0 set cache_key [a+ Web-red web-black]$cache_key[a] } } } #JMN debug #set usecache 0 set is_cached 0 if {$usecache && [dict exists $frame_cache $cache_key]} { set cache_patternwidth [dict get $frame_cache $cache_key patternwidth] set template [dict get $frame_cache $cache_key frame] set used [dict get $frame_cache $cache_key used] dict set frame_cache $cache_key used [expr {$used+1}] ;#update existing record set is_cached 1 } # -- --- --- --- --- --- --- --- --- if {!$is_cached} { set rst [a] #set column [string repeat " " $frame_inner_width] ;#default - may need to override for custom frame set underlayline [string repeat " " $frame_inner_width] set underlay [::join [lrepeat $linecount $underlayline] \n] set vll_width 1 ;#default for all except custom (printing width) set vlr_width 1 set framedef [textblock::framedef $framedef -joins $opt_joins] 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 set vlr $sub set hl $sub } elseif {$boxelement eq "hl"} { set hlt $sub set hlb $sub set hl $sub } else { set $boxelement $sub } } 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] set hlt_width [punk::ansi::printing_length $hlt] set vlr_width [punk::ansi::printing_length $vlr] set tlc_width [punk::ansi::printing_length $tlc] set trc_width [punk::ansi::printing_length $trc] set blc_width [punk::ansi::printing_length $blc] set brc_width [punk::ansi::printing_length $brc] set framewidth [expr {$frame_inner_width + 2}] ;#reverse default assumption if {$opt_width eq ""} { #width wasn't specified - so user is expecting frame to adapt to title/contents #content shouldn't truncate because of extra wide frame #review - punk::console::get_size ? wrapping? quite hard to support with colspans set frame_inner_width $content_or_title_width set tbarwidth [expr {$content_or_title_width + 2 - $tlc_width - $trc_width - 2 + $vll_width + $vlr_width}] ;#+/2's for difference between border element widths and standard element single-width set bbarwidth [expr {$content_or_title_width + 2 - $blc_width - $brc_width - 2 + $vll_width + $vlr_width}] } else { set frame_inner_width [expr $opt_width - $vll_width - $vlr_width] ;#content may be truncated set tbarwidth [expr {$opt_width - $tlc_width - $trc_width}] set bbarwidth [expr {$opt_width - $blc_width - $brc_width}] } #set column [string repeat " " $frame_inner_width] set underlayline [string repeat " " $frame_inner_width] set underlay [::join [lrepeat $linecount $underlayline] \n] #cache? if {$hlt_width == 1} { set tbar [string repeat $hlt $tbarwidth] } else { #possibly mixed width chars that make up hlt - string range won't get width right set blank [string repeat " " $tbarwidth] if {$hlt_width > 0} { set count [expr {($tbarwidth / $hlt_width) + 1}] } else { set count 0 } set tbar [string repeat $hlt $count] #set tbar [string range $tbar 0 $tbarwidth-1] set tbar [overtype::left -overflow 0 -exposed1 " " -exposed2 " " $blank $tbar];#spaces for exposed halves of 2w chars instead of default replacement character } if {$hlb_width == 1} { set bbar [string repeat $hlb $bbarwidth] } else { set blank [string repeat " " $bbarwidth] if {$hlb_width > 0} { set count [expr {($bbarwidth / $hlb_width) + 1}] } else { set count 0 } set bbar [string repeat $hlb $count] #set bbar [string range $bbar 0 $bbarwidth-1] set bbar [overtype::left -overflow 0 -exposed1 " " -exposed2 " " $blank $bbar] } } altg { set tbar [string repeat $hlt $frame_inner_width] set tbar [cd::groptim $tbar] set bbar [string repeat $hlb $frame_inner_width] set bbar [cd::groptim $bbar] } default { set tbar [string repeat $hlt $frame_inner_width] set bbar [string repeat $hlb $frame_inner_width] } } set leftborder 0 set rightborder 0 set topborder 0 set bottomborder 0 # hl - hlt - hlb - vl - vll - vlr - tlc - trc - blc - brc {} #puts "----->$exact_boxlimits" foreach lim $exact_boxlimits { switch -- $lim { hlt { set topborder 1 } hlb { set bottomborder 1 } vll { set leftborder 1 } vlr { set rightborder 1 } tlc { set topborder 1 set leftborder 1 } trc { set topborder 1 set rightborder 1 } blc { set bottomborder 1 set leftborder 1 } brc { set bottomborder 1 set rightborder 1 } } } if {$opt_width ne "" && $opt_width < 2} { set rightborder 0 } #keep lhs/rhs separate? can we do vertical text on sidebars? set lhs [string repeat $vll\n $linecount] set lhs [string range $lhs 0 end-1] set rhs [string repeat $vlr\n $linecount] set rhs [string range $rhs 0 end-1] if {$opt_ansiborder ne ""} { set tbar $opt_ansiborder$tbar$rst set bbar $opt_ansiborder$bbar$rst set tlc $opt_ansiborder$tlc$rst 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 } #boxlimits used for partial borders in table generation set all_exact_boxlimits [list vll vlr hlt hlb tlc blc trc brc] set unspecified_limits [struct::set difference $all_exact_boxlimits $exact_boxlimits] foreach lim $unspecified_limits { switch -- $lim { vll { set blank_vll [string repeat " " $vll_width] set lhs [string repeat $blank_vll\n $linecount] set lhs [string range $lhs 0 end-1] } vlr { set blank_vlr [string repeat " " $vlr_width] set rhs [string repeat $blank_vlr\n $linecount] set rhs [string range $rhs 0 end-1] } hlt { set bar_width [punk::ansi::printing_length $tbar] set tbar [string repeat " " $bar_width] } tlc { set tlc_width [punk::ansi::printing_length $tlc] set tlc [string repeat " " $tlc_width] } trc { set trc_width [punk::ansi::printing_length $trc] set trc [string repeat " " $trc_width] } hlb { set bar_width [punk::ansi::printing_length $bbar] set bbar [string repeat " " $bar_width] } blc { set blc_width [punk::ansi::printing_length $blc] set blc [string repeat " " $blc_width] } brc { set brc_width [punk::ansi::printing_length $brc] set brc [string repeat " " $brc_width] } } } if {$opt_title ne ""} { set topbar [overtype::centre -overflow 0 -exposed1 " " -exposed2 " " -ellipsis 1 -bias left $tbar $opt_title] ;#overtype supports gx0 on/off } else { set topbar $tbar } if {$opt_subtitle ne ""} { set bottombar [overtype::centre -overflow 0 -exposed1 " " -exposed2 " " -ellipsis 1 -bias left $bbar $opt_subtitle] ;#overtype supports gx0 on/off } else { set bottombar $bbar } if {$opt_ansibase eq ""} { set rstbase [a] } else { set rstbase [a]$opt_ansibase } if {$opt_title ne ""} { #title overrides -boxlimits for topborder set topborder 1 } set fs "" set fscached "" set cache_patternwidth 0 #todo - output nothing except maybe newlines depending on if opt_height 0 and/or opt_width 0? if {$topborder} { if {$leftborder && $rightborder} { append fs $tlc$topbar$trc } else { if {$leftborder} { append fs $tlc$topbar } elseif {$rightborder} { append fs $topbar$trc } else { append fs $topbar } } } append fscached $fs if {$has_contents || $opt_height > 2} { #if {$topborder && $fs ne "xx"} { # append fs \n #} if {$topborder} { append fs \n append fscached \n } switch -- $opt_textalign { right {set pad "left"} left {set pad "right"} default {set pad $opt_textalign} } #set textaligned_contents [textblock::pad $contents -width $actual_contentwidth -which $pad -within_ansi 1] #set inner [overtype::block -blockalign $opt_blockalign -ellipsis $opt_ellipsis $opt_ansibase$underlay$rstbase $opt_ansibase$textaligned_contents] set cache_contentline [string repeat $FSUB $actual_contentwidth] set cache_patternwidth $actual_contentwidth set cache_contentpattern [::join [lrepeat $linecount $cache_contentline] \n] set cache_inner [overtype::block -blockalign $opt_blockalign -ellipsis $opt_ellipsis $opt_ansibase$underlay$rstbase $opt_ansibase$cache_contentpattern] #after overtype::block - our actual patternwidth may be less set cache_patternwidth [string length [lindex [regexp -inline "\[^$FSUB]*(\[$FSUB]*).*" $cache_inner] 1]] if {$leftborder && $rightborder} { #set bodyparts [list $lhs $inner $rhs] set cache_bodyparts [list $lhs $cache_inner $rhs] } else { if {$leftborder} { #set bodyparts [list $lhs $inner] set cache_bodyparts [list $lhs $cache_inner] } elseif {$rightborder} { #set bodyparts [list $inner $rhs] set cache_bodyparts [list $cache_inner $rhs] } else { #set bodyparts [list $inner] set cache_bodyparts [list $cache_inner] } } #set body [textblock::join -- {*}$bodyparts] set cache_body [textblock::join -- {*}$cache_bodyparts] append fscached $cache_body #append fs $body } if {$opt_height eq "" || $opt_height > 1} { if {$opt_subtitle ne ""} { #subtitle overrides boxlimits for bottomborder set bottomborder 1 } if {$bottomborder} { if {($topborder & $fs ne "xx" ) || ($has_contents || $opt_height > 2)} { #append fs \n append fscached \n } if {$leftborder && $rightborder} { #append fs $blc$bottombar$brc append fscached $blc$bottombar$brc } else { if {$leftborder} { #append fs $blc$bottombar append fscached $blc$bottombar } elseif {$rightborder} { #append fs $bottombar$brc append fscached $bottombar$brc } else { #append fs $bottombar append fscached $bottombar } } } } set template $fscached ;#end !$is_cached } #use the same mechanism to build the final frame - whether from cache or template if {$actual_contentwidth == 0} { set fs [string map [list $FSUB " "] $template] } else { set resultlines [list] set overwritable [string repeat $FSUB $cache_patternwidth] set contentindex 0 switch -- $opt_textalign { left {set pad right} right {set pad left} default {set pad $opt_textalign} } #review if {[string is integer -strict $opt_height] && $actual_contentheight < ($opt_height -2)} { set diff [expr {($opt_height -2) - $actual_contentheight}] append contents [::join [lrepeat $diff \n] ""] } set paddedcontents [textblock::pad $contents -which $pad -within_ansi 1] ;#autowidth to width of content (should match cache_patternwidth) set paddedwidth [textblock::widthtopline $paddedcontents] #review - horizontal truncation if {$paddedwidth > $cache_patternwidth} { set paddedcontents [overtype::renderspace -width $cache_patternwidth "" $paddedcontents] } set contentblock [textblock::join $paddedcontents] ;#make sure each line has ansi replays set tlines [split $template \n] #we will need to strip off the leading reset on each line when stitching together with template lines so that ansibase can come into play too. #after textblock::join the reset will be a separate code ie should be exactly ESC[0m set R [a] set rlen [string length $R] set clines [split $contentblock \n] foreach tline $tlines { if {[string first $FSUB $tline] >= 0} { set content_line [lindex $clines $contentindex] if {[string first $R $content_line] == 0} { set content_line [string range $content_line $rlen end] } #make sure to replay opt_ansibase to the right of the replacement lappend resultlines [string map [list $overwritable $content_line$opt_ansibase] $tline] incr contentindex } else { lappend resultlines $tline } } set fs [::join $resultlines \n] } if {$is_cached} { return $fs } else { if {$buildcache} { dict set frame_cache $cache_key [list frame $template used 0 patternwidth $cache_patternwidth] } return $fs } } proc gcross {{size 1} args} { if {$size == 0} { return "" } set defaults [list\ -max_cross_size 0 ] set opts [dict merge $defaults $args] set opt_max_cross_size [dict get $opts -max_cross_size] #set fit_size [punk::lib::greatestOddFactor $size] set fit_size $size if {$opt_max_cross_size == 0} { set max_cross_size $fit_size } else { #todo - only allow divisors #set testsize [expr {min($fit_size,$opt_max_cross_size)}] set factors [punk::lib::factors $size] #pick odd size in list that is smaller or equal to test_size set max_cross_size [lindex $factors end] set last_ok [lindex $factors 0] for {set i 0} {$i < [llength $factors]} {incr i} { set s [lindex $factors $i] if {$s > $opt_max_cross_size} { break } set last_ok $s } set max_cross_size $last_ok } set crosscount [expr {$size / $max_cross_size}] package require punk::char set x [punk::char::charshort boxd_ldc] set bs [punk::char::charshort boxd_ldgullr] set fs [punk::char::charshort boxd_ldgurll] set onecross "" set crossrows [list] set armsize [expr {int(floor($max_cross_size /2))}] set row [lrepeat $max_cross_size " "] #toparm for {set i 0} {$i < $armsize} {incr i} { set r $row lset r $i $bs lset r end-$i $fs #append onecross [::join $r ""] \n lappend crossrows [::join $r ""] } if {$max_cross_size % 2 != 0} { #only put centre cross in for odd sized crosses set r $row lset r $armsize $x #append onecross [::join $r ""] \n lappend crossrows [::join $r ""] } for {set i [expr {$armsize -1}]} {$i >= 0} {incr i -1} { set r $row lset r $i $fs lset r end-$i $bs #append onecross [::join $r ""] \n lappend crossrows [::join $r ""] } #set onecross [string trimright $onecross \n] set onecross [::join $crossrows \n] #fastest to do row first then columns - because textblock::join must do line by line if {$crosscount > 1} { package require textblock set row [textblock::join {*}[lrepeat $crosscount $onecross]] set rows [lrepeat $crosscount $row] set out [::join $rows \n] } else { set out $onecross } return $out } #Test we can join two coloured blocks proc test_colour {} { set b1 [a= red]1\n2\n3[a=] set b2 [a= green]a\nb\nc[a=] set result [textblock::join $b1 $b2] puts $result #return [list $b1 $b2 $result] return [ansistring VIEW $result] } namespace import ::punk::ansi::stripansi } namespace eval ::textblock::piper { namespace export * proc join {rhs pipelinedata} { tailcall ::textblock::join -- $pipelinedata $rhs } } interp alias {} piper_blockjoin {} ::textblock::piper::join # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ ## Ready package provide textblock [namespace eval textblock { variable version set version 999999.0a1.0 }] return