You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
6375 lines
320 KiB
6375 lines
320 KiB
# -*- tcl -*- |
|
# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from <pkg>-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 <unspecified> |
|
# @@ 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 <map>} |
|
#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 <data>]] 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 -- <input/0,indent/1| |
|
#pipealias ::textblock::padright .= {list $input [string repeat " " $colsize]} |/0,padding/1> punk::lib::lines_as_list -- |> .= {lmap v $data {overtype::left $padding $v}} |> punk::lib::list_as_lines -- <input/0,colsize/1| |
|
|
|
proc ::textblock::pad {block args} { |
|
set defaults [dict create\ |
|
-padchar " "\ |
|
-which "right"\ |
|
-width ""\ |
|
-overflow 0\ |
|
-within_ansi 0\ |
|
] |
|
#review!? |
|
#-within_ansi means after a leading ansi code when doing left pad on all but last line |
|
#-within_ansi means before a trailing ansi code when doing right pad on all but last line |
|
set usage "pad ?-padchar <sp>|<character>? ?-which right|left|centre? ?-width auto|<int>? ?-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 -- <lhs/0,w1/1,rhs/2,w2/3| |
|
# |
|
|
|
# pipealias ::textblock::joinpair .= {list $lhs [string repeat " " [width $lhs]] $rhs [string repeat " " [width $rhs]]} {| |
|
# /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 -- <lhs/0,rhs/1| |
|
|
|
|
|
proc ::textblock::join1 {args} { |
|
lassign [punk::args::opts_values { |
|
-ansiresets -default 1 -type integer |
|
blocks -type string -multiple 1 |
|
} $args] _o opts _v values |
|
set blocks [dict get $values blocks] |
|
|
|
set idx 0 |
|
set fordata [list] |
|
foreach b $blocks { |
|
set c($idx) [string repeat " " [width $b]] |
|
lappend fordata "v($idx)" [punk::lib::lines_as_list {*}$opts -- $b] |
|
incr idx |
|
} |
|
set outlines [list] |
|
foreach {*}$fordata { |
|
set row "" |
|
foreach colidx [lsort -integer -increasing [array names c]] { |
|
append row [overtype::left $c($colidx) $v($colidx)] |
|
} |
|
lappend outlines $row |
|
} |
|
return [punk::lib::list_as_lines -- $outlines] |
|
} |
|
|
|
#for joining 'rendered' blocks of plain or ansi text. Being 'rendered' means they are without ansi movement sequences as these have been processed |
|
#they may however still be 'ragged' ie differing line lengths |
|
proc ::textblock::join {args} { |
|
#lassign [punk::lib::opts_values { |
|
# blocks -type string -multiple 1 |
|
#} $args] _o opts _v values |
|
#set blocks [dict get $values blocks] |
|
|
|
#-ansireplays is always on (if ansi detected) |
|
|
|
#we will support -- at posn 0 and 2 only to allow an optional single option pair for -ansiresets |
|
#textblock::join is already somewhat expensive - we don't want to do much argument processing |
|
#also "--" is a legitimate joining block - so we need to support that too without too much risk of misinterpretation |
|
#this makes for a somewhat messy api.. -- is required if first block is actually -- (or the very unlikely case the first block is intended to be -ansiresets) |
|
set ansiresets auto |
|
switch -- [lindex $args 0] { |
|
-- { |
|
set blocks [lrange $args 1 end] |
|
} |
|
-ansiresets { |
|
if {[lindex $args 2] eq "--"} { |
|
set blocks [lrange $args 3 end] |
|
} else { |
|
set blocks [lrange $args 2 end] |
|
} |
|
set ansiresets [lindex $args 1] |
|
} |
|
default { |
|
set blocks $args |
|
} |
|
} |
|
|
|
if {![llength $blocks]} { |
|
return |
|
} |
|
|
|
set idx 0 |
|
set fordata [list] |
|
set colindices [list] |
|
foreach b $blocks { |
|
set c($idx) [string repeat " " [width $b]] |
|
set w($idx) [width $b] ;#we need the width of a rendered block for per-row renderline calls |
|
#fairly commonly a block may be a vertical list of chars e.g something like 1\n2\n3 or -\n-\n-\n- |
|
#for determining a shortcut to avoid unnecessary ta::detect - (e.g width <=1) we can't use result of textblock::width as it strips ansi. |
|
#testing of any decent size block line by line - even with max_string_length_line is slower than ta::detect anyway. |
|
if {[punk::ansi::ta::detect $b]} { |
|
lappend fordata "v($idx)" [punk::lib::lines_as_list -ansireplays 1 -ansiresets $ansiresets -- $b] |
|
} else { |
|
#each block is being rendered into its own empty column - we don't need resets if it has no ansi, even if blocks to left and right do have ansi |
|
lappend fordata "v($idx)" [split $b \n] |
|
} |
|
lappend colindices $idx |
|
incr idx |
|
} |
|
set outlines [list] |
|
#set colindices [lsort -integer -increasing [array names c]] |
|
foreach {*}$fordata { |
|
set row "" |
|
foreach colidx $colindices { |
|
#we know we have a single line, no ansi in underlay and no overflow required - so we can use renderline directly |
|
#append row [overtype::renderline -width $w($colidx) -insert_mode 0 -info 0 $c($colidx) $v($colidx)] |
|
append row [textblock::pad $v($colidx) -width $w($colidx) -which right -padchar " "] |
|
} |
|
lappend outlines $row |
|
} |
|
return [::join $outlines \n] |
|
} |
|
|
|
proc ::textblock::trim {block} { |
|
error "textblock::trim unimplemented" |
|
set trimlines "" |
|
} |
|
|
|
#pipealias ::textblock::join_right .= {list $lhs [string repeat " " [width $lhs]] $rhs [string repeat " " [width $rhs]]} {| |
|
# /2,col1/1,col2/3 |
|
# >} .=> 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 <lhs/0,rhs/1| |
|
|
|
proc example {} { |
|
set pleft [>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 <value> 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|<dict hl (c) vl (c) tlc (c) trc (c) blc (c) brc (c)>? ?-title <ansitext>? ?-subtitle <ansitext>? ?-width <columns>? ?-ansiborder <ansi_sgr>? ?-boxlimits hl|hlt|hlb|vl|vll|vlr|tlc|blc|brc? ?-joins left|right|up|down? <contents>" |
|
} |
|
#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::<cmd> |
|
# -- --- --- --- --- --- |
|
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 |