From 50de46ff653a3b958d8c8b3091c5059b127e4fcd Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Sun, 31 May 2026 15:45:45 +1000 Subject: [PATCH] update bootsupport,project_layouts,vfs --- src/bootsupport/modules/punk-0.1.tm | 10 +- .../modules/punk/aliascore-0.1.0.tm | 1 + .../punk/cap/handlers/templates-0.1.0.tm | 2 +- src/bootsupport/modules/punk/char-0.1.0.tm | 4 +- src/bootsupport/modules/punk/config-0.1.tm | 2 +- src/bootsupport/modules/punk/du-0.1.0.tm | 18 +- src/bootsupport/modules/punk/lib-0.1.6.tm | 373 +- src/bootsupport/modules/punk/mix/base-0.1.tm | 39 +- src/bootsupport/modules/punk/mix/cli-0.3.1.tm | 32 +- .../punk/mix/commandset/module-0.1.0.tm | 16 +- .../modules/punk/mix/util-0.1.0.tm | 30 + src/bootsupport/modules/punk/ns-0.1.0.tm | 5 +- .../modules/punk/packagepreference-0.1.0.tm | 23 +- src/bootsupport/modules/punk/repl-0.1.2.tm | 133 +- src/bootsupport/modules/punk/repo-0.1.1.tm | 34 +- src/bootsupport/modules/punkcheck-0.1.0.tm | 20 - src/bootsupport/modules/shellfilter-0.2.2.tm | 444 +- src/bootsupport/modules/shellthread-1.6.2.tm | 177 +- src/bootsupport/modules/textblock-0.1.3.tm | 59 +- src/bootsupport/modules/zipper-0.14.tm | Bin 9926 -> 9926 bytes .../src/bootsupport/modules/punk-0.1.tm | 10 +- .../modules/punk/aliascore-0.1.0.tm | 1 + .../punk/cap/handlers/templates-0.1.0.tm | 2 +- .../bootsupport/modules/punk/char-0.1.0.tm | 4 +- .../bootsupport/modules/punk/config-0.1.tm | 2 +- .../src/bootsupport/modules/punk/du-0.1.0.tm | 18 +- .../src/bootsupport/modules/punk/lib-0.1.6.tm | 373 +- .../bootsupport/modules/punk/mix/base-0.1.tm | 39 +- .../bootsupport/modules/punk/mix/cli-0.3.1.tm | 32 +- .../punk/mix/commandset/module-0.1.0.tm | 16 +- .../modules/punk/mix/util-0.1.0.tm | 30 + .../src/bootsupport/modules/punk/ns-0.1.0.tm | 5 +- .../modules/punk/packagepreference-0.1.0.tm | 23 +- .../bootsupport/modules/punk/repl-0.1.2.tm | 133 +- .../bootsupport/modules/punk/repo-0.1.1.tm | 34 +- .../bootsupport/modules/punkcheck-0.1.0.tm | 20 - .../bootsupport/modules/shellfilter-0.2.2.tm | 444 +- .../bootsupport/modules/shellthread-1.6.2.tm | 177 +- .../bootsupport/modules/textblock-0.1.3.tm | 59 +- .../src/bootsupport/modules/zipper-0.14.tm | Bin 9926 -> 9926 bytes .../src/bootsupport/modules/punk-0.1.tm | 10 +- .../modules/punk/aliascore-0.1.0.tm | 1 + .../punk/cap/handlers/templates-0.1.0.tm | 2 +- .../bootsupport/modules/punk/char-0.1.0.tm | 4 +- .../bootsupport/modules/punk/config-0.1.tm | 2 +- .../src/bootsupport/modules/punk/du-0.1.0.tm | 18 +- .../src/bootsupport/modules/punk/lib-0.1.6.tm | 373 +- .../bootsupport/modules/punk/mix/base-0.1.tm | 39 +- .../bootsupport/modules/punk/mix/cli-0.3.1.tm | 32 +- .../punk/mix/commandset/module-0.1.0.tm | 16 +- .../modules/punk/mix/util-0.1.0.tm | 30 + .../src/bootsupport/modules/punk/ns-0.1.0.tm | 5 +- .../modules/punk/packagepreference-0.1.0.tm | 23 +- .../bootsupport/modules/punk/repl-0.1.2.tm | 133 +- .../bootsupport/modules/punk/repo-0.1.1.tm | 34 +- .../bootsupport/modules/punkcheck-0.1.0.tm | 20 - .../bootsupport/modules/shellfilter-0.2.2.tm | 444 +- .../bootsupport/modules/shellthread-1.6.2.tm | 177 +- .../bootsupport/modules/textblock-0.1.3.tm | 59 +- .../src/bootsupport/modules/zipper-0.14.tm | Bin 9926 -> 9926 bytes .../vendor/punk/project-0.1/tclint.toml | 34 + .../modules/punk/mix/base-0.1.tm | 39 +- .../_vfscommon.vfs/modules/punk/repo-0.1.1.tm | 34 +- .../modules/shellfilter-0.2.1.tm | 3395 ------------- .../_vfscommon.vfs/modules/shellfilter-0.2.tm | 3347 ------------- .../modules/shellthread-1.6.1.tm | 829 ---- src/vfs/_vfscommon.vfs/modules/sqids-0.3.0.tm | 873 ---- .../treectrl2.4.2/filelist-bindings.tcl | 1293 ----- .../htmldoc/What-is-New-in-TkTreeCtrl.html | 3604 -------------- .../treectrl2.4.2/htmldoc/treectrl.html | 4408 ----------------- .../lib_tcl9/treectrl2.4.2/pkgIndex.tcl | 8 - .../lib_tcl9/treectrl2.4.2/treectrl.tcl | 1951 -------- .../lib_tcl9/treectrl2.4.2/treectrl24.dll | Bin 458240 -> 0 bytes 73 files changed, 3156 insertions(+), 20925 deletions(-) create mode 100644 src/project_layouts/vendor/punk/project-0.1/tclint.toml delete mode 100644 src/vfs/_vfscommon.vfs/modules/shellfilter-0.2.1.tm delete mode 100644 src/vfs/_vfscommon.vfs/modules/shellfilter-0.2.tm delete mode 100644 src/vfs/_vfscommon.vfs/modules/shellthread-1.6.1.tm delete mode 100644 src/vfs/_vfscommon.vfs/modules/sqids-0.3.0.tm delete mode 100644 src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/filelist-bindings.tcl delete mode 100644 src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/htmldoc/What-is-New-in-TkTreeCtrl.html delete mode 100644 src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/htmldoc/treectrl.html delete mode 100644 src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/pkgIndex.tcl delete mode 100644 src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/treectrl.tcl delete mode 100644 src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/treectrl24.dll diff --git a/src/bootsupport/modules/punk-0.1.tm b/src/bootsupport/modules/punk-0.1.tm index a0ae202c..91ead8b0 100644 --- a/src/bootsupport/modules/punk-0.1.tm +++ b/src/bootsupport/modules/punk-0.1.tm @@ -26,13 +26,11 @@ namespace eval punk { #we call tcl::tm::list to trigger the initial set of tm paths before #we can override it, otherwise our changes will be lost #REVIEW - won't work on safebase interp where paths are mapped to {$p(:x:)} etc - return "\ - apply {{ap tmlist} { - set ::auto_path \$ap + list apply {{ap tmlist} { + set ::auto_path $ap tcl::tm::list - set ::tcl::tm::paths \$tmlist - }} {$::auto_path} {[tcl::tm::list]} - " + set ::tcl::tm::paths $tmlist + }} $::auto_path [tcl::tm::list] } diff --git a/src/bootsupport/modules/punk/aliascore-0.1.0.tm b/src/bootsupport/modules/punk/aliascore-0.1.0.tm index 22c27388..296fa148 100644 --- a/src/bootsupport/modules/punk/aliascore-0.1.0.tm +++ b/src/bootsupport/modules/punk/aliascore-0.1.0.tm @@ -109,6 +109,7 @@ tcl::namespace::eval punk::aliascore { set aliases [tcl::dict::create {*}{ val ::punk::pipe::val tstr ::punk::args::lib::tstr + FOR ::punk::lib::FOR list_as_lines ::punk::lib::list_as_lines lines_as_list ::punk::lib::lines_as_list linelist ::punk::lib::linelist diff --git a/src/bootsupport/modules/punk/cap/handlers/templates-0.1.0.tm b/src/bootsupport/modules/punk/cap/handlers/templates-0.1.0.tm index 7a6e4416..45327f96 100644 --- a/src/bootsupport/modules/punk/cap/handlers/templates-0.1.0.tm +++ b/src/bootsupport/modules/punk/cap/handlers/templates-0.1.0.tm @@ -628,7 +628,7 @@ namespace eval punk::cap::handlers::templates { #we will ignore any .tm files that don't have versions that tcl understands - but warn #this reduces the cases we have to test later set fname [file tail $tf] - lassign [split [punk::mix::cli::lib::split_modulename_version $fname]] mname ver + lassign [split [punk::mix::util::split_modulename_version $fname]] mname ver if {[catch {punk::mix::cli::lib::validate_modulename $mname} errM]} { puts stderr "Invalid module name/version $tf - please rename with standard Tcl .tm module name and version (or leave out version)" if {[string match *-* $mname]} { diff --git a/src/bootsupport/modules/punk/char-0.1.0.tm b/src/bootsupport/modules/punk/char-0.1.0.tm index 0bdc60a0..53ef8ec1 100644 --- a/src/bootsupport/modules/punk/char-0.1.0.tm +++ b/src/bootsupport/modules/punk/char-0.1.0.tm @@ -2468,10 +2468,10 @@ tcl::namespace::eval punk::char { #short-circuit basic cases #support tcl pre 2023-11 - see regexp bug below - #if {![regexp {[\uFF-\U10FFFF]} $text]} { + #if {![regexp {[\u100-\U10FFFF]} $text]} { # return [tcl::string::length $text] #} - if {![regexp "\[\uFF-\U10FFFF\]" $text]} { + if {![regexp "\[\u100-\U10FFFF\]" $text]} { return [tcl::string::length $text] #punk::char::wcswidth has to split and examine dec value of each code #By stripping controls + 7F (leaving tab) we've already eliminated the non-printable ascii - REVIEW diff --git a/src/bootsupport/modules/punk/config-0.1.tm b/src/bootsupport/modules/punk/config-0.1.tm index 43edd6a3..61885dcc 100644 --- a/src/bootsupport/modules/punk/config-0.1.tm +++ b/src/bootsupport/modules/punk/config-0.1.tm @@ -563,7 +563,7 @@ tcl::namespace::eval punk::config { lappend PUNKARGS [list { @dynamic @id -id ::punk::config::show - @cmd -name punk::config::get -help\ + @cmd -name punk::config::show -help\ "Display configuration values from a config. Accepts globs eg XDG*" @leaders -min 1 -max 1 diff --git a/src/bootsupport/modules/punk/du-0.1.0.tm b/src/bootsupport/modules/punk/du-0.1.0.tm index 5ee354a6..8dd91089 100644 --- a/src/bootsupport/modules/punk/du-0.1.0.tm +++ b/src/bootsupport/modules/punk/du-0.1.0.tm @@ -1947,15 +1947,15 @@ namespace eval punk::du { #e.g we can populate compsizes for files (compressed size) proc du_dirlisting_zipfs {folderpath args} { puts stderr "zipfs: $folderpath" - set defaults [dict - -glob *\ - -filedebug 0\ - -patterndebug 0\ - -link_info 1\ - -with_sizes 0\ - -with_times 0\ - -types {}\ - ] + set defaults [dict create {*}{ + -glob * + -filedebug 0 + -patterndebug 0 + -link_info 1 + -with_sizes 0 + -with_times 0 + -types {} + }] set opts [dict merge $defaults $args] # -- --- --- --- --- --- --- --- --- --- --- --- --- --- set opt_glob [dict get $opts -glob] diff --git a/src/bootsupport/modules/punk/lib-0.1.6.tm b/src/bootsupport/modules/punk/lib-0.1.6.tm index 8f25047c..ada0f900 100644 --- a/src/bootsupport/modules/punk/lib-0.1.6.tm +++ b/src/bootsupport/modules/punk/lib-0.1.6.tm @@ -2011,6 +2011,7 @@ namespace eval punk::lib { #for each of the above strings we should get a command recognised for the 'puts e*' items as well as the 'list' item, but not for the 'puts n' items since they are within curly braces and not subject to command substitution. #--------------------------------- proc tclscript_info {script {nscontext ""}} { + package require parser #if the script is ANSI highlighted - the square brackets within the ANSI will disrupt our parsing. if {[punk::ansi::ta::detect $script]} { #we will strip it - but be noisy on stderr since a) it's a bi inefficient to pass in ansi highlighted scripts. @@ -3414,85 +3415,10 @@ namespace eval punk::lib { return [list $stdout $stderr 0] } - proc pdict {args} { - package require punk::args - variable has_punk_ansi - if {!$has_punk_ansi} { - set sep " = " - } else { - #set sep " [a+ Web-seagreen]=[a] " - set sep " [punk::ansi::a+ Green]=[punk::ansi::a] " - } - set argspec [string map [list %sep% $sep] { - @id -id ::punk::lib::pdict - @cmd -name pdict -help\ - "Print dict keys,values to channel - The pdict function operates on variable names - passing the value to the showdict function which operates on values - (see also showdict)" - - @opts -any 1 - - #default separator to provide similarity to tcl's parray function - -separator -default "%sep%" - -roottype -default "dict" - -substructure -default {} - -channel -default stdout -help\ - "existing channel - or 'none' to return as string" - - @values -min 1 -max -1 - - dictvar -type string -help "name of variable. Can be a dict, list or array" - - patterns -type string -default "*" -multiple 1 -help {Multiple patterns can be specified as separate arguments. - Each pattern consists of 1 or more segments separated by the hierarchy separator (forward slash) - The system uses similar patterns to the punk pipeline pattern-matching system. - The default assumed type is dict - but an array will automatically be extracted into key value pairs so will also work. - Segments are classified into list,dict and string operations. - Leading % indicates a string operation - e.g %# gives string length - A segment with a single @ is a list operation e.g @0 gives first list element, @1-3 gives the lrange from 1 to 3 - (todo - change to indexset syntax @1..3 @1..end-1 etc) - A segment containing 2 @ symbols is a dict operation. e.g @@k1 retrieves the value for dict key 'k1' - The operation type indicator is not always necessary if lower segments in the hierarchy are of the same type as the previous one. - e.g1 pdict env */%# - the pattern starts with default type dict, so * retrieves all keys & values, - the next hierarchy switches to a string operation to get the length of each value. - e.g2 pdict env W* S* - Here we supply 2 patterns, each in default dict mode - to display keys and values where the keys match the glob patterns - e.g3 pdict punk_testd */* - This displays 2 levels of the dict hierarchy. - Note that if the sublevel can't actually be interpreted as a dictionary (odd number of elements or not a list at all) - - then the normal = separator will be replaced with a coloured (or underlined if colour off) 'mismatch' indicator. - e.g4 set list {{k1 v1 k2 v2} {k1 vv1 k2 vv2}}; pdict list @0-end/@@k2 @*/@@k1 - Here we supply 2 separate pattern hierarchies, where @0-end and @* are list operations and are equivalent - The second level segment in each pattern switches to a dict operation to retrieve the value by key. - When a list operation such as @* is used - integer list indexes are displayed on the left side of the = for that hierarchy level. - } - }] - #puts stderr "$argspec" - set argd [punk::args::parse $args withdef $argspec] - - set opts [dict get $argd opts] - set dvar [dict get $argd values dictvar] - set patterns [dict get $argd values patterns] - set isarray [uplevel 1 [list ::tcl::array::exists $dvar]] - if {$isarray} { - set dvalue [uplevel 1 [list ::tcl::array::get $dvar]] - if {![dict exists $opts -keytemplates]} { - set arrdisplay [string map [list %dvar% $dvar] {${[if {[lindex $key 1] eq "query"} {val "%dvar% [lindex $key 0]"} {val "%dvar%($key)"}]}}] - dict set opts -keytemplates [list $arrdisplay] - } - dict set opts -keysorttype dictionary - } else { - set dvalue [uplevel 1 [list set $dvar]] - } - showdict {*}$opts $dvalue {*}$patterns - } - namespace eval argdoc { variable PUNKARGS upvar ::punk::lib::has_punk_ansi has_punk_ansi - #if {!$has_punk_ansi} { # set RST "" # set sep " = " @@ -3532,6 +3458,77 @@ namespace eval punk::lib { } set DYN_SEP {${[get_sep]}} set DYN_SEP_MISMATCH {${[get_sep_mismatch]}} + lappend PUNKARGS [list { + @dynamic + @id -id ::punk::lib::pdict + @cmd -name pdict -help\ + "Print dict keys,values to channel + The pdict function operates on variable names - passing the value to the showdict function which operates on values + (see also showdict)" + + @opts -any 1 + + #default separator to provide similarity to tcl's parray function + -separator -default "${$DYN_SEP}" + -roottype -default "dict" + -substructure -default {} + -channel -default stdout -help\ + "existing channel - or 'none' to return as string" + + @values -min 1 -max -1 + + dictvar -type string -help "name of variable. Can be a dict, list or array" + + patterns -type string -default "*" -multiple 1 -help {Multiple patterns can be specified as separate arguments. + Each pattern consists of 1 or more segments separated by the hierarchy separator (forward slash) + The system uses similar patterns to the punk pipeline pattern-matching system. + The default assumed type is dict - but an array will automatically be extracted into key value pairs so will also work. + Segments are classified into list,dict and string operations. + Leading % indicates a string operation - e.g %# gives string length + A segment with a single @ is a list operation e.g @0 gives first list element, @1-3 gives the lrange from 1 to 3 + (todo - change to indexset syntax @1..3 @1..end-1 etc) + A segment containing 2 @ symbols is a dict operation. e.g @@k1 retrieves the value for dict key 'k1' + The operation type indicator is not always necessary if lower segments in the hierarchy are of the same type as the previous one. + e.g1 pdict env */%# + the pattern starts with default type dict, so * retrieves all keys & values, + the next hierarchy switches to a string operation to get the length of each value. + e.g2 pdict env W* S* + Here we supply 2 patterns, each in default dict mode - to display keys and values where the keys match the glob patterns + e.g3 pdict punk_testd */* + This displays 2 levels of the dict hierarchy. + Note that if the sublevel can't actually be interpreted as a dictionary (odd number of elements or not a list at all) + - then the normal = separator will be replaced with a coloured (or underlined if colour off) 'mismatch' indicator. + e.g4 set list {{k1 v1 k2 v2} {k1 vv1 k2 vv2}}; pdict list @0-end/@@k2 @*/@@k1 + Here we supply 2 separate pattern hierarchies, where @0-end and @* are list operations and are equivalent + The second level segment in each pattern switches to a dict operation to retrieve the value by key. + When a list operation such as @* is used - integer list indexes are displayed on the left side of the = for that hierarchy level. + } + }] + } + proc pdict {args} { + package require punk::args + variable has_punk_ansi + + set argd [punk::args::parse $args withid ::punk::lib::pdict] + set opts [dict get $argd opts] + set dvar [dict get $argd values dictvar] + set patterns [dict get $argd values patterns] + set isarray [uplevel 1 [list ::tcl::array::exists $dvar]] + if {$isarray} { + set dvalue [uplevel 1 [list ::tcl::array::get $dvar]] + if {![dict exists $opts -keytemplates]} { + set arrdisplay [string map [list %dvar% $dvar] {${[if {[lindex $key 1] eq "query"} {val "%dvar% [lindex $key 0]"} {val "%dvar%($key)"}]}}] + dict set opts -keytemplates [list $arrdisplay] + } + dict set opts -keysorttype dictionary + } else { + set dvalue [uplevel 1 [list set $dvar]] + } + showdict {*}$opts $dvalue {*}$patterns + } + + namespace eval argdoc { + variable PUNKARGS lappend PUNKARGS [list { @dynamic @@ -4935,6 +4932,23 @@ namespace eval punk::lib { the range will come out the same, so the result needs to be treated as a 1-based set of indices when performing further operations. " + -return -type string -default indices -choices {indices pairs} -choicecolumns 1 -choicelabels { + indices + " return a list of all indices in the order specified by the indexset, + with duplicates if specified by the indexset. + So for example + indexset_resolve 6 3..0,2,4,end + would return 3 2 1 0 2 4 5" + pairs + " return a list of index pairs representing the start and end of each range, + which may be increasing or decreasing, or just a single index + (where start and end are the same). + So for example + indexset_resolve -return pairs 6 3..0,2,4,end + would return {3 0} {2 2} {4 5} + indexset_resolve -return pairs 7 3..0,2,4,end + would return {3 0} {2 2} {4 4} {6 6}" + } @values -min 2 -max 3 numitems -type integer indexset -type indexset -help "comma delimited specification for indices to return" @@ -4948,32 +4962,66 @@ namespace eval punk::lib { # for the unhappy path - the punk::args::parse is fine to generate the usage/error information. # -------------------------------------------------- if {[llength $args] < 2} { + #too few args - use parser to generate error message punk::args::resolve $args withid ::punk::lib::indexset_resolve } - set indexset [lindex $args end] set numitems [lindex $args end-1] + set indexset [lindex $args end] if {![string is integer -strict $numitems] || ![is_indexset $indexset]} { #use parser on unhappy path only set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] } #assert we have 2 or more args + set optlist [lrange $args 0 end-2] + if {[llength $optlist] % 2 != 0} { + #options should come in pairs + set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] + uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + } + set returntype "indices" ;#default set base 0 ;#default - if {[llength $args] > 2} { - #if more than just numitems and indexset - we expect only -base ie 4 args in total - if {[llength $args] != 4} { - set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] - } - set optname [lindex $args 0] - set optval [lindex $args 1] - set fulloptname [tcl::prefix::match -error "" -base $optname] - if {$fulloptname ne "-base" || ![string is integer -strict $optval]} { - set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + dict for {opt val} $optlist { + set fulloptname [tcl::prefix::match -error "" {-base -return} $opt] + switch -exact -- $fulloptname { + -return { + set fullval [tcl::prefix::match -error "" {indices pairs} $val] + if {$fullval ni {indices pairs}} { + set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] + uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + } + set returntype $fullval + } + -base { + if {![string is integer -strict $val]} { + set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] + uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + } + set base $val + } + default { + set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] + uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + } } - set base $optval } + + #set base 0 ;#default + #if {[llength $args] > 2} { + # #if more than just numitems and indexset - we expect only -base ie 4 args in total + # if {[llength $args] != 4} { + # set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] + # uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + # } + # set optname [lindex $args 0] + # set optval [lindex $args 1] + # set fulloptname [tcl::prefix::match -error "" -base $optname] + # if {$fulloptname ne "-base" || ![string is integer -strict $optval]} { + # set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] + # uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + # } + # set base $optval + #} # -------------------------------------------------- @@ -5160,8 +5208,84 @@ namespace eval punk::lib { } } } + if {$returntype eq "pairs"} { + return [indices_to_pairs $index_list] + } return $index_list } + proc indices_to_pairs {indices} { + #convert a list of indices to a list of pairs representing the start and end of contiguous runs of indices which can be increasing or decreasing. + #if the direction of the run changes - we end the previous run and start a new one + if {[llength $indices] == 0} { + return [list] + } + set pairs [list] + set start [lindex $indices 0] + set prev $start + set direction 0 ;#0 = unknown, 1 = increasing, -1 = decreasing + for {set i 1} {$i < [llength $indices]} {incr i} { + set idx [lindex $indices $i] + if {![string is integer -strict $idx]} { + error "non-integer index '$idx' in indices list" + } + if {$idx == $prev + 1} { + #increase since prev + if {$direction == 0} { + set direction 1 + } elseif {$direction == -1} { + #direction changed - end previous run and start new one + lappend pairs [list $start $prev] + set start $idx + set direction 0 + } else { + #still increasing + } + set prev $idx + } elseif {$idx == $prev - 1} { + #decrease since prev + if {$direction == 0} { + set direction -1 + } elseif {$direction == 1} { + #direction changed - end previous run and start new one + lappend pairs [list $start $prev] + set start $idx + set direction 0 + } + set prev $idx + } else { + #run ended - add pair to list + lappend pairs [list $start $prev] + set start $idx + set prev $idx + set direction 0 + } + } + # add final run + lappend pairs [list $start $prev] + return $pairs + } + + #proc indices_to_pairs {indices} { + # #convert a list of indices to a list of pairs representing the start and end of contiguous runs of indices + # set pairs [list] + # set start [lindex $indices 0] + # set prev $start + # for {set i 1} {$i < [llength $indices]} {} { + # set idx [lindex $indices $i] + # if {$idx == $prev + 1} { + # #still in a run + # set prev $idx + # } else { + # #run ended - add pair to list + # lappend pairs [list $start $prev] + # set start $idx + # set prev $idx + # } + # } + # # add final run + # lappend pairs [list $start $prev] + # return $pairs + #} # showdict uses lindex_resolve results -Inf & Inf to determine whether index is out of bounds on lower vs upper side #This doesn't need the list itself - just the length suffices. punk::args::define { @@ -7835,6 +7959,77 @@ namespace eval punk::lib { } } + #sugar + #exclusive end - more intuitive for some cases and more consistent with other languages + namespace eval argdoc { + variable PUNKARGS + lappend PUNKARGS [list { + @id -id ::punk::lib::FOR + @cmd -name punk::lib::FOR\ + -summary\ + "BASIC style integer 'for loop'"\ + -help\ + "Sugar syntax for a common looping pattern. + The loop variable takes on values from start to end (exclusive) in increments of step. + If step is not specified, it defaults to 1 or -1 depending on the relative values of start and end. + This is a common looping pattern that isn't directly supported by Tcl's built in control structures, + and this syntax is more concise for convenient interactive usage. For example: + + FOR i 0 10 {puts $i} + + will print the numbers 0 to 9. + + This wrapper necessarily has some slight overhead compared to builtin Tcl for, foreach and while loops, + so may not be suitable for performance critical inner loops. + + See also: https://wiki.tcl-lang.org/page/Simple+shorthand+%27for%27+loop + " + @values -min 3 -max 4 + varname -type string -help "loop variable name" + start -type integer -help "initial value for loop variable" + end -type integer -help "end value for loop variable (exclusive)" + step -type integer -optional 1 -help "step value for loop variable (defaults to 1 or -1 depending on start and end values)" + script -type script -help "script to execute for each loop iteration" + }] + } + proc FOR { var args } { + switch -- [llength $args] { + 3 { + # FOR x start end {} + lassign $args start end script + set step [ expr {$start > $end ? - 1 : 1} ] + } + 4 { + # FOR x start end step {} + lassign $args start end step script + } + default { + error "FOR: wrong # args, should be: FOR varName startValue endValue ?stepValue? script" + } + } + if {![string is integer -strict $start] || ![string is integer -strict $end] || ![string is integer -strict $step]} { + error "FOR: start,end and step values must be integers" + } + upvar $var loopVar + set loopVar [expr {$start - $step}] + #to support 'continue' we have to increment the loopVar prior to the script evaluation, within the loop condition + if {$start < $end} { + if {$step <= 0} { + error "FOR: step value must be positive when start < end" + } + while {[incr loopVar $step] < $end} { + uplevel $script + } + } else { + if {$step >= 0} { + error "FOR: step value must be negative when start > end" + } + while {[incr loopVar $step] > $end} { + uplevel $script + } + } + } + #review - there are various type of uuid - we should use something consistent across platforms #twapi is used on windows because it's about 5 times faster - but is this more important than consistency? #twapi is much slower to load in the first place (e.g 75ms vs 6ms if package names already loaded) - so for oneshots tcllib uuid is better anyway diff --git a/src/bootsupport/modules/punk/mix/base-0.1.tm b/src/bootsupport/modules/punk/mix/base-0.1.tm index eb1e1c5e..ba24f31d 100644 --- a/src/bootsupport/modules/punk/mix/base-0.1.tm +++ b/src/bootsupport/modules/punk/mix/base-0.1.tm @@ -677,7 +677,7 @@ namespace eval punk::mix::base { if {$opt_use_tar != 0} { set target [file tail $path] set tmplocation [punk::mix::util::tmpdir] - set archivename $tmplocation/[punk::mix::util::tmpfile].tar + set archivename $tmplocation/[punk::mix::util::tmpfile].tar ;#generates a unique filename - does not create the file. cd $base ;#cd is process-wide.. keep cd in effect for as small a scope as possible. (review for thread issues) @@ -687,14 +687,38 @@ namespace eval punk::mix::base { set tsstart [clock millis] if {[set tarpath [auto_execok tar]] ne ""} { #using an external binary is *significantly* faster than tar::create - but comes with some risks - #review - need to check behaviour/flag variances across platforms + set versioninfo [exec {*}$tarpath --version] + #look for "bsdtar" vs "GNU" + #GNU tar is more common on linux - but also available on windows via gnuutils or msys + #/tar.exe on windows is likely to be bsdtar. + #GNU tar on windows will commonly fail with 'cannot connect to C: resolve failed' - may need --force-local flag + + #review - need to further check behaviour/flag variances across platforms #don't use -z flag. On at least some tar versions the zipped file will contain a timestamped subfolder of filename.tar - which ruins the checksum #also - tar is generally faster without the compression (although this may vary depending on file size and disk speed?) - exec {*}$tarpath -cf $archivename $target ;#{*} needed in case spaces in tarpath - set tsend [clock millis] - set ms [expr {$tsend - $tsstart}] - puts stdout " tar -cf done ($ms ms)" - } else { + + if {[string match "*GNU*" $versioninfo]} { + set flags "--force-local" + } else { + #presumably bsdtar - which is more likely to be present on windows - and doesn't seem to have the same issue with drive letters in paths + set flags "" + } + if {[catch { + #{*}$tarpath needed in case spaces in tarpath + exec {*}$tarpath -cf {*}$flags $archivename $target + } errMsg]} { + set tsend [clock millis] + set ms [expr {$tsend - $tsstart}] + puts stdout " 'tar -cf $flags' ERROR ($ms ms) - falling back to tar::create\n error info: $errMsg" + } else { + set tsend [clock millis] + set ms [expr {$tsend - $tsstart}] + puts stdout " 'tar -cf $flags' done ($ms ms)" + } + } + + if {![file exists $archivename]} { + #fallback to tar library approach if external tar failed to create the archive. set tsstart [clock millis] ;#don't include auto_exec search time for tar::create tar::create $archivename $target set tsend [clock millis] @@ -703,6 +727,7 @@ namespace eval punk::mix::base { puts stdout " NOTE: install tar executable for potentially *much* faster directory checksum processing" } + if {$ftype eq "file"} { set sizeinfo "(size [punk::lib::format_number [file size $target]] bytes)" } else { diff --git a/src/bootsupport/modules/punk/mix/cli-0.3.1.tm b/src/bootsupport/modules/punk/mix/cli-0.3.1.tm index 87449fb3..793736b8 100644 --- a/src/bootsupport/modules/punk/mix/cli-0.3.1.tm +++ b/src/bootsupport/modules/punk/mix/cli-0.3.1.tm @@ -333,11 +333,11 @@ namespace eval punk::mix::cli { set defaults [list {*}{ -errorprefix projectname }] - if {[llength $args] %2 != 0} {error "validate_modulename args must be name-value pairs: received '$args'"} + if {[llength $args] %2 != 0} {error "validate_projectname args must be name-value pairs: received '$args'"} set known_opts [dict keys $defaults] foreach k [dict keys $args] { if {$k ni $known_opts} { - error "validate_modulename error: unknown option $k. known options: $known_opts" + error "validate_projectname error: unknown option $k. known options: $known_opts" } } set opts [dict merge $defaults $args] @@ -381,34 +381,6 @@ namespace eval punk::mix::cli { return $name } - #split modulename (as present in a filename or namespaced name) into name/version ignoring leading namespace path - #ignore trailing .tm .TM if present - #if version doesn't pass validation - treat it as part of the modulename and return empty version string without error - #Up to caller to validate. - proc split_modulename_version {fullmodulename} { - set lastpart [namespace tail $fullmodulename] - set lastpart [file tail $lastpart] ;# should be ok to use file tail now that we've ensured no namespace components - if {[string equal -nocase [file extension $fullmodulename] ".tm"]} { - set fileparts [split [file rootname $lastpart] -] - } else { - set fileparts [split $lastpart -] - } - if {[punk::mix::util::is_valid_tm_version [lindex $fileparts end]]} { - set versionsegment [lindex $fileparts end] - set namesegment [join [lrange $fileparts 0 end-1] -];#re-stitch - } else { - # - set namesegment [join $fileparts -] - set versionsegment "" - } - set base [namespace qualifiers $fullmodulename] - if {$base ne ""} { - set modulename "${base}::$namesegment" - } else { - set modulename $namesegment - } - return [list $modulename $versionsegment] - } proc get_status {{workingdir ""} args} { set result "" diff --git a/src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm b/src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm index 3b40d838..4c75b10e 100644 --- a/src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm +++ b/src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm @@ -9,7 +9,7 @@ # @@ Meta Begin # Application punk::mix::commandset::module 0.1.0 # Meta platform tcl -# Meta license BSD +# Meta license BSD # @@ Meta End @@ -204,7 +204,7 @@ namespace eval punk::mix::commandset::module { # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- set opt_version_supplied [dict get $opts -version] set opt_version $opt_version_supplied - if {![util::is_valid_tm_version $opt_version]} { + if {![punk::mix::util::is_valid_tm_version $opt_version]} { error "deck module.new error - supplied -version $opt_version doesn't appear to be a valid Tcl module version" } # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- @@ -213,8 +213,8 @@ namespace eval punk::mix::commandset::module { set mversion_supplied "" ;#version supplied directly in module argument if {[string first - $module]> 0} { #if it has a dash then version is required to be valid - lassign [punk::mix::cli::lib::split_modulename_version $module] modulename mversion - if {![util::is_valid_tm_version $mversion]} { + lassign [punk::mix::util::split_modulename_version $module] modulename mversion + if {![punk::mix::util::is_valid_tm_version $mversion]} { error "deck module.new error - unable to determine modulename-version from supplied value '$module'" } set mversion_supplied $mversion ;#record as may need to compare to version from templatefile name @@ -315,7 +315,7 @@ namespace eval punk::mix::commandset::module { } else { set module [string range [string length $vendor.] end] } - lassign [punk::mix::cli::lib::split_modulename_version $m] _tailmname mversion + lassign [punk::mix::util::split_modulename_version $m] _tailmname mversion lappend key_version_list [list $m $mversion] } if {[llength $matches]} { @@ -383,7 +383,7 @@ namespace eval punk::mix::commandset::module { } else { error "module.new error: Unable to interpret filename components of template file '$templatefile'" } - lassign [punk::mix::cli::lib::split_modulename_version $template_modulename_part] t_mname t_version + lassign [punk::mix::util::split_modulename_version $template_modulename_part] t_mname t_version #t_version may be empty string if template is unversioned e.g template_whatever.tm set fd [open $templatefile r]; set template_filedata [read $fd]; close $fd @@ -398,7 +398,7 @@ namespace eval punk::mix::commandset::module { } else { # - if {[util::is_valid_tm_version $t_version]} { + if {[punk::mix::util::is_valid_tm_version $t_version]} { if {$mversion_supplied eq ""} { set build_version $t_version } else { @@ -500,7 +500,7 @@ namespace eval punk::mix::commandset::module { set name_version_pairs [list] lappend name_version_pairs [list $moduletail $infile_version] foreach existing $existing_versions { - lassign [punk::mix::cli::lib::split_modulename_version $existing] namepart version ;# .tm is stripped and ignored + lassign [punk::mix::util::split_modulename_version $existing] namepart version ;# .tm is stripped and ignored if {[string match #modpod-* $namepart]} { set namepart [string range $namepart 8 end] } diff --git a/src/bootsupport/modules/punk/mix/util-0.1.0.tm b/src/bootsupport/modules/punk/mix/util-0.1.0.tm index 9de4a33d..7f55005b 100644 --- a/src/bootsupport/modules/punk/mix/util-0.1.0.tm +++ b/src/bootsupport/modules/punk/mix/util-0.1.0.tm @@ -330,6 +330,35 @@ namespace eval punk::mix::util { return 0 } } + + #split modulename (as present in a filename or namespaced name) into name/version ignoring leading namespace path + #ignore trailing .tm .TM if present + #if version doesn't pass validation - treat it as part of the modulename and return empty version string without error + #Up to caller to validate. + proc split_modulename_version {fullmodulename} { + set lastpart [namespace tail $fullmodulename] + set lastpart [file tail $lastpart] ;# should be ok to use file tail now that we've ensured no namespace components + if {[string equal -nocase [file extension $fullmodulename] ".tm"]} { + set fileparts [split [file rootname $lastpart] -] + } else { + set fileparts [split $lastpart -] + } + if {[is_valid_tm_version [lindex $fileparts end]]} { + set versionsegment [lindex $fileparts end] + set namesegment [join [lrange $fileparts 0 end-1] -];#re-stitch + } else { + set namesegment [join $fileparts -] + set versionsegment "" + } + set base [namespace qualifiers $fullmodulename] + if {$base ne ""} { + set modulename "${base}::$namesegment" + } else { + set modulename $namesegment + } + return [list $modulename $versionsegment] + } + #Note that semver only has a small overlap with tcl tm versions. #todo - work out what overlap and whether it's even useful #see also TIP #439: Semantic Versioning (tcl 9??) @@ -337,6 +366,7 @@ namespace eval punk::mix::util { set re {^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$} } #todo - semver conversion/validation for other systems? + proc magic_tm_version {} { set magicbase 999999 ;#deliberately large so given load-preference when testing! #we split the literal to avoid the literal appearing here - reduce risk of accidentally converting to a release version diff --git a/src/bootsupport/modules/punk/ns-0.1.0.tm b/src/bootsupport/modules/punk/ns-0.1.0.tm index e89997c5..db6acbb4 100644 --- a/src/bootsupport/modules/punk/ns-0.1.0.tm +++ b/src/bootsupport/modules/punk/ns-0.1.0.tm @@ -4186,7 +4186,7 @@ y" {return quirkykeyscript} #this *very odd* construct is to avoid using the namespace argument of apply. (handling of *weird/inadvisable* namespaces) #we use an uplevel from within the apply which runs in the global namespace, (but called via nseval from within targetns) and a result var for 'info default' in the current punk::ns namespace. nseval $targetns [list apply [list {procname argname} { - set has_default [uplevel 1 [list info default $procname $argname ::punk::ns::corp_defvar]] + set has_default [uplevel 1 [list ::info default $procname $argname ::punk::ns::corp_defvar]] if {$has_default} { set answer [dict create exists 1 default $::punk::ns::corp_defvar] } else { @@ -5025,7 +5025,7 @@ y" {return quirkykeyscript} set queryargs [lrange $args $i end] set resolvedargs [list] set queryargs_untested $queryargs - puts "punk::args::id_exists $docid queryargs_untested: $queryargs" + #puts "punk::ns::cmdtraverse punk::args::id_exists $docid queryargs_untested: $queryargs" } else { #we cannot generate autodoc for any deeper (e.g ensemble/proc after undocumented parent) #There is nothing to indicate the locations of subcommands - they could be anywhere. @@ -7191,6 +7191,7 @@ y" {return quirkykeyscript} nstest eval {package require punk::ns} set ns "" if {![catch {nstest eval [list punk::ns::pkguse $pkg_unqualified]} errMsg]} { + #review set script [string map [list %p% $pkg_unqualified] {dict get $::punk::ns::pkguse_package_to_namespace %p%}] set ns [nstest eval $script] } else { diff --git a/src/bootsupport/modules/punk/packagepreference-0.1.0.tm b/src/bootsupport/modules/punk/packagepreference-0.1.0.tm index e45cefa7..87e33d8c 100644 --- a/src/bootsupport/modules/punk/packagepreference-0.1.0.tm +++ b/src/bootsupport/modules/punk/packagepreference-0.1.0.tm @@ -110,6 +110,19 @@ tcl::namespace::eval punk::packagepreference { #[para]This comes at some slight cost for packages that are only available with uppercase letters in the name - but at minimal cost for recommended lowercase package names #[para]Return to the standard ::package builtin by calling punk::packagepreference::uninstall + if {![catch {commandstack::get_stack} cstack]} { + if {[dict exists $cstack ::package]} { + set pstack [dict get $cstack ::package] + foreach record $pstack { + if {[dict get $record rename] eq "punk::packagepreference"} { + #already installed - silently ignore. + return 0 + } + } + } + } + + #todo - review/update commandstack package #modern module/lib names should preferably be lower case #see tip 590 - "Recommend lowercase Package names". Where non-lowercase are deprecated (but not removed even in Tcl9) @@ -170,7 +183,10 @@ tcl::namespace::eval punk::packagepreference { if {[llength $pkgloadedinfo]} { if {[llength $available_versions] > 1} { - puts stderr "--> pkg $pkg not already 'provided' but shared object seems to be loaded: $pkgloadedinfo - and [llength $available_versions] versions available" + if {[catch {thread::id} threadid]} { + set threadid "unknown-threadid" + } + #puts stderr "--> pkg $pkg not already 'provided' but shared object seems to be loaded: $pkgloadedinfo - and [llength $available_versions] versions available. $available_versions threadid: $threadid" } lassign $pkgloadedinfo loaded_path name set lc_loadedpath [string tolower $loaded_path] @@ -302,7 +318,10 @@ tcl::namespace::eval punk::packagepreference { puts stderr "Failed to load punk::args::moduledoc::$dp - error was: $errMsg" } } else { - puts stdout "Loaded punk::args::moduledoc::$dp for package $pkg threadid: [thread::id]" + if {[catch {thread::id} threadid]} { + set threadid "unknown-threadid" + } + puts stdout "punk::packagepreference overloaded 'package require': Loaded punk::args::moduledoc::$dp for package $pkg threadid: $threadid" } } #--------------------------------------------------------------- diff --git a/src/bootsupport/modules/punk/repl-0.1.2.tm b/src/bootsupport/modules/punk/repl-0.1.2.tm index a7aff5a6..91f7a31a 100644 --- a/src/bootsupport/modules/punk/repl-0.1.2.tm +++ b/src/bootsupport/modules/punk/repl-0.1.2.tm @@ -432,7 +432,7 @@ proc repl::start {inchan args} { variable codethread #review if {$codethread eq ""} { - error "start - no codethread. call init first. (options -safe 0|1)" + error "start - no codethread. call init first. (options -type 0|1)" } variable commandstr @@ -478,7 +478,7 @@ proc repl::start {inchan args} { #set ::punk::repl::codethread::running 1 #the interp in which commands such as d/ run - #we need to namespace eval for the -safe interp which may not have the packages loaded (or be able to) but still needs default values + #we need to namespace eval for the -type interp which may not have the packages loaded (or be able to) but still needs default values #punk::repl::codethread::running is required whether safe or not. interp eval code { namespace eval ::punk::repl::codethread {} @@ -690,7 +690,7 @@ proc repl::reopen_stdinX {} { } -#add to sliding buffer of last x chars emmitted to screen by repl +# add to sliding buffer of last x chars emmitted to screen by repl #(we could maintain only one char - more kept merely for debug assistance) #will not detect emissions from exec with stdout redirected and presumably some extensions etc proc repl::screen_last_char_add {c what {why ""}} { @@ -2123,8 +2123,10 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config #if {$chunk eq "\x1b\[C"} { #} + #---------------------------------------------------------------------------------------------------------------------------------------------------------- punk::console::cursor_off flush stdout + #---------------------------------------------------------------------------------------------------------------------------------------------------------- $editbuf add_chunk $chunk @@ -2235,8 +2237,10 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config lappend input_chunks_waiting($inputchan) $waiting } } + #---------------------------------------------------------------------------------------------------------------------------------------------------------- punk::console::cursor_on flush stdout + #---------------------------------------------------------------------------------------------------------------------------------------------------------- if {$editbuf_linenum_submitted == 0} { #(there is no line 0 - lines start at 1) @@ -2264,9 +2268,9 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config rputs stderr "trap1 POSIX '$e' eopts:'$eopts" flush stderr } on error {repl_error erropts} { - rputs stderr "error1 in repl_handler: $repl_error" + rputs stderr "error1 in repl_process_data: $repl_error" rputs stderr "-------------" - rputs stderr "$::errorInfo" + rputs stderr "erroropts: $erropts" rputs stderr "-------------" set stdinreader [chan event $inputchan readable] if {![string length $stdinreader]} { @@ -2485,7 +2489,13 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config variable codethread_cond variable codethread_mutex - lappend errstack [shellfilter::stack::add stderr tee_to_var -settings {-varname ::repl::output_stderr}] + #------------------------------- + #JJJJ JMN test + #REVIEW + #lappend errstack [shellfilter::stack::add stderr tee_to_var -settings {-varname ::repl::output_stderr}] + #------------------------------- + + #thread::transfer $codethread stderr #chan configure stdout -buffering none @@ -2939,9 +2949,9 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config rputs stderr "trap POSIX '$e' eopts:'$eopts" flush stderr } on error {repl_error erropts} { - rputs stderr "error in repl_handler: $repl_error" + rputs stderr "error2 in repl_process_data: $repl_error" rputs stderr "-------------" - rputs stderr "$::errorInfo" + rputs stderr "$erropts" rputs stderr "-------------" set stdinreader [chan event $inputchan readable] if {![string length $stdinreader]} { @@ -2986,10 +2996,10 @@ namespace eval repl { variable codethread_cond variable codethread_mutex - set opts [list -force 0 -safe 0 -safelog 0 -paths {} -callback_interp $default_callback_interp] + set opts [list -force 0 -type 0 -safelog 0 -paths {} -callback_interp $default_callback_interp] foreach {k v} $args { switch -- $k { - -force - -safe - -safelog - -paths - -callback_interp { + -force - -type - -safelog - -paths - -callback_interp { dict set opts $k $v } default { @@ -2998,7 +3008,7 @@ namespace eval repl { } } set opt_force [dict get $opts -force] - set opt_safe [dict get $opts -safe] + set opt_repltype [dict get $opts -type] set opt_safelog [dict get $opts -safelog] if {$opt_safelog eq "0"} { set opt_safelog "" @@ -3113,46 +3123,50 @@ namespace eval repl { package require punk::args #package require Thread - if {[catch {package require thread} errM]} { - puts stdout ">>repl::init initscript lib load fail on package require thread\n$errM" + #if {[catch {package require thread} errM]} { + # puts stdout ">>repl::init initscript lib load fail on package require thread\n$errM" if {[catch {package require Thread} errM2]} { puts stdout ">>repl::init initscript lib load fail on package require Thread\n$errM2" puts stdout ">>repl::init auto_path : $::auto_path" puts stdout ">>repl::init tcl::tm::list: [tcl::tm::list]" } - } + #} + namespace eval ::punk::repl::codethread {} + + # ------------------------------------------------------------------------------------------------------------------------------------------------------- #----- #review - icomm as a possible way to talk to thread outside of the code interp. #thread::send msgs arrive at a specific interp based on initial setup - review for when/whether androwish thread enhancements are made to allow #thread::send to caller defined interp targets (reference?) #snit required for icomm - if {[catch {package require snit} errM]} { - #puts stdout "punk::repl::initscript: lib load fail ---snit $errM" - } - if {[catch {package require punk::icomm} errM]} { - #puts stdout "punk::repl::initscript: lib load fail ---icomm $errM" - } + #if {[catch {package require snit} errM]} { + # #puts stdout "punk::repl::initscript: lib load fail ---snit $errM" + #} + #if {[catch {package require punk::icomm} errM]} { + # #puts stdout "punk::repl::initscript: lib load fail ---icomm $errM" + #} #----- - - namespace eval ::punk::repl::codethread {} - #todo - review. According to fifo2 docs Memchan involves one less thread (may offer better performance/resource use) - catch {package require tcl::chan::fifo2} - if {[catch { - #first use can raise error being a version number e.g 0.1.0 - why? - lassign [tcl::chan::fifo2] ::punk::repl::codethread::repltalk replside - } errMsg]} { - puts stdout "punk::repl::initscript tcl::chan::fifo2 error: $errMsg" - } else { - #experimental? - #puts stdout "transferring chan $replside to thread %replthread%" - #flush stdout - #if {[catch { - # #after 0 [list thread::transfer %replthread% $replside] - #} errMsg]} { - # #puts stdout "---thread::transfer error: $errMsg" - #} - } + #---------------------------------------------------------------------------------------------------------------------- + # todo - review. According to fifo2 docs Memchan involves one less thread (may offer better performance/resource use) + # catch {package require tcl::chan::fifo2} + # if {[catch { + # #first use can raise error being a version number e.g 0.1.0 - why? + # lassign [tcl::chan::fifo2] ::punk::repl::codethread::repltalk replside + # } errMsg]} { + # puts stdout "punk::repl::initscript tcl::chan::fifo2 error: $errMsg" + # } else { + # #experimental? + # #puts stdout "transferring chan $replside to thread %replthread%" + # #flush stdout + # # if {[catch { + # # #after 0 [list thread::transfer %replthread% $replside] + # # } errMsg]} { + # # #puts stdout "---thread::transfer error: $errMsg" + # # } + # } + #---------------------------------------------------------------------------------------------------------------------- + # ------------------------------------------------------------------------------------------------------------------------------------------------------- package require punk::console package require punk::repl::codethread @@ -3333,7 +3347,7 @@ namespace eval repl { set ts_start [clock seconds] set replresult [interp eval code { package require punk::repl - repl::init -safe punk + repl::init -type punk repl::start stdin }] return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]] @@ -3343,7 +3357,7 @@ namespace eval repl { interp eval code { package require punk::repl } - interp eval code [list repl::init -safe safe {*}$args] + interp eval code [list repl::init -type safe {*}$args] set replresult [interp eval code [list repl::start stdin]] return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]] @@ -3353,7 +3367,7 @@ namespace eval repl { interp eval code { package require punk::repl } - set codethread [interp eval code [list repl::init -safe safebase {*}$args]] + set codethread [interp eval code [list repl::init -type safebase {*}$args]] puts stdout "safebase codethread:$codethread" set replresult [interp eval code [list repl::start stdin]] @@ -3364,7 +3378,7 @@ namespace eval repl { interp eval code { package require punk::repl } - interp eval code [list repl::init -safe punksafe {*}$args] + interp eval code [list repl::init -type punksafe {*}$args] set replresult [interp eval code [list repl::start stdin]] return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]] @@ -3376,14 +3390,14 @@ namespace eval repl { #flush stdout set args %args% - set safe [dict get $args -safe] + set repltype [dict get $args -type] set safelog [dict get $args -safelog] set paths [list] if {[dict exists $args -paths]} { set paths [dict get $args -paths] } - switch -- $safe { + switch -- $repltype { safe { interp create -safe -- code code eval [list namespace eval ::punk::libunknown {}] @@ -3444,7 +3458,7 @@ namespace eval repl { #todo a specific punk::libunknown 'package unknown' handler for safe interps #pull in code via calls to source cached code? - switch -- $safe { + switch -- $repltype { safe { if {[llength $paths]} { package require punk::island @@ -3797,8 +3811,27 @@ namespace eval repl { } punk - 0 { + #------------------------------------------------------------------------ + #Test - experimental 2026 + if {"stdout" in [chan names]} { + interp share {} stdout code + } else { + interp share {} [shellfilter::stack::item_tophandle stdout] code + } + if {"stderr" in [chan names]} { + interp share {} stderr code + } else { + interp share {} [shellfilter::stack::item_tophandle stderr] code + } + foreach nm {shellspyout shellspyerr} { + set thandle [shellfilter::stack::item_tophandle $nm] + if {$thandle ne ""} { + interp share {} $thandle code + } + } + #------------------------------------------------------------------------ interp eval code { - #safe !=1 and safe !=2, tmlist: %tmlist% + #repltype !=1 and repltype !=2, tmlist: %tmlist% set ::argv0 %argv0% set ::argv %argv% set ::argc %argc% @@ -3978,8 +4011,8 @@ namespace eval repl { thread::id } set init_script [string map $scriptmap $init_script] - #REVIEW - the same initscript sent for all values of $safe and it switches on values of $safe provided in %args% - #we already know $safe in this thread when generating the script - so why send the large script to the thread to then switch on that? + #REVIEW - the same initscript sent for all values of $repltype and it switches on values of $repltype provided in %args% + #we already know $repltype in this thread when generating the script - so why send the large script to the thread to then switch on that? #thread::send $codethread $init_script if {![catch { @@ -3994,7 +4027,7 @@ namespace eval repl { error $errMsg } } - #init - don't auto init - require init with possible options e.g -safe + #init - don't auto init - require init with possible options e.g -type } package provide punk::repl [namespace eval punk::repl { variable version diff --git a/src/bootsupport/modules/punk/repo-0.1.1.tm b/src/bootsupport/modules/punk/repo-0.1.1.tm index 482fbbeb..dd446ae8 100644 --- a/src/bootsupport/modules/punk/repo-0.1.1.tm +++ b/src/bootsupport/modules/punk/repo-0.1.1.tm @@ -83,34 +83,38 @@ namespace eval punk::repo { proc get_fossil_usage {} { set allcmds [runout -n fossil help -a] + set allcmds [punk::ansi::ansistrip $allcmds] set mainhelp [runout -n fossil help] + set mainhelp [punk::ansi::ansistrip $mainhelp] set maincommands [list] #only start parsing for TOPICS after a line such as "Other comman values for TOPIC:" set parsing_topics 0 foreach ln [split $mainhelp \n] { set ln [string trim $ln] - if {$ln eq ""} { - continue - } - if {[string match "*values for TOPIC*" $ln]} { - set parsing_topics 1 + if {$ln eq ""} { + continue + } + if {[string match "*values for TOPIC*" $ln]} { + set parsing_topics 1 + continue + } + if {$parsing_topics} { + #lines starting with uppercase are topic headers - we want to ignore these and any blank lines + if {[regexp {^[A-Z]+} $ln]} { continue } - if {$parsing_topics} { - #lines starting with uppercase are topic headers - we want to ignore these and any blank lines - if {[regexp {^[A-Z]+} $ln]} { - continue - } - lappend maincommands {*}$ln - } + lappend maincommands {*}$ln + } } #fossil output was ordered in columns, but we loaded list in row-wise, messing up the order set maincommands [lsort $maincommands] set allcmds [lsort $allcmds] set othercmds [punk::lib::ldiff $allcmds $maincommands] - set fossil_setting_names [lsort [runout -n fossil help -s]] + set setting_info [runout -n fossil help -s] + set setting_info [punk::ansi::ansistrip $setting_info] + set fossil_setting_names [lsort $setting_info] set result "@leaders -min 0\n" @@ -186,6 +190,8 @@ namespace eval punk::repo { foreach ln $basic_opt_lines { set ln [string trim $ln] + #fossil sometimes emits cursor control sequences e.g CSI 3 q + set ln [punk::ansi::ansistrip $ln] if {$ln eq ""} { continue } @@ -250,6 +256,7 @@ namespace eval punk::repo { ${[punk::repo::get_fossil_subcommand_usage add]} @form -form "raw" -synopsis "exec fossil add \[OPTIONS\] FILE1 \[FILE2\]..." + #fossil help may have ansi - review @formdisplay -header "fossil help add" -body {${[runout -n fossil help add]}} } ""] @@ -264,6 +271,7 @@ namespace eval punk::repo { ${[punk::repo::get_fossil_subcommand_usage diff]} @form -form "raw" -synopsis "exec fossil diff \[OPTIONS\] FILE1 \[FILE2\]..." + #fossil help may have ansi - review @formdisplay -header "fossil help diff" -body {${[runout -n fossil help diff]}} } ""] diff --git a/src/bootsupport/modules/punkcheck-0.1.0.tm b/src/bootsupport/modules/punkcheck-0.1.0.tm index eb439c6e..86823a45 100644 --- a/src/bootsupport/modules/punkcheck-0.1.0.tm +++ b/src/bootsupport/modules/punkcheck-0.1.0.tm @@ -121,14 +121,6 @@ namespace eval punkcheck { } method as_record {} { - #set fields [list\ - # -targets $o_targets\ - # -keep_installrecords $o_keep_installrecords\ - # -keep_skipped $o_keep_skipped\ - # -keep_inprogress $o_keep_inprogress\ - # body $o_records\ - #] - dict create {*}{ } tag FILEINFO {*}{ } -targets $o_targets {*}{ @@ -216,18 +208,6 @@ namespace eval punkcheck { } else { set tsiso_end "" } - #set fields [list\ - # -tsiso_begin $tsiso_begin\ - # -ts_begin $o_ts_begin\ - # -tsiso_end $tsiso_end\ - # -ts_end $o_ts_end\ - # -id $o_id\ - # -source $o_rel_sourceroot\ - # -targets $o_rel_targetroot\ - # -types $o_types\ - # -config $o_configdict\ - #] - #set record [dict create tag EVENT {*}$fields] dict create {*}{ } tag EVENT {*}{ diff --git a/src/bootsupport/modules/shellfilter-0.2.2.tm b/src/bootsupport/modules/shellfilter-0.2.2.tm index a0fa060a..6a948593 100644 --- a/src/bootsupport/modules/shellfilter-0.2.2.tm +++ b/src/bootsupport/modules/shellfilter-0.2.2.tm @@ -67,6 +67,8 @@ tcl::namespace::eval shellfilter::log { } proc ::shellfilter::log::close {tag} { #shellthread::manager::close_worker $tag + + #unsubscribe involves a thread::send to the worker thread if we are the last subscriber to the tag. shellthread::manager::unsubscribe [list $tag]; #workertid will be added back to free list if no tags remain subscribed } @@ -106,6 +108,13 @@ namespace eval shellfilter::pipe { package require shellthread #we are only using the fifo in a single direction to pipe to another thread # - so whilst wchan and rchan could theoretically each be both read & write we're only using them for one operation each + #---------------------------------------------------------------------------------------- + # Differences beetween Memchan's fifo2 and tcl::chan::fifo2 implementations: + # (not necessarily a comprehensive list) + # - Memchan's fifo2 is reportedly faster and more efficient than tcl::chan::fifo2, but it may not be available on all platforms + # tcl::chan::fifo2 is a pure Tcl implementation. + # - Closing one side of a tcl::chan::fifo2 (ver 1.1) will cause the other side to close whereas this is not the case with Memchan's fifo2. + #---------------------------------------------------------------------------------------- if {![catch {package require Memchan}]} { lassign [fifo2] wchan rchan } else { @@ -289,7 +298,8 @@ namespace eval shellfilter::chan { set o_enc [tcl::dict::get $tf -encoding] set o_encbuf "" set settingsdict [tcl::dict::get $tf -settings] - set varname [tcl::dict::get $settingsdict -varname] + set varname [tcl::dict::get $settingsdict -varname] ;#review - should we support multiple vars here? e.g with a list of varnames in settings and append to all of them? + #these are not upvared - must be fully qualified variable names. set o_datavars $varname if {[tcl::dict::exists $tf -junction]} { set o_is_junction [tcl::dict::get $tf -junction] @@ -298,18 +308,18 @@ namespace eval shellfilter::chan { } } method initialize {ch mode} { - return [list initialize finalize write flush clear] + if {"read" in $mode} { + #this should raise an error in 'chan push' + error "shellfilter::chan::tee_to_var transform does not support read mode" + } + return [list initialize finalize write flush] } method finalize {ch} { my destroy } - method clear {ch} { - return - } - method watch {ch events} { - # must be present but we ignore it because we do not - # post any events - } + #method clear {ch} { + # return + #} #method read {ch count} { # return ? #} @@ -320,9 +330,14 @@ namespace eval shellfilter::chan { #puts stdout "" #review - just clear o_encbuf and emit nothing? #we wouldn't have a value there if it was convertable from the channel encoding? - set clear $o_encbuf + if {[string length $o_encbuf]} { + #if we have data in the buffer that we haven't been able to convert to a string + #- then we probably have some kind of encoding mismatch. Is it safer to discard it than to emit garbage chars to the channel or var? + #REVIEW - log that we are discarding the buffer contents on flush? + puts stderr "WARNING: flush called on tee_to_var with non-empty buffer. This probably indicates an encoding mismatch between the channel encoding and the encoding expected by the transform. Discarding buffer contents: '$o_encbuf'" + } set o_encbuf "" - return $o_encbuf + return "" } method write {ch bytes} { #test with set x [string repeat " \U1f6c8" 2043] @@ -387,10 +402,12 @@ namespace eval shellfilter::chan { } } method initialize {transform_handle mode} { - return [list initialize read drain write flush clear finalize] + #return [list initialize read drain write flush clear finalize] + return [list initialize write flush clear finalize] } method finalize {transform_handle} { - ::shellfilter::log::close $o_logsource + #Note that an error in the finalize can stop 'chan pop' from running properly. + #::shellfilter::log::close $o_logsource my destroy } method watch {transform_handle events} { @@ -398,26 +415,53 @@ namespace eval shellfilter::chan { # post any events } method clear {transform_handle} { + set o_encbuf "" return } - method drain {transform_handle} { - return "" - } - method read {transform_handle bytes} { - set logdata [tcl::encoding::convertfrom $o_enc $bytes] - #::shellfilter::log::write $o_logsource $logdata - puts -nonewline $o_localchan $logdata - return $bytes - } + #method drain {transform_handle} { + # return "" + #} + #method read {transform_handle bytes} { + # set logdata [tcl::encoding::convertfrom $o_enc $bytes] + # #::shellfilter::log::write $o_logsource $logdata + # puts -nonewline $o_localchan $logdata + # return $bytes + #} + #method flush {transform_handle} { + # #return "" + # set clear $o_encbuf[set o_encbuf ""] + # if {[catch {tcl::encoding::convertfrom $o_enc $clear} stringdata]} { + # #if we can't convert the buffer contents to a string - put it back and try again with more data later + # #REVIEW? + # set o_encbuf $clear + # puts -nonewline $o_localchan "" + # return "" + # } + # #jjj + # puts -nonewline $o_localchan $stringdata + # flush $o_localchan + # return $clear + #} method flush {transform_handle} { - #return "" - set clear $o_encbuf + #we wouldn't have a value in o_encbuf if it was convertable from the channel encoding? + if {[string length $o_encbuf]} { + #if we have data in the buffer that we haven't been able to convert to a string + #- then we probably have some kind of encoding mismatch. Is it safer to discard it than to emit garbage chars to the channel or var? + #REVIEW - log that we are discarding the buffer contents on flush? + puts stderr "WARNING: flush called on tee_to_pipe with non-empty buffer. This probably indicates an encoding mismatch between the channel encoding and the encoding expected by the transform. Discarding buffer contents: '$o_encbuf'" + } set o_encbuf "" - return $o_encbuf + return "" } method write {transform_handle bytes} { #set logdata [tcl::encoding::convertfrom $o_enc $bytes] set inputbytes $o_encbuf$bytes + if {$inputbytes eq ""} { + #review - do we even get empty writes? + puts stderr "WARNING: write called on tee_to_pipe with empty inputbytes. This may be a no-op, but it may also indicate an issue with the upstream transform or channel. Emitting no data to the pipe for this write." + return "" + } + set o_encbuf "" set tail_offset 0 while {$tail_offset < [::tcl::string::length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [::tcl::string::range $inputbytes 0 end-$tail_offset]} stringdata]} { @@ -426,18 +470,23 @@ namespace eval shellfilter::chan { if {$tail_offset > 0} { if {$tail_offset < [::tcl::string::length $inputbytes]} { #stringdata from catch statement must be a valid result + set converted [::tcl::string::range $inputbytes 0 end-$tail_offset] set t [expr {$tail_offset - 1}] set o_encbuf [::tcl::string::range $inputbytes end-$t end] } else { + #nothing convertable in the buffer - put it back and try again with more data later set stringdata "" set o_encbuf $inputbytes return "" } + } else { + #no catch on conversion - everything is convertable + #stringdata must be valid result from convertfrom of whole $o_encbuf$bytes + set converted $inputbytes } #::shellfilter::log::write $o_logsource $logdata puts -nonewline $o_localchan $stringdata - #return $bytes - return [::tcl::string::range $inputbytes 0 end-$tail_offset] + return $converted ;#same data as $stringdata but in bytes form. } #a tee is not a redirection - because data still flows along the main path method meta_is_redirection {} { @@ -501,6 +550,7 @@ namespace eval shellfilter::chan { if {$tail_offset > 0} { if {$tail_offset < [::tcl::string::length $inputbytes]} { #stringdata from catch statement must be a valid result + set converted [::tcl::string::range $inputbytes 0 end-$tail_offset] set t [expr {$tail_offset - 1}] set o_encbuf [::tcl::string::range $inputbytes end-$t end] } else { @@ -508,10 +558,15 @@ namespace eval shellfilter::chan { set o_encbuf $inputbytes return "" } + } else { + #no catch on conversion - everything is convertable + #stringdata must be valid result from convertfrom of whole $o_encbuf$bytes + set converted $inputbytes } - set bytes [::tcl::string::range $inputbytes 0 end-$tail_offset] + #set bytes [::tcl::string::range $inputbytes 0 end-$tail_offset] ::shellfilter::log::write $o_logsource $stringdata - return $bytes + #return $bytes + return $converted ;#same data as $stringdata but in bytes form. } method meta_is_redirection {} { return $o_is_junction @@ -519,6 +574,10 @@ namespace eval shellfilter::chan { } + #see TIP 230 Tcl Channel Transformation Reflection API + + #the logonly transform does not emit any data downwards towards the base channel - it only writes to the log. + # - ie we are redirecting all data to the log. oo::class create logonly { variable o_tid variable o_logsource @@ -537,19 +596,35 @@ namespace eval shellfilter::chan { set o_tid [::shellfilter::log::open $o_logsource $settingsdict] } method initialize {transform_handle mode} { - return [list initialize finalize write] + #mode is a list containing any of the strings 'read' or 'write'. + if {"read" in $mode} { + #Note that raising an error prevents creation of the transformation. + #The thrown error will appear as a error thrown by 'chan push'. + error "logonly transform does not support read mode" + } + #return all methods supported by the handler + #(we exclude our additional meta_is_redirection method becuase it is not a standard transform method called by the Tcl framework - it's only for our own use in shellfilter) + return [list initialize finalize write flush] } method finalize {transform_handle} { - ::shellfilter::log::close $o_logsource + #review - tip 230 states that any return value or error raised by finalize is ignored + #we wrap in catch to ensure the 'my destroy' is always called. + catch {::shellfilter::log::close $o_logsource} my destroy } - method watch {transform_handle events} { - # must be present but we ignore it because we do not - # post any events + #clear? + method flush {transform_handle} { + #we wouldn't have a value in o_encbuf if it was convertable from the channel encoding? + if {[string length $o_encbuf]} { + #if we have data in the buffer that we haven't been able to convert to a string + #- then we probably have some kind of encoding mismatch. Is it safer to discard it than to emit garbage chars to the log? + #REVIEW. - we are writing the raw bytes to the log here because we can't convert them to a string. + #This may be useful for debugging issues, but it may also result in garbage data in the log. + ::shellfilter::log::write $o_logsource $o_encbuf + set o_encbuf "" + } + return } - #method read {transform_handle count} { - # return ? - #} method write {transform_handle bytes} { #set logdata [encoding convertfrom $o_enc $bytes] set inputbytes $o_encbuf$bytes @@ -600,26 +675,49 @@ namespace eval shellfilter::chan { } } method initialize {transform_handle mode} { - return [list initialize read write clear flush drain finalize] + #return [list initialize read write clear flush drain finalize] + + #REVIEW - we aren't using 'read' mode - but if we raise an error if 'read' is in the mode then the system doesn't work. + #this is probably because we add it to one end of a fifo2 channel, which although we only use for writing is a bidirectional channel. + #---------------------- + #don't do this + #---------------------- + #if {"read" in $mode} { + # #Note that raising an error prevents creation of the transformation. + # #The thrown error will appear as a error thrown by 'chan push'. + # error "shellfilter::chan::ansistrip transform does not support read mode" + #} + #---------------------- + return [list initialize read write flush finalize] } method finalize {transform_handle} { my destroy } - method clear {transform_handle} { - return - } - method watch {transform_handle events} { - } - method drain {transform_handle} { - return "" - } + #method clear {transform_handle} { + # return + #} + #method drain {transform_handle} { + # return "" + #} method read {transform_handle bytes} { set instring [encoding convertfrom $o_enc $bytes] set outstring [punk::ansi::ansistrip $instring] return [encoding convertto $o_enc $outstring] } + #method flush {transform_handle} { + # return "" + #} method flush {transform_handle} { - return "" + #return "" + set clear $o_encbuf[set o_encbuf ""] + if {[catch {tcl::encoding::convertfrom $o_enc $clear} stringdata]} { + #if we can't convert the buffer contents to a string - put it back and try again with more data later + #REVIEW? + set o_encbuf $clear + return "" + } + #review + return $stringdata } #method write {transform_handle bytes} { # #broken due to occasional unexpected byte sequence @@ -989,29 +1087,44 @@ namespace eval shellfilter::chan { } method initialize {transform_handle mode} { #clear undesirable in terminal output channels (review) - return [list initialize write flush read drain finalize] + #return [list initialize write flush read drain finalize] + if {$mode eq "read"} { + error "shellfilter::chan::ansiwrap channel transform does not support read mode" + } + return [list initialize write flush finalize] } method finalize {transform_handle} { my destroy } - method watch {transform_handle events} { - } method clear {transform_handle} { #In the context of stderr/stdout - we probably don't want clear to run. #Terminals might call it in the middle of a split ansi code - resulting in broken output. - #Leave clear of it the init call + #Leave clear out of the initialize call for now puts stdout "" set emit [tcl::encoding::convertto $o_enc $o_buffered] set o_buffered "" return $emit } + #method flush {transform_handle} { + # #puts stdout "" + # set inputbytes $o_buffered$o_encbuf + # set emit [tcl::encoding::convertto $o_enc $inputbytes] + # set o_buffered "" + # set o_encbuf "" + # return $emit + #} method flush {transform_handle} { - #puts stdout "" - set inputbytes $o_buffered$o_encbuf - set emit [tcl::encoding::convertto $o_enc $inputbytes] + #return "" + set clear $o_buffered$o_encbuf + if {[catch {tcl::encoding::convertfrom $o_enc $clear} stringdata]} { + #if we can't convert the buffer contents to a string - does it make sense to emit the raw bytes? + # - probably not. + #REVIEW? + return "" + } set o_buffered "" set o_encbuf "" - return $emit + return $stringdata } method write {transform_handle bytes} { #set instring [tcl::encoding::convertfrom $o_enc $bytes] ;naive approach will break due to unexpected byte sequence - occasionally @@ -1076,14 +1189,14 @@ namespace eval shellfilter::chan { #set outstring ">>>$instring" return [tcl::encoding::convertto $o_enc $outstring] } - method drain {transform_handle} { - return "" - } - method read {transform_handle bytes} { - set instring [tcl::encoding::convertfrom $o_enc $bytes] - set outstring "$o_do_colour$instring$o_do_normal" - return [tcl::encoding::convertto $o_enc $outstring] - } + #method drain {transform_handle} { + # return "" + #} + #method read {transform_handle bytes} { + # set instring [tcl::encoding::convertfrom $o_enc $bytes] + # set outstring "$o_do_colour$instring$o_do_normal" + # return [tcl::encoding::convertto $o_enc $outstring] + #} method meta_is_redirection {} { return $o_is_junction } @@ -1326,9 +1439,11 @@ namespace eval shellfilter::stack { } proc status {{pipename *} args} { variable pipelines + package require textblock set pipecount [dict size $pipelines] set tabletitle "$pipecount pipelines active" set t [textblock::class::table new $tabletitle] + $t configure -frametype ascii; #be conservative here - may need to emit in various debugging contexts. $t add_column -headers [list channel-ident] $t add_column -headers [list device-info localchan] $t configure_column 1 -header_colspans {3} @@ -1337,6 +1452,9 @@ namespace eval shellfilter::stack { $t add_column -headers [list stack-info] foreach k [dict keys $pipelines $pipename] { set lc [dict get $pipelines $k device localchan] + if {[catch {chan configure $lc -encoding} lc_enc]} { + set lc_enc "" + } set rc [dict get $pipelines $k device remotechan] if {[dict exists $k device workertid]} { set tid [dict get $pipelines $k device workertid] @@ -1348,18 +1466,35 @@ namespace eval shellfilter::stack { set stackinfo "" } else { set tbl_inner [textblock::class::table new] + $tbl_inner configure -frametype ascii $tbl_inner configure -show_edge 0 + $tbl_inner add_column -headers id + $tbl_inner add_column -headers transform + $tbl_inner add_column -headers handle + $tbl_inner add_column -headers settings + $tbl_inner add_column -headers aside foreach rec $stack { - set handle [punk::lib::dict_getdef $rec -handle ""] set id [punk::lib::dict_getdef $rec -id ""] set transform [namespace tail [punk::lib::dict_getdef $rec -transform ""]] + set handle [punk::lib::dict_getdef $rec -handle ""] + if {$handle ne ""} { + if {[catch {chan configure $handle -encoding} handle_enc]} { + set handle_enc "" + } + } else { + set handle_enc "" + } set settings [punk::lib::dict_getdef $rec -settings ""] - $tbl_inner add_row [list $id $transform $handle $settings] + set aside [punk::lib::dict_getdef $rec -aside ""] + if {$aside ne ""} { + set aside [punk::lib::showdict $aside] + } + $tbl_inner add_row [list $id $transform $handle\n$handle_enc $settings $aside] } set stackinfo [$tbl_inner print] $tbl_inner destroy } - $t add_row [list $k $lc $rc $tid $stackinfo] + $t add_row [list $k "$lc\n$lc_enc" $rc $tid $stackinfo] } set result [$t print] $t destroy @@ -1511,15 +1646,24 @@ namespace eval shellfilter::stack { proc unwind {pipename} { variable pipelines - set stack [dict get $pipelines $pipename stack] - set localchan [dict get $pipelines $pipename device localchan] + set stack [dict get $pipelines $pipename stack] + set localchan [dict get $pipelines $pipename device localchan] foreach tf [lreverse $stack] { - chan pop $localchan + if {[catch {chan eof $localchan} _eof]} { + #We don't actually care about eof state - but we use this to test if the channel exists. + #('chan names' doesn't reliably show all channels in some cases) + #do nothing. + } else { + #if there are no transforms on the the channel - this is equivalent to 'chan close' of the channel + # but here we should only be calling it when there are transforms on the channel as indicated by the stack variable. + chan pop $localchan + } } dict set pipelines $pipename [list] } #todo proc delete {pipename {wait 0}} { + #::shellfilter::log::open shellfilter-delete [list -syslog "127.0.0.1:514"] variable pipelines set pipeinfo [dict get $pipelines $pipename] set deviceinfo [dict get $pipeinfo device] @@ -1534,18 +1678,29 @@ namespace eval shellfilter::stack { thread::release $tid } - #Memchan closes without error - tcl::chan::fifo2 raises something like 'can not find channel named "rc977"' - REVIEW. why? + #Memchan closes without error - tcl::chan::fifo2 raises something like 'can not find channel named "rc977"' + #- REVIEW. why? It could have something to do with the fact that tcl::memchan::fifo2 closes both sides when one side is closed. catch {chan close $localchan} + #if {[catch {chan close $localchan} errMsg]} { + # ::shellfilter::log::write shellfilter-delete "WARNING: error closing localchan '$localchan' for pipename '$pipename': $errMsg" + #} } #review - proc name clarity is questionable. remove_stackitem? proc remove {pipename remove_id} { + #::shellfilter::log::open shellfilter-remove [list -syslog "127.0.0.1:514"] variable pipelines if {![dict exists $pipelines $pipename]} { - puts stderr "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]" + #puts stderr "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]" + ::shellfilter::log::write shellfilter-remove "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]" return } set stack [dict get $pipelines $pipename stack] set localchan [dict get $pipelines $pipename device localchan] + set previous_blockingstate [chan configure $localchan -blocking] + if {$previous_blockingstate} { + chan configure $localchan -blocking 0 + } + set posn 0 set idposn -1 set asideposn -1 @@ -1572,10 +1727,11 @@ namespace eval shellfilter::stack { dict set container -aside {} lset stack $asideposn $container dict set pipelines $pipename stack $stack + #::shellfilter::log::write shellfilter-remove "cleared '-aside' record for pipename $pipename aside_posn $asideposn remove_id:'$remove_id'" } else { if {$idposn < 0} { - ::shellfilter::log::write shellfilter "ERROR shellfilter::stack::remove $pipename id '$remove_id' not found" - puts stderr "|WARNING>shellfilter::stack::remove $pipename id '$remove_id' not found" + #::shellfilter::log::write shellfilter-remove "ERROR shellfilter::stack::remove $pipename id '$remove_id' not found" + #puts stderr "|WARNING>shellfilter::stack::remove $pipename id '$remove_id' not found" return 0 } set removed_item [lindex $stack $idposn] @@ -1589,32 +1745,58 @@ namespace eval shellfilter::stack { #pop all chans before adding anything back in! foreach p $poplist { + #review + #update idletasks + #puts stderr "DEBUG> popping transform from pipename $pipename for stack p:$p poplist len:[llength $poplist]" + #::shellfilter::log::write shellfilter-remove "popping transform from pipename $pipename for stack p:$p poplist len:[llength $poplist] ---" + #after 0 [list chan pop $localchan] + chan pop $localchan + #::shellfilter::log::write shellfilter-remove "POPPED" + #update idletasks } + #after 5 + #::shellfilter::log::write shellfilter-remove "remove. popped all transforms above and including idposn $idposn for pipename $pipename poplist len:[llength $poplist]" + #puts stderr "DEBUG> popped all transforms above and including idposn $idposn for pipename $pipename poplist len:[llength $poplist]" - if {[llength [dict get $removed_item -aside]]} { - set restore [dict get $removed_item -aside] - set t [dict get $restore -transform] - set tsettings [dict get $restore -settings] + if {[llength [dict get $removed_item -aside]]} { + set restore [dict get $removed_item -aside] + set t [dict get $restore -transform] + set tsettings [dict get $restore -settings] set obj [$t new $restore] set h [chan push $localchan $obj] dict set restore -handle $h dict set restore -obj $obj lappend stack $restore + #puts stderr "DEBUG> restored aside for pipename $pipename asideposn $asideposn remove_id:'$remove_id' transform: $t handle:$h obj:$obj" + #::shellfilter::log::write shellfilter-remove "restored aside for pipename $pipename asideposn $asideposn remove_id:'$remove_id' transform: $t handle:$h obj:$obj" } + #after 5 #put popped back except for the first one, which we want to remove foreach p [lrange $poplist 1 end] { - set t [dict get $p -transform] - set tsettings [dict get $p -settings] + set t [dict get $p -transform] + set tsettings [dict get $p -settings] set obj [$t new $p] set h [chan push $localchan $obj] + dict set p -handle $h dict set p -obj $obj lappend stack $p + #update idletasks + #puts stderr "DEBUG> restored for pipename $pipename id '$remove_id' transform:$t handle $h obj:$obj" + #::shellfilter::log::write shellfilter-remove "restored for pipename $pipename id '$remove_id' transform:$t handle $h obj:$obj" } + #after 5 dict set pipelines $pipename stack $stack } + + #puts stderr "DEBUG> pipename $pipename id '$remove_id' DONE" + #::shellfilter::log::write shellfilter-remove "pipename $pipename id '$remove_id' DONE" + + if {$previous_blockingstate} { + chan configure $localchan -blocking 1 + } #JMNJMN 2025 review! #show_pipeline $pipename -note "after_remove $remove_id" return 1 @@ -1853,6 +2035,41 @@ namespace eval shellfilter::stack { namespace eval shellfilter { variable sources [list] variable stacks [dict create] + #------------------------------------------------------------------------------------------------- + #tcllib logger infrastructure. + #------------------------------------------------------------------------------------------------- + namespace eval ::shellfilter::loggerprocs { + #container for procs/aliases to be pointed to by tcllib logger using log::logproc + + proc Dolog {lvl txt} { + #logger calls this in such a way that a straight uplevel can get us the vars/commands in messages substituted + set msg "[clock format [clock seconds] -format "%Y-%m-%dT%H:%M:%S"] ::shellspy $lvl '[uplevel [list subst $txt]]'" + puts stderr $msg + } + proc Runlog {lvl script} { + uplevel 1 $script + } + } + if {![catch { + package require logger + }]} { + logger::initNamespace ::shellfilter + foreach lvl [logger::levels] { + interp alias {} ::shellfilter::loggerprocs::Log_$lvl {} ::shellfilter::loggerprocs::Runlog $lvl + log::logproc $lvl ::shellfilter::loggerprocs::Log_$lvl + } + logger::setlevel warn + #namespace path ::shellfilter::log + } else { + #e.g tcllib not available, safe interp? + #fake out the logger calls + namespace eval ::shellfilter::log { + foreach lvl {debug info notice warn error critical alert emergency} { + proc $lvl {args} {} + } + } + } + #------------------------------------------------------------------------------------------------- proc ::shellfilter::redir_channel_to_log {chan args} { variable sources @@ -2396,13 +2613,9 @@ namespace eval shellfilter { #must be a list. If it was a shell commandline string. convert it elsewhere first. variable sources - set runtag "shellfilter-run" - #set tid [::shellfilter::log::open $runtag [list -syslog 127.0.0.1:514]] - set tid [::shellfilter::log::open $runtag [list -syslog ""]] if {[catch {llength $commandlist} listlen]} { set listlen "" } - ::shellfilter::log::write $runtag " commandlist:'$commandlist' listlen:$listlen strlen:[string length $commandlist]" #flush stdout #flush stderr @@ -2416,6 +2629,7 @@ namespace eval shellfilter { -errchan stderr -inchan stdin -tclscript 0 + -syslog "" }] set opts [dict merge $defaults $args] @@ -2432,6 +2646,13 @@ namespace eval shellfilter { set teehandle_err ${teehandle}err set teehandle_in ${teehandle}in + set syslog [dict get $opts -syslog] + dict unset opts -syslog + set runtag "shellfilter-run" + set tid [::shellfilter::log::open $runtag [list -syslog 127.0.0.1:514]] + #set tid [::shellfilter::log::open $runtag [list -syslog $syslog]] + log::info {::shellfilter::log::write $runtag " opts: $opts"} + log::info {::shellfilter::log::write $runtag " commandlist:'$commandlist' listlen:$listlen strlen:[string length $commandlist]"} #puts stdout "shellfilter initialising tee_to_pipe transforms for in/out/err" @@ -2441,19 +2662,32 @@ namespace eval shellfilter { lappend sources $source } } - set outdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_out device] - set outpipechan [dict get $outdeviceinfo localchan] - set errdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_err device] - set errpipechan [dict get $errdeviceinfo localchan] + set outdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_out device] + set outpipechan [dict get $outdeviceinfo localchan] + set errdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_err device] + set errpipechan [dict get $errdeviceinfo localchan] #set indeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_in device] #set inpipechan [dict get $indeviceinfo localchan] + #--------------------- + # #TEST + # chan configure $outpipechan -blocking 0 + # chan configure $errpipechan -blocking 0 + log::debug {::shellfilter::log::write $runtag " outchan $outchan -pipechan $outpipechan config: [chan configure $outpipechan]"} + log::debug {::shellfilter::log::write $runtag " errchan $errchan -pipechan $errpipechan config: [chan configure $errpipechan]"} + #--------------------- + + + log::info {::shellfilter::log::write $runtag " calling shellfilter::stack::add for outchan:$outchan and errchan:$errchan with tee_to_pipe transforms. out tag: $teehandle_out outpipechan:$outpipechan err tag $teehandle_err errpipechan:$errpipechan"} #NOTE:These transforms are not necessarily at the top of each stack! #The float/sink mechanism, along with whether existing transforms are diversionary decides where they sit. set id_out [shellfilter::stack::add $outchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_out -pipechan $outpipechan]] set id_err [shellfilter::stack::add $errchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_err -pipechan $errpipechan]] + log::critical { + ::shellfilter::log::write $runtag "[punk::ansi::ansistrip [shellfilter::stack status]]\nchan names:[chan names]" + } # need to use os level channel handle for stdin - try named pipes (or even sockets) instead of fifo2 for this # If non os-level channel - the command can't be run with the redirection # stderr/stdout can be run with non-os handles in the call - @@ -2493,6 +2727,7 @@ namespace eval shellfilter { set exitinfo [list error "$errMsg" source shellcommand_stdout_stderr] } } + log::notice {::shellfilter::log::write $runtag "finished shell command execution with exitinfo '$exitinfo'"} } else { if {[catch { #script result @@ -2500,29 +2735,45 @@ namespace eval shellfilter { } errMsg]} { set exitinfo [list error "$errMsg" errorCode $::errorCode errorInfo "$::errorInfo"] } + log::notice {::shellfilter::log::write $runtag "finished script execution with exitinfo '$exitinfo'"} } #puts "shellfilter::run finished call" #------------------------- #warning - without flush stdout - we can get hang, but only on some terminals # - mechanism for this problem not understood! + #todo - test/document. flush stdout flush stderr + #update idletasks #------------------------- - #the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal - #Remove execution-time Tees from stack - shellfilter::stack::remove stdout $id_out - shellfilter::stack::remove stderr $id_err - #shellfilter::stack::remove stderr $id_in - #puts stderr "shellfilter::run complete..." + #---------------------------------------------------------------------------------------------- + # wrapped using tcllib logger - avoid even generating the shellfilter::stack status table if log level above debug. + # Logger allows the contents to be evaluated only if logging is switched on. + #---------------------------------------------------------------------------------------------- + #todo - change to log::debug + log::critical { + if {![catch {package require punk::ansi}]} { + set stackstatus [punk::ansi::ansistrip [shellfilter::stack status]] + } else { + set stackstatus [shellfilter::stack status] + } + ::shellfilter::log::write $runtag "shellfilter::stack status after execution: \n$stackstatus\nchan:names [chan names]" + } + #---------------------------------------------------------------------------------------------- + - #chan configure stderr -buffering line - #flush stdout + #the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal + #Remove execution-time Tees from stack + log::debug {::shellfilter::log::write $runtag "removing $id_out from stdout stack"} + shellfilter::stack::remove $outchan $id_out + log::debug {::shellfilter::log::write $runtag "removing $id_err from stderr stack"} + shellfilter::stack::remove $errchan $id_err - ::shellfilter::log::write $runtag " return '$exitinfo'" + log::info {::shellfilter::log::write $runtag " return '$exitinfo'"} ::shellfilter::log::close $runtag return $exitinfo } @@ -2552,6 +2803,7 @@ namespace eval shellfilter { } if {$close} { lappend tidied_sources $s + #unsubscribe from source tag s. shellfilter::log::close $s lappend worker_errorlist {*}[shellthread::manager::get_and_clear_errors $s] } diff --git a/src/bootsupport/modules/shellthread-1.6.2.tm b/src/bootsupport/modules/shellthread-1.6.2.tm index 6338d1c6..79d12149 100644 --- a/src/bootsupport/modules/shellthread-1.6.2.tm +++ b/src/bootsupport/modules/shellthread-1.6.2.tm @@ -119,29 +119,58 @@ namespace eval shellthread::worker { set waitvar ::shellthread::worker::wait($inpipe,[clock micros]) #tcl::chan::fifo2 based pipe seems slower to establish events upon than Memchan - chan event $readchan readable [list ::shellthread::worker::pipe_read $readchan $source $waitvar $readbuffering $writebuffering] - vwait $waitvar - } - proc pipe_read {chan source waitfor readbuffering writebuffering} { + #chan event $readchan readable [list ::shellthread::worker::pipe_read $readchan $source $waitvar $readbuffering $writebuffering] if {$readbuffering eq "line"} { - set chunksize [chan gets $chan chunk] - if {$chunksize >= 0} { - if {![chan eof $chan]} { - ::shellthread::worker::log pipe 0 - $source - info $chunk\n $writebuffering - } else { - ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering + chan event $readchan readable [list apply {{chan source waitfor writebuffering} { + set chunksize [chan gets $chan chunk] + if {$chunksize >= 0} { + if {![chan eof $chan]} { + ::shellthread::worker::log pipe 0 - $source - info $chunk\n $writebuffering + } else { + ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering + } } - } + if {[chan eof $chan]} { + chan event $chan readable {} + set $waitfor "pipe" + chan close $chan + } + }} $readchan $source $waitvar $writebuffering] + } else { - set chunk [chan read $chan] - ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering - } - if {[chan eof $chan]} { - chan event $chan readable {} - set $waitfor "pipe" - chan close $chan + chan event $readchan readable [list apply {{chan source waitfor writebuffering} { + set chunk [chan read $chan] + ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering + if {[chan eof $chan]} { + chan event $chan readable {} + set $waitfor "pipe" + chan close $chan + } + }} $readchan $source $waitvar $writebuffering] } + + vwait $waitvar } + #proc pipe_read {chan source waitfor readbuffering writebuffering} { + # if {$readbuffering eq "line"} { + # set chunksize [chan gets $chan chunk] + # if {$chunksize >= 0} { + # if {![chan eof $chan]} { + # ::shellthread::worker::log pipe 0 - $source - info $chunk\n $writebuffering + # } else { + # ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering + # } + # } + # } else { + # set chunk [chan read $chan] + # ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering + # } + # if {[chan eof $chan]} { + # chan event $chan readable {} + # set $waitfor "pipe" + # chan close $chan + # } + #} proc start_pipe_write {source writechan args} { variable outpipe @@ -181,8 +210,8 @@ namespace eval shellthread::worker { chan configure $writechan -blocking 0 set waitvar ::shellthread::worker::wait($outpipe,[clock micros]) - chan event $readchan readable [list apply {{chan writechan source waitfor readbuffering} { - if {$readbuffering eq "line"} { + if {$readbuffering eq "line"} { + chan event $readchan readable [list apply {{chan writechan source waitfor} { set chunksize [chan gets $chan chunk] if {$chunksize >= 0} { if {![chan eof $chan]} { @@ -191,19 +220,55 @@ namespace eval shellthread::worker { puts -nonewline $writechan $chunk } } - } else { + if {[chan eof $chan]} { + chan event $chan readable {} + set $waitfor "pipe" + flush $writechan ;#2026-05-19 - ensure all data is sent before closing + chan close $writechan + if {$chan ne "stdin"} { + chan close $chan + } + } + }} $readchan $writechan $source $waitvar] + } else { + chan event $readchan readable [list apply {{chan writechan source waitfor} { set chunk [chan read $chan] puts -nonewline $writechan $chunk - } - if {[chan eof $chan]} { - chan event $chan readable {} - set $waitfor "pipe" - chan close $writechan - if {$chan ne "stdin"} { - chan close $chan + if {[chan eof $chan]} { + chan event $chan readable {} + set $waitfor "pipe" + chan close $writechan + if {$chan ne "stdin"} { + chan close $chan + } } - } - }} $readchan $writechan $source $waitvar $readbuffering] + }} $readchan $writechan $source $waitvar] + + } + + # chan event $readchan readable [list apply {{chan writechan source waitfor readbuffering} { + # if {$readbuffering eq "line"} { + # set chunksize [chan gets $chan chunk] + # if {$chunksize >= 0} { + # if {![chan eof $chan]} { + # puts $writechan $chunk + # } else { + # puts -nonewline $writechan $chunk + # } + # } + # } else { + # set chunk [chan read $chan] + # puts -nonewline $writechan $chunk + # } + # if {[chan eof $chan]} { + # chan event $chan readable {} + # set $waitfor "pipe" + # chan close $writechan + # if {$chan ne "stdin"} { + # chan close $chan + # } + # } + # }} $readchan $writechan $source $waitvar $readbuffering] vwait $waitvar } @@ -479,6 +544,10 @@ namespace eval shellthread::manager { set sourcetag [lindex $sourcetaglist 0] ;#todo - use all set defaults [dict create {*}{ + -raw 0 + -file {} + -syslog {} + -direction out -workertype message }] set settingsdict [dict merge $defaults $settingsdict] @@ -501,6 +570,10 @@ namespace eval shellthread::manager { return [dict get $winfo tid] } elseif {$existing_settings eq {-raw 0 -file {} -syslog {} -direction out}} { #review - magic dict seems brittle - shouldn't hard code here.??? + + #existing worker has default settings - so we'll assume it's a placeholder and update it with our settings + #review - where/when do we override the default settings? + dict lappend winfo list_client_tids $tidclient dict set workers $sourcetag $winfo ;#writeback return [dict get $winfo tid] @@ -575,11 +648,11 @@ namespace eval shellthread::manager { package require Thread package require shellthread if {![catch {::shellthread::worker::init %tidcli% %ts_start% $::settingsinfo} errmsg]} { - unset ::settingsinfo - set ::shellthread_init "ok" + unset ::settingsinfo + set ::shellthread_init "ok" } else { - unset ::settingsinfo - set ::shellthread_init "err $errmsg" + unset ::settingsinfo + set ::shellthread_init "err $errmsg" } }] @@ -622,15 +695,23 @@ namespace eval shellthread::manager { proc write_log {source msg args} { variable workers - set ts_micros_sent [clock micros] - set defaults [list -async 1 -level info] - set opts [dict merge $defaults $args] - - if {[dict exists $workers $source]} { + if {[dict exists $workers $source tid]} { set tidworker [dict get $workers $source tid] if {$tidworker eq "noop"} { return } + } else { + set tidworker "" + } + + set ts_micros_sent [clock micros] + set defaults [list {*}{ + -async 1 + -level info + }] + set opts [dict merge $defaults $args] + + if {$tidworker ne ""} { if {![thread::exists $tidworker]} { # -syslog -file ? set tidworker [new_worker $source] @@ -674,10 +755,7 @@ namespace eval shellthread::manager { if {[dict exists $workers $source]} { set list_client_tids [dict get $workers $source list_client_tids] if {[set posn [lsearch $list_client_tids $mytid]] >= 0} { - #set list_client_tids [lreplace $list_client_tids $posn $posn] - #set list_client_tids [lreplace $list_client_tids[set list_client_tids {}] $posn $posn] ledit list_client_tids $posn $posn - dict set workers $source list_client_tids $list_client_tids } if {![llength $list_client_tids]} { @@ -685,7 +763,6 @@ namespace eval shellthread::manager { } } } - #we've removed our own tid from all the tags - possibly across multiplew workertids, and possibly leaving some workertids with no subscribers for a particular tag - or no subscribers at all. set subscriberless_workers [list] @@ -696,8 +773,8 @@ namespace eval shellthread::manager { set subscriber_count 0 set kill_count 0 ;#number of ts_end_list entries - even one indicates thread is doomed foreach taginfo $worker_tags { - incr subscriber_count [llength [dict get $taginfo list_client_tids]] - incr kill_count [llength [dict get $taginfo ts_end_list]] + incr subscriber_count [llength [dict get $taginfo list_client_tids]] + incr kill_count [llength [dict get $taginfo ts_end_list]] } if {$subscriber_count == 0} { lappend subscriberless_workers $workertid @@ -760,7 +837,7 @@ namespace eval shellthread::manager { set ::shellthread::waitfor waiting #after $timeout [list set ::shellthread::waitfor] #2025-07 timed-out untested review - set cancelid [after $timeout [list set ::shellthread::waitfor timed-out]] + set timeout_timer [after $timeout {set ::shellthread::waitfor timed-out}] set waiting_for [list] set ended [list] @@ -769,7 +846,9 @@ namespace eval shellthread::manager { if {[thread::exists $tid]} { lappend waiting_for $tid #thread::send -async $tid [list shellthread::worker::terminate [thread::id]] timeoutarr(shutdown_free_threads) - thread::send -async $tid [list shellthread::worker::terminate [thread::id]] ::shellthread::waitfor + set tid_client [thread::id] + #shellthread::worker::terminate will return thread id of terminating thread (or empty string) + thread::send -async $tid [list shellthread::worker::terminate $tid_client] ::shellthread::waitfor } } if {[llength $waiting_for]} { @@ -779,13 +858,13 @@ namespace eval shellthread::manager { set timedout 1 break } else { - after cancel $cancelid + after cancel $timeout_timer lappend ended $::shellthread::waitfor } } } set free_threads [list] - return [dict create existed $waiting_for ended $ended timedout $timedout] + return [dict create existed $waiting_for ended $ended timedout $timedout allthreads [thread::names]] } #TODO - important. diff --git a/src/bootsupport/modules/textblock-0.1.3.tm b/src/bootsupport/modules/textblock-0.1.3.tm index d664aad0..7609c2ed 100644 --- a/src/bootsupport/modules/textblock-0.1.3.tm +++ b/src/bootsupport/modules/textblock-0.1.3.tm @@ -5107,9 +5107,25 @@ tcl::namespace::eval textblock { tcl::mathfunc::min {*}[lmap v [split $textblock \n] {tcl::string::length $v}] } - if {[catch {package require parser}]} { - #tclparser c extension not available - use tcl string functions to count line-endings + + #try not to load parser (and associated punk::args::moduledoc::parser) immediately. + if {[package provide parser] ne ""} { + #parser already loaded. proc height {textblock} { + if {[string first \v $textblock] >= 0} { + #use standard (slower) mechanism for counting lines + #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 + } else { + return [expr {[parse countnewline $textblock {}] + 1}] + } + } + } else { + #parser not loaded - but might be loadable. + #install a 'height' function that will load parser + proc _height_tcl {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) @@ -5119,8 +5135,7 @@ tcl::namespace::eval 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 return [expr {$num_le + 1}] ;# one line if no le - 2 if there is one trailing le even if no data follows le } - } else { - proc height {textblock} { + proc _height_c {textblock} { if {[string first \v $textblock] >= 0} { #use standard (slower) mechanism for counting lines #vertical tab on a proper terminal should move directly down. @@ -5131,7 +5146,22 @@ tcl::namespace::eval textblock { return [expr {[parse countnewline $textblock {}] + 1}] } } + + #oneshot height function - renames itself on first call to the appropriate implementation. + proc height {textblock} { + if {[catch {package require parser}]} { + #parser not available - use tcl implementation + rename ::textblock::height "" + rename ::textblock::_height_tcl ::textblock::height + } else { + #parser available - use c implementation + rename ::textblock::height "" + rename ::textblock::_height_c ::textblock::height + } + tailcall ::textblock::height $textblock + } } + #MAINTENANCE - same as overtype::blocksize? proc size {textblock} { if {$textblock eq ""} { @@ -8141,16 +8171,17 @@ tcl::namespace::eval textblock { -etabs -default 0\ -help "expanding tabs - experimental/unimplemented." #review - -choicelabels placeholder dollarsign of textblock::frame_samples must be left aligned with -choicelabels - -type -default light\ - -type dict\ - -typesynopsis {${$I}choice${$NI}|<${$I}dict${$NI}>}\ - -choices {${$DYN_FRAMETYPES}}\ - -choicerestricted 0 -choicecolumns 8\ - -unindentedfields {-choicelabels}\ - -choicelabels { - ${$DYN_FRAMESAMPLES} - }\ - -help "Type of border for frame." + -type -default light\ + -type dict\ + -typesynopsis {${$I}choice${$NI}|<${$I}dict${$NI}>}\ + -choices {${$DYN_FRAMETYPES}}\ + -choicerestricted 0\ + -choicecolumns 8\ + -unindentedfields {-choicelabels}\ + -choicelabels { + ${$DYN_FRAMESAMPLES} + }\ + -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]}" diff --git a/src/bootsupport/modules/zipper-0.14.tm b/src/bootsupport/modules/zipper-0.14.tm index d0cb4f1c740f48ca0a5d46b092f71346ce4ef9e6..7f7817f1ddce7fa6abfdea66e3c84636d1fe44cd 100644 GIT binary patch delta 24 dcmX@+d(3x(s1)-pjy; 2} { - #if more than just numitems and indexset - we expect only -base ie 4 args in total - if {[llength $args] != 4} { - set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] - } - set optname [lindex $args 0] - set optval [lindex $args 1] - set fulloptname [tcl::prefix::match -error "" -base $optname] - if {$fulloptname ne "-base" || ![string is integer -strict $optval]} { - set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + dict for {opt val} $optlist { + set fulloptname [tcl::prefix::match -error "" {-base -return} $opt] + switch -exact -- $fulloptname { + -return { + set fullval [tcl::prefix::match -error "" {indices pairs} $val] + if {$fullval ni {indices pairs}} { + set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] + uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + } + set returntype $fullval + } + -base { + if {![string is integer -strict $val]} { + set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] + uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + } + set base $val + } + default { + set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] + uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + } } - set base $optval } + + #set base 0 ;#default + #if {[llength $args] > 2} { + # #if more than just numitems and indexset - we expect only -base ie 4 args in total + # if {[llength $args] != 4} { + # set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] + # uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + # } + # set optname [lindex $args 0] + # set optval [lindex $args 1] + # set fulloptname [tcl::prefix::match -error "" -base $optname] + # if {$fulloptname ne "-base" || ![string is integer -strict $optval]} { + # set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] + # uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + # } + # set base $optval + #} # -------------------------------------------------- @@ -5160,8 +5208,84 @@ namespace eval punk::lib { } } } + if {$returntype eq "pairs"} { + return [indices_to_pairs $index_list] + } return $index_list } + proc indices_to_pairs {indices} { + #convert a list of indices to a list of pairs representing the start and end of contiguous runs of indices which can be increasing or decreasing. + #if the direction of the run changes - we end the previous run and start a new one + if {[llength $indices] == 0} { + return [list] + } + set pairs [list] + set start [lindex $indices 0] + set prev $start + set direction 0 ;#0 = unknown, 1 = increasing, -1 = decreasing + for {set i 1} {$i < [llength $indices]} {incr i} { + set idx [lindex $indices $i] + if {![string is integer -strict $idx]} { + error "non-integer index '$idx' in indices list" + } + if {$idx == $prev + 1} { + #increase since prev + if {$direction == 0} { + set direction 1 + } elseif {$direction == -1} { + #direction changed - end previous run and start new one + lappend pairs [list $start $prev] + set start $idx + set direction 0 + } else { + #still increasing + } + set prev $idx + } elseif {$idx == $prev - 1} { + #decrease since prev + if {$direction == 0} { + set direction -1 + } elseif {$direction == 1} { + #direction changed - end previous run and start new one + lappend pairs [list $start $prev] + set start $idx + set direction 0 + } + set prev $idx + } else { + #run ended - add pair to list + lappend pairs [list $start $prev] + set start $idx + set prev $idx + set direction 0 + } + } + # add final run + lappend pairs [list $start $prev] + return $pairs + } + + #proc indices_to_pairs {indices} { + # #convert a list of indices to a list of pairs representing the start and end of contiguous runs of indices + # set pairs [list] + # set start [lindex $indices 0] + # set prev $start + # for {set i 1} {$i < [llength $indices]} {} { + # set idx [lindex $indices $i] + # if {$idx == $prev + 1} { + # #still in a run + # set prev $idx + # } else { + # #run ended - add pair to list + # lappend pairs [list $start $prev] + # set start $idx + # set prev $idx + # } + # } + # # add final run + # lappend pairs [list $start $prev] + # return $pairs + #} # showdict uses lindex_resolve results -Inf & Inf to determine whether index is out of bounds on lower vs upper side #This doesn't need the list itself - just the length suffices. punk::args::define { @@ -7835,6 +7959,77 @@ namespace eval punk::lib { } } + #sugar + #exclusive end - more intuitive for some cases and more consistent with other languages + namespace eval argdoc { + variable PUNKARGS + lappend PUNKARGS [list { + @id -id ::punk::lib::FOR + @cmd -name punk::lib::FOR\ + -summary\ + "BASIC style integer 'for loop'"\ + -help\ + "Sugar syntax for a common looping pattern. + The loop variable takes on values from start to end (exclusive) in increments of step. + If step is not specified, it defaults to 1 or -1 depending on the relative values of start and end. + This is a common looping pattern that isn't directly supported by Tcl's built in control structures, + and this syntax is more concise for convenient interactive usage. For example: + + FOR i 0 10 {puts $i} + + will print the numbers 0 to 9. + + This wrapper necessarily has some slight overhead compared to builtin Tcl for, foreach and while loops, + so may not be suitable for performance critical inner loops. + + See also: https://wiki.tcl-lang.org/page/Simple+shorthand+%27for%27+loop + " + @values -min 3 -max 4 + varname -type string -help "loop variable name" + start -type integer -help "initial value for loop variable" + end -type integer -help "end value for loop variable (exclusive)" + step -type integer -optional 1 -help "step value for loop variable (defaults to 1 or -1 depending on start and end values)" + script -type script -help "script to execute for each loop iteration" + }] + } + proc FOR { var args } { + switch -- [llength $args] { + 3 { + # FOR x start end {} + lassign $args start end script + set step [ expr {$start > $end ? - 1 : 1} ] + } + 4 { + # FOR x start end step {} + lassign $args start end step script + } + default { + error "FOR: wrong # args, should be: FOR varName startValue endValue ?stepValue? script" + } + } + if {![string is integer -strict $start] || ![string is integer -strict $end] || ![string is integer -strict $step]} { + error "FOR: start,end and step values must be integers" + } + upvar $var loopVar + set loopVar [expr {$start - $step}] + #to support 'continue' we have to increment the loopVar prior to the script evaluation, within the loop condition + if {$start < $end} { + if {$step <= 0} { + error "FOR: step value must be positive when start < end" + } + while {[incr loopVar $step] < $end} { + uplevel $script + } + } else { + if {$step >= 0} { + error "FOR: step value must be negative when start > end" + } + while {[incr loopVar $step] > $end} { + uplevel $script + } + } + } + #review - there are various type of uuid - we should use something consistent across platforms #twapi is used on windows because it's about 5 times faster - but is this more important than consistency? #twapi is much slower to load in the first place (e.g 75ms vs 6ms if package names already loaded) - so for oneshots tcllib uuid is better anyway diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/base-0.1.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/base-0.1.tm index eb1e1c5e..ba24f31d 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/base-0.1.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/base-0.1.tm @@ -677,7 +677,7 @@ namespace eval punk::mix::base { if {$opt_use_tar != 0} { set target [file tail $path] set tmplocation [punk::mix::util::tmpdir] - set archivename $tmplocation/[punk::mix::util::tmpfile].tar + set archivename $tmplocation/[punk::mix::util::tmpfile].tar ;#generates a unique filename - does not create the file. cd $base ;#cd is process-wide.. keep cd in effect for as small a scope as possible. (review for thread issues) @@ -687,14 +687,38 @@ namespace eval punk::mix::base { set tsstart [clock millis] if {[set tarpath [auto_execok tar]] ne ""} { #using an external binary is *significantly* faster than tar::create - but comes with some risks - #review - need to check behaviour/flag variances across platforms + set versioninfo [exec {*}$tarpath --version] + #look for "bsdtar" vs "GNU" + #GNU tar is more common on linux - but also available on windows via gnuutils or msys + #/tar.exe on windows is likely to be bsdtar. + #GNU tar on windows will commonly fail with 'cannot connect to C: resolve failed' - may need --force-local flag + + #review - need to further check behaviour/flag variances across platforms #don't use -z flag. On at least some tar versions the zipped file will contain a timestamped subfolder of filename.tar - which ruins the checksum #also - tar is generally faster without the compression (although this may vary depending on file size and disk speed?) - exec {*}$tarpath -cf $archivename $target ;#{*} needed in case spaces in tarpath - set tsend [clock millis] - set ms [expr {$tsend - $tsstart}] - puts stdout " tar -cf done ($ms ms)" - } else { + + if {[string match "*GNU*" $versioninfo]} { + set flags "--force-local" + } else { + #presumably bsdtar - which is more likely to be present on windows - and doesn't seem to have the same issue with drive letters in paths + set flags "" + } + if {[catch { + #{*}$tarpath needed in case spaces in tarpath + exec {*}$tarpath -cf {*}$flags $archivename $target + } errMsg]} { + set tsend [clock millis] + set ms [expr {$tsend - $tsstart}] + puts stdout " 'tar -cf $flags' ERROR ($ms ms) - falling back to tar::create\n error info: $errMsg" + } else { + set tsend [clock millis] + set ms [expr {$tsend - $tsstart}] + puts stdout " 'tar -cf $flags' done ($ms ms)" + } + } + + if {![file exists $archivename]} { + #fallback to tar library approach if external tar failed to create the archive. set tsstart [clock millis] ;#don't include auto_exec search time for tar::create tar::create $archivename $target set tsend [clock millis] @@ -703,6 +727,7 @@ namespace eval punk::mix::base { puts stdout " NOTE: install tar executable for potentially *much* faster directory checksum processing" } + if {$ftype eq "file"} { set sizeinfo "(size [punk::lib::format_number [file size $target]] bytes)" } else { diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm index 87449fb3..793736b8 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm @@ -333,11 +333,11 @@ namespace eval punk::mix::cli { set defaults [list {*}{ -errorprefix projectname }] - if {[llength $args] %2 != 0} {error "validate_modulename args must be name-value pairs: received '$args'"} + if {[llength $args] %2 != 0} {error "validate_projectname args must be name-value pairs: received '$args'"} set known_opts [dict keys $defaults] foreach k [dict keys $args] { if {$k ni $known_opts} { - error "validate_modulename error: unknown option $k. known options: $known_opts" + error "validate_projectname error: unknown option $k. known options: $known_opts" } } set opts [dict merge $defaults $args] @@ -381,34 +381,6 @@ namespace eval punk::mix::cli { return $name } - #split modulename (as present in a filename or namespaced name) into name/version ignoring leading namespace path - #ignore trailing .tm .TM if present - #if version doesn't pass validation - treat it as part of the modulename and return empty version string without error - #Up to caller to validate. - proc split_modulename_version {fullmodulename} { - set lastpart [namespace tail $fullmodulename] - set lastpart [file tail $lastpart] ;# should be ok to use file tail now that we've ensured no namespace components - if {[string equal -nocase [file extension $fullmodulename] ".tm"]} { - set fileparts [split [file rootname $lastpart] -] - } else { - set fileparts [split $lastpart -] - } - if {[punk::mix::util::is_valid_tm_version [lindex $fileparts end]]} { - set versionsegment [lindex $fileparts end] - set namesegment [join [lrange $fileparts 0 end-1] -];#re-stitch - } else { - # - set namesegment [join $fileparts -] - set versionsegment "" - } - set base [namespace qualifiers $fullmodulename] - if {$base ne ""} { - set modulename "${base}::$namesegment" - } else { - set modulename $namesegment - } - return [list $modulename $versionsegment] - } proc get_status {{workingdir ""} args} { set result "" diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm index 3b40d838..4c75b10e 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm @@ -9,7 +9,7 @@ # @@ Meta Begin # Application punk::mix::commandset::module 0.1.0 # Meta platform tcl -# Meta license BSD +# Meta license BSD # @@ Meta End @@ -204,7 +204,7 @@ namespace eval punk::mix::commandset::module { # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- set opt_version_supplied [dict get $opts -version] set opt_version $opt_version_supplied - if {![util::is_valid_tm_version $opt_version]} { + if {![punk::mix::util::is_valid_tm_version $opt_version]} { error "deck module.new error - supplied -version $opt_version doesn't appear to be a valid Tcl module version" } # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- @@ -213,8 +213,8 @@ namespace eval punk::mix::commandset::module { set mversion_supplied "" ;#version supplied directly in module argument if {[string first - $module]> 0} { #if it has a dash then version is required to be valid - lassign [punk::mix::cli::lib::split_modulename_version $module] modulename mversion - if {![util::is_valid_tm_version $mversion]} { + lassign [punk::mix::util::split_modulename_version $module] modulename mversion + if {![punk::mix::util::is_valid_tm_version $mversion]} { error "deck module.new error - unable to determine modulename-version from supplied value '$module'" } set mversion_supplied $mversion ;#record as may need to compare to version from templatefile name @@ -315,7 +315,7 @@ namespace eval punk::mix::commandset::module { } else { set module [string range [string length $vendor.] end] } - lassign [punk::mix::cli::lib::split_modulename_version $m] _tailmname mversion + lassign [punk::mix::util::split_modulename_version $m] _tailmname mversion lappend key_version_list [list $m $mversion] } if {[llength $matches]} { @@ -383,7 +383,7 @@ namespace eval punk::mix::commandset::module { } else { error "module.new error: Unable to interpret filename components of template file '$templatefile'" } - lassign [punk::mix::cli::lib::split_modulename_version $template_modulename_part] t_mname t_version + lassign [punk::mix::util::split_modulename_version $template_modulename_part] t_mname t_version #t_version may be empty string if template is unversioned e.g template_whatever.tm set fd [open $templatefile r]; set template_filedata [read $fd]; close $fd @@ -398,7 +398,7 @@ namespace eval punk::mix::commandset::module { } else { # - if {[util::is_valid_tm_version $t_version]} { + if {[punk::mix::util::is_valid_tm_version $t_version]} { if {$mversion_supplied eq ""} { set build_version $t_version } else { @@ -500,7 +500,7 @@ namespace eval punk::mix::commandset::module { set name_version_pairs [list] lappend name_version_pairs [list $moduletail $infile_version] foreach existing $existing_versions { - lassign [punk::mix::cli::lib::split_modulename_version $existing] namepart version ;# .tm is stripped and ignored + lassign [punk::mix::util::split_modulename_version $existing] namepart version ;# .tm is stripped and ignored if {[string match #modpod-* $namepart]} { set namepart [string range $namepart 8 end] } diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/util-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/util-0.1.0.tm index 9de4a33d..7f55005b 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/util-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/util-0.1.0.tm @@ -330,6 +330,35 @@ namespace eval punk::mix::util { return 0 } } + + #split modulename (as present in a filename or namespaced name) into name/version ignoring leading namespace path + #ignore trailing .tm .TM if present + #if version doesn't pass validation - treat it as part of the modulename and return empty version string without error + #Up to caller to validate. + proc split_modulename_version {fullmodulename} { + set lastpart [namespace tail $fullmodulename] + set lastpart [file tail $lastpart] ;# should be ok to use file tail now that we've ensured no namespace components + if {[string equal -nocase [file extension $fullmodulename] ".tm"]} { + set fileparts [split [file rootname $lastpart] -] + } else { + set fileparts [split $lastpart -] + } + if {[is_valid_tm_version [lindex $fileparts end]]} { + set versionsegment [lindex $fileparts end] + set namesegment [join [lrange $fileparts 0 end-1] -];#re-stitch + } else { + set namesegment [join $fileparts -] + set versionsegment "" + } + set base [namespace qualifiers $fullmodulename] + if {$base ne ""} { + set modulename "${base}::$namesegment" + } else { + set modulename $namesegment + } + return [list $modulename $versionsegment] + } + #Note that semver only has a small overlap with tcl tm versions. #todo - work out what overlap and whether it's even useful #see also TIP #439: Semantic Versioning (tcl 9??) @@ -337,6 +366,7 @@ namespace eval punk::mix::util { set re {^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$} } #todo - semver conversion/validation for other systems? + proc magic_tm_version {} { set magicbase 999999 ;#deliberately large so given load-preference when testing! #we split the literal to avoid the literal appearing here - reduce risk of accidentally converting to a release version diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm index e89997c5..db6acbb4 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm @@ -4186,7 +4186,7 @@ y" {return quirkykeyscript} #this *very odd* construct is to avoid using the namespace argument of apply. (handling of *weird/inadvisable* namespaces) #we use an uplevel from within the apply which runs in the global namespace, (but called via nseval from within targetns) and a result var for 'info default' in the current punk::ns namespace. nseval $targetns [list apply [list {procname argname} { - set has_default [uplevel 1 [list info default $procname $argname ::punk::ns::corp_defvar]] + set has_default [uplevel 1 [list ::info default $procname $argname ::punk::ns::corp_defvar]] if {$has_default} { set answer [dict create exists 1 default $::punk::ns::corp_defvar] } else { @@ -5025,7 +5025,7 @@ y" {return quirkykeyscript} set queryargs [lrange $args $i end] set resolvedargs [list] set queryargs_untested $queryargs - puts "punk::args::id_exists $docid queryargs_untested: $queryargs" + #puts "punk::ns::cmdtraverse punk::args::id_exists $docid queryargs_untested: $queryargs" } else { #we cannot generate autodoc for any deeper (e.g ensemble/proc after undocumented parent) #There is nothing to indicate the locations of subcommands - they could be anywhere. @@ -7191,6 +7191,7 @@ y" {return quirkykeyscript} nstest eval {package require punk::ns} set ns "" if {![catch {nstest eval [list punk::ns::pkguse $pkg_unqualified]} errMsg]} { + #review set script [string map [list %p% $pkg_unqualified] {dict get $::punk::ns::pkguse_package_to_namespace %p%}] set ns [nstest eval $script] } else { diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/packagepreference-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/packagepreference-0.1.0.tm index e45cefa7..87e33d8c 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/packagepreference-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/packagepreference-0.1.0.tm @@ -110,6 +110,19 @@ tcl::namespace::eval punk::packagepreference { #[para]This comes at some slight cost for packages that are only available with uppercase letters in the name - but at minimal cost for recommended lowercase package names #[para]Return to the standard ::package builtin by calling punk::packagepreference::uninstall + if {![catch {commandstack::get_stack} cstack]} { + if {[dict exists $cstack ::package]} { + set pstack [dict get $cstack ::package] + foreach record $pstack { + if {[dict get $record rename] eq "punk::packagepreference"} { + #already installed - silently ignore. + return 0 + } + } + } + } + + #todo - review/update commandstack package #modern module/lib names should preferably be lower case #see tip 590 - "Recommend lowercase Package names". Where non-lowercase are deprecated (but not removed even in Tcl9) @@ -170,7 +183,10 @@ tcl::namespace::eval punk::packagepreference { if {[llength $pkgloadedinfo]} { if {[llength $available_versions] > 1} { - puts stderr "--> pkg $pkg not already 'provided' but shared object seems to be loaded: $pkgloadedinfo - and [llength $available_versions] versions available" + if {[catch {thread::id} threadid]} { + set threadid "unknown-threadid" + } + #puts stderr "--> pkg $pkg not already 'provided' but shared object seems to be loaded: $pkgloadedinfo - and [llength $available_versions] versions available. $available_versions threadid: $threadid" } lassign $pkgloadedinfo loaded_path name set lc_loadedpath [string tolower $loaded_path] @@ -302,7 +318,10 @@ tcl::namespace::eval punk::packagepreference { puts stderr "Failed to load punk::args::moduledoc::$dp - error was: $errMsg" } } else { - puts stdout "Loaded punk::args::moduledoc::$dp for package $pkg threadid: [thread::id]" + if {[catch {thread::id} threadid]} { + set threadid "unknown-threadid" + } + puts stdout "punk::packagepreference overloaded 'package require': Loaded punk::args::moduledoc::$dp for package $pkg threadid: $threadid" } } #--------------------------------------------------------------- diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm index a7aff5a6..91f7a31a 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm @@ -432,7 +432,7 @@ proc repl::start {inchan args} { variable codethread #review if {$codethread eq ""} { - error "start - no codethread. call init first. (options -safe 0|1)" + error "start - no codethread. call init first. (options -type 0|1)" } variable commandstr @@ -478,7 +478,7 @@ proc repl::start {inchan args} { #set ::punk::repl::codethread::running 1 #the interp in which commands such as d/ run - #we need to namespace eval for the -safe interp which may not have the packages loaded (or be able to) but still needs default values + #we need to namespace eval for the -type interp which may not have the packages loaded (or be able to) but still needs default values #punk::repl::codethread::running is required whether safe or not. interp eval code { namespace eval ::punk::repl::codethread {} @@ -690,7 +690,7 @@ proc repl::reopen_stdinX {} { } -#add to sliding buffer of last x chars emmitted to screen by repl +# add to sliding buffer of last x chars emmitted to screen by repl #(we could maintain only one char - more kept merely for debug assistance) #will not detect emissions from exec with stdout redirected and presumably some extensions etc proc repl::screen_last_char_add {c what {why ""}} { @@ -2123,8 +2123,10 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config #if {$chunk eq "\x1b\[C"} { #} + #---------------------------------------------------------------------------------------------------------------------------------------------------------- punk::console::cursor_off flush stdout + #---------------------------------------------------------------------------------------------------------------------------------------------------------- $editbuf add_chunk $chunk @@ -2235,8 +2237,10 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config lappend input_chunks_waiting($inputchan) $waiting } } + #---------------------------------------------------------------------------------------------------------------------------------------------------------- punk::console::cursor_on flush stdout + #---------------------------------------------------------------------------------------------------------------------------------------------------------- if {$editbuf_linenum_submitted == 0} { #(there is no line 0 - lines start at 1) @@ -2264,9 +2268,9 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config rputs stderr "trap1 POSIX '$e' eopts:'$eopts" flush stderr } on error {repl_error erropts} { - rputs stderr "error1 in repl_handler: $repl_error" + rputs stderr "error1 in repl_process_data: $repl_error" rputs stderr "-------------" - rputs stderr "$::errorInfo" + rputs stderr "erroropts: $erropts" rputs stderr "-------------" set stdinreader [chan event $inputchan readable] if {![string length $stdinreader]} { @@ -2485,7 +2489,13 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config variable codethread_cond variable codethread_mutex - lappend errstack [shellfilter::stack::add stderr tee_to_var -settings {-varname ::repl::output_stderr}] + #------------------------------- + #JJJJ JMN test + #REVIEW + #lappend errstack [shellfilter::stack::add stderr tee_to_var -settings {-varname ::repl::output_stderr}] + #------------------------------- + + #thread::transfer $codethread stderr #chan configure stdout -buffering none @@ -2939,9 +2949,9 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config rputs stderr "trap POSIX '$e' eopts:'$eopts" flush stderr } on error {repl_error erropts} { - rputs stderr "error in repl_handler: $repl_error" + rputs stderr "error2 in repl_process_data: $repl_error" rputs stderr "-------------" - rputs stderr "$::errorInfo" + rputs stderr "$erropts" rputs stderr "-------------" set stdinreader [chan event $inputchan readable] if {![string length $stdinreader]} { @@ -2986,10 +2996,10 @@ namespace eval repl { variable codethread_cond variable codethread_mutex - set opts [list -force 0 -safe 0 -safelog 0 -paths {} -callback_interp $default_callback_interp] + set opts [list -force 0 -type 0 -safelog 0 -paths {} -callback_interp $default_callback_interp] foreach {k v} $args { switch -- $k { - -force - -safe - -safelog - -paths - -callback_interp { + -force - -type - -safelog - -paths - -callback_interp { dict set opts $k $v } default { @@ -2998,7 +3008,7 @@ namespace eval repl { } } set opt_force [dict get $opts -force] - set opt_safe [dict get $opts -safe] + set opt_repltype [dict get $opts -type] set opt_safelog [dict get $opts -safelog] if {$opt_safelog eq "0"} { set opt_safelog "" @@ -3113,46 +3123,50 @@ namespace eval repl { package require punk::args #package require Thread - if {[catch {package require thread} errM]} { - puts stdout ">>repl::init initscript lib load fail on package require thread\n$errM" + #if {[catch {package require thread} errM]} { + # puts stdout ">>repl::init initscript lib load fail on package require thread\n$errM" if {[catch {package require Thread} errM2]} { puts stdout ">>repl::init initscript lib load fail on package require Thread\n$errM2" puts stdout ">>repl::init auto_path : $::auto_path" puts stdout ">>repl::init tcl::tm::list: [tcl::tm::list]" } - } + #} + namespace eval ::punk::repl::codethread {} + + # ------------------------------------------------------------------------------------------------------------------------------------------------------- #----- #review - icomm as a possible way to talk to thread outside of the code interp. #thread::send msgs arrive at a specific interp based on initial setup - review for when/whether androwish thread enhancements are made to allow #thread::send to caller defined interp targets (reference?) #snit required for icomm - if {[catch {package require snit} errM]} { - #puts stdout "punk::repl::initscript: lib load fail ---snit $errM" - } - if {[catch {package require punk::icomm} errM]} { - #puts stdout "punk::repl::initscript: lib load fail ---icomm $errM" - } + #if {[catch {package require snit} errM]} { + # #puts stdout "punk::repl::initscript: lib load fail ---snit $errM" + #} + #if {[catch {package require punk::icomm} errM]} { + # #puts stdout "punk::repl::initscript: lib load fail ---icomm $errM" + #} #----- - - namespace eval ::punk::repl::codethread {} - #todo - review. According to fifo2 docs Memchan involves one less thread (may offer better performance/resource use) - catch {package require tcl::chan::fifo2} - if {[catch { - #first use can raise error being a version number e.g 0.1.0 - why? - lassign [tcl::chan::fifo2] ::punk::repl::codethread::repltalk replside - } errMsg]} { - puts stdout "punk::repl::initscript tcl::chan::fifo2 error: $errMsg" - } else { - #experimental? - #puts stdout "transferring chan $replside to thread %replthread%" - #flush stdout - #if {[catch { - # #after 0 [list thread::transfer %replthread% $replside] - #} errMsg]} { - # #puts stdout "---thread::transfer error: $errMsg" - #} - } + #---------------------------------------------------------------------------------------------------------------------- + # todo - review. According to fifo2 docs Memchan involves one less thread (may offer better performance/resource use) + # catch {package require tcl::chan::fifo2} + # if {[catch { + # #first use can raise error being a version number e.g 0.1.0 - why? + # lassign [tcl::chan::fifo2] ::punk::repl::codethread::repltalk replside + # } errMsg]} { + # puts stdout "punk::repl::initscript tcl::chan::fifo2 error: $errMsg" + # } else { + # #experimental? + # #puts stdout "transferring chan $replside to thread %replthread%" + # #flush stdout + # # if {[catch { + # # #after 0 [list thread::transfer %replthread% $replside] + # # } errMsg]} { + # # #puts stdout "---thread::transfer error: $errMsg" + # # } + # } + #---------------------------------------------------------------------------------------------------------------------- + # ------------------------------------------------------------------------------------------------------------------------------------------------------- package require punk::console package require punk::repl::codethread @@ -3333,7 +3347,7 @@ namespace eval repl { set ts_start [clock seconds] set replresult [interp eval code { package require punk::repl - repl::init -safe punk + repl::init -type punk repl::start stdin }] return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]] @@ -3343,7 +3357,7 @@ namespace eval repl { interp eval code { package require punk::repl } - interp eval code [list repl::init -safe safe {*}$args] + interp eval code [list repl::init -type safe {*}$args] set replresult [interp eval code [list repl::start stdin]] return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]] @@ -3353,7 +3367,7 @@ namespace eval repl { interp eval code { package require punk::repl } - set codethread [interp eval code [list repl::init -safe safebase {*}$args]] + set codethread [interp eval code [list repl::init -type safebase {*}$args]] puts stdout "safebase codethread:$codethread" set replresult [interp eval code [list repl::start stdin]] @@ -3364,7 +3378,7 @@ namespace eval repl { interp eval code { package require punk::repl } - interp eval code [list repl::init -safe punksafe {*}$args] + interp eval code [list repl::init -type punksafe {*}$args] set replresult [interp eval code [list repl::start stdin]] return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]] @@ -3376,14 +3390,14 @@ namespace eval repl { #flush stdout set args %args% - set safe [dict get $args -safe] + set repltype [dict get $args -type] set safelog [dict get $args -safelog] set paths [list] if {[dict exists $args -paths]} { set paths [dict get $args -paths] } - switch -- $safe { + switch -- $repltype { safe { interp create -safe -- code code eval [list namespace eval ::punk::libunknown {}] @@ -3444,7 +3458,7 @@ namespace eval repl { #todo a specific punk::libunknown 'package unknown' handler for safe interps #pull in code via calls to source cached code? - switch -- $safe { + switch -- $repltype { safe { if {[llength $paths]} { package require punk::island @@ -3797,8 +3811,27 @@ namespace eval repl { } punk - 0 { + #------------------------------------------------------------------------ + #Test - experimental 2026 + if {"stdout" in [chan names]} { + interp share {} stdout code + } else { + interp share {} [shellfilter::stack::item_tophandle stdout] code + } + if {"stderr" in [chan names]} { + interp share {} stderr code + } else { + interp share {} [shellfilter::stack::item_tophandle stderr] code + } + foreach nm {shellspyout shellspyerr} { + set thandle [shellfilter::stack::item_tophandle $nm] + if {$thandle ne ""} { + interp share {} $thandle code + } + } + #------------------------------------------------------------------------ interp eval code { - #safe !=1 and safe !=2, tmlist: %tmlist% + #repltype !=1 and repltype !=2, tmlist: %tmlist% set ::argv0 %argv0% set ::argv %argv% set ::argc %argc% @@ -3978,8 +4011,8 @@ namespace eval repl { thread::id } set init_script [string map $scriptmap $init_script] - #REVIEW - the same initscript sent for all values of $safe and it switches on values of $safe provided in %args% - #we already know $safe in this thread when generating the script - so why send the large script to the thread to then switch on that? + #REVIEW - the same initscript sent for all values of $repltype and it switches on values of $repltype provided in %args% + #we already know $repltype in this thread when generating the script - so why send the large script to the thread to then switch on that? #thread::send $codethread $init_script if {![catch { @@ -3994,7 +4027,7 @@ namespace eval repl { error $errMsg } } - #init - don't auto init - require init with possible options e.g -safe + #init - don't auto init - require init with possible options e.g -type } package provide punk::repl [namespace eval punk::repl { variable version diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repo-0.1.1.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repo-0.1.1.tm index 482fbbeb..dd446ae8 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repo-0.1.1.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repo-0.1.1.tm @@ -83,34 +83,38 @@ namespace eval punk::repo { proc get_fossil_usage {} { set allcmds [runout -n fossil help -a] + set allcmds [punk::ansi::ansistrip $allcmds] set mainhelp [runout -n fossil help] + set mainhelp [punk::ansi::ansistrip $mainhelp] set maincommands [list] #only start parsing for TOPICS after a line such as "Other comman values for TOPIC:" set parsing_topics 0 foreach ln [split $mainhelp \n] { set ln [string trim $ln] - if {$ln eq ""} { - continue - } - if {[string match "*values for TOPIC*" $ln]} { - set parsing_topics 1 + if {$ln eq ""} { + continue + } + if {[string match "*values for TOPIC*" $ln]} { + set parsing_topics 1 + continue + } + if {$parsing_topics} { + #lines starting with uppercase are topic headers - we want to ignore these and any blank lines + if {[regexp {^[A-Z]+} $ln]} { continue } - if {$parsing_topics} { - #lines starting with uppercase are topic headers - we want to ignore these and any blank lines - if {[regexp {^[A-Z]+} $ln]} { - continue - } - lappend maincommands {*}$ln - } + lappend maincommands {*}$ln + } } #fossil output was ordered in columns, but we loaded list in row-wise, messing up the order set maincommands [lsort $maincommands] set allcmds [lsort $allcmds] set othercmds [punk::lib::ldiff $allcmds $maincommands] - set fossil_setting_names [lsort [runout -n fossil help -s]] + set setting_info [runout -n fossil help -s] + set setting_info [punk::ansi::ansistrip $setting_info] + set fossil_setting_names [lsort $setting_info] set result "@leaders -min 0\n" @@ -186,6 +190,8 @@ namespace eval punk::repo { foreach ln $basic_opt_lines { set ln [string trim $ln] + #fossil sometimes emits cursor control sequences e.g CSI 3 q + set ln [punk::ansi::ansistrip $ln] if {$ln eq ""} { continue } @@ -250,6 +256,7 @@ namespace eval punk::repo { ${[punk::repo::get_fossil_subcommand_usage add]} @form -form "raw" -synopsis "exec fossil add \[OPTIONS\] FILE1 \[FILE2\]..." + #fossil help may have ansi - review @formdisplay -header "fossil help add" -body {${[runout -n fossil help add]}} } ""] @@ -264,6 +271,7 @@ namespace eval punk::repo { ${[punk::repo::get_fossil_subcommand_usage diff]} @form -form "raw" -synopsis "exec fossil diff \[OPTIONS\] FILE1 \[FILE2\]..." + #fossil help may have ansi - review @formdisplay -header "fossil help diff" -body {${[runout -n fossil help diff]}} } ""] diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm index eb439c6e..86823a45 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm @@ -121,14 +121,6 @@ namespace eval punkcheck { } method as_record {} { - #set fields [list\ - # -targets $o_targets\ - # -keep_installrecords $o_keep_installrecords\ - # -keep_skipped $o_keep_skipped\ - # -keep_inprogress $o_keep_inprogress\ - # body $o_records\ - #] - dict create {*}{ } tag FILEINFO {*}{ } -targets $o_targets {*}{ @@ -216,18 +208,6 @@ namespace eval punkcheck { } else { set tsiso_end "" } - #set fields [list\ - # -tsiso_begin $tsiso_begin\ - # -ts_begin $o_ts_begin\ - # -tsiso_end $tsiso_end\ - # -ts_end $o_ts_end\ - # -id $o_id\ - # -source $o_rel_sourceroot\ - # -targets $o_rel_targetroot\ - # -types $o_types\ - # -config $o_configdict\ - #] - #set record [dict create tag EVENT {*}$fields] dict create {*}{ } tag EVENT {*}{ diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm index a0fa060a..6a948593 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm @@ -67,6 +67,8 @@ tcl::namespace::eval shellfilter::log { } proc ::shellfilter::log::close {tag} { #shellthread::manager::close_worker $tag + + #unsubscribe involves a thread::send to the worker thread if we are the last subscriber to the tag. shellthread::manager::unsubscribe [list $tag]; #workertid will be added back to free list if no tags remain subscribed } @@ -106,6 +108,13 @@ namespace eval shellfilter::pipe { package require shellthread #we are only using the fifo in a single direction to pipe to another thread # - so whilst wchan and rchan could theoretically each be both read & write we're only using them for one operation each + #---------------------------------------------------------------------------------------- + # Differences beetween Memchan's fifo2 and tcl::chan::fifo2 implementations: + # (not necessarily a comprehensive list) + # - Memchan's fifo2 is reportedly faster and more efficient than tcl::chan::fifo2, but it may not be available on all platforms + # tcl::chan::fifo2 is a pure Tcl implementation. + # - Closing one side of a tcl::chan::fifo2 (ver 1.1) will cause the other side to close whereas this is not the case with Memchan's fifo2. + #---------------------------------------------------------------------------------------- if {![catch {package require Memchan}]} { lassign [fifo2] wchan rchan } else { @@ -289,7 +298,8 @@ namespace eval shellfilter::chan { set o_enc [tcl::dict::get $tf -encoding] set o_encbuf "" set settingsdict [tcl::dict::get $tf -settings] - set varname [tcl::dict::get $settingsdict -varname] + set varname [tcl::dict::get $settingsdict -varname] ;#review - should we support multiple vars here? e.g with a list of varnames in settings and append to all of them? + #these are not upvared - must be fully qualified variable names. set o_datavars $varname if {[tcl::dict::exists $tf -junction]} { set o_is_junction [tcl::dict::get $tf -junction] @@ -298,18 +308,18 @@ namespace eval shellfilter::chan { } } method initialize {ch mode} { - return [list initialize finalize write flush clear] + if {"read" in $mode} { + #this should raise an error in 'chan push' + error "shellfilter::chan::tee_to_var transform does not support read mode" + } + return [list initialize finalize write flush] } method finalize {ch} { my destroy } - method clear {ch} { - return - } - method watch {ch events} { - # must be present but we ignore it because we do not - # post any events - } + #method clear {ch} { + # return + #} #method read {ch count} { # return ? #} @@ -320,9 +330,14 @@ namespace eval shellfilter::chan { #puts stdout "" #review - just clear o_encbuf and emit nothing? #we wouldn't have a value there if it was convertable from the channel encoding? - set clear $o_encbuf + if {[string length $o_encbuf]} { + #if we have data in the buffer that we haven't been able to convert to a string + #- then we probably have some kind of encoding mismatch. Is it safer to discard it than to emit garbage chars to the channel or var? + #REVIEW - log that we are discarding the buffer contents on flush? + puts stderr "WARNING: flush called on tee_to_var with non-empty buffer. This probably indicates an encoding mismatch between the channel encoding and the encoding expected by the transform. Discarding buffer contents: '$o_encbuf'" + } set o_encbuf "" - return $o_encbuf + return "" } method write {ch bytes} { #test with set x [string repeat " \U1f6c8" 2043] @@ -387,10 +402,12 @@ namespace eval shellfilter::chan { } } method initialize {transform_handle mode} { - return [list initialize read drain write flush clear finalize] + #return [list initialize read drain write flush clear finalize] + return [list initialize write flush clear finalize] } method finalize {transform_handle} { - ::shellfilter::log::close $o_logsource + #Note that an error in the finalize can stop 'chan pop' from running properly. + #::shellfilter::log::close $o_logsource my destroy } method watch {transform_handle events} { @@ -398,26 +415,53 @@ namespace eval shellfilter::chan { # post any events } method clear {transform_handle} { + set o_encbuf "" return } - method drain {transform_handle} { - return "" - } - method read {transform_handle bytes} { - set logdata [tcl::encoding::convertfrom $o_enc $bytes] - #::shellfilter::log::write $o_logsource $logdata - puts -nonewline $o_localchan $logdata - return $bytes - } + #method drain {transform_handle} { + # return "" + #} + #method read {transform_handle bytes} { + # set logdata [tcl::encoding::convertfrom $o_enc $bytes] + # #::shellfilter::log::write $o_logsource $logdata + # puts -nonewline $o_localchan $logdata + # return $bytes + #} + #method flush {transform_handle} { + # #return "" + # set clear $o_encbuf[set o_encbuf ""] + # if {[catch {tcl::encoding::convertfrom $o_enc $clear} stringdata]} { + # #if we can't convert the buffer contents to a string - put it back and try again with more data later + # #REVIEW? + # set o_encbuf $clear + # puts -nonewline $o_localchan "" + # return "" + # } + # #jjj + # puts -nonewline $o_localchan $stringdata + # flush $o_localchan + # return $clear + #} method flush {transform_handle} { - #return "" - set clear $o_encbuf + #we wouldn't have a value in o_encbuf if it was convertable from the channel encoding? + if {[string length $o_encbuf]} { + #if we have data in the buffer that we haven't been able to convert to a string + #- then we probably have some kind of encoding mismatch. Is it safer to discard it than to emit garbage chars to the channel or var? + #REVIEW - log that we are discarding the buffer contents on flush? + puts stderr "WARNING: flush called on tee_to_pipe with non-empty buffer. This probably indicates an encoding mismatch between the channel encoding and the encoding expected by the transform. Discarding buffer contents: '$o_encbuf'" + } set o_encbuf "" - return $o_encbuf + return "" } method write {transform_handle bytes} { #set logdata [tcl::encoding::convertfrom $o_enc $bytes] set inputbytes $o_encbuf$bytes + if {$inputbytes eq ""} { + #review - do we even get empty writes? + puts stderr "WARNING: write called on tee_to_pipe with empty inputbytes. This may be a no-op, but it may also indicate an issue with the upstream transform or channel. Emitting no data to the pipe for this write." + return "" + } + set o_encbuf "" set tail_offset 0 while {$tail_offset < [::tcl::string::length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [::tcl::string::range $inputbytes 0 end-$tail_offset]} stringdata]} { @@ -426,18 +470,23 @@ namespace eval shellfilter::chan { if {$tail_offset > 0} { if {$tail_offset < [::tcl::string::length $inputbytes]} { #stringdata from catch statement must be a valid result + set converted [::tcl::string::range $inputbytes 0 end-$tail_offset] set t [expr {$tail_offset - 1}] set o_encbuf [::tcl::string::range $inputbytes end-$t end] } else { + #nothing convertable in the buffer - put it back and try again with more data later set stringdata "" set o_encbuf $inputbytes return "" } + } else { + #no catch on conversion - everything is convertable + #stringdata must be valid result from convertfrom of whole $o_encbuf$bytes + set converted $inputbytes } #::shellfilter::log::write $o_logsource $logdata puts -nonewline $o_localchan $stringdata - #return $bytes - return [::tcl::string::range $inputbytes 0 end-$tail_offset] + return $converted ;#same data as $stringdata but in bytes form. } #a tee is not a redirection - because data still flows along the main path method meta_is_redirection {} { @@ -501,6 +550,7 @@ namespace eval shellfilter::chan { if {$tail_offset > 0} { if {$tail_offset < [::tcl::string::length $inputbytes]} { #stringdata from catch statement must be a valid result + set converted [::tcl::string::range $inputbytes 0 end-$tail_offset] set t [expr {$tail_offset - 1}] set o_encbuf [::tcl::string::range $inputbytes end-$t end] } else { @@ -508,10 +558,15 @@ namespace eval shellfilter::chan { set o_encbuf $inputbytes return "" } + } else { + #no catch on conversion - everything is convertable + #stringdata must be valid result from convertfrom of whole $o_encbuf$bytes + set converted $inputbytes } - set bytes [::tcl::string::range $inputbytes 0 end-$tail_offset] + #set bytes [::tcl::string::range $inputbytes 0 end-$tail_offset] ::shellfilter::log::write $o_logsource $stringdata - return $bytes + #return $bytes + return $converted ;#same data as $stringdata but in bytes form. } method meta_is_redirection {} { return $o_is_junction @@ -519,6 +574,10 @@ namespace eval shellfilter::chan { } + #see TIP 230 Tcl Channel Transformation Reflection API + + #the logonly transform does not emit any data downwards towards the base channel - it only writes to the log. + # - ie we are redirecting all data to the log. oo::class create logonly { variable o_tid variable o_logsource @@ -537,19 +596,35 @@ namespace eval shellfilter::chan { set o_tid [::shellfilter::log::open $o_logsource $settingsdict] } method initialize {transform_handle mode} { - return [list initialize finalize write] + #mode is a list containing any of the strings 'read' or 'write'. + if {"read" in $mode} { + #Note that raising an error prevents creation of the transformation. + #The thrown error will appear as a error thrown by 'chan push'. + error "logonly transform does not support read mode" + } + #return all methods supported by the handler + #(we exclude our additional meta_is_redirection method becuase it is not a standard transform method called by the Tcl framework - it's only for our own use in shellfilter) + return [list initialize finalize write flush] } method finalize {transform_handle} { - ::shellfilter::log::close $o_logsource + #review - tip 230 states that any return value or error raised by finalize is ignored + #we wrap in catch to ensure the 'my destroy' is always called. + catch {::shellfilter::log::close $o_logsource} my destroy } - method watch {transform_handle events} { - # must be present but we ignore it because we do not - # post any events + #clear? + method flush {transform_handle} { + #we wouldn't have a value in o_encbuf if it was convertable from the channel encoding? + if {[string length $o_encbuf]} { + #if we have data in the buffer that we haven't been able to convert to a string + #- then we probably have some kind of encoding mismatch. Is it safer to discard it than to emit garbage chars to the log? + #REVIEW. - we are writing the raw bytes to the log here because we can't convert them to a string. + #This may be useful for debugging issues, but it may also result in garbage data in the log. + ::shellfilter::log::write $o_logsource $o_encbuf + set o_encbuf "" + } + return } - #method read {transform_handle count} { - # return ? - #} method write {transform_handle bytes} { #set logdata [encoding convertfrom $o_enc $bytes] set inputbytes $o_encbuf$bytes @@ -600,26 +675,49 @@ namespace eval shellfilter::chan { } } method initialize {transform_handle mode} { - return [list initialize read write clear flush drain finalize] + #return [list initialize read write clear flush drain finalize] + + #REVIEW - we aren't using 'read' mode - but if we raise an error if 'read' is in the mode then the system doesn't work. + #this is probably because we add it to one end of a fifo2 channel, which although we only use for writing is a bidirectional channel. + #---------------------- + #don't do this + #---------------------- + #if {"read" in $mode} { + # #Note that raising an error prevents creation of the transformation. + # #The thrown error will appear as a error thrown by 'chan push'. + # error "shellfilter::chan::ansistrip transform does not support read mode" + #} + #---------------------- + return [list initialize read write flush finalize] } method finalize {transform_handle} { my destroy } - method clear {transform_handle} { - return - } - method watch {transform_handle events} { - } - method drain {transform_handle} { - return "" - } + #method clear {transform_handle} { + # return + #} + #method drain {transform_handle} { + # return "" + #} method read {transform_handle bytes} { set instring [encoding convertfrom $o_enc $bytes] set outstring [punk::ansi::ansistrip $instring] return [encoding convertto $o_enc $outstring] } + #method flush {transform_handle} { + # return "" + #} method flush {transform_handle} { - return "" + #return "" + set clear $o_encbuf[set o_encbuf ""] + if {[catch {tcl::encoding::convertfrom $o_enc $clear} stringdata]} { + #if we can't convert the buffer contents to a string - put it back and try again with more data later + #REVIEW? + set o_encbuf $clear + return "" + } + #review + return $stringdata } #method write {transform_handle bytes} { # #broken due to occasional unexpected byte sequence @@ -989,29 +1087,44 @@ namespace eval shellfilter::chan { } method initialize {transform_handle mode} { #clear undesirable in terminal output channels (review) - return [list initialize write flush read drain finalize] + #return [list initialize write flush read drain finalize] + if {$mode eq "read"} { + error "shellfilter::chan::ansiwrap channel transform does not support read mode" + } + return [list initialize write flush finalize] } method finalize {transform_handle} { my destroy } - method watch {transform_handle events} { - } method clear {transform_handle} { #In the context of stderr/stdout - we probably don't want clear to run. #Terminals might call it in the middle of a split ansi code - resulting in broken output. - #Leave clear of it the init call + #Leave clear out of the initialize call for now puts stdout "" set emit [tcl::encoding::convertto $o_enc $o_buffered] set o_buffered "" return $emit } + #method flush {transform_handle} { + # #puts stdout "" + # set inputbytes $o_buffered$o_encbuf + # set emit [tcl::encoding::convertto $o_enc $inputbytes] + # set o_buffered "" + # set o_encbuf "" + # return $emit + #} method flush {transform_handle} { - #puts stdout "" - set inputbytes $o_buffered$o_encbuf - set emit [tcl::encoding::convertto $o_enc $inputbytes] + #return "" + set clear $o_buffered$o_encbuf + if {[catch {tcl::encoding::convertfrom $o_enc $clear} stringdata]} { + #if we can't convert the buffer contents to a string - does it make sense to emit the raw bytes? + # - probably not. + #REVIEW? + return "" + } set o_buffered "" set o_encbuf "" - return $emit + return $stringdata } method write {transform_handle bytes} { #set instring [tcl::encoding::convertfrom $o_enc $bytes] ;naive approach will break due to unexpected byte sequence - occasionally @@ -1076,14 +1189,14 @@ namespace eval shellfilter::chan { #set outstring ">>>$instring" return [tcl::encoding::convertto $o_enc $outstring] } - method drain {transform_handle} { - return "" - } - method read {transform_handle bytes} { - set instring [tcl::encoding::convertfrom $o_enc $bytes] - set outstring "$o_do_colour$instring$o_do_normal" - return [tcl::encoding::convertto $o_enc $outstring] - } + #method drain {transform_handle} { + # return "" + #} + #method read {transform_handle bytes} { + # set instring [tcl::encoding::convertfrom $o_enc $bytes] + # set outstring "$o_do_colour$instring$o_do_normal" + # return [tcl::encoding::convertto $o_enc $outstring] + #} method meta_is_redirection {} { return $o_is_junction } @@ -1326,9 +1439,11 @@ namespace eval shellfilter::stack { } proc status {{pipename *} args} { variable pipelines + package require textblock set pipecount [dict size $pipelines] set tabletitle "$pipecount pipelines active" set t [textblock::class::table new $tabletitle] + $t configure -frametype ascii; #be conservative here - may need to emit in various debugging contexts. $t add_column -headers [list channel-ident] $t add_column -headers [list device-info localchan] $t configure_column 1 -header_colspans {3} @@ -1337,6 +1452,9 @@ namespace eval shellfilter::stack { $t add_column -headers [list stack-info] foreach k [dict keys $pipelines $pipename] { set lc [dict get $pipelines $k device localchan] + if {[catch {chan configure $lc -encoding} lc_enc]} { + set lc_enc "" + } set rc [dict get $pipelines $k device remotechan] if {[dict exists $k device workertid]} { set tid [dict get $pipelines $k device workertid] @@ -1348,18 +1466,35 @@ namespace eval shellfilter::stack { set stackinfo "" } else { set tbl_inner [textblock::class::table new] + $tbl_inner configure -frametype ascii $tbl_inner configure -show_edge 0 + $tbl_inner add_column -headers id + $tbl_inner add_column -headers transform + $tbl_inner add_column -headers handle + $tbl_inner add_column -headers settings + $tbl_inner add_column -headers aside foreach rec $stack { - set handle [punk::lib::dict_getdef $rec -handle ""] set id [punk::lib::dict_getdef $rec -id ""] set transform [namespace tail [punk::lib::dict_getdef $rec -transform ""]] + set handle [punk::lib::dict_getdef $rec -handle ""] + if {$handle ne ""} { + if {[catch {chan configure $handle -encoding} handle_enc]} { + set handle_enc "" + } + } else { + set handle_enc "" + } set settings [punk::lib::dict_getdef $rec -settings ""] - $tbl_inner add_row [list $id $transform $handle $settings] + set aside [punk::lib::dict_getdef $rec -aside ""] + if {$aside ne ""} { + set aside [punk::lib::showdict $aside] + } + $tbl_inner add_row [list $id $transform $handle\n$handle_enc $settings $aside] } set stackinfo [$tbl_inner print] $tbl_inner destroy } - $t add_row [list $k $lc $rc $tid $stackinfo] + $t add_row [list $k "$lc\n$lc_enc" $rc $tid $stackinfo] } set result [$t print] $t destroy @@ -1511,15 +1646,24 @@ namespace eval shellfilter::stack { proc unwind {pipename} { variable pipelines - set stack [dict get $pipelines $pipename stack] - set localchan [dict get $pipelines $pipename device localchan] + set stack [dict get $pipelines $pipename stack] + set localchan [dict get $pipelines $pipename device localchan] foreach tf [lreverse $stack] { - chan pop $localchan + if {[catch {chan eof $localchan} _eof]} { + #We don't actually care about eof state - but we use this to test if the channel exists. + #('chan names' doesn't reliably show all channels in some cases) + #do nothing. + } else { + #if there are no transforms on the the channel - this is equivalent to 'chan close' of the channel + # but here we should only be calling it when there are transforms on the channel as indicated by the stack variable. + chan pop $localchan + } } dict set pipelines $pipename [list] } #todo proc delete {pipename {wait 0}} { + #::shellfilter::log::open shellfilter-delete [list -syslog "127.0.0.1:514"] variable pipelines set pipeinfo [dict get $pipelines $pipename] set deviceinfo [dict get $pipeinfo device] @@ -1534,18 +1678,29 @@ namespace eval shellfilter::stack { thread::release $tid } - #Memchan closes without error - tcl::chan::fifo2 raises something like 'can not find channel named "rc977"' - REVIEW. why? + #Memchan closes without error - tcl::chan::fifo2 raises something like 'can not find channel named "rc977"' + #- REVIEW. why? It could have something to do with the fact that tcl::memchan::fifo2 closes both sides when one side is closed. catch {chan close $localchan} + #if {[catch {chan close $localchan} errMsg]} { + # ::shellfilter::log::write shellfilter-delete "WARNING: error closing localchan '$localchan' for pipename '$pipename': $errMsg" + #} } #review - proc name clarity is questionable. remove_stackitem? proc remove {pipename remove_id} { + #::shellfilter::log::open shellfilter-remove [list -syslog "127.0.0.1:514"] variable pipelines if {![dict exists $pipelines $pipename]} { - puts stderr "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]" + #puts stderr "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]" + ::shellfilter::log::write shellfilter-remove "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]" return } set stack [dict get $pipelines $pipename stack] set localchan [dict get $pipelines $pipename device localchan] + set previous_blockingstate [chan configure $localchan -blocking] + if {$previous_blockingstate} { + chan configure $localchan -blocking 0 + } + set posn 0 set idposn -1 set asideposn -1 @@ -1572,10 +1727,11 @@ namespace eval shellfilter::stack { dict set container -aside {} lset stack $asideposn $container dict set pipelines $pipename stack $stack + #::shellfilter::log::write shellfilter-remove "cleared '-aside' record for pipename $pipename aside_posn $asideposn remove_id:'$remove_id'" } else { if {$idposn < 0} { - ::shellfilter::log::write shellfilter "ERROR shellfilter::stack::remove $pipename id '$remove_id' not found" - puts stderr "|WARNING>shellfilter::stack::remove $pipename id '$remove_id' not found" + #::shellfilter::log::write shellfilter-remove "ERROR shellfilter::stack::remove $pipename id '$remove_id' not found" + #puts stderr "|WARNING>shellfilter::stack::remove $pipename id '$remove_id' not found" return 0 } set removed_item [lindex $stack $idposn] @@ -1589,32 +1745,58 @@ namespace eval shellfilter::stack { #pop all chans before adding anything back in! foreach p $poplist { + #review + #update idletasks + #puts stderr "DEBUG> popping transform from pipename $pipename for stack p:$p poplist len:[llength $poplist]" + #::shellfilter::log::write shellfilter-remove "popping transform from pipename $pipename for stack p:$p poplist len:[llength $poplist] ---" + #after 0 [list chan pop $localchan] + chan pop $localchan + #::shellfilter::log::write shellfilter-remove "POPPED" + #update idletasks } + #after 5 + #::shellfilter::log::write shellfilter-remove "remove. popped all transforms above and including idposn $idposn for pipename $pipename poplist len:[llength $poplist]" + #puts stderr "DEBUG> popped all transforms above and including idposn $idposn for pipename $pipename poplist len:[llength $poplist]" - if {[llength [dict get $removed_item -aside]]} { - set restore [dict get $removed_item -aside] - set t [dict get $restore -transform] - set tsettings [dict get $restore -settings] + if {[llength [dict get $removed_item -aside]]} { + set restore [dict get $removed_item -aside] + set t [dict get $restore -transform] + set tsettings [dict get $restore -settings] set obj [$t new $restore] set h [chan push $localchan $obj] dict set restore -handle $h dict set restore -obj $obj lappend stack $restore + #puts stderr "DEBUG> restored aside for pipename $pipename asideposn $asideposn remove_id:'$remove_id' transform: $t handle:$h obj:$obj" + #::shellfilter::log::write shellfilter-remove "restored aside for pipename $pipename asideposn $asideposn remove_id:'$remove_id' transform: $t handle:$h obj:$obj" } + #after 5 #put popped back except for the first one, which we want to remove foreach p [lrange $poplist 1 end] { - set t [dict get $p -transform] - set tsettings [dict get $p -settings] + set t [dict get $p -transform] + set tsettings [dict get $p -settings] set obj [$t new $p] set h [chan push $localchan $obj] + dict set p -handle $h dict set p -obj $obj lappend stack $p + #update idletasks + #puts stderr "DEBUG> restored for pipename $pipename id '$remove_id' transform:$t handle $h obj:$obj" + #::shellfilter::log::write shellfilter-remove "restored for pipename $pipename id '$remove_id' transform:$t handle $h obj:$obj" } + #after 5 dict set pipelines $pipename stack $stack } + + #puts stderr "DEBUG> pipename $pipename id '$remove_id' DONE" + #::shellfilter::log::write shellfilter-remove "pipename $pipename id '$remove_id' DONE" + + if {$previous_blockingstate} { + chan configure $localchan -blocking 1 + } #JMNJMN 2025 review! #show_pipeline $pipename -note "after_remove $remove_id" return 1 @@ -1853,6 +2035,41 @@ namespace eval shellfilter::stack { namespace eval shellfilter { variable sources [list] variable stacks [dict create] + #------------------------------------------------------------------------------------------------- + #tcllib logger infrastructure. + #------------------------------------------------------------------------------------------------- + namespace eval ::shellfilter::loggerprocs { + #container for procs/aliases to be pointed to by tcllib logger using log::logproc + + proc Dolog {lvl txt} { + #logger calls this in such a way that a straight uplevel can get us the vars/commands in messages substituted + set msg "[clock format [clock seconds] -format "%Y-%m-%dT%H:%M:%S"] ::shellspy $lvl '[uplevel [list subst $txt]]'" + puts stderr $msg + } + proc Runlog {lvl script} { + uplevel 1 $script + } + } + if {![catch { + package require logger + }]} { + logger::initNamespace ::shellfilter + foreach lvl [logger::levels] { + interp alias {} ::shellfilter::loggerprocs::Log_$lvl {} ::shellfilter::loggerprocs::Runlog $lvl + log::logproc $lvl ::shellfilter::loggerprocs::Log_$lvl + } + logger::setlevel warn + #namespace path ::shellfilter::log + } else { + #e.g tcllib not available, safe interp? + #fake out the logger calls + namespace eval ::shellfilter::log { + foreach lvl {debug info notice warn error critical alert emergency} { + proc $lvl {args} {} + } + } + } + #------------------------------------------------------------------------------------------------- proc ::shellfilter::redir_channel_to_log {chan args} { variable sources @@ -2396,13 +2613,9 @@ namespace eval shellfilter { #must be a list. If it was a shell commandline string. convert it elsewhere first. variable sources - set runtag "shellfilter-run" - #set tid [::shellfilter::log::open $runtag [list -syslog 127.0.0.1:514]] - set tid [::shellfilter::log::open $runtag [list -syslog ""]] if {[catch {llength $commandlist} listlen]} { set listlen "" } - ::shellfilter::log::write $runtag " commandlist:'$commandlist' listlen:$listlen strlen:[string length $commandlist]" #flush stdout #flush stderr @@ -2416,6 +2629,7 @@ namespace eval shellfilter { -errchan stderr -inchan stdin -tclscript 0 + -syslog "" }] set opts [dict merge $defaults $args] @@ -2432,6 +2646,13 @@ namespace eval shellfilter { set teehandle_err ${teehandle}err set teehandle_in ${teehandle}in + set syslog [dict get $opts -syslog] + dict unset opts -syslog + set runtag "shellfilter-run" + set tid [::shellfilter::log::open $runtag [list -syslog 127.0.0.1:514]] + #set tid [::shellfilter::log::open $runtag [list -syslog $syslog]] + log::info {::shellfilter::log::write $runtag " opts: $opts"} + log::info {::shellfilter::log::write $runtag " commandlist:'$commandlist' listlen:$listlen strlen:[string length $commandlist]"} #puts stdout "shellfilter initialising tee_to_pipe transforms for in/out/err" @@ -2441,19 +2662,32 @@ namespace eval shellfilter { lappend sources $source } } - set outdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_out device] - set outpipechan [dict get $outdeviceinfo localchan] - set errdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_err device] - set errpipechan [dict get $errdeviceinfo localchan] + set outdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_out device] + set outpipechan [dict get $outdeviceinfo localchan] + set errdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_err device] + set errpipechan [dict get $errdeviceinfo localchan] #set indeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_in device] #set inpipechan [dict get $indeviceinfo localchan] + #--------------------- + # #TEST + # chan configure $outpipechan -blocking 0 + # chan configure $errpipechan -blocking 0 + log::debug {::shellfilter::log::write $runtag " outchan $outchan -pipechan $outpipechan config: [chan configure $outpipechan]"} + log::debug {::shellfilter::log::write $runtag " errchan $errchan -pipechan $errpipechan config: [chan configure $errpipechan]"} + #--------------------- + + + log::info {::shellfilter::log::write $runtag " calling shellfilter::stack::add for outchan:$outchan and errchan:$errchan with tee_to_pipe transforms. out tag: $teehandle_out outpipechan:$outpipechan err tag $teehandle_err errpipechan:$errpipechan"} #NOTE:These transforms are not necessarily at the top of each stack! #The float/sink mechanism, along with whether existing transforms are diversionary decides where they sit. set id_out [shellfilter::stack::add $outchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_out -pipechan $outpipechan]] set id_err [shellfilter::stack::add $errchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_err -pipechan $errpipechan]] + log::critical { + ::shellfilter::log::write $runtag "[punk::ansi::ansistrip [shellfilter::stack status]]\nchan names:[chan names]" + } # need to use os level channel handle for stdin - try named pipes (or even sockets) instead of fifo2 for this # If non os-level channel - the command can't be run with the redirection # stderr/stdout can be run with non-os handles in the call - @@ -2493,6 +2727,7 @@ namespace eval shellfilter { set exitinfo [list error "$errMsg" source shellcommand_stdout_stderr] } } + log::notice {::shellfilter::log::write $runtag "finished shell command execution with exitinfo '$exitinfo'"} } else { if {[catch { #script result @@ -2500,29 +2735,45 @@ namespace eval shellfilter { } errMsg]} { set exitinfo [list error "$errMsg" errorCode $::errorCode errorInfo "$::errorInfo"] } + log::notice {::shellfilter::log::write $runtag "finished script execution with exitinfo '$exitinfo'"} } #puts "shellfilter::run finished call" #------------------------- #warning - without flush stdout - we can get hang, but only on some terminals # - mechanism for this problem not understood! + #todo - test/document. flush stdout flush stderr + #update idletasks #------------------------- - #the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal - #Remove execution-time Tees from stack - shellfilter::stack::remove stdout $id_out - shellfilter::stack::remove stderr $id_err - #shellfilter::stack::remove stderr $id_in - #puts stderr "shellfilter::run complete..." + #---------------------------------------------------------------------------------------------- + # wrapped using tcllib logger - avoid even generating the shellfilter::stack status table if log level above debug. + # Logger allows the contents to be evaluated only if logging is switched on. + #---------------------------------------------------------------------------------------------- + #todo - change to log::debug + log::critical { + if {![catch {package require punk::ansi}]} { + set stackstatus [punk::ansi::ansistrip [shellfilter::stack status]] + } else { + set stackstatus [shellfilter::stack status] + } + ::shellfilter::log::write $runtag "shellfilter::stack status after execution: \n$stackstatus\nchan:names [chan names]" + } + #---------------------------------------------------------------------------------------------- + - #chan configure stderr -buffering line - #flush stdout + #the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal + #Remove execution-time Tees from stack + log::debug {::shellfilter::log::write $runtag "removing $id_out from stdout stack"} + shellfilter::stack::remove $outchan $id_out + log::debug {::shellfilter::log::write $runtag "removing $id_err from stderr stack"} + shellfilter::stack::remove $errchan $id_err - ::shellfilter::log::write $runtag " return '$exitinfo'" + log::info {::shellfilter::log::write $runtag " return '$exitinfo'"} ::shellfilter::log::close $runtag return $exitinfo } @@ -2552,6 +2803,7 @@ namespace eval shellfilter { } if {$close} { lappend tidied_sources $s + #unsubscribe from source tag s. shellfilter::log::close $s lappend worker_errorlist {*}[shellthread::manager::get_and_clear_errors $s] } diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellthread-1.6.2.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellthread-1.6.2.tm index 6338d1c6..79d12149 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellthread-1.6.2.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellthread-1.6.2.tm @@ -119,29 +119,58 @@ namespace eval shellthread::worker { set waitvar ::shellthread::worker::wait($inpipe,[clock micros]) #tcl::chan::fifo2 based pipe seems slower to establish events upon than Memchan - chan event $readchan readable [list ::shellthread::worker::pipe_read $readchan $source $waitvar $readbuffering $writebuffering] - vwait $waitvar - } - proc pipe_read {chan source waitfor readbuffering writebuffering} { + #chan event $readchan readable [list ::shellthread::worker::pipe_read $readchan $source $waitvar $readbuffering $writebuffering] if {$readbuffering eq "line"} { - set chunksize [chan gets $chan chunk] - if {$chunksize >= 0} { - if {![chan eof $chan]} { - ::shellthread::worker::log pipe 0 - $source - info $chunk\n $writebuffering - } else { - ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering + chan event $readchan readable [list apply {{chan source waitfor writebuffering} { + set chunksize [chan gets $chan chunk] + if {$chunksize >= 0} { + if {![chan eof $chan]} { + ::shellthread::worker::log pipe 0 - $source - info $chunk\n $writebuffering + } else { + ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering + } } - } + if {[chan eof $chan]} { + chan event $chan readable {} + set $waitfor "pipe" + chan close $chan + } + }} $readchan $source $waitvar $writebuffering] + } else { - set chunk [chan read $chan] - ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering - } - if {[chan eof $chan]} { - chan event $chan readable {} - set $waitfor "pipe" - chan close $chan + chan event $readchan readable [list apply {{chan source waitfor writebuffering} { + set chunk [chan read $chan] + ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering + if {[chan eof $chan]} { + chan event $chan readable {} + set $waitfor "pipe" + chan close $chan + } + }} $readchan $source $waitvar $writebuffering] } + + vwait $waitvar } + #proc pipe_read {chan source waitfor readbuffering writebuffering} { + # if {$readbuffering eq "line"} { + # set chunksize [chan gets $chan chunk] + # if {$chunksize >= 0} { + # if {![chan eof $chan]} { + # ::shellthread::worker::log pipe 0 - $source - info $chunk\n $writebuffering + # } else { + # ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering + # } + # } + # } else { + # set chunk [chan read $chan] + # ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering + # } + # if {[chan eof $chan]} { + # chan event $chan readable {} + # set $waitfor "pipe" + # chan close $chan + # } + #} proc start_pipe_write {source writechan args} { variable outpipe @@ -181,8 +210,8 @@ namespace eval shellthread::worker { chan configure $writechan -blocking 0 set waitvar ::shellthread::worker::wait($outpipe,[clock micros]) - chan event $readchan readable [list apply {{chan writechan source waitfor readbuffering} { - if {$readbuffering eq "line"} { + if {$readbuffering eq "line"} { + chan event $readchan readable [list apply {{chan writechan source waitfor} { set chunksize [chan gets $chan chunk] if {$chunksize >= 0} { if {![chan eof $chan]} { @@ -191,19 +220,55 @@ namespace eval shellthread::worker { puts -nonewline $writechan $chunk } } - } else { + if {[chan eof $chan]} { + chan event $chan readable {} + set $waitfor "pipe" + flush $writechan ;#2026-05-19 - ensure all data is sent before closing + chan close $writechan + if {$chan ne "stdin"} { + chan close $chan + } + } + }} $readchan $writechan $source $waitvar] + } else { + chan event $readchan readable [list apply {{chan writechan source waitfor} { set chunk [chan read $chan] puts -nonewline $writechan $chunk - } - if {[chan eof $chan]} { - chan event $chan readable {} - set $waitfor "pipe" - chan close $writechan - if {$chan ne "stdin"} { - chan close $chan + if {[chan eof $chan]} { + chan event $chan readable {} + set $waitfor "pipe" + chan close $writechan + if {$chan ne "stdin"} { + chan close $chan + } } - } - }} $readchan $writechan $source $waitvar $readbuffering] + }} $readchan $writechan $source $waitvar] + + } + + # chan event $readchan readable [list apply {{chan writechan source waitfor readbuffering} { + # if {$readbuffering eq "line"} { + # set chunksize [chan gets $chan chunk] + # if {$chunksize >= 0} { + # if {![chan eof $chan]} { + # puts $writechan $chunk + # } else { + # puts -nonewline $writechan $chunk + # } + # } + # } else { + # set chunk [chan read $chan] + # puts -nonewline $writechan $chunk + # } + # if {[chan eof $chan]} { + # chan event $chan readable {} + # set $waitfor "pipe" + # chan close $writechan + # if {$chan ne "stdin"} { + # chan close $chan + # } + # } + # }} $readchan $writechan $source $waitvar $readbuffering] vwait $waitvar } @@ -479,6 +544,10 @@ namespace eval shellthread::manager { set sourcetag [lindex $sourcetaglist 0] ;#todo - use all set defaults [dict create {*}{ + -raw 0 + -file {} + -syslog {} + -direction out -workertype message }] set settingsdict [dict merge $defaults $settingsdict] @@ -501,6 +570,10 @@ namespace eval shellthread::manager { return [dict get $winfo tid] } elseif {$existing_settings eq {-raw 0 -file {} -syslog {} -direction out}} { #review - magic dict seems brittle - shouldn't hard code here.??? + + #existing worker has default settings - so we'll assume it's a placeholder and update it with our settings + #review - where/when do we override the default settings? + dict lappend winfo list_client_tids $tidclient dict set workers $sourcetag $winfo ;#writeback return [dict get $winfo tid] @@ -575,11 +648,11 @@ namespace eval shellthread::manager { package require Thread package require shellthread if {![catch {::shellthread::worker::init %tidcli% %ts_start% $::settingsinfo} errmsg]} { - unset ::settingsinfo - set ::shellthread_init "ok" + unset ::settingsinfo + set ::shellthread_init "ok" } else { - unset ::settingsinfo - set ::shellthread_init "err $errmsg" + unset ::settingsinfo + set ::shellthread_init "err $errmsg" } }] @@ -622,15 +695,23 @@ namespace eval shellthread::manager { proc write_log {source msg args} { variable workers - set ts_micros_sent [clock micros] - set defaults [list -async 1 -level info] - set opts [dict merge $defaults $args] - - if {[dict exists $workers $source]} { + if {[dict exists $workers $source tid]} { set tidworker [dict get $workers $source tid] if {$tidworker eq "noop"} { return } + } else { + set tidworker "" + } + + set ts_micros_sent [clock micros] + set defaults [list {*}{ + -async 1 + -level info + }] + set opts [dict merge $defaults $args] + + if {$tidworker ne ""} { if {![thread::exists $tidworker]} { # -syslog -file ? set tidworker [new_worker $source] @@ -674,10 +755,7 @@ namespace eval shellthread::manager { if {[dict exists $workers $source]} { set list_client_tids [dict get $workers $source list_client_tids] if {[set posn [lsearch $list_client_tids $mytid]] >= 0} { - #set list_client_tids [lreplace $list_client_tids $posn $posn] - #set list_client_tids [lreplace $list_client_tids[set list_client_tids {}] $posn $posn] ledit list_client_tids $posn $posn - dict set workers $source list_client_tids $list_client_tids } if {![llength $list_client_tids]} { @@ -685,7 +763,6 @@ namespace eval shellthread::manager { } } } - #we've removed our own tid from all the tags - possibly across multiplew workertids, and possibly leaving some workertids with no subscribers for a particular tag - or no subscribers at all. set subscriberless_workers [list] @@ -696,8 +773,8 @@ namespace eval shellthread::manager { set subscriber_count 0 set kill_count 0 ;#number of ts_end_list entries - even one indicates thread is doomed foreach taginfo $worker_tags { - incr subscriber_count [llength [dict get $taginfo list_client_tids]] - incr kill_count [llength [dict get $taginfo ts_end_list]] + incr subscriber_count [llength [dict get $taginfo list_client_tids]] + incr kill_count [llength [dict get $taginfo ts_end_list]] } if {$subscriber_count == 0} { lappend subscriberless_workers $workertid @@ -760,7 +837,7 @@ namespace eval shellthread::manager { set ::shellthread::waitfor waiting #after $timeout [list set ::shellthread::waitfor] #2025-07 timed-out untested review - set cancelid [after $timeout [list set ::shellthread::waitfor timed-out]] + set timeout_timer [after $timeout {set ::shellthread::waitfor timed-out}] set waiting_for [list] set ended [list] @@ -769,7 +846,9 @@ namespace eval shellthread::manager { if {[thread::exists $tid]} { lappend waiting_for $tid #thread::send -async $tid [list shellthread::worker::terminate [thread::id]] timeoutarr(shutdown_free_threads) - thread::send -async $tid [list shellthread::worker::terminate [thread::id]] ::shellthread::waitfor + set tid_client [thread::id] + #shellthread::worker::terminate will return thread id of terminating thread (or empty string) + thread::send -async $tid [list shellthread::worker::terminate $tid_client] ::shellthread::waitfor } } if {[llength $waiting_for]} { @@ -779,13 +858,13 @@ namespace eval shellthread::manager { set timedout 1 break } else { - after cancel $cancelid + after cancel $timeout_timer lappend ended $::shellthread::waitfor } } } set free_threads [list] - return [dict create existed $waiting_for ended $ended timedout $timedout] + return [dict create existed $waiting_for ended $ended timedout $timedout allthreads [thread::names]] } #TODO - important. diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/textblock-0.1.3.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/textblock-0.1.3.tm index d664aad0..7609c2ed 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/textblock-0.1.3.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/textblock-0.1.3.tm @@ -5107,9 +5107,25 @@ tcl::namespace::eval textblock { tcl::mathfunc::min {*}[lmap v [split $textblock \n] {tcl::string::length $v}] } - if {[catch {package require parser}]} { - #tclparser c extension not available - use tcl string functions to count line-endings + + #try not to load parser (and associated punk::args::moduledoc::parser) immediately. + if {[package provide parser] ne ""} { + #parser already loaded. proc height {textblock} { + if {[string first \v $textblock] >= 0} { + #use standard (slower) mechanism for counting lines + #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 + } else { + return [expr {[parse countnewline $textblock {}] + 1}] + } + } + } else { + #parser not loaded - but might be loadable. + #install a 'height' function that will load parser + proc _height_tcl {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) @@ -5119,8 +5135,7 @@ tcl::namespace::eval 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 return [expr {$num_le + 1}] ;# one line if no le - 2 if there is one trailing le even if no data follows le } - } else { - proc height {textblock} { + proc _height_c {textblock} { if {[string first \v $textblock] >= 0} { #use standard (slower) mechanism for counting lines #vertical tab on a proper terminal should move directly down. @@ -5131,7 +5146,22 @@ tcl::namespace::eval textblock { return [expr {[parse countnewline $textblock {}] + 1}] } } + + #oneshot height function - renames itself on first call to the appropriate implementation. + proc height {textblock} { + if {[catch {package require parser}]} { + #parser not available - use tcl implementation + rename ::textblock::height "" + rename ::textblock::_height_tcl ::textblock::height + } else { + #parser available - use c implementation + rename ::textblock::height "" + rename ::textblock::_height_c ::textblock::height + } + tailcall ::textblock::height $textblock + } } + #MAINTENANCE - same as overtype::blocksize? proc size {textblock} { if {$textblock eq ""} { @@ -8141,16 +8171,17 @@ tcl::namespace::eval textblock { -etabs -default 0\ -help "expanding tabs - experimental/unimplemented." #review - -choicelabels placeholder dollarsign of textblock::frame_samples must be left aligned with -choicelabels - -type -default light\ - -type dict\ - -typesynopsis {${$I}choice${$NI}|<${$I}dict${$NI}>}\ - -choices {${$DYN_FRAMETYPES}}\ - -choicerestricted 0 -choicecolumns 8\ - -unindentedfields {-choicelabels}\ - -choicelabels { - ${$DYN_FRAMESAMPLES} - }\ - -help "Type of border for frame." + -type -default light\ + -type dict\ + -typesynopsis {${$I}choice${$NI}|<${$I}dict${$NI}>}\ + -choices {${$DYN_FRAMETYPES}}\ + -choicerestricted 0\ + -choicecolumns 8\ + -unindentedfields {-choicelabels}\ + -choicelabels { + ${$DYN_FRAMESAMPLES} + }\ + -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]}" diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/zipper-0.14.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/zipper-0.14.tm index d0cb4f1c740f48ca0a5d46b092f71346ce4ef9e6..7f7817f1ddce7fa6abfdea66e3c84636d1fe44cd 100644 GIT binary patch delta 24 dcmX@+d(3x(s1)-pjy; 2} { - #if more than just numitems and indexset - we expect only -base ie 4 args in total - if {[llength $args] != 4} { - set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] - } - set optname [lindex $args 0] - set optval [lindex $args 1] - set fulloptname [tcl::prefix::match -error "" -base $optname] - if {$fulloptname ne "-base" || ![string is integer -strict $optval]} { - set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + dict for {opt val} $optlist { + set fulloptname [tcl::prefix::match -error "" {-base -return} $opt] + switch -exact -- $fulloptname { + -return { + set fullval [tcl::prefix::match -error "" {indices pairs} $val] + if {$fullval ni {indices pairs}} { + set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] + uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + } + set returntype $fullval + } + -base { + if {![string is integer -strict $val]} { + set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] + uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + } + set base $val + } + default { + set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] + uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + } } - set base $optval } + + #set base 0 ;#default + #if {[llength $args] > 2} { + # #if more than just numitems and indexset - we expect only -base ie 4 args in total + # if {[llength $args] != 4} { + # set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] + # uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + # } + # set optname [lindex $args 0] + # set optval [lindex $args 1] + # set fulloptname [tcl::prefix::match -error "" -base $optname] + # if {$fulloptname ne "-base" || ![string is integer -strict $optval]} { + # set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve] + # uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg] + # } + # set base $optval + #} # -------------------------------------------------- @@ -5160,8 +5208,84 @@ namespace eval punk::lib { } } } + if {$returntype eq "pairs"} { + return [indices_to_pairs $index_list] + } return $index_list } + proc indices_to_pairs {indices} { + #convert a list of indices to a list of pairs representing the start and end of contiguous runs of indices which can be increasing or decreasing. + #if the direction of the run changes - we end the previous run and start a new one + if {[llength $indices] == 0} { + return [list] + } + set pairs [list] + set start [lindex $indices 0] + set prev $start + set direction 0 ;#0 = unknown, 1 = increasing, -1 = decreasing + for {set i 1} {$i < [llength $indices]} {incr i} { + set idx [lindex $indices $i] + if {![string is integer -strict $idx]} { + error "non-integer index '$idx' in indices list" + } + if {$idx == $prev + 1} { + #increase since prev + if {$direction == 0} { + set direction 1 + } elseif {$direction == -1} { + #direction changed - end previous run and start new one + lappend pairs [list $start $prev] + set start $idx + set direction 0 + } else { + #still increasing + } + set prev $idx + } elseif {$idx == $prev - 1} { + #decrease since prev + if {$direction == 0} { + set direction -1 + } elseif {$direction == 1} { + #direction changed - end previous run and start new one + lappend pairs [list $start $prev] + set start $idx + set direction 0 + } + set prev $idx + } else { + #run ended - add pair to list + lappend pairs [list $start $prev] + set start $idx + set prev $idx + set direction 0 + } + } + # add final run + lappend pairs [list $start $prev] + return $pairs + } + + #proc indices_to_pairs {indices} { + # #convert a list of indices to a list of pairs representing the start and end of contiguous runs of indices + # set pairs [list] + # set start [lindex $indices 0] + # set prev $start + # for {set i 1} {$i < [llength $indices]} {} { + # set idx [lindex $indices $i] + # if {$idx == $prev + 1} { + # #still in a run + # set prev $idx + # } else { + # #run ended - add pair to list + # lappend pairs [list $start $prev] + # set start $idx + # set prev $idx + # } + # } + # # add final run + # lappend pairs [list $start $prev] + # return $pairs + #} # showdict uses lindex_resolve results -Inf & Inf to determine whether index is out of bounds on lower vs upper side #This doesn't need the list itself - just the length suffices. punk::args::define { @@ -7835,6 +7959,77 @@ namespace eval punk::lib { } } + #sugar + #exclusive end - more intuitive for some cases and more consistent with other languages + namespace eval argdoc { + variable PUNKARGS + lappend PUNKARGS [list { + @id -id ::punk::lib::FOR + @cmd -name punk::lib::FOR\ + -summary\ + "BASIC style integer 'for loop'"\ + -help\ + "Sugar syntax for a common looping pattern. + The loop variable takes on values from start to end (exclusive) in increments of step. + If step is not specified, it defaults to 1 or -1 depending on the relative values of start and end. + This is a common looping pattern that isn't directly supported by Tcl's built in control structures, + and this syntax is more concise for convenient interactive usage. For example: + + FOR i 0 10 {puts $i} + + will print the numbers 0 to 9. + + This wrapper necessarily has some slight overhead compared to builtin Tcl for, foreach and while loops, + so may not be suitable for performance critical inner loops. + + See also: https://wiki.tcl-lang.org/page/Simple+shorthand+%27for%27+loop + " + @values -min 3 -max 4 + varname -type string -help "loop variable name" + start -type integer -help "initial value for loop variable" + end -type integer -help "end value for loop variable (exclusive)" + step -type integer -optional 1 -help "step value for loop variable (defaults to 1 or -1 depending on start and end values)" + script -type script -help "script to execute for each loop iteration" + }] + } + proc FOR { var args } { + switch -- [llength $args] { + 3 { + # FOR x start end {} + lassign $args start end script + set step [ expr {$start > $end ? - 1 : 1} ] + } + 4 { + # FOR x start end step {} + lassign $args start end step script + } + default { + error "FOR: wrong # args, should be: FOR varName startValue endValue ?stepValue? script" + } + } + if {![string is integer -strict $start] || ![string is integer -strict $end] || ![string is integer -strict $step]} { + error "FOR: start,end and step values must be integers" + } + upvar $var loopVar + set loopVar [expr {$start - $step}] + #to support 'continue' we have to increment the loopVar prior to the script evaluation, within the loop condition + if {$start < $end} { + if {$step <= 0} { + error "FOR: step value must be positive when start < end" + } + while {[incr loopVar $step] < $end} { + uplevel $script + } + } else { + if {$step >= 0} { + error "FOR: step value must be negative when start > end" + } + while {[incr loopVar $step] > $end} { + uplevel $script + } + } + } + #review - there are various type of uuid - we should use something consistent across platforms #twapi is used on windows because it's about 5 times faster - but is this more important than consistency? #twapi is much slower to load in the first place (e.g 75ms vs 6ms if package names already loaded) - so for oneshots tcllib uuid is better anyway diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/base-0.1.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/base-0.1.tm index eb1e1c5e..ba24f31d 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/base-0.1.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/base-0.1.tm @@ -677,7 +677,7 @@ namespace eval punk::mix::base { if {$opt_use_tar != 0} { set target [file tail $path] set tmplocation [punk::mix::util::tmpdir] - set archivename $tmplocation/[punk::mix::util::tmpfile].tar + set archivename $tmplocation/[punk::mix::util::tmpfile].tar ;#generates a unique filename - does not create the file. cd $base ;#cd is process-wide.. keep cd in effect for as small a scope as possible. (review for thread issues) @@ -687,14 +687,38 @@ namespace eval punk::mix::base { set tsstart [clock millis] if {[set tarpath [auto_execok tar]] ne ""} { #using an external binary is *significantly* faster than tar::create - but comes with some risks - #review - need to check behaviour/flag variances across platforms + set versioninfo [exec {*}$tarpath --version] + #look for "bsdtar" vs "GNU" + #GNU tar is more common on linux - but also available on windows via gnuutils or msys + #/tar.exe on windows is likely to be bsdtar. + #GNU tar on windows will commonly fail with 'cannot connect to C: resolve failed' - may need --force-local flag + + #review - need to further check behaviour/flag variances across platforms #don't use -z flag. On at least some tar versions the zipped file will contain a timestamped subfolder of filename.tar - which ruins the checksum #also - tar is generally faster without the compression (although this may vary depending on file size and disk speed?) - exec {*}$tarpath -cf $archivename $target ;#{*} needed in case spaces in tarpath - set tsend [clock millis] - set ms [expr {$tsend - $tsstart}] - puts stdout " tar -cf done ($ms ms)" - } else { + + if {[string match "*GNU*" $versioninfo]} { + set flags "--force-local" + } else { + #presumably bsdtar - which is more likely to be present on windows - and doesn't seem to have the same issue with drive letters in paths + set flags "" + } + if {[catch { + #{*}$tarpath needed in case spaces in tarpath + exec {*}$tarpath -cf {*}$flags $archivename $target + } errMsg]} { + set tsend [clock millis] + set ms [expr {$tsend - $tsstart}] + puts stdout " 'tar -cf $flags' ERROR ($ms ms) - falling back to tar::create\n error info: $errMsg" + } else { + set tsend [clock millis] + set ms [expr {$tsend - $tsstart}] + puts stdout " 'tar -cf $flags' done ($ms ms)" + } + } + + if {![file exists $archivename]} { + #fallback to tar library approach if external tar failed to create the archive. set tsstart [clock millis] ;#don't include auto_exec search time for tar::create tar::create $archivename $target set tsend [clock millis] @@ -703,6 +727,7 @@ namespace eval punk::mix::base { puts stdout " NOTE: install tar executable for potentially *much* faster directory checksum processing" } + if {$ftype eq "file"} { set sizeinfo "(size [punk::lib::format_number [file size $target]] bytes)" } else { diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm index 87449fb3..793736b8 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm @@ -333,11 +333,11 @@ namespace eval punk::mix::cli { set defaults [list {*}{ -errorprefix projectname }] - if {[llength $args] %2 != 0} {error "validate_modulename args must be name-value pairs: received '$args'"} + if {[llength $args] %2 != 0} {error "validate_projectname args must be name-value pairs: received '$args'"} set known_opts [dict keys $defaults] foreach k [dict keys $args] { if {$k ni $known_opts} { - error "validate_modulename error: unknown option $k. known options: $known_opts" + error "validate_projectname error: unknown option $k. known options: $known_opts" } } set opts [dict merge $defaults $args] @@ -381,34 +381,6 @@ namespace eval punk::mix::cli { return $name } - #split modulename (as present in a filename or namespaced name) into name/version ignoring leading namespace path - #ignore trailing .tm .TM if present - #if version doesn't pass validation - treat it as part of the modulename and return empty version string without error - #Up to caller to validate. - proc split_modulename_version {fullmodulename} { - set lastpart [namespace tail $fullmodulename] - set lastpart [file tail $lastpart] ;# should be ok to use file tail now that we've ensured no namespace components - if {[string equal -nocase [file extension $fullmodulename] ".tm"]} { - set fileparts [split [file rootname $lastpart] -] - } else { - set fileparts [split $lastpart -] - } - if {[punk::mix::util::is_valid_tm_version [lindex $fileparts end]]} { - set versionsegment [lindex $fileparts end] - set namesegment [join [lrange $fileparts 0 end-1] -];#re-stitch - } else { - # - set namesegment [join $fileparts -] - set versionsegment "" - } - set base [namespace qualifiers $fullmodulename] - if {$base ne ""} { - set modulename "${base}::$namesegment" - } else { - set modulename $namesegment - } - return [list $modulename $versionsegment] - } proc get_status {{workingdir ""} args} { set result "" diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm index 3b40d838..4c75b10e 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm @@ -9,7 +9,7 @@ # @@ Meta Begin # Application punk::mix::commandset::module 0.1.0 # Meta platform tcl -# Meta license BSD +# Meta license BSD # @@ Meta End @@ -204,7 +204,7 @@ namespace eval punk::mix::commandset::module { # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- set opt_version_supplied [dict get $opts -version] set opt_version $opt_version_supplied - if {![util::is_valid_tm_version $opt_version]} { + if {![punk::mix::util::is_valid_tm_version $opt_version]} { error "deck module.new error - supplied -version $opt_version doesn't appear to be a valid Tcl module version" } # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- @@ -213,8 +213,8 @@ namespace eval punk::mix::commandset::module { set mversion_supplied "" ;#version supplied directly in module argument if {[string first - $module]> 0} { #if it has a dash then version is required to be valid - lassign [punk::mix::cli::lib::split_modulename_version $module] modulename mversion - if {![util::is_valid_tm_version $mversion]} { + lassign [punk::mix::util::split_modulename_version $module] modulename mversion + if {![punk::mix::util::is_valid_tm_version $mversion]} { error "deck module.new error - unable to determine modulename-version from supplied value '$module'" } set mversion_supplied $mversion ;#record as may need to compare to version from templatefile name @@ -315,7 +315,7 @@ namespace eval punk::mix::commandset::module { } else { set module [string range [string length $vendor.] end] } - lassign [punk::mix::cli::lib::split_modulename_version $m] _tailmname mversion + lassign [punk::mix::util::split_modulename_version $m] _tailmname mversion lappend key_version_list [list $m $mversion] } if {[llength $matches]} { @@ -383,7 +383,7 @@ namespace eval punk::mix::commandset::module { } else { error "module.new error: Unable to interpret filename components of template file '$templatefile'" } - lassign [punk::mix::cli::lib::split_modulename_version $template_modulename_part] t_mname t_version + lassign [punk::mix::util::split_modulename_version $template_modulename_part] t_mname t_version #t_version may be empty string if template is unversioned e.g template_whatever.tm set fd [open $templatefile r]; set template_filedata [read $fd]; close $fd @@ -398,7 +398,7 @@ namespace eval punk::mix::commandset::module { } else { # - if {[util::is_valid_tm_version $t_version]} { + if {[punk::mix::util::is_valid_tm_version $t_version]} { if {$mversion_supplied eq ""} { set build_version $t_version } else { @@ -500,7 +500,7 @@ namespace eval punk::mix::commandset::module { set name_version_pairs [list] lappend name_version_pairs [list $moduletail $infile_version] foreach existing $existing_versions { - lassign [punk::mix::cli::lib::split_modulename_version $existing] namepart version ;# .tm is stripped and ignored + lassign [punk::mix::util::split_modulename_version $existing] namepart version ;# .tm is stripped and ignored if {[string match #modpod-* $namepart]} { set namepart [string range $namepart 8 end] } diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/util-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/util-0.1.0.tm index 9de4a33d..7f55005b 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/util-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/util-0.1.0.tm @@ -330,6 +330,35 @@ namespace eval punk::mix::util { return 0 } } + + #split modulename (as present in a filename or namespaced name) into name/version ignoring leading namespace path + #ignore trailing .tm .TM if present + #if version doesn't pass validation - treat it as part of the modulename and return empty version string without error + #Up to caller to validate. + proc split_modulename_version {fullmodulename} { + set lastpart [namespace tail $fullmodulename] + set lastpart [file tail $lastpart] ;# should be ok to use file tail now that we've ensured no namespace components + if {[string equal -nocase [file extension $fullmodulename] ".tm"]} { + set fileparts [split [file rootname $lastpart] -] + } else { + set fileparts [split $lastpart -] + } + if {[is_valid_tm_version [lindex $fileparts end]]} { + set versionsegment [lindex $fileparts end] + set namesegment [join [lrange $fileparts 0 end-1] -];#re-stitch + } else { + set namesegment [join $fileparts -] + set versionsegment "" + } + set base [namespace qualifiers $fullmodulename] + if {$base ne ""} { + set modulename "${base}::$namesegment" + } else { + set modulename $namesegment + } + return [list $modulename $versionsegment] + } + #Note that semver only has a small overlap with tcl tm versions. #todo - work out what overlap and whether it's even useful #see also TIP #439: Semantic Versioning (tcl 9??) @@ -337,6 +366,7 @@ namespace eval punk::mix::util { set re {^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$} } #todo - semver conversion/validation for other systems? + proc magic_tm_version {} { set magicbase 999999 ;#deliberately large so given load-preference when testing! #we split the literal to avoid the literal appearing here - reduce risk of accidentally converting to a release version diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm index e89997c5..db6acbb4 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm @@ -4186,7 +4186,7 @@ y" {return quirkykeyscript} #this *very odd* construct is to avoid using the namespace argument of apply. (handling of *weird/inadvisable* namespaces) #we use an uplevel from within the apply which runs in the global namespace, (but called via nseval from within targetns) and a result var for 'info default' in the current punk::ns namespace. nseval $targetns [list apply [list {procname argname} { - set has_default [uplevel 1 [list info default $procname $argname ::punk::ns::corp_defvar]] + set has_default [uplevel 1 [list ::info default $procname $argname ::punk::ns::corp_defvar]] if {$has_default} { set answer [dict create exists 1 default $::punk::ns::corp_defvar] } else { @@ -5025,7 +5025,7 @@ y" {return quirkykeyscript} set queryargs [lrange $args $i end] set resolvedargs [list] set queryargs_untested $queryargs - puts "punk::args::id_exists $docid queryargs_untested: $queryargs" + #puts "punk::ns::cmdtraverse punk::args::id_exists $docid queryargs_untested: $queryargs" } else { #we cannot generate autodoc for any deeper (e.g ensemble/proc after undocumented parent) #There is nothing to indicate the locations of subcommands - they could be anywhere. @@ -7191,6 +7191,7 @@ y" {return quirkykeyscript} nstest eval {package require punk::ns} set ns "" if {![catch {nstest eval [list punk::ns::pkguse $pkg_unqualified]} errMsg]} { + #review set script [string map [list %p% $pkg_unqualified] {dict get $::punk::ns::pkguse_package_to_namespace %p%}] set ns [nstest eval $script] } else { diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/packagepreference-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/packagepreference-0.1.0.tm index e45cefa7..87e33d8c 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/packagepreference-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/packagepreference-0.1.0.tm @@ -110,6 +110,19 @@ tcl::namespace::eval punk::packagepreference { #[para]This comes at some slight cost for packages that are only available with uppercase letters in the name - but at minimal cost for recommended lowercase package names #[para]Return to the standard ::package builtin by calling punk::packagepreference::uninstall + if {![catch {commandstack::get_stack} cstack]} { + if {[dict exists $cstack ::package]} { + set pstack [dict get $cstack ::package] + foreach record $pstack { + if {[dict get $record rename] eq "punk::packagepreference"} { + #already installed - silently ignore. + return 0 + } + } + } + } + + #todo - review/update commandstack package #modern module/lib names should preferably be lower case #see tip 590 - "Recommend lowercase Package names". Where non-lowercase are deprecated (but not removed even in Tcl9) @@ -170,7 +183,10 @@ tcl::namespace::eval punk::packagepreference { if {[llength $pkgloadedinfo]} { if {[llength $available_versions] > 1} { - puts stderr "--> pkg $pkg not already 'provided' but shared object seems to be loaded: $pkgloadedinfo - and [llength $available_versions] versions available" + if {[catch {thread::id} threadid]} { + set threadid "unknown-threadid" + } + #puts stderr "--> pkg $pkg not already 'provided' but shared object seems to be loaded: $pkgloadedinfo - and [llength $available_versions] versions available. $available_versions threadid: $threadid" } lassign $pkgloadedinfo loaded_path name set lc_loadedpath [string tolower $loaded_path] @@ -302,7 +318,10 @@ tcl::namespace::eval punk::packagepreference { puts stderr "Failed to load punk::args::moduledoc::$dp - error was: $errMsg" } } else { - puts stdout "Loaded punk::args::moduledoc::$dp for package $pkg threadid: [thread::id]" + if {[catch {thread::id} threadid]} { + set threadid "unknown-threadid" + } + puts stdout "punk::packagepreference overloaded 'package require': Loaded punk::args::moduledoc::$dp for package $pkg threadid: $threadid" } } #--------------------------------------------------------------- diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm index a7aff5a6..91f7a31a 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm @@ -432,7 +432,7 @@ proc repl::start {inchan args} { variable codethread #review if {$codethread eq ""} { - error "start - no codethread. call init first. (options -safe 0|1)" + error "start - no codethread. call init first. (options -type 0|1)" } variable commandstr @@ -478,7 +478,7 @@ proc repl::start {inchan args} { #set ::punk::repl::codethread::running 1 #the interp in which commands such as d/ run - #we need to namespace eval for the -safe interp which may not have the packages loaded (or be able to) but still needs default values + #we need to namespace eval for the -type interp which may not have the packages loaded (or be able to) but still needs default values #punk::repl::codethread::running is required whether safe or not. interp eval code { namespace eval ::punk::repl::codethread {} @@ -690,7 +690,7 @@ proc repl::reopen_stdinX {} { } -#add to sliding buffer of last x chars emmitted to screen by repl +# add to sliding buffer of last x chars emmitted to screen by repl #(we could maintain only one char - more kept merely for debug assistance) #will not detect emissions from exec with stdout redirected and presumably some extensions etc proc repl::screen_last_char_add {c what {why ""}} { @@ -2123,8 +2123,10 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config #if {$chunk eq "\x1b\[C"} { #} + #---------------------------------------------------------------------------------------------------------------------------------------------------------- punk::console::cursor_off flush stdout + #---------------------------------------------------------------------------------------------------------------------------------------------------------- $editbuf add_chunk $chunk @@ -2235,8 +2237,10 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config lappend input_chunks_waiting($inputchan) $waiting } } + #---------------------------------------------------------------------------------------------------------------------------------------------------------- punk::console::cursor_on flush stdout + #---------------------------------------------------------------------------------------------------------------------------------------------------------- if {$editbuf_linenum_submitted == 0} { #(there is no line 0 - lines start at 1) @@ -2264,9 +2268,9 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config rputs stderr "trap1 POSIX '$e' eopts:'$eopts" flush stderr } on error {repl_error erropts} { - rputs stderr "error1 in repl_handler: $repl_error" + rputs stderr "error1 in repl_process_data: $repl_error" rputs stderr "-------------" - rputs stderr "$::errorInfo" + rputs stderr "erroropts: $erropts" rputs stderr "-------------" set stdinreader [chan event $inputchan readable] if {![string length $stdinreader]} { @@ -2485,7 +2489,13 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config variable codethread_cond variable codethread_mutex - lappend errstack [shellfilter::stack::add stderr tee_to_var -settings {-varname ::repl::output_stderr}] + #------------------------------- + #JJJJ JMN test + #REVIEW + #lappend errstack [shellfilter::stack::add stderr tee_to_var -settings {-varname ::repl::output_stderr}] + #------------------------------- + + #thread::transfer $codethread stderr #chan configure stdout -buffering none @@ -2939,9 +2949,9 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config rputs stderr "trap POSIX '$e' eopts:'$eopts" flush stderr } on error {repl_error erropts} { - rputs stderr "error in repl_handler: $repl_error" + rputs stderr "error2 in repl_process_data: $repl_error" rputs stderr "-------------" - rputs stderr "$::errorInfo" + rputs stderr "$erropts" rputs stderr "-------------" set stdinreader [chan event $inputchan readable] if {![string length $stdinreader]} { @@ -2986,10 +2996,10 @@ namespace eval repl { variable codethread_cond variable codethread_mutex - set opts [list -force 0 -safe 0 -safelog 0 -paths {} -callback_interp $default_callback_interp] + set opts [list -force 0 -type 0 -safelog 0 -paths {} -callback_interp $default_callback_interp] foreach {k v} $args { switch -- $k { - -force - -safe - -safelog - -paths - -callback_interp { + -force - -type - -safelog - -paths - -callback_interp { dict set opts $k $v } default { @@ -2998,7 +3008,7 @@ namespace eval repl { } } set opt_force [dict get $opts -force] - set opt_safe [dict get $opts -safe] + set opt_repltype [dict get $opts -type] set opt_safelog [dict get $opts -safelog] if {$opt_safelog eq "0"} { set opt_safelog "" @@ -3113,46 +3123,50 @@ namespace eval repl { package require punk::args #package require Thread - if {[catch {package require thread} errM]} { - puts stdout ">>repl::init initscript lib load fail on package require thread\n$errM" + #if {[catch {package require thread} errM]} { + # puts stdout ">>repl::init initscript lib load fail on package require thread\n$errM" if {[catch {package require Thread} errM2]} { puts stdout ">>repl::init initscript lib load fail on package require Thread\n$errM2" puts stdout ">>repl::init auto_path : $::auto_path" puts stdout ">>repl::init tcl::tm::list: [tcl::tm::list]" } - } + #} + namespace eval ::punk::repl::codethread {} + + # ------------------------------------------------------------------------------------------------------------------------------------------------------- #----- #review - icomm as a possible way to talk to thread outside of the code interp. #thread::send msgs arrive at a specific interp based on initial setup - review for when/whether androwish thread enhancements are made to allow #thread::send to caller defined interp targets (reference?) #snit required for icomm - if {[catch {package require snit} errM]} { - #puts stdout "punk::repl::initscript: lib load fail ---snit $errM" - } - if {[catch {package require punk::icomm} errM]} { - #puts stdout "punk::repl::initscript: lib load fail ---icomm $errM" - } + #if {[catch {package require snit} errM]} { + # #puts stdout "punk::repl::initscript: lib load fail ---snit $errM" + #} + #if {[catch {package require punk::icomm} errM]} { + # #puts stdout "punk::repl::initscript: lib load fail ---icomm $errM" + #} #----- - - namespace eval ::punk::repl::codethread {} - #todo - review. According to fifo2 docs Memchan involves one less thread (may offer better performance/resource use) - catch {package require tcl::chan::fifo2} - if {[catch { - #first use can raise error being a version number e.g 0.1.0 - why? - lassign [tcl::chan::fifo2] ::punk::repl::codethread::repltalk replside - } errMsg]} { - puts stdout "punk::repl::initscript tcl::chan::fifo2 error: $errMsg" - } else { - #experimental? - #puts stdout "transferring chan $replside to thread %replthread%" - #flush stdout - #if {[catch { - # #after 0 [list thread::transfer %replthread% $replside] - #} errMsg]} { - # #puts stdout "---thread::transfer error: $errMsg" - #} - } + #---------------------------------------------------------------------------------------------------------------------- + # todo - review. According to fifo2 docs Memchan involves one less thread (may offer better performance/resource use) + # catch {package require tcl::chan::fifo2} + # if {[catch { + # #first use can raise error being a version number e.g 0.1.0 - why? + # lassign [tcl::chan::fifo2] ::punk::repl::codethread::repltalk replside + # } errMsg]} { + # puts stdout "punk::repl::initscript tcl::chan::fifo2 error: $errMsg" + # } else { + # #experimental? + # #puts stdout "transferring chan $replside to thread %replthread%" + # #flush stdout + # # if {[catch { + # # #after 0 [list thread::transfer %replthread% $replside] + # # } errMsg]} { + # # #puts stdout "---thread::transfer error: $errMsg" + # # } + # } + #---------------------------------------------------------------------------------------------------------------------- + # ------------------------------------------------------------------------------------------------------------------------------------------------------- package require punk::console package require punk::repl::codethread @@ -3333,7 +3347,7 @@ namespace eval repl { set ts_start [clock seconds] set replresult [interp eval code { package require punk::repl - repl::init -safe punk + repl::init -type punk repl::start stdin }] return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]] @@ -3343,7 +3357,7 @@ namespace eval repl { interp eval code { package require punk::repl } - interp eval code [list repl::init -safe safe {*}$args] + interp eval code [list repl::init -type safe {*}$args] set replresult [interp eval code [list repl::start stdin]] return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]] @@ -3353,7 +3367,7 @@ namespace eval repl { interp eval code { package require punk::repl } - set codethread [interp eval code [list repl::init -safe safebase {*}$args]] + set codethread [interp eval code [list repl::init -type safebase {*}$args]] puts stdout "safebase codethread:$codethread" set replresult [interp eval code [list repl::start stdin]] @@ -3364,7 +3378,7 @@ namespace eval repl { interp eval code { package require punk::repl } - interp eval code [list repl::init -safe punksafe {*}$args] + interp eval code [list repl::init -type punksafe {*}$args] set replresult [interp eval code [list repl::start stdin]] return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]] @@ -3376,14 +3390,14 @@ namespace eval repl { #flush stdout set args %args% - set safe [dict get $args -safe] + set repltype [dict get $args -type] set safelog [dict get $args -safelog] set paths [list] if {[dict exists $args -paths]} { set paths [dict get $args -paths] } - switch -- $safe { + switch -- $repltype { safe { interp create -safe -- code code eval [list namespace eval ::punk::libunknown {}] @@ -3444,7 +3458,7 @@ namespace eval repl { #todo a specific punk::libunknown 'package unknown' handler for safe interps #pull in code via calls to source cached code? - switch -- $safe { + switch -- $repltype { safe { if {[llength $paths]} { package require punk::island @@ -3797,8 +3811,27 @@ namespace eval repl { } punk - 0 { + #------------------------------------------------------------------------ + #Test - experimental 2026 + if {"stdout" in [chan names]} { + interp share {} stdout code + } else { + interp share {} [shellfilter::stack::item_tophandle stdout] code + } + if {"stderr" in [chan names]} { + interp share {} stderr code + } else { + interp share {} [shellfilter::stack::item_tophandle stderr] code + } + foreach nm {shellspyout shellspyerr} { + set thandle [shellfilter::stack::item_tophandle $nm] + if {$thandle ne ""} { + interp share {} $thandle code + } + } + #------------------------------------------------------------------------ interp eval code { - #safe !=1 and safe !=2, tmlist: %tmlist% + #repltype !=1 and repltype !=2, tmlist: %tmlist% set ::argv0 %argv0% set ::argv %argv% set ::argc %argc% @@ -3978,8 +4011,8 @@ namespace eval repl { thread::id } set init_script [string map $scriptmap $init_script] - #REVIEW - the same initscript sent for all values of $safe and it switches on values of $safe provided in %args% - #we already know $safe in this thread when generating the script - so why send the large script to the thread to then switch on that? + #REVIEW - the same initscript sent for all values of $repltype and it switches on values of $repltype provided in %args% + #we already know $repltype in this thread when generating the script - so why send the large script to the thread to then switch on that? #thread::send $codethread $init_script if {![catch { @@ -3994,7 +4027,7 @@ namespace eval repl { error $errMsg } } - #init - don't auto init - require init with possible options e.g -safe + #init - don't auto init - require init with possible options e.g -type } package provide punk::repl [namespace eval punk::repl { variable version diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repo-0.1.1.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repo-0.1.1.tm index 482fbbeb..dd446ae8 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repo-0.1.1.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repo-0.1.1.tm @@ -83,34 +83,38 @@ namespace eval punk::repo { proc get_fossil_usage {} { set allcmds [runout -n fossil help -a] + set allcmds [punk::ansi::ansistrip $allcmds] set mainhelp [runout -n fossil help] + set mainhelp [punk::ansi::ansistrip $mainhelp] set maincommands [list] #only start parsing for TOPICS after a line such as "Other comman values for TOPIC:" set parsing_topics 0 foreach ln [split $mainhelp \n] { set ln [string trim $ln] - if {$ln eq ""} { - continue - } - if {[string match "*values for TOPIC*" $ln]} { - set parsing_topics 1 + if {$ln eq ""} { + continue + } + if {[string match "*values for TOPIC*" $ln]} { + set parsing_topics 1 + continue + } + if {$parsing_topics} { + #lines starting with uppercase are topic headers - we want to ignore these and any blank lines + if {[regexp {^[A-Z]+} $ln]} { continue } - if {$parsing_topics} { - #lines starting with uppercase are topic headers - we want to ignore these and any blank lines - if {[regexp {^[A-Z]+} $ln]} { - continue - } - lappend maincommands {*}$ln - } + lappend maincommands {*}$ln + } } #fossil output was ordered in columns, but we loaded list in row-wise, messing up the order set maincommands [lsort $maincommands] set allcmds [lsort $allcmds] set othercmds [punk::lib::ldiff $allcmds $maincommands] - set fossil_setting_names [lsort [runout -n fossil help -s]] + set setting_info [runout -n fossil help -s] + set setting_info [punk::ansi::ansistrip $setting_info] + set fossil_setting_names [lsort $setting_info] set result "@leaders -min 0\n" @@ -186,6 +190,8 @@ namespace eval punk::repo { foreach ln $basic_opt_lines { set ln [string trim $ln] + #fossil sometimes emits cursor control sequences e.g CSI 3 q + set ln [punk::ansi::ansistrip $ln] if {$ln eq ""} { continue } @@ -250,6 +256,7 @@ namespace eval punk::repo { ${[punk::repo::get_fossil_subcommand_usage add]} @form -form "raw" -synopsis "exec fossil add \[OPTIONS\] FILE1 \[FILE2\]..." + #fossil help may have ansi - review @formdisplay -header "fossil help add" -body {${[runout -n fossil help add]}} } ""] @@ -264,6 +271,7 @@ namespace eval punk::repo { ${[punk::repo::get_fossil_subcommand_usage diff]} @form -form "raw" -synopsis "exec fossil diff \[OPTIONS\] FILE1 \[FILE2\]..." + #fossil help may have ansi - review @formdisplay -header "fossil help diff" -body {${[runout -n fossil help diff]}} } ""] diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm index eb439c6e..86823a45 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm @@ -121,14 +121,6 @@ namespace eval punkcheck { } method as_record {} { - #set fields [list\ - # -targets $o_targets\ - # -keep_installrecords $o_keep_installrecords\ - # -keep_skipped $o_keep_skipped\ - # -keep_inprogress $o_keep_inprogress\ - # body $o_records\ - #] - dict create {*}{ } tag FILEINFO {*}{ } -targets $o_targets {*}{ @@ -216,18 +208,6 @@ namespace eval punkcheck { } else { set tsiso_end "" } - #set fields [list\ - # -tsiso_begin $tsiso_begin\ - # -ts_begin $o_ts_begin\ - # -tsiso_end $tsiso_end\ - # -ts_end $o_ts_end\ - # -id $o_id\ - # -source $o_rel_sourceroot\ - # -targets $o_rel_targetroot\ - # -types $o_types\ - # -config $o_configdict\ - #] - #set record [dict create tag EVENT {*}$fields] dict create {*}{ } tag EVENT {*}{ diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm index a0fa060a..6a948593 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm @@ -67,6 +67,8 @@ tcl::namespace::eval shellfilter::log { } proc ::shellfilter::log::close {tag} { #shellthread::manager::close_worker $tag + + #unsubscribe involves a thread::send to the worker thread if we are the last subscriber to the tag. shellthread::manager::unsubscribe [list $tag]; #workertid will be added back to free list if no tags remain subscribed } @@ -106,6 +108,13 @@ namespace eval shellfilter::pipe { package require shellthread #we are only using the fifo in a single direction to pipe to another thread # - so whilst wchan and rchan could theoretically each be both read & write we're only using them for one operation each + #---------------------------------------------------------------------------------------- + # Differences beetween Memchan's fifo2 and tcl::chan::fifo2 implementations: + # (not necessarily a comprehensive list) + # - Memchan's fifo2 is reportedly faster and more efficient than tcl::chan::fifo2, but it may not be available on all platforms + # tcl::chan::fifo2 is a pure Tcl implementation. + # - Closing one side of a tcl::chan::fifo2 (ver 1.1) will cause the other side to close whereas this is not the case with Memchan's fifo2. + #---------------------------------------------------------------------------------------- if {![catch {package require Memchan}]} { lassign [fifo2] wchan rchan } else { @@ -289,7 +298,8 @@ namespace eval shellfilter::chan { set o_enc [tcl::dict::get $tf -encoding] set o_encbuf "" set settingsdict [tcl::dict::get $tf -settings] - set varname [tcl::dict::get $settingsdict -varname] + set varname [tcl::dict::get $settingsdict -varname] ;#review - should we support multiple vars here? e.g with a list of varnames in settings and append to all of them? + #these are not upvared - must be fully qualified variable names. set o_datavars $varname if {[tcl::dict::exists $tf -junction]} { set o_is_junction [tcl::dict::get $tf -junction] @@ -298,18 +308,18 @@ namespace eval shellfilter::chan { } } method initialize {ch mode} { - return [list initialize finalize write flush clear] + if {"read" in $mode} { + #this should raise an error in 'chan push' + error "shellfilter::chan::tee_to_var transform does not support read mode" + } + return [list initialize finalize write flush] } method finalize {ch} { my destroy } - method clear {ch} { - return - } - method watch {ch events} { - # must be present but we ignore it because we do not - # post any events - } + #method clear {ch} { + # return + #} #method read {ch count} { # return ? #} @@ -320,9 +330,14 @@ namespace eval shellfilter::chan { #puts stdout "" #review - just clear o_encbuf and emit nothing? #we wouldn't have a value there if it was convertable from the channel encoding? - set clear $o_encbuf + if {[string length $o_encbuf]} { + #if we have data in the buffer that we haven't been able to convert to a string + #- then we probably have some kind of encoding mismatch. Is it safer to discard it than to emit garbage chars to the channel or var? + #REVIEW - log that we are discarding the buffer contents on flush? + puts stderr "WARNING: flush called on tee_to_var with non-empty buffer. This probably indicates an encoding mismatch between the channel encoding and the encoding expected by the transform. Discarding buffer contents: '$o_encbuf'" + } set o_encbuf "" - return $o_encbuf + return "" } method write {ch bytes} { #test with set x [string repeat " \U1f6c8" 2043] @@ -387,10 +402,12 @@ namespace eval shellfilter::chan { } } method initialize {transform_handle mode} { - return [list initialize read drain write flush clear finalize] + #return [list initialize read drain write flush clear finalize] + return [list initialize write flush clear finalize] } method finalize {transform_handle} { - ::shellfilter::log::close $o_logsource + #Note that an error in the finalize can stop 'chan pop' from running properly. + #::shellfilter::log::close $o_logsource my destroy } method watch {transform_handle events} { @@ -398,26 +415,53 @@ namespace eval shellfilter::chan { # post any events } method clear {transform_handle} { + set o_encbuf "" return } - method drain {transform_handle} { - return "" - } - method read {transform_handle bytes} { - set logdata [tcl::encoding::convertfrom $o_enc $bytes] - #::shellfilter::log::write $o_logsource $logdata - puts -nonewline $o_localchan $logdata - return $bytes - } + #method drain {transform_handle} { + # return "" + #} + #method read {transform_handle bytes} { + # set logdata [tcl::encoding::convertfrom $o_enc $bytes] + # #::shellfilter::log::write $o_logsource $logdata + # puts -nonewline $o_localchan $logdata + # return $bytes + #} + #method flush {transform_handle} { + # #return "" + # set clear $o_encbuf[set o_encbuf ""] + # if {[catch {tcl::encoding::convertfrom $o_enc $clear} stringdata]} { + # #if we can't convert the buffer contents to a string - put it back and try again with more data later + # #REVIEW? + # set o_encbuf $clear + # puts -nonewline $o_localchan "" + # return "" + # } + # #jjj + # puts -nonewline $o_localchan $stringdata + # flush $o_localchan + # return $clear + #} method flush {transform_handle} { - #return "" - set clear $o_encbuf + #we wouldn't have a value in o_encbuf if it was convertable from the channel encoding? + if {[string length $o_encbuf]} { + #if we have data in the buffer that we haven't been able to convert to a string + #- then we probably have some kind of encoding mismatch. Is it safer to discard it than to emit garbage chars to the channel or var? + #REVIEW - log that we are discarding the buffer contents on flush? + puts stderr "WARNING: flush called on tee_to_pipe with non-empty buffer. This probably indicates an encoding mismatch between the channel encoding and the encoding expected by the transform. Discarding buffer contents: '$o_encbuf'" + } set o_encbuf "" - return $o_encbuf + return "" } method write {transform_handle bytes} { #set logdata [tcl::encoding::convertfrom $o_enc $bytes] set inputbytes $o_encbuf$bytes + if {$inputbytes eq ""} { + #review - do we even get empty writes? + puts stderr "WARNING: write called on tee_to_pipe with empty inputbytes. This may be a no-op, but it may also indicate an issue with the upstream transform or channel. Emitting no data to the pipe for this write." + return "" + } + set o_encbuf "" set tail_offset 0 while {$tail_offset < [::tcl::string::length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [::tcl::string::range $inputbytes 0 end-$tail_offset]} stringdata]} { @@ -426,18 +470,23 @@ namespace eval shellfilter::chan { if {$tail_offset > 0} { if {$tail_offset < [::tcl::string::length $inputbytes]} { #stringdata from catch statement must be a valid result + set converted [::tcl::string::range $inputbytes 0 end-$tail_offset] set t [expr {$tail_offset - 1}] set o_encbuf [::tcl::string::range $inputbytes end-$t end] } else { + #nothing convertable in the buffer - put it back and try again with more data later set stringdata "" set o_encbuf $inputbytes return "" } + } else { + #no catch on conversion - everything is convertable + #stringdata must be valid result from convertfrom of whole $o_encbuf$bytes + set converted $inputbytes } #::shellfilter::log::write $o_logsource $logdata puts -nonewline $o_localchan $stringdata - #return $bytes - return [::tcl::string::range $inputbytes 0 end-$tail_offset] + return $converted ;#same data as $stringdata but in bytes form. } #a tee is not a redirection - because data still flows along the main path method meta_is_redirection {} { @@ -501,6 +550,7 @@ namespace eval shellfilter::chan { if {$tail_offset > 0} { if {$tail_offset < [::tcl::string::length $inputbytes]} { #stringdata from catch statement must be a valid result + set converted [::tcl::string::range $inputbytes 0 end-$tail_offset] set t [expr {$tail_offset - 1}] set o_encbuf [::tcl::string::range $inputbytes end-$t end] } else { @@ -508,10 +558,15 @@ namespace eval shellfilter::chan { set o_encbuf $inputbytes return "" } + } else { + #no catch on conversion - everything is convertable + #stringdata must be valid result from convertfrom of whole $o_encbuf$bytes + set converted $inputbytes } - set bytes [::tcl::string::range $inputbytes 0 end-$tail_offset] + #set bytes [::tcl::string::range $inputbytes 0 end-$tail_offset] ::shellfilter::log::write $o_logsource $stringdata - return $bytes + #return $bytes + return $converted ;#same data as $stringdata but in bytes form. } method meta_is_redirection {} { return $o_is_junction @@ -519,6 +574,10 @@ namespace eval shellfilter::chan { } + #see TIP 230 Tcl Channel Transformation Reflection API + + #the logonly transform does not emit any data downwards towards the base channel - it only writes to the log. + # - ie we are redirecting all data to the log. oo::class create logonly { variable o_tid variable o_logsource @@ -537,19 +596,35 @@ namespace eval shellfilter::chan { set o_tid [::shellfilter::log::open $o_logsource $settingsdict] } method initialize {transform_handle mode} { - return [list initialize finalize write] + #mode is a list containing any of the strings 'read' or 'write'. + if {"read" in $mode} { + #Note that raising an error prevents creation of the transformation. + #The thrown error will appear as a error thrown by 'chan push'. + error "logonly transform does not support read mode" + } + #return all methods supported by the handler + #(we exclude our additional meta_is_redirection method becuase it is not a standard transform method called by the Tcl framework - it's only for our own use in shellfilter) + return [list initialize finalize write flush] } method finalize {transform_handle} { - ::shellfilter::log::close $o_logsource + #review - tip 230 states that any return value or error raised by finalize is ignored + #we wrap in catch to ensure the 'my destroy' is always called. + catch {::shellfilter::log::close $o_logsource} my destroy } - method watch {transform_handle events} { - # must be present but we ignore it because we do not - # post any events + #clear? + method flush {transform_handle} { + #we wouldn't have a value in o_encbuf if it was convertable from the channel encoding? + if {[string length $o_encbuf]} { + #if we have data in the buffer that we haven't been able to convert to a string + #- then we probably have some kind of encoding mismatch. Is it safer to discard it than to emit garbage chars to the log? + #REVIEW. - we are writing the raw bytes to the log here because we can't convert them to a string. + #This may be useful for debugging issues, but it may also result in garbage data in the log. + ::shellfilter::log::write $o_logsource $o_encbuf + set o_encbuf "" + } + return } - #method read {transform_handle count} { - # return ? - #} method write {transform_handle bytes} { #set logdata [encoding convertfrom $o_enc $bytes] set inputbytes $o_encbuf$bytes @@ -600,26 +675,49 @@ namespace eval shellfilter::chan { } } method initialize {transform_handle mode} { - return [list initialize read write clear flush drain finalize] + #return [list initialize read write clear flush drain finalize] + + #REVIEW - we aren't using 'read' mode - but if we raise an error if 'read' is in the mode then the system doesn't work. + #this is probably because we add it to one end of a fifo2 channel, which although we only use for writing is a bidirectional channel. + #---------------------- + #don't do this + #---------------------- + #if {"read" in $mode} { + # #Note that raising an error prevents creation of the transformation. + # #The thrown error will appear as a error thrown by 'chan push'. + # error "shellfilter::chan::ansistrip transform does not support read mode" + #} + #---------------------- + return [list initialize read write flush finalize] } method finalize {transform_handle} { my destroy } - method clear {transform_handle} { - return - } - method watch {transform_handle events} { - } - method drain {transform_handle} { - return "" - } + #method clear {transform_handle} { + # return + #} + #method drain {transform_handle} { + # return "" + #} method read {transform_handle bytes} { set instring [encoding convertfrom $o_enc $bytes] set outstring [punk::ansi::ansistrip $instring] return [encoding convertto $o_enc $outstring] } + #method flush {transform_handle} { + # return "" + #} method flush {transform_handle} { - return "" + #return "" + set clear $o_encbuf[set o_encbuf ""] + if {[catch {tcl::encoding::convertfrom $o_enc $clear} stringdata]} { + #if we can't convert the buffer contents to a string - put it back and try again with more data later + #REVIEW? + set o_encbuf $clear + return "" + } + #review + return $stringdata } #method write {transform_handle bytes} { # #broken due to occasional unexpected byte sequence @@ -989,29 +1087,44 @@ namespace eval shellfilter::chan { } method initialize {transform_handle mode} { #clear undesirable in terminal output channels (review) - return [list initialize write flush read drain finalize] + #return [list initialize write flush read drain finalize] + if {$mode eq "read"} { + error "shellfilter::chan::ansiwrap channel transform does not support read mode" + } + return [list initialize write flush finalize] } method finalize {transform_handle} { my destroy } - method watch {transform_handle events} { - } method clear {transform_handle} { #In the context of stderr/stdout - we probably don't want clear to run. #Terminals might call it in the middle of a split ansi code - resulting in broken output. - #Leave clear of it the init call + #Leave clear out of the initialize call for now puts stdout "" set emit [tcl::encoding::convertto $o_enc $o_buffered] set o_buffered "" return $emit } + #method flush {transform_handle} { + # #puts stdout "" + # set inputbytes $o_buffered$o_encbuf + # set emit [tcl::encoding::convertto $o_enc $inputbytes] + # set o_buffered "" + # set o_encbuf "" + # return $emit + #} method flush {transform_handle} { - #puts stdout "" - set inputbytes $o_buffered$o_encbuf - set emit [tcl::encoding::convertto $o_enc $inputbytes] + #return "" + set clear $o_buffered$o_encbuf + if {[catch {tcl::encoding::convertfrom $o_enc $clear} stringdata]} { + #if we can't convert the buffer contents to a string - does it make sense to emit the raw bytes? + # - probably not. + #REVIEW? + return "" + } set o_buffered "" set o_encbuf "" - return $emit + return $stringdata } method write {transform_handle bytes} { #set instring [tcl::encoding::convertfrom $o_enc $bytes] ;naive approach will break due to unexpected byte sequence - occasionally @@ -1076,14 +1189,14 @@ namespace eval shellfilter::chan { #set outstring ">>>$instring" return [tcl::encoding::convertto $o_enc $outstring] } - method drain {transform_handle} { - return "" - } - method read {transform_handle bytes} { - set instring [tcl::encoding::convertfrom $o_enc $bytes] - set outstring "$o_do_colour$instring$o_do_normal" - return [tcl::encoding::convertto $o_enc $outstring] - } + #method drain {transform_handle} { + # return "" + #} + #method read {transform_handle bytes} { + # set instring [tcl::encoding::convertfrom $o_enc $bytes] + # set outstring "$o_do_colour$instring$o_do_normal" + # return [tcl::encoding::convertto $o_enc $outstring] + #} method meta_is_redirection {} { return $o_is_junction } @@ -1326,9 +1439,11 @@ namespace eval shellfilter::stack { } proc status {{pipename *} args} { variable pipelines + package require textblock set pipecount [dict size $pipelines] set tabletitle "$pipecount pipelines active" set t [textblock::class::table new $tabletitle] + $t configure -frametype ascii; #be conservative here - may need to emit in various debugging contexts. $t add_column -headers [list channel-ident] $t add_column -headers [list device-info localchan] $t configure_column 1 -header_colspans {3} @@ -1337,6 +1452,9 @@ namespace eval shellfilter::stack { $t add_column -headers [list stack-info] foreach k [dict keys $pipelines $pipename] { set lc [dict get $pipelines $k device localchan] + if {[catch {chan configure $lc -encoding} lc_enc]} { + set lc_enc "" + } set rc [dict get $pipelines $k device remotechan] if {[dict exists $k device workertid]} { set tid [dict get $pipelines $k device workertid] @@ -1348,18 +1466,35 @@ namespace eval shellfilter::stack { set stackinfo "" } else { set tbl_inner [textblock::class::table new] + $tbl_inner configure -frametype ascii $tbl_inner configure -show_edge 0 + $tbl_inner add_column -headers id + $tbl_inner add_column -headers transform + $tbl_inner add_column -headers handle + $tbl_inner add_column -headers settings + $tbl_inner add_column -headers aside foreach rec $stack { - set handle [punk::lib::dict_getdef $rec -handle ""] set id [punk::lib::dict_getdef $rec -id ""] set transform [namespace tail [punk::lib::dict_getdef $rec -transform ""]] + set handle [punk::lib::dict_getdef $rec -handle ""] + if {$handle ne ""} { + if {[catch {chan configure $handle -encoding} handle_enc]} { + set handle_enc "" + } + } else { + set handle_enc "" + } set settings [punk::lib::dict_getdef $rec -settings ""] - $tbl_inner add_row [list $id $transform $handle $settings] + set aside [punk::lib::dict_getdef $rec -aside ""] + if {$aside ne ""} { + set aside [punk::lib::showdict $aside] + } + $tbl_inner add_row [list $id $transform $handle\n$handle_enc $settings $aside] } set stackinfo [$tbl_inner print] $tbl_inner destroy } - $t add_row [list $k $lc $rc $tid $stackinfo] + $t add_row [list $k "$lc\n$lc_enc" $rc $tid $stackinfo] } set result [$t print] $t destroy @@ -1511,15 +1646,24 @@ namespace eval shellfilter::stack { proc unwind {pipename} { variable pipelines - set stack [dict get $pipelines $pipename stack] - set localchan [dict get $pipelines $pipename device localchan] + set stack [dict get $pipelines $pipename stack] + set localchan [dict get $pipelines $pipename device localchan] foreach tf [lreverse $stack] { - chan pop $localchan + if {[catch {chan eof $localchan} _eof]} { + #We don't actually care about eof state - but we use this to test if the channel exists. + #('chan names' doesn't reliably show all channels in some cases) + #do nothing. + } else { + #if there are no transforms on the the channel - this is equivalent to 'chan close' of the channel + # but here we should only be calling it when there are transforms on the channel as indicated by the stack variable. + chan pop $localchan + } } dict set pipelines $pipename [list] } #todo proc delete {pipename {wait 0}} { + #::shellfilter::log::open shellfilter-delete [list -syslog "127.0.0.1:514"] variable pipelines set pipeinfo [dict get $pipelines $pipename] set deviceinfo [dict get $pipeinfo device] @@ -1534,18 +1678,29 @@ namespace eval shellfilter::stack { thread::release $tid } - #Memchan closes without error - tcl::chan::fifo2 raises something like 'can not find channel named "rc977"' - REVIEW. why? + #Memchan closes without error - tcl::chan::fifo2 raises something like 'can not find channel named "rc977"' + #- REVIEW. why? It could have something to do with the fact that tcl::memchan::fifo2 closes both sides when one side is closed. catch {chan close $localchan} + #if {[catch {chan close $localchan} errMsg]} { + # ::shellfilter::log::write shellfilter-delete "WARNING: error closing localchan '$localchan' for pipename '$pipename': $errMsg" + #} } #review - proc name clarity is questionable. remove_stackitem? proc remove {pipename remove_id} { + #::shellfilter::log::open shellfilter-remove [list -syslog "127.0.0.1:514"] variable pipelines if {![dict exists $pipelines $pipename]} { - puts stderr "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]" + #puts stderr "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]" + ::shellfilter::log::write shellfilter-remove "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]" return } set stack [dict get $pipelines $pipename stack] set localchan [dict get $pipelines $pipename device localchan] + set previous_blockingstate [chan configure $localchan -blocking] + if {$previous_blockingstate} { + chan configure $localchan -blocking 0 + } + set posn 0 set idposn -1 set asideposn -1 @@ -1572,10 +1727,11 @@ namespace eval shellfilter::stack { dict set container -aside {} lset stack $asideposn $container dict set pipelines $pipename stack $stack + #::shellfilter::log::write shellfilter-remove "cleared '-aside' record for pipename $pipename aside_posn $asideposn remove_id:'$remove_id'" } else { if {$idposn < 0} { - ::shellfilter::log::write shellfilter "ERROR shellfilter::stack::remove $pipename id '$remove_id' not found" - puts stderr "|WARNING>shellfilter::stack::remove $pipename id '$remove_id' not found" + #::shellfilter::log::write shellfilter-remove "ERROR shellfilter::stack::remove $pipename id '$remove_id' not found" + #puts stderr "|WARNING>shellfilter::stack::remove $pipename id '$remove_id' not found" return 0 } set removed_item [lindex $stack $idposn] @@ -1589,32 +1745,58 @@ namespace eval shellfilter::stack { #pop all chans before adding anything back in! foreach p $poplist { + #review + #update idletasks + #puts stderr "DEBUG> popping transform from pipename $pipename for stack p:$p poplist len:[llength $poplist]" + #::shellfilter::log::write shellfilter-remove "popping transform from pipename $pipename for stack p:$p poplist len:[llength $poplist] ---" + #after 0 [list chan pop $localchan] + chan pop $localchan + #::shellfilter::log::write shellfilter-remove "POPPED" + #update idletasks } + #after 5 + #::shellfilter::log::write shellfilter-remove "remove. popped all transforms above and including idposn $idposn for pipename $pipename poplist len:[llength $poplist]" + #puts stderr "DEBUG> popped all transforms above and including idposn $idposn for pipename $pipename poplist len:[llength $poplist]" - if {[llength [dict get $removed_item -aside]]} { - set restore [dict get $removed_item -aside] - set t [dict get $restore -transform] - set tsettings [dict get $restore -settings] + if {[llength [dict get $removed_item -aside]]} { + set restore [dict get $removed_item -aside] + set t [dict get $restore -transform] + set tsettings [dict get $restore -settings] set obj [$t new $restore] set h [chan push $localchan $obj] dict set restore -handle $h dict set restore -obj $obj lappend stack $restore + #puts stderr "DEBUG> restored aside for pipename $pipename asideposn $asideposn remove_id:'$remove_id' transform: $t handle:$h obj:$obj" + #::shellfilter::log::write shellfilter-remove "restored aside for pipename $pipename asideposn $asideposn remove_id:'$remove_id' transform: $t handle:$h obj:$obj" } + #after 5 #put popped back except for the first one, which we want to remove foreach p [lrange $poplist 1 end] { - set t [dict get $p -transform] - set tsettings [dict get $p -settings] + set t [dict get $p -transform] + set tsettings [dict get $p -settings] set obj [$t new $p] set h [chan push $localchan $obj] + dict set p -handle $h dict set p -obj $obj lappend stack $p + #update idletasks + #puts stderr "DEBUG> restored for pipename $pipename id '$remove_id' transform:$t handle $h obj:$obj" + #::shellfilter::log::write shellfilter-remove "restored for pipename $pipename id '$remove_id' transform:$t handle $h obj:$obj" } + #after 5 dict set pipelines $pipename stack $stack } + + #puts stderr "DEBUG> pipename $pipename id '$remove_id' DONE" + #::shellfilter::log::write shellfilter-remove "pipename $pipename id '$remove_id' DONE" + + if {$previous_blockingstate} { + chan configure $localchan -blocking 1 + } #JMNJMN 2025 review! #show_pipeline $pipename -note "after_remove $remove_id" return 1 @@ -1853,6 +2035,41 @@ namespace eval shellfilter::stack { namespace eval shellfilter { variable sources [list] variable stacks [dict create] + #------------------------------------------------------------------------------------------------- + #tcllib logger infrastructure. + #------------------------------------------------------------------------------------------------- + namespace eval ::shellfilter::loggerprocs { + #container for procs/aliases to be pointed to by tcllib logger using log::logproc + + proc Dolog {lvl txt} { + #logger calls this in such a way that a straight uplevel can get us the vars/commands in messages substituted + set msg "[clock format [clock seconds] -format "%Y-%m-%dT%H:%M:%S"] ::shellspy $lvl '[uplevel [list subst $txt]]'" + puts stderr $msg + } + proc Runlog {lvl script} { + uplevel 1 $script + } + } + if {![catch { + package require logger + }]} { + logger::initNamespace ::shellfilter + foreach lvl [logger::levels] { + interp alias {} ::shellfilter::loggerprocs::Log_$lvl {} ::shellfilter::loggerprocs::Runlog $lvl + log::logproc $lvl ::shellfilter::loggerprocs::Log_$lvl + } + logger::setlevel warn + #namespace path ::shellfilter::log + } else { + #e.g tcllib not available, safe interp? + #fake out the logger calls + namespace eval ::shellfilter::log { + foreach lvl {debug info notice warn error critical alert emergency} { + proc $lvl {args} {} + } + } + } + #------------------------------------------------------------------------------------------------- proc ::shellfilter::redir_channel_to_log {chan args} { variable sources @@ -2396,13 +2613,9 @@ namespace eval shellfilter { #must be a list. If it was a shell commandline string. convert it elsewhere first. variable sources - set runtag "shellfilter-run" - #set tid [::shellfilter::log::open $runtag [list -syslog 127.0.0.1:514]] - set tid [::shellfilter::log::open $runtag [list -syslog ""]] if {[catch {llength $commandlist} listlen]} { set listlen "" } - ::shellfilter::log::write $runtag " commandlist:'$commandlist' listlen:$listlen strlen:[string length $commandlist]" #flush stdout #flush stderr @@ -2416,6 +2629,7 @@ namespace eval shellfilter { -errchan stderr -inchan stdin -tclscript 0 + -syslog "" }] set opts [dict merge $defaults $args] @@ -2432,6 +2646,13 @@ namespace eval shellfilter { set teehandle_err ${teehandle}err set teehandle_in ${teehandle}in + set syslog [dict get $opts -syslog] + dict unset opts -syslog + set runtag "shellfilter-run" + set tid [::shellfilter::log::open $runtag [list -syslog 127.0.0.1:514]] + #set tid [::shellfilter::log::open $runtag [list -syslog $syslog]] + log::info {::shellfilter::log::write $runtag " opts: $opts"} + log::info {::shellfilter::log::write $runtag " commandlist:'$commandlist' listlen:$listlen strlen:[string length $commandlist]"} #puts stdout "shellfilter initialising tee_to_pipe transforms for in/out/err" @@ -2441,19 +2662,32 @@ namespace eval shellfilter { lappend sources $source } } - set outdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_out device] - set outpipechan [dict get $outdeviceinfo localchan] - set errdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_err device] - set errpipechan [dict get $errdeviceinfo localchan] + set outdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_out device] + set outpipechan [dict get $outdeviceinfo localchan] + set errdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_err device] + set errpipechan [dict get $errdeviceinfo localchan] #set indeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_in device] #set inpipechan [dict get $indeviceinfo localchan] + #--------------------- + # #TEST + # chan configure $outpipechan -blocking 0 + # chan configure $errpipechan -blocking 0 + log::debug {::shellfilter::log::write $runtag " outchan $outchan -pipechan $outpipechan config: [chan configure $outpipechan]"} + log::debug {::shellfilter::log::write $runtag " errchan $errchan -pipechan $errpipechan config: [chan configure $errpipechan]"} + #--------------------- + + + log::info {::shellfilter::log::write $runtag " calling shellfilter::stack::add for outchan:$outchan and errchan:$errchan with tee_to_pipe transforms. out tag: $teehandle_out outpipechan:$outpipechan err tag $teehandle_err errpipechan:$errpipechan"} #NOTE:These transforms are not necessarily at the top of each stack! #The float/sink mechanism, along with whether existing transforms are diversionary decides where they sit. set id_out [shellfilter::stack::add $outchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_out -pipechan $outpipechan]] set id_err [shellfilter::stack::add $errchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_err -pipechan $errpipechan]] + log::critical { + ::shellfilter::log::write $runtag "[punk::ansi::ansistrip [shellfilter::stack status]]\nchan names:[chan names]" + } # need to use os level channel handle for stdin - try named pipes (or even sockets) instead of fifo2 for this # If non os-level channel - the command can't be run with the redirection # stderr/stdout can be run with non-os handles in the call - @@ -2493,6 +2727,7 @@ namespace eval shellfilter { set exitinfo [list error "$errMsg" source shellcommand_stdout_stderr] } } + log::notice {::shellfilter::log::write $runtag "finished shell command execution with exitinfo '$exitinfo'"} } else { if {[catch { #script result @@ -2500,29 +2735,45 @@ namespace eval shellfilter { } errMsg]} { set exitinfo [list error "$errMsg" errorCode $::errorCode errorInfo "$::errorInfo"] } + log::notice {::shellfilter::log::write $runtag "finished script execution with exitinfo '$exitinfo'"} } #puts "shellfilter::run finished call" #------------------------- #warning - without flush stdout - we can get hang, but only on some terminals # - mechanism for this problem not understood! + #todo - test/document. flush stdout flush stderr + #update idletasks #------------------------- - #the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal - #Remove execution-time Tees from stack - shellfilter::stack::remove stdout $id_out - shellfilter::stack::remove stderr $id_err - #shellfilter::stack::remove stderr $id_in - #puts stderr "shellfilter::run complete..." + #---------------------------------------------------------------------------------------------- + # wrapped using tcllib logger - avoid even generating the shellfilter::stack status table if log level above debug. + # Logger allows the contents to be evaluated only if logging is switched on. + #---------------------------------------------------------------------------------------------- + #todo - change to log::debug + log::critical { + if {![catch {package require punk::ansi}]} { + set stackstatus [punk::ansi::ansistrip [shellfilter::stack status]] + } else { + set stackstatus [shellfilter::stack status] + } + ::shellfilter::log::write $runtag "shellfilter::stack status after execution: \n$stackstatus\nchan:names [chan names]" + } + #---------------------------------------------------------------------------------------------- + - #chan configure stderr -buffering line - #flush stdout + #the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal + #Remove execution-time Tees from stack + log::debug {::shellfilter::log::write $runtag "removing $id_out from stdout stack"} + shellfilter::stack::remove $outchan $id_out + log::debug {::shellfilter::log::write $runtag "removing $id_err from stderr stack"} + shellfilter::stack::remove $errchan $id_err - ::shellfilter::log::write $runtag " return '$exitinfo'" + log::info {::shellfilter::log::write $runtag " return '$exitinfo'"} ::shellfilter::log::close $runtag return $exitinfo } @@ -2552,6 +2803,7 @@ namespace eval shellfilter { } if {$close} { lappend tidied_sources $s + #unsubscribe from source tag s. shellfilter::log::close $s lappend worker_errorlist {*}[shellthread::manager::get_and_clear_errors $s] } diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellthread-1.6.2.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellthread-1.6.2.tm index 6338d1c6..79d12149 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellthread-1.6.2.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellthread-1.6.2.tm @@ -119,29 +119,58 @@ namespace eval shellthread::worker { set waitvar ::shellthread::worker::wait($inpipe,[clock micros]) #tcl::chan::fifo2 based pipe seems slower to establish events upon than Memchan - chan event $readchan readable [list ::shellthread::worker::pipe_read $readchan $source $waitvar $readbuffering $writebuffering] - vwait $waitvar - } - proc pipe_read {chan source waitfor readbuffering writebuffering} { + #chan event $readchan readable [list ::shellthread::worker::pipe_read $readchan $source $waitvar $readbuffering $writebuffering] if {$readbuffering eq "line"} { - set chunksize [chan gets $chan chunk] - if {$chunksize >= 0} { - if {![chan eof $chan]} { - ::shellthread::worker::log pipe 0 - $source - info $chunk\n $writebuffering - } else { - ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering + chan event $readchan readable [list apply {{chan source waitfor writebuffering} { + set chunksize [chan gets $chan chunk] + if {$chunksize >= 0} { + if {![chan eof $chan]} { + ::shellthread::worker::log pipe 0 - $source - info $chunk\n $writebuffering + } else { + ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering + } } - } + if {[chan eof $chan]} { + chan event $chan readable {} + set $waitfor "pipe" + chan close $chan + } + }} $readchan $source $waitvar $writebuffering] + } else { - set chunk [chan read $chan] - ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering - } - if {[chan eof $chan]} { - chan event $chan readable {} - set $waitfor "pipe" - chan close $chan + chan event $readchan readable [list apply {{chan source waitfor writebuffering} { + set chunk [chan read $chan] + ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering + if {[chan eof $chan]} { + chan event $chan readable {} + set $waitfor "pipe" + chan close $chan + } + }} $readchan $source $waitvar $writebuffering] } + + vwait $waitvar } + #proc pipe_read {chan source waitfor readbuffering writebuffering} { + # if {$readbuffering eq "line"} { + # set chunksize [chan gets $chan chunk] + # if {$chunksize >= 0} { + # if {![chan eof $chan]} { + # ::shellthread::worker::log pipe 0 - $source - info $chunk\n $writebuffering + # } else { + # ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering + # } + # } + # } else { + # set chunk [chan read $chan] + # ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering + # } + # if {[chan eof $chan]} { + # chan event $chan readable {} + # set $waitfor "pipe" + # chan close $chan + # } + #} proc start_pipe_write {source writechan args} { variable outpipe @@ -181,8 +210,8 @@ namespace eval shellthread::worker { chan configure $writechan -blocking 0 set waitvar ::shellthread::worker::wait($outpipe,[clock micros]) - chan event $readchan readable [list apply {{chan writechan source waitfor readbuffering} { - if {$readbuffering eq "line"} { + if {$readbuffering eq "line"} { + chan event $readchan readable [list apply {{chan writechan source waitfor} { set chunksize [chan gets $chan chunk] if {$chunksize >= 0} { if {![chan eof $chan]} { @@ -191,19 +220,55 @@ namespace eval shellthread::worker { puts -nonewline $writechan $chunk } } - } else { + if {[chan eof $chan]} { + chan event $chan readable {} + set $waitfor "pipe" + flush $writechan ;#2026-05-19 - ensure all data is sent before closing + chan close $writechan + if {$chan ne "stdin"} { + chan close $chan + } + } + }} $readchan $writechan $source $waitvar] + } else { + chan event $readchan readable [list apply {{chan writechan source waitfor} { set chunk [chan read $chan] puts -nonewline $writechan $chunk - } - if {[chan eof $chan]} { - chan event $chan readable {} - set $waitfor "pipe" - chan close $writechan - if {$chan ne "stdin"} { - chan close $chan + if {[chan eof $chan]} { + chan event $chan readable {} + set $waitfor "pipe" + chan close $writechan + if {$chan ne "stdin"} { + chan close $chan + } } - } - }} $readchan $writechan $source $waitvar $readbuffering] + }} $readchan $writechan $source $waitvar] + + } + + # chan event $readchan readable [list apply {{chan writechan source waitfor readbuffering} { + # if {$readbuffering eq "line"} { + # set chunksize [chan gets $chan chunk] + # if {$chunksize >= 0} { + # if {![chan eof $chan]} { + # puts $writechan $chunk + # } else { + # puts -nonewline $writechan $chunk + # } + # } + # } else { + # set chunk [chan read $chan] + # puts -nonewline $writechan $chunk + # } + # if {[chan eof $chan]} { + # chan event $chan readable {} + # set $waitfor "pipe" + # chan close $writechan + # if {$chan ne "stdin"} { + # chan close $chan + # } + # } + # }} $readchan $writechan $source $waitvar $readbuffering] vwait $waitvar } @@ -479,6 +544,10 @@ namespace eval shellthread::manager { set sourcetag [lindex $sourcetaglist 0] ;#todo - use all set defaults [dict create {*}{ + -raw 0 + -file {} + -syslog {} + -direction out -workertype message }] set settingsdict [dict merge $defaults $settingsdict] @@ -501,6 +570,10 @@ namespace eval shellthread::manager { return [dict get $winfo tid] } elseif {$existing_settings eq {-raw 0 -file {} -syslog {} -direction out}} { #review - magic dict seems brittle - shouldn't hard code here.??? + + #existing worker has default settings - so we'll assume it's a placeholder and update it with our settings + #review - where/when do we override the default settings? + dict lappend winfo list_client_tids $tidclient dict set workers $sourcetag $winfo ;#writeback return [dict get $winfo tid] @@ -575,11 +648,11 @@ namespace eval shellthread::manager { package require Thread package require shellthread if {![catch {::shellthread::worker::init %tidcli% %ts_start% $::settingsinfo} errmsg]} { - unset ::settingsinfo - set ::shellthread_init "ok" + unset ::settingsinfo + set ::shellthread_init "ok" } else { - unset ::settingsinfo - set ::shellthread_init "err $errmsg" + unset ::settingsinfo + set ::shellthread_init "err $errmsg" } }] @@ -622,15 +695,23 @@ namespace eval shellthread::manager { proc write_log {source msg args} { variable workers - set ts_micros_sent [clock micros] - set defaults [list -async 1 -level info] - set opts [dict merge $defaults $args] - - if {[dict exists $workers $source]} { + if {[dict exists $workers $source tid]} { set tidworker [dict get $workers $source tid] if {$tidworker eq "noop"} { return } + } else { + set tidworker "" + } + + set ts_micros_sent [clock micros] + set defaults [list {*}{ + -async 1 + -level info + }] + set opts [dict merge $defaults $args] + + if {$tidworker ne ""} { if {![thread::exists $tidworker]} { # -syslog -file ? set tidworker [new_worker $source] @@ -674,10 +755,7 @@ namespace eval shellthread::manager { if {[dict exists $workers $source]} { set list_client_tids [dict get $workers $source list_client_tids] if {[set posn [lsearch $list_client_tids $mytid]] >= 0} { - #set list_client_tids [lreplace $list_client_tids $posn $posn] - #set list_client_tids [lreplace $list_client_tids[set list_client_tids {}] $posn $posn] ledit list_client_tids $posn $posn - dict set workers $source list_client_tids $list_client_tids } if {![llength $list_client_tids]} { @@ -685,7 +763,6 @@ namespace eval shellthread::manager { } } } - #we've removed our own tid from all the tags - possibly across multiplew workertids, and possibly leaving some workertids with no subscribers for a particular tag - or no subscribers at all. set subscriberless_workers [list] @@ -696,8 +773,8 @@ namespace eval shellthread::manager { set subscriber_count 0 set kill_count 0 ;#number of ts_end_list entries - even one indicates thread is doomed foreach taginfo $worker_tags { - incr subscriber_count [llength [dict get $taginfo list_client_tids]] - incr kill_count [llength [dict get $taginfo ts_end_list]] + incr subscriber_count [llength [dict get $taginfo list_client_tids]] + incr kill_count [llength [dict get $taginfo ts_end_list]] } if {$subscriber_count == 0} { lappend subscriberless_workers $workertid @@ -760,7 +837,7 @@ namespace eval shellthread::manager { set ::shellthread::waitfor waiting #after $timeout [list set ::shellthread::waitfor] #2025-07 timed-out untested review - set cancelid [after $timeout [list set ::shellthread::waitfor timed-out]] + set timeout_timer [after $timeout {set ::shellthread::waitfor timed-out}] set waiting_for [list] set ended [list] @@ -769,7 +846,9 @@ namespace eval shellthread::manager { if {[thread::exists $tid]} { lappend waiting_for $tid #thread::send -async $tid [list shellthread::worker::terminate [thread::id]] timeoutarr(shutdown_free_threads) - thread::send -async $tid [list shellthread::worker::terminate [thread::id]] ::shellthread::waitfor + set tid_client [thread::id] + #shellthread::worker::terminate will return thread id of terminating thread (or empty string) + thread::send -async $tid [list shellthread::worker::terminate $tid_client] ::shellthread::waitfor } } if {[llength $waiting_for]} { @@ -779,13 +858,13 @@ namespace eval shellthread::manager { set timedout 1 break } else { - after cancel $cancelid + after cancel $timeout_timer lappend ended $::shellthread::waitfor } } } set free_threads [list] - return [dict create existed $waiting_for ended $ended timedout $timedout] + return [dict create existed $waiting_for ended $ended timedout $timedout allthreads [thread::names]] } #TODO - important. diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/textblock-0.1.3.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/textblock-0.1.3.tm index d664aad0..7609c2ed 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/textblock-0.1.3.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/textblock-0.1.3.tm @@ -5107,9 +5107,25 @@ tcl::namespace::eval textblock { tcl::mathfunc::min {*}[lmap v [split $textblock \n] {tcl::string::length $v}] } - if {[catch {package require parser}]} { - #tclparser c extension not available - use tcl string functions to count line-endings + + #try not to load parser (and associated punk::args::moduledoc::parser) immediately. + if {[package provide parser] ne ""} { + #parser already loaded. proc height {textblock} { + if {[string first \v $textblock] >= 0} { + #use standard (slower) mechanism for counting lines + #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 + } else { + return [expr {[parse countnewline $textblock {}] + 1}] + } + } + } else { + #parser not loaded - but might be loadable. + #install a 'height' function that will load parser + proc _height_tcl {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) @@ -5119,8 +5135,7 @@ tcl::namespace::eval 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 return [expr {$num_le + 1}] ;# one line if no le - 2 if there is one trailing le even if no data follows le } - } else { - proc height {textblock} { + proc _height_c {textblock} { if {[string first \v $textblock] >= 0} { #use standard (slower) mechanism for counting lines #vertical tab on a proper terminal should move directly down. @@ -5131,7 +5146,22 @@ tcl::namespace::eval textblock { return [expr {[parse countnewline $textblock {}] + 1}] } } + + #oneshot height function - renames itself on first call to the appropriate implementation. + proc height {textblock} { + if {[catch {package require parser}]} { + #parser not available - use tcl implementation + rename ::textblock::height "" + rename ::textblock::_height_tcl ::textblock::height + } else { + #parser available - use c implementation + rename ::textblock::height "" + rename ::textblock::_height_c ::textblock::height + } + tailcall ::textblock::height $textblock + } } + #MAINTENANCE - same as overtype::blocksize? proc size {textblock} { if {$textblock eq ""} { @@ -8141,16 +8171,17 @@ tcl::namespace::eval textblock { -etabs -default 0\ -help "expanding tabs - experimental/unimplemented." #review - -choicelabels placeholder dollarsign of textblock::frame_samples must be left aligned with -choicelabels - -type -default light\ - -type dict\ - -typesynopsis {${$I}choice${$NI}|<${$I}dict${$NI}>}\ - -choices {${$DYN_FRAMETYPES}}\ - -choicerestricted 0 -choicecolumns 8\ - -unindentedfields {-choicelabels}\ - -choicelabels { - ${$DYN_FRAMESAMPLES} - }\ - -help "Type of border for frame." + -type -default light\ + -type dict\ + -typesynopsis {${$I}choice${$NI}|<${$I}dict${$NI}>}\ + -choices {${$DYN_FRAMETYPES}}\ + -choicerestricted 0\ + -choicecolumns 8\ + -unindentedfields {-choicelabels}\ + -choicelabels { + ${$DYN_FRAMESAMPLES} + }\ + -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]}" diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/zipper-0.14.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/zipper-0.14.tm index d0cb4f1c740f48ca0a5d46b092f71346ce4ef9e6..7f7817f1ddce7fa6abfdea66e3c84636d1fe44cd 100644 GIT binary patch delta 24 dcmX@+d(3x(s1)-pjy;/tar.exe on windows is likely to be bsdtar. + #GNU tar on windows will commonly fail with 'cannot connect to C: resolve failed' - may need --force-local flag + + #review - need to further check behaviour/flag variances across platforms #don't use -z flag. On at least some tar versions the zipped file will contain a timestamped subfolder of filename.tar - which ruins the checksum #also - tar is generally faster without the compression (although this may vary depending on file size and disk speed?) - exec {*}$tarpath -cf $archivename $target ;#{*} needed in case spaces in tarpath - set tsend [clock millis] - set ms [expr {$tsend - $tsstart}] - puts stdout " tar -cf done ($ms ms)" - } else { + + if {[string match "*GNU*" $versioninfo]} { + set flags "--force-local" + } else { + #presumably bsdtar - which is more likely to be present on windows - and doesn't seem to have the same issue with drive letters in paths + set flags "" + } + if {[catch { + #{*}$tarpath needed in case spaces in tarpath + exec {*}$tarpath -cf {*}$flags $archivename $target + } errMsg]} { + set tsend [clock millis] + set ms [expr {$tsend - $tsstart}] + puts stdout " 'tar -cf $flags' ERROR ($ms ms) - falling back to tar::create\n error info: $errMsg" + } else { + set tsend [clock millis] + set ms [expr {$tsend - $tsstart}] + puts stdout " 'tar -cf $flags' done ($ms ms)" + } + } + + if {![file exists $archivename]} { + #fallback to tar library approach if external tar failed to create the archive. set tsstart [clock millis] ;#don't include auto_exec search time for tar::create tar::create $archivename $target set tsend [clock millis] @@ -703,6 +727,7 @@ namespace eval punk::mix::base { puts stdout " NOTE: install tar executable for potentially *much* faster directory checksum processing" } + if {$ftype eq "file"} { set sizeinfo "(size [punk::lib::format_number [file size $target]] bytes)" } else { diff --git a/src/vfs/_vfscommon.vfs/modules/punk/repo-0.1.1.tm b/src/vfs/_vfscommon.vfs/modules/punk/repo-0.1.1.tm index 482fbbeb..dd446ae8 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/repo-0.1.1.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/repo-0.1.1.tm @@ -83,34 +83,38 @@ namespace eval punk::repo { proc get_fossil_usage {} { set allcmds [runout -n fossil help -a] + set allcmds [punk::ansi::ansistrip $allcmds] set mainhelp [runout -n fossil help] + set mainhelp [punk::ansi::ansistrip $mainhelp] set maincommands [list] #only start parsing for TOPICS after a line such as "Other comman values for TOPIC:" set parsing_topics 0 foreach ln [split $mainhelp \n] { set ln [string trim $ln] - if {$ln eq ""} { - continue - } - if {[string match "*values for TOPIC*" $ln]} { - set parsing_topics 1 + if {$ln eq ""} { + continue + } + if {[string match "*values for TOPIC*" $ln]} { + set parsing_topics 1 + continue + } + if {$parsing_topics} { + #lines starting with uppercase are topic headers - we want to ignore these and any blank lines + if {[regexp {^[A-Z]+} $ln]} { continue } - if {$parsing_topics} { - #lines starting with uppercase are topic headers - we want to ignore these and any blank lines - if {[regexp {^[A-Z]+} $ln]} { - continue - } - lappend maincommands {*}$ln - } + lappend maincommands {*}$ln + } } #fossil output was ordered in columns, but we loaded list in row-wise, messing up the order set maincommands [lsort $maincommands] set allcmds [lsort $allcmds] set othercmds [punk::lib::ldiff $allcmds $maincommands] - set fossil_setting_names [lsort [runout -n fossil help -s]] + set setting_info [runout -n fossil help -s] + set setting_info [punk::ansi::ansistrip $setting_info] + set fossil_setting_names [lsort $setting_info] set result "@leaders -min 0\n" @@ -186,6 +190,8 @@ namespace eval punk::repo { foreach ln $basic_opt_lines { set ln [string trim $ln] + #fossil sometimes emits cursor control sequences e.g CSI 3 q + set ln [punk::ansi::ansistrip $ln] if {$ln eq ""} { continue } @@ -250,6 +256,7 @@ namespace eval punk::repo { ${[punk::repo::get_fossil_subcommand_usage add]} @form -form "raw" -synopsis "exec fossil add \[OPTIONS\] FILE1 \[FILE2\]..." + #fossil help may have ansi - review @formdisplay -header "fossil help add" -body {${[runout -n fossil help add]}} } ""] @@ -264,6 +271,7 @@ namespace eval punk::repo { ${[punk::repo::get_fossil_subcommand_usage diff]} @form -form "raw" -synopsis "exec fossil diff \[OPTIONS\] FILE1 \[FILE2\]..." + #fossil help may have ansi - review @formdisplay -header "fossil help diff" -body {${[runout -n fossil help diff]}} } ""] diff --git a/src/vfs/_vfscommon.vfs/modules/shellfilter-0.2.1.tm b/src/vfs/_vfscommon.vfs/modules/shellfilter-0.2.1.tm deleted file mode 100644 index 42ae92cd..00000000 --- a/src/vfs/_vfscommon.vfs/modules/shellfilter-0.2.1.tm +++ /dev/null @@ -1,3395 +0,0 @@ -#copyright 2023 Julian Marcel Noble -#license: BSD (revised 3-clause) -# -#Note shellfilter is currently only directly useful for unidirectional channels e.g stdin,stderr,stdout, or for example fifo2 where only one direction is being used. -#To generalize this to bidrectional channels would require shifting around read & write methods on transform objects in a very complicated manner. -#e.g each transform would probably be a generic transform container which holds sub-objects to which read & write are indirected. -#This is left as a future exercise...possibly it's best left as a concept for uni-directional channels anyway -# - as presumably the reads/writes from a bidirectional channel could be diverted off to unidirectional pipelines for processing with less work -# (and maybe even better speed/efficiency if the data volume is asymmetrical and there is significant processing on one direction) -# - - -tcl::namespace::eval shellfilter::log { - variable allow_adhoc_tags 1 - variable open_logs [tcl::dict::create] - variable is_enabled 0 - - proc disable {} { - variable is_enabled - set is_enabled 0 - proc ::shellfilter::log::open {tag settingsdict} {} - proc ::shellfilter::log::write {tag msg} {} - proc ::shellfilter::log::write_sync {tag msg} {} - proc ::shellfilter::log::close {tag} {} - } - - proc enable {} { - variable is_enabled - set is_enabled 1 - #'tag' is an identifier for the log source. - #(well.. really it's a common *target* of file and/or syslog host:port which can be written to from any thread that uses the tag) - #the terminology here is kinda ratshit. - - # each tag will use it's own thread to write to the configured log target - proc ::shellfilter::log::open {tag {settingsdict {}}} { - upvar ::shellfilter::sources sourcelist - if {![dict exists $settingsdict -tag]} { - tcl::dict::set settingsdict -tag $tag - } else { - #review - if {$tag ne [tcl::dict::get $settingsdict -tag]} { - error "shellfilter::log::open first argument tag: '$tag' does not match -tag '[tcl::dict::get $settingsdict -tag]' omit -tag, or supply same value" - } - } - if {$tag ni $sourcelist} { - lappend sourcelist $tag - } - - #review new_worker/assign_worker? - set worker_tid [shellthread::manager::new_worker $tag $settingsdict] - #puts stderr "shellfilter::log::open this_threadid: [thread::id] tag: $tag worker_tid: $worker_tid" - return $worker_tid - } - proc ::shellfilter::log::write {tag msg} { - upvar ::shellfilter::sources sourcelist - variable allow_adhoc_tags - if {!$allow_adhoc_tags} { - if {$tag ni $sourcelist} { - error "shellfilter::log::write tag '$tag' hasn't been initialised with a call to shellfilter::log::open $tag , and allow_adhoc_tags has been set false. use shellfilter::log::require_open false to allow adhoc tags" - } - } - shellthread::manager::write_log $tag $msg - } - #write_sync - synchronous processing with logging thread, slower but potentially useful for debugging/testing or forcing delay til log written - proc ::shellfilter::log::write_sync {tag msg} { - shellthread::manager::write_log $tag $msg -async 0 - } - proc ::shellfilter::log::close {tag} { - #shellthread::manager::close_worker $tag - shellthread::manager::unsubscribe [list $tag]; #workertid will be added back to free list if no tags remain subscribed - } - - } - - #review - #configure whether we can call shellfilter::log::write without having called open first - proc require_open {{is_open_required {}}} { - variable allow_adhoc_tags - if {![string length $is_open_required]} { - return $allow_adhoc_tags - } else { - #why not use string is boolean? - set truevalues [list y yes true 1] - set falsevalues [list n no false 0] - if {[string tolower $is_open_required] in $truevalues} { - set allow_adhoc_tags 1 - } elseif {[string tolower $is_open_required] in $falsevalues} { - set allow_adhoc_tags 0 - } else { - error "shellfilter::log::require_open unrecognised value '$is_open_required' try one of $truevalues or $falsevalues" - } - } - } - if {[catch {package require shellthread}]} { - shellfilter::log::disable - } else { - shellfilter::log::enable - } - -} -namespace eval shellfilter::pipe { - #write channel for program. workerthread reads other end of fifo2 and writes data somewhere - proc open_out {tag_pipename {pipesettingsdict {}}} { - set defaultsettings {-buffering full} - set settingsdict [dict merge $defaultsettings $pipesettingsdict] - package require shellthread - #we are only using the fifo in a single direction to pipe to another thread - # - so whilst wchan and rchan could theoretically each be both read & write we're only using them for one operation each - if {![catch {package require Memchan}]} { - lassign [fifo2] wchan rchan - } else { - package require tcl::chan::fifo2 - lassign [tcl::chan::fifo2] wchan rchan - } - #default -translation for both types of fifo on windows is {auto crlf} - # -encoding is as per '[encoding system]' on the platform - e.g utf-8 (e.g windows when beta-utf8 enabled) - chan configure $wchan -buffering [dict get $settingsdict -buffering] ;# - #application end must not be binary for our filters to operate on it - - - #chan configure $rchan -buffering [dict get $settingsdict -buffering] -translation binary ;#works reasonably.. - chan configure $rchan -buffering [dict get $settingsdict -buffering] -translation lf - - set worker_tid [shellthread::manager::new_pipe_worker $tag_pipename $settingsdict] - #puts stderr "worker_tid: $worker_tid" - - #set_read_pipe does the thread::transfer of the rchan end. -buffering setting is maintained during thread transfer - shellthread::manager::set_pipe_read_from_client $tag_pipename $worker_tid $rchan - - set pipeinfo [list localchan $wchan remotechan $rchan workertid $worker_tid direction out] - return $pipeinfo - } - - #read channel for program. workerthread writes to other end of fifo2 from whereever it's reading (stdin, file?) - proc open_in {tag_pipename {settingsdict {} }} { - package require shellthread - package require tcl::chan::fifo2 - lassign [tcl::chan::fifo2] wchan rchan - set program_chan $rchan - set worker_chan $wchan - chan configure $worker_chan -buffering [dict get $settingsdict -buffering] - chan configure $program_chan -buffering [dict get $settingsdict -buffering] - - #review - chan configure $program_chan -blocking 0 - chan configure $worker_chan -blocking 0 - set worker_tid [shellthread::manager::new_worker $tag_pipename $settingsdict] - - shellthread::manager::set_pipe_write_to_client $tag_pipename $worker_tid $worker_chan - - set pipeinfo [list localchan $program_chan remotechan $worker_chan workertid $worker_tid direction in] - puts stderr "|jn>pipe::open_in returning $pipeinfo" - puts stderr "program_chan: [chan conf $program_chan]" - return $pipeinfo - } - -} - - -namespace eval shellfilter::chan { - set testobj ::shellfilter::chan::var - if {$testobj ni [info commands $testobj]} { - - oo::class create var { - variable o_datavar - variable o_trecord - variable o_enc - variable o_is_junction - constructor {tf} { - set o_trecord $tf - set o_enc [dict get $tf -encoding] - set settingsdict [dict get $tf -settings] - set varname [dict get $settingsdict -varname] - set o_datavar $varname - if {[dict exists $tf -junction]} { - set o_is_junction [dict get $tf -junction] - } else { - set o_is_junction 1 ;# as a var is diversionary - default it to be a jucntion - } - } - method initialize {ch mode} { - return [list initialize finalize write] - } - method finalize {ch} { - my destroy - } - method watch {ch events} { - # must be present but we ignore it because we do not - # post any events - } - #method read {ch count} { - # return ? - #} - method write {ch bytes} { - set stringdata [encoding convertfrom $o_enc $bytes] - append $o_datavar $stringdata - return "" - } - method meta_is_redirection {} { - return $o_is_junction - } - method meta_buffering_supported {} { - return [list line full none] - } - } - - #todo - something similar for multiple grep specs each with own -pre & -post .. store to dict? - oo::class create tee_grep_to_var { - variable o_datavar - variable o_lastxlines - variable o_trecord - variable o_grepfor - variable o_prelines - variable o_postlines - variable o_postcountdown - variable o_enc - variable o_is_junction - constructor {tf} { - set o_trecord $tf - set o_enc [tcl::dict::get $tf -encoding] - set o_lastxlines [list] - set o_postcountdown 0 - set defaults [tcl::dict::create -pre 1 -post 1] - set settingsdict [tcl::dict::get $tf -settings] - set settings [tcl::dict::merge $defaults $settingsdict] - set o_datavar [tcl::dict::get $settings -varname] - set o_grepfor [tcl::dict::get $settings -grep] - set o_prelines [tcl::dict::get $settings -pre] - set o_postlines [tcl::dict::get $settings -post] - if {[tcl::dict::exists $tf -junction]} { - set o_is_junction [tcl::dict::get $tf -junction] - } else { - set o_is_junction 0 - } - } - method initialize {transform_handle mode} { - return [list initialize finalize write] - } - method finalize {transform_handle} { - my destroy - } - method watch {transform_handle events} { - } - #method read {transform_handle count} { - # return ? - #} - method write {transform_handle bytes} { - set logdata [tcl::encoding::convertfrom $o_enc $bytes] - set lastx $o_lastxlines - lappend o_lastxlines $logdata - - if {$o_postcountdown > 0} { - append $o_datavar $logdata - if {[regexp $o_grepfor $logdata]} { - #another match in postlines - set o_postcountdown $o_postlines - } else { - incr o_postcountdown -1 - } - } else { - if {[regexp $o_grepfor $logdata]} { - append $o_datavar [join $lastx] - append $o_datavar $logdata - set o_postcountdown $o_postlines - } - } - - if {[llength $o_lastxlines] > $o_prelines} { - set o_lastxlines [lrange $o_lastxlines 1 end] - } - return $bytes - } - method meta_is_redirection {} { - return $o_is_junction - } - method meta_buffering_supported {} { - return [list line] - } - } - - oo::class create tee_to_var { - variable o_datavars - variable o_trecord - variable o_enc - variable o_encbuf - variable o_is_junction - constructor {tf} { - set o_trecord $tf - set o_enc [tcl::dict::get $tf -encoding] - set o_encbuf "" - set settingsdict [tcl::dict::get $tf -settings] - set varname [tcl::dict::get $settingsdict -varname] - set o_datavars $varname - if {[tcl::dict::exists $tf -junction]} { - set o_is_junction [tcl::dict::get $tf -junction] - } else { - set o_is_junction 0 - } - } - method initialize {ch mode} { - return [list initialize finalize write flush clear] - } - method finalize {ch} { - my destroy - } - method clear {ch} { - return - } - method watch {ch events} { - # must be present but we ignore it because we do not - # post any events - } - #method read {ch count} { - # return ? - #} - #method flush {ch} { - # return "" - #} - method flush {transform_handle} { - #puts stdout "" - #review - just clear o_encbuf and emit nothing? - #we wouldn't have a value there if it was convertable from the channel encoding? - set clear $o_encbuf - set o_encbuf "" - return $o_encbuf - } - method write {ch bytes} { - #test with set x [string repeat " \U1f6c8" 2043] - #or - #test with set x [string repeat " \U1f6c8" 683] - #most windows terminals (at least) may emit two unrecognised chars "??" at the end - - #Our goal with the while loop here is to avoid encoding conversion errors - #the source of the bogus chars in terminals is unclear. - #Alacritty on windows doesn't seem to have the problem, but wezterm,cmd,windows terminal do. - - #set stringdata [tcl::encoding::convertfrom $o_enc $bytes] - set inputbytes $o_encbuf$bytes - set o_encbuf "" - set tail_offset 0 - while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { - incr tail_offset - } - if {$tail_offset > 0} { - if {$tail_offset < [string length $inputbytes]} { - #stringdata from catch statement must be a valid result - set t [expr {$tail_offset - 1}] - set o_encbuf [string range $inputbytes end-$t end] - } else { - set stringdata "" - set o_encbuf $inputbytes - return "" - } - } - - foreach v $o_datavars { - append $v $stringdata - } - #return $bytes - return [string range $inputbytes 0 end-$tail_offset] - } - method meta_is_redirection {} { - return $o_is_junction - } - } - oo::class create tee_to_pipe { - variable o_logsource - variable o_localchan - variable o_enc - variable o_encbuf - variable o_trecord - variable o_is_junction - constructor {tf} { - set o_trecord $tf - set o_enc [tcl::dict::get $tf -encoding] - set o_encbuf "" - set settingsdict [tcl::dict::get $tf -settings] - if {![dict exists $settingsdict -tag]} { - error "tee_to_pipe constructor settingsdict missing -tag" - } - set o_localchan [tcl::dict::get $settingsdict -pipechan] - set o_logsource [tcl::dict::get $settingsdict -tag] - if {[tcl::dict::exists $tf -junction]} { - set o_is_junction [tcl::dict::get $tf -junction] - } else { - set o_is_junction 0 - } - } - method initialize {transform_handle mode} { - return [list initialize read drain write flush clear finalize] - } - method finalize {transform_handle} { - ::shellfilter::log::close $o_logsource - my destroy - } - method watch {transform_handle events} { - # must be present but we ignore it because we do not - # post any events - } - method clear {transform_handle} { - return - } - method drain {transform_handle} { - return "" - } - method read {transform_handle bytes} { - set logdata [tcl::encoding::convertfrom $o_enc $bytes] - #::shellfilter::log::write $o_logsource $logdata - puts -nonewline $o_localchan $logdata - return $bytes - } - method flush {transform_handle} { - #return "" - set clear $o_encbuf - set o_encbuf "" - return $o_encbuf - } - method write {transform_handle bytes} { - #set logdata [tcl::encoding::convertfrom $o_enc $bytes] - set inputbytes $o_encbuf$bytes - set o_encbuf "" - set tail_offset 0 - while {$tail_offset < [::tcl::string::length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [::tcl::string::range $inputbytes 0 end-$tail_offset]} stringdata]} { - incr tail_offset - } - if {$tail_offset > 0} { - if {$tail_offset < [::tcl::string::length $inputbytes]} { - #stringdata from catch statement must be a valid result - set t [expr {$tail_offset - 1}] - set o_encbuf [::tcl::string::range $inputbytes end-$t end] - } else { - set stringdata "" - set o_encbuf $inputbytes - return "" - } - } - #::shellfilter::log::write $o_logsource $logdata - puts -nonewline $o_localchan $stringdata - #return $bytes - return [::tcl::string::range $inputbytes 0 end-$tail_offset] - } - #a tee is not a redirection - because data still flows along the main path - method meta_is_redirection {} { - return $o_is_junction - } - - } - oo::class create tee_to_log { - variable o_tid - variable o_logsource - variable o_trecord - variable o_enc - variable o_encbuf - variable o_is_junction - constructor {tf} { - set o_trecord $tf - set o_enc [tcl::dict::get $tf -encoding] - set o_encbuf "" - set settingsdict [tcl::dict::get $tf -settings] - if {![tcl::dict::exists $settingsdict -tag]} { - error "tee_to_log constructor settingsdict missing -tag" - } - set o_logsource [tcl::dict::get $settingsdict -tag] - set o_tid [::shellfilter::log::open $o_logsource $settingsdict] - if {[tcl::dict::exists $tf -junction]} { - set o_is_junction [tcl::dict::get $tf -junction] - } else { - set o_is_junction 0 - } - } - method initialize {ch mode} { - return [list initialize read write flush finalize] - } - method finalize {ch} { - ::shellfilter::log::close $o_logsource - my destroy - } - method watch {ch events} { - # must be present but we ignore it because we do not - # post any events - } - method read {ch bytes} { - set logdata [tcl::encoding::convertfrom $o_enc $bytes] - ::shellfilter::log::write $o_logsource $logdata - return $bytes - } - method flush {transform_handle} { - #return "" - set clear $o_encbuf - set o_encbuf "" - return $o_encbuf - } - method write {ch bytes} { - #set logdata [tcl::encoding::convertfrom $o_enc $bytes] - set inputbytes $o_encbuf$bytes - set o_encbuf "" - set tail_offset 0 - while {$tail_offset < [::tcl::string::length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [::tcl::string::range $inputbytes 0 end-$tail_offset]} stringdata]} { - incr tail_offset - } - if {$tail_offset > 0} { - if {$tail_offset < [::tcl::string::length $inputbytes]} { - #stringdata from catch statement must be a valid result - set t [expr {$tail_offset - 1}] - set o_encbuf [::tcl::string::range $inputbytes end-$t end] - } else { - set stringdata "" - set o_encbuf $inputbytes - return "" - } - } - set bytes [::tcl::string::range $inputbytes 0 end-$tail_offset] - ::shellfilter::log::write $o_logsource $stringdata - return $bytes - } - method meta_is_redirection {} { - return $o_is_junction - } - } - - - oo::class create logonly { - variable o_tid - variable o_logsource - variable o_trecord - variable o_enc - variable o_encbuf - constructor {tf} { - set o_trecord $tf - set o_enc [dict get $tf -encoding] - set o_encbuf "" - set settingsdict [dict get $tf -settings] - if {![dict exists $settingsdict -tag]} { - error "logonly constructor settingsdict missing -tag" - } - set o_logsource [dict get $settingsdict -tag] - set o_tid [::shellfilter::log::open $o_logsource $settingsdict] - } - method initialize {transform_handle mode} { - return [list initialize finalize write] - } - method finalize {transform_handle} { - ::shellfilter::log::close $o_logsource - my destroy - } - method watch {transform_handle events} { - # must be present but we ignore it because we do not - # post any events - } - #method read {transform_handle count} { - # return ? - #} - method write {transform_handle bytes} { - #set logdata [encoding convertfrom $o_enc $bytes] - set inputbytes $o_encbuf$bytes - set o_encbuf "" - set tail_offset 0 - while {$tail_offset < [::tcl::string::length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [::tcl::string::range $inputbytes 0 end-$tail_offset]} stringdata]} { - incr tail_offset - } - if {$tail_offset > 0} { - if {$tail_offset < [::tcl::string::length $inputbytes]} { - #stringdata from catch statement must be a valid result - set t [expr {$tail_offset - 1}] - set o_encbuf [::tcl::string::range $inputbytes end-$t end] - } else { - set stringdata "" - set o_encbuf $inputbytes - return - } - } - - #::shellfilter::log::write_sync $o_logsource $logdata - ::shellfilter::log::write $o_logsource $stringdata - return - } - method meta_is_redirection {} { - return 1 - } - } - - #review - we should probably provide a more narrow filter than only strips color - and one that strips most(?) - # - but does it ever really make sense to strip things like "esc(0" and "esc(B" which flip to the G0 G1 characters? (once stripped - things like box-lines become ordinary letters - unlikely to be desired?) - #punk::ansi::ansistrip converts at least some of the box drawing G0 chars to unicode - todo - more complete conversion - #assumes line-buffering. a more advanced filter required if ansicodes can arrive split across separate read or write operations! - oo::class create ansistrip { - variable o_trecord - variable o_enc - variable o_encbuf - variable o_is_junction - constructor {tf} { - package require punk::ansi - set o_trecord $tf - set o_enc [::tcl::dict::get $tf -encoding] - set o_encbuf "" - if {[::tcl::dict::exists $tf -junction]} { - set o_is_junction [::tcl::dict::get $tf -junction] - } else { - set o_is_junction 0 - } - } - method initialize {transform_handle mode} { - return [list initialize read write clear flush drain finalize] - } - method finalize {transform_handle} { - my destroy - } - method clear {transform_handle} { - return - } - method watch {transform_handle events} { - } - method drain {transform_handle} { - return "" - } - method read {transform_handle bytes} { - set instring [encoding convertfrom $o_enc $bytes] - set outstring [punk::ansi::ansistrip $instring] - return [encoding convertto $o_enc $outstring] - } - method flush {transform_handle} { - return "" - } - #method write {transform_handle bytes} { - # #broken due to occasional unexpected byte sequence - # set instring [encoding convertfrom $o_enc $bytes] - # set outstring [punk::ansi::ansistrip $instring] - # return [encoding convertto $o_enc $outstring] - #} - method write {transform_handle bytes} { - #set instring [tcl::encoding::convertfrom $o_enc $bytes] ;naive approach will break due to unexpected byte sequence - occasionally - #bytes can break at arbitrary points making encoding conversions invalid. - - set inputbytes $o_encbuf$bytes - set o_encbuf "" - set tail_offset 0 - while {$tail_offset < [::tcl::string::length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [::tcl::string::range $inputbytes 0 end-$tail_offset]} stringdata]} { - incr tail_offset - } - if {$tail_offset > 0} { - if {$tail_offset < [::tcl::string::length $inputbytes]} { - #stringdata from catch statement must be a valid result - set t [expr {$tail_offset - 1}] - set o_encbuf [::tcl::string::range $inputbytes end-$t end] - } else { - set stringdata "" - set o_encbuf $inputbytes - return "" - } - } - - set outstring [punk::ansi::ansistrip $stringdata] - return [tcl::encoding::convertto $o_enc $outstring] - } - method meta_is_redirection {} { - return $o_is_junction - } - } - - #a test - oo::class create reconvert { - variable o_trecord - variable o_enc - constructor {tf} { - set o_trecord $tf - set o_enc [dict get $tf -encoding] - } - method initialize {transform_handle mode} { - return [list initialize read write finalize] - } - method finalize {transform_handle} { - my destroy - } - method watch {transform_handle events} { - } - method read {transform_handle bytes} { - set instring [encoding convertfrom $o_enc $bytes] - - set outstring $instring - - return [encoding convertto $o_enc $outstring] - } - method write {transform_handle bytes} { - set instring [encoding convertfrom $o_enc $bytes] - - set outstring $instring - - return [encoding convertto $o_enc $outstring] - } - } - oo::define reconvert { - method meta_is_redirection {} { - return 0 - } - } - - - #this isn't a particularly nice thing to do to a stream - especially if someone isn't expecting ansi codes sprinkled through it. - #It can be useful for test/debugging - #Due to chunking at random breaks - we have to check if an ansi code in the underlying stream has been split - otherwise our wrapping will break the existing ansi - # - set sixelstart_re {\x1bP([;0-9]*)q} ;#7-bit - todo 8bit - #todo kitty graphics \x1b_G... - #todo iterm graphics - - oo::class create ansiwrap { - variable o_trecord - variable o_enc - variable o_encbuf ;#buffering for partial encoding bytes - variable o_colour - variable o_do_colour - variable o_do_colourlist - variable o_do_normal - variable o_is_junction - variable o_codestack - variable o_gx_state ;#on/off alt graphics - variable o_buffered ;#buffering for partial ansi codes - constructor {tf} { - package require punk::ansi - set o_trecord $tf - set o_enc [tcl::dict::get $tf -encoding] - set settingsdict [tcl::dict::get $tf -settings] - if {[tcl::dict::exists $settingsdict -colour]} { - set o_colour [tcl::dict::get $settingsdict -colour] - #warning - we can't merge certain extended attributes such as undercurly into single SGR escape sequence - #while some terminals may handle these extended attributes even when merged - we need to cater for those that - #don't. Keeping them as a separate escape allows terminals that don't handle them to ignore just that code without - #affecting the interpretation of the other codes. - set o_do_colour [punk::ansi::a+ {*}$o_colour] - set o_do_colourlist [punk::ansi::ta::get_codes_single $o_do_colour] - set o_do_normal [punk::ansi::a] - } else { - set o_colour {} - set o_do_colour "" - set o_do_colourlist {} - set o_do_normal "" - } - set o_codestack [list] - set o_gx_state [expr {off}] - set o_encbuf "" - set o_buffered "" ;#hold back data that potentially contains partial ansi codes - if {[tcl::dict::exists $tf -junction]} { - set o_is_junction [tcl::dict::get $tf -junction] - } else { - set o_is_junction 0 - } - } - - - #todo - track when in sixel,iterm,kitty graphics data - can be very large - method Trackcodes {chunk} { - #note - caller can use 2 resets in a single unit to temporarily reset to no sgr (override ansiwrap filter) - #e.g [a+ reset reset] (0;0m vs 0;m) - - #puts stdout "===[ansistring VIEW -lf 1 $o_buffered]" - set buf $o_buffered$chunk - set emit "" - #Note 8-bit csi \x9b has already been mapped in the chunk to 7-bit form \x1b\[ by the caller - so we only need to check for \x1b here - #(under review - ideally we might not want to normalize 8-bit to 7-bit in a channel transform)) - if {[string last \x1b $buf] >= 0} { - #detect will detect ansi SGR and gron groff and other codes - #REVIEW - ta::detect won't detect SOS without paired ST for things like PM - # ta::detectcode will - but then split_codes_single will treat unpaired SOS as text? - if {[punk::ansi::ta::detect $buf]} { - #split_codes_single regex faster than split_codes - but more resulting parts - #'single' refers to number of escapes - but can still contain e.g multiple SGR codes (or mode set operations etc) - set parts [punk::ansi::ta::split_codes_single $buf] - #process all pt/code pairs except for trailing pt - foreach {pt code} [lrange $parts 0 end-1] { - #puts "<==[ansistring VIEW -lf 1 $pt]==>" - switch -- [llength $o_codestack] { - 0 { - append emit $o_do_colour$pt$o_do_normal - } - 1 { - if {[punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]} { - append emit $o_do_colour$pt$o_do_normal - set o_codestack [list] - } else { - #append emit [lindex $o_codestack 0]$pt - append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$pt - } - } - default { - append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$pt - } - } - #if {( ![llength $o_codestack] || ([llength $o_codestack] == 1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]))} { - # append emit $o_do_colour$pt$o_do_normal - # #append emit $pt - #} else { - # append emit $pt - #} - - set c1c2 [tcl::string::range $code 0 1] - set leadernorm [tcl::string::range [tcl::string::map [list {*}{ - \x1b\[ 7CSI - \x9b 8CSI - \x1b\( 7GFX - }] $c1c2] 0 3] - switch -- $leadernorm { - 7CSI - 8CSI { - set code_endswith_m [expr {[tcl::string::index $code end] eq "m"}] - if {$code_endswith_m} { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - #review this normalizing of reset to a single form. - set o_codestack [list "\x1b\[m"] - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set o_codestack [list $code] - } elseif {[punk::ansi::codetype::is_sgr $code]} { - #todo - make caching is_sgr method - set dup_posns [lsearch -all -exact $o_codestack $code] - set o_codestack [lremove $o_codestack {*}$dup_posns] - lappend o_codestack $code - } - } - - } - 7GFX { - switch -- [tcl::string::index $code 2] { - "0" { - set o_gx_state on - } - "B" { - set o_gx_state off - } - } - } - default { - #other ansi codes - } - } - append emit $code - } - - - set trailing_pt [lindex $parts end] - if {[string first \x1b $trailing_pt] >= 0} { - #puts stdout "...[ansistring VIEW -lf 1 $trailing_pt]...buffered:<[ansistring VIEW $o_buffered]> '[ansistring VIEW -lf 1 $emit]'" - #may not be plaintext after all - set o_buffered $trailing_pt - #puts stdout "=-=[ansistring VIEWCODES $o_buffered]" - } else { - #puts [a+ yellow]???[ansistring VIEW "'$o_buffered'<+>'$trailing_pt'"]???[a] - switch -- [llength $o_codestack] { - 0 { - append emit $o_do_colour$trailing_pt$o_do_normal - } - 1 { - if {[punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]} { - append emit $o_do_colour$trailing_pt$o_do_normal - set o_codestack [list] - } else { - #append emit [lindex $o_codestack 0]$trailing_pt - append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$trailing_pt - } - } - default { - append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$trailing_pt - } - } - #if {![llength $o_codestack] || ([llength $o_codestack] ==1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]])} { - # append emit $o_do_colour$trailing_pt$o_do_normal - #} else { - # append emit $trailing_pt - #} - #the previous o_buffered formed the data we emitted - nothing new to buffer because we emitted all parts including the trailing plaintext - set o_buffered "" - } - - - } else { - #REVIEW - this holding a buffer without emitting as we go is ugly. - # - we may do better to detect and retain the opener, then use that opener to avoid false splits within the sequence. - # - we'd then need to detect the appropriate close to restart splitting and codestacking - # - we may still need to retain and append the data to the opener (in some cases?) - which is a slight memory issue - but at least we would emit everything immediately. - - - #puts "-->esc but no detect" - #no complete ansi codes - but at least one esc is present - if {[string index $buf end] eq "\x1b" && [string first \x1b $buf] == [string length $buf]-1} { - #string index in first part of && clause to avoid some unneeded scans of whole string for this test - #we can't use 'string last' - as we need to know only esc is last char in buf - #puts ">>trailing-esc<<" - set o_buffered \x1b - set emit $o_do_colour[string range $buf 0 end-1]$o_do_normal - #set emit [string range $buf 0 end-1] - set buf "" - } else { - set emit_anyway 0 - #todo - ensure non-ansi escapes in middle of chunks don't lead to ever growing buffer - if {[punk::ansi::ta::detect_st_open $buf]} { - #no detect - but we have an ST open (privacy msg etc) - allow a larger chunk before we give up - could include newlines (and even nested codes - although not widely interpreted that way in terms) - set st_partial_len [expr {[string length $buf] - [string last \x1b $buf]}] ;#length of unclosed ST code - #todo - configurable ST max - use 1k for now - if {$st_partial_len < 1001} { - append o_buffered $chunk - set emit "" - set buf "" - } else { - set emit_anyway 1 - set o_buffered "" - } - } else { - set possible_code_len [expr {[string length $buf] - [string last \x1b $buf]}] ;#length of possible code - #most opening sequences are 1,2 or 3 chars - review? - set open_sequence_detected [punk::ansi::ta::detect_open $buf] - if {$possible_code_len > 10 && !$open_sequence_detected} { - set emit_anyway 1 - set o_buffered "" - } else { - #could be composite sequence with params - allow some reasonable max sequence length - #todo - configurable max sequence length - #len 40-50 quite possible for SGR sequence using coloured underlines etc, even without redundancies - # - allow some headroom for redundant codes when the caller didn't merge. - if {$possible_code_len < 101} { - append o_buffered $chunk - set buf "" - set emit "" - } else { - #allow a little more grace if we at least have an opening ansi sequence of any type.. - if {$open_sequence_detected && $possible_code_len < 151} { - append o_buffered $chunk - set buf "" - set emit "" - } else { - set emit_anyway 1 - set o_buffered "" - } - } - } - } - if {$emit_anyway} { - #assert: any time emit_anyway == 1 buf already contains all of previous o_buffered and o_buffered has been cleared. - - #looked ansi-like - but we've given enough length without detecting close.. - #treat as possible plain text with some esc or unrecognised ansi sequence - switch -- [llength $o_codestack] { - 0 { - set emit $o_do_colour$buf$o_do_normal - } - 1 { - if {[punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]} { - set emit $o_do_colour$buf$o_do_normal - set o_codestack [list] - } else { - #set emit [lindex $o_codestack 0]$buf - set emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$buf - } - } - default { - #set emit [punk::ansi::codetype::sgr_merge_singles $o_codestack]$buf - set emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$buf - } - } - #if {( ![llength $o_codestack] || ([llength $o_codestack] == 1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]))} { - # set emit $o_do_colour$buf$o_do_normal - #} else { - # set emit $buf - #} - } - } - } - } else { - #no esc - #puts stdout [a+ yellow]...[a] - #test! - switch -- [llength $o_codestack] { - 0 { - set emit $o_do_colour$buf$o_do_normal - } - 1 { - if {[punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]} { - set emit $o_do_colour$buf$o_do_normal - set o_codestack [list] - } else { - #set emit [lindex $o_codestack 0]$buf - set emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$buf - } - } - default { - #set emit [punk::ansi::codetype::sgr_merge_singles $o_codestack]$buf - set emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$buf - } - } - set o_buffered "" - } - return [dict create emit $emit stacksize [llength $o_codestack]] - } - method initialize {transform_handle mode} { - #clear undesirable in terminal output channels (review) - return [list initialize write flush read drain finalize] - } - method finalize {transform_handle} { - my destroy - } - method watch {transform_handle events} { - } - method clear {transform_handle} { - #In the context of stderr/stdout - we probably don't want clear to run. - #Terminals might call it in the middle of a split ansi code - resulting in broken output. - #Leave clear of it the init call - puts stdout "" - set emit [tcl::encoding::convertto $o_enc $o_buffered] - set o_buffered "" - return $emit - } - method flush {transform_handle} { - #puts stdout "" - set inputbytes $o_buffered$o_encbuf - set emit [tcl::encoding::convertto $o_enc $inputbytes] - set o_buffered "" - set o_encbuf "" - return $emit - } - method write {transform_handle bytes} { - #set instring [tcl::encoding::convertfrom $o_enc $bytes] ;naive approach will break due to unexpected byte sequence - occasionally - #bytes can break at arbitrary points making encoding conversions invalid. - - set inputbytes $o_encbuf$bytes - set o_encbuf "" - set tail_offset 0 - while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { - incr tail_offset - } - if {$tail_offset > 0} { - if {$tail_offset < [string length $inputbytes]} { - #stringdata from catch statement must be a valid result - set t [expr {$tail_offset - 1}] - set o_encbuf [string range $inputbytes end-$t end] - } else { - set stringdata "" - set o_encbuf $inputbytes - return "" - } - } - - #------------------------------------------------------ - # REVIEW - #Trackcodes logic is primarily designed for 7-bit codes - #It would be complex for it to support 8-bit as well - #- we can do a simple pre-map to convert 8-bit CSI to 7-bit CSI before processing - #we already normalize things like resets to a single 7-bit form anyway. - #review - is there a need for an ansiwrap channel that preserves 8-bit codes? - #8-bit are rarely used these days - and many terminals don't support them. - #We could take the view here that we should understand them but not emit them in general. - #Nonetheless - converting them on a channel transform like this is potentially suprising in some circumstances, - #and we don't necessarily know the intent of both the producer and consumer of the stream. - set stringdata [string map [list \x9b \x1b\[ ] $stringdata] - #------------------------------------------------------ - - set streaminfo [my Trackcodes $stringdata] - set emit [dict get $streaminfo emit] - - #review - wrapping already done in Trackcodes - #if {[dict get $streaminfo stacksize] == 0} { - # #no ansi on the stack - we can wrap - # #review - # set outstring "$o_do_colour$emit$o_do_normal" - #} else { - #} - #if {[llength $o_codestack]} { - # set outstring [punk::ansi::codetype::sgr_merge_singles $o_codestack]$emit - #} else { - # set outstring $emit - #} - #set outstring $emit - - #puts stdout "decoded >>>[ansistring VIEWCODES $outstring]<<<" - #puts stdout "re-encoded>>>[ansistring VIEW [tcl::encoding::convertto $o_enc $outstring]]<<<" - return [tcl::encoding::convertto $o_enc $emit] - } - method Write_naive {transform_handle bytes} { - set instring [tcl::encoding::convertfrom $o_enc $bytes] - set outstring "$o_do_colour$instring$o_do_normal" - #set outstring ">>>$instring" - return [tcl::encoding::convertto $o_enc $outstring] - } - method drain {transform_handle} { - return "" - } - method read {transform_handle bytes} { - set instring [tcl::encoding::convertfrom $o_enc $bytes] - set outstring "$o_do_colour$instring$o_do_normal" - return [tcl::encoding::convertto $o_enc $outstring] - } - method meta_is_redirection {} { - return $o_is_junction - } - } - #todo - something - oo::class create rebuffer { - variable o_trecord - variable o_enc - constructor {tf} { - set o_trecord $tf - set o_enc [dict get $tf -encoding] - } - method initialize {transform_handle mode} { - return [list initialize read write finalize] - } - method finalize {transform_handle} { - my destroy - } - method watch {transform_handle events} { - } - method read {transform_handle bytes} { - set instring [encoding convertfrom $o_enc $bytes] - - set outstring $instring - - return [encoding convertto $o_enc $outstring] - } - method write {transform_handle bytes} { - set instring [encoding convertfrom $o_enc $bytes] - - #set outstring [string map [list \n ] $instring] - set outstring $instring - - return [encoding convertto $o_enc $outstring] - #return [encoding convertto utf-16le $outstring] - } - } - oo::define rebuffer { - method meta_is_redirection {} { - return 0 - } - } - - #has slight buffering/withholding of lone training cr - we can't be sure that a cr at end of chunk is part of \r\n sequence - oo::class create tounix { - variable o_trecord - variable o_enc - variable o_last_char_was_cr - variable o_is_junction - constructor {tf} { - set o_trecord $tf - set o_enc [dict get $tf -encoding] - set settingsdict [dict get $tf -settings] - if {[dict exists $tf -junction]} { - set o_is_junction [dict get $tf -junction] - } else { - set o_is_junction 0 - } - set o_last_char_was_cr 0 - } - method initialize {transform_handle mode} { - return [list initialize write finalize] - } - method finalize {transform_handle} { - my destroy - } - method watch {transform_handle events} { - } - #don't use read - method read {transform_handle bytes} { - set instring [encoding convertfrom $o_enc $bytes] - - set outstring $instring - - return [encoding convertto $o_enc $outstring] - } - method write {transform_handle bytes} { - set instring [encoding convertfrom $o_enc $bytes] - #set outstring [string map [list \n ] $instring] - - if {$o_last_char_was_cr} { - set instring "\r$instring" - } - - set outstring [string map {\r\n \n} $instring] - set lastchar [string range $outstring end end] - if {$lastchar eq "\r"} { - set o_last_char_was_cr 1 - set outstring [string range $outstring 0 end-1] - } else { - set o_last_char_was_cr 0 - } - #review! can we detect eof here on the transform_handle? - #if eof, we don't want to strip a trailing \r - - return [encoding convertto $o_enc $outstring] - #return [encoding convertto utf-16le $outstring] - } - } - oo::define tounix { - method meta_is_redirection {} { - return $o_is_junction - } - } - #write to handle case where line-endings already \r\n too - oo::class create towindows { - variable o_trecord - variable o_enc - variable o_last_char_was_cr - variable o_is_junction - constructor {tf} { - set o_trecord $tf - set o_enc [dict get $tf -encoding] - set settingsdict [dict get $tf -settings] - if {[dict exists $tf -junction]} { - set o_is_junction [dict get $tf -junction] - } else { - set o_is_junction 0 - } - set o_last_char_was_cr 0 - } - method initialize {transform_handle mode} { - return [list initialize write finalize] - } - method finalize {transform_handle} { - my destroy - } - method watch {transform_handle events} { - } - #don't use read - method read {transform_handle bytes} { - set instring [encoding convertfrom $o_enc $bytes] - - set outstring $instring - - return [encoding convertto $o_enc $outstring] - } - method write {transform_handle bytes} { - set instring [encoding convertfrom $o_enc $bytes] - #set outstring [string map [list \n ] $instring] - - if {$o_last_char_was_cr} { - set instring "\r$instring" - } - - set outstring [string map {\r\n \uFFFF} $instring] - set outstring [string map {\n \r\n} $outstring] - set outstring [string map {\uFFFF \r\n} $outstring] - - set lastchar [string range $outstring end end] - if {$lastchar eq "\r"} { - set o_last_char_was_cr 1 - set outstring [string range $outstring 0 end-1] - } else { - set o_last_char_was_cr 0 - } - #review! can we detect eof here on the transform_handle? - #if eof, we don't want to strip a trailing \r - - return [encoding convertto $o_enc $outstring] - #return [encoding convertto utf-16le $outstring] - } - } - oo::define towindows { - method meta_is_redirection {} { - return $o_is_junction - } - } - - } -} - -# ---------------------------------------------------------------------------- -#review float/sink metaphor. -#perhaps something with the concept of upstream and downstream? -#need concepts for push towards data, sit in middle where placed, and lag at tail of data stream. -## upstream for stdin is at the bottom of the stack and for stdout is the top of the stack. -#upstream,neutral-upstream,downstream,downstream-aside,downstream-replace (default neutral-upstream - require action 'stack' to use standard channel stacking concept and ignore other actions) -#This is is a bit different from the float/sink metaphor which refers to the channel stacking order as opposed to the data-flow direction. -#The idea would be that whether input or output -# upstream additions go to the side closest to the datasource -# downstream additions go furthest from the datasource -# - all new additions go ahead of any diversions as the most upstream diversion is the current end of the stream in a way. -# - this needs review regarding subsequent removal of the diversion and whether filters re-order in response.. -# or if downstream & neutral additions are reclassified upon insertion if they land among existing upstreams(?) -# neutral-upstream goes to the datasource side of the neutral-upstream list. -# No 'neutral' option provided so that we avoid the need to think forwards or backwards when adding stdin vs stdout shellfilter does the necessary pop/push reordering. -# No 'neutral-downstream' to reduce complexity. -# downstream-replace & downstream-aside head downstream to the first diversion they encounter. ie these actions are no longer referring to the stack direction but only the dataflow direction. -# -# ---------------------------------------------------------------------------- -# -# 'filters' are transforms that don't redirect -# - limited range of actions to reduce complexity. -# - any requirement not fulfilled by float,sink,sink-replace,sink-sideline should be done by multiple pops and pushes -# -#actions can float to top of filters or sink to bottom of filters -#when action is of type sink, it can optionally replace or sideline the first non-filter it encounters (highest redirection on the stack.. any lower are starved of the stream anyway) -# - sideline means to temporarily replace the item and keep a record, restoring if/when we are removed from the transform stack -# -##when action is of type float it can't replace or sideline anything. A float is added above any existing floats and they stay in the same order relative to each other, -#but non-floats added later will sit below all floats. -#(review - float/sink initially designed around output channels. For stdin the dataflow is reversed. implement float-aside etc?) -# -# -#action: float sink sink-replace,sink-sideline -# -# -## note - whether stack is for input or output we maintain it in the same direction - which is in sync with the tcl chan pop chan push concept. -## -namespace eval shellfilter::stack { - namespace export {[a-z]*} - namespace ensemble create - #todo - implement as oo ? - variable pipelines [list] - - proc items {} { - #review - stdin,stdout,stderr act as pre-existing pipelines, and we can't create a new one with these names - so they should probably be autoconfigured and listed.. - # - but in what contexts? only when we find them in [chan names]? - variable pipelines - return [dict keys $pipelines] - } - proc item {pipename} { - variable pipelines - return [dict get $pipelines $pipename] - } - proc item_tophandle {pipename} { - variable pipelines - set handle "" - if {[dict exists $pipelines $pipename stack]} { - set stack [dict get $pipelines $pipename stack] - set topstack [lindex $stack end] ;#last item in stack is top (for output channels anyway) review comment. input chans? - if {$topstack ne ""} { - if {[dict exists $topstack -handle]} { - set handle [dict get $topstack -handle] - } - } - } - return $handle - } - proc status {{pipename *} args} { - variable pipelines - set pipecount [dict size $pipelines] - set tabletitle "$pipecount pipelines active" - set t [textblock::class::table new $tabletitle] - $t add_column -headers [list channel-ident] - $t add_column -headers [list device-info localchan] - $t configure_column 1 -header_colspans {3} - $t add_column -headers [list "" remotechan] - $t add_column -headers [list "" tid] - $t add_column -headers [list stack-info] - foreach k [dict keys $pipelines $pipename] { - set lc [dict get $pipelines $k device localchan] - set rc [dict get $pipelines $k device remotechan] - if {[dict exists $k device workertid]} { - set tid [dict get $pipelines $k device workertid] - } else { - set tid "-" - } - set stack [dict get $pipelines $k stack] - if {![llength $stack]} { - set stackinfo "" - } else { - set tbl_inner [textblock::class::table new] - $tbl_inner configure -show_edge 0 - foreach rec $stack { - set handle [punk::lib::dict_getdef $rec -handle ""] - set id [punk::lib::dict_getdef $rec -id ""] - set transform [namespace tail [punk::lib::dict_getdef $rec -transform ""]] - set settings [punk::lib::dict_getdef $rec -settings ""] - $tbl_inner add_row [list $id $transform $handle $settings] - } - set stackinfo [$tbl_inner print] - $tbl_inner destroy - } - $t add_row [list $k $lc $rc $tid $stackinfo] - } - set result [$t print] - $t destroy - return $result - } - proc status1 {{pipename *} args} { - variable pipelines - - set pipecount [dict size $pipelines] - set tableprefix "$pipecount pipelines active\n" - foreach p [dict keys $pipelines] { - append tableprefix " " $p \n - } - package require overtype - #todo -verbose - set table "" - set ac1 [string repeat " " 15] - set ac2 [string repeat " " 42] - set ac3 [string repeat " " 70] - append table "[overtype::left $ac1 channel-ident] " - append table "[overtype::left $ac2 device-info] " - append table "[overtype::left $ac3 stack-info]" - append table \n - - - set bc1 [string repeat " " 5] ;#stack id - set bc2 [string repeat " " 25] ;#transform - set bc3 [string repeat " " 50] ;#settings - - foreach k [dict keys $pipelines $pipename] { - set lc [dict get $pipelines $k device localchan] - if {[dict exists $k device workertid]} { - set tid [dict get $pipelines $k device workertid] - } else { - set tid "" - } - - - set col1 [overtype::left $ac1 $k] - set col2 [overtype::left $ac2 "localchan: $lc tid:$tid"] - - set stack [dict get $pipelines $k stack] - if {![llength $stack]} { - set col3 $ac3 - } else { - set rec [lindex $stack 0] - set bcol1 [overtype::left $bc1 [dict get $rec -id]] - set bcol2 [overtype::left $bc2 [namespace tail [dict get $rec -transform]]] - set bcol3 [overtype::left $bc3 [dict get $rec -settings]] - set stackrow "$bcol1 $bcol2 $bcol3" - set col3 [overtype::left $ac3 $stackrow] - } - - append table "$col1 $col2 $col3\n" - - - foreach rec [lrange $stack 1 end] { - set col1 $ac1 - set col2 $ac2 - if {[llength $rec]} { - set bc1 [overtype::left $bc1 [dict get $rec -id]] - set bc2 [overtype::left $bc2 [namespace tail [dict get $rec -transform]]] - set bc3 [overtype::left $bc3 [dict get $rec -settings]] - set stackrow "$bc1 $bc2 $bc3" - set col3 [overtype::left $ac3 $stackrow] - } else { - set col3 $ac3 - } - append table "$col1 $col2 $col3\n" - } - - } - return $tableprefix$table - } - #used for output channels - we usually want to sink redirections below the floaters and down to topmost existing redir - proc _get_stack_floaters {stack} { - set floaters [list] - foreach t [lreverse $stack] { - switch -- [dict get $t -action] { - float { - lappend floaters $t - } - default { - break - } - } - } - return [lreverse $floaters] - } - - - - #for output-channel sinking - proc _get_stack_top_redirection {stack} { - set r 0 ;#reverse index - foreach t [lreverse $stack] { - set obj [dict get $t -obj] - if {[$obj meta_is_redirection]} { - set idx [expr {[llength $stack] - ($r + 1) }] ;#forward index - return [list index $idx record $t] - } - incr r - } - #not found - return [list index -1 record {}] - } - #exclude float-locked, locked, sink-locked - proc _get_stack_top_redirection_replaceable {stack} { - set r 0 ;#reverse index - foreach t [lreverse $stack] { - set action [dict get $t -action] - if {![string match "*locked*" $action]} { - set obj [dict get $t -obj] - if {[$obj meta_is_redirection]} { - set idx [expr {[llength $stack] - ($r + 1) }] ;#forward index - return [list index $idx record $t] - } - } - incr r - } - #not found - return [list index -1 record {}] - } - - - #for input-channels ? - proc _get_stack_bottom_redirection {stack} { - set i 0 - foreach t $stack { - set obj [dict get $t -obj] - if {[$obj meta_is_redirection]} { - return [linst index $i record $t] - } - incr i - } - #not found - return [list index -1 record {}] - } - - - proc get_next_counter {pipename} { - variable pipelines - #use dictn incr ? - set counter [dict get $pipelines $pipename counter] - incr counter - dict set pipelines $pipename counter $counter - return $counter - } - - proc unwind {pipename} { - variable pipelines - set stack [dict get $pipelines $pipename stack] - set localchan [dict get $pipelines $pipename device localchan] - foreach tf [lreverse $stack] { - chan pop $localchan - } - dict set pipelines $pipename [list] - } - #todo - proc delete {pipename {wait 0}} { - variable pipelines - set pipeinfo [dict get $pipelines $pipename] - set deviceinfo [dict get $pipeinfo device] - set localchan [dict get $deviceinfo localchan] - unwind $pipename - - #release associated thread - set tid [dict get $deviceinfo workertid] - if {$wait} { - thread::release -wait $tid - } else { - thread::release $tid - } - - #Memchan closes without error - tcl::chan::fifo2 raises something like 'can not find channel named "rc977"' - REVIEW. why? - catch {chan close $localchan} - } - #review - proc name clarity is questionable. remove_stackitem? - proc remove {pipename remove_id} { - variable pipelines - if {![dict exists $pipelines $pipename]} { - puts stderr "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]" - return - } - set stack [dict get $pipelines $pipename stack] - set localchan [dict get $pipelines $pipename device localchan] - set posn 0 - set idposn -1 - set asideposn -1 - foreach t $stack { - set id [dict get $t -id] - if {$id eq $remove_id} { - set idposn $posn - break - } - #look into asides (only can be one for now) - if {[llength [dict get $t -aside]]} { - set a [dict get $t -aside] - if {[dict get $a -id] eq $remove_id} { - set asideposn $posn - break - } - } - incr posn - } - - if {$asideposn > 0} { - #id wasn't found directly in stack, but in an -aside. we don't need to pop anything - just clear this aside record - set container [lindex $stack $asideposn] - dict set container -aside {} - lset stack $asideposn $container - dict set pipelines $pipename stack $stack - } else { - if {$idposn < 0} { - ::shellfilter::log::write shellfilter "ERROR shellfilter::stack::remove $pipename id '$remove_id' not found" - puts stderr "|WARNING>shellfilter::stack::remove $pipename id '$remove_id' not found" - return 0 - } - set removed_item [lindex $stack $idposn] - - #include idposn in poplist - set poplist [lrange $stack $idposn end] - #set stack [lreplace $stack $idposn end] - set stack [lreplace $stack[set stack {}] $idposn end] - #pop all chans before adding anything back in! - foreach p $poplist { - chan pop $localchan - } - - if {[llength [dict get $removed_item -aside]]} { - set restore [dict get $removed_item -aside] - set t [dict get $restore -transform] - set tsettings [dict get $restore -settings] - set obj [$t new $restore] - set h [chan push $localchan $obj] - dict set restore -handle $h - dict set restore -obj $obj - lappend stack $restore - } - - #put popped back except for the first one, which we want to remove - foreach p [lrange $poplist 1 end] { - set t [dict get $p -transform] - set tsettings [dict get $p -settings] - set obj [$t new $p] - set h [chan push $localchan $obj] - dict set p -handle $h - dict set p -obj $obj - lappend stack $p - } - dict set pipelines $pipename stack $stack - } - #JMNJMN 2025 review! - #show_pipeline $pipename -note "after_remove $remove_id" - return 1 - } - - #pop a number of items of the top of the stack, add our transform record, and add back all (or the tail of poplist if pushstartindex > 0) - proc insert_transform {pipename stack transformrecord poplist {pushstartindex 0}} { - variable pipelines - set bottom_pop_posn [expr {[llength $stack] - [llength $poplist]}] - set poplist [lrange $stack $bottom_pop_posn end] - #set stack [lreplace $stack $bottom_pop_posn end] - set stack [lreplace $stack[set stack {}] $bottom_pop_posn end] - - set localchan [dict get $pipelines $pipename device localchan] - foreach p [lreverse $poplist] { - chan pop $localchan - } - set transformname [dict get $transformrecord -transform] - set transformsettings [dict get $transformrecord -settings] - set obj [$transformname new $transformrecord] - set h [chan push $localchan $obj] - dict set transformrecord -handle $h - dict set transformrecord -obj $obj - dict set transformrecord -note "insert_transform" - lappend stack $transformrecord - foreach p [lrange $poplist $pushstartindex end] { - set t [dict get $p -transform] - set tsettings [dict get $p -settings] - set obj [$t new $p] - set h [chan push $localchan $obj] - #retain previous -id - code that added it may have kept reference and not expecting it to change - dict set p -handle $h - dict set p -obj $obj - dict set p -note "re-added" - - lappend stack $p - } - return $stack - } - - #fifo2 - proc new {pipename args} { - variable pipelines - if {($pipename in [dict keys $pipelines]) || ($pipename in [chan names])} { - error "shellfilter::stack::new error: pipename '$pipename' already exists" - } - - set opts [dict merge {-settings {}} $args] - set defaultsettings [dict create -raw 1 -buffering line -direction out] - set targetsettings [dict merge $defaultsettings [dict get $opts -settings]] - - set direction [dict get $targetsettings -direction] - - #pipename is the source/facility-name ? - if {$direction eq "out"} { - set pipeinfo [shellfilter::pipe::open_out $pipename $targetsettings] - } else { - puts stderr "|jn> pipe::open_in $pipename $targetsettings" - set pipeinfo [shellfilter::pipe::open_in $pipename $targetsettings] - } - #open_out/open_in will configure buffering based on targetsettings - - set program_chan [dict get $pipeinfo localchan] - set worker_chan [dict get $pipeinfo remotechan] - set workertid [dict get $pipeinfo workertid] - - - set deviceinfo [dict create pipename $pipename localchan $program_chan remotechan $worker_chan workertid $workertid direction $direction] - dict set pipelines $pipename [list counter 0 device $deviceinfo stack [list]] - - return $deviceinfo - } - #we 'add' rather than 'push' because transforms can float,sink and replace/sideline so they don't necessarily go to the top of the transform stack - proc add {pipename transformname args} { - variable pipelines - #chan names doesn't reflect available channels when transforms are in place - #e.g stdout may exist but show as something like file191f5b0dd80 - if {($pipename ni [dict keys $pipelines])} { - if {[catch {eof $pipename} is_eof]} { - error "shellfilter::stack::add no existing chan or pipename matching '$pipename' in channels:[chan names] or pipelines:$pipelines use stdin/stderr/stdout or shellfilter::stack::new " - } - } - set args [dict merge {-action "" -settings {}} $args] - set action [dict get $args -action] - set transformsettings [dict get $args -settings] - if {[string first "::" $transformname] < 0} { - set transformname ::shellfilter::chan::$transformname - } - if {![llength [info commands $transformname]]} { - error "shellfilter::stack::push unknown transform '$transformname'" - } - - - if {![dict exists $pipelines $pipename]} { - #pipename must be in chan names - existing device/chan - #record a -read and -write end even if the device is only being used as one or the other - set deviceinfo [dict create pipename $pipename localchan $pipename remotechan {}] - dict set pipelines $pipename [list counter 0 device $deviceinfo stack [list]] - } else { - set deviceinfo [dict get $pipelines $pipename device] - } - - set id [get_next_counter $pipename] - set stack [dict get $pipelines $pipename stack] - set localchan [dict get $deviceinfo localchan] - - #we redundantly store chan in each transform - makes debugging clearer - # -encoding similarly could be stored only at the pipeline level (or even queried directly each filter-read/write), - # but here it may help detect unexpected changes during lifetime of the stack and avoids the chance of callers incorrectly using the transform handle?) - # jn - set transform_record [list -id $id -chan $pipename -encoding [chan configure $localchan -encoding] -transform $transformname -aside {} {*}$args] - switch -glob -- $action { - float - float-locked { - set obj [$transformname new $transform_record] - set h [chan push $localchan $obj] - dict set transform_record -handle $h - dict set transform_record -obj $obj - lappend stack $transform_record - } - "" - locked { - set floaters [_get_stack_floaters $stack] - if {![llength $floaters]} { - set obj [$transformname new $transform_record] - set h [chan push $localchan $obj] - dict set transform_record -handle $h - dict set transform_record -obj $obj - lappend stack $transform_record - } else { - set poplist $floaters - set stack [insert_transform $pipename $stack $transform_record $poplist] - } - } - "sink*" { - set redirinfo [_get_stack_top_redirection $stack] - set idx_existing_redir [dict get $redirinfo index] - if {$idx_existing_redir == -1} { - #no existing redirection transform on the stack - #pop everything.. add this record as the first redirection on the stack - set poplist $stack - set stack [insert_transform $pipename $stack $transform_record $poplist] - } else { - switch -glob -- $action { - "sink-replace" { - #include that index in the poplist - set poplist [lrange $stack $idx_existing_redir end] - #pop all from idx_existing_redir to end, but put back 'lrange $poplist 1 end' - set stack [insert_transform $pipename $stack $transform_record $poplist 1] - } - "sink-aside*" { - set existing_redir_record [lindex $stack $idx_existing_redir] - if {[string match "*locked*" [dict get $existing_redir_record -action]]} { - set put_aside 0 - #we can't aside this one - sit above it instead. - set poplist [lrange $stack $idx_existing_redir+1 end] - set stack [lrange $stack 0 $idx_existing_redir] - } else { - set put_aside 1 - dict set transform_record -aside [lindex $stack $idx_existing_redir] - set poplist [lrange $stack $idx_existing_redir end] - set stack [lrange $stack 0 $idx_existing_redir-1] - } - foreach p $poplist { - chan pop $localchan - } - set transformname [dict get $transform_record -transform] - set transform_settings [dict get $transform_record -settings] - set obj [$transformname new $transform_record] - set h [chan push $localchan $obj] - dict set transform_record -handle $h - dict set transform_record -obj $obj - dict set transform_record -note "insert_transform-with-aside" - lappend stack $transform_record - # add back poplist *except* the one we transferred into -aside (if we were able) - foreach p [lrange $poplist $put_aside end] { - set t [dict get $p -transform] - set tsettings [dict get $p -settings] - set obj [$t new $p] - set h [chan push $localchan $obj] - #retain previous -id - code that added it may have kept reference and not expecting it to change - dict set p -handle $h - dict set p -obj $obj - dict set p -note "re-added-after-sink-aside" - lappend stack $p - } - } - default { - #plain "sink" - #we only sink to the topmost redirecting filter - which makes sense for an output channel - #For stdin.. this is more problematic as we're more likely to want to intercept the bottom most redirection. - #todo - review. Consider making default insert position for input channels to be at the source... and float/sink from there. - # - we don't currently know from the stack api if adding input vs output channel - so this needs work to make intuitive. - # consider splitting stack::add to stack::addinput stack::addoutput to split the different behaviour - set poplist [lrange $stack $idx_existing_redir+1 end] - set stack [insert_transform $pipename $stack $transform_record $poplist] - } - } - } - } - default { - error "shellfilter::stack::add unimplemented action '$action'" - } - } - - dict set pipelines $pipename stack $stack - #puts stdout "==" - #puts stdout "==>stack: $stack" - #puts stdout "==" - - #JMNJMN - #show_pipeline $pipename -note "after_add $transformname $args" - return $id - } - proc show_pipeline {pipename args} { - variable pipelines - set stack [dict get $pipelines $pipename stack] - set tag "SHELLFILTER::STACK" - #JMN - load from config - #::shellfilter::log::open $tag {-syslog 127.0.0.1:514} - if {[catch { - ::shellfilter::log::open $tag {-syslog ""} - } err]} { - #e.g safebase interp can't load required modules such as shellthread (or Thread) - puts stderr "shellfilter::show_pipeline cannot open log" - return - } - ::shellfilter::log::write $tag "transform stack for $pipename $args" - foreach tf $stack { - ::shellfilter::log::write $tag " $tf" - } - - } -} - - -namespace eval shellfilter { - variable sources [list] - variable stacks [dict create] - - proc ::shellfilter::redir_channel_to_log {chan args} { - variable sources - set default_logsettings [dict create {*}{ - -tag redirected_$chan - -syslog "" - -file "" - }] - if {[dict exists $args -action]} { - set action [dict get $args -action] - } else { - # action "sink" is a somewhat reasonable default for an output redirection transform - # but it can make it harder to configure a plain ordered stack if the user is not expecting it, so we'll default to stack - # also.. for stdin transform sink makes less sense.. - #todo - default "stack" instead of empty string - set action "" - } - if {[dict exists $args -settings]} { - set logsettings [dict get $args -settings] - } else { - set logsettings {} - } - - set logsettings [dict merge $default_logsettings $logsettings] - set tag [dict get $logsettings -tag] - if {$tag ni $sources} { - lappend sources $tag - } - - set id [shellfilter::stack::add $chan logonly -action $action -settings $logsettings] - return $id - } - - proc ::shellfilter::redir_output_to_log {tagprefix args} { - variable sources - - set default_settings [list -tag ${tagprefix} -syslog "" -file ""] - - set opts [dict create -action "" -settings {}] - set opts [dict merge $opts $args] - set optsettings [dict get $opts -settings] - set settings [dict merge $default_settings $optsettings] - - set tag [dict get $settings -tag] - if {$tag ne $tagprefix} { - error "shellfilter::redir_output_to_log -tag value must match supplied tagprefix:'$tagprefix'. Omit -tag, or make it the same. It will automatically be suffixed with stderr and stdout. Use redir_channel_to_log if you want to separately configure each channel" - } - lappend sources ${tagprefix}stdout ${tagprefix}stderr - - set stdoutsettings $settings - dict set stdoutsettings -tag ${tagprefix}stdout - set stderrsettings $settings - dict set stderrsettings -tag ${tagprefix}stderr - - set idout [redir_channel_to_log stdout -action [dict get $opts -action] -settings $stdoutsettings] - set iderr [redir_channel_to_log stderr -action [dict get $opts -action] -settings $stderrsettings] - - return [list $idout $iderr] - } - - #eg try: set v [list #a b c] - #vs set v {#a b c} - proc list_is_canonical l { - #courtesy DKF via wiki https://wiki.tcl-lang.org/page/BNF+for+Tcl - if {[catch {llength $l}]} {return 0} - string equal $l [list {*}$l] - } - - #return a dict keyed on numerical list index showing info about each element - # - particularly - # 'wouldbrace' to indicate that the item would get braced by Tcl when added to another list - # 'head_tail_chars' to show current first and last character (in case it's wrapped e.g in double or single quotes or an existing set of braces) - proc list_element_info {inputlist} { - set i 0 - set info [dict create] - set testlist [list] - foreach original_item $inputlist { - #--- - # avoid sharing internal rep with original items in the list (avoids shimmering of rep in original list for certain items such as paths) - unset -nocomplain item - append item $original_item {} - #--- - - set iteminfo [dict create] - set itemlen [string length $item] - lappend testlist $item - set tcl_len [string length $testlist] - set diff [expr {$tcl_len - $itemlen}] - if {$diff == 0} { - dict set iteminfo wouldbrace 0 - dict set iteminfo wouldescape 0 - } else { - #test for escaping vs bracing! - set testlistchars [split $testlist ""] - if {([lindex $testlistchars 0] eq "\{") && ([lindex $testlistchars end] eq "\}")} { - dict set iteminfo wouldbrace 1 - dict set iteminfo wouldescape 0 - } else { - dict set iteminfo wouldbrace 0 - dict set iteminfo wouldescape 1 - } - } - set testlist [list] - set charlist [split $item ""] - set char_a [lindex $charlist 0] - set char_b [lindex $charlist 1] - set char_ab ${char_a}${char_b} - set char_y [lindex $charlist end-1] - set char_z [lindex $charlist end] - set char_yz ${char_y}${char_z} - - if { ("{" in $charlist) || ("}" in $charlist) } { - dict set iteminfo has_braces 1 - set innerchars [lrange $charlist 1 end-1] - if {("{" in $innerchars) || ("}" in $innerchars)} { - dict set iteminfo has_inner_braces 1 - } else { - dict set iteminfo has_inner_braces 0 - } - } else { - dict set iteminfo has_braces 0 - dict set iteminfo has_inner_braces 0 - } - - #todo - brace/char counting to determine if actually 'wrapped' - #e.g we could have list element {((abc)} - which appears wrapped if only looking at first and last chars. - #also {(x) (y)} as a list member.. how to treat? - if {$itemlen <= 1} { - dict set iteminfo apparentwrap "not" - } else { - #todo - switch on $char_a$char_z - if {($char_a eq {"}) && ($char_z eq {"})} { - dict set iteminfo apparentwrap "doublequotes" - } elseif {($char_a eq "'") && ($char_z eq "'")} { - dict set iteminfo apparentwrap "singlequotes" - } elseif {($char_a eq "(") && ($char_z eq ")")} { - dict set iteminfo apparentwrap "brackets" - } elseif {($char_a eq "\{") && ($char_z eq "\}")} { - dict set iteminfo apparentwrap "braces" - } elseif {($char_a eq "^") && ($char_z eq "^")} { - dict set iteminfo apparentwrap "carets" - } elseif {($char_a eq "\[") && ($char_z eq "\]")} { - dict set iteminfo apparentwrap "squarebrackets" - } elseif {($char_a eq "`") && ($char_z eq "`")} { - dict set iteminfo apparentwrap "backquotes" - } elseif {($char_a eq "\n") && ($char_z eq "\n")} { - dict set iteminfo apparentwrap "lf-newline" - } elseif {($char_ab eq "\r\n") && ($char_yz eq "\r\n")} { - dict set iteminfo apparentwrap "crlf-newline" - } else { - dict set iteminfo apparentwrap "not-determined" - } - - } - dict set iteminfo wrapbalance "unknown" ;#a hint to caller that apparentwrap is only a guide. todo - possibly make wrapbalance indicate 0 for unbalanced.. and positive numbers for outer-count of wrappings. - #e.g {((x)} == 0 {((x))} == 1 {(x) (y (z))} == 2 - dict set iteminfo head_tail_chars [list $char_a $char_z] - set namemap [list {*}{ - \r cr - \n lf - {"} doublequote - {'} singlequote - "`" backquote - "^" caret - \t tab - " " sp - "\[" lsquare - "\]" rsquare - "(" lbracket - ")" rbracket - "\{" lbrace - "\}" rbrace - \\ backslash - / forwardslash - }] - if {[string length $char_a]} { - set char_a_name [string map $namemap $char_a] - } else { - set char_a_name "emptystring" - } - if {[string length $char_z]} { - set char_z_name [string map $namemap $char_z] - } else { - set char_z_name "emptystring" - } - - dict set iteminfo head_tail_names [list $char_a_name $char_z_name] - dict set iteminfo len $itemlen - dict set iteminfo difflen $diff ;#2 for braces, 1 for quoting?, or 0. - dict set info $i $iteminfo - incr i - } - return $info - } - - - #parse bracketed expression (e.g produced by vim "shellxquote=(" ) into a tcl (nested) list - #e.g {(^c:/my spacey/path^ >^somewhere^)} - #e.g {(blah (etc))}" - #Result is always a list - even if only one toplevel set of brackets - so it may need [lindex $result 0] if input is the usual case of {( ...)} - # - because it also supports the perhaps less likely case of: {( ...) unbraced (...)} etc - # Note that - #maintenance warning - duplication in branches for bracketed vs unbracketed! - proc parse_cmd_brackets {str} { - #wordwrappers currently best suited to non-bracket entities - no bracket matching within - anything goes until end-token reached. - # - but.. they only take effect where a word can begin. so a[x y] may be split at the space unless it's within some other wraper e.g " a[x y]" will not break at the space - # todo - consider extending the in-word handling of word_bdepth which is currently only applied to () i.e aaa(x y) is supported but aaa[x y] is not as the space breaks the word up. - set wordwrappers [list {*}{ - } "\"" [list "\"" "\"" "\""] {*}{ - } {^} [list "\"" "\"" "^"] {*}{ - } "'" [list "'" "'" "'"] {*}{ - } "\{" [list "\{" "\}" "\}"] {*}{ - } {[} [list {[} {]} {]}] {*}{ - } - ] ;#dict mapping start_character to {replacehead replacetail expectedtail} - set shell_specials [list "|" "|&" "<" "<@" "<<" ">" "2>" ">&" ">>" "2>>" ">>&" ">@" "2>@" "2>@1" ">&@" "&" "&&" ] ;#words/chars that may precede an opening bracket but don't merge with the bracket to form a word. - #puts "pb:$str" - set in_bracket 0 - set in_word 0 - set word "" - set result {} - set word_bdepth 0 - set word_bstack [list] - set wordwrap "" ;#only one active at a time - set bracketed_elements [dict create] - foreach char [split $str ""] { - #puts "c:$char bracketed:$bracketed_elements" - if {$in_bracket > 0} { - if {$in_word} { - if {[string length $wordwrap]} { - #anything goes until end-char - #todo - lookahead and only treat as closing if before a space or ")" ? - lassign [dict get $wordwrappers $wordwrap] _open closing endmark - if {$char eq $endmark} { - set wordwrap "" - append word $closing - dict lappend bracketed_elements $in_bracket $word - set word "" - set in_word 0 - } else { - append word $char - } - } else { - if {$word_bdepth == 0} { - #can potentially close off a word - or start a new one if word-so-far is a shell-special - if {$word in $shell_specials} { - if {$char eq ")"} { - dict lappend bracketed_elements $in_bracket $word - set subresult [dict get $bracketed_elements $in_bracket] - dict set bracketed_elements $in_bracket [list] - incr in_bracket -1 - if {$in_bracket == 0} { - lappend result $subresult - } else { - dict lappend bracketed_elements $in_bracket $subresult - } - set word "" - set in_word 0 - } elseif {[regexp {[\s]} $char]} { - dict lappend bracketed_elements $in_bracket $word - set word "" - set in_word 0 - } elseif {$char eq "("} { - dict lappend bracketed_elements $in_bracket $word - set word "" - set in_word 0 - incr in_bracket - } else { - #at end of shell-specials is another point to look for word started by a wordwrapper char - #- expect common case of things like >^/my/path^ - if {$char in [dict keys $wordwrappers]} { - dict lappend bracketed_elements $in_bracket $word - set word "" - set in_word 1 ;#just for explicitness.. we're straight into the next word. - set wordwrap $char - set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. - } else { - #something unusual.. keep going with word! - append word $char - } - } - } else { - - if {$char eq ")"} { - dict lappend bracketed_elements $in_bracket $word - set subresult [dict get $bracketed_elements $in_bracket] - dict set bracketed_elements $in_bracket [list] - incr in_bracket -1 - if {$in_bracket == 0} { - lappend result $subresult - } else { - dict lappend bracketed_elements $in_bracket $subresult - } - set word "" - set in_word 0 - } elseif {[regexp {[\s]} $char]} { - dict lappend bracketed_elements $in_bracket $word - set word "" - set in_word 0 - } elseif {$char eq "("} { - #ordinary word up-against and opening bracket - brackets are part of word. - incr word_bdepth - append word "(" - } else { - append word $char - } - } - } else { - #currently only () are used for word_bdepth - todo add all or some wordwrappers chars so that the word_bstack can have multiple active. - switch -- $char { - "(" { - incr word_bdepth - lappend word_bstack $char - append word $char - } - ")" { - incr word_bdepth -1 - set word_bstack [lrange $word_bstack 0 end-1] - append word $char - } - default { - #spaces and chars added to word as it's still in a bracketed section - append word $char - } - } - } - } - } else { - - if {$char eq "("} { - incr in_bracket - - } elseif {$char eq ")"} { - set subresult [dict get $bracketed_elements $in_bracket] - dict set bracketed_elements $in_bracket [list] - incr in_bracket -1 - if {$in_bracket == 0} { - lappend result $subresult - } else { - dict lappend bracketed_elements $in_bracket $subresult - } - } elseif {[regexp {[\s]} $char]} { - # - } else { - #first char of word - look for word-wrappers - if {$char in [dict keys $wordwrappers]} { - set wordwrap $char - set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. - } else { - set word $char - } - set in_word 1 - } - } - } else { - if {$in_word} { - if {[string length $wordwrap]} { - lassign [dict get $wordwrappers $wordwrap] _open closing endmark - if {$char eq $endmark} { - set wordwrap "" - append word $closing - lappend result $word - set word "" - set in_word 0 - } else { - append word $char - } - } else { - - if {$word_bdepth == 0} { - if {$word in $shell_specials} { - if {[regexp {[\s]} $char]} { - lappend result $word - set word "" - set in_word 0 - } elseif {$char eq "("} { - lappend result $word - set word "" - set in_word 0 - incr in_bracket - } else { - #at end of shell-specials is another point to look for word started by a wordwrapper char - #- expect common case of things like >^/my/path^ - if {$char in [dict keys $wordwrappers]} { - lappend result $word - set word "" - set in_word 1 ;#just for explicitness.. we're straight into the next word. - set wordwrap $char - set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. - } else { - #something unusual.. keep going with word! - append word $char - } - } - - } else { - if {[regexp {[\s)]} $char]} { - lappend result $word - set word "" - set in_word 0 - } elseif {$char eq "("} { - incr word_bdepth - append word $char - } else { - append word $char - } - } - } else { - switch -- $char { - "(" { - incr word_bdepth - append word $char - } - ")" { - incr word_bdepth -1 - append word $char - } - default { - append word $char - } - } - } - } - } else { - if {[regexp {[\s]} $char]} { - #insig whitespace(?) - } elseif {$char eq "("} { - incr in_bracket - dict set bracketed_elements $in_bracket [list] - } elseif {$char eq ")"} { - error "unbalanced bracket - unable to proceed result so far: $result bracketed_elements:$bracketed_elements" - } else { - #first char of word - look for word-wrappers - if {$char in [dict keys $wordwrappers]} { - set wordwrap $char - set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. - } else { - set word $char - } - set in_word 1 - } - } - } - #puts "----$bracketed_elements" - } - if {$in_bracket > 0} { - error "shellfilter::parse_cmd_brackets missing close bracket. input was '$str'" - } - if {[dict exists $bracketed_elements 0]} { - #lappend result [lindex [dict get $bracketed_elements 0] 0] - lappend result [dict get $bracketed_elements 0] - } - if {$in_word} { - lappend result $word - } - return $result - } - - #only double quote if argument not quoted with single or double quotes - proc dquote_if_not_quoted {a} { - set wrapchars [string cat [string range $a 0 0] [string range $a end end]] - switch -- $wrapchars { - {""} - {''} { - return $a - } - default { - set newinner [string map [list {"} "\\\""] $a] - return "\"$newinner\"" - } - } - } - - #proc dquote_if_not_bracketed/braced? - - #wrap in double quotes if not double-quoted - proc dquote_if_not_dquoted {a} { - set wrapchars [string cat [string range $a 0 0] [string range $a end end]] - switch -- $wrapchars { - {""} { - return $a - } - default { - #escape any inner quotes.. - set newinner [string map [list {"} "\\\""] $a] - return "\"$newinner\"" - } - } - } - proc dquote {a} { - #escape any inner quotes.. - set newinner [string map [list {"} "\\\""] $a] - return "\"$newinner\"" - } - proc get_scriptrun_from_cmdlist_dquote_if_not {cmdlist {shellcmdflag ""}} { - set scr [auto_execok "script"] - if {[string length $scr]} { - #set scriptrun "( $c1 [lrange $cmdlist 1 end] )" - set arg1 [lindex $cmdlist 0] - if {[string first " " $arg1]>0} { - set c1 [dquote_if_not_quoted $arg1] - #set c1 "\"$arg1\"" - } else { - set c1 $arg1 - } - - if {[string length $shellcmdflag]} { - set scriptrun "$shellcmdflag \$($c1 " - } else { - set scriptrun "\$($c1 " - } - #set scriptrun "$c1 " - foreach a [lrange $cmdlist 1 end] { - #set a [string map [list "/" "//"] $a] - #set a [string map [list "\"" "\\\""] $a] - if {[string first " " $a] > 0} { - append scriptrun [dquote_if_not_quoted $a] - } else { - append scriptrun $a - } - append scriptrun " " - } - set scriptrun [string trim $scriptrun] - append scriptrun ")" - #return [list $scr -q -e -c $scriptrun /dev/null] - return [list $scr -e -c $scriptrun /dev/null] - } else { - return $cmdlist - } - } - - proc ::shellfilter::trun {commandlist args} { - #jmn - } - - - # run a command (or tcl script) with tees applied to stdout/stderr/stdin (or whatever channels are being used) - # By the point run is called - any transforms should already be in place on the channels if they're needed. - # The tees will be inline with none,some or all of those transforms depending on how the stack was configured - # (upstream,downstream configured via -float,-sink etc) - proc ::shellfilter::run {commandlist args} { - #must be a list. If it was a shell commandline string. convert it elsewhere first. - - variable sources - set runtag "shellfilter-run" - #set tid [::shellfilter::log::open $runtag [list -syslog 127.0.0.1:514]] - set tid [::shellfilter::log::open $runtag [list -syslog ""]] - if {[catch {llength $commandlist} listlen]} { - set listlen "" - } - ::shellfilter::log::write $runtag " commandlist:'$commandlist' listlen:$listlen strlen:[string length $commandlist]" - - #flush stdout - #flush stderr - - #adding filters with sink-aside will temporarily disable the existing redirection - #All stderr/stdout from the shellcommand will now tee to the underlying stderr/stdout as well as the configured syslog - - set defaults [dict create {*}{ - -teehandle command - -outchan stdout - -errchan stderr - -inchan stdin - -tclscript 0 - }] - set opts [dict merge $defaults $args] - - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- - set outchan [dict get $opts -outchan] - set errchan [dict get $opts -errchan] - set inchan [dict get $opts -inchan] - set teehandle [dict get $opts -teehandle] - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- - set is_script [dict get $opts -tclscript] - dict unset opts -tclscript ;#don't pass it any further - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- - set teehandle_out ${teehandle}out ;#default commandout - set teehandle_err ${teehandle}err - set teehandle_in ${teehandle}in - - - #puts stdout "shellfilter initialising tee_to_pipe transforms for in/out/err" - - # sources should be added when stack::new called instead(?) - foreach source [list $teehandle_out $teehandle_err] { - if {$source ni $sources} { - lappend sources $source - } - } - set outdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_out device] - set outpipechan [dict get $outdeviceinfo localchan] - set errdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_err device] - set errpipechan [dict get $errdeviceinfo localchan] - - #set indeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_in device] - #set inpipechan [dict get $indeviceinfo localchan] - - #NOTE:These transforms are not necessarily at the top of each stack! - #The float/sink mechanism, along with whether existing transforms are diversionary decides where they sit. - set id_out [shellfilter::stack::add $outchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_out -pipechan $outpipechan]] - set id_err [shellfilter::stack::add $errchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_err -pipechan $errpipechan]] - - # need to use os level channel handle for stdin - try named pipes (or even sockets) instead of fifo2 for this - # If non os-level channel - the command can't be run with the redirection - # stderr/stdout can be run with non-os handles in the call - - # but then it does introduce issues with terminal-detection and behaviour for stdout at least - # - # input is also a tee - we never want to change the source at this point - just log/process a side-channel of it. - # - #set id_in [shellfilter::stack::add $inchan tee_to_pipe -action sink-aside -settings [list -tag commandin -pipechan $inpipechan]] - - - #set id_out [shellfilter::stack::add stdout tee_to_log -action sink-aside -settings [list -tag shellstdout -syslog 127.0.0.1:514 -file ""]] - #set id_err [shellfilter::stack::add stderr tee_to_log -action sink-aside -settings [list -tag shellstderr -syslog 127.0.0.1:514 -file "stderr.txt"]] - - #we need to catch errors - and ensure stack::remove calls occur. - #An error can be raised if the command couldn't even launch, as opposed to a non-zero exitcode and stderr output from the command itself. - # - if {!$is_script} { - set experiment 0 - if {$experiment} { - try { - set results [exec {*}$commandlist] - set exitinfo [list exitcode 0] - } trap CHILDSTATUS {results options} { - set exitcode [lindex [dict get $options -errorcode] 2] - set exitinfo [list exitcode $exitcode] - } - } else { - if {[catch { - #run process with stdout/stderr/stdin or with configured channels - #set exitinfo [shellcommand_stdout_stderr $commandlist $outchan $errchan $inpipechan {*}$opts] - set exitinfo [shellcommand_stdout_stderr $commandlist $outchan $errchan stdin {*}$opts] - #puts stderr "---->exitinfo $exitinfo" - - #subprocess result should usually have an "exitcode" key - #but for background execution we will get a "pids" key of process ids. - } errMsg]} { - set exitinfo [list error "$errMsg" source shellcommand_stdout_stderr] - } - } - } else { - if {[catch { - #script result - set exitinfo [list result [uplevel #0 [list eval $commandlist]]] - } errMsg]} { - set exitinfo [list error "$errMsg" errorCode $::errorCode errorInfo "$::errorInfo"] - } - } - #puts "shellfilter::run finished call" - - #------------------------- - #warning - without flush stdout - we can get hang, but only on some terminals - # - mechanism for this problem not understood! - flush stdout - flush stderr - #------------------------- - - #the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal - #Remove execution-time Tees from stack - shellfilter::stack::remove stdout $id_out - shellfilter::stack::remove stderr $id_err - #shellfilter::stack::remove stderr $id_in - - #puts stderr "shellfilter::run complete..." - - #chan configure stderr -buffering line - #flush stdout - - - ::shellfilter::log::write $runtag " return '$exitinfo'" - ::shellfilter::log::close $runtag - return $exitinfo - } - proc ::shellfilter::logtidyup { {tags {}} } { - variable sources - set worker_errorlist [list] - set tidied_sources [list] - set tidytag "logtidy" - - - # opening a thread or writing to a log/syslog close to possible process exit is probably not a great idea. - # we should ensure the thread already exists early on if we really need logging here. - # - #set tid [::shellfilter::log::open $tidytag {-syslog 127.0.0.1:514}] - #::shellfilter::log::write $tidytag " logtidyuptags '$tags'" - - foreach s $sources { - if {$s eq $tidytag} { - continue - } - #puts "logtidyup source $s" - set close 1 - if {[llength $tags]} { - if {$s ni $tags} { - set close 0 - } - } - if {$close} { - lappend tidied_sources $s - shellfilter::log::close $s - lappend worker_errorlist {*}[shellthread::manager::get_and_clear_errors $s] - } - } - set remaining_sources [list] - foreach s $sources { - if {$s ni $tidied_sources} { - lappend remaining_sources $s - } - } - - #set sources [concat $remaining_sources $tidytag] - set sources $remaining_sources - - #shellfilter::stack::unwind stdout - #shellfilter::stack::unwind stderr - return [list tidied $tidied_sources errors $worker_errorlist] - } - - #package require tcl::chan::null - # e.g set errchan [tcl::chan::null] - # e.g chan push stdout [shellfilter::chan::var new ::some_var] - proc ::shellfilter::shellcommand_stdout_stderr {commandlist outchan errchan inchan args} { - set valid_flags [list {*}{ - -timeout - -outprefix - -errprefix - -debug - -copytempfile - -outbuffering - -errbuffering - -inbuffering - -readprocesstranslation - -outtranslation - -stdinhandler - -outchan - -errchan - -inchan - -teehandle - }] - - set runtag shellfilter-run2 - #JMN - load from config - #set tid [::shellfilter::log::open $runtag [list -syslog "127.0.0.1:514"]] - set tid [::shellfilter::log::open $runtag [list -syslog ""]] - - if {[llength $args] % 2} { - error "Trailing arguments after any positional arguments must be in pairs of the form -argname argvalue. Valid flags are:'$valid_flags'" - } - set invalid_flags [list] - foreach {k -} $args { - switch -- $k { - -timeout - - -outprefix - - -errprefix - - -debug - - -copytempfile - - -outbuffering - - -errbuffering - - -inbuffering - - -readprocesstranslation - - -outtranslation - - -stdinhandler - - -outchan - - -errchan - - -inchan - - -teehandle { - } - default { - lappend invalid_flags $k - } - } - } - if {[llength $invalid_flags]} { - error "Unknown option(s)'$invalid_flags': must be one of '$valid_flags'" - } - #line buffering generally best for output channels.. keeps relative output order of stdout/stdin closer to source order - #there may be data where line buffering is inappropriate, so it's configurable per std channel - #reading inputs with line buffering can result in extraneous newlines as we can't detect trailing data with no newline before eof. - set defaults [dict create {*}{ - -outchan stdout - -errchan stderr - -inchan stdin - -outbuffering none - -errbuffering none - -readprocesstranslation auto - -outtranslation lf - -inbuffering none - -timeout 900000 - -outprefix "" - -errprefix "" - -debug 0 - -copytempfile 0 - -stdinhandler "" - }] - - - - set args [dict merge $defaults $args] - set outbuffering [dict get $args -outbuffering] - set errbuffering [dict get $args -errbuffering] - set inbuffering [dict get $args -inbuffering] - set readprocesstranslation [dict get $args -readprocesstranslation] - set outtranslation [dict get $args -outtranslation] - set timeout [dict get $args -timeout] - set outprefix [dict get $args -outprefix] - set errprefix [dict get $args -errprefix] - set debug [dict get $args -debug] - set copytempfile [dict get $args -copytempfile] - set stdinhandler [dict get $args -stdinhandler] - - set debugname "shellfilter-debug" - - if {$debug} { - set tid [::shellfilter::log::open $debugname [list -syslog "127.0.0.1:514"]] - ::shellfilter::log::write $debugname " commandlist '$commandlist'" - } - #'clock micros' good enough id for shellcommand calls unless one day they can somehow be called concurrently or sequentially within a microsecond and within the same interp. - # a simple counter would probably work too - #consider other options if an alternative to the single vwait in this function is used. - set call_id [tcl::clock::microseconds] ; - set ::shellfilter::shellcommandvars($call_id,exitcode) "" - set ::shellfilter::shellcommandvars($call_id,timeoutid) "" - set waitvar ::shellfilter::shellcommandvars($call_id,waitvar) - if {$debug} { - ::shellfilter::log::write $debugname " waitvar '$waitvar'" - } - lassign [chan pipe] rderr wrerr - - #--------------- - #JMN 2025 - # e.g cannot run ansible cmdline tools if non-blocking - #chan configure $wrerr -blocking 0 - #------------------ - - set custom_stderr "" - set lastitem [lindex $commandlist end] - #todo - ensure we can handle 2> file (space after >) - - #review - reconsider the handling of redirections such that tcl-style are handled totally separately to other shell syntaxes! - # - #note 2>@1 must ocur as last word for tcl - but 2@stdout can occur elsewhere - #(2>@stdout echoes to main stdout - not into pipeline) - #To properly do pipelines it looks like we will have to split on | and call this proc multiple times and wire it up accordingly (presumably in separate threads) - - switch -- [string trim $lastitem] { - {&} { - set name [lindex $commandlist 0] - #background execution - stdout and stderr from child still comes here - but process is backgrounded - #FIX! - this is broken for paths with backslashes for example - #set pidlist [exec {*}[concat $name [lrange $commandlist 1 end]]] - set pidlist [exec {*}$commandlist] - return [list pids $pidlist] - } - {2>&1} - {2>@1} { - set custom_stderr {2>@1} ;#use the tcl style - set commandlist [lrange $commandlist 0 end-1] - } - default { - # 2> filename - # 2>> filename - # 2>@ openfileid - set redir2test [string range $lastitem 0 1] - if {$redir2test eq "2>"} { - set custom_stderr $lastitem - set commandlist [lrange $commandlist 0 end-1] - } - } - } - set lastitem [lindex $commandlist end] - - set teefile "" ;#empty string, write, append - #an ugly hack.. because redirections seem to arrive wrapped - review! - #There be dragons here.. - #Be very careful with list manipulation of the commandlist string.. backslashes cause havoc. commandlist must always be a well-formed list. generally avoid string manipulations on entire list or accidentally breaking a list element into parts if it shouldn't be.. - #The problem here - is that we can't always know what was intended on the commandline regarding quoting - - ::shellfilter::log::write $runtag "checking for redirections in $commandlist" - #sometimes we see a redirection without a following space e.g >C:/somewhere - #normalize - switch -regexp -- $lastitem {*}{ - } {^>[/[:alpha:]]+} { - set lastitem "> [string range $lastitem 1 end]" - } {*}{ - } {^>>[/[:alpha:]]+} { - set lastitem ">> [string range $lastitem 2 end]" - } - - - #for a redirection, we assume either a 2-element list at tail of form {> {some path maybe with spaces}} - #or that the tail redirection is not wrapped.. x y z > {some path maybe with spaces} - #we can't use list methods such as llenth on a member of commandlist - set wordlike_parts [regexp -inline -all {\S+} $lastitem] - - if {([llength $wordlike_parts] >= 2) && ([lindex $wordlike_parts 0] in [list ">>" ">"])} { - #wrapped redirection - but maybe not 'well' wrapped (unquoted filename) - set lastitem [string trim $lastitem] ;#we often see { > something} - - #don't use lassign or lrange on the element itself without checking first - #we can treat the commandlist as a whole as a well formed list but not neccessarily each element within. - #lassign $lastitem redir redirtarget - #set commandlist [lrange $commandlist 0 end-1] - # - set itemchars [split $lastitem ""] - set firstchar [lindex $itemchars 0] - set lastchar [lindex $itemchars end] - - #NAIVE test for double quoted only! - #consider for example {"a" x="b"} - #testing first and last is not decisive - #We need to decide what level of drilling down is even appropriate here.. - #if something was double wrapped - it was perhaps deliberate so we don't interpret it as something(?) - set head_tail_chars [list $firstchar $lastchar] - set doublequoted [expr {[llength [lsearch -all $head_tail_chars "\""]] == 2}] - if {[string equal "\{" $firstchar] && [string equal "\}" $lastchar]} { - set curlyquoted 1 - } else { - set curlyquoted 0 - } - - if {$curlyquoted} { - #these are not the tcl protection brackets but ones supplied in the argument - #it's still not valid to use list operations on a member of the commandlist - set inner [string range $lastitem 1 end-1] - #todo - fix! we still must assume there could be list-breaking data! - set innerwords [regexp -inline -all {\S+} $inner] ;#better than [split $inner] because we don't get extra empty elements for each whitespace char - set redir [lindex $innerwords 0] ;#a *potential* redir - to be tested below - set redirtarget [lrange $innerwords 1 end] ;#all the rest - } elseif {$doublequoted} { - ::shellfilter::log::write $debugname "doublequoting at tail of command '$commandlist'" - set inner [string range $lastitem 1 end-1] - set innerwords [regexp -inline -all {\S+} $inner] - set redir [lindex $innerwords 0] - set redirtarget [lrange $innerwords 1 end] - } else { - set itemwords [regexp -inline -all {\S+} $lastitem] - # e.g > c:\test becomes > {c:\test} - # but > c/mnt/c/test/temp.txt stays as > /mnt/c/test/temp.txt - set redir [lindex $itemwords 0] - set redirtarget [lrange $itemwords 1 end] - } - set commandlist [lrange $commandlist 0 end-1] - - } elseif {[lindex $commandlist end-1] in [list ">>" ">"]} { - #unwrapped redirection - #we should be able to use list operations like lindex and lrange here as the command itself is hopefully still a well formed list - set redir [lindex $commandlist end-1] - set redirtarget [lindex $commandlist end] - set commandlist [lrange $commandlist 0 end-2] - } else { - #no redirection - set redir "" - set redirtarget "" - #no change to command list - } - - - switch -- $redir { - ">>" - ">" { - set redirtarget [string trim $redirtarget "\""] - ::shellfilter::log::write $runtag " have redirection '$redir' to '$redirtarget'" - - set winfile $redirtarget ;#default assumption - switch -glob -- $redirtarget { - "/c/*" { - set winfile "c:/[string range $redirtarget 3 end]" - } - "/mnt/c/*" { - set winfile "c:/[string range $redirtarget 7 end]" - } - } - - if {[file exists [file dirname $winfile]]} { - #containing folder for target exists - if {$redir eq ">"} { - set teefile "write" - } else { - set teefile "append" - } - ::shellfilter::log::write $runtag "Directory exists '[file dirname $winfile]' operation:$teefile" - } else { - #we should be writing to a file.. but can't - ::shellfilter::log::write $runtag "cannot verify directory exists '[file dirname $winfile]'" - } - } - default { - ::shellfilter::log::write $runtag "No redir found!!" - } - } - - #often first element of command list is wrapped and cannot be run directly - #e.g {{ls -l} {> {temp.tmp}}} - #we will assume that if there is a single element which is a pathname containing a space - it is doubly wrapped. - # this may not be true - and the command may fail if it's just {c:\program files\etc} but it is the less common case and we currently have no way to detect. - #unwrap first element.. will not affect if not wrapped anyway (subject to comment above re spaces) - set commandlist [concat [lindex $commandlist 0] [lrange $commandlist 1 end]] - - #todo? - #child process environment. - # - to pass a different environment to the child - we would need to save the env array, modify as required, and then restore the env array. - - #to restore buffering states after run - set remember_in_out_err_buffering [list {*}{ - } [chan configure $inchan -buffering] {*}{ - } [chan configure $outchan -buffering] {*}{ - } [chan configure $errchan -buffering] {*}{ - } - ] - - set remember_in_out_err_translation [list {*}{ - } [chan configure $inchan -translation] {*}{ - } [chan configure $outchan -translation] {*}{ - } [chan configure $errchan -translation] {*}{ - } - ] - - - - - #chan configure $inchan -buffering none -blocking 1 ;#test - #chan configure $inchan -buffering $inbuffering -blocking 0 ;#we are setting up a readable handler for this - so non-blocking ok - chan configure $inchan -buffering $inbuffering - - - chan configure $errchan -buffering $errbuffering - #chan configure $outchan -blocking 0 - chan configure $outchan -buffering $outbuffering ;#don't configure non-blocking. weird duplicate of *second* line occurs if you do. - # - - #-------------------------------------------- - #Tested on windows. Works to stop in output when buffering is none, reading from channel with -translation auto - #cmd, pwsh, tcl - #chan configure $outchan -translation lf - #chan configure $errchan -translation lf - #-------------------------------------------- - chan configure $outchan -translation $outtranslation - chan configure $errchan -translation $outtranslation - - #puts stderr "chan configure $wrerr [chan configure $wrerr]" - if {$debug} { - ::shellfilter::log::write $debugname "COMMAND [list $commandlist] strlen:[string length $commandlist] llen:[llength $commandlist]" - } - #todo - handle custom redirection of stderr to a file? - if {[string length $custom_stderr]} { - #::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist $custom_stderr] a+" - #set rdout [open |[concat $commandlist $custom_stderr] a+] - ::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist [list $custom_stderr <@$inchan]] [list RDONLY]" - set rdout [open |[concat $commandlist [list <@$inchan $custom_stderr]] [list RDONLY]] - set rderr "bogus" ;#so we don't wait for it - } else { - ::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist [list 2>@$wrerr <@$inchan]] [list RDONLY]" - #set rdout [open |[concat $commandlist [list 2>@$wrerr]] a+] - #set rdout [open |[concat $commandlist [list 2>@$wrerr]] [list RDWR]] - - # If we don't redirect stderr to our own tcl-based channel - then the transforms don't get applied. - # This is the whole reason we need these file-event loops. - # Ideally we need something like exec,open in tcl that interacts with transformed channels directly and emits as it runs, not only at termination - # - and that at least appears like a terminal to the called command. - #set rdout [open |[concat $commandlist [list 2>@stderr <@$inchan]] [list RDONLY]] - - #REVIEW! - #if the child process takes a while to begin reading stdin - the data on stdin between when we stopped the parent chan event handler and when the child gets data, - #seems to stay buffered somewhere. It is then read by the parent, after the child returns. (ie not lost, but out-of-order) - #This can be apparent sometimes even with fast typing upon calling an executable. (e.g occasionally even vim - but seems to be timing based so might only happen first time if at all) - # see scriptlib/stdin_race.tcl etc test files. - #similar problem with python & perl - issue seems to be in libc or OS buffering behaviour for standard channels. - #note that zig (repo/jn/zig/stdin_race) seems to avoid this issue - todo - make zig based binary extension for open/exec? - - set rdout [open |[concat $commandlist [list 2>@$wrerr <@$inchan]] [list RDONLY]] - - chan configure $rderr -buffering $errbuffering -blocking 0 - chan configure $rderr -translation $readprocesstranslation - } - - - - set command_pids [pid $rdout] - #puts stderr "command_pids: $command_pids" - #tcl::process ensemble only available in 8.7+ - and it didn't prove useful here anyway - # the child process generally won't shut down until channels are closed. - # premature EOF on grandchild process launch seems to be due to lack of terminal emulation when redirecting stdin/stdout. - # worked around in punk/repl using 'script' command as a fake tty. - #set subprocesses [tcl::process::list] - #puts stderr "subprocesses: $subprocesses" - #if {[lindex $command_pids 0] ni $subprocesses} { - # puts stderr "pid [lindex $command_pids 0] not running $errMsg" - #} else { - # puts stderr "pid [lindex $command_pids 0] is running" - #} - - - if {$debug} { - ::shellfilter::log::write $debugname "pipeline pids: $command_pids" - } - - #jjj - - - chan configure $rdout -buffering $outbuffering -blocking 0 - chan configure $rdout -translation $readprocesstranslation - - if {![string length $custom_stderr]} { - chan event $rderr readable [list apply {{chan other wrerr outchan errchan waitfor errprefix errbuffering debug debugname pids} { - if {$errbuffering eq "line"} { - set countchunk [chan gets $chan chunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved - #errprefix only applicable to line buffered output - if {$countchunk >= 0} { - if {[chan eof $chan]} { - puts -nonewline $errchan ${errprefix}$chunk - } else { - puts $errchan "${errprefix}$chunk" - } - } - } else { - set chunk [chan read $chan] - if {[string length $chunk]} { - puts -nonewline $errchan $chunk - } - } - if {[chan eof $chan]} { - flush $errchan ;#jmn - #set subprocesses [tcl::process::list] - #puts stderr "subprocesses: $subprocesses" - #if {[lindex $pids 0] ni $subprocesses} { - # puts stderr "stderr reader: pid [lindex $pids 0] no longer running" - #} else { - # puts stderr "stderr reader: pid [lindex $pids 0] still running" - #} - chan close $chan - #catch {chan close $wrerr} - #if {$other ni [chan names]} { - # set $waitfor stderr - #} - if {[catch {chan configure $other}]} { - set $waitfor stderr - } - } - }} $rderr $rdout $wrerr $outchan $errchan $waitvar $errprefix $errbuffering $debug $debugname $command_pids] - } - - #todo - handle case where large amount of stdin coming in faster than rdout can handle - #as is - arbitrary amount of memory could be used because we aren't using a filevent for rdout being writable - # - we're just pumping it in to the non-blocking rdout buffers - # ie there is no backpressure and stdin will suck in as fast as possible. - # for most commandlines this probably isn't too big a deal.. but it could be a problem for multi-GB disk images etc - # - # - - ## Note - detecting trailing missing nl before eof is basically the same here as when reading rdout from executable - # - but there is a slight difference in that with rdout we get an extra blocked state just prior to the final read. - # Not known if that is significant - ## with inchan configured -buffering line - #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl -r cat - #warning reading input with -buffering line. Cannot detect missing trailing-newline at eof - #instate b:0 eof:0 pend:-1 count:4 - #test - #instate b:0 eof:0 pend:-1 count:3 - #etc - #instate b:0 eof:1 pend:-1 count:-1 - - #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl -r cat - #warning reading input with -buffering line. Cannot detect missing trailing-newline at eof - #instate b:0 eof:0 pend:-1 count:4 - #test - #instate b:0 eof:1 pend:-1 count:3 - #etc - - if 0 { - chan event $inchan readable [list apply {{chan wrchan inbuffering waitfor} { - #chan copy stdin $chan ;#doesn't work in a chan event - if {$inbuffering eq "line"} { - set countchunk [chan gets $chan chunk] - #puts $wrchan "stdinstate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" - if {$countchunk >= 0} { - if {[chan eof $chan]} { - puts -nonewline $wrchan $chunk - } else { - puts $wrchan $chunk - } - } - } else { - set chunk [chan read $chan] - if {[string length $chunk]} { - puts -nonewline $wrchan $chunk - } - } - if {[chan eof $chan]} { - puts stderr "|stdin_reader>eof [chan configure stdin]" - chan event $chan readable {} - #chan close $chan - chan close $wrchan write ;#half close - #set $waitfor "stdin" - } - }} $inchan $rdout $inbuffering $waitvar] - - if {[string length $stdinhandler]} { - chan configure stdin -buffering line -blocking 0 - chan event stdin readable $stdinhandler - } - } - - set actual_proc_out_buffering [chan configure $rdout -buffering] - set actual_outchan_buffering [chan configure $outchan -buffering] - #despite whatever is configured - we match our reading to how we need to output - set read_proc_out_buffering $actual_outchan_buffering - - - - if {[string length $teefile]} { - set logname "redir_[string map {: _} $winfile]_[tcl::clock::microseconds]" - set tid [::shellfilter::log::open $logname {-syslog 127.0.0.1:514}] - if {$teefile eq "write"} { - ::shellfilter::log::write $logname "opening '$winfile' for write" - set fd [open $winfile w] - } else { - ::shellfilter::log::write $logname "opening '$winfile' for appending" - set fd [open $winfile a] - } - #chan configure $fd -translation lf - chan configure $fd -translation $outtranslation - chan configure $fd -encoding utf-8 - - set tempvar_bytetotal [namespace current]::totalbytes[tcl::clock::microseconds] - set $tempvar_bytetotal 0 - chan event $rdout readable [list apply {{chan other wrerr outchan errchan read_proc_out_buffering waitfor outprefix call_id debug debugname writefile writefilefd copytempfile bytevar logtag} { - #review - if we write outprefix to normal stdout.. why not to redirected file? - #usefulness of outprefix is dubious - upvar $bytevar totalbytes - if {$read_proc_out_buffering eq "line"} { - #set outchunk [chan read $chan] - set countchunk [chan gets $chan outchunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved - if {$countchunk >= 0} { - if {![chan eof $chan]} { - set numbytes [expr {[string length $outchunk] + 1}] ;#we are assuming \n not \r\n - but count won't/can't be completely accurate(?) - review - puts $writefilefd $outchunk - } else { - set numbytes [string length $outchunk] - puts -nonewline $writefilefd $outchunk - } - incr totalbytes $numbytes - ::shellfilter::log::write $logtag "${outprefix} wrote $numbytes bytes to $writefile" - #puts $outchan "${outprefix} wrote $numbytes bytes to $writefile" - } - } else { - set outchunk [chan read $chan] - if {[string length $outchunk]} { - puts -nonewline $writefilefd $outchunk - set numbytes [string length $outchunk] - incr totalbytes $numbytes - ::shellfilter::log::write $logtag "${outprefix} wrote $numbytes bytes to $writefile" - } - } - if {[chan eof $chan]} { - flush $writefilefd ;#jmn - #set blocking so we can get exit code - chan configure $chan -blocking 1 - catch {::shellfilter::log::write $logtag "${outprefix} total bytes $totalbytes written to $writefile"} - #puts $outchan "${outprefix} total bytes $totalbytes written to $writefile" - catch {close $writefilefd} - if {$copytempfile} { - catch {file copy $writefile "[file rootname $writefile]_copy[file extension $writefile]"} - } - try { - chan close $chan - set ::shellfilter::shellcommandvars($call_id,exitcode) 0 - if {$debug} { - ::shellfilter::log::write $debugname "(teefile) -- child process returned no error. (exit code 0) --" - } - } trap CHILDSTATUS {result options} { - set code [lindex [dict get $options -errorcode] 2] - if {$debug} { - ::shellfilter::log::write $debugname "(teefile) CHILD PROCESS EXITED with code: $code" - } - set ::shellfilter::shellcommandvars($call_id,exitcode) $code - } - catch {chan close $wrerr} - #if {$other ni [chan names]} { - # set $waitfor stdout - #} - if {[catch {chan configure $other}]} { - set $waitfor stdout - } - } - }} $rdout $rderr $wrerr $outchan $errchan $read_proc_out_buffering $waitvar $outprefix $call_id $debug $debugname $winfile $fd $copytempfile $tempvar_bytetotal $logname] - - } else { - - # This occurs when we have outbuffering set to 'line' - as the 'input' from rdout which comes from the executable is also configured to 'line' - # where b:0|1 is whether chan blocked $chan returns 0 or 1 - # pend is the result of chan pending $chan - # eof is the resot of chan eof $chan - - - ##------------------------- - ##If we still read with gets,to retrieve line by line for output to line-buffered output - but the input channel is configured with -buffering none - ## then we can detect the difference - # there is an extra blocking read - but we can stil use eof with data to detect the absent newline and avoid passing an extra one on. - #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl /c cat - #instate b:0 eof:0 pend:-1 count:4 - #test - #instate b:0 eof:0 pend:-1 count:3 - #etc - #instate b:0 eof:1 pend:-1 count:-1 - - #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl /u/c cat - #instate b:0 eof:0 pend:-1 count:4 - #test - #instate b:1 eof:0 pend:-1 count:-1 - #instate b:0 eof:1 pend:-1 count:3 - #etc - ##------------------------ - - - #this should only occur if upstream is coming from stdin reader that has line buffering and hasn't handled the difference properly.. - ###reading with gets from line buffered input with trailing newline - #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl /c cat - #instate b:0 eof:0 pend:-1 count:4 - #test - #instate b:0 eof:0 pend:-1 count:3 - #etc - #instate b:0 eof:1 pend:-1 count:-1 - - ###reading with gets from line buffered input with trailing newline - ##No detectable difference! - #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl /c cat - #instate b:0 eof:0 pend:-1 count:4 - #test - #instate b:0 eof:0 pend:-1 count:3 - #etc - #instate b:0 eof:1 pend:-1 count:-1 - ##------------------------- - - #Note that reading from -buffering none and writing straight out gives no problem because we pass the newlines through as is - - - #set ::shellfilter::chan::lastreadblocked_nodata_noeof($rdout) 0 ;#a very specific case of readblocked prior to eof.. possibly not important - #this detection is disabled for now - but left for debugging in case it means something.. or changes - chan event $rdout readable [list apply {{chan other wrerr outchan errchan read_proc_out_buffering waitfor outprefix call_id debug debugname pids} { - #set outchunk [chan read $chan] - - if {$read_proc_out_buffering eq "line"} { - set countchunk [chan gets $chan outchunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved - #countchunk can be -1 before eof e.g when blocked - #debugging output inline with data - don't leave enabled - #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" - if {$countchunk >= 0} { - if {![chan eof $chan]} { - puts $outchan ${outprefix}$outchunk - } else { - puts -nonewline $outchan ${outprefix}$outchunk - #if {$::shellfilter::chan::lastreadblocked_nodata_noeof($chan)} { - # seems to be the usual case - #} else { - # #false alarm, or ? we've reached eof with data but didn't get an empty blocking read just prior - # #Not known if this occurs - # #debugging output inline with data - don't leave enabled - # puts $outchan "!!!prev read didn't block: instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" - #} - } - #set ::shellfilter::chan::lastreadblocked_nodata_noeof($chan) 0 - } else { - #set ::shellfilter::chan::lastreadblocked_nodata_noeof($chan) [expr {[chan blocked $chan] && ![chan eof $chan]}] - } - } else { - #puts $outchan "read CHANNEL $chan [chan configure $chan]" - #puts $outchan "write CHANNEL $outchan b:[chan configure $outchan -buffering] t:[chan configure $outchan -translation] e:[chan configure $outchan -encoding]" - set outchunk [chan read $chan] - #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:[string length $outchunk]" - if {[string length $outchunk]} { - #set stringrep [encoding convertfrom utf-8 $outchunk] - #set newbytes [encoding convertto utf-16 $stringrep] - #puts -nonewline $outchan $newbytes - puts -nonewline $outchan $outchunk - #jmn test 2025 - flush $outchan - } - } - - if {[chan eof $chan]} { - flush $outchan ;#jmn - #for now just look for first element in the pid list.. - #set subprocesses [tcl::process::list] - #puts stderr "subprocesses: $subprocesses" - #if {[lindex $pids 0] ni $subprocesses} { - # puts stderr "stdout reader pid: [lindex $pids 0] no longer running" - #} else { - # puts stderr "stdout reader pid: [lindex $pids 0] still running" - #} - - #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan]" - chan configure $chan -blocking 1 ;#so we can get exit code - try { - chan close $chan - set ::shellfilter::shellcommandvars($call_id,exitcode) 0 - if {$debug} { - ::shellfilter::log::write $debugname " -- child process returned no error. (exit code 0) --" - } - } trap CHILDSTATUS {result options} { - set code [lindex [dict get $options -errorcode] 2] - set ::shellfilter::shellcommandvars($call_id,exitcode) $code - if {$debug} { - ::shellfilter::log::write $debugname " CHILD PROCESS EXITED with code: $code" - } - } trap CHILDKILLED {result options} { - #set code [lindex [dict get $options -errorcode] 2] - #set ::shellfilter::shellcommandvars(%id%,exitcode) $code - set ::shellfilter::shellcommandvars($call_id,exitcode) "childkilled" - if {$debug} { - ::shellfilter::log::write $debugname " CHILD PROCESS EXITED with result:'$result' options:'$options'" - } - - } finally { - #puts stdout "HERE" - #flush stdout - - } - catch {chan close $wrerr} - #if {$other ni [chan names]} { - # set $waitfor stdout - #} - if {[catch {chan configure $other}]} { - set $waitfor stdout - } - - } - }} $rdout $rderr $wrerr $outchan $errchan $read_proc_out_buffering $waitvar $outprefix $call_id $debug $debugname $command_pids] - } - - #todo - add ability to detect activity/data-flow and change timeout to only apply for period with zero data - #e.g x hrs with no data(?) - #reset timeout when data detected. - #review - stdin??? - set ::shellfilter::shellcommandvars($call_id,timeoutid) [after $timeout [string map [list %cpids% $command_pids %w% $waitvar %id% $call_id %wrerr% $wrerr %rdout% $rdout %rderr% $rderr %debug% $debug %debugname% $debugname] { - if {[info exists ::shellfilter::shellcommandvars(%id%,exitcode)]} { - #killing the task (on windows) doesn't seem to work if done after we close the output channels - catch {puts stderr "timeout - closing.";flush stderr} - set command_pids "{%cpids%}" - if {[llength $command_pids]} { - set pid [lindex $command_pids 0] - if {$::tcl_platform(platform) eq "windows"} { - set killcmd [list [auto_execok taskkill] /F /PID $pid] - } else { - #set killcmd [list kill -9 $pid] - set killcmd [list kill -TERM $pid] - } - if {[catch { - exec {*}$killcmd - } errM]} { - puts stderr "Failed to kill '$pid': errMsg $errM" - flush stderr - } - } - if {[set ::shellfilter::shellcommandvars(%id%,exitcode)] ne ""} { - catch { chan close %wrerr% } - catch { chan close %rdout%} - catch { chan close %rderr%} - } else { - chan configure %rdout% -blocking 1 - try { - chan close %rdout% - set ::shellfilter::shellcommandvars(%id%,exitcode) 0 - if {%debug%} { - ::shellfilter::log::write %debugname% "(timeout) -- child process returned no error. (exit code 0) --" - } - } trap CHILDSTATUS {result options} { - set code [lindex [dict get $options -errorcode] 2] - if {%debug%} { - ::shellfilter::log::write %debugname% "(timeout) CHILD PROCESS EXITED with code: $code" - } - set ::shellfilter::shellcommandvars(%id%,exitcode) $code - } trap CHILDKILLED {result options} { - set code [lindex [dict get $options -errorcode] 2] - #set code [dict get $options -code] - #set ::shellfilter::shellcommandvars(%id%,exitcode) $code - #set ::shellfilter::shellcommandvars($call_id,exitcode) "childkilled-timeout" - set ::shellfilter::shellcommandvars(%id%,exitcode) "childkilled-timeout" - if {%debug%} { - ::shellfilter::log::write %debugname% "(timeout) CHILDKILLED with code: $code" - ::shellfilter::log::write %debugname% "(timeout) result:$result options:$options" - } - - } - catch { chan close %wrerr% } - catch { chan close %rderr%} - } - set %w% "timeout" - } - }]] - - - vwait $waitvar - after cancel $::shellfilter::shellcommandvars($call_id,timeoutid) - - #puts stderr "waitvar:[set $waitvar]" - #flush stderr - #if {[set $waitvar] eq "timeout"} { - # #note: attempting to kill a process here (after channels closed) doesn't work (on windows at least) - # puts stderr "command_pids: $command_pids" - # flush stderr - #} - - set exitcode [set ::shellfilter::shellcommandvars($call_id,exitcode)] - if {![string is digit -strict $exitcode]} { - puts stderr "Process exited with non-numeric code: $exitcode closed_by:[set $waitvar]" - flush stderr - } - if {[string length $teefile]} { - #cannot be called from within an event handler above.. vwait reentrancy etc - catch {::shellfilter::log::close $logname} - } - - if {$debug} { - ::shellfilter::log::write $debugname " closed by: [set $waitvar] with exitcode: $exitcode" - catch {::shellfilter::log::close $debugname} - } - array unset ::shellfilter::shellcommandvars $call_id,* - - - #restore buffering to pre shellfilter::run state - lassign $remember_in_out_err_buffering bin bout berr - chan configure $inchan -buffering $bin - chan configure $outchan -buffering $bout - chan configure $errchan -buffering $berr - - lassign $remember_in_out_err_translation tin tout terr - chan configure $inchan -translation $tin - chan configure $outchan -translation $tout - chan configure $errchan -translation $terr - - - #in channel probably closed..(? review - should it be?) - catch { - chan configure $inchan -buffering $bin - } - - - return [list exitcode $exitcode] - } - -} - -package provide shellfilter [namespace eval shellfilter { - variable version - set version 0.2.1 -}] diff --git a/src/vfs/_vfscommon.vfs/modules/shellfilter-0.2.tm b/src/vfs/_vfscommon.vfs/modules/shellfilter-0.2.tm deleted file mode 100644 index 8017d3f5..00000000 --- a/src/vfs/_vfscommon.vfs/modules/shellfilter-0.2.tm +++ /dev/null @@ -1,3347 +0,0 @@ -#copyright 2023 Julian Marcel Noble -#license: BSD (revised 3-clause) -# -#Note shellfilter is currently only directly useful for unidirectional channels e.g stdin,stderr,stdout, or for example fifo2 where only one direction is being used. -#To generalize this to bidrectional channels would require shifting around read & write methods on transform objects in a very complicated manner. -#e.g each transform would probably be a generic transform container which holds sub-objects to which read & write are indirected. -#This is left as a future exercise...possibly it's best left as a concept for uni-directional channels anyway -# - as presumably the reads/writes from a bidirectional channel could be diverted off to unidirectional pipelines for processing with less work -# (and maybe even better speed/efficiency if the data volume is asymmetrical and there is significant processing on one direction) -# - - -tcl::namespace::eval shellfilter::log { - variable allow_adhoc_tags 1 - variable open_logs [tcl::dict::create] - variable is_enabled 0 - - proc disable {} { - variable is_enabled - set is_enabled 0 - proc ::shellfilter::log::open {tag settingsdict} {} - proc ::shellfilter::log::write {tag msg} {} - proc ::shellfilter::log::write_sync {tag msg} {} - proc ::shellfilter::log::close {tag} {} - } - - proc enable {} { - variable is_enabled - set is_enabled 1 - #'tag' is an identifier for the log source. - # each tag will use it's own thread to write to the configured log target - proc ::shellfilter::log::open {tag {settingsdict {}}} { - upvar ::shellfilter::sources sourcelist - if {![dict exists $settingsdict -tag]} { - tcl::dict::set settingsdict -tag $tag - } else { - #review - if {$tag ne [tcl::dict::get $settingsdict -tag]} { - error "shellfilter::log::open first argument tag: '$tag' does not match -tag '[tcl::dict::get $settingsdict -tag]' omit -tag, or supply same value" - } - } - if {$tag ni $sourcelist} { - lappend sourcelist $tag - } - - #note new_worker - set worker_tid [shellthread::manager::new_worker $tag $settingsdict] - #puts stderr "shellfilter::log::open this_threadid: [thread::id] tag: $tag worker_tid: $worker_tid" - return $worker_tid - } - proc ::shellfilter::log::write {tag msg} { - upvar ::shellfilter::sources sourcelist - variable allow_adhoc_tags - if {!$allow_adhoc_tags} { - if {$tag ni $sourcelist} { - error "shellfilter::log::write tag '$tag' hasn't been initialised with a call to shellfilter::log::open $tag , and allow_adhoc_tags has been set false. use shellfilter::log::require_open false to allow adhoc tags" - } - } - shellthread::manager::write_log $tag $msg - } - #write_sync - synchronous processing with logging thread, slower but potentially useful for debugging/testing or forcing delay til log written - proc ::shellfilter::log::write_sync {tag msg} { - shellthread::manager::write_log $tag $msg -async 0 - } - proc ::shellfilter::log::close {tag} { - #shellthread::manager::close_worker $tag - shellthread::manager::unsubscribe [list $tag]; #workertid will be added back to free list if no tags remain subscribed - } - - } - - #review - #configure whether we can call shellfilter::log::write without having called open first - proc require_open {{is_open_required {}}} { - variable allow_adhoc_tags - if {![string length $is_open_required]} { - return $allow_adhoc_tags - } else { - set truevalues [list y yes true 1] - set falsevalues [list n no false 0] - if {[string tolower $is_open_required] in $truevalues} { - set allow_adhoc_tags 1 - } elseif {[string tolower $is_open_required] in $falsevalues} { - set allow_adhoc_tags 0 - } else { - error "shellfilter::log::require_open unrecognised value '$is_open_required' try one of $truevalues or $falsevalues" - } - } - } - if {[catch {package require shellthread}]} { - shellfilter::log::disable - } else { - shellfilter::log::enable - } - -} -namespace eval shellfilter::pipe { - #write channel for program. workerthread reads other end of fifo2 and writes data somewhere - proc open_out {tag_pipename {pipesettingsdict {}}} { - set defaultsettings {-buffering full} - set settingsdict [dict merge $defaultsettings $pipesettingsdict] - package require shellthread - #we are only using the fifo in a single direction to pipe to another thread - # - so whilst wchan and rchan could theoretically each be both read & write we're only using them for one operation each - if {![catch {package require Memchan}]} { - lassign [fifo2] wchan rchan - } else { - package require tcl::chan::fifo2 - lassign [tcl::chan::fifo2] wchan rchan - } - #default -translation for both types of fifo on windows is {auto crlf} - # -encoding is as per '[encoding system]' on the platform - e.g utf-8 (e.g windows when beta-utf8 enabled) - chan configure $wchan -buffering [dict get $settingsdict -buffering] ;# - #application end must not be binary for our filters to operate on it - - - #chan configure $rchan -buffering [dict get $settingsdict -buffering] -translation binary ;#works reasonably.. - chan configure $rchan -buffering [dict get $settingsdict -buffering] -translation lf - - set worker_tid [shellthread::manager::new_pipe_worker $tag_pipename $settingsdict] - #puts stderr "worker_tid: $worker_tid" - - #set_read_pipe does the thread::transfer of the rchan end. -buffering setting is maintained during thread transfer - shellthread::manager::set_pipe_read_from_client $tag_pipename $worker_tid $rchan - - set pipeinfo [list localchan $wchan remotechan $rchan workertid $worker_tid direction out] - return $pipeinfo - } - - #read channel for program. workerthread writes to other end of fifo2 from whereever it's reading (stdin, file?) - proc open_in {tag_pipename {settingsdict {} }} { - package require shellthread - package require tcl::chan::fifo2 - lassign [tcl::chan::fifo2] wchan rchan - set program_chan $rchan - set worker_chan $wchan - chan configure $worker_chan -buffering [dict get $settingsdict -buffering] - chan configure $program_chan -buffering [dict get $settingsdict -buffering] - - chan configure $program_chan -blocking 0 - chan configure $worker_chan -blocking 0 - set worker_tid [shellthread::manager::new_worker $tag_pipename $settingsdict] - - shellthread::manager::set_pipe_write_to_client $tag_pipename $worker_tid $worker_chan - - set pipeinfo [list localchan $program_chan remotechan $worker_chan workertid $worker_tid direction in] - puts stderr "|jn>pipe::open_in returning $pipeinfo" - puts stderr "program_chan: [chan conf $program_chan]" - return $pipeinfo - } - -} - - -namespace eval shellfilter::chan { - set testobj ::shellfilter::chan::var - if {$testobj ni [info commands $testobj]} { - - oo::class create var { - variable o_datavar - variable o_trecord - variable o_enc - variable o_is_junction - constructor {tf} { - set o_trecord $tf - set o_enc [dict get $tf -encoding] - set settingsdict [dict get $tf -settings] - set varname [dict get $settingsdict -varname] - set o_datavar $varname - if {[dict exists $tf -junction]} { - set o_is_junction [dict get $tf -junction] - } else { - set o_is_junction 1 ;# as a var is diversionary - default it to be a jucntion - } - } - method initialize {ch mode} { - return [list initialize finalize write] - } - method finalize {ch} { - my destroy - } - method watch {ch events} { - # must be present but we ignore it because we do not - # post any events - } - #method read {ch count} { - # return ? - #} - method write {ch bytes} { - set stringdata [encoding convertfrom $o_enc $bytes] - append $o_datavar $stringdata - return "" - } - method meta_is_redirection {} { - return $o_is_junction - } - method meta_buffering_supported {} { - return [list line full none] - } - } - - #todo - something similar for multiple grep specs each with own -pre & -post .. store to dict? - oo::class create tee_grep_to_var { - variable o_datavar - variable o_lastxlines - variable o_trecord - variable o_grepfor - variable o_prelines - variable o_postlines - variable o_postcountdown - variable o_enc - variable o_is_junction - constructor {tf} { - set o_trecord $tf - set o_enc [tcl::dict::get $tf -encoding] - set o_lastxlines [list] - set o_postcountdown 0 - set defaults [tcl::dict::create -pre 1 -post 1] - set settingsdict [tcl::dict::get $tf -settings] - set settings [tcl::dict::merge $defaults $settingsdict] - set o_datavar [tcl::dict::get $settings -varname] - set o_grepfor [tcl::dict::get $settings -grep] - set o_prelines [tcl::dict::get $settings -pre] - set o_postlines [tcl::dict::get $settings -post] - if {[tcl::dict::exists $tf -junction]} { - set o_is_junction [tcl::dict::get $tf -junction] - } else { - set o_is_junction 0 - } - } - method initialize {transform_handle mode} { - return [list initialize finalize write] - } - method finalize {transform_handle} { - my destroy - } - method watch {transform_handle events} { - } - #method read {transform_handle count} { - # return ? - #} - method write {transform_handle bytes} { - set logdata [tcl::encoding::convertfrom $o_enc $bytes] - set lastx $o_lastxlines - lappend o_lastxlines $logdata - - if {$o_postcountdown > 0} { - append $o_datavar $logdata - if {[regexp $o_grepfor $logdata]} { - #another match in postlines - set o_postcountdown $o_postlines - } else { - incr o_postcountdown -1 - } - } else { - if {[regexp $o_grepfor $logdata]} { - append $o_datavar [join $lastx] - append $o_datavar $logdata - set o_postcountdown $o_postlines - } - } - - if {[llength $o_lastxlines] > $o_prelines} { - set o_lastxlines [lrange $o_lastxlines 1 end] - } - return $bytes - } - method meta_is_redirection {} { - return $o_is_junction - } - method meta_buffering_supported {} { - return [list line] - } - } - - oo::class create tee_to_var { - variable o_datavars - variable o_trecord - variable o_enc - variable o_encbuf - variable o_is_junction - constructor {tf} { - set o_trecord $tf - set o_enc [tcl::dict::get $tf -encoding] - set o_encbuf "" - set settingsdict [tcl::dict::get $tf -settings] - set varname [tcl::dict::get $settingsdict -varname] - set o_datavars $varname - if {[tcl::dict::exists $tf -junction]} { - set o_is_junction [tcl::dict::get $tf -junction] - } else { - set o_is_junction 0 - } - } - method initialize {ch mode} { - return [list initialize finalize write flush clear] - } - method finalize {ch} { - my destroy - } - method clear {ch} { - return - } - method watch {ch events} { - # must be present but we ignore it because we do not - # post any events - } - #method read {ch count} { - # return ? - #} - #method flush {ch} { - # return "" - #} - method flush {transform_handle} { - #puts stdout "" - #review - just clear o_encbuf and emit nothing? - #we wouldn't have a value there if it was convertable from the channel encoding? - set clear $o_encbuf - set o_encbuf "" - return $o_encbuf - } - method write {ch bytes} { - #test with set x [string repeat " \U1f6c8" 2043] - #or - #test with set x [string repeat " \U1f6c8" 683] - #most windows terminals (at least) may emit two unrecognised chars "??" at the end - - #Our goal with the while loop here is to avoid encoding conversion errors - #the source of the bogus chars in terminals is unclear. - #Alacritty on windows doesn't seem to have the problem, but wezterm,cmd,windows terminal do. - - #set stringdata [tcl::encoding::convertfrom $o_enc $bytes] - set inputbytes $o_encbuf$bytes - set o_encbuf "" - set tail_offset 0 - while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { - incr tail_offset - } - if {$tail_offset > 0} { - if {$tail_offset < [string length $inputbytes]} { - #stringdata from catch statement must be a valid result - set t [expr {$tail_offset - 1}] - set o_encbuf [string range $inputbytes end-$t end] - } else { - set stringdata "" - set o_encbuf $inputbytes - return "" - } - } - - foreach v $o_datavars { - append $v $stringdata - } - #return $bytes - return [string range $inputbytes 0 end-$tail_offset] - } - method meta_is_redirection {} { - return $o_is_junction - } - } - oo::class create tee_to_pipe { - variable o_logsource - variable o_localchan - variable o_enc - variable o_encbuf - variable o_trecord - variable o_is_junction - constructor {tf} { - set o_trecord $tf - set o_enc [tcl::dict::get $tf -encoding] - set o_encbuf "" - set settingsdict [tcl::dict::get $tf -settings] - if {![dict exists $settingsdict -tag]} { - error "tee_to_pipe constructor settingsdict missing -tag" - } - set o_localchan [tcl::dict::get $settingsdict -pipechan] - set o_logsource [tcl::dict::get $settingsdict -tag] - if {[tcl::dict::exists $tf -junction]} { - set o_is_junction [tcl::dict::get $tf -junction] - } else { - set o_is_junction 0 - } - } - method initialize {transform_handle mode} { - return [list initialize read drain write flush clear finalize] - } - method finalize {transform_handle} { - ::shellfilter::log::close $o_logsource - my destroy - } - method watch {transform_handle events} { - # must be present but we ignore it because we do not - # post any events - } - method clear {transform_handle} { - return - } - method drain {transform_handle} { - return "" - } - method read {transform_handle bytes} { - set logdata [tcl::encoding::convertfrom $o_enc $bytes] - #::shellfilter::log::write $o_logsource $logdata - puts -nonewline $o_localchan $logdata - return $bytes - } - method flush {transform_handle} { - #return "" - set clear $o_encbuf - set o_encbuf "" - return $o_encbuf - } - method write {transform_handle bytes} { - #set logdata [tcl::encoding::convertfrom $o_enc $bytes] - set inputbytes $o_encbuf$bytes - set o_encbuf "" - set tail_offset 0 - while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { - incr tail_offset - } - if {$tail_offset > 0} { - if {$tail_offset < [string length $inputbytes]} { - #stringdata from catch statement must be a valid result - set t [expr {$tail_offset - 1}] - set o_encbuf [string range $inputbytes end-$t end] - } else { - set stringdata "" - set o_encbuf $inputbytes - return "" - } - } - #::shellfilter::log::write $o_logsource $logdata - puts -nonewline $o_localchan $stringdata - #return $bytes - return [string range $inputbytes 0 end-$tail_offset] - } - #a tee is not a redirection - because data still flows along the main path - method meta_is_redirection {} { - return $o_is_junction - } - - } - oo::class create tee_to_log { - variable o_tid - variable o_logsource - variable o_trecord - variable o_enc - variable o_encbuf - variable o_is_junction - constructor {tf} { - set o_trecord $tf - set o_enc [tcl::dict::get $tf -encoding] - set o_encbuf "" - set settingsdict [tcl::dict::get $tf -settings] - if {![tcl::dict::exists $settingsdict -tag]} { - error "tee_to_log constructor settingsdict missing -tag" - } - set o_logsource [tcl::dict::get $settingsdict -tag] - set o_tid [::shellfilter::log::open $o_logsource $settingsdict] - if {[tcl::dict::exists $tf -junction]} { - set o_is_junction [tcl::dict::get $tf -junction] - } else { - set o_is_junction 0 - } - } - method initialize {ch mode} { - return [list initialize read write flush finalize] - } - method finalize {ch} { - ::shellfilter::log::close $o_logsource - my destroy - } - method watch {ch events} { - # must be present but we ignore it because we do not - # post any events - } - method read {ch bytes} { - set logdata [tcl::encoding::convertfrom $o_enc $bytes] - ::shellfilter::log::write $o_logsource $logdata - return $bytes - } - method flush {transform_handle} { - #return "" - set clear $o_encbuf - set o_encbuf "" - return $o_encbuf - } - method write {ch bytes} { - #set logdata [tcl::encoding::convertfrom $o_enc $bytes] - set inputbytes $o_encbuf$bytes - set o_encbuf "" - set tail_offset 0 - while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { - incr tail_offset - } - if {$tail_offset > 0} { - if {$tail_offset < [string length $inputbytes]} { - #stringdata from catch statement must be a valid result - set t [expr {$tail_offset - 1}] - set o_encbuf [string range $inputbytes end-$t end] - } else { - set stringdata "" - set o_encbuf $inputbytes - return "" - } - } - ::shellfilter::log::write $o_logsource $logdata - #return $bytes - return [string range $inputbytes 0 end-$tail_offset] - } - method meta_is_redirection {} { - return $o_is_junction - } - } - - - oo::class create logonly { - variable o_tid - variable o_logsource - variable o_trecord - variable o_enc - variable o_encbuf - constructor {tf} { - set o_trecord $tf - set o_enc [dict get $tf -encoding] - set o_encbuf "" - set settingsdict [dict get $tf -settings] - if {![dict exists $settingsdict -tag]} { - error "logonly constructor settingsdict missing -tag" - } - set o_logsource [dict get $settingsdict -tag] - set o_tid [::shellfilter::log::open $o_logsource $settingsdict] - } - method initialize {transform_handle mode} { - return [list initialize finalize write] - } - method finalize {transform_handle} { - ::shellfilter::log::close $o_logsource - my destroy - } - method watch {transform_handle events} { - # must be present but we ignore it because we do not - # post any events - } - #method read {transform_handle count} { - # return ? - #} - method write {transform_handle bytes} { - #set logdata [encoding convertfrom $o_enc $bytes] - set inputbytes $o_encbuf$bytes - set o_encbuf "" - set tail_offset 0 - while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { - incr tail_offset - } - if {$tail_offset > 0} { - if {$tail_offset < [string length $inputbytes]} { - #stringdata from catch statement must be a valid result - set t [expr {$tail_offset - 1}] - set o_encbuf [string range $inputbytes end-$t end] - } else { - set stringdata "" - set o_encbuf $inputbytes - return - } - } - - #::shellfilter::log::write_sync $o_logsource $logdata - ::shellfilter::log::write $o_logsource $stringdata - return - } - method meta_is_redirection {} { - return 1 - } - } - - #review - we should probably provide a more narrow filter than only strips color - and one that strips most(?) - # - but does it ever really make sense to strip things like "esc(0" and "esc(B" which flip to the G0 G1 characters? (once stripped - things like box-lines become ordinary letters - unlikely to be desired?) - #punk::ansi::ansistrip converts at least some of the box drawing G0 chars to unicode - todo - more complete conversion - #assumes line-buffering. a more advanced filter required if ansicodes can arrive split across separate read or write operations! - oo::class create ansistrip { - variable o_trecord - variable o_enc - variable o_encbuf - variable o_is_junction - constructor {tf} { - package require punk::ansi - set o_trecord $tf - set o_enc [dict get $tf -encoding] - set o_encbuf "" - if {[dict exists $tf -junction]} { - set o_is_junction [dict get $tf -junction] - } else { - set o_is_junction 0 - } - } - method initialize {transform_handle mode} { - return [list initialize read write clear flush drain finalize] - } - method finalize {transform_handle} { - my destroy - } - method clear {transform_handle} { - return - } - method watch {transform_handle events} { - } - method drain {transform_handle} { - return "" - } - method read {transform_handle bytes} { - set instring [encoding convertfrom $o_enc $bytes] - set outstring [punk::ansi::ansistrip $instring] - return [encoding convertto $o_enc $outstring] - } - method flush {transform_handle} { - return "" - } - #method write {transform_handle bytes} { - # #broken due to occasional unexpected byte sequence - # set instring [encoding convertfrom $o_enc $bytes] - # set outstring [punk::ansi::ansistrip $instring] - # return [encoding convertto $o_enc $outstring] - #} - method write {transform_handle bytes} { - #set instring [tcl::encoding::convertfrom $o_enc $bytes] ;naive approach will break due to unexpected byte sequence - occasionally - #bytes can break at arbitrary points making encoding conversions invalid. - - set inputbytes $o_encbuf$bytes - set o_encbuf "" - set tail_offset 0 - while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { - incr tail_offset - } - if {$tail_offset > 0} { - if {$tail_offset < [string length $inputbytes]} { - #stringdata from catch statement must be a valid result - set t [expr {$tail_offset - 1}] - set o_encbuf [string range $inputbytes end-$t end] - } else { - set stringdata "" - set o_encbuf $inputbytes - return "" - } - } - - set outstring [punk::ansi::ansistrip $stringdata] - return [tcl::encoding::convertto $o_enc $outstring] - } - method meta_is_redirection {} { - return $o_is_junction - } - } - - #a test - oo::class create reconvert { - variable o_trecord - variable o_enc - constructor {tf} { - set o_trecord $tf - set o_enc [dict get $tf -encoding] - } - method initialize {transform_handle mode} { - return [list initialize read write finalize] - } - method finalize {transform_handle} { - my destroy - } - method watch {transform_handle events} { - } - method read {transform_handle bytes} { - set instring [encoding convertfrom $o_enc $bytes] - - set outstring $instring - - return [encoding convertto $o_enc $outstring] - } - method write {transform_handle bytes} { - set instring [encoding convertfrom $o_enc $bytes] - - set outstring $instring - - return [encoding convertto $o_enc $outstring] - } - } - oo::define reconvert { - method meta_is_redirection {} { - return 0 - } - } - - - #this isn't a particularly nice thing to do to a stream - especially if someone isn't expecting ansi codes sprinkled through it. - #It can be useful for test/debugging - #Due to chunking at random breaks - we have to check if an ansi code in the underlying stream has been split - otherwise our wrapping will break the existing ansi - # - set sixelstart_re {\x1bP([;0-9]*)q} ;#7-bit - todo 8bit - #todo kitty graphics \x1b_G... - #todo iterm graphics - - oo::class create ansiwrap { - variable o_trecord - variable o_enc - variable o_encbuf ;#buffering for partial encoding bytes - variable o_colour - variable o_do_colour - variable o_do_colourlist - variable o_do_normal - variable o_is_junction - variable o_codestack - variable o_gx_state ;#on/off alt graphics - variable o_buffered ;#buffering for partial ansi codes - constructor {tf} { - package require punk::ansi - set o_trecord $tf - set o_enc [tcl::dict::get $tf -encoding] - set settingsdict [tcl::dict::get $tf -settings] - if {[tcl::dict::exists $settingsdict -colour]} { - set o_colour [tcl::dict::get $settingsdict -colour] - #warning - we can't merge certain extended attributes such as undercurly into single SGR escape sequence - #while some terminals may handle these extended attributes even when merged - we need to cater for those that - #don't. Keeping them as a separate escape allows terminals that don't handle them to ignore just that code without - #affecting the interpretation of the other codes. - set o_do_colour [punk::ansi::a+ {*}$o_colour] - set o_do_colourlist [punk::ansi::ta::get_codes_single $o_do_colour] - set o_do_normal [punk::ansi::a] - } else { - set o_colour {} - set o_do_colour "" - set o_do_colourlist {} - set o_do_normal "" - } - set o_codestack [list] - set o_gx_state [expr {off}] - set o_encbuf "" - set o_buffered "" ;#hold back data that potentially contains partial ansi codes - if {[tcl::dict::exists $tf -junction]} { - set o_is_junction [tcl::dict::get $tf -junction] - } else { - set o_is_junction 0 - } - } - - - #todo - track when in sixel,iterm,kitty graphics data - can be very large - method Trackcodes {chunk} { - #note - caller can use 2 resets in a single unit to temporarily reset to no sgr (override ansiwrap filter) - #e.g [a+ reset reset] (0;0m vs 0;m) - - #puts stdout "===[ansistring VIEW -lf 1 $o_buffered]" - set buf $o_buffered$chunk - set emit "" - if {[string last \x1b $buf] >= 0} { - #detect will detect ansi SGR and gron groff and other codes - #REVIEW - ta::detect won't detect SOS without paired ST for things like PM - # ta::detectcode will - but then split_codes_single will treat unpaired SOS as text? - if {[punk::ansi::ta::detect $buf]} { - #split_codes_single regex faster than split_codes - but more resulting parts - #'single' refers to number of escapes - but can still contain e.g multiple SGR codes (or mode set operations etc) - set parts [punk::ansi::ta::split_codes_single $buf] - #process all pt/code pairs except for trailing pt - foreach {pt code} [lrange $parts 0 end-1] { - #puts "<==[ansistring VIEW -lf 1 $pt]==>" - switch -- [llength $o_codestack] { - 0 { - append emit $o_do_colour$pt$o_do_normal - } - 1 { - if {[punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]} { - append emit $o_do_colour$pt$o_do_normal - set o_codestack [list] - } else { - #append emit [lindex $o_codestack 0]$pt - append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$pt - } - } - default { - append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$pt - } - } - #if {( ![llength $o_codestack] || ([llength $o_codestack] == 1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]))} { - # append emit $o_do_colour$pt$o_do_normal - # #append emit $pt - #} else { - # append emit $pt - #} - - set c1c2 [tcl::string::range $code 0 1] - set leadernorm [tcl::string::range [tcl::string::map [list\ - \x1b\[ 7CSI\ - \x9b 8CSI\ - \x1b\( 7GFX\ - ] $c1c2] 0 3] - switch -- $leadernorm { - 7CSI - 8CSI { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set o_codestack [list "\x1b\[m"] - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set o_codestack [list $code] - } elseif {[punk::ansi::codetype::is_sgr $code]} { - #todo - make caching is_sgr method - set dup_posns [lsearch -all -exact $o_codestack $code] - set o_codestack [lremove $o_codestack {*}$dup_posns] - lappend o_codestack $code - } else { - - } - } - 7GFX { - switch -- [tcl::string::index $code 2] { - "0" { - set o_gx_state on - } - "B" { - set o_gx_state off - } - } - } - default { - #other ansi codes - } - } - append emit $code - } - - - set trailing_pt [lindex $parts end] - if {[string first \x1b $trailing_pt] >= 0} { - #puts stdout "...[ansistring VIEW -lf 1 $trailing_pt]...buffered:<[ansistring VIEW $o_buffered]> '[ansistring VIEW -lf 1 $emit]'" - #may not be plaintext after all - set o_buffered $trailing_pt - #puts stdout "=-=[ansistring VIEWCODES $o_buffered]" - } else { - #puts [a+ yellow]???[ansistring VIEW "'$o_buffered'<+>'$trailing_pt'"]???[a] - switch -- [llength $o_codestack] { - 0 { - append emit $o_do_colour$trailing_pt$o_do_normal - } - 1 { - if {[punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]} { - append emit $o_do_colour$trailing_pt$o_do_normal - set o_codestack [list] - } else { - #append emit [lindex $o_codestack 0]$trailing_pt - append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$trailing_pt - } - } - default { - append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$trailing_pt - } - } - #if {![llength $o_codestack] || ([llength $o_codestack] ==1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]])} { - # append emit $o_do_colour$trailing_pt$o_do_normal - #} else { - # append emit $trailing_pt - #} - #the previous o_buffered formed the data we emitted - nothing new to buffer because we emitted all parts including the trailing plaintext - set o_buffered "" - } - - - } else { - #REVIEW - this holding a buffer without emitting as we go is ugly. - # - we may do better to detect and retain the opener, then use that opener to avoid false splits within the sequence. - # - we'd then need to detect the appropriate close to restart splitting and codestacking - # - we may still need to retain and append the data to the opener (in some cases?) - which is a slight memory issue - but at least we would emit everything immediately. - - - #puts "-->esc but no detect" - #no complete ansi codes - but at least one esc is present - if {[string index $buf end] eq "\x1b" && [string first \x1b $buf] == [string length $buf]-1} { - #string index in first part of && clause to avoid some unneeded scans of whole string for this test - #we can't use 'string last' - as we need to know only esc is last char in buf - #puts ">>trailing-esc<<" - set o_buffered \x1b - set emit $o_do_colour[string range $buf 0 end-1]$o_do_normal - #set emit [string range $buf 0 end-1] - set buf "" - } else { - set emit_anyway 0 - #todo - ensure non-ansi escapes in middle of chunks don't lead to ever growing buffer - if {[punk::ansi::ta::detect_st_open $buf]} { - #no detect - but we have an ST open (privacy msg etc) - allow a larger chunk before we give up - could include newlines (and even nested codes - although not widely interpreted that way in terms) - set st_partial_len [expr {[string length $buf] - [string last \x1b $buf]}] ;#length of unclosed ST code - #todo - configurable ST max - use 1k for now - if {$st_partial_len < 1001} { - append o_buffered $chunk - set emit "" - set buf "" - } else { - set emit_anyway 1 - set o_buffered "" - } - } else { - set possible_code_len [expr {[string length $buf] - [string last \x1b $buf]}] ;#length of possible code - #most opening sequences are 1,2 or 3 chars - review? - set open_sequence_detected [punk::ansi::ta::detect_open $buf] - if {$possible_code_len > 10 && !$open_sequence_detected} { - set emit_anyway 1 - set o_buffered "" - } else { - #could be composite sequence with params - allow some reasonable max sequence length - #todo - configurable max sequence length - #len 40-50 quite possible for SGR sequence using coloured underlines etc, even without redundancies - # - allow some headroom for redundant codes when the caller didn't merge. - if {$possible_code_len < 101} { - append o_buffered $chunk - set buf "" - set emit "" - } else { - #allow a little more grace if we at least have an opening ansi sequence of any type.. - if {$open_sequence_detected && $possible_code_len < 151} { - append o_buffered $chunk - set buf "" - set emit "" - } else { - set emit_anyway 1 - set o_buffered "" - } - } - } - } - if {$emit_anyway} { - #assert: any time emit_anyway == 1 buf already contains all of previous o_buffered and o_buffered has been cleared. - - #looked ansi-like - but we've given enough length without detecting close.. - #treat as possible plain text with some esc or unrecognised ansi sequence - switch -- [llength $o_codestack] { - 0 { - set emit $o_do_colour$buf$o_do_normal - } - 1 { - if {[punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]} { - set emit $o_do_colour$buf$o_do_normal - set o_codestack [list] - } else { - #set emit [lindex $o_codestack 0]$buf - set emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$buf - } - } - default { - #set emit [punk::ansi::codetype::sgr_merge_singles $o_codestack]$buf - set emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$buf - } - } - #if {( ![llength $o_codestack] || ([llength $o_codestack] == 1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]))} { - # set emit $o_do_colour$buf$o_do_normal - #} else { - # set emit $buf - #} - } - } - } - } else { - #no esc - #puts stdout [a+ yellow]...[a] - #test! - switch -- [llength $o_codestack] { - 0 { - set emit $o_do_colour$buf$o_do_normal - } - 1 { - if {[punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]} { - set emit $o_do_colour$buf$o_do_normal - set o_codestack [list] - } else { - #set emit [lindex $o_codestack 0]$buf - set emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$buf - } - } - default { - #set emit [punk::ansi::codetype::sgr_merge_singles $o_codestack]$buf - set emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$buf - } - } - set o_buffered "" - } - return [dict create emit $emit stacksize [llength $o_codestack]] - } - method initialize {transform_handle mode} { - #clear undesirable in terminal output channels (review) - return [list initialize write flush read drain finalize] - } - method finalize {transform_handle} { - my destroy - } - method watch {transform_handle events} { - } - method clear {transform_handle} { - #In the context of stderr/stdout - we probably don't want clear to run. - #Terminals might call it in the middle of a split ansi code - resulting in broken output. - #Leave clear of it the init call - puts stdout "" - set emit [tcl::encoding::convertto $o_enc $o_buffered] - set o_buffered "" - return $emit - } - method flush {transform_handle} { - #puts stdout "" - set inputbytes $o_buffered$o_encbuf - set emit [tcl::encoding::convertto $o_enc $inputbytes] - set o_buffered "" - set o_encbuf "" - return $emit - } - method write {transform_handle bytes} { - #set instring [tcl::encoding::convertfrom $o_enc $bytes] ;naive approach will break due to unexpected byte sequence - occasionally - #bytes can break at arbitrary points making encoding conversions invalid. - - set inputbytes $o_encbuf$bytes - set o_encbuf "" - set tail_offset 0 - while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { - incr tail_offset - } - if {$tail_offset > 0} { - if {$tail_offset < [string length $inputbytes]} { - #stringdata from catch statement must be a valid result - set t [expr {$tail_offset - 1}] - set o_encbuf [string range $inputbytes end-$t end] - } else { - set stringdata "" - set o_encbuf $inputbytes - return "" - } - } - set streaminfo [my Trackcodes $stringdata] - set emit [dict get $streaminfo emit] - - #review - wrapping already done in Trackcodes - #if {[dict get $streaminfo stacksize] == 0} { - # #no ansi on the stack - we can wrap - # #review - # set outstring "$o_do_colour$emit$o_do_normal" - #} else { - #} - #if {[llength $o_codestack]} { - # set outstring [punk::ansi::codetype::sgr_merge_singles $o_codestack]$emit - #} else { - # set outstring $emit - #} - #set outstring $emit - - #puts stdout "decoded >>>[ansistring VIEWCODES $outstring]<<<" - #puts stdout "re-encoded>>>[ansistring VIEW [tcl::encoding::convertto $o_enc $outstring]]<<<" - return [tcl::encoding::convertto $o_enc $emit] - } - method Write_naive {transform_handle bytes} { - set instring [tcl::encoding::convertfrom $o_enc $bytes] - set outstring "$o_do_colour$instring$o_do_normal" - #set outstring ">>>$instring" - return [tcl::encoding::convertto $o_enc $outstring] - } - method drain {transform_handle} { - return "" - } - method read {transform_handle bytes} { - set instring [tcl::encoding::convertfrom $o_enc $bytes] - set outstring "$o_do_colour$instring$o_do_normal" - return [tcl::encoding::convertto $o_enc $outstring] - } - method meta_is_redirection {} { - return $o_is_junction - } - } - #todo - something - oo::class create rebuffer { - variable o_trecord - variable o_enc - constructor {tf} { - set o_trecord $tf - set o_enc [dict get $tf -encoding] - } - method initialize {transform_handle mode} { - return [list initialize read write finalize] - } - method finalize {transform_handle} { - my destroy - } - method watch {transform_handle events} { - } - method read {transform_handle bytes} { - set instring [encoding convertfrom $o_enc $bytes] - - set outstring $instring - - return [encoding convertto $o_enc $outstring] - } - method write {transform_handle bytes} { - set instring [encoding convertfrom $o_enc $bytes] - - #set outstring [string map [list \n ] $instring] - set outstring $instring - - return [encoding convertto $o_enc $outstring] - #return [encoding convertto utf-16le $outstring] - } - } - oo::define rebuffer { - method meta_is_redirection {} { - return 0 - } - } - - #has slight buffering/withholding of lone training cr - we can't be sure that a cr at end of chunk is part of \r\n sequence - oo::class create tounix { - variable o_trecord - variable o_enc - variable o_last_char_was_cr - variable o_is_junction - constructor {tf} { - set o_trecord $tf - set o_enc [dict get $tf -encoding] - set settingsdict [dict get $tf -settings] - if {[dict exists $tf -junction]} { - set o_is_junction [dict get $tf -junction] - } else { - set o_is_junction 0 - } - set o_last_char_was_cr 0 - } - method initialize {transform_handle mode} { - return [list initialize write finalize] - } - method finalize {transform_handle} { - my destroy - } - method watch {transform_handle events} { - } - #don't use read - method read {transform_handle bytes} { - set instring [encoding convertfrom $o_enc $bytes] - - set outstring $instring - - return [encoding convertto $o_enc $outstring] - } - method write {transform_handle bytes} { - set instring [encoding convertfrom $o_enc $bytes] - #set outstring [string map [list \n ] $instring] - - if {$o_last_char_was_cr} { - set instring "\r$instring" - } - - set outstring [string map {\r\n \n} $instring] - set lastchar [string range $outstring end end] - if {$lastchar eq "\r"} { - set o_last_char_was_cr 1 - set outstring [string range $outstring 0 end-1] - } else { - set o_last_char_was_cr 0 - } - #review! can we detect eof here on the transform_handle? - #if eof, we don't want to strip a trailing \r - - return [encoding convertto $o_enc $outstring] - #return [encoding convertto utf-16le $outstring] - } - } - oo::define tounix { - method meta_is_redirection {} { - return $o_is_junction - } - } - #write to handle case where line-endings already \r\n too - oo::class create towindows { - variable o_trecord - variable o_enc - variable o_last_char_was_cr - variable o_is_junction - constructor {tf} { - set o_trecord $tf - set o_enc [dict get $tf -encoding] - set settingsdict [dict get $tf -settings] - if {[dict exists $tf -junction]} { - set o_is_junction [dict get $tf -junction] - } else { - set o_is_junction 0 - } - set o_last_char_was_cr 0 - } - method initialize {transform_handle mode} { - return [list initialize write finalize] - } - method finalize {transform_handle} { - my destroy - } - method watch {transform_handle events} { - } - #don't use read - method read {transform_handle bytes} { - set instring [encoding convertfrom $o_enc $bytes] - - set outstring $instring - - return [encoding convertto $o_enc $outstring] - } - method write {transform_handle bytes} { - set instring [encoding convertfrom $o_enc $bytes] - #set outstring [string map [list \n ] $instring] - - if {$o_last_char_was_cr} { - set instring "\r$instring" - } - - set outstring [string map {\r\n \uFFFF} $instring] - set outstring [string map {\n \r\n} $outstring] - set outstring [string map {\uFFFF \r\n} $outstring] - - set lastchar [string range $outstring end end] - if {$lastchar eq "\r"} { - set o_last_char_was_cr 1 - set outstring [string range $outstring 0 end-1] - } else { - set o_last_char_was_cr 0 - } - #review! can we detect eof here on the transform_handle? - #if eof, we don't want to strip a trailing \r - - return [encoding convertto $o_enc $outstring] - #return [encoding convertto utf-16le $outstring] - } - } - oo::define towindows { - method meta_is_redirection {} { - return $o_is_junction - } - } - - } -} - -# ---------------------------------------------------------------------------- -#review float/sink metaphor. -#perhaps something with the concept of upstream and downstream? -#need concepts for push towards data, sit in middle where placed, and lag at tail of data stream. -## upstream for stdin is at the bottom of the stack and for stdout is the top of the stack. -#upstream,neutral-upstream,downstream,downstream-aside,downstream-replace (default neutral-upstream - require action 'stack' to use standard channel stacking concept and ignore other actions) -#This is is a bit different from the float/sink metaphor which refers to the channel stacking order as opposed to the data-flow direction. -#The idea would be that whether input or output -# upstream additions go to the side closest to the datasource -# downstream additions go furthest from the datasource -# - all new additions go ahead of any diversions as the most upstream diversion is the current end of the stream in a way. -# - this needs review regarding subsequent removal of the diversion and whether filters re-order in response.. -# or if downstream & neutral additions are reclassified upon insertion if they land among existing upstreams(?) -# neutral-upstream goes to the datasource side of the neutral-upstream list. -# No 'neutral' option provided so that we avoid the need to think forwards or backwards when adding stdin vs stdout shellfilter does the necessary pop/push reordering. -# No 'neutral-downstream' to reduce complexity. -# downstream-replace & downstream-aside head downstream to the first diversion they encounter. ie these actions are no longer referring to the stack direction but only the dataflow direction. -# -# ---------------------------------------------------------------------------- -# -# 'filters' are transforms that don't redirect -# - limited range of actions to reduce complexity. -# - any requirement not fulfilled by float,sink,sink-replace,sink-sideline should be done by multiple pops and pushes -# -#actions can float to top of filters or sink to bottom of filters -#when action is of type sink, it can optionally replace or sideline the first non-filter it encounters (highest redirection on the stack.. any lower are starved of the stream anyway) -# - sideline means to temporarily replace the item and keep a record, restoring if/when we are removed from the transform stack -# -##when action is of type float it can't replace or sideline anything. A float is added above any existing floats and they stay in the same order relative to each other, -#but non-floats added later will sit below all floats. -#(review - float/sink initially designed around output channels. For stdin the dataflow is reversed. implement float-aside etc?) -# -# -#action: float sink sink-replace,sink-sideline -# -# -## note - whether stack is for input or output we maintain it in the same direction - which is in sync with the tcl chan pop chan push concept. -## -namespace eval shellfilter::stack { - namespace export {[a-z]*} - namespace ensemble create - #todo - implement as oo ? - variable pipelines [list] - - proc items {} { - #review - stdin,stdout,stderr act as pre-existing pipelines, and we can't create a new one with these names - so they should probably be autoconfigured and listed.. - # - but in what contexts? only when we find them in [chan names]? - variable pipelines - return [dict keys $pipelines] - } - proc item {pipename} { - variable pipelines - return [dict get $pipelines $pipename] - } - proc item_tophandle {pipename} { - variable pipelines - set handle "" - if {[dict exists $pipelines $pipename stack]} { - set stack [dict get $pipelines $pipename stack] - set topstack [lindex $stack end] ;#last item in stack is top (for output channels anyway) review comment. input chans? - if {$topstack ne ""} { - if {[dict exists $topstack -handle]} { - set handle [dict get $topstack -handle] - } - } - } - return $handle - } - proc status {{pipename *} args} { - variable pipelines - set pipecount [dict size $pipelines] - set tabletitle "$pipecount pipelines active" - set t [textblock::class::table new $tabletitle] - $t add_column -headers [list channel-ident] - $t add_column -headers [list device-info localchan] - $t configure_column 1 -header_colspans {3} - $t add_column -headers [list "" remotechan] - $t add_column -headers [list "" tid] - $t add_column -headers [list stack-info] - foreach k [dict keys $pipelines $pipename] { - set lc [dict get $pipelines $k device localchan] - set rc [dict get $pipelines $k device remotechan] - if {[dict exists $k device workertid]} { - set tid [dict get $pipelines $k device workertid] - } else { - set tid "-" - } - set stack [dict get $pipelines $k stack] - if {![llength $stack]} { - set stackinfo "" - } else { - set tbl_inner [textblock::class::table new] - $tbl_inner configure -show_edge 0 - foreach rec $stack { - set handle [punk::lib::dict_getdef $rec -handle ""] - set id [punk::lib::dict_getdef $rec -id ""] - set transform [namespace tail [punk::lib::dict_getdef $rec -transform ""]] - set settings [punk::lib::dict_getdef $rec -settings ""] - $tbl_inner add_row [list $id $transform $handle $settings] - } - set stackinfo [$tbl_inner print] - $tbl_inner destroy - } - $t add_row [list $k $lc $rc $tid $stackinfo] - } - set result [$t print] - $t destroy - return $result - } - proc status1 {{pipename *} args} { - variable pipelines - - set pipecount [dict size $pipelines] - set tableprefix "$pipecount pipelines active\n" - foreach p [dict keys $pipelines] { - append tableprefix " " $p \n - } - package require overtype - #todo -verbose - set table "" - set ac1 [string repeat " " 15] - set ac2 [string repeat " " 42] - set ac3 [string repeat " " 70] - append table "[overtype::left $ac1 channel-ident] " - append table "[overtype::left $ac2 device-info] " - append table "[overtype::left $ac3 stack-info]" - append table \n - - - set bc1 [string repeat " " 5] ;#stack id - set bc2 [string repeat " " 25] ;#transform - set bc3 [string repeat " " 50] ;#settings - - foreach k [dict keys $pipelines $pipename] { - set lc [dict get $pipelines $k device localchan] - if {[dict exists $k device workertid]} { - set tid [dict get $pipelines $k device workertid] - } else { - set tid "" - } - - - set col1 [overtype::left $ac1 $k] - set col2 [overtype::left $ac2 "localchan: $lc tid:$tid"] - - set stack [dict get $pipelines $k stack] - if {![llength $stack]} { - set col3 $ac3 - } else { - set rec [lindex $stack 0] - set bcol1 [overtype::left $bc1 [dict get $rec -id]] - set bcol2 [overtype::left $bc2 [namespace tail [dict get $rec -transform]]] - set bcol3 [overtype::left $bc3 [dict get $rec -settings]] - set stackrow "$bcol1 $bcol2 $bcol3" - set col3 [overtype::left $ac3 $stackrow] - } - - append table "$col1 $col2 $col3\n" - - - foreach rec [lrange $stack 1 end] { - set col1 $ac1 - set col2 $ac2 - if {[llength $rec]} { - set bc1 [overtype::left $bc1 [dict get $rec -id]] - set bc2 [overtype::left $bc2 [namespace tail [dict get $rec -transform]]] - set bc3 [overtype::left $bc3 [dict get $rec -settings]] - set stackrow "$bc1 $bc2 $bc3" - set col3 [overtype::left $ac3 $stackrow] - } else { - set col3 $ac3 - } - append table "$col1 $col2 $col3\n" - } - - } - return $tableprefix$table - } - #used for output channels - we usually want to sink redirections below the floaters and down to topmost existing redir - proc _get_stack_floaters {stack} { - set floaters [list] - foreach t [lreverse $stack] { - switch -- [dict get $t -action] { - float { - lappend floaters $t - } - default { - break - } - } - } - return [lreverse $floaters] - } - - - - #for output-channel sinking - proc _get_stack_top_redirection {stack} { - set r 0 ;#reverse index - foreach t [lreverse $stack] { - set obj [dict get $t -obj] - if {[$obj meta_is_redirection]} { - set idx [expr {[llength $stack] - ($r + 1) }] ;#forward index - return [list index $idx record $t] - } - incr r - } - #not found - return [list index -1 record {}] - } - #exclude float-locked, locked, sink-locked - proc _get_stack_top_redirection_replaceable {stack} { - set r 0 ;#reverse index - foreach t [lreverse $stack] { - set action [dict get $t -action] - if {![string match "*locked*" $action]} { - set obj [dict get $t -obj] - if {[$obj meta_is_redirection]} { - set idx [expr {[llength $stack] - ($r + 1) }] ;#forward index - return [list index $idx record $t] - } - } - incr r - } - #not found - return [list index -1 record {}] - } - - - #for input-channels ? - proc _get_stack_bottom_redirection {stack} { - set i 0 - foreach t $stack { - set obj [dict get $t -obj] - if {[$obj meta_is_redirection]} { - return [linst index $i record $t] - } - incr i - } - #not found - return [list index -1 record {}] - } - - - proc get_next_counter {pipename} { - variable pipelines - #use dictn incr ? - set counter [dict get $pipelines $pipename counter] - incr counter - dict set pipelines $pipename counter $counter - return $counter - } - - proc unwind {pipename} { - variable pipelines - set stack [dict get $pipelines $pipename stack] - set localchan [dict get $pipelines $pipename device localchan] - foreach tf [lreverse $stack] { - chan pop $localchan - } - dict set pipelines $pipename [list] - } - #todo - proc delete {pipename {wait 0}} { - variable pipelines - set pipeinfo [dict get $pipelines $pipename] - set deviceinfo [dict get $pipeinfo device] - set localchan [dict get $deviceinfo localchan] - unwind $pipename - - #release associated thread - set tid [dict get $deviceinfo workertid] - if {$wait} { - thread::release -wait $tid - } else { - thread::release $tid - } - - #Memchan closes without error - tcl::chan::fifo2 raises something like 'can not find channel named "rc977"' - REVIEW. why? - catch {chan close $localchan} - } - #review - proc name clarity is questionable. remove_stackitem? - proc remove {pipename remove_id} { - variable pipelines - if {![dict exists $pipelines $pipename]} { - puts stderr "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]" - return - } - set stack [dict get $pipelines $pipename stack] - set localchan [dict get $pipelines $pipename device localchan] - set posn 0 - set idposn -1 - set asideposn -1 - foreach t $stack { - set id [dict get $t -id] - if {$id eq $remove_id} { - set idposn $posn - break - } - #look into asides (only can be one for now) - if {[llength [dict get $t -aside]]} { - set a [dict get $t -aside] - if {[dict get $a -id] eq $remove_id} { - set asideposn $posn - break - } - } - incr posn - } - - if {$asideposn > 0} { - #id wasn't found directly in stack, but in an -aside. we don't need to pop anything - just clear this aside record - set container [lindex $stack $asideposn] - dict set container -aside {} - lset stack $asideposn $container - dict set pipelines $pipename stack $stack - } else { - if {$idposn < 0} { - ::shellfilter::log::write shellfilter "ERROR shellfilter::stack::remove $pipename id '$remove_id' not found" - puts stderr "|WARNING>shellfilter::stack::remove $pipename id '$remove_id' not found" - return 0 - } - set removed_item [lindex $stack $idposn] - - #include idposn in poplist - set poplist [lrange $stack $idposn end] - set stack [lreplace $stack $idposn end] - #pop all chans before adding anything back in! - foreach p $poplist { - chan pop $localchan - } - - if {[llength [dict get $removed_item -aside]]} { - set restore [dict get $removed_item -aside] - set t [dict get $restore -transform] - set tsettings [dict get $restore -settings] - set obj [$t new $restore] - set h [chan push $localchan $obj] - dict set restore -handle $h - dict set restore -obj $obj - lappend stack $restore - } - - #put popped back except for the first one, which we want to remove - foreach p [lrange $poplist 1 end] { - set t [dict get $p -transform] - set tsettings [dict get $p -settings] - set obj [$t new $p] - set h [chan push $localchan $obj] - dict set p -handle $h - dict set p -obj $obj - lappend stack $p - } - dict set pipelines $pipename stack $stack - } - #JMNJMN 2025 review! - #show_pipeline $pipename -note "after_remove $remove_id" - return 1 - } - - #pop a number of items of the top of the stack, add our transform record, and add back all (or the tail of poplist if pushstartindex > 0) - proc insert_transform {pipename stack transformrecord poplist {pushstartindex 0}} { - variable pipelines - set bottom_pop_posn [expr {[llength $stack] - [llength $poplist]}] - set poplist [lrange $stack $bottom_pop_posn end] - set stack [lreplace $stack $bottom_pop_posn end] - - set localchan [dict get $pipelines $pipename device localchan] - foreach p [lreverse $poplist] { - chan pop $localchan - } - set transformname [dict get $transformrecord -transform] - set transformsettings [dict get $transformrecord -settings] - set obj [$transformname new $transformrecord] - set h [chan push $localchan $obj] - dict set transformrecord -handle $h - dict set transformrecord -obj $obj - dict set transformrecord -note "insert_transform" - lappend stack $transformrecord - foreach p [lrange $poplist $pushstartindex end] { - set t [dict get $p -transform] - set tsettings [dict get $p -settings] - set obj [$t new $p] - set h [chan push $localchan $obj] - #retain previous -id - code that added it may have kept reference and not expecting it to change - dict set p -handle $h - dict set p -obj $obj - dict set p -note "re-added" - - lappend stack $p - } - return $stack - } - - #fifo2 - proc new {pipename args} { - variable pipelines - if {($pipename in [dict keys $pipelines]) || ($pipename in [chan names])} { - error "shellfilter::stack::new error: pipename '$pipename' already exists" - } - - set opts [dict merge {-settings {}} $args] - set defaultsettings [dict create -raw 1 -buffering line -direction out] - set targetsettings [dict merge $defaultsettings [dict get $opts -settings]] - - set direction [dict get $targetsettings -direction] - - #pipename is the source/facility-name ? - if {$direction eq "out"} { - set pipeinfo [shellfilter::pipe::open_out $pipename $targetsettings] - } else { - puts stderr "|jn> pipe::open_in $pipename $targetsettings" - set pipeinfo [shellfilter::pipe::open_in $pipename $targetsettings] - } - #open_out/open_in will configure buffering based on targetsettings - - set program_chan [dict get $pipeinfo localchan] - set worker_chan [dict get $pipeinfo remotechan] - set workertid [dict get $pipeinfo workertid] - - - set deviceinfo [dict create pipename $pipename localchan $program_chan remotechan $worker_chan workertid $workertid direction $direction] - dict set pipelines $pipename [list counter 0 device $deviceinfo stack [list]] - - return $deviceinfo - } - #we 'add' rather than 'push' because transforms can float,sink and replace/sideline so they don't necessarily go to the top of the transform stack - proc add {pipename transformname args} { - variable pipelines - #chan names doesn't reflect available channels when transforms are in place - #e.g stdout may exist but show as something like file191f5b0dd80 - if {($pipename ni [dict keys $pipelines])} { - if {[catch {eof $pipename} is_eof]} { - error "shellfilter::stack::add no existing chan or pipename matching '$pipename' in channels:[chan names] or pipelines:$pipelines use stdin/stderr/stdout or shellfilter::stack::new " - } - } - set args [dict merge {-action "" -settings {}} $args] - set action [dict get $args -action] - set transformsettings [dict get $args -settings] - if {[string first "::" $transformname] < 0} { - set transformname ::shellfilter::chan::$transformname - } - if {![llength [info commands $transformname]]} { - error "shellfilter::stack::push unknown transform '$transformname'" - } - - - if {![dict exists $pipelines $pipename]} { - #pipename must be in chan names - existing device/chan - #record a -read and -write end even if the device is only being used as one or the other - set deviceinfo [dict create pipename $pipename localchan $pipename remotechan {}] - dict set pipelines $pipename [list counter 0 device $deviceinfo stack [list]] - } else { - set deviceinfo [dict get $pipelines $pipename device] - } - - set id [get_next_counter $pipename] - set stack [dict get $pipelines $pipename stack] - set localchan [dict get $deviceinfo localchan] - - #we redundantly store chan in each transform - makes debugging clearer - # -encoding similarly could be stored only at the pipeline level (or even queried directly each filter-read/write), - # but here it may help detect unexpected changes during lifetime of the stack and avoids the chance of callers incorrectly using the transform handle?) - # jn - set transform_record [list -id $id -chan $pipename -encoding [chan configure $localchan -encoding] -transform $transformname -aside {} {*}$args] - switch -glob -- $action { - float - float-locked { - set obj [$transformname new $transform_record] - set h [chan push $localchan $obj] - dict set transform_record -handle $h - dict set transform_record -obj $obj - lappend stack $transform_record - } - "" - locked { - set floaters [_get_stack_floaters $stack] - if {![llength $floaters]} { - set obj [$transformname new $transform_record] - set h [chan push $localchan $obj] - dict set transform_record -handle $h - dict set transform_record -obj $obj - lappend stack $transform_record - } else { - set poplist $floaters - set stack [insert_transform $pipename $stack $transform_record $poplist] - } - } - "sink*" { - set redirinfo [_get_stack_top_redirection $stack] - set idx_existing_redir [dict get $redirinfo index] - if {$idx_existing_redir == -1} { - #no existing redirection transform on the stack - #pop everything.. add this record as the first redirection on the stack - set poplist $stack - set stack [insert_transform $pipename $stack $transform_record $poplist] - } else { - switch -glob -- $action { - "sink-replace" { - #include that index in the poplist - set poplist [lrange $stack $idx_existing_redir end] - #pop all from idx_existing_redir to end, but put back 'lrange $poplist 1 end' - set stack [insert_transform $pipename $stack $transform_record $poplist 1] - } - "sink-aside*" { - set existing_redir_record [lindex $stack $idx_existing_redir] - if {[string match "*locked*" [dict get $existing_redir_record -action]]} { - set put_aside 0 - #we can't aside this one - sit above it instead. - set poplist [lrange $stack $idx_existing_redir+1 end] - set stack [lrange $stack 0 $idx_existing_redir] - } else { - set put_aside 1 - dict set transform_record -aside [lindex $stack $idx_existing_redir] - set poplist [lrange $stack $idx_existing_redir end] - set stack [lrange $stack 0 $idx_existing_redir-1] - } - foreach p $poplist { - chan pop $localchan - } - set transformname [dict get $transform_record -transform] - set transform_settings [dict get $transform_record -settings] - set obj [$transformname new $transform_record] - set h [chan push $localchan $obj] - dict set transform_record -handle $h - dict set transform_record -obj $obj - dict set transform_record -note "insert_transform-with-aside" - lappend stack $transform_record - #add back poplist *except* the one we transferred into -aside (if we were able) - foreach p [lrange $poplist $put_aside end] { - set t [dict get $p -transform] - set tsettings [dict get $p -settings] - set obj [$t new $p] - set h [chan push $localchan $obj] - #retain previous -id - code that added it may have kept reference and not expecting it to change - dict set p -handle $h - dict set p -obj $obj - dict set p -note "re-added-after-sink-aside" - lappend stack $p - } - } - default { - #plain "sink" - #we only sink to the topmost redirecting filter - which makes sense for an output channel - #For stdin.. this is more problematic as we're more likely to want to intercept the bottom most redirection. - #todo - review. Consider making default insert position for input channels to be at the source... and float/sink from there. - # - we don't currently know from the stack api if adding input vs output channel - so this needs work to make intuitive. - # consider splitting stack::add to stack::addinput stack::addoutput to split the different behaviour - set poplist [lrange $stack $idx_existing_redir+1 end] - set stack [insert_transform $pipename $stack $transform_record $poplist] - } - } - } - } - default { - error "shellfilter::stack::add unimplemented action '$action'" - } - } - - dict set pipelines $pipename stack $stack - #puts stdout "==" - #puts stdout "==>stack: $stack" - #puts stdout "==" - - #JMNJMN - #show_pipeline $pipename -note "after_add $transformname $args" - return $id - } - proc show_pipeline {pipename args} { - variable pipelines - set stack [dict get $pipelines $pipename stack] - set tag "SHELLFILTER::STACK" - #JMN - load from config - #::shellfilter::log::open $tag {-syslog 127.0.0.1:514} - if {[catch { - ::shellfilter::log::open $tag {-syslog ""} - } err]} { - #e.g safebase interp can't load required modules such as shellthread (or Thread) - puts stderr "shellfilter::show_pipeline cannot open log" - return - } - ::shellfilter::log::write $tag "transform stack for $pipename $args" - foreach tf $stack { - ::shellfilter::log::write $tag " $tf" - } - - } -} - - -namespace eval shellfilter { - variable sources [list] - variable stacks [dict create] - - proc ::shellfilter::redir_channel_to_log {chan args} { - variable sources - set default_logsettings [dict create \ - -tag redirected_$chan -syslog "" -file ""\ - ] - if {[dict exists $args -action]} { - set action [dict get $args -action] - } else { - # action "sink" is a somewhat reasonable default for an output redirection transform - # but it can make it harder to configure a plain ordered stack if the user is not expecting it, so we'll default to stack - # also.. for stdin transform sink makes less sense.. - #todo - default "stack" instead of empty string - set action "" - } - if {[dict exists $args -settings]} { - set logsettings [dict get $args -settings] - } else { - set logsettings {} - } - - set logsettings [dict merge $default_logsettings $logsettings] - set tag [dict get $logsettings -tag] - if {$tag ni $sources} { - lappend sources $tag - } - - set id [shellfilter::stack::add $chan logonly -action $action -settings $logsettings] - return $id - } - - proc ::shellfilter::redir_output_to_log {tagprefix args} { - variable sources - - set default_settings [list -tag ${tagprefix} -syslog "" -file ""] - - set opts [dict create -action "" -settings {}] - set opts [dict merge $opts $args] - set optsettings [dict get $opts -settings] - set settings [dict merge $default_settings $optsettings] - - set tag [dict get $settings -tag] - if {$tag ne $tagprefix} { - error "shellfilter::redir_output_to_log -tag value must match supplied tagprefix:'$tagprefix'. Omit -tag, or make it the same. It will automatically be suffixed with stderr and stdout. Use redir_channel_to_log if you want to separately configure each channel" - } - lappend sources ${tagprefix}stdout ${tagprefix}stderr - - set stdoutsettings $settings - dict set stdoutsettings -tag ${tagprefix}stdout - set stderrsettings $settings - dict set stderrsettings -tag ${tagprefix}stderr - - set idout [redir_channel_to_log stdout -action [dict get $opts -action] -settings $stdoutsettings] - set iderr [redir_channel_to_log stderr -action [dict get $opts -action] -settings $stderrsettings] - - return [list $idout $iderr] - } - - #eg try: set v [list #a b c] - #vs set v {#a b c} - proc list_is_canonical l { - #courtesy DKF via wiki https://wiki.tcl-lang.org/page/BNF+for+Tcl - if {[catch {llength $l}]} {return 0} - string equal $l [list {*}$l] - } - - #return a dict keyed on numerical list index showing info about each element - # - particularly - # 'wouldbrace' to indicate that the item would get braced by Tcl when added to another list - # 'head_tail_chars' to show current first and last character (in case it's wrapped e.g in double or single quotes or an existing set of braces) - proc list_element_info {inputlist} { - set i 0 - set info [dict create] - set testlist [list] - foreach original_item $inputlist { - #--- - # avoid sharing internal rep with original items in the list (avoids shimmering of rep in original list for certain items such as paths) - unset -nocomplain item - append item $original_item {} - #--- - - set iteminfo [dict create] - set itemlen [string length $item] - lappend testlist $item - set tcl_len [string length $testlist] - set diff [expr {$tcl_len - $itemlen}] - if {$diff == 0} { - dict set iteminfo wouldbrace 0 - dict set iteminfo wouldescape 0 - } else { - #test for escaping vs bracing! - set testlistchars [split $testlist ""] - if {([lindex $testlistchars 0] eq "\{") && ([lindex $testlistchars end] eq "\}")} { - dict set iteminfo wouldbrace 1 - dict set iteminfo wouldescape 0 - } else { - dict set iteminfo wouldbrace 0 - dict set iteminfo wouldescape 1 - } - } - set testlist [list] - set charlist [split $item ""] - set char_a [lindex $charlist 0] - set char_b [lindex $charlist 1] - set char_ab ${char_a}${char_b} - set char_y [lindex $charlist end-1] - set char_z [lindex $charlist end] - set char_yz ${char_y}${char_z} - - if { ("{" in $charlist) || ("}" in $charlist) } { - dict set iteminfo has_braces 1 - set innerchars [lrange $charlist 1 end-1] - if {("{" in $innerchars) || ("}" in $innerchars)} { - dict set iteminfo has_inner_braces 1 - } else { - dict set iteminfo has_inner_braces 0 - } - } else { - dict set iteminfo has_braces 0 - dict set iteminfo has_inner_braces 0 - } - - #todo - brace/char counting to determine if actually 'wrapped' - #e.g we could have list element {((abc)} - which appears wrapped if only looking at first and last chars. - #also {(x) (y)} as a list member.. how to treat? - if {$itemlen <= 1} { - dict set iteminfo apparentwrap "not" - } else { - #todo - switch on $char_a$char_z - if {($char_a eq {"}) && ($char_z eq {"})} { - dict set iteminfo apparentwrap "doublequotes" - } elseif {($char_a eq "'") && ($char_z eq "'")} { - dict set iteminfo apparentwrap "singlequotes" - } elseif {($char_a eq "(") && ($char_z eq ")")} { - dict set iteminfo apparentwrap "brackets" - } elseif {($char_a eq "\{") && ($char_z eq "\}")} { - dict set iteminfo apparentwrap "braces" - } elseif {($char_a eq "^") && ($char_z eq "^")} { - dict set iteminfo apparentwrap "carets" - } elseif {($char_a eq "\[") && ($char_z eq "\]")} { - dict set iteminfo apparentwrap "squarebrackets" - } elseif {($char_a eq "`") && ($char_z eq "`")} { - dict set iteminfo apparentwrap "backquotes" - } elseif {($char_a eq "\n") && ($char_z eq "\n")} { - dict set iteminfo apparentwrap "lf-newline" - } elseif {($char_ab eq "\r\n") && ($char_yz eq "\r\n")} { - dict set iteminfo apparentwrap "crlf-newline" - } else { - dict set iteminfo apparentwrap "not-determined" - } - - } - dict set iteminfo wrapbalance "unknown" ;#a hint to caller that apparentwrap is only a guide. todo - possibly make wrapbalance indicate 0 for unbalanced.. and positive numbers for outer-count of wrappings. - #e.g {((x)} == 0 {((x))} == 1 {(x) (y (z))} == 2 - dict set iteminfo head_tail_chars [list $char_a $char_z] - set namemap [list \ - \r cr\ - \n lf\ - {"} doublequote\ - {'} singlequote\ - "`" backquote\ - "^" caret\ - \t tab\ - " " sp\ - "\[" lsquare\ - "\]" rsquare\ - "(" lbracket\ - ")" rbracket\ - "\{" lbrace\ - "\}" rbrace\ - \\ backslash\ - / forwardslash\ - ] - if {[string length $char_a]} { - set char_a_name [string map $namemap $char_a] - } else { - set char_a_name "emptystring" - } - if {[string length $char_z]} { - set char_z_name [string map $namemap $char_z] - } else { - set char_z_name "emptystring" - } - - dict set iteminfo head_tail_names [list $char_a_name $char_z_name] - dict set iteminfo len $itemlen - dict set iteminfo difflen $diff ;#2 for braces, 1 for quoting?, or 0. - dict set info $i $iteminfo - incr i - } - return $info - } - - - #parse bracketed expression (e.g produced by vim "shellxquote=(" ) into a tcl (nested) list - #e.g {(^c:/my spacey/path^ >^somewhere^)} - #e.g {(blah (etc))}" - #Result is always a list - even if only one toplevel set of brackets - so it may need [lindex $result 0] if input is the usual case of {( ...)} - # - because it also supports the perhaps less likely case of: {( ...) unbraced (...)} etc - # Note that - #maintenance warning - duplication in branches for bracketed vs unbracketed! - proc parse_cmd_brackets {str} { - #wordwrappers currently best suited to non-bracket entities - no bracket matching within - anything goes until end-token reached. - # - but.. they only take effect where a word can begin. so a[x y] may be split at the space unless it's within some other wraper e.g " a[x y]" will not break at the space - # todo - consider extending the in-word handling of word_bdepth which is currently only applied to () i.e aaa(x y) is supported but aaa[x y] is not as the space breaks the word up. - set wordwrappers [list \ - "\"" [list "\"" "\"" "\""]\ - {^} [list "\"" "\"" "^"]\ - "'" [list "'" "'" "'"]\ - "\{" [list "\{" "\}" "\}"]\ - {[} [list {[} {]} {]}]\ - ] ;#dict mapping start_character to {replacehead replacetail expectedtail} - set shell_specials [list "|" "|&" "<" "<@" "<<" ">" "2>" ">&" ">>" "2>>" ">>&" ">@" "2>@" "2>@1" ">&@" "&" "&&" ] ;#words/chars that may precede an opening bracket but don't merge with the bracket to form a word. - #puts "pb:$str" - set in_bracket 0 - set in_word 0 - set word "" - set result {} - set word_bdepth 0 - set word_bstack [list] - set wordwrap "" ;#only one active at a time - set bracketed_elements [dict create] - foreach char [split $str ""] { - #puts "c:$char bracketed:$bracketed_elements" - if {$in_bracket > 0} { - if {$in_word} { - if {[string length $wordwrap]} { - #anything goes until end-char - #todo - lookahead and only treat as closing if before a space or ")" ? - lassign [dict get $wordwrappers $wordwrap] _open closing endmark - if {$char eq $endmark} { - set wordwrap "" - append word $closing - dict lappend bracketed_elements $in_bracket $word - set word "" - set in_word 0 - } else { - append word $char - } - } else { - if {$word_bdepth == 0} { - #can potentially close off a word - or start a new one if word-so-far is a shell-special - if {$word in $shell_specials} { - if {$char eq ")"} { - dict lappend bracketed_elements $in_bracket $word - set subresult [dict get $bracketed_elements $in_bracket] - dict set bracketed_elements $in_bracket [list] - incr in_bracket -1 - if {$in_bracket == 0} { - lappend result $subresult - } else { - dict lappend bracketed_elements $in_bracket $subresult - } - set word "" - set in_word 0 - } elseif {[regexp {[\s]} $char]} { - dict lappend bracketed_elements $in_bracket $word - set word "" - set in_word 0 - } elseif {$char eq "("} { - dict lappend bracketed_elements $in_bracket $word - set word "" - set in_word 0 - incr in_bracket - } else { - #at end of shell-specials is another point to look for word started by a wordwrapper char - #- expect common case of things like >^/my/path^ - if {$char in [dict keys $wordwrappers]} { - dict lappend bracketed_elements $in_bracket $word - set word "" - set in_word 1 ;#just for explicitness.. we're straight into the next word. - set wordwrap $char - set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. - } else { - #something unusual.. keep going with word! - append word $char - } - } - } else { - - if {$char eq ")"} { - dict lappend bracketed_elements $in_bracket $word - set subresult [dict get $bracketed_elements $in_bracket] - dict set bracketed_elements $in_bracket [list] - incr in_bracket -1 - if {$in_bracket == 0} { - lappend result $subresult - } else { - dict lappend bracketed_elements $in_bracket $subresult - } - set word "" - set in_word 0 - } elseif {[regexp {[\s]} $char]} { - dict lappend bracketed_elements $in_bracket $word - set word "" - set in_word 0 - } elseif {$char eq "("} { - #ordinary word up-against and opening bracket - brackets are part of word. - incr word_bdepth - append word "(" - } else { - append word $char - } - } - } else { - #currently only () are used for word_bdepth - todo add all or some wordwrappers chars so that the word_bstack can have multiple active. - switch -- $char { - "(" { - incr word_bdepth - lappend word_bstack $char - append word $char - } - ")" { - incr word_bdepth -1 - set word_bstack [lrange $word_bstack 0 end-1] - append word $char - } - default { - #spaces and chars added to word as it's still in a bracketed section - append word $char - } - } - } - } - } else { - - if {$char eq "("} { - incr in_bracket - - } elseif {$char eq ")"} { - set subresult [dict get $bracketed_elements $in_bracket] - dict set bracketed_elements $in_bracket [list] - incr in_bracket -1 - if {$in_bracket == 0} { - lappend result $subresult - } else { - dict lappend bracketed_elements $in_bracket $subresult - } - } elseif {[regexp {[\s]} $char]} { - # - } else { - #first char of word - look for word-wrappers - if {$char in [dict keys $wordwrappers]} { - set wordwrap $char - set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. - } else { - set word $char - } - set in_word 1 - } - } - } else { - if {$in_word} { - if {[string length $wordwrap]} { - lassign [dict get $wordwrappers $wordwrap] _open closing endmark - if {$char eq $endmark} { - set wordwrap "" - append word $closing - lappend result $word - set word "" - set in_word 0 - } else { - append word $char - } - } else { - - if {$word_bdepth == 0} { - if {$word in $shell_specials} { - if {[regexp {[\s]} $char]} { - lappend result $word - set word "" - set in_word 0 - } elseif {$char eq "("} { - lappend result $word - set word "" - set in_word 0 - incr in_bracket - } else { - #at end of shell-specials is another point to look for word started by a wordwrapper char - #- expect common case of things like >^/my/path^ - if {$char in [dict keys $wordwrappers]} { - lappend result $word - set word "" - set in_word 1 ;#just for explicitness.. we're straight into the next word. - set wordwrap $char - set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. - } else { - #something unusual.. keep going with word! - append word $char - } - } - - } else { - if {[regexp {[\s)]} $char]} { - lappend result $word - set word "" - set in_word 0 - } elseif {$char eq "("} { - incr word_bdepth - append word $char - } else { - append word $char - } - } - } else { - switch -- $char { - "(" { - incr word_bdepth - append word $char - } - ")" { - incr word_bdepth -1 - append word $char - } - default { - append word $char - } - } - } - } - } else { - if {[regexp {[\s]} $char]} { - #insig whitespace(?) - } elseif {$char eq "("} { - incr in_bracket - dict set bracketed_elements $in_bracket [list] - } elseif {$char eq ")"} { - error "unbalanced bracket - unable to proceed result so far: $result bracketed_elements:$bracketed_elements" - } else { - #first char of word - look for word-wrappers - if {$char in [dict keys $wordwrappers]} { - set wordwrap $char - set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. - } else { - set word $char - } - set in_word 1 - } - } - } - #puts "----$bracketed_elements" - } - if {$in_bracket > 0} { - error "shellfilter::parse_cmd_brackets missing close bracket. input was '$str'" - } - if {[dict exists $bracketed_elements 0]} { - #lappend result [lindex [dict get $bracketed_elements 0] 0] - lappend result [dict get $bracketed_elements 0] - } - if {$in_word} { - lappend result $word - } - return $result - } - - #only double quote if argument not quoted with single or double quotes - proc dquote_if_not_quoted {a} { - set wrapchars [string cat [string range $a 0 0] [string range $a end end]] - switch -- $wrapchars { - {""} - {''} { - return $a - } - default { - set newinner [string map [list {"} "\\\""] $a] - return "\"$newinner\"" - } - } - } - - #proc dquote_if_not_bracketed/braced? - - #wrap in double quotes if not double-quoted - proc dquote_if_not_dquoted {a} { - set wrapchars [string cat [string range $a 0 0] [string range $a end end]] - switch -- $wrapchars { - {""} { - return $a - } - default { - #escape any inner quotes.. - set newinner [string map [list {"} "\\\""] $a] - return "\"$newinner\"" - } - } - } - proc dquote {a} { - #escape any inner quotes.. - set newinner [string map [list {"} "\\\""] $a] - return "\"$newinner\"" - } - proc get_scriptrun_from_cmdlist_dquote_if_not {cmdlist {shellcmdflag ""}} { - set scr [auto_execok "script"] - if {[string length $scr]} { - #set scriptrun "( $c1 [lrange $cmdlist 1 end] )" - set arg1 [lindex $cmdlist 0] - if {[string first " " $arg1]>0} { - set c1 [dquote_if_not_quoted $arg1] - #set c1 "\"$arg1\"" - } else { - set c1 $arg1 - } - - if {[string length $shellcmdflag]} { - set scriptrun "$shellcmdflag \$($c1 " - } else { - set scriptrun "\$($c1 " - } - #set scriptrun "$c1 " - foreach a [lrange $cmdlist 1 end] { - #set a [string map [list "/" "//"] $a] - #set a [string map [list "\"" "\\\""] $a] - if {[string first " " $a] > 0} { - append scriptrun [dquote_if_not_quoted $a] - } else { - append scriptrun $a - } - append scriptrun " " - } - set scriptrun [string trim $scriptrun] - append scriptrun ")" - #return [list $scr -q -e -c $scriptrun /dev/null] - return [list $scr -e -c $scriptrun /dev/null] - } else { - return $cmdlist - } - } - - proc ::shellfilter::trun {commandlist args} { - #jmn - } - - - # run a command (or tcl script) with tees applied to stdout/stderr/stdin (or whatever channels are being used) - # By the point run is called - any transforms should already be in place on the channels if they're needed. - # The tees will be inline with none,some or all of those transforms depending on how the stack was configured - # (upstream,downstream configured via -float,-sink etc) - proc ::shellfilter::run {commandlist args} { - #must be a list. If it was a shell commandline string. convert it elsewhere first. - - variable sources - set runtag "shellfilter-run" - #set tid [::shellfilter::log::open $runtag [list -syslog 127.0.0.1:514]] - set tid [::shellfilter::log::open $runtag [list -syslog ""]] - if {[catch {llength $commandlist} listlen]} { - set listlen "" - } - ::shellfilter::log::write $runtag " commandlist:'$commandlist' listlen:$listlen strlen:[string length $commandlist]" - - #flush stdout - #flush stderr - - #adding filters with sink-aside will temporarily disable the existing redirection - #All stderr/stdout from the shellcommand will now tee to the underlying stderr/stdout as well as the configured syslog - - set defaults [dict create \ - -teehandle command \ - -outchan stdout \ - -errchan stderr \ - -inchan stdin \ - -tclscript 0 \ - ] - set opts [dict merge $defaults $args] - - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- - set outchan [dict get $opts -outchan] - set errchan [dict get $opts -errchan] - set inchan [dict get $opts -inchan] - set teehandle [dict get $opts -teehandle] - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- - set is_script [dict get $opts -tclscript] - dict unset opts -tclscript ;#don't pass it any further - # -- --- --- --- --- --- --- --- --- --- --- --- --- --- - set teehandle_out ${teehandle}out ;#default commandout - set teehandle_err ${teehandle}err - set teehandle_in ${teehandle}in - - - #puts stdout "shellfilter initialising tee_to_pipe transforms for in/out/err" - - # sources should be added when stack::new called instead(?) - foreach source [list $teehandle_out $teehandle_err] { - if {$source ni $sources} { - lappend sources $source - } - } - set outdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_out device] - set outpipechan [dict get $outdeviceinfo localchan] - set errdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_err device] - set errpipechan [dict get $errdeviceinfo localchan] - - #set indeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_in device] - #set inpipechan [dict get $indeviceinfo localchan] - - #NOTE:These transforms are not necessarily at the top of each stack! - #The float/sink mechanism, along with whether existing transforms are diversionary decides where they sit. - set id_out [shellfilter::stack::add $outchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_out -pipechan $outpipechan]] - set id_err [shellfilter::stack::add $errchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_err -pipechan $errpipechan]] - - # need to use os level channel handle for stdin - try named pipes (or even sockets) instead of fifo2 for this - # If non os-level channel - the command can't be run with the redirection - # stderr/stdout can be run with non-os handles in the call - - # but then it does introduce issues with terminal-detection and behaviour for stdout at least - # - # input is also a tee - we never want to change the source at this point - just log/process a side-channel of it. - # - #set id_in [shellfilter::stack::add $inchan tee_to_pipe -action sink-aside -settings [list -tag commandin -pipechan $inpipechan]] - - - #set id_out [shellfilter::stack::add stdout tee_to_log -action sink-aside -settings [list -tag shellstdout -syslog 127.0.0.1:514 -file ""]] - #set id_err [shellfilter::stack::add stderr tee_to_log -action sink-aside -settings [list -tag shellstderr -syslog 127.0.0.1:514 -file "stderr.txt"]] - - #we need to catch errors - and ensure stack::remove calls occur. - #An error can be raised if the command couldn't even launch, as opposed to a non-zero exitcode and stderr output from the command itself. - # - if {!$is_script} { - set experiment 0 - if {$experiment} { - try { - set results [exec {*}$commandlist] - set exitinfo [list exitcode 0] - } trap CHILDSTATUS {results options} { - set exitcode [lindex [dict get $options -errorcode] 2] - set exitinfo [list exitcode $exitcode] - } - } else { - if {[catch { - #run process with stdout/stderr/stdin or with configured channels - #set exitinfo [shellcommand_stdout_stderr $commandlist $outchan $errchan $inpipechan {*}$opts] - set exitinfo [shellcommand_stdout_stderr $commandlist $outchan $errchan stdin {*}$opts] - #puts stderr "---->exitinfo $exitinfo" - - #subprocess result should usually have an "exitcode" key - #but for background execution we will get a "pids" key of process ids. - } errMsg]} { - set exitinfo [list error "$errMsg" source shellcommand_stdout_stderr] - } - } - } else { - if {[catch { - #script result - set exitinfo [list result [uplevel #0 [list eval $commandlist]]] - } errMsg]} { - set exitinfo [list error "$errMsg" errorCode $::errorCode errorInfo "$::errorInfo"] - } - } - - - #the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal - #Remove execution-time Tees from stack - shellfilter::stack::remove stdout $id_out - shellfilter::stack::remove stderr $id_err - #shellfilter::stack::remove stderr $id_in - - - #chan configure stderr -buffering line - #flush stdout - - - ::shellfilter::log::write $runtag " return '$exitinfo'" - ::shellfilter::log::close $runtag - return $exitinfo - } - proc ::shellfilter::logtidyup { {tags {}} } { - variable sources - set worker_errorlist [list] - set tidied_sources [list] - set tidytag "logtidy" - - - # opening a thread or writing to a log/syslog close to possible process exit is probably not a great idea. - # we should ensure the thread already exists early on if we really need logging here. - # - #set tid [::shellfilter::log::open $tidytag {-syslog 127.0.0.1:514}] - #::shellfilter::log::write $tidytag " logtidyuptags '$tags'" - - foreach s $sources { - if {$s eq $tidytag} { - continue - } - #puts "logtidyup source $s" - set close 1 - if {[llength $tags]} { - if {$s ni $tags} { - set close 0 - } - } - if {$close} { - lappend tidied_sources $s - shellfilter::log::close $s - lappend worker_errorlist {*}[shellthread::manager::get_and_clear_errors $s] - } - } - set remaining_sources [list] - foreach s $sources { - if {$s ni $tidied_sources} { - lappend remaining_sources $s - } - } - - #set sources [concat $remaining_sources $tidytag] - set sources $remaining_sources - - #shellfilter::stack::unwind stdout - #shellfilter::stack::unwind stderr - return [list tidied $tidied_sources errors $worker_errorlist] - } - - #package require tcl::chan::null - # e.g set errchan [tcl::chan::null] - # e.g chan push stdout [shellfilter::chan::var new ::some_var] - proc ::shellfilter::shellcommand_stdout_stderr {commandlist outchan errchan inchan args} { - set valid_flags [list \ - -timeout \ - -outprefix \ - -errprefix \ - -debug \ - -copytempfile \ - -outbuffering \ - -errbuffering \ - -inbuffering \ - -readprocesstranslation \ - -outtranslation \ - -stdinhandler \ - -outchan \ - -errchan \ - -inchan \ - -teehandle\ - ] - - set runtag shellfilter-run2 - #JMN - load from config - #set tid [::shellfilter::log::open $runtag [list -syslog "127.0.0.1:514"]] - set tid [::shellfilter::log::open $runtag [list -syslog ""]] - - if {[llength $args] % 2} { - error "Trailing arguments after any positional arguments must be in pairs of the form -argname argvalue. Valid flags are:'$valid_flags'" - } - set invalid_flags [list] - foreach {k -} $args { - switch -- $k { - -timeout - - -outprefix - - -errprefix - - -debug - - -copytempfile - - -outbuffering - - -errbuffering - - -inbuffering - - -readprocesstranslation - - -outtranslation - - -stdinhandler - - -outchan - - -errchan - - -inchan - - -teehandle { - } - default { - lappend invalid_flags $k - } - } - } - if {[llength $invalid_flags]} { - error "Unknown option(s)'$invalid_flags': must be one of '$valid_flags'" - } - #line buffering generally best for output channels.. keeps relative output order of stdout/stdin closer to source order - #there may be data where line buffering is inappropriate, so it's configurable per std channel - #reading inputs with line buffering can result in extraneous newlines as we can't detect trailing data with no newline before eof. - set defaults [dict create \ - -outchan stdout \ - -errchan stderr \ - -inchan stdin \ - -outbuffering none \ - -errbuffering none \ - -readprocesstranslation auto \ - -outtranslation lf \ - -inbuffering none \ - -timeout 900000\ - -outprefix ""\ - -errprefix ""\ - -debug 0\ - -copytempfile 0\ - -stdinhandler ""\ - ] - - - - set args [dict merge $defaults $args] - set outbuffering [dict get $args -outbuffering] - set errbuffering [dict get $args -errbuffering] - set inbuffering [dict get $args -inbuffering] - set readprocesstranslation [dict get $args -readprocesstranslation] - set outtranslation [dict get $args -outtranslation] - set timeout [dict get $args -timeout] - set outprefix [dict get $args -outprefix] - set errprefix [dict get $args -errprefix] - set debug [dict get $args -debug] - set copytempfile [dict get $args -copytempfile] - set stdinhandler [dict get $args -stdinhandler] - - set debugname "shellfilter-debug" - - if {$debug} { - set tid [::shellfilter::log::open $debugname [list -syslog "127.0.0.1:514"]] - ::shellfilter::log::write $debugname " commandlist '$commandlist'" - } - #'clock micros' good enough id for shellcommand calls unless one day they can somehow be called concurrently or sequentially within a microsecond and within the same interp. - # a simple counter would probably work too - #consider other options if an alternative to the single vwait in this function is used. - set call_id [tcl::clock::microseconds] ; - set ::shellfilter::shellcommandvars($call_id,exitcode) "" - set ::shellfilter::shellcommandvars($call_id,timeoutid) "" - set waitvar ::shellfilter::shellcommandvars($call_id,waitvar) - if {$debug} { - ::shellfilter::log::write $debugname " waitvar '$waitvar'" - } - lassign [chan pipe] rderr wrerr - chan configure $wrerr -blocking 0 - - set custom_stderr "" - set lastitem [lindex $commandlist end] - #todo - ensure we can handle 2> file (space after >) - - #review - reconsider the handling of redirections such that tcl-style are handled totally separately to other shell syntaxes! - # - #note 2>@1 must ocur as last word for tcl - but 2@stdout can occur elsewhere - #(2>@stdout echoes to main stdout - not into pipeline) - #To properly do pipelines it looks like we will have to split on | and call this proc multiple times and wire it up accordingly (presumably in separate threads) - - switch -- [string trim $lastitem] { - {&} { - set name [lindex $commandlist 0] - #background execution - stdout and stderr from child still comes here - but process is backgrounded - #FIX! - this is broken for paths with backslashes for example - #set pidlist [exec {*}[concat $name [lrange $commandlist 1 end]]] - set pidlist [exec {*}$commandlist] - return [list pids $pidlist] - } - {2>&1} - {2>@1} { - set custom_stderr {2>@1} ;#use the tcl style - set commandlist [lrange $commandlist 0 end-1] - } - default { - # 2> filename - # 2>> filename - # 2>@ openfileid - set redir2test [string range $lastitem 0 1] - if {$redir2test eq "2>"} { - set custom_stderr $lastitem - set commandlist [lrange $commandlist 0 end-1] - } - } - } - set lastitem [lindex $commandlist end] - - set teefile "" ;#empty string, write, append - #an ugly hack.. because redirections seem to arrive wrapped - review! - #There be dragons here.. - #Be very careful with list manipulation of the commandlist string.. backslashes cause havoc. commandlist must always be a well-formed list. generally avoid string manipulations on entire list or accidentally breaking a list element into parts if it shouldn't be.. - #The problem here - is that we can't always know what was intended on the commandline regarding quoting - - ::shellfilter::log::write $runtag "checking for redirections in $commandlist" - #sometimes we see a redirection without a following space e.g >C:/somewhere - #normalize - switch -regexp -- $lastitem\ - {^>[/[:alpha:]]+} { - set lastitem "> [string range $lastitem 1 end]" - }\ - {^>>[/[:alpha:]]+} { - set lastitem ">> [string range $lastitem 2 end]" - } - - - #for a redirection, we assume either a 2-element list at tail of form {> {some path maybe with spaces}} - #or that the tail redirection is not wrapped.. x y z > {some path maybe with spaces} - #we can't use list methods such as llenth on a member of commandlist - set wordlike_parts [regexp -inline -all {\S+} $lastitem] - - if {([llength $wordlike_parts] >= 2) && ([lindex $wordlike_parts 0] in [list ">>" ">"])} { - #wrapped redirection - but maybe not 'well' wrapped (unquoted filename) - set lastitem [string trim $lastitem] ;#we often see { > something} - - #don't use lassign or lrange on the element itself without checking first - #we can treat the commandlist as a whole as a well formed list but not neccessarily each element within. - #lassign $lastitem redir redirtarget - #set commandlist [lrange $commandlist 0 end-1] - # - set itemchars [split $lastitem ""] - set firstchar [lindex $itemchars 0] - set lastchar [lindex $itemchars end] - - #NAIVE test for double quoted only! - #consider for example {"a" x="b"} - #testing first and last is not decisive - #We need to decide what level of drilling down is even appropriate here.. - #if something was double wrapped - it was perhaps deliberate so we don't interpret it as something(?) - set head_tail_chars [list $firstchar $lastchar] - set doublequoted [expr {[llength [lsearch -all $head_tail_chars "\""]] == 2}] - if {[string equal "\{" $firstchar] && [string equal "\}" $lastchar]} { - set curlyquoted 1 - } else { - set curlyquoted 0 - } - - if {$curlyquoted} { - #these are not the tcl protection brackets but ones supplied in the argument - #it's still not valid to use list operations on a member of the commandlist - set inner [string range $lastitem 1 end-1] - #todo - fix! we still must assume there could be list-breaking data! - set innerwords [regexp -inline -all {\S+} $inner] ;#better than [split $inner] because we don't get extra empty elements for each whitespace char - set redir [lindex $innerwords 0] ;#a *potential* redir - to be tested below - set redirtarget [lrange $innerwords 1 end] ;#all the rest - } elseif {$doublequoted} { - ::shellfilter::log::write $debugname "doublequoting at tail of command '$commandlist'" - set inner [string range $lastitem 1 end-1] - set innerwords [regexp -inline -all {\S+} $inner] - set redir [lindex $innerwords 0] - set redirtarget [lrange $innerwords 1 end] - } else { - set itemwords [regexp -inline -all {\S+} $lastitem] - # e.g > c:\test becomes > {c:\test} - # but > c/mnt/c/test/temp.txt stays as > /mnt/c/test/temp.txt - set redir [lindex $itemwords 0] - set redirtarget [lrange $itemwords 1 end] - } - set commandlist [lrange $commandlist 0 end-1] - - } elseif {[lindex $commandlist end-1] in [list ">>" ">"]} { - #unwrapped redirection - #we should be able to use list operations like lindex and lrange here as the command itself is hopefully still a well formed list - set redir [lindex $commandlist end-1] - set redirtarget [lindex $commandlist end] - set commandlist [lrange $commandlist 0 end-2] - } else { - #no redirection - set redir "" - set redirtarget "" - #no change to command list - } - - - switch -- $redir { - ">>" - ">" { - set redirtarget [string trim $redirtarget "\""] - ::shellfilter::log::write $runtag " have redirection '$redir' to '$redirtarget'" - - set winfile $redirtarget ;#default assumption - switch -glob -- $redirtarget { - "/c/*" { - set winfile "c:/[string range $redirtarget 3 end]" - } - "/mnt/c/*" { - set winfile "c:/[string range $redirtarget 7 end]" - } - } - - if {[file exists [file dirname $winfile]]} { - #containing folder for target exists - if {$redir eq ">"} { - set teefile "write" - } else { - set teefile "append" - } - ::shellfilter::log::write $runtag "Directory exists '[file dirname $winfile]' operation:$teefile" - } else { - #we should be writing to a file.. but can't - ::shellfilter::log::write $runtag "cannot verify directory exists '[file dirname $winfile]'" - } - } - default { - ::shellfilter::log::write $runtag "No redir found!!" - } - } - - #often first element of command list is wrapped and cannot be run directly - #e.g {{ls -l} {> {temp.tmp}}} - #we will assume that if there is a single element which is a pathname containing a space - it is doubly wrapped. - # this may not be true - and the command may fail if it's just {c:\program files\etc} but it is the less common case and we currently have no way to detect. - #unwrap first element.. will not affect if not wrapped anyway (subject to comment above re spaces) - set commandlist [concat [lindex $commandlist 0] [lrange $commandlist 1 end]] - - #todo? - #child process environment. - # - to pass a different environment to the child - we would need to save the env array, modify as required, and then restore the env array. - - #to restore buffering states after run - set remember_in_out_err_buffering [list \ - [chan configure $inchan -buffering] \ - [chan configure $outchan -buffering] \ - [chan configure $errchan -buffering] \ - ] - - set remember_in_out_err_translation [list \ - [chan configure $inchan -translation] \ - [chan configure $outchan -translation] \ - [chan configure $errchan -translation] \ - ] - - - - - #chan configure $inchan -buffering none -blocking 1 ;#test - chan configure $inchan -buffering $inbuffering -blocking 0 ;#we are setting up a readable handler for this - so non-blocking ok - - - chan configure $errchan -buffering $errbuffering - #chan configure $outchan -blocking 0 - chan configure $outchan -buffering $outbuffering ;#don't configure non-blocking. weird duplicate of *second* line occurs if you do. - # - - #-------------------------------------------- - #Tested on windows. Works to stop in output when buffering is none, reading from channel with -translation auto - #cmd, pwsh, tcl - #chan configure $outchan -translation lf - #chan configure $errchan -translation lf - #-------------------------------------------- - chan configure $outchan -translation $outtranslation - chan configure $errchan -translation $outtranslation - - #puts stderr "chan configure $wrerr [chan configure $wrerr]" - if {$debug} { - ::shellfilter::log::write $debugname "COMMAND [list $commandlist] strlen:[string length $commandlist] llen:[llength $commandlist]" - } - #todo - handle custom redirection of stderr to a file? - if {[string length $custom_stderr]} { - #::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist $custom_stderr] a+" - #set rdout [open |[concat $commandlist $custom_stderr] a+] - ::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist [list $custom_stderr <@$inchan]] [list RDONLY]" - set rdout [open |[concat $commandlist [list <@$inchan $custom_stderr]] [list RDONLY]] - set rderr "bogus" ;#so we don't wait for it - } else { - ::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist [list 2>@$wrerr <@$inchan]] [list RDONLY]" - #set rdout [open |[concat $commandlist [list 2>@$wrerr]] a+] - #set rdout [open |[concat $commandlist [list 2>@$wrerr]] [list RDWR]] - - # If we don't redirect stderr to our own tcl-based channel - then the transforms don't get applied. - # This is the whole reason we need these file-event loops. - # Ideally we need something like exec,open in tcl that interacts with transformed channels directly and emits as it runs, not only at termination - # - and that at least appears like a terminal to the called command. - #set rdout [open |[concat $commandlist [list 2>@stderr <@$inchan]] [list RDONLY]] - - #REVIEW! - #if the child process takes a while to begin reading stdin - the data on stdin between when we stopped the parent chan event handler and when the child gets data, - #seems to stay buffered somewhere. It is then read by the parent, after the child returns. (ie not lost, but out-of-order) - #This can be apparent sometimes even with fast typing upon calling an executable. (e.g occasionally even vim - but seems to be timing based so might only happen first time if at all) - # see scriptlib/stdin_race.tcl etc test files. - #similar problem with python & perl - issue seems to be in libc or OS buffering behaviour for standard channels. - #note that zig (repo/jn/zig/stdin_race) seems to avoid this issue - todo - make zig based binary extension for open/exec? - - set rdout [open |[concat $commandlist [list 2>@$wrerr <@$inchan]] [list RDONLY]] - - chan configure $rderr -buffering $errbuffering -blocking 0 - chan configure $rderr -translation $readprocesstranslation - } - - - - set command_pids [pid $rdout] - #puts stderr "command_pids: $command_pids" - #tcl::process ensemble only available in 8.7+ - and it didn't prove useful here anyway - # the child process generally won't shut down until channels are closed. - # premature EOF on grandchild process launch seems to be due to lack of terminal emulation when redirecting stdin/stdout. - # worked around in punk/repl using 'script' command as a fake tty. - #set subprocesses [tcl::process::list] - #puts stderr "subprocesses: $subprocesses" - #if {[lindex $command_pids 0] ni $subprocesses} { - # puts stderr "pid [lindex $command_pids 0] not running $errMsg" - #} else { - # puts stderr "pid [lindex $command_pids 0] is running" - #} - - - if {$debug} { - ::shellfilter::log::write $debugname "pipeline pids: $command_pids" - } - - #jjj - - - chan configure $rdout -buffering $outbuffering -blocking 0 - chan configure $rdout -translation $readprocesstranslation - - if {![string length $custom_stderr]} { - chan event $rderr readable [list apply {{chan other wrerr outchan errchan waitfor errprefix errbuffering debug debugname pids} { - if {$errbuffering eq "line"} { - set countchunk [chan gets $chan chunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved - #errprefix only applicable to line buffered output - if {$countchunk >= 0} { - if {[chan eof $chan]} { - puts -nonewline $errchan ${errprefix}$chunk - } else { - puts $errchan "${errprefix}$chunk" - } - } - } else { - set chunk [chan read $chan] - if {[string length $chunk]} { - puts -nonewline $errchan $chunk - } - } - if {[chan eof $chan]} { - flush $errchan ;#jmn - #set subprocesses [tcl::process::list] - #puts stderr "subprocesses: $subprocesses" - #if {[lindex $pids 0] ni $subprocesses} { - # puts stderr "stderr reader: pid [lindex $pids 0] no longer running" - #} else { - # puts stderr "stderr reader: pid [lindex $pids 0] still running" - #} - chan close $chan - #catch {chan close $wrerr} - #if {$other ni [chan names]} { - # set $waitfor stderr - #} - if {[catch {chan configure $other}]} { - set $waitfor stderr - } - } - }} $rderr $rdout $wrerr $outchan $errchan $waitvar $errprefix $errbuffering $debug $debugname $command_pids] - } - - #todo - handle case where large amount of stdin coming in faster than rdout can handle - #as is - arbitrary amount of memory could be used because we aren't using a filevent for rdout being writable - # - we're just pumping it in to the non-blocking rdout buffers - # ie there is no backpressure and stdin will suck in as fast as possible. - # for most commandlines this probably isn't too big a deal.. but it could be a problem for multi-GB disk images etc - # - # - - ## Note - detecting trailing missing nl before eof is basically the same here as when reading rdout from executable - # - but there is a slight difference in that with rdout we get an extra blocked state just prior to the final read. - # Not known if that is significant - ## with inchan configured -buffering line - #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl -r cat - #warning reading input with -buffering line. Cannot detect missing trailing-newline at eof - #instate b:0 eof:0 pend:-1 count:4 - #test - #instate b:0 eof:0 pend:-1 count:3 - #etc - #instate b:0 eof:1 pend:-1 count:-1 - - #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl -r cat - #warning reading input with -buffering line. Cannot detect missing trailing-newline at eof - #instate b:0 eof:0 pend:-1 count:4 - #test - #instate b:0 eof:1 pend:-1 count:3 - #etc - - if 0 { - chan event $inchan readable [list apply {{chan wrchan inbuffering waitfor} { - #chan copy stdin $chan ;#doesn't work in a chan event - if {$inbuffering eq "line"} { - set countchunk [chan gets $chan chunk] - #puts $wrchan "stdinstate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" - if {$countchunk >= 0} { - if {[chan eof $chan]} { - puts -nonewline $wrchan $chunk - } else { - puts $wrchan $chunk - } - } - } else { - set chunk [chan read $chan] - if {[string length $chunk]} { - puts -nonewline $wrchan $chunk - } - } - if {[chan eof $chan]} { - puts stderr "|stdin_reader>eof [chan configure stdin]" - chan event $chan readable {} - #chan close $chan - chan close $wrchan write ;#half close - #set $waitfor "stdin" - } - }} $inchan $rdout $inbuffering $waitvar] - - if {[string length $stdinhandler]} { - chan configure stdin -buffering line -blocking 0 - chan event stdin readable $stdinhandler - } - } - - set actual_proc_out_buffering [chan configure $rdout -buffering] - set actual_outchan_buffering [chan configure $outchan -buffering] - #despite whatever is configured - we match our reading to how we need to output - set read_proc_out_buffering $actual_outchan_buffering - - - - if {[string length $teefile]} { - set logname "redir_[string map {: _} $winfile]_[tcl::clock::microseconds]" - set tid [::shellfilter::log::open $logname {-syslog 127.0.0.1:514}] - if {$teefile eq "write"} { - ::shellfilter::log::write $logname "opening '$winfile' for write" - set fd [open $winfile w] - } else { - ::shellfilter::log::write $logname "opening '$winfile' for appending" - set fd [open $winfile a] - } - #chan configure $fd -translation lf - chan configure $fd -translation $outtranslation - chan configure $fd -encoding utf-8 - - set tempvar_bytetotal [namespace current]::totalbytes[tcl::clock::microseconds] - set $tempvar_bytetotal 0 - chan event $rdout readable [list apply {{chan other wrerr outchan errchan read_proc_out_buffering waitfor outprefix call_id debug debugname writefile writefilefd copytempfile bytevar logtag} { - #review - if we write outprefix to normal stdout.. why not to redirected file? - #usefulness of outprefix is dubious - upvar $bytevar totalbytes - if {$read_proc_out_buffering eq "line"} { - #set outchunk [chan read $chan] - set countchunk [chan gets $chan outchunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved - if {$countchunk >= 0} { - if {![chan eof $chan]} { - set numbytes [expr {[string length $outchunk] + 1}] ;#we are assuming \n not \r\n - but count won't/can't be completely accurate(?) - review - puts $writefilefd $outchunk - } else { - set numbytes [string length $outchunk] - puts -nonewline $writefilefd $outchunk - } - incr totalbytes $numbytes - ::shellfilter::log::write $logtag "${outprefix} wrote $numbytes bytes to $writefile" - #puts $outchan "${outprefix} wrote $numbytes bytes to $writefile" - } - } else { - set outchunk [chan read $chan] - if {[string length $outchunk]} { - puts -nonewline $writefilefd $outchunk - set numbytes [string length $outchunk] - incr totalbytes $numbytes - ::shellfilter::log::write $logtag "${outprefix} wrote $numbytes bytes to $writefile" - } - } - if {[chan eof $chan]} { - flush $writefilefd ;#jmn - #set blocking so we can get exit code - chan configure $chan -blocking 1 - catch {::shellfilter::log::write $logtag "${outprefix} total bytes $totalbytes written to $writefile"} - #puts $outchan "${outprefix} total bytes $totalbytes written to $writefile" - catch {close $writefilefd} - if {$copytempfile} { - catch {file copy $writefile "[file rootname $writefile]_copy[file extension $writefile]"} - } - try { - chan close $chan - set ::shellfilter::shellcommandvars($call_id,exitcode) 0 - if {$debug} { - ::shellfilter::log::write $debugname "(teefile) -- child process returned no error. (exit code 0) --" - } - } trap CHILDSTATUS {result options} { - set code [lindex [dict get $options -errorcode] 2] - if {$debug} { - ::shellfilter::log::write $debugname "(teefile) CHILD PROCESS EXITED with code: $code" - } - set ::shellfilter::shellcommandvars($call_id,exitcode) $code - } - catch {chan close $wrerr} - #if {$other ni [chan names]} { - # set $waitfor stdout - #} - if {[catch {chan configure $other}]} { - set $waitfor stdout - } - } - }} $rdout $rderr $wrerr $outchan $errchan $read_proc_out_buffering $waitvar $outprefix $call_id $debug $debugname $winfile $fd $copytempfile $tempvar_bytetotal $logname] - - } else { - - # This occurs when we have outbuffering set to 'line' - as the 'input' from rdout which comes from the executable is also configured to 'line' - # where b:0|1 is whether chan blocked $chan returns 0 or 1 - # pend is the result of chan pending $chan - # eof is the resot of chan eof $chan - - - ##------------------------- - ##If we still read with gets,to retrieve line by line for output to line-buffered output - but the input channel is configured with -buffering none - ## then we can detect the difference - # there is an extra blocking read - but we can stil use eof with data to detect the absent newline and avoid passing an extra one on. - #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl /c cat - #instate b:0 eof:0 pend:-1 count:4 - #test - #instate b:0 eof:0 pend:-1 count:3 - #etc - #instate b:0 eof:1 pend:-1 count:-1 - - #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl /u/c cat - #instate b:0 eof:0 pend:-1 count:4 - #test - #instate b:1 eof:0 pend:-1 count:-1 - #instate b:0 eof:1 pend:-1 count:3 - #etc - ##------------------------ - - - #this should only occur if upstream is coming from stdin reader that has line buffering and hasn't handled the difference properly.. - ###reading with gets from line buffered input with trailing newline - #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl /c cat - #instate b:0 eof:0 pend:-1 count:4 - #test - #instate b:0 eof:0 pend:-1 count:3 - #etc - #instate b:0 eof:1 pend:-1 count:-1 - - ###reading with gets from line buffered input with trailing newline - ##No detectable difference! - #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl /c cat - #instate b:0 eof:0 pend:-1 count:4 - #test - #instate b:0 eof:0 pend:-1 count:3 - #etc - #instate b:0 eof:1 pend:-1 count:-1 - ##------------------------- - - #Note that reading from -buffering none and writing straight out gives no problem because we pass the newlines through as is - - - #set ::shellfilter::chan::lastreadblocked_nodata_noeof($rdout) 0 ;#a very specific case of readblocked prior to eof.. possibly not important - #this detection is disabled for now - but left for debugging in case it means something.. or changes - chan event $rdout readable [list apply {{chan other wrerr outchan errchan read_proc_out_buffering waitfor outprefix call_id debug debugname pids} { - #set outchunk [chan read $chan] - - if {$read_proc_out_buffering eq "line"} { - set countchunk [chan gets $chan outchunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved - #countchunk can be -1 before eof e.g when blocked - #debugging output inline with data - don't leave enabled - #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" - if {$countchunk >= 0} { - if {![chan eof $chan]} { - puts $outchan ${outprefix}$outchunk - } else { - puts -nonewline $outchan ${outprefix}$outchunk - #if {$::shellfilter::chan::lastreadblocked_nodata_noeof($chan)} { - # seems to be the usual case - #} else { - # #false alarm, or ? we've reached eof with data but didn't get an empty blocking read just prior - # #Not known if this occurs - # #debugging output inline with data - don't leave enabled - # puts $outchan "!!!prev read didn't block: instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" - #} - } - #set ::shellfilter::chan::lastreadblocked_nodata_noeof($chan) 0 - } else { - #set ::shellfilter::chan::lastreadblocked_nodata_noeof($chan) [expr {[chan blocked $chan] && ![chan eof $chan]}] - } - } else { - #puts $outchan "read CHANNEL $chan [chan configure $chan]" - #puts $outchan "write CHANNEL $outchan b:[chan configure $outchan -buffering] t:[chan configure $outchan -translation] e:[chan configure $outchan -encoding]" - set outchunk [chan read $chan] - #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:[string length $outchunk]" - if {[string length $outchunk]} { - #set stringrep [encoding convertfrom utf-8 $outchunk] - #set newbytes [encoding convertto utf-16 $stringrep] - #puts -nonewline $outchan $newbytes - puts -nonewline $outchan $outchunk - } - } - - if {[chan eof $chan]} { - flush $outchan ;#jmn - #for now just look for first element in the pid list.. - #set subprocesses [tcl::process::list] - #puts stderr "subprocesses: $subprocesses" - #if {[lindex $pids 0] ni $subprocesses} { - # puts stderr "stdout reader pid: [lindex $pids 0] no longer running" - #} else { - # puts stderr "stdout reader pid: [lindex $pids 0] still running" - #} - - #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan]" - chan configure $chan -blocking 1 ;#so we can get exit code - try { - chan close $chan - set ::shellfilter::shellcommandvars($call_id,exitcode) 0 - if {$debug} { - ::shellfilter::log::write $debugname " -- child process returned no error. (exit code 0) --" - } - } trap CHILDSTATUS {result options} { - set code [lindex [dict get $options -errorcode] 2] - set ::shellfilter::shellcommandvars($call_id,exitcode) $code - if {$debug} { - ::shellfilter::log::write $debugname " CHILD PROCESS EXITED with code: $code" - } - } trap CHILDKILLED {result options} { - #set code [lindex [dict get $options -errorcode] 2] - #set ::shellfilter::shellcommandvars(%id%,exitcode) $code - set ::shellfilter::shellcommandvars($call_id,exitcode) "childkilled" - if {$debug} { - ::shellfilter::log::write $debugname " CHILD PROCESS EXITED with result:'$result' options:'$options'" - } - - } finally { - #puts stdout "HERE" - #flush stdout - - } - catch {chan close $wrerr} - #if {$other ni [chan names]} { - # set $waitfor stdout - #} - if {[catch {chan configure $other}]} { - set $waitfor stdout - } - - } - }} $rdout $rderr $wrerr $outchan $errchan $read_proc_out_buffering $waitvar $outprefix $call_id $debug $debugname $command_pids] - } - - #todo - add ability to detect activity/data-flow and change timeout to only apply for period with zero data - #e.g x hrs with no data(?) - #reset timeout when data detected. - #review - stdin??? - set ::shellfilter::shellcommandvars($call_id,timeoutid) [after $timeout [string map [list %cpids% $command_pids %w% $waitvar %id% $call_id %wrerr% $wrerr %rdout% $rdout %rderr% $rderr %debug% $debug %debugname% $debugname] { - if {[info exists ::shellfilter::shellcommandvars(%id%,exitcode)]} { - #killing the task (on windows) doesn't seem to work if done after we close the output channels - catch {puts stderr "timeout - closing.";flush stderr} - set command_pids "{%cpids%}" - if {[llength $command_pids]} { - set pid [lindex $command_pids 0] - if {$::tcl_platform(platform) eq "windows"} { - set killcmd [list [auto_execok taskkill] /F /PID $pid] - } else { - #set killcmd [list kill -9 $pid] - set killcmd [list kill -TERM $pid] - } - if {[catch { - exec {*}$killcmd - } errM]} { - puts stderr "Failed to kill '$pid': errMsg $errM" - flush stderr - } - } - if {[set ::shellfilter::shellcommandvars(%id%,exitcode)] ne ""} { - catch { chan close %wrerr% } - catch { chan close %rdout%} - catch { chan close %rderr%} - } else { - chan configure %rdout% -blocking 1 - try { - chan close %rdout% - set ::shellfilter::shellcommandvars(%id%,exitcode) 0 - if {%debug%} { - ::shellfilter::log::write %debugname% "(timeout) -- child process returned no error. (exit code 0) --" - } - } trap CHILDSTATUS {result options} { - set code [lindex [dict get $options -errorcode] 2] - if {%debug%} { - ::shellfilter::log::write %debugname% "(timeout) CHILD PROCESS EXITED with code: $code" - } - set ::shellfilter::shellcommandvars(%id%,exitcode) $code - } trap CHILDKILLED {result options} { - set code [lindex [dict get $options -errorcode] 2] - #set code [dict get $options -code] - #set ::shellfilter::shellcommandvars(%id%,exitcode) $code - #set ::shellfilter::shellcommandvars($call_id,exitcode) "childkilled-timeout" - set ::shellfilter::shellcommandvars(%id%,exitcode) "childkilled-timeout" - if {%debug%} { - ::shellfilter::log::write %debugname% "(timeout) CHILDKILLED with code: $code" - ::shellfilter::log::write %debugname% "(timeout) result:$result options:$options" - } - - } - catch { chan close %wrerr% } - catch { chan close %rderr%} - } - set %w% "timeout" - } - }]] - - - vwait $waitvar - after cancel $::shellfilter::shellcommandvars($call_id,timeoutid) - - #puts stderr "waitvar:[set $waitvar]" - #flush stderr - #if {[set $waitvar] eq "timeout"} { - # #note: attempting to kill a process here (after channels closed) doesn't work (on windows at least) - # puts stderr "command_pids: $command_pids" - # flush stderr - #} - - set exitcode [set ::shellfilter::shellcommandvars($call_id,exitcode)] - if {![string is digit -strict $exitcode]} { - puts stderr "Process exited with non-numeric code: $exitcode closed_by:[set $waitvar]" - flush stderr - } - if {[string length $teefile]} { - #cannot be called from within an event handler above.. vwait reentrancy etc - catch {::shellfilter::log::close $logname} - } - - if {$debug} { - ::shellfilter::log::write $debugname " closed by: [set $waitvar] with exitcode: $exitcode" - catch {::shellfilter::log::close $debugname} - } - array unset ::shellfilter::shellcommandvars $call_id,* - - - #restore buffering to pre shellfilter::run state - lassign $remember_in_out_err_buffering bin bout berr - chan configure $inchan -buffering $bin - chan configure $outchan -buffering $bout - chan configure $errchan -buffering $berr - - lassign $remember_in_out_err_translation tin tout terr - chan configure $inchan -translation $tin - chan configure $outchan -translation $tout - chan configure $errchan -translation $terr - - - #in channel probably closed..(? review - should it be?) - catch { - chan configure $inchan -buffering $bin - } - - - return [list exitcode $exitcode] - } - -} - -package provide shellfilter [namespace eval shellfilter { - variable version - set version 0.2 -}] diff --git a/src/vfs/_vfscommon.vfs/modules/shellthread-1.6.1.tm b/src/vfs/_vfscommon.vfs/modules/shellthread-1.6.1.tm deleted file mode 100644 index 94f70842..00000000 --- a/src/vfs/_vfscommon.vfs/modules/shellthread-1.6.1.tm +++ /dev/null @@ -1,829 +0,0 @@ -#package require logger - - -package require Thread - -namespace eval shellthread { - - proc iso8601 {{tsmicros ""}} { - if {$tsmicros eq ""} { - set tsmicros [tcl::clock::microseconds] - } else { - set microsnow [tcl::clock::microseconds] - if {[tcl::string::length $tsmicros] != [tcl::string::length $microsnow]} { - error "iso8601 requires 'clock micros' or empty string to create timestamp" - } - } - set seconds [expr {$tsmicros / 1000000}] - return [tcl::clock::format $seconds -format "%Y-%m-%d_%H-%M-%S"] - } -} - -namespace eval shellthread::worker { - variable settings - variable sysloghost_port - variable sock - variable logfile "" - variable fd - variable client_ids [list] - variable ts_start_micros - variable errorlist [list] - variable inpipe "" - - proc bgerror {args} { - variable errorlist - lappend errorlist $args - } - proc send_errors_now {tidcli} { - variable errorlist - thread::send -async $tidcli [list shellthread::manager::report_worker_errors [list worker_tid [thread::id] errors $errorlist]] - } - proc add_client_tid {tidcli} { - variable client_ids - if {$tidcli ni $client_ids} { - lappend client_ids $tidcli - } - } - proc init {tidclient start_m settingsdict} { - variable sysloghost_port - variable logfile - variable settings - interp bgerror {} shellthread::worker::bgerror - #package require overtype ;#overtype uses tcllib textutil, punk::char etc - currently too heavyweight in terms of loading time for use in threads. - variable client_ids - variable ts_start_micros - lappend client_ids $tidclient - set ts_start_micros $start_m - - set defaults [list -raw 0 -file "" -syslog "" -direction out] - set settings [dict merge $defaults $settingsdict] - - set syslog [dict get $settings -syslog] - if {[string length $syslog]} { - lassign [split $syslog :] s_host s_port - set sysloghost_port [list $s_host $s_port] - if {[catch {package require udp} errm]} { - #disable rather than bomb and interfere with any -file being written - #review - log/notify? - set sysloghost_port "" - } - } else { - set sysloghost_port "" - } - - set logfile [dict get $settings -file] - } - - proc start_pipe_read {source readchan args} { - #assume 1 inpipe for now - variable inpipe - variable sysloghost_port - variable logfile - - set defaults [dict create -buffering \uFFFF ] - set opts [dict merge $defaults $args] - if {[dict exists $opts -readbuffering]} { - set readbuffering [dict get $opts -readbuffering] - } else { - if {[dict get $opts -buffering] eq "\uFFFF"} { - #get buffering setting from the channel as it was set prior to thread::transfer - set readbuffering [chan configure $readchan -buffering] - } else { - set readbuffering [dict get $opts -buffering] - chan configure $readchan -buffering $readbuffering - } - } - if {[dict exists $opts -writebuffering]} { - set writebuffering [dict get $opts -writebuffering] - } else { - if {[dict get $opts -buffering] eq "\uFFFF"} { - set writebuffering line - #set writebuffering [chan configure $writechan -buffering] - } else { - set writebuffering [dict get $opts -buffering] - #can configure $writechan -buffering $writebuffering - } - } - - chan configure $readchan -translation lf - - if {$readchan ni [chan names]} { - error "shellthread::worker::start_pipe_read - inpipe not configured. Use shellthread::manager::set_pipe_read_from_client to thread::transfer the pipe end" - } - set inpipe $readchan - chan configure $readchan -blocking 0 - set waitvar ::shellthread::worker::wait($inpipe,[clock micros]) - - #tcl::chan::fifo2 based pipe seems slower to establish events upon than Memchan - chan event $readchan readable [list ::shellthread::worker::pipe_read $readchan $source $waitvar $readbuffering $writebuffering] - vwait $waitvar - } - proc pipe_read {chan source waitfor readbuffering writebuffering} { - if {$readbuffering eq "line"} { - set chunksize [chan gets $chan chunk] - if {$chunksize >= 0} { - if {![chan eof $chan]} { - ::shellthread::worker::log pipe 0 - $source - info $chunk\n $writebuffering - } else { - ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering - } - } - } else { - set chunk [chan read $chan] - ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering - } - if {[chan eof $chan]} { - chan event $chan readable {} - set $waitfor "pipe" - chan close $chan - } - } - - proc start_pipe_write {source writechan args} { - variable outpipe - set defaults [dict create -buffering \uFFFF ] - set opts [dict merge $defaults $args] - - #todo! - set readchan stdin - - if {[dict exists $opts -readbuffering]} { - set readbuffering [dict get $opts -readbuffering] - } else { - if {[dict get $opts -buffering] eq "\uFFFF"} { - set readbuffering [chan configure $readchan -buffering] - } else { - set readbuffering [dict get $opts -buffering] - chan configure $readchan -buffering $readbuffering - } - } - if {[dict exists $opts -writebuffering]} { - set writebuffering [dict get $opts -writebuffering] - } else { - if {[dict get $opts -buffering] eq "\uFFFF"} { - #nothing explicitly set - take from transferred channel - set writebuffering [chan configure $writechan -buffering] - } else { - set writebuffering [dict get $opts -buffering] - can configure $writechan -buffering $writebuffering - } - } - - if {$writechan ni [chan names]} { - error "shellthread::worker::start_pipe_write - outpipe not configured. Use shellthread::manager::set_pipe_write_to_client to thread::transfer the pipe end" - } - set outpipe $writechan - chan configure $readchan -blocking 0 - chan configure $writechan -blocking 0 - set waitvar ::shellthread::worker::wait($outpipe,[clock micros]) - - chan event $readchan readable [list apply {{chan writechan source waitfor readbuffering} { - if {$readbuffering eq "line"} { - set chunksize [chan gets $chan chunk] - if {$chunksize >= 0} { - if {![chan eof $chan]} { - puts $writechan $chunk - } else { - puts -nonewline $writechan $chunk - } - } - } else { - set chunk [chan read $chan] - puts -nonewline $writechan $chunk - } - if {[chan eof $chan]} { - chan event $chan readable {} - set $waitfor "pipe" - chan close $writechan - if {$chan ne "stdin"} { - chan close $chan - } - } - }} $readchan $writechan $source $waitvar $readbuffering] - - vwait $waitvar - } - - - proc _initsock {} { - variable sysloghost_port - variable sock - if {[string length $sysloghost_port]} { - if {[catch {chan configure $sock} state]} { - set sock [udp_open] - chan configure $sock -buffering none -translation binary - chan configure $sock -remote $sysloghost_port - } - } - } - proc _reconnect {} { - variable sock - catch {close $sock} - _initsock - return [chan configure $sock] - } - - proc send_info {client_tid ts_sent source msg} { - set ts_received [clock micros] - set lag_micros [expr {$ts_received - $ts_sent}] - set lag [expr {$lag_micros / 1000000.0}] ;#lag as x.xxxxxx seconds - log $client_tid $ts_sent $lag $source - info $msg line 1 - } - proc log {client_tid ts_sent lag source service level msg writebuffering {islog 0}} { - variable sock - variable fd - variable sysloghost_port - variable logfile - variable settings - - set logchunk $msg - - if {![dict get $settings -raw]} { - set tail_crlf 0 - set tail_lf 0 - set tail_cr 0 - #for cooked - always remove the trailing newline before splitting.. - # - #note that if we got our data from reading a non-line-buffered binary channel - then this naive line splitting will not split neatly for mixed line-endings. - # - #Possibly not critical as cooked is for logging and we are still preserving all \r and \n chars - but review and consider implementing a better split - #but add it back exactly as it was afterwards - #we can always split on \n - and any adjacent \r will be preserved in the rejoin - set lastchar [string range $logchunk end end] - if {[string range $logchunk end-1 end] eq "\r\n"} { - set tail_crlf 1 - set logchunk [string range $logchunk 0 end-2] - } else { - if {$lastchar eq "\n"} { - set tail_lf 1 - set logchunk [string range $logchunk 0 end-1] - } elseif {$lastchar eq "\r"} { - #\r line-endings are obsolete..and unlikely... and ugly as they can hide characters on the console. but we'll pass through anyway. - set tail_cr 1 - set logchunk [string range $logchunk 0 end-1] - } else { - #possibly a single line with no linefeed.. or has linefeeds only in the middle - } - } - - if {$ts_sent != 0} { - set micros [lindex [split [expr {$ts_sent / 1000000.0}] .] end] - set time_info [::shellthread::iso8601 $ts_sent].$micros - #set time_info "${time_info}+$lag" - set lagfp "+[format %f $lag]" - } else { - #from pipe - no ts_sent/lag info available - set time_info "" - set lagfp "" - } - - set idtail [string range $client_tid end-8 end] ;#enough for display purposes id - mostly zeros anyway - - - #set col0 [string repeat " " 9] - #set col1 [string repeat " " 27] - #set col2 [string repeat " " 11] - #set col3 [string repeat " " 22] - ##do not columnize the final data column or append to tail - or we could muck up the crlf integrity - #lassign [list [overtype::left $col0 $idtail] [overtype::left $col1 $time_info] [overtype::left $col2 $lagfp] [overtype::left $col3 $source]] c0 c1 c2 c3 - - set w0 9 - set w1 27 - set w2 11 - set w3 22 ;#review - this can truncate source name without indication tail is missing - #do not columnize the final data column or append to tail - or we could muck up the crlf integrity - lassign [list \ - [format %-${w0}s $idtail]\ - [format %-${w1}s $time_info]\ - [format %-${w2}s $lagfp]\ - [format %-${w3}s $source]\ - ] c0 c1 c2 c3 - set c2_blank [string repeat " " $w2] - - - #split on \n no matter the actual line-ending in use - #shouldn't matter as long as we don't add anything at the end of the line other than the raw data - #ie - don't quote or add spaces - set lines [split $logchunk \n] - - set i 1 - set outlines [list] - foreach ln $lines { - if {$i == 1} { - lappend outlines "$c0 $c1 $c2 $c3 $ln" - } else { - lappend outlines "$c0 $c1 $c2_blank $c3 $ln" - } - incr i - } - if {$tail_lf} { - set logchunk "[join $outlines \n]\n" - } elseif {$tail_crlf} { - set logchunk "[join $outlines \r\n]\r\n" - } elseif {$tail_cr} { - set logchunk "[join $outlines \r]\r" - } else { - #no trailing linefeed - set logchunk [join $outlines \n] - - } - - #set logchunk "[overtype::left $col0 $idtail] [overtype::left $col1 $time_info] [overtype::left $col2 "+$lagfp"] [overtype::left $col3 $source] $msg" - } - - if {[string length $sysloghost_port]} { - _initsock - catch {puts -nonewline $sock $logchunk} - } - #todo - sockets etc? - if {[string length $logfile]} { - #todo - setting to maintain open filehandle and reduce io. - # possible settings for buffersize - and maybe logrotation, although this could be left to client - #for now - default to safe option of open/close each write despite the overhead. - set fd [open $logfile a] - chan configure $fd -translation auto -buffering $writebuffering - #whether line buffered or not - by now our logchunk includes newlines - puts -nonewline $fd $logchunk - close $fd - } - } - - # - withdraw just this client - proc finish {tidclient} { - variable client_ids - if {($tidclient in $clientids) && ([llength $clientids] == 1)} { - terminate $tidclient - } else { - set posn [lsearch $client_ids $tidclient] - set client_ids [lreplace $clientids $posn $posn] - } - } - - #allow any client to terminate - proc terminate {tidclient} { - variable sock - variable fd - variable client_ids - if {$tidclient in $client_ids} { - catch {close $sock} - catch {close $fd} - set client_ids [list] - #review use of thread::release -wait - #docs indicate deprecated for regular use, and that we should use thread::join - #however.. how can we set a timeout on a thread::join ? - #by telling the thread to release itself - we can wait on the thread::send variable - # This needs review - because it's unclear that -wait even works on self - # (what does it mean to wait for the target thread to exit if the target is self??) - thread::release -wait - return [thread::id] - } else { - return "" - } - } - - -} - - -namespace eval shellthread::manager { - variable workers [dict create] - variable worker_errors [list] - variable timeouts - - variable free_threads [list] - #variable log_threads - - proc dict_getdef {dictValue args} { - if {[llength $args] < 2} { - error {wrong # args: should be "dict_getdef dictValue ?key ...? key default"} - } - set keys [lrange $args 0 end-1] - if {[tcl::dict::exists $dictValue {*}$keys]} { - return [tcl::dict::get $dictValue {*}$keys] - } else { - return [lindex $args end] - } - } - #new datastructure regarding workers and sourcetags required. - #one worker can service multiple sourcetags - but each sourcetag may be used by multiple threads too. - #generally each thread will use a specific sourcetag - but we may have pools doing similar things which log to same destination. - # - #As a convention we may use a sourcetag for the thread which started the worker that isn't actually used for logging - but as a common target for joins - #If the thread which started the thread calls leave_worker with that 'primary' sourcetag it means others won't be able to use that target - which seems reasonable. - #If another thread want's to maintain joinability beyond the span provided by the starting client, - #it can join with both the primary tag and a tag it will actually use for logging. - #A thread can join the logger with any existingtag - not just the 'primary' - #(which is arbitrary anyway. It will usually be the first in the list - but may be unsubscribed by clients and disappear) - proc join_worker {existingtag sourcetaglist} { - set client_tid [thread::id] - #todo - allow a source to piggyback on existing worker by referencing one of the sourcetags already using the worker - } - - proc new_pipe_worker {sourcetaglist {settingsdict {}}} { - if {[dict exists $settingsdict -workertype]} { - if {[string tolower [dict get $settingsdict -workertype]] ne "pipe"} { - error "new_pipe_worker error: -workertype ne 'pipe'. Set to 'pipe' or leave empty" - } - } - dict set settingsdict -workertype pipe - new_worker $sourcetaglist $settingsdict - } - - #it is up to caller to use a unique sourcetag (e.g by prefixing with own thread::id etc) - # This allows multiple threads to more easily write to the same named sourcetag if necessary - # todo - change sourcetag for a list of tags which will be handled by the same thread. e.g for multiple threads logging to same file - # - # todo - some protection mechanism for case where target is a file to stop creation of multiple worker threads writing to same file. - # Even if we use open fd,close fd wrapped around writes.. it is probably undesirable to have multiple threads with same target - # On the other hand socket targets such as UDP can happily be written to by multiple threads. - # For now the mechanism is that a call to new_worker (rename to open_worker?) will join the same thread if a sourcetag matches. - # but, as sourcetags can get removed(unsubbed via leave_worker) this doesn't guarantee two threads with same -file settings won't fight. - # Also.. the settingsdict is ignored when joining with a tag that exists.. this is problematic.. e.g logrotation where previous file still being written by existing worker - # todo - rename 'sourcetag' concept to 'targettag' ?? the concept is a mixture of both.. it is somewhat analagous to a syslog 'facility' - # probably new_worker should disallow auto-joining and we allow different workers to handle same tags simultaneously to support overlap during logrotation etc. - proc new_worker {sourcetaglist {settingsdict {}}} { - variable workers - set ts_start [clock micros] - set tidclient [thread::id] - set sourcetag [lindex $sourcetaglist 0] ;#todo - use all - - set defaults [dict create\ - -workertype message\ - ] - set settingsdict [dict merge $defaults $settingsdict] - - set workertype [string tolower [dict get $settingsdict -workertype]] - set known_workertypes [list pipe message] - if {$workertype ni $known_workertypes} { - error "new_worker - unknown -workertype $workertype. Expected one of '$known_workertypes'" - } - - if {[dict exists $workers $sourcetag]} { - set winfo [dict get $workers $sourcetag] - if {[dict get $winfo tid] ne "noop" && [thread::exists [dict get $winfo tid]]} { - #add our client-info to existing worker thread - dict lappend winfo list_client_tids $tidclient - dict set workers $sourcetag $winfo ;#writeback - return [dict get $winfo tid] - } - } - - #noop fake worker for empty syslog and empty file - if {$workertype eq "message"} { - if {[dict_getdef $settingsdict -syslog ""] eq "" && [dict_getdef $settingsdict -file ""] eq ""} { - set winfo [dict create tid noop list_client_tids [list $tidclient] ts_start $ts_start ts_end_list [list] workertype "message"] - dict set workers $sourcetag $winfo - return noop - } - } - - #check if there is an existing unsubscribed thread first - #don't use free_threads for pipe workertype for now.. - variable free_threads - if {$workertype ne "pipe"} { - if {[llength $free_threads]} { - #todo - re-use from tail - as most likely to have been doing similar work?? review - - set free_threads [lassign $free_threads tidworker] - #todo - keep track of real ts_start of free threads... kill when too old - set winfo [dict create tid $tidworker list_client_tids [list $tidclient] ts_start $ts_start ts_end_list [list] workertype [dict get $settingsdict -workertype]] - #puts stderr "shellfilter::new_worker Re-using free worker thread: $tidworker with tag $sourcetag" - dict set workers $sourcetag $winfo - return $tidworker - } - } - - - #set ts_start [::shellthread::iso8601] - set tidworker [thread::create -preserved] - set init_script [string map [list %ts_start% $ts_start %mp% [tcl::tm::list] %ap% $::auto_path %tidcli% $tidclient %sd% $settingsdict] { - #set tclbase [file dirname [file dirname [info nameofexecutable]]] - #set tcllib $tclbase/lib - #if {$tcllib ni $::auto_path} { - # lappend ::auto_path $tcllib - #} - - set ::settingsinfo [dict create %sd%] - #if the executable running things is something like a tclkit, - # then it's likely we will need to use the caller's auto_path and tcl::tm::list to find things - #The caller can tune the thread's package search by providing a settingsdict - #tcl::tm::add * must add in reverse order to get reulting list in same order as original - if {![dict exists $::settingsinfo tcl_tm_list]} { - #JMN2 - ::tcl::tm::add {*}[lreverse [list %mp%]] - } else { - tcl::tm::remove {*}[tcl::tm::list] - ::tcl::tm::add {*}[lreverse [dict get $::settingsinfo tcl_tm_list]] - } - if {![dict exists $::settingsinfo auto_path]} { - set ::auto_path [list %ap%] - } else { - set ::auto_path [dict get $::settingsinfo auto_path] - } - - package require punk::packagepreference - punk::packagepreference::install - - package require Thread - package require shellthread - if {![catch {::shellthread::worker::init %tidcli% %ts_start% $::settingsinfo} errmsg]} { - unset ::settingsinfo - set ::shellthread_init "ok" - } else { - unset ::settingsinfo - set ::shellthread_init "err $errmsg" - } - }] - - thread::send -async $tidworker $init_script - #thread::send $tidworker $init_script - set winfo [dict create tid $tidworker list_client_tids [list $tidclient] ts_start $ts_start ts_end_list [list]] - dict set workers $sourcetag $winfo - return $tidworker - } - - proc set_pipe_read_from_client {tag_pipename worker_tid rchan args} { - variable workers - if {![dict exists $workers $tag_pipename]} { - error "workerthread::manager::set_pipe_read_from_client source/pipename $tag_pipename not found" - } - set match_worker_tid [dict get $workers $tag_pipename tid] - if {$worker_tid ne $match_worker_tid} { - error "workerthread::manager::set_pipe_read_from_client source/pipename $tag_pipename workert_tid mismatch '$worker_tid' vs existing:'$match_worker_tid'" - } - #buffering set during channel creation will be preserved on thread::transfer - thread::transfer $worker_tid $rchan - #start_pipe_read will vwait - so we have to send async - thread::send -async $worker_tid [list ::shellthread::worker::start_pipe_read $tag_pipename $rchan] - #client may start writing immediately - but presumably it will buffer in fifo2 - } - - proc set_pipe_write_to_client {tag_pipename worker_tid wchan args} { - variable workers - if {![dict exists $workers $tag_pipename]} { - error "workerthread::manager::set_pipe_write_to_client pipename $tag_pipename not found" - } - set match_worker_tid [dict get $workers $tag_pipename tid] - if {$worker_tid ne $match_worker_tid} { - error "workerthread::manager::set_pipe_write_to_client pipename $tag_pipename workert_tid mismatch '$worker_tid' vs existing:'$match_worker_tid'" - } - #buffering set during channel creation will be preserved on thread::transfer - thread::transfer $worker_tid $wchan - thread::send -async $worker_tid [list ::shellthread::worker::start_pipe_write $tag_pipename $wchan] - } - - proc write_log {source msg args} { - variable workers - set ts_micros_sent [clock micros] - set defaults [list -async 1 -level info] - set opts [dict merge $defaults $args] - - if {[dict exists $workers $source]} { - set tidworker [dict get $workers $source tid] - if {$tidworker eq "noop"} { - return - } - if {![thread::exists $tidworker]} { - # -syslog -file ? - set tidworker [new_worker $source] - } - } else { - #auto create with no requirement to call new_worker.. warn? - # -syslog -file ? - error "write_log no log opened for source: $source" - set tidworker [new_worker $source] - } - set client_tid [thread::id] - if {[dict get $opts -async]} { - thread::send -async $tidworker [list ::shellthread::worker::send_info $client_tid $ts_micros_sent $source $msg] - } else { - thread::send $tidworker [list ::shellthread::worker::send_info $client_tid $ts_micros_sent $source $msg] - } - } - proc report_worker_errors {errdict} { - variable workers - set reporting_tid [dict get $errdict worker_tid] - dict for {src srcinfo} $workers { - if {[dict get $srcinfo tid] eq $reporting_tid} { - dict set srcinfo errors [dict get $errdict errors] - dict set workers $src $srcinfo ;#writeback updated - break - } - } - } - - #aka leave_worker - #Note that the tags may be on separate workertids, or some tags may share workertids - proc unsubscribe {sourcetaglist} { - variable workers - #workers structure example: - #[list sourcetag1 [list tid list_client_tids ] ts_start ts_end_list {}] - variable free_threads - set mytid [thread::id] ;#caller of shellthread::manager::xxx is the client thread - - set subscriberless_tags [list] - foreach source $sourcetaglist { - if {[dict exists $workers $source]} { - set list_client_tids [dict get $workers $source list_client_tids] - if {[set posn [lsearch $list_client_tids $mytid]] >= 0} { - set list_client_tids [lreplace $list_client_tids $posn $posn] - dict set workers $source list_client_tids $list_client_tids - } - if {![llength $list_client_tids]} { - lappend subscriberless_tags $source - } - } - } - - #we've removed our own tid from all the tags - possibly across multiplew workertids, and possibly leaving some workertids with no subscribers for a particular tag - or no subscribers at all. - - set subscriberless_workers [list] - set shuttingdown_workers [list] - foreach deadtag $subscriberless_tags { - set workertid [dict get $workers $deadtag tid] - set worker_tags [get_worker_tagstate $workertid] - set subscriber_count 0 - set kill_count 0 ;#number of ts_end_list entries - even one indicates thread is doomed - foreach taginfo $worker_tags { - incr subscriber_count [llength [dict get $taginfo list_client_tids]] - incr kill_count [llength [dict get $taginfo ts_end_list]] - } - if {$subscriber_count == 0} { - lappend subscriberless_workers $workertid - } - if {$kill_count > 0} { - lappend shuttingdown_workers $workertid - } - } - - #if worker isn't shutting down - add it to free_threads list - foreach workertid $subscriberless_workers { - if {$workertid ni $shuttingdown_workers} { - if {$workertid ni $free_threads && $workertid ne "noop"} { - lappend free_threads $workertid - } - } - } - - #todo - #unsub this client_tid from the sourcetags in the sourcetaglist. if no more client_tids exist for sourcetag, remove sourcetag, - #if no more sourcetags - add worker to free_threads - } - proc get_worker_tagstate {workertid} { - variable workers - set taginfo_list [list] - dict for {source sourceinfo} $workers { - if {[dict get $sourceinfo tid] eq $workertid} { - lappend taginfo_list $sourceinfo - } - } - return $taginfo_list - } - - #finalisation - proc shutdown_free_threads {{timeout 2500}} { - variable free_threads - if {![llength $free_threads]} { - return - } - upvar ::shellthread::manager::timeouts timeoutarr - if {[info exists timeoutarr(shutdown_free_threads)]} { - #already called - return false - } - #set timeoutarr(shutdown_free_threads) waiting - #after $timeout [list set timeoutarr(shutdown_free_threads) timed-out] - set ::shellthread::waitfor waiting - #after $timeout [list set ::shellthread::waitfor] - #2025-07 timed-out untested review - set cancelid [after $timeout [list set ::shellthread::waitfor timed-out]] - - set waiting_for [list] - set ended [list] - set timedout 0 - foreach tid $free_threads { - if {[thread::exists $tid]} { - lappend waiting_for $tid - #thread::send -async $tid [list shellthread::worker::terminate [thread::id]] timeoutarr(shutdown_free_threads) - thread::send -async $tid [list shellthread::worker::terminate [thread::id]] ::shellthread::waitfor - } - } - if {[llength $waiting_for]} { - for {set i 0} {$i < [llength $waiting_for]} {incr i} { - vwait ::shellthread::waitfor - if {$::shellthread::waitfor eq "timed-out"} { - set timedout 1 - break - } else { - after cancel $cancelid - lappend ended $::shellthread::waitfor - } - } - } - set free_threads [list] - return [dict create existed $waiting_for ended $ended timedout $timedout] - } - - #TODO - important. - #REVIEW! - #since moving to the unsubscribe mechansm - close_worker $source isn't being called - # - we need to set a limit to the number of free threads and shut down excess when detected during unsubscription - #instruction to shut-down the thread that has this source. - #instruction to shut-down the thread that has this source. - proc close_worker {source {timeout 2500}} { - variable workers - variable worker_errors - variable free_threads - upvar ::shellthread::manager::timeouts timeoutarr - set ts_now [clock micros] - #puts stderr "close_worker $source" - if {[dict exists $workers $source]} { - set tidworker [dict get $workers $source tid] - if {$tidworker in $freethreads} { - #make sure a thread that is being closed is removed from the free_threads list - set posn [lsearch $freethreads $tidworker] - set freethreads [lreplace $freethreads $posn $posn] - } - set mytid [thread::id] - set client_tids [dict get $workers $source list_client_tids] - if {[set posn [lsearch $client_tids $mytid]] >= 0} { - set client_tids [lreplace $client_tids $posn $posn] - #remove self from list of clients - dict set workers $source list_client_tids $client_tids - } - set ts_end_list [dict get $workers $source ts_end_list] ;#ts_end_list is just a list of timestamps of closing calls for this source - only one is needed to close, but they may all come in a flurry. - if {[llength $ts_end_list]} { - set last_end_ts [lindex $ts_end_list end] - if {(($tsnow - $last_end_ts) / 1000) >= $timeout} { - lappend ts_end_list $ts_now - dict set workers $source ts_end_list $ts_end_list - } else { - #existing close in progress.. assume it will work - return - } - } - - if {[thread::exists $tidworker]} { - #puts stderr "shellthread::manager::close_worker: thread $tidworker for source $source still running.. terminating" - - #review - timeoutarr is local var (?) - set timeoutarr($source) 0 - after $timeout [list set timeoutarr($source) 2] - - thread::send -async $tidworker [list shellthread::worker::send_errors_now [thread::id]] - thread::send -async $tidworker [list shellthread::worker::terminate [thread::id]] timeoutarr($source) - - #thread::send -async $tidworker [string map [list %tidclient% [thread::id]] { - # shellthread::worker::terminate %tidclient% - #}] timeoutarr($source) - - vwait timeoutarr($source) - #puts stderr "shellthread::manager::close_worker: thread $tidworker for source $source DONE1" - - thread::release $tidworker - #puts stderr "shellthread::manager::close_worker: thread $tidworker for source $source DONE2" - if {[dict exists $workers $source errors]} { - set errlist [dict get $workers $source errors] - if {[llength $errlist]} { - lappend worker_errors [list $source [dict get $workers $source]] - } - } - dict unset workers $source - } else { - #thread may have been closed by call to close_worker with another source with same worker - #clear workers record for this source - #REVIEW - race condition for re-creation of source with new workerid? - #check that record is subscriberless to avoid this - if {[llength [dict get $workers $source list_client_tids]] == 0} { - dict unset workers $source - } - } - } - #puts stdout "close_worker $source - end" - } - - #worker errors only available for a source after close_worker called on that source - #It is possible for there to be multiple entries for a source because new_worker can be called multiple times with same sourcetag, - proc get_and_clear_errors {source} { - variable worker_errors - set source_errors [lsearch -all -inline -index 0 $worker_errors $source] - set worker_errors [lsearch -all -inline -index 0 -not $worker_errors $source] - return $source_errors - } - - -} - -package provide shellthread [namespace eval shellthread { - variable version - set version 1.6.1 -}] - - - - - - - - - diff --git a/src/vfs/_vfscommon.vfs/modules/sqids-0.3.0.tm b/src/vfs/_vfscommon.vfs/modules/sqids-0.3.0.tm deleted file mode 100644 index 2d815e67..00000000 --- a/src/vfs/_vfscommon.vfs/modules/sqids-0.3.0.tm +++ /dev/null @@ -1,873 +0,0 @@ -#Do not update version of this file. Update sqids-buildversion.txt and run make.tcl to update the version in this file and copy to modules folder. -package require Tcl 8.6- - -#MIT license -#Julian Noble 2026 - -#example: -# % package require sqids -# % set s1 [sqids::idscope new] -# ::oo::Obj275 -# % $s1 encode {1 2 3} -# 86Rf07 -# % $s1 decode 86Rf07 -# 1 2 3 - - -namespace eval sqids { - oo::class create idscope { - variable o_alphabet - variable o_alphabet_configured - variable o_alpha_re - variable o_minlength - variable o_blocklist - #note that methods beginning with uppercase letters are private. - constructor {args} { - set defaults [dict create {*}{ - -alphabet "" - -minlength "" - -blocklist "" - }] - if {[llength $args] %2 !=0} { - error "sqids::idscope constructor: Require option value pairs. Known options:[dict keys $defaults]." - } - set useropts [dict create] - set explicit_empty_blocklist 0 ;#as opposed to default due to being unspecified. - dict for {k v} $args { - set fullmatch [tcl::prefix::match -error "" {-alphabet -minlength -blocklist} $k] - switch -exact -- $fullmatch { - -alphabet - -minlength { - dict set useropts $fullmatch $v - } - -blocklist { - if {[llength $v] == 0} { - set explicit_empty_blocklist 1 - } - dict set useropts -blocklist $v - } - default { - error "sqids::idscope constructor: unknown option '$k'. Known options:[dict keys $defaults]." - } - } - } - set opts [dict merge $defaults $useropts] - - set opt_alphabet [dict get $opts -alphabet] - if {$opt_alphabet eq ""} { - set o_alphabet $::sqids::data::default_alphabet - } else { - if {[string length $opt_alphabet] < 3} { - error "sqids::idscope constructor: -alphabet length must be at least 3." - } - #review - deny multibyte - if {[regexp {[^\u00-\u7F]} $opt_alphabet]} { - error "sqids::idscope constructor: -alphabet must not contain multibyte characters." - } - if {[regexp {(.).*\1} $opt_alphabet]} { - error "sqids::idscope constructor: -alphabet must contain unique characters." - } - set o_alphabet $opt_alphabet - } - set o_alphabet_configured $o_alphabet ;#for use in public method alphabet, which returns the configured alphabet in the order it was configured, not the shuffled order used for encoding. - set alphamatch [string map [list . \\. \[ \\\[ \] \\\] \{ \\\{ \} \\\}] $o_alphabet] ;#review - set o_alpha_re "^\[$alphamatch\]+\$" ;#independent of shuffled order. - - set o_alphabet [my shuffle $o_alphabet[set o_alphabet {}]] - - set opt_minlength [dict get $opts -minlength] - if {$opt_minlength eq ""} { - set o_minlength $::sqids::data::default_minlength - } else { - set maxval 255 - if {![string is integer -strict $opt_minlength] || $opt_minlength < 0 || $opt_minlength > $maxval} { - error "sqids constructor: -minlength must be an integer from 0 to $maxval inclusive." - } - set o_minlength $opt_minlength - } - set opt_blocklist [dict get $opts -blocklist] - if {!$explicit_empty_blocklist && $opt_blocklist eq ""} { - set o_blocklist $::sqids::data::default_blocklist - } else { - set o_blocklist $opt_blocklist - } - set o_blocklist [string tolower $o_blocklist] - - } - method config {{option {}}} { - #introspection method. - #return a dict of the configured options if no option specified, otherwise return the value of the specified option. - - #no facility is provided to change options after construction as a new idscope object should be used for different configurations (different scope of sqid ids). - #note that -alphabet refers to the configured alphabet in the order it was configured, not the shuffled order used for encoding. - if {$option eq ""} { - # - return [dict create {*}{ - } -blocklist $o_blocklist {*}{ - } -minlength $o_minlength {*}{ - } -alphabet $o_alphabet_configured {*}{ - } - ] - } - set fullmatch [tcl::prefix::match -error "" {-alphabet -minlength -blocklist} $option] - switch -exact -- $fullmatch { - -alphabet {return $o_alphabet_configured} - -minlength {return $o_minlength} - -blocklist {return $o_blocklist} - default { - error "sqids::idscope config: unknown option '$option'. Known options:-alphabet -minlength -blocklist." - } - } - } - method encode {numlist} { - if {[llength $numlist] == 0} {return} - #cannot encode negative numbers, or non-integers. - foreach num $numlist { - #tcl 9 'string is integer' supports bignums - which can be arbitrarily large. - #tcl 8.6 'string is integer' is limited to 2**32-1 - if {![string is integer -strict $num] || $num < 0} { - error "sqids encode: can only encode non-negative integers. Invalid value: '$num'" - } - } - return [my EncodeNumbers $numlist] - } - method EncodeNumbers {numlist {increment 0}} { - #assert number of letters in o_alphabet and number of letters in local alpha are the same and don't effectively change during this function. - #('set alpha {}' in calls to my shuffle is an optimization to avoid shared string and copy-on-write overhead. As alpha is set to the result, it doesn't violate the previous assertion.) - set alpha_len [string length $o_alphabet] - - if {$increment > $alpha_len} { - error "sqids EncodeNumbers: Reached max attempts to re-generate the ID" - } - - set offset [llength $numlist] - set i -1 - foreach v $numlist { - incr i - set x [scan [string index $o_alphabet [expr {$v % $alpha_len}]] %c] - set offset [expr {$offset + $x + $i}] - } - set offset [expr {$offset % $alpha_len}] - set offset [expr {($offset + $increment) % $alpha_len}] - set alpha [string range $o_alphabet $offset end][string range $o_alphabet 0 $offset-1] - set prefix [string index $alpha 0] - set alpha [string reverse $alpha] - set id $prefix - set i -1 - foreach num $numlist { - incr i - append id [my ToId $num [string range $alpha 1 end]] - if {$i < [llength $numlist]-1} { - append id [string index $alpha 0] - set alpha [my shuffle $alpha[set alpha {}]] - } - } - if {$o_minlength > [string length $id]} { - append id [string index $alpha 0] - while {$o_minlength - [string length $id] > 0} { - set alpha [my shuffle $alpha[set alpha {}]] - set numchars [expr {min($o_minlength - [string length $id],$alpha_len)}] - append id [string range $alpha 0 $numchars-1] - } - } - if {[my is_blocked $id]} { - set id [my EncodeNumbers $numlist [expr {$increment+1}]] - } - return $id - } - method is_blocked {id} { - #deliberately public method. - if {![llength $o_blocklist]} { - return 0 - } - #o_blocklist is stored in lowercase, so compare against lowercase id. - set idtest [string tolower $id] - set idlen [string length $idtest] - if {$idlen < 3} { - #sqids rule: short ids less than 3 chars will not be blocked. - return 0 - } - if {$idlen == 3} { - if {$idtest in $o_blocklist} { - return 1 - } - } else { - foreach blocked $o_blocklist { - if {[string length $blocked] <= 3} { - #sqids rule: blocklist entries of 3 chars will only be blocked if they match the entire id exactly, - #so skip them in this loop as we've already checked for exact matches of the whole id when idlen == 3. - continue - } - set posn [string first $blocked $idtest] - if {$posn == -1} { - continue - } - if {$posn == 0} { - #whether leetspeak or not, blocklist entries that match at the beginning of the id will be blocked. - return 1 - } - if {[regexp {[0-9]} $blocked]} { - #sqids rule: blocklist entries with digits (leetspeak) will only be blocked if the match is at the beginning or end of the id. - #we've already checked the beginning, so check the end now. - set endpos [expr {$idlen - [string length $blocked]}] - if {$posn == $endpos} { - return 1 - } - } else { - #sqids rule: blocklist entries without digits will be blocked if they match anywhere in the id. - return 1 - } - } - } - return 0 - } - method ToId {num alpha} { - set id "" - set alpha_len [string length $alpha] - while 1 { - set id [string index $alpha [expr {$num % $alpha_len}]]$id - set num [expr {$num / $alpha_len}] - if {$num == 0} break - } - return $id - } - method ToNumber {id alpha} { - set number 0 - set alpha_len [string length $alpha] - for {set i 0} {$i < [string length $id]} {incr i} { - set posn [string first [string index $id $i] $alpha] - set number [expr {($number * $alpha_len) + $posn}] - } - return $number - } - method shuffle {alpha} { - #public method. Primarily for internal use but can be used externally to examine the shuffled alphabet being used for encoding. - #e.g myscopeobject shuffle [myscopeobject config -alphabet] would show the shuffled alphabet being used for encoding. - #consistent shuffle (always produce the same result for same input) - set alpha_len [string length $alpha] - if {$alpha_len < 2} { - return $alpha - } - set chars [split $alpha ""] - - for {set i 0; set j [expr {$alpha_len-1}]} {$j > 0} {incr i; incr j -1} { - set iv [scan [lindex $chars $i] %c] - set jv [scan [lindex $chars $j] %c] - set r [expr {($i * $j + $iv + $jv) % $alpha_len}] - set item2 [lindex $chars $r] - lset chars $r [lindex $chars $i] - lset chars $i $item2 - } - return [join $chars ""] - } - method decode {id} { - if {$id eq ""} {return} - set result [list] - - if {![regexp $o_alpha_re $id]} { - puts stderr "sqids decode: ID contains characters not in the alphabet. re: $o_alpha_re id: $id" - return [list] - } - set prefix [string index $id 0] - set offset [string first $prefix $o_alphabet] - set alpha [string range $o_alphabet $offset end][string range $o_alphabet 0 $offset-1] - set alpha [string reverse $alpha] - set id [string range $id 1 end] - while {[string length $id] > 0} { - set separator [string index $alpha 0] - #split on first occurrence of separator only. - set sep_posn [string first $separator $id] - if {$sep_posn == -1} { - set parts [list $id] - } else { - set parts [list [string range $id 0 $sep_posn-1] [string range $id $sep_posn+1 end]] - } - #assert parts has 1 or 2 elements - - if {[lindex $parts 0] eq ""} { - #separator was at start of the id - done. - return $result - } - lappend result [my ToNumber [lindex $parts 0] [string range $alpha 1 end]] - if {[llength $parts] == 2} { - set alpha [my shuffle $alpha[set alpha {}]] - set id [lindex $parts 1] - } else { - set id "" - } - } - return $result - } - } -} -namespace eval sqids::data { - variable default_alphabet {abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789} - variable default_minlength 0 - variable default_blocklist { - 0rgasm - 1d10t - 1d1ot - 1di0t - 1diot - 1eccacu10 - 1eccacu1o - 1eccacul0 - 1eccaculo - 1mbec11e - 1mbec1le - 1mbeci1e - 1mbecile - a11upat0 - a11upato - a1lupat0 - a1lupato - aand - ah01e - ah0le - aho1e - ahole - al1upat0 - al1upato - allupat0 - allupato - ana1 - ana1e - anal - anale - anus - arrapat0 - arrapato - arsch - arse - ass - b00b - b00be - b01ata - b0ceta - b0iata - b0ob - b0obe - b0sta - b1tch - b1te - b1tte - ba1atkar - balatkar - bastard0 - bastardo - batt0na - battona - bitch - bite - bitte - bo0b - bo0be - bo1ata - boceta - boiata - boob - boobe - bosta - bran1age - bran1er - bran1ette - bran1eur - bran1euse - branlage - branler - branlette - branleur - branleuse - c0ck - c0g110ne - c0g11one - c0g1i0ne - c0g1ione - c0gl10ne - c0gl1one - c0gli0ne - c0glione - c0na - c0nnard - c0nnasse - c0nne - c0u111es - c0u11les - c0u1l1es - c0u1lles - c0ui11es - c0ui1les - c0uil1es - c0uilles - c11t - c11t0 - c11to - c1it - c1it0 - c1ito - cabr0n - cabra0 - cabrao - cabron - caca - cacca - cacete - cagante - cagar - cagare - cagna - cara1h0 - cara1ho - caracu10 - caracu1o - caracul0 - caraculo - caralh0 - caralho - cazz0 - cazz1mma - cazzata - cazzimma - cazzo - ch00t1a - ch00t1ya - ch00tia - ch00tiya - ch0d - ch0ot1a - ch0ot1ya - ch0otia - ch0otiya - ch1asse - ch1avata - ch1er - ch1ng0 - ch1ngadaz0s - ch1ngadazos - ch1ngader1ta - ch1ngaderita - ch1ngar - ch1ngo - ch1ngues - ch1nk - chatte - chiasse - chiavata - chier - ching0 - chingadaz0s - chingadazos - chingader1ta - chingaderita - chingar - chingo - chingues - chink - cho0t1a - cho0t1ya - cho0tia - cho0tiya - chod - choot1a - choot1ya - chootia - chootiya - cl1t - cl1t0 - cl1to - clit - clit0 - clito - cock - cog110ne - cog11one - cog1i0ne - cog1ione - cogl10ne - cogl1one - cogli0ne - coglione - cona - connard - connasse - conne - cou111es - cou11les - cou1l1es - cou1lles - coui11es - coui1les - couil1es - couilles - cracker - crap - cu10 - cu1att0ne - cu1attone - cu1er0 - cu1ero - cu1o - cul0 - culatt0ne - culattone - culer0 - culero - culo - cum - cunt - d11d0 - d11do - d1ck - d1ld0 - d1ldo - damn - de1ch - deich - depp - di1d0 - di1do - dick - dild0 - dildo - dyke - encu1e - encule - enema - enf01re - enf0ire - enfo1re - enfoire - estup1d0 - estup1do - estupid0 - estupido - etr0n - etron - f0da - f0der - f0ttere - f0tters1 - f0ttersi - f0tze - f0utre - f1ca - f1cker - f1ga - fag - fica - ficker - figa - foda - foder - fottere - fotters1 - fottersi - fotze - foutre - fr0c10 - fr0c1o - fr0ci0 - fr0cio - fr0sc10 - fr0sc1o - fr0sci0 - fr0scio - froc10 - froc1o - froci0 - frocio - frosc10 - frosc1o - frosci0 - froscio - fuck - g00 - g0o - g0u1ne - g0uine - gandu - go0 - goo - gou1ne - gouine - gr0gnasse - grognasse - haram1 - harami - haramzade - hund1n - hundin - id10t - id1ot - idi0t - idiot - imbec11e - imbec1le - imbeci1e - imbecile - j1zz - jerk - jizz - k1ke - kam1ne - kamine - kike - leccacu10 - leccacu1o - leccacul0 - leccaculo - m1erda - m1gn0tta - m1gnotta - m1nch1a - m1nchia - m1st - mam0n - mamahuev0 - mamahuevo - mamon - masturbat10n - masturbat1on - masturbate - masturbati0n - masturbation - merd0s0 - merd0so - merda - merde - merdos0 - merdoso - mierda - mign0tta - mignotta - minch1a - minchia - mist - musch1 - muschi - n1gger - neger - negr0 - negre - negro - nerch1a - nerchia - nigger - orgasm - p00p - p011a - p01la - p0l1a - p0lla - p0mp1n0 - p0mp1no - p0mpin0 - p0mpino - p0op - p0rca - p0rn - p0rra - p0uff1asse - p0uffiasse - p1p1 - p1pi - p1r1a - p1rla - p1sc10 - p1sc1o - p1sci0 - p1scio - p1sser - pa11e - pa1le - pal1e - palle - pane1e1r0 - pane1e1ro - pane1eir0 - pane1eiro - panele1r0 - panele1ro - paneleir0 - paneleiro - patakha - pec0r1na - pec0rina - pecor1na - pecorina - pen1s - pendej0 - pendejo - penis - pip1 - pipi - pir1a - pirla - pisc10 - pisc1o - pisci0 - piscio - pisser - po0p - po11a - po1la - pol1a - polla - pomp1n0 - pomp1no - pompin0 - pompino - poop - porca - porn - porra - pouff1asse - pouffiasse - pr1ck - prick - pussy - put1za - puta - puta1n - putain - pute - putiza - puttana - queca - r0mp1ba11e - r0mp1ba1le - r0mp1bal1e - r0mp1balle - r0mpiba11e - r0mpiba1le - r0mpibal1e - r0mpiballe - rand1 - randi - rape - recch10ne - recch1one - recchi0ne - recchione - retard - romp1ba11e - romp1ba1le - romp1bal1e - romp1balle - rompiba11e - rompiba1le - rompibal1e - rompiballe - ruff1an0 - ruff1ano - ruffian0 - ruffiano - s1ut - sa10pe - sa1aud - sa1ope - sacanagem - sal0pe - salaud - salope - saugnapf - sb0rr0ne - sb0rra - sb0rrone - sbattere - sbatters1 - sbattersi - sborr0ne - sborra - sborrone - sc0pare - sc0pata - sch1ampe - sche1se - sche1sse - scheise - scheisse - schlampe - schwachs1nn1g - schwachs1nnig - schwachsinn1g - schwachsinnig - schwanz - scopare - scopata - sexy - sh1t - shit - slut - sp0mp1nare - sp0mpinare - spomp1nare - spompinare - str0nz0 - str0nza - str0nzo - stronz0 - stronza - stronzo - stup1d - stupid - succh1am1 - succh1ami - succhiam1 - succhiami - sucker - t0pa - tapette - test1c1e - test1cle - testic1e - testicle - tette - topa - tr01a - tr0ia - tr0mbare - tr1ng1er - tr1ngler - tring1er - tringler - tro1a - troia - trombare - turd - twat - vaffancu10 - vaffancu1o - vaffancul0 - vaffanculo - vag1na - vagina - verdammt - verga - w1chsen - wank - wichsen - x0ch0ta - x0chota - xana - xoch0ta - xochota - z0cc01a - z0cc0la - z0cco1a - z0ccola - z1z1 - z1zi - ziz1 - zizi - zocc01a - zocc0la - zocco1a - zoccola - } -} - -package provide sqids [namespace eval sqids { - variable version - set version 0.3.0 -}] - diff --git a/src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/filelist-bindings.tcl b/src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/filelist-bindings.tcl deleted file mode 100644 index 821bb43a..00000000 --- a/src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/filelist-bindings.tcl +++ /dev/null @@ -1,1293 +0,0 @@ -# Copyright (c) 2002-2011 Tim Baker - -bind TreeCtrlFileList { - TreeCtrl::FileListEditCancel %W - TreeCtrl::DoubleButton1 %W %x %y - break -} -bind TreeCtrlFileList { - set TreeCtrl::Priv(selectMode) toggle - TreeCtrl::FileListButton1 %W %x %y - break -} -bind TreeCtrlFileList { - set TreeCtrl::Priv(selectMode) add - TreeCtrl::FileListButton1 %W %x %y - break -} -bind TreeCtrlFileList { - set TreeCtrl::Priv(selectMode) set - TreeCtrl::FileListButton1 %W %x %y - break -} -bind TreeCtrlFileList { - TreeCtrl::FileListMotion1 %W %x %y - break -} -bind TreeCtrlFileList { - TreeCtrl::FileListLeave1 %W %x %y - break -} -bind TreeCtrlFileList { - TreeCtrl::FileListRelease1 %W %x %y - break -} - -# Escape cancels any drag-and-drop operation in progress -bind TreeCtrlFileList { - TreeCtrl::FileListEscapeKey %W -} - -## Bindings for the Entry widget used for editing - -# Accept edit when we lose the focus -bind TreeCtrlEntry { - if {[winfo ismapped %W]} { - TreeCtrl::EditClose [winfo parent %W] entry 1 0 - } -} - -# Accept edit on -bind TreeCtrlEntry { - TreeCtrl::EditClose [winfo parent %W] entry 1 1 - break -} - -# Cancel edit on , use break as we are doing a "closing" action -# and don't want that propagated upwards -bind TreeCtrlEntry { - TreeCtrl::EditClose [winfo parent %W] entry 0 1 - break -} - -## Bindings for the Text widget used for editing - -# Accept edit when we lose the focus -bind TreeCtrlText { - if {[winfo ismapped %W]} { - TreeCtrl::EditClose [winfo parent %W] text 1 0 - } -} - -# Accept edit on -bind TreeCtrlText { - TreeCtrl::EditClose [winfo parent %W] text 1 1 - break -} - -# Cancel edit on , use break as we are doing a "closing" action -# and don't want that propagated upwards -bind TreeCtrlText { - TreeCtrl::EditClose [winfo parent %W] text 0 1 - break -} - -namespace eval TreeCtrl { - variable Priv - - # Number of milliseconds after clicking a selected item before the Edit - # widget appears. - set Priv(edit,delay) 500 - - # Try to deal with people importing ttk::entry into the global namespace; - # we want tk::entry - if {[llength [info commands ::tk::entry]]} { - set Priv(entryCmd) ::tk::entry - } else { - set Priv(entryCmd) ::entry - } -} - -# ::TreeCtrl::IsSensitive -# -# Returns 1 if the given window coordinates are over an element that should -# respond to mouse clicks. The list of elements that respond to mouse clicks -# is set by calling ::TreeCtrl::SetSensitive. -# -# Arguments: -# T The treectrl widget. -# x Window coord of pointer. -# y Window coord of pointer. - -proc ::TreeCtrl::IsSensitive {T x y} { - variable Priv - $T identify -array id $x $y - if {$id(where) ne "item" || $id(element) eq ""} { - return 0 - } - if {![$T item enabled $id(item)]} { - return 0 - } - foreach list $Priv(sensitive,$T) { - set eList [lassign $list C S] - if {[$T column compare $id(column) != $C]} continue - if {[$T item style set $id(item) $C] ne $S} continue - if {[lsearch -exact $eList $id(element)] == -1} continue - return 1 - } - return 0 -} - -# ::TreeCtrl::IsSensitiveMarquee -# -# Returns 1 if the given window coordinates are over an element that -# should respond to the marquee. The list of elements that respond to the -# marquee is set by calling ::TreeCtrl::SetSensitiveMarquee, or if that list -# is empty then the same list passed to ::TreeCtrl::SetSensitive. -# -# Arguments: -# T The treectrl widget. -# x Window coord of pointer. -# y Window coord of pointer. - -proc ::TreeCtrl::IsSensitiveMarquee {T x y} { - variable Priv - $T identify -array id $x $y - if {$id(where) ne "item" || $id(element) eq ""} { - return 0 - } - if {![$T item enabled $id(item)]} { - return 0 - } - if {![info exists Priv(sensitive,marquee,$T)]} { - set sensitive $Priv(sensitive,$T) - } elseif {[llength $Priv(sensitive,marquee,$T)] == 0} { - set sensitive $Priv(sensitive,$T) - } else { - set sensitive $Priv(sensitive,marquee,$T) - } - foreach list $sensitive { - set eList [lassign $list C S] - if {[$T column compare $id(column) != $C]} continue - if {[$T item style set $id(item) $C] ne $S} continue - if {[lsearch -exact $eList $id(element)] == -1} continue - return 1 - } - return 0 -} - -# ::TreeCtrl::FileListButton1 -# -# Handle . -# -# Arguments: -# T The treectrl widget. -# x Window coord of pointer. -# y Window coord of pointer. - -proc ::TreeCtrl::FileListButton1 {T x y} { - variable Priv - focus $T - $T identify -array id $x $y - set marquee 0 - set Priv(buttonMode) "" - foreach e {text entry} { - if {[winfo exists $T.$e] && [winfo ismapped $T.$e]} { - EditClose $T $e 1 0 - } - } - FileListEditCancel $T - # Click outside any item - if {$id(where) eq ""} { - set marquee 1 - - # Click in header - } elseif {$id(where) eq "header"} { - ButtonPress1 $T $x $y - - # Click in item - } elseif {$id(where) eq "item"} { - if {$id(button) || $id(line) ne ""} { - ButtonPress1 $T $x $y - } elseif {$id(column) ne ""} { - set item $id(item) - set drag 0 - if {[IsSensitive $T $x $y]} { - set Priv(drag,wasSel) [$T selection includes $item] - $T activate $item - if {$Priv(selectMode) eq "add"} { - BeginExtend $T $item - } elseif {$Priv(selectMode) eq "toggle"} { - BeginToggle $T $item - } elseif {![$T selection includes $item]} { - BeginSelect $T $item - } - - # Changing the selection might change the list - if {[$T item id $item] eq ""} return - - # Click selected item(s) to drag or rename - if {[$T selection includes $item]} { - set drag 1 - } - } elseif {[FileListEmulateWin7 $T] && [IsSensitiveMarquee $T $x $y]} { - # Click selected item(s) to drag or rename - if {[$T selection includes $item]} { - set Priv(drag,wasSel) 1 - $T activate $item - set drag 1 - # Click marquee-sensitive parts of an unselected item - # in single-select mode changes nothing until a drag - # occurs or the mouse button is released. - } elseif {[$T cget -selectmode] eq "single"} { - set Priv(drag,wasSel) 0 - set drag 1 - } else { - set marquee 1 - } - } else { - set marquee 1 - } - if {$drag} { - set Priv(drag,motion) 0 - set Priv(drag,click,x) $x - set Priv(drag,click,y) $y - set Priv(drag,x) [$T canvasx $x] - set Priv(drag,y) [$T canvasy $y] - set Priv(drop) "" - set Priv(drag,item) $item - set Priv(drag,C) $id(column) - set Priv(drag,E) $id(element) - set Priv(buttonMode) drag - } - } - } - if {$marquee && [$T cget -selectmode] eq "single"} { - set marquee 0 - $T selection clear - } - if {$marquee} { - set Priv(buttonMode) marquee - if {![info exists Priv(sensitive,marquee,$T)]} { - set Priv(sensitive,marquee,$T) {} - } - if {$Priv(selectMode) ne "set"} { - set Priv(selection) [$T selection get] - } else { - if {![FileListEmulateWin7 $T]} { - $T selection clear - } - set Priv(selection) {} - } - MarqueeBegin $T $x $y - - set Priv(marquee,motion) 0 - if {[FileListEmulateWin7 $T]} { - if {[IsSensitiveMarquee $T $x $y]} { - set item $id(item) - $T activate $item - if {$Priv(selectMode) ne "add"} { - $T selection anchor $item - } - } - } - } - return -} - -# ::TreeCtrl::FileListMotion1 -# -# Override default to handle "drag" and "marquee". -# -# Arguments: -# T The treectrl widget. -# x Window coord of pointer. -# y Window coord of pointer. - -proc ::TreeCtrl::FileListMotion1 {T x y} { - variable Priv - if {![info exists Priv(buttonMode)]} return - switch $Priv(buttonMode) { - "drag" - - "marquee" { - set Priv(autoscan,command,$T) {FileListMotion %T %x %y} - AutoScanCheck $T $x $y - FileListMotion $T $x $y - } - default { - Motion1 $T $x $y - } - } - return -} - -# ::TreeCtrl::FileListMotion -# -# Handle . -# -# Arguments: -# T The treectrl widget. -# x Window coord of pointer. -# y Window coord of pointer. - -proc ::TreeCtrl::FileListMotion {T x y} { - variable Priv - if {![info exists Priv(buttonMode)]} return - switch $Priv(buttonMode) { - "marquee" { - MarqueeUpdate $T $x $y - set select $Priv(selection) - set deselect {} - set items {} - - set Priv(marquee,motion) 1 - - set sensitive $Priv(sensitive,marquee,$T) - if {[llength $sensitive] == 0} { - set sensitive $Priv(sensitive,$T) - } - - # Check items covered by the marquee - foreach list [$T marque identify] { - set item [lindex $list 0] - if {![$T item enabled $item]} continue - - # Check covered columns in this item - foreach sublist [lrange $list 1 end] { - set column [lindex $sublist 0] - set ok 0 - - # Check covered elements in this column - foreach E [lrange $sublist 1 end] { - foreach sList $sensitive { - set sEList [lassign $sList sC sS] - if {[$T column compare $column != $sC]} continue - if {[$T item style set $item $sC] ne $sS} continue - if {[lsearch -exact $sEList $E] == -1} continue - set ok 1 - break - } - } - # Some sensitive elements in this column are covered - if {$ok} { - lappend items $item - } - } - } - foreach item $items { - # Toggle selected status - if {$Priv(selectMode) eq "toggle"} { - set i [lsearch -exact $Priv(selection) $item] - if {$i == -1} { - lappend select $item - } else { - set i [lsearch -exact $select $item] - set select [lreplace $select $i $i] - } - } else { - lappend select $item - } - } - $T selection modify $select all - } - "drag" { - if {!$Priv(drag,motion)} { - # Detect initial mouse movement - if {(abs($x - $Priv(drag,click,x)) <= 4) && - (abs($y - $Priv(drag,click,y)) <= 4)} return - - # In Win7 single-selectmode, when the insensitive parts of an - # unselected item are clicked, the active item and selection - # aren't changed until the drag begins. - if {[FileListEmulateWin7 $T] - && [$T cget -selectmode] eq "single" - && !$Priv(drag,wasSel)} { - $T activate $Priv(drag,item) - $T selection modify $Priv(drag,item) all - } - - set Priv(selection) [$T selection get] - set Priv(drop) "" - $T dragimage clear - # For each selected item, add some elements to the dragimage - foreach I $Priv(selection) { - foreach list $Priv(dragimage,$T) { - set EList [lassign $list C S] - if {[$T item style set $I $C] eq $S} { - eval $T dragimage add $I $C $EList - } - } - } - set Priv(drag,motion) 1 - TryEvent $T Drag begin {} - } - - # Find the element under the cursor - set drop "" - $T identify -array id $x $y - if {[IsSensitive $T $x $y]} { - set sensitive 1 - } elseif {[FileListEmulateWin7 $T] && [IsSensitiveMarquee $T $x $y]} { - set sensitive 1 - } else { - set sensitive 0 - } - if {$sensitive} { - set item $id(item) - # If the item is not in the pre-drag selection - # (i.e. not being dragged) and it is a directory, - # see if we can drop on it - if {[lsearch -exact $Priv(selection) $item] == -1} { - if {[$T item order $item -visible] < $Priv(DirCnt,$T)} { - set drop $item - # We can drop if dragged item isn't an ancestor - foreach item2 $Priv(selection) { - if {[$T item isancestor $item2 $item]} { - set drop "" - break - } - } - } - } - } - - # Select the directory under the cursor (if any) and deselect - # the previous drop-directory (if any) - $T selection modify $drop $Priv(drop) - set Priv(drop) $drop - - # Show the dragimage in its new position -if {0 && [$T dragimage cget -style] ne ""} { - set x [$T canvasx $x] - set y [$T canvasy $y] -} else { - set x [expr {[$T canvasx $x] - $Priv(drag,x)}] - set y [expr {[$T canvasy $y] - $Priv(drag,y)}] -} - $T dragimage offset $x $y - $T dragimage configure -visible yes - } - default { - Motion1 $T $x $y - } - } - return -} - -# ::TreeCtrl::FileListLeave1 -# -# Handle . -# -# Arguments: -# T The treectrl widget. -# x Window coord of pointer. -# y Window coord of pointer. - -proc ::TreeCtrl::FileListLeave1 {T x y} { - variable Priv - # This gets called when I click the mouse on Unix, and buttonMode is unset - if {![info exists Priv(buttonMode)]} return - switch $Priv(buttonMode) { - default { - Leave1 $T $x $y - } - } - return -} - -# ::TreeCtrl::FileListRelease1 -# -# Handle . -# -# Arguments: -# T The treectrl widget. -# x Window coord of pointer. -# y Window coord of pointer. - -proc ::TreeCtrl::FileListRelease1 {T x y} { - variable Priv - if {![info exists Priv(buttonMode)]} return - switch $Priv(buttonMode) { - "marquee" { - AutoScanCancel $T - MarqueeEnd $T $x $y - - if {[FileListEmulateWin7 $T]} { - # If the mouse was clicked in whitespace or insensitive part - # of an item and the mouse did not move then the selection - # is not modified until after the mouse button is released. - if {!$Priv(marquee,motion)} { - if {[IsSensitiveMarquee $T $x $y]} { - set item [$T item id active] - if {$Priv(selectMode) eq "add"} { - BeginExtend $T $item - } elseif {$Priv(selectMode) eq "toggle"} { - BeginToggle $T $item - } else { - BeginSelect $T $item - } - } elseif {$Priv(selectMode) eq "set"} { - # Clicked whitespace - $T selection clear - } - } - } - } - "drag" { - AutoScanCancel $T - - # Some dragging occurred - if {$Priv(drag,motion)} { - $T dragimage configure -visible no - if {$Priv(drop) ne ""} { - $T selection modify {} $Priv(drop) - TryEvent $T Drag receive \ - [list I $Priv(drop) l $Priv(selection)] - } - TryEvent $T Drag end {} - } else { - set rename 0 - if {[FileListEmulateWin7 $T]} { - # If the mouse was clicked in the insensitive parts of - # a selected item and multiple items are selected and the - # mouse did not move then the selection is not modified - # until after the mouse button is released. - set item $Priv(drag,item) - if {[$T selection count] == 1 && $Priv(selectMode) eq "set"} { - # In single-selectmode, when clicking the insensitive - # parts of an unselected item, the active item and - # selection aren't changed until the button is released. - if {[$T cget -selectmode] eq "single" && !$Priv(drag,wasSel)} { - $T activate $item - $T selection modify $item all - } else { - # If clicked already-selected item, rename it - set rename $Priv(drag,wasSel) - } - } elseif {[IsSensitive $T $x $y] && !$Priv(drag,wasSel)} { - # Selection was modified on ButtonPress, do nothing - } elseif {$Priv(selectMode) eq "add"} { - # Shift-click does nothing to already-selected item - #BeginExtend $T $item - } elseif {$Priv(selectMode) eq "toggle"} { - BeginToggle $T $item - } else { - # Make this the only selected item - BeginSelect $T $item - } - } elseif {$Priv(selectMode) eq "toggle"} { - # don't rename - } elseif {$Priv(drag,wasSel)} { - # Clicked/released a selected item, but didn't drag - set rename 1 - } - if {$rename} { - set I [$T item id active] - set C $Priv(drag,C) - set E $Priv(drag,E) - set S [$T item style set $I $C] - set ok 0 - foreach list $Priv(edit,$T) { - set eEList [lassign $list eC eS] - if {[$T column compare $C != $eC]} continue - if {$S ne $eS} continue - if {[lsearch -exact $eEList $E] == -1} continue - set ok 1 - break - } - if {$ok} { - FileListEditCancel $T - set Priv(editId,$T) \ - [after $Priv(edit,delay) [list ::TreeCtrl::FileListEdit $T $I $C $E]] - } - } - } - } - default { - Release1 $T $x $y - } - } - set Priv(buttonMode) "" - return -} - -# ::TreeCtrl::EscapeKey -# -# Handle the key. -# -# T The treectrl widget. - -proc ::TreeCtrl::FileListEscapeKey {T} { - variable Priv - if {[info exists Priv(buttonMode)] && $Priv(buttonMode) eq "drag"} { - set Priv(buttonMode) "" - AutoScanCancel $T - if {$Priv(drag,motion)} { - $T selection modify $Priv(selection) all - $T dragimage configure -visible no - TryEvent $T Drag end {} - } - return -code break - } - return -} - -# ::TreeCtrl::FileListEdit -# -# Displays an Entry or Text widget to allow the user to edit the specified -# text element. -# -# Arguments: -# T The treectrl widget. -# I Item. -# C Column. -# E Element. - -proc ::TreeCtrl::FileListEdit {T I C E} { - variable Priv - array unset Priv editId,$T - - if {![winfo exists $T]} return - - set lines [$T item element cget $I $C $E -lines] - if {$lines eq ""} { - set lines [$T element cget $E -lines] - } - - # Scroll item into view - $T see $I ; update - - # Multi-line edit - if {$lines ne "1"} { - scan [$T item bbox $I $C] "%d %d %d %d" x1 y1 x2 y2 - set S [$T item style set $I $C] - set padx [$T style layout $S $E -padx] - # FIXME: max of padx or union padding - GetPadding $padx padw pade - AddUnionPadding $T $S $E padw pade - TextExpanderOpen $T $I $C $E [expr {$x2 - $x1 - $padw - $pade}] - - # Single-line edit - } else { - EntryExpanderOpen $T $I $C $E - } - - TryEvent $T Edit begin [list I $I C $C E $E] - - return -} - -proc ::TreeCtrl::GetPadding {pad _padw _pade} { - upvar $_padw padw - upvar $_pade pade - if {[llength $pad] == 2} { - lassign $pad padw pade - } else { - set pade [set padw $pad] - } - return -} - -# Recursively adds -padx and -ipadx values of other elements in a style that -# contain the given element in its -union. -proc ::TreeCtrl::AddUnionPadding {T S E _padw _pade} { - upvar $_padw padw - upvar $_pade pade - foreach E2 [$T style elements $S] { - set union [$T style layout $S $E2 -union] - if {[lsearch -exact $union $E] == -1} continue - foreach option {-padx -ipadx} { - set pad [$T style layout $S $E2 $option] - GetPadding $pad p1 p2 - # FIXME: max of padx or union padding - incr padw $p1 - incr pade $p2 - } - AddUnionPadding $T $S $E2 padw pade - } - return -} - -# ::TreeCtrl::FileListEditCancel -# -# Aborts any scheduled display of the text-edit widget. -# -# Arguments: -# T The treectrl widget. - -proc ::TreeCtrl::FileListEditCancel {T} { - variable Priv - if {[info exists Priv(editId,$T)]} { - after cancel $Priv(editId,$T) - array unset Priv editId,$T - } - return -} - -# ::TreeCtrl::SetDragImage -# -# Specifies the list of elements that should be added to the dragimage. -# -# Arguments: -# T The treectrl widget. -# listOfLists {{column style element ...} {column style element ...}} - -proc ::TreeCtrl::SetDragImage {T listOfLists} { - variable Priv - foreach list $listOfLists { - set elements [lassign $list column style] - if {[$T column id $column] eq ""} { - error "column \"$column\" doesn't exist" - } - if {[lsearch -exact [$T style names] $style] == -1} { - error "style \"$style\" doesn't exist" - } - foreach element $elements { - if {[lsearch -exact [$T element names] $element] == -1} { - error "element \"$element\" doesn't exist" - } - } - } - set Priv(dragimage,$T) $listOfLists - return -} - -# ::TreeCtrl::SetEditable -# -# Specifies the list of text elements that can be edited. -# -# Arguments: -# T The treectrl widget. -# listOfLists {{column style element ...} {column style element ...}} - -proc ::TreeCtrl::SetEditable {T listOfLists} { - variable Priv - foreach list $listOfLists { - set elements [lassign $list column style] - if {[$T column id $column] eq ""} { - error "column \"$column\" doesn't exist" - } - if {[lsearch -exact [$T style names] $style] == -1} { - error "style \"$style\" doesn't exist" - } - foreach element $elements { - if {[lsearch -exact [$T element names] $element] == -1} { - error "element \"$element\" doesn't exist" - } - if {[$T element type $element] ne "text"} { - error "element \"$element\" is not of type \"text\"" - } - } - } - set Priv(edit,$T) $listOfLists - return -} - -# ::TreeCtrl::SetSensitive -# -# Specifies the list of elements that respond to mouse clicks. -# -# Arguments: -# T The treectrl widget. -# listOfLists {{column style element ...} {column style element ...}} - -proc ::TreeCtrl::SetSensitive {T listOfLists} { - variable Priv - foreach list $listOfLists { - set elements [lassign $list column style] - if {[$T column id $column] eq ""} { - error "column \"$column\" doesn't exist" - } - if {[lsearch -exact [$T style names] $style] == -1} { - error "style \"$style\" doesn't exist" - } - foreach element $elements { - if {[lsearch -exact [$T element names] $element] == -1} { - error "element \"$element\" doesn't exist" - } - } - } - set Priv(sensitive,$T) $listOfLists - return -} - -# ::TreeCtrl::SetSensitiveMarquee -# -# Specifies the list of elements that are sensitive to the marquee. -# If the list is empty then the same list passed to SetSensitive -# is used. -# -# Arguments: -# T The treectrl widget. -# sensitive Boolean value. - -proc ::TreeCtrl::SetSensitiveMarquee {T listOfLists} { - variable Priv - foreach list $listOfLists { - set elements [lassign $list column style] - if {[$T column id $column] eq ""} { - error "column \"$column\" doesn't exist" - } - if {[lsearch -exact [$T style names] $style] == -1} { - error "style \"$style\" doesn't exist" - } - foreach element $elements { - if {[lsearch -exact [$T element names] $element] == -1} { - error "element \"$element\" doesn't exist" - } - } - } - set Priv(sensitive,marquee,$T) $listOfLists - return -} - -# ::TreeCtrl::SetSelectedItemsSensitive -# -# Specifies whether or not entire items are sensitive to mouse clicks -# when they are already selected. -# -# Arguments: -# T The treectrl widget. -# sensitive Boolean value. - -proc ::TreeCtrl::SetSelectedItemsSensitive {T sensitive} { - variable Priv - if {![string is boolean -strict $sensitive]} { - error "expected boolean but got \"$sensitive\"" - } - set Priv(sensitiveSelected,$T) $sensitive - return -} - -# ::TreeCtrl::FileListEmulateWin7 -# -# Test the flag telling the bindings to use Windows 7 behavior. -# -# Arguments: -# T The treectrl widget. -# win7 Boolean value. - -proc ::TreeCtrl::FileListEmulateWin7 {T args} { - variable Priv - if {[llength $args]} { - set win7 [lindex $args 0] - if {![string is boolean -strict $win7]} { - error "expected boolean but got \"$win7\"" - } - set Priv(win7,$T) $win7 - return - } - if {[info exists Priv(win7,$T)]} { - return $Priv(win7,$T) - } - return 0 -} - -# ::TreeCtrl::EntryOpen -# -# Display a ::tk::entry so the user can edit the specified text element. -# -# Arguments: -# T The treectrl widget. -# item Item. -# column Column. -# element Element. - -proc ::TreeCtrl::EntryOpen {T item column element} { - - variable Priv - - set Priv(entry,$T,item) $item - set Priv(entry,$T,column) $column - set Priv(entry,$T,element) $element - set Priv(entry,$T,focus) [focus] - - # Get window coords of the Element - scan [$T item bbox $item $column $element] "%d %d" x y - - # Get the font used by the Element - set font [$T item element perstate $item $column $element -font] - if {$font eq ""} { - set font [$T cget -font] - } - - # Get the text used by the Element. Could check master Element too. - set text [$T item element cget $item $column $element -text] - - # Create the Entry widget if needed - set e $T.entry - if {[winfo exists $e]} { - $e delete 0 end - } else { - $Priv(entryCmd) $e -borderwidth 1 -relief solid -highlightthickness 0 - bindtags $e [linsert [bindtags $e] 1 TreeCtrlEntry] - } - - # Pesky MouseWheel - $T notify bind $e { TreeCtrl::EditClose %T entry 0 1 } - - $e configure -font $font - $e insert end $text - $e selection range 0 end - - set ebw [$e cget -borderwidth] - set ex [expr {$x - $ebw - 1}] - place $e -x $ex -y [expr {$y - $ebw - 1}] -bordermode outside - - # Make the Entry as wide as the text plus "W" but keep it within the - # TreeCtrl borders - set width [font measure $font ${text}W] - set width [expr {$width + ($ebw + 1) * 2}] - scan [$T contentbox] "%d %d %d %d" left top right bottom - if {$ex + $width > $right} { - set width [expr {$right - $ex}] - } - scan [$T item bbox $item $column] "%d %d %d %d" left top right bottom - if {$ex + $width > $right} { - set width [expr {$right - $ex}] - } - place configure $e -width $width - - focus $e - - return -} - -# ::TreeCtrl::EntryExpanderOpen -# -# Display a ::tk::entry so the user can edit the specified text element. -# Like EntryOpen, but Entry widget expands/shrinks during typing. -# -# Arguments: -# T The treectrl widget. -# item Item. -# column Column. -# element Element. - -proc ::TreeCtrl::EntryExpanderOpen {T item column element} { - - variable Priv - - set Priv(entry,$T,item) $item - set Priv(entry,$T,column) $column - set Priv(entry,$T,element) $element - set Priv(entry,$T,focus) [focus] - - # Get window coords of the Element - scan [$T item bbox $item $column $element] "%d %d" x y - - # Get the font used by the Element - set font [$T item element perstate $item $column $element -font] - if {$font eq ""} { - set font [$T cget -font] - } - - set Priv(entry,$T,font) $font - - # Get the text used by the Element. Could check master Element too. - set text [$T item element cget $item $column $element -text] - - # Create the Entry widget if needed - set e $T.entry - if {[winfo exists $e]} { - $e delete 0 end - } else { - $Priv(entryCmd) $e -borderwidth 1 -highlightthickness 0 \ - -selectborderwidth 0 -relief solid - bindtags $e [linsert [bindtags $e] 1 TreeCtrlEntry] - - # Resize as user types - bind $e { - after idle [list TreeCtrl::EntryExpanderKeypress [winfo parent %W]] - } - } - - # Pesky MouseWheel - $T notify bind $e { TreeCtrl::EditClose %T entry 0 1 } - - $e configure -font $font -background [$T cget -background] - $e insert end $text - $e selection range 0 end - - set ebw [$e cget -borderwidth] - set ex [expr {$x - $ebw - 1}] - place $e -x $ex -y [expr {$y - $ebw - 1}] \ - -bordermode outside - - # Make the Entry as wide as the text plus "W" but keep it within the - # TreeCtrl borders - set width [font measure $font ${text}W] - set width [expr {$width + ($ebw + 1) * 2}] - scan [$T contentbox] "%d %d %d %d" left top right bottom - if {$ex + $width > $right} { - set width [expr {$right - $ex}] - } - place configure $e -width $width - - focus $e - - return -} - -# ::TreeCtrl::EditClose -# -# Hides the text-edit widget and restores the focus if needed. -# Generates and events as needed. -# -# Arguments: -# T The treectrl widget. -# type "entry" or "text". -# accept 0/1: should an event be generated. -# refocus 0/1: should the focus be restored to what it was before editing. - -proc ::TreeCtrl::EditClose {T type accept {refocus 0}} { - variable Priv - - set w $T.$type - # We need the double-idle to get winfo ismapped to report properly - # so this don't get the FocusOut following Escape immediately - update idletasks - place forget $w - focus $T - update idletasks - - if {$accept} { - if {$type eq "entry"} { - set t [$w get] - } else { - set t [$w get 1.0 end-1c] - } - TryEvent $T Edit accept \ - [list I $Priv($type,$T,item) C $Priv($type,$T,column) \ - E $Priv($type,$T,element) t $t] - } - - $T notify unbind $w - - TryEvent $T Edit end \ - [list I $Priv($type,$T,item) C $Priv($type,$T,column) \ - E $Priv($type,$T,element)] - - if {$refocus} { - focus $Priv($type,$T,focus) - } - - return -} - -# ::TreeCtrl::EntryExpanderKeypress -# -# Maintains the width of the text-edit widget during typing. -# -# Arguments: -# T The treectrl widget. - -proc ::TreeCtrl::EntryExpanderKeypress {T} { - - variable Priv - - if {![winfo exists $T]} return - - set font $Priv(entry,$T,font) - set text [$T.entry get] - set ebw [$T.entry cget -borderwidth] - set ex [winfo x $T.entry] - - set width [font measure $font ${text}W] - set width [expr {$width + ($ebw + 1) * 2}] - - scan [$T contentbox] "%d %d %d %d" left top right bottom - if {$ex + $width > $right} { - set width [expr {$right - $ex}] - } - - place configure $T.entry -width $width - - return -} - -# ::TreeCtrl::TextOpen -# -# Display a ::tk::text so the user can edit the specified text element. -# -# Arguments: -# T The treectrl widget. -# item Item. -# column Column. -# element Element. -# width unused. -# height unused. - -proc ::TreeCtrl::TextOpen {T item column element {width 0} {height 0}} { - variable Priv - - set Priv(text,$T,item) $item - set Priv(text,$T,column) $column - set Priv(text,$T,element) $element - set Priv(text,$T,focus) [focus] - - # Get window coords of the Element - scan [$T item bbox $item $column $element] "%d %d %d %d" x1 y1 x2 y2 - - # Get the font used by the Element - set font [$T item element perstate $item $column $element -font] - if {$font eq ""} { - set font [$T cget -font] - } - - # Get the text used by the Element. Could check master Element too. - set text [$T item element cget $item $column $element -text] - - set justify [$T element cget $element -justify] - if {$justify eq ""} { - set justify left - } - - set wrap [$T element cget $element -wrap] - if {$wrap eq ""} { - set wrap word - } - - # Create the Text widget if needed - set w $T.text - if {[winfo exists $w]} { - $w delete 1.0 end - } else { - text $w -borderwidth 1 -highlightthickness 0 -relief solid - bindtags $w [linsert [bindtags $w] 1 TreeCtrlText] - } - - # Pesky MouseWheel - $T notify bind $w { TreeCtrl::EditClose %T text 0 1 } - - $w tag configure TAG -justify $justify - $w configure -font $font -background [$T cget -background] -wrap $wrap - $w insert end $text - $w tag add sel 1.0 end - $w tag add TAG 1.0 end - - set tbw [$w cget -borderwidth] - set tx [expr {$x1 - $tbw - 1}] - place $w -x $tx -y [expr {$y1 - $tbw - 1}] \ - -width [expr {$x2 - $x1 + ($tbw + 1) * 2}] \ - -height [expr {$y2 - $y1 + ($tbw + 1) * 2}] \ - -bordermode outside - - focus $w - - return -} - -# ::TreeCtrl::TextExpanderOpen -# -# Display a ::tk::text so the user can edit the specified text element. -# Like TextOpen, but Text widget expands/shrinks during typing. -# -# Arguments: -# T The treectrl widget. -# item Item. -# column Column. -# element Element. -# width Width of the text element. - -proc ::TreeCtrl::TextExpanderOpen {T item column element width} { - - variable Priv - - set Priv(text,$T,item) $item - set Priv(text,$T,column) $column - set Priv(text,$T,element) $element - set Priv(text,$T,focus) [focus] - - # Get window coords of the Element - scan [$T item bbox $item $column $element] "%d %d %d %d" x1 y1 x2 y2 - - set Priv(text,$T,center) [expr {$x1 + ($x2 - $x1) / 2}] - - # Get the font used by the Element - set font [$T item element perstate $item $column $element -font] - if {$font eq ""} { - set font [$T cget -font] - } - - # Get the text used by the Element. Could check master Element too. - set text [$T item element cget $item $column $element -text] - - set justify [$T element cget $element -justify] - if {$justify eq ""} { - set justify left - } - - set wrap [$T element cget $element -wrap] - if {$wrap eq ""} { - set wrap word - } - - # Create the Text widget if needed - set w $T.text - if {[winfo exists $w]} { - $w delete 1.0 end - } else { - text $w -borderwidth 1 -highlightthickness 0 \ - -selectborderwidth 0 -relief solid - bindtags $w [linsert [bindtags $w] 1 TreeCtrlText] - - # Resize as user types - bind $w { - after idle TreeCtrl::TextExpanderKeypress [winfo parent %W] - } - } - - # Pesky MouseWheel - $T notify bind $w { TreeCtrl::EditClose %T text 0 1 } - - $w tag configure TAG -justify $justify - $w configure -font $font -background [$T cget -background] -wrap $wrap - $w insert end $text - $w tag add sel 1.0 end - $w tag add TAG 1.0 end - - set Priv(text,$T,font) $font - set Priv(text,$T,justify) $justify - set Priv(text,$T,width) $width - - scan [textlayout $font $text -justify $justify -width $width] \ - "%d %d" width height - - set tbw [$w cget -borderwidth] - incr tbw - place $w -x [expr {$x1 - $tbw}] -y [expr {$y1 - $tbw}] \ - -width [expr {$width + $tbw * 2}] \ - -height [expr {$height + $tbw * 2}] \ - -bordermode outside - - focus $w - - return -} - -# ::TreeCtrl::TextExpanderKeypress -# -# Maintains the size of the text-edit widget during typing. -# -# Arguments: -# T The treectrl widget. - -proc ::TreeCtrl::TextExpanderKeypress {T} { - - variable Priv - - if {![winfo exists $T]} return - - set font $Priv(text,$T,font) - set justify $Priv(text,$T,justify) - set width $Priv(text,$T,width) - set center $Priv(text,$T,center) - - set text [$T.text get 1.0 end-1c] - - scan [textlayout $font $text -justify $justify -width $width] \ - "%d %d" width height - - set tbw [$T.text cget -borderwidth] - incr tbw - place configure $T.text \ - -x [expr {$center - ($width + $tbw * 2) / 2}] \ - -width [expr {$width + $tbw * 2}] \ - -height [expr {$height + $tbw * 2}] - - $T.text tag add TAG 1.0 end - - return -} - diff --git a/src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/htmldoc/What-is-New-in-TkTreeCtrl.html b/src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/htmldoc/What-is-New-in-TkTreeCtrl.html deleted file mode 100644 index 4230cc28..00000000 --- a/src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/htmldoc/What-is-New-in-TkTreeCtrl.html +++ /dev/null @@ -1,3604 +0,0 @@ - - - - - - - What's New in TkTreeCtrl - -

What's New in TkTreeCtrl 2.4.1

-

Bug Fixes

-
    -
  • [BUG 3421503] Creating a new treectrl widget would crash the application if the default value of either of the -buttonbitmap or -buttonimage options was changed using the Tk option database.
  • -
  • [BUG 3421503] Random crashes could result when using hidden columns due to writing past the end of an array.
    -
  • -
-

What's New in TkTreeCtrl 2.4

Changes since 2.4b2:
-
-
    -
  • There were no changes since 2.4b2.
  • -
-Changes since 2.4b1:
-
    -
  • Fixed the header config option -text being treated like a per-state option.
  • -
  • Fixed rectangle and 3D border drawing on X11 due to 16-bit -coordinates.  When items or headers were very wide, rectangles and -borders wouldn't be drawn correctly.
  • -
  • Fixed dragging headers in locked columns not displaying correctly.
  • -
  • Fixed [identify] when right-locked headers were drawn over -left-locked headers, the point was reported as being in the left-locked -headers.
  • -
  • Fixed a library-script error when dragging headers when only locked columns were visible.
  • -
  • Fixed -canvaspady being ignored when only locked columns were visible.
  • -
  • Fixed the column-resize proxy line going past the right edge of a column when resizing right-locked columns.
  • -
  • Right-locked columns are always resized by their left edge -now.  Previously, only the leftmost and rightmost right-locked -columns were resized by their left edge, which was inconsistent and -weird.
  • -
  • If an instance element was displaying its master's -textvariable string, the instance element would not be redisplayed when that -textvariable variable was modified.
  • -
  • The spans of dragged headers are restricted to the range of -dragged columns, and the spans of non-dragged headers are terminated if -they reach the first dragged column.
  • -
  • Improved column-spanning behavior.  See "Column Spanning" below.
  • -
  • Fixed widget borders not being redrawn if an <Expose> event didn't also overlap the content area.
  • -
  • Fixed the -background and -borderwidth header options being ignored in the tail column.
  • -
  • Fixed a few memory leaks with the new column header code.
    -
  • - -
-

Header Command

- - - - - - - - - - - - - - - - - - - - - - - - - - - -
New
-
Comment
header create
-
Creates a new row of column headers.  The result is a unique ID for that header-row.
-
header delete
-
Deletes one or more header-rows.  The top header-row, created when a treectrl is created, cannot be deleted.
-
header cget
-header configure
-
These commands work on both entire header-rows and individual columns within header-rows.  The column configure command may also be used to configure column headers in the top header-row.
-
header bbox
-header compare
-header count
-header element
-header id
-header image
-header span
-header state
-header style
-header text
-header tag
-
These commands have the same syntax as the item subcommands of the same name.
-Header-rows are actually implemented as items.
-
-The header image and header text -commands will change either the first image or text element in a custom -style assigned to a column header, or the -image and -text options of a -column header when no custom style is assigned.
-
-The header state command operates on header states, which are distinct from item states. See the updated section called STATES in the manual.
-
header dragcget
-header dragconfigure
-
Same as the column dragcget and column dragconfigure commands.
-
-The visual feedback when dragging column headers has changed.  As -a result, the following options are deprecated and have no effect:
-
    -
  • -imagecolor
  • -
  • -indicatorcolor
  • -
  • -indicatorside
  • -
- -
-

Column Configuration Options
-

- - - - - - - -
Option
-
Comment
-
-
-arrow direction
-arrowbitmap bitmap
-arrowgravity direction
-arrowimage image
-arrowpadx amount
-arrowpady amount
-arrowside side
-background color
-bitmap bitmap
-borderwidth size
-button boolean
-font fontName
-image image
-imagepadx amount
-imagepady amount
-justify justification
-state state
-text text
-textcolor color
-textlines count
-textpadx amount
-textpady amount
-
-
All -these options related to column headers are no longer a part of a -column itself.  Instead, these options were moved to the new -column header API.
-
-These options can still be accessed using column cget and column configure, but only for the top row of column headers.
-
-To access these options in header-rows other than the top row, use the header cget and header configure command.
-
-

Element Command

- - - - - - - - - - - -
Arguments Changed
-
Comment
element create
-element cget
-
The new option -statedomain accepts a value of item or header.  The default value is item.  -This option is used to distinguish between elements used in items and -elements used in column headers, since items and headers have a -different -set of state flags.  The value of this option cannot  be changed.
-
- - -

- - - -

Item Command

- - - - - - -
New
-
Comment
item state define
-item state linkage
-item state names
-item state undefine
- -
All the old subcommands of the widget state command are now in the updated item state -command.  This was done because items and headers have a different -set of states.  Defining new header states is done using the new header state command.
-
-

Style Command

- - - - - - - - - - -
Arguments Changed
-
Comment
style create
-style cget
-
The new option -statedomain accepts a value of item or header.  The default value is item.  -This option is used to distinguish between styles used in items and -styles used in column headers, since items and headers have a different -set of state flags.  The value of this option cannot  be changed.
-
style layout
-
A new style layout option was added called -center.  The -center option allows one or more elements to be centered within a style.
-
-

-

TreeCtrl Command

- - - - - - - - - - -
Arguments/Result Changed
-
Comment
bbox
-
Three new areas are defined, header.left, header.none, and header.right, -to get the bounds of the different groups of locked/unlocked column -headers.  The words after "header." are each possible value of the -column option -lock.
-
identify
-
To make the result of this -command easier to use, especially with the new column header code, a -new option was added that sets the elements of an array variable rather -than returning the result as a list.
-
.t identify -array id $x $y
-The above call will alter the array variable named "id" with info about what is under the given coordinates.
-
-

TreeCtrl Configuration Options

- - - - - - - - - - - - - - - - - - - - - - - - - -
NewComment
-headerfont
-
This -is the font used for drawing text in column headers.  The default -value is TkHeadingFont where that font is defined (usually on Tk 8.5+), -otherwise it is the default listbox font.  On Mac OS X, -TkHeadingFont is the small system font used for drawing text in the -fixed-height headers.
- This new option results in a different default look to column text on X11, where TkHeadingFont is a bold font.
-
-headerfg
--headerforeground
-
This is the foreground text -color used when drawing text in column headers.  On Gtk+, the -system theme may override this color.
-

Headers

- - -The treectrl widget now supports multiple rows of column headers.  -In the documentation, an entire row of column headers is referred to as -a header-row.
-
    -
  • There is always one header-row with the unique ID "0".  It may -also be referred to using the header description "first". This top -header-row cannot be deleted.
  • -
  • Configuring column header options (such as -image and -text) in the top header-row can be done using column configure, the same as in previous versions. To configure column header options in specific header-rows, the header configure command must be used.
    -
  • -
  • Column spanning is supported using the header span command.
  • -
  • The appearance of column headers may be customized using styles. -When creating elements and styles for use in column headers, the -"-statedomain header" option must be used. -
    .t element create MyHeaderElement text -statedomain header
    .t style create MyHeaderStyle -statedomain header
    -
  • -
-

Element Changes

-
    -
  • There is a brand-new element type called header which displays a themed (or non-themed) column header background and sort arrow.
    -
  • -
  • To support the new column header code - which is implemented using -styles - bitmap, image, and text elements are offset by 1,1 pixels -when displayed in a column header that is in the "pressed" state.
  • - -
  • The default color of text elements in column headers is -headerforeground, not -foreground.
  • -
  • The default font of text elements in column headers is -headerfont, not -font.
    -
  • -
-

-Event Changes -

-
    -
  • The <ColumnDrag> and <Header> events have a new substitution character, %H, which is replaced by the unique ID of the header-row involved.
  • -
  • A new event, <ColumnDrag-indicator>, is generated whenever the place to drop a dragged column header is updated.
  • -
  • A new event, <Header-state>, is generated whenever the state of an individual column header is changed during mouse-pointer events.
    -
  • -
-

Column Spanning

Previously, a style spanning more than one -column would request all of its width in the first column of the span. -Now, a style distributes its width across all the visible columns in a -span, taking into account the -minwidth, -width, and -maxwidth column options.
-
-Also, when calculating the needed height of an item or header, spans -were ignored.  Only the width of the first column in a span was -considered when calculating the needed height of a style.  As a -result, an item might appear much taller than it needed when lines of -text were wrapping.
-

Demo Changes

- -
    -
  • A new demo called "Headers" was added to demonstrate multiple rows -of column headers and customizing the appearance of column headers -using styles.
  • -
  • The "Column Spanning" demo was updated to take advantage of the new column-spanning algorithm.  Previously, the -width -option of each column needed to be set to the width of the "Span 1" -style, otherwise the columns would have been as wide as the widest -style in that column, and resizing columns caused gaps to appear -between styles.
  • -
  • The three "Gradients" demos were updated to take advantage of the new column-spanning algorithm.  Previously, the -width -of the text elements displaying long lines of descriptive text was set -to the total width of the columns.  If the text was allowed to -wrap, the height of the items would be wrong since spans were -ignored.  Now the text wraps nicely when columns are resized.
    -
  • - -

What's New in TkTreeCtrl 2.3.2

-

Bug Fixes

-
    -
  • Fixed a bug where elements could expand too much, due to not accounting for the padding of elements in a -union correctly.
  • -
  • Build fix: don't use GNU Make conditionals in the Makefile.  -Instead, parts of the name of the archive created by the 'dist-win' -target are calculated in configure.ac.
    -
  • -
  • Build fix: make sure $MATH_LIBS (including -lm on AIX) is added to the list of link libraries on Unix platforms.
  • -
  • Build fix: removed trailing commas from some enum declarations to make strict C89 compilers happy (i.e., AIX).
  • -
  • Build fix: the Mac OS X SDK prior to version 10.5 does not have CGFloat defined.
  • -
  • Build fix: the Microsoft Platform SDK does not include -<vsstyle.h>, unlike the Windows SDK, therefore that header file -is no longer used.
    -
  • -
-

What's New in TkTreeCtrl 2.3.1

-

Style Configuration Options

- - - - - - -
NewComment
-buttony
-
This -option allows you to specify the distance from the top of an item that -the expand/collapse button is drawn.  When the value of this -option is unspecified, the button is centered vertically in the item.
-
-

Demo Changes

-
    -
  • The Style Editor now has 2 scale widgets to make testing the -expand and -squeeze layout options easier.
  • -
  • Rewrote the "Outlook Express (Folders)" demo to use a single item style instead of 4 different styles.
    -
  • - -
  • Fixed an off-by-1 error in the placement of the iMovie demo name-editing entry widget.
  • - -
-

Bug Fixes

-
    -
  • 64bit: Fixed pointer truncation leading to crashes when memory allocations exceeded 4GB.
  • -
  • 64bit: Lots of casting to quiet MSVC compiler warnings.
  • -
  • 64bit: Fixed shellicon debug build failure when using MSVC.
    -
  • -
  • Removed the use of <gdiplus.h> which broke MinGW-w64 builds and MinGW32 cross-compilation.
  • -
  • Fixed panic() when running the X11 debug build.
  • -
  • Use Tcl_PrintDouble() and Tcl_NewDoubleObj() instead of the %g -specifier to fix a bug reported on comp.lang.tcl where the floating -point numbers passed to the scrollbar command contained a comma (due to -a locale issue).
    -
  • -
  • Hit-testing of buttons now considers the vertical placement and -size of buttons. Previously, the [identify] command would report that -the given coordinates were over a button no matter how tall the item -was.
  • -
  • Mac OS X: Column header layout was broken, resulting in incorrect -justification of the header bitmap/image/text as well as the sort arrow -being drawn overtop the bitmap/image/text.
  • -
  • The needed width of all item styles in a column was not being recalculated when deleting items.
  • -
  • The needed width of column headers was not being recalculated when the -usetheme option changed.
    -
  • - -
  • Some commands, such as [item bbox], would return incorrect -results if called after a column was resized but before the next -display update.
  • -
  • The calculation of the minimum size needed by a style was broken -when -squeeze layout was used except for -squeeze=x -orient=horizontal.
  • -
  • The -itembackground colors were not being drawn when a transparent -backgroundimage was used.
  • -
- -
-

What's New in TkTreeCtrl 2.3

- -Changes since 2.3b1 -
    -
  • Removed the gradient api command.
  • -
  • New option -enabled may be passed to the item create command.
    -
  • -
  • The Escape key cancels any drag-and-drop in progress with the TreeCtrlFileList bindings.
  • -
  • With -selectmode=single the marquee is not used with the TreeCtrlFileList bindings.
  • -
  • Fixed the <Escape> key causing an error when the last visible item was selected.
  • -
  • Updated the documentation to include -gridleftcolor and -gridrightcolor.
  • -
  • Added a section to the documentation titled THE CANVAS and linked to that section wherever the canvas is mentioned.
    -
  • -
-

Column Configuration Options

- - - - - - - - - - - - - - - - - -
New
-
Comment
-gridleftcolor
--gridrightcolor
-
So-called "grid lines". These -two options specify a color or gradient to draw on the left and/or -right edge of the column.  The lines are drawn overtop an item's -style and down in the whitespace region below any items.
-
Usage Changed
-
How it changed
-
-itembackground
-
Item background colors can be a Tk color or a gradient name.
-
-Item background colors are now -drawn below items in the whitespace region even when item wrapping is -being used.  Previously item background colors were not drawn -below items when wrapping was used.
-
-textcolor
-
This is now a per-state option, just like the -background option, which may be incompatible.  -If you previously specified a Tk color name with a space in it, such as -"light blue", then you will need to make the value a proper list:
-
$T column configure $C -textcolor [list "light blue"]
-When this option is unspecified (the default), the system theme can -specify the color.  Currently that behavior is only used with the -Gtk+ build of treectrl.
-
-

Item Command

- - - - - - - - - - - - - - - - - -
New
-
Comment
buttonstate
-
The value of this option can be active, normal or pressed.  -This is used to change the appearance of the expand/collapse -buttons.  On Gtk+ the buttons change appearance when the mouse -pointer is over them and when they are clicked.  On Mac OS X the -buttons change appearance when they are clicked.  On MS Windows -the buttons change appearance when the mouse pointer is over them, but -only when the Explorer theme is used (see the new theme setwindowtheme command).
-
Arguments Changed
-
What changed
-
create
-
New option -enabled.
-
collapse
-expand
-toggle
-
A new option -animate -was added to support animated disclosure triangles on Gtk+ and Mac OS -X.  The library scripts pass the -animate option when clicking on -a button, but not when toggling items using the keyboard.
-
-

Marquee Configuration Options

- - - - - - - - - - -
NewComment
-fill
-
Specifies the fill color for the -selection rectangle.  The value can be a Tk color or a gradient -name or an empty string (the default).  When this option isn't an -empty string the dotted outline is not drawn.  By specifying the -name of a semi-transparent gradient a modern-looking selection -rectangle can be achieved.
-
-outline
-
Specifies the outline color for -the selection rectangle.  The value can be a Tk color, a gradient name, or an empty -string which is the default.  When this option isn't an empty -string the dotted outline is not drawn.
-
-

Style Layout Options

- - - - - - -
Behavior Changed
-
What changed
-
-union
-
Previously, nesting -union elements had undefined behavior.  Now it is ok to include an element with -union layout in another element's -union list.
-
-

TreeCtrl Configuration Options

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NewComment
-bgimage
-
This is a synonym for the -backgroundimage option.
-
-bgimageanchorControls -the alignment of the -backgroundimage.  The value is a standard Tk -anchor position such as "nw", "se" or "center", etc.  The image is -aligned to the content area when the image doesn't scroll, otherwise -the image is aligned to the canvas.  The default is "nw".
-
-bgimageopaque
-
A boolean that indicates whether -or not the -backgroundimage is partially transparent.  This is -needed because there is no way to tell in Tk whether an image contains -transparency.  The default is true, so if you use a transparent --backgroundimage you must set this to false.
-
-bgimagescroll
-
Controls whether the --backgroundimage scrolls along with the items or remains locked in -place.  The value can be an emptry string for no scrolling, "x" -for horizontal scrolling only, "y" for vertical scrolling only, or "xy" -(the default) for scrolling in both directions.
-
-bgimagetile
-
Controls whether the --backgroundimage is tiled along the x and y axes.  The value can -be "", "x", "y" or "xy" (the default).
-
-buttontracking
-
On Mac OS X and Gtk+ the -expand/collapse buttons don't toggle when they are clicked, only when -the mouse button is released over them, like regular pushbuttons.  -The value of this option is a boolean; when true the buttons toggle -when the mouse button is released, when false the buttons toggle when -clicked.  The default is true on Mac OS X and Gtk+, false on Win32 and X11.
-
-canvaspadx
--canvaspady
-
These options allow whitespace -margins around the edges of the canvas.  This is useful for 2D -views where you don't want the items to butt against the window -borders.  The value of each option is a list of one or two screen -distances specifying the width of the left/right margins and the height -of the top/bottom margins respectively.
-
-itemgapx
--itemgapy
-
These options allow whitespace gaps between adjacent items.  -This is useful for 2D views such as an icon view in a file browser or -an image thumbnail list.  The value of each option is a screen -distance defaulting to zero.
-
-xscrollsmoothing
--yscrollsmoothing
-
When these options are set to -true and the xview or yview commands are called to scroll by "units", -scrolling occurs according to the -xscrollincrement or --yscrollincrement options, and all other scrolling is done as if the --xscrollincrement or -yscrollincrement options were set to 1.  The -effect is that when dragging the scrollbar thumb scrolling is very -smooth, but when clicking the scrollbar buttons scrolling is done in -coarser increments.
-
Usage -ChangedHow -it changed
-columnresizemode
-
The default value is "realtime", it used to be "proxy".
-
-showlines
-
The default value is false on Mac OS X and Gtk+, true on Win32 and X11.
-
-usetheme
-
The default value is now true.  Complete reversal from previous versions, head for the hills.
-
-

TreeCtrl Command

- - - - - - - - - - - - - - - - - - - - - - - - - -
NewComment
gradientLinear -gradients!  There are a number of issues when using gradients, see -the demos and the relevant sections in the manual page for more info.
-
theme
-
The theme platform command returns the API used to draw themed parts of the treectrl.
-
    -
  • On Mac OS X the result -is always aqua.
  • -
  • On MS Windows the result is visualstyles if the -uxtheme.dll was loaded and visual themes are in use, otherwise X11 is -returned to indicate the Tk Xlib calls are drawing the themed parts.
  • -
  • On Unix systems the result is gtk if the Gtk+ version of treectrl -was built, otherwise X11 is returned.
  • -
- MS Windows only: The theme setwindowtheme command takes the name of an application whose theme should be used.  If you call...
-
-
$T theme setwindowtheme "Explorer"
-
- ...on Windows 7 the disclosure triangles of the Explorer file browser will be used rather than +/- buttons.
-
-

Gtk+ Theme

-
    -
  • Pass --enable-gtk=yes to -configure to build a Gtk+ version of treectrl.  This requires -proper pkg-config setup for the gtk+-2.0 and gdk-pixbuf-xlib-2.0 -libraries.
  • -
  • Leave the column option -textcolor={} so the theme colors will be used for column header labels.
    -
  • -
  • Leave the new widget option -buttontracking=yes for native behavior when clicking buttons.
    -
  • -
  • BUG: Currently the theme does not change when the system theme changes.
    -
  • -
-

Rect Element

-
    -
  • Rounded rectangles can be drawn using the new rect element configuration options -rx and -ry.  -These options specify the radius of the corners as screen -distances.  The values are restricted to a maximum of half the -width or height of the rectangle when being displayed.
  • -
  • The -open option is now -per-state.  See the "Outlook Express (Newsgroup)" demo where the -active outline of the selection rectangle appears to span across -columns.  In previous versions that required 3 different styles.
  • -
  • The -fill and -outline options can be a Tk color or a gradient.
    -
  • - -
-

Library Scripts

-
    -
  • The treectrl window gets the focus after a <ButtonPress-2> event begins scan-dragging.
  • -
  • The Escape key cancels any drag-and-drop in progress with the TreeCtrlFileList bindings.
  • -
  • With -selectmode=single the marquee is not used with the TreeCtrlFileList bindings.
    -
  • - - - -
  • Added some hackery to the TreeCtrlFileList bindings for emulating the complicated selection behavior of Windows 7 Explorer:
    -
      -
    • Added TreeCtrl::FileListEmulateWin7 to indicate the Windows 7 behavior should be used.
    • -
    • Added TreeCtrl::SetSensitiveMarquee, just like -TreeCtrl::SetSensitive but indicates which elements respond to the -selection rectangle. Typically this is the same as the elements passed -toTreeCtrl::SetSensitive with the addition of the selection rectangle elements.
    • -
    -
  • - -
-

Build Changes

- -
    -
  • Added macosx/ unix/ and win/ subdirectories with platform-specific code in each.
    -
  • -
  • The --enable-cocoa -configure option was removed when building on Mac OS X.  Instead -the configure script determines whether Tk was built using the Cocoa -API by examining the TK_LIBS variable.
  • -
  • Cross-compiling on a Unix host using the MingW toolchain should -work out-of-the-box now.  If the --host configure option contains -"mingw32" then TEA_PLATFORM is set to "windows".
  • -
  • The 'make clean' target does a better job cleaning what it -should.  It doesn't remove the .manifest file generated by -configure (make distclean does that), only the linker-generated -.manifest when building with MSVC is removed.  Resource file -objects are cleaned as well.
  • -
  • Various tweaks were made to allow building using the Visual Studio 2008 compiler.
    -
  • -
  • Updated to TEA 3.9.
  • -
-

Shellicon Package

-
    -
  • The shellicon package is used to display native file and folder icons on MS Windows.  It was updated with a new option -useselected.  The value of -useselected may be always, auto or never to control whether the selected version of an icon should be drawn.  The default is auto meaning draw the selected icon if the item is selected.
  • -
-

Demo Changes

-
    -
  • Added "Explorer (Details, Win7)" and "Explorer (Large Icons, -Win7)" demos which very closely match the appearance and behavior of -the Windows 7 file explorer.  These demos use gradients, rounded -rectangles and the new -canvaspadx, -canvaspady, -itemgapx and --itemgapy options.
  • -
  • New demos "Gradients", "Gradients II" and "Gradients II" demonstrate and describe various gradient features.
    -
  • -
  • The "Column Spanning" demo uses gradients.
  • -
  • The ::tk::mac::iconBitmap is used where available for native filesystem icons on Mac OS X.
    -
  • -
  • Fixed a bug with increasing/decreasing the font size if the font size was negative (i.e. on X11).
  • -
  • New options were added to the context menu: -bgimageanchor, --bgimageopaque, -bgimagescroll, -bgimagetile, -buttontracking, --xscrollsmoothing and -yscrollsmoothing.
    -
  • - -
-

Bug Fixes

-
    -
  • Fixed non-themed expand/collapse buttons being one pixel too large on Mac OS X.
  • -
  • Fixed dotted line and dotted rectangle drawing on Mac OS X.
  • -
  • Fixed the column option -arrowpady being broken for -arrowimage, -arrowbitmap and when themed arrows were drawn. It only worked when the X11 fallback was used.
  • -
  • Fix -"Bug 3104147 - Error after widget was destroyed".  Various [after] -callbacks did not check that the treectrl still existed.
  • -
  • Fix -"Bug 3104148 - column "202" doesn't exist".  Check that the -previously-highlighted column exists before configuring its state in -MotionInHeader.
  • -
  • Fixed <Left> and <Right> arrow keys not moving to adjacent items when some items had -wrap=true.
  • -
  • Fixed error with autoscanning when the content area was completely obscurred by locked columns.
  • -
  • Fixed the ordering of some entries in the treectrl option table.
  • -
  • Fixed the <Escape> key causing an error when the last visible item was selected.
    -
  • - -
- - - -
-

What's New in TkTreeCtrl 2.2.10

- - -

TreeCtrl Configuration Options

- - - - - - - - - - - - - - - - - - - - - - -
IgnoredComment
-doublebufferThis option no longer has any effect but was left in to -avoid incompatibilities. - Instead, the amount of double-buffering is chosen depending -on -the platform.  Modern platforms such as Mac OS X double-buffer -each toplevel whereas older platforms such as Windows XP do not.
- -

Text Element Options

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NewComment
-lmargin1Specifies how much the first line of text should be -indented.
- -
-lmargin2Specifies how much the 2nd or greater lines should be -indented when a line of text wraps.
- -

Mac OS X support for Tk 8.6

- -Tk 8.6 dropped support for the Carbon API in favor of the modern Cocoa -API on Mac OS X. TkTreeCtrl was updated to build using either Carbon -(Tk 8.4/8.5) or Cocoa (Tk 8.6). If you are using the back-port of the -Cocoa version with Tk 8.5 you can pass the --enable-cocoa=yes -configure option when configuring TkTreeCtrl.
- -

Build Changes

- -
    - -
  • Updated to TEA version 3.7 which supports -the MODULE_SCOPE -macro.  As a result, private symbols are not exported by the -treectrl shared library on platforms that support it (i.e., gcc's __visibility__("hidden")).
  • - -
- -

Bug Fixes

- -
    - -
  • Fixed a crash during error processing in [item -create] due to missing return statements for the -count and -height -options.
  • - -
  • Fixed a bug where all item styles would be lost when -deleting the -3rd or greater column. This bug affected any item that never had a -column record allocated for the 3rd or greater column (usually because -no style had been assigned).
  • - -
  • Fixed a crash in the [item span] command when specifying -"all" for the item description.
  • - -
  • The pkgIndex.tcl file correctly handles installation -directories with space or bracket characters.
  • - -
  • Removed C++ style comments and a spurious semicolon for C89 -AIX compliance. [BUG 2886595]
    - -
  • - -
  • Fixed [item sort -decreasing] not being a "stable" sort. -[BUG ID 2909930]
  • -
  • Fixed flickering of the dragimage under Windows 7; this requires more doublebuffering than WinXP even with Aero.
  • -
  • Use more double-buffering on the dragimage to fix flickering/slowness (tested on Ubuntu VM).
    -
  • -
  • Fixed drawing of column/row proxy lines where XOR isn't supported (i.e. MacOSX Cocoa).
  • -
  • Fixed the loupe (screen capture) command under Windows 7; tested on Win7 x64 (with and without Aero) and on WinXP x32.
  • -
  • Fixed an X server error caused by adding a bad rectangle to an -XRegion while processing <Expose> events.  This would show -up with -doublebuffer=window while resizing the window. [BUG 3015429]
    -
  • - - - -
- -

Demo Changes

- -
    - -
  • The "Layout" demo uses the new Text element option --lmargin2.
  • -
  • Fixed an undefined-variable error when double-clicking between column headers on X11 in the "Random" demo.
  • -
  • Fixed the resizing of items containing child windows when the -Tile theme changes in the "Big List" demo.  Also added some -comments.
  • -
  • Disabled the dynamic-appearing scrollbars under X11 where I saw some infinite looping.
    -
  • - - -
- -
-

What's New in TkTreeCtrl 2.2.9

- -

TreeCtrl Command

- - - - - - - - - - - - - - - - - - - - - - -
Arguments/Result -ChangedWhat -changed
seeOLD: $T see $item
- -NEW: $T see $item ?$column? ?-center xy?
- -You can specify a particular column to scroll into view horizontally.
- -The -center option will center the item/column in the window instead of -performing the minimal amount of scrolling to bring it into view at the -edge of the window.
- -
- -

Item Configuration Options

- - - - - - - - - - - - - - - - - - - - - - -
NewComment
-wrapWhen this option is true an item will be the first in a -horizontal (when the treectrl option -orient=horizontal) -or a vertical (when the treectrl option -orient=vertical) -range of items.  See the new "iMovie (Wrap)" demo.
- -
- -

Bug Fixes

- -
    - -
  • Fixed a compile error on SunOS due to DUMP_ALLOC being -defined in the system headers.
  • - -
  • Fixed a bug that caused a panic in B_IncrementFind when -calling -the [see] command before the widget had displayed itself for the first -time.
  • - -
  • Fixed a library script error when attempting to drag a -column header when the window was so small the items weren't visible.
  • - -
  • Fixed two cases where the whitespace would not be properly -erased when items being deleted caused scrolling.
  • - -
  • Fixed column headers being drawn overtop of the bottom edge -of window borders if the window was very short.
  • - -
- -

Demo Changes

- -
    - -
  • Added the "iMovie (Wrap)" demo to demonstrate the new item -option -wrap.
  • - -
- -
-

What's New in TkTreeCtrl 2.2.8

- -

Bug Fixes

- -
    - -
  • Fixed a bug that caused a panic in Range_ItemUnderPoint. -Thanks to SF.net user 'nobody' who found the problem.
  • - -
  • Fixed reading an uninitialized variable when calculating -column header layout.
  • - -
- -
-

What's New in TkTreeCtrl 2.2.7

- -

TreeCtrl Configuration Options

- - - - - - - - - - - - - - - - - - - - - - -
NewComment
-showrootchildbuttonsSimilar to -showrootlines, -this boolean option controls the display of expand/collapse buttons -next to child items of the root item.
- -
- -

Indentation change/fix

- -
Previously with -showroot=0, -showbuttons=0, and -showrootlines=0 -there was still an extra level of indentation displayed to the left of -the root's children. The only way to remove all indentation -from the -root's children was to set -showlines=0 -as well.  This is no longer the case and now behaves -as expected. With the new -showrootchildbuttons -option it is now possible to remove all indentation from the root's -children without affecting the display of buttons/lines on deeper items.
- -

Library Script Changes

- -

treectrl.tcl:

- -
    - -
  • The <Left> and <Right> key -bindings will collapse and expand the active item if -orient=vertical and - -wrap={}. - Otherwise the previous behavior of setting the active item to -an adjacent item is used.
  • - -
- -

Bug Fixes

- -
    - -
  • Fixed corruption/crash with ".t column configure -foo" with -no option value if -foo is an invalid option.
  • - -
- -
-

What's New in TkTreeCtrl 2.2.6

- -

Bug Fixes

- -
    - -
  • Fixed ".t item bbox" returning bogus values when asking for -the -bounds of a column or element in a list with many items (integer -overflow).
  • - -
  • Fixed a segfault on Win64 machines when the system theme -changed.
  • - -
  • Fixed a panic on Win64 when drawing the the marquee and -drag-image dotted rectangles due to a too-small struct.
  • - -
- -
-

What's New in TkTreeCtrl 2.2.5

- -

-Issues regarding the incompatibility of 8.4 built TkTreeCtrl working in -8.5 -were resolved. The Mac OS X API issues noted for 2.2.4 remain (they -relate to difficult to reconcile core drawing changes). -

- -

-A Windows DLL manifest is now embedded to address native theme drawing -issues. -

- -
-

What's New in TkTreeCtrl 2.2.4

- -

NOTE regarding Tk version compatibility

- -

-Under Mac OSX some internal changes to Tk 8.4.15 and Tk 8.4.17 result -in -incompatibilities: -

- -
    - -
  • This version of TkTreeCtrl built for Tk 8.4.15 will work -with Tk 8.4.15 and 8.4.16 only - (under Mac OSX).
  • - -
  • This version of TkTreeCtrl built for Tk 8.4.17 will work -with Tk 8.4.17 only (under Mac OSX).
  • - -
- -

TreeCtrl Configuration Options

- - - - - - - - - - - - - - - - - - - - - - -
NewComment
-columntagexpr
- --itemtagexpr
These boolean options can be used to turn off tag -expressions in column descriptions and item descriptions. - When the value of these options is false the -characters (', ')', '&', '|', '^' -and '!' have no special significance when using tags in column/item -descriptions. This is useful for applications which may have -arbitrary tags applied to columns or items.
- -

Bug Fixes

- -
    - -
  • Fixed partially-exposed transparent photo images not being -redrawn when scrolling under X11.
  • - -
  • Fixed potential crash with Windows theme if the system -theme was changed.
  • - -
  • Fixed ".t -item cget -button" always returning 0 when the value of -this option wasn't auto.
  • - -
  • Fixed a drawing issue under Mac OSX where parts of the -window would not be erased properly under Tk 8.4.15+ and Tk 8.5a7+.
  • - -
  • Fixed crashes under Mac OSX with Tk 8.4.17 and Tk 8.5.0.
  • - -
  • Fixed an old bug caused by a MSVC compiler optimization bug -that -stopped items being redrawn when the only change in appearance was the -expand/collapse button needing to be redrawn.
  • - -
- -

Misc Changes

- -
    - -
  • Changed the item -sort code to be a "stable" sort. This means that the -pre-sort order of two equal items is used as a tie-breaker.
  • - -
- -

Demo Changes

- -
    - -
  • Added "Increase Font Size" and "Decrease Font Size" menu -commands.  Also the Console font is not changed when running -under Tk 8.5.
  • - -
- -
-

What's New in TkTreeCtrl 2.2.3

- -

Build Changes

- -
    - -
  • When building with configure on Windows the ---enable-shellicon option will run configure in the shellicon/ -subdirectory.
  • - -
  • 'make dist' will create the source distribution -tktreectrl-VERSION.tar.gz.
  • - -
  • 'make dist-win' will create the Windows binary distribution -tktreectrl-VERSION-win32.zip.
  • - -
- -

Bug Fixes

- -
    - -
  • Fixed flickering when redrawing the borders with -"-doublebuffer window" when the widget was resized or parts of it were -exposed.
  • - -
  • Fixed undefined reference to vsnprintf when -building with the MS compiler under Windows; it should be _vsnprintf.
  • - -
  • Fixed a symbol conflict with Python 2.5 on ELF-based -systems which also defines a symbol "Ellipsis".
  • - -
- -
-

What's New in TkTreeCtrl 2.2.2

- -

Column Configuration Options

- - - - - - - - - - - - - - - - - - - - - - -
NewComment
-itemjustifyThis option allows item styles to be justified -separately -from the image/text in the column header. If the value of this -option is unspecified (the default), then item styles are justified -according to the -justify -option of the column.
- -

Item Configuration Options

- - - - - - - - - - - - - - - - - - - - - - -
Usage -ChangedHow -it changed
-buttonThe value of this option can now be the word auto (or any -abbreviation) in which case a button is drawn only when the item has at -least one child item with its item option -visible set to true.
- -

Style Layout Changes

- -
    - -
  • The element option -draw -of every element type is now deprecated. Use the new style layout -option -draw -instead.
  • - -
  • The new per-state style layout option -visible -controls the visibility of an element. When the value of this option -evaluates to false for a given state, the element consumes no space in -the layout and is not displayed. If none of the elements surrounded by -an element with -union -layout are visible, then the element with -union layout is not -displayed.
  • - -
- -

Item Descriptions

- -
    - -
  • The index argument to the child and sibling modifiers -can now take the form "end?-integer?".
  • - -
- -
-
.t item id "root child end-1" ; # get the second-to-last child of the root item
- -
- -

Bug Fixes

- -
    - -
  • Text elements were requesting some height from the style -layout -when displaying an empty string. If you were depending on this -behaviour, it is suggested that you set the height of the text element -using the -minheight -or -height -style layout options.
  • - -
  • Window elements might not be scrolled along with the rest -of the -list if the area needing to be redrawn due to scrolling was obscurred -by other windows. This could only happen on Win32.
  • - -
  • The ellipsis "..." in text elements is now always displayed -if -the text element has less space than is needed to display its string. -Previously the ellipsis would disappear when there wasn't room for a -single character plus the ellipsis.
  • - -
  • Fixed a layout bug with multi-line text elements when the -unsqueezed element did not require a multi-line layout but the squeezed -element did.
  • - -
- -
-

What's New in TkTreeCtrl 2.2.1

- -

Bug Fixes

- -
    - -
  • Fixed panic with -xscrollincrement=0, -showheader=yes, no -visible items and headers wider than the window.
  • - -
  • Fixed the wrong loop variable being used when calculating -onscreen columns for an item which resulted in a random crash.
  • - -
  • Fixed a crash when invalidating a column of an item if the -column wasn't the first in a span.
  • - -
  • Fixed a leak on X11 where the clipping region was not being -freed after drawing dotted rectangles.
  • - -
- -
-

What's New in TkTreeCtrl 2.2

- -

TreeCtrl Configuration Options

- - - - - - - - - - - - - - - - - - - - - - -
DeprecatedWhat -to use -instead
-defaultstyleThe -itemstyle option of a column.
- -

TreeCtrl Command

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Arguments/Result -ChangedWhat -changed
contentboxThe return value is an empty string if the -content area is totally obscurred by column headers, borders, and/or -locked columns. Typically this will only happen if the window is too -small.
selection getAccepts 2 optional arguments to allow in-place lindex and lrange queries of -the selection. For example:
- -
.t selection get 0       ; # the first selected item
.t selection get end     ; # the last selected item
.t selection get 1 end-1 ; # every selected item except the first and last
- -
NewComment
bboxReturns the bounding box of different areas of the -window. For example:
- -
.t bbox
- -will return the bounds of the window, and:
- -
.t bbox header
- -will return the bounds of the column headers, and:
- -
.t bbox content
- -will return the same result as the [contentbox] command, and:
- -
.t bbox left
.t bbox right
- -will return the bounds of the left-locked and right-locked columns.
- -

Column Configuration Options

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Renamed
- -
New -name
-tag-tags
NewComment
-lockThis option allows columns to stick to the left or -right edge -of the window. Locked columns can scroll vertically but not -horizontally. Valid values for this option are none (the default), left or right.
-itemstyleSpecifies the name of a style to set in this column for -newly-created items. This option replaces the treectrl option --defaultstyle.
-uniformThese two options -operate the same as the grid geometry manager options of the same name. -For example:
- -
.t column configure 0 -uniform a
.t column configure 1 -uniform a
- -will give columns 0 and 1 the same requested width, whichever is the -larger of the two columns. And:
- -
.t column configure 0 -uniform a -weight 2
.t column configure 1 -uniform a
- -will give column 0 twice the maximum of the requested widths of columns -0 and 1. And:
- -
.t column configure 0 -expand yes -weight 2
.t column configure 1 -expand yes
- -will give column 0 twice the extra space as column 1.
-weight
- -

Column Command

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Arguments/Result -ChangedWhat -changed
column countTakes an optional column-description argument; the -result is -the number of columns that match the column description. For example:
- -
.t column count visible
- -will return the number of columns whose -visible option is true, and:
- -
.t column count {tag a^b}
- -will return the number of columns with either tag "a" or "b", but not -both.
NewComment
column tag addColumns can have -a list of tag names. Previously only a single tag was allowed. The tail column no -longer has the word "tail" as a tag, but it is still referred to by the -word "tail" in column descriptions.
column tag expr
column tag names
column tag remove
- -

Item Configuration Options

- - - - - - - - - - - - - - - - - - - - - - -
NewComment
-tagsTags are textual labels applied to items to group them. -Tags -do not affect the appearance or behaviour of items. Tags can be used in -item descriptions to operate on multiple items. More information can be -found in the man page.
- -

Item Command

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Arguments/Result -ChangedWhat -changed
item countTakes an optional item-description argument; the result -is the number of items that match the item description. For example:
- -
.t item count visible
- -will return the number of items that are displayed (i.e., those -whose ancestors are all expanded, -visible options are true, etc), and:
- -
.t item count {tag a^b}
- -will return the number of items with either tag "a" or "b", but not -both.
item createNew option -tags -specifies an initial list of tags for created items.
item idReturns a list of item ids if the item description -matches multiple items. For example:
- -
.t item id all
- -will return a list of ids for all items, and:
- -
.t item id "$item children"
- -will return the ids of every child of an item.
NewComment
item descendantsReturns the ids of the children, grandchildren, etc of -an item.
item enabledGets and sets the enabled -state for items. All items are enabled when first created. Disabled -items cannot be selected, and are ignored by the default key-navigation -and mouse bindings.
item tag addAdd tags to items. For example:
- -
.t item tag add all {a b c}
- -will add tags "a", "b" and "c" to every item.
- -
item tag exprEvaluate a tag expression against items. For example:
- -
.t item tag expr $item a
- -returns 1 if an item has tag "a". Also:
- -
.t item tag expr $item a||b
- -returns 1 if  an item has tag "a" or "b".
item tag namesReturn a list of tag names assigned to items. For -example:
- -
.t item tag names $item
- -returns the tag list for an item, and:
- -
.t item tag names all
- -returns every tag assigned to any item.
item tag removeRemove tags from items. For example:
- -
.t item tag remove all {b c}
- -will remove tags "b" and "c" from any items that have them.
- -

Text Element

- -The new option -underline -draws an underline under a single character of the displayed text.
- -

Window Element

- -Window elements can now be properly clipped so they don't draw over the -column header, borders, or outside the bounds of the item -columns they occupy. This is accomplished by making the window you want -to display a child of a borderless frame widget, and setting the new -clip option of the -window element to TRUE. So if your program displays a canvas widget in -a window element, you would change this code:
- -
set canvas [canvas .t.canvas ...]
.t item element configure $item $column myElement -window $canvas
- -to this:
- -
set frame [frame .t.clip -borderwidth 0]
set canvas [canvas $frame.canvas ...]
.t item element configure $item $column myElement -window $frame -clip yes
- -The -clip -option tells the window element to manage the geometry of both the -window -widget (i.e, -the frame) and its first child widget (i.e., the canvas). In this case, -the frame widget is kept sized and positioned so that it is never -out-of-bounds. You can see this in the "Big List" and "Firefox Privacy" -demos. -

Item Descriptions

- -New keywords were added to allow multiple items to be specified by an item description:
- -
    - -
  • The keyword list -specifies a list of other item descriptions: -
    .t item id "list [list $a $b $c]"
    - -
  • - -
  • The keyword range -operates like the item -range command: -
    .t item id "range $first $last"
    - -
  • - -
- -New modifiers were added to match multiple items: -
    - -
  • The modifier ancestors -operates like the item -ancestors command: -
    .t item id "$item ancestors"
    - -
  • - -
  • The modifier children -operates like the item -children command: -
    .t item id "$item children"
    - -
  • - -
  • The modifier descendants -operates like the item -descendants command: -
    .t item id "$item descendants"
    - -
  • - -
- -New qualifiers were added to refine which items are matched:
- -
    - -
  • The qualifier depth -matches items at a given depth in the heirarchy:
  • - -
- -
-
.t item id "all depth 2" ; # find all items that are children of the root's children
.t item id "depth 2" ; # ditto
- -
- -
    - -
  • The qualifier !visible -matches items that are not displayed: -
    .t item id "first !visible" ; # find the first item that is not displayed
    - -
  • - -
  • The qualifier state -matches items that have certain states set (or not set if '!' is used): -
    .t item id "first state {selected !open}" ; # find the first item that is selected and collapsed
    - -
  • - -
  • The qualifier tag -matches items that meet a tag expression: -
    .t item id "$item children tag {a && !b}" ; # find children of $item that have tag "a" but not tag "b"
    - -
  • - -
- -
- -The keyword all -may now be followed by a list of qualifiers. For example:
- -
.t item id "all !visible state myState" ; # find every item that is not displayed with user-defined state "myState"
- -A list of qualifiers may be used as the first part of an item -description. This gives the same result as  "all" followed by -the -qualifiers. For example:
- -
-
.t item id "!visible state myState" ; # same as the previous example
- -
- -

Column Descriptions

- -New keywords list -and -range can be used to -match multiple columns.
- -New qualifiers state, -tag, !tail and !visible can be used -to restrict which columns are specified.
- -The keyword all -may be followed by a list of qualifiers.
- -A list of qualifiers may used as the first part of a column -description. This gives the same result as  "all" followed by -the -qualifiers. -

Multi-item and multi-column commands

- -Many commands can now operate on multiple items and/or columns by using -the improved item descriptions and column descriptions mentioned above. -For example: -
-
.t column configure "range 1 10" -tags {a b c}
.t column delete "tag a"
.t column id "tag {a || b}"
.t item configure "depth 1" -button yes
.t item count visible
.t item element configure "root children" all elem1 -text "Hello"
.t item id "visible"
.t item image all all image1
.t item style map "tag {a && !b}" "tag c" style2 {style1.elem1 style2.elem2}
.t item style set all all style1
.t item state forcolumn all all state1
.t item state set "tag current" ~mouseover
.t item remove "state selected"
.t item span "range 1 10" "range 10 last" 2
.t item text "root children" all "Hello"
- -
- -

Demo Changes

- -
    - -
  • New demo "My Computer". Demonstrates disabled items used as -headers.
  • - -
  • New demo "Column Locking". Demonstrates columns that do not -scroll horizontally.
  • - -
  • The "Big List" and "Firefox Privacy" demos were changed to -use the new -clip -option of window elements.
  • - -
- -

Misc Changes

- -
    - -
  • The tail column header will not be drawn if the tail -column's -visible -option is false. This can look nicer with some themes.
  • - -
  • The -itembackground -colors for a column are now extended below any items in the simplest -(and most typical) case where the treectrl's -orient option is -vertical and -wrap -option is unspecified. The height of the rows is determined by the -itemheight or -minitemheight -options; if neither of those options is specified, then -itembackground -colors are not drawn below the items.
  • - -
  • If -itembackground -colors are specified for the tail column, then they are drawn.
  • - -
  • Memory usage is improved, especially for text elements.
  • - -
- -

Bug Fixes

- -
    - -
  • item create: -Fixed bug where -nextsibling -and -prevsibling -options could specify an orphan item.
  • - -
  • item delete: -Stopped items possibly being double-deleted by nested calls through <Selection> -and <ItemDelete> -binding scripts.
  • - -
  • item expanditem collapseitem toggle: Only -operate on items which exist when the command is called, not any that -might get created by <Expand> -or <Collapse> -binding scripts.
  • - -
  • Fixed a crash and a redisplay problem when a master element -was -configured with a -textvariable and the associated variable changed.
  • - -
  • When a style with window elements spanned more than one -column, the window might be improperly sized during display updates.
  • - -
  • Windows in window elements would not always be unmapped if -columns or items were hidden, or if the span of an item-column changed.
  • - -
  • The disclosure triangles (i.e., the item buttons) are drawn -without a white background under OSX.
  • - -
- -
-

What's New in TkTreeCtrl 2.1.1

- -

Column Command

- - - - - - - - - - - - - - - - - - - - - - -
Arguments/Result -ChangedWhat -changed
column delete
- -
Added an optional second argument allowing a range of -columns to be deleted.
- -

Bug Fixes

- -
    - -
  • The item sort -command -will be much faster in many cases. There was a silly error in the -pivot-finding code of quicksort which resulted in the slow-down.
  • - -
  • A column header will be redisplayed if an image in -the header is altered.
  • - -
  • Fixed bus errors on some Unix systems due to alignment -problems.
  • - -
- -

Other Changes

- -
    - -
  • The Tk caret is now positioned over the active item when it -changes. The Tk caret is used for the Magnifier accessibility -application and IME on Windows, as well as XIM under Unix.
  • - -
  • Improved the appearance of the column headers under OSX.
  • - -
  • Added <MouseWheel> support to OSX.
  • - -
  • Changing the -visible option of an item could be slow if -any -items were selected. That is because non-visible items may not be part -of the selection. A change was made so that changes to the selection -caused by modifying the -visible option of an item do not occur until -the next display update.
  • - -
- -

Demo Changes

- -
    - -
  • New demo "Column Spanning". Demonstrates a 100-column list -where styles span from 1 to 20 columns each.
  • - -
  • The loupe -command (which performs screen capture to an image) is now implemented -on Windows and OSX thanks to Jeff Hobbs.
  • - -
  • The screen-magnifier image now resizes with the "loupe" -window.
  • - -
- -
-

What's New in TkTreeCtrl 2.1

- -This version should be backwards compatible with 2.0, except for a few -obscure changes.
- -

TreeCtrl Configuration Options

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NewComment
-itemwidth
- -
-itemwidthequalDeprecates the column -widthhack option.
-itemwidthmultipleDeprecates the column -stepwidth option.
- -

Column Configuration Options

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
DeprecatedWhat -to use -instead
-stepwidthtreectrl's -itemwidthmultiple option
-widthhacktreectrl's -itemwidthequal option
- -

Element Command

- - - - - - - - - - - - - - - - - - - - - - -
NewComment
element perstateLike [item element perstate].
- -

Item Configuration Options

- - - - - - - - - - - - - - - - - - - - - - -
NewComment
-heightOverrides the treectrl's -itemheight option
- -

Item Command

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
DeprecatedWhat -to use -instead
item element actualitem element perstate
item complexitem element configure
Behaviour -ChangedWhat -changed
item bboxNo longer returns an error if no style had been -assigned to the column.
item state forcolumnNo longer returns an error if no style had been -assigned to the column.
item style setDoes nothing when replacing a style with the same -style. -Previously the old style was freed before assigning the new style, -losing the element config info if the old and new styles were the same.
- - Potential -incompatibility
Arguments/Result -ChangedWhat -changed
item createAdded options: -count -height, -nextsibling, -open, --parent, --prevsibling, and -returnid. Multiple items may be created with one -call using the -count option.
item element configureMultiple elements in multiple columns may be configured -with -a single call. Use '+' to separate elements, and ',' to separate -columns. See the docs.
item style setWhen no column is specified, returns a list of one -style name -per column. Previously, the list would have less values than the number -of columns if no styles had ever been assigned to the rightmost -column(s).
- - Potential -incompatibility
item textWhen no column is specified, returns a list of one -string per column.
NewComment
item imagePartner to the [item text] command.
item element perstateNot really new, just renamed from [item element actual] -to -better describe what it does. Accepts a new optional argument which -specifies the state to use when determining the value of the per-state -option.
- -
- -The following options no longer return a default value if the per-state -option itself does not have a value specified:
- -
    - -
  • bitmap -foreground, -background
  • - -
  • border -relief
  • - -
  • text -fill, -font
  • - -
- - Potential -incompatibility
item spanA style may now be displayed over multiple adjacent -columns.
- -
- -

Notify -Command

- - - - - - - - - - - - - - - - - - - - - - -
NewComment
notify unbind
- -
Let's you unbind all scripts from an object with one -call.
- -

Style Layout Changes

- -
    - -
  • Column justification will now affect the position of -elements in 2 situations which previously had no effect (Potential incompatibility):
  • - -
- -
    - -
  1. If a -detach element had a fixed width larger than the -other elements.
  2. - -
  3. If an element had -iexpand x specified as well as --maxwidth, leaving some space available.
  4. - -
- -

Element Changes

- -
    - -
  • Bitmap, image and text elements are drawn clipped if given -less space than they need.
  • - -
  • Fixed line wrapping of text elements. It did not work for -single lines of text at all (Potential -incompatibility).
  • - -
  • The text -wrap option can now be none to disable line -wrapping.
  • - -
- -

Event Changes

- -
    - -
  • The new static event <ItemVisibility> -is generated when items become visible on screen and when items are no -longer visible on screen. -This event allows you to create really big lists by only assigning -styles when items are about to be displayed. See the EVENTS AND SCRIPT -SUBSTITUTIONS section in the help -file, and the new demo "Big List".
  • - -
- -

Other Changes

- -
    - -
  • On WinXP, the column header sort arrow is drawn like -Explorer draws it if -usetheme -is true.
  • - -
- -

Demo Changes

- -
    - -
  • New demo "Big List". Demonstrates the new <ItemVisibility> -event, using <Expand-before> to add items on demand, and -column spanning.
  • - -
  • The context menu has a Span -submenu that lets you manipulate column spanning in items. See -the item span -command in the help file.
  • - -
  • Under WinXP, the "Explorer" demos will use the new shellicon -extension if available. This extension allows a treectrl to display -file/folder icons using the Win32 Shell API. It may work on other -versions of Windows but it hasn't been tested.
  • - -
- -
-

What's New in TkTreeCtrl 2.0

- -

TreeCtrl Configuration Options

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ReplacedWhat -to use -instead
-openbuttonimage-buttonimage
-closedbuttonimage-buttonimage
-openbuttonbitmap-buttonbitmap
-closedbuttonbitmap-buttonbitmap
Usage -ChangedHow -it changed
-backgroundmodeThe values "index" and "visindex" are deprecated. The -value -"order" should be used instead of "index", and -"ordervisible" should be used instead of "visindex". This brings -agreement with the new "item order" command which replaces the "item -index" command.
-treecolumnThis used to be any integer value which may or may not -have -corresponded to an actual column. Now the value must be a valid column -description, or an empty string to indicate no column should display -buttons/lines.
New
-
-backgroundimage
-
-columnprefix
-
-columnresizemode
-
-itemprefix
-
-minitemheight
-
-usetheme
-
- -

TreeCtrl Commands

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
DeprecatedWhat -to use -instead
compareitem compare
indexitem id
numcolumnscolumn count
numitemsitem count
rangeitem range
- -

Column Configuration Options

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
RemovedWhat -to use -instead
-relief
- -
-state
-sunken-state
Renamed
- -
New -name
-arrowpad-arrowpadx
Usage -ChangedHow -it changed
-backgroundThis is now a per-state option. See COLUMNS in the help -file -for valid state names.
New
- -

-
-arrowbitmap
- -

-
-arrowimage
-
-arrowpady
-
-maxwidth
-
-resize
-
-state
-
-textlines
-
- -

Column Command

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
DeprecatedWhat -to use -instead
column indexcolumn id
Arguments/Result -ChangedWhat -changed
column configure
- -
A column description of "all" is allowed if at least -one -option-value pair is given.
- -
column createThe result is a unique identifier. Previously the -result was -an -index in the list of columns.
column deleteA column description of "all" is allowed.
NewComment
column compare
-
column countreplaces "numcolumns"
column dragconfigure
-
column dragcget
-
column idreplaces "column index"
column list
-
column order
-
- -

Item Command

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
RemovedWhat -to use -instead
item indexitem order
NewComment
item comparereplaces "compare"
item countreplaces "numitems"
item idreplaces "index"
item orderreplaces "item index"
item rangereplaces "range"
- -

Notify Command

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Arguments/Result -ChangedWhat -changed
notify generate
- -
Added optional percentsCommand -argument
notify installOld syntax (supported but deprecated):
- -
notify install event eventName
- -
notify install detail eventName detail
- -New syntax:
- -
notify install <eventName>
- -
notify install <eventName-detail>
- -
notify linkageOld syntax (supported but deprecated):
- -
-
notify linkage eventName
- -
notify linkage eventName detail
- -
- -New syntax:
- -
-
notify linkage <eventName>
- -
notify linkage <eventName-detail>
- -
- -
notify uninstallsee notify -install -above
- -

Style Layout Options

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Usage -ChangedHow -it changed
-iexpandTwo new flags "x' and "y" are allowed. Previously, only -the --ipadx and -ipady padding could be expanded by this option. The new xy -flags expand the display area of the element, not the padding. To -update your code, you will probably want to change this:
- -
-
$T style layout $S $E -iexpand we
- -
- -to this:
- -
-
$T style layout $S $E -iexpand x
- -
- -Keep in mind that -union elements are not affected by -iexpand xy, -since the size of a -union element is determined by the elements it -surrounds.
New
- -

-
-height
-
-maxheight
-
-maxwidth
-
-minheight
-
-minwidth
-
-sticky
-
-width
-
- -

Element Changes

- -
    - -
  • A new element type window -was added. -See the new demo "Firefox Privacy" and the ELEMENTS section in the help -file.
  • - -
  • All element types have a new -per-state boolean option called -draw.
  • - -
  • The text -element type -has a new option called -textvariable. -See the new demo -"Textvariable" and the ELEMENTS section in the help file.
  • - -
- -

Event Changes

- -
    - -
  • 2 new %-substitution characters %P -and %? are -allowed in binding -scripts. See the EVENTS AND SCRIPT SUBSTITUTIONS section in the help -file.
  • - -
  • The new static event <ItemDelete> -is generated when items are deleted. See the EVENTS AND SCRIPT -SUBSTITUTIONS section in the help file.
  • - -
- -

Library Script Changes

- -

filelist-bindings.tcl:

- -
    - -
  • The Priv(edit) variable, which is used to specify which -text -elements may be edited, now has the same format as Priv(sensitive). -Previously only elements in the first column could be edited.
  • - -
  • 3 new commands in the TreeCtrl namespace should be used to -access -the Priv(dragimage), Priv(edit) and Priv(sensitive) variables. The -commands are SetDragImage, SetEditable and SetSensitive.
  • - -
  • Two new dynamic events <Edit-begin> -and <Edit-end> -are -generated when editing a file name.
  • - -
- -

treectrl.tcl:

- -
    - -
  • On OSX/Aqua, the Command key is used to perform -discontinuous -selection. Previously the Control key was used but Command is specified -by Apple's user-interface guidelines.
  • - -
- -

Other Changes

- -
    - -
  • On WinXP, the column headers and open/close buttons are -drawn -using the system theme if -usetheme -is true. The sort arrow is drawn the old-fashioned way.
  • - -
  • On OSX/Aqua,  the column headers and open/close -buttons are -drawn -using the system theme if -usetheme -is true. The sort arrow will be drawn by the Appearance Manager as -well. This will override the -arrowside -and -arrowgravity -options.
  • - -
  • Columns can be moved by drag-and-drop. See column dragconfigure -in the help -file.
  • - -
  • Columns can be specified in new ways. See the COLUMN -DESCRIPTION -section in the help file.
  • - -
  • Added new section DYNAMIC EVENTS to the help file.
  • - -
  • Added new section PER-STATE OPTIONS to the help file.
  • - -
  • The new style layout option -indent -allows elements to be displayed in the button/line area. See the style layout command -in the help -file and the new demo "Firefox Privacy".
  • - -
  • The new item description end -is equivalent to last.
  • - -
  • If you -have version 1.1 -installed, replace the old pkgIndex.tcl file with the one from this -version (but replace the version number 2.0 with 1.1). Otherwise the old pkgIndex.tcl -file will -set the TREECTRL_LIBRARY variable which will override where the library -scripts are found.
  • - -
- -

Demo Changes

- -
    - -
  • New demo "Firefox Privacy". Demonstrates the new window element type -and -indent -style layout option.
  • - -
  • New demo "Textvariable". Demonstrates the new -textvariable option -of the text -element.
  • - -
  • Added a new Event Browser window to display events -generated by -the main treectrl widget.
  • - -
  • The context menu can be popped up in all the demo lists. A -<Control-ButtonPress-1> binding for this was added under -OSX/Aqua.
  • - -
  • In the "Explorer" demos, the file name is hidden while -editing -the file name.
  • - -
- - \ No newline at end of file diff --git a/src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/htmldoc/treectrl.html b/src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/htmldoc/treectrl.html deleted file mode 100644 index 9c849115..00000000 --- a/src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/htmldoc/treectrl.html +++ /dev/null @@ -1,4408 +0,0 @@ - - -treectrl - Tk Commands - - - - -
-

treectrl(n) 2.4.1 treectrl "Tk Commands"

-

Name

-

treectrl - Create and manipulate hierarchical multicolumn widgets

-
- -

Synopsis

-
-
    -
  • package require treectrl 2.4.1
  • -
- -
-
-

Description

-
-
treectrl pathName ?options?
-
-
-

The treectrl command creates a new window (given by the -pathName argument) and makes it into a treectrl widget. -Additional options, described above, may be specified on the -command line or in the option database -to configure aspects of the treectrl such as its background color -and relief. The treectrl command returns the -path name of the new window. -At the time this command is invoked, there must not -exist a window named pathName, but pathName's parent must exist.

-

A treectrl is a listbox widget which displays items in a one- or -two-dimensional arrangement. -Items have a parent-child relationship with other items. -Items may be arranged from top-to-bottom or from left-to-right. -Items may be spread about one or more columns. -Each item-column may be configured to span one or more adjacent item-columns. -The visibility of items can be set individually.

-

Items have a set of states, which are boolean properties. -For each column of an item there is a style associated, -which determines how to display the item's column -taking into account the item's current state set. -New states may be defined to further control the appearance of items; -these custom states may be turned on or off in individual columns of items.

-

Multiple rows of column headers are supported. -Column headers have platform-native appearance on Windows, Mac OS X, and Gtk+. -The appearance of column headers may be customized using styles.

-

Columns may be rearranged by the user using drag-and-drop. -One column can be specified to display the data in a hierarchical structure. -The visibility of columns can be set individually.

-

A treectrl can display a user-resizable selection rectangle called the -marquee. Another feature, the drag image, may be used to provide feedback -during drag-and-drop operations. Both of these are features commonly found in -file browsers.

-

A treectrl can generate events when various things happen, such as changes to -the selection, or a parent item being toggled open or closed. Scripts may be -bound to these events. New events can be defined.

-

A treectrl can display a background image. The background image can be -configured to be scrolled and tiled on each axis individually.

-
-

STANDARD OPTIONS

-
-
-background
-
-
-borderwidth
-
-
-cursor
-
-
-font
-
-
-highlightbackground
-
-
-highlightcolor
-
-
-highlightthickness
-
-
-orient
-
-
-relief
-
-
-takefocus
-
-
-xscrollcommand
-
-
-yscrollcommand
-
-
-foreground
-
-
-

See the option manual entry for details on the standard options.

-
-

WIDGET SPECIFIC OPTIONS

-
-
Command-Line Switch: -backgroundimage
-Database Name: backgroundImage
-Database Class: BackgroundImage
-
-

Specifies the name of an image to draw as the list background. Other options -control whether the image is tiled and whether the image scrolls. If the image is -transparent it is drawn on top of any column -itembackground colors.

-
Command-Line Switch: -backgroundmode
-Database Name: backgroundMode
-Database Class: BackgroundMode
-
-

Specifies how the background color of items is chosen in each column. -The value should be one of row, column, order, -or ordervisible. The default is row. -This option has only an effect -for columns which have -itembackground defined as list of two or more -colors (see section COLUMNS below for more on this). If row or -column is specified, the background color is chosen based on the location -of the item in the 1- or 2-dimensional grid of items as layed out on the screen; -this layout of items is affected by the -orient and -wrap options as -well as item visibility. -When order or ordervisible is specified, -the background color is chosen based on the result of the item order -command, regardless of the layout of items.

-
Command-Line Switch: -bgimage
-Database Name: bgImage
-Database Class: BgImage
-
-

Synonym for -backgroundimage.

-
Command-Line Switch: -bgimageanchor
-Database Name: bgImageAnchor
-Database Class: BgImageAnchor
-
-

Specifies how the background image should be aligned in any of the forms -acceptable to Tk_GetAnchor. Must be one of the values -n, ne, e, se, s, sw, -w, nw, or center. The default is nw. -When the background image scrolls, the anchor position is relative to -the canvas, otherwise it is relative to the contentbox.

-
Command-Line Switch: -bgimageopaque
-Database Name: bgImageOpaque
-Database Class: BgImageOpaque
-
-

Specifies a boolean indicating whether or not the background image is fully -opaque. This is needed because there is no way in Tk to determine whether an image -contains transparency or not. The default value is true, so if you use a -transparent -backgroundimage you must set this to false.

-
Command-Line Switch: -bgimagescroll
-Database Name: bgImageScroll
-Database Class: BgImageScroll
-
-

Specifies whether the background image scrolls along with the items or whether it -remains locked in place relative to the edges of the window. -The value must be a string that contains zero or more of the characters -x or y. The default is xy.

-
Command-Line Switch: -bgimagetile
-Database Name: bgImageTile
-Database Class: BgImageTile
-
-

Specifies whether the background image is tiled along the x and/or y axes. -The value must be a string that contains zero or more of the characters -x or y. The default is xy.

-
Command-Line Switch: -buttonbitmap
-Database Name: buttonBitmap
-Database Class: ButtonBitmap
-
-

Specifies the name of a bitmap be used to display the expand/collapse button of an item. -This is a per-state option. -If a bitmap is specified for a certain item state, it overrides the effects of --usetheme.

-
Command-Line Switch: -buttoncolor
-Database Name: buttonColor
-Database Class: ButtonColor
-
-

Specifies the foreground color which should be used for drawing the outline -and the plus or minus sign of an item's expand/collapse button.

-
Command-Line Switch: -buttonimage
-Database Name: buttonImage
-Database Class: ButtonImage
-
-

Specifies the name of an image to be used to display the expand/collapse button of an item. -This is a per-state option. -If an image is specified for a certain item state, it overrides the effects of --buttonbitmap and -usetheme.

-
Command-Line Switch: -buttonsize
-Database Name: buttonSize
-Database Class: ButtonSize
-
-

Specifies the width and height of the expand/collapse button of an item -in any of the forms acceptable to Tk_GetPixels.

-
Command-Line Switch: -buttonthickness
-Database Name: buttonThickness
-Database Class: ButtonThickness
-
-

Specifies the width of the outline and the plus or minus sign -of the expand/collapse button of an item -in any of the forms acceptable to Tk_GetPixels.

-
Command-Line Switch: -buttonttracking
-Database Name: buttonTracking
-Database Class: ButtonTracking
-
-

Specifies a boolean that determines if the expand/collapse buttons are tracked -like pushbuttons when clicking them. When true, buttons are not toggled until -the <ButtonRelease> event occurs over them. When false, buttons are toggled as -soon as the <ButtonPress> event occurs over them. This option defaults to -true on Mac OS X and Gtk+, false on Win32 and X11.

-
Command-Line Switch: -canvaspadx
-Database Name: canvasPadX
-Database Class: CanvasPadX
-
-

Specifies the width of extra whitespace on the left and right edges of the -canvas in any of the forms acceptable to Tk_GetPixels. -The option value may be a list of one or two screen distances to specify padding -for the two edges separately. The default is 0.

-
Command-Line Switch: -canvaspady
-Database Name: canvasPadY
-Database Class: CanvasPadY
-
-

Specifies the height of extra whitespace on the top and bottom edges of the -canvas in any of the forms acceptable to Tk_GetPixels. -The option value may be a list of one or two screen distances to specify padding -for the two edges separately. The default is 0.

-
Command-Line Switch: -columnprefix
-Database Name: columnPrefix
-Database Class: ColumnPrefix
-
-

Specifies an ascii string that changes the way column ids are reported and -processed. If this option is a non-empty string, the usual integer value -of a column id is prefixed with the given string. This can aid debugging -but it is important your code doesn't assume column ids are integers if you -use it.

-
Command-Line Switch: -columnproxy
-Database Name: columnProxy
-Database Class: ColumnProxy
-
-

If this option specifies a non empty value, -it should be a screen distance -in any of the forms acceptable to Tk_GetPixels. -Then a 1 pixel thick vertical line will be drawn -at the specified screen distance from the left edge of the treectrl widget, -which reaches from top to bottom of the treectrl widget -and uses an inverting color -(i.e black on lighter background, white on darker background). -This line can be used to give the user a visual feedback -during column resizing.

-
Command-Line Switch: -columnresizemode
-Database Name: columnResizeMode
-Database Class: ColumnResizeMode
-
-

Specifies the visual feedback used when resizing columns. The value should be -one of proxy or realtime. For proxy, a 1-pixel thick -vertical line is drawn representing where the right edge of the column will -be after resizing. For realtime, the column's size is changed while -the user is dragging the right edge of the column. The default is realtime.

-
Command-Line Switch: -columntagexpr
-Database Name: columnTagExpr
-Database Class: ColumnTagExpr
-
-

Specifies a boolean that enables or disables tag expressions in column -descriptions. See ITEM AND COLUMN TAGS.

-
Command-Line Switch: -defaultstyle
-Database Name: defaultStyle
-Database Class: DefaultStyle
-
-

This option is deprecated; use the column option -itemstyle instead. -Specifies a list of styles, one per column, to apply to each item created by -the item create command. The number of styles in the list can be -different from the number of tree columns. -Each list element should be a valid style name or an empty string to -indicate no style should be applied to a specific column. The list of styles -is updated if a style is deleted or if a column is moved.

-
Command-Line Switch: -doublebuffer
-Database Name: doubleBuffer
-Database Class: DoubleBuffer
-
-

This option no longer has any effect, but was left in for compatibility. -It used to control the amount of double-buffering that was used when -displaying a treectrl.

-
Command-Line Switch: -headerfont
-Database Name: headerFont
-Database Class: Font
-
-

Specifies the font to draw text in column headers with. The default value is -TkHeadingFont where available (on Tk 8.5+). This option can be overridden by -setting the -font option for individual column headers.

-
Command-Line Switch: -headerfg
-Database Name: headerForeground
-Database Class: Foreground
-
-

Synonym for -headerforeground.

-
Command-Line Switch: -headerforeground
-Database Name: headerForeground
-Database Class: Foreground
-
-

Specifies the color to draw text in column headers with. -The default value is the Tk button foreground color (usually black). -On Gtk+, the system theme may override this color. -This option (and the Gtk+ system theme color) can be overridden by setting the --textcolor option for individual column headers.

-
Command-Line Switch: -height
-Database Name: height
-Database Class: Height
-
-

Specifies the desired height for the window -in any of the forms acceptable to Tk_GetPixels. -The default is 200 pixels. -If this option is less than or equal to zero then the window will -not request any size at all.

-
Command-Line Switch: -indent
-Database Name: indent
-Database Class: Indent
-
-

Specifies the screen distance an item is indented relative to its parent item -in any of the forms acceptable to Tk_GetPixels. -The default is 19 pixels.

-
Command-Line Switch: -itemgapx
-Database Name: itemGapX
-Database Class: ItemGapX
-
-

Specifies the horizontal spacing between adjacent items -in any of the forms acceptable to Tk_GetPixels. -The default is 0.

-
Command-Line Switch: -itemgapy
-Database Name: itemGapY
-Database Class: ItemGapY
-
-

Specifies the vertical spacing between adjacent items -in any of the forms acceptable to Tk_GetPixels. -The default is 0.

-
Command-Line Switch: -itemheight
-Database Name: itemHeight
-Database Class: ItemHeight
-
-

Specifies a fixed height for every item -in any of the forms acceptable to Tk_GetPixels. If non-zero, this -option overrides the requested height of an item and the -minitemheight option. -If an item's own -height option is specified then that is the height used for -the item. In any case, items are never shorter than the maximum height of a -button if they display one. The default is 0.

-
Command-Line Switch: -itemprefix
-Database Name: itemPrefix
-Database Class: ItemPrefix
-
-

Specifies an ascii string that changes the way item ids are reported and -processed. If this option is a non-empty string, the usual integer value -of an item id is prefixed with the given string. This can aid debugging -but it is important your code doesn't assume item ids are integers if you -use it.

-
Command-Line Switch: -itemtagexpr
-Database Name: itemTagExpr
-Database Class: ItemTagExpr
-
-

Specifies a boolean that enables or disables tag expressions in item -descriptions. See ITEM AND COLUMN TAGS.

-
Command-Line Switch: -itemwidth
-Database Name: itemWidth
-Database Class: ItemWidth
-
-

Specifies a fixed width for every item in any of the forms acceptable to Tk_GetPixels. -If more than one column is visible, then this option has no effect. -If the -orient option is vertical, and the -wrap option is unspecified, then this -option has no effect (in that case all items are as wide as the column).

-
Command-Line Switch: -itemwidthequal
-Database Name: itemWidthEqual
-Database Class: ItemWidthEqual
-
-

Specifies a boolean that says whether all items should have the same width. -If more than one column is visible, then this option has no effect. -If the -orient option is vertical, and the -wrap option is unspecified, then this -option has no effect (in that case all items are as wide as the column). -If the -itemwidth option is specified, then this option has no effect.

-
Command-Line Switch: -itemwidthmultiple
-Database Name: itemWidthMultiple
-Database Class: ItemWidthMultiple
-
-

Specifies a screen distance that every item's width will be evenly divisible by in any of the forms acceptable to Tk_GetPixels. -If more than one column is visible, then this option has no effect. -If the -orient option is vertical, and the -wrap option is unspecified, then this -option has no effect (in that case all items are as wide as the column). -If the -itemwidth option is specified, then this option has no effect.

-
Command-Line Switch: -linecolor
-Database Name: lineColor
-Database Class: LineColor
-
-

Specifies the color which should be used for drawing -the connecting lines between related items.

-
Command-Line Switch: -linestyle
-Database Name: lineStyle
-Database Class: LineStyle
-
-

Specifies the appearance of the connecting lines between related items. -The value should be dot, which is the default, or solid.

-
Command-Line Switch: -linethickness
-Database Name: lineThickness
-Database Class: LineThickness
-
-

Specifies the thickness of the connecting lines between related items -in any of the forms acceptable to Tk_GetPixels.

-
Command-Line Switch: -minitemheight
-Database Name: minItemHeight
-Database Class: MinItemHeight
-
-

Specifies a minimum height for every item -in any of the forms acceptable to Tk_GetPixels. -The default is 0, which means that every item has the height requested by the -arrangement of elements in each column. -This option has no effect if either the -itemheight widget option or -height -item option is specified. -In any case, items are never shorter than the maximum height of an expand/collapse button.

-
Command-Line Switch: -rowproxy
-Database Name: rowProxy
-Database Class: RowProxy
-
-

If this option specifies a non empty value, -it should be a screen distance -in any of the forms acceptable to Tk_GetPixels. -Then a 1 pixel thick horizontal line will be drawn -at the specified screen distance from the top edge of the treectrl widget, -which reaches from left to right of the treectrl widget -and uses an inverting color -(i.e black on lighter background, white on darker background). -This line can be used to give the user a visual feedback -during row resizing.

-
Command-Line Switch: -scrollmargin
-Database Name: scrollMargin
-Database Class: ScrollMargin
-
-

Specifies a positive screen distance -in any of the forms acceptable to Tk_GetPixels. -This option is used by the default bindings to determine how close to the -edges of the contentbox the mouse pointer must be before scrolling occurs. -Specifying a positive value is useful when items may be drag-and-dropped. -Defaults to 0.

-
Command-Line Switch: -selectmode
-Database Name: selectMode
-Database Class: SelectMode
-
-

Specifies one of several styles for manipulating the selection. -The value of the option may be arbitrary, but the default bindings -expect it to be either single, browse, multiple, -or extended; the default value is browse.

-
Command-Line Switch: -showbuttons
-Database Name: showButtons
-Database Class: ShowButtons
-
-

Specifies a boolean value that determines whether this widget -leaves indentation space to display the expand/collapse buttons next to items. -The default value is true. -The item option -button determines whether an item has a button. -See also the widget options -showrootbutton and -showrootchildbuttons.

-
Command-Line Switch: -showheader
-Database Name: showHeader
-Database Class: ShowHeader
-
-

Specifies a boolean value that determines whether this widget -should display the header line with the column names at the top of the widget. -The default value is true.

-
Command-Line Switch: -showlines
-Database Name: showLines
-Database Class: ShowLines
-
-

Specifies a boolean value that determines whether this widget -should draw the connecting lines between related items. -The default value is true on Win32 and X11, false on Mac OS X and Gtk+.

-
Command-Line Switch: -showroot
-Database Name: showRoot
-Database Class: ShowRoot
-
-

Specifies a boolean value that determines whether this widget -should draw the root item. -By suppressing the drawing of the root item the widget can have -multiple items that appear as toplevel items. -The default value is true.

-
Command-Line Switch: -showrootbutton
-Database Name: showRootButton
-Database Class: ShowRootButton
-
-

Specifies a boolean value that determines whether this widget -leaves indentation space to display the expand/collapse button next to the -root item. The default value is false. -The item option -button determines whether the root item has a button.

-
Command-Line Switch: -showrootchildbuttons
-Database Name: showRootChildButtons
-Database Class: ShowRootChildButtons
-
-

Specifies a boolean value that determines whether this widget -should draw the expand/collapse buttons next to children of the root item. -The default value is true.

-
Command-Line Switch: -showrootlines
-Database Name: showRootLines
-Database Class: ShowRootLines
-
-

Specifies a boolean value that determines whether this widget -should draw the connecting lines between children of the root item. -The default value is true.

-
Command-Line Switch: -treecolumn
-Database Name: treeColumn
-Database Class: TreeColumn
-
-

Specifies a column description that determines which -column displays the expand/collapse buttons and connecting lines between items. -The default is unspecified.

-
Command-Line Switch: -usetheme
-Database Name: useTheme
-Database Class: UseTheme
-
-

Specifies a boolean value that determines whether this widget should draw -parts of itself using a platform-specific theme manager. -The default is true.

-
Command-Line Switch: -width
-Database Name: width
-Database Class: Width
-
-

Specifies the desired width for the window -in any of the forms acceptable to Tk_GetPixels. -The default is 200 pixel. -If this option is less than or equal to zero then the window will -not request any size at all.

-
Command-Line Switch: -wrap
-Database Name: wrap
-Database Class: Wrap
-
-

Specifies whether items are arranged in a 1- or 2-dimensional layout.

-

If the value is an empty string (the default), then items are arranged from top -to bottom (-orient=vertical) or from left to right (-orient=horizontal) in -a 1-dimensional layout.

-

If the value is "N items", then no more than N items will appear in -a vertical group (-orient=vertical) or horizontal group (-orient=horizontal).

-

If the value is "N pixels", then no vertical group of items will be -taller than N pixels (-orient=vertical) or no horizontal group of items will -be wider than N pixels (-orient=horizontal).

-

If the value is window, then a no vertical group of items will be -taller than the window (-orient=vertical) or no horizontal group of items will -be wider than the window (-orient=horizontal).

-

It is also possible to cause wrapping to occur on a per-item basis by using -the item option -wrap. See the item create command for that option.

-
Command-Line Switch: -xscrolldelay
-Database Name: xScrollDelay
-Database Class: ScrollDelay
-
-

This option controls how quickly horizontal scrolling occurs while dragging -the mouse with button 1 pressed. -The value should be a list of 1 or 2 integers interpreted as milliseconds. -If 2 values are specified, then the first value determines the intial delay -after the first scroll, and the second value determines the delay for all -scrolling after the first. If only 1 value is specified, each scroll takes -place after that delay.

-
Command-Line Switch: -xscrollincrement
-Database Name: xScrollIncrement
-Database Class: ScrollIncrement
-
-

Specifies an increment for horizontal scrolling, in any of the usual forms -permitted for screen distances. If the value of this option is greater -than zero, the horizontal view in the window will be constrained so that -the canvas x coordinate at the left edge of the window is always an even -multiple of -xscrollincrement; furthermore, the units for scrolling -(e.g., the change in view when the left and right arrows of a scrollbar -are selected) will also be -xscrollincrement. If the value of -this option is less than or equal to zero, then horizontal scrolling -snaps to the left of an item, or part of an item if items are wider than the -contentbox.

-
Command-Line Switch: -xscrollsmoothing
-Database Name: xScrollSmoothing
-Database Class: ScrollSmoothing
-
-

Specifies whether scrolling should be done as if -xscrollincrement=1 whenever -scrolling is performed by non-unit amounts. When the value of this option is true -and the xview command is called to scroll by "units", scrolling occurs according -to the -xscrollincrement option, and all other scrolling is done as if the --xscrollincrement option was set to 1. The effect is that when dragging the -scrollbar thumb scrolling is very smooth, but when clicking the scrollbar buttons -scrolling is done in coarser increments. The default value is false.

-
Command-Line Switch: -yscrolldelay
-Database Name: yScrollDelay
-Database Class: ScrollDelay
-
-

This option controls how quickly vertical scrolling occurs while dragging -the mouse with button 1 pressed. -The value should be a list of 1 or 2 integers interpreted as milliseconds. -If 2 values are specified, then the first value determines the intial delay -after the first scroll, and the second value determines the delay for all -scrolling after the first. If only 1 value is specified, each scroll takes -place after that delay.

-
Command-Line Switch: -yscrollincrement
-Database Name: yScrollIncrement
-Database Class: ScrollIncrement
-
-

Specifies an increment for vertical scrolling, in any of the usual forms -permitted for screen distances. If the value of this option is greater -than zero, the vertical view in the window will be constrained so that -the canvas y coordinate at the top edge of the window is always an even -multiple of -yscrollincrement; furthermore, the units for scrolling -(e.g., the change in view when the top and bottom arrows of a scrollbar -are selected) will also be -yscrollincrement. If the value of -this option is less than or equal to zero, then vertical scrolling -snaps to the top of an item, or part of an item if items are taller than the -contentbox.

-
Command-Line Switch: -yscrollsmoothing
-Database Name: yScrollSmoothing
-Database Class: ScrollSmoothing
-
-

Specifies whether scrolling should be done as if -yscrollincrement=1 whenever -scrolling is performed by non-unit amounts. When the value of this option is true -and the yview command is called to scroll by "units", scrolling occurs according -to the -yscrollincrement option, and all other scrolling is done as if the --yscrollincrement option was set to 1. The effect is that when dragging the -scrollbar thumb scrolling is very smooth, but when clicking the scrollbar buttons -scrolling is done in coarser increments. The default value is false.

-
-
-

THE CANVAS

-

Throughout this manual page the term canvas is sometimes used. The canvas -can be thought of as the virtual sheet of paper upon which all visible items are -drawn. The treectrl window displays different areas of the canvas within its -borders as the list is scrolled.

-
-

ITEM AND COLUMN TAGS

-

Columns and items may have any number of tags associated with them. -A tag is just a string of characters, and it may take any form, -including that of an integer, although the characters '(', ')', '&', '|', '^' -and '!' should be avoided.

-

The same tag may be associated with many columns or items. This is commonly done to group -items in various interesting ways; for example, in a file browser all directories -might be given the tag "directory".

-

Tag expressions are used in column descriptions -and item descriptions to specify which columns -and items to operate on. -A tag expression can be a single tag name or a logical expression of tags -using operators '&&', '||', '^' and '!', and parenthesized subexpressions. -For example:

-
-.t item id "tag {(a && !b) || (!a && b)}"
-
-

or equivalently:

-
-.t item id "tag {a ^ b}"
-
-

will return the unique ids of any items with either "a" or "b" tags, but not both.

-

Within a tag expression a tag name may be enclosed in double quotes to avoid special -processing of the operator characters. For example:

-
-.t item id {tag {"a&&b"||c}}
-
-

will return the unique ids of any items with either "a&&b" or "c" tags; in this -example the && is not treated as an operator. A double-quote may be escaped within -a quoted tag name using a backslash '\'.

-

Tag operators may be bypassed completely by setting the -columntagexpr and --itemtagexpr options. This can be useful if your application has column -or item tags containing arbitrary text.

-
-.t configure -itemtagexpr false
-.t item delete "tag a&&b"
-
-
-

WIDGET COMMAND

-

The treectrl command creates a new Tcl command whose -name is the same as the path name of the treectrl's window. -This command may be used to invoke various -operations on the widget. It has the following general form:

-

pathName option ?arg arg ...?

-

PathName is the name of the command, which is the same as -the treectrl widget's path name. Option and the args -determine the exact behavior of the command. The following -commands are possible for treectrl widgets:

-
-
pathName activate itemDesc
-

Sets the active item to the one described by itemDesc, -and switches on the state active for that item. -The active item can be referred to by the item description active. -If this command changes which item is active an <ActiveItem> event is generated. -If the active item is deleted the root item becomes the new active item.

-
pathName bbox ?area?
-

Returns a list with four elements giving the bounding box (left, top, right -and bottom) of an area of the window. If area is not specified, then -the result is the bounding box of the entire window. -If area is content, then the result is the part of the window -not including borders, headers, or locked columns. -If area is header, then the result is the part of the window -not including borders where column titles are displayed. -If area is left, then the result is the part of the window -not including borders or headers where left-locked columns are displayed. -If area is right, then the result is the part of the window -not including borders or headers where right-locked columns are displayed.

-

If area is one of header.left, header.none or -header.right then the area of the column headers occupied by columns -with -lock=left, -lock=none or -lock=right is returned.

-

An empty string is returned if the display area has -no height or width, which can be true for various reasons such as the window -is too small, or the header is not displayed, or there aren't any locked -columns.

-
pathName canvasx windowx
-

Translates the given window x-coordinate windowx in the treectrl -to canvas coordinate space. The marquee command -expects canvas coordinates.

-
pathName canvasy windowy
-

Translates the given window y-coordinate windowy in the treectrl -to canvas coordinate space. The marquee command -expects canvas coordinates.

-
pathName cget option
-

Returns the current value of the configuration option given -by option. -Option may have any of the values accepted by the tree -command.

-
pathName collapse ?-recurse? ?itemDesc ...?
-

Deprecated. Use item collapse instead.

-
pathName column option column ?arg ...?
-

This command is used to manipulate the columns of the treectrl widget -(see section COLUMNS below). -The exact behavior of the command depends on the option argument -that follows the column argument. -The following forms of the command are supported:

-
-
pathName column bbox columnDesc
-

Returns a list with four elements giving the bounding box -of the header of the column specified by the -column description columnDesc. -The returned coordinates are relative to the top-left corner of the widget. -If the column option -visible=false or if the widget option --showheader=false, then an empty list is returned.

-
pathName column cget columnDesc option
-

This command returns the current value of the option named option -for the column specified by the -column description columnDesc, -ColumnDesc may also be the string tail to specify the tail column. -Option may have any of the values accepted by the -column configure widget command.

-
pathName column configure columnDesc ?option? ?value? ?option value ...?
-

This command is similar to the configure widget command except -that it modifies options associated with the columns specified by the -column description columnDesc -instead of modifying options for the overall treectrl widget. -ColumnDesc may be the string tail to specify the tail column. -If columnDesc refers to more than one column, then at least one option-value pair -must be given. -If no option is specified, the command returns a list describing -all of the available options for columnDesc (see Tk_ConfigureInfo -for information on the format of this list). -If option is specified with no value, then the command returns -a list describing the one named option (this list will be identical to -the corresponding sublist of the value returned if no option -is specified). -If one or more option-value pairs are specified, then the command -modifies the given option(s) to have the given value(s) for columnDesc; -in this case the command returns an empty string.

-

See COLUMNS below for details on the options available for columns.

-

For compatibility with older versions of treectrl (which did not support more -than one row of column headers) any of the configuration options mentioned in -the HEADERS section, such as -arrow, -text, etc, -may be passed to the top header-row through this command.

-
pathName column compare column1 op column2
-

For both column descriptions column1 -and column2 the index is retrieved -(as returned from the column order widget command). -Then these indexes are compared using the operator op, which must -be either <, <=, ==, >=, >, -or !=. -The return value of this command is 1 if the comparison evaluated to true, -0 otherwise.

-
pathName column count ?columnDesc?
-

If no additional arguments are given, the result is a decimal string giving the number of -columns created by the column create widget command which haven't been -deleted by the column delete widget command; in this case the tail column -is not counted. -If columnDesc is given, then the result is the number of columns that -match that column description.

-
pathName column create ?option value ...?
-

This command creates a new column in the treectrl widget. The new column is -placed to the right of all other columns (except the tail column). Any -option-value arguments configure the new column according to the -column configure command. The return value is the unique identifier -of the new column.

-
pathName column delete first ?last?
-

Deletes the specified column(s). First and last must be valid -column descriptions. If both first -and last are specified, then they may refer to a single column only. -The tail column cannot be deleted and it is an error to specify it. -The order of first and last doesn't matter, and first may -be equal to last.

-
pathName column dragcget option
-

Deprecated. Use header dragcget instead.

-
pathName column dragconfigure ?option? ?value? ?option value ...?
-

Deprecated. Use header dragconfigure instead.

-
pathName column index columnDesc
-

Deprecated. Use column id instead.

-
pathName column id columnDesc
-

This command resolves the column description -columnDesc into a list of unique column identifiers. If the column(s) described by -columnDesc don't exist, this command returns an empty list.

-
pathName column list ?-visible?
-

This command returns a list of identifiers for every column (except the tail) -from left to right. If -visible is given, only columns whose -visible -option is true are returned.

-
pathName column move columnDesc beforeDesc
-

Moves the column specified by columnDesc to the left of the column -specified by beforeDesc. Both columnDesc and beforeDesc -must be valid column descriptions. -If beforeDesc is the string tail, -the column columnDesc will become the last column.

-
pathName column neededwidth columnDesc
-

This command returns a decimal string giving the needed width -of the column specified by the column description -columnDesc. -The needed width is the maximum of the width of the column header -and the width of the widest style in any visible item.

-

When an item style or column header spans multiple columns, the needed width -of a column is affected by the widths of other columns in the span, in which -case the result of this command isn't particularly useful.

-
pathName column order columnDesc ?-visible?
-

This command returns a decimal string giving the position of the column -specified by the column description -columnDesc -in the list of columns starting from zero for the leftmost column. -If -visible is given, only columns whose -visible -option is true are considered, and -1 is returned if columnDesc's -visible -option is false.

-
pathName column tag option ?arg arg ...?
-

This command is used to manipulate tags on columns. -The exact behavior of the command depends on the option argument -that follows the column tag argument. -The following forms of the command are supported:

-
-
pathName column tag add columnDesc tagList
-

Adds each tag in tagList to the columns specified by the -column description columnDesc. -Duplicate tags are ignored. The list of tags for a column can also be -changed via a column's -tags option.

-
pathName column tag expr columnDesc tagExpr
-

Evaluates the tag expression tagExpr against every column -specified by the column description -columnDesc. The result is 1 if the tag expression evaluates to true -for every column, 0 otherwise.

-
pathName column tag names columnDesc
-

Returns a list of tag names assigned to the columns -specified by the column description -columnDesc. The result is the union of any tags assigned to the -columns.

-
pathName column tag remove columnDesc tagList
-

Removes each tag in tagList from the columns specified by the -column description columnDesc. -It is not an error if any of the columns do not use any of the tags. -The list of tags for a column can also be changed via a column's --tags option.

-
-
pathName column width columnDesc
-

This command returns a decimal string giving the width in pixels -of the column specified by the column description -columnDesc, -even if the treectrl is configured to not display the column headers -by means of the -showheader option.

-
-
pathName compare itemDesc1 op itemDesc2
-

Deprecated. Use the item compare command instead.

-
pathName configure ?option? ?value option value ...?
-

Query or modify the configuration options of the widget. -If no option is specified, returns a list describing all of -the available options for pathName (see Tk_ConfigureInfo for -information on the format of this list). If option is specified -with no value, then the command returns a list describing the -one named option (this list will be identical to the corresponding -sublist of the value returned if no option is specified). If -one or more option-value pairs are specified, then the command -modifies the given widget option(s) to have the given value(s); in -this case the command returns an empty string. -Option may have any of the values accepted by the treectrl -command.

-
pathName contentbox
-

Returns a list with four elements giving the bounding box -of the screen area used to display items. -This is the area of the window not including borders, column headers, or -locked columns. An empty string is returned if the display area has -no height or width, which can happen if the window is too small. -The result of this command is the same as that of bbox content.

-
pathName debug option ?arg arg ...?
-

This command is used to facilitate debugging of the treectrl widget. -The exact behavior of the command depends on the option argument -that follows the debug argument. -The following forms of the command are supported:

-
-
pathName debug alloc
-

Returns a string giving partial statistics on memory allocations, if the package -was built with TREECTRL_DEBUG defined.

-
pathName debug cget option
-

This command returns the current value of the debugging option -named option. -Option may have any of the values accepted by the -debug configure widget command.

-
pathName debug configure ?option? ?value? ?option value ...?
-

This command is similar to the configure widget command except -that it modifies debugging options -instead of modifying options for the overall treectrl widget. -If no option is specified, the command returns a list describing -all of the available debugging options (see Tk_ConfigureInfo -for information on the format of this list). -If option is specified with no value, then the command returns -a list describing the one named option (this list will be identical to -the corresponding sublist of the value returned if no option -is specified). -If one or more option-value pairs are specified, then the command -modifies the given debugging option(s) to have the given value(s); -in this case the command returns an empty string.

-

The following debugging options are supported:

-
-
-displaydelay millis
-

Specifies a time duration in milliseconds, which should be waited -after something has been drawn to the screen. -Setting this option has only an effect, if the -debugging options -enable and -display are switched on.

-
-data boolean
-

If this option is switched on -(together with the debugging option -enable), -at various places a consistence check -on the internal data structure is made -(e.g. for every item is checked, -if the registered number of children is equal to the number of child items). -If an inconsistency was found, a Tcl background error is raised.

-
-display boolean
-

If this option is switched on -(together with the debugging option -enable), -at varios places additional debugging output is printed to stdout.

-
-drawcolor color
-

When specified, areas of the window are painted with this color when drawing -in those areas is about to occur. -Setting this option has only an effect if the -debugging options -enable and -display are switched on.

-
-enable boolean
-

All other debugging options only take effect -if this option is also switched on.

-
-erasecolor color
-

When specified, areas of the window which have been marked as "invalid" -(for example, when part of the window is exposed) are -painted with this color. -If you use an unusual color for this option (like pink), -superflous screen redraws can be spotted more easily. -Setting this option has only an effect if the -debugging options -enable and -display are switched on.

-
-span boolean
-

Debugging related to column spanning.

-
-textlayout boolean
-

Debugging related to text-element layout.

-
-
pathName debug dinfo option
-

Returns a string describing display-related stuff. Option must be one -of alloc, ditem, onscreen or range.

-
pathName debug expose x1 y1 x2 y2
-

Causes the area of the window bounded by the given window-coords to be -marked as invalid. This simulates uncovering part of the window.

-
-
pathName depth ?itemDesc?
-

If the additional argument itemDesc is given, -then the result is a decimal string giving the depth of -the item described by itemDesc. -If no itemDesc is specified, then the maximum depth of all -items in the treectrl widget is returned instead. -Depth is defined as the number of ancestors an item has.

-
pathName dragimage option ?arg ...?
-

This command is used to manipulate the drag image, which is used to provide -feedback when items are drag-and-dropped within the window. The drag image is -displayed as the dotted outlines of one or more items, columns and/or elements. -The exact behavior of the command depends on the option argument -that follows the dragimage argument. -The following forms of the command are supported:

-
-
pathName dragimage add itemDesc ?column? ?element?
-

Adds the shapes of the item described by itemDesc -to the shapes of the dragimage. -Specifying additional arguments reduces -the number of rectangles that are added to the dragimage. -If no additional arguments is specified, -for every element of the item in every column a dotted rectangles is added. -If column is specified, all elements in other columns are ignored. -If also element is specified, only a rectangle for this one element -of the specified item in the given column is added.

-
pathName dragimage cget option
-

This command returns the current value of the dragimage option -named option. -Option may have any of the values accepted by the -dragimage configure widget command.

-
pathName dragimage clear
-

Removes all shapes (if there are any) from the dragimage. -This command does not modify the dragimage offset.

-
pathName dragimage configure ?option? ?value? ?option value ...?
-

This command is similar to the configure widget command except -that it modifies the dragimage options -instead of modifying options for the overall treectrl widget. -If no option is specified, the command returns a list describing -all of the available dragimage options (see Tk_ConfigureInfo -for information on the format of this list). -If option is specified with no value, then the command returns -a list describing the one named dragimage option (this list will be identical -to the corresponding sublist of the value returned if no option -is specified). -If one or more option-value pairs are specified, then the command -modifies the given dragimage option(s) to have the given value(s); -in this case the command returns an empty string.

-

The following dragimage options are supported:

-
-
-visible boolean
-

Specifies a boolean value which determines -whether the dragimage should currently be visible.

-
-
pathName dragimage offset ?x y?
-

Returns a list containing the x and y offsets of the dragimage, -if no additional arguments are specified. -The dragimage offset is the screen distance the image is displayed at -relative to the item(s) its shape is derived from. -If two coordinates are specified, -sets the dragimage offset to the given coordinates x and y.

-
-
pathName element option ?element? ?arg arg ...?
-

This command is used to manipulate elements (see ELEMENTS AND STYLES below). -The exact behavior of the command depends on the option argument -that follows the element argument. -The following forms of the command are supported:

-
-
pathName element cget element option
-

This command returns the current value of the option named option -associated with the element given by element. -Option may have any of the values accepted by the -element configure widget command.

-

This command also accepts the -statedomain option.

-
pathName element configure element ?option? ?value? ?option value ...?
-

This command is similar to the configure widget command except -that it modifies options associated with the element given by element -instead of modifying options for the overall treectrl widget. -If no option is specified, the command returns a list describing -all of the available options for element (see Tk_ConfigureInfo -for information on the format of this list). -If option is specified with no value, then the command returns -a list describing the one named option (this list will be identical to -the corresponding sublist of the value returned if no option -is specified). -If one or more option-value pairs are specified, then the command -modifies the given option(s) to have the given value(s) in element; -in this case the command returns an empty string. -See ELEMENTS AND STYLES below for details on the options available for elements.

-
pathName element create name type ?option value ...?
-

Creates a new master element of type type with the unique user-defined name -name and configures it with zero or more option/value pairs. -See the subsections on individual element types in ELEMENTS AND STYLES -for the options that are valid for each type of element. -This command returns the name of the new element (the same as the name argument).

-

This command also accepts the -statedomain option with a value of -either header or item to specify where this element will be -displayed.

-
pathName element delete ?element ...?
-

Deletes each of the named elements and returns an empty string. -If an element is deleted while it is still configured -as an element of one or more styles -by means of the style elements widget command, -it is also removed from the element lists of these styles.

-
pathName element names
-

Returns a list containing the names of all existing elements.

-
pathName element perstate element option stateList
-

This command returns the value of the per-state -option named option for element for a certain state. -StateList is a list of state names (static and dynamic, see STATES) -which specifies the state to use.

-
pathName element type element
-

Returns the type of the element given by element, -such as rect or text.

-
-
pathName expand ?-recurse? ?itemDesc ...?
-

Deprecated. Use item expand instead.

-
pathName gradient option ?arg ...?
-

This command is used to manipulate color gradients. -See GRADIENTS for more information about using gradients. -The exact behavior of the command depends on the option argument -that follows the gradient argument. -The following forms of the command are supported:

-
-
pathName gradient cget gradient option
-

Returns the current value of the configuration option for the gradient specified by -gradient whose name is option. Option may have any of the -values accepted by the gradient configure command.

-
pathName gradient configure gradient ?option value ...?
-

If no option is specified, the command returns a list describing -all of the available gradient options (see Tk_ConfigureInfo -for information on the format of this list). -If option is specified with no value, then the command returns -a list describing the one named gradient option (this list will be identical -to the corresponding sublist of the value returned if no option -is specified). -If one or more option-value pairs are specified, then the command -modifies the given gradient option(s) to have the given value(s); -in this case the command returns an empty string.

-

The following options are supported (see gradient create for the meaning of each option):

-
-
-bottom coordSpec
-
-
-left coordSpec
-
-
-orient direction
-
-
-right coordSpec
-
-
-steps stepCount
-
-
-stops stopsList
-
-
-top coordSpec
-
-
-
pathName gradient create name ?option value ...?
-

Creates a new gradient with the name name, which must be a unique name -not used by another gradient created by this treectrl widget.

-

The following options are supported:

-
-
-bottom coordSpec
-
-
-left coordSpec
-
-
-right coordSpec
-
-
-top coordSpec
-

Each of these options specifies one edge of the gradient brush. -If the option is specified as an empty string (the default), the gradient brush's -edge is the same as that of whatever rectangle is being painted using the gradient. -See GRADIENT COORDINATES for details on gradient brush coordinates.

-

The format of each of these options is a list of 2 or more values {value coordType ?arg ...?}, -where value is a floating point number (usually from 0.0 to 1.0) and -coordType is one of area, canvas, column -or item. The area keyword must be followed by one of the same -area names that the bbox command accepts. The column keyword may -be followed by a column description specifying exactly one column. The -item keyword may be followed by an item description specifying exactly -one item.

-
-orient direction
-

This option specifies the direction a linear gradient changes color in. -Must be either horizontal (the default) -or vertical or an abbreviation of one of these.

-
-steps stepCount
-

Specifies the number of bands of color drawn for each color stop described by -the -stops option. The default value is 1, the maximum is 25. -This option has no effect if gradients are drawn using something better than -Tk API calls. See GRADIENTS for more on this.

-
-stops stopsList
-

Specifies the color stops along this gradient. The argument stopsList -has the following form:

-
-{{offset color ?opacity?} {offset color ?opacity?} ...}
-
-

Each offset is a floating point number from 0.0 to 1.0 specifying the -distance from the start of the gradient where the color begins. -Each color is a Tk color name or description. -Each optional opacity is a floating point number from 0.0 to 1.0 -specifying how transparent the gradient is.

-

If stopsList is non-empty there must be at least two stops specified, and -the first offset must be 0.0 and the last offset must be 1.0. Any other -stop offsets must be listed in increasing order. -Specifying opacity has no effect if gradients are drawn using Tk API calls. -See GRADIENTS for more on this.

-
-
pathName gradient delete ?name ...?
-

Deletes each gradient specified by name. If the gradient is still being -used then it is not actually deleted until all elements etc -using the gradient have stopped using it. A deleted-but-in-use gradient is -not recognized by the various gradient commands. Creating a new gradient -with the same name as a deleted-but-in-use gradient resurrects the deleted -gradient.

-
pathName gradient names
-

Returns a list of names of all the gradients that have been created by this treectrl -widget.

-
pathName gradient native ?preference?
-

Without any arguments, this command returns a boolean indicating whether -or not the platform supports native transparent gradients. The preference -argument is a boolean that indicates whether native gradients should be used; -this can be used to test the appearance of the application.

-
-
pathName header option ?arg ...?
-

This command is used to manipulate column headers. -The exact behavior of the command depends on the option argument -that follows the header argument. -The following forms of the command are supported:

-
-
pathName header bbox headerDesc ?column? ?element?
-

See the item bbox command.

-
pathName header compare headerDesc1 op headerDesc2
-

See the item compare command.

-
pathName header configure headerDesc ?arg ...?
-

There are two forms of this command distinguished by whether or not a -column description appears after the -headerDesc argument. If the first argument after headerDesc begins -with a '-' character it is assumed to be an option name, not a column description, -in which case the command applies to the header-row. If the first argument -after headerDesc does not being with a '-' it is assumed to be a -column description, in which case the command applies to a header-column.

-
-
pathName header configure headerDesc ?option? ?value? ?option value ...?
-

If no option is specified, returns a list describing all of the available -options for the header given by headerDesc (see Tk_ConfigureInfo for -information on the format of this list). If option is specified with no -value, then the command returns a list describing the one named option (this -list will be identical to the corresponding sublist of the value returned if -no option is specified).

-

If one or more option-value pairs -are specified, then the command modifies the given option(s) to have the -given value(s); in this case the command returns an empty string. This is the -only case where headerDesc may refer to multiple header-rows.

-

The following options are supported by this command (see header create for -the meaning of each option):

-
-
-height height
-
-
-tags tagList
-
-
-visible boolean
-
-
-
pathName header configure headerDesc column ?option? ?value? ?option value ...?
-

If no option is specified, returns a list describing all of the available -options for the single column column of the header-row given by headerDesc -(see Tk_ConfigureInfo for information on the format of this list). -If option is specified with no value, then the command returns a list -describing the one named option (this list will be identical to the corresponding -sublist of the value returned if no option is specified).

-

If one or more option-value pairs -are specified, then the command modifies the given option(s) to have the -given value(s); in this case the command returns an empty string. This is the -only case where both headerDesc may refer to multiple header-rows and -column may refer to multiple header-columns.

-

The following options are supported by this command (see HEADERS) for -the meaning of each option):

-
-
-arrow direction
-
-
-arrowbitmap bitmap
-
-
-arrowgravity direction
-
-
-arrowimage image
-
-
-arrowpadx amount
-
-
-arrowpady amount
-
-
-arrowside side
-
-
-background color
-
-
-bitmap bitmap
-
-
-borderwidth size
-
-
-button boolean
-
-
-font fontName
-
-
-image image
-
-
-imagepadx amount
-
-
-imagepady amount
-
-
-justify justification
-
-
-state state
-
-
-text text
-
-
-textcolor color
-
-
-textlines count
-
-
-textpadx amount
-
-
-textpady amount
-
-
-
-
pathName header count ?headerDesc?
-

If no additional arguments are given, the result is a decimal string giving -the number of header-rows created by the header create widget command -which haven't been deleted by the header delete widget command, plus 1 -for the ever-present top header-row created along with the widget. -If the optional argument headerDesc is given, then the result is the -number of header-rows that match that header description.

-
pathName header create ?option value?
-

Creates a new header-row and returns its unique identifier. -The following configuration options are supported:

-
-
-height height
-

Specifies a fixed height for the header-row in any of the forms acceptable to -Tk_GetPixels. Must be >= 0. If height is zero then the -header-row's height is the maximum height of all of its column headers. -Defaults to 0.

-
-tags tagList
-

TagList is a list of tag names to be added to the new header-row. The -header tag command can also be used to manipulate this list of tags.

-
-visible boolean
-

Boolean must have one of the forms accepted by Tcl_GetBoolean. It -indicates whether or not the header-row should be displayed. If the widget -option -showheader is false then the header-row will not be displayed -regardless of the value of this option.

-
-
pathName header delete headerDesc
-

Deletes the header-rows given by the header description -headerDesc. Attempts to delete the ever-present top header-row are -ignored without raising an error.

-
pathName header dragcget ?arg ...?
-

There are two forms of this command distinguished by whether or not a -header description appears as the first -argument. If the first argument begins with a '-' character it is assumed to -be an option name, not a header description, in which case the command applies -to the header-drag-and-drop options for the widget. If the first argument -does not being with a '-' it is assumed to be a header description, in which -case the command applies to a header-row.

-
-
pathName header dragcget option
-

This command returns the current value of the header-drag-and-drop option named -option for the widget. -The following configuration options are supported (see header dragconfigure -for the meaning of each option):

-
-
-enable boolean
-
-
-imagealpha alpha
-
-
-imagecolor background
-
-
-imagecolumn column
-
-
-imageoffset offset
-
-
-imagespan count
-
-
-indicatorcolor color
-
-
-indicatorcolumn column
-
-
-indicatorside side
-
-
-indicatorspan count
-
-
-
pathName header dragcget headerDesc option
-

This command returns the current value of the header-drag-and-drop option named -option for a header-row. -The following configuration options are supported (see header dragconfigure -for the meaning of each option):

-
-
-draw boolean
-
-
-enable boolean
-
-
-
-
pathName header dragconfigure ?arg ...?
-

There are two forms of this command distinguished by whether or not a -header description appears as the first -argument. If the first argument begins with a '-' character it is assumed to -be an option name, not a header description, in which case the command applies -to the header-drag-and-drop options for the widget. If the first argument -does not being with a '-' it is assumed to be a header description, in which -case the command applies to a header-row.

-
-
pathName header dragconfigure ?option? ?value? ?option value ...?
-

This command queries and sets header-drag-and-drop options for the widget, not -for individual header-rows. -The following configuration options are supported:

-
-
-enable boolean
-

Controls whether the user is allowed to rearrange columns by drag-and-drop. -The default is false. Each header-row also has an -enable -dragconfigure option.

-
-imagealpha alpha
-

Alpha is an integer from 0 (invisible) to 255 (opaque) controlling the -transparency of the drag image. Any value outside this range is clipped. -The default is 200.

-
-imagecolor background
-

Unused.

-
-imagecolumn column
-

Column specifies the column to create the drag image from.

-
-imageoffset offset
-

Offset is the horizontal screen distance the drag image is offset from its -starting position.

-
-imagespan count
-

Count is the number of columns, starting with -imagecolumn, that will -be dragged as a group.

-
-indicatorcolor color
-

Unused.

-
-indicatorcolumn column
-

The 2-pixel-thick line will be drawn over the left or right edge of column.

-
-indicatorside side
-

Unused.

-
-indicatorspan count
-

Count is the number of columns, starting with -indicatorcolumn, that -will be displaced as a group by the dragged column(s)

-
-
pathName header dragconfigure header ?option? ?value? ?option value ...?
-

This command queries and sets header-drag-and-drop options for header-rows, -not for the widget as a whole. -The following configuration options are supported:

-
-
-draw boolean
-

Controls whether a header-row displays any feedback during header drag-and-drop. -The default is true.

-
-enable boolean
-

Controls whether clicking and dragging in this header-row initiates drag-and-drop. -The default is true. If the -enable option for the widget is false -(see above) then this option has no effect.

-
-
-
pathName header element ?arg ...?
-

See the item element command.

-
pathName header id headerDesc
-

This command resolves the header description -headerDesc into a list of unique header-row identifiers. If headerDesc -doesn't refer to any existing header-rows, then this command returns an empty list.

-
pathName header image headerDesc ?column? ?image? ?column image ...?
-

The behavior of this command depends on whether or not a column header was assigned -a style containing an image element. If a column header has no style or no style -with an image element then this command operates on the same -image option as -header configure. Otherwise this command operates on the -image option -of the first image element in a column header's style. See the item image -command.

-
pathName header span headerDesc ?column? ?numColumns? ?column numColumns ...?
-

See the item span command.

-
pathName header state command headerDesc ?arg ...?
-

See the item state command.

-
pathName header style command headerDesc ?arg ...?
-

See the item style command.

-
pathName header text headerDesc ?column? ?text? ?column text ...?
-

The behavior of this command depends on whether or not a column header was assigned -a style containing a text element. If a column header has no style or no style -with a text element then this command operates on the same -text option as -header configure. Otherwise this command operates on the -text option -of the first text element in a column header's style. See item text.

-
pathName header tag command headerDesc ?arg ...?
-

See the item tag command.

-
-
pathName identify ?-array varName? x y
-

This command returns information about the what -is displayed at the given window coordinates x and y. -When the -array option is used to specify the name of an array variable, -elements of the array variable are set as follows:

-
    -
  1. If the coordinates are outside the window, over the borders, or over any -whitespace in the window, then:

    -

    $varName(where) is ""

  2. -
  3. If the coordinates are over a column header, then:

    -

    $varName(where) is header

    -

    $varName(header) is the unique id of the header-row

    -

    $varName(column) is the unique id of the column

    -

    $varName(element) is the name of an element, or ""

    -

    $varName(side) is left or right if the coordinates are close -to the edge of the column header, otherwise ""

  4. -
  5. If the coordinates are over an item, then:

    -

    $varName(where) is item

    -

    $varName(item) is the unique id of the item

    -

    $varName(column) is the unique id of the column

    -

    $varName(element) is the name of an element, or ""

    -

    $varName(button) is a boolean indicating whether or not the coordinates are -over the item's expand/collapse button

    -

    $varName(line) is the unique id of an ancestor of the item -(but not the parent of the item) if the coordinates -are over a line descending from that ancestor. If the coordinates are not -over such a line then $varName(line) is "". -This is used to collapse the ancestor when the line is clicked on.

  6. -
-

When the -array option is not used, this command returns a list -describing what is displayed at the given window coordinates. The format -of this list can be like one of the following:

-
    -
  1. {}

    -

    An empty list is returned if the coordinates are outside the window, over the -borders, or over any whitespace in the window.

  2. -
  3. header C ?left|right?

    -

    header C elem E ?left|right?

    -

    header H column C ?left|right?

    -

    header H column C elem E ?left|right?

    -

    Only when there is more than one header-row is there a -unique id of a header-row H followed by the keyword column. -This is for compatibility with older versions when there was only one row -of column headers allowed.

  4. -
  5. item I column C

  6. -
  7. item I column C elem E

  8. -
  9. item I button

    -

    This is the result when the coordinates are over the expand/collapse button -next to an item.

  10. -
  11. item I line I2

    -

    This is the result when the coordinates are over a line descending from an -ancestor I2 of the item I (but not the parent of that item). This is used to -collapse the ancestor when the line is clicked on.

  12. -
-
pathName index itemDesc
-

Deprecated. Use item id instead.

-
pathName item option ?arg ...?
-

This command is used to manipulate items. -The exact behavior of the command depends on the option argument -that follows the item argument. -The following forms of the command are supported:

-
-
pathName item ancestors itemDesc
-

Returns a list containing the item ids of the ancestors -of the item specified by itemDesc. The first list value is the parent, -the second is the parent's parent, an so on. The last list value will be the -root item if itemDesc is a descendant of the root item.

-
pathName item bbox itemDesc ?column? ?element?
-

Returns a list with four elements giving the bounding box of the item described -by itemDesc. If no further argument is specified, the bbox spans the area -of the item over all non-locked columns. If a column is specified, only the -area of the item in this column is considered. If an additional element is -specified, the area of this element in column of the specified item -is returned. The returned coordinates are relative to the top-left corner of the -widget. If the item is not visible for any reason, the result in an empty string.

-
pathName item buttonstate itemDesc ?state?
-

If state is specified, this command sets the state of the expand/collapse -button for the single item specified by itemDesc. The state -argument may be one of active, normal or pressed. -The current (or newly-set) state of the button is returned. The button state -is used by the system theme, if any, to change the appearance of the button.

-
pathName item cget itemDesc option
-

Returns the current value of the configuration option for the item specified by -itemDesc whose name is option. Option may have any of the -values accepted by the item configure command.

-
pathName item children itemDesc
-

Returns a list containing the item ids of all children -of the item specified by itemDesc in the correct order from -the first child to the last child.

-
pathName item collapse itemDesc ?-animate? ?-recurse?
-

Switches off the open state of the item(s) described by itemDesc. -If an item has descendants, then they are no longer displayed. -If an item is already closed, then this command has no effect on that item. -If -animate is specified, then the item's button will animate as it -transitions between states if the theme supports it; in this case only one item -may be specified. -If -recurse is specified, then all descendants of the items described -by itemDesc will also be collapsed. -For every item that actually will be collapsed, two events are generated: -a <Collapse-before> event before the item state is changed, -and a <Collapse-after> event after the item state was changed.

-
pathName item compare itemDesc1 op itemDesc2
-

From both items described by the itemDescs the index is retrieved -(as returned from the item order widget command). -Then these indexes are compared using the operator op, which must -be either <, <=, ==, >=, >, -or !=. -The return value of this command is 1 if the comparison evaluated to true, -0 otherwise.

-
pathName item complex itemDesc ?list...?
-

This horrible command is now deprecated. Use item element configure -instead. For every column of the treectrl there may be specified one list. -Each list should look like this:

-
-{ {element option value ...} {element option value ...} ...}
-
-

Every option must be known by the element's type -(see ELEMENTS AND STYLES below). -Each option will be set to value for the element in this -one column in this item.

-
pathName item configure itemDesc ?option? ?value? ?option value ...?
-

If no option is specified, returns a list describing all of the available -options for the item given by itemDesc (see Tk_ConfigureInfo for -information on the format of this list). If option is specified with no -value, then the command returns a list describing the one named option (this -list will be identical to the corresponding sublist of the value returned if -no option is specified).

-

If one or more option-value pairs -are specified, then the command modifies the given item option(s) to have the -given value(s); in this case the command returns an empty string. This is the -only case where itemDesc may refer to multiple items.

-

The following options are supported by this command (see item create for -the meaning of each option):

-
-
-button boolean|auto
-
-
-height height
-
-
-tags tagList
-
-
-visible boolean
-
-
-wrap boolean
-
-
-
pathName item count ?itemDesc?
-

If no additional arguments are given, the result is a decimal string giving the number of -items created by the item create widget command which haven't been -deleted by the item delete widget command, plus 1 for the ever-present -root item. -If the optional argument itemDesc is given, then the result is the -number of items that match that item description.

-
pathName item create ?option value ...?
-

Creates some new items and optionally returns a list of unique identifiers for -those items. -The new items have the states open and enabled set by default. -If the treectrl widget currently has the focus, -the state focus is also set.

-

The following options are supported by this command:

-
-
-button boolean|auto
-

The value of this option must have one of the forms accepted by Tcl_GetBoolean -or be the word auto (or any abbreviation of it). It indicates whether or not an expand/collapse -button should be drawn next to the item, typically to indicate that the item has -children. -If the value of this option is auto, then a button is displayed next to the -item whenever the item has any children whose item option -visible is true. -The button will only be displayed if:

-
    -
  1. the column specified by the treectrl option -treecolumn is visible, and

  2. -
  3. the treectrl option -showbuttons is true, and

  4. -
  5. for the root item, the treectrl option -showrootbutton is true, and

  6. -
  7. for immediate children of the root item, the treectrl option -showrootchildbuttons is true.

  8. -
-
-count numItems
-

Specifies the number of items to create. Must be >= 0. Defaults to 1.

-
-enabled boolean
-

Specifies whether the items should be enabled. Default is true.

-
-height height
-

Specifies a fixed height in any of the forms acceptable to Tk_GetPixels. -Must be >= 0. If height is zero then the item's height is unspecified. -Defaults to 0. See also the widget options -itemheight and --minitemheight.

-
-nextsibling itemDesc
-

Specifies the item before which the new items will be inserted. The new items -will have the same parent as itemDesc.

-
-open boolean
-

Specifies whether the items should be open or closed. Default is true.

-
-parent itemDesc
-

Specifies the item which the new items will be the children of. The new items -will be appended to the list of children of itemDesc. When no parent is -specified, the new items are orphan items (see the widget command -orphans) and will not be displayed in the list.

-
-prevsibling itemDesc
-

Specifies the item after which the new items will be inserted. The new items -will have the same parent as itemDesc.

-
-returnid boolean
-

Specifies whether or not to return a list of item identifiers for the newly -created items. Specifying false is useful when creating a large number of items in the -console or to improve performance. Default is true.

-
-tags tagList
-

TagList is a list of tag names to be added to the new items. -The item tag command can also be used to manipulate this list of tags.

-
-visible boolean
-

Boolean must have one of the forms accepted by Tcl_GetBoolean. It -indicates that the item should be displayed in the list. The item will only be -displayed if:

-
    -
  1. each ancestor is a descendant of the root item (not an orphan), and

  2. -
  3. each ancestor's -visible option is true

  4. -
-
-wrap boolean
-

Boolean must have one of the forms accepted by Tcl_GetBoolean. It -indicates that this item should be the first one in a horizontal range or -vertical range of items. See also the widget option -wrap.

-
-
pathName item delete first ?last?
-

Deletes the specified item(s). -First and last must be valid -item descriptions. -If last isn't specified, then first may specify multiple items. -If both first and last are specified, -they must each decribe a single item with a common ancestor; -then the range of items between first and last is deleted. -The order of first and last doesn't matter.

-

Deleting an item deletes any child items of the deleted item recursively. -If the current active item is deleted, the root item becomes the new active item. -If the current selection anchor item is deleted, the root item becomes the new anchor item. -There is no way to delete the root item of the treectrl widget; -in all cases the specification of the root item is ignored.

-

For each call to this command, two events may be generated. -If any of the deleted items are selected, then they are removed from the selection -and a <Selection> event is generated just before the items are deleted. -If any items are going to be deleted, then an <ItemDelete> event is -generated just before the items are deleted.

-
pathName item descendants itemDesc
-

Returns a list containing the item ids of the descendants -of the item specified by itemDesc, i.e. the children, grandchildren, -great-grandchildren etc, of the item.

-
pathName item dump itemDesc
-

Debug command. Returns a list with 4 words in the form -index index indexVis indexVis.

-
pathName item element command itemDesc column element ?arg ...?
-

This command is used to manipulate elements of the item. -The exact behavior of the command depends on the command argument -that follows the element argument. -The following forms of the command are supported:

-
-
pathName item element actual itemDesc column element option
-

Deprecated. Use item element perstate instead.

-
pathName item element cget itemDesc column element option
-

This command returns the value of the option named option -associated with element inside column of the item described by -itemDesc, if it was already configured for the actual item. -Option may have any of the values accepted by the type of the -specified element (see ELEMENTS AND STYLES below)

-
pathName item element configure itemDesc column element ?option? ?value? ?option value ...?
-

This command modifies configuration options for an element in a column of -an item. -If no option is specified, the command returns a list describing -all of the available options for the element (see Tk_ConfigureInfo -for information on the format of this list). -If option is specified with no value, then the command returns -a list describing the one named option (this list will be identical to -the corresponding sublist of the value returned if no option -is specified).

-

If one or more option-value pairs are specified, then the command -modifies the given option(s) to have the given value(s) in the -element inside column of the item(s) described by itemDesc; -in this case the command returns an empty string. This is the only case where -itemDesc may refer to multiple items.

-

It is possible to configure multiple elements in multiple columns with -a single call. To configure another element in the same column, append a -'+' argument followed by the element name. To configure elements in -another column, append a ',' argument followed by the column. -For example:

-
-.t item element configure $I \
-	$C1 $E1 -text "hello" + $E2 -text "world" , \
-	$C2 $E3 -fill Blue , \
-	$C3 $E1 -text "apples and oranges"
-
-

Each of the column description arguments -to this command may refer to multiple columns if at least one -option-value pair is given.

-
pathName item element perstate itemDesc column element option ?stateList?
-

This command returns the current value of the per-state -option named option for element inside column of the item described by -itemDesc. If stateList is specified, the list of state names (static -and dynamic, see STATES) is used in place of the current state for -item and column.

-
-
pathName item enabled itemDesc ?boolean?
-

Returns 1 if the item described by itemDesc has the -state enabled switched on, 0 otherwise. If boolean is specified, -then the enabled state of every item described by the -item description itemDesc is set accordingly. -New items are enabled by default when created. Disabled items cannot be selected, -and are ignored by the default key-navigation and mouse bindings.

-
pathName item expand itemDesc ?-animate? ?-recurse?
-

Switches on the open state of the item(s) described by itemDesc. -If an item has descendants, then they are now displayed. -If an item is already open, then this command has no effect on that item. -If -animate is specified, then the item's button will animate as it -transitions between states if the theme supports it; in this case only one item -may be specified. -If -recurse is specified, then all descendants of the items described by -itemDesc will also be expanded. -For every item that actually will be expanded, two events are generated: -an <Expand-before> event before the item state is changed, -and an <Expand-after> event after the item state was changed.

-
pathName item firstchild parent ?child?
-

If child is not specified, returns the item id of the first -child of the item described by parent. -If child is specified, it must describe an item -that is neither the root item nor an ancestor of parent. -Then it will become the new first child of parent.

-
pathName item id itemDesc
-

This command resolves the item description -itemDesc into a list of unique item identifiers. If itemDesc -doesn't refer to any existing items, then this command returns an empty list.

-
pathName item image itemDesc ?column? ?image? ?column image ...?
-

This command sets or retrieves the value of the per-state --image option for the first image element in one or more columns. -If no column is specified, this command returns a list of values, -one per column. -If no image is specified, this command returns the value for column.

-

If one or more column-image pairs is specified, -then the value of the -image option in each column is set to image. -In this case itemDesc may refer to multiple items and each column -may refer to multiple columns.

-

Note that this command is provided as a convenience. Use the -item element configure or item element cget commands if you want -to set or retrieve the value of the -image option for a specific image element.

-
pathName item isancestor itemDesc descendant
-

Returns 1 if the item described by itemDesc is a direct or indirect -parent of the item decribed by descendant, 0 otherwise.

-
pathName item isopen itemDesc
-

Returns 1 if the item described by itemDesc has the -state open switched on, 0 otherwise.

-
pathName item lastchild parent ?child?
-

If child is not specified, returns the item id of the last -child of the item described by parent. -If child is specified, it must describe an item -that is not an ancestor of parent. -Then it will become the new last child of parent.

-
pathName item nextsibling sibling ?next?
-

If next is not specified, returns the item id of the next -sibling of the item described by sibling. -If next is specified, it must describe an item -that is not an ancestor of sibling. -Then it will become the new next sibling of sibling.

-
pathName item numchildren itemDesc
-

Returns the number of children of the item described by itemDesc.

-
pathName item order itemDesc ?-visible?
-

This command returns the position of the item itemDesc relative to -its toplevel ancestor (usually the root item, unless the ancestor is an -orphan). If you imagine all the items flattened into a vertical list, the -result of this command is the row the item falls in. If the optional argument --visible is given, only the items whose ancestors are expanded, and whose --visible option is true, get counted; in this case -1 is returned if the item -is not visible.

-
pathName item parent itemDesc
-

Returns the item id of the parent of the item -described by itemDesc.

-
pathName item prevsibling sibling ?prev?
-

If prev is not specified, returns the item id of the previous -sibling of the item described by sibling. -If prev is specified, it must describe an item -that is not an ancestor of sibling. -Then it will become the new previous sibling of sibling.

-
pathName item range first last
-

Returns a list containing the item ids of all items -in the range between first and last, inclusive. -The order between first and last doesn't matter, -and the result is always sorted by the increasing order of the items (as -returned by the item order command). -The items specified by first and last must share a common -ancestor.

-
pathName item remove itemDesc
-

Removes the item described by itemDesc -from the list of children of its parent, so that it will become an orphan.

-
pathName item rnc itemDesc
-

Returns a list of two integers, which corresponds to the row and column -of the item described by itemDesc. The row and column corresponds to -the on-screen arrangement of items as determined by the -orient and -wrap -options. If the item is not displayed, this command returns an empty string.

-
pathName item sort itemDesc ?option ...?
-

Sorts the children of the item described by itemDesc, -and redisplays the tree with the items in the new order.

-

The range of items which should be sorted can be restricted -by means of the -first and/or -last options, -which should be children of the item described by itemDesc; -the order between these two limiting items doesn't matter.

-

The sort column can be specified by means of the -column option; -this option can be used repeatedly to define a multicolumn sort. -The sorting is done by looking at the text -of the element specified by the -element option, -which must be a text element defined in the style of the sorting column, -by default the first text element is used.

-

If the -notreally option is specified, -no rearranging of the items is done; -instead the sorted items are returned as result of the command.

-

By default ASCII sorting is used with the result returned in increasing order. -Any of the following options may be specified to control -the sorting process of the previously specified column -(unique abbreviations are accepted):

-
-
-ascii
-

Use string comparison with ASCII collation order. This is the default.

-
-command command
-

Use command as a comparison command. -To compare two items, evaluate a Tcl script consisting of -command with the numerical ids of the two items appended as additional -arguments. The script should return an integer less than, -equal to, or greater than zero if the first item is to -be considered less than, equal to, or greater than the second, -respectively.

-
-decreasing
-

Sort the items in decreasing order ("largest" items first).

-
-dictionary
-

Use dictionary-style comparison. This is the same as -ascii -except (a) case is ignored except as a tie-breaker and (b) if two -strings contain embedded numbers, the numbers compare as integers, -not characters. For example, in -dictionary mode, bigBoy -sorts between bigbang and bigboy, and x10y -sorts between x9y and x11y.

-
-increasing
-

Sort the items in increasing order ("smallest" items first). This is the default.

-
-integer
-

Convert to integers and use integer comparison.

-
-real
-

Convert to floating-point values and use floating comparison.

-
-
pathName item span itemDesc ?column? ?numColumns? ?column numColumns ...?
-

This command sets or retrieves the number of columns that a style covers. -If no column is specified, the return value is a list of spans, one per column. -If no numColumns is specified, the return value is the span for column.

-

If one or more column-numColumns pairs is specified, the -span for each column is set to numColumns. In this case itemDesc -may refer to multiple items and each column may refer to multiple -columns.

-
pathName item state command itemDesc ?arg ...?
-

This command is used to manipulate the states of an item. -The exact behavior of the command depends on the command argument -that follows the style argument. -The following forms of the command are supported:

-
-
pathName item state define stateName
-

Defines a new state with the name stateName, -which must not be the name of an existing state.

-
pathName item state forcolumn itemDesc column ?stateDescList?
-

Just like item state set but manipulates dynamic states for a single -item column, not the item as a whole. If stateDescList is unspecified, -this command returns a list containing the names of all the dynamic states -which are switched on in column.

-

If stateDescList is specified, then itemDesc may refer to multiple -items and column may refer to multiple columns.

-
pathName item state get itemDesc ?stateName?
-

If no stateName is specified, returns a list containing -the names of all (static and dynamic) states -which are currently switched on for the item described by itemDesc. -If a stateName is specified, -1 is returned if the specified state is currently switched on for the item, -0 otherwise.

-
pathName item state linkage stateName
-

Returns a string indicating whether the specified state is user-defined -by means of the item state define widget command (dynamic) -or predefined by the treectrl widget itself (static).

-
pathName item state names
-

Returns a list containing the names of all user-defined states.

-
pathName item state set itemDesc ?lastItem? stateDescList
-

Every element of stateDescList -must be the name of a dynamic state (see STATES below), -optionally preceded by a ~ or ! character. -Every state with a leading ! will be switched off for the -item described by itemDesc, -every state with a leading ~ will be toggled, and -every state without leading ! or ~ will be switched on. -If lastItem is specified, the state changes will be made for all items -in the range between itemDesc and lastItem. -If lastItem unspecified, -then the state changes are made for all items described by itemDesc.

-
pathName item state undefine ?stateName ...?
-

Every stateName must be the name of a user-defined state. -Removes this state from the list of user-defined states.

-
-
pathName item style command itemDesc ?arg ...?
-

This command is used to manipulate the styles of an item. -The exact behavior of the command depends on the command argument -that follows the style argument. -The following forms of the command are supported:

-
-
pathName item style elements itemDesc column
-

This command returns a list containing the names of elements which were -configured by the item element configure command for the item -described by itemDesc in column. If there is no style assigned -to column an error is returned.

-
pathName item style map itemDesc column style map
-

Like the item style set command, this command may be used to assign a -style to a specific column of an item. Unlike item style set, this -command can transfer configuration values of elements in the current style -to elements in the new style specified by style. -Map must be a list of elementOld-elementNew pairs, where -elementOld is an element in the current style, and elementNew is -an element in the style specified by style. Both elementOld and -elementNew must be of the same type (bitmap, text etc). -ItemDesc may refer to multiple items and column may refer to -multiple columns.

-
pathName item style set itemDesc ?column? ?style? ?column style ...?
-

This command sets or retrieves the style assigned to one or more columns. -If no column is specified, this command returns a list containing the names of the -styles set for all columns of the item described by itemDesc. -If no style is specified, this command -returns the name of the style set for the item described by -itemDesc in column.

-

If one or more column-style pairs is specified, -then the style in each column is set to style. In this case -itemDesc may refer to multiple items and each column may refer to -multiple columns.

-
-
pathName item tag option ?arg arg ...?
-

This command is used to manipulate tags on items. -The exact behavior of the command depends on the option argument -that follows the item tag argument. -The following forms of the command are supported:

-
-
pathName item tag add itemDesc tagList
-

Adds each tag in tagList to the items specified by the -item description itemDesc. -Duplicate tags are ignored. The list of tags for an item can also be -changed via an item's -tags option.

-
pathName item tag expr itemDesc tagExpr
-

Evaluates the tag expression tagExpr against every item -specified by the item description -itemDesc. The result is 1 if the tag expression evaluates to true -for every item, 0 otherwise.

-
pathName item tag names itemDesc
-

Returns a list of tag names assigned to the items -specified by the item description -itemDesc. The result is the union of any tags assigned to the -items.

-
pathName item tag remove itemDesc tagList
-

Removes each tag in tagList from the items specified by the -item description itemDesc. -It is not an error if any of the items do not use any of the tags. -The list of tags for an item can also be changed via an item's --tags option.

-
-
pathName item text itemDesc ?column? ?text? ?column text ...?
-

This command sets or retrieves the value of the -text option for the first -text element in one or more columns. -If no column is specified, this command returns a list of values, -one per column. -If no text is specified, this command returns the value for column.

-

If one or more column-text pairs is specified, -then the value of the -text option in each column is set to text. -In this case itemDesc may refer to multiple items and each column -may refer to multiple columns.

-

Note that this command is provided as a convenience. Use the -item element configure or item element cget commands if you -want to set or retrieve the value of the -text option for a specific text element.

-
pathName item toggle itemDesc ?-animate? ?-recurse?
-

Changes the open state of the item(s) described by itemDesc. -If the open state is currently switched off, then -this command does the same as the item expand widget command; -otherwise the same as the item collapse widget command. -If -animate is specified, then the item's button will animate as it -transitions between states if the theme supports it; in this case only one item -may be specified. -If -recurse is specified, then the open state of all descendants of -the items described by itemDesc will also be toggled.

-
-
pathName marquee option ?arg ...?
-

This command is used to manipulate the marquee, -which can be used to implement a resizable selection rectangle, in a -file browser for example. -One corner point of the marquee is fixed as long as the marquee is visible -and called the anchor; the diagonally opposite corner is dragged with the -mouse while resizing the marquee and simply called the corner.

-

All coordinates handled by this widget command are canvas -coordinates, i.e. the canvasx or canvasy widget command should be -used to translate window coordinates to canvas coordinates.

-

By default, the marquee is displayed as a 1-pixel thick dotted rectangle. -If either of the -fill or -outline options is specified, then -the marquee is drawn as a filled and/or outlined rectangle of the specified -color(s). The -fill option should specify a transparent gradient to -avoid hiding what is inside the marquee. See GRADIENTS for more info.

-

The exact behavior of the command depends on the option argument -that follows the marquee argument. -The following forms of the command are supported:

-
-
pathName marquee anchor ?x y?
-

Returns a list containing the x and y coordinates of the anchor, -if no additional arguments are specified. -If two coordinates are specified, -sets the anchor to the given coordinates x and y.

-
pathName marquee cget option
-

This command returns the current value of the marquee option -named option. -Option may have any of the values accepted by the -marquee configure widget command.

-
pathName marquee configure ?option? ?value? ?option value ...?
-

This command is similar to the configure widget command except -that it modifies the marquee options -instead of modifying options for the overall treectrl widget. -If no option is specified, the command returns a list describing -all of the available marquee options (see Tk_ConfigureInfo -for information on the format of this list). -If option is specified with no value, then the command returns -a list describing the one named marquee option (this list will be identical to -the corresponding sublist of the value returned if no option -is specified). -If one or more option-value pairs are specified, then the command -modifies the given marquee option(s) to have the given value(s); -in this case the command returns an empty string.

-

The following marquee options are supported:

-
-
-fill color
-

Specifies the color to fill the marquee rectangle with. See the comments above -about using a transparent gradient here.

-
-outline color
-

Specifies the color to outline the marquee rectangle with.

-
-outlinewidth color
-

Specifies the width of the outline drawn inside the marquee's rectangle. -The outline is not drawn if this value is less than 1. -This option has no effect if the -outline option is unspecified, -i.e., the default dotted rectangle is unaffected by this option. -outlineWidth may be in any of the forms acceptable to Tk_GetPixels. -Defaults to 1.

-
-visible boolean
-

Specifies a boolean value which determines -whether the marquee is displayed.

-
-
pathName marquee coords ?x1 y1 x2 y2?
-

Returns a list containing the x and y coordinates of the anchor -followed by the x and y coordinates of the corner, -if no additional arguments are specified. -If four coordinates are specified, -sets the anchor to the given coordinates x1 and y1 -and the corner to the coordinates x2 and y2.

-
pathName marquee corner ?x y?
-

Returns a list containing the x and y coordinates of the corner, -if no additional arguments are specified. -If two coordinates are specified, -sets the corner to the given coordinates x and y.

-
pathName marquee identify
-

Returns a list with information about any items intersecting the marquee. -The format of the returned list is:

-
-{
-    {item {column element element ...} {column element element ...} ...}
-    {item {column element element ...} {column element element ...} ...}
-    ...
-}
-
-

There may be zero sublists following an item id if the marquee is in the -button/line area of an item. There may be zero element names following a -column id if the item-column has no style or if the marquee does not -intersect any elements in that column.

-
-
pathName notify option ?arg ...?
-

Many Tk widgets communicate with the outside world via -command -callbacks and/or virtual events. For example, the Text widget -evaluates its -yscrollcommand when the view in the widget changes, -and generates a <<Modified>> virtual event when text is inserted or deleted. -A treectrl widget replaces both methods of communication with its own event -mechanism accessed through the notify subcommands.

-

The exact behavior of the command depends on the option argument -that follows the notify argument. -The following forms of the command are supported:

-
-
pathName notify bind ?object? ?pattern? ?+??script?
-

This command associates Tcl scripts with events generated by a -treectrl widget. -If all three arguments are specified, notify bind will arrange for -script (a Tcl script) to be evaluated whenever the event(s) specified -by pattern are generated by this treectrl widget. -If script is prefixed with a "+", then it is appended to any existing -binding for pattern; otherwise script replaces any existing binding. -If script is an empty string then the current binding for pattern -is destroyed, leaving pattern unbound. In all of the cases where a script -argument is provided, notify bind returns an empty string.

-

If pattern is specified without a script, then the script currently -bound to pattern is returned, or an empty string is returned if there is -no binding for pattern. If neither pattern nor script is -specified, then the return value is a list whose elements are all the patterns -for which there exist bindings for object.

-

The object argument determines which window(s) the binding applies to. -If object begins with a dot, as in .a.b.c, then it must be the path name -for a window; otherwise it may be an arbitrary string. Like the regular -bind command, bindings on window names are automatically removed if -that window is destroyed.

-
pathName notify configure object pattern ?option? ?value? ?option value ...?
-

This command sets and retrieves options for bindings created by the -notify bind command.

-

If no option is specified, the command returns a list with -option-value pairs describing all -the available binding options for pattern on object. -If option is specified with no value, -then the command returns the current value of that option. -If one or more option-value pairs are specified, then the command -modifies the given option(s) to have the given value(s) for the binding; -in this case the command returns an empty string.

-

The following binding options are supported:

-
-
-active boolean
-

Specifies if the binding should be active. -As long as this option is specified as false, -a binding script will not be evaluated when the corresponding event is -generated.

-
-
pathName notify detailnames eventName
-

Returns a list containing the names of all details, -which are installed for the event with the name eventName -by means of the notify install widget command -or by the treectrl widget itself.

-
pathName notify eventnames
-

Returns a list containing the names of all events, -which are installed by means of the notify install widget command -or by the treectrl widget itself.

-
pathName notify generate pattern ?charMap? ?percentsCommand?
-

This command causes the treectrl widget to generate an event. This command is -typically used to generate dynamic events created by the notify install -command, but may be used to generate static events also. -The event specified by pattern is generated, and any active binding -scripts on the event are evaluated after undergoing %-substitution. -If there are details defined for the event, -pattern must describe an <eventName-detail> pair, -otherwise pattern should be <eventName>.

-

The optional charMap is a list of char-value pairs -as in the form returned by array get. -Each char has to be exactly one character. -The charMap is used in %-substitution.

-

If percentsCommand is specified, then it will be used to perform %-substitution -on any scripts bound to the event. If percentsCommand is not specified and -the event is dynamic, then the %-subtitution command passed to notify install -will be used if it was provided. If the event is static or no %-substitution -command is available, then all %-substitution is done using charMap only . -See notify install for a description of percentsCommand.

-
pathName notify install pattern ?percentsCommand?
-

This command installs a new event or detail specified by pattern. -Events created by this command are called dynamic, -whereas events created by the treectrl widget itself are called static. -This command may be called to set or retrieve the percentsCommand for -an existing dynamic event.

-

The optional percentsCommand is a list containing the name of a Tcl -command, plus any optional arguments, to which five additional arguments -will be appended. The command will be called to perform %-substitution on any -scripts bound to the event specified by pattern (see EVENTS AND SCRIPT SUBSTITUTIONS). -PercentsCommand should be defined as follows:

-
-proc percentsCommand {?arg arg ...? char object event detail charMap} {
-	switch -- $char {
-		...
-	}
-	return $value
-}
-
-

The optional arg arguments are part of the percentsCommand list. -Char is the %-character to be substituted. Object is the same -as the argument to notify bind. Event and detail specify -the event. CharMap is the same as the argument to notify generate. -PercentsCommand should return the value to replace the %-character by. -If an error occurs evaluating percentsCommand, the %-character is replaced -by itself.

-

notify install returns the current percentsCommand for the event, -or an error if the event is not dynamic.

-
pathName notify install detail eventName detail ?percentsCommand?
-

Deprecated. -Use notify install with a pattern of <eventName-detail> instead.

-
pathName notify install event eventName ?percentsCommand?
-

Deprecated. -Use notify install with a pattern of <eventName> instead.

-
pathName notify linkage pattern
-

Returns a string indicating -whether the specified event or detail is created -by means of the notify install widget command (dynamic) -or by the treectrl widget itself (static).

-
pathName notify linkage eventName ?detail?
-

Deprecated. -Use notify linkage with a pattern of <eventName> or -<eventName-detail> instead.

-
pathName notify unbind object ?pattern?
-

If no pattern is specified, all bindings on object are removed. -If pattern is specified, then the current binding for pattern -is destroyed, leaving pattern unbound.

-
pathName notify uninstall pattern
-

If the event or detail specified by pattern is static -(i.e. created by the treectrl widget itself), an error is generated. -Otherwise the dynamic event or detail is removed. If an event name is specified -without a detail, all details for that event are also removed.

-
pathName notify uninstall detail eventName detail
-

Deprecated. -Use notify uninstall with a pattern of <eventName-detail> instead.

-
pathName notify uninstall event eventName
-

Deprecated. -Use notify uninstall with a pattern of <eventName> instead.

-
-
pathName numcolumns
-

Deprecated. Use the column count command instead.

-
pathName numitems
-

Deprecated. Use the item count command instead.

-
pathName orphans
-

Returns a list containing the item ids of all items -which have no parent. -When an item is created, it has no parent by default, -and can later become an orphan -by means of the item remove widget command. The root item is not returned.

-
pathName range first last
-

Deprecated. Use the item range command instead.

-
pathName scan option args
-

This command is used to implement scanning on treectrls. It has two forms, -depending on option:

-
-
pathName scan mark x y
-

Records x and y and the treectrl's current view; used in conjunction with -later scan dragto commands. Typically this command is associated with a -mouse button press in the widget and x and y are the coordinates of the -mouse. It returns an empty string.

-
pathName scan dragto x y ?gain?
-

This command computes the difference between its x and y arguments (which -are typically mouse coordinates) and the x and y arguments to the last -scan mark command for the widget. It then adjusts the view by gain -times the difference in coordinates, where gain defaults to 10. This -command is typically associated with mouse motion events in the widget, -to produce the effect of dragging the treectrl at high speed through its window. -The return value is an empty string.

-
-
pathName see itemDesc ?columnDesc? ?option value ...?
-

Adjust the view in the treectrl so that the item described by itemDesc -is visible. -If the item is already visible then the command has no effect; -otherwise the treectrl scrolls to bring the item into view, -and the corresponding <Scroll-x> and/or <Scroll-y> -events are generated. If columnDesc is specified then a specific column -of the item is scrolled into view instead of the entire item.

-

The following options are supported:

-
-
-center flags
-

Flags is a string that contains zero or more of the characters x -or y. This option is used to center the item horizontally and/or -vertically in the window. The item will be centered regardless of whether it -is already visible.

-
-
pathName selection option args
-

This command is used to adjust the selection within a treectrl. -It has several forms, depending on option:

-
-
pathName selection add first ?last?
-

First and last (if specified) -must be valid item descriptions. If both -first and last are specified, then they may refer to a single -item only; in this case -the command adds every unselected item in the range between -first and last, inclusive, to the selection -without affecting the selected state of items outside that range. -If only first is specified, then -every unselected item specified by first is added to the selection. -A <Selection> event is generated if any items were added to the -selection.

-
pathName selection anchor ?itemDesc?
-

If itemDesc is specified, -the selection anchor is set to the described item. -The selection anchor is the end of the selection that is fixed -while dragging out a selection with the mouse. -The item description anchor may be used to refer to the anchor item. -This command doesn't modify the selection state of any item. -Returns the unique id of the selection anchor item.

-
pathName selection clear ?first? ?last?
-

First and last (if specified) -must be valid item descriptions. If both -first and last are specified, then they may refer to a single -item only; in this case any selected items between first and last -(inclusive) are removed from the selection without affecting -the selected state of items outside that range. -If only first is specified, then -every selected item specified by first is removed from the selection. -If neither first nor last are specified, -then all selected items are removed from the selection. -A <Selection> event is generated if any items were removed from the -selection.

-
pathName selection count
-

Returns an integer indicating the number -of items in the treectrl that are currently selected.

-
pathName selection get ?first? ?last?
-

When no additional arguments are given, the result is an unsorted list -containing the item ids of all of the items in the treectrl that are currently selected. -If there are no items selected in the treectrl, then an empty string is returned. -The optional arguments first and last are treated as indices into -the sorted list of selected items; these arguments allow in-place lindex -and lrange operations on the selection. For example:

-
-.t selection get 0       ; # return the first selected item
-.t selection get end     ; # return the last selected item
-.t selection get 1 end-1 ; # return every selected item except the first and last
-
-
-
pathName selection includes itemDesc
-

Returns 1 if the item described by itemDesc is currently -selected, 0 if it isn't.

-
pathName selection modify select deselect
-

Both arguments select and deselect are -a possibly-empty list of item descriptions. -Any unselected items in select are added to the selection, -and any selected items in deselect are removed from the selection (except -for those items which are also in select). -A <Selection> event is generated if any items were selected or deselected.

-
-
pathName state option args
-

This command is used to manipulate the list of user-defined item states, -see section STATES below. Item states can also be managed using the -item state command. To manage states for header-rows, -use the header state widget command. -The exact behavior of the command depends on the option argument -that follows the state argument. -The following forms of the command are supported:

-
-
pathName state define stateName
-

Defines a new state with the name stateName, -which must not be the name of an existing state.

-
pathName state linkage stateName
-

Returns a string indicating -whether the specified state is user-defined -by means of the state define widget command (dynamic) -or predefined by the treectrl widget itself (static).

-
pathName state names
-

Returns a list containing the names of all user-defined states.

-
pathName state undefine ?stateName ...?
-

Every stateName must be the name of a user-defined state. -Removes this state from the list of user-defined states.

-
-
pathName style option ?element? ?arg arg ...?
-

This command is used to manipulate styles, which can be thought of -as a geometry manager for elements. -The exact behavior of the command depends on the option argument -that follows the style argument. -The following forms of the command are supported:

-
-
pathName style cget style option
-

This command returns the current value of the option named option -associated with the style given by style. -Option may have any of the values accepted by the -style configure widget command.

-

This command also accepts the -statedomain option.

-
pathName style configure style ?option? ?value? ?option value ...?
-

This command is similar to the configure widget command except -that it modifies options associated with the style given by style -instead of modifying options for the overall treectrl widget. -If no option is specified, the command returns a list describing -all of the available options for style (see Tk_ConfigureInfo -for information on the format of this list). -If option is specified with no value, then the command returns -a list describing the one named option (this list will be identical to -the corresponding sublist of the value returned if no option -is specified). -If one or more option-value pairs are specified, then the command -modifies the given option(s) to have the given value(s) in style; -in this case the command returns an empty string.

-

The following options are supported:

-
-
-buttony offset
-

Specifies the distance from the top of the item that the expand/collapse button -should be drawn. If offset is an empty string (the default) then the button -is centered vertically in the item. -The value may have any of the forms acceptable to Tk_GetPixels. -This option only has effect when the style is set in an item in the tree column.

-
-orient varName
-

This option specifies which orientation should be used -when laying out the elements associated with this style. -Must be either horizontal (the default) -or vertical or an abbreviation of one of these.

-
-
pathName style create name ?option value ...?
-

Creates a new style with the unique user-defined name name. -After name there may be any number of option-value -pairs, each of which sets one of the configuration options -for the style. See the style configure command for the possible -options. -The result of this command is the name of the new style (the same as the -name option).

-

This command also accepts the -statedomain option with a value of -either header or item to specify where this style will be -displayed.

-
pathName style delete ?style ...?
-

Deletes each of the named styles and returns an empty string. -If a style is deleted while it is still used to display -one or more items, -it is also removed from the style list of these items.

-
pathName style elements style ?elementList?
-

Specifies the elements which should be layed out by this style. -Each element of elementList must be the name of an element -created by the widget command element create. -Duplicate names in elementList are ignored. -An element which was specified in a former call of this command -for style but is not included in elementList, -will be deleted from the elements layed out by style.

-

Every element used by a style must have been created with the same value -for the -statedomain option.

-

If the elementList argument is not specified, a list is returned -containing the currently defined elements of style.

-
pathName style layout style element ?option? ?value? ?option value ...?
-

This command is similar to the configure widget command except -that it modifies options used by style for laying out element -instead of modifying options for the overall treectrl widget. -If no option is specified, the command returns a list with -option-value pairs describing -all of the available options for the layout. -If option is specified with no value, then the command returns -the value of the named option. -If one or more option-value pairs are specified, then the command -modifies the given option(s) to have the given value(s) for the layout; -in this case the command returns an empty string.

-

The options of a layout have effect on exactly the one element element -managed by style. -The following options are supported:

-
-
-detach boolean
-

Specifies whether the element should be positioned by itself, -i.e. independent from the other elements. The default is false.

-
-center flags
-

Flags is a string that contains zero or more of the characters -x or y. -x causes the element to be centered horizontally, -y causes the element to be centered vertically. -When more than one element has -center layout, all the elements between the -first and last with -center layout in the style's list of elements are centered -as a group. Consider the following when there is another element to the right -of MyElement:

-
-.t style layout MyStyle MyElement -expand we
-.t style layout MyStyle MyElement -center x
-
-

With the first call, MyElement will be centered only within the space that -is not occupied by the other element, so MyElement will appear off-center -towards the left of the style. With the second call, MyElement will be -centered within the style so long as it doesn't overlap the other element.

-
-draw boolean
-

This is a per-state option that determines whether -an element should be drawn. If the value of the option evaluates to false for a given -item state, then the element is not drawn, although it still consumes space in -the layout.

-
-expand flags
-

This option allows the external padding around the element -to increase when a style has more screen space than it needs. -Flags is a string that contains zero or more of the characters -n, s, w or e. -Each letter refers to the padding on the top, bottom, -left, or right that should be allowed to increase. -This option is typically used to justify an element. -The default is an empty string.

-
-iexpand flags
-

This option allows the internal padding of the element -and the display area of the element to increase -when a style has more screen space than it needs. -Flags is a string that contains zero or more of the characters -x, y, n, s, w or e. -For n, s, w and e, each letter refers to the -padding on the top, bottom, left, or right that should be allowed to increase. -For x and y, each letter refers to the horizontal and -vertical screen space the element can display itself in (i.e., the space -between the padding). Note that if the --union option is specified for this element, then the x and -y flags have no effect, since the size of an element with --union layout is determined by the elements it surrounds. -The default is an empty string.

-
-indent boolean
-

For item styles, this option specifies whether the element should be -positioned to the right of the button/line area in the tree column. When -false, the element is displayed beneath the buttons and lines in the tree -column. This option is ignored unless the -detach option is true.

-

For header styles, this option specifies whether the element should be -positioned to the right of the -canvaspadx padding. This option is ignored -unless the -detach option is true or the -union option is -specified.

-

The default is true.

-
-ipadx amount
-
-
-ipady amount
-

Amount specifies how much internal padding to leave on the left and -right (for -ipadx) or top and bottom (for -ipady) sides of -the element. Amount may be a list of two values to specify padding for -the two sides separately. The default value is 0. This option is typically -used with the -union layout option, to create space around the -enclosed elements.

-
-minheight pixels
-
-
-height pixels
-
-
-maxheight pixels
-

Specifies the minimum, fixed, and maximum height of the display area of the element. -The default is unspecified.

-
-minwidth pixels
-
-
-width pixels
-
-
-maxwidth pixels
-

Specifies the minimum, fixed, and maximum width of the display area of the element. -The default is unspecified.

-
-padx amount
-
-
-pady amount
-

Amount specifies how much external padding to leave on the left and -right (for -padx) or top and bottom (for -pady) sides of the -element. Amount may be a list of two values to specify padding for the -two sides separately. The default value is 0.

-
-squeeze flags
-

This option allows the display area of an element to decrease when a -style has less space than it needs. -Flags is a string that contains zero or more of the characters -x or y. -x allows display area to decrease horizontally, -y allows display area to decrease vertically. -This option is typically used for text elements and will cause -the text element to display an ellipsis (...) and/or wrap lines. -The default is an empty string.

-
-sticky flags
-

This option controls how the actual display information (image, text, etc) -of an element is positioned (or stretched) within its display area. -Flags is a string that contains zero or more of the characters n, -s, w or e. Each letter refers to the top, bottom, left -or right side of the display area that the display information should "stick" to. -The default is nswe.

-
-union elementList
-

Specifies a list of other elements which this element will surround. -The size of an element with -union layout is determined by the -size and position of the elements in elementList. -The -ipadx and -ipady options in this case refer to the -distance of the edges of the display area of this element from those elements -it surrounds. This option is typically used to display a selection rectangle -around a piece of text. If none of the elements in elementList are -visible, then the element is not displayed.

-
-visible boolean
-

This is a per-state option that controls visibility -of an element. If the value of the option evaluates to false for a given -item state, then the element is not displayed and consumes no space in the layout.

-
-
pathName style names
-

Returns a list containing the names of all existing styles.

-
-
pathName theme option ?arg ...?
-

This command is used to interact with the platform-specific theme. -The exact behavior of the command depends on the option argument -that follows the theme argument. -The following forms of the command are supported:

-
-
pathName theme platform
-

Returns the API used to draw themed parts of the treectrl. On Mac OS X the result -is always aqua. On MS Windows the result is visualstyles if the -uxtheme.dll was loaded and visual themes are in use, otherwise X11 is -returned to indicate the Tk Xlib calls are drawing the themed parts. -On Unix systems the result is gtk if the Gtk+ version of treectrl -was built, otherwise X11 is returned.

-
pathName theme setwindowtheme appname
-

The command is available on MS Windows only. If appname is "Explorer" then -the item buttons look like those in the Explorer file browser (disclosure -triangles under Windows Vista/7). If appname is an empty string then -the buttons revert to their default appearance according to the system's -current visual style.

-
-
pathName toggle ?-recurse? ?itemDesc ...?
-

Use item toggle instead.

-
pathName xview ?args?
-

This command is used to query and change the horizontal position of the -information displayed in the treectrl's window. -It can take any of the following forms:

-
-
pathName xview
-

Returns a list containing two elements. -Each element is a real fraction between 0 and 1; together they describe -the horizontal span that is visible in the window. -For example, if the first element is .2 and the second element is .6, -20% of the tree's area -is off-screen to the left, the middle 40% is visible -in the window, and 40% of the tree is off-screen to the right. -These are the same values passed to scrollbars via the -xscrollcommand -option.

-
pathName xview moveto fraction
-

Adjusts the view in the window so that fraction of the -total width of the tree is off-screen to the left. -Fraction must be a fraction between 0 and 1. -A <Scroll-x> event is generated.

-
pathName xview scroll number what
-

This command shifts the view in the window left or right according to -number and what. -Number must be an integer. -What must be either units or pages or an abbreviation -of one of these. -If what is units, the view adjusts left or right in units -determined by the -xscrollincrement option (which may be zero, -see the description of that option). -If what is pages then the view -adjusts in units of nine-tenths the window's width. -If number is negative then information farther to the left -becomes visible; if it is positive then information farther to the right -becomes visible. -A <Scroll-x> event is generated.

-
-
pathName yview ?args?
-

This command is used to query and change the vertical position of the -information displayed in the treectrl's window. -It can take any of the following forms:

-
-
pathName yview
-

Returns a list containing two elements. -Each element is a real fraction between 0 and 1; together they describe -the vertical span that is visible in the window. -For example, if the first element is .6 and the second element is 1.0, -the lowest 40% of the tree's area is visible in the window. -These are the same values passed to scrollbars via the -yscrollcommand -option.

-
pathName yview moveto fraction
-

Adjusts the view in the window so that fraction of the tree's -area is off-screen to the top. -Fraction is a fraction between 0 and 1. -A <Scroll-y> event is generated.

-
pathName yview scroll number what
-

This command adjusts the view in the window up or down according to -number and what. -Number must be an integer. -What must be either units or pages. -If what is units, the view adjusts up or down in units -of the -yscrollincrement option (which may be zero, -see the description of that option). -If what is pages then -the view adjusts in units of nine-tenths the window's height. -If number is negative then higher information becomes -visible; if it is positive then lower information -becomes visible. -A <Scroll-y> event is generated.

-
-
-
-

HEADERS

-

A treectrl widget can display zero or more rows of column headers. -When a treectrl widget is created, a single row of column headers (aka a header-row) -is created as well; this top header-row cannot be deleted. Additional header-rows -can be created with the header create command and deleted with header delete.

-

There are no commands for changing the order of header-rows; they are displayed -from top to bottom in the order they were created.

-

Drag-and-drop reordering of column headers is supported within a widget. -To control column header drag-and-drop, use the header dragconfigure command.

-

Header-rows in a treectrl may be specified in a number of ways. -See HEADER DESCRIPTION below.

-

The appearance of individual column headers within a header-row may be -customized in two different ways:

-
    -
  1. By configuring various column header options with the header configure command

  2. -
  3. By assigning a style to a column header with the header style command.

  4. -
-

When one of the options below is specified as per-state, -the state names are those described in STATES for headers only, -i.e. do not use item state names.

-

The following options are supported for each individual column header:

-
-
-arrow direction
-

Indicates whether or not a sort arrow should be drawn in the column header. -Direction must have one of the values -none (the default), up, or down.

-
-arrowbitmap bitmap
-

Specifies as a per-state option the name of a bitmap to -use to draw the arrow if this column's -arrow option is not none.

-
-arrowgravity direction
-

Indicates onto which side the sort arrow should be packed, -if there is more space available for drawing the arrow then needed. -direction must be either left (the default) or right.

-
-arrowimage image
-

Specifies as a per-state option the name of an image to -use to draw the sort arrow if this column's -arrow option is not none. -If an image is specified for a certain state, it overrides the -arrowbitmap option.

-
-arrowpadx amount
-

Amount specifies how much padding to -leave on the left and right of the sort arrow. -Amount may be a list -of two values to specify padding for left and right separately; -it defaults to 6.

-
-arrowpady amount
-

Amount specifies how much padding to -leave on the top and bottom of the sort arrow. -Amount may be a list -of two values to specify padding for top and bottom separately; -it defaults to 0.

-
-arrowside side
-

Indicates on which side of the bitmap/image/text the sort arrow should be drawn. -Side must be either left or right (the default).

-
-bitmap bitmap
-

Specifies the name of a bitmap to display to the left of the column title.

-
-background color
-

Specifies as a per-state option the color to -use for the background of the column header.

-
-borderwidth size
-

Specifies a non-negative value indicating the width -of the 3-D border to draw around the outside of the column header - (if such a border is being drawn; the -relief column option -determines this). -The value may have any of the forms acceptable to Tk_GetPixels.

-
-button boolean
-

Indicates whether or not the column header should be treated like a pushbutton. -When this option is true, the default bindings track <Button-1> events -in the header and generate a <Header-invoke> event when a <ButtonRelease-1> -event occurs in the header. See DYNAMIC EVENTS.

-
-font fontName
-

Specifies the font to use for displaying the column title inside the column header. -When the value of this option is unspecified, the font specified by the widget -option -headerfont is used.

-
-image image
-

Specifies the name of an image to display to the left of the column title. -This option overrides the -bitmap column option.

-
-imagepadx amount
-

Amount specifies how much padding to -leave on the left and right of the image (or bitmap). -Amount may be a list -of two values to specify padding for left and right separately; -it defaults to 6.

-
-imagepady amount
-

Amount specifies how much padding to -leave on the top and bottom of the image (or bitmap). -Amount may be a list -of two values to specify padding for top and bottom separately; -it defaults to 0.

-
-justify justification
-

This option determines how the image and text in the column header are -positioned. -Must be one of left (the default), center, or right.

-
-state state
-

Specifies one of three states for the column header: normal, active, -or pressed. The active state is used when the mouse is over the header. -The pressed state is used when the mouse button is pressed in the header.

-

Changing the value of this option also affects the current set of -header states for the column header, which may -affect both the per-state options mentioned -here (such as -arrowimage) as well as the elements in any style that -may be assigned to the column header.

-
-text text
-

Specifies a text string to be displayed as the column title.

-
-textcolor color
-

Specifies as a per-state option the color to -display the column title with. When the value of this option is unspecified, -the title will be drawn according to the system theme color, if any, otherwise -the widget option -headerforeground is used. The default is unspecified.

-
-textlines count
-

Specifies the maximum number of lines of text to display in the column title. -If this value is zero, the number of lines displayed is determined by any newline -characters and the effects of wrapping when the column width is less than -needed. The default is 1. Note: Under OSX/Aqua this value is always set to 1 when the -treectrl's -usetheme option is true, because the Appearance Manager -uses a fixed height for the column header; there is only room for a single line -of text.

-
-textpadx amount
-

Amount specifies how much padding to -leave on the left and right of the text. -Amount may be a list -of two values to specify padding for left and right separately; -it defaults to 6.

-
-textpady amount
-

Amount specifies how much padding to -leave on the top and bottom of the text. -Amount may be a list -of two values to specify padding for top and bottom separately; -it defaults to 0.

-
-
-

HEADER DESCRIPTION

-

Many of the commands for a treectrl take as an argument a description of which -header-rows to operate on. A header description is a properly-formed -tcl list of keywords and arguments. The first word of a header description must -be one of the following:

-
-
id
-

Specifies a unique header-row identifier, where id should be -the return value of a prior call of the header create widget command, -or 0 to specify the ever-present top header-row.

-
QUALIFIERS
-

Specifies a list of qualifiers. This gives the same result as all followed -by QUALIFIERS; i.e., every header-row is tested for a match.

-
tagExpr QUALIFIERS
-

TagExpr is a tag expression (see ITEM AND COLUMN TAGS) against -which every header-row's tags are tested for a match. -You may run into trouble if tagExpr looks like a header-row id -or other keyword; also, tagExpr must look like a single list element -since header-row descriptions are properly-formed lists. To be safe you may want to -use the tag qualifier followed by tagExpr.

-
-.t header dragconfigure {tag -funky} -draw yes
-
-
-
all QUALIFIERS
-

Matches every header-row which satisfies QUALIFIERS.

-
first QUALIFIERS
-

Indicates the top header-row of the treectrl, or the first header-row starting -from the top that satisfies QUALIFIERS.

-
end QUALIFIERS
-
-
last QUALIFIERS
-

Indicates the last header-row which satisfies QUALIFIERS.

-
-

The word QUALIFIERS above represents a series of zero or more of the -following terms that changes which header-row is chosen:

-
-
tag tagExpr
-

TagExpr is a tag expression (see ITEM AND COLUMN TAGS) against which -a header-row's tags are tested for a match.

-
visible
-

When this qualifier is given, only header-rows that are displayed are matched. -A header-row is displayed only if both the -showheader widget option and --visible header-row option are true. Also, if only the tail column is -visible, then header-rows are not displayed.

-
!visible
-

When this qualifier is given, only header-rows that are *not* displayed are -matched.

-
-
-

COLUMNS

-

A treectrl widget is capable of displaying multiple columns next to each -other. -An item can be considered as a row, which reaches over all columns.

-

Columns in a treectrl may be specified in a number of ways. -See COLUMN DESCRIPTION below.

-

There is always one special column, the tail column, which fills -all space to the right of the last ordinary column. -This column has no unique ID; -it can only be specified by the keyword tail.

-

For compatibility with older versions of treectrl (which did not support more -than one row of column headers) any of the configuration options mentioned in -the HEADERS section, such as -arrow, -text, etc, -may be passed to the top header-row through the column configure -command and queried with the column cget command.

-

The following options are supported for columns:

-
-
-expand boolean
-

Indicates whether or not any extra horizontal space should be distributed -to this column. -This option has no effect if the -width option is set.

-
-gridleftcolor color
-
-
-gridrightcolor color
-

Specifies the color of the lines drawn down the left and right edges of the -column. These so-called "grid lines" are drawn over the elements of each item -style in the column and down into the whitespace region below any items. -The default value for each option is an empty string meaning no lines are drawn.

-
-itembackground colorList
-

Specifies a list of zero or more colors, which are used as -alternating background colors for items in this column. -See also the -backgroundmode widget option for more on this.

-
-itemjustify justification
-

This option determines how the item styles in this column are aligned -horizontally. -Must be one of left, center, or right. The default -value is an empty string (for compatibility with older versions), in which -case the column option -justify is used to align item styles in this -column.

-
-itemstyle style
-

Style is the name of a style that should be set in this column -for newly-created items.

-
-justify justification
-

This option determines how item styles in this column are aligned horizontally -unless overriden by the -itemjustify option for this column. -Must be one of left (the default), center, or right.

-

For compatibility with older versions of treectrl (which did not allow multiple -rows of column headers), changing the value of this option also changes the --justify option of the column header in the top header-row.

-
-lock lock
-

This option allows a column to stick to the left or right edge of the window. -A locked column scrolls vertically but not horizontally. -Must be one of none (the default), left, or right.

-
-maxwidth size
-

Specifies the maximum size, in screen units, that will be permitted for this column. -If size is an empty string, then there is no limit on the maximum size of the column. -This option has no effect if the -width option is set.

-
-minwidth size
-

Specifies the minimum size, in screen units, that will be permitted for this column. -If size is an empty string, then the minimum size of the column is zero. -This option has no effect if the -width option is set.

-
-resize boolean
-

Specifies a boolean value that indicates whether the user should be allowed to -resize the column by dragging the edge of the column's header. Default is true.

-
-squeeze boolean
-

Specifies a boolean value that indicates whether or not the column should -shrink when the content width of the treectrl is less than the total needed width -of all visible columns. Defaults to false, which means the column will not get -smaller than its needed width. The column will not get smaller than the value -of its -minwidth option, if specified. This option has no effect if the --width option is set.

-
-stepwidth size
-

Deprecated. Use the treectrl's -itemwidthmultiple option instead.

-
-tags tagList
-

TagList is a list of tag names that can be used to identify the column. -See also the column tag command.

-
-uniform group
-

When a non-empty value is supplied, this option places the column in a -uniform group with other columns that have the same value for --uniform. The space for columns belonging to a uniform group is -allocated so that their sizes are always in strict proportion to their --weight values. -This option is based on the grid geometry manager.

-
-visible boolean
-

Indicates whether or not the column should be displayed.

-
-weight integer
-

Sets the relative weight for apportioning any extra space among columns. -A weight of zero (0) indicates the column will not deviate from its requested -size. A column whose weight is two will grow at twice the rate as a column of -weight one when extra space is allocated to columns. -This option is based on the grid geometry manager.

-
-width size
-

Specifies a fixed width for the column. If this value is an empty string, -then the column width is calculated as the maximum of: -a) the width requested by items; -b) the width requested by the column's header; -and c) the column's -minwidth option. -This calculated width is also affected by the -expand, --squeeze, -uniform and -weight options. In any case, -the calculated width will not be greater than the -maxwidth option, -if specified.

-
-widthhack boolean
-

Deprecated. Use the treectrl's -itemwidthequal option instead.

-
-
-

COLUMN DESCRIPTION

-

Many of the commands and options for a treectrl take as an argument a -description of which column to operate on. -See the EXAMPLES section for examples. -The initial part of a column description must begin with one of the following terms:

-
-
id
-

Specifies the unique column identifier, where id should be -the return value of a prior call of the column create widget command. -See also the -columnprefix option.

-
QUALIFIERS
-

Specifies a list of qualifiers. This gives the same result as all followed -by QUALIFIERS; i.e., every column is tested for a match.

-
tagExpr QUALIFIERS
-

TagExpr is a tag expression (see ITEM AND COLUMN TAGS) against which -every column's tags are tested for a match. -This keyword cannot be followed by any modifiers unless a single column is -matched. You may run into trouble if tagExpr looks like a column id -or other keyword; also, tagExpr must look like a single list element -since column descriptions are properly-formed lists. To be safe you may want to -use the tag qualifier followed by tagExpr.

-
all QUALIFIERS
-

Indicates every column, including the tail column if the command allows it, -which match QUALIFIERS.

-
first QUALIFIERS
-

Indicates the leftmost column of the treectrl which matches QUALIFIERS.

-
end QUALIFIERS
-
-
last QUALIFIERS
-

Indicates the rightmost column of the treectrl (but not the tail column) -which matches QUALIFIERS.

-
list columnDescs
-

ColumnDescs is a list (a single argument, i.e. "list {a b c}" not "list a b c") -of other column descriptions. -This keyword cannot be followed by any modifiers unless a single column is matched.

-
order n QUALIFIERS
-

Indicates the nth column in the list of columns as returned by the -column order command.

-
range first last QUALIFIERS
-

First and last specify a range of columns. -This keyword cannot be followed by any modifiers unless a single column is specified.

-
tail
-

Indicates the ever-present tail column of the treectrl.

-
tree
-

Indicates the column specified by the -treecolumn option of the treectrl.

-
-

The initial part of the column description (matching any of the values above) -may be followed by one or more modifiers. -A modifier changes the column used relative to -the description up to this point. -It may be specified in any of the following forms:

-
-
next QUALIFIERS
-

Use the column to the right matching QUALIFIERS.

-
prev QUALIFIERS
-

Use the column to the left matching QUALIFIERS.

-
span N QUALIFIERS
-

Starting with (and counting) the single column specified by the column -description so far, walk at most N columns rightwards, stopping if any -of the following conditions is met:

-
    -
  1. A column does not match QUALIFIERS.

  2. -
  3. A column's -lock option does not match the first column's -lock option.

  4. -
-
-

The word QUALIFIERS above represents a sequence of zero or more of the -following terms that changes which column is chosen:

-
-
tag tagExpr
-

TagExpr is a tag expression (see ITEM AND COLUMN TAGS) against which -a column's tags are tested for a match.

-
!tail
-

When this qualifier is given, the tail column is not matched.

-
visible
-

When this qualifier is given, only columns whose -visible option is -TRUE are considered.

-
!visible
-

When this qualifier is given, only columns whose -visible option is -FALSE are considered.

-
-
-

STATES

-

For every column header and every item a set of boolean states is managed. -These states play an integral role in the appearance of headers and items; -that role is described in detail in PER-STATE OPTIONS. -The set of states available to headers is separate from the set of states -available to items.

-
-
HEADER STATES
-

The following states are predefined for every column header:

-
-
active
-
-
normal
-
-
pressed
-

These states mirror the value of a column header's configuration option --state. Exactly one of these states is set at any time in each -column header.

-
down
-
-
up
-

These states mirror the value of a column header's configuration option --arrow. If the -arrow option is none, then neither -of these states is set.

-
background
-

This state is set for every header-row if the toplevel window containing the -treectrl is not the foreground active window. This state cannot be modified -by means of a widget command, but is maintained in reaction to the <Activate> -and <Deactivate> windowing system events.

-
focus
-

This state is set for every header-row if the treectrl widget currently has the -focus. It cannot be modified by means of a widget command, but is maintained -in reaction to the <FocusIn> and <FocusOut> windowing system events.

-
-
ITEM STATES
-

The following states are predefined for every item:

-
-
active
-

At all times this state is set for exactly one item. The active item is -used with keyboard navigation. -When the treectrl widget is created or when the active item is deleted, -the root item will become the active item. -This state can be modified by means of the widget command activate.

-
enabled
-

This state is set for every item when it is created. -Disabled items cannot be selected and are ignored by the default bindings -when navigating via the keyboard. -This state can be modified by means of the widget command item enabled.

-
focus
-

This state is set for every item -if the treectrl widget currently has the focus. -It cannot be modified by means of a widget command, -but is maintained in reaction to the <FocusIn> and <FocusOut> events.

-
open
-

If this state is switched on, -the descendants of the item are displayed -- the item is expanded. -If this state is switched off, -the descendants of the item are not displayed -- the item is collapsed. -For a new item this state is switched on by default. -This state can be modified by means of the widget commands -item expand, item collapse, or item toggle.

-
selected
-

This state is set for every item included in the selection. -It can be modified by means of the widget command selection.

-
-

By means of the state define widget command, -up to 27 additional states can be defined.

-
-
-

PER-STATE OPTIONS

-

The visual appearance of an item can change depending on the state the item -is in, such as being the active item, being included in the selection, being -collapsed, or some combination of those or other states. When a configuration -option is described as per-state, it means the option describes a -value which varies depending on the state of the item. If a per-state option is -specified as a single value, the value is used for all states. Otherwise -the per-state option must be specified as an even-numbered list. For example, -to use the font "Times 12 bold" in a text element regardless of the -item state you can write:

-
-$T element configure MyTextElement -font {{Times 12 bold}}
-
-

However, to use a different font when the item is selected you could write:

-
-$T element configure MyTextElement -font {{Courier 10} selected {Times 12 bold} {}}
-
-

In the example above, the -font option reads "value stateList value stateList". -If stateList is an empty list, the preceding value is used regardless -of the item state. A non-empty stateList specifies a list of states which must be -set for the item in order to use the preceding value. Each stateList can also -include state names preceded by a ! sign, indicating the state must *not* be -set for the item. For example:

-
-$T element configure MyRectElement -fill {blue {selected focus} gray {selected !focus}}
-
-

In the example above, the rect element is filled with blue when the treectrl -has the focus and the item is selected. If the treectrl does not have the focus, the -example specifies that gray should be used for selected items. Also note that if the -item is not selected, no color is specified for the -fill option.

-

Each value-stateList pair is checked in order from left to right. The value -associated with the first stateList that matches the current item state is -used. So stateLists should be listed from most-specific to least-specific.

-
-$T element configure MyRectElement -fill {gray {selected} blue {selected focus}}
-
-

Written this way, gray will always be used for selected items since -it appears first, and blue will never be used for selected items regardless -of the focus.

-

A value followed by an empty stateList should always be last since it will be -chosen regardless of the item's state.

-
-

ELEMENTS AND STYLES

-

Elements and styles are the core visual building blocks that -determine the appearance of items (and optionally column headers). -An element can be of type bitmap, border, header, image, -rect, text or window. One or more elements can be -assigned to a style which manages the layout of those elements. It may be -helpful to think of an element as a Tk widget and a style as a Tk geometry -manager such as grid, pack or place.

-

When an element is created by the element create command, that element is -referred to as a master element. Similarly, a style that is created by -style create is called a master style. When a master style is -assigned to a column of an item by the item style set command, a new -instance style is allocated -which refers back to the master style and its master elements. In this way, a -single master style may be shared by multiple columns of multiple items. If a -master element or master style is modified, those changes affect all the items -whose instance styles and elements refer to those masters.

-

Although you probably want the font and selection-rectangle colors to be shared by all items, -you most likely don't want the text to be the same for every column of every -item. The item element configure command can be used to override a -master element's configuration options for a specific column of an item. When you call -item element configure (or item text or item image), a -new instance element is allocated, if one wasn't already, and that instance -element's options will override the master element's.

-

All of the element configuration options described below are unspecified -by default, meaning that no value whatsoever has been given to the option. -It may seem strange to you that a boolean option would be unspecified -instead of simply "true" or "false". The reason for this is that when an -instance element used by an item has no value specified for an option, that -instance element refers to the master element for the value of that option. -This allows items which are displaying a certain element to be redisplayed -when the master element's options change. The benefits of this are that you -don't need to configure the font or text color for every item in a treectrl -individually, saving CPU cycles and memory.

-

You may be thinking that to change the color of a selection rectangle you -would call item element configure when an item was selected, but that -is not usually the case. It would be wasteful to allocate a new instance -element for a selection rectangle just because an item became selected. The -solution is to allow the appearance of the selection rectangle master element -to change based on the selected state of the item. This is described in -PER-STATE OPTIONS.

-

For each element type there is a section below describing the options -which can modify an element of that type.

-
-

BITMAP ELEMENT

-

An element of type bitmap can be used to display a bitmap in an item. -The following options are supported for bitmap elements:

-
-
-background color
-

Specifies as a per-state option -the color to use for each of the bitmap's '0' valued pixels. -If the value for a certain state is an empty string (the default), -the bitmap is drawn transparent.

-
-bitmap bitmap
-

Specifies as a per-state option -the bitmap to display in the element.

-
-draw boolean
-

Deprecated; use the style layout option -draw instead. -Specifies as a per-state option -whether to draw the element. If the value for a certain state is an empty -string (the default), it is treated as true and the element will be drawn.

-
-foreground color
-

Specifies as a per-state option -the color to use for each of the bitmap's '1' valued pixels. -If the value for a certain state is an empty string (the default), -the bitmap's foreground color is black.

-
-
-

BORDER ELEMENT

-

An element of type border can be used to display a 3D border in an item. -The following options are supported for border elements:

-
-
-background color
-

Specifies as a per-state option -the color to use for the background of the border. -If the value for a certain state is an empty string (the default), the -element will not be drawn.

-
-draw boolean
-

Deprecated; use the style layout option -draw instead. -Specifies as a per-state option -whether to draw the element. If the value for a certain state is an empty -string (the default), it is treated as true and the element will be drawn.

-
-filled boolean
-

Specifies whether the interior of the border should be filled with -the background color. If this option is unspecified (the default), -it it treated as false which means that only the edges of the -border will be drawn.

-
-height size
-

Specifies the height of the border. If this value is unspecified -(the default), the border will be exactly as tall as its display area as -determined by the style layout options.

-
-relief relief
-

Specifies as a per-state option the -relief of the border. If the value for a certain state is an empty string -(the default), it is treated as flat. -For acceptable values see the description of the --relief option in the options manual page.

-
-thickness thickness
-

Specifies the thickness of the edges of the border.

-
-width size
-

Specifies the width of the border. If this value is unspecified -(the default), the border will be exactly as wide as its display area as -determined by the style layout options.

-
-
-

HEADER ELEMENT

-

An element of type header can be used to display a themed (or -non-themed) column header background and sort arrow. Header elements are -best used surrounding other elements via the style layout option -union, -so that the sort arrow can be displayed correctly.

-

Some of the options for this type of element get their default values from -the header state flags that are set in the column header in which -the element is displayed. In particular, the -arrow option gets its -default value by checking the up and down state flags, and -the -state option gets its default value by checking the active, -normal, and pressed state flags. If elements of this type are -displayed in an item instead of a column header, then this behavior isn't used -since those state flags aren't meaningful for items.

-

The following options are supported for header elements:

-
-
-arrow direction
-

Indicates whether or not a sort arrow should be drawn. Direction must -have one of the values none, up, or down. If -unspecified, the value defaults to none (but see the note above -regarding header states).

-
-arrowbitmap bitmap
-

Specifies as a per-state option the name of a -bitmap to use to draw the sort arrow if this element's -arrow option is not -none. -This option is ignored when drawing themed headers on Mac OS X.

-
-arrowgravity direction
-

Indicates onto which side the sort arrow should be packed, -if there is more space available for drawing the arrow than needed. -Direction must be either left or right. If unspecified, -the value defaults to left. -This option is ignored when drawing themed headers on Mac OS X.

-
-arrowimage image
-

Specifies as a per-state option the name of an -image to use to draw the sort arrow if this element's -arrow option is not -none. If an image is specified for a certain state, it overrides the --arrowbitmap option. -This option is ignored when drawing themed headers on Mac OS X.

-
-arrowpadx amount
-

Amount specifies how much padding to leave on the left and right of the -sort arrow. Amount may be a list of two values to specify padding for -the left and right separately. If unspecified, the value defaults to 6. -Padding to the right of the sort arrow is ignored when drawing themed headers -on Mac OS X.

-
-arrowpady amount
-

Amount specifies how much padding to leave on the top and bottom of the -sort arrow. Amount may be a list of two values to specify padding for -the top and bottom separately. If unspecified, the value defaults to 0. -This option is ignored when drawing themed headers on Mac OS X.

-
-arrowside side
-

Indicates on which side of the element the sort arrow should be drawn. -Side must be either left or right. If unspecified, the -value defaults to right.

-
-background color
-

Specifies as a per-state option the color to -use for the non-themed background and 3D border. If unspecified, the value -defaults to either the Tk button widget's -background or -activebackground -color.

-
-borderwidth size
-

Specifies a non-negative value indicating the width of the non-themed 3D -border to draw around the inner edges of the element (if such a border is being -drawn; the -relief option determines this). -The value may have any of the forms acceptable to Tk_GetPixels. -If unspecified, the value defaults to 2.

-
-state state
-

Specifies one of three states for the element: normal, active, -or pressed. The active state is used when the mouse is over the header. -The pressed state is used when the mouse button is pressed in the header. -If unspecified, the value defaults to normal (but see the note above -regarding header states).

-
-
-

IMAGE ELEMENT

-

An element of type image can be used to display an image in an item. -The following options are supported for image elements:

-
-
-draw boolean
-

Deprecated; use the style layout option -draw instead. -Specifies as a per-state option -whether to draw the element. If the value for a certain state is an empty -string (the default), it is treated as true and the element will be drawn.

-
-height size
-

Specifies the requested height of the display area for this element. -If unspecified (the default), the element requests a height equal to the -height of the image, or zero if there is no image.

-
-image image
-

Specifies as a per-state option -the image to display in the element.

-
-tiled boolean
-

Specifies a boolean indicating whether or not the image should be tiled -horizontally and vertically within the display area for the element. -The default is false.

-
-width size
-

Specifies the requested width of the display area for this element. -If unspecified (the default), the element requests a width equal to the -width of the image, or zero if there is no image.

-
-
-

RECTANGLE ELEMENT

-

An element of type rect can be used to display a rectangle in an item. -The following options are supported for rectangle elements:

-
-
-draw boolean
-

Deprecated; use the style layout option -draw instead. -Specifies as a per-state option -whether to draw the element. If the value for a certain state is an empty -string (the default), it is treated as true and the element will be drawn.

-
-fill color
-

Specifies as a per-state option the color to -be used to fill the rectangle's area. -If the color for a certain state is an empty string (the default), then -the rectangle will not be filled (but the outline may still be drawn).

-
-height size
-

Specifies the height of the rectangle. If this value is unspecified -(the default), the rectangle will be exactly as tall as its display area as -determined by the style layout options.

-
-open open
-

Specifies as a per-state option -which edges of the rectangle should be left open. -This option may be used to get an incomplete drawing of the outline and rounded -corners, often to give the appearance of the rectangle extending over adjacent -columns or items. -Open is a string that contains zero or more of the characters -n, s, e or w. -Each letter refers to an edge (north, south, east, or west) on which the outline -and rounded corners will not be drawn. -The default is the empty string, which causes all rounded corners and the outline -to be drawn.

-
-outline color
-

Specifies as a per-state option the color to -be used to draw the outline of the rectangle. -If the color for a certain state is an empty string (the default), -then no outline is drawn for the rectangle.

-
-outlinewidth outlineWidth
-

Specifies the width of the outline to be drawn around the rectangle's region. -outlineWidth may be in any of the forms acceptable to Tk_GetPixels. -If this option is specified as an empty string (the default), then no outline -is drawn.

-
-rx radius
-
-
-ry radius
-

Specifies the x and y radius of each corner of a rounded rectangle -in any of the forms acceptable to Tk_GetPixels.

-
-showfocus boolean
-

Specifies a boolean value indicating whether -a "focus ring" should be drawn around the rectangle, -if the item containing the rectangle is the active item -and the treectrl widget currently has the focus. -If this option is specified as an empty string (the default), -then a focus rectangle is not drawn.

-
-width size
-

Specifies the width of the rectangle. If this value is unspecified -(the default), the rectangle will be exactly as wide as its display area as -determined by the style layout options.

-
-
-

TEXT ELEMENT

-

An element of type text can be used to display a text in an item. -The following options are supported for text elements:

-
-
-draw boolean
-

Deprecated; use the style layout option -draw instead. -Specifies as a per-state option -whether to draw the element. If the value for a certain state is an empty -string (the default), it is treated as true and the element will be drawn.

-
-data data
-

Specifies a value that together with the -datatype and -format -options will be displayed as text.

-
-datatype dataType
-

Specifies the type of information in the -data option. -Acceptable values are double, integer, long, -string, or time.

-
-fill color
-

Specifies as a per-state option the -foreground color to use when displaying text.

-

In items, if the color for a certain state is an empty string (the default), -then the text will be displayed using the color specified by the treectrl's --foreground option.

-

In headers, if the color for a certain state is an empty string, -then the text will be displayed using the system theme color on Gtk+; if that -color is not specified then the -headerforeground option is used.

-
-font font
-

Specifies as a per-state option the font to -use when displaying the text. -If the font for a certain state is an empty string, the text is displayed -using the font specified by the treectrl's -font option in items -or the -headerfont option in headers.

-
-format formatString
-

This option specifies the format string used to display the value of the -data option. -If -datatype is time, formatString should be -a valid format string for the Tcl clock command. -For all other -datatype values formatString should be -a valid format string for the Tcl format command. -If this value is unspecified the following defaults are used: -for -datatype double "%g", for -datatype integer "%d", for -datatype long "%ld", -for -datatype string "%s", and for -datatype time the default format string of the Tcl clock -command.

-
-justify how
-

Specifies how to justify the text when multiple lines are displayed. -How must be one of the values left, right, or center. -If this option is specified as an empty string (the default), left is used.

-
-lines lineCount
-

Specifies the maximum number of lines to display. -If more than lineCount lines would be displayed, the last line will -be truncated with an ellipsis at the right. -If this option is specified as zero or an empty string (the default), -there is no limit to the number of lines displayed.

-
-lmargin1 pixels
-

Pixels is a screen distance that specifies how much a line of text should be -indented. If a line of text wraps, this option only applies to the first line -on the display; the -lmargin2 option controls the indentation for -subsequent lines. -If this option is specified as zero or an empty string (the default), -then the line is not indented. -This option was based on the Tk Text widget tag option of the same name.

-
-lmargin2 pixels
-

Pixels is a screen distance that specifies how much a line of text should be -indented. If a line of text wraps, this option only applies to the second and -later display lines for a line of text. -If this option is specified as zero or an empty string (the default), -then the line is not indented. -This option was based on the Tk Text widget tag option of the same name.

-
-text string
-

String specifies a string to be displayed by the element. -String may contain newline characters in which case multiple lines of text -will be displayed. -If this option is specified, the -data, -datatype, --format, and -textvariable options are ignored.

-
-textvariable varName
-

Specifies the name of a variable. The value of the variable is a string -to be displayed by the element; if the variable value changes then the element -will automatically update itself to display the new value. -If this option is specified, the -data, -datatype, -and -format options are ignored.

-
-underline charIndex
-

Specifies the integer index of a character to underline. -0 corresponds to the first character. -If charIndex is unspecified (the default), less than zero or greater -than the index of the last displayed character, the underline is not drawn.

-
-width size
-

Specifies the maximum line length in any of the forms acceptable to Tk_GetPixels. -For text to wrap lines the value of the -width -option must be less than the needed width of the text, or the display area -for this element must be less than the needed width of the text. -For the display area to be less than the needed width of the text, -one of the style layout options -maxwidth, -width or --squeeze must be used.

-
-wrap mode
-

Mode specifies how to handle lines in the text that are longer than the -maximum line length. -Acceptable values are none, char or word. -If this option is unspecified (the default), word is used. -See the -width option for a description of how the maximum line -length is determined.

-
-
-

WINDOW ELEMENT

-

An element of type window can be used to display a Tk window in an item. -The following options are supported for window elements:

-
-
-clip boolean
-

Specifies whether the associated Tk window is a borderless frame which should -be used to clip its child window so it doesn't overlap the header, borders, or -other items or columns. When this option is true, the treectrl manages the -geometry of both the -window widget and its first child widget; in -this case the -window widget (which should be a borderless frame) is -kept sized and positioned so that it is never out-of-bounds.

-
-destroy boolean
-

Specifies whether the associated Tk window should be destroyed when the -element is deleted. The element is deleted when the item containing the -element is deleted, when the column containing the element is deleted, -or when the style assigned to the item's column is changed. If this option -is unspecified (the default), it is treated as false and the Tk window -will not be destroyed.

-
-draw boolean
-

Deprecated; use the style layout option -draw instead. -Specifies as a per-state option -whether to draw the element. If the value for a certain state is an empty -string (the default), it is treated as true and the element will be drawn.

-
-window pathName
-

Specifies the window to associate with this element. The window specified by -pathName must either be a child of the treectrl widget or a child of -some ancestor of the treectrl widget. PathName may not refer to a -top-level window. This option cannot be specified by -the element create or element configure commands, only by the -item element configure command; i.e., the element must be associated -with a particular item.

-
-
-

ITEM DESCRIPTION

-

Many of the commands for a treectrl take as an argument a -description of which items to operate on. An item description is a properly-formed -tcl list of keywords and arguments. -The first word of an item description must be one of the following:

-
-
id
-

Specifies the unique item identifier, where id should be -the return value of a prior call of the item create widget command, -or 0 to specify the ever-present root item. See also the --itemprefix option.

-
QUALIFIERS
-

Specifies a list of qualifiers. This gives the same result as all followed -by QUALIFIERS; i.e., every item is tested for a match.

-
tagExpr QUALIFIERS
-

TagExpr is a tag expression (see ITEM AND COLUMN TAGS) against which -every item's tags are tested for a match. -This keyword cannot be followed by any modifiers unless a single item is -matched. You may run into trouble if tagExpr looks like an item id -or other keyword; also, tagExpr must look like a single list element -since item descriptions are properly-formed lists. To be safe you may want to -use the tag qualifier followed by tagExpr.

-
active
-

Indicates the item that is currently active, i.e. normally -the item specified as argument of the last successful activate -widget command, or the root item if no such call happened yet.

-
anchor
-

Indicates the anchor item of the selection, i.e. normally -the item specified as argument of the last successful selection anchor -widget command, or the root item if no such call happened yet.

-
all QUALIFIERS
-

Indicates every item including orphans which match QUALIFIERS. -This keyword cannot be followed by any modifiers unless a single item is matched.

-
first QUALIFIERS
-

Indicates the first item of the treectrl (the root item), -or the first item matching QUALIFIERS.

-
end QUALIFIERS
-
-
last QUALIFIERS
-

Indicates the last item which matches QUALIFIERS.

-
list itemDescs
-

ItemDescs is a list (a single argument, i.e. "list {a b c}" not "list a b c") -of other item descriptions. -This keyword cannot be followed by any modifiers unless a single item is matched.

-
nearest x y
-

Indicates the item nearest to the point given by x and y.

-
rnc row column
-

Indicates the item in the given row and column. -The row and column corresponds to -the on-screen arrangement of items as determined by the -orient and -wrap -options. -You can memorize rnc as an abbreviation of "row 'n' column".

-
range first last QUALIFIERS
-

First and last specify a range of items. -This keyword cannot be followed by any modifiers unless a single item is matched.

-
root
-

Indicates the root item of the treectrl.

-
-

The initial part of the item description (matching any of the values above) -may be followed by one or more modifiers. -A modifier changes the item used relative to -the description up to this point. -It may be specified in any of the following forms:

-
-
above
-

Use the item one row above in this column.

-
ancestors QUALIFIERS
-

Use the ancestors of the item (like item ancestors but QUALIFIERS -may change which ancestors match). -This keyword cannot be followed by any modifiers.

-
below
-

Use the item one row below in this column.

-
bottom
-

Use the item in the last row of this column.

-
child n QUALIFIERS
-

Use the nth child of the item.

-
children QUALIFIERS
-

Use the children of the item (like item children but QUALIFIERS -may change which children match). -This keyword cannot be followed by any modifiers.

-
descendants QUALIFIERS
-

Use the descendants of the item (like item descendants but QUALIFIERS -may change which descendants match). -This keyword cannot be followed by any modifiers.

-
firstchild QUALIFIERS
-

Use the first child of the item.

-
lastchild QUALIFIERS
-

Use the last child of the item.

-
left
-

Use the item one column to the left in the same row.

-
leftmost
-

Use the item of the first column in the same row.

-
next QUALIFIERS
-

Use the next item, which is the first item from the -following list: the first child, the next sibling or the next sibling of -the nearest ancestor which has one.

-
nextsibling QUALIFIERS
-

Use the next sibling of the item.

-
parent
-

Use the parent of the item.

-
prev QUALIFIERS
-

Use the last child of the previous sibling, -or the parent if there is no previous sibling.

-
prevsibling QUALIFIERS
-

Use the previous sibling of the item.

-
right
-

Use the item one column to the right in the same row.

-
rightmost
-

Use the item of the last column in the same row.

-
sibling n QUALIFIERS
-

Use the nth child of the item's parent.

-
top
-

Use the item in the first row of this column.

-
-

The word QUALIFIERS above represents a series of zero or more of the -following terms that changes which item is chosen:

-
-
depth depth
-

Matches items whose depth (as returned by the depth command) is equal to -depth.

-
state stateList
-

StateList is a list of item state names (static and dynamic, see STATES). -Only items that have the given states set (or unset if the '!' prefix is used) -are considered.

-
tag tagExpr
-

TagExpr is a tag expression (see ITEM AND COLUMN TAGS) against which -an item's tags are tested for a match.

-
visible
-

When this qualifier is given, only items that are displayed are considered.

-
!visible
-

When this qualifier is given, only items that are *not* displayed are considered.

-
-

To get the first item in the list that is enabled:

-
-$T item id "first state enabled"
-
-

To get the ancestors that are not open of the last item in the list:

-
-$T item id "last ancestors state !open"
-
-

To get the visible descendants of the root item:

-
-$T item id "root descendants visible"
-
-

To get the every hidden item with tag "a" or "b":

-
-$T item id "all !visible tag a||b"
-$T item id "!visible tag a||b"
-$T item id "tag a||b !visible"
-$T item id "a||b !visible"
-
-
-

EVENTS AND SCRIPT SUBSTITUTIONS

-

The script argument to notify bind is a Tcl script, which will be -evaluated whenever the given event is generated. Script will be executed -in the same interpreter that the notify bind command was executed in, -and it will run at global level (only global variables will be accessible). -If script contains any % characters, then the script will not be -evaluated directly. Instead, a new script will be generated by replacing each -%, and the character following it, with information from the current -event. Unlike the regular Tk bind mechanism, each event generated by -a treectrl widget has its own set of %-substitutions.

-

The following %-substitutions are valid for all static events:

-
-
%%
-

Replaced with a single %

-
%d
-

The detail name

-
%e
-

The event name

-
%P
-

The pattern, either <event> or <event-detail>

-
%W
-

The object argument to the notify bind command

-
%T
-

The treectrl widget which generated the event

-
%?
-

A list of the format {char value char value ...} for each -%-substitution character and the value it is replaced by

-
-

The following events may be generated by a treectrl widget:

-
-
<ActiveItem>
-

Generated whenever the active item changes.

-
-
%c
-

The current active item

-
%p
-

The previous active item

-
-
<Collapse-before>
-

Generated before an item is collapsed.

-
-
%I
-

The item id

-
-
<Collapse-after>
-

Generated after an item is collapsed.

-
-
%I
-

The item id

-
-
<Expand-before>
-

Generated before an item is expanded. This event is useful if you want to add -child items to the item just before the item is expanded.

-
-
%I
-

The item id

-
-
<Expand-after>
-

Generated after an item is expanded.

-
-
%I
-

The item id

-
-
<ItemDelete>
-

Generated when items are about to be deleted by the item delete command.

-
-
%i
-

List of items ids being deleted.

-
-
<ItemVisibility>
-

Generated when items become visible on screen and when items are no longer visible on screen. -This event is useful if you have a very large number of items and want to assign -styles only when items are actually going to be displayed.

-
-
%h
-

List of items ids which are no longer visible.

-
%v
-

List of items ids which are now visible.

-
-
<Scroll-x>
-

Generated whenever the view in the treectrl changes in such a way that a -horizontal scrollbar should be redisplayed.

-
-
%l
-

Same as the first fraction appended to -xscrollcommand. Think lower.

-
%u
-

Same as the second fraction appended to -xscrollcommand. Think upper.

-
-
<Scroll-y>
-

Generated whenever the view in the treectrl changes in such a way that a -vertical scrollbar should be redisplayed.

-
-
%l
-

Same as the first fraction appended to -yscrollcommand. Think lower.

-
%u
-

Same as the second fraction appended to -yscrollcommand. Think upper.

-
-
<Selection>
-

Generated whenever the selection changes. This event gives information about -how the selection changed.

-
-
%c
-

Same as the selection count widget command

-
%D
-

List of newly-deselected item ids

-
%S
-

List of newly-selected item ids

-
-
-
-

DYNAMIC EVENTS

-

In addition to the pre-defined static events such as <ActiveItem> -and <Selection>, new dynamic events can be created by using the -notify install command.

-

The library scripts provide an example -of using a dynamic event called <Header-invoke>, which is generated when -the mouse button is clicked and released over a column header.

-
-# Example application code
-treectrl .t
-.t notify install <Header-invoke>
-.t notify bind MyTag <Header-invoke> {
-	puts "column header %C clicked in header-row %H in treectrl %T"
-}
-# Library code in treectrl.tcl
-proc ::TreeCtrl::Release1 {w x y} {
-	...
-	$w notify generate <Header-invoke> [list H $Priv(header) C $Priv(column)] \
-		[list ::TreeCtrl::PercentsCmd $w]
-	...
-}
-
-

In the example above, a new treectrl widget is created and the <Header-invoke> -event is installed. A script is bound to the event with notify bind -which will print out the column ID, header ID and widget name to the console. -In a real application, any script bound to <Header-invoke> would be used to -sort the list based on the column header that was clicked.

-

Note there is no percentsCommand argument to notify install; -instead, the call to notify generate specifies the %-substitution -command. -The charMap argument to notify generate -provides a list of %-substitution characters and values which is used by -::TreeCtrl::PercentsCmd. In the example, any %C in any script bound to the -<Header-invoke> event would be replaced by the value of $Priv(column), and -%H would be replaced by $Priv(header). The library procedure -::TreeCtrl::PercentsCmd also supports the same common %-substitution -characters as the built-in static events, such as %T, %P, %? etc.

-

The following dynamic events may be generated by the library scripts:

-
-
<ColumnDrag-begin>
-

This event is generated just after the user begins dragging a column header. -At the time this event is generated, the header dragconfigure option --imagecolumn is set to the unique ID of the column being dragged, the --imageoffset option is set to the horizontal distance the mouse -pointer has moved, and the -imagespan option is set to the span of -the column header that was initially clicked.

-
<ColumnDrag-indicator>
-

This event is generated each time a new place to drop the dragged -column header is found. At the time this event is generated, the -header dragconfigure option -indicatorcolumn is set to the -unique ID of the column before or after which the dragged column will be -dropped, and the -indicatorspan option is set to the span of the -column header for this newly-chosen indicator column.

-
<ColumnDrag-receive>
-

This event is generated when the user has successfully dragged and dropped a -column header to a new position. The library scripts do not actually move the -dragged column. You must bind a script to this event to move the column. -See EXAMPLES.

-
<ColumnDrag-end>
-

This event is generated after the user finally releases the left mouse button -while dragging a column header. This event is generated after all the other -<ColumnDrag> events even when the column wasn't dragged to a new location -(i.e., even when no <ColumnDrag-receive> event was generated).

-
-
%H
-

The header-row that contains the column header.

-
%C
-

The column whose header is dragged within the header-row.

-
%b
-

The column to move the dragged column(s) before. Valid for -<ColumnDrag-receive> only.

-
-
<Drag-begin>
-
-
<Drag-receive>
-
-
<Drag-end>
-

Generated whenever the user drag-and-drops a file into a directory. This -event is generated by the filelist-bindings.tcl library code, which is not -used by default. See the "Explorer" demos.

-
-
%I
-

The item that the user dropped the dragged items on.

-
%l
-

(lowercase L) The list of dragged items.

-
-
<Edit-begin>
-
-
<Edit-accept>
-
-
<Edit-end>
-

The filelist-bindings.tcl code will display a text-editing window if the user -clicks on a selected file/folder name. See the "Explorer" demos.

-
-
%I
-

The item containing the edited text element.

-
%C
-

The column containing the edited text element.

-
%E
-

The name of the edited text element.

-
%t
-

The edited text.

-
-
<Header-invoke>
-

Generated whenever the user clicks and releases the left mouse button -in a column header if the column header's -button option is true. You can bind a -script to this event to sort the list.

-
-
%H
-

The header-row that contains the column header.

-
%C
-

The column whose header was clicked.

-
-
<Header-state>
-

Generated when the column header option -state is changed by the -library scripts during Motion and Button events.

-
-
%H
-

The header-row that displays the column header.

-
%C
-

The column within the header-row whose header option -state changed.

-
%s
-

The new value of the column header option -state.

-
-
-
-

DEFAULT BINDINGS

-

Tk automatically creates class bindings for treectrl widgets that give them -the following default behavior.

-
    -
  1. Clicking mouse button 1 over an item positions the active cursor -on the item, sets the input focus to this widget, -and resets the selection of the widget to this item, if it is not -already in the selection.

  2. -
  3. Clicking mouse button 1 with the Control key down will reposition the -active cursor and add the item to the selection -without ever removing any items from the selection.

  4. -
  5. If the mouse is dragged out of the widget -while button 1 is pressed, the treectrl will automatically scroll to -make more items visible (if there are more items off-screen on the side -where the mouse left the window).

  6. -
  7. The Left and Right keys move the active cursor one item to the left or right; -for an hierarchical tree with vertical orientation nothing will happen, -since it has no two items in the same row. -The selection is set to include only the active item. -If Left or Right is typed with the Shift key down, then the active -cursor moves and the selection is extended to include the new item.

  8. -
  9. The Up and Down keys move the active cursor one item up or down. -The selection is set to include only the active item. -If Up or Down is typed with the Shift key down, then the active -cursor moves and the selection is extended to include the new item.

  10. -
  11. The Next and Prior keys move the active cursor forward or backwards -by one screenful, without affecting the selection.

  12. -
  13. Control-Next and Control-Prior scroll the view right or left by one page -without moving the active cursor or affecting the selection. -Control-Left and Control-Right behave the same.

  14. -
  15. The Home and End keys scroll to the left or right end of the widget -without moving the active cursor or affecting the selection.

  16. -
  17. The Control-Home and Control-End keys scroll to the top or bottom -of the widget, they also activate and select the first or last item. -If also the Shift key is down, then the active -cursor moves and the selection is extended to include the new item.

  18. -
  19. The Space and Select keys set the selection to the active item.

  20. -
  21. Control-/ selects the entire contents of the widget.

  22. -
  23. Control-\\ clears any selection in the widget.

  24. -
  25. The + and - keys expand or collapse the active item, -the Return key toggles the active item.

  26. -
  27. The mousewheel scrolls the view of the widget four lines up or down -depending on the direction, the wheel was turned. -The active cursor or the selection is not affected.

  28. -
-
-

GRADIENTS

-

Color gradients are an easy way to give your lists a more modern appearance. -Since Tk provides no support for drawing gradients, the TkPath extension was -used as a guide when implementing gradients in TkTreeCtrl. The current -implementation has some limitations, however:

-
    -
  1. Only linear gradients are supported.

  2. -
  3. Gradients can only be painted left-to-right or top-to-bottom, not at -arbitrary angles.

  4. -
  5. Gradients look bad on low-color displays. Before using gradients, you should -check that the display's color depth is at least 15 or 16 by calling the -winfo depth command.

  6. -
  7. Gradients are fully opaque when XFillRectangle() is used to draw them (see below). -This means the opacity value of each color stop is ignored. Keep that -in mind if your application is cross-platform.

  8. -
  9. Rounded rectangles cannot be filled or outlined with a gradient when XFillRectangle() -is used to draw gradients (see below). Instead, the rounded rectangle is painted -with the gradient's first -stops color.

  10. -
-

Gradients may be used in the following places:

-
    -
  1. The -gridleftcolor and -gridrightcolor options of columns.

  2. -
  3. The -itembackground option of columns.

  4. -
  5. The -fill and -outline options of rect elements.

  6. -
  7. The -fill and -outline options of the marquee configure command.

  8. -
-

On Microsoft Windows, GDI+ is used where it is available (gdiplus.dll is -dynamically loaded at run-time). -On Mac OS X, CoreGraphics is used to draw gradients. -With the Gtk+ build of treectrl, libcairo is used to draw gradients. -When native gradient support is available, all the talk below -about -steps can safely be ignored.

-

When no native support for gradients is available, gradients -are drawn simply by filling sub-rectangles using XFillRectangle(). The number -of sub-rectangles drawn and number of colors that make up the displayed gradient -are controlled by the gradient's -steps and -stops options. -The number of sub-rectangles is equal to the length of the -stops -option multiplied by the value of the -steps option. For example:

-
-$T gradient create myGradient -stops {{0 white} {1 gray}} -steps 8
-
-

This gradient will be drawn with 2x8=16 sub-rectangles of color. -The higher the -steps value, the smoother the color transitions -will be, and the slower the gradient will be to draw. -For the best appearance, make the number of sub-rectangles drawn less than or equal -to the height or width of the gradient being drawn. So if you have a rect element -18 pixels tall, use a vertical gradient that has steps X stops=18. Avoid using -gradients with steps X stops greater than the height or width of the rectangle -being drawn, because then colors will overlap.

-
-

GRADIENT COORDINATES

-

By default, a gradient brush is exactly the same size as whatever rectangle -is being painted. For example, if a column's -itembackground option -specifies a gradient name, then the background of an item is painted with all -the colors of the gradient. So a vertical gradient from blue to green will -start blue at the top and end with green at the bottom of every item.

-

By specifying any of the -bottom, -left, -right or --top gradient options the size of the gradient brush does not need -to match that of the rectangle being painted. These options can be used to -make a gradient appear to span across the entire width or height of the treectrl -window, or across the entire canvas, for example.

-

There is no point specifying -left or -right if the gradient -is vertical, since the gradient's colors are constant horizontally, so changing -the horizontal size of the brush won't change the appearance of the gradient. -The same reasoning applies for the -top and -bottom options -for a horizontal gradient.

-
-package require treectrl
-set T [treectrl .t -itemheight 20 -showheader no]
-$T gradient create G1 -orient vertical -top {0.0 canvas} -bottom {1.0 canvas} \
-	-stops {{0.0 blue} {0.5 green} {1.0 red}} -steps 25
-$T column create -expand yes -itembackground G1
-pack $T -expand yes -fill both
-
-
-

EXAMPLES

-

Get the unique identifier for the leftmost visible column:

-
-set id [$T column index "first visible"]
-
-

Delete the leftmost column:

-
-$T column delete "order 0"
-
-

Take the visible column that is to the left of the last column, and move that -column in front of the tail column:

-
-$T column move "last prev visible" tail
-
-

Get the unique identifier for the first visible item:

-
-set id [$T item index "first visible"]
-
-

Delete the parent of the item that is under the point x,y:

-
-$T item delete "nearest $x $y parent"
-
-

Add the 10th child of the second child of the root item to the selection:

-
-$T selection add "root firstchild nextsibling child 10"
-
-

Move a column that the user drag-and-dropped:

-
-$T header dragconfigure -enable yes
-$T notify install <ColumnDrag-receive>
-$T notify bind MyTag <ColumnDrag-receive> {
-	%T column move %C %b
-}
-
-
-

See Also

-

bind(n), bitmap(n), image(n), listbox(n), options(n)

-
-

Keywords

-

tree, widget

-
-
diff --git a/src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/pkgIndex.tcl b/src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/pkgIndex.tcl deleted file mode 100644 index 4d3e420f..00000000 --- a/src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/pkgIndex.tcl +++ /dev/null @@ -1,8 +0,0 @@ -if {[catch {package require Tcl 8.6-}]} return -set script "" -if {![info exists ::env(TREECTRL_LIBRARY)] - && [file exists [file join $dir treectrl.tcl]]} { - append script "[list set ::treectrl_library $dir]\n" -} -append script [list load [file join $dir treectrl24.dll] [string totitle treectrl 0 0]] -package ifneeded treectrl 2.4.2 $script diff --git a/src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/treectrl.tcl b/src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/treectrl.tcl deleted file mode 100644 index b3e2ac88..00000000 --- a/src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/treectrl.tcl +++ /dev/null @@ -1,1951 +0,0 @@ -# Copyright (c) 2002-2011 Tim Baker - -bind TreeCtrl { - TreeCtrl::CursorCheck %W %x %y - TreeCtrl::MotionInHeader %W %x %y - TreeCtrl::MotionInButtons %W %x %y -} -bind TreeCtrl { - TreeCtrl::CursorCancel %W - TreeCtrl::MotionInHeader %W - TreeCtrl::MotionInButtons %W -} -bind TreeCtrl { - TreeCtrl::ButtonPress1 %W %x %y -} -bind TreeCtrl { - TreeCtrl::DoubleButton1 %W %x %y -} -bind TreeCtrl { - TreeCtrl::Motion1 %W %x %y -} -bind TreeCtrl { - TreeCtrl::Release1 %W %x %y -} -bind TreeCtrl { - set TreeCtrl::Priv(buttonMode) normal - TreeCtrl::BeginExtend %W [%W item id {nearest %x %y}] -} -# Command-click should provide a discontinuous selection on OSX -switch -- [tk windowingsystem] { - "aqua" { set modifier Command } - default { set modifier Control } -} -bind TreeCtrl <$modifier-ButtonPress-1> { - set TreeCtrl::Priv(buttonMode) normal - TreeCtrl::BeginToggle %W [%W item id {nearest %x %y}] -} -bind TreeCtrl { - TreeCtrl::Leave1 %W %x %y -} -bind TreeCtrl { - TreeCtrl::Enter1 %W %x %y -} - -bind TreeCtrl { - TreeCtrl::SetActiveItem %W [TreeCtrl::UpDown %W active -1] -} -bind TreeCtrl { - TreeCtrl::Extend %W above -} -bind TreeCtrl { - TreeCtrl::SetActiveItem %W [TreeCtrl::UpDown %W active 1] -} -bind TreeCtrl { - TreeCtrl::Extend %W below -} -bind TreeCtrl { - if {![TreeCtrl::Has2DLayout %W]} { - %W item collapse [%W item id active] - } else { - TreeCtrl::SetActiveItem %W [TreeCtrl::LeftRight %W active -1] - } -} -bind TreeCtrl { - TreeCtrl::Extend %W left -} -bind TreeCtrl { - %W xview scroll -1 pages -} -bind TreeCtrl { - if {![TreeCtrl::Has2DLayout %W]} { - %W item expand [%W item id active] - } else { - TreeCtrl::SetActiveItem %W [TreeCtrl::LeftRight %W active 1] - } -} -bind TreeCtrl { - TreeCtrl::Extend %W right -} -bind TreeCtrl { - %W xview scroll 1 pages -} -bind TreeCtrl { - %W yview scroll -1 pages - if {[%W item id {nearest 0 0}] ne ""} { - %W activate {nearest 0 0} - } -} -bind TreeCtrl { - %W yview scroll 1 pages - if {[%W item id {nearest 0 0}] ne ""} { - %W activate {nearest 0 0} - } -} -bind TreeCtrl { - %W xview scroll -1 pages -} -bind TreeCtrl { - %W xview scroll 1 pages -} -bind TreeCtrl { - %W xview moveto 0 -} -bind TreeCtrl { - %W xview moveto 1 -} -bind TreeCtrl { - TreeCtrl::SetActiveItem %W [%W item id {first visible state enabled}] -} -bind TreeCtrl { - TreeCtrl::DataExtend %W [%W item id {first visible state enabled}] -} -bind TreeCtrl { - TreeCtrl::SetActiveItem %W [%W item id {last visible state enabled}] -} -bind TreeCtrl { - TreeCtrl::DataExtend %W [%W item id {last visible state enabled}] -} -bind TreeCtrl <> { - if {[string equal [selection own -displayof %W] "%W"]} { - clipboard clear -displayof %W - clipboard append -displayof %W [selection get -displayof %W] - } -} -bind TreeCtrl { - TreeCtrl::BeginSelect %W [%W item id active] -} -bind TreeCtrl { - TreeCtrl::BeginSelect %W [%W item id active] -} -bind TreeCtrl { - TreeCtrl::BeginExtend %W [%W item id active] -} -bind TreeCtrl { - TreeCtrl::BeginExtend %W [%W item id active] -} -bind TreeCtrl { - TreeCtrl::Cancel %W -} -bind TreeCtrl { - TreeCtrl::SelectAll %W -} -bind TreeCtrl { - if {[string compare [%W cget -selectmode] "browse"]} { - %W selection clear - } -} - -bind TreeCtrl { - %W item expand [%W item id active] -} -bind TreeCtrl { - %W item collapse [%W item id active] -} -bind TreeCtrl { - %W item toggle [%W item id active] -} - - -# Additional Tk bindings that aren't part of the Motif look and feel: - -bind TreeCtrl { - focus %W - TreeCtrl::ScanMark %W %x %y -} -bind TreeCtrl { - TreeCtrl::ScanDrag %W %x %y -} - -if {$tcl_platform(platform) eq "windows"} { - bind TreeCtrl { - TreeCtrl::ScanMark %W %x %y - } - bind TreeCtrl { - TreeCtrl::ScanDrag %W %x %y - } -} -if {[string equal [tk windowingsystem] "aqua"]} { - # Middle mouse on Mac OSX - bind TreeCtrl { - TreeCtrl::ScanMark %W %x %y - } - bind TreeCtrl { - TreeCtrl::ScanDrag %W %x %y - } -} - -# MouseWheel -if {[string equal "x11" [tk windowingsystem]]} { - # Support for mousewheels on Linux/Unix commonly comes through mapping - # the wheel to the extended buttons. If you have a mousewheel, find - # Linux configuration info at: - # http://www.inria.fr/koala/colas/mouse-wheel-scroll/ - bind TreeCtrl <4> { - if {!$tk_strictMotif} { - %W yview scroll -5 units - } - } - bind TreeCtrl <5> { - if {!$tk_strictMotif} { - %W yview scroll 5 units - } - } -} elseif {[string equal [tk windowingsystem] "aqua"]} { - bind TreeCtrl { - %W yview scroll [expr {- (%D)}] units - } -} else { - bind TreeCtrl { - %W yview scroll [expr {- (%D / 120) * 4}] units - } -} - -namespace eval ::TreeCtrl { - variable Priv - array set Priv { - prev {} - } - - if {[info procs ::lassign] eq ""} { - proc lassign {values args} { - uplevel 1 [list foreach $args [linsert $values end {}] break] - lrange $values [llength $args] end - } - } -} - -# Retrieve filelist bindings from this dir -source [file join [file dirname [info script]] filelist-bindings.tcl] - -# ::TreeCtrl::ColumnCanResizeLeft -- -# -# Return 1 if the given column should be resized by the left edge. -# -# Arguments: -# w The treectrl widget. -# column The column. - -proc ::TreeCtrl::ColumnCanResizeLeft {w column} { - if {[$w column cget $column -lock] eq "right"} { - return 1 - } - return 0 -} - -# ::TreeCtrl::ColumnCanMoveHere -- -# -# Return 1 if the given column can be moved before another. -# -# Arguments: -# w The treectrl widget. -# column The column. -# before The column to place 'column' before. - -proc ::TreeCtrl::ColumnCanMoveHere {w column before} { - if {[$w column compare $column == $before] || - ([$w column order $column] == [$w column order $before] - 1)} { - return 0 - } - set lock [$w column cget $column -lock] - return [expr {[$w column compare $before >= "first lock $lock"] && - [$w column compare $before <= "last lock $lock next"]}] -} - -# ::TreeCtrl::ColumnDragFindBefore -- -# -# This is called when dragging a column header. The result is 1 if the given -# coordinates are near a column header before which the dragged column can -# be moved. -# -# Arguments: -# w The treectrl widget. -# x Window x-coord. -# y Window y-coord. -# dragColumn The column being dragged. -# indColumn_ Out: what to set -indicatorcolumn to. -# indSide_ Out: what to set -indicatorside to. - -proc ::TreeCtrl::ColumnDragFindBefore {w x y dragColumn indColumn_ indSide_} { - upvar $indColumn_ indColumn - upvar $indSide_ indSide - - set lock [$w column cget $dragColumn -lock] - scan [$w bbox header.$lock] "%d %d %d %d" minX y1 maxX y2 - if {$x < $minX} { - set x $minX - } - if {$x >= $maxX} { - set x [expr {$maxX - 1}] - } - $w identify -array id $x $y - if {$id(where) ne "header"} { - return 0 - } - set indColumn $id(column) - if {[$w column compare $indColumn == $dragColumn]} { - return 0 - } - - # The given $x is either the left edge or the right edge of the column - # header that is being dragged depending on which direction the user - # is dragging the column. - # When dragging to the left, the indicator column is chosen to be the - # leftmost column whose mid-way point is greater than the left edge of the - # dragged header. - # When dragging to the right, the indicator column is chosen to be the - # rightmost column whose mid-way point is less than the right edge of the - # dragged header. - if {[$w column compare $indColumn != "tail"]} { - variable Priv - scan [$w header bbox $Priv(header) $indColumn] "%d %d %d %d" x1 y1 x2 y2 - # Hack - ignore canvaspadx - if {[$w column cget $indColumn -lock] eq "none" && - [$w column compare $indColumn == "first visible lock none"]} { - incr x1 [lindex [$w cget -canvaspadx] 0] - } - if {[$w column compare $dragColumn < $indColumn]} { - if {$x < $x1 + ($x2 - $x1) / 2} { - set indColumn [$w column id "$indColumn prev visible"] - set indColumn [GetSpanStartColumn $w $Priv(header) $indColumn] - } - } else { - if {$x > $x1 + ($x2 - $x1) / 2} { - # Find the column at the start of the next visible span - set starts [GetSpanStarts $w $Priv(header)] - for {set i [$w column order $indColumn]} {true} {incr i} { - if {[$w column compare [lindex $starts $i] > $indColumn]} break - } - set indColumn [lindex $starts $i] - } - } - } - - set before $indColumn - set prev [$w column id "$dragColumn prev visible"] - set next [$w column id "$dragColumn next visible"] - if {[$w column compare $indColumn == "tail"]} { - set indSide left - set indColumn [$w column id "last lock none visible"] - set indSide right - } elseif {$prev ne "" && [$w column compare $prev == $indColumn]} { - set indSide left - } elseif {$next ne "" && [$w column compare $next == $indColumn]} { - set before [$w column id "$indColumn next visible"] - set indSide right - } else { - scan [$w column bbox $indColumn] "%d %d %d %d" x1 y1 x2 y2 - if {$x < $x1 + ($x2 - $x1) / 2} { - set indSide left - } else { - set before [$w column id "$indColumn next visible"] - set indSide right - } - } - if {$before eq "" || [$w column compare $before > "last lock $lock next"]} { - set before [$w column id "last lock $lock next"] - } - return [ColumnCanMoveHere $w $dragColumn $before] -} - -# ::TreeCtrl::ListElementWindows -- -# -# Return a list of Tk windows in window elements in a column header. -# -# Arguments: -# T The treectrl widget. -# H Header id -# C Column id - -proc ::TreeCtrl::ListElementWindows {T H C} { - set S [$T header style set $H $C] - if {$S eq ""} return - set result {} - foreach E [$T header style elements $H $C] { - if {[$T element type $E] eq "window"} { - set window [$T header element cget $H $C $E -window] - if {$window ne ""} { - lappend result $window - } - } - } - return $result -} - -# ::TreeCtrl::ColumnDragRestackWindows -- -# -# Restack windows in window elements so that windows in dragged headers -# are above all other windows in undragged headers. -# -# Arguments: -# T The treectrl widget. - -proc ::TreeCtrl::ColumnDragRestackWindows {T} { - variable Priv - set C [$T header dragcget -imagecolumn] - set lock [$T column cget $C -lock] - set span [$T header dragcget -imagespan] - set last [$T column id [list $C span $span]] - set dragged [$T column id [list range $C $last]] - foreach H [$T header id all] { - set prev "" - set lowest "" - foreach C $dragged { - foreach win [ListElementWindows $T $H $C] { - if {$prev eq ""} { - set lowest $win - } else { - raise $win $prev - } - set prev $win - } - } - if {$lowest eq ""} continue - foreach C [$T column id "lock $lock !tail"] { - if {[lsearch -exact $dragged $C] != -1} continue - foreach win [ListElementWindows $T $H $C] { - lower $win $lowest - } - } - } - return -} - -# ::TreeCtrl::CursorAction -- -# -# If the given point is at the left or right edge of a resizable column -# header, the result is "action header-resize header H column C". -# If the given point is in a header with -button=TRUE, the result is -# "action header-button header H column C". -# -# Arguments: -# w The treectrl widget. -# x Window coord of pointer. -# y Window coord of pointer. - -proc ::TreeCtrl::CursorAction {w x y var_} { - upvar $var_ var - variable Priv - $w identify -array id $x $y - - set var(action) "" - if {$id(where) eq "header"} { - set var(header) $id(header) - set column $id(column) - set side $id(side) - if {$side eq ""} { - if {[scan [$w bbox header.left] "%d %d %d %d" x1 y1 x2 y2] == 4} { - if {$x < $x2 + 4 && $x >= $x2} { - set column [$w column id "last visible lock left"] - set side right - } - } - if {[scan [$w bbox header.right] "%d %d %d %d" x1 y1 x2 y2] == 4} { - if {$x >= $x1 - 4 && $x < $x1} { - set column [$w column id "first visible lock right"] - set side left - } - } - } - if {$side eq "left"} { - if {[ColumnCanResizeLeft $w $column]} { - if {[$w column cget $column -resize]} { - array set var [list action "header-resize" column $column] - return - } - } else { - # Resize the previous column - if {[$w column compare $column == tail]} { - set prev [$w column id "last visible lock none"] - if {$prev eq ""} { - set prev [$w column id "last visible lock left"] - } - } else { - set prev [$w column id "$column prev visible"] - } - if {$prev ne "" && [$w column cget $prev -resize]} { - array set var [list action "header-resize" column $prev] - return - } - } - } elseif {$side eq "right"} { - # Get the last visible column in the span - set span [$w header span $id(header) $column] - set last [$w column id "$column span $span"] - set columns [$w column id [list range $column $last visible]] - set column2 [lindex $columns end] - if {[ColumnCanResizeLeft $w $column2]} { - # Resize the next column - set next [$w column id "$column2 next visible !tail"] - if {$next ne "" && [$w column cget $next -resize]} { - array set var [list action "header-resize" column $next] - return - } - } else { - if {[$w column cget $column2 -resize]} { - array set var [list action "header-resize" column $column2] - return - } - } - } - if {[$w column compare $column == "tail"]} { - # Can't -resize or -button the tail column - } elseif {[$w header cget $id(header) $column -button]} { - array set var [list action "header-button" column $column] - return - } - } - return -} - -# ::TreeCtrl::CursorCheck -- -# -# Sees if the given pointer coordinates are near the edge of a resizable -# column in the header. If so and the treectrl's cursor is not already -# set to sb_h_double_arrow, then the current cursor is saved and changed -# to sb_h_double_arrow, and an [after] callback to CursorCheckAux is -# scheduled. -# -# Arguments: -# w The treectrl widget. -# x Window coord of pointer. -# y Window coord of pointer. - -proc ::TreeCtrl::CursorCheck {w x y} { - variable Priv - CursorAction $w $x $y action - # If we are in the middle of resizing a column, don't cancel the cursor - if {[info exists Priv(buttonMode)] && $Priv(buttonMode) eq "resize"} { - array set action {action "header-resize" header XXX column XXX} - } - if {$action(action) ne "header-resize"} { - CursorCancel $w - return - } - set cursor sb_h_double_arrow - if {$cursor ne [$w cget -cursor]} { - if {![info exists Priv(cursor,$w)]} { - set Priv(cursor,$w) [$w cget -cursor] - } - $w configure -cursor $cursor - } - if {[info exists Priv(cursor,afterId,$w)]} { - after cancel $Priv(cursor,afterId,$w) - } - set Priv(cursor,afterId,$w) [after 150 [list TreeCtrl::CursorCheckAux $w]] - return -} - -# ::TreeCtrl::CursorCheckAux -- -# -# Get's the location of the pointer and calls CursorCheck if the treectrl's -# cursor was previously set to sb_h_double_arrow. -# -# Arguments: -# w The treectrl widget. - -proc ::TreeCtrl::CursorCheckAux {w} { - variable Priv - if {![winfo exists $w]} return - set x [winfo pointerx $w] - set y [winfo pointery $w] - if {[info exists Priv(cursor,$w)]} { - set x [expr {$x - [winfo rootx $w]}] - set y [expr {$y - [winfo rooty $w]}] - CursorCheck $w $x $y - } - return -} - -# ::TreeCtrl::CursorCancel -- -# -# Restores the treectrl's cursor if it was changed to sb_h_double_arrow. -# Cancels any pending [after] callback to CursorCheckAux. -# -# Arguments: -# w The treectrl widget. - -proc ::TreeCtrl::CursorCancel {w} { - variable Priv - if {[info exists Priv(cursor,$w)]} { - $w configure -cursor $Priv(cursor,$w) - unset Priv(cursor,$w) - } - if {[info exists Priv(cursor,afterId,$w)]} { - after cancel $Priv(cursor,afterId,$w) - unset Priv(cursor,afterId,$w) - } - return -} - -# ::TreeCtrl::GetSpanStarts -- -# -# This procedure returns a list of column ids, one per tree column. -# Each column id indicates the column at the start of a span. -# -# Arguments: -# T The treectrl widget. -# H Header id - -proc ::TreeCtrl::GetSpanStarts {T H} { - set columns [list] - set spans [$T header span $H] - if {[lindex [lsort -integer $spans] end] eq 1} { - return [$T column list] - } - for {set index 0} {$index < [$T column count]} {} { - set Cspan [$T column id "order $index"] - set span [lindex $spans $index] - if {![$T column cget $Cspan -visible]} { - set span 1 - } - while {$span > 0 && $index < [$T column count]} { - if {[$T column cget "order $index" -lock] ne [$T column cget $Cspan -lock]} break - lappend columns $Cspan - incr span -1 - incr index - } - } - return $columns -} - -# ::TreeCtrl::GetSpanStartColumn -- -# -# This procedure returns the column at the start of a span which covers the -# given column. -# -# Arguments: -# T The treectrl widget. -# H Header id -# C Column id - -proc ::TreeCtrl::GetSpanStartColumn {T H C} { - set columns [GetSpanStarts $T $H] - return [lindex $columns [$T column order $C]] -} - -# ::TreeCtrl::SetHeaderState -- -# -# This procedure sets the state of a header-column and remembers that -# header-column. If a different header-column is passed later the previous -# header-column's state is set to 'normal'. -# -# Arguments: -# T The treectrl widget. -# H Header id -# C Column id -# state active|normal|pressed - -proc ::TreeCtrl::SetHeaderState {T H C state} { - variable Priv - if {[info exists Priv(inheader,$T)]} { - lassign $Priv(inheader,$T) Hprev Cprev - } else { - if {$H eq "" || $C eq ""} return - set Hprev [set Cprev ""] - } - if {$H ne $Hprev || $C ne $Cprev} { - if {$Hprev ne "" && [$T header id $Hprev] ne ""} { - if {$Cprev ne "" && [$T column id $Cprev] ne ""} { - $T header configure $Hprev $Cprev -state normal - TryEvent $T Header state [list H $Hprev C $Cprev s normal] - } - } - } - if {$H eq "" || $C eq ""} { - unset Priv(inheader,$T) - } else { - $T header configure $H $C -state $state - TryEvent $T Header state [list H $H C $C s $state] - set Priv(inheader,$T) [list $H $C] - } - return -} - -# ::TreeCtrl::ClearHeaderState -- -# -# If a header-column's state was previously set via SetHeaderState then -# that column's state is set to normal and the header-column is forgotten. -# -# Arguments: -# T The treectrl widget. -# H Header id -# C Column id -# state active|normal|pressed - -proc ::TreeCtrl::ClearHeaderState {T} { - SetHeaderState $T "" "" "" - return -} - -# ::TreeCtrl::MotionInHeader -- -# -# This procedure updates the active/normal states of column headers as the -# mouse pointer moves in and out of them. Typically this results in visual -# feedback by changing the appearance of the headers. -# -# Arguments: -# w The treectrl widget. -# args x y coords if the pointer is in the window, or an empty list. - -proc ::TreeCtrl::MotionInHeader {w args} { - variable Priv - if {[llength $args]} { - set x [lindex $args 0] - set y [lindex $args 1] - CursorAction $w $x $y action - } else { - array set action {action ""} - } - if {[info exists Priv(inheader,$w)]} { - lassign $Priv(inheader,$w) headerPrev columnPrev - } else { - set headerPrev [set columnPrev ""] - } - set header "" - set column "" - if {$action(action) eq "header-button"} { - set header $action(header) - set column $action(column) - } elseif {$action(action) eq "header-resize"} { - set header $action(header) - set column [GetSpanStartColumn $w $header $action(column)] - } - if {$header ne $headerPrev || $column ne $columnPrev} { - if {$column ne ""} { - SetHeaderState $w $header $column active - } else { - ClearHeaderState $w - } - } - return -} - -# ::TreeCtrl::MotionInButtons -- -# -# This procedure updates the active/normal states of item buttons. -# Typically this results in visual feedback by changing the appearance -# of the buttons. -# -# Arguments: -# T The treectrl widget. -# args x y coords if the pointer is in the window, or an empty list. - -proc ::TreeCtrl::MotionInButtons {T args} { - variable Priv - set button "" - if {[llength $args]} { - set x [lindex $args 0] - set y [lindex $args 1] - $T identify -array id $x $y - if {$id(where) eq "item" && $id(button)} { - set button $id(item) - } - } - if {[info exists Priv(inbutton,$T)]} { - set prevButton $Priv(inbutton,$T) - } else { - set prevButton "" - } - if {$button ne $prevButton} { - if {$prevButton ne ""} { - if {[$T item id $prevButton] ne ""} { - $T item buttonstate $prevButton normal - } - } - if {$button ne ""} { - $T item buttonstate $button active - set Priv(inbutton,$T) $button - } else { - unset Priv(inbutton,$T) - } - } - if {[$T notify bind TreeCtrlButtonNotifyScroll] eq ""} { - $T notify bind TreeCtrlButtonNotifyScroll { - TreeCtrl::ButtonNotifyScroll %T - } - } - return -} - -# ::TreeCtrl::ButtonNotifyScroll -- -# -# Called when a event occurs and a button is in the active state. -# Finds the mouse pointer coords and calls MotionInButtons to update the -# state of affected buttons. -# -# Arguments: -# T The treectrl widget. - -proc ::TreeCtrl::ButtonNotifyScroll {T} { - set x [expr {[winfo pointerx $T] - [winfo rootx $T]}] - set y [expr {[winfo pointery $T] - [winfo rooty $T]}] - MotionInButtons $T $x $y - return -} - -# ::TreeCtrl::ButtonPress1 -- -# -# Handle event. -# -# Arguments: -# w The treectrl widget. -# x Window x coord. -# y Window y coord. - -proc ::TreeCtrl::ButtonPress1 {w x y} { - variable Priv - focus $w - - $w identify -array id $x $y - if {$id(where) eq ""} { - return - } - - if {$id(where) eq "item"} { - set item $id(item) - if {$id(button)} { - if {[$w cget -buttontracking]} { - $w item buttonstate $item pressed - set Priv(buttonMode) buttonTracking - set Priv(buttontrack,item) $item - } else { - $w item toggle $item -animate - } - return - } elseif {$id(line) ne ""} { - $w item toggle $id(line) - return - } - } - set Priv(buttonMode) "" - if {$id(where) eq "header"} { - CursorAction $w $x $y action - if {$action(action) eq "header-resize"} { - set column $action(column) - set Priv(buttonMode) resize - set Priv(header) $action(header) - set Priv(column) $column - set Priv(x) $x - set Priv(y) $y - set Priv(width) [$w column width $column] - return - } - set column $id(column) - if {$action(action) eq "header-button"} { - set Priv(buttonMode) header - SetHeaderState $w $action(header) $column pressed - } else { - if {[$w column compare $column == "tail"]} return - if {![$w header dragcget -enable]} return - if {![$w header dragcget $action(header) -enable]} return - set Priv(buttonMode) dragColumnWait - } - set Priv(header) $action(header) - set Priv(column) $column - set Priv(columnDrag,x) $x - set Priv(columnDrag,y) $y - return - } - set item $id(item) - if {![$w item enabled $item]} { - return - } - - # If the initial mouse-click is in a locked column, restrict scrolling - # to the vertical. - set count [scan [$w contentbox] "%d %d %d %d" x1 y1 x2 y2] - if {$count != -1 && $x >= $x1 && $x < $x2} { - set Priv(autoscan,direction,$w) xy - } else { - set Priv(autoscan,direction,$w) y - } - - set Priv(buttonMode) normal - BeginSelect $w $item - return -} - -# ::TreeCtrl::DoubleButtonPress1 -- -# -# Handle event. -# -# Arguments: -# w The treectrl widget. -# x Window x coord. -# y Window y coord. - -proc ::TreeCtrl::DoubleButton1 {w x y} { - - $w identify -array id $x $y - if {$id(where) eq ""} { - return - } - if {$id(where) eq "item"} { - if {$id(button)} { - if {[$w cget -buttontracking]} { - # There is no so just toggle it - $w item toggle $id(item) -animate - } else { - $w item toggle $id(item) -animate - } - return - } elseif {$id(line) ne ""} { - $w item toggle $id(line) - return - } - } - if {$id(where) eq "header"} { - CursorAction $w $x $y action - # Double-click between columns to set default column width - if {$action(action) eq "header-resize"} { - set column $action(column) - $w column configure $column -width "" - CursorCheck $w $x $y - MotionInHeader $w $x $y - } else { - ButtonPress1 $w $x $y - } - } - return -} - -# ::TreeCtrl::Motion1 -- -# -# Handle event. -# -# Arguments: -# w The treectrl widget. -# x Window x coord. -# y Window y coord. - -proc ::TreeCtrl::Motion1 {w x y} { - variable Priv - if {![info exists Priv(buttonMode)]} return - switch $Priv(buttonMode) { - header { - $w identify -array id $x $y - if {$id(where) ne "header" || - $id(header) ne $Priv(header) || - $id(column) ne $Priv(column)} { - if {[$w header cget $Priv(header) $Priv(column) -state] eq "pressed"} { - ClearHeaderState $w - } - } else { - if {[$w header cget $Priv(header) $Priv(column) -state] ne "pressed"} { - SetHeaderState $w $Priv(header) $Priv(column) pressed - } - if {[$w header dragcget -enable] && - [$w header dragcget $Priv(header) -enable] && - (abs($Priv(columnDrag,x) - $x) > 4)} { - set Priv(columnDrag,x) $x - $w header dragconfigure \ - -imagecolumn $Priv(column) \ - -imageoffset [expr {$x - $Priv(columnDrag,x)}] \ - -imagespan [$w header span $Priv(header) $Priv(column)] - ColumnDragRestackWindows $w - set Priv(buttonMode) dragColumn - TryEvent $w ColumnDrag begin [list H $Priv(header) C $Priv(column)] - # Allow binding scripts to cancel the drag - if {[$w header dragcget -imagecolumn] eq ""} { - set Priv(buttonMode) header - } - } - } - } - buttonTracking { - $w identify -array id $x $y - set itemTrack $Priv(buttontrack,item) - set exists [expr {[$w item id $itemTrack] ne ""}] - set mouseover 0 - if {$id(where) eq "item" && $id(button)} { - if {$exists && [$w item compare $itemTrack == $id(item)]} { - set mouseover 1 - } - } - if {$mouseover} { - $w item buttonstate $itemTrack pressed - } elseif {$exists} { - $w item buttonstate $itemTrack normal - } - } - dragColumnWait { - if {(abs($Priv(columnDrag,x) - $x) > 4)} { - set Priv(columnDrag,x) $x - $w header dragconfigure \ - -imagecolumn $Priv(column) \ - -imageoffset [expr {$x - $Priv(columnDrag,x)}] \ - -imagespan [$w header span $Priv(header) $Priv(column)] - ColumnDragRestackWindows $w - set Priv(buttonMode) dragColumn - TryEvent $w ColumnDrag begin [list H $Priv(header) C $Priv(column)] - # Allow binding scripts to cancel the drag - if {[$w header dragcget -imagecolumn] eq ""} { - unset Priv(buttonMode) - } - } - } - dragColumn { - scan [$w bbox header] "%d %d %d %d" x1 y1 x2 y2 - if {$y < $y1 - 30 || $y >= $y2 + 30} { - set inside 0 - } else { - set inside 1 - } - if {$inside && ([$w header dragcget -imagecolumn] eq "")} { - $w header dragconfigure -imagecolumn $Priv(column) - } elseif {!$inside && ([$w header dragcget -imagecolumn] ne "")} { - $w header dragconfigure -imagecolumn "" -indicatorcolumn "" - } - if {$inside} { - set offset [expr {$x - $Priv(columnDrag,x)}] - $w header dragconfigure -imageoffset $offset - - # When dragging to the left, use the left edge of the dragged - # header to choose the -indicatorcolumn. When dragging to the - # right, use the right edge. - scan [$w header bbox $Priv(header) $Priv(column)] "%d %d %d %d" x1 y1 x2 y2 - if {$offset > 0} { - set xEdge [expr {$offset + $x2}] - } else { - set xEdge [expr {$offset + $x1}] - } - - if {[ColumnDragFindBefore $w $xEdge $Priv(columnDrag,y) $Priv(column) indColumn indSide]} { - set prevIndColumn [$w header dragcget -indicatorcolumn] - $w header dragconfigure \ - -indicatorcolumn $indColumn \ - -indicatorside $indSide \ - -indicatorspan [$w header span $Priv(header) $indColumn] - if {$indColumn != $prevIndColumn} { - TryEvent $w ColumnDrag indicator [list H $Priv(header) C $indColumn] - } - } else { - $w header dragconfigure -indicatorcolumn "" - } - } - if {[$w column cget $Priv(column) -lock] eq "none"} { - ColumnDragScrollCheck $w $x $y - } - } - normal { - set Priv(x) $x - set Priv(y) $y - SelectionMotion $w [$w item id [list nearest $x $y]] - set Priv(autoscan,command,$w) {SelectionMotion %T [%T item id "nearest %x %y"]} - AutoScanCheck $w $x $y - } - resize { - if {[ColumnCanResizeLeft $w $Priv(column)]} { - set width [expr {$Priv(width) + $Priv(x) - $x}] - } else { - set width [expr {$Priv(width) + $x - $Priv(x)}] - } - set minWidth [$w column cget $Priv(column) -minwidth] - set maxWidth [$w column cget $Priv(column) -maxwidth] - if {$minWidth eq ""} { - set minWidth 0 - } - if {$width < $minWidth} { - set width $minWidth - } - if {($maxWidth ne "") && ($width > $maxWidth)} { - set width $maxWidth - } - if {$width == 0} { - incr width - } - switch -- [$w cget -columnresizemode] { - proxy { - scan [$w column bbox $Priv(column)] "%d %d %d %d" x1 y1 x2 y2 - if {[ColumnCanResizeLeft $w $Priv(column)]} { - # Use "ne" because -columnproxy could be "" - if {$x2 - $width ne [$w cget -columnproxy]} { - $w configure -columnproxy [expr {$x2 - $width}] - } - } else { - if {($x1 + $width - 1) ne [$w cget -columnproxy]} { - $w configure -columnproxy [expr {$x1 + $width - 1}] - } - } - } - realtime { - if {[$w column cget $Priv(column) -width] != $width} { - $w column configure $Priv(column) -width $width - } - } - } - } - } - return -} - -# ::TreeCtrl::Leave1 -- -# -# Handle event. -# -# Arguments: -# w The treectrl widget. -# x Window x coord. -# y Window y coord. - -proc ::TreeCtrl::Leave1 {w x y} { - variable Priv - if {![info exists Priv(buttonMode)]} return - switch $Priv(buttonMode) { - header { - if {[$w header cget $Priv(header) $Priv(column) -state] eq "pressed"} { - ClearHeaderState $w - } - } - } - return -} - -# ::TreeCtrl::Enter1 -- -# -# Handle event. -# -# Arguments: -# w The treectrl widget. -# x Window x coord. -# y Window y coord. - -proc ::TreeCtrl::Enter1 {w x y} { - variable Priv - if {![info exists Priv(buttonMode)]} return - switch $Priv(buttonMode) { - default {} - } - return -} - -# ::TreeCtrl::Release1 -- -# -# Handle event. -# -# Arguments: -# w The treectrl widget. -# x Window x coord. -# y Window y coord. - -proc ::TreeCtrl::Release1 {w x y} { - variable Priv - if {![info exists Priv(buttonMode)]} return - switch $Priv(buttonMode) { - header { - if {[$w header cget $Priv(header) $Priv(column) -state] eq "pressed"} { - SetHeaderState $w $Priv(header) $Priv(column) active - TryEvent $w Header invoke [list H $Priv(header) C $Priv(column)] - } - CursorCheck $w $x $y - MotionInHeader $w $x $y - } - buttonTracking { - $w identify -array id $x $y - set itemTrack $Priv(buttontrack,item) - set exists [expr {[$w item id $itemTrack] ne ""}] - if {$id(where) eq "item" && $id(button)} { - if {$exists && [$w item compare $itemTrack == $id(item)]} { - $w item buttonstate $id(item) active - $w item toggle $itemTrack -animate - } - } - } - dragColumn { - AutoScanCancel $w - ClearHeaderState $w - if {[$w header dragcget -imagecolumn] ne ""} { - set visible 1 - } else { - set visible 0 - } - set column [$w header dragcget -indicatorcolumn] - $w header dragconfigure -imagecolumn "" -indicatorcolumn "" - if {$visible && ($column ne "")} { - # If dragging to the right, drop after the last column in the - # span of the indicator column. - if {[$w column order $Priv(column)] < [$w column order $column]} { - set span [$w header dragcget -indicatorspan] - set column [$w column id "$column span $span next"] - } - set lock [$w column cget $Priv(column) -lock] - if {$column eq "" || [$w column compare $column > "last lock $lock next"]} { - set column [$w column id "last lock $lock next"] - } - TryEvent $w ColumnDrag receive [list H $Priv(header) C $Priv(column) b $column] - } - CursorCheck $w $x $y - MotionInHeader $w $x $y - TryEvent $w ColumnDrag end [list H $Priv(header) C $Priv(column)] - } - normal { - AutoScanCancel $w - set nearest [$w item id [list nearest $x $y]] - if {$nearest ne ""} { - $w activate $nearest - } -set Priv(prev) "" - } - resize { - if {[$w cget -columnproxy] ne ""} { - scan [$w column bbox $Priv(column)] "%d %d %d %d" x1 y1 x2 y2 - if {[ColumnCanResizeLeft $w $Priv(column)]} { - set width [expr {$x2 - [$w cget -columnproxy]}] - } else { - set width [expr {[$w cget -columnproxy] - $x1 + 1}] - } - $w configure -columnproxy {} - $w column configure $Priv(column) -width $width - } - # Clear buttonMode early so CursorCheck doesn't exit - unset Priv(buttonMode) - CursorCheck $w $x $y - MotionInHeader $w $x $y - return - } - } - unset Priv(buttonMode) - return -} - -# ::TreeCtrl::BeginSelect -- -# -# This procedure is typically invoked on button-1 presses. It begins -# the process of making a selection in the treectrl. Its exact behavior -# depends on the selection mode currently in effect for the treectrl. -# -# Arguments: -# w The treectrl widget. -# item The item for the selection operation (typically the -# one under the pointer). - -proc ::TreeCtrl::BeginSelect {w item} { - variable Priv - if {$item eq ""} return - if {[string equal [$w cget -selectmode] "multiple"]} { - if {[$w selection includes $item]} { - $w selection clear $item - } else { - $w selection add $item - } - } else { - $w selection anchor $item - $w selection modify $item all - set Priv(selection) {} - set Priv(prev) $item - } - return -} - -# ::TreeCtrl::SelectionMotion -- -# -# This procedure is called to process mouse motion events while -# button 1 is down. It may move or extend the selection, depending -# on the treectrl's selection mode. -# -# Arguments: -# w The treectrl widget. -# item- The item under the pointer. - -proc ::TreeCtrl::SelectionMotion {w item} { - variable Priv - - if {$item eq ""} return - set item [$w item id $item] - if {$item eq $Priv(prev)} return - if {![$w item enabled $item]} return - - switch [$w cget -selectmode] { - browse { - $w selection modify $item all - set Priv(prev) $item - } - extended { - set i $Priv(prev) - set select {} - set deselect {} - if {$i eq ""} { - set i $item - lappend select $item - set hack [$w item compare $item == anchor] - } else { - set hack 0 - } - if {[$w selection includes anchor] || $hack} { - set deselect [concat $deselect [$w item range $i $item]] - set select [concat $select [$w item range anchor $item]] - } else { - set deselect [concat $deselect [$w item range $i $item]] - set deselect [concat $deselect [$w item range anchor $item]] - } - if {![info exists Priv(selection)]} { - set Priv(selection) [$w selection get] - } - while {[$w item compare $i < $item] && [$w item compare $i < anchor]} { - if {[lsearch $Priv(selection) $i] >= 0} { - lappend select $i - } - set i [$w item id "$i next visible"] - } - while {[$w item compare $i > $item] && [$w item compare $i > anchor]} { - if {[lsearch $Priv(selection) $i] >= 0} { - lappend select $i - } - set i [$w item id "$i prev visible"] - } - set Priv(prev) $item - $w selection modify $select $deselect - } - } - return -} - -# ::TreeCtrl::BeginExtend -- -# -# This procedure is typically invoked on shift-button-1 presses. It -# begins the process of extending a selection in the treectrl. Its -# exact behavior depends on the selection mode currently in effect -# for the treectrl. -# -# Arguments: -# w The treectrl widget. -# item- The item for the selection operation (typically the -# one under the pointer). - -proc ::TreeCtrl::BeginExtend {w item} { - if {[string equal [$w cget -selectmode] "extended"]} { - if {[$w selection includes anchor]} { - SelectionMotion $w $item - } else { - # No selection yet; simulate the begin-select operation. - BeginSelect $w $item - } - } - return -} - -# ::TreeCtrl::BeginToggle -- -# -# This procedure is typically invoked on control-button-1 presses. It -# begins the process of toggling a selection in the treectrl. Its -# exact behavior depends on the selection mode currently in effect -# for the treectrl. -# -# Arguments: -# w The treectrl widget. -# item The item for the selection operation (typically the -# one under the pointer). - -proc ::TreeCtrl::BeginToggle {w item} { - variable Priv - if {$item eq ""} return - if {[string equal [$w cget -selectmode] "extended"]} { - set Priv(selection) [$w selection get] - set Priv(prev) $item - $w selection anchor $item - if {[$w selection includes $item]} { - $w selection clear $item - } else { - $w selection add $item - } - } - return -} - -# ::TreeCtrl::AutoScanCheck -- -# -# Sees if the given pointer coords are outside the content area of the -# treectrl (ie, not including borders or column headers) or within -# -scrollmargin distance of the edges of the content area. If so and -# auto-scanning is not already in progress, then the window is scrolled -# and an [after] callback to AutoScanCheckAux is scheduled. -# -# Arguments: -# w The treectrl widget. -# x Window x coord. -# y Window y coord. - -proc ::TreeCtrl::AutoScanCheck {w x y} { - variable Priv - # Could have clicked in locked column - if {[scan [$w contentbox] "%d %d %d %d" x1 y1 x2 y2] == -1} { - if {[scan [$w bbox left] "%d %d %d %d" x1 y1 x2 y2] == -1} { - scan [$w bbox right] "%d %d %d %d" x1 y1 x2 y2 - } - } - set margin [winfo pixels $w [$w cget -scrollmargin]] - if {![info exists Priv(autoscan,direction,$w)]} { - set Priv(autoscan,direction,$w) xy - } - set scrollX [string match *x* $Priv(autoscan,direction,$w)] - set scrollY [string match *y* $Priv(autoscan,direction,$w)] - if {($scrollX && (($x < $x1 + $margin) || ($x >= $x2 - $margin))) || - ($scrollY && (($y < $y1 + $margin) || ($y >= $y2 - $margin)))} { - if {[info exists Priv(autoscan,afterId,$w)]} return - if {$scrollY && $y >= $y2 - $margin} { - $w yview scroll 1 units - set delay [$w cget -yscrolldelay] - } elseif {$scrollY && $y < $y1 + $margin} { - $w yview scroll -1 units - set delay [$w cget -yscrolldelay] - } elseif {$scrollX && $x >= $x2 - $margin} { - $w xview scroll 1 units - set delay [$w cget -xscrolldelay] - } elseif {$scrollX && $x < $x1 + $margin} { - $w xview scroll -1 units - set delay [$w cget -xscrolldelay] - } - set count [scan $delay "%d %d" d1 d2] - if {[info exists Priv(autoscan,scanning,$w)]} { - if {$count == 2} { - set delay $d2 - } - } else { - if {$count == 2} { - set delay $d1 - } - set Priv(autoscan,scanning,$w) 1 - } - if {$Priv(autoscan,command,$w) ne ""} { - set command [string map [list %T $w %x $x %y $y] $Priv(autoscan,command,$w)] - eval $command - } - set Priv(autoscan,afterId,$w) [after $delay [list TreeCtrl::AutoScanCheckAux $w]] - return - } - AutoScanCancel $w - return -} - -# ::TreeCtrl::AutoScanCheckAux -- -# -# Gets the location of the pointer and calls AutoScanCheck. -# -# Arguments: -# w The treectrl widget. - -proc ::TreeCtrl::AutoScanCheckAux {w} { - variable Priv - if {![winfo exists $w]} return - # Not quite sure how this can happen - if {![info exists Priv(autoscan,afterId,$w)]} return - unset Priv(autoscan,afterId,$w) - set x [winfo pointerx $w] - set y [winfo pointery $w] - set x [expr {$x - [winfo rootx $w]}] - set y [expr {$y - [winfo rooty $w]}] - AutoScanCheck $w $x $y - return -} - -# ::TreeCtrl::AutoScanCancel -- -# -# Cancels any pending [after] callback to AutoScanCheckAux. -# -# Arguments: -# w The treectrl widget. - -proc ::TreeCtrl::AutoScanCancel {w} { - variable Priv - if {[info exists Priv(autoscan,afterId,$w)]} { - after cancel $Priv(autoscan,afterId,$w) - unset Priv(autoscan,afterId,$w) - } - unset -nocomplain Priv(autoscan,scanning,$w) - return -} - -# ::TreeCtrl::ColumnDragScrollCheck -- -# -# Sees if the given pointer coords are outside the left or right edges of -# the content area of the treectrl (ie, not including borders). If so and -# auto-scanning is not already in progress, then the window is scrolled -# horizontally and the column drag-image is repositioned, and an [after] -# callback to ColumnDragScrollCheckAux is scheduled. -# -# Arguments: -# w The treectrl widget. -# x Window coord of pointer. -# y Window coord of pointer. - -proc ::TreeCtrl::ColumnDragScrollCheck {w x y} { - variable Priv - - # When dragging to the left, use the left edge of the dragged - # header to choose the -indicatorcolumn. When dragging to the - # right, use the right edge. - scan [$w header bbox $Priv(header) $Priv(column)] "%d %d %d %d" x1 y1 x2 y2 - set offset [$w header dragcget -imageoffset] - if {$offset > 0} { - set xEdge [expr {$offset + $x2}] - } else { - set xEdge [expr {$offset + $x1}] - } - - scan [$w bbox header.none] "%d %d %d %d" x1 y1 x2 y2 - - if {($x < $x1) || ($x >= $x2)} { - if {![info exists Priv(autoscan,afterId,$w)]} { - set bbox1 [$w column bbox $Priv(column)] - if {$xEdge >= $x2} { - $w xview scroll 1 units - } else { - $w xview scroll -1 units - } - set bbox2 [$w column bbox $Priv(column)] - if {[lindex $bbox1 0] != [lindex $bbox2 0]} { - incr Priv(columnDrag,x) [expr {[lindex $bbox2 0] - [lindex $bbox1 0]}] - $w header dragconfigure -imageoffset [expr {$x - $Priv(columnDrag,x)}] - - if {[ColumnDragFindBefore $w $xEdge $Priv(columnDrag,y) $Priv(column) indColumn indSide]} { - $w header dragconfigure -indicatorcolumn $indColumn \ - -indicatorside $indSide - } else { - $w header dragconfigure -indicatorcolumn "" - } - } - set Priv(autoscan,afterId,$w) [after 50 [list TreeCtrl::ColumnDragScrollCheckAux $w]] - } - return - } - AutoScanCancel $w - return -} - -# ::TreeCtrl::ColumnDragScrollCheckAux -- -# -# Gets the location of the pointer and calls ColumnDragScrollCheck. -# -# Arguments: -# w The treectrl widget. - -proc ::TreeCtrl::ColumnDragScrollCheckAux {w} { - variable Priv - if {![winfo exists $w]} return - # Not quite sure how this can happen - if {![info exists Priv(autoscan,afterId,$w)]} return - unset Priv(autoscan,afterId,$w) - set x [winfo pointerx $w] - set y [winfo pointery $w] - set x [expr {$x - [winfo rootx $w]}] - set y [expr {$y - [winfo rooty $w]}] - ColumnDragScrollCheck $w $x $y - return -} - -# ::TreeCtrl::Has2DLayout -- -# -# Determine if items are displayed in a 2-dimensional arrangement. -# This is used by the and bindings. -# -# Arguments: -# w The treectrl widget. - -proc ::TreeCtrl::Has2DLayout {T} { - if {[$T cget -orient] ne "vertical" || [$T cget -wrap] ne ""} { - return 1 - } - set item [$T item id "last visible"] - if {$item ne ""} { - lassign [$T item rnc $item] row column - if {$column > 0} { - return 1 - } - } - return 0 -} - -# ::TreeCtrl::UpDown -- -# -# Returns the id of an item above or below the given item that the active -# item could be set to. If the given item isn't visible, the first visible -# enabled item is returned. An attempt is made to choose an item in the -# same column over repeat calls; this gives a better result if some rows -# have less items than others. Only enabled items are considered. -# -# Arguments: -# w The treectrl widget. -# item Item to move from, typically the active item. -# n +1 to move down, -1 to move up. - -proc ::TreeCtrl::UpDown {w item n} { - variable Priv - set rnc [$w item rnc $item] - if {$rnc eq ""} { - return [$w item id {first visible state enabled}] - } - scan $rnc "%d %d" row col - set Priv(keyNav,row,$w) [expr {$row + $n}] - if {![info exists Priv(keyNav,rnc,$w)] || $rnc ne $Priv(keyNav,rnc,$w)} { - set Priv(keyNav,col,$w) $col - } - set item2 [$w item id "rnc $Priv(keyNav,row,$w) $Priv(keyNav,col,$w)"] - if {[$w item compare $item == $item2]} { - set Priv(keyNav,row,$w) $row - if {![$w item enabled $item2]} { - return "" - } - } else { - set Priv(keyNav,rnc,$w) [$w item rnc $item2] - if {![$w item enabled $item2]} { - return [UpDown $w $item2 $n] - } - } - return $item2 -} - -# ::TreeCtrl::LeftRight -- -# -# Returns the id of an item left or right of the given item that the active -# item could be set to. If the given item isn't visible, the first visible -# enabled item is returned. An attempt is made to choose an item in the -# same row over repeat calls; this gives a better result if some columns -# have less items than others. Only enabled items are considered. -# -# Arguments: -# w The treectrl widget. -# item Item to move from, typically the active item. -# n +1 to move right, -1 to move left. - -proc ::TreeCtrl::LeftRight {w item n} { - variable Priv - set rnc [$w item rnc $item] - if {$rnc eq ""} { - return [$w item id {first visible state enabled}] - } - scan $rnc "%d %d" row col - set Priv(keyNav,col,$w) [expr {$col + $n}] - if {![info exists Priv(keyNav,rnc,$w)] || $rnc ne $Priv(keyNav,rnc,$w)} { - set Priv(keyNav,row,$w) $row - } - set item2 [$w item id "rnc $Priv(keyNav,row,$w) $Priv(keyNav,col,$w)"] - if {[$w item compare $item == $item2]} { - set Priv(keyNav,col,$w) $col - if {![$w item enabled $item2]} { - return "" - } - } else { - set Priv(keyNav,rnc,$w) [$w item rnc $item2] - if {![$w item enabled $item2]} { - return [LeftRight $w $item2 $n] - } - } - return $item2 -} - -# ::TreeCtrl::SetActiveItem -- -# -# Sets the active item, scrolls it into view, and makes it the only selected -# item. If -selectmode is extended, makes the active item the anchor of any -# future extended selection. -# -# Arguments: -# w The treectrl widget. -# item The new active item, or "". - -proc ::TreeCtrl::SetActiveItem {w item} { - if {$item eq ""} return - $w activate $item - $w see active - $w selection modify active all - switch [$w cget -selectmode] { - extended { - $w selection anchor active - set Priv(prev) [$w item id active] - set Priv(selection) {} - } - } - return -} - -# ::TreeCtrl::Extend -- -# -# Does nothing unless we're in extended selection mode; in this -# case it moves the location cursor (active item) up, down, left or -# right, and extends the selection to that point. -# -# Arguments: -# w The treectrl widget. -# dir up, down, left or right - -proc ::TreeCtrl::Extend {w dir} { - variable Priv - if {[string compare [$w cget -selectmode] "extended"]} { - return - } - if {![info exists Priv(selection)]} { - $w selection add active - set Priv(selection) [$w selection get] - } - switch -- $dir { - above { set item [UpDown $w active -1] } - below { set item [UpDown $w active 1] } - left { set item [LeftRight $w active -1] } - right { set item [LeftRight $w active 1] } - } - if {$item eq ""} return - $w activate $item - $w see active - SelectionMotion $w [$w item id active] - return -} - -# ::TreeCtrl::DataExtend -# -# This procedure is called for key-presses such as Shift-KEndData. -# If the selection mode isn't multiple or extended then it does nothing. -# Otherwise it moves the active item and, if we're in -# extended mode, extends the selection to that point. -# -# Arguments: -# w The treectrl widget. -# item Item to become new active item. - -proc ::TreeCtrl::DataExtend {w item} { - if {$item eq ""} return - set mode [$w cget -selectmode] - if {[string equal $mode "extended"]} { - $w activate $item - $w see $item - if {[$w selection includes anchor]} { - SelectionMotion $w $item - } - } elseif {[string equal $mode "multiple"]} { - $w activate $item - $w see $item - } - return -} - -# ::TreeCtrl::Cancel -# -# This procedure is invoked to cancel an extended selection in -# progress. If there is an extended selection in progress, it -# restores all of the items between the active one and the anchor -# to their previous selection state. -# -# Arguments: -# w The treectrl widget. - -proc ::TreeCtrl::Cancel w { - variable Priv - if {[string compare [$w cget -selectmode] "extended"]} { - return - } - set first [$w item id anchor] - set last $Priv(prev) - if { [string equal $last ""] } { - # Not actually doing any selection right now - return - } - if {[$w item compare $first > $last]} { - set tmp $first - set first $last - set last $tmp - } - set select {} - set deselect {} - foreach item [$w item id "range $first $last visible"] { - if {[lsearch $Priv(selection) $item] == -1} { - lappend deselect $item - } else { - lappend select $item - } - } - $w selection modify $select $deselect - return -} - -# ::TreeCtrl::SelectAll -# -# This procedure is invoked to handle the "select all" operation. -# For single and browse mode, it just selects the active item. -# Otherwise it selects everything in the widget. -# -# Arguments: -# w The treectrl widget. - -proc ::TreeCtrl::SelectAll w { - set mode [$w cget -selectmode] - if {[string equal $mode "single"] || [string equal $mode "browse"]} { - $w selection modify active all - } else { - $w selection add all - } - return -} - -# ::TreeCtrl::MarqueeBegin -- -# -# Shows the selection rectangle at the given coords. -# -# Arguments: -# w The treectrl widget. -# x Window coord of pointer. -# y Window coord of pointer. - -proc ::TreeCtrl::MarqueeBegin {w x y} { - set x [$w canvasx $x] - set y [$w canvasy $y] - $w marquee coords $x $y $x $y - $w marquee configure -visible yes - return -} - -# ::TreeCtrl::MarqueeUpdate -- -# -# Resizes the selection rectangle. -# -# Arguments: -# w The treectrl widget. -# x Window coord of pointer. -# y Window coord of pointer. - -proc ::TreeCtrl::MarqueeUpdate {w x y} { - set x [$w canvasx $x] - set y [$w canvasy $y] - $w marquee corner $x $y - return -} - -# ::TreeCtrl::MarqueeEnd -- -# -# Hides the selection rectangle. -# -# Arguments: -# w The treectrl widget. -# x Window coord of pointer. -# y Window coord of pointer. - -proc ::TreeCtrl::MarqueeEnd {w x y} { - $w marquee configure -visible no - return -} - -# ::TreeCtrl::ScanMark -- -# -# Marks the start of a possible scan drag operation. -# -# Arguments: -# w The treectrl widget. -# x Window coord of pointer. -# y Window coord of pointer. - -proc ::TreeCtrl::ScanMark {w x y} { - variable Priv - $w scan mark $x $y - set Priv(x) $x - set Priv(y) $y - set Priv(mouseMoved) 0 - return -} - -# ::TreeCtrl::ScanDrag -- -# -# Performs a scan drag if the mouse moved. -# -# Arguments: -# w The treectrl widget. -# x Window coord of pointer. -# y Window coord of pointer. - -proc ::TreeCtrl::ScanDrag {w x y} { - variable Priv - if {![info exists Priv(x)]} { set Priv(x) $x } - if {![info exists Priv(y)]} { set Priv(y) $y } - if {($x != $Priv(x)) || ($y != $Priv(y))} { - set Priv(mouseMoved) 1 - } - if {[info exists Priv(mouseMoved)] && $Priv(mouseMoved)} { - $w scan dragto $x $y - } - return -} - -# ::TreeCtrl::TryEvent -- -# -# This procedure is used to cause a treectrl to generate a dynamic event. -# If the treectrl doesn't have the event defined (because you didn't call -# the [notify install] command) nothing happens. TreeCtrl::PercentsCmd is -# used to perform %-substitution on any scripts bound to the event. -# -# Arguments: -# T The treectrl widget. -# event Name of event. -# detail Name of detail or "". -# charMap %-char substitution list (even number of elements). - -proc ::TreeCtrl::TryEvent {T event detail charMap} { - if {[lsearch -exact [$T notify eventnames] $event] == -1} return - if {$detail ne ""} { - if {[lsearch -exact [$T notify detailnames $event] $detail] == -1} return - $T notify generate <$event-$detail> $charMap "::TreeCtrl::PercentsCmd $T" - } else { - $T notify generate <$event> $charMap "::TreeCtrl::PercentsCmd $T" - } - return -} - -# ::TreeCtrl::PercentsCmd -- -# -# This command is passed to [notify generate] to perform %-substitution on -# scripts bound to dynamic events. It supports the same set of substitution -# characters as the built-in static events (plus any event-specific chars). -# -# Arguments: -# T The treectrl widget. -# char %-char to be replaced in bound scripts. -# object Same arg passed to [notify bind]. -# event Name of event. -# detail Name of detail or "". -# charMap %-char substitution list (even number of elements). - -proc ::TreeCtrl::PercentsCmd {T char object event detail charMap} { - if {$detail ne ""} { - set pattern <$event-$detail> - } else { - set pattern <$event> - } - switch -- $char { - d { return $detail } - e { return $event } - P { return $pattern } - W { return $object } - T { return $T } - ? { - array set map $charMap - array set map [list T $T W $object P $pattern e $event d $detail] - return [array get map] - } - default { - array set map [list $char $char] - array set map $charMap - return $map($char) - } - } - return -} - -namespace eval TreeCtrl { -catch { - foreach theme [ttk::style theme names] { - ttk::style theme settings $theme { - ttk::style configure TreeCtrlHeading -relief raised -font TkHeadingFont - ttk::style map TreeCtrlHeading -relief { - pressed sunken - } - } - } -} -} diff --git a/src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/treectrl24.dll b/src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/treectrl24.dll deleted file mode 100644 index daa47d250f1a23f6979124a7928dd2737121bf85..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 458240 zcmd44dz@5t{rEo%yRa&7Rs~%@X|A=}l1OY`VgzePX5b9Y!m5arsMxAu-uUQ@t%a~S zGsx-fz^*ZOw%%Y#Z|Ni!&%(=WT zulMWyy1!rV_i6uDPpE$=6sqO_QYjQ#%&+{l>A(N|pKe|cIr51ip+D@m{;0(@iSQY|dk{XDqkzD{oBuX~_A z{QIv5>bNMckG;Bbsg0C>%1@dGhP2^P14CVdc{*cYXj=`xj}8d!{IZnt|7Hygwe1(& zeEPSkV_>M|(~m>bj~^J?<%9ScuR|k~7j-3h(efx`77%6ZGAUJmGeR9RMqYS9@&cZX z>g1mdl(~@K$N8=LYYWZj9eI^kaMX8r(FP7@^7}vhR{c?~ZR9MkoQ{#PhDK1}`hCld zY8%<*m7D$*-f!dmD1LwY8Rfox_0`fv#{vA)z+8OBU$?+_k=LH$9Ng>lz5W^PUE-C~ zIbm)>LugR?ulj2Xjp`ja?u;q&Xqx=f=C}9WzUA6_M*_yipx=1TkBxob!0||7db03 z<*c)(O0zB5O>yVhc4t-U3N!51(f|r)H_bUK{Z5T-j>yn-YiX%8<_!BDFKu(&h~|Nz z;=m;?Etj^~&aR!d^}FNkY;Wpa+nm@*1v4PMtDlv5jY><7 z@jE?U$2C^w8C#jKpzUdB59@U7$D4 zD{?dQfyzz9S?PA|DV3m_{;}4#qRzJb0DvzknJR&-aHr8yZd{4kyQYa2y+-~qqb`*G zq(Ajj>x%c6>AC#=i`r=NH{}N({;CeZD4#qh{Yg#gargAw6pSYnGl2C|TGRqoHU$J_ z*d3miZyutR^3Bu#DU}Lw!S%?1pJ>|39?zrV%O3vI@7lJhTeXlm0IQ)8#Gc%r(b~H9yL)^xTxlUD(`YI~x+tavP9FMNEzD z%m%b*b6dnjYWa=EO{894+ScR!#vYyQ6n@O~CeN{*&BY&dQZrk_A+POY__=YYq~#b*h~wmG%l&P{15!&1ntNxbw2T_}BiQ?ew>uam@C( z2L5m4f9j5Fk>V6pE#h?$1w+u|kB1|44|Yc`t_>A$H^VNLJ~yQf61;+554DBs>Ci8K zBIRXDTBx-9Pqo0nb#cSRJ(05`Q_D0v@7)##ideAIPd4AlCFLG!?%^WtJkOw`PNun= zyLM+iRA(|;CSmUD0BdHTxkq2pi;X6GHQC=xkjYHQ1l#f49uf@pd>xJd1lDUoFP+b`X_StH@9&q8ox*8TAn$7jT@`fvsG_r zLks4z=9zZQ9y|QLmEJ?EiQGd<14tsu+s?<4CBnQumb7vUJ_o`s;;eR0{~4M;H+x`B zBsYE!O3a!sQX?h=)y;)y#E}$9_gdQ&_X9e_NBM-sWg zGmjA(t((D5!iuh8boX-U_LFXaCcj*!UTT_0LkA&!dNwdXfU=$CkyPSc`Jou15q70FLM40JG&!!34)^q zR!cU-ot1?hP;a0wdC@2@CX`t``R%^!B5dNZhjPNJA{r~4aQJRi@CVwUAd_)wN2xo? z?WGVaqNeUH+=CbrbZ8_$zES!poQiGX>pNP4 zKL@|oJOI`|#69l!2*8(mD*CBxFSXguGw3p`B)jt8<3^wsTo?3)01QK17Ts9CW$I zbM8YQ!$HzyYM4sxHg9fLl=1J^jPcg_P;1hHo1)rhmPZ&DZ>k!f=Q%Jo>+$_EYpvUQ zyLTU#JQ=lZPW(mVz)2Q{J8GhTnIaV%UizQ>Kcep z42{TYLKr8HO@0j{Z)Zjn`36{Y#21A{hkZ$Y@GTHuO#lxowjtkxLE8+v$IeZJ>6a(4 z!A6TMILh509y5cR@m<9R9F@TS8=YO7x~6;d*OM0{oOS3^DLu;FF2ZrxCm%>b?$Ng? zDRT;tT&$36g--$I-Y+OH>*VLNfhk-;pB_G1dkYtMHyMGLQ4H>;Vqg}!iAQU-yV0Vt zM-{#!0wFi}TrC1H*@gI%0re4o?WP?hVv5kO7{Iw<*B-@0z+snj=sxqx6a#$a3yNA&gR`wPuFy$B| zmo?iqDma#XOXN`-C}`uwSbn3)HmP<(>5es0TD_V)ba|an4=88@l+8NI zLMc?G%6}>)q$2)`S9;(+rI8nZ782?67;>D@gg*S)!?mHpVGxR`dx^jjllC33otQTu zw8RTBV9B-s4YptnIuLRea?JyGH`f!2-??cq_A1kteOah~ldIKKz02*nP4uXvDof6>i@6 zc@Z*OZ8Joo9d)z2nb_R0N9f|y%ngm;X!apT+u7*a2zSu&SR!|s!1)SpiZBY%kJ9a< z7^#`vJT~s+B}gI;k6GFcQ7;+IWx||z03GHg)Hw6yVcZM=iI6R7-MxeeN6xO{nccZo zALH#W6ZUkc@+yKmS#Ni~L^pA0sDWNZ2gVVOigUSW5yLoyOAbck(RZ%zzB$ zcS)BbV-n6U<+|`W@CiIOjYxJtmY!t3eubko2FbEd$Y)6G3+}E%tgbbpHrPU&)?bOfob-Z(fmaub+ z)DpNOumm0pl=_2l%+5xwtQ8SAsRN30ZF4ySX-D!JTLg>qN>yjO?x+jx)7eI7Mj(a}Iy4M0s}VE28Atn2&u8^@h;0rDbci{$?9fUG&nX0c!@zj1y*}Z5 zSn9d6R%C0hm07?@B(T!b`f5d9v^6Hcy+jBh^W^b3e&L4HVUfl4QrP(`Ek;T=i62HW ztQqx;5ra)ZmDGB40!11T+&oJ?oP9N6E^O{d9zl#Oo}0e1Ugi<|Kb~G$@BZaII3)aL zsu%$4neFF^l1rX$KNUFq75GkTNtjMZY%O}t4Aj8Dv0C|M+qo2n>rx^DmxA|8oA@8z zzZ7nKlVTX6sbz7K6UMZ|&)Ft2Dpg;+T+r%lAZ%-!j!`L;+38?+AmU3}##yfJs4DzG zLDZ@^c;o4BJpGNQzwz`pUg~>6h=;1?uN(haP?(E->1jSFNZgZATo`+bNJ z%n!s#kw8mzKnCL;a>EC5CV-I%05k!NOaLPjMg_$^R{`(@N`+6I1qlL_saDPUO7FIF z_9(zBd|y>l$P1^z$|rt7U_q8U&o2?92A0;?xb5JE*g&y@mw5PfjE+*d(`G#Z*~8}L zCq|S?>!$J(FVLDZD(`|~UYj1=pDL-rGq0qxHJ=uZ!+c{crDc}o&$rR5)-$T(M;d!Q z<(t>aKn5mq|67kR&_Jiyj?Q1poSK|rFAW&MHq(?h#(MTJ6>S5l2QjkarI* zKNop%9nHR)JXPZj!k<_7<n*9}KV62R?=)i-lsJ_%--M4!WV6#L#9&3RLX* zlu7j8?5~+NI;f-Fc{Z{{DGx=FP{HjvpJEI*xeNZOZpUgX`x6R@^_S3iV2{ivhGYkv z{lYb9Uzi%Qu+6s`aQ9o>(LKQBtTLJA4U`mxaRv36X)AM)Tp)3VQ6!hB>sNXSojl@@ z0oX*_#rhMWns+VUN}~Zv0M&N78q^+{8ORq6!j$dR?r#vA7IPL;t_PTKYs84t*jKw5 zLM$up^zsk|)8(l#EI}*tkqjkgWS&|5pj7FWnOuuHXRJW}mf+ePw)#EW9GNg8Fy4_L|a5 zD$f<84=-d&n1JjrC{g$||1hXV2345t$wzTWcvL5kF5*!idqc>+T{OFxkydt(AkCck zauWX;D`1A$+igk!KEhRSP6h|>l)1sm%n?jtgp6+8q5LkM6HS8Cgx2 zAAhpL)9RD6eER_PiY+IGI{gk1dM`_$KV^5=W_t6CJ1o#kCaE zUZE13$DQ_p^2>_UkuqPSW!ZwOy5C(b5 zj}i0)*7>r>`FG-;Wf+<(Rqwt z+WsH{wmjr{IBUm@BO?A*3Uxt@wQBYp?WTY7e_Pk8Le)_un%i)@BFtwzzY0=hS}(z3 zNv;YN2bcNXPOpL)2ynauN)l%AEUJ2u1yBv$!|jpMOY!h~^p|15hmC+IZ;;X1|@P23*GC=Y#=`Z^Wf0Ln5*ltZ%<8xudYvyB}~+W+>q%N-l_22 z^TOlOZ5f9GVuh6?KyC);B6+E2r&tG_2Mgm?{1lcJkgP<9&46}s$sgh#WpH!@5ogbs zl;(x!C{bRh*&WA)(WflXf%9*fgW)~kWHlMdIa)ZcsdU>e86U65=(qrt>=ADjDEU=7 z*)zDhlMQ`3kma$UTfYu9daQkGNKZ4==;#@Z3h-oadA+=BZ{FpZK_ss{m;3^L+0U;W zfQ(~I7%U`93@K@CKOC2G)rxHry678E@!G?pmEbIPzSy9p+l6k$`i{^L4secPX$+$3hv)4`pD-W)Wl=7 z1Q&@dJzYf6OBY)$Yk;!UGebN*AtTA2@MqXVy%;kAb7Ta(kQblUCL+!n791Ge5E&iD z#I_qyZl*3SAWK^x1k@rDR|v?rjaF32S3frhdO77lf2g#HPYtt-fSDAu^F*5EhV~ak zbYk!;9ec)P=Vq}HM%euiiKN${8yMT}VS>tz&^O zW9;ObWP3apX^^y}Wq+-;YnB%WV^APv8=|Azb|y#J;SH(xZ1ca3f`pL_|6pcYhQED2 z!xz*2L-!b@rQav)cRHbjc)fdI)x)y<>=Tb;Jz5CK95LW>nRRTEYg>eDM2Ezk=Mv^h ztY$>Om6!-$5*^9g{(;1)T9hSs;IkJ;FCA39n3|I{;XX^XG#Bu^toe@ZQ@p%J%bFJE zvL_Mxlppkem9JVOu5SM?2b94PH5ZJ)${eF;5T_FD;CH0gyP%>72CdFo@0@Rn$@BHr z9j#^itDRu3%%dnI=L6Pg;x+H1qZ%UC;{#Vmo4*kv2juIDwO*BIvep<^bjf`r{G>KL zcQHk@!!n+I20*b9RXI{!9h}ud$vNDS(_<>hEZmwXm?wdJO z)M6XXtcc=eECQkXxp4o(TA=pzv5HRQIIZggUoYyKCj;su(kuMVEZw=k(4UW}!X~Ve zn6*?^4M9!2xfnVfHcN{X+^=VZL|OCwtDE*a_E!dSgQrLt^FU=uH$f<_1tlrY)D}Qe z|5`mOS=KF#hnz3&@%c$u0fxfH6@gHr-M#ESpO)@q$0WSVue!irBs$N_P^cmI3uUfLUl$DmCt!}10HS1yYKB4l^ocJr7zw!q z;uVK=1FI#;PKnC+qr4EA=tVtCclb)!$~>a|bu|V&h~tYk81}sZWi}KI4L$lD{Z`8# zTHOydDz%wuz^usjO4O$;-%vc^nG7X2>;=$PsaiEO-hIeBzt@7u&I&#~2)EQ#)h*+M zic6|0o<&6-{gOwTG{^Pba=itPD8o}dKP>j{_>Q>Ewas?tD-yRu@0k6Dzn*ZO_oQmv zS(O`4VqpJkWc_s(g>z@tNZy1;1wyeBB^UkHL44ISmwZ*Dsi!^Zy`+R5uhcj84J7*r z+7MBK@h_rVJtd|bfU&|wLrLbP(dJ^vRt6PsZWJqQC zIdj#zw)^8_Q~RN-YBZc6iFfEOq8(y&6RDVkv0OMu5FFT>?PgFjD<-hHjHKd~Cs{5hdMlZ~qjQcOMwN&cx5JLPg=LL@_SzDZ|p|-642vK8S{5 z*f5VXX#(**AL{L|xK&Q8nm^)HM;~`_-}%EF*7;jsHGg~4BzDUDJ$jKpe=hU)b4W_( z&vOW?r;qFaKXaJ;)!b#TF~LV9sVlf|?>ldiedi7PE4{a0@*p22?f$fZ3>_bya3wLh zaOBQXsd@se3jT@J2s!UN2gQ5$l{eMdY}*L11X$Cl0*MY*vBu>;He7j!BNxa^&81bt(6+8aMke>~K=RpGma1Xq?l z*V)Uq*XrU|M!Qh#_8EeJ482h@@!*Lltzj5FCJ%+_PR6jUH5a5DNS-aRG zWXu$g@*tBO#U{eu=0+Y%sKkTD%8K5=a=(z}nA@u+%A5h-@01B3@wGBd#LJhLHWzLJ zEKk2yuW!wJca31%9DU7so|=se-@pb;pxEw4-ROAZz zPWZ>CoC4rW=ojc$|Kpcd<}M+(+$qP&W8C}T@%22;owZj?4tW2XlaxGm#j#2>Hwhwn zWiLYwU%=ou{aNQ#uV1;KWEwrc2*bS{U!+jCDz zFh{c#cL%HSvh;A5gcL}s?|xESMbUhR;t4Z{<=j&0bA{d9mE~)@nafHTVYD}Ss!6f0 zxV_hCPS;!MJ^ih$(dSs4;0kFl$u|(-xR{yHq-z}`DMFwO;n0$7ljT}V?XZqf@%)4q zK=gX8_OzSBo8^A~ z0>+m+vmdfcnqXol(FGIB+KiZCI^BiCsf0WRXQ|T^^iSc-jBlo!Y%kLz1(1e5q0|kN z_K;BsRn{U^;}@RQoZ66?+ML~##7vyjQji44h?(33hL;vLc>U#K|7kZvJvh7fh4Y60 z_;B7Va2C#l)1u_(N^n{dWeV;P)V~ZpEIbIxa?_iqIV>N=y{NCJ&t8GJcIgyT(xRk9 zUzKM1al?Qk`3X(D4_VnU0HuRxUaLu^ut7(I{18J*@@fn27hduDeu_GuCSoz5g^2Ux zo2BW(SG0jPctjco^TH@JMoY>cE8I;ld+l8=FnBAmjIx?B&eS%iH3D*eoHonCB7VL% zUAHQ?(0W-*ug0zrz7!0dLOI+NyK{|{R?;O-zzz^%A`7l&H`w76VEzO6ZdOJPz`(F2 z*SwN+DSPfDYAU74)-3nQZV;IrZ&2Z&Inn@Kw zA4!H*<{WO)dusFJb(SQj6VB2ZY6=VzqJG|3C4%ukeL2+DG?QiSPp_&YtFv%CVFXzc z%y@lF(pB?-+= zY93=i(Mxia%6(V%Ix>cHC|DNxo}-Xn+1DhHIiF0pQ*B9rn4u;Rs}JIpU_LHapT>Q2 zGr2X(wF%Y(mxw(u!;#%OA|J;?KT#7=@td88KxsFJ(EF-?RbdeH{T=ir48nF$vu{Qs z>k|see?XfoaJN zyY(ZF5~@KxOoa{30wl>A;&#(f@7{_uE`;<&8%djXGZ`Q$q7Q|H?a*zARh>uz?y9yml89wG7`iT&!i2GIy7l_j~<&Gmm4p!8f*SQ zI8epFHVI1Uk}Sh~UAR%K1l7K}a66-7{Q2v*5_~!B({&H-Q@6OhY_H|cz>uYz{eoT! zLQVl)7w-}xXvbMr{i8CDsFaxDDqd_~yi9X5&#Ns-BKzND-0HfCp4-lyu`w~&Q8h$> zD979D<<5$&(bc`GjC_qKN=m*am-E&GArNqy5+&Jj6e%k}Z=Wa`Q(z^K_`!feAJ7kI zgeuPZFktb5SP8nnlV!BrDSz2nN9NDADlx%|M_gs#$$}3VFZIXDb=joS4hl)7`zQ`p z2bNV~nA$TaN+0fk)!5=FVI`cB1B%8r*N|ZLQSy3OE@ds^-b;mHoE53J&3IVGx5c_a zo$CK8{D0=?Gio>O?UxV*Z3d?1$1n{6_b0*^>`##&&*GJZ0V}r7pM)S2K=Pb%f?8iK z{0asuZ*A5K+x&2vh_TMKr ziXD#=oBV#_rEO(;+czISjyiEdmHa|~y*h3%HC|0PtCOSP#k5-kddAhyC7gE3I~~&Z z0SwgJr^^KkERwR87mW&2>9!)DANFm(DxZjh9@xDs`%?*Hz_zRAoZ0#+`>!N#WkAH- zCPtBM#%?9)v_9M;cXJOk6GlhUD{IobO34$$tE_~ZV;?qI#NqMvbJs>{MzX)&?_ZBz zAdIIC7T*f=qanF8cXp$MoO6>$I}=8`--Ti#H7jYk`0&L&<@I1+){KRgG;ksLsdU6Ks-j zVrh1W7m-L2A(+xU+xcKPOXC|NX6WtVjn<7b!DV_^?f1SytgbVf4cRqrX&d@k?B)&h z9kz*==!L+K6D^Hn3xQYONVCIWX2&2oFZwSkcA3&kw*6bgFv!S`e;8E{#3AUp5l zA#v2w=3s!PXEY7tJMV}0(cn&5Tk8ex3tjY3@gFJ(k2DxZtE?rz*EC2E)7?GxppPp%ZZ; zbHjcx7uTXi@aL=$-}sGx*uOz))`9@r`F*z{b`sDaJqaC#^5bkK1K*Bk5Fcho{W!zm5u1~cNHeq+ z)(R!tYu){v5y~;Vss}zcH2aWZ~U1l+Kkx5KQbKS)q_FnC5o#r#4!X z{Xd;LDgl3ayQy8B$Q`cbtc7m@d#;V$TZ2~BQ?a!dmW>K-1`&u)K=GtaCPBr4nG+IVU9#F1>gwI_ph#WYVbO<3`kQ7P(XQ;LC1+XOM<~}H5YwIBPYlJ=t3gAuxhZ+{M3XoXN?4nG&7Hb`?LMzl+dceI zTi1_B^Ya!C;?4~UWja(N0L>Bjh_^uAz^}6uE+mIar#x{UktZ?dSAuNY-14Xl-J}=G zk9fS3APq#AcMmm>)cYPO77zbPGIWw8@7%7{q`bV3n;RY#95{DtHRmV#*74eQelHJ; zUn|RNN^3@ibEj82t)=q{0ws}o_sV;}(RlubUEIkZEW%v~s**o0>;22*^wv$q=D@y* zr=QjuRz~PhavJzyQ#I=S1h!&3YDbxlE(GZd`1Yx}RM19Ko57@9Ro6*9YsO8X>Z!of z!&wV_{Mah zU!vyaL~inBoAUQNzLTUaCA)?b9lu0Hj56#7w|gr2#l!Zx1I ziSqn3zZLGL91^#6Q(>$`1wB!snICbZkQRR^i`X574|whIujXsDBsw{1$<&BDq=~F^ zzGRPSNS=$FsgLAr5t{3YmKWbhFDJYFTf5kK&C2cv(eU^|bHB`KUKd6&v2q>z<0{K6 z5f#(4wvN$>PF8ZA0rJk7I*6YdXKMXK=kQ)n2cqbr|0UjtrEq;W=t-Cdgx=$_(}C?1 ztIYLHq86Ozvb}RIv(3$o+~E~LEX(VQB)kZa%ado=hIR7e?eN~zyKFKNWM3r7~FBCg%%u^yQtjsJp&35({KhOR`f5WP69#?@C{-alGKw-|JA!OXVT&MpfQr<=q_K{Z<}w{7kAR8h(5b*NUL|a^>j1 zSg}2nMHGFW5<|-+Z{}#RlL>Ei_hVomva^}mwg|WO9`we?s8lru_C#zjM$gcG| zN{$KIx7z)F^rtYL5eCTm8k}1=fd_KF99VT?4Y~;@=$xt^5c*R%ijskR_UIpF6WCI{ zK$BMcu_k$v_}aT_lH;-X2AZi;2o(R1Nvjdtn>y8`#a9s5cjB1wT}fGaQL~O%ztXH7 z?okc(cdwPy6K2vBwCUEMh2&D(Or630q6T-sQZ<7gF8&!Ms0KR|M~1%s+A`;u z2xgHcuP2w+gTI482IpTG%YAe+_BK0pSOZV}9a`^h6Pp=x2eN4!<+XqQxpryWr}MYr z+nXK`Ak6Q&_2)sQRz-92;HXL8FHduhymTdx45B4x)m{Hh4m#mH5$)+^kRi5rf*87_ z)U{xH+r}u3n^Qjg2GD?=VApUE_DKI$P3+YEc{R+b572G^?Y{CRA-hx#iuqQvxn4K` z!+8#Ir~?zu{!!MIoL*-`ba*d;oE4hiI`7?SqCa!fS$a>eNY5BKv{ zN*NAZk@)v$418>@5%_JkbBH_qky=SZ7=Z@%3svE7AA6s4N``+yLqaz6ZqdcPkD*iq zpL;V0Jjjwpm#j8olb^W#>v)3Lc(ZK5wK^cS<1J~~tZ9jfM{3ZW{;R4yCdp~qDkQ@n zp&snGp)wXOlQSe61ry#2jSQQ;odpls5!mbAaw((mT7^3he7oI_-}*ivBNM>)u`Rl_ z{oIYs_i^EV@z0!Gw6;g{jY7#g1y78`^V0FoFT7lqR=zCSZf3A5B4I~cQy$8mDYj?e zmZvThFL!1lcS_6O>KMnA=2`8XwKt%K65-|T&N3@El^6$u5-$WI?(6jKd_zZ8ZWzz; zVIHZ=TNtXsO5r!ROjlENfS8+-d!nuR2r{?asRmXaH$;lz?WhMyP{sZIREcR?a&9LB zBUjAMc`}ssNP$((mbnaI#VYSeaCSwL+r(aa@&0c^UQy#RsXn4ks{cl87E*w%%zq#= z$w%)f4pUBVHxJ9~>$`38&Ur_Cu3;#AsB^p6>fFBl8<78k$lnA|2#?94D5w;_S-KSA zUFjw{l9`rLk#JU|b?Ttv;l97(KS>U%bet*@)KjZNg7+EOV;fKJ*=t?@cdFXyKMU+A zwZVc%481W7KJL6^<<9gF3+69>z0O;EW@D3tCSuvroO2_&D@(1f65P6sw{d3&S^PC3 z@1Ywe6|3tNrot?)gfevXdOI!{@sLRPHOavopwCzy_ZyZ_rEEcD`A;$thwjsF;d3Y! z4TZQb>{bL4FS$j8x*+kVzg9lJjE#NB24@+g)&ZZwd%0Njd5{P)Z(T04acgg8xs`pb zo+;^kIT4=TJj+^m1qnlmoR>x-M`5w?_uY8zY$6X2HM7WyR(#ENo@|~WB;&jxls_%% z{GoX!_r-5{_M0Af`)_a7GP=OVA%M5SyIwVOb^}VQKEss8=vq01`2OwiF%cq}(*8ql zsM%t7?y<*^_-EyA&`D7896azf4rlcyo`U5F8cYa-{`?XcCO2Y_yh)f7b_oUnkRV17 zFALaK&!`BpksekkSP)I*hBW|%&!>ZaXgh2^+8iJLNqhL2c4v)sh4vTPxI_VB-zObUgR+8Ax2a`|PI@&YM^Wu@afzdXAz zwI3^co^)bKDd4>)-& z{ldNSI>rGnU4#x;JGfOJSPP$GItU3Aqk%mHf~9C2>JTvTY%mAV9`caW*6@7pUDmXQ zQzqbyX!yHb?atfeU;Jh z=v69+n%Wx*$1*HgU`Uu2jv=1V01RYeoYhbAKdt#o5o3i1@p8)kd*q|lwN{2hmPDlr zD=HrruBV{NLv2|rFM1Zy88tuhf~r{X6ND5^-N#>(P^hW|C~2`5Rt<7dHJIuP!nl(M z{mBVzwZJUfl>DmM-_D&{pItkrB{Amv^@w&s*y-41Q#RBW5AgUiF=lk~2cC7dA@yOE zkX)jWXa5YKflM(I#oV(pzv5ZNPRaEAvfhBrV&1iC-0D zyGj{9D!mZx?!o_3lFmr9+SdZxSR>J7g+zZ8h@JC@BC;3}AhhT1np`KzFv>ta2_?e2 z$kjm>c8?BO*%P=ZY~df|!PO!xm;#xwn5pg7xb-2RJiV*lx?uwogn<3l%e2^Qch(Y> z-cZ_RcZwkJh#l#PD8DSyf9N7-V_`XsDrrZSAphZCA_oB^tbSjkBxYrQ0Bd}@n9wRw z$p&|_Uu+7+66T>kBQY9ziG+1oYYuy0AMyyF7-BGHdXy54CwqM#=1)dnRwkg0GzzqTYMtyi z7yN?Q!q1eeR8riiSU>{`Lc3*n0W6-?XJwB87CFsQUWVpeEug6MpgktI1LP`7rb*0R zXd@;^#<5R{#ifa6Xp0;6C@uRszrudgP?quZs(QEg=Muk{u>Ub&O(k*UgXwOS9!!HR72O2_6MHn8kyHQxrlHja+0C?vEZ7 zWH2NK@{0Q?yH+r=T%jc?5f}CSNuY|$T@U+=ZoC+jr?Wq!8xh6jZsLA7#XDlo&PaM& zEu%y>d_yh4z%oo8F9%t`m7pVkJnA=_g+ps~~&%Aqd`Fcq6-Jp~KPc^2pcnPeFY zTjvo?**_~MVX4?)K}?ijbWs*&#V(1Y-)0|(coS%$zRtrZFQ|zv&coF6>1HX^oG?Gv z@Z3|JTjmMG0#t*x^*P5{^fLEqKBk}Ns_+oR$#$T_6%HY2v6TJ8Qe+<7;$KzHWoOWO zVerRtliH8*S{XTKl0Gm=UjV##E(S%h6KMbR+y>H9dIe<8Jn?{Z|8O1<9!?v<01H;t zO%!qw^ksHJRIF$h9uS`K(WqeD&(10-aIp%qvdwt;eKdQ9NKB@o>;eR4KjAC!0SL>x z&Hga%V!snvF*A{&Hwc-Evdj%T2`I}_l-HMqtjzn&T1AAU4k^4w`JReIurkle5CuBX z2mKYmBowV4!3+1|5o{7fUwv?-`|aA~;6zxB5G(T()%;Swr4$4qCYTIvD8Sh{-=RAL zf0UUC$urqh@`0G+N+~{B@8fYjw&_zI*BBmo9@o#H@pew0_o=K;LjkE3z5?>q-If{H zAL?Ls`(g3&VFl>BUOcq#ffV`uqRc(RLV0n;lZ7XWG{Ddxpr8i*7+7etuvxmdUiNzD zhHfZ&VU;m9I!`%}1(_X%|A;IV$$@|ssWDz=7^xvLQqe?Cd9O_3H22WZU#`}=o za4lolZ1qG!+yL`k?p>A4)hlT9I2Z;N!p+0-brtTuR`7`5jN3YJiWdmDldXJ z17%W}#tkbpiZZE;_L$*L@WB0DA25FQu7Z3|@DP!F<&Obdc3D-{hb#FVo-*N7j%i9= z?7d7l&vhm3{+?J|xfLCT(j$KyIc!*^bq27S55S9dg+j`*Yr@(sEF;yyhT zyt#Q=Ns%~{*M@KJN-nr$?U@5H8Hr*g(Hi93iRsuu#yblcAJ@e5ZJ46S&U{;g{Bl1^ zeyg!c~0ONtS=@CkeJ8i_3JDOJX zWHzNZ-0gaqFL}6v6K|8Jm<1Z^Rx=(K+LdFSwnA1m_$mlVrHe7d{AV?hQ$Y}dc+tl)Xh)833JzP#|u)~o?3HwEw zijOgC`ADO{67(Mt$!uFuuilyVym#;8tsTBeaFF^67XRX7i5!;m?uI;%ES2!$21Y^T zN=lzo?$6AVKn{!RLMW(`BdOm-{dgzyN+sRgRH=jqF;8MZdLRaT_xN)(&UW`LQR$@a z>s38Fgg5cYT>5#FAnwJ z4)T{0nTFK6L4G8k2RV&JzvM|w0rMSHPce(VLaf?+B_E9kyXdcsAMRzDu=-vXEvdYe zhu}kFya{uY(xulNev0&$I;fq0+NC3#vfW@f)E>S`+DrY7rySY^CYi~q^OmxBY8#Rp z+sA0JN{PT;;Vn_xihKb+>9`ucNBi76H*M&1Z|3#APhVRf;!_SaD;6^ar4@GiS`KTh zHIWAYfzC=*f9Y#m^nKI+`F^y%ANASqO$2T)CF=uxm`JqGcub^ipZku!_w(+T;gsnK z=Y2!hkz|F)W5M*ncSZh9ux8!^+{%u4QGfe-GTCFO$hCNgE8@$|^s z@_0<7YoGTb!z%CPyHXK_%ADR6XQADrKX)jGK+K#v1!fW~%#O1>c(rMp!u6MkrIpVI zN`yJ>d>w>OhOQOc7!PksM$AOkZBi$a-PuiFWPl1qwVMUPcN$~@vq~bez-zVOmes>yMPf>epDvdi^+$+D^pG=gW$OuvV z_-t^lSQc$&sXPPC9AD49?QVIpAM5*D{Zh^<_YV(B7S9f17FMQ31RJi~XzGN*pID0| zZXL`|=L$@4ra^D{g>i*NsX%3uqTJb1edm&KWI{<`fEGk+I%VC_^in=GXs z!L}{0Tes&H`qUKe2;uh32)c!MY~AAjp(tJXx=)jP7^ba^Vsye=!<27wjEaoWVP1D6 z5m{+=0>`K`V0Xl=49t7SSY~er{DqRCg`S=End=MAbAGyA1~}2FX1*%9t#ek91s>io zXGnTQzwp*Mf6}|TA3B?*McFo2d`RNq;ZNq?&nYpjANlx+nA>&0o02E{13s)W;CpYb z8gTMF87z2em*W*a)S-^tE<;_-fZDyWCRta~aZLeN3`kOC-$>nEIF2Ea14}&yP1S_E z`GJ0@9nKhx)GRCf5%{WKk9PmyzaGu&;`|Ez!r8J=?y)GGg8bD)5ix=fWJ37Pl31i~ zfEwsoD1!q8*$~Km+;l&xk8d6&1@qnaaVJ7g)OG^3Esta7PQhn1e7F2u*H}D0VY=mo zxmg}WJXJIM2#PD&VVm$cfBk*@>$jC!k3pg|MZ4eh3&xB#=(juP^TjrgUua$0>C^gL zlFJns+Z#a@q~(TFtMl`=!JpEncjjw;UXYd-$wxFRrtroovf3D^yySZLwYn|M)e&X+ zPVuoozx3hPdGFo{@6$pzqZ5)w+D(x&U!4_1SY6&Bgrelx?&1;#Oi~QqpMe

9g+0cd37MK;^vxq7=&CxSVAI8F8T>l)0{OEJKD;urX$hXtkT@5%e_$0swP<*_T3#~IZ7}7 zIO&Yj$gsAG(ci;P@WIGx|?&lTc#zX>rU=(2(lv%5B+n9EF%$p@oTZMY#%v`ttRsAjo)9mfK#p-XJz^Be zHP0Cq55!nvk;OfBv&@9_6>6fd>Gb9EzytO2Q8nf$pWf;t;el>GZpigVE9G+#f=ZvM=KkU3iL@n6&XOBtR&2qeo6&5XWwv#&6BK2cIi-!o<^uBP+WA zYAgJ>&w63*lz{-^E8Ao&3Y^mmztE_k2j`aqIB%MuaDJDsMi=f^U_4Iu#ekI^3>bx- zwC-taZA#7{)~Oe@U1+D>cE?j`&Qmheg*#{|;;brsl~?h6R)|3UB6%eQxh(usI`h#B zq6&veTABvNt8x1;Lh={)_!7V~ipv(ET`>lI_AhMaP&V|1fBxVzZkqdWx``RJGDUiI z^qOd~4lkcx1LeqHJ}nYk{={X(bvz-8_a`EE7`ZN53YE<3v=(IWH+#14rsV$Y=k1Dy zPwh%RD}*ZNV-PHr@Jc@2NXPOgk+HAX4zXy@9ySxZ!gg2c@F>2kII;uNT9jtX*6Lqq zM8t1CeR!o2nqa5p@jZV1FQXO!k=jzGy7ueEA?6kt5~6OwVBwXte89P^@10v@Oofrk zuxOaH+wE4~elz1Q_i74fBwU@D@VyTs=vH}rVEF!8nLggWQg~RKd1>&#F*+09sYk;%hKUF(}>j!|K_BAE>C;!^=N#MZlz&3YD;$7FX5aRWsdE9+K(mql@z6loCY) zjuyk1gtaz3d1tr#OuIP3c+9%6@fw?t;wIZoBi;wZL8V6tc9SOkxT>-AJY+e$V3d{l z1KlS$>5@3ZYZv##Ogr&k^79YkvE*LIO^U&IyF1t@nE99?vy$z6=;z-^>+5+g zf5KG#RY-Y!8h$0kCfHt^rPe1ERVNumAAKxX1TcL1FB)DOu_mvrSs-mhh7%Z8UQ{rq z^GH(B4>1d*A1tNbGIcCZ5!2y_&KTQlomCz@TIp~qf%Anzb5UoF9%FOYzxOcNlk$<8 z718hxE0d@17)Rha9~SPU;b8uv<}sOE`P!!=cDs2(<~V9@7TD0A3r?e6B>eGp*HBUj z!&yezu7+6nmFqgA&dbr^8^g=2$!j9c-q`RLW8u{?Yx2%W__>HR>A6^L?EcYkY3}|L za$~fWJWeg*T_(iiMXYPl!3o z3Ill&#G_!(Hd%A+rIsCDEW*ah{uqgGZfKyJ!hKvONMvg9+BZuTt)=8)X8wIt3C|Z` zNFtXXM<10_RHhkN>jx4>SJaaN5*@L zAmNS4lf|iCWA4;GUzJG}WM|i2JHRZE-fPxHoDMcO>HH^P{}&?R%~tME1|dr(LO<=K zPSc&7#U&xI@B@pfXJvjWDm&*d|8`XDcbK7tF4|2_Y+R9PI1z(oP z&vi!?zY20PT*|a4A4iP73TwH!_~8qrxk^hZ?0?QQBk>MvMRRyWNs|+Fdn4uC7ngzm z1R1C>*2geby9YudOt6xLP7;&gZ(KgMlsf+^4yj@9otKCbUX?sIW`69Ez^h3&VzP>{ z-fLftncRH<7&Euw0)(P9>mH7A9#v4o|F9OyCmYNI-T;c8`EGio8LBMY$V+HR z5E?fpoAZZ8IqfeJe%Z=>PadV0%eu2C{M&QQ5ZXrPm11)wydi2$ULFrWn;cHEIyo-& z5p(*`n29&AtI%3FhPu(=>o|p{#Fnw}vk?^iOTvX05o@ud;pI_l((;L^E+iX^z5EYJvq!&k&nKj#lk6~pgYiGPJ( zoINCxevgepAMp`&jTd{g9%u7^wijbHItC0|*({gBS=0JZd3GdRS9lzww@jar{5(Oh z{IwfLctJK{77FWyAH2|WBK!^+epC{FJfIIJd!JTOGK+_|q~0!^N0A<*>QRLAZLDKQ z2w<}d5x3zFlxFJ{sN7yi@Fw67Po49U0Dz9m%E-}@J`3xx3Iep8+bR9G8{NrJzBfOV z$wvYl5)JpFWUWbSL`)Xm)IQqSx%!Hg8;A{DC0nE6W#|j9+@C2I3%_YC8l@FNh<9%1 z>?$<*_s+A0d(=ztO%1hPG^na(k1WWrIZ9*%Fbh+g$3zH(A5~Fc5h=P)04!OKe1a2k z@+d!|ZIMB%b#`nB1&b)KALZK&3#2`x!Xm7KW6lussJ4Szxt#_OD=LpF@z!--P>k3e zSwFtvr4ZhO9Ue2fwMb5~q^J65ST`xpK1Ox|M$GRNVEuP45wz19{dI8m2a-Z(zDYWXWjgv5-^x-M*Dmb!T6e#RZJ*@nSeDL@G z_}f&RXWY}sBk@feR)NyGhsF;7MD$Q>kjf=5Vm;s z7$y;Qpo>F%`DMm+p?1D(PMhK^$H2Gj^bhJ$FaM%3b3n~{$K0dM{zy2Dl!(;x@H`2s zXmZWiFOor^T*x?*7lu#oN}W$g8%^V~@L3;9ZD)PEm#U&Lw?xg7HC$t%h8Q`=Ipe%=3EJ9%@$6N9rEQ>&;9BtIkH&#s->6T`Z-IB-PKE zegOd9`&E^wc>4n;{=&!n2^4UL6!L>m=i@sjc~6fza?RA>81gm z@rVd5r`WjX=yR^%f)@RDL?r!$Z`nQ%SmEW?!oTaJ|3Jc*Jhm}x7pG3x-I%w|1 z2un_8TmqyH^9oba>9yio5oKq_b=0gkQ@h0642kS)BJi|FuG7_MTbim=efM{3!37xNREJHT-n;D0bsAyjAF1x=g!xOtw zPe;vFII|8jE z%YgqXBqC(v)5zDzMw6wBCTF=&NK2VQvTuv@37UMF8*_^``5c!jAF`X&o3rcBU0(e0EoM*t#oYj%xF7S(H|Fhgbj&1HCnzOcg#imhA zeaizMhNpooQ*jfJ#??*r$zh(L2mLi^R1@zC#B@L zXn6CSMk&5+X3N0j*J3rV#D;H;I$F!hoDa^aVJ*B?kYDqh zew{exJQKAZ+3MHrQzP|iVFV?~8a!EtOyof0FGiQ@WUc#|E^_dy;}34}s2T$FDe{Rl zQX(--pr$67@iyy;+G>c;R0yrp=e@6X1~mjN6R*bu>n--vcYZ_u-Qm z_-Dg+S_Qr!27XlW=yLtfgl|Cl`(6E#Z&d;|`LuLU1ztHn0r;d@H-k;!XsOSD54^d@ zla(fs?B77#@;eSh8;yTwd%G|rJ*2{q8qjhjl^>LL!w-&@lE72U%VYSuxmo0HKJ9`T z;ce%Zw?uvzBO-%Z62LKsRfdM7zFTT|y$~Xj;__E~CWx;Ec^L@>O9i@uwC{VN50kDm z7VKZO1C71K+%7ONWWLfM;rVs+qdLpmpJ(Qa$P)$Ugxsa4uwV}(&qAXM&`!VPWQNgA z6?eG`1cAgEh!$mqBY72WLb9V(Qiunbr7AQ1!n!3-!K&HC2Zbdx?4Rf&%bwq(sblBc z(t*6o4-}+TO2|&B4@_6ru}f7XKM4O1&lm6a>}jg6L4^E!tCKI@ba9Ut3;Jo9e)`xO zd>@G9G=lbT>tXhou)F$^I>Js-j^6_vb1qm=fusreC4-jr|;Cejk$!0;liPC+YqXYvIOx7J zxVc9mkgU}fzYn$;044Df_l|Db);o8s*9~hoDYAb)P~^McTaKw4g$OV1$&KHm`1z-X z@9Li7#Cz~_(ytUhh1V$~r!QtV(I@EpiTC=AUwW@))ik}gj`hhQ`g$&}K`6M4I-k(Y zJj!|4)l|rObg3x5^j^*-JqZJr7872TU=FDCEw6EZHty2$=9X(7ro4);q>$; zSfqzR@YcdDU`;=4kte#G{scs|@?6K)!#9rr$MO;;rdJCauv(*r2NFabd^9I5vqQ_%v4&Zk8rKd2OHwmA9K?g=9D#J)j zf%`*Fo<;HKil~(-h~Rp%`AKb^t>(QV=yS5wJ9{Bu#o1uv&GwFDtC#P>u$U&?EQ!jX zctznH^L?36$v}0Vxyql=RVmH_th-E(z}JK0bf|Yq^YGr7fPNK^Rqb(DoHV#a)Rw6m zskL`&)ga2TKB(e$U4yakw4{MX`PjPd zf}AP%6qXs(2iwG2ux)NsbjwOk<{suz$WvH2n3U8}g(kE}8A^A}6V)?T%R2~Ia6Ori zB$dZ}74e1gP2D0N0mN8z9r%d=1*u;JSqiBi7w=bjpZV)x9A+$v4FU!GA3nxl>wE^R z`z+N*Air4nF7iuQBKx|4V(J!9K9T!_%6-06)&xg-bUif0$I6a_;5iOSn$8XT*H1XX zdoeV}5SGa%Qc)cHC9FxletGy{Jq06sp+X{hTKG{jX1BZdfd?V#EFn0NVv00h`4oq7 z-eGcB`ukJJ#z)vyoZQsg>1(HisEW{J(}y|bWj!N)4X2vh<}s1p91va6H!l_p94{w0 zT};=a%Cjem7&d1&+5Q+|f3VM@q* zSP8O4OwS;ZUs5>bwtQM)-BEy}-g@21` zCec(Ts~}tRMUD$M$)CFeYZ|7J@7B+>vXAh(uv$dk5-5vlcZJILTjEXoLW#sN1;VnT zDpZ<1xlS7B^`+Scwtd>^d{g?p+TtO?emNO#`StI3&E$ax`N-rlck&C;a`HtnF@FCY zOpIgc0?H162U6)w{;bS#p8Wq2XVCXG=ob9Qn=;RRaAiZL7`PvK7WJi{D*wVg)#abk zJ(522I3JLY>b3|dztDY>9QKvFRhh&~wpDf_3spIlZx_<_e`BZf8a#ITsj^eu8y`_T z_-0b!4GEU#hFwJ)y4GHB-}oXOe@QwH5}C3a;Pt`_{Nve!a4Q6WJt+1}1CuBG_%Fg= zvK+|HQ;&`1RGt|!B~6lIs=9{?v)uF9SV)Bh!|7Ra*GVoOcKY3-APUGp5d{)Hn-Q&9 zBe69eN!4hLBt$p4rwVZI!}=a1RhFrga?_7{z|8#rsC)PLsH&^~KSL6Yt$0!er7hLi zCsrb8p@qs6tQnZdL=(k&Y4p)bi}gz@ZF$`?K~rGYMk*{QJx61#`|m`?B`hYpuQZ+H0>ZVBo=d;@CK- zZ{>^sq?P=nPth29{!9O1BM>idOUnS3xv|Rcb?$H7`o@@|PEgc>_lw&4fc|78hyVZ| zzj>7|#;TIk8RwMlEs+-${UU@<<)K3M?23-2L=WK&Y#~xasq3!PSgB{>pKo>@y3`s` zUj}tq)rZ|tjyB8T5FNLTH1u+-5KC^J#*-S$o9(K2xob(5YZ2xERx(SdN`?V1cL`6} z4+*`)Lr`&$W;Tz&;oleq(?rbJ9<`aUHY@W)crWp!)FOEX!rc=dGU0+2wk{mi2bzjZ zxVLyTIN}7Al-i9HzI~p=3rgb;*7HrgDa~7||C{081{H4WD?BWsHM|YNh7rJde{uMF z$m z`yqIodT1+j_~MC6E6c)TYoPgGa6&a@7h*+Yunjd_eAfV7iOS=-*Xe*D6>!7NQKlL) zF(DV;fpcO_W5NbD;t_7!fJlFOmVz8VOYPTRJZNnP^q;7OhUNEif7d6sALwr)PwQd% z;}gD}84WTrq;6(=#_PO*Y$|0nw{d34HPXMPI5`L&(YlLrn#ryoFs+{+D6Q{>Zm6YF4?3_D^J$1Ky_)lyfrkt!XK@crXANdxD{4*)p?ZhuV5?w%$SwG!5 z09CBlf=+_#MSU&o%L_yOJH<(lDncU1r1~s` zqef06su?$a*3=w7|GQCd-QO*#!e@SYR?lKcv7Vm| ze@jWS{m`-vRo|Z+JpUJ}(0`k_$IK*wFcQofY}$l=!KfLrhJvUbk0G zu_fxlZ#7PF#)UM;%Ahypzl{8iO!n2S-w4iAqFjQ&&&cntAlMJ{d^m?QAB7tVYQ~8O zH791InMSYS&Hl^uK?(g`;U|BkwXKNkzuJeD6`s;IMt-`#}@=O@C0YQ$9D)Ou{@DQ)5xFo^5TQ z=JgZj^34%l`1B`5>nKb%q80Clgm~ourDhZ`zHl%ZO8W_Yi(`8hKIXAGYtZ@i$lrtwx@lKy zvC`wV0wz4v*CE~k#k9sT;=zu^TtT2&v@d%nETX@@tkwtm3+GCG(y6rk$O}Frs+@F) zpi=}mNv$R>hKa8+*oa2O2jzD67?Al7FOxYRjG)>_T}jFxHZfA5C)ZMNb} zHNHg&!4cH}SL$j6rXt68&$qh1$^AP|w9Zz@Upj?1_EA*|kd5OuK9;R}3~baJkBpU` zEqmK09uzaAb)wsED;2K(mxWbEg>k1~eQe=xYuFJZ`YpXEjla&Hvkqv`3WPX4cnGt( z_3mh-*K;`PkTjkohMm-BmHG4v2=NM zK24Obm}9c9V=3fPzQRhEXH}Ojw#h2G2~ZUG8*WcA?uAQ+C8BsXJMB+uIQ@lFrC!gEk`GTFPRk2~OAs1^wzkt)GV6Gdy?j>J`%j+GoL6$Ah z&|O7aFvK^+Bf39inN_TVhYq#9;TQL*prQT5zZcG9{BRKCGEYYg8UBkC!i;P*P^#VEdb`|Nsb1BE$6r^@wNI9?HB3)paey1ko-B8zHE#_+ ze~)NyYxwd%2y@2R3I~ksPV&+DlLU)x_2#CX-7?w9RX_PcbWp(I6MhTuguCA*Jv*@HpYgT-rlLG_@;Dfl4fkLevfD=gF>Suo~G@A^j zn;mpm5)Vzpk_vC&T9|n@We~xRJ|Bp61ea&cSaDel#wa zR>SeNG3mrQxr7ipIk75C9+?2auGQ|HDqNr@vu?`bc6qSaXKzOP!3m=v%A=nXQA#R9 zE;H2uXe~dI?Q;#NoR$$zu6ibx)JdrmBOjj(g@;>V^552@To861DF}P88&&e2J$TUk z%L-KpZT+@ZsIbDI`-h&l1YO21D9H3h5tX0`#xSF}^dQ@TZ5l7T34}@DsCd5CF3TzH zXJzNx1`Uvp;D-3Vs_@fivW!l?)b;}P%;Q^_IbN#z*08>=+=(nd zifJ(LP+VxyWAIZwT1j}_SyZ*6Oxu?49k6K1tp|usX5X91PyJinM)-#dOnR#ZkN5I2 zgsR+?kzl*&06t^V#MKoq1}mx=4b?Jf*R{C)&gIp#wNv@Vp0yU>jvmJ#A}O*W5{y5g$fu-pAS>s3LD0a zF4xm|tnP|VZ9A(-zgI6dIix#i26rF`W{C_F&H4&fklq{B39*ANh7>br;%RUvdAXDR zW((io&_|yV4>#b$Omy7Mc$SqExO~HVn3>P{M2ee%y-xGy3MQRQe(7Rd11+w%&<;Lv z5m}ivpw+OHsum(7kD14@OpEw*cWtir`dBV}gH;NQVOmo(-RS!b#qZq1>%OP_H7s(e z0vIa2WEz{;Zc7jmoAtty2&FpC8af&>*)JK|1C9WjE)+P5|FKJeIW&HS*9$J4R) z)W_3tJYg}E@-XliYKn@c{M9Hbs$P*i1a#n5n<@VRsW8fX`Mdb8ty&cxz=lVI_op{E zZA;bl@=d_-&0oFhEuS-aNlZ3{vFTX2Cu05T{E+cKqbclhG zUnj9C7aW%t#mR=qf^c{aO;Sdj)s-anVt9EWhs*AJ;k2RT>aVi)HB zSy4u${QImDtO`fv_q>_svW)3UbQ0&nU)RdfrrH0XiJU!_^q1Q8_UkT%)IAsv^A3T~ ztz7Ms{F3wnKM)s5SJq}A8E(tu=miHI{*J!c;Dcd&g|-ij>;yi=Zu`9dxcArlQcEjF z5MydpSe;tBCr`sHVS0ILT}1`XQhkYRZ$(4$-S)e6B0f!62#uSUe}(_03UB1M|6<*5 zU|bYinwaX_j*7mCoZL6AgmN6wb_lGnyVt3xXHhN@t;;n&&c&Hz?Zw_HHqb?_ zySNd|wLkpWoVyPgdB@n)kYAenEK*%*Y6Qpqw+r6NQy!Q8yDiof(0^ag#uBNJzf@<93<-xE>DqZcBCc;_2sp02L4 zsYOZk8xb<+*G6uAi777JtaB_=;w8ivL=~zzN1yqX9 z)W`uVCsLb&Nk^z+`9iH@TaDYlhX~esFZX?hg=A$3epZdU2Mci~lxLu4DfTLSyYNiiK9p?YO(8mf^2Z=To#N=iJMe*gpRf^tO9{^?T|Y~Z8+*} z8;rHH0k#shqEx%iV9r+nj(Om-1=My>dRl(!UHmZ9KS(N?U*$dkp@wIU;c$^8$$aai zsxI<0Ew*du;O7n%jw}Z;|1Kkqq>K0w|9V}E_&=4OG4PgGJ@WaBg}JwQ9R7?R^G)Yh zz|Se~mUL~!S>EeCL=>-h%R8j!E$^GF-||jb^_DlTN9jwaePEe(+x5pC_OWW`k4vY0 z$d+x}N-)p@-n#w-D2j@e;c$$E<7o~4i;ey(X@7^LT}DvawJzvAo$61le@^J>e09-h z`-|rRApN;ZPu#7X08AB`r-k(}Xx+ywEw&Jxe5BewxmYa)z_*=V^}G;a6L^@KFtlZN4)y;|nJtUm`o{(L91b zGMz#u=WyUYLFs#S2*V@4s2_}3G*qJoo!BUNRy3y+!7x|9g}=J`w0@gOr4>}ll_8&` zhIrGz5qd_M=%t9cwu@w&>`<3>AjV@u8Q8nz&yGQj2wqJQ=Thv)!&kR3aVi?X5|-s@`^5La9u zzBOTkeVSZMZI-LdGexYjG?HlS9?u}MrSvJM0cG?6(nW`(H>|h05|G%Kj4axr>Q5N&BvkwrDRca_V zuMu?#Yv`lCy@&e~O9L%mYh4ZXxXkR011m-$vNQg?QQ7Q77Dx$56&q~4{x8<|YtAbS zu21jb1K7T(?;4&<^_8)&?&@1f`cCx~&(|1O+|Xt#;}!D-LvX}G0<<$(v=Y?|tK2HA zatKy)=wKmq-8MCOOsPphPt>bY&LFE6Sr4*e@aKzC58p{iH{V&D)BSiAZ`);*2O2xG_TGx7fCTJOz9h+!l zMm!=RaLkax4u~P%HY4Ka$_ef}OD+VfTWDe`WnczxRscl}P(VIT-tMHYX!krg?;rFP z5!y<4P3aH*V;0kLJ4y3{^Cpv&=CP*H$7vk9F3;_Q4dO&b#F>MML^B!jHIA;4k&1aQ z{NV}O)VAf?!_pzHua)gb;W!S`Yh?IEo@XQ;{I!c;EbG8Rwr68-ONWgL7IVLsqtA<< zFRLfRX|*$3_f7@3==R{~4rrDYGi^$krz*0~SCQ$&)_uve5wxa)s~_fvOec~)kJF|T zxf#{1ML9b5Me!Y`*WWSsFalogYnTSOF;I-j}Xum?4yOb^lBE4jSJUYiCTwq_0{okk8W zPF6)+@$SFkzm6K5A9>ul5U*cIEq+1puz$JuC1E7o@jl`;C^suYz45GK^jmN@j`*S; ziXC)TxAS@29Xru6%4H?8iOF*c^;E`i6Zv+qyUJTvBGYZKr(0J5l@g zCG|vJg<&4&@hB_{OE1ebA;4#(b~_8!e=b?3Tv=;X)+N@E?U^Ad)uiZN>Olp;7m31bsUQ}%<^;b=>baMV_wT|aEk4wi~JDk)5ZY;-371t@qX35tlqqfsJ*ow}dFj)@!F z=3?F>ja8ZXa-x03sAR`f+$OzVgtX!y1C}A)?b$-rh zz~wDDFc7B_&xIvsH=--52y##PcIUa*bt)ZI11psdpUjznp}n{Ni)JqaVx{X3C$qp+ z!g4mlZo5_Fj~MTD?WZcJ`ZBU*@)LI`J6rJE!Y2Os}e!TU?rT2G6_HXc013fh6mq=8RUFfJmu1{Ye@m~3KTV+|e-=S*M>wLY$ z#}H@k2v)z8AMeUbt_>>xe7lvuUgaNyomKu%rSgR`3ZpM_@wcRd{)p4}ym{<_v52KK z7FZ-B)OgG3Ni0Yf-(Oa5@hm$ZwnCGB^fwJF$Ns3wu>Q`P4lZ_87)y2~yw0Vtlz4EY z;$@@yR=#$9)licNEYc|HsJ8i_{O($S+n@+Uw3?-mft6M^BltwJLbQax` zoyN7bq_pq!e`qe=eJVBMFHwC+gqi%5wQ)C!SzcE164AWU(WDc4kNY#Yyb59r9E>~S zzWtQcj{D+JzJNW41PvD^b%JFMLIK~p#WxWq08s<7HwHDKEEQaOgGRx5(cM0RB!?gw zAk`)qQq>l0ZW2Tg3&v)K*&uFKUbFj8V*qj0b6)CJ5DU{MJbipJevBDf+dR0LRy z{3?m?*dxZO;qvJBaF$+5f1US&B6O7WL;rOqO@!u}zREH;M{e|n ztzv{Qf-8Jtsg5ug(A_iK&9>^93U2B{;rPoufZz>Jz1_D_OSs}~(=^~tO(QY}=#E+H zoAmp2U^fHonlG3-g3rw$g%92O@J)3p@66QlZ9!wgcKp`B#*WE9@jA=w>k$iSZ7_P& zb%!34jt)I?6-WH4dtg@$d{GUI*1mYrK4Q^(mmsVC)r+4is{ua`d7bZr4ht##CuZ!X z03=HAV-cAZ-gjfUFeC)0k`{4}7-aS_gkMg1ks#}**C{HG;@8%Do!9b4 z)vZ(`VbS^-*n1`AMX0;6v|(h%12p)_l|9vUgyur8;vJ;T>3L!>5`ZQqI$d+lanJe z04G{7#AJu>w}(gKCK!YHEf|~Oo=-t(B0RPWEj$+97bwi^#F{z(as)dXGG~drE z^m0f*=o@vVQ#Y2<^TKUXD)EQi9&Xw|U86~JuNU$fkw)QdGDWPQ>I%As){QSj*8yHt z{sNpi3wszWPT&+?#LZlaw_9x#E~5RYJ2AId9Jayt=?qyfW?z{Mg1)<2+`Svd?F5&= zvPIbJKfvXUw92CONe-KqrJ!s-ndKcRik2EK32_+#JZ=kL7E|Ur|$~Dg)a|cAK=3uSUM|@w1g9E1AhP3Y*+pneyi=*SohW_txv)s@>aN z?roISoUO6uD*IgG828rW-p0GP+uhq__x30ERYwrNR><=rLu2&kTDC4WV7e}d4ixb7H(5l4G zcEbxc+}ALI0OgF5&e|0>R!lUrQ|ye2{U9IUmq$~#8UVUkG9PUFNQ$gJ#XNES&Z;uF zU^ND%C|iDoTsF+4yEQzLyUN2WSS-kFf)^5B!ebs5yKfD9Wl!h#7oT&#(^omCjeg?P z+V|bv?8+`|(aM`j>AVJTRXN{_8&L4{b%VLb=8$Jx5dsPnP{@S)H%{<2P;^>z6)--n#KlY$D zmVH!9vU(S$-p&^|`LmO}$Z$gnjhOZ5zRYYQ0aq%Xc$2@(blglHU&1C78=0`o0)i*{ zWDvqFMsERP+WYI6RL713xr6|Sd0GhYa!U z+4pO_+#kuw0lcThe<}YdjC-xmr6(WO#yzbV^R|aycubgW!_@zBX4P}3K96Kpy;zY- zUO7&j;w|r?yy1}0x0|0sW1c1D?HW?cTBor58mH-U4*-!`sN|-6W5sPWMuN*nc+RHT z?X}}l?`~{3Fp)m6!kf0oheZui^>=Z6r^t9l!9f z41Js2=)Y;(Cpcpg*X&F-%ETV3D4f6~iwXIDx2vTc+Cf7Q+v9hl_c_dujli4(p7_xqP44(=e)x4~X$H|mm9KpDT}cxJu?y0a(v3CyQw5-( z$oxwf9bUYyqxj#Bd|qV_#I4cBEIt9{E)--c-|#_WKE19+te9!epEW8oyRRwVN~|b< zZ6hS#m{?m_Ns(^Z6iiz9?=@!EaC#|BZGJ{V=s#|O82Ctnbmx)12-v1PFqD;92Jv|K zwNK&0N+y@3>^95tjIz8)9omjOCO`MkvT$GWX*aZmbJr?4S(ypW;H>$9+Ffx3L4DbaynSa0MLQ7#z!)cJq0c0B^z|3{kC-(dta^ zuVqsYY(0JILazOq-IuyYFmZZEPGyi`M0f7z@BIfDKB+(K-Xy8qI#t3YlN~^g_X2!l zuxy9vQA82lN-VK~w3i;}e*#&#YJJJD`Sf6TW$t=v@w6tPek?j90Am3MBQ`YIU6|% zH_mlylYk(=!7%k{Z_=mhYk0j& z$}P1PIi2LV!oGDMY`cl_L8JoPP#jp%!{ATGBe0 zUlWd{0TkynP%GFGdFnorXLpz9hEkr&kK}oNtkrW`DbKSv@7&fsE>BgIryI-^_uI7V z8a_Y<06&J&Pu)7Gr@#He;GVLHs4kNcm#dLKwPsLJ_IBH!+3Vs?+r}ClYWWBtUvYpO zPy%-1NAm3L@_ac4*3ExI_hZA)8NhAlqWwxk2DJn6-t2RKq5HtdR!V8dHOBpQ)#MKU zbcl7Uhv|10d$SYiWV|=4=hpvVAje2t4m!W)DhzcYp=o)z#mcB5(m9^mZNFAq+r^^B z#U*-7edE?SBbka$jvWWWpWlb5Z{b%DcIm!#bARbvu9fT56SF< zzbW|zWp!NvO<>1F|%`0~*FY5(nL68Z{7x}khG z1EII_o|(N|)I20Ekf9YXu|g#oQ(P5HY>W27ePRIT^r%+tP}y9X zbj^CpD^)I^tF4zmf_9n+@}O<)sv0Pm^cpo!%B)O&xf7DO#QD`wV62W|;ZP$f(*yv$;ULrY|I500GZ<54%lxWHq$h(PvoA6isK*1{7u zpx~Oy!IgwUm%o;CR_(gpH%WvHP}H85H8p6nmkKAb0mg4*vbuF*Du1*)9mX{g3B`m5^qxdx^2DBP1W6+rw!f@+2Ay77v}>_2WTfxJ6s0w^jr5dHwSIMIM@OJ6DIlq<02fn9hWPrFq}#1h1HCP zqs!f*rkyWr;j<*f_;2#x&<&$a$sTXnxslv3(iF*hUDuj4 zc(rhfDeUL zOc?po+$RMezm`gFXyf1}^MfkgN3E|#_g+`UFbRl7xx#?}!3a-<1ltIM2)Q`#&85lwucN8v<*@40gJ~OJN>kq9yQXc1FvI?j z1VebLE6rq4qR9;r*@(c<6QHpCI6HbQXn*d!tQS^97yyoiVSnK(*Gx6uhd`AXd^!P zY}KGC{*M~EQaz^sT;n3-y74%a#wKh4#TTy{;M_zsFzpok*8_9$iE$IhSQA5rW)K4u zhbi|nC|^9rfYEuVUQ3`sC`^xa3S+GF?XxdLAL$?>2BHNl?-AO_Fi7cZJE-tEMz$oM zor;$!(z=)1p_c8~Dr!u`6pzd_BySpooN$7LWE+g+1Aw9b4^Cum7CfMSX1IZ;!intV z?Hc>He^*s|onIn{2+ob+67fPIl<1_AOqf$89$8{cFr`Hpuzm6oBh}n3E3L=C{Z{z( z<5X|kxcrozu))qlYu8@}!U-e2nz0Jg~~ z=7hmK;&ux)hPhvvMkQ?AVSs9SxsfO7nPM4necpLKgr7EoPI2E#Zhz4+S&EV_((u2) zbqxLNTWk+Uj~3vLw>3B`BMzvw-nV{!a9Rg}-HPwV6+fV!Vv0g3QM&CK`XaH#(x}Qd zWSLK#5*jW{Ahq6R&XY7gUjfggIbUxpjX`R^Qz~?+lQvWbtvDJ!M*K>TXF4(yU$`BD z7^M8myvIg)kwDw-AYriDU)W)UDCB;V{uDZi9@&Vyh;GWc4Fndpm^9G$ukl@Csqj~NR`@K@AquDS&8eiOO=yVD zC2#^L)y!J9@JQ5jP6q-c8rq9@c$&*Cc-d zZg|+6))h;F`>P`8SAu&%*%JOtwf_(FTxhjPQji}EuomK4S!RW!^!jtC2o2?6R1Q2h4~d6#T9Ms|BEA)W4Jg0&WYA%vHdT&z=^KE zg{MAl-8dNo)Ybnj1C)g-P~0>O5%`EOp-P2Vg{?9_yzV&JnpTH5-!G*#+O4({9d@!N zYu@!16etG@K!Qst`rBt0&q0+>6yGCw0!!}f;h0hD*=RgWkCw7f=OZGA{SMU^EbgE@ zdeN7p7h&z5YI@P2`Q`liWUwGbZuJ(Pt7z=l>WNR#rOocB#=?JBOBhvNyOJ*DhtED% zeaDfjru$?4Jr=(*2fd;xIP@GVGwD-JGI2N1+9Me3FwSwk;5zxh^#G}b`Dv}KFw8JOkHb)^0rj{nxoWSc-;sB(@(jwR^-k>`_aPcoz8tV1#BS|c)3&eM;DxvP>=_G-9{o| zw)1m_@gmrUrP!5z0o1pBqLJP*d6$iytehcfa`wbu&;HcC`O&jH-j>yN(5djKa|Q-j zV+njOH;KlCGoeNDXcqa22_IWTtP|dyN|VUtVqtWMp-^ zO2QFOB0R=$5!c~r#KdA% zq`ZszVtWFk5w|_A1FLN80{Tz+Y8A0H(f~8%x{P|aq9+w*Z zp_FvbLZ)1j2HCsuk0IKCQ#Zmheq65aw$TvDZ};5=i0pnwetLa{78Ju$dn(Tw6tNR7 zCjg-`9Bvsp1ds*wa2%@LT$j{ZTOHKThqCHZP+dJVErmX;zDWLf&+2h)8xg~5_ z5vQN2>J*iOZ0 z3*)NWi*@8`I{-5r7FFX8o4AJO!{tl)sJsTYi{ura41|coVSJWe;Vr+N?E@Ra{O~Uw zEwbBkJ5&NCzv(su+^`k)V!l;5WrdT)MNAl!-y+LLe)vPknNnGFMD2Xj3s*e+qABu` z02iSY9zt6&xtwdjSgmRY6{o^;sL<8Ye`~aV?po&>B&x<{F(`kOad`}3+cS-`m(|)j zEx1eucQTX${ckSR)A$g28o-P$dN`O55j{~CJ`vyFC2$9%W8ey-jDpe?wi*sCp`6UBXoo$*hT+l zQEW(iufOQpn^CH7$aim&@mt~GkNJM}S&rX!|G4kv4&Ij#2r>ThM{$`l8&o?Ou8)rI zUmJ=jd<3MQ)m`jg*KW{<)K{AZf)lET>IZH^FJC!zGfIlAMQ9T7h8{!cb?Ur1>bOd4 z-d}Qp)(!cQ)rkQ4myQ1*_@Q<$w{IljG}){dU!c-C2eb(rj8TZo@8zBc zVi>K54CWG(-L_Zjo)ccpZmX&HE?&!p=&9tRUY90X-6fov&9%$&3dm3GP1QXrA7ssc zO$qdk4>S2Rc6CI8#TAq!ifjm18q@jfao}2tJi;HNooE1YX!yopUpax@GF>XG*Lfb} zWGh9@{NdtCLP^TlB|oka)y$Am68hR|3fDtI(Rjh}VTSN8?wgmpK-6WfGvOx6)4eAF zl6Cx}9Zauftq;O~d{_N9Qf2==jsY5&$6uzSuC z>6u~XvE8IDTx2=GmjsqMNh)DlAUCBqig`Z^CqE<|C{v{^jVxAucj`9 z)6YpR?f(kh-twc=#h~&yT36ppLZigWrJ`dVxNWUyf?}HkR1E1!HE#anq$pI9(@oON zNM75oEZ~PCQBwF1nqhy4y09All$4K-&qLFS%sYxYit?HJdLw2p{3q!KVs4K0cW3#2 z(hi3U+`DgvSwlq<$(6|H5LVFknl}Nm~eHEnuYm zD^hhgkhA#nx<1FW(U3oO2b*N0gm(-J&)JIH_!&=x=Ghyx(hxaGpiMSO##(5gyN6MO zRpC&1102cp455%#_z|Gch7?g&dE;3R$nhs$<{=NDHc_43ox;u{SG5y^?x0Villq2omvoIz}% zYPUj1`O5zLO#q`Qj*%)bLE#7GVT8V#jfIeRSw2p}6!ArZF z0Ce|Gt0*H;vUjnNRSHf^C^*I|@0tzy1P%{4oyfk_Gsv>6Ac@`siz*Z?JW}P$B8gzL zc4qd5ofMPb{hRu9TxX~HBu*kNp#P7Eg7gI-%;*uG7xSATw8Z+|^F?5qBofHd!f0wV z{xyxC@oLH+BTq7V(!dNJ{_`G24B<0ekmZl~!`C=$gL)4d?0A~y#69e=jh>?8Y5(An zQAT}w3_G#;f7l)3g)NYU!=Z#-Y-YTWjSh!Z^D%g|kVRkOk_(90GUq29_Eq>S@|Na$ zZBERHaYT7g5SfTEFaI7zb~(tX5(>?@Haztpa6c8*p@_B>C*i);pQ!y}ke`Ee@xV|9`6X3L*)L9o;!_c# zlnE_8a<#wKKmdE1lDyu)YxwZ-#d)%(2;AIg6(BXo&_vX1r{>(IMp1HFInZ+gTQ(4n zx>1~VE02>6n#!-_gV%XKXp}^}4anJ>nu^w)!s3~ky~)2min%vpbKQF4GTlmsO&sgN zm&@Hkm(HJ($Rv7LU99Aj{vFhnzDS1~>i(?R{D@PoXpES)0ZDt;P-yp@9QGn$vx;I2+ z_Bu}%BoJ%u_e#?!yg#k3TSp!nPV&_7jWj!DQ`Z|_56{zU zdkr7t#f?@QW(}v?-*GwJI+?RdAKR_d04Iibm))8ocq1r5l%R|O;#B}ix=P2_bwv(% zr{L>dYEj`4+xOUM!J^>{kFh--uk+DpGcaPMa38K18N^y})`1W}BUVx^Ac-u}n-I|4 z<1)k<(|AYDn6qgSnfsv(zU!H)Q};-OYeh-w`$Y?QCV%!Q0^7&DknO>1vOh!Q9hD(B z+t!sgOmLE`rGG5KZiJd;9jVi3b_9!D^*Bjc%=&ba<*1jNPFuQ3LwWY668Y|W*uV?Q&z-HLmB2bgEEvzZxE~G24ygAM>GZ}6eDz+oSH?R zh#ui80TJn$@(@$A$&D$0Q`&1@-$1O_Bu~e#N^bOy)pN>gx>o}r372}F{*1HRi7s+` z1QIQuCBgb&qQ$J$;W3ir?X~rcVPx=T^Q5wxF)>5&dy%2J56rO#V77n8?9_=GB1kqr z`~)5|UcluYuR$zs7Fll;!m0LppPQMk@7#r{yI ztF=Vuo(lL{Q0+V^LwXW zVau^FF#l%Cy?su4Y-{aY!V+VmE>G8<=|75G#s185E3(O?ggqp*-0+BnrR<5`a4%ko zlpG&BO~b7-!>vCbB+@_NC9XfsV9vDNmwjM8mwC_Av}7gSHpbou3Mg(RAfttTi{`b5 zrL|Ftt2Ke|*L+BibviJw9lWOMHkCV@c|&r2``)R?oyQr1?j%{}eHsvRZCY^7gR z6|t<5y0yr($WYLt_BA`mA@`{9G|2c+*yHd% z%i@8@)ajKt10}rWU#A&m)wt4CjRRf@rgwhfBvDYYklLQ3wp)+3y7R+7J)V|q|IZ41 zq!|Z-6K!#&$4@HBWZy+&49JCbFsqoaJPcGvY;*~nzFhT(#03LXk4+sb>ZUmpOW zwwb-o-#jZt^3K{86{Rc(N;I4%Bq?@wzJ+ft|V|z7gCJa0`OM_%dcfS*!+Z#Dwru$ zuSTj+AWb&t^#+Jpny-YxWt*FehD0-(tEr=`fLOH=h!Eb)YoVV11e==FU*QB2;Gm3I z;T5sIH|@W1dE)JNx7YPLE;)3uv4X!kVwG^*0`!qyjAC66(UE9a@-q`y+BHChKPwdx zoOs*cQn%4Yt!Yv=g*+Mhhx`kZf z28?UO^pA%m(aQkUGd6X-{VNUd{O}tPGx+gHDwwN@m-`BYp^qPtNK+3M`TR?Xq;i0( zAu-322xq-1Eo+O?g0@6kCM{Z<28jy9UI*|LHn^^7usBeq;q<6Gpi1-#Ze3c%VL$l~#NjN);1tKC z^$P2?%I{D{-e?vYNXMa7=lDwo2BP!KsoOT+{;LmCx5_Oa4nGR~aKt%$?Oe2IiMxkI zDfy9yk!~BLTf^sn7Ja%+pV${U?>7WG3o1VeiD23r@s1T3ri${zce{f?FpIZahNE4E zx7k&q#@FjC!;80*VHO!kD!8PFl(b+QN!j@|w6IPWG#t@Hpw<*kidnYCH>TxBu2LC= zJ{n_-VjjKRei|Lfu_Dd^q0ww-Mt_v@>T!;VbT+42@T>E`A7Y%(vFy%+S zH%9WVTDAoRI02?zVEoHBt7{#?H^e0T=^Ddsmopw1j;ZHIHmf=t80Obgc>%W?hwz4< zX%M{5pU{Zgy*Qw(a5lQ1-Q4K@vv}NN6vC8aC_Vnhl-IZgjd=d?xZpNSc%pv@GlWm)BLGR>9HF@LL8xy^!7jVG5yU1zgI_z z5)-t{x4!z1j=j8I9i8|CsY<_-D$nL0hQ{po1$|xy+?7?&t&FmJgR^g4v`O7=rrTd>``B(2ZE!SpEe`G#*k4Cnd9`^xncfr<0=USBPmE1 z;NjoaS=qY{i;E5qx)f-VSR;D{mrT_Y=WqW+uAoau)zMY%KxMVzvlUyS&TiIq;T)82 zf0aMIdPplSzC$LD+kgx|d7ufuhGglRUd@2Rq~-nlp4A;(GLJ<}CfhS6vB{tLMq)el zW13@*9Frw@2)*La$D3hWL7MRiRcr+N^Q(10Ff*d9w{O}gRh8IYaNz;KOeW~k7)}@n zSA#QG8qpNySjlK*Q*CG4tj<-qyeh=rB`QE8*ek*#-bpB+&$vWicKblx6={S}=5 z0J{;Enu70?g_i*x{Rd6#n1`oo5fWj_cF^ApFXhy55mRb)5Lt&hzXW0OEW~u)}#V0ul&BfZ&em2G?4G~<{?%b79pN8}L#=G9;Z$Ix9!neI~H zkaTb+5&Uu0mO@8J`v(7BFV}3O_Pwg$^crGjFYU6Pes#h-{mBTQ>0nNEL*nlZ$w$29 zvy?fB_2C;lq=R`=8}Q#-eittd$<2$J^FK-?pIUTmzGDC4O>Ow}W%3mr{Od~xEhEUb z@Q;}!GHRsNFeglx&v`|4cNJ*4)Yihw@}~|YpIUfR_kOx-p*}Aa!A^X7+S23;3UGNIvW>U#Z?D*S21lPR^XV=r@99p0%^+ z@_z&LZ3gr_+N3^!o(0ekdtGO^gs6dqzv{oSn_0|TD&W#njvnFVC3Hb_K{-v*f~5Yg zOul6cS1ogL;Zi{2X;{Wf(>vr1`!K|9YOkF|jD7hg%}V>6uxjUec_R8j=IODS5lWwv z_V*Zs8Hg`ORrJr5-7`J5-trAD|7XhYqAc+>Sdx5@Xl>yCav)hln~Z<*{J+S&rW{~r7L|Wt;WXO+5#`gtar0vU;>OQZ<3zDPrYb7jP>0XZN5BAz z6t#@|I4B4vH$DO)1|XQxUiU^_NcUag2naWwlir8*+}~H)tN! z>~Bp43#uAdZLetv8un}oaJ;EEkKj4VCF569fX?pH<|%s1s%z@Fq&4V)IrA3(`ea+}vh zRB7V66M;p)u;{t6mO3hG{eB}B4kMw~Z-ekVzQ?eE!uC`hcTc|A7<_$++II6WKymtCBqywM@Cmog(<=W zr-No)MpN8$2VLgZtqQdv^`GP$QH=}|SWN45xgUh^|OaB1XGuJG@l}Vo^}x0YDwPz70|p>HLUc4T)8$K_isZoixucrORYUHfCQijfGiTjZ=?Wd~RxB98-LGjJfQ%HNBj|*PX{!y~Ay1i%c zeP>cn8uBBm;H`$_szo)asp%jaub*myU<}-3 zB-y!P(LTY7$zt}07VQy@2Y2aZQ8__ZG~Wllc0R(1Xc|c8zw|#fbi9k_cR$0bo)CV0 z(>SGzLOLm`FB6#+nd=#U8Vr;lc}kXY{@HG)sAhv2jguJ+OT zeRsBXf=dN+=R<9GO(6rdEZnLZjvCSSXh4F1B&14X%iUPwnD7)9m1{d?GN?S^J2Hz_D<*0rf{a6Q zwN$6&lBQG116{_a)---lN-;iQaGNuA5Dr)BcR2kCTc)&~oe9pU*R}R1(a7Cr;@4{g zBwG>d&R*SR`1EZAPcM5ra~@(Kgpyp7InVkTh_L^!iAJOt2_Fh3ij}itx0fc@~VZq`GIQFLTC|V#As6S;X$DB zhE(laaPoMT_N(#rC0iO}pR2aDraUX+ac}*ru!qPmd~4W-AnLyp{XK54JXPT`l>qJl zAdqlNmEa}(=@b1q*INIkLHgcceIok`E8xW%$9Fq4#=~labfQtq!J_km3RTA(z!(K* z)P!$d!S-Zt+xImcS=wb~v=rWcDm$h4XMRc9F%4YHdtA(4Vp$IVPZqyRaCjuvkJ=G= zW!I1L?d6>{!`i+_FXqP*BD#Dl;!kRvPs8cpufm@{qb90i_2PRQkb}6E_?N0qFDIKr z_HMhL!|VJ8F9yYM9RzeO6{f|B+4TwLvN^mW=>I5WhU{z6p?6$-09u?0&GrR%I|xRCZvuVye6SLHLEa zYjS4x`I9N0oIY;>u@;V)h^Gm`c{{Lr;vsYlO5g5SJ-K#ZPGI@`>GPxuP{VtBxEXCJ(CvzQBG-9q@q9bV@x>;aj4h6T5>({$Tp;4lV+$=6Qki7r!l9VXB8HB<|; z`RNI8bz(ZW#0K*8QHrRRTV|U{=g*?QLS;QC=qEfe0k_<#9Y^OYcBp&J{_FZQrm)Ul zo{8Xm?om-iQ|-iNe`;+bat!f?R@ewOMyVLH!@{qpooOwFJ~4u|iPtH8O1I0-A#gH% zd({S^skXj>3aW!dVoN#^!duC7?L==G!6q368N^ZmFe=fTPV}=;pCaT3yp;G?>(dr6eZ*`nMilOI838IX=^u)d&3vykv`5Mf9AVpMj5lBgiAZU;I1q)&SBcR46pTZ)|L1 zh=AKGIR6d$L;CXfA(Kqs`CI6QhFdkiE`K6^m!MFY)L6V!)sdVWN7& z;qWcOr1K{BO6^l%f~oKr?@k}Esr`1f5Z>@*HN(aP^2p`uJ70Q%{`|gGuMf|@gv5S= zTi>|}-pf@0po}em^q1Nme*5=7t6K*cx2CMput)Vt(?Oc8v3y&@Cjm$~{J!u;_lSe} zjER=9(O=WH2+VFd9R`3O>5QA@*uGiK$Iv5R=R0ZdxPS8RRqwbR4UnqU)>>N;E}mov z&7Eu5+8W-dTJdrdXj7~sUf1c-w~GgT^X;1nv$MLV%&YKnPk=fX4bg?B!M%XqU8tGR z;Ww()A&Cpts-0-WQ#_J%h*91BhOp1BuW-Uo79qXgTd$Zj4XY2@@wBA42GA~YS-jlF zvV+%Gp$}^)8egToNm_9J!%zdPGg0dF#8^)0#;~sAv>R@AZ?>%vU|#MyhAz%h7;gh! zA9L0tEG3C$DK-Rl_|-{~of(!eelnEjbRao6ze8xD#OOqCVVaSf@L^`pa3TnGTq|qz zI~Y_Mc`vsYm_t8``1y+)Bury_G&flTLU=7^Y9N@KN~|@ZfnnOqX(}N?Kh|kAk9;4% z?<;Y?Whk&+0rBQQ9O49lCFA{H^?bLffS|4cIr6AdW{xH#;vRLI`|3^Y^G`=L;!8HqI(_iB_<{nM)p2N_Q4xe~P5f2qNLrEQ%Pv=i{(oMDpod5PJ= zQ?l75CwsY73}A_u3V-I6tFT8g;2@@1{Y20#T+e{HiBF^X6(t*bg3o>V)XK6cGv;yO z=)xMC2!rG1Xfm|TKkg**XT&q%jCnSj@d16^v;nGyokigw8kftJCRc-&IpO^rH8G=( zn>+VMh~Y_TQ9=}5MbqZ1{nH#NvLHyTP5VEtF}2s`+WjE_IXe1pmY-c6K2uKVY%lA* zA0W4oLrf$8OaB^tT*8zo<4@sEp{lTtpkV&loGw%%OOL+khS%_FRRB24<5j~25h0(? z2WF+hIApkEN=DUm2gZ43q4!;qjTq5gItSXm%JwBSkNu?<72&zK8wGc}oy#}bx!izT zN}{jsedg4OVr#hBAI)$S=Bk=s+Kw+82fT z?%-32jZ-WerC6c35|ma%yk7+N+W(1Cep_Csk<#+Q$uDNFunGbo4!i2bqBD(!h;kMV zH2(j{`l7hdTKqPb!7S14Np+hxSaw$UJNN>;H?^qQpsPo3*b0f5R(a0J(Fh{#x;E-V z-}(BkzrOR=yN6=#Offg=Nc1fo{7TrPoQmrYi!A@6v9}$HtmwK)SjzsIB)$E{^3E7V zmj)soh(kMtqj=Tl_o3@xg=Kzd5qAn8KDIVF$b*~20KuI0vDbF14$%BFJC@yG4 zI%tuW>oNUV3$pC|@kqkQc!w!Y`~*2o`si@XtU>xCp8J^w|HZVV?n`HNCx0+v;n{9& zn$j`3w^FyB1#PFt9$OCI`3a$9OC*M?ynQoq<2DoW6+ifzq-tDpGvF?2N=V2 z-9Lr-bzYaQwgda0IdLcKUo4sd#?-AsVx!+Fim_qH>q|5YZF}_}Wc*_NY5g?lNGG3L z$Wmo%`0kUm6P#6pYSI5xc1ObNx&fRv&bFGl%BeB1I>$D3e~`T2%dZf^jn7kcuZ6c$ zI2ClNsm9=9wT(Yd%D05fWof_lyblWBqJ-;z6AlXE7q$_C^5) z$S)J#f(8;?=Op;nx^?01yo(&y*1g8}IpGZENg)M~(CtVxkWzWVPDr^_Ova@uF>P^a zVh<^XmgV{up~cI+0P8en7m64)szehY9-@d)*Bi~X`$m}1nDQA){0)v89h_X9Se<-% z;h8ZRhLB-Qe5tyg)NP{3T~Ohy=R}30i&SW&2Mz9?O5O*>J!@3Bm!m?`9wqIqBK@W0 zjeQufLSx=CZJ#zIUljcjj#jrx^VYg|I{DIq;c5THLIVV6^xNS7qo3uKBf4J+^}(F^ zW3Lmg6{aT>9)p`hx`gbt;T5)ce41$~UH58)>N`w3sl4L&olqTxgeeiA3jt%O zcD~j{T)zg`6E^hkH<-VT9^HM(ASZe6!dFC+Cq4nLjvXzEd{S-!CrWo9IBJqgI`yYDtep8V2X7h{PF$?%PWyxKQOq?sFk`d6h=?PvfS=yfA5u=^D3=tsaj3Nc^Yxuzyb?r2D?M%9sbwyUFNZdo0M%T`yYnR3?fHJpQ zk0g8N7XT>cw!-(IKiyjtjG55mn1|amx3rbSvE2F+tmiRC!xX^3jF;PG|Tp zKtY}};*aZAq)e$~V{ll1wUZx>Q+`k-+jLa^oYAH72W~BvPaA=}C|%s1%6M|+2klN~ zU#ZEyw>v6NGlFCZ2Zp43wGSsg(nEN|Uyw@lLVUAY(E@69=k$Jn0(ng_T{8i7|d+< zn(fIC$F9(1obucFx1a(+%`WA0b?mt{RHglA>bAl4SFh%6&j~$h?aH15H%6LIbgf~$ zzY@Xk8gNa={2y-Ybnpt&)dn>1+Ul>wpMjMTrJRRMDfXPn=|W1Ap1=cY5Dq>dR~_SC zPUwZPS=Wg*{a1nf93+MGiE-yD4x3ypP5>qP!qJqsT?eFnFfFPg9sFp1+D#DXAmL@? z;j|u+2)ul^O^On4rE!>VFcjJ(AEN%p^GkS|>L;^-!PIgeo2s;z=qolBd@quZG*x(( zI*KT8g~D^wVxeC}G>{)RK3t+o;NAplu5uh@VqnA9$Qnbc>$DehypKtS__3Dm?6!W+f;cF28uZXYJNQ3m8D=lj|D4jX07 z!GbZE#x9phnAK6|?%2Va^POv$R-!O?g~q;y$r?X?$3(xgqTiP2w^hI4FA%(xeOP57 zVenEw1GV!3Ca$V**r4!&b1f>e3ueY`S@&qVzT+I49mI86>(RlKgZqPde3uH^R;9AP5n%TW z+O#WP%Npt54WbVT;F~2c!(XJ}$YW-~{(+fx)x{rY{OL_z=jlv3AJfy_Kenf@(!BL_ zR^fA$XNbMrr{N+H)g*LsQaGlKIm}-okD8b~a8`#rwlj<+@+dqh-Yed(o(`^hg%%** z*4lJ%v8a-Lgtk%XARkh_oRm73ptr~JF_XPZ^Z*qp{ExgK@N*C~V5)P}$_=SJvzzbb zW|1X(OuD@A4IbS523>XjkCim+h~T`gO9AQsBkos2fAPUy{S|Q?uM(rhKCT)+WlZHzHwejF(1rYQ$ z%z^_n`+Kbd22N`|7Fc>=*!N$<;8SJ#)P~~Ri|RM}*aM>&aGjve?1YKs2pac(D^6hP z>a^}djaufgUU4#))0j7Nh&FBhOkVFsD^qj+S7vEuDl3i^XEg}&54r`QEt4nU3%&Kx zy2bRBb|qL?^d+(n+^ZQm z2vHk>+XI^2)wFN!5*?1f;S*=02xk0tQ8Swt?iGgcSDBq&KAHOJWP{tOTE9=NUTX|0 zF?(a@Qi;=6!e6sPHrhN4BxVE;sCyb~5gFF9TrZqsO7*SWoyZ0#Tk=0{bjOtWMbB&6 zNw-kK8&k$mr;Kb#8ENEXHk1mIvoHbvQt%}slS^ye^RU?Tkf2TM;xVP(8Pls5YL^V2}d&!l?+fqB$jqC2bDA@FPwjzJv}~pjQS%9!E*+1T}JMY2ig9be(ee1aFa*Qi;B&F$K>5rs%!?1?p7+O z@4mm6U(u(k-pw!Oa9?&3o8na;#+-yNw=;DPhpxCR4^W&w3AK6&`?LS53Eqlxca>$< zglBz?Z4dym3oYcHV5JLBllUkE@;KE@0Y4{dz$Z=t*!pNKXQS~pd#w_Ql$`EoPL{}8 zsHx8EQ=VYvmN>T9ZL#Vz6~@G}P!fzZqMOCDCmd4Us*zV%1dQ{pGaoWBG2!^s}n}>l&7t3W3gKD>h zy$jI(Csl;2Z`Anu4-gp*&a1`Fk@+U&>LMTY;k@!3hs%QfTf=^}5%i({>=*UMSJq`O zF0WlRM?<22Obt!{wSA7A`3{MCV=r3FPV{F`Iate0ijF>e3W2R@9q}J^$5+ywNjBsg zvXWswztsQR6OvZk3P;Stc!@Wzyw0867%(WOPSz+cOQkf0j0He9vd-;_rE4zQ)OV8f8a6XqNly+l-&-t%DPb=9e zoMu>=D`DCz=;w-*(HwU_5LHai6HTmoQpKczM6R|SXX!(ngaoeQS`$x=yF0XIYg9zn zskJW4uBjE1#P7A<5nzuam;O@r8VvQA^Ophj)GgVAdL}XWhI-QG;bYWe3&*`nhQy1f zRva3uzEkqVZ+fU#a!;jXGJ+SQA%vv+PSeNeXD{MA=ABxZ$Z`^)SB%HK3(BL=`;e$8 z=}UP^I@B9u2&sa0VxxZm5go!)ULS*)V(zJim|EAw+~eFatk^Cnapo;!)C00{Pc0Kt zyo1;sL6SH7U%@vZFUiLhD9D>wHu`K4(Afb2A^*S-g_mWd?D%_bIV>Kx#kn*61-)?E%D?H-@7Do1E4sNsZCVXk@L^ zx|A1h;Tz(|-JzLZ$fN4aipF2;kjxs>48b*3L5mXZrH z++4MmT#X!rK@djGplra$S3{GN+LXr!zEba{izk+jTrV*?k(Asl{^W~XP7x_Mt&e_Unct|)llZ&Jl-ltm`C>tPKFnKawC9bfgA6T(Higz29JOYU;Q6sk zo#`z!s?XR`QD|g+YQJDKoy;WiMI-C-U+`g9^zPy@Q(_+Bo9$qJ01GwZ>|+*bAFxJi z{Q`MnuBbZK#(3t(VfR6dgJR4)NV*+2s70g^+H8mJMiO%G@<;2LzWZOZ-_SDC#M8}G zOn?q_-_~kA8DCPuesJjDWayB3knoTDT`_9mzN7 zPYk}xA4=b;$$UR7_&$v93Ix?aC%l_w?_dFjXZOvw@F;(d)(Z`UwnLmGV5`7stv25V zF8S&}!gZcTgz`0s&X4t$|RWYW@|_~1lV z+G`b`#OI3C+Vt|$S{m9Zd%(ju-J~XY3k>jxvHjF%Z{QCjkOmHeUQz1n(d-4>X#7s% z!sim5f5&3CSq^f1Ia?QukXuS^N2oUS*3j5`Gg`Wg zZO7tbwsEH=igdiiPzz*Fa(BZ$+1XRL*Zlh)@Gng{_$Skeh zghm!tmy_3@cM*#!I~V?bCwPwl~EQj7S2r^68jmeo)fj|Nm&0cfaF8~b1iNj zoS)!_!xKaJsl;l)PgrX_f8Q-=SodlDEef|6*qJIAaj^_lB>Gp8+_Q?+co5~8DR&nO zb>bFnM|_ASyu+OZ+xY|!KnB&!#B#f|8#0{fr_%rH4>hK| zg;p9?r`)WjJ&onZ9Shx(yK1GbtS@4#<&0kY^rc_2iBtd-MUidOPxSt1q3rU{A`!l@ zn*#;o`-^<7S{d0fWAGADUv~-qj2-M-m}nhVAL+M0)1VW69pp^7&vIDIJFwRMOWZ>W zKH9gLYL#n?K&W`T^-KVW~>u9_#KUyO@G`=iYD`SM2 zS=gb?|1gX)!oPbdR1AS+9yA=yWWBsEDh>YK99a|onNLbKO*G=k^e_oxI7!AaLbEOr zyxti@bPg-Dmg7@S_B+&>-dW-uA*RHgbSwp;)i)Xrf`AWNC0(aaaf;$GM)=yQ{7A@5 zr92X4ds;tZM((`T8Fb)%#4?;b!99Yh(><#wr`;%{e`r79SzZ`9jbCJ=Vqg;$6|s_B zT0C(`gl)zR&I0Y+7e?6zHGGU#Em!Ok zD<&YgK3I+wBcgLxsX=#hjvy!;=U%PH+YQ2s3g?O3b@n_Hr^>vvj+yI zukMH|gx872BoKq*OnS^_$Q^Dz_lu7s|2J+`+@rkK_13>+V?5srhp-Jx> zpew=A!bEt+=^(GNwTGlrMkL&|wUG@?FDAI)$R3MQv=S0IC7j}XIRq?(hOIPYhn)VK z6U6;KgMHTyLNEaR@*6bwe%S+m9~rdt#UIh1`r~Ks3%-AwPw%C_XYgEL zQcQx@9hi?W!)ga7^%YBlkRU2fm;a}=1&sda7Bs=YC$s4g5u#T+44(07!tNlpH1ee8 z5gHGSAtgp`^lFCnOY3uAzCJD{LC8)Njy4jhA=#8xwm&+)Vg zZ5rV*3@__cG!|KgX9KLiKIP=cV?-=cGpuR5=sf8aMJ^3H<1Y;-F655$ArIDOhpbO{ zgxU_+q8+`33h)J3pW5HZGq0vk`Gi**`4;8BMS1Q59dbSYH}osddEP(tJ5SnoJxq*& zg!c>MG z@5hHm-kPyL>Og2KTN=)SYQ~K$n)I7qreB4l_2yU^`)NNzS@_2c2b)B`DyVp^rsm}QK75})L`X36CQ+L@!)ncJBtjhSOWiJ=gCNZlH|c^P=7;@5|B92huVoL__byB2Ugp#J!pl?~IaI zXjx?QjITyR8{_QkemU}y(TJVo4%9W~ruFg2y0|lbojcM!y@XThvB*=9+*6#IE+t%7 z%sssffs*&P*~6Zqz9#cWPuKb5e<13>`$V(6b^gRl${+U~EsXXJZS_tM+=9H?>D8Xu zmwVR9Ze)s5n9Q~OCcH02h{0FYIhjfSl&amy>_ruc$!IKzI@h1fjF)78WipfhcoUN{ zj_bf`+L9>P@IS@4-^kxOp}12{+Z8;r%OWp|TJKufB5}y9lr}Me&wmwRQei!5Y&6Q5 zQb`RYy%Ho%A?XeCjBQGJo1F}MM3`~NXS5PMvCjQdMGJ_~ml53~xO7{~Q3lM(8134d zYgf5Qj<2dA@4LxC4X97_@k{lrSj!f{Gbz@~m=tPV%gRdLFM$H;cwTpts^kgc%Xv<^nis zxP?Of!I9N$PjJTmUx8D=T;)X%zxaQEojwECIu3B`W+fQU zL7TztQ&tR8oMu_}`cH`GklI^YzB zL0mi@N;P!Isdn+N`l+==Wny%FgVQ=0+LPs;-NX+JO$)e$Gy22goGCHwD;2z8Xe&sNGhsXQT0`(OG%v@!kw){F&Svosbdka(r;unA)7&E z)z0d)GAcarf}C)!o&0Z0M|p_q;!FyL(M*@dt4rERK1Cf$D90$%wO+Q$eW4$m5--5H z)xs$O(-%(vU&6V%CC?Fsf=t^&H3<5=WEs3U=hX{wSGQ~Gb3~&K~ z(Ey|IDwru_57|hFu!}B&c5K zU#;A1NU8G^4C`o`s<**@jLQ>tV&r0gfmTa7ED(2%-0p9<7iovj>6?&K0iTFSb-$m8 zgt??*{wm0SG*$NI2MXavGbEb@&C9C?L?H%{1iA)yNPO${KL1EWF-ajTA4hP=f{p-mzD1MjeQpo=AcQ+rHi%8FS5@o%r1V|qlCQ$6xnt>JIoW9(77FU z-7l(0C9-VR%l)G{f%IyjD!IXB&*T(F)fQX@fM~iu_B|sHGP19TFGo-xu{Qnj-$p<7 z2A*?-V!dSSXOb(GEW7X0p7whfagu8EiCQO!z$QN?#`j_%mxUzO7q!D+bYF?n+LNtgeQUo5?)e}4Z5M&)M7w8F`D z0jYP*PCjJ(}obs>xvN4S;#d9a1{4Rc53yi%Byp6)MYY zX0O?sW0cU2rj$3wEQXLA_t0d5C~5kIXWR5$x7IA;U7UezN{URT5)8n`4bvrmqw2i5 zr6u5j@O+|VpBih6fT6q6kF9ugA$ea;LO?RX)yl4Bul ztPVAl%Hgiq@j`&K-pn!YDD0p9QxiGmjNRltr|kvW4@QCbeAN%BC^coD1_lUd*XQ%3 zT=iBzVn)+e>r{CNm{lMg)# zp#AV9FQafHII@~fQ#g-rP+*1%BMHlgKRBqqr5d>F6KK6?1Q~%1GulGtLUuN~{(vIy%M5YFWQkoCtZK5nR%D~v>H&MKUZ=z)VR;?%VY90`&>KO+8Bp|C;)oKlR z#tgWhIk*21WW%n-rUah0QaKT?8AJlTg-<+= zkiMe68YRV_&&(3w4t8s<>Wtu*!W}o+f06}bY#$V+YNFo7Ly%Cd<3VMeH@?(#2EA#% zsnD-kEKFH^V`v3PwFB&suNfU$APSM1SxuP^5-kS$;D<>snYEjk0czwuH~Cxru``(? zCIteg_p9l>+&7x*7~Rb0OT>p}j!Ehc?Oh-tk3HlDi@=L2!g<{FL0Ss{2}AxgJi8!P zYh_`R{$i%U+VtaUrHE4myy2j#fE}YDvVh`2l*gAbN@VO8kyUs1Xr@lO?@Gg<ErTHBXOyxjx8N!& zE}kQP(mhjW!CqQ=N8M-8WH^t)doW{&75g^g4?ZnFUCf%@hm*IBH=NvzKn=tsCH?31 zb!F}_#Hiu#J_R~W0fovOqB zCGyWZowZu8e#)L7Y_ZEf;kR^?UT7S&;FQ?FHHcu{<|Mdwusi2{Jb)L71HuZKuTWqx?U@@DN!*O?~JqcNa3e zJ?TBDq00TxxD3{SZA-(s|1lqmqE(#hzNMmt?&OZs+-pW75KV+0=`#rVF#@KYU`%to zlI`5S=P-b8gJ|_IKzttOLUmz2|0k@OyVJlT7$o2lS^Qf5}H`|PX+@?j?`Ksx@H1NSsa}M_A+7Ug7xn+C83jaRBSzRKAIcbUV;@``cGa z84#_XwgL!&s4p31)bkBQU9V%G_Qlt=>Ujnu$>GwSkR+t*xw)<{AW8`s$E zkP*=c7cgjj6)iriRHu7TY>;L*m}tVW>KiIr1PPr{x9D{RSp%&BmAP#wuV7>mY2sn! zMbzsRDw0U4byuTC=@QC>2K>5zmWSK6-BECI||&Ja58f6mSUpPfgEBL zZQD>iNr=WO9g`3OaLWiXf;?D1FBbJO3I$LST6kmr;g7kJfx|!~hSh+2Zs%jwZi&qdzf^Y|3{R-pB!~J+NI_` zA0x19t_zL3V6WO^eJ!5TSTzzdSL(EkAIvwk?h`tsp9#ZeY^QH!_+{m6E9YG&)a(wM zg4o=ydDdGXRflP+^}qS8O+OZBI>+hF1`JDjvj~uCfos?$ZKnrv7GD3_*Vdo%f&?OD>n&a! z6HmTh#y~hXSU{g%>+M6OobRQ_S`zw?4AcVanqX0ka?7O}z?;l2cm+fXY;AFI0Jt!p zrZk_z-~QXoJa8oB13QY8_F%`D?ayNL`k+63dyzX!G7qs6bN_BElr$Y6)KqW42rwxO zOJ-G84|6xgSoxqf$kG({e*g?o@3eAD4rVR5I$k|T$Td#0QR0^hebU>5A)-zx6!~sS zspva8s@C?;q3&EKVzgI3lRwJCEtD?e0RI)E;D_|f#p&I}seP<}i#MCtYemaGiQgoc z2)o3VM6GvvDGu}HG|nV3j32`QwO)uA{n~&iQp)|t7AL@`OWI74@lFYn3s{4VNqpey zeFwso!J&19JZ+ZB*rt621oCt-?hKygG8`1IjiA2L3F}YD$1v$^=nq5OHbkDCso)EcdXhjf8C|kR*SEbrR#ALlitw9 zpTS`A7(IZ`4@5jNK_;cRb1Og$6jkO!*K58C2<>&g*O<8V>^7~pO$$KoG*vg)`LGYE za(X?rSUq3w%>OzBi?9{4g8l-rx=FDX71P|K2UBbc#Trzslww>|3Xf>``|$|+^3cKk zz=t0+jn>lzD4r9W`7zUIE9kag0%g}xr6YSu90){lW)tdoNLBaH%7Z59-9C)(!c*(# z`JT+q*1MvqQ&G4U0xI>7{WTprvtl|{eiJ=r;Ecfo5%^G0p;fbM5FWV^?jHs83LZ8> zzv3P)ju$khfG92Z2V=&m=^M}3#*Zf6kB~NC8TW{c@@5Z*_VAl6PukRIq+Q9WFL+m6 z2aHQJlo$d(i-Q@k4##i*=~cLFuRj5Y3caV7kNO1vkHw|XpsmzUCa!R7?Zw1Svd(6& ztEcY7z^ya@#u83!6J~IF@Bj6(Vi=@@4HjvOE%eHqRFH&un52X^S4d?`Dd-7cG#%c3 zMtj0LIGL>(MmhgTIn#N@mqMSPq3L!JqLB;frkX&)zdyqMpU%d8t>F(GTRX5PP+8VA2yXF zy@s$kg0;(k0=`%4T>616N0yN3wx2=z;WXZTx7dyj}BZvFgv=YxPa zF-y!_P2T2%BWq_6pt+@km&An|y%J6UQ4S;QuLr`4T(b!E_bk_4k=jV^bKev#(;4Fx z$UTaJK+z;`xC@SM8rHhKc_-PnX|(C-bfX+;HpOm%V1@k*?}L8eQP7 z548p=M1ou8e@M_TB9q0c-cI*~W*y}=4eMTv?y!W`ifn+OY8z%Lu~ljF+gGIpiT*|1 z_o+C_VSd!>_d!2zuJLPFjW~)%R?IpGUp4nP%5NOxyMG0}MYCsfMPQdNm*egi+0XC2 zZ%iJ*Pvh7s56q@Jtz+w(VrQ~11aLDzUH;l}!2vh4G_ZExb=+Rg<$%yYaaxL0BJ#dF zt|awEhA^hW#$9;fDj3B)hQ^inU%ZRA$O`s~2mH~*b^FI`0qEv;T9Ly{lvHmgPw#p1-}AOgV51*sdtr)u$-#5-Hzc69;^P1p{+B3 zlZx02v3-bzwzz}vKrD`j84v$>+YLObKrtbe>=}DAxTV0D`;M9MirALoMW?Rq_!Ycw zU$d8da0DRiojru(28XtGc;m~P&WVNA#%O>u31&GzL z3)_xe2E_%~_M^NPFOP*-Vmj_QCGKfu24%VTJ~saa@m{@qMds<(pzSB!r<%TBkQ5{Q z*U|R91&c@~%nWC02>+(YOC9c3r*;{ijKltEsGau45C4(hNz|I@h_e}95KnJj2NeW) za)W+e=%2q1%WLa%v20C`VOr|Mizvz+O!*46YVOr<3;b4NrP=_5O4A|U>|3ZF!4|fH zt;UK+?)xzEE&mGtaw(wQr)>^LOqkM{!}1|nCEc!|)qHMb++#&(u84&*D^B))Z%2VLaF?M!b8ZP1q1i1bS# zc6%$OXKQ#Sax&enWn)gIMFy&XFx0CGK=0OxSOo@rMJM|5!)pS@72l_62B9|{p=XBl z1%PBTHeU5!H1uBX1<8$vCAa-sIMG2Z=D=R@A0^X!IG_Z0$zZ;eEz4Qa>d-& zh&D1c_c+gc$S2r6kWT@_BcHCL-UWL7_vF(dxAvD$n?xkuy!HCAGA#GK)?WE^F4n34 zjeH7w_X?2^DA!K;XT4|9DXk`$Sl?F+GZW<2f0oq3S;?xE{uhJXtNb6{f^XqRaR0Q( z${Ev4xYDcuWqhw>Ctxy{GxGnrZO(tKp6ndk<)oh{l`k9l@^r{t#K_aiZO-`{_}{tB zDc|qF$4kg@>}F@KT<5kq=TWkwrblWhu-_yn!5=f9xjTFFWS$vi5Z_O(6`ybJ_e zNL{`y>KrhnLVx#Ys+}GToIp7>(Y=Y28qTr6{`SVpXkNYGD<_@ow`p$#Vfy>Z+k(F4 z-D}088K0-|B`$=b?87TAqm&na_mao3gg@}qhP~4}T%&(aauubbU{V1{iy+-SNertc$AI`favP&0<$Yes&N&VW7 z6sfGv)G=_Hz+&!5CN6wha|l*-DiwIl7A&J*>Sm zVv{DCO)#}U#8G#Ji#_^hv{9wt%KjF%f72fhVBT~xGx_SYeZ*hY_PO+jCB_PZfe!da zZtVVEL7%Wvugm|=+omVkA^Q|xrTN3Zg6gQ&UFxp%zxz&4Pj!B&U_~FR^y4VP6p`c} z=nrQMo-M<0OZsm;p=VmRKO#Ihwa*axmO5~u|Eqr_brhx{@Aqc4vdZsf=3(iuvuZ0h z{e}za{!@IXB=gh`*UV@BODMx;Q8Oh!gFK9w*em$c-b{Ahdhh)nBBK zb_C4gG~q329T4Y7wy(OTmq!S*N3+Ghf#^U66#3G%KCt&;KG^w zP8^G=B0FSIFhE<`duC74#Ad}OVlKH2{~1EbZ3JELBhlbn%SaIb2T*}+#*nigzkf1+Tq!i>`Qu^9W{KW=jhgb6FXct=Kifaf72{0Ff!`)< z!hg;cR){#QM;osAu$LrbXhhspOJw3n|I}yPTU#G~w&krvtc2Qp6^M0>3xb^7+4ixs~hy0r0 z?|i2%%MS*g4G{ev#F*!YmH2?b4|l)&|06$a>y<{W^B^lB%JkMBMpyRa2lXh=4}F|vUhZ>ZGpgF#3OnLo}C11yQ;HudtufuG0^cRmpC!wIJ0kMo1M50o8D z1OfJ5*0RU2f3t^J!9AJ5`uRa*I=Hmj*J_Z00}KIQm>3pvRc)_1XBkZ@G<%j+ETUBnfG&QjqGsR=BP8wO26zjvz61PlhIW;N5et>iQJ4q zjLkxD@>f}HkP=#%9dfKcj6^ek@C7lLSjuhB<5yAY#n=1DLuRGBDHU4fZY7?&nVXu9 z>!;?*9@pA4>*U^MW~#u}9h+?S6n-ULuAS8i>T&L;uz9b3Kn{Ns&2l>#Pcz>V*twX| zidyJTFrHUvG6P}$qfnODTjR1m=dD9tUPv0u29mO@GA}7NdV!KU6D4(~T5AUnMp?bP z59Y2%Y6x?AE?D=)KrRS#GfCVl=9&P!pf7{Dg_O!;PQ26fMKEWh51_GrwYU1I%4jgA zY$^QB!I&QN`E#deUDInH6uyKdgRi%k^1;{mkKxPI0KUu$HeVmthtrTiE&d`~;u_J8SsH0@`FvXTix!}BF75hH?e$M_{1m^Zd(Dc23z4t!kMAG|}*(CCQA)9l;eWG<^)P35$ zrjkl=$Dsk7p;BKR+4Cr{B2lgVc{BIwL>0OC9slw3MH?LddB=b6LLMctmNh+Z;!|Bk z5q5ANEmt+(e`ns*WUTGMY#VkjQ;aJf+pp6zJ@~IcU&PCP7!8=?ERX|0ic$pFOFZ&U2bqgNh1 zD!Gt4eSUQue$$Ty7&mhf&ehyp3Y^ZU(ozZV~OpI5-gD>||r;KRGd43pM0 zb0j?(<$VD(92g~|Y1@$!deQqicLplJ9_It0+`u9TbueDF?z4&PC6#sV^3;rEb{Z}t z48T(a@mNui@4@hV_c*i3kQ%}bQgn&FGEnCmArj@2awqlIO>X2dkeAM>b&k!Mf>9MbW z9`O`R7oK3_g!3|NoTNquBf{Apf03;|&f!R{-cn`J^j}Kiu6>Knezhjd#|eE&z{E&U z(9T$T%RU6tNlnm8L+~;t5qZ(cTu)WzxUT{<1%12h+xgMcmB*&L$JQ>is$0Gmg*m)(M|F~i4Edy9fRyeK4nA|x(3><*O z$I7U;4?n~G4}sEvHUx+gk-It!t3PCVp7USKr(!B(=iT}SK*dhxEM>nCf0oD)mPBO> zDF8$LNorR%%7q2&T0<#VDZ7u8FD@3!4Spx~I_=|*(Cwi?WnKxEqRx`hfTXqQh^AA> zX}$IcHRwM^5n?Wc=teB^;Cg^-x~Kb#>R}@HIeIm@UC-sY{q$_)5gjjyR;zOrjBJHg zH99P8LYHB3AGwjOXNz6`fZ4D=naa4$u}mF^rcWEjDtFuv>#LWSZ=(1Uhg*IDyqxcM z$m?xzD5rc9aDpuNZHY$AK(&Poq%=U z>1o#{HpS|d+uhxBm06A&NtF6BoAn?3BVTMEQ)(ZCD^?6sD&SSD83p>x^Y32$7t(LA zs6Fg4+22}-|6-(53U@5`rKI~0mbe9J50f-~4u!8a`=|v|=T&VgNt`T5 zO{(+GA66&N{x|7k~=v+~EIruX1C(}zylaXDiF@)twUWjMN zMgpody-EWHA%Wk48ofOk`A2FFeX7qd@r{J2b2sBlVJfY~Y5mmfkY(M2?RX~Lj#{r| znLgLL15`DQ)uz8%)by&iP|hDr)7N;dx9F)OHD8WG?qeVI)Mn2PvnS?_OHwes1uutM zE>weR;(1bt$6HH^5N^LY5ZA|ALnp<2jqz3c=h9ZhOt-*m!T}>cbku7M zp<-XM5s&i%qn7A-IX{2$wyckQ4fOl(u(B#Q>Z?yO1eLs^s}DGfu;RPdjZ zZOTUHrVg8gzlga{eXy~Bv8L$+@-z^)+e^!FFtT&T z(Z%UE^yKS?0m5=ew#+Dx7LO^@q1OeBhsxL4D^3JdYa^o-xl4-_r|lXF&h|7dweW=A zcHb){1jX6PNiy#-v4)(El4yRl-hwSc67EQ0j7SugXjw=hj*^I=ut26R^x77QqSmCo z$)&LO@K}^O4L=%G4G#lPLT8asJo%2kgibK zr8H+Q^WIBv4fGApN)U!3dyf%cV}^v*6NISk*Y-^)ak6IbEI><}R^Fy-b;L%Gnv?zh z;-4{X$t`nt95hC~Q_D=IGiSi z(}ZbGj8^jr?Oexq+KGj7`%{vML~9vm!Fjyk>EnH)xq6Olt)hxrzUgMCIWaz%-bMM6 z`!atld`OV0x7iN!?>o8}v1&KkCZlt-FeYfb;C{`^n7eg$@p3+Kuo#75GTw$6-uQe= zg;}U-Lvb~2kpCCtCr>~KA_;eOAR4Wzv6!ZMiNR@o8_PoA15o6TK14fotKwP$zrbEp z+7Mld@f8gsBfsN+->6C0C9_-=N3e~_<|}$1^SeLv zDC6oliqRZTwkHzhjPeNrmvc}MEY?v9XXl9^yUrXkdi`oB)z%$ z>Bz5NEH6eo>o^$`T6AtO^JM;kEYsLkFnQ)S^Wu$3>;=JgS1dqqJy3ljg0D9qLoWuw zLkv4XNV?>vO_xa#a}^X5=QBX@zD=Ku;tNPK%jyuE;@DB8W_fB2_>_fHag z$NwgE*szDwcBP&t5|8U#PQd2J9~T&XGZ(OkWtMzb36 zf;H_$33z}^KDpY)Jm7%W0KNvNl0H#?+XSuPZ8V&Ul6mu3gz&cnR(4vyysw$R9)1pe z-+kLyvFh1YggNTm59{0={smu^lzA;DYe`-o<;)h^j~|PRtDIIP8AW^5T$#zBtfEL- zjF&36>Qk%*Lz?*#wU=wxq>dE9T=*^u>O%rdamo{vhz1u~KW-^%+cf)oN(LO7Gpo_a zxYAT<)EXm(+H<>nD@*Mgooy^1p;8HKt|X!UgVHE-%TipcwaXvGb5QJatPh$0E->F0 zz=Dxw#ESrO4q{3WEiQFhwankXyo7VpJgEE^87fYpDILM~`GogS0=R1<(@Hr)4dg9P zv_xH#9F5HN$(1_5d@|2Lig)u?_4PS9Zrv>PUEvB3>Ft=43H7TfaIxFIwA@?VL3Zvx z;cv@pz_*hYV%Du#Y|*+e3EYEc_P3qPhooxpzRNG)U{5$`d=hrt^0zQ79Ak&?OVAKW z6Evg;utlaJtyT8)<7Sh>X+44*+BJIIY$D*JoU^+j1$nj0Przncb_XcjFT`qmacR^w z2z4@=5pQoeV6@&y#Z?3GKAs`VjB*Oh3FHEz}U z9N6H6iVU7gJ$3F!weE);vKaBh_SXXB-g2ZtZe~5|Snej% zmln0XeQAx8S&0P5JtQ&EZr+PX&%I;bp*3rxBsk*T>Q6RfV&UX3%}9E;X*GgvuhzQ& z%Zxq2;tzD`M)v9;uoLk*)+;E|bu?%+NW@*E|I$30r$OVbbK+N5EFuGWjW$W1DgLA6 zv16z67R{lnP2(ih^EiQm1(beMHU0;jGwb%cWvD#Iw%Bg>nQa$9A@Smc&sr3Nf z5bkKOPV42S7SrIjrqcd)u4Qkbln!sV0Gs|fnOa6_Cx_jJpqVuCt)nHy0!on80>eoskR7ed*>5fKj=Lq5=(l&F<4^( zaiBe^;=gz*Bf##2s`19waF-^RT3OVdUz2qI&V8Sr5zN~=@hX^IqAS{J2q{zPw4T6A zP|L%dTQN4wdidO$S78#ejp#k4vM`_LgnXWZ$;0()%?KtsIY77s48BYcc)-bAcMQn& z;@Ddteq$lgk9elv}3643dxTbU05d_2^xwdL8W5>lz$5Io^FYlX#X80!kbPZrw z%-+y()V6jgVobJVK(uPTmT1jbZ>m;8z(CtL>wq!Y69#A*)JQ)Y`IN_}(>hr7idy0! zLMSWlU3-fe=z&X~3XBtQDbj*NV&=pf=h-f*7{wdb3UktJ!30PH`AI2pO=C3lJRakS z8ro}C=CGfAQ}GM}ypEM1%g${le_pI)tuw7gfkWZrN)s&07YPc1n;^9a)JP@@g-TpZ zVv^OmL>-c$`)ASgMHQ7FXqf^Y__WKPi)ATw4^rO9ni)q!*3 zGsXd^@&}MR08&IC7?ShH7&vr4B1%|Z0Wt!CEOF3T?wS~RR^GaE?*P}v8V)EU^dOy< zeFEU*A*?G;{o^<|5iGKWh^?zFBA$g$qR?U`=L5J#l*uQ-Ay*yzf(YYmT zgU8#m^1}Dvr6Q09N%2zN&5^8NNGLi*PTl3oN72N-NJczWP1T>p8rj_CyC@lG3%MLhM>{5? z+%tU5!=r=24SBg`=7UGpNvidv&eqsBAeAj`a%N0b`qjVHAfy!GNE4=IdTyUA=ffyI z@TkBOCz!OP2AMnI`EasLD}$HnrTHAsd(gPlnqc<(Z`(LmU6CAod-%K7Sk0-o))umw zmErA`;=QxVMeG{S7V@)AM9*?b{uJ<^iK{1){iK!=GHSm??kYA8b?25=Y7_a1~ zcI7e(e-hQ^w(idJx}6-w=)?a7qsw@)2cwT|>c{AVR#--V>qy9FJG-Y~rWA6|vw0FB zKN3gtOXeFnDt;esn05~aPe5!_6oJS{UBz4c7=vGJ7~DvL|BT1)V^0mI#2*z95S9G_ znl-{M@h@;7`@$YL_(rN94vLps9K0U3IA{O|N70y-V4Z&tW8RJ`bfFn;O(#M_A~ATg zLI`O}?knbcC0c+1iS6JRJ<&ptntS&C28%Qj!C*gecS9f(KPL7TH5imkhyNrt7F%>A zm>UJ2f57)NZLG+B2ak}XXQ#ZpQWu=hO5di14I!fuWZvBMlrnK0H90z&8#rlaM4Wez zF`jKt9phcMQ+b|CeLm)0C$k&st1|1KxkYbmDXNLFNii@Zw|h6sBt>8ECxhQu94bP> zt3YTIO*UgrBK9AKi)v%EurUD1-9ir2GJ>XqXO3L4Ere$E9J6pDdxPm8-SkP*Db7Dt zgc+EGyGl4-%|}`1rB-OB-(-I8sMx}9_ZKwb#R7kTWA&1E4mc8Pc%A#aRl=3Pv3!E# zP$<~?U!faJr|Jf2%(}Z$>*MZ++~rnF&cnHB(nR#HWYGdfBs%KO<7V74wfu5M6WNvUbkIj}GOdgiV8cnX;pc=C} zJ_((MxWLw)iUIJ8yzAa<)R{>YT+m~KLQ~-cr|sRNjZUh8<*J>kZBh4cRc8|Og1e~r ziG?=OW(SFpr)CV|3T+jNx!sJ*B0eVFzbS}93y+=MN8s|nU@qpxx!KZ_^PrstDb*;9 zh+RZB?$f#3_^HXf>11x@0o$cWQ%?M6CDOCF5RcslWKdo9%!+b$?Let z$_ng%(D&3>==JF6i5Q}e!$hHNA^0TNMd5Oos*lXR(?O)wjULnJWEB4=SHf$p`>tk5 z4YUGuwLZtfX%R07Ekhic?}6G{m1)KCswkcva*{$F67OkQ)P2K!KjuEuJ)*#0DINpX zO5u&yyC3jb$|gsjOD+5HRM_fjS^?--;6vPd_T-EwBHL!HEsRsbjuV}mlFUxrld=S` z|C{qAWi8pFv}4g#WvC=f92@=jNtH2|P;s;gxl$Ed0TYMsF$AWn`cJLucc!WsRf$iv zP&tA@HD>RLdyc57Zt$m8cdn_9LDUYG`&P_-HR*v<4TOYhddWOm;TGQ`4`Nh;kA}q) z-nA_n9K;KXwU%2-L~ay>oR}*n%Bw5sawtG`DQ*L-yLT8 zEkjv;Yd1eXlxq#DWP6WI}mj9J)gER8(lWV$sV>1M?x-NIwcYYAk((<;p+=Cv>9$GdK|P5iU@S)?Sc zuc{oEEgOJ=fyH0yKrd15Ca{DQmV?AV-&MuERRoraL0}Cc!+O6R)(Aoxx-X018VuEH zQfZd&!1(p<*3O+ra6XFcjnR?s5j#K*FKy1}Xj7qfS?k%Ve)~Um+qWTaw4r`3DdZfJ z*%xncmuf<;dYM~8^thA&Kje`4Wr#7q)B{yLpMXGylB%x2tc8=>=hZ*-w{K+2xK1f= zgE?X8$80b_QzMW*W`dFR#_}BXG+Gmk+No!vXb`{o`#dkG=!71q&##fpt%@#uVQ;R3C^EWLT60p%*}OB(0PUH&x# zii`3+%p8leaOv9LbkSVi&FcgA6JK>s8}(x0g0km;mEWmvYTHqOl26in;sV(A2x_1 z&}OKv)wCsC;BlA_Ios3T!AI!YHZ<^cP8(ZvVb=rSpj)dgwZt|=rJ&+BtY4C9NYcpi zIEqJ&I*%g^z)8%>RBc&}8K<3^wTvfvcmReW9xV${I+=Qg8~1xyU;$Y9X!cIwTL2YyJ{#dw!?{h=mUV+Y0n`?mv^Pgnt< z-t0zK-gt+KH@@68*D=@tr0%tM0KIp1nFgE~FQP*p;n9Qrr#UWR_nWT2&)OLp*=ve< z7gt8puhQ=Z#ym=Igd|M+I0CZfy1GKxNl%V_AOoJRMoUk|+`Lhm(b%cp*k)(p(rD-z z@9c7S>~!~Rh%+Ci%gGEZRfShqn3elRs&q0VQ4{lRa8e^Zx$*;s<2u_bYfhp|!#|}< zvLqF{G@#I>)qk*Ex{=6N+sDR2O_$|iS6A48j?;1;d6F)g8$GVH`efwEUQ=l4I!+!~ zt%XG<0a1WtYQa9W zryUEa(QCoUTWX9D3oJ7&rWdfk$VS3RqBrdCe;O#}x&R^=S!e=_kp%|O5*DCW{$Bw! zgQHRcNXorIk2TQ;*a4t&3()faR{*_H2S6C59M*fR-sjOSWIqpd%Dz!xW@gF+^B!zW zoHeu%r*(+3qO@oDk^n5O-(&k&z zn_S5kY2U_9RfGK*)prZ;sJPOAHBYvXGjFVtt?K@Y4pL;RTFtKrbG`Y!!|2E|#8wW- zFEX4~oUGtzB)m*e?fpaYmdHV`V$~ z)bggoqu!5)^e)LmT)C|!d1@amty51#o(0SGSf6%j)m62j>In&9qjy@<)0(O-ZYhsv z%MNjpx;N9P%`=C<(^YFi>#CkW)57lb6hF-p+D2WDvvk9DhJnoO!u+&9S+P5TRw2<; z(>-%vw<}h~QL`7KG-;7JE*m>hCd0+#%IjtdTP}=(-$nXSAzd5matmkj9oeyNMpJhl zWEQ}-1QZQP_c?`VZgijM{)(&`L+Hl=@yN0n`(lr2K-Za%P*PkI&Y2wfe3GSmT)7(e#H_{8Wab_u}|&mMY`=)IwPp9JI5bg3E01}qT9%TzXs zi7~>Rj+a3qiXW|x=vm`QVDM_87QYPeutNZxflL+(edoNL%o695D#!$m%oFlQ$s0ePkj;u zNLvj05seU`AutQvNLQkf7o6l&3aCwTm?{O$5>a|MOXIXFzkJ9#e zJfzM$tv>Eft1n|5lugOaR5r%t@78yDbQ(k2LeX=QAuqaZNDye882&`!(KsE zp`Nn*`+L0eYDMCKaD`t9;}Uf3+M3(zoklX zb?Yo9{lzV*{o-C62OEOy-%I=3mxof#e5v8f%u5J%KLtB(3|gziJxjk~zYFz)(fr1J za@k_HsE)uaxcJ4Pmm!16QNwc$zc^Kno6dbUOz}Kn2uN0htI#Po^M%cTc!J=zTY6z` zdF7*HW-+Y>(B^*MrDiPU`AzQ*U__?MtC=YbBaO(vhJ~UShvmWuI?b2|bjsE`$zei& z`hS{HEIf|NlPgvYjTQ(<5j1d~h6^P+RfPuWIEisNks+)!Ec<__n*ZU`y3w&`_+9`y(8w5SsPiblTn8J_HBI@3&AS* zj(hjI3Vsz(V8BYyr#0{k^j;ePb`AU-cW{~y#)nv4jRnh4j#}ndt|Rxda((eQ_pUTQ?hQ1KE6%zN*QO{7Pst%c&>c>4TwN{^Pu=aC5MR%00SGU|J`m zGj@lHayLC^J{UHceFz=}KQSNdDlKn;2rLU%fd0Ogg$1{AzCZUhQ>AeYs>L7H$#lM@ zDnH9&S|^vh1=^9rds~K4PQrL+G}~>rYO@8$87`-70VQRCU?8E&TYOV-rIJI3<_ZEKwL|?RHKnj=s(0kOql6 z%{W|cI>|FZ4qqgt^eq)s%1@*J)HuPT>#cFn;8<;*8O@yQKk&A2EAtmdX(rD+`Ja=S z1>wn=NsP<0mGDtF&{kWm5w2AWV}B&Rne2QJ_}okbC`l5F!M=B3!N}&#Hi2>IOTz@K zonmHh*A&x)3Cp}EI$sUg9g>*)#TBm(gBwJLn%oUe#smSNE6C>R@kpK z<8h0CGxh`fBdme(bqTcmd9agOj zuHfLfz-r#TEB?%1LYrvM5@`%-tc!zh?S~37pzy9f)rtr(!OWC0(^6Jq_PQt2X5=#I0d0 zOolVDfq$W(g$^9?`#`gVToFVfbE{kGv=OaTNaa{8 zBV8&q7S@`a^sbduAInaMt zJWJG@ov%Q)_X_a!-4eUvAOE4tMcjezhN*ZvB(s%2-du(H&rhY&FODYEO>RRDd5(6^ z&H|1Lnw}qA#^=xblE8*+6L7BuY+9xNFdaupUJNvK-W&nS%_wb)`Q>&Zt+v_F`z3$T z>ArTmB-%m=qJs0jthby}Kb|C+*;SyQ&^t)TQ&&Z}a;sCr%BcoLV{Azx3JCemPmMAe zvsZt4$60eMW__B5cIbKlFPHNA2fZ#ve84awHpssctt59T6C3L3$c?52Fsa|LKWelj z|B1g^EV|*GXdNa7Lh83j*YmQK>W{lhT3zB!4?f!@lpZV zha4Ye*8Y#kaRn4oAjc)Vk}rj>nO?xpoy8=Is`DG{9Wxg*QKiT*_M zLO_f8W&P?O6#%=4FJ#IqAq9YylV~LdT(fefQL~7tWWj9U1=zT|-v8#047|te9p0af z(iB0EMeJdcc=mCiVEx19dNUwT8@_2mJ59X0fb~ zdjout`A%e9@NC3#exYx8Zx7&aX#6zbA3I5)NGtb`2l(JI0RXvQ0xIW&`otfsv=Mq^ ztiW3U!F`%=354$_c7cEE4GJO|d23c1U9Fg~+f?mp5U0^;I!AI2L)G&EBUWansi89F{G_$;sXJSiyg$aJhkhlweqq z`x&JJNc07NT*Z^{zjuf^3B&y*6M}Qy zsn2>70zWh&L`(?$WSR!HrxO>21Y17-hbjNX|ET_!I+_9~?zH|`5}I3$=rGsW%_@(1zc3mu9&BywMldF6 zO@LG{(n*TwwIvjrJ#PtBw2;Gp<94BQUqVNymY8?FDv-EKdQqcTB)r<`)oE$H)t&|p zE{fq*hi{Qnw+&T=g+ymOvMKJ=ZHoNGIV~4iiEPH!lU+v>F^oxVO9ifl#N9m^R;*`s zomBCyL>7IbYTINWn#4_cRYVOqv*P?-bJLH0t)=sNvj@1^K$W&j|Blr6JTU7F40gT<^rHULuD05BJt`ok{#sKNxg%Gj|+S695-u3-5o-K!gn~~U zlJM>fa?n7fDmA%AG`+@qjp||_7LE&7tZYb!sT%1>mcL$!5Vq(l!q_+|}^RW(I2i$w+!;JZOf z5Q_t=+hMYQE5ec;E7E(fF*5OmlG$79Ascf;80Y@;!R(IdrH4;1Z(3`%jhn~;!87E` zK(r$ig9r+U-wgdg&h?F#Hv5fcweAi4!cFQ=5dk_9gkV~UrYSGME0;_DH?s4?8XISzJBe&Q8=yAd`5V1(cZvGJ6OC( z--HA(g|Q0Y;AHEqhIIxmxDl|j_R?_4c%ga=07=JEozh{y7ESNMwO$H1Pgs%P62R$Q z#ZG3cLCnBf)gt$x)5=W^KA&?ZG7V~`kiRty(~&wjSf5%au4uY5#Mh>+N{0zPF`d=q z8oCibucWAROG&o2Pt<6AQtoo9Dg*UW`~ZZD-;tN*cG#j;u?Eq0Mfu(WY3<8 zOyoUZI9*oGm$3i-Cas1t6|x)%b`}nD%!@-9%gt}bcq)XJf_BPZT_NLwN^(0+FcJp} zmv;vBZ0fCNOHhwC`E}dePJVmqd7iia^=vzrdcGUf#=M!s0C`Hxy9~p}#hCpkm-=!t&?Y8cPhC`=U|_-W6t-wwI!9z0xG2U7LNSzh;N7`j(}-H=f~ovZs3GWgWmb zkUo{#X-jbfHAB|XIXrm;$Rm^xit7!Ek5JXwFHI+VcbU%ndzUc*ZchO>AsaZWrLKkP z)1ekXOMaKBj!3Gaq3KCA$epf3&$SJzp@e_7X#=zxu54yamLD&|>>Wz(^K9;1Mgv^H2qG#X6p{3SrUk(kc@&lMe3!IRkT13c$?yF+bMNjZ zE$Hv}`@jBuy_&uE&dixJXU?2CbLPw$ewzHYdaJEXm10q9s|sp>nDl$AhH8?T)pRxM zbPP(1P+7mF4y_N4-^~W|!45FpsjkDoXZGf2gH?}FzO}cucOdD1T~oZ5`zDku{)Hn+ zvO-}_6N_OdF%}91Z1HB2+u3P!S_Vhb*GU_zHR0I@8-`Ou@g-a2JE?49ICmB$i;MWv zKjNRolVirY<7)ax7Ysaj_Y}aOs-Z0+%}PX3C-aJL!a#IL_st_($>< zG?!DAvoJaKn&zfYp$0K=pubevPBkwWRr2JXQOoC)zZ>&EDa;WcazPZIV(z^QzQV;V zj9FZX-*wuIDW!z>(O|B*;k%;9-InSfOBbz8Pm=!dSt_?si@B%aMtPo$8?>xm(mxRl zm{|#wiek}d=wZ-76^k_)6~6c-yIG=QJ=8paDp2w6$LqMIpp2i3_jg>2;=?}JyEDnm zg1bZ{je+5Kf5r#L&Y1M^#Yv{O{ETV-jQX)NrhI&H%3fpsjE4LSPAfF#XC#A%uVtHI zMoUNw#ZPwd<1IPp_h6bPj1O*hnOsj>Z&u(F>BFn~paevD8bvsfA57#QO3$LOE?C4C zyE%CDi(`?AF(h`PVG8Px+%6^&VJX2nC0-pQ#?Gjz-Zpkmji;dJOEQ>#9wvIamjG0+ z1y}O{WoctyxIn3>9|9035gLT|-EyM<+oozO{^FuQ{GU(7|M_Hbwhf8cX#J`OM#hbK zHD|xu{hEe2;Rf3C55Z`O{OS}jahB-13nJxMuNU@t<3uqYi$`wDh0RcloDwC+C232i z|6Lp@Ta77Ek=(}r*L?dp^y(J@q!ZQixPh#CO+`G;!pwgWPSVSEAQXj> z&VkbkK}F;HA;p}U7s;!8)OXmW7OzrMMyY(P?X?T=&oOj3PQFRRc9WuDXqBulI356Uz$8rX!BCKzhx?m&XZr5SJdg4_ml_BJ z%b@jg7eNd3z2>Kb=C`R_;RuB~EYk(u;rnCihWlgEuaOg(Dn!R>V6rf^ju5e5cX@gw zrmzneItkjfg?xK?m;PO+s;oy|3%j`r(iH!~XCqWAWLLV@*i5$wq7$Z28+k;H+r!dI z?RKfC{ZXpoS+)DW(Ma%TA|Jfm)0A`bHt`2%PU5kf-zkCOb`*OoyG_p%L_WMLHZokqGp%$PZrS7nR~a@(s#nu>MQMoE--s>4*&^i=VysUUAT4zJI&Ts zVV=-U8zE}DZ#5&xJP4fFs3{Xv<4sya5WSsf z3Z1yRF8Hzb-sWu1*ZFha$amKGbGGC=YyCNI>+;Xe3B6CY6UrlgSNB=mZn|tKUPkeI z^q0K@4YDS`*6*z0Kw-i=_9+Zu{5&!?$qx8hJmA)j?VP}TQipw(w-w~?T^H`Sw&A!( z=+*oeKiHO^!>;zMhPM1n_FK>eifiekc7M0v58s6~2WMI*6iZY<7cU|@a;eok_j z8uop07vtF6?XXgr7|L~PuB3WB@dDOW8Y4-4(F2-dLLD=>aJjylNB{v7)i713SMz&3GLixsIItx28um@ zc~yx$i}ymAHh&f(Ve)>TG2zmN0vgPI@OQpnBmvd(wOv= z=Cx4AANScMHC5hKL~r3*simD9OkvJ1@qe7Fsqz)~wGUQ{JmvHdUTeITP-gSi0B#q+ zjes>me<1%c^IZLCKf5RW2`iF=Z0)Ftaj9dW(oM!f0X z+qV?vsRq&zbKY%kx;6tnN(anMOJp85mv$WAzjz z(}Kp5#6*^>m@fQ80&&ofr__?a3#7W$yzv%<)y&iMd{dK*Q1h$LKo%s4KZ{8n^dxYax!=zH-OMGlCu=%_;58$oaXvKqQ#*UM`8d$%UZ{JPABYOifMH$|@H zKJDFjtc?G>oxZrE@-^l1hmR_s^y9UIebA%|d|0*IYpqHd-rl@8TQ46j;dw;y-PHFbMY9+S?y?>@E41? zy>*H4EAs2t7rA#c^ISE7wm>9LgmnJ(_u zWme!D2THPJYMtKcbxdd5t+J~isF%%<+MYQpiN^GqF6?!@##`et7OHH0#A86{T|qiW z?h}0PjPr3680!yUrO#>4-0k|BgYJvf*@l&uJB2n4{kQt>H@N}3XC_31J9{6va)+6?!r`%C z&%wAB$HQfM4-V)(nXk^rC$ZA1>@T*6YBl^~>Al+rW9*X-4;wp$Lvf7ytv$1_WjKwA zCofGaK7Ks`x!@=px=}A@p0li0c4);&`Bs*%pG9!a!00jHWgmtBtT_8FmUy0ux`*UD zhjk7XmbEGW@}<<2K8oeWt0&QC`KO>H*0$kC&;}ugF1|UYXSm;4SJ{Zr%fL86+$lLY zqes+D`TYW773P;mAb|J-AUa4KJJM0LW4a4{qLhWjV_%!?|>oYP(1 z`(LWYiVdiS!5Lkuuq55Ni6i|==6o`~F!UJxQ%Udqy7d9Vj?rjK9;)nSn5C9YUGS**?AJ zFB>7VpiHcbvc0QBNcO+{#Z4+T(0i!EB&Hrp(BVaxm#%X2eI88-DJT{<1ycGb$`={&<-Z_Ac!QeRiP;s%@Ugp0K^iprXCn@BQdF zmYs#kw{O$xvoj`PYdgBsAM#gMa-KU~D*cQ#HP^2^>|Ojr^$bTWm-5>V9{H8Q+^5G> zg<%Ej)3q9Ee^VxZ+>dgg%gQJft$2%srVUqBtcOmljTXIbjp*;?7QeQ;2Kn|^9KjDJ zGe=etXaDi`5d+VBl92Bmi~p2!N9rsv`mZi6^;&aNT}eq5#2{wUawdzroo1wdm^I6Yc8Y@q6*NW&ECWKlk6PjBImy zuJGUJ!cemhtzzTkrJ2$8Rnq?|axAQ_|5w*=8>Pn??ub zR$*8R3MK2z>fZS*@)9hfIXg;%w`3}CFa@Q*$?@mj+1RM+l0Z#bmrb6!Sd?vJ{VZeE zt`MIXaY;50?|1j;bZDn11q78zSz+buph)rkeEvQEtMZ#8Uf0b@Wqu!3*AM!vF7#^E z^+R?a%5@b1V|8udVY|8-xk%(olrG*zYPtVL!9UQ!k2(0C9swVf-NEnM5qu=xDYE4P$luDWX)b*9?L@=>jr^Y-YQXULfzMcLC*O2rnS0JcduX<8!4cZSsZM*C zRni{PkByb?Vv2)KsACv+{xH4!fMx=#1sD0Q>I_XrW%(58GEquI$0SB>D0uHe5$ULq zNcZ7W9`e#)`cs@NmuuJL?~%v)mLgW%X%iq5ETKRJm9}Vg#R5!wsN1wH>Sjn=4@3yC zUgoSuX}ug$r-gc&RgbS_=u~#qsA)>7Fayk3E)-4ukjs1c8S!1@Uge|eD7QD|EPX*a zomsy)K1|>G?J&KwoF1!4zd1^8B7MB{q85?S>9-a5T}Hn5#BcK-OZW+Xgx^;q`0L8) z!au_Adr|uBq`yb~)jO&mNn4`Nf-?M`iu$KU@H@-tkhN6*+HZyQX?pMaFC*W3xBuZ% z`@1XZKP!U2u9TiJ(NIR=lqk#3OIcmFaocj=kUtjz*wtCWK}VZ z@YtZopvsBbMpW(93^#Ph`efk*3ds4|R#w4ZkzlNv8e3U8p*C)otH^$=h7O?*l*4VG zah(mavfkES3ou-uz18P4G6e9{kk80~3Cda)eIY(~#QysT|2*z9^%vKp&gTZSTfU3G zc#6Gn`Jt_3R9bf<#kE+=str&C`(1&9!dkR&AjDrhfg;Jm8EiCt1)-ymO2N*qp)L58 z-=A)^2oqOQHsNQKxr5Cp_=o-Ab9JH8>mAIrkJ0XUeg){aw6enTE>&&h$BPIH5j!2` zBmWC0-&@9}n7XERt?%EwEXU@fl7Ocs;rBlB6UjG%-{H-AwWIZayqtQ|&p00QvT`j9(M=V4Il$y< zeLYwcH7}kY#7T`p*1W}-!Yi5w3vJ)LLe#u?W%Cq=Gwf(MBdUld$d#===%SE()m-!9 zFqE3tmVYL_U#Zk1v`$SsfV`x(u&$eiYdvf=%wCw-YDLR>FM9+v*_2uQ5Us;L7y_Wq zuVl zq)UO{lV+xc>0@3lyv`}bkV?S!aD5?BRb9M@UA|~MZ-?LK!7uD)*#qEdBYsK`n9pME z`9WiErDIV zu#5XAvblXM=g5ybiKZ>|v*t<9D(7U{kFM)@;Dt=Ui@3 z@*Q~pN`sL;Q=Y?V{x1MJv2h0$?W!Y(vKzC%rW$b~InTbycb(Xj@=xyUAZ!<}DGK)R zo`eugZ84f93_!ezR9hVBSN>7>f8P#QKgbALsZ!ErrWUqW58ukA4sCSvqD)NtzC(>8T`ctL8YMRuDyXR$+PN*f=*LR># z^`fszpE4PC;+k-dLg7fyuqo=JOvIt|wwcPL@+e)Zo?H%zmXQC{i*8$^EWMMtXu%FyyFyO26jIVMrF@iXw$LixjC}M71($6F`%$vbH_X zLq*%*yzLdKG)1j#BlJXFAXvh-Qjv0c;-{@pq)4Hh3IclKjufdqE85tOB9-F0%%MpC#7~_WpNKJH6a1fHegRd-bUK_GwCm`qWygFft720o{SF5A~^nV~LS?Ev2YwT4kW2 z_Hby#DB_cS=>!A27Y{~OqiONw5irA&Z`0QNU{Rym#@mSTx_G1HB=dh#^Z!@1RHi$6 znEz9(+?W9#BoHB6Q*+=ETrdL?=lk()e$Ww4LcZ~o4(?tRX?=qdLhf_bEA2GA0(5|aEsw^Ia`*G%>S-(-~@{(Z*^$$FW12cDcAA2 ziaMf6|Jo7=8q*cL?D|NkuC4&XlwSt3k~|ftZjNW(8Fro{F4juIOJGV`v=9mQT}@wJ z_M67Lix$i%9PsHxe#<|fjAypty19+dI=PZ~z(rY3G6u=UBrtfU1vT#l4JPmYs3pE=s<&Nj#j;Ai@T$d#tAk-VTf7<-F!H?j$1Qq*2j(uAVZUOQhjpC|JV z1P>i10YUwWu$KK2h@*umu|r|>#_e{BCb272z7!{c2*^0Mlg+-eff}AQn0rJTf|r{gW>psQoN9SoL{W88aSk7lAFhudbtS#*Brs^+)I7jipjbzaejBL2 zBe@MNX(_)}AUA{nX~UC7NCgrma$S=Bocz5P7q{}s&5tUdYbP<8`MpCW_lWX8!5`(< z`gX}O)cX;1{>e%IPjucuD~cxrG;B(Q>5;10_T@|TS93T5taj9ZWz_boX?yfMrsDgd z*363i;_y7sbgX-qi#+V`x8>s>j1Pi>nB+TB;5bplK+=|EBEPF@9Gp?3WE|9zUUqM? zCi2JE$Th6fpC02}PHte~9Dnh}Xj|xHU*IzA!5NcOU~t9+{p4p%$#XAr(?4Rw)ual?Oz`&%HXNc@621MIb85%h zy3FgCQQJDBF2BO-IKHlR#uVPC@~DTFru3m@Wd4%DZ22Bydx z&uoZ!S^OkKPqoL6D!9zwb(w3@I}MJtyl)QFdD%YY9cx*8c@+Ow+Rqkmd=e-`QINzU z1|zG--v2`NTf9o&M+o>5ec2Gu+|Y^U9T_WiRu^|PX8ymya0K<%G^rE3(t})t?qC%u zh%%Qda0 zsQHcGh10m!X;WooXowIgoY-lY$OiBb*${VTZqgDh)%v2BJ%c)I+`mAp&9UHVJ^1^0 zUoM%Ao{KC)F;w@FW+Tnq?`bx=71Go8(PpEo`0SSAvcAklXldkuwKLS5)=BGxIO^J7 zQR@isQf{5{vGOgq?p55+qt-E%Za~IphR|iSS_W7fb9`_{h{=5FFIU$?T}EOmq~_p) zdm~FhWE|SQYL#)umD%L3tXk8B>c+Ls%jw1)3CfGa0qSOXXcCd#ESJnXdB-+t4`Bht zJ&EQ4xjE;)DYkJ^+_ZFzL!Dku$45dw;=^nX6!F1X(K9O%46hmG{u~TL4@lcG*6g~U z7P%0=*#$h5+RYlo8gVK*6U_8n@BoF}YB4gNFB#_wglz4)CAc91j+(3Ug&I}N=S12K?x}iL-BQ#-E#X@T%2uB10LYV!0BDrX4Q`_VN;39C zA`YU|&xLiKRP674Q1zJf7CCe5g)yx;h4vad}jrWHNe%5HK7+In*?xI_SHNOGB zZ4fl~TRr$4O->KES%vPf%*|`rluA@T-1hN~dTByAXc)btS!p0FVk3zt;kN-7l^_Ea0xo^ z47|kjs)4*Z^v(i#t|ls367e$58YE?z&9tLlqFfR)*tmJ0lL{Hx z5yKA49c$y?l_FAxQ?L%#xEK#c%k+nr+4?Q+uaQXGno(0$(@(|E@xeXX*W%F?z=Q&q zf)2zp=hbkPxI>-#Zn(obN~9VB%-f^Tvigigx6m4Z6ALQUgSnmAX_L zCLOkW+(IU`wP}j`Z9K{S*7H*p=u$1=$dRl^((i{kPLc;N|I>W!hQ)gLKR>^m4!DN= z`R|Gg;W4els@E7!`Rm@`1>I?YAIDSd6pxT+i#KYBwC5l7KU2f8VvbNuBEPwH2z>42b>52M5|@*&A$#J-Ha<)M(Z&gax#!+lBR%$T_o_R8*@9|V`Fe5 zhtgO}_rw(@{wYD1L<%KIcW~8jG}*JGn`k&KTyG&%1|=3xOCN|xWN+XEBnl?>Es-5N z=%x@oFF+X9wQ5A1Y(ZL%feTi2<%F6jmX{HiSrKvF_L zUg8T0jL0o-WACtw6x0~?<^pAFB-<(#Wxd(NdwuZy_b|Tv9*)#t>!xX)u4##j>8Rkv zUoq6&x>4;&cz5zOeJEq@w5s$cG&D!GO+S7SR$(JF7oLogZP102_mF%ZODI zYyjNOHx&5KO)pHscs(5Y5A#Hv8-mbPdB1fm10T*OE2ov1ChssVihFlH&{3E<3Cq?g z)klx1UW4yL%_;fmZ?_X@FZ*Ho(fjR~p%+9P1Cna597t5Z77@}& ztL3?_h*#fZBXDsh{F;W~t*KfBdD%0W-osW!Es6T6)Oe#}abHGZ$=;)|_IIfz`8AnW zsx!svI09Ia953f%{66SX)mFVmQAKrA3n%Ik&AX>Th+TXeND9$eSP0^uyb$L89t)aTNjs!k`we&gVdx|$9 zcV>Uj`0!=@;K#a->U-G?I+Hn>gAxZ`KAR)#NSgTyq+uu1Z_qh3#`bV$z4nVb3bSfy zT_cUdt^X=FUI+M6FRI3eIltNl-DxI!+JLypitDhYD^5tXsC0fH1fbHQ*6?G+3$zE> z51+LAcHu+hTf{bSj0TCAt;KCfH3^dnm|~)&^s0I#1iN3&h&%wO;upsg539G0 z6PEwoxI#>zZ|XVys<&qp61Br^)vsA+U(1L1`mG8LD%sjry$Pck?gQ6vWx%8hw*sN@Lg?Y4dZ!nIw~0CcWnCn{CFKKGvv>+NGdj@hSZ) z^vHU`&)mcVtP2j5PWbcYSFa=Vdg}n^k_IqY)FrB)Yb7$(HlP9gHIWxJvtvVW+TTTR z?!Q997EY*76*`)@HX`wHbLsN094nFJUov(;`KNNV;Bro$j)~_d5an@z*fd+}djIT* zfHj6-gfoyX1e&G_?0$VJgivt0usau|_H=?IyNn8pfH3LgOH63<+aiYMw~0%T`Kb>k z?C-?O!El}U!eqT|Guy&(Vet9eOetE36Birp+VRCbz~yUajDm<%jdO!fkq-iyDH?@H z`n8wa4X7#qz*CQt<~s4^Ns8K`-8gls@9cNX=ez48>4T)Zv<`*A)ezP8-CmVLbkLTH z^BIV=q-2OK8DX;xqKP#!>kO`Hcet%8UUA#AG(a7iUr{~q&p*SZVr72By^gBYE5>HB z?xT1cQ!FLL`@!KKHaV=~g!C2Wuy>3%ICq30R^yVs(X`8BWRT^tk*@~Z%&6sPT*#A`kDQX{{vw`Or_9={8u5p^?vE(Q3QQS~yp6cVv9l#4~pbZUoT z;?xewDg*3@Bu*fDKzM%!9y;mW)G)$c4EIYM^`ixCph#H@D%tadw;@}J{C0SM_f1^I zI1U_RMtu{8m~$*H&WAH0xcy>$5|_OKOEf=isuf2landAARB$8?Fi7U~zW#}I79t1- zyjfFVY|x$D+(Ce^TYt=c%t{=A&{|HT1B1avl)8 ze2a|*^J?^#=*X~5FN3`-m1os2E7H_p9u6!(8Jb3tm(3{;@v>*RC0@O0&eUF7;*|@2 zOa&EEaxgtFiJooXSY*@(b(X@)=cj^}Ils1wn^<}D-^a@o?S<5*zX7T8S8V2M$!*do zW2c(1KbGrR@sJ_NI8L7(0@L}J3ShmzrlT8%ZdVIjXqGvs9yajhB31P4Z6HlB*@aW8 z(gzdu0&Ts%crM>d_DXv91EkkhrJwTaETi!3{ZnKg=0&yGX=350mI#WM`=siS6QVfe zWd~y?!m5&JxzZO~ZMJV*vad(}$&TO98RSq7ztR22)&~#ck0NOM`p1{}?bwLlmLCnj zP5Tl2wi-EHyKIPVG`e5&UZrkPshub_-j&)ImHNtimAXo$*4>~|`AQG#s^~mp{^6v5 zG;H!G$4!$%V2Pw(tK+HDV5aMjsj1T8VvxM6u2$#gdM1IfrVkwzmNjvof4z_C{O&K) zuTh-x;V1ABHdk84cMQ)a`bMqC8lhJFcZ@R{!7^6P z?hERYiulAVhnXwopQ$M*kO>P-@HWU6&%9l2>k{P5-+5P77W-{6J+LK-%V7GGQt>3+ z%q;|g8%q+phlPm5o{QZBm@(-u$1|%~D7;9y@{Z({NyAs>L(mVf1E{tKlHhj6vZgC3%ldzytqE}f;bDfrUSba^a_0>#1E;f_7 zhl%4(#TBeA|5oIk)1Kes{0~@B=|z~v)*A|a|!13z{o2+1u{Oq71QM zcUD;X94p=qmE>_|p_&3HzK3_|>!eyel~q`zCa@X0Mm^a3%aPP}V6~a(zWtqZ(Dp?gQ+JZ$Pl z?|S)s#3{ACd(Yvnpls{Z7gn%WR6%$&NTA~RRVlR1$T1y=02eAb1E7pEL0&K+grI8HYbm&sql&vHAGnZbn)FiWPOg?SFN**jzF#X3^Ei1B(n zpH43~gw@7EbvPQEkY?zYSGR3tCwsEg$`7JBF+!N62qy zFE)Q~%oS%bPd2km6zR?7%UvjmZrP4#Zf8+#UQ=7hqE)t!<5#v}z@G5^dX1M=yadax z6T)WdJ%c6Q8onXNSIN|SM#xx-E1NbKP9>5u>F&LDjFBx2|-Zdx^0@WTABzz^^A!6)O5@cPhV@%Ou*D zZ>aAWzq~EGDaA*&&i{@KgJeA*xul-WQ`(U5W^E`w&3EbJ!LA7TsAuGDAcH9#BZH}w zsY<5UaP8<`DT&JGg$DI>r$T#|CU)ty@+*V5OO+0NL`Ln}-o!7)xSN<#sU*~SGf!G7 zCWWM*aXF>QY7h0)d^v6%TJWg-#B%+q|$E zL!5+j5>t&3N*0{uMsWJbedTzN%v#2j%4T(>9$c5 zG(VG3^eT^XEub^GkmogB<=R=IT1Z$Txu&q8FI83oj*lRutsMR>6~`0xVLK{@;Y`tg zqDU^zGXC#r>IexPyGGLyvdKOugDMu$N?N{D923T;Abl>X%AuK;r(}kw^d98k=$!Lh z%%C{Fnhi`4@SIdX_%pisA)fYF#RT5LZkn9?UWn%(a{UxvzG>fhHS4?A;_Ls_*7{sUE6iV#b4#A%A=t#OQ+af(OdsY(2M181(IMXL z(Vl|I&3nlo_OtffbpS=*>1=Jgd)bY=*fLqzCT6Z;q@egh zlxU>DX9UpagX4GRd;hyOTrgUsxKwfEZ0Zs z18=Hxod&u&Bwiw28EPVmEPDyiu< zXF^#sdcdT~y4`@;Y=`~UGK+=-=J!0RW=4rKv%3Qc8aU_`CVAYzb^<{IM);K^&wn;x zp5(Nu8!)ex#f}IXqw#j@Z#a71Tl}c@K57@4m+IFL-+}BAnbil-Re5$We&(=fDK3XW z@gKzNM@Kx9nR~CH98zQ}ZwdeBl7uDl3(It{F9KZWcr`aiTumiiUapt;w8iWBlgMu< zN(qeg6IRV#0$cn(iL#fezK>FaU$~3U%(5`STrW!iUYj;^-z8r%b6%23^C;p7>eW9X z#+*q*2Pnbq>n=x^rd9ir)fbrrER$w5FL!}ux>xDZ^y_6kYuS%AC0ESFxiopm;>Y--2ItOT zH97Qz=1b}U!evFz0LF|Sxd|kXhKp{~lAd zn!eEN)V;R1~SIXtdD%Nk{N}$fcPA;q~@>lMUaV82b zw1^MzH-4TtcjJM9St{kvL;F0Z3-eTTzAB}y_;9AKJYQ7=`TF1v+`GcdVZKih zo!%pvIj63wcUOKyn*FS{Lw@$ZaI(~P6Hxi}f(&=#go)zs zxd?~CmGgsM_6AUF_ov5F+SX_tNFNk`v+qs5jq!3aW~K5ghW0Rid-Hs~ zkLy_5+vG6|m^;=6DRBPn2m_1YRT$aLlKDN)?>AC~huY*pozVI@$)@4t+}5!bRdx8& zyMi;mf>aH|LlzZy*^N(OQkeW<@&}#du)zK8{?Se1g#0l*oS29Ip}{|PAH@ok@-B}E zP>z48E(-}-Ty6_5u&NjjQG6Ie`nher+<@-#3L_-mD@x(_)5mWZ-|w*RShPI}+c3W0 zlRU~b>j1mE2hGNP^WczlF+SiWGF?t+6Y_(Zm#ce|H;qYqx?874n{o%VwJtxm zSAqY+2#MM@Cov5ECrNI>|Ax^!oh@BqDU4oi{sin34#c9LL4z4v#xlbcgFU}Y9ELyk zD?Z@G`3j5`AMohrr;EoJ#nCn~OUWa0IeEF}ZX-2}fYD8AzT57@dJe%*-x_(kMiy@>Awi@32I9RK`@ z22Mu$D)N}2witZBLLrWo#QQ z{`+9B(a=r#Gdd&c7{QJ=4-s_(Sj2+EiH)Hhpm4v`;zc&PNvD~Kp|L8RB47QoS0Qh3 z9WFUy4bI2fw$r%acjxn^a6H_@j=&Uv#_zJ6Suv5e^N~jx-QrUG@D~khllI5ZR<|g%{Eg@ssR(qgW9&$HZB{nOaYptgZTwiScI`m zDCrNQ3s?*qv|wV;*lul4&`uUc$w~Oz_GnqbrYgs+Uf_yD%pEjtX41!5SF^~Mkj(yf zTA-DGxdo#|l|rBI&aGWklY8X+8M+mbjT(R4x6~3M?;?rQ`?zUIG#%;6YVyGG!O(M& z{-&L)?vh19EJQP6`a0Lm(rlX0bzW_P$GM68azRNP4i@K))2D9#7Rj^f<=m!O=%nch z|3~I0;a|(=gnx?!Yohwuc=e{%2e<_E2g?(!f%k)f+T~~vWT&3)c^Z^&`IZuNL=OHAFydw1K^&CqT3;m6^tM{;Bg zup4oJS6fqtmki-;v(=9-o#ccHLcFS|qiO)|D&Eh7@q>S{u~&a3dlj#>Y-$b6wa?x( zNM{1O)ha_N<$u6%gl@Aj_^Cr&%Wx)I15@e*BR7j)p+*+d086wkT;OHp+~x9ZwoKbb z;oaZh=rFxhJ-4VVAHLjB*EOPv!{cc$w=*&`(b~Sy%l(UQt?_fc+#5U;C-c2Exgb4P zf%mm3pI;6Rm4x|3Si!R_U|eriRPYBrCR!IzL6=pKq=H!nk_y^*C@Km;qBWN8y)Mz3 zT-bX}qP6qf-phyXuDpL~l>A_q?1=mUHQIjUmX>yQ(esZprB2{xDn=rGmh$U43o9kA zEtP-1dDGC8Wd6K`c1w(|q0+H{{9lv#x6&W6V8FYgbT16u_d z0ZQK82c5ZLL3&yq9Sxm~sdQ#-l2c{;A`&?}NBj?dr|=7%X4LcB$S=}tW)r_n{I>Ah z!Y|X%3_1JNfP87pWlo%gk#pCT zPDqje0_)N8g~t{F%jpYI{OkPVsr+DY`p=N2ExXf<-cR6g5vxjjHmTO5uR8sP`5n<8 z^!|Vz1l3^5!PgXLntv|7ALvh-qy!xE$}e z!=K+k$II)4OW1>I{dFyXCNF5L zA=nt2kLHK^U_TJ2;Yl2ez;n7R=w7Q$E7PU{(s zgx!U`8HnE{hVPCdWJdZG>r`sOpN4g5J>lle$4bs#l1bom+DGLdgS-ICq01tFFwyc^ z!}O4!ZH#DXbTk?oLaq@;LL*y5$M0pzp&z5DkGS@J81khcTlxTt`THY?NY{VE+-thD z-ZFoeQB7gE?g)Rm{)G5H_!01*DG3R*Uuk_QPa3*4^xPN)f5-4D=<#{MQ+%PJ+hhsY zaehb=)qk4uuO`3UL_YdFgN!Ok|7^znbhtu3Lx__si{Q0uDdxlVsHJZ5vMWg7J=R}XtGMOLbJYsIvTHq%*pD)sdTU9qN%=i>S=7e$a5pA4 zSqNqW1=ik)PE!=W1=^^TR$Q3;2t3fidJzV~BUE+_Y3ZW9hWZ{!-z{pj-GDULASb64 zu;SmRxGQI2(I;C2C36(f=1I&p++^onG)C1ICSOfvuCiN}tX?Wtq7)#z$77GKy$ik_2cRFDfV=1u!J-Emo5EP)X_$AUt}WU@xvcw8#fE+^x@<%z3r!_1_?(WzMU^cy~3A#V+!ZrHd@ZFEB{* z>x)U-uSebT8U zZpA)YpBfoY;9{dQReTQ5=Q9WraUJ_MAjt)wm)QAGUpnY%bp#t=uy>}(@!gxqp-2F^ zD%w+C``>WAwF;-nG_!^+|M99@B6xO*R>Hr~l|&3&M9JX#Z?GXd$UeLT>1jcE#(lVW}_nkJ*V$B7()rvZ4g8hf2Y zl%KBZ^es#o^-L>R1<=3QFy-io?cxP7hM?`H0Yb8^Tf>`mk(Ma7`zKejHvh~?I$-Q& z_v4Kx%(^XP;CREux{%y5p2kBvVJfP;Y&Va@?_+X7%z*?^1~2_Bzo9;aVPR1_XUW>C zrcGqnlQ3-;FU#b^UUge_9Jw)^tX@8}Uo@Y2ch+$bq`Ge#xzOjT`nFN*+dMFR_de&y z&1}x`7ZDp$yEG-l602-8755{q`FA#{HWU4UFYR{i%p3N!sE&?3z1GW4;Wh4m5%wH6_+Q$RC^|n? zovGb|V>5l%g9vYlqiu4Lr?yJD49i-ny}x?;9g+MBSsH_#=E?QSc(pyhvtl4*7b6%e zST?K31_;Ua7cgvJeKdycZa$oX{9=z|6J|#4?dDWRvM}z9=b0c16Vv=8Xg6^kpG2)Z-Dsr&AjV~;#o2#< zQq*0XHb>)ln#H$G3Y?~i`_oI~Ag3t`fn)-<__|;hCVE0?Wjm04&%)B!q|%Mb(9E{j z+2Z08Kngf@{dh&StKktpIzEv!xRO3>pSMZ_r?XfjPqi4UFbZ?HLAzmwQz&PZxEAD%gA3 zXN*8bFygZC)$f{8e3;+%c^nttis~)kJxq2_oDa)V2Qr{$g%|=17(QKUT77WyQI50l zaVLB6cCaneL$=*Y!>%a+)(|GN`|z4c6&BVF#y$uUhl|4$C2G;rhF$_pnm4(gRhV5m z)oz4Co!lToP+9!_SD1&~Pl4!n*0#Q$ZdUG8>+9zw-My?9 z_Z0bF492qIEdZVcQ(05=bV2B$@JBBxi^q{FCOw`MVV$BHOR8y zh@LT3iUBCUhQ>vA41KSJc5nl(Gdk;IF_RtMUQRI=iuzNM7Ypi&$5p&n(62>@y@*k? z#ppBDV$0_rFr&z#ij!Cq*K-k6!asdVplC^IQmvy&ZyOfmrmFOOyWV)w=Tfqr73MI{ zKdTM3udtV{+#H&m_!4J-)vyM;+g+>6S}0GO4j5w1Lbo?R5`5-Vlqw$cnYV>dxC3J6 zqV##_<#4?*oZigwBI+&rN3{%H7|;BxIz2w==Oi13uHa#V9=;RK_oM8OUB(13Hce0Y zv{^xdb%XMQ2%>2ebtb2r`YygU1QMVambW%1fQT=dXB!YFv1JsA#5@QdoV~UvrrD zSi{^%CVx>&ZdjwTanU*D-a|zz1~2<>y4t2j9okKX1?lUU#Q;q`tt$ zP-e7j^!?KcQ~u66$ZLP9M(g%VG%2 ztkd85b1;K#3%&r@q{5KsEQrk?8Jj)^y!?%PDgKB*6y2bryHQVM4{O=c`a)JHtuLk! zPFkYWI7aNY-iu-Mp^dKd*%qSet+H6wM1=fo^)%RP|#JCHz3| zL=;~|s{VFU95zwpqU?7_((y3?_Og#EBh1F##hj^lDyTR10>ZZ~L9RvJbeKC%AL2qj zD-vAsn&^AK>_FFm5&g}KPdSeFCdFB0ly8uyE5;%B$zPN>kwfi0Q%j8)T+;@fXhnIa57^!*n@x^cJc2*ZFgA290 z5hlkyC0*k6;CVOSa4m#2X55)Vp>U<0O^;BK&?VA&H=6(~XAyIaR179AZUDK!MSF{A z)w=-JO~}hS@$&cQbaRA`wktWo{oti@+_&<9w|;)oJL9uHFz=5 zJ}F7tEg#J#3V`cl)rLOr_(3m?!6$O&x!7HmuFJUZ)%=PfszX%btuwpN#j3f9ccH-h zN#`w6=k9sDw5W;G_7E8^F;cU!L^iP-Ue>k@V+E%03F-$@1X!=X5+KN==R4fTG*cWD%SL zl^nF(nwV0*Fi_uG7&QHbF0eE8!HdU@G4ErI|493>U9KcULgPvQZ2T2jj8y2m8q=pSchuR4 zu=)08;)O`u&Spf|O#rbs?HXP|gK$ZV(s!yYd;wg%)Y2K%Q%%{F(UhUj*+&Z4yD zgZ9By8e+xkz(Vf~ZZG=;up~yj?5_-}451Efg1x$-U);$GKwX3s2!J=IJouI@{LVO& zzBDqr>qXSymBs(bAo4DG0~tfdMx)5PZOvQpI*1qI zV~?nUX@K1AExAymYTgH$qhlFN7#sNLU8=*oO78~frvVxcRk=hnq{H!QL$y8}65(jT_D?0112s7hB#ro^ zhb(SIS4`B9g^XGP%AZI!6AjvL^JaqvMNSm z@nRK&KAIbCl0}Z6^P*iua8GIC^{)`Hq2D?DYyd4g=pevX6FaGHb!^N*#b!7z)c++@ zs?`XvB;*AQtFaW$Q^lLqcq(}*rJ;C)CnAKC1xjzo$?a?*5_Age#aST}{JVJqGq{^4 zcn|=wlL_1WHXI$q#)@F1o$f6m#2pQ;cN7TlxLD&qR5f(5jy zc#-6u>ww}+7ST!nmJK4s9NeF==W9Ib;A^JKaIzsOX6h>TntwDYvZ#ioT*~K388Ofp zc*RR0r4!(R?Z;;a|r!5!SL@v-Z|UdvA+weoEg!ExqCD}|G#)3N++5Au_YcXFWA2A3cEoFSNX(0U&!J}dEAik~rJJ?rA#i(Kf`cK&f{w2Wg#3&Zx&)2P;1 zpDggb&a0n3h3{Fu2g|7f=hlB4uinK0Bm8L?syNSrlhY9Wz`(VErQuCr&jQfP{!m@P z8n2>+rnBHgN+5n=bQNlKTBNsRft5p#J{u}%v27teWE}4bR$VOhP1(}588-zTF?_A5NgF8Tn!2(&ZVMUeh0IfCbpM2Z~dOeMFNole9slj7}% zpC3c5o^;LUn(@F@*?Ef;+DKuIiS?aog!tEssHu@*vv6Pd!jyzJ;MZxIEEJK0O9+;= z2-)zPQBkJ6fJF*@!;1vYZs5j zpYg}(LnQv7ocJ9j>h_0F-p1Y>tO*+wHqYI%k$IW@7z_E`>fZ$3=TB(O+{&-Fq=wGV ztfb5f+Sg9S5EObSWR#s&zh?iKmX(~v+ktwfAz9UU9A#A z7M=j_j;+CuD55zKBI$-if+ze>vi0x1=SEUBx=+s}tJ+7?wsw%6{&+n&`BjYV;S1kG zk(j?fNRfa_J>q7>-x~Xa=CeHjPVi zfDJmWt<;~@5S_v&ej876zxDi71)FX(jd3D zwAy@IRdFU=Ra%eWirqsY3gq|7j~)6`ynNm(vyr`ruMmgoAs6cDar0JUuf8MyaL{mp zG%#f0K~T`|f~bv|w}x%^^@Vh8mVIDZ5iGFjIv&nHQTx@?MRhGG<>jXA+2803{_52h zoF`vY1U76&7AT6Xt-C0i%T`04%tPf;{#}bl7RlS8GcpgGlGQ=wRA07%b z-pOqEmvdjFp53U&B%oBOu&yL1(2%QE>Ml``VWO44mQA!r%6(I#SAlr%IuZRn~D!HOfqj(_Z|aw z3n;9CTJKaLD~jDrMRQxpCwUUj@5G*)uK#i4$tZjg6)id$QuIDA8ZfnCwq{RxsSuF- zw_P$IRR!6-VRM9nn+Ucl!7Gi)iowCS1E4%^K=13F z)t>)Ln6H;(+b^U)*OC872mPazd`8Y$ z%21g2&<;}N-Q`k$wS!c-gt^pf%BlVp2C^{mBEBW_8M%`sGh;ZVc!fc8k5*KVA&NRc zsID1_ocsY@1+j7kVTpXEuar8qoa$d;h4LBs2!{1qMpZ^F)-Rm@3H{03eKki4dEB4h4#$JcFiGEcB-t+8uSaEbwAn7(C8M$#bqFmts?w83Wpjks zF58i#vN?)um+gI{vKeVcR=PJW-Taodl=E}e)C^8$$>9xfXT|+F;eJap<&QgV8+=@t z__2jHR&QHD;ldGr;OMiM&7U}(2{kC3BgJdnn;g$1>+rY5j1wC*)fOgSOo?E=%kZMh z@HdxXXP4m{%5aR!@Pf;5ugmb#*A2pKW%!uO@FSPuT9@JeuUUozm0`TgaJkEHj>~YP z%dllR8G^G7$>lD?=Uj%2%kVE{7$gID*PWtPjN99#3@POcf7EJx++MEezOHC+vQ@rb z87^@}ALA26Z~LmHtWwH{T+zc_(HFjDr1`lk`b%Xv&K3P{SM*F*^hZj0`C+SdV2e@p zW>@qKSM(#wu$L?PC0F!fSM*G!+)7cr-g~X+xYJd%FmWfPeB~@_dDdKT;0ao59C@w;mb2U}|MoweoUt`LkwX>cuIXy_rd1BO1R*Lv;hFIYqG zyN`x`f1zbK*kzdLGJMNrm<*)gY?onwmtkYtAe`VbY!igfx(u&#ER-r9bQunI8J_8Ht_>SVrIo78Y@p z+rI({==9aEL#GCT^e5koQzCNYPD$Cq#98+O`DeXKnYd>eDD-hUo$J0)<92Tfsr#s_ zDtPEy2KGV+_Dc@z=>mJa1N%F+VM6{I1y)0(q9T@&*jc}Q^x1imVqyK4;<{hj1enw+&aD*2{SrRHEUr5} z27v^+Qu`+N-(@E^b~e^Kk10;3^R_`T*yPjaxz|J8>+l!d>qp&d|6KPv#l0@>altiF3e>jM25Nty$m@_^?L<0n$hXsu<$Gar52&;M0~V&BnTy!U{(jaIgg>pY7mQx- zm+k5KCpeXd2guMt*gJyRMExOVVv>uGICVY1scSs>J#LsL5i`sP|YWiK-V9ysA2^n;>rZ4F-gG9=6msLJkA zd96B#t2FLV{2$!eT8v6^a{8FOO@{lsZ1<>*#P4dorGs0;{kNNjexX_CRE^^DZVkTK z9l`ItIjnL?kA1@Y@8y_t(OXL`*s=c?_Q32d8}yCY^Sw*aV|45r?D0bG=7vXK_BUlcr+d zT|rokTos4ME*YR0j*4Cm4HTYo@$v>^T8g`cRSA_#nT9cEB|`36IC2Y~b#=16F!zJC zet#7`EF-$}nH+Zz!&o7#cV$7G^6%1yp6fOj$r?w#Cv^Fk5MiaGwYp(y7#9D*BDTK* zEiyT@icgUe_K(0dKY}k?(!VAKFGPz|1^9|c!zk&!+eXRR{IHQQ%n9=_I-nibhMhPg z>AoZj$+}>#uZK%ZbK;7Ubo$Zn6IZcvJ(LBloYRv*JDK@F#QTtfmPJIYXv{|pBzXsA4j!S{Yop>@9k%?F>%J!sD`%4R%50W~=&T!D9Kb~XA*y-2U3lXZZu5<@K z1)$<%@o^G^WWKkg5l+ERJ1Kr`T}6_b#Umx z43P<{GaKH?42&h&TptF+V7zpe@fB3;cb#^huH(v;=(1OxG?s(XSi_!GH)8NqQ8ufF zc5Bb9?u=)SBxyBC?fxuWh&M|_ePw5}XgtFq|F%M+p<2H6SnP?$GvR)6R<+*hTAiP? z+FPQdAW8qI+IVKw7%82ypD*8&Q={=ndlcqEY$jgo=G;PUu(i2beVok! zG@_cfWTV>V?=v4=C=7_hJ#;cas5)xb_N}y$NF&vnZqZB= z=X7k480qJG*?%$?2QyE?V$hih?Mb;yOAM(9v%6eK^DBe(1b0@!b}krRMiOaH1=HAZ zE#3(LxU&ibkTK_~cP)%0%mhO)0ma5CcNo@bP`%KvcurzA;?`Q09w`jGmkby0JAZCA@N@N7^P9A~T~@I*K%|q~nxE$8I*eGhQ6Qm! zo54B(}FiRq2WG z%)pq;@-gmoG~a5fxCWYy9-I$%iDy=hiCc$}gEX9t_f%R})uLL4lbu<2DPyYA-%)Ik zg&f#Zxm9Caz%WvUOzun=mSD49?$HTVRk190LH?2WLb_ZY=c;QPv@6`k(Pds-!`5%j zY@I~cEG5BPav6D{M{?fUB!^Z)!P%)R{x*=#QD%J2@YZCKy3y@>^S+rBMSB#PlJioB zAO)v=8=zcqbnqgsnb|1>)t;~U_F&==a*;2yx+b}JaH&1MQ`alUPtla_5P2`=QUCNT z9Y_C?g##CNJL>n@2W;mIE%CAT6~!V8ID$=oT+FNAMt|H-f7}uF$FE&~{C!vJ53efC z41aDzPrJL)T3Grg+{N7G+-b4ZT^UFdz@p70PK)l zgy16b!9eOtp_CelQp&&42#)7cONi2fSdpB>sU)O(9N4zb8)rhDt}2QM%!U9a1U8g+O+!o9?rI4cS8!LOnX{)KOHc3%-4v z`xMSuHr2nxq{im3S|&hJH?GRx&TK09$Rd*d~Q4<5XX~&<0+IT+cDj zWd50CVeZP>@_JY+oe>)poLypuGsega)+Sru>SbHe#SGRuxqei3=I?Pw39>d;9XmO{ ztar=&{?VBwzTYkLrsqaLKIE^wYn$_THlJjs{czHvUBI{9pYV(n)67ctl-{c8eF<%6 zDPSipB2dP-n+%9naNyY4M4D!Kmv%{*hSX6SireBQcS3|c&lKgoG_ zgsKU&Wm$lu=1f7MUpG*vZrBujc^+u;E4!JlOpTuZmcKNXL(-Fu%^z^;%>2RoFPbO~ z4&}77@tAzgsnZ7&9|j;YFSDwqeewNE?eXop-Up*5SoeBMZU

m4Ro~!Q2li1Cw~F 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

&rJ8LD z=@2Jnk3ScCLNQ-y={=dH)#NMl@_&?)$h5BmwE#@jQ2q#13G==UdyjS3yJ*=PEmdAc z6d3jpv|vcQormZi>3)W9sVtc*u!1GKVC*d|2XTnEmc=iFF#9nK$rkT?s0=z7xmpRu`Ae&KaqVHT#f^1UuC6s$ zh!PF)y~z?`HBSU#hZ32F={`2&cUL4mg4Yrq!oDZ#h(E#698ICDO{fzoPfC097OM{v z!9_Bs)18ZlNcYk@TQi&8*s}0TpGE9=_mH!zTf{wyC<-F8m`F@;F;FDXsMQ!=MSun| z0awfw%5y;_>O>|BE&_5J_U>I^xR=5+nG@xz1$J7GA+q6q?Y9 zS(1%_mtQP2lz0q{fRuIOF|#pkOM!VSwr%>x;W3z!fz@~T1!^=1eaEnVrF89YTE!@( zZnWGO3jw~VhP!h+DF>Yy<@o36M%)*#dRk+rK{`UJXM1p3t@oR!{^yWvX~cFNFdDNc zS)K3I>1GCvWW4Yz17x9;eV}^b9XQi^tK2 zx_$|xZMh56-aSk3&4A+DM1G2XZ1BfFPxmtW-!%Rf-n*A>{DFgM9IgPf@-%Mg?5p$g zM>8tpgR8d3{p@%M8QVr^-?W#}6-8~m|AJWav<_Isn!8rX0>6nag*y&mv!{aqbtZi58}h4u zE*#G~GpVEFzQEmd)wrdro4xcE(FRtH3W#aw1h?N=sfEFY3QiK)PjJ=Ht+2B2x{rhC zN&_=*mpqzIM)z!NQPM`(^yHAP77Wwqf4_TQ$?&0NS9^F|4s6DB(50wq(x|i~!j+!4 zKtU4oU$yV%5U|T!fvv27zCU2DuzngiT@JUETCnej%oYWdbL?&4YOIu`5Y&DVV3IBN%ELpX835Mi|u zPl*l6T1~L08HKkjiIx22xeKHxzum*Tm;o@co`686h1Yyfp5;+cO{YXM(}6V=g=-j!p|&Cn(DYt1oV;f`U%ME`ZlvXx}Rw! z#k?f(#O1{_z|088CMAS7aj&-9>UwC4H0#}=G*J~4Y#gRI>=tyOnAB1@ZcFPbRu=de zM}TPw+*X%&%F0+O0g>C>8by^lv&KoLKhhnB(8k6uww7bi%}%jpiB6)jyjq+sf3u}H zZ-PngLgq1cG~WzAQ5EweP3>24P7)PsNCg|CUFwU5n-(;gv3x02f8oB?JFn{viW(xd zU9daMxhw9TTXcBWEp`#bV#pthW~e~1=5urt!LHBecgy5~Y1B=&5vbphf*HzRvgAMY zJm;WySs&D*{{u zG{xOy^eqm%V;S`qYp&x*s-hf{#2DFPbE+xv?V-iB7=_f8UG;IQxQ4Q^Rz0v#?<(ov zx-z;;=K<7q#}zV?4p>q|VyK%)Wtw^Uryg>$hYUXPV4`eYwp8HTu+Ypyl&!kQf@74oyxjoAD*iV%)qD0}bQjysZ(LD3m9QvDnPs61StfD?12nuDv{nPhci z^vR_*M#K1tCb;BEh<>X23dMKpmp?Cm8JH}Un!m_O&EY3Hw4}fO*8%v6l)jS=bGnl* zR&khjO~B@|a4kPPixrxr!4JrtDZFP+ttI*XU+b?^{jb)$`gPxvu<_pp*%Z9~LB5Rg zA2!Hi805ddszLt$Kg8#i2F}Zmb!Zqg$iHKtl0gpGv@`kL)3G*U#{!$qe<5)l_6bAQ z8q!YX%sz;=8vDYHs0CEVDJeKzH=~+m*K4Zm0o*_hy|Ay)?qrUF@ziA@IzRkt0#8r) z{$wlP@gz)K#U-p4I81Bl5?fk1J>mwHAT48UDVOM1j7SpQE6AcVIUUw1uy)IklgQZ6 zu>Wz$Lv75&r(RTzTaFXTs)RGm_+uSL)5b!g19=MPTxUJcA41QgUm-}tOX4s80dtWq zRtdiyf7w6*J9QRLjK6$WUoIfeAf$zh+z52+)7z%_%2Bvgk`f%Ig)B2*g~xo;Hfx^a z6rC`JN*sQUPY%%7{FI0R_^hl=htit475Bmb?zWCbcUa@`OD7rT_VfQCW*_o{6WW&! zh+i69rpTXo`|QT)dd4q$2z&K_botxX!X5k1k9~w-1@gBoj`{B2kwEuLUh7}WeM8Ht z%XTl@k125Q*88=n=A7a_nuE(ZS(aYdMneh~Q!p>ee};RMlZT_%4g0bX-9E%OYa8@O z!7+!ZYPmkmuUG807CQ)5hJLRCO?)BG!OCNy?UaDvf8YUE;R2UlNxJQH0)vWm^O;<~ zR<7chE&M3;t~hnIOTB6&tx>S7Y{6_9e9(8vK#wj_{$l0%aT{x`je}>!wQNu(+m2CD zs=vt+=GRxN+P~?e{vpY3xm!STXOb!as!gt6U(H{jK(URsCJ@?*@Kh$#bbjW5#0}U-X!ztFtWE z{Q9{r*TOi7Tp^DM`WazRq=6EO)Q2j*0Q^b~IdL zT1XJX-@QH%r_SZo4abnO3afm9%W!j?;V$?3>-e?Xz1|eRZgQ{J#jkZpXW+bbjcdzy zuQTJ+HupL`er+H8I?KJb$EkO@*J-?pNUopN!Jvxa?f}onOCzU)3$$LnvIW-{P2;?{lNE!M#2e zzg82+fz=h;XqXJ{tB742LzT05Gx&AJ4wcN)p7>q<=HSeo0vy5Y|jLWsTa@X)Ge20|NJSn4` zn1!!a3U}7GS!+igf3M3n-IDQIv|lbxj(^uG8=F%b?42{k)UN94>=y?gwMH|OU8|p< zRmNo&x#MxECc%vhe9Q`6RX^2!=hv^)@86U<6xtPZtM#RL@K6JOe*Fgb+6Q}3U$$B~ zPEGEwT*Kb7&-3bM+2{H7n_TX-@#kvg0@NnUU32rj$P_ARbA|tAx#ri`+3Ueu(EXKp zy35@2ipxC9Wxkfo>g8NoU`)iWl=)Vd`M9T)d4By`SJgC2<(P{rJD)YZ%HHM5ej+Yg zh0K82ZD12mWvlJ=;B%gH?T)h7VTV~}CXdQ83JcUj2zffmpt+q_hg&oap7~Tpqp10M zY~azQ-dTT_U=c1d4|`8B?#-{7!K8`MwO8?V}b_2%gOuoRi=^~?PV=|v4Z zalD#QS`MSOeGYcxY-OVbomM@iQy@z_G8W>{DybkG6LybaUd632*H%3G&S)Wafj0B7 z2W(FFJz7|5>lBI8LF*LLKYCwq_Ehly92=Qh=a|4KY_?>#*y(*>Zl?IP?U~g43JHiA z4XwfOdS4M0$u49F1i-Z-+2E=QiH|snPBfF1w@O~cw@WajgH8C1xwGJbja?13Ld*WP za4Ml1lwQYD)uqQtlfzb$%=lsf6PKz<%-cdADvN(XcLT196_PILcNUAGd3nxrvbK7U z%l&S)+|K$Wq^|}MI?Q^(qYWI|3+4@)(26EAySMRV;G8g>H8cgOg+!C~Izf8rCms1XQp_ry<0HG&ePi^EQSRPKVevDSmkOSLAN1 zBcd|Sc6GNFE^6}f>w!F5XlrUIoG^;iy4C_7vQ~;Ob^6lTv{!j}9?+yaACP8NHbaL^ z!()E!s-q^QuU&4C&3Qy5Y)In&xw%PkDvF14Z zrJ=r&Jz}Z)>p_DYA*w)@jc+y>m5i*0bxXeDCyWkPur!b}y=9yN@8B-o*Q{&mYrX-= z(s*kydoDc;fAN@4+b&3b5u+QCBKk5Jx z$2)~**c(2F)+%ZjHy>(;3OIrxSqNUfj?5!HrL zvpCmT*6}H7$sLM6r!qgK%&V38G{uWhX2N$aX5d`Q70l$d9jzJ$KP)=~Bn#X8nzCb^ z!j#G3hGz?J`Gp;dyJ9_OC`Lfx*`PX-Ur_)47J;&`!&qDYX#rNQ@>xR&c=w?-wKzAA zx=eH4k;Y-GSksFyWdG1Gzei}5T*tVDWw?e%&&g8yA?Wb(djP3n4ezVI=vjueb1Sn# zw~bQw?-+J$g~@~<05%^l{EHfVH=%`JKvK~qG{W9Sv8H2CVhNf|uxI#vWjvjXnpdeO zCHtE&=;?O&$-`tlxWfhvE^lAWlT8U>;dIT7iu(TbBR&beW1w+AxK(>n;kX5WtGTfa z;_I^zf2zdi4lUKdDGRQRXJm3mN&HxnY>Z@4y&Bg)+k^ZnFd@m$C(Pd7{bXz<{qlxy36M%_%+G=$6$w*FgY{Yt9r|ucI$=XSvaR#c+S$<}X zI?`M%YRLg2&jFF?g+K_d7t8-nx%bF=C+F=c%)RBjC?mqGh&|N%4x*MRrwlJ005E?e zc^Wyk5iMi+=vW%ON`X~c6gP3M7L@~7+qyGV)wrestc{pWTLFc>kAjgk#jLIA16bQt z(Eec7_LUo5by1xf1mA#&L$Voe-IBKiUws1Kga$AFzYt?f3iE*;2Evze@HHgxiP_4B zrmYEfi`RmfH0-!8-8l|k`ySLY7@$@21;>Pj=v1`;K|zE`=X2#d!K9$~@whic zyE98@WJK?E5b=Nkjy*>jyCLk3kMw@j-7;}n9mwXqr#|YI34h8;d6UG0ajaE5h^)od z<%!#qu+$PSJrid$2w3r&awA3gTs9CB|4HE2Dou!Atf&JIFK$URg2pm#6ukmf&PIR2 z2rqvoGu*JNhk@8#Z?g?ccP!X6-t0{5?M7dn-xM*@TfTX+i?*;+Os|VI`=fD!m9V}7 zZc-_qMr0{Nx0|CP_sU~-VIy%kWIam z`y_7o#=(wYBRFUV5UN^co5JyBvb}4a5W2Y4`Vbu}&K@*3sTqsEwB!UxVzIoq+Cd0j zTe+_`zQ*~@8RMiFOpTEg3^&VK_~Iek`+A@2++L9m9?;n3+Kk+%a3A!oqNYK!?Ca)jio2_GCZE&=y*HYGhiH?T})g5Qc?Zzn?(8- z!@klwg(*Dz4JX-pxl&KcHXfMy&zVZvoO<7zqfLAD!KXb^xl0*;oL9p&mx{94+TPsN zZKfNqInYjdV^&i%SxhAB2zE+dKSQ6fllHGExVM1{f!4@UlgvUE-G*M(xfR!<1*Sr> zBVbB^m%o^IwCLW~)4}(qHEmfw%gf_9rWKMZ(4P%VvF3TTHO0YJk^o#x*!ds^ztvKT z63%l!0Hiy6QQ37Cu-N=QdkyT>;j&ZF!)&z?&_1?7SUmX3n{i9}mRix-AZ)>#liOH` zMM)=HXt>m8Z4q%MLXm7HCY|=WCjDnXm(A58H&f?{rs6bbLqkk^=&q+qJ05<=wV+4p zTjy33Qu(kqzJ^-DhV*joDBx6l?tJ_L`f__S!Kz7xt6a-siV^(y?}hKp;chH_qE}cH zH+En4l9oYmVIs66{M9GBz$4B5Qmh899N3f=oTa8f2(2XyBv~hIOLOQX{8ZYh-Z)7l zx^IbeZyzsyjB~`6+_8|WX#aAumzcqR&H2}Idz%eiv>bjL!gf=)yTwZOE=wk9I`y9! z%zQUHSqyF3assh3_hiwjf}2kyJMngy`Lyu@Z6tEeoAVTXSDvS@L#=z57c;U)-4dlS z{Ss4>96B!`_8DIL8|Q|wfXq6NCPabJ-5f?0{lT)`$sTER33&wafbO&Hk<8uO@P>>L za5T{%>dJB-chlBvis3~tDD#f6-fw(Sk(DK~mY~5a8Ob>%EP*!y~8cVl0yl2v+_>tHbIq(j)SJ%z9- z!|{83ugRrdQ z<8Ce1wFmvNao3=QTQYQU3!QL-7|ynXYq`hL)Xb786?W`F7rd^=`m+qZjVZB|cRB4` zfbKy(jgN*KZ+9sC7r)UNdG4_%UW>$xH=~)~sO$_I# z7urhocsF&U=_u49+KwOGYqH`ec4Jn;e!d70WV}%sdmlHQ$(z^R`w-uLY2Oy}G~b^7 z>Qb-k?)^QNF5Jdwp229^+}Ru+`aUB$WRt-Y)&q-J6v`#L+|Db&`P)bD!F;ta^`nZ+{~uarhzJvGcWxWU80wd@*YR28+W-z>wI2 zxRP;mxI3#Utph_NEfM!i%@7kB<7}N#Xk3q6H7@JTX{0QUknm5|vEn|&dq%e)rj#v^ z*=1F`ntEEg%p!~4jn;U-LhmRkep%sF&m#G?20tX^#g^O;!JLlo8~}NxR(r*L_-ze- zpnEEMU&gn3IDVi&(Gq-APHDlccFb;G{zzsSL9cN3o8893!XyTMAW#{r2zB*Zf_X~` zHG>gGFQ-{f16hTo(Kln9`LTV>N_9{G2-7LTDx$Qd6JrQ?;p|s#W9(}9mR+9l<68@IWx>B^6176wA{0C4o&S3~~ za2Qa)kphnJCC=9NUTYYFTN&ku6D|-wq0``uH|nsj)z9L6;HLk@nyk$H1$+lx(h-R} zA~V=QN}+~Z`6N+FH{10-pLe}XkIWtsvr*bm^%_3eY1)nHfYzifVoRw$a;rYm5a!&V zCphJFWF|ZQ;!z5TD^h3;POcJOrGaq&!bw$56$uDM&ia&Hq?nfK!mj~^DD%%$Mqxp> z2Nt<3VWSuD>|OO0QyU>!?XQN9{EDF?m>ohtqoeq9;Ry*oZTZAFMgQa@cs=R2Hok+$ z{?e_?r4eujMS@#-!AC%PD=N?sVywAEwbqBPq^rr=ru%idzma0denwmKZ5 zQjc!b;C|0^@2Y5--ta6|7$mstf5b2bTNU6Pre}Nk`5FLrHpycCkqnqKHavATy|INA zoFLuzcs8}YV^wq?l*xP|Vk6~L$VIg^Vj`#|GFNjr3A3#L27a$36wZ`zyb8us9qd8Q{1^Zd9|vNTV>A;CT8tz;uR}vW|n1Evz*@ z8mDdEPZvL70(m!lB5unMk4Rh1EY>=HLqRPa%5+0xJU3rEOjzRn@{|s?gUwY*?J*NB zxi=XvQPhDi(eJeNGuls652ozpJCHn-blTk2=kaDZ>|dBKj)p|%z$>-HkA8pziPtt> zmpoGB0{8qe$i$O)Nq5fNiy8UktFDs%0X9Owbo7bA1G>kY!2{|C-(l6n(SwTmj>n@{ zUQ=6@FUs+eh$z8F2l7W)CsNV)mbz@jGK1YO!coYZdyF?WsJOr5!q{&KBiN&mDi;k zEx}J`iGF2^dYX5~PBmF3{Tt@^pkc|bDuz|pG=_K9KM0dPZd!y}h@EUX(_N@K3O|K9J2IHUJcXg+F=BNX z#NC)$F*nVlSAfrD1u2q_S5oRLP3{IFKN#Btrj^D)W$E#LYT_ubYdK{y!T7y6Zs=Vx zR-c0lFm;&PCaq_a<$Rj+b6PE=zED{pVt@v_+-sX~j zk&J~+T>sismYe5tU-TOd-@D;YO1bxU3Ae=wKS~lfMmb75LrY!yK5_b4ACms3I6d_N z=~7k}{n|Brz%J>sG<+H$8_GInqBmXc+S?xIkKT;C6{ zH`j*-?w$1%BqB;hJwJGpKg`KqTziP|qj&a^S^jY^ZMgmctsAF*16^eU-{CZbPh-7@ zMmtIwb zP9cJoWk3E=%kpW>di@Mm#y=_tuUcNhXI8{)~*IbCFSELSF6F@dsn#fPCPaQms!)CM1KUDmh*Al8qV46 zzmVM_qVul}^A10wAhjddGNqv>Osdr}{9L!c7?}XN z!niSfo|o({=l2a&R6emZO_9d(EYBULUNnUl_w5i<%s);?cFjCjnHt<_oUrc4qJa)9 z|LekKk61s+_Ewz`9ZkF1Bhyf(eV7k(-N zBJi&te4k_fTXILsDYCaF6)J!U3uBg3m|r$2=>R|Ya+S1?YBl+z2W-uh$1FgL)ENG( zRF_UyEY22Bm>zb18m;eeNJLqh!WZeLCSODJMZWdZzk#4R-sBjM8n_;=&io$V^%|q; z=RR_HXZ60udwk9K4Cu}r9%PwbIt$-KAF`RlJAJ*EKbR@+hT)G8w_$+(dLtZvk6!Eu z8sPph=x<-HtGfSEm}n`il%6@lB2ZwY7A(42gL(INPFHc4BSa>UqMY?hUhtGAoBYt< zywoivk0q1vWpkM@mm1`cUgFXYR9a`x2t%8hm3q5A?0ob05lY{h8}*JJRZ}`ruQm2$|Am4~`=(N9i+uXw z`TJ*s%gr=XT6&N>UV7vvluR@0V$%%VW)K~4{=oLy3Xz@Tb(?Dt&XvDV!<=304j)8R z>{QJ1_WrL%ZqR*P@QlW8?og3q+5%=rGkjH!WjxAfY$3nioNM@hT03)KqLr=($H)OK zTRgWuyC(FDM;?9^P2<y*EUUM>v>wA%cqLdo zrall-D(9ydYsSbZ!psc0`nrTtl8$oW(*)?Eje4*<`rHN(bULEYWupwFsellMta_$B ziv`BSp+?}@!l(@mCHio;kCd?R?HjZ;rXacD?Jr|GTbAG}TRgHr8ZijI&q0sm zlyPxbs|o~Te$U6=Pjl``+oD{Bnp~eu52j^2jNCe6XNU=;2~OED#1x4Br*3!~-;{)`S>X7O** zi>eu!INnXM<_5MMG=RlnuQMOR0*l1-!a5O0v8IQeCK4W3!iLdGXmSbDRH!)YJ|*;! zV2~DT?&O`Jx&=RWMX{JG_eO28G39N@!uM)eE&H`qJxvubS?Hv5fwnUXowvH5yZA9~ zUiVdqw9M%Tb$)Qe^~eZ$DWTgOvjs;Tr$1P@9*#c*7i05U#mb~1w+FUcec=MGV0^9A zp#fh6$a^yG_H`^{Qq)N&m^FT8e<$<*qoLa9asSkDRPBKD;+pVdKf=!6%@xz1D4Ms{Zozed76U09P|{O%!{M!r3m zDV%-;OUlCO4L3Bd9=Te3mt&6(J(dDJW9ymALI5p(Yp#PQU2mE`Wm6y0aJ$W+SJ z-=9JJ>ava3lPNg=py0ycnczot!CAFb;B(@q30BUEB=xq={%-Y7UY;&IolZU5?4A4u zGi&t7*E1vE3pdTMs9K|KlXF}#b6DBpZHF{IG4hEMio+WUYcs`@%NK7wq;cuUr9{Eg z#&9@eCKCf&$vymPJ*Vae^Pm2MDE?&|rQl}+sN(xo!KskVL$YVRMECvRR4hm%5$~Cx z=rp4)A=u0Ro78yygYsJzj6aCC1w7f?Sh{&ZU21We-vUXB2hwa{q*98Rzz{`FwthQf2RVi}ZH^d#)XZV4wfZMq$N zJD0=X!T3>9ASSA!CMlPb)gDjUl}Dbm>Dvg^=Z6{MATR}d1MSZ=6O2`WgE_xb(}L1L zqce4w_rapjHyGSqAPu^NYT6)v#`qXqfFs{1b;N)j1U?Y^lGp%dU;{B=41|}C7`#;4 z5O;(;GOz0ZxOe)Ub?JB3D%6p#_Um13=bLt-Z)2jtm?z|? z9_*fGRimg(g(o0ybP;dP{WOpbZZoz~Jhyj)E3rD!*09Ig>(^x1D)k|1$!Tyn=YQXy>r8Mu2Lsi z?6x@UXD&Zkt(iFM&z<~;^LPBqLG@T=T#A5-6u@n+wm&@Axwa~x%9vO!9jkQ{b5O#T zHLjKqm!wn8!*Tqws%{jjVUqPM3v&(;n=wAYzY| z=#5mz;q$>;nnh~^ey%ucth^baJxdszI?pgZ7=zK76yd&u9TmCHI7A=!f!U4&EB*W! z-Jo!nQI$Xxfa*q~#Mz0)(#FCBN=9!$>n*|8pi?ddM>K1KccixOnpR)+va5bU^xO ztOAL@pPkdx7dQhdVYi9ejuEt6VB`AmUA*q1?~svRG8I_riAJQ`*$}fj)BWCV>x>Oelf7W~RhLH&0Vd{4cb%VE z=3RY&tBzR_lW!R`6jkxn(c!f)9z*H}(IEkE1)piKJACf$fnCbQ8Ej$!kEWD* z8r4Eo>7ezXba3YI)S7hR39oex!f`X->e7YPe6H2!HSY5&MBOh5pWL1Hez_#G$C~EU zf9$Dhjn{P+KxI-Zy*X_>G_GncyhVJujOVXwPAzNpPT9bePkSw^QmgHpfb<_^LtS%W zZMv`unK_Bk*OvJ#=t<<7)_R$BK2jq*a~5WwI3_VS^ICr``iDDEoFpl;@6EZ18i=}w z%7OKEKsFj+``2eguOP=s!9klyMjOorSF}@Q79iTfZ5PU;M4sR}+$@*Gbl(~oVu0@{ zYOsq#HK`h8&mLiY2s_yGqAq+EC?`Qkd?r;1J0*tAit^)K>|O6F?TInlfGbP4jygzU z6cCa!=BT?5jE;i>vw$(3y0p&gYGoco@8LMx|FzcZ`Xkw*jmmoo!luQtxZgdZM0ww4K}aN_BpxsJ3sNm^^I(n0J9QV!y` zF8rPHUf}9c-tkG^=7YjOY3^2Y^)P!NZb$O@jn@=|v|qdl z3tcdKBJM2kG?p9YYXZ<;FreQh3{q8i;xe~=Z42%QCP|wusb@R>_>zAOlBOkg=D|y6(!g>q03J=zV!}^&r?tM`Xh1ud^9~UfY{|YVA z#@8Y`lyXIe99B{o(o?+MVyk;p19e_Yt|z^FPVCEfcXS)8rvit;6=SQuwDUF&J!Gj= zuP#3OL0y#C;41m>&Er0({{u=N`oX2gBKHUo0JQw)PYiGWpn6L5qn3PbZ25rQrdf<| znb{*JvTgO2%vMDfHM&Li5#P(KMBj%Rp5;4+Rge&FTCCY!dDg9(Ww4ITEj`^#e;5U* z7p7u@8fZ`S?nzgTo|>x|{ezDBY|t`d^bfK@y0#_QLn>*DmpQd6T7+!qhqqX{55`6s zUP8@Lkr_T{{W|MMRp;kEhVJ^h!CQj|OkZ@K<+A?hWQy^{W#6({i64-O+&|;?&wV z(_VE`byPlzVFupySl+wazcYGXo!EDtJ4?^~5>2tSuZTPQWU)jW21(dnzSa_IbXu>h_6-OgldJkE?rn_Wx}aU%-%cbGW_I& z(r?X@9(;KE)G_#`P6f5V_d(^SqGv^T4=WvX0rHW6-1#26L{|c{V24h2f<4-w0^ge3 z`5u~mmr`x*B_GXQJFxqSH1G<9rT9I&CSOngqLq%4KPWvsh4foV$N0s;7H3pVGNR&y zJx7z_axx?z!*A+K2Vb1MuZf*@L`O!RGAu>IEeBvUe5D*=JSvwCDtq=BTGq8iqB*Gi zE6-l0@=kvE09y?-99mnmz&5+E7T9l7zqn5m!@M-8yl6lL-*y$86IalC{~&(CZp_5M zVv0~Z%Dq90e=M%sXKTmerB4hhXOrzsubRcb+^hnF6dhL3 z1(jbX5saN}dG?`Aa-L8{8tz&&_@>9VCWk;ijtCch-r;Vdvqx+M3m>EGz`ET6Or_Pt z2Oyv$uEVDeW<@{0GW=Gq@cxhW8F~|BxSrRQ0PC;M*+;=`&{7@V^lAAQR9+7Q!ock4 zqULj#i$UNtc&Ii=jasyoSFEWcK`scDQ-LNtog3u)lQTi(X@ap>Q%KUnDt>IPOp+5@Cn3){FjH{3i309tsFujIPPYIsap9_i0Q3+=9jIhOuM zrGK%%1=6K;&P-q#1-6C7;sYJSYOfqxeb-pRpPa zR+c0BvjD*iJD^sxm!-cujP&{aEs(yqOMjmZ#}u0JP6;#R=a1lDU8eli=8<7@`Kx3poXWwR zHE>NPk@Le)xr%h>;&M*)9Z;TbN|!GwPmd^1*QH02Lva`^5jR(yyj5(*r2sc$dUI;A ziISOw=j*;9{m#41Y^irXSCJcDo<7ltLC zu4X744R;p+OGF(3U{1KL5OD9xvxAvUF~{L_RbAWHo`;V5$5JYjqvc&2p=Zo4jkjEWQ--T%hRJu{qpkioUjp&NJ5hC zy8fM4zmn>>(ol!@x_QKi*HZXb>-nn|@{(qXBT9R+7%f)w?w`L{pE-skL$;zJ737o!>14aA)_q?7UrA9LxPmOUqLyj+$|XVC-G1 zu5}C>)Oq!>!`lf3LEFR_+CtmJplwo`n-+k*4fITkoO>#gd6y+WJ;E%89f>ohII&>{ z!YZm=j}c)VLu@{^;>≧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