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.
8520 lines
434 KiB
8520 lines
434 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 |
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
# doctools header |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
#*** !doctools |
|
#[manpage_begin punkshell_module_textblock 0 999999.0a1.0] |
|
#[copyright "2024"] |
|
#[titledesc {punk textblock functions}] [comment {-- Name section and table of contents description --}] |
|
#[moddesc {punk textblock}] [comment {-- Description at end of page heading --}] |
|
#[require textblock] |
|
#[keywords module ansi text layout colour table frame console terminal] |
|
#[description] |
|
#[para] Ansi-aware terminal textblock manipulation |
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
|
|
#*** !doctools |
|
#[section Overview] |
|
#[para] overview of textblock |
|
#[subsection Concepts] |
|
#[para] |
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
## Requirements |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
|
|
#*** !doctools |
|
#[subsection dependencies] |
|
#[para] packages used by textblock |
|
#[list_begin itemized] |
|
|
|
#*** !doctools |
|
#[item] [package {Tcl 8.6-}] |
|
#[item] [package {punk::args}] |
|
#[item] [package {punk::char}] |
|
#[item] [package {punk::ansi}] |
|
#[item] [package {punk::lib}] |
|
#[item] [package {overtype}] |
|
#[item] [package {term::ansi::code::macros}] |
|
#[item] [package {textutil}] |
|
|
|
## Requirements |
|
package require Tcl 8.6- |
|
package require punk::args |
|
package require punk::char |
|
package require punk::ansi |
|
package require punk::lib |
|
catch {package require patternpunk} |
|
package require overtype |
|
|
|
#safebase interps as at 2024-08 can't access deeper paths - even though they are below the supposed safe list. |
|
if {[catch { |
|
package require term::ansi::code::macros ;#required for frame if old ansi g0 used - review - make package optional? |
|
} errM]} { |
|
#catch this too in case stderr not available |
|
catch { |
|
puts stderr "textblock package failed to load term::ansi::code::macros with error: $errM" |
|
} |
|
} |
|
package require textutil |
|
|
|
|
|
#*** !doctools |
|
#[list_end] |
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
|
|
#*** !doctools |
|
#[section API] |
|
|
|
|
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
|
|
tcl::namespace::eval textblock { |
|
#review - what about ansi off in punk::console? |
|
tcl::namespace::import ::punk::ansi::a ::punk::ansi::a+ |
|
tcl::namespace::export block frame frame_cache framedef frametypes gcross height width widthtopline join join_basic list_as_table pad testblock |
|
|
|
#NOTE sha1, although computationally more intensive, tends to be faster than md5 on modern cpus |
|
#(more likely to be optimised for modern cpu features?) |
|
#(This speed improvement may not apply for short strings) |
|
|
|
variable use_hash ;#framecache |
|
set use_hash none ;#slightly faster but uglier layout for viewing frame_cache display |
|
#if {![catch {package require sha1}]} { |
|
# set use_hash sha1 |
|
#} elseif {![catch {package require md5}]} { |
|
# set use_hash md5 |
|
#} else { |
|
# set use_hash none |
|
#} |
|
|
|
proc use_hash {args} { |
|
set choices [list none] |
|
set unavailable [list] |
|
set pkgs [package names] |
|
if {"md5" in $pkgs} { |
|
lappend choices md5 |
|
} else { |
|
lappend unavailable md5 |
|
} |
|
if {"sha1" in $pkgs} { |
|
lappend choices sha1 |
|
} else { |
|
lappend unavailable sha1 |
|
} |
|
set choicemsg "" |
|
if {[llength $unavailable]} { |
|
set choicemsg " (unavailable packages: $unavailable)" |
|
} |
|
set argd [punk::args::get_dict [tstr -return string { |
|
@id -id ::textblock::use_hash |
|
@cmd -name "textblock::use_hash" -help\ |
|
"Hashing algorithm to use for framecache lookup. |
|
'none' may be slightly faster but less compact |
|
when viewing textblock::framecache" |
|
@values -min 0 -max 1 |
|
hash_algorithm -choices {${$choices}} -optional 1 -help\ |
|
"algorithm choice ${$choicemsg}" |
|
}] $args] |
|
variable use_hash |
|
if {![dict exists $argd received hash_algorithm]} { |
|
return $use_hash |
|
} |
|
set use_hash [dict get $argd values hash_algorithm] |
|
} |
|
tcl::namespace::eval class { |
|
variable opts_table_defaults |
|
set opts_table_defaults [tcl::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 ""\ |
|
] |
|
variable opts_column_defaults |
|
set opts_column_defaults [tcl::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 |
|
|
|
|
|
|
|
#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 [tcl::dict::create\ |
|
topleft [struct::set intersect $C [concat $tops $lefts]]\ |
|
topinner [struct::set intersect $C $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 [tcl::dict::create\ |
|
topleft [struct::set intersect $C [list {*}$tops {*}$lefts]]\ |
|
topinner [struct::set intersect $C $tops]\ |
|
topright [struct::set intersect $O [list {*}$tops {*}$rights]]\ |
|
topsolo [struct::set intersect $O [list {*}$tops {*}$lefts {*}$rights]]\ |
|
middleleft [struct::set intersect $L $lefts]\ |
|
middleinner [list]\ |
|
middleright [struct::set intersect $U $rights]\ |
|
middlesolo [struct::set intersect $U [list {*}$lefts {*}$rights]]\ |
|
bottomleft [struct::set intersect $L $lefts]\ |
|
bottominner [list]\ |
|
bottomright [struct::set intersect $U $rights]\ |
|
bottomsolo [struct::set intersect $U [list {*}$lefts {*}$rights]]\ |
|
onlyleft [struct::set intersect $C [list {*}$tops {*}$lefts]]\ |
|
onlyinner [struct::set intersect $C $tops]\ |
|
onlyright [struct::set intersect $O [list {*}$tops {*}$rights]]\ |
|
onlysolo [struct::set intersect $O [list {*}$tops {*}$lefts {*}$rights]]\ |
|
] |
|
variable table_hseps |
|
set table_hseps [tcl::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 [tcl::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]\ |
|
] |
|
|
|
#ensembles seem to be not compiled in safe interp |
|
#https://core.tcl-lang.org/tcl/tktview/1095bf7f75 |
|
#as we want textblock to be usable in safe interps - use tcl::dict::for as a partial workaround |
|
#This at least means the script argument, especially switch statements can get compiled. |
|
#It does however affect all ensemble commands - so even 'dict get/set' etc won't be as efficient in a safe interp. |
|
|
|
#e.g $t configure -framemap_body [table_edge_map " "] |
|
|
|
# -- --- --- --- --- |
|
#unused? |
|
proc table_edge_map {char} { |
|
variable table_edge_parts |
|
set map [list] |
|
tcl::dict::for {celltype parts} $table_edge_parts { |
|
set tmap [list] |
|
foreach p $parts { |
|
tcl::dict::set tmap $p $char |
|
} |
|
tcl::dict::set map $celltype $tmap |
|
} |
|
return $map |
|
} |
|
proc table_sep_map {char} { |
|
variable table_hseps |
|
set map [list] |
|
tcl::dict::for {celltype parts} $table_hseps { |
|
set tmap [list] |
|
foreach p $parts { |
|
tcl::dict::set tmap $p $char |
|
} |
|
tcl::dict::set map $celltype $tmap |
|
} |
|
return $map |
|
} |
|
proc header_edge_map {char} { |
|
variable header_edge_parts |
|
set map [list] |
|
tcl::dict::for {celltype parts} $header_edge_parts { |
|
set tmap [list] |
|
foreach p $parts { |
|
tcl::dict::set tmap $p $char |
|
} |
|
tcl::dict::set map $celltype $tmap |
|
} |
|
return $map |
|
} |
|
# -- --- --- --- --- |
|
|
|
if {[tcl::info::commands [tcl::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] |
|
|
|
#this makes new table objects a little faster when multiple opts specified as well as to configure |
|
#as tables are a heavyweight thing, but handy to nest sometimes - we'll take what we can get |
|
set topt_keys [tcl::dict::keys $::textblock::class::opts_table_defaults] |
|
set switch_keys_valid_topts [punk::lib::lmapflat v $topt_keys {list $v -}] |
|
set switch_keys_valid_topts [lrange $switch_keys_valid_topts 0 end-1] ;#remove last dash |
|
|
|
set copt_keys [tcl::dict::keys $::textblock::class::opts_column_defaults] |
|
set switch_keys_valid_copts [punk::lib::lmapflat v $copt_keys {list $v -}] |
|
set switch_keys_valid_copts [lrange $switch_keys_valid_copts 0 end-1] |
|
|
|
oo::class create [tcl::namespace::current]::table [tcl::string::map [list %topt_keys% $topt_keys %topt_switchkeys% $switch_keys_valid_topts %copt_keys% $copt_keys %copt_switchkeys% $switch_keys_valid_copts] { |
|
|
|
#*** !doctools |
|
#[enum] CLASS [class textblock::class::table] |
|
#[list_begin definitions] |
|
#[para] Create a table suitable for terminal output with various border styles. |
|
#[para] The table can contain multiline cells and ANSI colour and text style attributes. |
|
#[para] Multiple header rows can be configured. |
|
#[para] Header rows can span columns - data rows cannot. |
|
#[para] The restriction on data rows is to maintain compatibility of the data with a Tcl matrix command |
|
#[para] (see get_matrix command) |
|
#[para] Both header and data cells can have various text and blockalignments configured. |
|
# [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_headerdefs |
|
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]] |
|
#[para] TODO - document the many options |
|
|
|
set o_opts_table_defaults $::textblock::class::opts_table_defaults |
|
set o_opts_column_defaults $::textblock::class::opts_column_defaults |
|
|
|
|
|
if {[llength $args] == 1} { |
|
set args [list -title [lindex $args 0]] |
|
} |
|
if {[llength $args] %2 !=0} { |
|
error "[tcl::namespace::current]::table constructor - unexpected argument count. Require single value being title, or name value pairs" |
|
} |
|
|
|
set o_opts_table $o_opts_table_defaults |
|
set o_opts_table_effective $o_opts_table_defaults |
|
|
|
##todo - test with punk::lib::show_jump_tables - how? |
|
foreach {k v} $args { |
|
switch -- $k { |
|
%topt_switchkeys% { |
|
tcl::dict::set o_opts_table $k $v |
|
} |
|
default { |
|
error "[tcl::namespace::current]::table unrecognised option '$k'. Known values [tcl::dict::keys $o_opts_table_defaults]" |
|
} |
|
} |
|
} |
|
|
|
#foreach {k v} $args { |
|
# #todo - convert to literal switch using tcl::string::map so we don't have to define o_opts_table_defaults here. |
|
# if {$k ni [tcl::dict::keys $o_opts_table_defaults]} { |
|
# error "[tcl::namespace::current]::table unrecognised option '$k'. Known values [tcl::dict::keys $o_opts_table_defaults]" |
|
# } |
|
#} |
|
#set o_opts_table [tcl::dict::merge $o_opts_table_defaults $args] |
|
#my configure {*}[tcl::dict::merge $o_opts_table_defaults $args] |
|
|
|
set o_columndefs [tcl::dict::create] |
|
set o_columndata [tcl::dict::create] ;#we store data by column even though it is often added row by row |
|
set o_columnstates [tcl::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_headerdefs [tcl::dict::create] ;#by header-row |
|
set o_headerstates [tcl::dict::create] |
|
set o_rowdefs [tcl::dict::create] ;#user requested row data e.g -minheight -maxheight |
|
set o_rowstates [tcl::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 o_opts_header_defaults [tcl::dict::create\ |
|
-colspans {}\ |
|
-values {}\ |
|
-ansibase {}\ |
|
-ansireset "\x1b\[m"\ |
|
-minheight 1\ |
|
-maxheight ""\ |
|
] |
|
my configure {*}$o_opts_table |
|
} |
|
|
|
method width_algorithm {{alg ""}} { |
|
if {$alg eq ""} { |
|
return $o_column_width_algorithm |
|
} |
|
if {$alg ne $o_column_width_algorithm} { |
|
#invalidate cached widths |
|
set o_calculated_column_widths [list] |
|
} |
|
set o_column_width_algorithm $alg |
|
} |
|
method Get_seps {} { |
|
set requested_seps [tcl::dict::get $o_opts_table -show_seps] |
|
set requested_seps_h [tcl::dict::get $o_opts_table -show_hseps] |
|
set requested_seps_v [tcl::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 [tcl::dict::create horizontal $seps_h vertical $seps_v] |
|
} |
|
method Get_frametypes {} { |
|
set requested_ft [tcl::dict::get $o_opts_table -frametype] |
|
set requested_ft_header [tcl::dict::get $o_opts_table -frametype_header] |
|
set requested_ft_body [tcl::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 |
|
} |
|
} |
|
light_b { |
|
if {$requested_ft_header eq ""} { |
|
set ft_header heavy_b |
|
} |
|
if {$requested_ft_body eq ""} { |
|
set ft_body light_b |
|
} |
|
} |
|
light_c { |
|
if {$requested_ft_header eq ""} { |
|
set ft_header heavy_c |
|
} |
|
if {$requested_ft_body eq ""} { |
|
set ft_body light_c |
|
} |
|
} |
|
default { |
|
if {$requested_ft_header eq ""} { |
|
set ft_header $requested_ft |
|
} |
|
if {$requested_ft_body eq ""} { |
|
set ft_body $requested_ft |
|
} |
|
} |
|
} |
|
return [tcl::dict::create header $ft_header body $ft_body] |
|
} |
|
method Set_effective_framelimits {} { |
|
upvar ::textblock::class::opts_table_defaults tdefaults |
|
set default_blims [tcl::dict::get $tdefaults -framelimits_body] |
|
set default_hlims [tcl::dict::get $tdefaults -framelimits_header] |
|
set eff_blims [tcl::dict::get $o_opts_table_effective -framelimits_body] |
|
set eff_hlims [tcl::dict::get $o_opts_table_effective -framelimits_header] |
|
|
|
set requested_blims [tcl::dict::get $o_opts_table -framelimits_body] |
|
set requested_hlims [tcl::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] |
|
} |
|
} |
|
tcl::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] |
|
} |
|
} |
|
tcl::dict::set o_opts_table_effective -framelimits_header $hlims |
|
return [tcl::dict::create body $blims header $hlims] |
|
} |
|
method configure {args} { |
|
#*** !doctools |
|
#[call class::table [method configure] [arg args]] |
|
#[para] get or set various table-level properties |
|
|
|
if {![llength $args]} { |
|
return $o_opts_table |
|
} |
|
if {[llength $args] == 1} { |
|
if {[lindex $args 0] in [list %topt_keys%]} { |
|
#query single option |
|
set k [lindex $args 0] |
|
set val [tcl::dict::get $o_opts_table $k] |
|
set returndict [tcl::dict::create option $k value $val ansireset "\x1b\[m"] |
|
set infodict [tcl::dict::create] |
|
switch -- $k { |
|
-ansibase_header - -ansibase_body - -ansiborder_header - -ansiborder_body - -ansiborder_footer { |
|
tcl::dict::set infodict debug [ansistring VIEW $val] |
|
} |
|
-framemap_body - -framemap_header - -framelimits_body - -framelimits_header { |
|
tcl::dict::set returndict effective [tcl::dict::get $o_opts_table_effective $k] |
|
} |
|
} |
|
tcl::dict::set returndict info $infodict |
|
return $returndict |
|
#return [tcl::dict::create option $k value $val ansireset "\x1b\[m" info $infodict] |
|
} else { |
|
error "textblock::table configure - unrecognised option '[lindex $args 0]'. Known values [tcl::dict::keys $o_opts_table_defaults]" |
|
} |
|
} |
|
if {[llength $args] %2 != 0} { |
|
error "[tcl::namespace::current]::table configure - unexpected argument count. Require name value pairs" |
|
} |
|
foreach {k v} $args { |
|
switch -- $k { |
|
%topt_switchkeys% {} |
|
default { |
|
error "[tcl::namespace::current]::table configure - unrecognised option '$k'. Known values [tcl::dict::keys $o_opts_table_defaults]" |
|
} |
|
} |
|
#if {$k ni [tcl::dict::keys $o_opts_table_defaults]} { |
|
# error "[tcl::namespace::current]::table configure - unrecognised option '$k'. Known values [tcl::dict::keys $o_opts_table_defaults]" |
|
#} |
|
} |
|
set checked_opts [list] |
|
foreach {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 [tcl::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 [tcl::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 { |
|
#safe jumptable test |
|
#dict for {subk subv} $v {} |
|
foreach {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}" |
|
} |
|
} |
|
#safe jumptable test |
|
#dict for {seg subst} $subv {} |
|
foreach {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 {![tcl::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 {![tcl::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 {![tcl::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 [tcl::dict::merge $o_opts_table $checked_opts] |
|
foreach {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"} { |
|
tcl::dict::set o_opts_table $k default |
|
} else { |
|
if {[tcl::dict::get $o_opts_table $k] eq "default"} { |
|
tcl::dict::set o_opts_table $k $v |
|
} else { |
|
tcl::dict::set o_opts_table $k [tcl::dict::merge [tcl::dict::get $o_opts_table $k] $v] |
|
} |
|
} |
|
} |
|
-title { |
|
set twidth [punk::ansi::printing_length $v] |
|
if {[my width] < [expr {$twidth+2}]} { |
|
set o_calculated_column_widths [list] |
|
tcl::dict::set o_opts_table -minwidth [expr {$twidth+2}] |
|
} |
|
tcl::dict::set o_opts_table -title $v |
|
} |
|
default { |
|
tcl::dict::set o_opts_table $k $v |
|
} |
|
} |
|
} |
|
#use values from checked_opts for the effective opts |
|
#safe jumptable test |
|
#dict for {k v} $checked_opts {} |
|
#foreach {k v} $checked_opts {} |
|
tcl::dict::for {k v} $checked_opts { |
|
switch -- $k { |
|
-framemap_body - -framemap_header { |
|
set existing [tcl::dict::get $o_opts_table_effective $k] |
|
#set updated $existing |
|
#dict for {subk subv} $v { |
|
# tcl::dict::set updated $subk $subv |
|
#} |
|
#tcl::dict::set o_opts_table_effective $k $updated |
|
tcl::dict::set o_opts_table_effective $k [tcl::dict::merge $existing $v] |
|
} |
|
-framelimits_body - -framelimits_header { |
|
#my Set_effective_framelimits |
|
tcl::dict::set o_opts_table_effective $k $v |
|
} |
|
default { |
|
tcl::dict::set o_opts_table_effective $k $v |
|
} |
|
} |
|
} |
|
#ansireset exception |
|
tcl::dict::set o_opts_table -ansireset [tcl::dict::get $o_opts_table_effective -ansireset] |
|
return $o_opts_table |
|
} |
|
|
|
#integrate with struct::matrix - allows ::m format 2string $table |
|
method printmatrix {matrix} { |
|
#*** !doctools |
|
#[call class::table [method printmatrix] [arg matrix]] |
|
#[para] clear all table rows and print a matrix into the table |
|
#[para] The rowxcolumn structure must match |
|
|
|
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 ""}} { |
|
#*** !doctools |
|
#[call class::table [method as_matrix] [arg ?cmd?]] |
|
#[para] return a struct::matrix command representing the data portion of the table. |
|
|
|
if {$cmd eq ""} { |
|
set m [struct::matrix] |
|
} else { |
|
set m [struct::matrix $cmd] |
|
} |
|
$m add columns [tcl::dict::size $o_columndata] |
|
$m add rows [tcl::dict::size $o_rowdefs] |
|
tcl::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]] |
|
|
|
|
|
if {[llength $args] %2 != 0} { |
|
error "[tcl::namespace::current]::table::add_column unexpected argument count. Require name value pairs. Known options: %copt_keys%" |
|
} |
|
set opts $o_opts_column_defaults |
|
foreach {k v} $args { |
|
switch -- $k { |
|
%copt_switchkeys% { |
|
tcl::dict::set opts $k $v |
|
} |
|
default { |
|
error "[tcl::namespace::current]::table::add_column unknown option '$k'. Known options: %copt_keys%" |
|
} |
|
} |
|
} |
|
set colcount [tcl::dict::size $o_columndefs] |
|
|
|
|
|
tcl::dict::set o_columndata $colcount [list] |
|
#tcl::dict::set o_columndefs $colcount $defaults ;#ensure record exists |
|
tcl::dict::set o_columndefs $colcount $o_opts_column_defaults ;#ensure record exists |
|
|
|
|
|
tcl::dict::set o_columnstates $colcount [tcl::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 |
|
tcl::dict::unset o_columndata $colcount |
|
tcl::dict::unset o_columndefs $colcount |
|
tcl::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 [tcl::dict::get $opts -defaultvalue] |
|
set width [textblock::width $dval] |
|
tcl::dict::set o_columndata $colcount [lrepeat $numrows $dval] |
|
tcl::dict::set o_columnstates $colcount maxwidthbodyseen $width |
|
tcl::dict::set o_columnstates $colcount minwidthbodyseen $width |
|
} |
|
return $colcount |
|
} |
|
method column_count {} { |
|
#*** !doctools |
|
#[call class::table [method column_count]] |
|
#[para] return the number of columns |
|
return [tcl::dict::size $o_columndefs] |
|
} |
|
method configure_column {index_expression args} { |
|
#*** !doctools |
|
#[call class::table [method configure_column] [arg index_expression] [arg args]] |
|
#[para] - undocumented |
|
|
|
set cidx [lindex [tcl::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 [tcl::dict::get $o_columndefs $cidx] |
|
} else { |
|
if {[llength $args] == 1} { |
|
if {[lindex $args 0] in [list %copt_keys%]} { |
|
#query single option |
|
set k [lindex $args 0] |
|
set val [tcl::dict::get $o_columndefs $cidx $k] |
|
set returndict [tcl::dict::create option $k value $val ansireset "\x1b\[m"] |
|
set infodict [tcl::dict::create] |
|
switch -- $k { |
|
-ansibase { |
|
tcl::dict::set infodict debug [ansistring VIEW $val] |
|
} |
|
} |
|
tcl::dict::set returndict info $infodict |
|
return $returndict |
|
} else { |
|
error "textblock::table configure_column - unrecognised option '[lindex $args 0]'. Known values %copt_keys%" |
|
} |
|
} |
|
if {[llength $args] %2 != 0} { |
|
error "textblock::table configure_column unexpected argument count. Require name value pairs. Known options: %copt_keys%" |
|
} |
|
foreach {k v} $args { |
|
switch -- $k { |
|
%copt_switchkeys% {} |
|
default { |
|
error "[tcl::namespace::current]::table configure_column unknown option '$k'. Known options: %copt_keys%" |
|
} |
|
} |
|
} |
|
set checked_opts [tcl::dict::get $o_columndefs $cidx] ;#copy of current state |
|
|
|
set hstates $o_headerstates ;#operate on a copy |
|
set colstate [tcl::dict::get $o_columnstates $cidx] |
|
set args_got_headers 0 |
|
set args_got_header_colspans 0 |
|
foreach {k v} $args { |
|
switch -- $k { |
|
-headers { |
|
set args_got_headers 1 |
|
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} { |
|
tcl::dict::set hstates $i maxheightseen $this_header_height |
|
} else { |
|
tcl::dict::set hstates $i maxheightseen $currentmax |
|
} |
|
if {$this_header_width >= $maxseen} { |
|
set maxseen $this_header_width |
|
} |
|
#if {$this_header_width > [tcl::dict::get $colstate maxwidthheaderseen]} { |
|
# tcl::dict::set colstate maxwidthheaderseen $this_header_width |
|
#} |
|
incr i |
|
} |
|
tcl::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 |
|
tcl::dict::set checked_opts $k $v |
|
} |
|
-header_colspans { |
|
set args_got_header_colspans 1 |
|
#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 'any' represents span all up to the next non-zero defined colspan. |
|
set cspans [my header_colspans] |
|
set h 0 |
|
if {[llength $v] > [tcl::dict::size $cspans]} { |
|
error "configure_column $cidx -header_colspans. Only [tcl::dict::size $cspans] headers exist. Too many values supplied" |
|
} |
|
foreach s $v { |
|
if {$cidx == 0} { |
|
if {[tcl::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 'any' or a positive integer" |
|
} |
|
} else { |
|
if {$s ne "any" && $s ne ""} { |
|
error "configure_column $cidx -header_colspans unrecognised value '$s' for header '$h' - must be a positive integer or the keyword 'any'" |
|
} |
|
} |
|
} else { |
|
#if {![tcl::string::is integer -strict $s]} { |
|
# if {$s ne "any" && $s ne ""} { |
|
# error "configure_column $cidx -header_colspans unrecognised value '$s' for header '$h' - must be a positive integer or the keyword 'any'" |
|
# } |
|
#} else { |
|
set header_spans [tcl::dict::get $cspans $h] |
|
set remaining [lindex $header_spans 0] |
|
if {$remaining ne "any"} { |
|
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 "any"} { |
|
set remaining "any" |
|
} else { |
|
if {$remaining eq "any"} { |
|
if {$span ne "0"} { |
|
#a previous column has ended the 'any' 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 "any"} { |
|
#any int >0 ok - what about 'any' immediately following any? |
|
} 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 'any'" |
|
} |
|
} |
|
} |
|
#} |
|
} |
|
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 |
|
tcl::dict::set checked_opts $k $v |
|
} |
|
-minwidth { |
|
set o_calculated_column_widths [list] ;#invalidate cached column widths - a recalc will be forced when needed |
|
tcl::dict::set checked_opts $k $v |
|
} |
|
-maxwidth { |
|
set o_calculated_column_widths [list] ;#invalidate cached column widths - a recalc will be forced when needed |
|
tcl::dict::set 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] |
|
tcl::dict::set checked_opts $k $col_ansibase |
|
} |
|
-ansireset { |
|
if {$v eq "\uFFEF"} { |
|
tcl::dict::set 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 { |
|
tcl::dict::set checked_opts $k $v |
|
} |
|
centre - centre { |
|
tcl::dict::set checked_opts $k centre |
|
} |
|
} |
|
} |
|
default { |
|
tcl::dict::set checked_opts $k $v |
|
} |
|
} |
|
} |
|
#args checked - ok to update headerstates, headerdefs and columndefs and columnstates |
|
tcl::dict::set o_columndefs $cidx $checked_opts |
|
set o_headerstates $hstates |
|
dict for {hidx hstate} $hstates { |
|
#configure_header |
|
if {![dict exists $o_headerdefs $hidx]} { |
|
#remove calculated members -values -colspans |
|
set hdefaults [dict remove $o_opts_header_defaults -values -colspans] |
|
dict set o_headerdefs $hidx $hdefaults |
|
} |
|
} |
|
|
|
tcl::dict::set o_columnstates $cidx $colstate |
|
|
|
if {$args_got_headers} { |
|
#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] |
|
tcl::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 { |
|
tcl::dict::unset o_headerstates $zidx |
|
} |
|
} |
|
if {$args_got_headers || $args_got_header_colspans} { |
|
#check and adjust header_colspans for all columns |
|
|
|
} |
|
|
|
return [tcl::dict::get $o_columndefs $cidx] |
|
} |
|
} |
|
|
|
method header_count {} { |
|
#*** !doctools |
|
#[call class::table [method header_count]] |
|
#[para] return the number of header rows |
|
return [tcl::dict::size $o_headerstates] |
|
} |
|
method header_count_calc {} { |
|
set max_headers 0 |
|
tcl::dict::for {k cdef} $o_columndefs { |
|
set num_headers [llength [tcl::dict::get $cdef -headers]] |
|
set max_headers [expr {max($max_headers,$num_headers)}] |
|
} |
|
return $max_headers |
|
} |
|
method header_height {header_index} { |
|
#*** !doctools |
|
#[call class::table [method header_height] [arg header_index]] |
|
#[para] return the height of a header as the number of content-lines |
|
|
|
set idx [lindex [tcl::dict::keys $o_headerstates $header_index]] |
|
return [tcl::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 [tcl::dict::keys $o_columndefs] $exclude_column] |
|
} |
|
tcl::dict::for {cidx cdef} $o_columndefs { |
|
if {$exclude_colidx == $cidx} { |
|
continue |
|
} |
|
set headerlist [tcl::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 {any 0 0 0} 1 {1 1 1 1} 2 {2 0 1 1} 3 {3 0 0 1} |
|
# |
|
method header_colspans {} { |
|
#*** !doctools |
|
#[call class::table [method header_colspans]] |
|
#[para] Show the colspans configured for all headers |
|
|
|
#set num_headers [my header_count_calc] |
|
set num_headers [my header_count] |
|
set colspans_by_header [tcl::dict::create] |
|
tcl::dict::for {cidx cdef} $o_columndefs { |
|
set headerlist [tcl::dict::get $cdef -headers] |
|
set colspans_for_column [tcl::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 "any"} { |
|
if {$spanremaining eq ""} { |
|
set spanremaining 1 |
|
} |
|
incr spanremaining -1 |
|
} |
|
foreach s $headerspans { |
|
if {$s eq "any"} { |
|
set spanremaining "any" |
|
} elseif {$s == 0} { |
|
if {$spanremaining ne "any"} { |
|
incr spanremaining -1 |
|
} |
|
} else { |
|
set spanremaining [expr {$s - 1}] |
|
} |
|
incr i |
|
} |
|
if {$defined_span eq ""} { |
|
if {$spanremaining eq "0"} { |
|
lappend headerspans 1 |
|
} else { |
|
#"any" or an integer |
|
lappend headerspans 0 |
|
} |
|
} else { |
|
lappend headerspans $defined_span |
|
} |
|
tcl::dict::set colspans_by_header $h $headerspans |
|
} |
|
} |
|
return $colspans_by_header |
|
} |
|
|
|
#e.g |
|
# 0 {any 0 0 0} 1 {1 1 1 1} 2 {2 0 any 1} 3 {any 0 0 1} |
|
#convert to |
|
# 0 {4 0 0 0} 1 {1 1 1 1} 2 {2 0 1 1} 3 {3 0 0 1} |
|
method header_colspans_numeric {} { |
|
set hcolspans [my header_colspans] |
|
if {![tcl::dict::size $hcolspans]} { |
|
return |
|
} |
|
set numcols [llength [tcl::dict::get $hcolspans 0]] ;#assert: all are the same |
|
tcl::dict::for {h spans} $hcolspans { |
|
set c 0 ;#column index |
|
foreach s $spans { |
|
if {$s eq "any"} { |
|
set spanlen 1 |
|
for {set i [expr {$c+1}]} {$i < $numcols} {incr i} { |
|
#next 'any' or non-zero ends an 'any' span |
|
if {[lindex $spans $i] ne "0"} { |
|
break |
|
} |
|
incr spanlen |
|
} |
|
#overwrite the 'any' with it's actual span |
|
set modified_spans [dict get $hcolspans $h] |
|
lset modified_spans $c $spanlen |
|
dict set hcolspans $h $modified_spans |
|
} |
|
incr c |
|
} |
|
} |
|
return $hcolspans |
|
} |
|
|
|
method configure_header {index_expression args} { |
|
#*** !doctools |
|
#[call class::table [method configure_header]] |
|
#[para] - configure header row-wise |
|
|
|
#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 [tcl::dict::keys $o_headerstates] $index_expression] |
|
if {$hidx eq ""} { |
|
error "textblock::table::configure_header - no header row defined at index '$index_expression'." |
|
} |
|
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 [tcl::dict::create] |
|
tcl::dict::set result -colspans [tcl::dict::get $colspans_by_header $hidx] |
|
set header_row_items [list] |
|
tcl::dict::for {cidx cdef} $o_columndefs { |
|
set colheaders [tcl::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. |
|
} |
|
tcl::dict::set result -values $header_row_items |
|
|
|
#review - ensure always a headerdef record for each header? |
|
if {[tcl::dict::exists $o_headerdefs $hidx]} { |
|
set result [tcl::dict::merge $result [tcl::dict::get $o_headerdefs $hidx]] |
|
} else { |
|
#warn for now |
|
puts stderr "no headerdef record for header $hidx" |
|
} |
|
return $result |
|
} |
|
if {[llength $args] == 1} { |
|
if {[lindex $args 0] in [tcl::dict::keys $o_opts_header_defaults]} { |
|
#query single option |
|
set k [lindex $args 0] |
|
#set val [tcl::dict::get $o_rowdefs $ridx $k] |
|
|
|
set infodict [tcl::dict::create] |
|
#todo |
|
# -blockalignments and -textalignments lists |
|
# must match number of values if not empty? - e.g -blockalignments {left right "" centre left ""} |
|
#if there is a value it overrides alignments specified on the column |
|
switch -- $k { |
|
-values { |
|
set header_row_items [list] |
|
tcl::dict::for {cidx cdef} $o_columndefs { |
|
set colheaders [tcl::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 [tcl::dict::create option $k value $val ansireset "\x1b\[m"] |
|
} |
|
-colspans { |
|
set colspans_by_header [my header_colspans] |
|
set result [tcl::dict::create] |
|
set val [tcl::dict::get $colspans_by_header $hidx] |
|
#ansireset not required |
|
set returndict [tcl::dict::create option $k value $val] |
|
} |
|
-ansibase { |
|
set val ??? |
|
set returndict [tcl::dict::create option $k value $val ansireset "\x1b\[m"] |
|
tcl::dict::set infodict debug [ansistring VIEW $val] |
|
} |
|
} |
|
tcl::dict::set returndict info $infodict |
|
return $returndict |
|
#return [tcl::dict::create option $k value $val ansireset "\x1b\[m" info $infodict] |
|
} else { |
|
error "textblock::table configure_header - unrecognised option '[lindex $args 0]'. Known values [tcl::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: [tcl::dict::keys $o_opts_header_defaults]" |
|
} |
|
foreach {k v} $args { |
|
if {$k ni [tcl::dict::keys $o_opts_header_defaults]} { |
|
error "[tcl::namespace::current]::table configure_row unknown option '$k'. Known options: [tcl::dict::keys $o_opts_header_defaults]" |
|
} |
|
} |
|
|
|
set checked_opts [list] |
|
#safe jumptable test |
|
#dict for {k v} $args {} |
|
foreach {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 $header_ansibase_items] |
|
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] > [tcl::dict::size $o_columndefs]} { |
|
error "textblock::table::configure_header -values length ([llength $v]) is longer than number of columns ([tcl::dict::size $o_columndefs])" |
|
} |
|
lappend checked_opts $k $v |
|
} |
|
-colspans { |
|
set numcols [tcl::dict::size $o_columndefs] |
|
if {[llength $v] > $numcols} { |
|
error "textblock::table::configure_header -colspans length ([llength $v]) is longer than number of columns ([tcl::dict::size $o_columndefs])" |
|
} |
|
if {[llength $v] < $numcols} { |
|
puts stderr "textblock::table::configure_header warning - only [llength $v] spans specified for [tcl::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 "any"} { |
|
set first_is_ok 1 |
|
} elseif {[tcl::string::is integer -strict $firstspan] && $firstspan > 0 && $firstspan <= $numcols} { |
|
set first_is_ok 1 |
|
} |
|
if {!$first_is_ok} { |
|
error "textblock::table::configure_header -colspans first value '$firstspan' must be integer > 0 & <= $numcols or the string \"any\"" |
|
} |
|
#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 "any"} { |
|
incr remaining -1 |
|
} |
|
set spanview $v |
|
set sidx 1 |
|
#because we allow 'any' - be careful when doing < or > comparisons - as we are mixing integer and string comparisons if we don't test for 'any' first |
|
foreach span [lrange $v 1 end] { |
|
if {$remaining eq "any"} { |
|
if {$span eq "any"} { |
|
set remaining "any" |
|
} elseif {$span > 0} { |
|
#ok to reset to higher val immediately or after an any and any number of following zeros |
|
if {$span > ($numcols - $sidx)} { |
|
lset spanview $sidx [a+ web-red]$span[a] |
|
error "textblock::table::configure_header -colspans sequence incorrect at span '$span'. Require span <= [expr {$numcols-$sidx}] or \"any\".[a] $spanview" |
|
} |
|
set remaining $span |
|
incr remaining -1 |
|
} else { |
|
#zero following an any - leave remaining as any |
|
} |
|
} else { |
|
if {$span eq "0"} { |
|
if {$remaining eq "0"} { |
|
lset spanview $sidx [a+ web-red]$span[a] |
|
error "textblock::table::configure_header -colspans sequence incorrect at span '$span' remaining is $remaining. Require positive or \"any\" value.[a] $spanview" |
|
} else { |
|
incr remaining -1 |
|
} |
|
} else { |
|
if {$remaining eq "0"} { |
|
#ok for new span value of any or > 0 |
|
if {$span ne "any" && $span > ($numcols - $sidx)} { |
|
lset spanview $sidx [a+ web-red]$span[a] |
|
error "textblock::table::configure_header -colspans sequence incorrect at span '$span'. Require span <= [expr {$numcols-$sidx}] or \"any\".[a] $spanview" |
|
} |
|
set remaining $span |
|
if {$remaining ne "any"} { |
|
incr remaining -1 |
|
} |
|
} else { |
|
lset spanview $sidx [a+ web-red]$span[a] |
|
error "textblock::table::configure_header -colspans sequence incorrect at span '$span' remaining is $remaining. Require zero value span.[a] $spanview" |
|
} |
|
} |
|
} |
|
incr sidx |
|
} |
|
} |
|
#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 |
|
#safe jumptable test |
|
#dict for {k v} $checked_opts {} |
|
#foreach {k v} $checked_opts {} |
|
|
|
# headerdefs excludes -values and -colspans |
|
set update_hdefs [tcl::dict::get $o_headerdefs $hidx] |
|
|
|
tcl::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 thiscol_headers_vertical [tcl::dict::get $o_columndefs $c -headers] |
|
set missing [expr {($hidx +1) - [llength $thiscol_headers_vertical]}] |
|
if {$missing > 0} { |
|
lappend thiscol_headers_vertical {*}[lrepeat $missing ""] |
|
} |
|
lset thiscol_headers_vertical $hidx $hval |
|
tcl::dict::set o_columndefs $c -headers $thiscol_headers_vertical |
|
#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 $thiscol_headers_vertical { |
|
lassign [textblock::size $hdr] _w this_header_width _h this_header_height |
|
set maxheightseen [punk::lib::dict_getdef $o_headerstates $i maxheightseen 0] |
|
if {$this_header_height >= $maxheightseen} { |
|
tcl::dict::set o_headerstates $i maxheightseen $this_header_height |
|
} else { |
|
tcl::dict::set o_headerstates $i maxheightseen $maxheightseen |
|
} |
|
if {$this_header_width >= $maxwidthseen} { |
|
set maxwidthseen $this_header_width |
|
} |
|
incr i |
|
} |
|
tcl::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 [tcl::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 [tcl::dict::get $colspans_by_header $h] |
|
set requiredval [lindex $cspans $c] |
|
lappend spanlist $requiredval |
|
} |
|
tcl::dict::set o_columndefs $c -header_colspans $spanlist |
|
|
|
set colspans [tcl::dict::get $o_columndefs $c -header_colspans] |
|
} |
|
|
|
lset colspans $hidx $span |
|
tcl::dict::set o_columndefs $c -header_colspans $colspans |
|
incr c |
|
} |
|
} |
|
default { |
|
dict set update_hdefs $k $v |
|
} |
|
} |
|
} |
|
set opt_minh [tcl::dict::get $update_hdefs -minheight] |
|
set opt_maxh [tcl::dict::get $update_hdefs -maxheight] |
|
|
|
#todo - allow zero values to hide/collapse |
|
# - see also configure_row |
|
if {![tcl::string::is integer $opt_minh] || ($opt_maxh ne "" && ![tcl::string::is integer -strict $opt_maxh])} { |
|
error "[tcl::namespace::current]::table::configure_header error -minheight '$opt_minh' and -maxheight '$opt_maxh' must be integers greater than or equal to 1 (for now)" |
|
} |
|
if {$opt_minh < 1 || ($opt_maxh ne "" && $opt_maxh < 1)} { |
|
error "[tcl::namespace::current]::table::configure_header error -minheight '$opt_minh' and -maxheight '$opt_maxh' must both be 1 or greater (for now)" |
|
} |
|
if {$opt_maxh ne "" && $opt_maxh < $opt_minh} { |
|
error "[tcl::namespace::current]::table::configure_header error -maxheight '$opt_maxh' must greater than -minheight '$opt_minh'" |
|
} |
|
|
|
#set o_headerstate $hidx -minheight? -maxheight? ??? |
|
tcl::dict::set o_headerdefs $hidx $update_hdefs |
|
} |
|
|
|
method add_row {valuelist args} { |
|
#*** !doctools |
|
#[call class::table [method add_row]\ |
|
# [arg valuelist]\ |
|
# [opt "[option -minheight] [arg int_minheight]"]\ |
|
# [opt "[option -maxheight] [arg int_maxheight]"]\ |
|
# [opt "[option -ansibase] [arg ansicode]"]\ |
|
#] |
|
if {[tcl::dict::size $o_columndefs] > 0 && ([llength $valuelist] && [llength $valuelist] != [tcl::dict::size $o_columndefs])} { |
|
set msg "" |
|
append msg "add_row - invalid number ([llength $valuelist]) of values in row - Must match existing column count: [tcl::dict::size $o_columndefs]" \n |
|
append msg "rowdata: $valuelist" |
|
error $msg |
|
} |
|
if {[tcl::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 [tcl::dict::create\ |
|
-minheight 1\ |
|
-maxheight ""\ |
|
-ansibase ""\ |
|
-ansireset "\uFFEF"\ |
|
] |
|
set o_opts_row_defaults $defaults |
|
|
|
if {[llength $args] %2 !=0} { |
|
error "[tcl::namespace::current]::table::add_row unexpected argument count. Require name value pairs. Known options: [tcl::dict::keys $defaults]" |
|
} |
|
#safe jumptable test |
|
#dict for {k v} $args {} |
|
foreach {k v} $args { |
|
switch -- $k { |
|
-minheight - -maxheight - -ansibase - -ansireset {} |
|
default { |
|
error "Invalid option '$k' Known options: [tcl::dict::keys $defaults] (-ansireset is read-only)" |
|
} |
|
} |
|
} |
|
set opts [tcl::dict::merge $defaults $args] |
|
|
|
set auto_columns 0 |
|
if {[tcl::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]} { |
|
tcl::dict::for {k coldef} $o_columndefs { |
|
lappend valuelist [tcl::dict::get $coldef -defaultvalue] |
|
} |
|
} |
|
} |
|
set rowcount [tcl::dict::size $o_rowdefs] |
|
tcl::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 |
|
tcl::dict::unset o_rowdefs $rowcount |
|
#remove auto_columns |
|
if {$auto_columns} { |
|
set o_columndata [tcl::dict::create] |
|
set o_columndefs [tcl::dict::create] |
|
set o_columnstate [tcl::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 [tcl::dict::get $o_columnstates $c maxwidthbodyseen] |
|
set prev_minwidth [tcl::dict::get $o_columnstates $c minwidthbodyseen] |
|
|
|
tcl::dict::lappend o_columndata $c $v |
|
lassign [textblock::size_as_list $v] valwidth valheight |
|
if {$valheight > $max_height_seen} { |
|
set max_height_seen $valheight |
|
} |
|
if {$valwidth > [tcl::dict::get $o_columnstates $c maxwidthbodyseen]} { |
|
tcl::dict::set o_columnstates $c maxwidthbodyseen $valwidth |
|
} |
|
if {$valwidth < [tcl::dict::get $o_columnstates $c minwidthbodyseen]} { |
|
tcl::dict::set o_columnstates $c minwidthbodyseen $valwidth |
|
} |
|
|
|
if {[tcl::dict::get $o_columnstates $c maxwidthbodyseen] > $prev_maxwidth || [tcl::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 [tcl::dict::get $o_rowdefs $rowcount -maxheight] |
|
if {$opt_maxh ne ""} { |
|
tcl::dict::set o_rowstates $rowcount -maxheight [expr {min($opt_maxh,$max_height_seen)}] |
|
} else { |
|
tcl::dict::set o_rowstates $rowcount -maxheight $max_height_seen |
|
} |
|
|
|
return $rowcount |
|
} |
|
method configure_row {index_expression args} { |
|
#*** !doctools |
|
#[call class::table [method configure_row]\ |
|
# [arg index_expression]\ |
|
# [opt "[option -minheight] [arg int_minheight]"]\ |
|
# [opt "[option -maxheight] [arg int_maxheight]"]\ |
|
# [opt "[option -ansibase] [arg ansicode]"]\ |
|
#] |
|
set ridx [lindex [tcl::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 [tcl::dict::get $o_rowdefs $ridx] |
|
} |
|
if {[llength $args] == 1} { |
|
if {[lindex $args 0] in [tcl::dict::keys $o_opts_row_defaults]} { |
|
#query single option |
|
set k [lindex $args 0] |
|
set val [tcl::dict::get $o_rowdefs $ridx $k] |
|
set returndict [tcl::dict::create option $k value $val ansireset "\x1b\[m"] |
|
set infodict [tcl::dict::create] |
|
switch -- $k { |
|
-ansibase { |
|
tcl::dict::set infodict debug [ansistring VIEW $val] |
|
} |
|
} |
|
tcl::dict::set returndict info $infodict |
|
return $returndict |
|
#return [tcl::dict::create option $k value $val ansireset "\x1b\[m" info $infodict] |
|
} else { |
|
error "textblock::table configure_row - unrecognised option '[lindex $args 0]'. Known values [tcl::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: [tcl::dict::keys $o_opts_row_defaults]" |
|
} |
|
foreach {k v} $args { |
|
if {$k ni [tcl::dict::keys $o_opts_row_defaults]} { |
|
error "[tcl::namespace::current]::table configure_row unknown option '$k'. Known options: [tcl::dict::keys $o_opts_row_defaults]" |
|
} |
|
} |
|
set checked_opts [list] |
|
foreach {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 [tcl::dict::get $o_rowdefs $ridx] |
|
set opts [tcl::dict::merge $current_opts $checked_opts] |
|
|
|
#check minheight and maxheight together |
|
set opt_minh [tcl::dict::get $opts -minheight] |
|
set opt_maxh [tcl::dict::get $opts -maxheight] |
|
|
|
#todo - allow zero values to hide/collapse rows as is possible with columns |
|
if {![tcl::string::is integer $opt_minh] || ($opt_maxh ne "" && ![tcl::string::is integer -strict $opt_maxh])} { |
|
error "[tcl::namespace::current]::table::configure_row error -minheight '$opt_minh' and -maxheight '$opt_maxh' must be integers greater than or equal to 1 (for now)" |
|
} |
|
if {$opt_minh < 1 || ($opt_maxh ne "" && $opt_maxh < 1)} { |
|
error "[tcl::namespace::current]::table::configure_row error -minheight '$opt_minh' and -maxheight '$opt_maxh' must both be 1 or greater (for now)" |
|
} |
|
if {$opt_maxh ne "" && $opt_maxh < $opt_minh} { |
|
error "[tcl::namespace::current]::table::configure_row error -maxheight '$opt_maxh' must greater than -minheight '$opt_minh'" |
|
} |
|
tcl::dict::set o_rowstates $ridx -minheight $opt_minh |
|
|
|
|
|
tcl::dict::set o_rowdefs $ridx $opts |
|
} |
|
method row_count {} { |
|
#*** !doctools |
|
#[call class::table [method row_count]] |
|
#[para] return the number of data rows in the table. |
|
return [tcl::dict::size $o_rowdefs] |
|
} |
|
method row_clear {} { |
|
#*** !doctools |
|
#[call class::table [method row_clear]] |
|
#[para] Remove all rows without resetting column data. |
|
#[para] When adding new rows the number of entries will need to match the existing column count. |
|
set o_rowdefs [tcl::dict::create] |
|
set o_rowstates [tcl::dict::create] |
|
#The data values are stored by column regardless of whether added row by row |
|
tcl::dict::for {cidx records} $o_columndata { |
|
tcl::dict::set o_columndata $cidx [list] |
|
#reset only the body fields in o_columnstates |
|
tcl::dict::set o_columnstates $cidx minwidthbodyseen 0 |
|
tcl::dict::set o_columnstates $cidx maxwidthbodyseen 0 |
|
} |
|
set o_calculated_column_widths [list] |
|
} |
|
method clear {} { |
|
#*** !doctools |
|
#[call class::table [method clear]] |
|
#[para] Remove all row and column data. |
|
#[para] If a subsequent call to add_row is made it can contain any number of values. |
|
#[para] Further calls to add_row will need to contain the same number of values |
|
#[para] as the first call unless default values have been set for the missing columns (review - in flux). |
|
my row_clear |
|
set o_columndefs [tcl::dict::create] |
|
set o_columndata [tcl::dict::create] |
|
set o_columnstates [tcl::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 {[tcl::string::is integer -strict $c]} { |
|
set colidx $c |
|
} else { |
|
tcl::dict::for {colidx coldef} $o_columndefs { |
|
#if {[tcl::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 { |
|
return [tcl::dict::create \ |
|
boxlimits [list hlb blc vll]\ |
|
boxlimits_top [list hlb blc vll hlt tlc]\ |
|
joins [list down]\ |
|
bodyjoins [list down-$fname_body]\ |
|
] |
|
} |
|
inner { |
|
return [tcl::dict::create \ |
|
boxlimits [list hlb blc vll]\ |
|
boxlimits_top [list hlb blc vll hlt tlc]\ |
|
joins [list down left]\ |
|
bodyjoins [list left down-$fname_body] |
|
] |
|
} |
|
right { |
|
return [tcl::dict::create \ |
|
boxlimits [list hlb blc vll vlr brc]\ |
|
boxlimits_top [list hlb blc vll vlr brc hlt tlc trc]\ |
|
joins [list down left]\ |
|
bodyjoins [list left down-$fname_body]\ |
|
] |
|
} |
|
solo { |
|
return [tcl::dict::create \ |
|
boxlimits [list hlb blc vll vlr brc]\ |
|
boxlimits_top [list hlb blc vll vlr brc hlt tlc trc]\ |
|
joins [list down]\ |
|
bodyjoins [list down-$fname_body]\ |
|
] |
|
} |
|
default { |
|
error "Get_boxlimits_and_joins unrecognised position '$position' expected: left inner right solo" |
|
} |
|
} |
|
} |
|
method Get_boxlimits_and_joins1 {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 [list hlb blc vll] |
|
#set boxlimits_toprow [concat $boxlimits_position {hlt tlc}] |
|
set boxlimits_toprow [list hlb blc vll hlt tlc] |
|
set joins [list down] |
|
} |
|
inner { |
|
#set header_boxlimits {hlb hlt tlc blc vll} |
|
set header_body_joins [list left down-$fname_body] |
|
set boxlimits_position [list hlb blc vll] |
|
#set boxlimits_toprow [concat $boxlimits_position {hlt tlc}] |
|
set boxlimits_toprow [list hlb blc vll hlt tlc] |
|
set joins [list 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 [list hlb blc vll vlr brc] |
|
#set boxlimits_toprow [concat $boxlimits_position {hlt tlc trc}] |
|
set boxlimits_toprow [list hlb blc vll vlr brc hlt tlc trc] |
|
set joins [list 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 [list hlb blc vll vlr brc] |
|
#set boxlimits_toprow [concat $boxlimits_position {hlt tlc trc}] |
|
set boxlimits_toprow [list hlb blc vll vlr brc hlt tlc trc] |
|
set joins [list down] |
|
} |
|
} |
|
return [tcl::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 [tcl::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 opts [tcl::dict::create\ |
|
-position "inner"\ |
|
-return "string"\ |
|
] |
|
foreach {k v} $args { |
|
switch -- $k { |
|
-position - -return { |
|
tcl::dict::set opts $k $v |
|
} |
|
default { |
|
error "[tcl::namespace::current]::table::get_column_by_index error invalid option '$k'. Known options [tcl::dict::keys $opts]" |
|
} |
|
} |
|
} |
|
set opt_posn [tcl::dict::get $opts -position] |
|
set opt_return [tcl::dict::get $opts -return] |
|
|
|
switch -- $opt_posn { |
|
left - inner - right - solo {} |
|
default { |
|
error "[tcl::namespace::current]::table::get_column_by_index error invalid value '$opt_posn' for -position. Valid values: [list left inner right solo]" |
|
} |
|
} |
|
switch -- $opt_return { |
|
string - dict {} |
|
default { |
|
error "[tcl::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 [tcl::dict::get $columninfo headers] |
|
#puts "===== header_list: $header_list" |
|
set cells [tcl::dict::get $columninfo cells] |
|
|
|
set topt_show_header [tcl::dict::get $o_opts_table -show_header] |
|
if {$topt_show_header eq ""} { |
|
set allheaders 0 |
|
set all_cols [tcl::dict::keys $o_columndefs] |
|
foreach c $all_cols { |
|
incr allheaders [llength [tcl::dict::get $o_columndefs $c -headers]] |
|
} |
|
if {$allheaders == 0} { |
|
set do_show_header 0 |
|
} else { |
|
set do_show_header 1 |
|
} |
|
} else { |
|
set do_show_header $topt_show_header |
|
} |
|
set topt_show_footer [tcl::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 [tcl::dict::get $ftypes body] |
|
if {[llength $ftype_body] >= 2} { |
|
set fname_body "custom" |
|
} else { |
|
set fname_body $ftype_body |
|
} |
|
set ftype_header [tcl::dict::get $ftypes header] |
|
if {[llength $ftype_header] >= 2} { |
|
set fname_header "custom" |
|
} else { |
|
set fname_header $ftype_header |
|
} |
|
|
|
set limj [my Get_boxlimits_and_joins $opt_posn $fname_body] |
|
set header_body_joins [tcl::dict::get $limj bodyjoins] |
|
set joins [tcl::dict::get $limj joins] |
|
set boxlimits_position [tcl::dict::get $limj boxlimits] |
|
set boxlimits_toprow [tcl::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 [tcl::dict::get $o_opts_table_effective -framelimits_body] $boxlimits_position] |
|
set boxlimits_headerless [struct::set intersect [tcl::dict::get $o_opts_table_effective -framelimits_body] $boxlimits_toprow] |
|
set header_boxlimits [struct::set intersect [tcl::dict::get $o_opts_table_effective -framelimits_header] $boxlimits_position] |
|
set header_boxlimits_toprow [struct::set intersect [tcl::dict::get $o_opts_table_effective -framelimits_header] $boxlimits_toprow] |
|
|
|
set fmap [tcl::dict::get $o_opts_table_effective -framemap_body] |
|
set hmap [tcl::dict::get $o_opts_table_effective -framemap_header] |
|
|
|
#if {![tcl::dict::get $o_opts_table -show_edge]} { |
|
# set body_edgemap [textblock::class::table_edge_map ""] |
|
# dict for {k v} $fmap { |
|
# #tcl::dict::set fmap $k [tcl::dict::merge $v [tcl::dict::get $body_edgemap $k]] |
|
# } |
|
# set header_edgemap [textblock::class::header_edge_map ""] |
|
# dict for {k v} $hmap { |
|
# #tcl::dict::set hmap $k [tcl::dict::merge $v [tcl::dict::get $header_edgemap $k]] |
|
# } |
|
#} |
|
set sep_elements_horizontal $::textblock::class::table_hseps |
|
set sep_elements_vertical $::textblock::class::table_vseps |
|
|
|
set topmap [tcl::dict::get $fmap top$opt_posn] |
|
set botmap [tcl::dict::get $fmap bottom$opt_posn] |
|
set midmap [tcl::dict::get $fmap middle$opt_posn] |
|
set onlymap [tcl::dict::get $fmap only$opt_posn] |
|
|
|
set hdrmap [tcl::dict::get $hmap only${opt_posn}] |
|
|
|
set topseps_h [tcl::dict::get $sep_elements_horizontal top$opt_posn] |
|
set midseps_h [tcl::dict::get $sep_elements_horizontal middle$opt_posn] |
|
set topseps_v [tcl::dict::get $sep_elements_vertical top$opt_posn] |
|
set midseps_v [tcl::dict::get $sep_elements_vertical middle$opt_posn] |
|
set botseps_v [tcl::dict::get $sep_elements_vertical bottom$opt_posn] |
|
set onlyseps_v [tcl::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 [tcl::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 [tcl::dict::keys $o_columndefs] $index_expression] |
|
|
|
set colwidth [my column_width $cidx] |
|
|
|
set col_blockalign [tcl::dict::get $o_columndefs $cidx -blockalign] |
|
|
|
if {$do_show_header} { |
|
#puts "boxlimitsinfo header $opt_posn: -- boxlimits $header_boxlimits -- boxmap $hdrmap" |
|
set ansibase_header [tcl::dict::get $o_opts_table -ansibase_header] ;#merged to single during configure |
|
set ansiborder_header [tcl::dict::get $o_opts_table -ansiborder_header] |
|
if {[tcl::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 [tcl::string::repeat " " $hcolwidth] |
|
|
|
set all_colspans [my header_colspans_numeric] |
|
|
|
#put our framedef calls together |
|
set fdef_header [textblock::framedef $ftype_header] |
|
set framedef_leftbox [textblock::framedef -joins left $ftype_header] |
|
set framedef_headerdown_same [textblock::framedef -joins {down} $ftype_header] |
|
set framedef_headerdown_body [textblock::framedef -joins [list down-$fname_body] $ftype_header] |
|
#default span_extend_map - used as base to customise with specific joins |
|
set span_extend_map [tcl::dict::create \ |
|
vll " "\ |
|
tlc [tcl::dict::get $fdef_header hlt]\ |
|
blc [tcl::dict::get $fdef_header hlb]\ |
|
] |
|
|
|
|
|
#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 [tcl::dict::get $all_colspans $hrow] |
|
set this_span [lindex $headerspans $cidx] |
|
#set hval $ansibase_header$header ;#no reset |
|
set hval $header |
|
set rowh [my header_height $hrow] |
|
|
|
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 {![tcl::dict::get $o_opts_table -show_edge]} { |
|
set hlims [struct::set difference $hlims [tcl::dict::get $::textblock::class::header_edge_parts $rowpos$opt_posn] ] |
|
} |
|
#puts ">>> headerspans: $headerspans cidx: $cidx" |
|
|
|
#if {$this_span eq "any" || $this_span > 0} {} |
|
#changed to processing only numeric colspans |
|
|
|
if {$this_span > 0} { |
|
set startmap [tcl::dict::get $hmap $rowpos${opt_posn}] |
|
#look at spans in header below to determine joins required at blc |
|
if {$show_seps_v} { |
|
if {[tcl::dict::exists $all_colspans [expr {$hrow+1}]]} { |
|
set next_spanlist [tcl::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 |
|
tcl::dict::set startmap blc [tcl::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 [tcl::dict::get $ftypes header]\ |
|
# -ansibase $ansibase_header -ansiborder $ansiborder_final\ |
|
# -boxlimits $hlims -boxmap $startmap -joins $header_joins $hval\ |
|
# ] |
|
|
|
if {$this_span == 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 -checkargs 0 -blockalign $col_blockalign -ellipsis 0 -usecache 1 -width [expr {$hcolwidth+2}] -type [tcl::dict::get $ftypes header]\ |
|
-ansibase $ansibase_header -ansiborder $ansiborder_final\ |
|
-boxlimits $hlims -boxmap $startmap -joins $header_joins $cellcontents\ |
|
] |
|
|
|
if {$this_span != 1} { |
|
#puts "===>\n$header_cell_startspan\n<===" |
|
set spanned_parts [list $header_cell_startspan] |
|
#assert this_span == "any" or >1 ie a header that spans other columns |
|
#therefore more parts to append |
|
#set remaining_cols [lrange [tcl::dict::keys $o_columndefs] $cidx end] |
|
set remaining_spans [lrange $headerspans $cidx+1 end] |
|
set spanval [join $remaining_spans ""] ;#so we can test for all zeros |
|
set spans_to_rhs 0 |
|
if {[expr {$spanval}] == 0} { |
|
#puts stderr "SPANS TO RHS" |
|
set spans_to_rhs 1 |
|
} |
|
|
|
#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 [tcl::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 [tcl::dict::get $limj bodyjoins] |
|
set span_joins [tcl::dict::get $limj joins] |
|
set span_boxlimits [tcl::dict::get $limj boxlimits] |
|
set span_boxlimits_top [tcl::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 [tcl::dict::get $o_opts_table_effective -framelimits_body] $span_boxlimits] |
|
#set span_boxlimits_top [struct::set intersect [tcl::dict::get $o_opts_table_effective -framelimits_body] $span_boxlimits_top] |
|
set header_span_boxlimits [struct::set intersect [tcl::dict::get $o_opts_table_effective -framelimits_header] $span_boxlimits] |
|
set header_span_boxlimits_top [struct::set intersect [tcl::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} { |
|
tcl::dict::set this_span_map blc [tcl::dict::get $framedef_headerdown_same hlbj] ;#horizontal line bottom with down join - to same frametype |
|
} |
|
} else { |
|
#join to body |
|
tcl::dict::set this_span_map blc [tcl::dict::get $framedef_headerdown_body 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 {![tcl::dict::get $o_opts_table -show_edge]} { |
|
set hlims [struct::set difference $hlims [tcl::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 [tcl::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 |
|
} |
|
|
|
#JMN |
|
#spanned_parts are all built with textblock::frame - therefore uniform-width lines - can use join_basic |
|
set spanned_frame [textblock::join_basic -- {*}$spanned_parts] |
|
|
|
if {$spans_to_rhs} { |
|
if {$cidx == 0} { |
|
set fake_posn solo |
|
} else { |
|
set fake_posn right |
|
} |
|
set x_limj [my Get_boxlimits_and_joins $fake_posn $fname_body] |
|
if {$hrow == 0} { |
|
set x_boxlimits_toprow [tcl::dict::get $x_limj boxlimits_top] |
|
set hlims [struct::set intersect [tcl::dict::get $o_opts_table_effective -framelimits_header] $x_boxlimits_toprow] |
|
} else { |
|
set x_boxlimits_position [tcl::dict::get $x_limj boxlimits] |
|
set hlims [struct::set intersect [tcl::dict::get $o_opts_table_effective -framelimits_header] $x_boxlimits_position] |
|
} |
|
} else { |
|
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 {![tcl::dict::get $o_opts_table -show_edge]} { |
|
if {$spans_to_rhs} { |
|
#assert fake_posn has been set above based on cidx and spans_to_rhs |
|
set hlims [struct::set difference $hlims [tcl::dict::get $::textblock::class::header_edge_parts ${rowpos}$fake_posn] ] |
|
} else { |
|
#use the edge_parts corresponding to the column being written to ie use opt_posn |
|
set hlims [struct::set difference $hlims [tcl::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 -checkargs 0 -ellipsis 0 -type $spacemap -boxlimits $hlims -ansibase $ansibase_header $hval] ;#review -ansibase |
|
#puts "==>boxlimits:'$hlims' hval_width:[tcl::string::length $hval] blockwidth:[textblock::width $hblock]" |
|
#puts $hblock |
|
#puts "==>hval:'$hval'[a]" |
|
#puts "==>hval:'[ansistring VIEW $hval]'" |
|
#set spanned_frame [overtype::renderspace -transparent 1 $spanned_frame $hblock] |
|
|
|
#spanned values default left - todo make configurable |
|
|
|
#TODO |
|
#consider that currently blockaligning for spanned columns must always use the blockalign value from the starting column of the span |
|
#we could conceivably have an override that could be set somehow with configure_header for customization of alignments of individual spans or span sizes? |
|
#this seems like a likely requirement. The first spanned column may well have different alignment requirements than the span. |
|
#(e.g if first spanned col happens to be numeric it probably warrants right textalign (if not blockalign) but we don't necessarily want the spanning header or even a non-spanning header to be right aligned) |
|
|
|
set spanned_frame [overtype::block -blockalign $col_blockalign -overflow 1 -transparent 1 $spanned_frame $hblock] |
|
#POTENTIAL BUG (fixed with spans_to_rhs above) |
|
#when -blockalign right and colspan extends to rhs - last char of longest of that spanlength will overlap right edge (if show_edge 1) |
|
#we need to shift 1 to the left when doing our overtype with blockalign right |
|
#we normally do this by using the hlims based on position - effectively we need a rhs position set of hlims for anything that colspans to the right edge |
|
#(even though the column position may be left or inner) |
|
|
|
|
|
|
|
} else { |
|
#this_span == 1 |
|
set spanned_frame [textblock::join_basic -- $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" && [tcl::dict::get $o_opts_table -show_edge]} { |
|
set padheight [expr {$rowh + 2}] |
|
} else { |
|
set padheight [expr {$rowh + 1}] |
|
} |
|
set bline [tcl::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 [tcl::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 -checkargs 0 -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 {![tcl::dict::get $o_opts_table -show_edge]} { |
|
set hlims [struct::set difference $hlims [tcl::dict::get $::textblock::class::header_edge_parts only$opt_posn] ] |
|
} |
|
set header_joins $header_body_joins |
|
set header_frame [textblock::frame -checkargs 0 -width [expr {$hcolwidth+2}] -type [tcl::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 [tcl::string::trimright $part_header \n] |
|
lassign [textblock::size $part_header] _w return_headerwidth _h return_headerheight |
|
|
|
set padline [tcl::string::repeat $TSUB $return_headerwidth] |
|
set adjusted_lines [list] |
|
foreach ln [split $part_header \n] { |
|
if {[tcl::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 [tcl::dict::keys $o_columndefs] $index_expression] ;#convert possible end-1,2+2 etc expression to >= 0 integer in dict range |
|
|
|
set opt_col_ansibase [tcl::dict::get $o_columndefs $colidx -ansibase] ;#ordinary merge of codes already done in configure_column |
|
#set colwidth [my column_width $colidx] |
|
|
|
set body_ansibase [tcl::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 [tcl::dict::get $o_opts_table -ansiborder_body] |
|
if {[tcl::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 {[tcl::dict::get $o_opts_table -frametype] eq "block"}] |
|
foreach c $cells { |
|
#cells in column - each new c is in a different row |
|
set row_ansibase [tcl::dict::get $o_rowdefs $r -ansibase] |
|
set row_bg "" |
|
if {$row_ansibase ne ""} { |
|
set row_bg [punk::ansi::codetype::sgr_merge_singles [list $row_ansibase] -filter_fg 1] |
|
} |
|
|
|
set ansibase $body_ansibase$opt_col_ansibase |
|
#todo - joinleft,joinright,joindown based on opts in args |
|
set cell_ansibase "" |
|
|
|
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 {![tcl::dict::get $o_opts_table -show_edge]} { |
|
set blims [struct::set difference $blims [tcl::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 {![tcl::dict::get $o_opts_table -show_edge]} { |
|
set blims [struct::set difference $blims [tcl::dict::get $::textblock::class::table_edge_parts top$opt_posn] ] |
|
} |
|
} |
|
set rowframe [textblock::frame -checkargs 0 -type [tcl::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::widthtopline $rowframe] ;#frame lines always same width - just look at top line |
|
append part_body $rowframe \n |
|
} else { |
|
if {$r == $rmax} { |
|
set joins [lremove $joins [lsearch $joins down*]] |
|
set bmap $botmap |
|
set blims $blims_bot |
|
if {![tcl::dict::get $o_opts_table -show_edge]} { |
|
set blims [struct::set difference $blims [tcl::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 {![tcl::dict::get $o_opts_table -show_edge]} { |
|
set blims [struct::set difference $blims [tcl::dict::get $::textblock::class::table_edge_parts middle$opt_posn] ] |
|
} |
|
} |
|
append part_body [textblock::frame -checkargs 0 -type [tcl::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 {![tcl::dict::get $o_opts_table -show_edge]} { |
|
#set blims [struct::set difference $blims [tcl::dict::get $::textblock::class::table_edge_parts only$opt_posn] ] |
|
#append output [textblock::frame -width [expr {$colwidth + 2}] -type [tcl::dict::get $ftypes body] -boxlimits $blims -boxmap $onlymap -joins $joins]\n |
|
append part_body [tcl::string::repeat " " $colwidth] \n |
|
set return_bodywidth $colwidth |
|
} else { |
|
set emptyframe [textblock::frame -checkargs 0 -width [expr {$colwidth + 2}] -type [tcl::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 {[tcl::string::index $part_body end] eq "\n"} { |
|
set part_body [tcl::string::range $part_body 0 end-1] |
|
} |
|
set return_bodyheight [textblock::height $part_body] |
|
#append output $part_body |
|
|
|
if {$opt_return eq "string"} { |
|
if {$part_header ne ""} { |
|
set output $part_header |
|
if {$part_body ne ""} { |
|
append output \n $part_body |
|
} |
|
} else { |
|
set output $part_body |
|
} |
|
return $output |
|
} else { |
|
return [tcl::dict::create header $part_header body $part_body headerwidth $return_headerwidth headerheight $return_headerheight bodywidth $return_bodywidth bodyheight $return_bodyheight] |
|
} |
|
} |
|
|
|
method get_column_cells_by_index {index_expression} { |
|
#*** !doctools |
|
#[call class::table [method get_column_cells_by_index] [arg index_expression]] |
|
#[para] Return a dict with keys 'headers' and 'cells' giving column header and data values |
|
|
|
set cidx [lindex [tcl::dict::keys $o_columndefs] $index_expression] |
|
if {$cidx eq ""} { |
|
set range "" |
|
if {[tcl::dict::size $o_columndefs] > 0} { |
|
set range "0..[expr {[tcl::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 num_header_rows [my header_count] |
|
set cdef [tcl::dict::get $o_columndefs $cidx] |
|
set headerlist [tcl::dict::get $cdef -headers] |
|
set ansibase_col [tcl::dict::get $cdef -ansibase] |
|
set textalign [tcl::dict::get $cdef -textalign] |
|
switch -- $textalign { |
|
left {set pad right} |
|
right {set pad left} |
|
default { |
|
set pad "centre" ;#todo? |
|
} |
|
} |
|
|
|
#set ansibase_body [tcl::dict::get $o_opts_table -ansibase_body] |
|
#set ansibase_header [tcl::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] |
|
#we need to replace the 'any' entries with their actual span lengths before passing any -colspan values to column_datawidth - hence use header_colspans_numeric |
|
set all_colspans [my header_colspans_numeric] |
|
#JMN |
|
#store configured widths so we don't look up for each header line |
|
#set configured_widths [list] |
|
#foreach c [tcl::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 [tcl::dict::create] |
|
tcl::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] |
|
#jjj |
|
set header_maxdataheight [tcl::dict::get $o_headerstates $hrow maxheightseen] |
|
#set header_maxdataheight [my header_height $hrow] ;#from cached headerstates |
|
set headerdefminh [tcl::dict::get $o_headerdefs $hrow -minheight] |
|
set headerdefmaxh [tcl::dict::get $o_headerdefs $hrow -maxheight] |
|
if {"$headerdefminh$headerdefmaxh" ne "" && $headerdefminh eq $headerdefmaxh} { |
|
set headerh $headerdefminh ;#exact height defined for the row |
|
} else { |
|
if {$headerdefminh eq ""} { |
|
if {$headerdefmaxh eq ""} { |
|
#both defs empty |
|
set headerh $header_maxdataheight |
|
} else { |
|
set headerh [expr {min(1,$headerdefmaxh,$header_maxdataheight)}] |
|
} |
|
} else { |
|
if {$headerdefmaxh eq ""} { |
|
set headerh [expr {max($headerdefminh,$header_maxdataheight)}] |
|
} else { |
|
if {$header_maxdataheight < $headerdefminh} { |
|
set headerh $headerdefminh |
|
} else { |
|
set headerh [expr {max($headerdefminh,$header_maxdataheight)}] |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
set headerrow_colspans [tcl::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 -cached 1] ;#widest of headers in this col with same span - allows textalign to work with blockalign |
|
|
|
set hcell_line_blank [tcl::string::repeat " " $this_hdrwidth] |
|
set hcell_lines [lrepeat $header_maxdataheight $hcell_line_blank] |
|
set hval_lines [split $hdr \n] |
|
#jmn concat |
|
#set hval_lines [concat $hval_lines $hcell_lines] |
|
set hval_lines [list {*}$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] |
|
tcl::dict::lappend output headers $hcell |
|
} |
|
|
|
|
|
#set colwidth [my column_width $cidx] |
|
#set cell_line_blank [tcl::string::repeat " " $colwidth] |
|
set datawidth [my column_datawidth $cidx -headers 0 -footers 0 -data 1 -cached 1] |
|
set cell_line_blank [tcl::string::repeat " " $datawidth] |
|
|
|
|
|
|
|
set items [tcl::dict::get $o_columndata $cidx] |
|
#puts "---> columndata $o_columndata" |
|
|
|
#set opt_row_ansibase [tcl::dict::get $o_rowdefs $r -ansibase] |
|
#set cell_ansibase $ansibase_body$ansibase_col$opt_row_ansibase |
|
|
|
tcl::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 [tcl::dict::get $o_rowstates $r -maxheight] |
|
set rowdefminh [tcl::dict::get $o_rowdefs $r -minheight] |
|
set rowdefmaxh [tcl::dict::get $o_rowdefs $r -maxheight] |
|
if {"$rowdefminh$rowdefmaxh" ne "" && $rowdefminh eq $rowdefmaxh} { |
|
set rowh $rowdefminh ;#an exact height is defined for the row |
|
} 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] |
|
#jmn |
|
#set cval_lines [concat $cval_lines $cell_lines] |
|
lappend cval_lines {*}$cell_lines |
|
set cval_lines [lrange $cval_lines 0 $rowh-1] |
|
set cval_block [::join $cval_lines \n] |
|
|
|
#//JMN assert widest cval_line = datawidth = known_blockwidth |
|
set cell [textblock::pad $cval_block -known_blockwidth $datawidth -width $datawidth -padchar " " -within_ansi 1 -which $pad] |
|
#set cell [textblock::pad $cval_block -width $colwidth -padchar " " -within_ansi 0 -which left] |
|
tcl::dict::lappend output cells $cell |
|
|
|
incr r |
|
} |
|
return $output |
|
} |
|
method get_column_values_by_index {index_expression} { |
|
#*** !doctools |
|
#[call class::table [method get_column_values_by_index] [arg index_expression]] |
|
#[para] List the cell values of a column from the data area only (no header values) |
|
|
|
set cidx [lindex [tcl::dict::keys $o_columndefs] $index_expression] |
|
if {$cidx eq ""} { |
|
return |
|
} |
|
return [tcl::dict::get $o_columndata $cidx] |
|
} |
|
method debug {args} { |
|
#*** !doctools |
|
#[call class::table [method debug]] |
|
#[para] display lots of debug information about how the table is constructed. |
|
|
|
#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 [tcl::dict::create\ |
|
-usetables 1\ |
|
] |
|
foreach {k v} $args { |
|
switch -- $k { |
|
-usetables {} |
|
default { |
|
error "table debug unrecognised option '$k'. Known options: [tcl::dict::keys $defaults]" |
|
} |
|
} |
|
} |
|
set opts [tcl::dict::merge $defaults $args] |
|
set opt_usetables [tcl::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} { |
|
tcl::dict::for {k v} $o_columndefs { |
|
puts " $k $v" |
|
} |
|
} else { |
|
set t [textblock::class::table new] |
|
$t add_column -headers "Col" |
|
tcl::dict::for {col coldef} $o_columndefs { |
|
foreach property [tcl::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 [tcl::dict::create] |
|
set max_widths [tcl::dict::create 0 0 1 0 2 0 3 0] ;#max inner table column widths |
|
tcl::dict::for {col coldef} $o_columndefs { |
|
set row [list $col] |
|
set colheaders [tcl::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 [tcl::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] |
|
tcl::dict::set col_header_tables $col $htable |
|
set colwidths [$htable column_widths] |
|
set icol 0 |
|
foreach w $colwidths { |
|
if {$w > [tcl::dict::get $max_widths $icol]} { |
|
tcl::dict::set max_widths $icol $w |
|
} |
|
incr icol |
|
} |
|
} |
|
|
|
#safe jumptable test |
|
#dict for {col coldef} $o_columndefs {} |
|
tcl::dict::for {col coldef} $o_columndefs { |
|
set row [list $col] |
|
#safe jumptable test |
|
#dict for {property val} $coldef {} |
|
tcl::dict::for {property val} $coldef { |
|
switch -- $property { |
|
-ansireset {continue} |
|
-headers { |
|
set htable [tcl::dict::get $col_header_tables $col] |
|
tcl::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 "headerdefs: $o_headerdefs" |
|
puts stdout "headerstates: $o_headerstates" |
|
tcl::dict::for {k coldef} $o_columndefs { |
|
if {[tcl::dict::exists $o_columndata $k]} { |
|
set headerlist [tcl::dict::get $coldef -headers] |
|
set coldata [tcl::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 {[tcl::dict::size $o_columndefs]-1}] |
|
foreach c [tcl::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 [tcl::dict::keys $o_columndefs] $index_expression] |
|
if {$cidx eq ""} { |
|
return |
|
} |
|
#assert cidx is now >=0 integer within the range of defined columns |
|
set cdef [tcl::dict::get $o_columndefs $cidx] |
|
set defminw [tcl::dict::get $cdef -minwidth] |
|
set defmaxw [tcl::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 [tcl::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 [tcl::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} { |
|
#*** !doctools |
|
#[call class::table [method column_width] [arg index_expression]] |
|
#[para] inner width of column ie the available cell-width without borders/separators |
|
|
|
if {[llength $o_calculated_column_widths] != [tcl::dict::size $o_columndefs]} { |
|
my calculate_column_widths -algorithm $o_column_width_algorithm |
|
} |
|
return [lindex $o_calculated_column_widths $index_expression] |
|
} |
|
method column_widths {} { |
|
#*** !doctools |
|
#[call class::table [method column_width]] |
|
#[para] ordered list of column widths (inner widths) |
|
|
|
if {[llength $o_calculated_column_widths] != [tcl::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 {} { |
|
#*** !doctools |
|
#[call class::table [method width]] |
|
#[para] width of the table including borders and separators |
|
#[para] calculate width based on assumption frame verticals are 1 screen-column wide |
|
#[para] (review - consider possibility of 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 {[tcl::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 [tcl::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 [tcl::dict::get $o_columndefs $cidx] |
|
set defminw [tcl::dict::get $cdef -minwidth] |
|
set defmaxw [tcl::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 [tcl::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 [tcl::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] |
|
set thiscol_widest_header [tcl::dict::get $o_columnstates $cidx maxwidthheaderseen] |
|
tcl::dict::for {h colspans} $header_colspans { |
|
set spanc [lindex $colspans $cidx] |
|
#set headers [tcl::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}]] |
|
#} |
|
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 "any" || $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 [tcl::dict::get $o_opts_table -show_seps] |
|
set vseps [tcl::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 opts [tcl::dict::create\ |
|
-headers 0\ |
|
-footers 0\ |
|
-colspan unspecified\ |
|
-data 1\ |
|
-cached 1\ |
|
] |
|
#NOTE: -colspan any is not the same as * |
|
# |
|
#-colspan is relevant to header/footer data only |
|
foreach {k v} $args { |
|
switch -- $k { |
|
-headers - -footers - -colspan - -data - -cached { |
|
tcl::dict::set opts $k $v |
|
} |
|
default { |
|
error "column_datawidth unrecognised flag '$k'. Known flags: [tcl::dict::keys $opts]" |
|
} |
|
} |
|
} |
|
set opt_colspan [tcl::dict::get $opts -colspan] |
|
switch -- $opt_colspan { |
|
* - unspecified {} |
|
any { |
|
error "method column_datawidth invalid -colspan '$opt_colspan' must be * or an integer >= 0 (use header_colspans_numeric to get actual spans)" |
|
} |
|
default { |
|
if {![string is integer -strict $opt_colspan]} { |
|
error "method column_datawidth invalid -colspan '$opt_colspan' must be * or an integer >= 0" |
|
} |
|
} |
|
} |
|
|
|
|
|
set cidx [lindex [tcl::dict::keys $o_columndefs] $index_expression] |
|
if {$cidx eq ""} { |
|
return |
|
} |
|
|
|
if {[tcl::dict::get $opts -cached]} { |
|
set hwidest 0 |
|
set bwidest 0 |
|
set fwidest 0 |
|
if {[tcl::dict::get $opts -headers]} { |
|
if {$opt_colspan in {* unspecified}} { |
|
set hwidest [tcl::dict::get $o_columnstates $cidx maxwidthheaderseen] |
|
} else { |
|
#this is not cached |
|
# -- --- --- --- |
|
set colheaders [tcl::dict::get $o_columndefs $cidx -headers] |
|
set all_colspans_by_header [my header_colspans_numeric] |
|
set hlist [list] |
|
tcl::dict::for {hrow cspans} $all_colspans_by_header { |
|
set s [lindex $cspans $cidx] |
|
if {$s eq $opt_colspan} { |
|
lappend hlist [lindex $colheaders $hrow] |
|
} |
|
} |
|
if {[llength $hlist]} { |
|
set hwidest [tcl::mathfunc::max {*}[lmap v $hlist {textblock::width $v}]] |
|
} else { |
|
set hwidest 0 |
|
} |
|
# -- --- --- --- |
|
} |
|
} |
|
if {[tcl::dict::get $opts -data]} { |
|
set bwidest [tcl::dict::get $o_columnstates $cidx maxwidthbodyseen] |
|
} |
|
if {[tcl::dict::get $opts -footers]} { |
|
#TODO! |
|
#set bwidest [tcl::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] |
|
set hwidest 0 |
|
if {[tcl::dict::get $opts -headers]} { |
|
if {$opt_colspan in {* unspecified}} { |
|
lappend values {*}[tcl::dict::get $o_columndefs $cidx -headers] |
|
} else { |
|
# -- --- --- --- |
|
set colheaders [tcl::dict::get $o_columndefs $cidx -headers] |
|
set all_colspans_by_header [my header_colspans_numeric] |
|
set hlist [list] |
|
tcl::dict::for {hrow cspans} $all_colspans_by_header { |
|
set s [lindex $cspans $cidx] |
|
if {$s eq $opt_colspan} { |
|
lappend hlist [lindex $colheaders $hrow] |
|
} |
|
} |
|
if {[llength $hlist]} { |
|
set hwidest [tcl::mathfunc::max {*}[lmap v $hlist {textblock::width $v}]] |
|
} else { |
|
set hwidest 0 |
|
} |
|
# -- --- --- --- |
|
} |
|
} |
|
if {[tcl::dict::get $opts -data]} { |
|
if {[tcl::dict::exists $o_columndata $cidx]} { |
|
lappend values {*}[tcl::dict::get $o_columndata $cidx] |
|
} |
|
} |
|
if {[tcl::dict::get $opts -footers]} { |
|
lappend values {*}[tcl::dict::get $o_columndefs $cidx -footers] |
|
} |
|
if {[llength $values]} { |
|
set valwidest [tcl::mathfunc::max {*}[lmap v $values {textblock::width $v}]] |
|
set widest [expr {max($valwidest,$hwidest)}] |
|
} else { |
|
set widest $hwidest |
|
} |
|
return $widest |
|
} |
|
#print1 uses basic column joining - useful for testing/debug especially with colspans |
|
method print1 {args} { |
|
if {![llength $args]} { |
|
set cols [tcl::dict::keys $o_columndata] |
|
} else { |
|
set cols [list] |
|
foreach colspec $args { |
|
set allcols [tcl::dict::keys $o_columndata] |
|
if {[tcl::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 [tcl::dict::create] ;# to use tcl::dict::incr |
|
set colspace_added [tcl::dict::create] |
|
|
|
set ordered_spans [tcl::dict::create] |
|
tcl::dict::for {col spandata} [my spangroups] { |
|
set dwidth [my column_datawidth $col -data 1 -headers 0 -footers 0 -cached 1] |
|
set minwidth [tcl::dict::get $o_columndefs $col -minwidth] |
|
set maxwidth [tcl::dict::get $o_columndefs $col -maxwidth] |
|
if {$minwidth ne ""} { |
|
if {$dwidth < $minwidth} { |
|
set dwidth $minwidth |
|
} |
|
} |
|
if {$maxwidth ne ""} { |
|
if {$dwidth > $maxwidth} { |
|
set dwidth $maxwidth |
|
} |
|
} |
|
tcl::dict::set colwidths $col $dwidth ;#spangroups is ordered by column - so colwidths dict will be correctly ordered |
|
tcl::dict::set colspace_added $col 0 |
|
|
|
set spanlengths [tcl::dict::get $spandata spanlengths] |
|
foreach slen $spanlengths { |
|
set spans [tcl::dict::get $spandata spangroups $slen] |
|
set spans [lsort -index 7 -integer $spans] |
|
foreach s $spans { |
|
set hwidth [tcl::dict::get $s headerwidth] |
|
set hrow [tcl::dict::get $s hrow] |
|
set scol [tcl::dict::get $s startcol] |
|
tcl::dict::set ordered_spans $scol,$hrow membercols $col $dwidth |
|
tcl::dict::set ordered_spans $scol,$hrow headerwidth $hwidth |
|
} |
|
} |
|
} |
|
|
|
#safe jumptable test |
|
#dict for {spanid spandata} $ordered_spans {} |
|
tcl::dict::for {spanid spandata} $ordered_spans { |
|
lassign [split $spanid ,] startcol hrow |
|
set memcols [tcl::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 [tcl::dict::keys $memcols] |
|
set hwidth [tcl::dict::get $spandata headerwidth] |
|
set num_cols_spanned [tcl::dict::size $memcols] |
|
if {$num_cols_spanned == 1} { |
|
set col [lindex $memcols 0] |
|
set space_to_alloc [expr {$hwidth - [tcl::dict::get $colwidths $col]}] |
|
if {$space_to_alloc > 0} { |
|
set maxwidth [tcl::dict::get $o_columndefs $col -maxwidth] |
|
if {$maxwidth ne ""} { |
|
if {$maxwidth > [tcl::dict::get $colwidths $col]} { |
|
set can_alloc [expr {$maxwidth - [tcl::dict::get $colwidths $col]}] |
|
} else { |
|
set can_alloc 0 |
|
} |
|
set will_alloc [expr {min($space_to_alloc,$can_alloc)}] |
|
} else { |
|
set will_alloc $space_to_alloc |
|
} |
|
if {$will_alloc} { |
|
#tcl::dict::set colwidths $col $hwidth |
|
tcl::dict::incr colwidths $col $will_alloc |
|
tcl::dict::set colspace_added $col $will_alloc |
|
} |
|
#log! |
|
#if {$will_alloc < $space_to_alloc} { |
|
# #todo - debug only |
|
# puts stderr "max width $maxwidth hit for col $col - cannot allocate extra [expr {$space_to_alloc - $will_alloc}]" |
|
#} |
|
} |
|
} elseif {$num_cols_spanned > 1} { |
|
set spannedwidth 0 |
|
foreach col $colids { |
|
incr spannedwidth [tcl::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 { |
|
least { |
|
#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 [tcl::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 |
|
} |
|
} |
|
tcl::dict::incr colwidths $colid |
|
tcl::dict::incr colspace_added $colid |
|
} |
|
} |
|
} |
|
least_unmaxed { |
|
#TODO - fix header truncation/overflow issues when they are restricted by column maxwidth |
|
#(we should be able to collapse column width to zero and have header colspans gracefully respond) |
|
#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 [tcl::dict::keys $ordered_colspace_added] |
|
set colid "" |
|
foreach testcolid $ordered_all_colids { |
|
set maxwidth [tcl::dict::get $o_columndefs $testcolid -maxwidth] |
|
set can_alloc [expr {$maxwidth eq "" || $maxwidth > [tcl::dict::get $colwidths $testcolid]}] |
|
if {$testcolid in $colids} { |
|
if {$can_alloc} { |
|
set colid $testcolid |
|
break |
|
} else { |
|
#remove from future consideration in for loop |
|
#log! |
|
#puts stderr "max width $maxwidth hit for col $testcolid" |
|
tcl::dict::unset colspace_added $testcolid |
|
} |
|
} |
|
} |
|
if {$colid ne ""} { |
|
tcl::dict::incr colwidths $colid |
|
tcl::dict::incr colspace_added $colid |
|
} |
|
} |
|
} |
|
} |
|
all { |
|
#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 [tcl::dict::keys $ordered_colspace_added] |
|
|
|
foreach col $ordered_colids { |
|
tcl::dict::incr colwidths $col |
|
tcl::dict::incr colspace_added $col |
|
incr space_to_alloc -1 |
|
if {$space_to_alloc == 0} { |
|
break |
|
} |
|
} |
|
} |
|
|
|
} |
|
} |
|
} |
|
} |
|
|
|
set column_widths [tcl::dict::values $colwidths] |
|
#todo - -maxwidth etc |
|
set table_minwidth [tcl::dict::get $o_opts_table -minwidth] ;#min width including frame elements |
|
if {[tcl::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 {[tcl::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 [tcl::dict::keys $ordered_colspace_added] |
|
|
|
foreach col $ordered_colids { |
|
tcl::dict::incr colwidths $col |
|
tcl::dict::incr colspace_added $col |
|
incr space_to_alloc -1 |
|
if {$space_to_alloc == 0} { |
|
break |
|
} |
|
} |
|
} |
|
set column_widths [tcl::dict::values $colwidths] |
|
} |
|
|
|
} |
|
|
|
return [list ordered_spans $ordered_spans colwidths $column_widths adjustments $colspace_added] |
|
} |
|
|
|
#spangroups keyed by column |
|
method spangroups {} { |
|
#*** !doctools |
|
#[call class::table [method spangroups]] |
|
#[para] return a dict keyed by column-index showing advanced span information |
|
#[para] (debug tool) |
|
|
|
set column_count [tcl::dict::size $o_columndefs] |
|
set spangroups [tcl::dict::create] |
|
set headerwidths [tcl::dict::create] ;#key on col,hrow |
|
foreach c [tcl::dict::keys $o_columndefs] { |
|
tcl::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 [tcl::dict::get $spaninfo startcol] |
|
set hrow [tcl::dict::get $spaninfo hrow] |
|
set header [lindex [tcl::dict::get $o_columndefs $hcol -headers] $hrow] |
|
if {[tcl::dict::exists $headerwidths $hcol,$hrow]} { |
|
set hwidth [tcl::dict::get $headerwidths $hcol,$hrow] |
|
} else { |
|
set hwidth [textblock::width $header] |
|
tcl::dict::set headerwidths $hcol,$hrow $hwidth |
|
} |
|
lappend spaninfo headerwidth $hwidth |
|
lappend sgroup $spaninfo |
|
} |
|
set spanlengths [tcl::dict::get $spangroups $c spanlengths] |
|
lappend spanlengths $spanlen |
|
tcl::dict::set spangroups $c spanlengths $spanlengths |
|
tcl::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 [tcl::dict::get $o_columndefs $cidx -header_colspans] |
|
} |
|
method column_get_spaninfo {cidx} { |
|
set spans_by_header [my header_colspans] |
|
set colspans_for_column [tcl::dict::get $o_columndefs $cidx -header_colspans] |
|
set spaninfo [list] |
|
set numcols [tcl::dict::size $o_columndefs] |
|
#note that 'any' can occur in positions other than column 0 - meaning any remaining until next non-zero span |
|
tcl::dict::for {hrow rawspans} $spans_by_header { |
|
set thiscol_spanval [lindex $rawspans $cidx] |
|
if {$thiscol_spanval eq "any" || $thiscol_spanval > 0} { |
|
set spanstartcol $cidx ;#own column |
|
if {$thiscol_spanval eq "any"} { |
|
#scan right to first non-zero to get actual length of 'any' span |
|
#REVIEW! |
|
set spanlen 1 |
|
for {set i [expr {$cidx+1}]} {$i < $numcols} {incr i} { |
|
#abort at next any or number or empty string |
|
if {[lindex $rawspans $i] ne "0"} { |
|
break |
|
} |
|
incr spanlen |
|
} |
|
#set spanlen [expr {$numcols - $cidx}] |
|
} else { |
|
set spanlen $thiscol_spanval |
|
} |
|
} else { |
|
#look left til we see an any or a non-zero value |
|
for {set i $cidx} {$i > -1} {incr i -1} { |
|
set s [lindex $rawspans $i] |
|
if {$s eq "any" || $s > 0} { |
|
set spanstartcol $i |
|
if {$s eq "any"} { |
|
#REVIEW! |
|
#set spanlen [expr {$numcols - $i}] |
|
set spanlen 1 |
|
#now scan right to see how long the 'any' actually is |
|
for {set j [expr {$i+1}]} {$j < $numcols} {incr j} { |
|
if {[lindex $rawspans $j] ne "0"} { |
|
break |
|
} |
|
incr spanlen |
|
} |
|
} 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 [tcl::dict::size $o_columndefs] |
|
|
|
set opts [tcl::dict::create\ |
|
-algorithm $o_column_width_algorithm\ |
|
] |
|
foreach {k v} $args { |
|
switch -- $k { |
|
-algorithm { |
|
tcl::dict::set opts $k $v |
|
} |
|
default { |
|
error "Unknown option '$k'. Known options: [tcl::dict::keys $opts]" |
|
} |
|
} |
|
} |
|
set opt_algorithm [tcl::dict::get $opts -algorithm] |
|
#puts stderr "--- recalculating column widths -algorithm $opt_algorithm" |
|
set known_algorithms [list basic simplistic span span2] |
|
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 least] |
|
set calcresult [my columncalc_spans least_unmaxed] |
|
set o_calculated_column_widths [tcl::dict::get $calcresult colwidths] |
|
} |
|
span2 { |
|
#allocates more evenly - but truncates headers sometimes |
|
set calcresult [my columncalc_spans all] |
|
set o_calculated_column_widths [tcl::dict::get $calcresult colwidths] |
|
} |
|
default { |
|
error "calculate_column_widths unknown algorithm $opt_algorithm. Known algorithms: $known_algorithms" |
|
} |
|
} |
|
#remember the last algorithm used |
|
set o_column_width_algorithm $opt_algorithm |
|
return $o_calculated_column_widths |
|
} |
|
method print2 {args} { |
|
variable full_column_cache |
|
set full_column_cache [tcl::dict::create] |
|
|
|
if {![llength $args]} { |
|
set cols [tcl::dict::keys $o_columndata] |
|
} else { |
|
set cols [list] |
|
foreach colspec $args { |
|
set allcols [tcl::dict::keys $o_columndata] |
|
if {[tcl::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 {[tcl::dict::exists $full_column_cache $c]} { |
|
#puts "!!print used full_column_cache for $c" |
|
set columninfo [tcl::dict::get $full_column_cache $c] |
|
} else { |
|
set columninfo [my get_column_by_index $c -return dict {*}$flags] |
|
tcl::dict::set full_column_cache $c $columninfo |
|
} |
|
set nextcol [tcl::string::cat [tcl::dict::get $columninfo header] \n [tcl::dict::get $columninfo body]] |
|
set bodywidth [tcl::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 -expand_right 1 -transparent $TSUB $table[unset table] $nextcol] |
|
#JMN |
|
|
|
#set nextcol [textblock::join -- [textblock::block $padwidth $height "\uFFFF"] $nextcol] |
|
#set table [overtype::renderspace -expand_right 1 -transparent \uFFFF $table $nextcol] |
|
} |
|
incr padwidth $bodywidth |
|
incr colposn |
|
} |
|
|
|
if {[llength $cols]} { |
|
#return [textblock::join -- {*}$blocks] |
|
if {[tcl::dict::get $o_opts_table -show_edge]} { |
|
#title is considered part of the edge ? |
|
set offset 1 ;#make configurable? |
|
set titlepad [tcl::string::repeat $TSUB $offset] |
|
if {[tcl::dict::get $o_opts_table -title] ne ""} { |
|
set titlealign [tcl::dict::get $o_opts_table -titlealign] |
|
switch -- $titlealign { |
|
left { |
|
set tstring $titlepad[tcl::dict::get $o_opts_table -title] |
|
} |
|
right { |
|
set tstring [tcl::dict::get $o_opts_table -title]$titlepad |
|
} |
|
default { |
|
set tstring [tcl::dict::get $o_opts_table -title] |
|
} |
|
} |
|
set opt_titletransparent [tcl::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 [tcl::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" |
|
} |
|
} |
|
|
|
# using -startcolumn to do slightly less work |
|
method print3 {args} { |
|
if {![llength $args]} { |
|
set cols [tcl::dict::keys $o_columndata] |
|
} else { |
|
set cols [list] |
|
foreach colspec $args { |
|
set allcols [tcl::dict::keys $o_columndata] |
|
if {[tcl::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 numposns [llength $cols] |
|
set colposn 0 |
|
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] |
|
} |
|
set columninfo [my get_column_by_index $c -return dict {*}$flags] |
|
set nextcol [tcl::string::cat [tcl::dict::get $columninfo header] \n [tcl::dict::get $columninfo body]] |
|
set bodywidth [tcl::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 table [overtype::renderspace -startcolumn [expr {$padwidth + 1}] -expand_right 1 -transparent $TSUB $table $nextcol] |
|
} |
|
incr padwidth $bodywidth |
|
incr colposn |
|
} |
|
|
|
if {[llength $cols]} { |
|
#return [textblock::join -- {*}$blocks] |
|
if {[tcl::dict::get $o_opts_table -show_edge]} { |
|
#title is considered part of the edge ? |
|
set offset 1 ;#make configurable? |
|
set titlepad [tcl::string::repeat $TSUB $offset] |
|
if {[tcl::dict::get $o_opts_table -title] ne ""} { |
|
set titlealign [tcl::dict::get $o_opts_table -titlealign] |
|
switch -- $titlealign { |
|
left { |
|
set tstring $titlepad[tcl::dict::get $o_opts_table -title] |
|
} |
|
right { |
|
set tstring [tcl::dict::get $o_opts_table -title]$titlepad |
|
} |
|
default { |
|
set tstring [tcl::dict::get $o_opts_table -title] |
|
} |
|
} |
|
set opt_titletransparent [tcl::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 [tcl::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" |
|
} |
|
} |
|
|
|
#print headers and body using different join mechanisms |
|
# using -startcolumn to do slightly less work |
|
method print {args} { |
|
#*** !doctools |
|
#[call class::table [method print]] |
|
#[para] Return the table as text suitable for console display |
|
|
|
if {![llength $args]} { |
|
set cols [tcl::dict::keys $o_columndata] |
|
} else { |
|
set cols [list] |
|
foreach colspec $args { |
|
set allcols [tcl::dict::keys $o_columndata] |
|
if {[tcl::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 numposns [llength $cols] |
|
set colposn 0 |
|
set padwidth 0 |
|
set header_build "" |
|
set body_blocks [list] |
|
set headerheight 0 |
|
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] |
|
} |
|
set columninfo [my get_column_by_index $c -return dict {*}$flags] |
|
#set nextcol [tcl::dict::get $columninfo column] |
|
set bodywidth [tcl::dict::get $columninfo bodywidth] |
|
set headerheight [tcl::dict::get $columninfo headerheight] |
|
#set nextcol_lines [split $nextcol \n] |
|
#set nextcol_header [join [lrange $nextcol_lines 0 $headerheight-1] \n] |
|
#set nextcol_body [join [lrange $nextcol_lines $headerheight end] \n] |
|
set nextcol_header [tcl::dict::get $columninfo header] |
|
set nextcol_body [tcl::dict::get $columninfo body] |
|
|
|
if {$header_build eq "" && ![llength $body_blocks]} { |
|
set header_build $nextcol_header |
|
} else { |
|
if {$headerheight > 0} { |
|
set header_build [overtype::renderspace -startcolumn [expr {$padwidth + 1}] -expand_right 1 -transparent $TSUB $header_build[unset header_build] $nextcol_header[unset nextcol_header]] |
|
} |
|
#set body_build [textblock::join -- $body_build[unset body_build] $nextcol_body] |
|
} |
|
lappend body_blocks $nextcol_body |
|
incr padwidth $bodywidth |
|
incr colposn |
|
} |
|
if {![llength $body_blocks]} { |
|
set body_build "" |
|
} else { |
|
#body blocks should not be ragged - so can use join_basic |
|
set body_build [textblock::join_basic -- {*}$body_blocks] |
|
} |
|
if {$headerheight > 0} { |
|
set table [tcl::string::cat $header_build \n $body_build] |
|
} else { |
|
set table $body_build |
|
} |
|
|
|
if {[llength $cols]} { |
|
if {[tcl::dict::get $o_opts_table -show_edge]} { |
|
#title is considered part of the edge ? |
|
set offset 1 ;#make configurable? |
|
set titlepad [tcl::string::repeat $TSUB $offset] |
|
if {[tcl::dict::get $o_opts_table -title] ne ""} { |
|
set titlealign [tcl::dict::get $o_opts_table -titlealign] |
|
switch -- $titlealign { |
|
left { |
|
set tstring $titlepad[tcl::dict::get $o_opts_table -title] |
|
} |
|
right { |
|
set tstring [tcl::dict::get $o_opts_table -title]$titlepad |
|
} |
|
default { |
|
set tstring [tcl::dict::get $o_opts_table -title] |
|
} |
|
} |
|
set opt_titletransparent [tcl::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 [tcl::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 {} { |
|
#*** !doctools |
|
#[call class::table [method print_bodymatrix]] |
|
#[para] output the matrix string corresponding to the body data using the matrix 2string format |
|
#[para] this will be a table without borders,headers,title etc and will exclude additional ANSI applied due to table, row or column settings. |
|
#[para] If the original cell data itself contains ANSI - the output will still contain those ansi codes. |
|
# |
|
|
|
|
|
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 |
|
# |
|
tcl::namespace::eval textblock { |
|
variable frametypes |
|
set frametypes [list light light_b light_c heavy heavy_b heavy_c arc arc_b arc_c double block block1 block2 block2hack ascii altg] |
|
#class::table needs to be able to determine valid frametypes |
|
proc frametypes {} { |
|
variable frametypes |
|
return $frametypes |
|
} |
|
|
|
tcl::namespace::eval cd { |
|
#todo - save and restore existing tcl::namespace::export in case macros::cd has default exports in future |
|
tcl::namespace::eval ::term::ansi::code::macros::cd {tcl::namespace::export *} |
|
tcl::namespace::import ::term::ansi::code::macros::cd::* |
|
tcl::namespace::eval ::term::ansi::code::macros::cd {tcl::namespace::export -clear} |
|
} |
|
proc spantest {} { |
|
set t [list_as_table -columns 5 -return tableobject {a b c d e aa bb cc dd ee X Y}] |
|
$t configure_column 0 -headers [list span3 "1-span4\n2-span4 second line" span5/5 "span-all etc blah 123 hmmmmm" span2] |
|
$t configure_column 0 -header_colspans {3 4 5 any 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 |
|
} |
|
proc spantest1 {} { |
|
set t [list_as_table -columns 5 -return tableobject {a b c d e aa bb cc dd ee X Y}] |
|
$t configure_column 0 -headers [list "span3-any longer than other span any" "1-span4\n2-span4 second line" "span-any short aligned?" "span5/5 longest etc blah 123" span2] |
|
$t configure_column 0 -header_colspans {any 4 any 5 2} |
|
$t configure_column 2 -headers {"" "" "" "" c2span2_etc} |
|
$t configure_column 2 -header_colspans {0 0 0 0 2} |
|
$t configure_column 3 -header_colspans {1 0 0 0 0} |
|
$t configure -show_header 1 -ansiborder_header [a+ cyan] |
|
$t configure_column 0 -blockalign right ;#trigger KNOWN BUG overwrite of right edge by last char of spanning col (in this case fullspan from left - but also happens for any span extending to rhs) |
|
return $t |
|
} |
|
|
|
#more complex colspans |
|
proc spantest2 {} { |
|
set t [list_as_table -columns 5 -return tableobject {a b c d e aa bb cc dd ee X Y}] |
|
$t configure_column 0 -headers {c0span3 c0span4 c0span1 "c0span-all etc blah 123 hmmmmm" c0span2} |
|
$t configure_column 0 -header_colspans {3 4 1 any 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 -columns 5 -return tableobject {a b c d e aa bb cc dd ee X Y}] |
|
$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 any 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 |
|
} |
|
|
|
punk::args::definition { |
|
@id -id ::textblock::periodic |
|
@cmd -name textblock::periodic -help "A rudimentary periodic table |
|
This is primarily a test of textblock::class::table" |
|
|
|
-return -default table\ |
|
-choices {table tableobject}\ |
|
-help "default choice 'table' returns the displayable table output" |
|
-compact -default 1 -type boolean -help "compact true defaults: -show_vseps 0 -show_header 0 -show_edge 0" |
|
-frame -default 1 -type boolean |
|
-show_vseps -default "" -type boolean |
|
-show_header -default "" -type boolean |
|
-show_edge -default "" -type boolean |
|
-forcecolour -default 0 -type boolean -help "If punk::colour is off - this enables the produced table to still be ansi coloured" |
|
@values -min 0 -max 0 |
|
} |
|
|
|
proc periodic {args} { |
|
#For an impressive interactive terminal app (javascript) |
|
# see: https://github.com/spirometaxas/periodic-table-cli |
|
set opts [dict get [punk::args::get_by_id ::textblock::periodic $args] opts] |
|
set opt_return [tcl::dict::get $opts -return] |
|
if {[tcl::dict::get $opts -forcecolour]} { |
|
set fc forcecolour |
|
} else { |
|
set fc "" |
|
} |
|
|
|
#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 [tcl::dict::create] |
|
|
|
set cat_alkaline_earth [list Be Mg Ca Sr Ba Ra] |
|
set ansi [a+ {*}$fc web-black Web-gold] |
|
set val [list ansi $ansi cat alkaline_earth] |
|
foreach e $cat_alkaline_earth { |
|
tcl::dict::set ecat $e $val |
|
} |
|
|
|
set cat_reactive_nonmetal [list H C N O F P S Cl Se Br I] |
|
#set ansi [a+ {*}$fc web-black Web-lightgreen] |
|
set ansi [a+ {*}$fc black Term-113] |
|
set val [list ansi $ansi cat reactive_nonmetal] |
|
foreach e $cat_reactive_nonmetal { |
|
tcl::dict::set ecat $e $val |
|
} |
|
|
|
set cat [list Li Na K Rb Cs Fr] |
|
#set ansi [a+ {*}$fc web-black Web-Khaki] |
|
set ansi [a+ {*}$fc black Term-lightgoldenrod2] |
|
set val [list ansi $ansi cat alkali_metals] |
|
foreach e $cat { |
|
tcl::dict::set ecat $e $val |
|
} |
|
|
|
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+ {*}$fc web-black Web-lightsalmon] |
|
set ansi [a+ {*}$fc black Term-orange1] |
|
set val [list ansi $ansi cat transition_metals] |
|
foreach e $cat { |
|
tcl::dict::set ecat $e $val |
|
} |
|
|
|
set cat [list Al Ga In Sn Tl Pb Bi Po] |
|
set ansi [a+ {*}$fc web-black Web-lightskyblue] |
|
set val [list ansi $ansi cat post_transition_metals] |
|
foreach e $cat { |
|
tcl::dict::set ecat $e $val |
|
} |
|
|
|
set cat [list B Si Ge As Sb Te At] |
|
#set ansi [a+ {*}$fc web-black Web-turquoise] |
|
set ansi [a+ {*}$fc black Brightcyan] |
|
set val [list ansi $ansi cat metalloids] |
|
foreach e $cat { |
|
tcl::dict::set ecat $e $val |
|
} |
|
|
|
set cat [list He Ne Ar Kr Xe Rn] |
|
set ansi [a+ {*}$fc web-black Web-orchid] |
|
set val [list ansi $ansi cat noble_gases] |
|
foreach e $cat { |
|
tcl::dict::set ecat $e $val |
|
} |
|
|
|
set cat [list Ac Th Pa U Np Pu Am Cm Bk Cf Es Fm Md No Lr] |
|
set ansi [a+ {*}$fc web-black Web-plum] |
|
set val [list ansi $ansi cat actinoids] |
|
foreach e $cat { |
|
tcl::dict::set ecat $e $val |
|
} |
|
|
|
set cat [list La Ce Pr Nd Pm Sm Eu Gd Tb Dy Ho Er Tm Yb Lu] |
|
#set ansi [a+ {*}$fc web-black Web-tan] |
|
set ansi [a+ {*}$fc black Term-tan] |
|
set val [list ansi $ansi cat lanthanoids] |
|
foreach e $cat { |
|
tcl::dict::set ecat $e $val |
|
} |
|
|
|
set ansi [a+ {*}$fc web-black Web-whitesmoke] |
|
set val [list ansi $ansi cat other] |
|
foreach e [list Mt Ds Rg Cn Nh Fl Mc Lv Ts Og] { |
|
tcl::dict::set ecat $e $val |
|
} |
|
|
|
set elements1 [list] |
|
set RST [a+] |
|
foreach e $elements { |
|
if {[tcl::dict::exists $ecat $e]} { |
|
set ansi [tcl::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 -columns 19 -return tableobject $elements1] |
|
#(defaults to show_hseps 0) |
|
|
|
#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 |
|
} |
|
set ccount [$t column_count] |
|
for {set c 0} {$c < $ccount} {incr c} { |
|
$t configure_column $c -minwidth 3 |
|
} |
|
if {[tcl::dict::get $opts -compact]} { |
|
#compact defaults - but let explicit arguments override |
|
set conf [dict create -show_vseps 0 -show_header 0 -show_edge 0] |
|
} else { |
|
set conf [dict create -show_vseps 1 -show_header 1 -show_edge 1] |
|
} |
|
dict for {k v} $conf { |
|
if {[dict get $opts $k] ne ""} { |
|
dict set conf $k [dict get $opts $k] |
|
} |
|
} |
|
|
|
set moreopts [dict create\ |
|
-frametype_header light\ |
|
-ansiborder_header [a+ {*}$fc brightwhite]\ |
|
-ansibase_header [a+ {*}$fc Black]\ |
|
-ansibase_body [a+ {*}$fc Black]\ |
|
-ansiborder_body [a+ {*}$fc black]\ |
|
-frametype block |
|
] |
|
$t configure {*}[dict merge $conf $moreopts] |
|
|
|
#-ansiborder_header [a+ {*}$fc web-white]\ |
|
|
|
if {$opt_return eq "table"} { |
|
if {[dict get $opts -frame]} { |
|
#set output [textblock::frame -ansiborder [a+ {*}$fc Black web-cornflowerblue] -type heavy -title "[a+ {*}$fc Black] Periodic Table " [$t print]] |
|
#set output [textblock::frame -ansiborder [a+ {*}$fc Black term-deepskyblue2] -type heavy -title "[a+ {*}$fc Black] Periodic Table " [$t print]] |
|
set output [textblock::frame -checkargs 0 -ansiborder [a+ {*}$fc Black cyan] -type heavy -title "[a+ {*}$fc Black] Periodic Table " [$t print]] |
|
} else { |
|
set output [$t print] |
|
} |
|
$t destroy |
|
return $output |
|
} |
|
return $t |
|
} |
|
|
|
proc bookend_lines {block start {end "\x1b\[m"}} { |
|
set out "" |
|
foreach ln [split $block \n] { |
|
append out $start $ln $end \n |
|
} |
|
return [string range $out 0 end-1] |
|
} |
|
proc ansibase_lines {block {newprefix ""}} { |
|
set base "" |
|
set out "" |
|
if {$newprefix eq ""} { |
|
foreach ln [split $block \n] { |
|
set parts [punk::ansi::ta::split_codes $ln] |
|
if {[lindex $parts 0] eq ""} { |
|
if {[lindex $parts 1] ne "" && ![punk::ansi::codetype::is_sgr_reset [lindex $parts 1]]} { |
|
set base [lindex $parts 1] |
|
append out $base |
|
} else { |
|
append out $base |
|
} |
|
} else { |
|
#leading plaintext - maintain our base |
|
append out $base [lindex $parts 0] [lindex $parts 1] |
|
} |
|
|
|
set code_idx 3 |
|
foreach {pt code} [lrange $parts 2 end] { |
|
if {[punk::ansi::codetype::is_sgr_reset $code]} { |
|
set parts [linsert $parts $code_idx+1 $base] |
|
} |
|
incr code_idx 2 |
|
} |
|
append out {*}[lrange $parts 2 end] \n |
|
} |
|
return [string range $out 0 end-1] |
|
} else { |
|
set base $newprefix |
|
foreach ln [split $block \n] { |
|
set parts [punk::ansi::ta::split_codes $ln] |
|
set code_idx 1 |
|
set offset 0 |
|
foreach {pt code} $parts { |
|
if {$code_idx == 1} { |
|
#first pt & code |
|
if {$pt ne ""} { |
|
#leading plaintext |
|
set parts [linsert $parts 0 $base] |
|
incr offset |
|
} |
|
} |
|
if {[punk::ansi::codetype::is_sgr_reset $code]} { |
|
set parts [linsert $parts [expr {$code_idx+1+$offset}] $base] |
|
incr offset |
|
} |
|
incr code_idx 2 |
|
} |
|
append out {*}$parts \n |
|
} |
|
return [string range $out 0 end-1] |
|
} |
|
} |
|
|
|
set FRAMETYPES [textblock::frametypes] |
|
punk::args::definition [punk::lib::tstr -return string { |
|
@id -id ::textblock::list_as_table |
|
@cmd -name "textblock::list_as_table" -help\ |
|
"Display a list in a bordered table |
|
" |
|
|
|
-return -default table -choices {table tableobject} |
|
-frametype -default "light" -type dict -choices {${$FRAMETYPES}} -choicerestricted 0\ |
|
-help "frame type or dict for custom frame" |
|
-show_edge -default "" -type boolean\ |
|
-help "show outer border of table" |
|
-show_seps -default "" -type boolean |
|
-show_vseps -default "" -type boolean\ |
|
-help "Show vertical table separators" |
|
-show_hseps -default "" -type boolean\ |
|
-help "Show horizontal table separators |
|
(default 0 if no existing -table supplied)" |
|
-table -default "" -type string\ |
|
-help "existing table object to use" |
|
-colheaders -default "" -type list\ |
|
-help "list of lists. list of column header values. Outer list must match number of columns" |
|
-header -default "" -type list -multiple 1\ |
|
-help "Each supplied -header argument is a header row. |
|
The number of values for each must be <= number of columns" |
|
-show_header -type boolean\ |
|
-help "Whether to show a header row. |
|
Omit for unspecified/automatic, |
|
in which case it will display only if -headers list was supplied." |
|
-action -default "append" -choices {append replace}\ |
|
-help "row insertion method if existing -table is supplied |
|
if append is chosen the new values will always start at the first column" |
|
-columns -default "" -type integer\ |
|
-help "Number of table columns |
|
Will default to 2 if not using an existing -table object" |
|
|
|
@values -min 0 -max 1 |
|
datalist -default {} -type list -help "flat list of table cell values which will be wrapped based on -columns value" |
|
}] |
|
|
|
proc list_as_table {args} { |
|
set FRAMETYPES [textblock::frametypes] |
|
set argd [punk::args::get_by_id ::textblock::list_as_table $args] |
|
|
|
set opts [dict get $argd opts] |
|
set datalist [dict get $argd values datalist] |
|
|
|
set existing_table [dict get $opts -table] |
|
set opt_columns [dict get $opts -columns] |
|
set count [llength $datalist] |
|
|
|
set is_new_table 0 |
|
if {$existing_table ne ""} { |
|
if {[tcl::namespace::tail [info object class $existing_table]] ne "table"} { |
|
error "textblock::list_as_table error -table must be an existing table object (e.g set t \[textblock::class::table new\])" |
|
} |
|
set t $existing_table |
|
foreach prop {-frametype -show_edge -show_seps -show_vseps -show_hseps} { |
|
if {[tcl::dict::get $opts $prop] ne ""} { |
|
$t configure $prop [tcl::dict::get $opts $prop] |
|
} |
|
} |
|
if {[dict get $opts -action] eq "replace"} { |
|
$t row_clear |
|
} |
|
set cols [$t column_count] |
|
if {[tcl::string::is integer -strict $opt_columns]} { |
|
if {$opt_columns > $cols} { |
|
set extra [expr {$opt_columns - $cols}] |
|
for {set c 0} {$c < $extra} {incr c} { |
|
$t add_column |
|
} |
|
} elseif {$opt_columns < $cols} { |
|
#todo - auto add blank values in the datalist |
|
error "Number of columns requested: $opt_columns is less than existing number of columns $cols - not yet supported" |
|
} |
|
set cols [$t column_count] |
|
} |
|
} else { |
|
set is_new_table 1 |
|
set colheaders {} |
|
if {[tcl::dict::get $opts -colheaders] ne ""} { |
|
set colheaders [dict get $opts -colheaders] |
|
} else { |
|
set colheaders [list] |
|
} |
|
set r 0 |
|
foreach ch $colheaders { |
|
set rows [llength $ch] |
|
if {$r < $rows} { |
|
set r $rows |
|
} |
|
} |
|
if {[llength [tcl::dict::get $opts -header]]} { |
|
foreach hrow [tcl::dict::get $opts -header] { |
|
set c 0 |
|
foreach cell $hrow { |
|
if {[llength $colheaders] < $c+1} { |
|
lappend colheaders [lrepeat $r {}] |
|
} |
|
set colinfo [lindex $colheaders $c] |
|
if {$r > [llength $colinfo]} { |
|
set diff [expr {$r - [llength $colinfo]}] |
|
lappend colinfo {*}[lrepeat $diff {}] |
|
} |
|
lappend colinfo $cell |
|
lset colheaders $c $colinfo |
|
incr c |
|
} |
|
incr r |
|
} |
|
} |
|
|
|
|
|
if {[llength $colheaders] > 0} { |
|
if {![tcl::dict::exists $opts received -show_header]} { |
|
set show_header 1 |
|
} else { |
|
set show_header [tcl::dict::get $opts -show_header] |
|
} |
|
} else { |
|
if {![tcl::dict::exists $opts received -show_header]} { |
|
set show_header 0 |
|
} else { |
|
set show_header [tcl::dict::get $opts -show_header] |
|
} |
|
} |
|
|
|
if {[tcl::string::is integer -strict $opt_columns]} { |
|
set cols $opt_columns |
|
if {[llength $colheaders] && $cols != [llength $colheaders]} { |
|
error "list_as_table number of columns ($cols) doesn't match supplied number of headers ([llength $colheaders])" |
|
} |
|
} else { |
|
#review |
|
if {[llength $colheaders]} { |
|
set cols [llength $colheaders] |
|
} else { |
|
set cols 2 ;#seems a reasonable default |
|
} |
|
} |
|
#defaults for new table only |
|
#if {[tcl::dict::get $opts -show_seps] eq ""} { |
|
# tcl::dict::set opts -show_seps 1 |
|
#} |
|
if {[tcl::dict::get $opts -show_edge] eq ""} { |
|
tcl::dict::set opts -show_edge 1 |
|
} |
|
if {[tcl::dict::get $opts -show_vseps] eq ""} { |
|
tcl::dict::set opts -show_vseps 1 |
|
} |
|
if {[tcl::dict::get $opts -show_hseps] eq ""} { |
|
tcl::dict::set opts -show_hseps 0 |
|
} |
|
|
|
set t [textblock::class::table new\ |
|
-show_header $show_header\ |
|
-show_edge [tcl::dict::get $opts -show_edge]\ |
|
-frametype [tcl::dict::get $opts -frametype]\ |
|
-show_seps [tcl::dict::get $opts -show_seps]\ |
|
-show_vseps [tcl::dict::get $opts -show_vseps]\ |
|
-show_hseps [tcl::dict::get $opts -show_hseps]\ |
|
] |
|
if {[llength $colheaders]} { |
|
for {set c 0} {$c < $cols} {incr c} { |
|
$t add_column -headers [lindex $colheaders $c] |
|
} |
|
} else { |
|
for {set c 0} {$c < $cols} {incr c} { |
|
$t add_column -headers [list $c] |
|
} |
|
} |
|
} |
|
|
|
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 ""]] |
|
lappend row {*}[lrepeat $shortfall ""] |
|
} |
|
$t add_row $row |
|
} |
|
#puts stdout $rowdata |
|
if {[tcl::dict::get $opts -return] eq "table"} { |
|
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 tcl::string::length is ok |
|
if {[tcl::string::length $char] == 1} { |
|
set row [tcl::string::repeat $char $blockwidth] |
|
set mtrx [lrepeat $blockheight $row] |
|
return [::join $mtrx \n] |
|
} else { |
|
set charblock [tcl::string::map [list \r\n \n] $char] |
|
if {[tcl::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_basic -- {*}[lrepeat $blockwidth $charblock]] |
|
} else { |
|
set row $charblock |
|
} |
|
} else { |
|
set row [tcl::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 [list {*}[punk::lib::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 [tcl::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_basic -ansiresets 0 -- {*}$clist] |
|
} else { |
|
return [textblock::join_basic -- {*}$clist] |
|
} |
|
} elseif {"rainbow" in $colour} { |
|
#direction must be horizontal |
|
set block "" |
|
for {set r 0} {$r < $size} {incr r} { |
|
set colour2 [tcl::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 [tcl::string::trimright $block \n] |
|
return $block |
|
} else { |
|
#row first - |
|
set rows [list] |
|
foreach ch $charsubset { |
|
lappend rows [tcl::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 {[tcl::string::last \t $textblock] >= 0} { |
|
if {[tcl::info::exists punk::console::tabwidth]} { |
|
set tw $::punk::console::tabwidth |
|
} else { |
|
set tw 8 |
|
} |
|
set textblock [textutil::tabify::untabify2 $textblock $tw] |
|
} |
|
if {[string length $textblock] > 1 && [punk::ansi::ta::detect $textblock]} { |
|
#ansistripraw slightly faster than ansistrip - and won't affect width (avoid detect_g0/conversions) |
|
set textblock [punk::ansi::ansistripraw $textblock] |
|
} |
|
if {[tcl::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] |
|
} |
|
#gather info about whether ragged (samewidth each line = false) and min width |
|
proc widthinfo {textblock} { |
|
#backspaces, vertical tabs ? |
|
if {$textblock eq ""} { |
|
return [dict create width 0 minwidth 0 ragged 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 {[tcl::string::last \t $textblock] >= 0} { |
|
if {[tcl::info::exists punk::console::tabwidth]} { |
|
set tw $::punk::console::tabwidth |
|
} else { |
|
set tw 8 |
|
} |
|
set textblock [textutil::tabify::untabify2 $textblock $tw] |
|
} |
|
if {[string length $textblock] > 1 && [punk::ansi::ta::detect $textblock]} { |
|
#ansistripraw slightly faster than ansistrip - and won't affect width (avoid detect_g0/conversions) |
|
set textblock [punk::ansi::ansistripraw $textblock] |
|
} |
|
if {[tcl::string::last \n $textblock] >= 0} { |
|
#return [tcl::mathfunc::max {*}[lmap v [split $textblock \n] {::punk::char::ansifreestring_width $v}]] |
|
set widths [lmap v [split $textblock \n] {::punk::char::ansifreestring_width $v}] |
|
set max [tcl::mathfunc::max {*}$widths] |
|
set min [tcl::mathfunc::min {*}$widths] |
|
set ragged [expr {$min != $max}] |
|
return [dict create width $max minwidth $min ragged $ragged] |
|
} |
|
#single line |
|
set w [punk::char::ansifreestring_width $textblock] |
|
return [dict create width $w minwidth $w ragged 0] |
|
} |
|
#when we know the block is uniform in width - just examine topline |
|
proc widthtopline {textblock} { |
|
set firstnl [tcl::string::first \n $textblock] |
|
if {$firstnl >= 0} { |
|
set tl [tcl::string::range $textblock 0 $firstnl] |
|
} else { |
|
set tl $textblock |
|
} |
|
if {[punk::ansi::ta::detect $tl]} { |
|
set tl [punk::ansi::ansistripraw $tl] |
|
} |
|
return [punk::char::ansifreestring_width $tl] |
|
} |
|
#uses tcl's tcl::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] {tcl::string::length $v}] |
|
set max 0 |
|
foreach ln [split $textblock \n] { |
|
if {[tcl::string::length $ln] > $max} {set max [tcl::string::length $ln]} |
|
} |
|
return $max |
|
} |
|
#*slightly* slower |
|
#proc string_length_line_max {textblock} { |
|
# tcl::mathfunc::max {*}[lmap v [split $textblock \n] {tcl::string::length $v}] |
|
#} |
|
proc string_length_line_min textblock { |
|
tcl::mathfunc::min {*}[lmap v [split $textblock \n] {tcl::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 {[tcl::string::length $textblock]-[tcl::string::length [tcl::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 [tcl::dict::create width 0 height 1] ;#no such thing as zero-height block - for consistency with non-empty strings having no line-endings |
|
} |
|
#strangely - tcl::string::last (windows tcl8.7 anway) is faster than tcl::string::first for large strings when the needle not in the haystack |
|
if {[tcl::string::last \t $textblock] >= 0} { |
|
if {[tcl::info::exists punk::console::tabwidth]} { |
|
set tw $::punk::console::tabwidth |
|
} else { |
|
set tw 8 |
|
} |
|
set textblock [textutil::tabify::untabify2 $textblock $tw] |
|
} |
|
#ansistripraw on entire block in one go rather than line by line - result should be the same - review - make tests |
|
if {[string length $textblock] > 1 && [punk::ansi::ta::detect $textblock]} { |
|
set textblock [punk::ansi::ansistripraw $textblock] |
|
} |
|
if {[tcl::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 {[tcl::string::length $textblock]-[tcl::string::length [tcl::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 [tcl::dict::create width $width height $height] ;#maintain order in 'image processing' standard width then height - caller may use lassign [tcl::dict::values [blocksize <data>]] width height |
|
} |
|
proc size2 {textblock} { |
|
if {$textblock eq ""} { |
|
return [tcl::dict::create width 0 height 1] ;#no such thing as zero-height block - for consistency with non-empty strings having no line-endings |
|
} |
|
#strangely - tcl::string::last (windows tcl8.7 anway) is faster than tcl::string::first for large strings when the needle not in the haystack |
|
if {[tcl::string::last \t $textblock] >= 0} { |
|
if {[tcl::info::exists punk::console::tabwidth]} { |
|
set tw $::punk::console::tabwidth |
|
} else { |
|
set tw 8 |
|
} |
|
set textblock [textutil::tabify::untabify2 $textblock $tw] |
|
} |
|
#ansistripraw on entire block in one go rather than line by line - result should be the same - review - make tests |
|
if {[string length $textblock] > 1 && [punk::ansi::ta::detect $textblock]} { |
|
set textblock [punk::ansi::ansistripraw $textblock] |
|
} |
|
if {[tcl::string::last \n $textblock] >= 0} { |
|
#set width [tcl::mathfunc::max {*}[lmap v [punk::lib::lines_as_list -- $textblock] {::punk::char::ansifreestring_width $v}]] |
|
set lines [split $textblock \n] |
|
set num_le [expr {[llength $lines]-1}] |
|
#set width [tcl::mathfunc::max {*}[lmap v $lines {::punk::char::ansifreestring_width $v}]] |
|
set width 0 |
|
foreach ln $lines { |
|
set w [::punk::char::ansifreestring_width $ln] |
|
if {$w > $width} { |
|
set width $w |
|
} |
|
} |
|
} else { |
|
set num_le 0 |
|
set width [punk::char::ansifreestring_width $textblock] |
|
} |
|
#set num_le [expr {[tcl::string::length $textblock]-[tcl::string::length [tcl::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 [tcl::dict::create width $width height $height] ;#maintain order in 'image processing' standard width then height - caller may use lassign [tcl::dict::values [blocksize <data>]] width height |
|
} |
|
proc size_as_opts {textblock} { |
|
set sz [size $textblock] |
|
return [dict create -width [dict get $sz width] -height [dict get $sz height]] |
|
} |
|
proc size_as_list {textblock} { |
|
set sz [size $textblock] |
|
return [list [dict get $sz width] [dict get $sz 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 {[tcl::info::exists punk::console::tabwidth]} { |
|
set tw $::punk::console::tabwidth |
|
} else { |
|
set tw 8 |
|
} |
|
set block [textutil::tabify::untabify2 $block $tw] |
|
if {[tcl::string::last \n $block] >= 0} { |
|
return [tcl::mathfunc::max {*}[lmap v [punk::lib::lines_as_list -- $block] {::punk::char::string_width [ansistrip $v]}]] |
|
} |
|
if {[catch {llength $block}]} { |
|
return [::punk::char::string_width [ansistrip $block]] |
|
} |
|
if {[llength $block] == 0} { |
|
#could be just a whitespace string |
|
return [tcl::string::length $block] |
|
} |
|
return [tcl::mathfunc::max {*}[lmap v $block {::punk::char::string_width [ansistrip $v]}]] |
|
} |
|
|
|
#we shouldn't make textblock depend on the punk pipeline system |
|
#pipealias ::textblock::padleft .= {list $input [tcl::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 [tcl::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 opts [tcl::dict::create\ |
|
-padchar " "\ |
|
-which "right"\ |
|
-known_blockwidth ""\ |
|
-known_samewidth ""\ |
|
-known_hasansi ""\ |
|
-width ""\ |
|
-overflow 0\ |
|
-within_ansi 0\ |
|
] |
|
#known_samewidth of empty string means we don't know either way, 0 is definitely 'ragged', 1 is definitely homogenous |
|
|
|
#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 block ?-padchar <sp>|<character>? ?-which right|left|centre? ?-known_hasansi \"\"|<bool>? ?-known_blockwidth \"\"|<int>? ?-width auto|<int>? ?-within_ansi 1|0?" |
|
foreach {k v} $args { |
|
switch -- $k { |
|
-padchar - -which - -known_hasansi - -known_samewidth - -known_blockwidth - -width - -overflow - -within_ansi { |
|
tcl::dict::set opts $k $v |
|
} |
|
default { |
|
error "textblock::pad unrecognised option '$k'. Usage: $usage" |
|
} |
|
} |
|
} |
|
# -- --- --- --- --- --- --- --- --- --- |
|
set padchar [tcl::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 tcl::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 [tcl::string::tolower [tcl::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 [tcl::dict::get $opts -width] |
|
switch -- $opt_width { |
|
"" - auto { |
|
set width auto |
|
} |
|
default { |
|
if {![tcl::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 [tcl::dict::get $opts -within_ansi] |
|
switch -- $opt_withinansi { |
|
0 - 1 {} |
|
default { |
|
set opt_withinansi 2 |
|
} |
|
} |
|
# -- --- --- --- --- --- --- --- --- --- |
|
set known_blockwidth [tcl::dict::get $opts -known_blockwidth] |
|
set known_samewidth [tcl::dict::get $opts -known_samewidth] ;# if true - we have a known non-ragged block, if false - could be either. |
|
set datawidth "" |
|
if {$width eq "auto"} { |
|
#for auto - we |
|
if {$known_blockwidth eq ""} { |
|
if {$known_samewidth ne "" && $known_samewidth} { |
|
set datawidth [textblock::widthtopline $block] |
|
} else { |
|
#set datawidth [textblock::width $block] |
|
set widthinfo [textblock::widthinfo $block] |
|
set known_samewidth [expr {![dict get $widthinfo ragged]}] ;#review - a false value here is definite, distinguished from unknown which would be empty string - so we can take advantage of it |
|
set datawidth [dict get $widthinfo width] |
|
} |
|
} else { |
|
set datawidth $known_blockwidth |
|
} |
|
set width $datawidth ;# this is the width we want to pad out to |
|
#assert datawidth has been set to widest line, taking ansi & 2wide chars into account |
|
} else { |
|
#only call width functions if known_samewidth - otherwise let the pad algorithm below determine it as we go |
|
if {$known_samewidth ne "" && $known_samewidth} { |
|
if {$known_blockwidth eq ""} { |
|
set datawidth [textblock::widthtopline $block |
|
} else { |
|
set datawidth $known_blockwidth |
|
} |
|
} |
|
#assert datawidth may still be empty string |
|
} |
|
#assertion |
|
#we can now use the states of datawidth and known_samewidth to determine if algorithm below should calculate widths as it goes. |
|
|
|
set lines [list] |
|
|
|
set padcharsize [punk::ansi::printing_length $padchar] |
|
set pad_has_ansi [punk::ansi::ta::detect $padchar] |
|
if {$block eq ""} { |
|
#we need to treat as a line |
|
set repeats [expr {int(ceil($width / double($padcharsize)))}] ;#will overshoot by 1 whenever padcharsize not an exact divisor of width |
|
#TODO |
|
#review - what happens when padchar has ansi, or the width would split a double-wide unicode char? |
|
#we shouldn't be using string range if there is ansi - (overtype? ansistring range?) |
|
#we should use overtype with suitable replacement char (space?) for chopped double-wides |
|
if {!$pad_has_ansi} { |
|
return [tcl::string::range [tcl::string::repeat $padchar $repeats] 0 $width-1] |
|
} else { |
|
set base [tcl::string::repeat " " $width] |
|
return [overtype::block -blockalign left -overflow 0 $base [tcl::string::repeat $padchar $repeats]] |
|
} |
|
} |
|
|
|
#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 |
|
#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 [tcl::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 [tcl::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] |
|
# } |
|
|
|
#todo? special case trailing double-reset - insert between resets? |
|
set lnum 0 |
|
|
|
set known_hasansi [tcl::dict::get $opts -known_hasansi] |
|
if {$known_hasansi eq ""} { |
|
set block_has_ansi [punk::ansi::ta::detect $block] |
|
} else { |
|
set block_has_ansi $known_hasansi |
|
} |
|
if {$block_has_ansi} { |
|
set parts [punk::ansi::ta::split_codes $block] |
|
} else { |
|
#single plaintext part |
|
set parts [list $block] |
|
} |
|
|
|
set line_chunks [list] |
|
set line_len 0 |
|
set pad_cache [dict create] ;#key on value of 'missing' - which is width of required pad |
|
foreach {pt ansi} $parts { |
|
if {$pt ne ""} { |
|
set has_nl [expr {[tcl::string::last \n $pt]>=0}] |
|
if {$has_nl} { |
|
set pt [tcl::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] |
|
if {$known_samewidth eq "" || ($known_samewidth ne "" && !$known_samewidth) || $datawidth eq ""} { |
|
incr line_len [punk::char::grapheme_width_cached $pl] ;#memleak - REVIEW |
|
} |
|
if {$p != $last} { |
|
#do padding |
|
if {$known_samewidth eq "" || ($known_samewidth ne "" && !$known_samewidth) || $datawidth eq ""} { |
|
set missing [expr {$width - $line_len}] |
|
} else { |
|
set missing [expr {$width - $datawidth}] |
|
} |
|
if {$missing > 0} { |
|
#commonly in a block - many lines will have the same pad - cache based on missing |
|
|
|
#padchar may be more than 1 wide - because of 2wide unicode and or multiple chars |
|
if {[tcl::dict::exists $pad_cache $missing]} { |
|
set pad [tcl::dict::get $pad_cache $missing] |
|
} else { |
|
set repeats [expr {int(ceil($missing / double($padcharsize)))}] ;#will overshoot by 1 whenever padcharsize not an exact divisor of width |
|
if {!$pad_has_ansi} { |
|
set pad [tcl::string::range [tcl::string::repeat $padchar $repeats] 0 $missing-1] |
|
} else { |
|
set base [tcl::string::repeat " " $missing] |
|
set pad [overtype::block -blockalign left -overflow 0 $base [tcl::string::repeat $padchar $repeats]] |
|
} |
|
dict set pad_cache $missing $pad |
|
} |
|
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 |
|
if {$known_samewidth eq "" || ($known_samewidth ne "" && !$known_samewidth) || $datawidth eq ""} { |
|
set missing [expr {$width - $line_len}] |
|
} else { |
|
set missing [expr {$width - $datawidth}] |
|
} |
|
if {$missing > 0} { |
|
if {[tcl::dict::exists $pad_cache $missing]} { |
|
set pad [tcl::dict::get $pad_cache $missing] |
|
} else { |
|
set repeats [expr {int(ceil($missing / double($padcharsize)))}] ;#will overshoot by 1 whenever padcharsize not an exact divisor of width |
|
if {!$pad_has_ansi} { |
|
set pad [tcl::string::range [tcl::string::repeat $padchar $repeats] 0 $missing-1] |
|
} else { |
|
set base [tcl::string::repeat " " $missing] |
|
set pad [overtype::block -blockalign left -overflow 0 $base [tcl::string::repeat $padchar $repeats]] |
|
} |
|
} |
|
#set pad [tcl::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 -known_blockwidth $width -width $padtowidth -padchar . -which left -within_ansi 0] |
|
set left1 [textblock::pad $block -known_blockwidth $width -width $padtowidth -padchar . -which left -within_ansi 1] |
|
set left2 [textblock::pad $block -known_blockwidth $width -width $padtowidth -padchar . -which left -within_ansi 2] |
|
set right0 [textblock::pad $block -known_blockwidth $width -width $padtowidth -padchar . -which right -within_ansi 0] |
|
set right1 [textblock::pad $block -known_blockwidth $width -width $padtowidth -padchar . -which right -within_ansi 1] |
|
set right2 [textblock::pad $block -known_blockwidth $width -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 -columns 3 -return tableobject $testlist] |
|
$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 opts [tcl::dict::create\ |
|
-description ""\ |
|
-blockheaders ""\ |
|
] |
|
foreach {k v} $args { |
|
switch -- $k { |
|
-description - -blockheaders { |
|
tcl::dict::set opts $k $v |
|
} |
|
default { |
|
error "pad_test_blocklist unrecognised option '$k'. Known options: [tcl::dict::keys $opts]" |
|
} |
|
} |
|
} |
|
set opt_blockheaders [tcl::dict::get $opts -blockheaders] |
|
set bheaders [tcl::dict::create] |
|
if {$opt_blockheaders ne ""} { |
|
set b 0 |
|
foreach h $opt_blockheaders { |
|
if {$b < [llength $blocklist]} { |
|
tcl::dict::set bheaders $b $h |
|
} |
|
incr b |
|
} |
|
} |
|
|
|
set b 0 |
|
set blockinfo [tcl::dict::create] |
|
foreach block $blocklist { |
|
set width [textblock::width $block] |
|
tcl::dict::set blockinfo $b width $width |
|
set padtowidth [expr {$width + 3}] |
|
tcl::dict::set blockinfo $b left0 [textblock::pad $block -width $padtowidth -padchar . -which left -within_ansi 0] |
|
tcl::dict::set blockinfo $b left1 [textblock::pad $block -width $padtowidth -padchar . -which left -within_ansi 1] |
|
tcl::dict::set blockinfo $b left2 [textblock::pad $block -width $padtowidth -padchar . -which left -within_ansi 2] |
|
tcl::dict::set blockinfo $b right0 [textblock::pad $block -width $padtowidth -padchar . -which right -within_ansi 0] |
|
tcl::dict::set blockinfo $b right1 [textblock::pad $block -width $padtowidth -padchar . -which right -within_ansi 1] |
|
tcl::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 [tcl::dict::get $blockinfo $b left0]] [textblock::join [tcl::dict::get $blockinfo $b right0]] |
|
# lappend r1 [textblock::join [tcl::dict::get $blockinfo $b left1]] [textblock::join [tcl::dict::get $blockinfo $b right1]] |
|
# lappend r2 [textblock::join [tcl::dict::get $blockinfo $b left2]] [textblock::join [tcl::dict::get $blockinfo $b right2]] |
|
#} |
|
|
|
#2 - the more useful one? |
|
tcl::dict::for {b bdict} $blockinfo { |
|
lappend r0 [tcl::dict::get $blockinfo $b left0] [tcl::dict::get $blockinfo $b right0] |
|
lappend r1 [tcl::dict::get $blockinfo $b left1] [tcl::dict::get $blockinfo $b right1] |
|
lappend r2 [tcl::dict::get $blockinfo $b left2] [tcl::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 -columns [expr {1 + (2 * [tcl::dict::size $blockinfo])}] -return tableobject $rows] |
|
$t configure_column 0 -headers [list [tcl::dict::get $opts -description] "within_ansi"] -ansibase $column_ansi |
|
set col 1 |
|
tcl::dict::for {b bdict} $blockinfo { |
|
if {[tcl::dict::exists $bheaders $b]} { |
|
set hdr [tcl::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 [tcl::string::repeat " " $w1] $rhs [tcl::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 [tcl::string::repeat " " [width $lhs]] $rhs [tcl::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::get_dict { |
|
-ansiresets -default 1 -type integer |
|
blocks -type string -multiple 1 |
|
} $args] _l leaders _o opts _v values |
|
set blocks [tcl::dict::get $values blocks] |
|
|
|
set idx 0 |
|
set fordata [list] |
|
foreach b $blocks { |
|
set c($idx) [tcl::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] |
|
} |
|
|
|
|
|
punk::args::definition { |
|
@id -id ::textblock::join_basic |
|
@cmd -name textblock::join_basic -help\ |
|
"Join blocks of text line by line but don't add padding on each line to enforce uniform width. |
|
Already uniform blocks will join faster than textblock::join, and ragged blocks will join in a ragged manner |
|
" |
|
-- -type none -optional 0 -help "end of options marker -- is mandatory because joined blocks may easily conflict with flags" |
|
-ansiresets -type any -default auto |
|
blocks -type any -multiple 1 |
|
} |
|
|
|
#join without regard to each line length in a block (no padding added to make each block uniform) |
|
proc ::textblock::join_basic {args} { |
|
set argd [punk::args::get_by_id ::textblock::join_basic $args] |
|
set ansiresets [tcl::dict::get $argd opts -ansiresets] |
|
set blocks [tcl::dict::get $argd values blocks] |
|
|
|
#-ansireplays is always on (if ansi detected) |
|
|
|
# -- is a legimate block |
|
#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) |
|
|
|
if {![llength $blocks]} { |
|
return |
|
} |
|
set rowcount 0 |
|
set blocklists [list] |
|
foreach b $blocks { |
|
if {[punk::ansi::ta::detect $b]} { |
|
#-ansireplays 1 quite expensive e.g 7ms in 2024 |
|
set bl [punk::lib::lines_as_list -ansireplays 1 -ansiresets $ansiresets -- $b] |
|
} else { |
|
set bl [split $b \n] |
|
} |
|
if {[llength $bl] > $rowcount} { |
|
set rowcount [llength $bl] |
|
} |
|
lappend blocklists $bl |
|
} |
|
set outlines [list] |
|
for {set r 0} {$r < $rowcount} {incr r} { |
|
set row "" |
|
for {set c 0} {$c < [llength $blocks]} {incr c} { |
|
append row [lindex $blocklists $c $r] |
|
} |
|
lappend outlines $row |
|
} |
|
return [::join $outlines \n] |
|
} |
|
proc ::textblock::join_basic2 {args} { |
|
#@cmd -name textblock::join_basic -help "Join blocks line by line but don't add padding on each line to enforce uniform width. |
|
# Already uniform blocks will join faster than textblock::join, and ragged blocks will join in a ragged manner |
|
#" |
|
set argd [punk::args::get_dict { |
|
-- -type none -optional 0 -help "end of options marker -- is mandatory because joined blocks may easily conflict with flags" |
|
-ansiresets -type any -default auto |
|
blocks -type any -multiple 1 |
|
} $args] |
|
set ansiresets [tcl::dict::get $argd opts -ansiresets] |
|
set blocks [tcl::dict::get $argd values blocks] |
|
|
|
#-ansireplays is always on (if ansi detected) |
|
|
|
# -- is a legimate block |
|
#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) |
|
|
|
if {![llength $blocks]} { |
|
return |
|
} |
|
set idx 0 |
|
set fordata [list] |
|
set colindices [list] |
|
foreach b $blocks { |
|
if {[punk::ansi::ta::detect $b]} { |
|
lappend fordata "v($idx)" [punk::lib::lines_as_list -ansireplays 1 -ansiresets $ansiresets -- $b] |
|
} else { |
|
lappend fordata "v($idx)" [split $b \n] |
|
} |
|
lappend colindices $idx |
|
incr idx |
|
} |
|
set outlines [list] |
|
foreach {*}$fordata { |
|
set row {} |
|
foreach colidx $colindices { |
|
lappend row $v($colidx) |
|
} |
|
lappend outlines [::join $row ""] |
|
} |
|
return [::join $outlines \n] |
|
} |
|
#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} { |
|
#set argd [punk::args::get_dict { |
|
# blocks -type string -multiple 1 |
|
#} $args] |
|
#set opts [tcl::dict::get $argd opts] |
|
#set blocks [tcl::dict::get $argd 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] |
|
set ansiresets [lindex $args 1] |
|
} else { |
|
error "end of opts marker -- is mandatory." |
|
} |
|
} |
|
default { |
|
if {[catch {tcl::prefix::match {-ansiresets} [lindex $args 0]} fullopt]} { |
|
error "first flag must be -ansiresets or end of opts marker --" |
|
} else { |
|
if {[lindex $args 2] eq "--"} { |
|
set blocks [lrange $args 3 end] |
|
set ansiresets [lindex $args 1] |
|
} else { |
|
error "end of opts marker -- is mandatory" |
|
} |
|
} |
|
} |
|
} |
|
|
|
if {![llength $blocks]} { |
|
return |
|
} |
|
|
|
set idx 0 |
|
set blocklists [list] |
|
set rowcount 0 |
|
foreach b $blocks { |
|
#we need the width of a rendered block for per-row renderline calls or padding |
|
#we may as well use widthinfo to also determine raggedness state to pass on to pad function |
|
#set bwidth [width $b] |
|
set widthinfo [widthinfo $b] |
|
set bwidth [dict get $widthinfo width] |
|
set is_samewidth [expr {![dict get $widthinfo ragged]}] |
|
|
|
#set c($idx) [tcl::string::repeat " " [set w($idx)]] |
|
#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. |
|
|
|
#blocks passed to join can be ragged - so we can't pass -known_samewidth to pad |
|
if {[punk::ansi::ta::detect $b]} { |
|
# - we need to join to use pad - even though we then need to immediately resplit REVIEW (make line list version of pad?) |
|
set replay_block [::join [punk::lib::lines_as_list -ansireplays 1 -ansiresets $ansiresets -- $b] \n] |
|
set bl [split [textblock::pad $replay_block -known_hasansi 1 -known_samewidth $is_samewidth -known_blockwidth $bwidth -width $bwidth -which right -padchar " "] \n] |
|
} 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 |
|
set bl [split [textblock::pad $b -known_hasansi 0 -known_samewidth $is_samewidth -known_blockwidth $bwidth -width $bwidth -which right -padchar " "] \n] |
|
} |
|
set rowcount [expr {max($rowcount,[llength $bl])}] |
|
lappend blocklists $bl |
|
set width($idx) $bwidth |
|
incr idx |
|
} |
|
|
|
set outlines [list] |
|
for {set r 0} {$r < $rowcount} {incr r} { |
|
set row "" |
|
for {set c 0} {$c < [llength $blocklists]} {incr c} { |
|
set cell [lindex $blocklists $c $r] |
|
if {$cell eq ""} { |
|
set cell [string repeat " " $width($c)] |
|
} |
|
append row $cell |
|
} |
|
lappend outlines $row |
|
} |
|
return [::join $outlines \n] |
|
} |
|
|
|
proc ::textblock::join2 {args} { |
|
#set argd [punk::args::get_dict { |
|
# blocks -type string -multiple 1 |
|
#} $args] |
|
#set opts [tcl::dict::get $argd opts] |
|
#set blocks [tcl::dict::get $argd 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] |
|
set ansiresets [lindex $args 1] |
|
} else { |
|
error "end of opts marker -- is mandatory." |
|
} |
|
} |
|
default { |
|
if {[catch {tcl::prefix::match {-ansiresets} [lindex $args 0]} fullopt]} { |
|
error "first flag must be -ansiresets or end of opts marker --" |
|
} else { |
|
if {[lindex $args 2] eq "--"} { |
|
set blocks [lrange $args 3 end] |
|
set ansiresets [lindex $args 1] |
|
} else { |
|
error "end of opts marker -- is mandatory" |
|
} |
|
} |
|
} |
|
} |
|
|
|
if {![llength $blocks]} { |
|
return |
|
} |
|
|
|
set idx 0 |
|
set fordata [list] |
|
set colindices [list] |
|
foreach b $blocks { |
|
set w($idx) [width $b] ;#we need the width of a rendered block for per-row renderline calls or padding |
|
#set c($idx) [tcl::string::repeat " " [set w($idx)]] |
|
#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] |
|
|
|
# - we need to join to use pad - even though we then need to immediately resplit REVIEW (make line list version of pad?) |
|
set replay_block [::join [punk::lib::lines_as_list -ansireplays 1 -ansiresets $ansiresets -- $b] \n] |
|
lappend fordata "v($idx)" [split [textblock::pad $replay_block -known_hasansi 1 -known_blockwidth $w($idx) -width $w($idx) -which right -padchar " "] \n] |
|
} 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 fordata "v($idx)" [split [textblock::pad $b -known_hasansi 0 -known_blockwidth $w($idx) -width $w($idx) -which right -padchar " "] \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 " "] |
|
|
|
#short blocks need to have empty lines padded too |
|
if {$v($colidx) eq ""} { |
|
append row [string repeat " " $w($colidx)] |
|
} else { |
|
append row $v($colidx) |
|
} |
|
} |
|
lappend outlines $row |
|
} |
|
return [::join $outlines \n] |
|
} |
|
# This calls textblock::pad per cell :/ |
|
proc ::textblock::join3 {args} { |
|
#set argd [punk::args::get_dict { |
|
# blocks -type string -multiple 1 |
|
#} $args] |
|
#set opts [tcl::dict::get $argd opts] |
|
#set blocks [tcl::dict::get $argd 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] |
|
set ansiresets [lindex $args 1] |
|
} else { |
|
error "end of opts marker -- is mandatory." |
|
} |
|
} |
|
default { |
|
if {[catch {tcl::prefix::match {-ansiresets} [lindex $args 0]} fullopt]} { |
|
error "first flag must be -ansiresets or end of opts marker --" |
|
} else { |
|
if {[lindex $args 2] eq "--"} { |
|
set blocks [lrange $args 3 end] |
|
set ansiresets [lindex $args 1] |
|
} else { |
|
error "end of opts marker -- is mandatory" |
|
} |
|
} |
|
} |
|
} |
|
|
|
if {![llength $blocks]} { |
|
return |
|
} |
|
|
|
set idx 0 |
|
set fordata [list] |
|
set colindices [list] |
|
foreach b $blocks { |
|
set w($idx) [width $b] ;#we need the width of a rendered block for per-row renderline calls or padding |
|
#set c($idx) [tcl::string::repeat " " [set w($idx)]] |
|
#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 |
|
} |
|
#puts stderr "--->outlines len: [llength $outlines]" |
|
return [::join $outlines \n] |
|
} |
|
|
|
proc ::textblock::trim {block} { |
|
error "textblock::trim unimplemented" |
|
set trimlines "" |
|
} |
|
|
|
#pipealias ::textblock::join_right .= {list $lhs [tcl::string::repeat " " [width $lhs]] $rhs [tcl::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 welcome_test {} { |
|
package require punk::ansi |
|
set ansi [textblock::join -- " " [punk::ansi::ansicat src/testansi/publicdomain/roysac/roy-welc.ans 80x8]] |
|
# Ansi art courtesy of Carsten Cumbrowski aka Roy/SAC - roysac.com |
|
set table [[textblock::spantest] print] |
|
set punks [a+ web-lawngreen][>punk . lhs][a]\n\n[a+ rgb#FFFF00][>punk . rhs][a] |
|
set ipunks [overtype::renderspace -width [textblock::width $punks] [punk::ansi::enable_inverse]$punks] |
|
set testblock [textblock::testblock 15 rainbow] |
|
set contents $ansi\n[textblock::join -- " " $table " " $punks " " $testblock " " $ipunks " " $punks] |
|
set framed [textblock::frame -checkargs 0 -type arc -title [a+ cyan]Compositing[a] -subtitle [a+ red]ANSI[a] -ansiborder [a+ web-orange] $contents] |
|
} |
|
|
|
|
|
proc example {args} { |
|
set opts [tcl::dict::create -forcecolour 0] |
|
foreach {k v} $args { |
|
switch -- $k { |
|
-forcecolour { |
|
tcl::dict::set opts $k $v |
|
} |
|
default { |
|
error "textblock::example unrecognised option '$k'. Known-options: [tcl::dict::keys $opts]" |
|
} |
|
} |
|
} |
|
set opt_forcecolour 0 |
|
if {[tcl::dict::get $opts -forcecolour]} { |
|
set fc forcecolour |
|
set opt_forcecolour 1 |
|
} else { |
|
set fc "" |
|
} |
|
set pleft [>punk . rhs] |
|
set pright [>punk . lhs] |
|
set prightair [>punk . lhs_air] |
|
set red [a+ {*}$fc red]; set redb [a+ {*}$fc red bold] |
|
set green [a+ {*}$fc green]; set greenb [a+ {*}$fc green bold] |
|
set cyan [a+ {*}$fc cyan];set cyanb [a+ {*}$fc cyan bold] |
|
set blue [a+ {*}$fc blue];set blueb [a+ {*}$fc 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 -checkargs 0 $cpunks] [textblock::frame -checkargs 0 $punks]] |
|
append out $2frames_a \n |
|
set 2frames_b [textblock::join -- [textblock::frame -checkargs 0 -ansiborder $cyanb -title "plainpunks" $punks] [textblock::frame -checkargs 0 -ansiborder $greenb -title "fancypunks" $cpunks]] |
|
append out [textblock::frame -checkargs 0 -title "punks" $2frames_b\n$RST$2frames_a] \n |
|
set punkdeck [overtype::right [overtype::left [textblock::frame -checkargs 0 -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\nD\nE\nC\nK"] |
|
set spantable [[spantest] print] |
|
append out [textblock::join -- $punkdeck " " $spantable] \n |
|
#append out [textblock::frame -title gr $gr0] |
|
append out [textblock::periodic -forcecolour $opt_forcecolour] |
|
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} { |
|
#todo - use punk::args |
|
upvar ::textblock::class::opts_table_defaults toptdefaults |
|
set defaults [tcl::dict::create\ |
|
-rows [list]\ |
|
-headers [list]\ |
|
-return string\ |
|
] |
|
|
|
|
|
set defaults [tcl::dict::merge $defaults $toptdefaults] ;# -title -frametype -show_header etc |
|
set opts [tcl::dict::merge $defaults $args] |
|
# -- --- --- --- |
|
set opt_return [tcl::dict::get $opts -return] |
|
set opt_rows [tcl::dict::get $opts -rows] |
|
set opt_headers [tcl::dict::get $opts -headers] |
|
# -- --- --- --- |
|
set topts [tcl::dict::create] |
|
set toptkeys [tcl::dict::keys $toptdefaults] |
|
tcl::dict::for {k v} $opts { |
|
if {$k in $toptkeys} { |
|
tcl::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 |
|
} |
|
} |
|
|
|
proc frametype {f} { |
|
#set custom_keys [list hl hlt hlb vl vll vlr tlc trc blc brc] |
|
switch -- $f { |
|
light - light_b - light_c - heavy - heavy_b - heavy_c - arc - arc_b - arc_c - double - block - block1 - block2 - block2hack - ascii - altg { |
|
return [tcl::dict::create category predefined type $f] |
|
} |
|
default { |
|
set is_custom_dict_ok 1 |
|
if {[llength $f] %2 == 0} { |
|
#custom dict may leave out keys - but cannot have unknown keys |
|
foreach {k v} $f { |
|
switch -- $k { |
|
all - hl - hlt - hlb - vl - vll - vlr - tlc - trc - blc - brc {} |
|
hltj - hlbj - vllj - vlrj { |
|
#also allow extra join arguments |
|
} |
|
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: $::textblock::frametypes or a dictionary with any of keys all,hl,hlt,hlb,vl,vll,vlr,tlc,trc,blc,brc" |
|
} |
|
if {[dict exists $f all]} { |
|
return [tcl::dict::create category custom type $f] |
|
} else { |
|
set default_custom [tcl::dict::create hl " " vl " " tlc " " trc " " blc " " brc " "] |
|
set custom_frame [tcl::dict::merge $default_custom $f] |
|
return [tcl::dict::create category custom type $custom_frame] |
|
} |
|
} |
|
} |
|
} |
|
variable framedef_cache [tcl::dict::create] |
|
proc framedef {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 |
|
|
|
#we use the simplest cache_key possible - performance sensitive as called multiple times in table building. |
|
variable framedef_cache |
|
set cache_key $args |
|
if {[tcl::dict::exists $framedef_cache $cache_key]} { |
|
return [tcl::dict::get $framedef_cache $cache_key] |
|
} |
|
|
|
|
|
#here we avoid the punk::args usage on the happy path, even though punk::args is fairly fast, in favour of an even faster literal switch on the happy path |
|
#this means we have some duplication in where our flags/opts are defined: here in opts, and in spec below to give nicer error output without affecting performance. |
|
#It also means we can't specify checks on the option types etc |
|
set opts [tcl::dict::create\ |
|
-joins ""\ |
|
-boxonly 0\ |
|
] |
|
set bad_option 0 |
|
set values [list] |
|
for {set i 0} {$i < [llength $args]} {incr i} { |
|
set a [lindex $args $i] |
|
set a2 [tcl::prefix match -error "" {-- -joins -boxonly} $a] |
|
switch -- $a2 { |
|
-joins - -boxonly { |
|
tcl::dict::set opts $a2 [lindex $args [incr i]] |
|
} |
|
-- { |
|
set values [lrange $args $i+1 end] |
|
break |
|
} |
|
default { |
|
if {[string match -* $a]} { |
|
set bad_option 1 |
|
} else { |
|
set values [lrange $args $i end] |
|
} |
|
break |
|
} |
|
} |
|
} |
|
set f [lindex $values 0] |
|
set rawglobs [lrange $values 1 end] |
|
if {![llength $rawglobs] || "all" in $rawglobs || "*" in $rawglobs} { |
|
set globs * |
|
} else { |
|
set globs [list] |
|
foreach g $rawglobs { |
|
switch -- $g { |
|
hl - hlt - hlb - vl - vll - vlr - tlc - trc - blc - brc - |
|
hltj - hlbj - vllj - vlrj { |
|
lappend globs $g |
|
} |
|
corner - corners { |
|
lappend globs tlc blc trc brc |
|
} |
|
noncorner - noncorners { |
|
#same as verticals + horizontals |
|
lappend globs hl* vl* |
|
} |
|
vertical - verticals { |
|
#we don't consider the corners part of this |
|
lappend globs vl* |
|
} |
|
horizontal - horizontals { |
|
lappend globs hl* |
|
} |
|
top - tops { |
|
lappend globs tlc trc hlt* |
|
} |
|
bottom - bottoms { |
|
lappend globs blc brc hlb* |
|
} |
|
left - lefts - lhs { |
|
lappend globs tlc blc vll* |
|
} |
|
right - rights - rhs { |
|
lappend globs trc brc vlr* |
|
} |
|
default { |
|
#must look like a glob search if not one of the above |
|
if {[regexp {[*?\[\]]} $g]} { |
|
lappend globs $g |
|
} else { |
|
set bad_option 1 |
|
} |
|
} |
|
} |
|
} |
|
} |
|
if {$bad_option || [llength $values] == 0} { |
|
#no framedef supplied, or unrecognised opt seen |
|
set spec [string map [list <ftlist> $::textblock::frametypes] { |
|
@id -id ::textblock::framedef |
|
@cmd -name textblock::framedef\ |
|
-help "Return a dict of the elements that make up a frame border. |
|
May return a subset of available elements based on memberglob values." |
|
|
|
-joins -default "" -type list\ |
|
-help "List of join directions, any of: up down left right |
|
or those combined with another frametype e.g left-heavy down-light." |
|
-boxonly -default 0 -type boolean\ |
|
-help "-boxonly true restricts results to the corner,vertical and horizontal box elements |
|
It excludes the extra top and side join elements htlj,hlbj,vllj,vlrj." |
|
|
|
@values -min 1 |
|
frametype -choices "<ftlist>" -choiceprefix 0 -choicerestricted 0 -type dict\ |
|
-help "name from the predefined frametypes or an adhoc dictionary." |
|
memberglob -type globstring -optional 1 -multiple 1 -choiceprefix 0 -choicerestricted 0 -choices { |
|
corner noncorner top bottom vertical horizontal left right |
|
hl hlt hlb vsl vll vlr tlc trc blc brc hltj hlbj vllj vlrj |
|
}\ |
|
-help "restrict to keys matching memberglob." |
|
}] |
|
#append spec \n "frametype -help \"A predefined \"" |
|
punk::args::get_dict $spec $args |
|
return |
|
} |
|
|
|
set joins [tcl::dict::get $opts -joins] |
|
set boxonly [tcl::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 [tcl::dict::create left "" down "" right "" up ""] |
|
foreach jt $joins { |
|
lassign [split $jt -] direction target |
|
if {$target ne ""} { |
|
tcl::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 rawtarget [tcl::dict::get $join_targets $dir] |
|
lassign [split $rawtarget _] target ;# treat variants light light_b lightc the same |
|
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) |
|
|
|
|
|
switch -- $targetleft-$targetright { |
|
heavy-light { |
|
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) |
|
set vlrj \u251c;#right light (ltj) |
|
} |
|
heavy-other { |
|
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) |
|
} |
|
heavy-heavy { |
|
set vllj \u2525 ;# left heavy (rtj) |
|
set vlrj \u251d;#right heavy (ltj) |
|
set tlc \u252d ;# Left Heavy and Right Down Light (ttj) |
|
set blc \u2535 ;# Left Heavy and Right Up Light (btj) |
|
set trc \u252e ;#Right Heavy and Left Down Light (ttj) |
|
set brc \u2536 ;#Right Heavy and Left up Light (btj) |
|
} |
|
light-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) |
|
set vllj \u2524 ;# left light (rtj) |
|
} |
|
light-other { |
|
set vllj \u2524 ;# left light (rtj) |
|
} |
|
light-light { |
|
set vllj \u2524 ;# left light (rtj) |
|
set vlrj \u251c;#right light (ltj) |
|
} |
|
} |
|
#set vllj \u2525 ;# left heavy (rtj) |
|
#set vllj \u2524 ;# left light (rtj) |
|
#set vlrj \u251d;#right heavy (ltj) |
|
#set vlrj \u251c;#right light (ltj) |
|
} |
|
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) (+) |
|
} |
|
light_b { |
|
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 " " |
|
set hlbj " " |
|
set vllj " " |
|
set vlrj " " |
|
|
|
set arcframe [textblock::framedef -boxonly $boxonly -joins $joins light corner vll vlr vllj vlrj] |
|
tcl::dict::with arcframe {} ;#extract keys as vars |
|
} |
|
light_c { |
|
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 " " |
|
set hlbj " " |
|
set vllj " " |
|
set vlrj " " |
|
|
|
set arcframe [textblock::framedef -boxonly $boxonly -joins $joins light corner] |
|
tcl::dict::with arcframe {} ;#extract keys as vars |
|
} |
|
"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 [tcl::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) |
|
} |
|
} |
|
} |
|
heavy_b { |
|
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 " " |
|
set hlbj " " |
|
set vllj " " |
|
set vlrj " " |
|
|
|
set arcframe [textblock::framedef -boxonly $boxonly -joins $joins heavy corner vll vlr vllj vlrj] |
|
tcl::dict::with arcframe {} ;#extract keys as vars |
|
} |
|
heavy_c { |
|
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 " " |
|
set hlbj " " |
|
set vllj " " |
|
set vlrj " " |
|
|
|
set arcframe [textblock::framedef -boxonly $boxonly -joins $joins heavy corner] |
|
tcl::dict::with arcframe {} ;#extract keys as vars |
|
} |
|
"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 [tcl::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 tlc \U2566 ;# (ttj) |
|
#set blc [punk::char::charshort boxd_huhz] ;# (btj) |
|
set blc \u2569 ;# (btj) |
|
#from3 |
|
set trc [punk::char::charshort boxd_ddhz] ;# (ttj) |
|
set brc [punk::char::charshort boxd_duhz] ;# (btj) |
|
} |
|
left_up { |
|
#9 |
|
set tlc [punk::char::charshort boxd_dvhz] ;# (fwj) |
|
set trc [punk::char::charshort boxd_dvl] ;# (rtj) |
|
set blc [punk::char::charshort boxd_duhz] ;# (btj) |
|
set hltj \u2569 ;# (btj) |
|
set vllj \u2563 ;# (rtj) |
|
} |
|
right_up { |
|
#10 |
|
set tlc [punk::char::charshort boxd_dvr] ;# (ltj) |
|
set trc [punk::char::charshort boxd_dvhz] ;# (fwj) |
|
set brc [punk::char::charshort boxd_duhz] ;# (btj) |
|
set hltj \u2569 ;# (btj) |
|
set vlrj \u2560 ;# (ltj) |
|
} |
|
down_left_right { |
|
#11 |
|
set blc [punk::char::charshort boxd_dvhz] ;# (fwj) |
|
set brc [punk::char::charshort boxd_dvhz] ;# (fwj) |
|
set trc [punk::char::charshort boxd_ddhz] ;# (ttj) |
|
set tlc [punk::char::charshort boxd_ddhz] ;# (ttj) |
|
set hlbj \u2566 ;# (ttj) |
|
set vlrj \u2560 ;# (ltj) |
|
|
|
} |
|
down_left_up { |
|
#12 |
|
set tlc [punk::char::charshort boxd_dvhz] ;# (fwj) |
|
set blc [punk::char::charshort boxd_dvhz] ;# (fwj) |
|
set trc [punk::char::charshort boxd_dvl] ;# (rtj) |
|
set brc [punk::char::charshort boxd_dvl] ;# (rtj) |
|
set hltj \u2569 ;# (btj) |
|
set hlbj \u2566 ;# (ttj) |
|
|
|
} |
|
down_right_up { |
|
#13 |
|
set tlc [punk::char::charshort boxd_dvr] ;# (ltj) |
|
set blc [punk::char::charshort boxd_dvr] ;# (ltj) |
|
set trc [punk::char::charshort boxd_dvhz] ;# (fwj) |
|
set brc [punk::char::charshort boxd_dvhz] ;# (fwj) |
|
set hltj \u2569 ;# (btj) |
|
set hlbj \u2566 ;# (ttj) |
|
} |
|
left_right_up { |
|
#14 |
|
set tlc [punk::char::charshort boxd_dvhz] ;# (fwj) |
|
set trc [punk::char::charshort boxd_dvhz] ;# (fwj) |
|
set blc [punk::char::charshort boxd_duhz] ;# (btj) |
|
set brc [punk::char::charshort boxd_duhz] ;# (btj) |
|
set hltj \u2569 ;# (btj) |
|
|
|
} |
|
down_left_right_up { |
|
#15 |
|
set tlc [punk::char::charshort boxd_dvhz] ;# (fwj) |
|
set blc [punk::char::charshort boxd_dvhz] ;# (fwj) |
|
set trc [punk::char::charshort boxd_dvhz] ;# (fwj) |
|
set brc [punk::char::charshort boxd_dvhz] ;# (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 [tcl::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 blc \u2bce ;# from "Miscellaneous Symbols and Arrows" - positioned too far right |
|
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) |
|
} |
|
} |
|
} |
|
down_right { |
|
switch -- $targetdown-$targetright { |
|
self-self { |
|
#set brc \u2bce ;# from "Miscellaneous Symbols and Arrows" - positioned too far right |
|
set trc \u252c ;# (ttj) |
|
set blc \u2524 ;# (rtj) |
|
} |
|
} |
|
} |
|
} |
|
} |
|
arc_b { |
|
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 " " |
|
set hlbj " " |
|
set vllj " " |
|
set vlrj " " |
|
|
|
set arcframe [textblock::framedef -boxonly $boxonly -joins $joins arc corner vll vlr vllj vlrj] |
|
tcl::dict::with arcframe {} ;#extract keys as vars |
|
} |
|
arc_c { |
|
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 " " |
|
set hlbj " " |
|
set vllj " " |
|
set vlrj " " |
|
|
|
set arcframe [textblock::framedef -boxonly $boxonly -joins $joins arc corner] |
|
tcl::dict::with arcframe {} ;#extract keys as vars |
|
} |
|
block1 { |
|
#box drawing as far as we can go without extra blocks from legacy computing unicode block - which is unfortunately not commonly supported |
|
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 |
|
|
|
} |
|
block2 { |
|
#the resultant table will have text appear towards top of each box |
|
#with 'legacy' computing unicode block - hopefully these become more widely supported - as they are useful and fill in some gaps |
|
set hlt \u2594 ;# upper one eighth block |
|
set hlb \u2581 ;# lower one eighth block |
|
set vlr \u2595 ;# right one eighth block |
|
set vll \u258f ;# left one eighth block |
|
|
|
#some terminals (on windows as at 2024) miscount width of these single-width blocks internally |
|
#resulting in extra spacing that is sometimes well removed from the character itself (e.g at next ansi reset) |
|
#This was fixed in windows-terminal based systems (2021) but persists in others. |
|
#https://github.com/microsoft/terminal/issues/11694 |
|
set tlc \U1fb7d ;#legacy block |
|
set trc \U1fb7e ;#legacy block |
|
set blc \U1fb7c ;#legacy block |
|
set brc \U1fb7f ;#legacy block |
|
|
|
if {[punk::console::check::has_bug_legacysymbolwidth]} { |
|
#rather than totally fail on some mixed layout that happens to use block2 - just degrade it - but prevent alignment problems |
|
set sp \u00a0 ;#non breaking space (plain space may act transparent in some use cases) |
|
set tlc $sp |
|
set trc $sp |
|
set blc $sp |
|
set brc $sp |
|
} |
|
|
|
#horizontal and vertical bar joins |
|
set hltj $hlt |
|
set hlbj $hlb |
|
set vllj $vll |
|
set vlrj $vlr |
|
|
|
} |
|
block2hack { |
|
#the resultant table will have text appear towards top of each box |
|
#with 'legacy' computing unicode block - hopefully these become more widely supported - as they are useful and fill in some gaps |
|
set hlt \u2594 ;# upper one eighth block |
|
set hlb \u2581 ;# lower one eighth block |
|
set vlr \u2595 ;# right one eighth block |
|
set vll \u258f ;# left one eighth block |
|
|
|
#see comments in block2 regarding the problems in some terminals that this *may* hack around to some extent. |
|
#the caller probably only needs block2hack if block2 doesn't work |
|
|
|
#1) |
|
#review - this hack looks sort of promising - but overtype::renderline needs fixing ? |
|
#set tlc \U1fb7d\b ;#legacy block |
|
#set trc \U1fb7e\b ;#legacy block |
|
#set blc \U1fb7c\b ;#legacy block |
|
#set brc \U1fb7f\b ;#legacy block |
|
|
|
#2) - works on cmd.exe and some others |
|
# a 'privacy message' is 'probably' also not supported on the old terminal but is on newer ones |
|
#known exception - conemu on windows - displays junk for various ansi codes - (and slow terminal anyway) |
|
#this hack has a reasonable chance of working |
|
#except that the punk overtype library does recognise PMs |
|
#A single backspace however is an unlikely and generally unuseful PM - so there is a corresponding hack in the renderline system to pass this PM through! |
|
#ugly - in that we don't know the application specifics of what the PM data contains and where it's going. |
|
set tlc \U1fb7d\x1b^\b\x1b\\ ;#legacy block |
|
set trc \U1fb7e\x1b^\b\x1b\\ ;#legacy block |
|
set blc \U1fb7c\x1b^\b\x1b\\ ;#legacy block |
|
set brc \U1fb7f\x1b^\b\x1b\\ ;#legacy 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 [tcl::dict::create hl " " vl " " tlc " " trc " " blc " " brc " "] ;#only default the general types - these form defaults for more specific types if they're missing |
|
set default_custom [tcl::dict::create hl " " vl " " tlc " " trc " " blc " " brc " "] |
|
if {"all" in [dict keys $f]} { |
|
set A [dict get $f all] |
|
set default_custom [tcl::dict::create hl $A vl $A tlc $A trc $A blc $A brc $A] |
|
} |
|
if {[llength $f] % 2} { |
|
#todo - retrieve usage from punk::args |
|
error "textblock::frametype frametype '$f' is not one of the predefined frametypes: $::textblock::frametypes and does not appear to be a dictionary for a custom frametype" |
|
} |
|
#unknown order of keys specified by user - validate before creating vars as we need more general elements to be available as defaults |
|
dict for {k v} $f { |
|
switch -- $k { |
|
all - hl - vl - tlc - trc - blc - brc - hlt - hlb - vll - vlr - hltj - hlbj - vllj - vlrj {} |
|
default { |
|
error "textblock::frametype '$f' has unknown element '$k'" |
|
} |
|
} |
|
} |
|
#verified keys - safe to extract as vars |
|
set custom_frame [tcl::dict::merge $default_custom $f] |
|
tcl::dict::with custom_frame {} ;#extract keys as vars |
|
#longer j vars must be after their more specific counterparts in the list being processed by foreach |
|
foreach t {hlt hlb vll vlr hltj hlbj vllj vlrj} { |
|
if {[tcl::dict::exists $custom_frame $t]} { |
|
set $t [tcl::dict::get $custom_frame $t] |
|
} else { |
|
#set more explicit type to it's more general counterpart if it's missing |
|
#e.g hlt -> hl |
|
#e.g hltj -> hlt |
|
set $t [set [string range $t 0 end-1]] |
|
} |
|
} |
|
#assert vars hl vl tlc trc blc brc hlt hlb vll vlr hltj hlbj vllj vlrj are all set |
|
#horizontal and vertical bar joins - key/variable ends with 'j' |
|
} |
|
} |
|
if {$boxonly} { |
|
set result [tcl::dict::create\ |
|
tlc $tlc hlt $hlt trc $trc\ |
|
vll $vll vlr $vlr\ |
|
blc $blc hlb $hlb brc $brc\ |
|
] |
|
} else { |
|
set result [tcl::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\ |
|
] |
|
} |
|
set result [dict filter $result key {*}$globs] |
|
tcl::dict::set framedef_cache $cache_key $result |
|
return $result |
|
} |
|
|
|
|
|
variable frame_cache |
|
set frame_cache [tcl::dict::create] |
|
|
|
punk::args::definition { |
|
@id -id ::textblock::frame_cache |
|
@cmd -name textblock::frame_cache -help\ |
|
"Display or clear the frame cache." |
|
-action -default {} -choices {clear} -help\ |
|
"Clear the textblock::frame_cache dictionary" |
|
-pretty -default 1 -help\ |
|
"Use 'pdict textblock::frame_cache */*' for prettier output" |
|
@values -min 0 -max 0 |
|
} |
|
proc frame_cache {args} { |
|
set argd [punk::args::get_by_id ::textblock::frame_cache $args] |
|
set action [dict get $argd opts -action] |
|
|
|
if {$action ni [list clear ""]} { |
|
error "frame_cache action '$action' not understood. Valid actions: clear" |
|
} |
|
variable frame_cache |
|
if {[dict get $argd opts -pretty]} { |
|
set out [pdict -chan none frame_cache */*] |
|
} else { |
|
set out "" |
|
if {[catch { |
|
set termwidth [tcl::dict::get [punk::console::get_size] columns] |
|
}]} { |
|
set termwidth 80 |
|
} |
|
|
|
tcl::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 {[tcl::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 [tcl::dict::create] |
|
append out \nCLEARED |
|
} |
|
return $out |
|
} |
|
|
|
|
|
variable FRAMETYPES |
|
set FRAMETYPES [textblock::frametypes] |
|
variable EG |
|
set EG [a+ brightblack] |
|
variable RST |
|
set RST [a] |
|
|
|
proc frame_samples {} { |
|
set FRAMETYPELABELS [dict create] |
|
if {[info commands ::textblock::frame] ne ""} { |
|
foreach ft [frametypes] { |
|
dict set FRAMETYPELABELS $ft [textblock::frame -checkargs 0 -type $ft " "] |
|
} |
|
} |
|
set FRAMETYPELABELS [dict remove $FRAMETYPELABELS block2hack] |
|
return $FRAMETYPELABELS |
|
} |
|
#proc EG {} "return {[a+ brightblack]}" |
|
#make EG fetch from SGR cache so as to abide by colour off/on |
|
proc EG {} { |
|
a+ brightblack |
|
} |
|
#proc RST {} "return {\x1b\[m}" |
|
proc RST {} { |
|
return "\x1b\[m" |
|
} |
|
|
|
#catch 22 for -choicelabels - need some sort of lazy evaluation |
|
# ${[textblock::frame_samples]} |
|
|
|
#todo punk::args alias for centre center etc? |
|
punk::args::definition -dynamic 1 { |
|
@id -id ::textblock::frame |
|
@cmd -name "textblock::frame"\ |
|
-help "Frame a block of text with a border." |
|
-checkargs -default 1 -type boolean\ |
|
-help "If true do extra argument checks and |
|
provide more comprehensive error info. |
|
Set false for slight performance improvement." |
|
-etabs -default 0\ |
|
-help "expanding tabs - experimental/unimplemented." |
|
-type -default light -choices {${[textblock::frametypes]}} -choicerestricted 0 -choicecolumns 8 -type dict\ |
|
-choicelabels { |
|
${[textblock::frame_samples]} |
|
}\ |
|
-help "Type of border for frame." |
|
-boxlimits -default {hl vl tlc blc trc brc} -type list -help "Limit the border box to listed elements. |
|
passing an empty string will result in no box, but title/subtitle will still appear if supplied. |
|
${[textblock::EG]}e.g: -frame -boxlimits {} -title things [a+ red White]my\\ncontent${[textblock::RST]}" |
|
-boxmap -default {} -type dict |
|
-joins -default {} -type list |
|
-title -default "" -type string -regexprefail {\n}\ |
|
-help "Frame title placed on topbar - no newlines. |
|
May contain ANSI - no trailing reset required. |
|
${[textblock::EG]}e.g 1: frame -title My[a+ green]Green[a]Thing |
|
e.g 2: frame -title [a+ red underline]MyThing${[textblock::RST]}" |
|
-subtitle -default "" -type string -regexprefail {\n}\ |
|
-help "Frame subtitle placed on bottombar - no newlines |
|
May contain Ansi - no trailing reset required." |
|
-width -default "" -type int\ |
|
-help "Width of resulting frame including borders. |
|
If omitted or empty-string, the width will be determined automatically based on content." |
|
-height -default "" -type int\ |
|
-help "Height of resulting frame including borders." |
|
-ansiborder -default "" -type ansistring\ |
|
-help "Ansi escape sequence to set border attributes. |
|
${[textblock::EG]}e.g 1: frame -ansiborder [a+ web-red] contents |
|
e.g 2: frame -ansiborder \"\\x1b\\\[31m\" contents${[textblock::RST]}" |
|
-ansibase -default "" -type ansistring\ |
|
-help "Default ANSI attributes within frame." |
|
-blockalign -default centre -choices {left right centre}\ |
|
-help "Alignment of the content block within the frame." |
|
-pad -default 1 -type boolean -help "Whether to pad within the ANSI so content background |
|
extends within the content block inside the frame. |
|
Has no effect if no ANSI in content." |
|
-textalign -default left -choices {left right centre}\ |
|
-help "Alignment of text within the content block. (centre unimplemented)" |
|
-ellipsis -default 1 -type boolean\ |
|
-help "Whether to show elipsis for truncated content and title/subtitle." |
|
-usecache -default 1 -type boolean |
|
-buildcache -default 1 -type boolean |
|
-crm_mode -default 0 -type boolean\ |
|
-help "Show ANSI control characters within frame contents. |
|
(Control Representation Mode) |
|
Frame width doesn't adapt and content may be truncated |
|
so -width may need to be manually set to display more." |
|
|
|
@values -min 0 -max 1 |
|
contents -default "" -type string\ |
|
-help "Frame contents - may be a block of text containing newlines and ANSI. |
|
Text may be 'ragged' - ie unequal line-lengths. |
|
No trailing ANSI reset required. |
|
${[textblock::EG]}e.g: frame \"[a+ blue White] \\nMy blue foreground text on\\nwhite background\\n\"${[textblock::RST]}" |
|
} |
|
|
|
#options before content argument - which is allowed to be absent |
|
#frame performance (noticeable with complex tables even of modest size) is improved somewhat by frame_cache - but is still (2024) a fairly expensive operation. |
|
# |
|
#consider if we can use -sticky nsew instead of -blockalign (as per Tk grid -sticky option) |
|
# This would require some sort of expand equivalent e.g for -sticky ew or -sticky ns - for which we have to decide a meaning for text.. e.g ansi padding? |
|
#We are framing 'rendered' text - so we don't have access to for example an inner frame or table to tell it to expand |
|
#we could refactor as an object and provide a -return object option, then use stored -sticky data to influence how another object renders into it |
|
# - but we would need to maintain support for the rendered-string based operations too. |
|
proc frame {args} { |
|
variable frametypes |
|
variable use_hash |
|
|
|
#counterintuitively - in-proc dict create each time is generally slightly faster than linked namespace var |
|
set opts [tcl::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\ |
|
-pad 1\ |
|
-crm_mode 0\ |
|
-checkargs 1\ |
|
] |
|
#-pad 1 is default so that simple 'textblock::frame "[a+ Red]a \nbbb[a]" extends the bg colour on the short ragged lines (and empty lines) |
|
# for ansi art - -pad 0 is likely to be preferable |
|
|
|
set has_contents 0 |
|
set optlist $args ;#initial only - content will be removed |
|
#no solo opts for frame |
|
if {[llength $args] %2 == 0} { |
|
if {[lindex $args end-1] eq "--"} { |
|
set contents [lpop optlist end] |
|
set has_contents 1 |
|
lpop optlist end ;#drop the end-of-opts flag |
|
} else { |
|
set optlist $args |
|
set contents "" |
|
} |
|
} else { |
|
set contents [lpop optlist end] |
|
set has_contents 1 |
|
} |
|
|
|
#todo args -justify left|centre|right (center) |
|
#todo -blockalignbias -textalignbias? |
|
#use -buildcache 1 with -usecache 0 for debugging cache issues so we can inspect using textblock::frame_cache |
|
set optnames [tcl::dict::keys $opts] |
|
set opts_ok 1 ;#default assumption |
|
foreach {k v} $optlist { |
|
set k2 [tcl::prefix::match -error "" $optnames $k] |
|
switch -- $k2 { |
|
-etabs - -type - -boxlimits - -boxmap - -joins |
|
- -title - -subtitle - -width - -height |
|
- -ansiborder - -ansibase |
|
- -blockalign - -textalign - -ellipsis |
|
- -crm_mode |
|
- -usecache - -buildcache - -pad |
|
- -checkargs { |
|
tcl::dict::set opts $k2 $v |
|
} |
|
default { |
|
#error "frame option '$k' not understood. Valid options are $optnames" |
|
set opts_ok 0 |
|
break |
|
} |
|
} |
|
} |
|
set check_args [dict get $opts -checkargs] |
|
|
|
#only use punk::args if check_args is true or our basic checks failed |
|
#never need to checkargs if only one argument supplied even if it looks like an option - as it will be treated as data to frame |
|
if {[llength $args] != 1 && (!$opts_ok || $check_args)} { |
|
set argd [punk::args::get_by_id ::textblock::frame $args] |
|
set opts [dict get $argd opts] |
|
set contents [dict get $argd values contents] |
|
} |
|
|
|
# -- --- --- --- --- --- |
|
# cache relevant |
|
set opt_usecache [tcl::dict::get $opts -usecache] |
|
set opt_buildcache [tcl::dict::get $opts -buildcache] |
|
set usecache $opt_usecache ;#may need to override |
|
set opt_etabs [tcl::dict::get $opts -etabs] ;#affects contentwidth - needed before cache determination |
|
set opt_crm_mode [tcl::dict::get $opts -crm_mode] |
|
# -- --- --- --- --- --- |
|
set opt_type [tcl::dict::get $opts -type] |
|
set opt_boxlimits [tcl::dict::get $opts -boxlimits] |
|
set opt_joins [tcl::dict::get $opts -joins] |
|
set opt_boxmap [tcl::dict::get $opts -boxmap] |
|
set buildcache $opt_buildcache |
|
set opt_pad [tcl::dict::get $opts -pad] |
|
# -- --- --- --- --- --- |
|
set opt_title [tcl::dict::get $opts -title] |
|
set opt_subtitle [tcl::dict::get $opts -subtitle] |
|
set opt_width [tcl::dict::get $opts -width] |
|
set opt_height [tcl::dict::get $opts -height] |
|
# -- --- --- --- --- --- |
|
set opt_ansiborder [tcl::dict::get $opts -ansiborder] |
|
set opt_ansibase [tcl::dict::get $opts -ansibase] ;#experimental |
|
set opt_ellipsis [tcl::dict::get $opts -ellipsis] |
|
|
|
set opt_blockalign [tcl::dict::get $opts -blockalign] |
|
set opt_textalign [tcl::dict::get $opts -textalign] |
|
|
|
set custom_keys [list hl hlt hlb vl vll vlr tlc trc blc brc] |
|
|
|
set known_frametypes $frametypes ;# light, heavey etc as defined in the ::textblock::frametypes variable |
|
set default_custom [tcl::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 |
|
} |
|
|
|
#if check_args? |
|
|
|
|
|
#REVIEW - now done in framedef? |
|
#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 [tcl::dict::create left "" down "" right "" up ""] |
|
#foreach jt $opt_joins { |
|
# lassign [split $jt -] direction target |
|
# if {$target ne ""} { |
|
# tcl::dict::set join_targets $direction $target |
|
# } |
|
# lappend join_directions $direction |
|
#} |
|
#set join_directions [lsort -unique $join_directions] |
|
#set do_joins [::join $join_directions _] |
|
|
|
|
|
|
|
|
|
# -- --- --- --- --- --- |
|
|
|
if {$has_contents} { |
|
if {[tcl::string::last \t $contents] >= 0} { |
|
if {[tcl::info::exists punk::console::tabwidth]} { |
|
set tw $::punk::console::tabwidth |
|
} else { |
|
set tw 8 |
|
} |
|
if {$opt_etabs} { |
|
#todo |
|
set contents [textutil::tabify::untabify2 $contents $tw] |
|
} |
|
} |
|
set contents [tcl::string::map {\r\n \n} $contents] |
|
if {$opt_crm_mode} { |
|
if {$opt_height eq ""} { |
|
set h [textblock::height $contents] |
|
} else { |
|
set h [expr {$opt_height -2}] |
|
} |
|
if {$opt_width eq ""} { |
|
set w [textblock::width $contents] |
|
} else { |
|
set w [expr {$opt_width -2}] |
|
} |
|
set contents [overtype::renderspace -crm_mode 1 -wrap 1 -width $w -height $h "" $contents] |
|
set actual_contentwidth $w |
|
set actual_contentheight $h |
|
} else { |
|
#set actual_contentwidth [textblock::width $contents] ;#length of longest line in contents (contents can be ragged) |
|
#set actual_contentheight [textblock::height $contents] |
|
lassign [textblock::size_as_list $contents] actual_contentwidth actual_contentheight |
|
} |
|
} 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 |
|
} |
|
#opt_subtitle ?? |
|
|
|
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 $optlist $frame_inner_width $frame_inner_height] |
|
#jmn |
|
#set hashables [concat $optlist $frame_inner_width $frame_inner_height] |
|
set hashables [list {*}$optlist $frame_inner_width $frame_inner_height] |
|
|
|
|
|
switch -- $use_hash { |
|
sha1 { |
|
package require sha1 |
|
set hash [sha1::sha1 [encoding convertto utf-8 $hashables]] |
|
} |
|
md5 { |
|
package require md5 |
|
if {[package vsatisfies [package present md5] 2- ] } { |
|
set hash [md5::md5 -hex [encoding convertto utf-8 $hashables]] ;#need fast and unique to content - not cryptographic - review |
|
} else { |
|
set hash [md5::md5 [encoding convertto utf-8 $hashables]] |
|
} |
|
} |
|
none { |
|
set hash $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 {[tcl::dict::exists $frame_cache $cache_key]} { |
|
set cache_patternwidth [tcl::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 && [tcl::dict::exists $frame_cache $cache_key]} { |
|
set cache_patternwidth [tcl::dict::get $frame_cache $cache_key patternwidth] |
|
set template [tcl::dict::get $frame_cache $cache_key frame] |
|
set used [tcl::dict::get $frame_cache $cache_key used] |
|
tcl::dict::set frame_cache $cache_key used [expr {$used+1}] ;#update existing record |
|
set is_cached 1 |
|
} |
|
|
|
|
|
# -- --- --- --- --- --- --- --- --- |
|
if {!$is_cached} { |
|
# -- --- --- --- --- |
|
# -- --- --- --- --- |
|
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 - light_b - light_c - heavy - heavy_b - heavy_c - ascii - altg - arc - arc_b - arc_c - double - custom - block - block1 - block2 {} |
|
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 |
|
tcl::dict::for {boxelement subst} $opt_boxmap { |
|
switch -- $boxelement { |
|
hl - hlt - hlb - vl - vll - vlr - tlc - trc - blc - brc {} |
|
hltj - hlbj - vllj - vlrj {} |
|
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,hltj,hlbj,vllj,vlrj" |
|
} |
|
# -- --- --- --- --- --- |
|
#these are all valid commands for overtype::<cmd> |
|
switch -- $opt_textalign { |
|
left - right - centre - center {} |
|
default { |
|
error "frame option -textalign must be left|right|centre|center - received: $opt_textalign" |
|
} |
|
} |
|
# -- --- --- --- --- --- |
|
switch -- $opt_blockalign { |
|
left - right - centre - center {} |
|
default { |
|
error "frame option -blockalign must be left|right|centre|center - received: $opt_blockalign" |
|
} |
|
} |
|
# -- --- --- --- --- |
|
# -- --- --- --- --- |
|
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 |
|
} |
|
} |
|
} |
|
#review vllj etc? |
|
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 rst [a] |
|
#set column [tcl::string::repeat " " $frame_inner_width] ;#default - may need to override for custom frame |
|
set underlayline [tcl::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 -joins $opt_joins $framedef] |
|
tcl::dict::with framedef {} ;#extract vll,hlt,tlc etc vars |
|
|
|
#puts "---> $opt_boxmap" |
|
#review - we handle double-wide in custom frames - what about for boxmaps? |
|
tcl::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 [tcl::string::repeat " " $frame_inner_width] |
|
set underlayline [tcl::string::repeat " " $frame_inner_width] |
|
set underlay [::join [lrepeat $linecount $underlayline] \n] |
|
#cache? |
|
|
|
if {$hlt_width == 1} { |
|
set tbar [tcl::string::repeat $hlt $tbarwidth] |
|
} else { |
|
#possibly mixed width chars that make up hlt - tcl::string::range won't get width right |
|
set blank [tcl::string::repeat " " $tbarwidth] |
|
if {$hlt_width > 0} { |
|
set count [expr {($tbarwidth / $hlt_width) + 1}] |
|
} else { |
|
set count 0 |
|
} |
|
set tbar [tcl::string::repeat $hlt $count] |
|
#set tbar [tcl::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 [tcl::string::repeat $hlb $bbarwidth] |
|
} else { |
|
set blank [tcl::string::repeat " " $bbarwidth] |
|
if {$hlb_width > 0} { |
|
set count [expr {($bbarwidth / $hlb_width) + 1}] |
|
} else { |
|
set count 0 |
|
} |
|
set bbar [tcl::string::repeat $hlb $count] |
|
#set bbar [tcl::string::range $bbar 0 $bbarwidth-1] |
|
set bbar [overtype::left -overflow 0 -exposed1 " " -exposed2 " " $blank $bbar] |
|
} |
|
} |
|
altg { |
|
set tbar [tcl::string::repeat $hlt $frame_inner_width] |
|
set tbar [cd::groptim $tbar] |
|
set bbar [tcl::string::repeat $hlb $frame_inner_width] |
|
set bbar [cd::groptim $bbar] |
|
} |
|
default { |
|
set tbar [tcl::string::repeat $hlt $frame_inner_width] |
|
set bbar [tcl::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 [tcl::string::repeat $vll\n $linecount] |
|
set lhs [tcl::string::range $lhs 0 end-1] |
|
set rhs [tcl::string::repeat $vlr\n $linecount] |
|
set rhs [tcl::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 [tcl::string::repeat " " $vll_width] |
|
set lhs [tcl::string::repeat $blank_vll\n $linecount] |
|
set lhs [tcl::string::range $lhs 0 end-1] |
|
} |
|
vlr { |
|
set blank_vlr [tcl::string::repeat " " $vlr_width] |
|
set rhs [tcl::string::repeat $blank_vlr\n $linecount] |
|
set rhs [tcl::string::range $rhs 0 end-1] |
|
} |
|
hlt { |
|
set bar_width [punk::ansi::printing_length $tbar] |
|
set tbar [tcl::string::repeat " " $bar_width] |
|
} |
|
tlc { |
|
set tlc_width [punk::ansi::printing_length $tlc] |
|
set tlc [tcl::string::repeat " " $tlc_width] |
|
} |
|
trc { |
|
set trc_width [punk::ansi::printing_length $trc] |
|
set trc [tcl::string::repeat " " $trc_width] |
|
} |
|
hlb { |
|
set bar_width [punk::ansi::printing_length $bbar] |
|
set bbar [tcl::string::repeat " " $bar_width] |
|
} |
|
blc { |
|
set blc_width [punk::ansi::printing_length $blc] |
|
set blc [tcl::string::repeat " " $blc_width] |
|
} |
|
brc { |
|
set brc_width [punk::ansi::printing_length $brc] |
|
set brc [tcl::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 [tcl::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 [tcl::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] |
|
|
|
#JMN test |
|
#assert - lhs, cache_inner, rhs non-ragged - so can use join_basic REVIEW |
|
#set cache_body [textblock::join -- {*}$cache_bodyparts] |
|
set cache_body [textblock::join_basic -- {*}$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 [tcl::string::map [list $FSUB " "] $template] |
|
} else { |
|
set resultlines [list] |
|
set overwritable [tcl::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 {[tcl::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] ""] ;# should not affect actual_contentwidth |
|
} |
|
|
|
#set cwidth [textblock::width $contents] |
|
set cwidth $actual_contentwidth |
|
if {$opt_pad} { |
|
set paddedcontents [textblock::pad $contents -known_blockwidth $cwidth -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] |
|
} |
|
#important to supply end of opts -- to textblock::join - particularly here with arbitrary data |
|
set contentblock [textblock::join -- $paddedcontents] ;#make sure each line has ansi replays |
|
} else { |
|
if {$cwidth > $cache_patternwidth} { |
|
set contents [overtype::renderspace -width $cache_patternwidth "" $contents] |
|
} |
|
set contentblock [textblock::join -- $contents] ;# adds ansiresets and any required replays on each line |
|
} |
|
|
|
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 [tcl::string::length $R] |
|
set clines [split $contentblock \n] |
|
|
|
foreach tline $tlines { |
|
if {[tcl::string::first $FSUB $tline] >= 0} { |
|
set content_line [lindex $clines $contentindex] |
|
if {[tcl::string::first $R $content_line] == 0} { |
|
set content_line [tcl::string::range $content_line $rlen end] |
|
} |
|
#make sure to replay opt_ansibase to the right of the replacement |
|
lappend resultlines [tcl::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} { |
|
tcl::dict::set frame_cache $cache_key [list frame $template used 0 patternwidth $cache_patternwidth] |
|
} |
|
return $fs |
|
} |
|
} |
|
punk::args::definition { |
|
@id -id ::textblock::gcross |
|
-max_cross_size -default 0 -type integer -help "Largest size cross to use to make up the block |
|
Only cross sizes that divide the size of the overall block will be used. |
|
e.g if the 'size' chosen is 19 (a prime number) - only 1 or the full size of 19 can be used as the crosses to make up the block. |
|
Whereas for a block size of 24, -max_cross_size of 1,2,3,4,6,8,12 or 24 will work. (all divisors) |
|
If a number chosen for -max_cross_size isn't a divisor, the largest divisor below the chosen value will be used. |
|
" |
|
@values -min 0 -max 1 |
|
size -default 1 -type integer |
|
} |
|
proc gcross {args} { |
|
set argd [punk::args::get_by_id ::textblock::gcross $args] |
|
set size [dict get $argd values size] |
|
set opts [dict get $argd opts] |
|
|
|
if {$size == 0} { |
|
return "" |
|
} |
|
|
|
set opt_max_cross_size [tcl::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} { |
|
#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 [tcl::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} { |
|
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] |
|
} |
|
tcl::namespace::import ::punk::ansi::ansistrip |
|
} |
|
|
|
|
|
tcl::namespace::eval ::textblock::piper { |
|
tcl::namespace::export * |
|
proc join {rhs pipelinedata} { |
|
tailcall ::textblock::join -- $pipelinedata $rhs |
|
} |
|
} |
|
interp alias {} piper_blockjoin {} ::textblock::piper::join |
|
|
|
|
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
## Ready |
|
package provide textblock [tcl::namespace::eval textblock { |
|
variable version |
|
set version 999999.0a1.0 |
|
}] |
|
return |
|
|
|
#*** !doctools |
|
#[manpage_end] |
|
|
|
|